LearnOpenGL-高级OpenGL-10.实例化

本人初学者,文中定有代码、术语等错误,欢迎指正

实例化

  • 引出

    • 假设有一个绘制了很多模型的场景,而大部分的模型包含的是同一组顶点数据,只不过进行的是不同的世界空间变换,比如:草

    • 渲染上千上万个草,渲染函数调用会极大地影响性能

      for(unsigned int i = 0; i < amount_of_models_to_draw; i++)
      {
          DoSomePreparations(); // 绑定VAO,绑定纹理,设置uniform等
          glDrawArrays(GL_TRIANGLES, 0, amount_of_vertices);
      }
      
    • 性能消耗的地方

      OpenGL在绘制顶点数据之前需要做很多准备工作(比如告诉GPU该从哪个缓冲读取数据,从哪寻找顶点属性,而且这些都是在相对缓慢的CPU到GPU总线(CPU to GPU Bus)上进行的)。

      即便渲染顶点非常快,命令GPU去渲染却未必。

  • 什么是实例化

    • 解决上述的性能消耗的地方

      我们能够将数据一次性发送给GPU,然后使用一个绘制函数让OpenGL利用这些数据绘制个物体。这就是实例化

    • 进一步解释

      实例化这项技术能够让我们使用一个渲染调用来绘制多个物体,来节省每次绘制物体时CPU -> GPU的通信,它只需要一次即可。

    • 使用什么函数

      将glDrawArrays和glDrawElements的渲染调用分别改为glDrawArraysInstanced和glDrawElementsInstanced

    • GLSL有内建变量:gl_InstanceID

      渲染同一个物体一千次对我们并没有什么用处,每个物体都是完全相同的,而且还在同一个位置。

      利用gl_InstanceID可以标识每个实例,可以用此gl_InstanceID对应专属的uniform变换矩阵,从而变换当前渲染的物体(改变位置、大小等)。

例子1.1:100个2D四边形使用Uniform

  • 思路

    • glsl的顶点着色器

      定义uniform数组,每个渲染的实例quad:根据gl_InstanceID当做uniform数组的下标得到当前渲染的实例的变换位置

    • cpp

      • 定义quad的顶点输入数据
      • 定义顶点缓冲数组、绑定顶点缓冲对象、指定好顶点属性布局
      • 生成偏移位置数组,并用uniform上传给glsl
      • 绘制时使用glDrawArraysInstanced,将同一个quad数据渲染100次
  • 代码

    #version 330 core
    layout (location = 0) in vec2 aPos;
    layout (location = 1) in vec3 aColor;
    
    out vec3 fColor;
    
    uniform vec2 offsets[100];
    
    void main()
    {
    	// gl_InstanceID当前绘制实例的ID,作为offsets的下标
    	vec2 offset = offsets[gl_InstanceID];
    	gl_Position = vec4(aPos + offset, 0.0, 1.0);
    	fColor = aColor;
    }
    
    #version 330 core
    out vec4 FragColor;
    
    in vec3 fColor;
    
    void main(){ 
    	FragColor = vec4(fColor, 1.0);
    }
    

    cpp

    float quadVertices[] = {
        // 位置,二维          // 颜色
        -0.05f,  0.05f,  1.0f, 0.0f, 0.0f,
        0.05f, -0.05f,  0.0f, 1.0f, 0.0f,
        -0.05f, -0.05f,  0.0f, 0.0f, 1.0f,
    
        -0.05f,  0.05f,  1.0f, 0.0f, 0.0f,
        0.05f, -0.05f,  0.0f, 1.0f, 0.0f,
        0.05f,  0.05f,  0.0f, 1.0f, 1.0f
    };
    // quad VAO
    unsigned int quadVAO, quadVBO;
    glGenVertexArrays(1, &quadVAO);
    glGenBuffers(1, &quadVBO);
    glBindVertexArray(quadVAO);
    glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), &quadVertices, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(2 * sizeof(float)));
    
    // 生成偏移位置数组
    glm::vec2 translations[100];
    int index = 0;
    float offset = 0.1f;
    for (int y = -10; y < 10; y += 2)
    {
        for (int x = -10; x < 10; x += 2)
        {
            glm::vec2 translation;
            translation.x = (float)x / 10.0f + offset;
            translation.y = (float)y / 10.0f + offset;
            translations[index++] = translation;
        }
    }
    // 注意这里:上传位置给glsl,只能一个一个传///
    shader.use();
    for (unsigned int i = 0; i < 100; i++)
    {
        shader.setVec2(("offsets[" + to_string(i) + "]").c_str(), translations[i]);
    }
    // render loop
    while (!glfwWindowShouldClose(window))
    {
        // quad
        glBindVertexArray(quadVAO);
        // 将数据一次性发送给GPU,然后使用一个绘制函数让OpenGL利用这些数据绘制多个物体
        // 注意:第三个参数,设置需要绘制的实例数量//
        glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);
    
    • 重点

      glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);,将quad实例渲染100次

  • 效果

    1.1效果实例化quad

