admin 管理员组

文章数量: 1184232


2024年3月13日发(作者:用vb编写计时秒表的程序)

setGeometry( 0, 0, 640, 480 );

setCaption( "NeHe's Animated Blended Textures Tutorial" );

if ( fullscreen )

showFullScreen();

startTimer( 5 ); // startTimer( 5 )就是每5毫秒执行一次timerEvent()函数做定

时操作

}

我们需要在构造函数中给各个变量赋初值。

void NeHeWidget::loadGLTextures()

{

QImage tex, buf;

if ( !( "./data/" ) )

{

qWarning( "Could not read image file, using single-color instead." );

QImage dummy( 128, 128, 32 );

( Qt::() );

buf = dummy;

}

tex = QGLWidget::convertToGLFormat( buf );

glGenTextures( 1, &texture[0] );

glBindTexture( GL_TEXTURE_2D, texture[0] );

glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );

glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );

glTexImage2D( GL_TEXTURE_2D, 0, 3, (), (), 0,

GL_RGBA, GL_UNSIGNED_BYTE, () );

}

loadGLTextures()函数就是用来载入纹理的。

void NeHeWidget::initializeGL()

{

loadGLTextures();

glEnable( GL_TEXTURE_2D );

glShadeModel( GL_SMOOTH );

glClearColor( 0.0, 0.0, 0.0, 0.5 );

glClearDepth( 1.0 );

glEnable( GL_DEPTH_TEST );

glDepthFunc( GL_LEQUAL );

glHint( GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST );

glBlendFunc( GL_SRC_ALPHA, GL_ONE );

glEnable( GL_BLEND );

上述设置前面的各章都提到过了,就不再讲了。

for ( loop = 0; loop < num; loop++ )

{

star[loop].angle = 0.0;

star[loop].dist = ( float(loop)/num ) * 5.0;

star[loop].r = rand() % 256;

star[loop].g = rand() % 256;

star[loop].b = rand() % 256;

}

设置了每颗星星的起始角度、距离和颜色。您会注意到修改结构的属性有多容易。全

部50颗星星都会被循环设置。要改变star[1]的角度我们所要做的只是star[1].angle={某

个数值};就这么简单!

第loop颗星星离中心的距离是将loop的值除以星星的总颗数,然后乘上5.0。基本

上这样使得后一颗星星比前一颗星星离中心更远一点。这样当loop为50时(最后一颗星

星),loop除以num正好是1.0。之所以要乘以5.0是因为1.0*5.0就是5.0。5.0已经很

接近屏幕边缘。我不想星星飞出屏幕,5.0是最好的选择了。当然如果如果您将场景设置的

更深入屏幕里面的话,也许可以使用大于5.0的数值,但星星看起来就更小一些(都是透

视的缘故)。

您还会注意到每颗星星的颜色都是从0~255之间的一个随机数。也许您会奇怪为何

这里的颜色得取值范围不是OpenGL通常的0.0~1.0之间。这里我们使用的颜色设置函

数是glColor4ub,而不是以前的glColor4f。ub意味着参数是unsigned byte型的。一

个byte的取值范围是0~255。这里使用byte值取随机整数似乎要比取一个浮点的随机

数更容易一些。

}

void NeHeWidget::paintGL()

