OpenGL超级宝典学习笔记——顶点数组
顶点数组
当我们有来自模型的大量数据的时候,使用显示列表来对这些数据进行预编译,需要遍历这些顶点数据(一次一个顶点数据)把数据传给OpenGL。依赖于顶点的数量,这会带来潜在的性能损耗。而且这些数据不一定是静态的,有可能在我们每次渲染的时候,我们需要对这些数据进行更改。这个时候就不适合使用显示列表。
在OpenGl中,使用顶点数组能够很好的解决这两个问题。使用顶点数组,我们可以随时进行预编译或修改几何图形,然后一次性传输这些数据。基本的顶点数组几乎和显示列表一样快,而且不要求数据是静态的。
在OpenGL中使用顶点数组有4个基本的步骤:
- 把几何图形的数据加载到一个或多个数组中(可以从磁盘中读取)
- 告诉OpenGL数据在哪里(参数传指向数据的指针)
- 你需要使用哪些数组,因为数组可能分为顶点数组,颜色数组,纹理坐标数组等,或者一个数组中包含这些数据,按照某种顺序排列。
- 执行OpenGL命令使用你的顶点数据进行渲染。
为了演示这些步骤,修改之前的第九章的pointsprite的例子。我们使用顶点数组的方式替代掉glBegin/glEnd的方式。修改的代码如下:
//画小星星 glPointSize(7.0); glVertexPointer(2, GL_FLOAT, 0, &smallStars[0]); glDrawArrays(GL_POINTS, 0, SMALL_NUM); ///画中等大小的星星 glPointSize(12.0); glVertexPointer(2, GL_FLOAT, 0, &mediumStars[0]); glDrawArrays(GL_POINTS, 0, MEDIUM_NUM); ////大星星 glPointSize(20.0); glVertexPointer(2, GL_FLOAT, 0, &largeStars[0]); glDrawArrays(GL_POINTS, 0, LARGE_NUM); glDisableClientState(GL_VERTEX_ARRAY);
加载几何图形
首先我们需要把几何图形的数据组装到数组中。在上面的例子中,在初始化的时候就组装好数据,代码如下:
//星星的坐标 M3DVector2f smallStars[SMALL_NUM]; M3DVector2f mediumStars[MEDIUM_NUM]; M3DVector2f largeStars[LARGE_NUM];
void SetupRC() { ... //随机获取星星的位置 for (int i = 0; i < SMALL_NUM; ++i) { smallStars[i][0] = (GLfloat)(rand() % SCREEN_X); smallStars[i][1] = (GLfloat)(rand() % SCREEN_Y); } for (int i = 0; i < MEDIUM_NUM; ++i) { mediumStars[i][0] = (GLfloat)(rand() % SCREEN_X); mediumStars[i][1] = (GLfloat)((rand() % SCREEN_Y) + 50); }
for (int i = 0; i < LARGE_NUM; ++i) { largeStars[i][0] = (GLfloat)(rand() % SCREEN_X); largeStars[i][1] = (GLfloat)(rand() % SCREEN_Y); } ... }
启用数组
像OpenGL大多数的特性一样,要使用顶点数组首先得启用它。
//使用顶点数组
glEnableClientState(GL_VERTEX_ARRAY);
启用和禁用的函数原型如下:
void glEnableClientState(GLenum array);
void glDisableClientState(GLenum array);
函数接受的参数值有:GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_SECONDARY_COLOR_ARRAY, GL_NORMAL_ARRAY, GL_FOG_CORRDINATE_ARRAY, GL_TEXTURE_COORD_ARRAY和GL_EDGE_FLAG_ARRAY。在上面的例子中我们只使用到了顶点数组。当然我们可以同时启用多种类型的数组。
为什么是使用glEnableClientState来启用数组而不是像以前那样用glEnable?因为OpenGL的设计时 client/server模式的。server服务器是图形硬件,client客户端是CPU和内存。对于PC来说,服务器就是显卡,客户端就是CPU和主存。
指定数据
在我们启用了顶点数组之后,我们需要告诉OpenGL数据在哪里(内存中的位置)。在上面的例子中相应的代码:
glVertexPointer(2, GL_FLOAT, 0, &smallStars[0]);
相应的有指定颜色数组,纹理坐标数组等等,列表如下:
//顶点 void glVertexPointer(GLint size, GLenum type, GLsizei stride, const void *pointer); //颜色 void glColorPointer(GLint size, GLenum type, GLsizei stride, const void *pointer); //纹理坐标 void glTexCoordPointer(GLint size, GLenum type, GLsizei stride, const void *pointer); //辅助颜色 void glSecondaryColorPointer(GLint size, GLenum type, GLsizei stride, const void *pointer); //法线 void glNormalPointer(GLenum type, GLsizei stride, const void *pData); //雾坐标 void glFogCoordPointer(GLenum type, GLsizei stride, const void *pointer); //边界 void glEdgeFlagPointer(GLenum type, GLsizei stride, const void *pointer);
上面同类型的参数意义都是一样的。 其中第一个参数size是指一个顶点或颜色等所包含的元素的个数,例如顶点有(x,y), (x,y,z), (x,y,z,w)的形式。像法线,雾坐标, 边界标记这几个函数没有size,因为它们的值一定是3(x,yz)的形式。
参数type指的是数据的类型,并不是所有的数据类型都可以被接受的。什么类型的数组能接受的数据类型和元素个数如下:
stride参数指定了数据之间的间隔。例子中的情况是0,为我们的顶点数据是紧挨着的。如果我们一个数组中即包含了顶点数据和颜色数据(混合数组),那么我们可以通过这个stride来区分。举个例子:
GLfloat data[] = { 10.0f, 5.0f, 0.0f, //顶点数据 1.0f, 0.0f, 0.0f , //颜色数据 5.0f, 10.0f, 0.0f, //顶点数据 0.0f, 1.0f, 0.0f //颜色数据 } glVertexPointer(3, GL_FLOAT, 3, &data[0]); //此时顶点数据的间隔就是3 glColorPointer(3, GL_FLOAT, 3, &data[3]); //此时颜色数据的间隔也是3
对于多重纹理的情况,如果我们是使用glBegin/glEnd的方式,那可以通过glMultiTexCoord来指示为哪一个纹理指定坐标。如果使用顶点数组的方式,那么我们可以在调用glTexCoordPointer之前调用:
glClientActiveTexture(GLenum texture);
其中texture是GL_TEXTURE0, GL_TEXTURE1等。来指定是哪一个纹理的坐标。
用数据绘制
到此为止OpenGl已经知道我们数据的位置了,那么我们可以用下面的代码遍历我们的数据:
glBegin(GL_POINTS); for(i = 0; i < SMALL_STARS; i++) glArrayElement(i); glEnd();
glArrayElement会从数组中提取相应的数据。假设我们已经启用和设置好了顶点,颜色,纹理坐标数组。那么上面的函数调用相当于:
glBegin(GL_POINTS); for (i = 0; i < SMALL_STARS; ++i) { glColor3fv(color[i]); glTexCoord3fv(texcoord[i]); glVertex3fv(vertex[i]); } glEnd();
当然OpenGL提供了一种更简便快速的方法:
void glDrawArrays(GLenum mode, GLint first, GLint count);
其中mode指定了渲染的图元模式GL_POINTS, GL_TRIANGLES等等。第二个参数first指定了顶点数组起始的下标,count指定了要使用的顶点的个数。在上面的例子中,渲染小星星的方式如下:
glDrawArrays(GL_POINTS, 0, SMALL_NUM);
这样OpenGL的实现可以优化这些数据块传输的过程,也节省了许多函数的调用。
顶点索引数组
顶点索引数组存储的是顶点数组的索引(数组的下标)。这样一来改变顶点遍历的顺序,其访问顺序是由一个单独的索引数组指定的。二来顶点数组可以减少存储顶点的数量,一些几何图形有许多的共享顶点,如果使用顶点索引数组的方式,这些共享的顶点就没必要重复存储在顶点数组中(许多情况下可以节省内存空间,节省传输的带宽,也减少对内存的操作),也减少了变换的开销。在理想的情况下,他们可能比显示列表更快。
虽然三角形带(GL_TRIANGLE_STRIPS)能够共享顶点。但没办法避免两个三角形带所共享顶点的变换的开销,因为每一个三角形带都必须是独立的。
下面举一个简单的例子。
简单的立方体
一个立方体有6个面,每个面都是由4个顶点组成的正方形,6x4=24个顶点,其实有许多被正方形共享的顶点,不重复的顶点只有8个。但按照以往的方式使用glBegin(GL_UQADS)/glEnd,我们还是需要传输24个顶点(调用glVertex 24次)。如果我们使用顶点索引数组的方式,就只需要8个顶点就够了,我们用索引指向这些顶点,索引数组中会有重复的值。图示如下:
每个顶点有浮点数值组成的,但每个索引只是一个整数值。在顶点数少的情况下,并不会节省多少空间。比如这个立方体,虽然顶点数组少存了16个顶点,但是索引数组需要额外的24个整数值来存储这些顶点的索引的。
代码示例:
static GLfloat cube[]={-1.0f, -1.0f, -5.0f, //前面的正方形 1.0f, -1.0f,-5.0f, 1.0f, 1.0f, -5.0f, -1.0f, 1.0f, -5.0f, -1.0f, -1.0f, -10.0f,//背面的正方形 1.0f, -1.0f, -10.0f, 1.0f, 1.0f, -10.0f, -1.0f, 1.0f, -10.0f}; static GLubyte index[]={0, 1, 2, 3, //前面 0, 3, 7, 4, //左面 5, 6, 2, 1, //右面 7, 6, 5, 4, //后面 3, 2, 6, 7, //上面 1, 0, 4, 5 //地面 }; void SetupRC() { glClearColor(1.0f, 1.0f, 1.0f, 1.0f); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); } void RenderScene() { glClear(GL_COLOR_BUFFER_BIT); glColor3f(0.0f, 0.0f, 1.0f); glPushMatrix(); glEnableClientState(GL_VERTEX_ARRAY); glVertexPointer(3, GL_FLOAT, 0, cube); glDrawElements(GL_QUADS, 24, GL_UNSIGNED_BYTE, index); glDisableClientState(GL_VERTEX_ARRAY); glPopMatrix(); glutSwapBuffers(); }
可以看到上面调用绘制的��数是
glDrawElements(GL_QUADS, 24, GL_UNSIGNED_BYTE, indexes);
第一个参数是图元的模式,第二个是索引数组包含的值的个数,第三个参数索引数组值的类型,最后一个参数是索引数组的指针。还有其他相应的函数。
glDrawRangeElements 可以指定索引数组的起始和结束位置.
glInterleavedArrays可以使用混合数组。相关的函数请参考文档。