实例化数组

  • 引出

    由上的实例化例子,使用uniform传递变换数据,但是我们最终会超过最大能够发送至着色器的uniform数据大小上限。

    从而需要使用实例化数组,即不使用uniform。

  • 什么是实例化数组

    • 实例化数组它被定义为一个顶点属性(可以用缓冲存储),仅在顶点着色器渲染一个新的实例(可设置为两个或三个等)时才会更新。

      既然是顶点属性,则有对应的顶点缓冲对象,附加到顶点数组时需指定顶点属性布局

    • 重点

      • 原本的顶点属性,比如:位置、法线、颜色,都是在顶点着色器的每次运行都会让GLSL获取新一组适用于当前顶点的属性。

        • 详细说明

          注意:是当前顶点,比如:一个quad有位置和颜色信息,它有6个顶点(两个三角形组成),需运行6次顶点着色器,每一次运行顶点着色器渲染当前顶点,都需更新当前顶点的属性。

          一个实例6个顶点运行6次顶点着色器、次都得更新顶点的属性。

      • 当我们将顶点属性定义为一个实例化数组时,顶点着色器就只需要对每个实例更新顶点属性的内容。

        • 详细说明

          注意:是当前实例,比如:一个quad有一个变换信息,即使它有6个顶点,需运行6次顶点着色器,但当每一次运行顶点着色器渲染当前顶点的变换信息是同一个,只需渲染完6个顶点即为一个实例时,才需要更新顶点属性的内容。

          一个实例6个顶点运行6次顶点着色器、6次后才更新顶点的属性。

