OpenGL超级宝典学习笔记——显示列表

klingde 2015-02-25

前言

在先前的章节中,我们已经讨论OpenGL基本的一些渲染技术。这些基本的技巧足够渲染简单的图像,然而在渲染精细的画面逼真的画面的时候(非常多的顶点和纹理),如果使用之前的方式渲染(立即模式)速度就很慢了,考虑到性能的原因(特别是实时渲染)我们需要以更快的方式完成画面的渲染。精细的画面有大量的数据需要CPU和GPU去处理,而且把数据从应用程序发送到显卡有带宽和显存的瓶颈。

显示列表

到目前为止,我们的图元都是在一对glBegin/glEnd之间调用glVertex来组装的。这种方式是非常灵活(可以动态地修改数据)而且易于使用的。但考虑性能的时候,以这种方式传送图元到显卡性能是最差。考虑下面的步骤:画一个受光照的带纹理的三角形

glBegin(GL_TRIANGLES);
    glNormal3f(x, y, z);
    glTexCoord2f(s, t);
    glVertex3f(x, y, z);

    glNormal3f(x, y, z);
    glTexCoord2f(s, t);
    glVertex3f(x, y, z);

    glNormal3f(x, y, z);
    glTexCoord2f(s, t);
    glVertex3f(x, y, z);
glEnd();

就一个简单的三角形就有11个函数调用。函数调用需要压栈,出栈等操作。如果一个复杂的图形有1万个或者更多三角形组成,那这些函数调就会消耗许多CPU时间。可以想象显卡空闲着等待CPU收集这些数据然后发送这批图元。这种批量提交图元的方式称为立即模式。OpenGL提供了更好的方式了处理这些数据。

批处理

OpenGL是图形硬件的一个软件接口。你可以想象成OpenGL的命令被转换为一些指定的硬件的命令和驱动的操作,然后传到显卡上立即执行。实际上,这些命令不是立即传到图形显卡上执行的,而是有一个本地的缓冲区,当这个缓冲区的命令累积到一个临界值的时候,就会被清空(flush)到硬件上去。

之所以这么做的主要原因是与图形硬件图形需要花费较长的CPU时间。这个时间对于我们来说可能是非常短的,但对于一个一秒钟运行几十亿个周期的CPU来说是相当缓慢的。打个比方有一艘从美国的英国的轮船,我们不会在一个人登船之后,就启程到英国,然后返回再接另一个人。而是会尽可能地等到轮船满载了之后才出发。对于计算机来说,通过系统总线一次性发送大量的数据,比分多次发送少量的数据要快。

发送缓冲数据到图形硬件是一个异步操作,这样CPU就可以空闲出来去处理其他的事情而不用等到这批渲染命令传送完成。这样图形硬件和CPU的并行是非常高效的。

有三个事件会出发缓冲区的刷新(flush)。第一个是缓冲区满了的时候,会把这批命令发给图形硬件。我们没有权限去访问这个缓冲区和调整这个缓冲区的大小。第二个是执行交换缓冲区命令(swapbuffer)的时候。缓冲区交换是告诉驱动程序,表面已经完成了一个特定的场景,所有的命令应该被执行去渲染场景。如果是使用单缓冲区的,则需要调用手工调用刷新:

void glFlush(void);

有些OpenGL的命令并不会进行缓冲,例如glReadPixels和glDrawPixels。这些函数要访问帧缓冲区进行直接的读写数据。在读写帧缓冲区是命令队列必须被清空。还有一种是强制刷新命令缓冲区,然后等待渲染任务的完成。函数调用如下:

void glFinish(void);

这个函数较少用到,一般用于多线程或者多渲染环境。

批预处理

OpenGL的命令要从高级的命令语言被翻译成低级的机器级的命令才能被机器所理解。如果命令非常多,那么翻译也需要消耗一定的时间。所以为了提高性能,有些万年不变的OpenGL命令,可以翻译后就保存起来,而不是每次都去翻译。例如画一个圆环(gltDrawTorus)的命令和顶点数据总是相同的,圆环是有一系列的三角形组成的,其中需要用到一些三角函数的命令。 我们只是通过改变模型视图矩阵,来变换圆环的位置而已。

一个较好的解决方案是把这些命令和数据先进行预处理,然后保存起来,以后调用的时候可以快速地拷贝到命令缓冲区然后执行。在OpenGl中这些预编译的命令列表称为显示列表。OpenGL使用glNewList/glEndList来包括显示列表。

