《OpenGL编程指南》一3.5 多实例渲染

简介:

本节书摘来自华章出版社《OpenGL编程指南》一书中的第3章,第3.5节,作者 Bill Licea-Kane ,更多章节内容可以访问云栖社区“华章计算机”公众号查看

3.5 多实例渲染

实例化(instancing)或者多实例渲染(instanced rendering)是一种连续执行多条相同的渲染命令的方法,并且每个渲染命令所产生的结果都会有轻微的差异。这是一种非常有效的,使用少量API调用来渲染大量几何体的方法。OpenGL中已经提供了一些常用绘制函数的多变量形式来优化命令的多次执行。此外,OpenGL中也提供了多种机制,允许着色器使用绘制的不同实例作为输入,并且对每个实例(而不是每个顶点)都赋予不同的顶点属性值。最简单的多实例渲染的命令是:
void glDrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei primCount);
通过mode、first和count所构成的几何体图元集(相当于glDrawArrays()函数所需的独立参数),绘制它的primCount个实例。对于每个实例,内置变量gl_InstanceID都会依次递增,新的数值会被传递到顶点着色器,以区分不同实例的顶点属性。
这个函数是glDrawArrays()的多实例版本,我们可以注意到这两个函数之间的相似之处。glDrawArraysInstanced()的参数与glDrawArrays()是完全等价的,只是多了一个primCount参数。这个参数用于设置准备渲染的实例个数。当OpenGL执行这个函数的时候,它实际上会执行glDrawArrays()的primCount次拷贝,每次的mode、first和count参数都是直接传入的。其他OpenGL的绘制命令也有对应的*Instanced版本,例如glDrawElementsInstanced()(对应glDrawElements())和glDrawElementsInstancedBaseVertex()(对应glDrawElementsBaseVertex())。glDrawElementsInstanced()函数的定义如下:
void glDrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei primCount);
通过mode、count和indices所构成的几何体图元集(相当于glDrawElements()函数所需的独立参数),绘制它的primCount个实例。与glDrawArraysInstanced()类似,对于每个实例,内置变量gl_InstanceID都会依次递增,新的数值会被传递到顶点着色器,以区分不同实例的顶点属性。
再次注意到,glDrawElementsInstanced()的参数与glDrawElements()的是等价的,只是新增了primCount参数。每次调用多实例函数时,在本质上OpenGL都会根据primCount参数来设置多次运行整个命令。这看起来并不是很有用的功能。不过,OpenGL提供了两种机制来设置对应不同实例的顶点属性,并且在顶点着色器中可以获取当前实例所对应的索引号。
void glDrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instanceCount, GLuint baseVertex);
通过mode、count、indices和baseVertex所构成的几何体图元集(相当于glDrawElementsBaseVertex()函数所需的独立参数),绘制它的instanceCount个实例。与glDrawArraysInstanced()类似,对于每个实例,内置变量gl_InstanceID都会依次递增,新的数值会被传递到顶点着色器,以区分不同实例的顶点属性。

3.5.1 多实例的顶点属性

多实例的顶点属性与正规的顶点属性是类似的。它们在顶点着色器中的声明和使用方式都是完全一致的。对于应用程序端来说,它们的配置方法与正规的顶点属性也是相同的。也就是说,它们需要保存到缓存对象中,可以通过glGetAttribLocation()查询,通过glVertexAttribPointer()来设置,以及通过glEnableVertexAttribArray()和glDisableVertexAttribArray()进行启用与禁用。下面的重要的函数就是用来启用多实例的顶点属性的:
void glVertexAttribDivisor(GLuint index, GLuint divisor);
设置多实例渲染时,位于index位置的顶点着色器中顶点属性是如何分配值到每个实例的。divisor的值如果是0的话,那么该属性的多实例特性将被禁用,而其他的值则表示顶点着色器,每divisor个实例都会分配一个新的属性值。
glVertexAttribDivisor()函数用于控制顶点属性更新的频率。index表示设置多实例特性的顶点属性的索引位置,它与传递给glVertexAttribPointer()和glEnableVertexAttribArray()的索引值是一致的。默认情况下,每个顶点都会分配到一个独立的属性值。如果divisor设置为0的话,那么顶点属性将遵循这一默认,非实例化的规则。如果divisor设置为一个非零的值,那么顶点属性将启用多实例的特性,此时OpenGL从属性数组中每隔divisor个实例都会读取一个新的数值(而不是之前的每个顶点)。此时在这个属性所对应的顶点属性数组中,数据索引值的计算将变成instance/divisor的形式,其中instance表示当前的实例数目,而divisor就是当前属性的更新频率值。对于每个多实例的顶点属性来说,在顶点着色器中,每个实例中的所有顶点都会共享同一个属性值。如果divisor设置为2的话,那么每两个实例会共享同一个属性值;如果值为3,那么就是每三个实例,以此类推。我们可以参考例3.9中的顶点属性声明,这其中已经包含了一些多实例的属性。
例3.9 多实例的顶点着色器属性示例
image
image

