10 - 实例化
如果我们要在同屏中渲染大量形状重复的物体,例如草原中的草,如果调用上千次如glDrawArrays()
的渲染绘制函数,会极大的影响性能(GPU渲染物体快,但是CPU和GPU通信太频繁了,反而更慢了)。不妨再次运用“批处理思想”,只调用一次渲染函数,并告诉GPU该渲染几个该物体。
实例化
实例化(Instancing),就是将数据一次性发给GPU,然后用一个绘制函数让OpenGL绘制一个物体多次。
要在OpenGL中使用实例化技术,只需将glDrawArrays()
和glDrawElements()
替换为xxxInstanced()
即可。该函数多了个叫做 实例数量(Instance Count) 的参数,它能够设置我们需要渲染的实例个数。
如何控制这么多实例,GLSL在顶点着色器中嵌入一个叫做gl_InstanceID
的内建变量,它从零开始,每个实例被渲染后会自增1,跟数组索引一样。
简单实例化
接下来尝试用实例化技术画100个2D四边形,首先是片段着色器:
#version 330 core
out vec4 FragColor;
in vec3 fColor;
void main()
{
FragColor = vec4(fColor, 1.0);
}
接下来是顶点着色器,定义了一个uniform数组用于存储每个实例的偏移向量:
#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;
}
别忘了将这些偏移数据给生成并传进去:
// 生成偏移向量
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;
}
}
// 传入偏移向量
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]);
}
最后,使用glDrawArraysInstanced()
或glDrawElementsInstanced()
即可。
最终结果如下:
实例化数组
其实,Uniform数组是有数据大小上限的,而实例化往往要渲染成千上万个物体,使用Uniform数组容易超出上限。因此有了 实例化数组(Instanced Array) 这一概念,它被定义为一个顶点属性,仅在顶点着色器渲染一个新的实例时才会更新。
以下是一个实例化数组的例子:
#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;
}
可以发现,我们多定义了个叫做aOffset
的顶点属性,接下来我们将刚刚生成的translations
数组存进去:
unsigned int instanceVBO;
glGenBuffers(1, &instanceVBO);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec2) * 100, &translations[0], GL_STATIC_DRAW);
最后设置顶点属性:
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glVertexAttribDivisor(2, 1);
其中,glVertexAttribDivisor()
函数告诉OpenGL什么时候更新顶点属性的内容至新一组数据。第一个参数是需要的顶点属性;第二个参数是 属性除数(Attribute Divisor),默认为0,表示顶点着色器每次迭代都要更新,1为渲染一个实例后更新顶点属性数据。
在合适的环境下,实例化渲染能够大大增加显卡的渲染能力。正是出于这个原因,实例化渲染通常会用于渲染草、植被、粒子,等场景,基本上只要场景中有很多重复的形状,都能够使用实例化渲染来提高性能。
参考资料
- 实例化 - LearnOpenGL CN (learnopengl-cn.github.io)