{

glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

清楚屏幕及深度缓存。

glBindTexture( GL_TEXTURE_2D, texture[0] );

选择纹理。

for ( loop = 0; loop < num; loop++ )

{

这段程序我们来循环绘制所有的星星。

glLoadIdentity();

绘制每颗星星之前,重置模型观察矩阵。

glTranslatef( 0.0, 0.0, zoom );

深入屏幕里面(使用“zoom”的值)。

glRotatef( tilt, 1.0, 0.0, 0.0 );

倾斜视角(使用“tilt”的值)。

glRotatef( star[loop].angle, 0.0, 1.0, 0.0 );

接下来的代码,我们来移动星星。星星开始时位于屏幕的中心。我们要做的第一件事

是把场景沿Y轴旋转。如果我们旋转90度的话,X轴不再是自左至右的了,他将由里向

外穿出屏幕。为了让大家更清楚些,举个例子。假想您站在房子中间。再设想您左侧的墙

上写着-x,前面的墙上写着-z,右面墙上就是+x咯,您身后的墙上则是+z。加入整个房子

向右转90度,但您没有动,那么前面的墙上将是-x而不再是-z了。所有其他的墙也都跟

着移动。-z出现在右侧,+z出现在左侧,+x出现在您背后。神经错乱了吧?通过旋转场

景,我们改变了x和z平面的方向。

glTranslatef( star[loop].dist, 0.0, 0.0 );

这代码沿x轴移动一个正值。通常x轴上的正值代表移向了屏幕的右侧(也就是通常

的x轴的正向),但这里由于我们绕y轴旋转了坐标系,x轴的正向可以是任意方向。如果

我们转180度的话,屏幕的左右侧就镜像反向了。因此,当我们沿x轴正向移动时,可能

向左,向右,向前或向后。

glRotatef( -star[loop].angle, 0.0, 1.0, 0.0 );

glRotatef( -tilt, 1.0, 0.0, 0.0 );

星星实际上是一个平面的纹理。现在您在屏幕中心画了个平面的四边形然后贴上纹理,

这看起来很不错。一切都如您所想的那样。但是当您当您沿着y轴转上个90度的话,纹

理在屏幕上就只剩右侧和左侧的两条边朝着您。看起来就是一条细线。这不是我们所想要

的。我们希望星星永远正面朝着我们,而不管屏幕如何旋转或倾斜。

我们通过在绘制星星之前,抵消对星星所作的任何旋转来实现这个愿望。您可以采用

逆序来抵消旋转。当我们倾斜屏幕时,我们实际上以当前角度旋转了星星。通过逆序,我

们又以当前角度“反旋转”星星。也就是以当前角度的负值来旋转星星。就是说,如果我

们将星星旋转了10度的话,又将其旋转-10度来使星星在那个轴上重新面对屏幕。上面的

第一行抵消了沿y轴的旋转。然后,我们还需要抵消掉沿x轴的屏幕倾斜。要做到这一点,

我们只需要将屏幕再旋转-tilt倾角。在抵消掉x和y轴的旋转后,星星又完全面对着我们

了。

if ( twinkle )

{

glColor4ub( star[(num-loop)-1].r,

star[(num-loop)-1].g,

star[(num-loop)-1].b, 255 );

glBegin( GL_QUADS );

glTexCoord2f( 0.0, 0.0 ); glVertex3f( -1.0, -1.0, 0.0 );

glTexCoord2f( 1.0, 0.0 ); glVertex3f( 1.0, -1.0, 0.0 );

glTexCoord2f( 1.0, 1.0 ); glVertex3f( 1.0, 1.0, 0.0 );

glTexCoord2f( 0.0, 1.0 ); glVertex3f( -1.0, 1.0, 0.0 );

glEnd();

}

如果twinkle为真,我们在屏幕上先画一次不旋转的星星:将星星总数(num)减去

当前的星星数(loop)再减去1,来提取每颗星星的不同颜色(这么做是因为循环范围从

0到num-1)。举例来说,结果为10的时候,我们就使用10号星星的颜色。这样相邻星

星的颜色总是不同的。这不是个好法子,但很有效。最后一个值是alpha通道分量。这个

值越小,这颗星星就越暗。

由于启用了twinkle,每颗星星最后会被绘制两遍。程序运行起来会慢一些,这要看

您的机器性能如何了。但两遍绘制的星星颜色相互融合,会产生很棒的效果。同时由于第

一遍的星星没有旋转,启用twinkle后的星星看起来有一种动画效果。(如果您这里看不懂

得话,就自己去看程序的运行效果吧。)

值得注意的是给纹理上色是件很容易的事。尽管纹理本身是黑白的,纹理将变成我们

在绘制它之前选定的任意颜色。此外,同样值得注意的是我们在这里使用的颜色值是byte

型的,而不是通常的浮点数。甚至alpha通道分量也是如此。

glRotatef( spin, 0.0, 0.0, 1.0 );

glColor4ub( star[loop].r, star[loop].g, star[loop].b, 255 );

glBegin( GL_QUADS );

glTexCoord2f( 0.0, 0.0 ); glVertex3f( -1.0, -1.0, 0.0 );

glTexCoord2f( 1.0, 0.0 ); glVertex3f( 1.0, -1.0, 0.0 );

glTexCoord2f( 1.0, 1.0 ); glVertex3f( 1.0, 1.0, 0.0 );

glTexCoord2f( 0.0, 1.0 ); glVertex3f( -1.0, 1.0, 0.0 );

glEnd();

上面是绘制第二遍的星星。唯一和前面的代码不同的是这一遍的星星肯定会被绘制,

并且这次的星星绕着z轴旋转。

spin += 0.01;

star[loop].angle += float(loop)/num;

star[loop].dist -= 0.01;

以上的代码代表星星的运动。我们增加spin的值来旋转所有的星星(公转)。然后,

将每颗星星的自转角度增加loop/num。这使离中心更远的星星转的更快。最后减少每颗

星星离屏幕中心的距离。这样看起来,星星们好像被不断地吸入屏幕的中心。

if ( star[loop].dist < 0.0 )

{

star[loop].dist += 5.0;

star[loop].r = rand() % 256;

star[loop].g = rand() % 256;

star[loop].b = rand() % 256;

}

}

检查星星是否已经碰到了屏幕中心。当星星碰到屏幕中心时,我们为它赋一个新颜色,

然后往外移5个单位,这颗星星将踏上它回归屏幕中心的旅程。

}

void NeHeWidget::timerEvent(QTimerEvent*)

{

updateGL();

}

这里就是定时操作函数timerEvent(),执行的操作就是updateGL(),就是刷新窗口了,

其实它也会调用paintGL(),所以就实现了每5毫秒刷新一次的动画效果。

void NeHeWidget::keyPressEvent( QKeyEvent *e )

{

switch ( e->key() )

{

case Qt::Key_T:

twinkle = !twinkle;

updateGL();

break;

按下了T键,就可以切换是否闪烁。

case Qt::Key_Up:

tilt -= 0.5;

updateGL();

break;

case Qt::Key_Down:

tilt += 0.5;

updateGL();

break;

case Qt::Key_Prior:

zoom -= 0.2;

updateGL();

break;

case Qt::Key_Next:

zoom += 0.2;

updateGL();

break;

case Qt::Key_F2:

fullscreen = !fullscreen;

if ( fullscreen )

{

showFullScreen();

}

else

{

showNormal();

setGeometry( 0, 0, 640, 480 );

}

update();

break;

case Qt::Key_Escape:

close();

}

}

这一课我尽我所能来解释如何加载一个灰阶位图纹理,(使用融合)去掉它的背景色后,

再给它上色,最后让它在三维场景中移动。我已经向您展示了如何创建漂亮的颜色与动画

效果。实现原理是在原始位图上再重叠一份位图拷贝。到现在为止,只要您很好的理解了

我所教您的一切,您应该已经能够毫无问题的制作您自己的3D Demo了。所有的基础知

识都已包括在内!

本课程的源代码。

旗的效果(波动纹理)

大家好!对那些想知道我在这里作了些什么的朋友,您可以先按文章的末尾所列出的