注意在例3.9中,多实例顶点属性color和model_matrix的声明并没有什么特别的地方。现在再阅读例3.10中的代码,其中已经将例3.9中的一部分顶点属性设置为多实例的形式。
例3.10 多实例顶点属性的设置示例
image
image
image

例3.10当中,position和normal是规则的,非实例化的顶点属性。而color是一个divisor被设置为1的多实例顶点属性。也就是说,每个实例的color属性都会有一个独立的值(而实例当中的所有顶点都会使用这一个值)。此外,model_matrix属性也被设置为多实例的属性,它可以为每个实例都提供一个新的模型变换矩阵。mat4类型的属性会占用多个连续的位置。因此我们需要遍历矩阵的每一列并且分别进行设置。顶点着色器中剩余的代码部分可以参见例3.11。
例3.11 多实例属性的顶点着色器示例
image

上面的代码设置了各个实例的模型矩阵,然后使用例3.12中的着色器代码来绘制几何体实例。每个实例都有自己的模型矩阵,而观察矩阵(包括一个绕Y轴的旋转,以及一个Z方向的平移操作)对于所有的实例都是相同的。模型矩阵是通过glMapBuffer()映射的方式直接写入到缓存中的。每个模型矩阵都会将物体移动到远离原点的位置,然后绕着原点对平移过的物体进行旋转。观察和投影矩阵都是简单地通过uniform变量来传递的。然后,我们直接调用一次glDrawArraysInstanced(),绘制模型的所有实例。
例3.12 多实例绘制的代码示例
image

程序运行的结果如图3-8所示。在这个例子中,常量INSTANCE_COUNT(在例3.10和例3.12的代码中被使用)的值为100。一共绘制了100份模型的拷贝,每个拷贝都有一个不同的位置和颜色。这些模型也可以很简单地改成森林中的数目、太空舰队中的飞船,或者城市中的一栋建筑。
例3.9到例3.12中存在一些效率问题。每个实例中的所有顶点都会产生一些相同的结果值,但是它们依然会被逐顶点地进行计算。有的时候应当考虑解决这类问题。例如,model_view_matrix的计算结果矩阵对于单个实例中的所有顶点都是相同的。这里,我们可以通过第二个实例化的mat4属性,输入逐实例的模型视点矩阵数据来避免重复的计算工作,其他时候可能无法避免这种计算,但是还是可以把它移动到几何着色器中完成,这样每次计算都是逐图元,而非逐顶点完成的,或者也可以用到几何着色器的多实例方法。我们会在第10章介绍这些技术的内容。

image

 调用一个多实例的绘制命令,与多次调用它的非实例化的版本然后再执行其他的OpenGL命令,几乎是等价的操作。因此,如果将循环当中已有的一系列OpenGL函数直接转换成一系列的实例化绘制命令,那么得到的结果不会是一致的。
另一个使用多实例顶点属性的例子就是将一系列纹理打包到一个2D纹理数组中,然后将数组的序号通过实例化的顶点属性传递给每个实例。顶点着色器可以将实例对应的序号传递到片元着色器中,然后使用不同的纹理来渲染不同的几何体实例。
我们也可以在系统内部设置一个偏移值,以改变顶点缓存中得到实例化的顶点属性时的索引位置。与glDrawElementsBaseVertex()中提供的baseVertex参数类似,在多实例绘制函数当中,实例的索引偏移值可以通过一个额外的baseInstance参数来设置。带有这个baseInstance参数的函数包括glDrawArraysInstancedBaseInstance()、glDrawElementsInstancedBaseInstan-ce()和glDrawElementsInstancedBaseVertexBaseInstance()。它们的原型如下:
void glDrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instanceCount, GLuint baseInstance);
对于通过mode、first和count所构成的几何体图元集(相当于glDrawArrays()函数所需的独立参数),绘制它的primCount个实例。对于每个实例,内置变量gl_InstanceID都会依次递增,新的数值会被传递到顶点着色器,以区分不同实例的顶点属性。此外,baseInstance的值用来对实例化的顶点属性设置一个索引的偏移值,从而改变OpenGL取出的索引位置。
void glDrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLsizei instanceCount, GLuint baseInstance);
对于通过mode、count和indices所构成的几何体图元集(相当于glDrawElements()函数所需的独立参数),绘制它的primCount个实例。与glDrawArraysInstanced()类似,对于每个实例,内置变量gl_InstanceID都会依次递增,新的数值会被传递到顶点着色器,以区分不同实例的顶点属性。此外,baseInstance的值用来对实例化的顶点属性设置一个索引的偏移值,从而改变OpenGL取出的索引位置。
void glDrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLsizei instanceCount, GLuint baseVertex, GLuint baseInstance);
对于通过mode、count、indices和baseVertex所构成的几何体图元集(相当于glDrawElementsBaseVertex()函数所需的独立参数),绘制它的primCount个实例。与glDrawArraysInstanced()类似,对于每个实例,内置变量gl_InstanceID都会依次递增,新的数值会被传递到顶点着色器,以区分不同实例的顶点属性。此外,baseInstance的值用来对实例化的顶点属性设置一个索引的偏移值,从而改变OpenGL取出的索引位置。
3.5.2 在着色器中使用实例计数器
除了多实例的顶点属性之外,当前实例的索引值可以在顶点着色器中通过内置gl_InstanceID变量获得。这个变量被声明为一个整数。它从0开始计数,每当一个实例被渲染之后,这个值都会加1。gl_InstanceID总是存在于顶点着色器中,即使当前的绘制命令并没有用到多实例的特性也是如此。这种时候,它的值保持为0。gl_InstanceID的值可以作为uniform数组的索引使用,也可以作为纹理查找的参数使用,或者作为某个分析函数的输入,以及其他的目的。
在下面的例子中,我们使用gl_InstanceID重现了例3.9到例3.12的功能,不过这一次使用的是纹理缓存对象(Texture Buffer Objects,TBO)而非实例化的顶点属性。这里我们将例3.9中的顶点属性替换为TBO的查找,因此移除了相应的顶点属性设置代码。使用一个TBO来记录每个实例的颜色值,而第二个TBO用来记录模型矩阵的值。其他顶点属性的声明和设置代码与例3.9和例3.10的内容相同(当然,忽略了color和model_matrix属性的设置)。因为现在采用显式的方法在顶点着色器中获得了每个实例的颜色和模型矩阵,所以在顶点着色器的主体中也要添加更多额外的代码,如例3.13所示。
例3.13 顶点着色器的gl_VertexID示例
image
image

