浏览量:1,654

顶点数组

绘制一个简单的几何图元,OpenGL需要很多函数的调用,例如绘制一个20面的多边形,可能就会要求22个函数的调用,因为需要调用glBegin()和glEnd()两个函数;此外,绘制一个相邻多边形时,可能还存在很多顶点的冗余,例如绘制一个正方体,需要绘制6个面,每个顶点就会被处理3次。但是如果有了顶点数组(vertex array),就能把顶点放在一个数组中,然后调用一次函数就可以完成绘制,即使每个顶点存在一个法向量,也可以把法向量放在另一个数组中,并用另外一个函数进行调用。使用数组可以避免上述提到的函数的多次调用和顶点冗余的问题,从而提高性能。使用顶点数组渲染几何图元主要有以下三个步骤:

1.       激活相应类型的数组

共有8种数组:1)顶点坐标,2)平面法向量;3)RGBA颜色值;4)二次色(secondary color);5)颜色索引;6)雾坐标;7)纹理坐标;8)多边形边标志。

通过函数glEnableClientState()(相对应的是glDisableClientState())来激活相应的数组,理论上可以激活8个数组,但是实际上最多激活6个数组,例如GL_COLOR_ARRAY,GL_INDEX_ARRAY不可以同时激活。这里采用gl*ClientState()而不使用glEnble和glDisable()的原因是glEnable()和glDisable()可以存储在显示列表中,但是顶点的数组却不能,因为这些数据保存在客户端。

2.       把数据放在数组中

数组能通过地址来访问,在OpenGL的客户端-服务器模型中,这个数据被存储在客户端地址空间中。OpenGL提供了8个函数进行存储顶点数组中的数据,如下所示:

(1)              glVertexPointer(): 顶点坐标数组

(2)              glNormalPointer():  法向量数组

(3)              glColorPointer():  颜色RGB数组

(4)              glIndexPointer():  索引颜色数组

(5)              glTexCoordPointer():  纹理坐标数组

(6)              glEdgeFlagPointer():  边标志数组

(7)              glSecondaryColorPointer():二级颜色数组。

glVertexPointer(GLint size, GLenum type, GLsizei stride, const GLvoid* pointer)

  1. size:顶点坐标的维度, 2 表示2D顶点坐标, 3 表示3D顶点坐标
  2. type: GL_FLOAT, GL_SHORT, GL_INT or GL_DOUBLE
  3. stride: 到下一个顶点偏移的字节数,在交插数组(interleaved array)中会用到
  4. pointer:指向数组的指针

这里给出了glVertexPointer()函数的具体的几个参数的解释,其它函数可以参见具体的文档。下面的代码片段演示了激活数组和装载数据到顶点数组的方法:

static GLint vertices[] = { 25,25,
                            100,325,
                            175,25,
                            175,325,
                            250,25,
                            325,325};
static GLfloat colors[] = {1.0f , 0.2f , 0.2f,
                           0.2f , 0.2f , 1.0f;
                           0.6f , 1.0f , 0.2f,
                           0.75f,0.75f,0.75f,
                           0.3f , 0.3f , 0.3f,
                           0.5f  ,0.5f , 0.5f};
glEnableClientState(GL_COLOR_ARRAY);
glEnableClientState(GL_VERTEX_ARRAY);
glColorPointer(3,GL_FLOAT,0,colors);
glVertexPointer(2,GL_INT,0,vertices);

3.       使用数组画几何图元

OpenGL通过解引用的方式(dereference)从激活的数组中获取数据,在客户端-服务器模型中,数据从客户端空间发送服务器地址空间中,交给图形渲染管线进行处理。

3.1       glArrayElement(),绘制单个数组元素

使用glArrayElement()函数进行读取数据,代码片段如下所示:

static GLint vertices[] = { 25,25,
                            100,325,
                            175,25,
                            175,325,
                            250,25,
                            325,325};
static GLfloat colors[] = {1.0f , 0.2f , 0.2f,
			   0.2f , 0.2f , 1.0f;
			   0.6f , 1.0f , 0.2f,
			   0.75f, 0.75f, 0.75f,
			   0.3f , 0.3f , 0.3f,
			   0.5f , 0.5f , 0.5f};
