3.4 OpenGL的绘制命令
大部分OpenGL绘制命令都是以Draw这个单词开始的。绘制命令大致可以分为两个部分:索引形式和非索引形式的绘制。索引形式的绘制需要用到绑定GL_ELEMENT_ARRAY_BUFFER的缓存对象中存储的索引数组,它可以用来间接地对已经启用的顶点数组进行索引。另一方面,非索引的绘制不需要使用GL_ELEMENT_ARRAY_BUFFER,只需要简单地按顺序读取顶点数据即可。OpenGL当中,最基本的非索引形式的绘制命令就是glDrawArrays()。
void glDrawArrays(GLenum mode, GLint f?irst, GLsizei count);
使用数组元素建立连续的几何图元序列,每个启用的数组中起始位置为f?irst,结束位置为f?irst + count–1。mode表示构建图元的类型,它必须是GL_TRIANGLES、GL_LINE_LOOP、GL_LINES、GL_POINTS等类型标识符之一。
与之类似,最基本的索引形式的绘制命令是glDrawElements()。
void glDrawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices);
使用count个元素来定义一系列几何图元,而元素的索引值保存在一个绑定到GL_ELEMENT_ARRAY_BUFFER的缓存中(元素数组缓存,element array buffer)。indices定义了元素数组缓存中的偏移地址,也就是索引数据开始的位置,单位为字节。type必须是GL_UNSIGNED_BYTE、GL_UNSIGNED_SHORT或者GL_UNSIGNED_INT中的一个,它给出了元素数组缓存中索引数据的类型。mode定义了图元构建的方式,它必须是图元类型标识符中的一个,例如GL_TRIANGLES、GL_LINE_LOOP、GL_LINES或者GL_POINTS。
这些函数都会从当前启用的顶点属性数组中读取顶点的信息,然后使用它们来构建mode指定的图元类型。顶点属性数组的启用可以通过glEnableVertexAttribArray()来完成,如第1章所介绍的。而glDrawArrays()只是直接将缓存对象中的顶点属性按照自身的排列顺序,直接取出并使用。glDrawElements()使用了元素数组缓存中的索引数据来索引各个顶点属性数组。所有看起来更为复杂的OpenGL绘制函数,在本质上都是基于这两个函数来完成功能实现的。例如,glDrawElementsBaseVertex()可以将元素数组缓存中的索引数据进行一个固定数量的偏移。
void glDrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex);
本质上与glDrawElements()并无区别,但是它的第i个元素在传入绘制命令时,实际上读取的是各个顶点属性数组中的第indices[i] + basevertex个元素。
glDrawElementsBaseVertex()可以根据某个索引基数来解析元素数组缓存中的索引数据。例如,如果一个模型存在多个版本(例如模型动画的多帧数据),并且保存在一个独立的顶点缓存集合中,只通过缓存中不同的偏移量来区分。那么glDrawElementsBaseVertex()就可以通过设置某一帧对应的索引基数,直接绘制这一帧所对应的动画数据。而每一帧用到的索引数据集总是一致的。
另一个与glDrawElements()行为很类似的函数是glDrawRangeElements()。
void glDrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const GLvoid* indices);
这是glDrawElements()的一种更严格的形式,它实际上相当于应用程序(也就是开发者)与OpenGL之间形成的一种约定,即元素数组缓存中所包含的任何一个索引值(来自indices)都会落入到start和end所定义的范围当中。
我们还可以通过这些功能的组合来实现一些更为高级的命令,例如,glDrawRange-ElementsBaseVertex()就相当于glDrawElementsBaseVertex()与glDrawRangeElements()功能的一种组合形式。
void glDrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex);
同应用程序之间建立一种约束,其形式与glDrawRangeElements()类似,不过它同时也支持使用basevertex来设置顶点索引的基数。在这里,这个函数将首先检查元素数组缓存中保存的数据是否落入start和end之间,然后再对其添加basevertex基数。
这些函数同时还存在一些多实例的版本。多实例的介绍请参见下一节“多实例渲染”。多实例形式的命令包括glDrawArraysInstanced()、glDrawElementsInstanced(),甚至还有glDrawElementsInstancedBaseVertex()。最后,我们还要介绍两个特殊的命令,它们的参数不是直接从程序中得到的,而是从缓存对象当中获取。它们被称作间接绘制函数,如果要使用的话,必须先将一个缓存对象绑定到GL_DRAW_INDIRECT_BUFFER目标上。glDrawArrays()的间接版本叫做glDrawArraysIndirect()。
void glDrawArraysIndirect(GLenum mode, const GLvoid* indirect);
特性与glDrawArraysInstanced()完全一致,不过绘制命令的参数是从绑定到GL_DRAW_INDIRECT_BUFFER的缓存(间接绘制缓存,draw indirect buffer)中获取的结构体数据。indirect记录间接绘制缓存中的偏移地址。mode必须是glDrawArrays()所支持的某个图元类型。
glDrawArraysIndirect()中的实际绘制命令参数,是从间接绘制缓存中indirect地址的结构体中获取的。这个结构体的C语言形式的声明如例3.3所示。
例3.3 DrawArraysIndirectCommand结构体的声明
DrawArraysIndirectCommand结构体的所有域成员都会作为glDrawArraysInstanced()的参数进行解析。其中f?irst和count会被直接传递到内部函数中。primCount表示多实例的个数,而baseInstance就相当于多实例顶点属性的baseInstance偏移(不用担心,我们马上就会介绍多实例渲染的相关命令)。
glDrawElements()的间接版本叫做glDrawElementsIndirect(),它的原型定义如下:
void glDrawElementsIndirect(GLenum mode, GLenum type, const GLvoid* indirect);
本质上与glDrawElements()是一致的,但是绘制命令的参数是从绑定到GL_DRAW_INDIRECT_BUFFER的缓存中获取的。indirect记录了间接绘制缓存中的偏移地址。mode必须是glDrawElements()所支持的某个图元类型,而type指定了绘制命令调用时元素数组缓存中索引值的类型。
如果要使用glDrawArraysIndirect(),那么glDrawArraysIndirect()中需要的参数也来自于元素数组缓存中indirect偏移地址所存储的结构体。这个结构体的C语言形式的声明如例3.4所示:
例3.4 DrawElementsIndirectCommand结构体的声明
DrawArraysIndirectCommand结构体中,所有DrawElementsIndirectCommand的域成员都会作为glDrawElementsInstancedBaseVertex()的参数进行解析。count和baseVertex会被直接传递到内部函数中。与glDrawArraysIndirect()中一致,primCount也表示多实例的个数,f?irstIndex可以与type参数所定义的索引数据大小相结合,以计算传递到glDrawEle-mentsInstancedBaseVertex()的索引数据结果。此外,baseInstance用来表示结果绘制命令中,所有多实例顶点属性的实例偏移值。
现在,我们将讨论一些不是以Draw开头的绘制命令。它们属于绘制命令的多变量形式,包括glMultiDrawArrays()、glMultiDrawElements()和glMultiDrawElementsBaseVertex()。每个函数都记录了一个f?irst参数的数组,以及一个count参数的数组,其工作方式相当于对每个数组的元素,都会执行一次原始的单一变量函数。举例来说,glMultiDrawArrays()函数的原型如下:
void glMultiDrawArrays(GLenum mode, const GLint f?irst, const GLint count, GLsizei primcount);
在一个OpenGL函数调用过程中绘制多组几何图元集。f?irst和count都是数组的形式,数组的每个元素都相当于一次glDrawArrays()调用,元素的总数由primcount决定。
调用glMultiDrawArrays()等价于下面的OpenGL代码段:
类似地,glDrawElements()的多变量版本就是glMultiDrawElements(),它的原型如下:
void glMultiDrawElements(GLenum mode, const GLint count, GLenum type, const GLvoid const* indices, GLsizei primcount);
在一个OpenGL函数调用过程中绘制多组几何图元集。f?irst和indices都是数组的形式,数组的每个元素都相当于一次glDrawElements()调用,元素的总数由primcount决定。
调用glMultiDrawElements()等价于下面的OpenGL代码段:
glMultiDrawElements()的扩展版本包含了额外的baseVertex参数,也就是glMulti-DrawElementsBaseVertex()函数。它的原型如下所示:
void glMultiDrawElementsBaseVertex(GLenum mode, const GLint count, GLenum type, const GLvoid const indices, GLsizei primcount, const GLint baseVertex);
在一个OpenGL函数调用过程中绘制多组几何图元集。f?irst、indices和baseVertex都是数组的形式,数组的每个元素都相当于一次glDrawElementsBaseVertex()调用,元素的总数由primcount决定。
与之前所述的其他OpenGL多变量绘制命令类似,glMultiDrawElementsBaseVertex()也可以等价于下面的OpenGL代码段:
最后,如果有大量的绘制内容需要处理,并且相关参数已经保存到一个缓存对象中,可以直接使用glDrawArraysIndirect()或者glDrawElementsIndirect()处理的话,那么也可以使用这两个函数的多变量版本,即glMultiDrawArraysIndirect()和glMultiDraw-ElementsIndirect()。
void glMultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
绘制多组图元集,相关参数全部保存到缓存对象中。在glMultiDrawArraysIndirect()的一次调用当中,可以分发总共drawcount个独立的绘制命令,命令中的参数与glDrawArraysIndirect()所用的参数是一致的。每个DrawArraysIndirectCommand结构体之间的间隔都是stride个字节。如果stride是0的话,那么所有的数据结构体将构成一个紧密排列的数组。
void glMultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
绘制多组图元集,相关参数全部保存到缓存对象中。在glMultiDrawElementsIndirect()的一次调用当中,可以分发总共drawcount个独立的绘制命令,命令中的参数与glDrawElementsIndirect()所用的参数是一致的。每个DrawElementsIndirectCommand结构体之间的间隔都是stride个字节。如果stride是0的话,那么所有的数据结构体将构成一个紧密排列的数组。
OpenGL绘制练习
这里给出一个相对比较简单的例子,它使用了本章中介绍的一部分OpenGL绘制命令。例3.5中所示为数据载入到缓存中,并准备用于绘制的过程。例3.6中所示为绘制命令调用的过程。
例3.5 绘制命令的准备过程示例
例3.6 绘制命令示例
例3.5和例3.6的程序运行结果如图3-5所示。它看起来并不是特别引人入胜,不过这里你可以看到四个相似的三角形,并且每个三角形的渲染都用到了一个不同的绘制命令。
3.4.1 图元的重启动
当需要处理较大的顶点数据集的时候,我们可能会被迫执行大量的OpenGL绘制操作,并且每次绘制的内容总是与前一次图元的类型相同(例如GL_TRIANGLE_STRIP)。当然,我们可以使用glMultiDraw*()形式的函数,但是这样需要额外去管理图元的起始索引位置和长度的数组。
OpenGL支持在同一个渲染命令中进行图元重启动的功能,此时需要指定一个特殊的值,叫做图元重启动索引(primitive restart index),OpenGL内部会对它做特殊的处理。如果绘制调用过程中遇到了这个重启动索引,那么就会从这个索引之后的顶点开始,重新开始进行相同图元类型的渲染。图元重启动索引的定义是通过glPrimitiveRestartIndex()函数来完成的。
void glPrimitiveRestartIndex(GLuint index);
设置一个顶点数组元素的索引值,用来指定渲染过程中,从什么地方启动新的图元绘制。如果在处理定点数组元素索引的过程中遇到了一个符合该索引的数值,那么系统不会处理它对应的顶点数据,而是终止当前的图元绘制,并且从下一个顶点重新开始渲染同一类型的图元集合。
如果顶点的渲染需要在某一个glDrawElements()系列的函数调用中完成,那么它可以用到glPrimitiveRestartIndex()所指定的索引,并且检查这个索引值是否会出现在元素数组缓存中。不过,我们必须启用图元重启动特性之后才可以进行这种检查。图元重启动的控制可以通过glEnable()和glDisable()函数来完成,调用的参数为GL_PRIMITIVE_RESTART。
考虑图3-6中的顶点布局,它给出了一个三角形条带,并且通过图元重启动的方式打断为两个部分。在图中,图元重启动索引设置为8。在三角形渲染过程中,OpenGL会一直监控元素数组缓存中是否出现索引8,当这个值出现的时候,OpenGL不会创建一个顶点,而是结束当前的三角形条带绘制。下一个顶点(索引9)将成为一个新的三角形条带的第一个顶点,因此我们最终构建了两个三角形条带。
下面的例子演示了图元重启动的一个简单应用—这里使用图元重启动索引将一个立方体分割为两个三角形条带。例3.7和例3.8所示为立方体的数据设置过程,以及绘制过程。
例3.7 初始化立方体数据,它是由两个三角形条带组成的
图3-7所示就是例3.7给出的三角形数据,它使用两个独立的三角形条带来表达一个立方体的形状。
例3.8 使用图元重启动的方式绘制由两个三角形条带组成的立方体
每当OpenGL在元素数组缓存中遇到当前设置的重启动索引时,都会执行图元重启动的操作。因此,不妨将重启动索引设置为一个代码中绝对不会用到的数值。默认的重启动索引为0,但是这个值非常容易出现在元素数组缓存当中。一个不错的选择是2n- 1,这里的n表示索引值的位数(例如GL_UNSIGNED_SHORT的索引就是16,而GL_UNSIGNED_INT的索引就是32)。这个数不太可能是一个真实的索引值。如果将它作为重启动索引标准值的话,那么我们也就不需要为程序中的每一个模型都单独设置一个索引了。
3.4.2 多实例渲染
实例化(instancing)或者多实例渲染(instanced rendering)是一种连续执行多条相同的渲染命令的方法,并且每个渲染命令所产生的结果都会有轻微的差异。这是一种非常有效的,使用少量API调用来渲染大量几何体的方法。OpenGL中已经提供了一些常用绘制函数的多变量形式来优化命令的多次执行。此外,OpenGL中也提供了多种机制,允许着色器使用绘制的不同实例作为输入,并且对每个实例(而不是每个顶点)都赋予不同的顶点属性值。最简单的多实例渲染的命令是:
void glDrawArraysInstanced(GLenum mode, GLint f?irst, GLsizei count, GLsizei primCount);
通过mode、f?irst和count所构成的几何体图元集(相当于glDrawArrays()函数所需的独立参数),绘制它的primCount个实例。对于每个实例,内置变量gl_InstanceID都会依次递增,新的数值会被传递到顶点着色器,以区分不同实例的顶点属性。
这个函数是glDrawArrays()的多实例版本,我们可以注意到这两个函数之间的相似之处。glDrawArraysInstanced()的参数与glDrawArrays()是完全等价的,只是多了一个primCount参数。这个参数用于设置准备渲染的实例个数。当OpenGL执行这个函数的时候,它实际上会执行glDrawArrays()的primCount次拷贝,每次的mode、f?irst和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都会依次递增,新的数值会被传递到顶点着色器,以区分不同实例的顶点属性。
多实例的顶点属性
多实例的顶点属性与正规的顶点属性是类似的。它们在顶点着色器中的声明和使用方式都是完全一致的。对于应用程序端来说,它们的配置方法与正规的顶点属性也是相同的。也就是说,它们需要保存到缓存对象中,可以通过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 多实例的顶点着色器属性示例
注意在例3.9中,多实例顶点属性color和model_matrix的声明并没有什么特别的地方。现在再阅读例3.10中的代码,其中已经将例3.9中的一部分顶点属性设置为多实例的形式。
例3.10 多实例顶点属性的设置示例
例3.10当中,position和normal是规则的,非实例化的顶点属性。而color是一个divisor被设置为1的多实例顶点属性。也就是说,每个实例的color属性都会有一个独立的值(而实例当中的所有顶点都会使用这一个值)。此外,model_matrix属性也被设置为多实例的属性,它可以为每个实例都提供一个新的模型变换矩阵。mat4类型的属性会占用多个连续的位置。因此我们需要遍历矩阵的每一列并且分别进行设置。顶点着色器中剩余的代码部分可以参见例3.11。
例3.11 多实例属性的顶点着色器示例
上面的代码设置了各个实例的模型矩阵,然后使用例3.12中的着色器代码来绘制几何体实例。每个实例都有自己的模型矩阵,而观察矩阵(包括一个绕Y轴的旋转,以及一个Z方向的平移操作)对于所有的实例都是相同的。模型矩阵是通过glMapBuffer()映射的方式直接写入到缓存中的。每个模型矩阵都会将物体移动到远离原点的位置,然后绕着原点对平移过的物体进行旋转。观察和投影矩阵都是简单地通过uniform变量来传递的。然后,我们直接调用一次glDrawArraysInstanced(),绘制模型的所有实例。
例3.12 多实例绘制的代码示例
程序运行的结果如图3-8所示。在这个例子中,常量INSTANCE_COUNT(在例3.10和例3.12的代码中被使用)的值为100。一共绘制了100份模型的拷贝,每个拷贝都有一个不同的位置和颜色。这些模型也可以很简单地改成森林中的数目、太空舰队中的飞船,或者城市中的一栋建筑。
例3.9到例3.12中存在一些效率问题。每个实例中的所有顶点都会产生一些相同的结果值,但是它们依然会被逐顶点地进行计算。有的时候应当考虑解决这类问题。例如,model_view_matrix的计算结果矩阵对于单个实例中的所有顶点都是相同的。这里,我们可以通过第二个实例化的mat4属性,输入逐实例的模型视点矩阵数据来避免重复的计算工作,其他时候可能无法避免这种计算,但是还是可以把它移动到几何着色器中完成,这样每次计算都是逐图元,而非逐顶点完成的,或者也可以用到几何着色器的多实例方法。我们会在第10章介绍这些技术的内容。
调用一个多实例的绘制命令,与多次调用它的非实例化的版本然后再执行其他的OpenGL命令,几乎是等价的操作。因此,如果将循环当中已有的一系列OpenGL函数直接转换成一系列的实例化绘制命令,那么得到的结果不会是一致的。
另一个使用多实例顶点属性的例子就是将一系列纹理打包到一个2D纹理数组中,然后将数组的序号通过实例化的顶点属性传递给每个实例。顶点着色器可以将实例对应的序号传递到片元着色器中,然后使用不同的纹理来渲染不同的几何体实例。
我们也可以在系统内部设置一个偏移值,以改变顶点缓存中得到实例化的顶点属性时的索引位置。与glDrawElementsBaseVertex()中提供的baseVertex参数类似,在多实例绘制函数当中,实例的索引偏移值可以通过一个额外的baseInstance参数来设置。带有这个baseInstance参数的函数包括glDrawArraysInstancedBaseInstance()、glDrawElementsIn-stancedBaseInstance()和glDrawElementsInstancedBaseVertexBaseInstance()。它们的原型如下:
void glDrawArraysInstancedBaseInstance(GLenum mode, GLint f?irst, GLsizei count, GLsizei instanceCount, GLuint baseInstance);
对于通过mode、f?irst和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取出的索引位置。
在着色器中使用实例计数器
除了多实例的顶点属性之外,当前实例的索引值可以在顶点着色器中通过内置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示例
为了使用例3.13中的着色器,我们还需要创建和初始化TBO对象,以存储color_tbo和model_matrix_tbo的采样信息,只是不需要再初始化多实例的顶点属性了。不过,除了这些代码设置之间存在差异之外,程序的本质是没有发生变化的。
例3.14 多实例顶点属性的设置示例
注意,例3.14中的代码实际上比例3.10更为短小和简单。这是因为不再使用内置的OpenGL功能来获取逐实例的数据,而是直接使用着色器写出。这一点从例3.13比例3.11增加的复杂性就可以看出。而这样的变化也带来了更多的强大功能和灵活性。举例来说,如果实例的数量较少,那么使用uniform数组可能比使用TBO来存储数据更为合适,但是后者对性能的改善更为理想。除此之外,使用gl_InstanceID来驱动的方法与原始的例子相比并没有更多的改动。实际上,例3.12中的渲染代码是被完整迁移过来的,它所产生的渲染结果与原来的程序完全相同。我们可以参看下面的截图(见图3-9)。
多实例方法的回顾
如果要在程序中使用多实例的方法,那么我们应当:
为准备实例化的内容创建顶点着色器输入。
使用glVertexAttribDivisor()设置顶点属性的分隔频率。
在顶点着色器中使用内置的gl_InstanceID变量。
使用渲染函数的多实例版本,例如glDrawArraysInstanced()、glDrawElementsInstanced()和glDrawElementsInstancedBaseVertex()。