OpenGL 实例化

我们有时候需要绘制大量的同一模型的物体,比如游戏中的子弹,它们使用同个模型,只是位置不同,我们的代码将会是这样的:

cpp 复制代码
for (int i = 0; i < amount; i++) {
    bulletShader.setMat4("model", modelMatrices.at(i));
    bulletShader.setMat4("invModel", glm::inverse(modelMatrices.at(i)));
    bullet.draw(bulletShader);
}

但是这样的话,我们的界面会变得卡顿,GPU 渲染这些数据是非常快的,但是从代码中我们可以看到,我们需要频繁地更新 model 矩阵和 invMode 矩阵,这会导致 CPU-GPU 通信成为瓶颈。同时一个模型是有多个网格的,这意味着需要频繁地切换 VAO、VBO、EBO 等绑定,以及调用 glDrawArrays 或 glDrawElements 时 OpenGL 内部做的一些操作。

我们可以很容易想到一些优化方案,比如,我们可以把所有的 model 矩阵存在一个 uniform 数组里,这样就避免了频繁更新 uniform ,但是这样我们可能会超过着色器的 uniform 数据大小上限。我们还可以优化一下绘制方式,把同一网格的顶点集中在一起绘制,但是这样操作繁琐且容易出错。

实例化

实例化就是用来解决这一问题的,我们只需要调用一次绘制函数就能绘制多个物体。

cpp 复制代码
glDrawElementsInstanced(GL_TRIANGLES, m_indices.size(), GL_UNSIGNED_INT, 0, instanceCount);
// glDrawArraysInstanced(GL_TRIANGLES, 0, m_vertices.size(), instanceCount);

glDrawElementsglDrawArrays 参数基本一致,最后一个参数表示实例化的数量(物体的数量)。
glDrawElementsInstancedglDrawArraysInstanced 解决了绘制多个物体的问题,但是我们还需要为这些物体设置不同的模型矩阵。在上文中提到,如果把所有的 model 矩阵存在一个 uniform 数组里,可能会超过着色器的数据大小上限,因此我们需要通过其他方式来实现这一功能。

实例化数组

实例化数组就是把实例化需要的 model 矩阵定义成一个顶点属性,并且设置它在顶点着色器渲染一个新的实例时才更新。

glVertexAttribDivisor

void glVertexAttribDivisor(GLuint index, GLuint divisor); 用来控制顶点属性按第几个实例才前进一次,它的第一个参数表示顶点的属性位置,第二个参数表示每 divisor 个实例才前进一次属性数组。

通常情况下,我们把顶点数据存在一个数组里,每个顶点对应数组的一个对象,而通过 glVertexAttribDivisor 我们可以设置 divisor 个实例对应数组的一个对象。

divisor 含义
0 每个顶点一个值(普通非实例化)
1 每个实例一个值
2 每 2 个实例共用一个值
n 每 n 个实例共用一个值

mat4 的顶点属性

顶点属性最大允许的数据大小等于一个 vec4 ,所以一个 mat4 的对象需要用 4 个顶点属性去存储。

cpp 复制代码
layout (location = 3) in mat4 instanceMatrix;

设置实例化矩阵的位置属性为 3,矩阵每一列的顶点属性位置值就是3、4、5 和 6。

cpp 复制代码
 unsigned int modelVBO;
 glGenBuffers(1, &modelVBO);
 glBindBuffer(GL_ARRAY_BUFFER, modelVBO);
 glBufferData(GL_ARRAY_BUFFER, modelMatrices.size() * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);
cpp 复制代码
 unsigned int VAO = m_meshes.at(i).vertexArrayObjectId();
glBindVertexArray(VAO);
GLsizei vec4Size = sizeof(glm::vec4);
// 绑定 modelVBO 并设置 location 3-6
glBindBuffer(GL_ARRAY_BUFFER, modelVBO);
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*)(1 * 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));

这部分的内容大家也是比较熟悉了,创建 VBO 对象,绑定 VBO,绑定 VAO,告诉 OpenGL 应该如何去读取数据。