链接,下载我那毫无意义的Demo看看先!我是bosco,我将尽我所能教您来实现一个以

正弦波方式运动的图象。这一课基于NeHe的教程第六课,当然您至少也应该学会了一至

六课的知识。您需要下载源码压缩包,并将压缩包内带的data目录连其下的位图一起释放

至您的代码目录下。或者使用您自己的位图,当然它的尺寸必须适合OpenGL纹理的要求。

NeHeWidget类

(由nehewidget.h展开。)

class NeHeWidget : public QGLWidget

{

Q_OBJECT

public:

NeHeWidget( QWidget* parent = 0, const char* name = 0, bool fs = false );

~NeHeWidget();

protected:

void initializeGL();

void paintGL();

void resizeGL( int width, int height );

void keyPressEvent( QKeyEvent *e );

void loadGLTextures();

void timerEvent( QTimerEvent * );

这些函数前几课中都提到过了。

protected:

bool fullscreen;

GLfloat xRot, yRot, zRot;

GLfloat hold;

GLuint texture[1];

float points[45][45][3];

int wiggle_count;

};

我们将使用points数组来存放网格各顶点独立的(x,y,z)坐标。这里网格由45×45点

形成,换句话说也就是由44格×44格的小方格子依次组成了。wiggle_count用来指定纹

理波浪的运动速度,每3帧一次看起来很不错。变量hold将存放一个用来对旗形波浪进

行光滑的浮点数。

(由展开。)

#include

因为我们在程序中要使用到sin和cos两个三角函数,所以我们需要包含math.h这

个头文件。

NeHeWidget::NeHeWidget( QWidget* parent, const char* name, bool fs )

: QGLWidget( parent, name )

{

xRot = yRot = zRot = 0.0;

hold = 0.0;

wiggle_count = 0;

fullscreen = fs;

setGeometry( 0, 0, 640, 480 );

setCaption( "bosco & NeHe's Waving Texture Tutorial" );

if ( fullscreen )

showFullScreen();

startTimer( 5 );

}

我们需要在构造函数中给各个变量赋初值。startTimer()函数我们在第九课中已经讲过

了。

void NeHeWidget::loadGLTextures()

{

QImage tex, buf;

if ( !( "./data/" ) )

{

qWarning( "Could not read image file, using single-color instead." );

QImage dummy( 128, 128, 32 );

( Qt::() );

buf = dummy;

}

tex = QGLWidget::convertToGLFormat( buf );

glGenTextures( 1, &texture[0] );

glBindTexture( GL_TEXTURE_2D, texture[0] );

glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );

glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );

glTexImage2D( GL_TEXTURE_2D, 0, 3, (), (), 0,

GL_RGBA, GL_UNSIGNED_BYTE, () );

}

loadGLTextures()函数就是用来载入纹理的。

void NeHeWidget::initializeGL()

{

loadGLTextures();

glEnable( GL_TEXTURE_2D );

glShadeModel( GL_SMOOTH );

glClearColor( 0.0, 0.0, 0.0, 0.5 );

glClearDepth( 1.0 );

glEnable( GL_DEPTH_TEST );

glDepthFunc( GL_LEQUAL );

glHint( GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST );

glPolygonMode( GL_BACK, GL_FILL );

glPolygonMode( GL_FRONT, GL_LINE );

上面的代码指定使用完全填充模式来填充多边形区域的背面(或者叫做后表面吧)。相

反,多边形的正面(前表面)则使用轮廓线填充了。这些方式完全取决于您的个人喜好。

并且与多边形的方位或者顶点的方向有关。详情请参考Red Book。这里我顺便推销一本

推动我学习OpenGL的好书-Addison-Wesley出版的《Programmer's Guide to

OpenGL》。个人以为这是学习OpenGL的无价之宝。

for ( int x = 0; x < 45; x++ )

{

for ( int y = 0; y < 45; y++ )

{

points[x][y][0] = float( ( x/5.0 ) - 4.5 );

points[x][y][1] = float( ( y/5.0 ) - 4.5 );

points[x][y][2] = float( sin( ( ( ( x/5.0 ) * 40.0 )/360.0 ) * 3.141592654 * 2.0 ) );

}

}

这里感谢Graham Gibbons关于使用整数循环变量消除波浪间的脉冲锯齿的建议。

上面的两个循环初始化网格上的点。使用整数循环可以消除由于浮点运算取整造成的

脉冲锯齿的出现。我们将x和y变量都除以5,再减去4.5。这样使得我们的波浪可以“居

中”(这样计算所得结果将落在区间[-4.5,4.5]之间)。

点[x][y][2]最后的值就是一个sin函数计算的结果。sin()函数需要一个弧度参变量。将

float_x乘以40.0,得到角度值。然后除以360.0再乘以PI,乘以2.0,就转换为弧度了。

}

void NeHeWidget::paintGL()