glNewList(<unsigned integer name>, GL_COMPILE);

//some OpenGL code

glEndList();

glNewList的第一个参数是显示列表的名称,用unsigned int类型来表示。第二个参数GL_COMPILE则告诉OpenGL编译这些列表,但不立即执行他们。你可以指定GL_COMPILE_AND_EXCUTE这样就编译后立即执行一次(在渲染场景的函数可能会这样做)。一般在初始化的时候,我们会预先编译(GL_COMPILE)好这些显示列表,然后在渲染场景时调用。

显示列表的名称可以是任意的无符号整数。当名称命名重复时,后一个显示列表会覆盖掉前一个。为防止这种情况发生,我们可以使用OpenGL提供的函数

GLuint glGenLists(GLsizei range);

这个函数会生成指定个数的显示列表的名称。显示列表的名称是按数字顺序保留的,返回值是第一个名称。例如:glGenLists(3); 如果返回值是5,那就代表5,6,7这三个名称是保留的,供给你使用的。下次再调用glGenLists就是从8开始了。执行显示列表命令的函数:

void glCallList(GLuint list);

或者执行一组显示列表命令:

void glCallLists(GLsizei n, GLenum type, const GLvoid *lists);

n代表显示列表的个数,type是显示列表数组lists的类型,lists是指向显示列表名称的指针。

显示列表例子

在这里修改之前的第八章的sphereworld的例子,使用显示列表来渲染球体,圆环和地面。显示列表非常容易使用,只需要对这个例子做一个简单的修改即可。首先在初始化(SetupRC)产生一些显示列表的名称(unsigned int),然后把绘制球体,圆环和地面的代码进行预编译。代码如下:

static unsigned int spherelist_1; static unsigned int spherelist_2; static unsigned int torulist; static unsigned int groundlist; void SetupRC()
{
....
... //给显示列表命名 spherelist_1 = glGenLists(4);
  spherelist_2 = spherelist_1 + 1;
  torulist = spherelist_1 + 2;
  groundlist = spherelist_1 + 3; //预编译这些显示列表 glNewList(spherelist_1, GL_COMPILE);
    glutSolidSphere(0.3, 20, 20);
  glEndList();


  glNewList(spherelist_1, GL_COMPILE);
    glutSolidSphere(0.3, 20, 20);
  glEndList();

  glNewList(torulist, GL_COMPILE);
    gltDrawTorus(0.25f, 0.15f, 25, 25);
  glEndList();

  glNewList(groundlist, GL_COMPILE);
    RenderGround();
  glEndList();

}

在添加一个右键菜单,来切换两种模式。在渲染时进行判断。执行显示列表的函数是 void glCallList(GLuint list)

if (iMode == LISTMODE)
  {
    glCallList(groundlist);
  } else {
    RenderGround();
  }

.... if (iMode == LISTMODE)
    glCallList(torulist); else gltDrawTorus(0.25f, 0.15f, 25, 25);
...

记录渲染一次所花费处理器时间,并显示在窗口标题上。仅仅通过一一次渲染的时间就能够对比出不同了。

static void RenderScene()
{
  clock_t t = clock();
  ....
  ....

  glutSwapBuffers();
  t = clock() - t; char buffer[128] = {0,};
  wsprintf(buffer, "render clicks is %d", t);
  glutSetWindowTitle(buffer);
}

看一下结果:

正常模式下,花的处理器时间是5左右:

OpenGL超级宝典学习笔记——显示列表

显示列表模式下是1左右:

OpenGL超级宝典学习笔记——显示列表

就这么一个简单的场景就有了5倍的差距。可见使用显示列表能极大的提高性能。但显示列表有一个缺点,就是数据得是静态的,灵活性差。

注意:并不是所有的OpenGL函数都可以在显示列表中存储且通过显示列表执行。一般来说,用于传递参数或返回数值的函数语句不能存入显示列表,因为这张表有可能在参数的作用域之外被调用;如果在定义显示列表时调用了这样的函数,则它们将按瞬时方式执行并且不保存在显示列表中,有时在调用执行显示列表函数时会产生错误。以下列出的是不能存入显示列表的OpenGL函数:

  glDeleteLists()    glIsEnable()
  glFeedbackBuffer()   glIsList()
  glFinish()       glPixelStore()
  glGenLists()      glRenderMode()
  glGet*()        glSelectBuffer()

 

相关推荐

Tokyo0 / 0评论 2014-12-15

bingxuelengmei / 0评论 2014-12-04