cpp 复制代码
glVertexAttribDivisor(3, 1);
glVertexAttribDivisor(4, 1);
glVertexAttribDivisor(5, 1);
glVertexAttribDivisor(6, 1);

最后调用 glVertexAttribDivisor 设置实例化数组每个实例更新一次。

实践

模型下载

模型和代码源于 learnOpenGL,这是一个小行星带的项目,包含一个行星模型和岩石模型,岩石通过非等比缩放和平移旋转分布在行星周围,形成一个圆环。

下面给出两个模型的下载链接
行星模型 岩石模型

初始化实例化矩阵

cpp 复制代码
unsigned int amount = 10000;
std::vector<glm::mat4> modelMatrices;
modelMatrices.reserve(amount);

srand(glfwGetTime()); // 初始化随机种子    
float radius = 50.0;
float offset = 2.5f;
for(unsigned int i = 0; i < amount; i++) {
    glm::mat4 model(1.0f);
    // 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));
    
    modelMatrices.emplace_back(model);
}

这段代码为为每个岩石生成一个变换矩阵,使得岩石形状各异,并且变换到行星的周围。

实例化绘制接口

cpp 复制代码
void drawInstanced(Shader& shader, unsigned int instanceCount);

我们在 Mesh 增加一个接口,这个接口用于实例化绘制,第二个参数表示实例化的数量。

cpp 复制代码
void Mesh::drawInstanced(Shader& shader, unsigned int instanceCount) {
    // 绑定纹理
    unsigned int diffuseNr  = 0;
    unsigned int specularNr = 0;
    unsigned int normalNr   = 0;
    unsigned int heightNr   = 0;
    for(unsigned int i = 0; i < m_textures.size(); i++) {
        glActiveTexture(GL_TEXTURE0 + i);
        std::string name = m_textures[i].type;
        std::string uniformName;
        
        if(name == "texture_diffuse") {
            uniformName = "texture_diffuse[" + std::to_string(diffuseNr++) + "]";
        } else if(name == "texture_specular") {
            uniformName = "texture_specular[" + std::to_string(specularNr++) + "]";
        } else if(name == "texture_normal") {
            uniformName = "texture_normal[" + std::to_string(normalNr++) + "]";
        } else if(name == "texture_height") {
            uniformName = "texture_height[" + std::to_string(heightNr++) + "]";
        }

        shader.setInt(uniformName, i);
        glBindTexture(GL_TEXTURE_2D, m_textures[i].id);
    }
    
    shader.setVec3("ambient", m_material.ambient);
    shader.setVec3("diffuse", m_material.diffuse);
    shader.setVec3("specular", m_material.specular);
    shader.setFloat("shininess", m_material.shininess);

    // 绘制
    glBindVertexArray(m_VAO);
    glDrawElementsInstanced(GL_TRIANGLES, m_indices.size(), GL_UNSIGNED_INT, 0, instanceCount);
}

代码和 draw 基本一致,只是把 glDrawElements 换成了 glDrawElementsInstanced

同样,我们需要在 Model 增加一个 drawInstanced 接口。

cpp 复制代码
void Model::drawInstanced(Shader& shader, unsigned int instanceCount) {
    for (auto& mesh : m_meshes) {
        mesh.drawInstanced(shader, instanceCount);
    }
}

实例化数组绑定

在 model 增加 void seInstacedModelMatrices(const std::vector<glm::mat4>& modelMatrices); 接口,用于设置实例化数组,并绑定。

