一、实例化
当我们渲染多个相同的物体时,会给性能带来很大的开销。因为我们在渲染之前,会绑定缓冲,设置顶点属性等一系列操作。而这些都是在相对缓慢的CPU到GPU总线上进行的。
而如果我们将数据一次性的发送给GPU,然后利用绘制函数让OpenGL利用这些数据去绘制多个物体,就方便了,这个过程叫做实例化
。
实例化这项技术能够让我们使用一个渲染调用来绘制多个物体,来节省每次绘制物体时CPU -> GPU的通信,它只需要一次即可。如果想使用实例化渲染,我们只需要将glDrawArrays
和glDrawElements
的渲染调用分别改为glDrawArraysInstanced
和glDrawElementsInstanced
就可以了。
这些渲染函数的实例化版本需要一个额外的参数,叫做实例数量(Instance Count),它能够设置我们需要渲染的实例个数。
这样我们只需要将必须的数据发送到GPU一次,然后使用一次函数调用告诉GPU它应该如何绘制这些实例。GPU将会直接渲染这些实例,而不用不断地与CPU进行通信。
然而,这样渲染出来的结果是完全相同的,并且还在同一个位置,所以我们只能看到一个物体,于是我们在顶点着色器中嵌入了另一个内建变量,gl_InstanceID
。
在使用实例化渲染调用时,gl_InstanceID
会从0开始,在每个实例被渲染时递增1。比如说,我们正在渲染第43个实例,那么顶点着色器中它的gl_InstanceID将会是42。因为每个实例都有唯一的ID,我们可以建立一个数组,将ID与位置值对应起来,将每个实例放置在世界的不同位置。
如下:我们在标准化设备坐标系中使用一个渲染调用,绘制100个2D四边形。我们会索引一个包含100个偏移向量的uniform数组,将偏移值加到每个实例化的四边形上。最终的结果是一个排列整齐的四边形网格:
- 每个四边形由2个三角形所组成,一共有6个顶点。每个顶点包含一个2D的标准化设备坐标位置向量和一个颜色向量。
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
};
- 片段着色器从顶点着色器中接受颜色变量,设置为颜色输出,实现四边形的颜色。
#version 330 core
out vec4 FragColor;
in vec3 fColor;
void main()
{
FragColor = vec4(fColor, 1.0);
}
- 在顶点着色器中,我们利用
gl_InstanceID
来索引包含偏移量的offset
数组,获取每个实例的偏移变量。
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;
out vec3 fColor;
uniform vec2 offsets[100];
void main()
{
vec2 offset = offsets[gl_InstanceID];
gl_Position = vec4(aPos + offset, 0.0, 1.0);
fColor = aColor;
}
- offset数组值由我们预先计算得出
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;
}
}
- 然后我们再将预先计算的偏移量赋给uniform数组offset
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]);
}
- 实例化渲染
glBindVertexArray(quadVAO);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);
二、实例化数组
当我们的实例化对象很多时,可能会超过最大能够发送至着色器的uniform数据大小上限,于是,我们可以采用实例化数组,被定义为一个顶点属性中,在顶点着色器渲染一个新的实例的时候更新。
于是,我们将顶点属性定义为一个实例化数组后,顶点着色器只需要对每一个实例进行更新,而不是对顶点进行属性的更新。
- 将偏移量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;
}
- 由于此时实例化数组定义为了顶点属性,所以我们需要将它的内容存在顶点缓冲对象中,并配置属性指针,然后存在一个新的缓冲对象中:
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);
- 设置顶点属性指针,并启用顶点属性
glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribDivisor(2, 1);
其中,glVertexAttribDivisor
定义了什么时候更新顶点属性到新一组数据,第一个参数定义了所需要的顶点属性,第二个参数是属性除数(默认是0):在顶点着色器每次迭代时更新;为1时在渲染一个新实例的时候更新顶点属性;为2时表示每2个实例更新一次属性。于是,我们再次得到我们的渲染结果:
- 我们还可以修改每个实例的大小
void main()
{
vec2 pos = aPos * (gl_InstanceID / 100.0);
gl_Position = vec4(pos + aOffset, 0.0, 1.0);
fColor = aColor;
}