glEnableClientState(GL_COLOR_ARRAY);
glEnableClientState(GL_VERTEX_ARRAY);
glColorPointer(3,GL_FLOAT,0,colors);
glVertexPointer(2,GL_INT,0,vertices);
glBegin(GL_TRIANGLES);
  glArrayElement(2);
  glArrayElement(3);
  glArrayElement(5);
glEnd();

这段程序执行结果,与下面几行代码有相同的效果:

glBegin(GL_TRIANGLES);
  glColor3fv(colors + (2*3));
  glVertex2iv(vertices +(2*2));

  glColor3fv(colors + (3*3));
  glVertex2iv(vertices +(3*2));

  glColor3fv(colors + (5*3));
  glVertex2iv(vertices +(5*2));
glEnd();

这里特别注意的一点是:在glBegin(),glEnd()之间使用到的数据发生改变的话,则并不能保证服务器端得到的是改变的数据还是原始的数据,尽量别修改数组中的数据。原因就在于OpenGL采用的是客户端-服务器模型,在执行完glBegin()和glEnd()之间的程序,数据不一定从客户端发送到服务器端。

3.2       glDrawElements(),绘制一个数组列表

glDrawElements()可以结合数组与数组索引,要画出一系列的图元,这个函数减少了函数调用次数和顶点数据由客户端向服务器传输的次数,OpenGL会缓存了最近处理过的顶点并重用它们,而不会将它们发送到顶点处理管线进行多次处理。glDrawElements()使用4个参数,第1个是图元的类型,第2个是索引数组的索引数,第3个是索引数组的数据类型,最后一个是索引数组的起始地址。下面的代码片段演示了相应的功能:

GLfloat vertices[] = {...};          // 8 of vertex coords
GLubyte indices[] = {0,1,2, 2,3,0,   // 36 of indices
                     0,3,4, 4,5,0,
                     0,5,6, 6,1,0,
                     1,6,7, 7,2,1,
                     7,4,3, 3,2,7,
                     4,7,6, 6,5,4};
...
// activate and specify pointer to vertex array
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3, GL_FLOAT, 0, vertices);
// draw a cube
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_BYTE, indices);
// deactivate vertex arrays after drawing
glDisableClientState(GL_VERTEX_ARRAY);

细节:这里的索引数组采用了GLubyte数据类型,它足以索引数组中的所有数据,而不GLuint或者GLshort,减小了索引数组的大小,提高了性能。

需要考虑到的一种情况是与共享顶点法向量有关,如果相邻的多边形共享一个顶点,但是在该顶点的法向量却是不同的,即每个面都至少有一个法向量。例如图1所示,顶点v0被上平面,右平面和前平面3个平面共享,三个平面的法向量如图1所示,共享顶点有不同的法向量,则顶点不能只定义一次。每个顶点必需在顶点数组中定义多次,与相应的向量数组匹配。在一个正方体中,就要求有24个不同的顶点,即6个面*4个顶点。

cube

图1 正方体

glMultiDrawElements (GLenum mode,GLsizei* count,GLenum type,const GLvoid**indices,GLsizei primcount);

OpenGL还提供了个glMultiDrawElements()命令,该方法相当于将多个glDrawElements()命令归并成一个命令调用,如下代码片段所示:

for( i = 0 ; i < primcount ; i++ )
{
	if( count[i]>0)
		glDrawElements(mode,count[i],type,indices[i]);
}

下面演示该命令的细节:

static GLubyte oneIndices[]={0,1,2,3,4,5,6};
static GLubyte twoIndices[]={7,1,8,9,10,11};
static GLsizei count[]={7,6};
static GLvoid* indices[2] = {oneIndices,twoIndices};
glMultiDrawElements(GL_LINE_STRIP,count,GL_UNSIGNED_BYTE,indices,2);

glDrawRangeElements()命令,对索引的范围进行了限制。如果索引值超过了一定范围会引起错误,OpenGL规范并没有要求OpenGL发现或者报告错误,所以非法的索引可能也可能不会产生错误的状态,这完全依赖于具体的硬件对OpenGL的实现。通过函数glGetInterv(),传入GL_MAX_ELEMENTS_VERTICES和GL_MAX_ELEMENTS_INDICES参数,可以获取的最大顶点元素数目和最大的索引数目。下面的代码演示了该命令的用法:

