OpenGL超级宝典学习笔记——顶点数组


顶点数组

当我们有来自模型的大量数据的时候,使用显示列表来对这些数据进行预编译,需要遍历这些顶点数据(一次一个顶点数据)把数据传给OpenGL。依赖于顶点的数量,这会带来潜在的性能损耗。而且这些数据不一定是静态的,有可能在我们每次渲染的时候,我们需要对这些数据进行更改。这个时候就不适合使用显示列表。

在OpenGl中,使用顶点数组能够很好的解决这两个问题。使用顶点数组,我们可以随时进行预编译或修改几何图形,然后一次性传输这些数据。基本的顶点数组几乎和显示列表一样快,而且不要求数据是静态的。

在OpenGL中使用顶点数组有4个基本的步骤:

  1. 把几何图形的数据加载到一个或多个数组中(可以从磁盘中读取)
  2. 告诉OpenGL数据在哪里(参数传指向数据的指针)
  3. 你需要使用哪些数组,因为数组可能分为顶点数组,颜色数组,纹理坐标数组等,或者一个数组中包含这些数据,按照某种顺序排列。
  4. 执行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);

 

image

 

加载几何图形

首先我们需要把几何图形的数据组装到数组中。在上面的例子中,在初始化的时候就组装好数据,代码如下:

//星星的坐标 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指的是数据的类型,并不是所有的数据类型都可以被接受的。什么类型的数组能接受的数据类型和元素个数如下:

image

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)能够共享顶点。但没办法避免两个三角形带所共享顶点的变换的开销,因为每一个三角形带都必须是独立的。

image

下面举一个简单的例子。

 

简单的立方体

一个立方体有6个面,每个面都是由4个顶点组成的正方形,6x4=24个顶点,其实有许多被正方形共享的顶点,不重复的顶点只有8个。但按照以往的方式使用glBegin(GL_UQADS)/glEnd,我们还是需要传输24个顶点(调用glVertex 24次)。如果我们使用顶点索引数组的方式,就只需要8个顶点就够了,我们用索引指向这些顶点,索引数组中会有重复的值。图示如下:

image

每个顶点有浮点数值组成的,但每个索引只是一个整数值。在顶点数少的情况下,并不会节省多少空间。比如这个立方体,虽然顶点数组少存了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();
}

image

可以看到上面调用绘制的函数是

glDrawElements(GL_QUADS, 24, GL_UNSIGNED_BYTE, indexes);

第一个参数是图元的模式,第二个是索引数组包含的值的个数,第三个参数索引数组值的类型,最后一个参数是索引数组的指针。还有其他相应的函数。

glDrawRangeElements 可以指定索引数组的起始和结束位置.

glInterleavedArrays可以使用混合数组。相关的函数请参考文档。

OpenGL超级宝典 第4版 中文版PDF+英文版+源代码 见 

OpenGL编程指南(原书第7版)中文扫描版PDF 下载

OpenGL 渲染篇

Ubuntu 13.04 安装 OpenGL

OpenGL三维球体数据生成与绘制【附源码】

Ubuntu下OpenGL编程基础解析

如何在Ubuntu使用eclipse for c++配置OpenGL  

更多《OpenGL超级宝典学习笔记》相关知识 见 

相关内容