我们有时候需要绘制大量的同一模型的物体,比如游戏中的子弹,它们使用同个模型,只是位置不同,我们的代码将会是这样的:
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);
和 glDrawElements 或 glDrawArrays 参数基本一致,最后一个参数表示实例化的数量(物体的数量)。
glDrawElementsInstanced 或 glDrawArraysInstanced 解决了绘制多个物体的问题,但是我们还需要为这些物体设置不同的模型矩阵。在上文中提到,如果把所有的 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,这已经能明显感受到卡顿了。