为了使用例3.13中的着色器,我们还需要创建和初始化TBO对象,以存储color_tbo和model_matrix_tbo的采样信息,只是不需要再初始化多实例的顶点属性了。不过,除了这些代码设置之间存在差异之外,程序的本质是没有发生变化的。
例3.14 多实例顶点属性的设置示例
image
image

注意,例3.14中的代码实际上比例3.10更为短小和简单。这是因为不再使用内置的OpenGL功能来获取逐实例的数据,而是直接使用着色器写出。这一点从例3.13比例3.11增加的复杂性就可以看出。而这样的变化也带来了更多的强大功能和灵活性。举例来说,如果实例的数量较少,那么使用uniform数组可能比使用TBO来存储数据更为合适,但是后者对性能的改善更为理想。除此之外,使用gl_InstanceID来驱动的方法与原始的例子相比并没有更多的改动。实际上,例3.12中的渲染代码是被完整迁移过来的,它所产生的渲染结果与原来的程序完全相同。我们可以参看下面的截图(见图3-9)。
image

3.5.3 多实例方法的回顾

如果要在程序中使用多实例的方法,那么我们应当:
为准备实例化的内容创建顶点着色器输入。
使用glVertexAttribDivisor()设置顶点属性的分隔频率。
在顶点着色器中使用内置的gl_InstanceID变量。
使用渲染函数的多实例版本,例如glDrawArraysInstanced()、glDrawElementsInstanced()和glDrawElementsInstancedBaseVertex()。

相关文章
|
存储 传感器 编解码
Android OpenGL 渲染图像读取哪家强
glReadPixels 是 OpenGL ES 的 API ,OpenGL ES 2.0 和 3.0 均支持。 使用非常方便,下面一行代码即可搞定,但是效率也是最低的。
1127 0
Android OpenGL 渲染图像读取哪家强
|
3月前
|
JavaScript C++
从OpenGL渲染的角度排查 creator native 局部换肤的问题
从OpenGL渲染的角度排查 creator native 局部换肤的问题
21 0
|
12月前
|
数据安全/隐私保护 开发者
OpenGL ES 多目标渲染(MRT)
Opengl ES连载系列
211 0
|
12月前
|
并行计算 C++
Opengl ES之YUV数据渲染
Opengl ES连载系列
128 0
|
缓存 算法 Java
Android硬件加速(二)-RenderThread与OpenGL GPU渲染
Android硬件加速(二)-RenderThread与OpenGL GPU渲染
1000 0
Android硬件加速(二)-RenderThread与OpenGL GPU渲染
OpenGL学习笔记(二):OpenGL语法、渲染管线以及具体实现过程详解
OpenGL学习笔记(二):OpenGL语法、渲染管线以及具体实现过程详解
OpenGL学习笔记(二):OpenGL语法、渲染管线以及具体实现过程详解
|
存储 缓存 Serverless
六、OpenGL 渲染技巧:深度测试、多边形偏移、 混合
OpenGL 渲染技巧:深度测试、多边形偏移、 混合
257 0
六、OpenGL 渲染技巧:深度测试、多边形偏移、 混合
|
算法 开发者
五、OpenGL 渲染技巧:正背面剔除
OpenGL 渲染技巧:正背面剔除
354 0
五、OpenGL 渲染技巧:正背面剔除
|
API iOS开发 异构计算
三、OpenGL 渲染架构分析
OpenGL 渲染架构分析
317 0
三、OpenGL 渲染架构分析
|
异构计算 索引
OpenGL ES 内建变量以及多重纹理渲染计算
我们用GLSL来编写着色器代码时, 要了解他们的一些内建变量参数的含义. 本文就是对内建变量做一些介绍.
217 0