假设你有一个绘制了很多模型的场景,而大部分的模型包含的是同一组顶点数据,只不过进行的是不同的世界空间变换。想象一个充满草的场景:每根草都是一个包含几个三角形的小模型。你可能会需要绘制很多根草,最终在每帧中你可能会需要渲染上千或者上万根草。因为每一根草仅仅是由几个三角形构成,渲染几乎是瞬间完成的,但上千个渲染函数调用却会极大地影响性能。
如果像这样绘制模型的大量实例(Instance),很快就会因为绘制调用过多而达到性能瓶颈。
如果我们能够将数据一次性发送给GPU,然后使用一个绘制函数让OpenGL利用这些数据绘制多个物体,就会更方便了。这就是实例化(Instancing)。
实例化这项技术能够让我们使用一个渲染调用来绘制多个物体,来节省每次绘制物体时CPU -> GPU的通信,它只需要一次即可。如果想使用实例化渲染,我们只需要将glDrawArrays和glDrawElements的渲染调用分别改为glDrawArraysInstanced和glDrawElementsInstanced就可以了。
GLSL在顶点着色器中嵌入了另一个内建变量,gl_InstanceID。
在使用实例化渲染调用时,gl_InstanceID会从0开始,在每个实例被渲染时递增1。比如说,我们正在渲染第43个实例,那么顶点着色器中它的gl_InstanceID将会是42。因为每个实例都有唯一的ID,我们可以建立一个数组,将ID与位置值对应起来,将每个实例放置在世界的不同位置。
//顶点着色器
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
uniform vec2 offsets[100];
out vec3 Color;
void main() {
Color=aNormal;
vec2 offset = offsets[gl_InstanceID];
gl_Position = vec4(aPos, 1.0)+vec4(offset,0.0,0.0);
}
//片段着色器
#version 330 core
out vec4 FragColor;
in vec3 Color;
void main() {
FragColor = vec4(Color, 1.0);
}
赋值数据:位置偏移量
QVector2D 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) {
QVector2D translation;
translation.setX((float)x / 10.0f + offset);
translation.setY((float)y / 10.0f + offset);
translations[index++] = translation;
}
}
m_ShaderProgram.bind();
for(unsigned int i = 0; i < 100; i++) {
std::string str="offsets[" + std::to_string(i) + "]";
m_ShaderProgram.setUniformValue(str.c_str(), translations[i]);
}
渲染:
m_glFuns->glBindVertexArray(VAO);
m_glFuns->glDrawArraysInstanced(GL_TRIANGLES, 0, vertices.size(),100);
虽然之前的实现在目前的情况下能够正常工作,但是如果我们要渲染远超过100个实例的时候(这其实非常普遍),我们最终会超过最大能够发送至着色器的uniform数据大小上限。它的一个代替方案是实例化数组,它被定义为一个顶点属性(能够让我们储存更多的数据),仅在顶点着色器渲染一个新的实例时才会更新。
我们需要在顶点着色器中再添加一个顶点属性:
#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()
{
gl_Position = vec4(aPos + aOffset, 0.0, 1.0);
fColor = aColor;
}
我们不再使用gl_InstanceID,现在不需要索引一个uniform数组就能够直接使用offset属性了。
因为实例化数组和position与color变量一样,都是顶点属性,我们还需要将它的内容存在顶点缓冲对象中,并且配置它的属性指针。
unsigned int instanceVBO;
m_glFuns->glGenBuffers(1, &instanceVBO);
m_glFuns->glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
m_glFuns->glBufferData(GL_ARRAY_BUFFER, sizeof(QVector2D) * 100, &translations[0], GL_STATIC_DRAW);
m_glFuns->glBindBuffer(GL_ARRAY_BUFFER, 0);
之后我们还需要设置它的顶点属性指针,并启用顶点属性:
m_glFuns->glEnableVertexAttribArray(2);
m_glFuns->glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
m_glFuns->glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
m_glFuns->glBindBuffer(GL_ARRAY_BUFFER, 0);
m_glFuns->glVertexAttribDivisor(2, 1);
我们调用了glVertexAttribDivisor。这个函数告诉了OpenGL该什么时候更新顶点属性的内容至新一组数据。它的第一个参数是需要的顶点属性;第二个参数是属性除数(Attribute Divisor),设置为1时,我们告诉OpenGL我们希望在渲染一个新实例的时候更新顶点属性。
现在使用glDrawArraysInstanced,再次渲染四边形,
m_glFuns->glDrawArraysInstanced(GL_TRIANGLES, 0, vertices.size(),100);