本节书摘来自华章出版社《OpenGL编程指南》一书中的第3章,第3.4节,作者 Bill Licea-Kane ,更多章节内容可以访问云栖社区“华章计算机”公众号查看
3.4 OpenGL的绘制命令
大部分OpenGL绘制命令都是以Draw这个单词开始的。绘制命令大致可以分为两个部分:索引形式和非索引形式的绘制。索引形式的绘制需要用到绑定GL_ELEMENT_ARRAY_BUFFER的缓存对象中存储的索引数组,它可以用来间接地对已经启用的顶点数组进行索引。另一方面,非索引的绘制不需要使用GL_ELEMENT_ARRAY_BUFFER,只需要简单地按顺序读取顶点数据即可。OpenGL当中,最基本的非索引形式的绘制命令就是glDrawArrays()。
void glDrawArrays(GLenum mode, GLint first, GLsizei count);
使用数组元素建立连续的几何图元序列,每个启用的数组中起始位置为first,结束位置为first + 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所定义的范围当中。
我们还可以通过这些功能的组合来实现一些更为高级的命令,例如,glDrawRangeElementsBaseVertex()就相当于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()的参数进行解析。其中first和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也表示多实例的个数,firstIndex可以与type参数所定义的索引数据大小相结合,以计算传递到glDrawEle-mentsInstancedBaseVertex()的索引数据结果。此外,baseInstance用来表示结果绘制命令中,所有多实例顶点属性的实例偏移值。
现在,我们将讨论一些不是以Draw开头的绘制命令。它们属于绘制命令的多变量形式,包括glMultiDrawArrays()、glMultiDrawElements()和glMultiDrawElementsBaseVertex()。每个函数都记录了一个first参数的数组,以及一个count参数的数组,其工作方式相当于对每个数组的元素,都会执行一次原始的单一变量函数。举例来说,glMultiDrawArrays()函数的原型如下:
void glMultiDrawArrays(GLenum mode, const GLint first, const GLint count, GLsizei primcount);
在一个OpenGL函数调用过程中绘制多组几何图元集。first和count都是数组的形式,数组的每个元素都相当于一次glDrawArrays()调用,元素的总数由primcount决定。
调用glMultiDrawArrays()等价于下面的OpenGL代码段:
类似地,glDrawElements()的多变量版本就是glMultiDrawElements(),它的原型如下:
void glMultiDrawElements(GLenum mode, const GLint count, GLenum type, const GLvoid const* indices, GLsizei primcount);
在一个OpenGL函数调用过程中绘制多组几何图元集。first和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函数调用过程中绘制多组几何图元集。first、indices和baseVertex都是数组的形式,数组的每个元素都相当于一次glDrawElementsBaseVertex()调用,元素的总数由primcount决定。
与之前所述的其他OpenGL多变量绘制命令类似,glMultiDrawElementsBaseVertex()也可以等价于下面的OpenGL代码段:
最后,如果有大量的绘制内容需要处理,并且相关参数已经保存到一个缓存对象中,可以直接使用glDrawArraysIndirect()或者glDrawElementsIndirect()处理的话,那么也可以使用这两个函数的多变量版本,即glMultiDrawArraysIndirect()和glMultiDrawElementsIndirect()。
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)。这个数不太可能是一个真实的索引值。如果将它作为重启动索引标准值的话,那么我们也就不需要为程序中的每一个模型都单独设置一个索引了。