{

int x, y;

float float_x, float_y, float_xb, float_yb;

x、y是循环变量,float_x、float_y、float_xb、float_yb是用来将旗形的波浪分割成

很小的四边形。

glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

glLoadIdentity();

glTranslatef( 0.0, 0.0, -12.0 );

glRotatef( xRot, 1.0, 0.0, 0.0 );

glRotatef( yRot, 0.0, 1.0, 0.0 );

glRotatef( zRot, 0.0, 0.0, 1.0 );

glBindTexture( GL_TEXTURE_2D, texture[0] );

glBegin( GL_QUADS );

开始绘制四边形。

for ( x = 0; x < 44; x++ )

{

沿X平面0-44循环(45点)

for ( y = 0; y < 44; y++ )

{

沿Y平面0-44循环(45点)

float_x = float(x)/44.0;

float_y = float(y)/44.0;

float_xb = float(x+1)/44.0;

float_yb = float(y+1)/44.0;

上面我们使用4个变量来存放纹理坐标。每个多边形(网格之间的四边形)分别映射

了纹理的1/44 x 1/44部分。循环首先确定左下顶点的值,然后我们据此得到其他三点的

值。

glTexCoord2f( float_x, float_y );

glVertex3f( points[x][y][0], points[x][y][1], points[x][y][2] );

glTexCoord2f( float_x, float_yb );

glVertex3f( points[x][y+1][0], points[x][y+1][1], points[x][y+1][2] );

glTexCoord2f( float_xb, float_yb );

glVertex3f( points[x+1][y+1][0], points[x+1][y+1][1], points[x+1][y+1][2] );

glTexCoord2f( float_xb, float_y );

glVertex3f( points[x+1][y][0], points[x+1][y][1], points[x+1][y][2] );

上面四个坐标分别为左下、左上、右上、右下。

上面几行使用glTexCoord2f()和 glVertex3f()载入数据。提醒一点:四边形是逆时针

绘制的。这就是说,您开始所见到的表面是背面。后表面完全填充了,前表面由线条组成。

如果您按顺时针顺序绘制的话,您初始时见到的可能是前表面。也就是说您将看到网

格型的纹理效果而不是完全填充的。

}

}

glEnd();

四边形绘制结束。

if ( wiggle_count == 2 )

{

for ( y = 0; y < 45; y++ )

{

hold = points[0][y][2];

for ( x = 0; x < 44; x++ )

{

points[x][y][2] = points[x+1][y][2];

}

points[44][y][2] = hold;

}

wiggle_count = 0;

}

wiggle_count++;

上面所作的事情是先存储每一行的第一个值,然后将波浪左移一下,是图象产生波浪。

存储的数值挪到末端以产生一个永无尽头的波浪纹理效果。然后重置计数器

wiggle_count以保持动画的进行。

上面的代码由NeHe(2000年2月)修改过,以消除波浪间出现的细小锯齿。

xRot += 0.3;

yRot += 0.2;

zRot += 0.4;

标准的NeHe旋转增量:)。

}

void NeHeWidget::timerEvent(QTimerEvent*)

{

updateGL();

}

void NeHeWidget::keyPressEvent( QKeyEvent *e )

{

switch ( e->key() )

{

case Qt::Key_F2:

fullscreen = !fullscreen;

if ( fullscreen )

{

showFullScreen();

}

else

{

showNormal();

setGeometry( 0, 0, 640, 480 );

}

update();

break;

case Qt::Key_Escape:

close();

}

}

现在编译并运行程序,您将看到一个漂亮的位图波浪。除了嘘声一片之外,我不敢确

信大家的反应。但我希望大家能从这一课中学到点什么。如果您有任何问题或者需要澄清

的地方,请随便联络我。感谢大家!

本课程的源代码。

显示列表

这次我将教你如何使用显示列表,显示列表将加快程序的速度,而且可以减少代码的

长度。

当你在制作游戏里的小行星场景时,每一层上至少需要两个行星,你可以用OpenGL

中的多边形来构造每一个行星。聪明点的做法是做一个循环,每个循环画出行星的一个面,

最终你用几十条语句画出了一个行星。每次把行星画到屏幕上都是很困难的。当你面临更

复杂的物体时你就会明白了。

那么,解决的办法是什么呢?用显示列表,你只需要一次性建立物体,你可以贴图,

用颜色,想怎么弄就怎么弄。给显示列表一个名字,比如给小行星的显示列表命名为

“asteroid”。现在,任何时候我想在屏幕上画出行星,我只需要调用glCallList(asteroid)。

之前做好的小行星就会立刻显示在屏幕上了。因为小行星已经在显示列表里建造好了,

OpenGL不会再计算如何构造它。它已经在内存中建造好了。这将大大降低CPU的使用,

让你的程序跑的更快。

那么,开始学习咯。我称这个DEMO为Q-Bert显示列表。最终这个DEMO将在屏

幕上画出15个立方体。每个立方体都由一个盒子和一个顶构成,顶部是一个单独的显示

列表,盒子没有顶。

这一课是建立在第六课的基础上的,我将重写大部分的代码,这样容易看懂。下面的

这些代码在所有的课程中差不多都用到了。

NeHeWidget类

(由nehewidget.h展开。)

class NeHeWidget : public QGLWidget

{

Q_OBJECT

public:

NeHeWidget( QWidget* parent = 0, const char* name = 0, bool fs = false );

~NeHeWidget();

protected:

void initializeGL();

void paintGL();

void resizeGL( int width, int height );

void keyPressEvent( QKeyEvent *e );

void loadGLTextures();

void buildLists();

这个是建立显示列表的函数。

protected:

bool fullscreen;

GLfloat xRot, yRot, zRot;

GLuint box, top;

这里是两个用来存放显示列表的指针。

GLuint xLoop, yLoop;

这里是两个表示立方体位置的变量。

GLuint texture[1];

};

(由展开。)

static GLfloat boxcol[5][3] =

{

{ 1.0, 0.0, 0.0 },

{ 1.0, 0.5, 0.0 },

{ 1.0, 1.0, 0.0 },

{ 0.0, 1.0, 0.0 },

{ 0.0, 1.0, 1.0 }

};

static GLfloat topcol[5][3] =

{

{ 0.5, 0.0, 0.0 },

{ 0.5, 0.25, 0.0 },

{ 0.5, 0.5, 0.0 },

{ 0.0, 0.5, 0.0 },

{ 0.0, 0.5, 0.5 }

};

