OpenGL 实例化(Instancing)是一种只调用一次渲染函数就能绘制出不少物体的技术,能够实现将数据一次性而不是多次发送给 GPU ,告诉 OpenGL使用一个绘制函数,将这些数据绘制成多个物体。数组实例化(Instancing)避免了 CPU 屡次向 GPU 下达渲染命令(避免屡次调用 glDrawArrays 或 glDrawElements 等绘制函数),节省了绘制多个物体时 CPU 与 GPU 之间的通讯时间,提高了渲染性能。 实例化是因为Verilog等语言里面有实例化,相当于提前定义好的一个模块,在别的模块中可以直接调用(即例化)。OpenGL的实例化与Verilog的例化很像,所以名字叫这个,本质上是对一个模块的多次调用,和批量渲染是一个意思。
//普通渲染
glDrawArrays (GLenum mode, GLint first, GLsizei count);
glDrawElements (GLenum mode, GLsizei count, GLenum type, const void *indices);
//实例化渲染
glDrawArraysInstanced (GLenum mode, GLint first, GLsizei count, GLsizei instancecount);
glDrawElementsInstanced (GLenum mode, GLsizei count, GLenum type, const void *indices, GLsizei instancecount);
相对于普通绘制,实例化绘制多了一个参数instancecount
,表示须要渲染的实例数量,调用完实例化绘制函数后,咱们便将绘制数据一次性发送给 GPU,而后告诉它该如何使用一个函数来绘制这些实例。
实例化(Instancing)的目标并非实现将同一物体绘制屡次,而是能基于某一物体绘制出位置、大小、形状或者颜色不一样的多个物体。glDrawArraysInstanced函数内部实现如下:
if ( mode or count is invalid )
generate appropriate error
else {
for (int i = 0; i < primcount ; i++) {
instanceID = i;
glDrawArrays(mode, first, count);
}
instanceID = 0;
}
其中上述伪代码中的instanceID就是类似于OpenGL 着色器中有一个与实例化绘制相关的内建变量 gl_InstanceID
。gl_InstanceID
表示当前正在绘制实例的 ID ,每一个实例对应一个惟一的 ID ,经过这个 ID 能够轻易实现基于一个物体而绘制出位置、大小、形状或者颜色不一样的多个物体(实例)。
利用内建变量gl_InstanceID
在 3D 空间绘制多个位于不一样位置的立方体,利用 u_offsets[gl_InstanceID]
对当前实例的位置进行偏移,对应的着色器脚本如下:
// vertex shader GLSL
#version 300 es
layout(location = 0) in vec4 a_position;
layout(location = 1) in vec2 a_texCoord;
out vec2 v_texCoord;
uniform mat4 u_MVPMatrix;
uniform vec3 u_offsets[125];
void main()
{
//经过 u_offsets[gl_InstanceID] 对当前实例的位置进行偏移
gl_Position = u_MVPMatrix * (a_position + vec4(u_offsets[gl_InstanceID], 1.0));
v_texCoord = a_texCoord;
}
// fragment shader GLSL
#version 300 es
precision mediump float;
in vec2 v_texCoord;
layout(location = 0) out vec4 outColor;
uniform sampler2D s_TextureMap;
void main()
{
outColor = texture(s_TextureMap, v_texCoord);
}
在 3D 空间中产生 125 个偏移量(offset):
glm::vec3 translations[125];
int index = 0;
GLfloat offset = 0.2f;
for(GLint y = -10; y < 10; y += 4)
{
for(GLint x = -10; x < 10; x += 4)
{
for(GLint z = -10; z < 10; z += 4)
{
glm::vec3 translation;
translation.x = (GLfloat)x / 10.0f + offset;
translation.y = (GLfloat)y / 10.0f + offset;
translation.z = (GLfloat)z / 10.0f + offset;
translations[index++] = translation;
}
}
}
对偏移量数组进行赋值,而后进行实例化绘制,绘制出 125 个不一样位置的立方体:
glUseProgram(m_ProgramObj);
glBindVertexArray(m_VaoId);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureId);
glUniform1i(m_SamplerLoc, 0);
for(GLuint i = 0; i < 125; i++)
{
stringstream ss;
string index;
ss << i;
index = ss.str();
GLint location = glGetUniformLocation(m_ProgramObj, ("u_offsets[" + index + "]").c_str())
glUniform2f(location, translations[i].x, translations[i].y, translations[i].z);
}
glDrawArraysInstanced(GL_TRIANGLES, 0, 36, 125);
glBindVertexArray(0);
利用内建变量 gl_InstanceID
和偏移数组进行实例化绘制还存在一个问题,那就是着色器中 uniform 类型数据存在上限(为何存在上限,参考《OpenGL中的Uniform block size 的大小限制》博文),也就是 u_offsets 这个数组的大小有限制,最终致使咱们绘制的实例存在上限。为了避免这个问题,咱们可使用实例化数组(Instanced Array),它使用顶点属性来定义,这样就容许咱们使用更多的数据,并且仅当顶点着色器渲染一个新实例时它才会被更新。这个时候咱们须要用到函数 glVertexAttribDivisor
。其在官方手册中用法如下:
glVertexAttribDivisor
modifies the rate at which generic vertex attributes advance when rendering multiple instances of primitives in a single draw call. Ifdivisor
is zero, the attribute at slotindex
advances once per vertex. Ifdivisor
is non-zero, the attribute advances once perdivisor
instances of the set(s) of vertices being rendered. An attribute is referred to as instanced if itsGL_VERTEX_ATTRIB_ARRAY_DIVISOR
value is non-zero.
大概的意思是:当单次调用绘制函数渲染多个图元实例时,glVertexAttribDivisor修改了通用顶点属性向前的比例即每次绘制的步长,即是逐顶点还是每隔divisor 个实例渲染一次。如果参数divisor 是0,则glVertexAttribDivisor函数第一个参数index表示的顶点属性每个顶点就执行渲染一次,如果divisor 非0,则参数index表示的顶点属性每隔divisor 实例就渲染一次。这就话的意思说白了就是当divisor 为0时,则每个顶点中参数index表示的顶点属性就渲染一次(逐顶点渲染,以点为单位渲染);而如果divisor 非0,则每divisor 实例,就渲染一次。例如:一次渲染100个三角形(这里每个三角形就是一个实例),如果divisor为0,则每个三角形都绘制(逐顶点渲染,以点为单位渲染,当然,这还跟硬件的UFO有关,参考《OpenGL中的Uniform block size 的大小限制》);如果divisor为1,则第0, 1, 2, 3,4,5.....个三角形会被绘制即每个三角形都会被绘制(逐实例渲染,以实例为单位渲染),也就是说每1个三角形才绘制一次(相比前面的divisor为0结果相同,但效率要高很多);如果divisor为3,则第0, 3, 6, 9,12,15......个三角形会被绘制,而第1,2,4,5,7,8,9,10,11,13,14......不会绘制,直接丢弃,也就是说每隔3个三角形才绘制一次,以此类推。
glVertexAttribDivisor(GLuint index, GLuint divisor)
该函数表示每隔divisor个实例,就会往顶点着色器中传入buffer中的一个新的属性值,其中index为属性在着色器中的位置索引值即下面代码中的location = 0、location = 1、location = 2等。默认的情况下本函数的第二个参数为0,即每次将指定index索引表示的所有属性数据更新到着色器中,比如:一个模型由30万个三角形组成,则这30万个三角形共90万(假设每个三角形不共顶点)个顶点,一次性更新到着色器,这对性能及Uniform block size无疑是一场灾难。当第二个参数为非0值时,如:1,则表示每隔1个三角形即3个顶点下的index索引表示的属性数据载入一次。即先载入更新第0个三角形数据到着色器,处理完后再更新第1个三角形数据到着色器,以此类推,直到全部三角形都处理完。如:2,则表示每隔2个三角形index索引表示的属性数据载入一次。即先载入更新第0个三角形数据到着色器,处理完后再更新第2个三角形数据到着色器,处理完后再更新第4个三角形数据到着色器,丢弃、不处理第1、3.....个三角形的数据,以此类推,直到全部三角形都处理完。
void glVertexAttribDivisor (GLuint index, GLuint divisor);
// index 表示顶点属性的索引
// divisor 表示每 divisor 个实例更新下顶点属性到下个元素,默认为 0
利用顶点属性来定义的实例化数组(Instanced Array) 在 3D 空间绘制多个位于不一样位置的立方体,对应的着色器脚本:
// vertex shader GLSL
#version 300 es
layout(location = 0) in vec4 a_position;
layout(location = 1) in vec2 a_texCoord;
layout(location = 2) in vec2 a_offset;
out vec2 v_texCoord;
uniform mat4 u_MVPMatrix;
void main()
{
gl_Position = u_MVPMatrix * (a_position + vec4(a_offset, 1.0));
v_texCoord = a_texCoord;
}
// fragment shader GLSL
#version 300 es
precision mediump float;
in vec2 v_texCoord;
layout(location = 0) out vec4 outColor;
uniform sampler2D s_TextureMap;
void main()
{
outColor = texture(s_TextureMap, v_texCoord);
}
设置 VAO 和 VBO :
// Generate VBO Ids and load the VBOs with data
glGenBuffers(2, m_VboIds);
glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[1]);
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec3) * 125, &translations[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
// Generate VAO Id
glGenVertexArrays(1, &m_VaoId);
glBindVertexArray(m_VaoId);
glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (const void *) 0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (const void *) (3* sizeof(GLfloat)));
glEnableVertexAttribArray(2);
//利用顶点属性来定义的实例化数组(Instanced Array)
glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[1]);
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
//指定 index=2 的属性为实例化数组,1 表示每绘制一个实例,更新一次数组中的元素
glVertexAttribDivisor(2, 1); // Tell OpenGL this is an instanced vertex attribute.
glBindVertexArray(GL_NONE);
其中glVertexAttribDivisor(2, 1);
是上述最重要的一步,用于指定 index = 2 的属性为实例化数组,1 表示每绘制一个实例,更新一次数组中的元素。利用顶点属性来定义的实例化数组,而后绘制出 125 个不一样位置的立方体:
glUseProgram(m_ProgramObj);
glBindVertexArray(m_VaoId);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureId);
glUniform1i(m_SamplerLoc, 0);
glDrawArraysInstanced(GL_TRIANGLES, 0, 36, 125);
glBindVertexArray(0);
另外《实例化》这篇文章也深刻阐述了实例化。