cpp 复制代码
void Model::setInstacedModelMatrices(const std::vector<glm::mat4>& modelMatrices) {
    unsigned int modelVBO;
    glGenBuffers(1, &modelVBO);
    glBindBuffer(GL_ARRAY_BUFFER, modelVBO);
    glBufferData(GL_ARRAY_BUFFER, modelMatrices.size() * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);

    std::vector<glm::mat4> invModelMatrices;
    invModelMatrices.reserve(modelMatrices.size());
     for(auto model :modelMatrices ) {
        invModelMatrices.emplace_back(glm::inverse(model));
    }

    unsigned int invModelVBO;
    glGenBuffers(1, &invModelVBO);
    glBindBuffer(GL_ARRAY_BUFFER, invModelVBO);
    glBufferData(GL_ARRAY_BUFFER, modelMatrices.size() * sizeof(glm::mat4), &invModelMatrices[0], GL_STATIC_DRAW);

    for(unsigned int i = 0; i < m_meshes.size(); i++)
    {
        unsigned int VAO = m_meshes.at(i).vertexArrayObjectId();
        glBindVertexArray(VAO);
        GLsizei vec4Size = sizeof(glm::vec4);
        
        // 绑定 modelVBO 并设置 location 3-6
        glBindBuffer(GL_ARRAY_BUFFER, modelVBO);
        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*)(1 * 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));

        // 绑定 invModelVBO 并设置 location 7-10
        glBindBuffer(GL_ARRAY_BUFFER, invModelVBO);
        glEnableVertexAttribArray(7); 
        glVertexAttribPointer(7, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)0);
        glEnableVertexAttribArray(8); 
        glVertexAttribPointer(8, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(1 * vec4Size));
        glEnableVertexAttribArray(9); 
        glVertexAttribPointer(9, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(2 * vec4Size));
        glEnableVertexAttribArray(10); 
        glVertexAttribPointer(10, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(3 * vec4Size));

        glVertexAttribDivisor(3, 1);
        glVertexAttribDivisor(4, 1);
        glVertexAttribDivisor(5, 1);
        glVertexAttribDivisor(6, 1);
        glVertexAttribDivisor(7, 1);
        glVertexAttribDivisor(8, 1);
        glVertexAttribDivisor(9, 1);
        glVertexAttribDivisor(10, 1);

        glBindVertexArray(0);
    }
}

这里除了设置了实例化矩阵数组之外,还设置了对应的逆矩阵数组,用于计算缩放后的法向量。

着色器代码

cpp 复制代码
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
layout (location = 2) in vec3 aNormal;
layout (location = 3) in mat4 instanceMatrix;
layout (location = 7) in mat4 instanceinvMatrix;

uniform mat4 view;
uniform mat4 projection;
out vec2 TexCoord;
out vec3 Normal;
out vec3 FragPos;

void main()
{
   gl_Position = projection * view * instanceMatrix * vec4(aPos, 1.0);
   TexCoord = aTexCoord;
   Normal = mat3(transpose(instanceinvMatrix)) * aNormal;
   FragPos = vec3(instanceMatrix * vec4(aPos, 1.0));
}

最后修改着色器代码
这里是完整代码。

我们可以对比一个使用实例化渲染和非实例化渲染的帧率

在不使用非实例化渲染的情况下,帧率仅有 5.22,这已经能明显感受到卡顿了。

相关推荐
隐士Xbox1 小时前
c++ 指针的用法
开发语言·c++·计算机视觉
salipopl2 小时前
C++ 面试题:C++中 constexpr 函数的限制有哪些?
c++
无限进步_2 小时前
【C++】从红黑树到 map 和 set:封装设计与迭代器实现
开发语言·数据结构·数据库·c++·windows·github·visual studio
handler012 小时前
速通蓝桥杯省一:二分算法
c语言·开发语言·c++·笔记·算法·职场和发展·蓝桥杯
汉克老师2 小时前
GESP5级C++考试语法知识(十六、分治算法(三))
c++·算法·分治算法·汉诺塔·逆序对·gesp5级·gesp五级
hele_two2 小时前
SDL2设置透明度
c++·图形渲染
小杰3122 小时前
网络框架源码阅读技巧
服务器·网络·c++·reactor·zlmediakit·zltoolkit
叼烟扛炮2 小时前
C++ 知识点12 构造函数
开发语言·c++·算法·构造函数
满天星83035772 小时前
定长内存池ObjectPool
数据结构·c++·算法·链表