这里是两个颜色数组。

NeHeWidget::NeHeWidget( QWidget* parent, const char* name, bool fs )

: QGLWidget( parent, name )

{

xRot = yRot = zRot = 0.0;

box = top = 0;

xLoop = yLoop = 0;

fullscreen = fs;

setGeometry( 0, 0, 640, 480 );

setCaption( "NeHe's Display List Tutorial" );

if ( fullscreen )

showFullScreen();

}

我们需要在构造函数中给各个变量赋初值。

void NeHeWidget::loadGLTextures()

{

QImage tex, buf;

if ( !( "./data/" ) )

{

qWarning( "Could not read image file, using single-color instead." );

QImage dummy( 128, 128, 32 );

( Qt::() );

buf = dummy;

}

tex = QGLWidget::convertToGLFormat( buf );

glGenTextures( 1, &texture[0] );

glBindTexture( GL_TEXTURE_2D, texture[0] );

glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );

glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );

glTexImage2D( GL_TEXTURE_2D, 0, 3, (), (), 0,

GL_RGBA, GL_UNSIGNED_BYTE, () );

}

loadGLTextures()函数就是用来载入纹理的。

贴图纹理的代码和之前教程里的代码是一样的。我们需要一个可以贴在立方体上的纹

理。我决定使用mipmapping处理让纹理看上去光滑,因为我讨厌看见像素点。纹理的文

件名是“”,存放在data目录下。

void NeHeWidget::buildLists()

{

box = glGenLists( 2 );

开始的时候我们告诉OpenGL我们要建立两个显示列表。glGenLists(2)建立了两个显

示列表的空间,并返回第一个显示列表的指针。“box”指向第一个显示列表,任何时候

调用“box”第一个显示列表就会显示出来。

现在开始构造第一个显示列表。我们已经申请了两个显示列表的空间了,并且有box

指针指向第一个显示列表。所以现在我们应该告诉OpenGL要建立什么类型的显示列表。

glNewList( box, GL_COMPILE );

我们用glNewList()命令来做这个事情。你一定注意到了box是第一个参数,这表示

OpenGL将把列表存储到box所指向的内存空间。第二个参数GL_COMPILE告诉OpenGL

我们想预先在内存中构造这个列表,这样每次画的时候就不必重新计算怎么构造物体了。

GL_COMPILE类似于编程。在你写程序的时候,把它装载到编译器里,你每次运行程

序都需要重新编译。而如果它已经编译成了.exe文件,那么每次你只需要点击那个.exe文

件就可以运行它了,不需要编译。当OpenGL编译过显示列表后,就不需要再每次显示的

时候重新编译它了。这就是为什么用显示列表可以加快速度。

你可以在glNewList()和glEngList()中间加上任何你想加上的代码。可以设置颜色,

贴图等等。唯一不能加进去的代码就是会改变显示列表的代码。显示列表一旦建立,你就

不能改变它。

比如你想加上glColor3ub( rand()%255, rand()%255, rand()%255 ),使得每一次画

物体时都会有不同的颜色。但因为显示列表只会建立一次,所以每次画物体的时候颜色都

不会改变。物体将会保持第一次建立显示列表时的颜色。 如果你想改变显示列表的颜色,

你只有在调用显示列表之前改变颜色。后面将详细解释这一点。

glBegin( GL_QUADS );

这部分的代码画出一个没有顶部的盒子,它不会出现在屏幕上,只会存储在显示列表

里。

glNormal3f( 0.0, -1.0, 0.0 );

glTexCoord2f( 1.0, 1.0 ); glVertex3f( -1.0, -1.0, -1.0 );

glTexCoord2f( 0.0, 1.0 ); glVertex3f( 1.0, -1.0, -1.0 );

glTexCoord2f( 0.0, 0.0 ); glVertex3f( 1.0, -1.0, 1.0 );

glTexCoord2f( 1.0, 0.0 ); glVertex3f( -1.0, -1.0, 1.0 );

glNormal3f( 0.0, 0.0, 1.0 );

glTexCoord2f( 0.0, 0.0 ); glVertex3f( -1.0, -1.0, 1.0 );

glTexCoord2f( 1.0, 0.0 ); glVertex3f( 1.0, -1.0, 1.0 );

glTexCoord2f( 1.0, 1.0 ); glVertex3f( 1.0, 1.0, 1.0 );

glTexCoord2f( 0.0, 1.0 ); glVertex3f( -1.0, 1.0, 1.0 );

glNormal3f( 0.0, 0.0, -1.0 );

glTexCoord2f( 1.0, 0.0 ); glVertex3f( -1.0, -1.0, -1.0 );

glTexCoord2f( 1.0, 1.0 ); glVertex3f( -1.0, 1.0, -1.0 );

glTexCoord2f( 0.0, 1.0 ); glVertex3f( 1.0, 1.0, -1.0 );

glTexCoord2f( 0.0, 0.0 ); glVertex3f( 1.0, -1.0, -1.0 );

glNormal3f( 1.0, 0.0, 0.0 );

glTexCoord2f( 1.0, 0.0 ); glVertex3f( 1.0, -1.0, -1.0 );

glTexCoord2f( 1.0, 1.0 ); glVertex3f( 1.0, 1.0, -1.0 );

glTexCoord2f( 0.0, 1.0 ); glVertex3f( 1.0, 1.0, 1.0 );

glTexCoord2f( 0.0, 0.0 ); glVertex3f( 1.0, -1.0, 1.0 );

glNormal3f( -1.0, 0.0, 0.0 );

glTexCoord2f( 0.0, 0.0 ); glVertex3f( -1.0, -1.0, -1.0 );

glTexCoord2f( 1.0, 0.0 ); glVertex3f( -1.0, -1.0, 1.0 );

glTexCoord2f( 1.0, 1.0 ); glVertex3f( -1.0, 1.0, 1.0 );

glTexCoord2f( 0.0, 1.0 ); glVertex3f( -1.0, 1.0, -1.0 );

glEnd();

glEndList();

用glEngList()命令,我们告诉OpenGL我们已经完成了一个显示列表。在glNewList()

和glEngList()之间的任何东西就是显示列表的一部分。

top = box + 1;

现在我们来建立第二个显示列表。在上一个显示列表的指针上加1,就得到了第二个

显示列表的指针。第二个显示列表的指针命名为“top”。

glNewList( top, GL_COMPILE );

glBegin( GL_QUADS );

这部分代码画出盒子的顶部。

glNormal3f( 0.0, 1.0, 0.0 );

glTexCoord2f( 0.0, 1.0 ); glVertex3f( -1.0, 1.0, -1.0 );

glTexCoord2f( 0.0, 0.0 ); glVertex3f( -1.0, 1.0, 1.0 );

glTexCoord2f( 1.0, 0.0 ); glVertex3f( 1.0, 1.0, 1.0 );

glTexCoord2f( 1.0, 1.0 ); glVertex3f( 1.0, 1.0, -1.0 );

glEnd();

glEndList();

然后告诉OpenGL第二个显示列表建立完毕。

}