例子1.2:100个2D四边形使用实例化数组

  • 思路

    • glsl的顶点着色器

      获取每个实例的实例化数组的顶点属性

    • cpp

      • 实例化数组(位置变换数据)当做顶点属性
      • 定义顶点数组,指定quad的顶点属性布局
      • 指定实例化数组(位置变换数据)的顶点属性布局,并指定对应顶点属性(顶点位置、颜色什么的)渲染几个实例时更新一次。
      • 绘制时使用glDrawArraysInstanced,将同一个quad数据渲染100次
  • 代码

    glsl

    #version 330 core
    layout (location = 0) in vec2 aPos;
    layout (location = 1) in vec3 aColor;
    layout (location = 2) in vec2 aOffset;// 实例化数组(位置变换数据)它被定义为一个顶点属性
    
    out vec3 fColor;
    
    void main()
    {
    	// quad的大小逐渐变大,从0.01到1
    	vec2 pos = aPos * (gl_InstanceID / 100.0);
    	gl_Position = vec4(pos + aOffset, 0.0, 1.0);
    	fColor = aColor;
    }
    

    cpp

    // uniform数组,偏移位置
    glm::vec2 translations[100];
    int index = 0;
    float offset = 0.1f;
    for (int y = -10; y < 10; y += 2) {
        for (int x = -10; x < 10; x += 2) {
            glm::vec2 translation;
            // 控制在 -1,1之间
            translation.x = (float)x / 10.0f + offset;
            translation.y = (float)y / 10.0f + offset;
            translations[index++] = translation;
        }
    }
    // 用顶点缓冲对象存储
    unsigned int instanceVBO;
    glGenBuffers(1, &instanceVBO);
    glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec2) * 100, &translations[0], GL_STATIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    
    float quadVertices[] = {
        // 位置          // 颜色
        -0.05f,  0.05f,  1.0f, 0.0f, 0.0f,
        0.05f, -0.05f,  0.0f, 1.0f, 0.0f,
        -0.05f, -0.05f,  0.0f, 0.0f, 1.0f,
    
        -0.05f,  0.05f,  1.0f, 0.0f, 0.0f,
        0.05f, -0.05f,  0.0f, 1.0f, 0.0f,
        0.05f,  0.05f,  0.0f, 1.0f, 1.0f
    };
    
    // quad VAO
    unsigned int quadVAO, quadVBO;
    glGenVertexArrays(1, &quadVAO);
    glGenBuffers(1, &quadVBO);
    glBindVertexArray(quadVAO);
    glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), &quadVertices, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(2 * sizeof(float)));
    
    // 重点:设置layout=2的属性,aOffset,实例化数组//
    glEnableVertexAttribArray(2);
    glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    // 设置顶点layout=2布局的属性是,每1个实例更新一次属性//
    glVertexAttribDivisor(2, 1); 
    // 解绑
    glBindVertexArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    
    // 记得绑定shader,即使没有数据上传给uniform
    shader.use();
    
    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // quad
        glBindVertexArray(quadVAO);
        // 将数据一次性发送给GPU,然后使用一个绘制函数让OpenGL利用这些数据绘制多个物体
        // 注意第三个参数,设置需要绘制的实例数量
        glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);
        glBindVertexArray(0);
    

    可以看到没有使用uniform,而是用顶点属性

    • glVertexAttribDivisor(2,1)

      • 第一个参数:对应glsl的layout=2,指向的aOffset

      • 第二个参数:

        0:在顶点着色器的每次迭代时更新顶点属性,默认

        1:渲染一个新实例的时候更新顶点属性

        2:每2个实例更新一次属性

  • 效果

    请添加图片描述

例子2.1:行星带不使用实例化数组

  • 说明

    行星周围的石头都有自己的变换矩阵model,每渲染一个石头时,上传自己的变换矩阵model给glsl的uniform,所以实际上是使用uniform来变换每个石头的位置,而没有用glDrawArraysInstanced或者glDrawElementsInstanced函数来用上述的实例化。

    很大的原因是因为石头是obj模型,之前声明的mesh与model类封装了渲染函数,不好变动。

  • 代码

    glsl

    #version 330 core
    layout (location = 0) in vec3 aPos;
    layout (location = 2) in vec2 aTexCoords;
    
    out vec2 TexCoords;  
    
    uniform mat4 model;
    uniform mat4 view;
    uniform mat4 projection;
    
    void main()
    {
        TexCoords = aTexCoords;
        gl_Position = projection * view * model * vec4(aPos, 1.0);
    }
    
    #version 330 core
    out vec4 FragColor;
    
    in vec2 TexCoords;
    
    uniform sampler2D texture_diffuse1;
    
    void main(){
    	FragColor = texture(texture_diffuse1, TexCoords);
    }
    

    cpp

    // 加载模型
    Model rock(FileSystem::getPath("assest/model/rock/rock.obj"));
    Model planet(FileSystem::getPath("assest/model/planet/planet.obj"));
    
    // model数组,石头的偏移位置
    unsigned int amount = 1000;
    glm::mat4* modelMatrices;
    modelMatrices = new glm::mat4[amount];
    srand(glfwGetTime());// 初始化随机种子
    float radius = 50.0f;
    float offset = 2.5f;
    for (unsigned int i = 0; i < amount; i++) {
        glm::mat4 model = glm::mat4(1.0f);
        // 角度,0-360度
        float angle = (float)i / (float)amount * 360.0f;
        // 1. 位移:分布在半径为 radius 的圆形上,偏移范围是[-0ffset, offset]
        // rand()范围为0~RAND_MAX, 700 % 500 = 200 / 100 = 2 - 2.5 = -0.5
        float displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
        float x = sin(angle) * radius + displacement;
    
        displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
        float y = displacement * 0.4f;
    
        displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
        float z = cos(angle) * radius + displacement;
        model = glm::translate(model, glm::vec3(x, y, z));
    
        // 2.缩放:在0.05和0.25f之间缩放
        float scale = (rand() % 20) / 100.0f + 0.05;
        model = glm::scale(model, glm::vec3(scale));
    
        // 3.旋转:绕着一个(半)随机选择的旋转轴向量进行随机的旋转
        float rotAngle = (rand() % 360);
        model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));
    
        // 4. 添加到矩阵的数组中
        modelMatrices[i] = model;
    }
    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    { 
        // 摄像机
        glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 1000.0f);
        glm::mat4 view = camera.GetViewMatrix();
        shader.use();
        shader.setMat4("projection", projection);
        shader.setMat4("view", view);
    
        // 绘画行星
        glm::mat4 model = glm::mat4(1.0f);
        model = glm::translate(model, glm::vec3(0.0f, -3.0f, 0.0f));
        model = glm::scale(model, glm::vec3(4.0f, 4.0f, 4.0f));
        shader.setMat4("model", model);
        planet.Draw(shader);
    
        // 绘画石头
        for (unsigned int i = 0; i < amount; i++) {
            // 设置偏移
            shader.setMat4("model", modelMatrices[i]);
            rock.Draw(shader);
        }
    
  • 效果

    请添加图片描述

  • 缺点

    由代码可见,渲染石头是用for循环+上传uniform,当要渲染的石头数量增加,即for循环的次数增加,调用uniform的次数会变多,而调用uniform的次数会影响性能,

    当amount=10000时,可以感到明显的卡顿(根据自己的机器配置,amount太大会感到卡顿)

