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)