void NeHeWidget::initializeGL()

{

loadGLTextures();

buildLists();

请注意代码的顺序,先读入纹理,然后建立显示列表,这样当我们建立显示列表的时

候就可以将纹理贴到立方体上了。

glEnable( GL_TEXTURE_2D );

glShadeModel( GL_SMOOTH );

glClearColor( 0.0, 0.0, 0.0, 0.5 );

glClearDepth( 1.0 );

glEnable( GL_DEPTH_TEST );

glDepthFunc( GL_LEQUAL );

glEnable( GL_LIGHT0 );

glEnable( GL_LIGHTING );

glEnable( GL_COLOR_MATERIAL );

上面的三行使灯光有效。Light0一般来说是在显卡中预先定义过的,如果Light0不

工作,把下面那行注释掉好了。

最后一行的GL_COLOR_MATERIAL使我们可以用颜色来贴纹理。如果没有这行代码,

纹理将始终保持原来的颜色,glColor3f( r, g, b )就没有用了。总之这行代码是很有用的。

glHint( GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST );

最后,设置投影校正。

}

现在来看绘画的代码。对数学我从来都是很头大的,没有sin,没有cos,但仍然看起

来很奇怪(相信读者不会觉得头大)。首先,按惯例,清除屏幕和深度缓冲。

然后捆绑纹理到立方体上(我知道捆绑这个词不太专业,但是……)。可以将这行放在

显示列表里,但放在外边,就可以在任何时候修改它。

void NeHeWidget::paintGL()

{

glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

glBindTexture( GL_TEXTURE_2D, texture[0] );

现在到了真正有趣的地方了。用一个循环,循环变量用于改变Y轴位置,在Y轴上画

5个立方体,所以用从1到5的循环。

for ( yLoop = 1; yLoop < 6; yLoop++ )

{

另外用一个循环,循环变量用于改变X轴位置。每行上的立方体数目取决于行数,所

以循环方式如下。

for ( xLoop = 0; xLoop < yLoop; xLoop++ )

{

下边的代码是移动和旋转当前坐标系到需要画出立方体的位置。(原文有很罗嗦的一大

段,相信大家的数学功底都不错,就不翻译了)

glLoadIdentity();

glTranslatef( 1.4 + (float(xLoop) * 2.8) - (float(yLoop) * 1.4),

( (6.0 - (float(yLoop)) ) * 2.4 ) - 7.0, -20.0 );

glRotatef( 45.0 - (2.0 * yLoop) + xRot, 1.0, 0.0, 0.0 );

glRotatef( 45.0 + yRot, 0.0, 1.0, 0.0 );

然后在正式画盒子之前设置颜色。每个盒子用不同的颜色。

glColor3fv( boxcol[yLoop-1] );

好了,颜色设置好了。现在需要做的就是画出盒子。不用写出画多边形的代码,只需

要用glCallList(box)命令调用显示列表。盒子将会用glColor3fv()所设置的颜色画出来。

glCallList( box );

然后用另外的颜色画顶部。搞定。

glColor3fv( topcol[yLoop-1] );

glCallList( top );

}

}

}

void NeHeWidget::keyPressEvent( QKeyEvent *e )

{

switch ( e->key() )

{

case Qt::Key_Up:

xRot -= 0.2;

updateGL();

break;

case Qt::Key_Down:

xRot += 0.2;

updateGL();

break;

case Qt::Key_Left:

yRot -= 0.2;

updateGL();

break;

case Qt::Key_Right:

yRot += 0.2;

updateGL();

break;

case Qt::Key_F2:

fullscreen = !fullscreen;

if ( fullscreen )

{

showFullScreen();

}

else

{

showNormal();

setGeometry( 0, 0, 640, 480 );

}

update();

break;

case Qt::Key_Escape:

close();

}

}

上面就是键盘控制,用上下左右键来控制立方体的运动。

本课程的源代码。

看起来很棒的雾

难道你不想把“雾”加入到你的OpenGL程序中吗?那么在这课里我将要为您展现如

何实现这项功能。这是我第一次写教程,而且相对来说我也是OpenGL/C++程序设计新

手,所以如果您发现有什么错误的话,请让我知道。这课的代码是基于第七课的。

NeHeWidget类

(由nehewidget.h展开。)

class NeHeWidget : public QGLWidget

