浏览量:899

显示列表

一.OpenGL状态机

OpenGL是一个状态机,可以设置它的各种状态,这些状态会一直有效直到你改变了这些状态为止。例如,当前的状态就是一种状态,你可以设置当前的颜色为白色,红色等等,接下来画的每个对象都是对应的颜色,如画出的drawCube1(),drawCube2()画出的对象都是(1.0f,0.0f,0.0f)颜色,直到你设置为其它颜色(0.0f,1.0f,0.0f)为止,而颜色就是状态机中的一个状态。如下面的代码片段所示:

glColor3f(1.0,0.0,0.0f);
drawCube1();
drawCube2();
glColor3f(0.0f,1.0f,0.0f);

OpenGL还包括其它状态,分别控制着当前的视点变换矩阵,投影变换矩阵,直线和多边形的点画模式,多边形绘图模式,像素打包转换,光照的位置和特点,物体的材质等。许多状态可以通过glEnable()和glDisable()进行控制。每个状态或者模式都有一个默认的值,在任何位置都能查询系统每个状态的值,典型的可以使用glGetBooleanv(),glGetDoublev(,glGetFloatv(),glGetIntegerv(),glGetPointerv()或者glIsEnabled()来查询。一些状态需要使用特别的查询命令,例如glGetLight*(),glGetError(),glGetPolygonStipple()。另外,你也可以将一组状态保存到属性堆栈(Attribute Stack)中,采用glPushAttrib()或者glPushClientAttrib()临时保存它们,用glPopAtrib()或者glPopClientAttrib()恢复这些状态。对于一些临时的状态改变,尽量采用这些状态保存和恢复的命令,而不是使用状态查询命令,这样更加高效。glPushClientAttrib()和glPopClientAttrib()分别用于存储和恢复客户端的状态变量,glPushAttrib()和glPopAttrib()分别用于存储和恢复服务器端的状态变量。

二.显示列表(Display List

2.1. 显示列表设计哲学

OpenGL采用客户端-服务器模型,显示列表是服务器的一部分,常驻在服务器端,如果重复使用存储在显示列表中的命令,减小了通过网络发送数据的开销,将常用的命令存储在显示列表中可以提高性能,而一些图形硬件能将显示列表存储在硬件专用内存中或者将数据存储在与图形硬件或软件相兼容的优化格式中。

在硬件产商对OpenGL的实现中为了优化性能,显示列表是一个指令的缓存,而不是动态的数据库,换句话说,显示列表不能被修改。如果可以修改显示列表,则由于要搜索显示列表和内存管理等操作,会导致性能降低。在显示列表中指令的优化方法可能由于实现的不同而不同,例如,简单的命令glRotate*(),需要进行复杂的矩阵运算(涉及平方根,三角函数计算),通过显示列表可以产生显著的改善,因为在显示列表中,可能只有最终的旋转矩阵被存储起来,这就能与它的执行与硬件端执行glMultMatrix*()命令一样快。此外,一些复杂硬件产商的OpenGL实现版本中,可能只将相邻的几个变换转化一个简单的矩阵乘法。虽然不能保证OpenGL的实现能够优化一些特别用途的显示列表,但是执行显示列表不比分开执行包含在显示列表中的命令慢。由于跳转到显示列表需要一些开销,如果某些列表特别短,跳转的开销可能抵消掉执行快的优势。在下面几种情况下,更有可能进行优化:

(1)       矩阵操作。很大一部分矩阵操作需要计算矩阵逆,在一些OpenGL实现中,显示列表可能存储计算后的矩阵和它的逆。

(2)       光栅化的位图和图像。一些光栅化的数据格式可能不是硬件的理想存储格式,但是通过显示列表的编译,OpenGL可能将数据转化为更适合硬件的存储格式,这在包含一系列位图的字体绘制、字符串绘制上速度有较大的提高。

(3)       光照、材质属性和光照模型。在绘制一个复杂的光照条件的场景时,可能需要改变每个表项的材质,设置材质是很慢的,如果把材质的定义放在显示列表中,每次变换材质时不需要再次进行计算,只需要存储在显示列表中的结果就行。

(4)       多边形的点画模式

2.2. 显示列表的使用

显示列表的使用的片段,如下所示:

listName = glGenList(1);
glNewList(listName,GL_COMPILE);
    glColor3f(1.0f,0.0f,0.0f);
    glBegin(GL_TRIANGLES);
           glVertex2f(0.0f,0.0f);
           glVertex2f(1.0f,0.0f);
           glVertex2f(0.0f,1.0f);
    glEnd();
    glTranslatef(1.5f,0.0f,0.0f);
glEndList();

至此创建显示列表完成,当需要使用到该显示列表的内容时,调用glCallList()即可。glNewList()命令中可以使用两个参数GL_COMPILE和GL_COMPILE_AND_EXECUTE,GL_COMPILE表示创建显示列表时不立刻执行,GL_COMPILE_AND_EXECUTE表示立刻开始执行,并把显示列表创建起来供以后使用。glGenList (),glNewList(),glEndList(),glCallList()四种方法具体的使用方法,参见相应的文档。

并不是所有的OpenGL命令都能用显示列表存储和执行,例如,一些设置客户状态和查询状态值的命令就不能存储在显示列表中。

display list

图1 不能存储在显示列表中的命令

为了深入理解这些命令不能存储在显示列表中的原理,可以将OpenGL的客户端-服务器模型理解为:客户端在一台机器上,服务器在另一台机器上,它们之间通过网络进行通信。显示列表创建后,它会常驻在服务器上,所以服务器不能依赖必需客户端提供信息的显示列表。下面的几种状况也不能用显示列表:对于改变客户端状态的命令,例如glPixelStore(),glSelectBuffer();一些定义顶点数组的命令也不能存储在显示列表中,像glVertexPointer(),glColorPointer();设置客户端指针命令,像glInterleavedArrays()。

特别注意glArrayElement(),glDrawArrays()和glDrawElements()是将数据发送到服务器以构造图元,存储在显示列表中顶点数组的数据是通过解引用(dereferencing)指针上的数据获得的,不是存储指针本身,对顶点数组中数据的改变不会影响到显示列表中图元的定义,所以可以存储在显示列表中。参考《顶点数组》一文。

2.3. 分层显示列表(hierarchy display list)

分层显示列表的代码片段如下所示:

glNewList(listIndex,GL_COMPILE);
    glCallList(handlebars);
    glCallList(frame);
    glTranslatef(1.0f,0.0f,0.0f);
    glCallList(wheel);
    glTranslatef(3.0f,0.0f,0.0f);
    glCallList(wheel);
glEndList();

可以在显示列表中嵌套别的显示列表,为了避免无穷嵌套,显示列表的嵌套数量有一个阈值,阈值的最小值是64,由于OpenGL实现的不同,阈值可能会更大,可以使用函数glGetIntegerv()传入参数GL_MAX_LIST_NESTING进行查询。

OpenGL推荐使用glGenLists()来获得未使用的显示列表索引,也可以不采用该命令,而采用手动指定索引的方法,此时需要使用glIsList()来查询这个索引是否正在使用,当调用glGenList()时并没有分配存储空间,只是指定一个未使用的显示列表索引号。可以使用glDeleteLists()来删除一个显示列表或者一块连续的显示列表。

2.4.显示列表管理状态变量

关于状态变量的叙述如第一章所述,执行显示列表后,状态变量可能发生变化,如下面的例子所示,颜色状态和模型视图矩阵发生了变化:

glNewList(listIndex,GL_COMPILE);
    glColor3f(1.0f,0.0f,0.0f);
    glBegin(GL_POLYGON);
         glVertex2f(0.0f,0.0f);
         glVertex2f(1.0f,0.0f);
         glVertex2f(0.0f,1.0f);
    glEnd();
    glTranlatef(1.5f,0.0f,0.0f);
glEndList();

在执行完显示列表后,有时候我们希望变化的状态能维持下去,也有时候却希望恢复原有的状态,这时候不能使用glGet*()来查询状态变量的方法进行恢复。但是可以通过使用glPushAttrib()和glPopAttrib()(注意不能使用glPushClientAttrib()和glPopClientAttrib()),对于矩阵的变换可以采用glPushMatrix(),glPopMatrix()。

2.5.显示列表设置模式,光照模型等

能够使用显示列表管理和存储一组命令来改变各种模式或者各种参数,当你想要将一组设置转换到另一组设置时,使用显示列表可能比直接调用命令更加有效。比如把显示列表在转换各种光照、光照模型,材质变量设置上更加有效。下面的代码演示了显示列表在改变多边点画模式上的应用:

GLuint offset;
offset = glGenLists(2);

glNewList(offset,GL_COMPILE);
    glDisable(GL_LINE_STIPPLE);
glEndList();

glNewList(offset+1,GL_COMPILE);
    glEnable(GL_LINE_STIPPLE);
    glLineStipple(1,0x0F0F);
glEndList();

#define drawOneLine(x1,y1,x2,y2) glBegin(GL_LINES); glVertex2f((x1),(y1)); glVertex2f((x2),(y2)); glEnd();

glCallList(offset);
drawOneLine(50.0f,125.0f,350.0f,125.0f);
glCallList(offset+1);
drawOneLine(50.0f,100.0f,350.0f,100.0f);

2.6.连续显示列表的执行

OpenGL提供了有种有效的机制来执行几个连续的显示列表,即使用命令glCallLists(),该命令有三个参数:第1个指要画的显示列表个数,第2个索引数组的数据类型,第3个是指向数组的指针。实际应用中,并不需要规定数组中具体的索引,而采用偏移地址,而设置首地址的方法,可以采用函数glListBase(),当调用glCallLists()时,实际的索引号通过起始地址加上偏移量得到。下面的例子说明了该方法的细节:

GLuint index = glGenLists(10);  // create 10 display lists
GLubyte lists[10];              // allow maximum 10 lists to be rendered
glNewList(index, GL_COMPILE);   // compile the first one
...
glEndList();
...
// compile more display lists
glNewList(index+9, GL_COMPILE); // compile the last (10th)
...
glEndList();
...
// draw odd placed display lists only (1st, 3rd, 5th, 7th, 9th)
lists[0]=0; lists[1]=2; lists[2]=4; lists[3]=6; lists[4]=8;
glListBase(index);              // set base offset
glCallLists(5, GL_UNSIGNED_BYTE, lists);

在参考连接【1】中,作者提供了显示列表的演示代码,著名的NEHE教程中的Lesson 12和Lesson 13也提供了显示列表的应用例子,特别是Lesson13中使用显示列表渲染字体的应用。

总结:

使用显示列表时,注意四个关键点:

(1)显示列表内容太短,由于额外跳转的开销,并不能充分发挥显示列表的性能;

(2)显示列表内容的不可变性;

(3)显示列表存储在服务器端,不能使用改变客户端状态的命令;

(4)不可以使用查询状态的命令。

二.  参考

【1】     http://www.songho.ca/opengl/gl_displaylist.html

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

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

【4】     NEHE教程,Lesson 12和Lesson 13

spacer

Leave a reply