第12课 显示列表
译者:樱
这是本课的例子的截图
本课我将教给大家如何使用显示列表。使用显示列表不仅能加快代码的运行速度,而且能大大缩短代码的长度。
举例来说。当你制作一个小行星游戏场景的时候,每一层都至少有两个小行星。所以你坐在你的图纸旁边,计算出如何来制作3D的小行星。当你了解了所有东西,你将在OPENGL中使用多边形或曲面来建立小行星。假设小行星都是八面体。如果你够聪明的话你会建立一个循环,然后在循环里把小行星绘制一遍。你将用大约18行代码或者更多来完成小行星的制作。每次当小行星被绘制到窗口时都创建它们对系统来说是非常困难的(译者:即开销特别大)。当你接触到更多的复合对象时你就会明白我的意思了。
那么如何解决了?使用显示列表吧!借助着显示列表,你将只创建一次对象。你可以对它进行纹理映射,给它染色,以及所有你想做的。你要给予显示列表一个名字,比如说我们把这个小行星的显示列表叫做“小行星”。那么现在不论任何时候当你想要把这个经过纹理映射或染色的小行星绘制到窗口时,你仅仅需要调用函数glCallList(“planet”)。我们先前创建的小行星将出现在窗口上。这是因为小行星已经在显示列表中被创建了,OPENGL不用计算如何来创建他,它之前就已经在内存中创建完了。这将节约很多CPU时间并且允许你的程序运行地更快!
那么你准备好了吗?在本示例里我们将称这个显示列表为Q-Bert。我们将用Q-Bert完成15个立方体。每个立方体都是由一个顶和一个盒子组成的。顶都是单独的显示列表,这是为了给予他们更深色的阴影。而盒子是没有顶的。
代码基于第六课。我将重写程序的大部分地方,所以你很容易看出哪些地方是经过修改的。以下几行代码是我们所有课程中的标准代码。
#include <windows.h> // Header File For Windows
#include <stdio.h> // Header File For Standard Input/Output
#include <gl\gl.h> // Header File For The OpenGL32 Library
#include <gl\glu.h> // Header File For The GLu32 Library
#include <gl\glaux.h> // Header File For The GLaux Library
HDC hDC=NULL; // Private GDI Device Context
HGLRC hRC=NULL; // Permanent Rendering Context
HWND hWnd=NULL; // Holds Our Window Handle
HINSTANCE hInstance; // Holds The Instance Of The Application
bool keys[256]; // Array Used For The Keyboard Routine
bool active=TRUE; // Window Active Flag Set To TRUE By Default
bool fullscreen=TRUE; // Fullscreen Flag Set To Fullscreen Mode By Default
接下来建立变量。首先建立存储纹理的变量。然后我们建立2个新的变量来保存显示列表。这2个变量就像指针一样指出了显示列表在内存中被储存的位置。他们被叫做box和top。接着我们再创建2个叫做xloop和yloop的变量,他们将被用做表示立方体在屏幕上的位置,另外2个变量xrot和yrot将表示立方体沿x和y轴旋转的角度。
GLuint texture[1]; // Storage For One Texture
GLuint box; // Storage For The Display List
GLuint top; // Storage For The Second Display List
GLuint xloop; // Loop For X Axis
GLuint yloop; // Loop For Y Axis
GLfloat xrot; // Rotates Cube On The X Axis
GLfloat yrot; // Rotates Cube On The Y Axis
接下来我们创建2个颜色数组。第一个是盒子的颜色,存储了亮红、橙色、黄色、绿色和兰色。每个颜色在{}中都包含了RGB分量。第二组颜色是暗红、暗橙、暗黄、暗绿和暗蓝。这些暗色将被用来创建盒子的顶。我们希望盖子比盒子的其他部分颜色更深些。
static GLfloat boxcol[5][3]= // Array For Box Colors
{
// Bright: Red, Orange, Yellow, Green, Blue
{1.0f,0.0f,0.0f},{1.0f,0.5f,0.0f},{1.0f,1.0f,0.0f},{0.0f,1.0f,0.0f},{0.0f,1.0f,1.0f}
};
static GLfloat topcol[5][3]= // Array For Top Colors
{
// Dark: Red, Orange, Yellow, Green, Blue
{.5f,0.0f,0.0f},{0.5f,0.25f,0.0f},{0.5f,0.5f,0.0f},{0.0f,0.5f,0.0f},{0.0f,0.5f,0.5f}
};
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Declaration For WndProc
现在该建立显示列表了。如果你注意到所有用来建立盒子的代码都在第一个列表里,而用来建立顶的代码都在另一个,我将解释这些细节。
GLvoid BuildList() // Build Box Display List
{
开始的时候我们告诉OPENGL我们要建立两个显示列表。glGenLists(2)建立了两个显示列表的空间,并返回第一个显示列表的指针。“box”指向第一个显示列表,任何时候调用“box”第一个显示列表就会显示出来。
box=glGenLists(2); // Building Two Lists
现在开始建立第一个显示列表。我们已经申请了两个显示列表的空间了,并且已经知道box指向第一个显示列表。所以现在我们应该告诉OPENGL要建立什么类型的显示列表。 我们用命令glNewList()来做这个事情。你一定注意到了box是第一个参数,这意味着OPENGL将把列表存储到box所指向的内存空间。第二个参数GL_COMPILE告诉OPENGL我们想预先在内存中建立这个列表,这样每次画的时候就不必计算怎么创建物体了。 GL_COMPILE跟编程相似。当你写程序时,把它装载到编译器里,你每次运行程序都需要重新编译。而如果他已经编译成了.exe文件,那么每次你只需要双击那个.exe文件就可以运行了,不需要编译。当OPENGL编译过显示列表后,就不需要再每次显示的时候重新编译它了。这就是为什么用显示列表可以加快速度。
glNewList(box,GL_COMPILE); // New Compiled box Display List
下面这部分的代码画出一个没有顶部的盒子,它不会出现在屏幕上,只会存储在显示列表里。
你可以在glNewList()和glEngList()中间加上任何你想加上的代码。可以设置颜色,可以改变纹理映射等等。唯一不能加进去的代码就是会改变显示列表的代码。显示列表一旦建立,你就不能改变它。
如果你加上glColor3ub(rand()%255,rand()%255,rand()%255),本以为每一次画物体时都会有不同的颜色。但因为显示列表只会建立一次,所以每次画物体的时候颜色都不会改变。物体将会保持第一次建立时的颜色。 如果你想改变显示列表的颜色,你只有在调用显示列表之前改变颜色。后面将详细解释这一点。
glBegin(GL_QUADS); // Start Drawing Quads
// Bottom Face
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, -1.0f); // Top Right Of The Texture and Quad
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, -1.0f, -1.0f); // Top Left Of The Texture and Quad
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f); // Bottom Left Of The Texture and Quad
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); // Bottom Right Of The Texture and Quad
// Front Face
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); // Bottom Left Of The Texture and Quad
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f); // Bottom Right Of The Texture and Quad
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f); // Top Right Of The Texture and Quad
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); // Top Left Of The Texture and Quad
// Back Face
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f); // Bottom Right Of The Texture and Quad
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); // Top Right Of The Texture and Quad
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); // Top Left Of The Texture and Quad
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f); // Bottom Left Of The Texture and Quad
// Right face
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f); // Bottom Right Of The Texture and Quad
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); // Top Right Of The Texture and Quad
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f); // Top Left Of The Texture and Quad
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f); // Bottom Left Of The Texture and Quad
// Left Face
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f); // Bottom Left Of The Texture and Quad
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); // Bottom Right Of The Texture and Quad
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); // Top Right Of The Texture and Quad
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); // Top Left Of The Texture and Quad
glEnd(); // Done Drawing Quads
我们用glEndList()告诉OPENGL,我们已经完成了显示列表。那么在glBeginList()和glEndList()之间的部分就是显示列表的内容。在他们之外的部分不是当前显示列表的内容。
glEndList();
那么我们来创建下一个显示列表。我们将上面的旧列表box递增一来得到第二个显示列表在内存中位置。下面的代码将建立‘top’显示列表。
top=box+1;
我们已经知道第二个显示列表存储的位置了,我们可以建立它了。我们将按照建立第一个显示列表的方法来建立第二个。不过我们会告诉OPENGL把列表存储在top而不是box。
glNewList(top,GL_COMPILE);
下面的代码段正是绘制盒子的顶的。它时在Z平面上的一个非常简单的图形。
glBegin(GL_QUADS); // Start Drawing Quad
// Top Face
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); // Top Left Of The Texture and Quad
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, 1.0f, 1.0f); // Bottom Left Of The Texture and Quad
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, 1.0f, 1.0f); // Bottom Right Of The Texture and Quad
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); // Top Right Of The Texture and Quad
glEnd(); // Done Drawing Quad
我们再一次告诉OPENGL列表结束了。当然还是用命令glEndList()。我们已经成功的建立了2个列表了。
glEndList(); // Done Building The top Display List
用来载入位图建立纹理的代码和以前课程里的一样。我们希望我们能够用纹理渲染立方体的全部6个面。所以我决定使用“多纹理映射(译者:也就是mipmapping)”来使纹理看起来平滑。我讨厌看到象素。载入的纹理叫做cube.bmp。它被存储在data文件夹里。找到LoadBMP函数,并且改变相应的代码行。
int InitGL(GLvoid) // All Setup For OpenGL Goes Here
{
if (!LoadGLTextures()) // Jump To Texture Loading Routine
{
return FALSE; // If Texture Didn't Load Return FALSE
}
BuildLists(); // Jump To The Code That Creates Our Display Lists
glEnable(GL_TEXTURE_2D); // Enable Texture Mapping
glShadeModel(GL_SMOOTH); // Enable Smooth Shading
glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Black Background
glClearDepth(1.0f); // Depth Buffer Setup
glEnable(GL_DEPTH_TEST); // Enables Depth Testing
glDepthFunc(GL_LEQUAL); // The Type Of Depth Testing To Do
下面3行代码激活光照。Light0在大多数显卡上都预先定义过,所以可以使我们避免对光源的过多讨论。在我们激活Light0以后我们就要激活光照。如果Light0不能在你的显卡上工作的话(你将看到一片黑暗),就关掉光照吧。
下面一行GL_COLOR_MATERIAL让我们给纹理映射加入颜色。如果不激活材质颜色,纹理将一直保持它本来的颜色。函数glColor3f(r,g,b)将对改变颜色不起任何作用。所以激活他是非常重要的。
glEnable(GL_LIGHT0);
glEnable(GL_LIGHTING);
glEnable(GL_COLOR_MATERIAL);
最后我们设置透视视图,使它看起来更漂亮些。并且返回TRUE,让我们的程序知道初始化成功了。
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // Nice Perspective Correction
return TRUE;
现在我们写绘制的代码。像往常一样,我对数学有一点点的抓狂。没有sin,也没有cos,但仍然有一点奇怪。我们仍然像往常一样以清空屏幕和深度缓存开始。
接着我们为立方体建立纹理。我本可以把这行代码加入到显示列表代码里。但是借着把它留在列表外面,我可以在任何时候改变纹理。如果我在列表里加入glBindTexture(GL_TEXTURE_2D, texture[0]),显示列表将永久保持我所为他选择的纹理。
int DrawGLScene(GLvoid) // Here's Where We Do All The Drawing
{
// Clear The Screen And The Depth Buffer
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glBindTexture(GL_TEXTURE_2D, texture[0]); // Select The Texture
接下来是有趣的物体。我们叫这个循环为yloop。这个循环用来定义立方体在y轴上的位置。我们希望上下共能有5个立方体,所以我们从1循环到5。
for (yloop=1;yloop<6;yloop++) // Loop Through The Y Plane
{
我们还有一个叫做xloop的循环。它被用来标出立方体在x轴上的位置。立方体的数目取决于我们在哪一行。如果我们在第一行,xloop将只从0到0,也就是只画一个立方体。下行就画2个立方体,依此类推。
for (xloop=0;xloop<yloop;xloop++) // Loop Through The X Plane
{
用glLoadIdentity()重新设置场景。
glLoadIdentity();
下面代码将对象在屏幕上平移。它看起来使人迷惑,但事实上却不是这样。在X轴上,会发生如下的事件:
为了使金字塔在屏幕的中央出现,我们向右移动1.4个单位。然后把xloop乘上2.8并且加上1.4(乘上2.8是为了让这些立方体不在其他立方体的顶上,当我们旋转45度时,2.8是是对立方体宽度的一个粗略近似)。最后减去yloop*1.4。这会把立方体左移,并且依赖于当前行号。如果我们没有左移的话,金字塔将在左边直线向上(它看起来将不再像个金字塔)。
在Y轴上我们把yloop减去6否则的话金字塔会被建立在上方。然后我们把结构乘上2.4。否则立方体会与其他立方体顶部接触(2.4是立方体高度的近似)。然后再减去7,这样金字塔旧能在屏幕底部开始向上建立。
最后,我们把对象沿Z轴向屏幕里面移动20单位。这样我们可爱的金字塔就完美的符合屏幕大小了。
glTranslatef(1.4f+(float(xloop)*2.8f)-(float(yloop)*1.4f),((6.0f-float(yloop))*2.4f)-7.0f,-20.0f);
现在我们绕X轴旋转。我们将会使立方体向视点倾斜(45-2*yloop)度。由于对立方体的透视模式倾斜会自动进行,所以我减去它来抵消倾斜效果。虽然不是最好的方法,但是它确实可以工作的很好 :)
最后我们累加xrot。它提供给我们用键盘控制旋转角度的接口(玩起来会非常有趣)。
在我们绕X轴旋转之后,我们在Y轴上旋转45度,并且累加yrot来控制绕Y轴的旋转。
glRotatef(45.0f-(2.0f*yloop)+xrot,1.0f,0.0f,0.0f); // Tilt The Cubes Up And Down
glRotatef(45.0f+yrot,0.0f,1.0f,0.0f);
在立方体盒子(不包括顶)实际绘制之前,我们要先选择颜色(亮色)。注意我们使用了glColor3fv()。这个命令将一次性地从大括号{}里载入RGB颜色并且设置它。3fv表示有3个值,浮点数(floating point),以及向量模式。我们选择的颜色的索引是yloop-1,它将为每排盒子给出不同的颜色。如果使用xloop-1的话那么每列盒子的颜色就不会相同。
glColor3fv(boxcol[yloop-1]);
颜色已经设置好了,我们应该开始画我们的盒子。我们所做的一切仅仅是调用显示列表,而不是写出所有的盒子的绘制函数。我们用命令glCallList(box)来调用显示列表。Box告诉OPENGL选择box显示列表而不是其他。Box显示列表绘制的是没有顶的立方体。盒子将会用我们选择的颜色来绘制,并且移动到了我们期望的地点。
glCallList(box);
在绘制盒子顶部之前,要先选择顶部的颜色(暗色)。这个颜色将依赖于行(yloop-1)。
glColor3fv(topcol[yloop-1]);
最后,我们所剩下的唯一一件事就是画上盒子的顶部。这将会给盒子加上一个暗色的盖子。这就是全部了。的确很简单!
glCallList(top);
}
}
return TRUE;
}
剩下的变动是在WinMain()里。在SwapBuffer(hDC)这行后面会加入一些代码,如果你按下了左、右、上或下键,立方体将会相应移动!
SwapBuffers(hDC);
if (keys[VK_LEFT])
{
yrot-=0.2f;
}
if (keys[VK_RIGHT])
{
yrot+=0.2f;
}
if (keys[VK_UP])
{
xrot-=0.2f;
}
if (keys[VK_DOWN])
{
xrot+=0.2f;
}
就像以前的教程一样,要确保窗口标题正确。
if (keys[VK_F1])
{
keys[VK_F1]=FALSE;
KillGLWindow();
fullscreen=!fullscreen;
if (!CreateGLWindow("NeHe's Display List Tutorial",640,480,16,fullscreen))
{
return 0;
}
}
}
}
到本课结束,你应该对显示列表有了一个很好的理解了。比如说如何去创建它们,如何在屏幕上显示他们。显示列表确实很棒!他们不仅仅使复杂对象的编码简单化,同时也提高了一些帧速率。
我希望你能喜欢本教程。如果你有什么问题或者有什么不明白的地方,请给我写信让我知道。
译者:
老规矩,公布我的联系方式……
Email:sakura@china.com
MSN: autsak@hotmail.com 本油箱不收信
网站:http://www.autsak.com