{

Q_OBJECT

public:

NeHeWidget( QWidget* parent = 0, const char* name = 0, bool fs = false );

~NeHeWidget();

protected:

void initializeGL();

void paintGL();

void resizeGL( int width, int height );

void keyPressEvent( QKeyEvent *e );

void loadGLTextures();

protected:

bool fullscreen;

GLfloat xRot, yRot, zRot;

GLfloat zoom;

GLfloat xSpeed, ySpeed;

GLuint texture[3];

GLuint filter;

bool light;

GLuint fogFilter;

};

变量fogfilter,将被用做记录您所选择的雾的类型的索引。

(由展开。)

GLfloat lightAmbient[4] = { 0.5, 0.5, 0.5, 1.0 };

GLfloat lightDiffuse[4] = { 1.0, 1.0, 1.0, 1.0 };

GLfloat lightPosition[4] = { 0.0, 0.0, 2.0, 1.0 };

GLuint fogMode[3] = { GL_EXP, GL_EXP2, GL_LINEAR };

GLfloat fogColor[4] = { 0.5, 0.5, 0.5, 1.0 };

数据设定

我们将要设定我们用来保存关于雾的信息的所有变量。变量fogMode,用来保存3

种有关雾的类型:GL_EXP,GL_EXP2,GL_LINEAR。稍后我会解释这三种类型间的差别。

这个变量将在代码的开头声明。变量fogColor会保存任何您想要的雾的颜色。

NeHeWidget::NeHeWidget( QWidget* parent, const char* name, bool fs )

: QGLWidget( parent, name )

{

xRot = yRot = zRot = 0.0;

zoom = -5.0;

xSpeed = ySpeed = 0.0;

filter = 0;

light = false;

fogFilter = 0;

fullscreen = fs;

setGeometry( 0, 0, 640, 480 );

setCaption( "Chris Aliotta & NeHe's Fog Tutorial" );

if ( fullscreen )

showFullScreen();

}

我们需要在构造函数中给各个变量赋初值。

void NeHeWidget::loadGLTextures()

{

QImage tex, buf;

if ( !( "./data/" ) )

{

qWarning( "Could not read image file, using single-color instead." );

QImage dummy( 128, 128, 32 );

( Qt::() );

buf = dummy;

}

tex = QGLWidget::convertToGLFormat( buf );

glGenTextures( 3, &texture[0] );

glBindTexture( GL_TEXTURE_2D, texture[0] );

glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );

glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );

glTexImage2D( GL_TEXTURE_2D, 0, 3, (), (), 0,

GL_RGBA, GL_UNSIGNED_BYTE, () );

glBindTexture( GL_TEXTURE_2D, texture[1] );

glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );

glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );

glTexImage2D( GL_TEXTURE_2D, 0, 3, (), (), 0,

GL_RGBA, GL_UNSIGNED_BYTE, () );

glBindTexture( GL_TEXTURE_2D, texture[2] );

glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );

gluBuild2DMipmaps( GL_TEXTURE_2D, GL_RGB, (), (),

GL_RGBA, GL_UNSIGNED_BYTE, () );

}

loadGLTextures()函数就是用来载入纹理的。

void NeHeWidget::initializeGL()

{

loadGLTextures();

glEnable( GL_TEXTURE_2D );

glShadeModel( GL_SMOOTH );

glClearColor( 0.5, 0.5, 0.5, 1.0 );

场景绘制设定

上面我们进行初始化OpenGL了。glClearColor()将会稍做改变以用来将屏幕颜色清

除为雾的颜色以获得更好的视觉效果。这里并没有复杂的代码用来进行雾的操作,而且您

会发现,这将是非常简单的。

glClearDepth( 1.0 );

glEnable( GL_DEPTH_TEST );

glDepthFunc( GL_LEQUAL );

glHint( GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST );

glLightfv( GL_LIGHT1, GL_AMBIENT, lightAmbient );

glLightfv( GL_LIGHT1, GL_DIFFUSE, lightDiffuse );

glLightfv( GL_LIGHT1, GL_POSITION, lightPosition );

glEnable( GL_LIGHT1 );

glFogi( GL_FOG_MODE, fogMode[fogFilter] );

确定了雾的类型。开始的时候我们声明了数组fogMode,它保存了GL_EXP,

GL_EXP2,GL_LINEAR。在我们使用这些值之前,让我稍做解释:

GL_EXP:简单渲染在屏幕上显示的雾的模式。它无法给予我们非常漂亮的雾的效果,

但是却可以在古老的电脑上工作的很好。

GL_EXP2:比1提高了一点,将渲染全屏幕的雾,然而她会给予场景更深的效果。

GL_LINEAR:这是最好的雾的渲染模式,对象在雾中消隐的很好。

glFogfv( GL_FOG_COLOR, fogColor );

设定了雾的颜色,开始的时候我们把它设定为(0.5,0.5,0.5,1.0),使用变量fogcolor

将给予我们漂亮的灰色。

glFogf( GL_FOG_DENSITY, 0.35 );

确定了雾的密度,增大这个数值雾将会变的更浓,减小它雾将会变的更淡。

glHint( GL_FOG_HINT, GL_DONT_CARE );

确定了雾的渲染方式,我使用GL_DONT_CARE是因为我并不关心建议值。然而这里

有一个用来解释关于这个项的不同值之间的区别:

GK_DONT_CARE:让OPENGL自己来确定雾的渲染方式,每顶点或是每像素。

GL_NICEST:对每一像素进行雾的渲染,它看起来是极棒的。

GL_FASTEST:对每一顶点进行雾的渲染,它速度较快,但是不够美丽。

glFogf( GL_FOG_START, 1.0 );

确定了雾的开始初离屏幕有多近。你可以将这个值改变为任意你想要的值,这个值描

述了那个你想要使雾开始的位置。下一行与上行相似,它告诉OpenGL雾能离开屏幕有多

远。

glFogf( GL_FOG_END, 5.0 );

glEnable( GL_FOG );

glEnable(GL_FOG)解释起来非常容易,它告诉OpenGL开始进行雾的计算。

}