GLfloat vertices[] = {...};          // 8 of vertex coords
GLubyte indices[] = {0,1,2, 2,3,0,   // first half (18 indices)
                     0,3,4, 4,5,0,
                     0,5,6, 6,1,0,
                     1,6,7, 7,2,1,   // second half (18 indices)
                     7,4,3, 3,2,7,
                     4,7,6, 6,5,4};
...
// activate and specify pointer to vertex array
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3, GL_FLOAT, 0, vertices);
// draw first half, range is 6 - 0 + 1 = 7 vertices used
glDrawRangeElements(GL_TRIANGLES, 0, 6, 18, GL_UNSIGNED_BYTE, indices);
// draw second half, range is 7 - 1 + 1 = 7 vertices used
glDrawRangeElements(GL_TRIANGLES, 1, 7, 18, GL_UNSIGNED_BYTE, indices+18);
// deactivate vertex arrays after drawing
glDisableClientState(GL_VERTEX_ARRAY);

glDrawArrays()命令从激活的数组中直接读取数据,只能顺序读到顶点数组,所以多个面共享顶点的数据还需要重复存储多次。该命令有3个参数,第1个参数是图元的类型,第2个参数指定数组的起始位置的偏移量,第3个参数表示传给图形渲染管线的顶点数。例如,下面的代码片段所示,画一组三角形,第1个参数为GL_TRIANGLES,第2个参数为0,则意味着从数组的起始位置开始读取数据,最后个参数是36,则表示共画12个角形。

GLfloat vertices[] = {...}; // 36 of vertex coords
...
// activate and specify pointer to vertex array
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3, GL_FLOAT, 0, vertices);
// draw a cube
glDrawArrays(GL_TRIANGLES, 0, 36);
// deactivate vertex arrays after drawing
glDisableClientState(GL_VERTEX_ARRAY);

OpenGL还提供了个glMultiDrawArrays()命令,该命令与glMultiDrawElements()的功能类似。

4.   交插数组

如第2小节所示,gl*pointer()命令中的stride参数,指定了相邻的两个数组元素之间的字节偏移量。例如把顶点的RGB和(x,y,z)坐标信息存在一个数组中,如下所示:

static GLfloat intertwined[]=
{ 1.0,0.2,1.0,	100.0,100.0f,0.0,
1.0, 0.2,0.2,	0.0, 200.0, 0.0,
1.0, 1.0, 0.2,	100.0, 200.0, 0.0,
0.2, 1.0, 0.2,	200.0, 300.0, 0.0,
0.2, 1.0, 0.2,	300.0, 200.0, 0.0,
0.2, 0,2 , 1.0,	200.0, 100.0, 0.0,};

相邻两个元素之间偏移的字节数是6,访问数组中的颜色值,可以通过以下的函数调用来处理:

glColorPointer(3,GL_FLOAT,6*sizeof(GLfloat),&intertwined[0]);

访问数组中的坐标值,可以通过以下的函数来处理:

glVertexPointer(3,GL_FLOAT,6*sizeof(GLfloat),&intertwined[3]);

在处理交插数组时,还有一个特别强大的函数,就是glInterLeavedArrays(),它的具体用法这里不做详细介绍。

5.  总结

本篇文章主要介绍顶点数组的用途和使用方法,顶点数组使用到的主要函数如下所示:

glEnableClientState()
glDisableClientState()

glVertexPointer()
glNormalPointer()
glColorPointer()
glIndexPointer()
glTexCoordPointer()
glEdgeFlagPointer()
glSecondaryColorPointer()

glArrayElement()
glDrawElements()
glMultiDrawElements
glDrawArrays()
glMultiDrawArrays()
glInterLeavedArrays()

6.  参考

【1】      OpenGL programming guide, the official guide to learning OpenGL, Version 2.1, sixth edition

【2】      http://www.songho.ca/opengl/gl_vertexarray.html

【3】      http://en.wikipedia.org/wiki/Dereference_operator

spacer

Leave a reply