例子2.2:行星带使用实例化数组

  • 说明

    不使用原本model类封装的draw函数,而是获取mesh的顶点缓冲数组再调用实例化函数glDrawElementsInstanced。

    这样我们就可以将每个实例的变换矩阵(实例化数组)当做顶点属性

  • 代码

    glsl

    #version 330 core
    layout (location = 0) in vec3 aPos;
    layout (location = 2) in vec2 aTexCoords;
    layout (location = 3) in mat4 instanceMatrix;// 实例化数组(位置变换数据)它被定义为一个顶点属性
    
    out vec2 TexCoords;  
    
    uniform mat4 view;
    uniform mat4 projection;
    
    void main()
    {
        TexCoords = aTexCoords;
        gl_Position = projection * view  * instanceMatrix*  vec4(aPos, 1.0);
    }
    
    
    #version 330 core
    out vec4 FragColor;
    
    in vec2 TexCoords;
    
    uniform sampler2D texture_diffuse1;
    
    void main(){
    	FragColor = texture(texture_diffuse1, TexCoords);
    }
    

    cpp

    Shader planetshader("assest/shader/4高级OpenGL/6.10.3.渲染大量物体-行星带-无实例化.vs", "assest/shader/4高级OpenGL/6.10.3.渲染大量物体-行星带-无实例化.fs");
    Shader rockshader("assest/shader/4高级OpenGL/6.10.4.渲染大量物体-行星带-实例化数组.vs", "assest/shader/4高级OpenGL/6.10.4.渲染大量物体-行星带-实例化数组.fs");
    
    // 加载模型
    Model planet(FileSystem::getPath("assest/model/planet/planet.obj"));
    Model rock(FileSystem::getPath("assest/model/rock/rock.obj"));
    
    // model数组,石头的偏移位置
    unsigned int amount = 100000;
    glm::mat4* modelMatrices;
    modelMatrices = new glm::mat4[amount];
    srand(static_cast<unsigned int>(glfwGetTime()));// 初始化随机种子
    float radius = 150.0f;
    float offset = 25.0f;
    for (unsigned int i = 0; i < amount; i++) {
        glm::mat4 model = glm::mat4(1.0f);
        // 角度,0-360度
        float angle = (float)i / (float)amount * 360.0f;
        // 1. 位移:分布在半径为 radius 的圆形上,偏移范围是[-0ffset, offset]
        // rand()范围为0~RAND_MAX, 700 % 500 = 200 / 100 = 2 - 2.5 = -0.5
        float displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
        float x = sin(angle) * radius + displacement;
    
        displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
        float y = displacement * 0.4f;
    
        displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
        float z = cos(angle) * radius + displacement;
        model = glm::translate(model, glm::vec3(x, y, z));
    
        // 2.缩放:在0.05和0.25f之间缩放
        float scale = (rand() % 20) / 100.0f + 0.05;
        model = glm::scale(model, glm::vec3(scale));
    
        // 3.旋转:绕着一个(半)随机选择的旋转轴向量进行随机的旋转
        float rotAngle = (rand() % 360);
        model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));
    
        // 4. 添加到矩阵的数组中
        modelMatrices[i] = model;
    }
    // 关键代码-开始///
    // 设置给rock的model,实例化数组当做顶点属性,需要指定顶点属性布局
    unsigned int buffer;
    glGenBuffers(1, &buffer);
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    // 注意这里将数组绑定到缓冲中
    glBufferData(GL_ARRAY_BUFFER, amount * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);
    for (unsigned int i = 0; i < rock.meshes.size(); i++) {// rock.meshes.size() = 1
        unsigned int VAO = rock.meshes[i].VAO;
        glBindVertexArray(VAO);
        // 设置mat4的顶点属性指针
        glEnableVertexAttribArray(3);
        glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void*)0);
        glEnableVertexAttribArray(4);
        glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void*)(sizeof(glm::vec4)));
        glEnableVertexAttribArray(5);
        glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void*)(2 * sizeof(glm::vec4)));
        glEnableVertexAttribArray(6);
        glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void*)(3 * sizeof(glm::vec4)));
        // layout=3矩阵的instanceMatrix顶点属性,每1个实例更新一次属性
        glVertexAttribDivisor(3, 1);
        glVertexAttribDivisor(4, 1);
        glVertexAttribDivisor(5, 1);
        glVertexAttribDivisor(6, 1);
    
        glBindVertexArray(0);
    }
    // 关键代码-结束///
    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // per-frame time logic
        // --------------------
        float currentFrame = static_cast<float>(glfwGetTime());
        deltaTime = currentFrame - lastFrame;
        lastFrame = currentFrame;
    
        // input
        // -----
        processInput(window);
    
        // render
        // ------
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
        // 摄像机
        glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 1000.0f);
        glm::mat4 view = camera.GetViewMatrix();
        rockshader.use();
        rockshader.setMat4("projection", projection);
        rockshader.setMat4("view", view);
        planetshader.use();
        planetshader.setMat4("projection", projection);
        planetshader.setMat4("view", view);
        // 绘画行星
        planetshader.use();
        glm::mat4 model = glm::mat4(1.0f);
        model = glm::translate(model, glm::vec3(0.0f, -3.0f, 0.0f));
        model = glm::scale(model, glm::vec3(4.0f, 4.0f, 4.0f));
        planetshader.setMat4("model", model);
        planet.Draw(planetshader);
    
        // 绘画石头
        rockshader.use();
        // 绑定纹理单元
        rockshader.setInt("texture_diffuse1", 0);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, rock.textures_loaded[0].id);;
        for (unsigned int i = 0; i < rock.meshes.size(); i++) {
            glBindVertexArray(rock.meshes[i].VAO);
            // 注意第5个参数,设置需要绘制的实例数量
            glDrawElementsInstanced(GL_TRIANGLES, rock.meshes[i].indices.size(), GL_UNSIGNED_INT, 0, amount);
            glBindVertexArray(0);
        }
    
    • 当矩阵当做顶点属性时

      由于顶点属性的类型只能是小于等于vec4大小,而mat4本质上是4个vec4,所以我们需要为这个矩阵预留4个顶点属性。

      因为我们将它的位置值设置为3,矩阵每一列的顶点属性位置值就是3、4、5和6。

  • 效果

    100000个石头

    请添加图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘建杰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值