void NeHeWidget::paintGL()

{

glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

glLoadIdentity();

glTranslatef( 0.0, 0.0, zoom );

glRotatef( xRot, 1.0, 0.0, 0.0 );

glRotatef( yRot, 0.0, 1.0, 0.0 );

glBindTexture( GL_TEXTURE_2D, texture[filter] );

glBegin( GL_QUADS );

glNormal3f( 0.0, 0.0, 1.0 );

glTexCoord2f( 0.0, 0.0 ); glVertex3f( -1.0, -1.0, 1.0 );

glTexCoord2f( 1.0, 0.0 ); glVertex3f( 1.0, -1.0, 1.0 );

glTexCoord2f( 1.0, 1.0 ); glVertex3f( 1.0, 1.0, 1.0 );

glTexCoord2f( 0.0, 1.0 ); glVertex3f( -1.0, 1.0, 1.0 );

glNormal3f( 0.0, 0.0, -1.0 );

glTexCoord2f( 1.0, 0.0 ); glVertex3f( -1.0, -1.0, -1.0 );

glTexCoord2f( 1.0, 1.0 ); glVertex3f( -1.0, 1.0, -1.0 );

glTexCoord2f( 0.0, 1.0 ); glVertex3f( 1.0, 1.0, -1.0 );

glTexCoord2f( 0.0, 0.0 ); glVertex3f( 1.0, -1.0, -1.0 );

glNormal3f( 0.0, 1.0, 0.0 );

glTexCoord2f( 0.0, 1.0 ); glVertex3f( -1.0, 1.0, -1.0 );

glTexCoord2f( 0.0, 0.0 ); glVertex3f( -1.0, 1.0, 1.0 );

glTexCoord2f( 1.0, 0.0 ); glVertex3f( 1.0, 1.0, 1.0 );

glTexCoord2f( 1.0, 1.0 ); glVertex3f( 1.0, 1.0, -1.0 );

glNormal3f( 0.0, -1.0, 0.0 );

glTexCoord2f( 1.0, 1.0 ); glVertex3f( -1.0, -1.0, -1.0 );

glTexCoord2f( 0.0, 1.0 ); glVertex3f( 1.0, -1.0, -1.0 );

glTexCoord2f( 0.0, 0.0 ); glVertex3f( 1.0, -1.0, 1.0 );

glTexCoord2f( 1.0, 0.0 ); glVertex3f( -1.0, -1.0, 1.0 );

glNormal3f( 1.0, 0.0, 0.0 );

glTexCoord2f( 1.0, 0.0 ); glVertex3f( 1.0, -1.0, -1.0 );

glTexCoord2f( 1.0, 1.0 ); glVertex3f( 1.0, 1.0, -1.0 );

glTexCoord2f( 0.0, 1.0 ); glVertex3f( 1.0, 1.0, 1.0 );

glTexCoord2f( 0.0, 0.0 ); glVertex3f( 1.0, -1.0, 1.0 );

glNormal3f( -1.0, 0.0, 0.0 );

glTexCoord2f( 0.0, 0.0 ); glVertex3f( -1.0, -1.0, -1.0 );

glTexCoord2f( 1.0, 0.0 ); glVertex3f( -1.0, -1.0, 1.0 );

glTexCoord2f( 1.0, 1.0 ); glVertex3f( -1.0, 1.0, 1.0 );

glTexCoord2f( 0.0, 1.0 ); glVertex3f( -1.0, 1.0, -1.0 );

glEnd();

xRot += xSpeed;

yRot += ySpeed;

}

void NeHeWidget::keyPressEvent( QKeyEvent *e )

{

switch ( e->key() )

{

case Qt::Key_L:

light = !light;

if ( !light )

{

glDisable( GL_LIGHTING );

}

else

{

glEnable( GL_LIGHTING );

}

updateGL();

break;

case Qt::Key_F:

filter += 1;;

if ( filter > 2 )

{

filter = 0;

}

updateGL();

break;

case Qt::Key_G:

fogFilter += 1;;

if ( fogFilter > 2 )

{

fogFilter = 0;

}

glFogi( GL_FOG_MODE, fogMode[fogFilter] );

updateGL();

break;

按下了G键,就可以变换一下所使用的雾的类型。

case Qt::Key_Prior:

zoom -= 0.2;

updateGL();

break;

case Qt::Key_Next:

zoom += 0.2;

updateGL();

break;

case Qt::Key_Up:

xSpeed -= 0.01;

updateGL();

break;

case Qt::Key_Down:

xSpeed += 0.01;

updateGL();

break;

case Qt::Key_Right:

ySpeed += 0.01;

updateGL();

break;

case Qt::Key_Left:

ySpeed -= 0.01;

updateGL();

break;

case Qt::Key_F2:

fullscreen = !fullscreen;

if ( fullscreen )

{

showFullScreen();

}

else

{

showNormal();

setGeometry( 0, 0, 640, 480 );

}

update();

break;

case Qt::Key_Escape:

close();

}

}

上面就是键盘控制,用上下左右键来控制立方体的运动。

这就是全部了!我们结束了我们的课程,现在你已经在你的OPENGL程序中有了雾。

我敢说这是决不费事的。

本课程的源代码。


本文标签: 显示 列表 屏幕 纹理 颜色