2.6 原语和顶点
在GL中,几何图形通过指定一系列的属性来画出,这些属性需要设置顶点数组(2.8部分)。有七种几何图形通过这种方式画出:点,连接的线段,循环的线段,分开的线段,三角形带,三角形扇,分开的三角形。
每个顶点有多个顶点属性,每个属性被一个,两个,三个或者四个标量值指定。顶点属性可以通过顶点着色器(2.10章节)访问,在顶点着色器中计算出的值,为下个阶段的处理提供数据源。
被传递到GL的属性的作用,换句话说,这些顶点的属性是如何通过顶点着色器产生从顶点到二维屏幕的映射的,后面会讨论。
在顶点着色器执行之前,顶点的状态是由它被设置的通用顶点属性决定的。在顶点着色器执行之后,顶点的状态由写在顶点着色器上的输出变量和屏幕空间坐标系决定。
图2.2展示了从一系列顶点构建原语(点,线段,三角形)的操作序列。在原语形成后,将会被裁剪到一个可视区域。改变顶点坐标系和输出变量可能会改变图元。在线段和三角形图元的例子中,裁剪的时候可能会插入新的顶点到图元中。这种顶点被定义为可光栅化的顶点,有相应的输出变量和它们对应。
2.6.1 图元类型
顶点会通过GL函数 DrawArrays或者DrawElements(2.8章节)被传递到GL中。顶点的数目没有限制,但顶点数组的大小是有限制的。
这些函数的参数模式决定了画在坐标系中的图元类型。相对应的参数模式有:
点:POINT模式指定了许多独立的点。每个顶点由一个单独的点定义。
线带:LINE_STRIP模式指定了一个或者多个连接的线段。至少要提供两个顶点。举个例子,第一个顶点是第一段线段的起始点,第二个顶点是第一条线段的终点同时也是第二条线段的起始点。概括来说,第i个顶点,是第i条线段的起始点,也是第i-1条线段的终点。最后一个顶点zhi指最后一条的终点。如果只有一个顶点,就不会有该图元产生。
由之前传递过来的顶点(这样一个线段就能从它和当前顶点构成)产生的处理过的顶点,和一个说明当前顶点是否是第一个顶点的布尔值标志,构成了这种图元的状态。
线回路:LINE_LOOP模式指线回路。除了这种模式的最后一条线段是通过最终顶点和起始顶点形成的之外,这种模式和线带模式一样。
第一个顶点和线带模式要求的所有共同组成了这种模式的状态要求。
独立线:LINES模式指独立的线段,每个线段由一对顶点构成。最开始的两个顶点定义了第一条线段,依次的顶点对定义更多的线段。如果顶点数目是奇数,最后的i一个顶点会被忽略。状态要求和线带模式一样,只是用法不同:正在处理的顶点持有当前线段的终点,和表示当前顶点是奇数还是偶数的标志位(一条线段的起点或者终点)。
条带状三角形簇: TRIANGLE_STRIP 模式指一系列顶点在这种模式下,形成互相连接的三角形,共用三角形的边。具体来说,最开始的三个顶点组成第一个三角形(顶点的顺序也很重要)。每个之后的顶点都会和前一个三角形的后两个顶点构成一个新的三角新。如果不够三个顶点,将不会有图元产生。可以看图2.3.
支持这种条带装的三角形簇要求的状态,由一个标识第一个三角形是否画完的标志位,存储着的两个正在处理的顶点,(被称作顶点A和顶点B),和一个指示哪个存储的顶点将要被新的顶点替换掉的位指针,共同组成。指针被初始化为指向顶点A。每个后续的顶点都会触发这个指针。因此,第一个顶点被存储为顶点A,第二个被存储为顶点B,第三个又被存储为顶点A这样的方式继续下去。任何在第二个顶点后被送进去的顶点都会和顶点A,顶点B以及自身,(按照A-B-当前顶点的顺序)构成新的三角形。
风扇状三角形簇:TRANGLE_FAN模式只有一点和条带状三角形簇不一样:每个在第一个三角形后的顶点将会代替两个存储顶点中的顶点B。
独立的三角形: TRANGLE模式指独立的互相分开的三角形。具体讲,第3i+1st, 3i+2nd, 3i+3rd个顶点组成一个三角形,参与画图的顶点有3n+k个;其中i = 0,1,...,n-1,k可以是0,1或者2;如果k不是0,最后的k个顶点就会被忽略。对于每个三角形,3i是顶点A,3i+1是顶点B。其他的方面和条带状三角形相同。
在条带状三角形,风扇状三角形和独立的三角形中,顶点产生的顺序将会对多边形的光栅化(3.5.1部分)和片段着色器(3.8.2部分)有非常重大的作用。
2.7顶点当前状态
顶点着色器(见2.10节)需要访问一个4通道的顶点属性数组。数组的第一个槽序列被赋予0,数组的大小由独立的常量MAX_VERTEX_ATTRIBS决定。
当定义数据的顶点数组没有被打开(见2.8节)的时候,当前通用的属性值将会成为所有顶点的通用属性。在把给定的值加载到当前通用属性对应的槽序列中,也是就是被称为x,y,z和w的四个通道时,当前值也许会因为下面两个函数中的任意一个的作用,随时发生变化。
void VertexAttrib {1234}{f} (uint index, T values);
void VertexAttrib {1234}{f}v (uint index, T values);
VertexAttrib1系列的函数只有x坐标用来设置,y,z被设置为0,w被设置为1。相同的VertexAttrib2系列函数把x和y作为变量,z被设置为0,w被设置为1;VertexAttrib3系列函数,x,y,x为变量,可被设置,w为设置为1,VertexAttrib4四个坐标都可以设置。当产生的序列号比MAX_VERTEX_ATTRIBS大或者相等时,将会产生INVALID_VALUE的错误。
VertexAttrib函数也可以用来加载在顶点着色器中的2X2,3X3,4X4矩阵类型的属性。矩阵的每一列占用来自MAX_VERTEX_ATTRIBS可用插槽的一个通用的4通道属性插槽。矩阵以列为主的顺序加载到这些插槽中。矩阵列需要按照插槽号增序来加载。
支持上面所述顶点的状态,包含了MAX_VERTEX_ATTRIBS个四通道浮点数向量来存储通用的顶点属性。所有通用顶点属性的初始值为(0,0,0,1);
2.8 顶点数组
存在数组中的顶点数据被存储在客户端的地址空间或者服务端的地址空间(见2.9节)。这些数组中的数据块可以通过执行一个简单的GL函数来指定多个几何图元。客户端可以最多指定MAX_VERTEX_ATTRIBS个用来描述一个或多个通用顶点属性的数组。这个函数是
函数 | 大小 | 归一化 | 类型 |
---|---|---|---|
VertexAttribPointer | 1,2,3,4 | flag | byte,ubyte, short, ushort, fixeld, float |
表2.4:顶点数组的大小(每个顶点的值)和数据类型。“归一化”表示接受整型数据或者接受被规范到[0,1]范围内(无符号类型)和[-1,1]范围内(有符号数类型)的数据。对于通用顶点属性,只有VertexAttribPointer归一化标志位状态开启,整型数据才会被归一化。
void VertexAttribPointer(uint index, int size, enum type, boolean normalized, sizei stride, const void *pointer);
这个函数描述了顶点数组的位置和结构。type指定了存储在数组中数值的类型。size指定了存储在数组中的每个顶点所包含的值的个数。表2.4说明了size和type被允许的值。BYTE,UNSIHNED_BYTE,SHORT,UNSIGNED_SHORT,FIXED,和FLOAT分别指类型byte,ubyte,short,ushort,fixed和float。如果指定的size值不是表中所列出的范围,将会产生INVALID_VALUE错误。
index参数指被描述的通用顶点属性。如果index的值超过MAX_VERTEX_ATTRIBS,将会产生INVALID_VALUE的错误。normalized参数指整数类型的值被转换成浮点类型时,是否需要归一化。如果normalized为TRUE,整数值将会像在章节2.1.2中描述的那样做转换,否则将会直接将整数值转换为浮点数。
一个符合通用顶点属性的数组中,一、二、三或者四个值构成一个数组元素。每个数组元中的值在内存中顺序排列。如果参数stride为0,数组元素会任然顺序排列。如果stride的值为负数,将会产生INVALID_VALUE错误。另外,指向数组的第i个和第(i + 1)个元素的指针不同于基本机器单位(通常是无符号字节),指向第(i + 1)个元素的指针更大。对每个函数来说,指针指向数组在内存空间中的第一个元素的第一个值的地址上。
开启或者关闭一个通用顶点属性数组通过调用下面的函数:
void EnableVertexAttribArray(uint index);
void DisableVertexAttribArray(uint index);
index唯一标识要开启或者关闭的通用顶点数组。当index大于或者等于MAX_VERTEX_ATTRIBS时,INVALID_VALUE错误产生。
传输数组元素
当数组元素i通过DrawArrays或DrawElements函数被传递给GL时,每个通用属性被扩张为四个通道。如果size是1,那么数组将会指定属性的x通道;y,z,w通道被隐式设置为0,0,1。如果size是2,数组指定属性的x和y通道,z,w通道被设置为0,1.如果size为3,那么指定x,y和z,w被隐式设置为1.如果size是4,所有通道都会被指定。
函数
void DrawArrays(enum mode, int first, sizei count);
通过把每个开启的顶点数组的第first个元素到第first + count - 1个元素传递给GL,构造了一个几何图元序列。mode指定是要构造的是哪种类型的图元,如2.6.1章节中所示的。如果顶点着色器需要的一个通用顶点属性所对应的数组没有被开启,那么对应的元素将会从当前的通用顶点属性状态中获取(章节2.7)。
指定first < 0会导致未定义的行为发生。这种情况下将会产生INVALID_VALUE错误。
函数
void DrawElements(enum mode, sizei count, enum type, void *indices);
通过把存储在indices中的count个元素的序列号传递给GL来构造几何图元序列。通过DrawElements传递的第i个元素将会从每个被开启的顶点数组中的indices[i]元素获取。type一定是UNSIGNED_BYTE或者UNSIGNED_SHORT类型,这意味着在indices中存储的序列号必须是ubyte或者ushort类型。同样的,mode指构成图元的类型,这和DrawArrays的mode参数相同。如果顶点着色器要求的通用顶点属性对应的顶点数组没有被开启,那么对应的元素将会从当前通用顶点属性状态中获取(章节2.7)。
如果支持的通用顶点属性数目(MAX_VERTEX_ATTRIBS的值)是n,那么客户端需要实现的顶点数组由以下组成:n个布尔值,n个内存指针,n个整型的对其的值,n个代表数组类型的符号常量,n个代表每个元素值的整型数,和n个指定是否归一化的布尔值。初始化状态时,所有布尔值都是flase,内存指针都是NULL,对其字节都是0,数组类型都是FLOAT,代表每个元素通道数的整型值都是4。
2.9 缓冲对象
在2.8节中描述的顶点数组数据被存储在客户端的内存中。一些被客户端高频次使用的数据,例如顶点数组数据,值得被存储在具有高性能的服务端内存中。GL 缓冲对象为此提供了这样的机制,允许客户端从GL服务端的缓冲对象中执行分配,初始化和渲染操作。
缓冲对象的命名空间是无符号整数,0被GL保留。缓冲对象通过给ARRAY_BUFFER绑定一个没有使用的名字来创建。绑定通过调用函数
void BindBuffer(enum target, uint buffer);
来实现,target被设置为ARRAY_BUFFER,buffer被设置一个没有使用过的名字。得到的缓冲对象是一个新的状态向量,初始状态是一个0字节的内存空间,状态列表如表2.5。
BindBuffer也可以去绑定一个已经存在的缓冲对象。如果绑定成功,新绑定的缓冲对象状态不会发生变化,而之前所绑定的target将会与之解绑。
当缓冲对象绑定后,对于target的GL操作将会影响与之绑定的缓冲对象,同时向绑定在target上的缓冲对象查询状态并返回。
初始化状态时,被保留的名字0和ARRAY_BUFFER绑定。没有缓冲对象会响应名字0的操作,所以当缓冲对象的名字为0时,客户端试图修改和查询缓冲对象的状态,都将会产生GL错误。
缓冲对象可以通过调用
void DeleteBuffers(sizei n, const uint *buffers);
来删除。buffers包括要被删除的n个缓冲对象的名字。缓冲对象被删除后,没有内容,同时名字也回到为使用状态。buffers的未使用的名字被忽略,就像数值0。
函数
void GenBuffers(sizei n, uint *buffers);
在buffers中生成n个之前没有使用过的缓冲对象的名字。并把这些名字标注未被已使用,但这仅仅是对GenBuffers而言的,只有在它们绑定之后才能够获取缓冲对象的状态。
缓冲对象被绑定后,任何在这个对象上的GL操作都会影响绑定在这个对象上的其他绑定。如果删除一个被绑定的缓冲对象,在当前上下文(指调用DeleteBuffer的线程)中,该缓冲对象的其他绑定都会被置为0.在其他上下文或者其他线程中绑定在该缓冲对象上的绑定操作,不会受到影响,但是在其他线程使用删除缓冲对象操作,将会产生未定义结果,比如超出可能的GL错误码范围,甚至渲染中断。但无论如何,在其他上下文或者线程执行删除缓冲对象操作,不会引起程序终止。
缓冲对象数据存储的创建和初始化通过调用函数
void BufferData(enum target, sizeiptr size, const void *data, enum usage);
来实现。target设置为ARRAY_BUFFER,size设置为存储数据在基本机器单元下的大小,data指针指向在客户端内存中的缓冲对象数据源。如果data是NULL,存储在缓冲对象中数据的内容将会是未定义的。usage是三个枚举值中的一个,表示应用对数据存储的使用模式。这些值是:
STATIC_DRAW : 数据存储内容只会被应用指定一次,而GL画图函数会调用很多次。
DYNAMIC_DRAW : 数据存储内容会被应用不停的修改,GL画图函数会调用很多次。
STREAM_DRAW : 数据存储内容只会被应用指定一次,GL画图函数也只是很少几次调用。
usage仅仅提供了一种性能建议。制定的使用方式并不会真正约束在数据存储是的实际使用模式。
BufferData删除所有已经存在的存储数据,然后设置表2.6中的缓冲对象的状态变量的值。
客户端必须校准客户端平台要求的数据元素以保持一致,最基本的要求是,构成N字节基本机器单元上的缓冲对象数据的偏移量必须是N的倍数。
如果GL不能按照要求的字节大小创建存储数据,将会产生OUT_OF_MEMORY的错误。
修改一些存储在缓冲对象中的数据是,客户端可能会用到函数
void BufferSubData(enum target, intptr offset, sizeiptr size, const void *data);
target设置为ARRAY_BUFFER,offset和size表示以机器基本单元为单位的要被代替的缓冲数据的范围。data指客户端内存区域中size长度的基本机器单元的内存,包含着要被新替代到指定缓冲范围的数据。如果offset或者size比0小,或者offset + size比BUFFER_SIZE大,将会产生INVALID_VALUE的错误。
2.9.1 缓冲对象中的顶点数组
顶点数组数据块会以同样的格式和客户端顶点数组支持的布局方式,被存储到缓冲对象中。
包括缓冲对象的绑定点在内,客户端状态和每个顶点数组类型都有联系。指定位置和顶点数组的组织的函数,把绑定在ARRAY_BUFFER上的缓冲对象的名字拷贝到被指定类型的相关联的顶点数组的绑定点上。例如,VertexAttribPointer函数把ARRAY_BUFFER_BINDING的值(和ARRAY_BUFFER相关联的可查询的缓冲绑定的名字)拷贝到指定序号的客户端状态变量VERTEX_ATTRIB_ARRAY_BUFFER_BINDING上。
渲染函数DrawArrays和DrawElements像之前那样工作,不一样的是,如果缓冲数组绑定的不是0,开启的通用属性数组的数据将会从这些缓冲对象中来。当数组来源于一个缓冲对象是,数组的指针会被用来以基本机器单元为单位,和存储在缓冲对象中的数据来计算偏移量。当所有指针都被当作是指向基本机器单元的指针时,偏移量为指针的值减去null指针。
在渲染操作执行期间,把任何客户端内存和各种缓冲对象的结合,作为通用顶点属性鼠族的数据来源都是可以的。
2.9.2 缓冲对象的数组序列
数组序列块会以客户端支持的序列数组相同的格式存储到缓冲对象中。绑定在ELEMENT_ARRAY_BUFFER上的初始值为0,意味着DrawElements把数组的序列当作数据源,传递给indices参数。
通过调用BindBuffer,缓冲对象被绑定到ELEMENT_ARRAY_BUFFER上,target被设置为ELEMTNE_ARRAY_BUFFER,buffer被设置为缓冲对象的名字。如果没有相对应的缓冲对象存在,初始化将会同2.9中所描述的一样。
函数BufferData和BufferSubData的target设置为ELEMENT_ARRAY_BUFFER。这种情形下,函数操作和2.9中描述的也相同,不同的仅仅时,当前绑定的target是ELEMENT_ARRAY_BUFFER。
当非零的缓冲对象绑定到ELEMENT_ARRAY_BUFFER上时,DrawElement和2.9.1中描述的那样,把元素的indices参数作为缓冲对象的偏移量,以这种方式从缓冲对象获取数组序列。
通过给ARRAY_BUFFER和ELEMENT_ARRAY_BUFFER绑定未使用的名字来创建的缓冲对象,大致是相同的。只是GL会根据初始化绑定来对存储做出一些不同的选择。某些情形下,分开存储序列和数组数据到不同的缓冲对象,以及根据绑定点创建不同的缓冲对象,会使得性能得以优化。