• 第二十二课,实例化(instancing)


    概述

    对于拥有同一模型数据的物体(例如:草),每一帧需要渲染成百上千的实例,这些渲染几乎可以瞬间完成,但是上千个渲染函数的调用却会造成极大的性能影响。

    glDrawArraysglDrawElements函数告诉GPU去绘制你的顶点数据会消耗更多的性能,因为OpenGL在绘制顶点数据之前需要做很多准备工作比如告诉GPU该从哪个缓冲读取数据,从哪寻找顶点属性,而且这些都是在相对缓慢的CPU到GPU总线(CPU to GPU Bus)上进行的)。所以,即便渲染顶点非常快,命令GPU去渲染却未必。

    如果我们能够将数据一次性发送给GPU,然后使用一个绘制函数让OpenGL利用这些数据绘制多个物体,就会更方便了。这就是实例化(Instancing)

    实例化

    1. 载入实例化对象

    加载模型 或 自定义数据

    2. 设置实例化对象数量以及各实例位置信息

    每一个实例都需要设置一个独有的变换,因为如果每一个实例都渲染在同一个位置,以同样的方式,那么实例渲染将毫无意义。

    故需要一个偏移量model矩阵的数组,来设置每一个实例的变化。

    例如:

    1. 二维空间平移变换
    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    1. 三维空间变换
    unsigned int amount = 1000;
    glm::mat4 *modelMatrices;
    modelMatrices = new glm::mat4[amount];
    srand(glfwGetTime()); // 初始化随机种子    
    float radius = 50.0;
    float offset = 2.5f;
    for(unsigned int i = 0; i < amount; i++)
    {
        glm::mat4 model;
        // 1. 位移:分布在半径为 'radius' 的圆形上,偏移的范围是 [-offset, offset]
        float angle = (float)i / (float)amount * 360.0f;
        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; // 让行星带的高度比x和z的宽度要小
        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;
    }  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    3. 向shader传参,设置顶点属性

    使用实例化位置信息有两种方式

    第一种,使用Uniform传参

    但GPU提供给Uniform的数据大小有上限,如果要渲染远超过100个实例的时候,便不能组成工作。替代它的方式是使用实例化数组(就是使用VBO)。

    shader.use();
    for(unsigned int i = 0; i < 100; i++)
    {
        stringstream ss;
        string index;
        ss << i; 
        index = ss.str(); 
        shader.setVec2(("offsets[" + index + "]").c_str(), translations[i]);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    疑问:使用VBO不会造成GPU内存不够,猜测Uniform和VBO存储在不同的分区。

    第二种,使用实例化数组

    (1)将数据绑定到VBO

    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    // 顶点缓冲对象
    unsigned int buffer;
    glGenBuffers(1, &buffer);
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    glBufferData(GL_ARRAY_BUFFER, amount * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    (2)绑定数组信息(一个VAO绑定多个VBO)

    注:在模型导入时,已经将模型数据导入到0,1,2号结点,故,在这里只需要将模型变换矩阵绑定。

    然而,当我们顶点属性的类型大于vec4时,就要多进行一步处理了。顶点属性最大允许的数据大小等于一个vec4。因为一个mat4本质上是4个vec4,我们需要为这个矩阵预留4个顶点属性。因为我们将它的位置值设置为3,矩阵每一列的顶点属性位置值就是3、4、5和6。

    glVertexAttribDivisor(顶点属性号,每隔几个实例走一步)
    因为对于一个实例,需要使用模型VBO中的所有数据,而对于每一个实例,只需要使用模型变换中的一个模型矩阵。故使用glVertexAttribDivisor函数来设置。

    • 当第二个参数实例数据的步长为0(默认位0),及对于一个实例中的每一个顶点使用不同的模型矩阵。
    • 当第二个参数实例数据的步长为1时,对于一个实例中的每一个顶点都使用相同的模型矩阵。
    • 当第二个参数实例数据的步长为2时,每2个实例更新一次属性(模型矩阵)
    • 依次类推
    for(unsigned int i = 0; i < rock.meshes.size(); i++)
    {
        unsigned int VAO = rock.meshes[i].VAO;
        glBindVertexArray(VAO);
        glBindBuffer(GL_ARRAY_BUFFER, buffer);
        // 顶点属性
        GLsizei vec4Size = sizeof(glm::vec4);
        glEnableVertexAttribArray(3); 
        glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)0);
        glEnableVertexAttribArray(4); 
        glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(vec4Size));
        glEnableVertexAttribArray(5); 
        glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(2 * vec4Size));
        glEnableVertexAttribArray(6); 
        glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(3 * vec4Size));
    
        glVertexAttribDivisor(3, 1);
        glVertexAttribDivisor(4, 1);
        glVertexAttribDivisor(5, 1);
        glVertexAttribDivisor(6, 1);
    
        glBindVertexArray(0);
    }  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    4. shader编写

    vert

    #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 projection;
    uniform mat4 view;
    
    void main()
    {
        gl_Position = projection * view * instanceMatrix * vec4(aPos, 1.0); 
        TexCoords = aTexCoords;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    frag

    #version 330 core
    out vec4 FragColor;
    
    in vec3 fColor;
    
    void main()
    {
        FragColor = vec4(fColor, 1.0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    Draw

    glDrawElementsInstanced多了一个参数amount表示实例化数量

    // 绘制小行星
    instanceShader.use();
    for(unsigned int i = 0; i < rock.meshes.size(); i++)
    {
        glBindVertexArray(rock.meshes[i].VAO);
        glDrawElementsInstanced(
            GL_TRIANGLES, rock.meshes[i].indices.size(), GL_UNSIGNED_INT, 0, amount
        );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    运行结果

    在这里插入图片描述

  • 相关阅读:
    正式开源:从 Greenplum 到 Cloudberry 迁移工具 cbcopy 发布
    基于Spring Boot+MyBatis+MySQL的高校试卷管理系统
    数据结构——排序算法(冒泡排序、选择排序、插入排序、归并排序、快速排序、搜索算法)
    [前端基础] 浏览器篇
    STM32 10个工程篇:1.IAP远程升级(六)
    watch在项目中的使用
    FFmpeg的makefile逻辑分析
    c++ 编译过程杂记等
    这些提高摸鱼效率的自动化测试技巧,提高打工人幸福感~
    node12-node的 get请求
  • 原文地址:https://blog.csdn.net/weixin_44518102/article/details/126061667