本人初学者,文中定有代码、术语等错误,欢迎指正
引出
假设有一个绘制了很多模型的场景,而大部分的模型包含的是同一组顶点数据,只不过进行的是不同的世界空间变换,比如:草
渲染上千上万个草,渲染函数调用会极大地影响性能
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变换矩阵,从而变换当前渲染的物体(改变位置、大小等)。
思路
glsl的顶点着色器
定义uniform数组,每个渲染的实例quad:根据gl_InstanceID当做uniform数组的下标得到当前渲染的实例的变换位置
cpp
代码
#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次
效果
引出
由上的实例化例子,使用uniform传递变换数据,但是我们最终会超过最大能够发送至着色器的uniform数据大小上限。
从而需要使用实例化数组,即不使用uniform。
什么是实例化数组
实例化数组它被定义为一个顶点属性(可以用缓冲存储),仅在顶点着色器渲染一个新的实例(可设置为两个或三个等)时才会更新。
既然是顶点属性,则有对应的顶点缓冲对象,附加到顶点数组时需指定顶点属性布局
重点
原本的顶点属性,比如:位置、法线、颜色,都是在顶点着色器的每次运行都会让GLSL获取新一组适用于当前顶点的属性。
详细说明
注意:是当前顶点,比如:一个quad有位置和颜色信息,它有6个顶点(两个三角形组成),需运行6次顶点着色器,每一次运行顶点着色器渲染当前顶点,都需更新当前顶点的属性。
一个实例6个顶点运行6次顶点着色器、每次都得更新顶点的属性。
当我们将顶点属性定义为一个实例化数组时,顶点着色器就只需要对每个实例更新顶点属性的内容。
详细说明
注意:是当前实例,比如:一个quad有一个变换信息,即使它有6个顶点,需运行6次顶点着色器,但当每一次运行顶点着色器渲染当前顶点的变换信息是同一个,只需渲染完6个顶点即为一个实例时,才需要更新顶点属性的内容。
一个实例6个顶点运行6次顶点着色器、6次后才更新顶点的属性。
思路
glsl的顶点着色器
获取每个实例的实例化数组的顶点属性
cpp
代码
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个实例更新一次属性
效果
说明
行星周围的石头都有自己的变换矩阵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太大会感到卡顿)
说明
不使用原本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个石头