Qt+OpenGL中文教程

阅读: 评论:0

2024年2月3日发(作者:)

Qt+OpenGL中文教程

Qt OpenGL教程

最近一段时间除了学习Qt,翻译Qt文档之外,由于工作和兴趣的原因,开始着手看Qt OpenGL编程。在网上搜索了有关OpenGL的教程,发现NeHe的OpenGL教程的还很不错,作者是NeHe。上面有很多种语言的实现,但是没有Qt和Gtk的,所以我就想着手写这个Qt OpenGL教程,每课的内容和NeHe是一样的。另外,介绍NeHe的一个中文翻译站点CSDN-CKer翻译的NeHe的OpenGL教程,翻译人是CKer,在我学习这个教程的过程中,给了我很大的帮助。

下面就是Qt OpenGL教程的内容:

Qt OpenGL的准备工作

第一课:创建一个OpenGL窗口

第二课:你的第一个多边形

第三课:上色

第四课:旋转

第五课:向三维进军

第六课:纹理映射

第七课:纹理滤波、光源和键盘控制

第八课:融合

第九课:在三维空间中移动位图

第十课:载入一个三维世界并在其中移动

第十一课:旗的效果(波动纹理)

第十二课:显示列表

第十三课:位图字体

第十四课:轮廓字体

第十五课:使用纹理映射的轮廓字体

第十六课:看起来很棒的雾

因为本教程是从NeHe的OpenGL教程迁移过来的,代码变为Qt实现的。所以有的课程一时还没有实现成功,所以可能有些教程是跳跃的。

因本人时间有限,所以难免有错误出现,如果您发现了这些错误,或者有什么建议,请来信指教,谢谢。

Qt OpenGL的准备工作

因为Qt存在很多版本,另外它支持的平台也很多,到目前为止我只实验了几个组合,所以就先把这些列出来吧,欢迎大家补充。

Unix/X11

Linux

Qt:自由版或者企业版都支持OpenGL模块,而专业版则不能。我现在使用的是3.1.0自由版和企业版。

gcc:编译器。我现在使用的是3.2。

X:Linux下的图形环境。我现在使用的是4.2.0。

Mesa:自由的OpenGL。我现在使用的是5.0。

Windows

Qt:企业版支持OpenGL模块,而专业版则不能。我现在使用的是3.1.0企业版。

Microsoft Visual Studio:编译器。我现在使用的是6.0。

创建一个OpenGL窗口

我假设您对Qt编程已经有了一定的了解,如果您还没有熟悉Qt编程,建议您先学习一下Qt编程的基础知识。

Qt中已经包含了OpenGL模块,具体情况您可以参考Qt OpenGL模块的相关内容。

NeHeWidget类

这就是我们继承QGLWidget类得到的OpenGL窗口部件类。

(由nehewidget.h展开。)

#include

class NeHeWidget : public QGLWidget

{

Q_OBJECT

因为QGLWidget类被包含在qgl.h头文件中,所以我们的类就需要包含这个头文件。Q_OBJECT是Qt中的一个专用的宏,具体说明请参见Qt的文档。

public:

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

~NeHeWidget();

protected:

void initializeGL();

void paintGL();

void resizeGL( int width, int height );

因为QGLWidget类已经内置了对OpenGL的处理,就是通过对initializeGL()、paintGL()和resizeGL()这个三个函数实现的,具体情况可以参考QGLWidget类的文档。

因为我们的这个Qt OpenGL教程取材于NeHe OpenGL教程,所以这里就用这个NeHeWidget类来继承QGLWidget类来使用相关OpenGL的功能。

initializeGL()是用来初始化这个OpenGL窗口部件的,可以在里面设定一些有关选项。paintGL()就是用来绘制OpenGL的窗口了,只要有更新发生,这个函数就会被调用。resizeGL()就是用来处理窗口大小变化这一事件的,width和height就是新的大小状态下的宽和高了,另外resizeGL()在处理完后会自动刷新屏幕。

void keyPressEvent( QKeyEvent *e );

这是Qt里面的鼠标按下事件处理函数。

protected:

bool fullscreen;

用来保存窗口是否处于全屏状态的变量。

};

(由展开。)

#include "nehewidget.h"

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

: QGLWidget( parent, name )

{

fullscreen = fs;

保存窗口是否为全屏的状态。

setGeometry( 0, 0, 640, 480 );

设置窗口的位置,即左上角为(0,0)点,大小为640*480。

setCaption( "NeHe's OpenGL Framework" );

设置窗口的标题为“NeHe's OpenGL Framework”。

if ( fullscreen )

showFullScreen();

如果fullscreen为真,那么就全屏显示这个窗口。

}

这个是构造函数,parent就是父窗口部件的指针,name就是这个窗口部件的名称,fs就是窗口是否最大化。

NeHeWidget::~NeHeWidget()

{

}

这个是析构函数。

void NeHeWidget::initializeGL()

{

glShadeModel( GL_SMOOTH );

这一行启用smooth shading(阴影平滑)。阴影平滑通过多边形精细的混合色彩,并对外部光进行平滑。我将在另一个教程中更详细的解释阴影平滑。

glClearColor( 0.0, 0.0, 0.0, 0.0 );

这一行设置清除屏幕时所用的颜色。如果您对色彩的工作原理不清楚的话,我快速解释一下。色彩值的范围从0.0到1.0。0.0代表最黑的情况,1.0就是最亮的情况。glClearColor后的第一个参数是红色,第二个是绿色,第三个是蓝色。最大值也是1.0,代表特定颜色分量的最亮情况。最后一个参数是Alpha值。当它用来清除屏幕的时候,我们不用关心第四个数字。现在让它为0.0。我会用另一个教程来解释这个参数。

通过混合三种原色(红、绿、蓝),您可以得到不同的色彩。希望您在学校里学过这些。因此,当您使用glClearColor(0.0, 0.0, 1.0, 0.0 ),您将用亮蓝色来清除屏幕。如果您用glClearColor(0.5, 0.0, 0.0, 0.0 )的话,您将使用中红色来清除屏幕。不是最亮(1.0),也不是最暗 (0.0)。要得到白色背景,您应该将所有的颜色设成最亮(1.0)。要黑色背景的话,您该将所有的颜色设为最暗(0.0)。

glClearDepth( 1.0 );

设置深度缓存。

glEnable( GL_DEPTH_TEST );

启用深度测试。

glDepthFunc( GL_LEQUAL );

所作深度测试的类型。

上面这三行必须做的是关于depth buffer(深度缓存)的。将深度缓存设想为屏幕后面的层。深度缓存不断的对物体进入屏幕内部有多深进行跟踪。我们本节的程序其实没有真正使用深度缓存,但几乎所有在屏幕上显示3D场景OpenGL程序都使用深度缓存。它的排序决定那个物体先画。这样您就不会将一个圆形后面的正方形画到圆形上来。深度缓存是OpenGL十分重要的部分。

glHint( GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST );

真正精细的透视修正。这一行告诉OpenGL我们希望进行最好的透视修正。这会十分轻微的影响性能。但使得透视图看起来好一点。

}

这个函数中,我们对OpenGL进行所有的设置。我们设置清除屏幕所用的颜色,打开深度缓存,启用smooth shading(阴影平滑),等等。这个例程直到OpenGL窗口创建之后才会被调用。

void NeHeWidget::paintGL()

{

glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

清楚屏幕和深度缓存。

glLoadIdentity();

重置当前的模型观察矩阵。

}

这个函数中包括了所有的绘图代码。任何您所想在屏幕上显示的东东都将在此段代码中出现。以后的每个教程中我都会在例程的此处增加新的代码。如果您对OpenGL已经有所了解的话,您可以在 glLoadIdentity()调用之后,函数返回之前,试着添加一些OpenGL代码来创建基本的形。如果您是OpenGL新手,等着我的下个教程。目前我们所作的全部就是将屏幕清除成我们前面所决定的颜色,清除深度缓存并且重置场景。我们仍没有绘制任何东东。

void NeHeWidget::resizeGL( int width, int height )

{

if ( height == 0 )

{

height = 1;

}

防止height为0。

glViewport( 0, 0, (GLint)width, (GLint)height );

重置当前的视口(Viewport)。

glMatrixMode( GL_PROJECTION );

选择投影矩阵。

glLoadIdentity();

重置投影矩阵。

gluPerspective( 45.0, (GLfloat)width/(GLfloat)height, 0.1, 100.0 );

建立透视投影矩阵。

glMatrixMode( GL_MODELVIEW );

选择模型观察矩阵。

glLoadIdentity();

重置模型观察矩阵。

}

上面几行为透视图设置屏幕。意味着越远的东西看起来越小。这么做创建了一个现实外观的场景。此处透视按照基于窗口宽度和高度的45度视角来计算。0.1,100.0是我们在场景中所能绘制深度的起点和终点。

glMatrixMode(GL_PROJECTION)指明接下来的两行代码将影响projection matrix(投影矩阵)。投影矩阵负责为我们的场景增加透视。 glLoadIdentity()近似于重置。它将所选的矩阵状态恢复成其原始状态。调用glLoadIdentity()之后我们为场景设置透视图。

glMatrixMode(GL_MODELVIEW)指明任何新的变换将会影响 modelview matrix(模型观察矩阵)。模型观察矩阵中存放了我们的物体讯息。最后我们重置模型观察矩阵。如果您还不能理解这些术语的含义,请别着急。在以后的教程里,我会向大家解释。只要知道如果您想获得一个精彩的透视场景的话,必须这么做。

这个函数的作用是重新设置OpenGL场景的大小,而不管窗口的大小是否已经改变(假定您没有使用全屏模式)。甚至您无法改变窗口的大小时(例如您在全屏模式下),它至少仍将运行一次——在程序开始时设置我们的透视图。OpenGL场景的尺寸将被设置成它显示时所在窗口的大小。

void NeHeWidget::keyPressEvent( QKeyEvent *e )

{

switch ( e->key() )

{

case Qt::Key_F2:

fullscreen = !fullscreen;

if ( fullscreen )

{

showFullScreen();

}

else

{

showNormal();

setGeometry( 0, 0, 640, 480 );

}

updateGL();

break;

如果按下了F2键,那么屏幕是否全屏的状态就切换一次。然后再根据需要,显示所要的全屏窗口或者普通窗口。

case Qt::Key_Escape:

close();

}

如果按下了Escape键,程序退出。

}

(由展开。)

#include

#include

Qt的应用程序都是一个QApplication类,所以qapplication.h必须要包含。因为我们在进入OpenGL窗口之前让用户选择是否使用全屏窗口,所以使用了QMessageBox类,所以qmessagebox.h也要包含。

#include "nehewidget.h"

int main( int argc, char **argv )

{

bool fs = false;

我们把这个布尔型变量的初始值设置为false。

QApplication a(argc,argv);

每一个Qt应用程序都使用QApplication类。

switch( QMessageBox::information( 0,

"Start FullScreen?",

"Would You Like To Run In Fullscreen Mode?",

QMessageBox::Yes,

QMessageBox::No | QMessageBox::Default ) )

{

case QMessageBox::Yes:

fs = true;

break;

case QMessageBox::No:

fs = false;

break;

}

这里弹出一个消息对话框,让用户选择是否使用全屏模式。

NeHeWidget w( 0, 0, fs );

创建一个NeHeWidget对象。

nWidget( &w );

设置应用程序的主窗口部件为w。

();

显示w。

return ();

程序返回。

}

本课程的源代码。

你的第一个多边形

上一课中,我教您如何创建一个OpenGL窗口。这一课中,我将教您如何创建三角形和四边形。我们讲使用GL_TRIANGLES来创建一个三角形,GL_QUADS来创建一个四边形。

我们只要修改第一课中的NeHeWidget类中的paintGL()函数就可以了。

NeHeWidget类

(由展开。)

void NeHeWidget::paintGL()

{

glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

清除屏幕和深度缓存。

GlLoadIdentity ();

重置当前的模型观察矩阵。

当您调用glLoadIdentity()之后,您实际上将当前点移到了屏幕中心,X坐标轴从左至右,Y坐标轴从下至上,Z坐标轴从里至外。OpenGL屏幕中心的坐标值是X和Y轴上的0.0点。中心左面的坐标值是负值,右面是正值。移向屏幕顶端是正值,移向屏幕底端是负值。移入屏幕深处是负值,移出屏幕则是正值。

glTranslatef( -1.5, 0.0, -6.0 );

glTranslatef(x, y, z)沿着 X, Y 和 Z 轴移动。根据前面的次序,下面的代码沿着X轴左移1.5个单位,Y轴不动(0.0),最后移入屏幕6.0个单位。注意在glTranslatef(x, y, z)中当您移动的时候,您并不是相对屏幕中心移动,而是相对与当前所在的屏幕位置。

现在我们已经移到了屏幕的左半部分,并且将视图推入屏幕背后足够的距离以便我们可以看见全部的场景-创建三角形。

glBegin( GL_TRIANGLES );

开始绘制三角形。

glBegin(GL_TRIANGLES)的意思是开始绘制三角形,glEnd() 告诉OpenGL三角形已经创建好了。通常您会需要画3个顶点,可以使用GL_TRIANGLES。在绝大多数的显卡上,绘制三角形是相当快速的。如果要画四个顶点,使用GL_QUADS的话会更方便。但据我所知,绝大多数的显卡都使用三角形来为对象着色。最后,如果您想要画更多的顶点时,可以使用GL_POLYGON。

本节的简单示例中,我们只画一个三角形。如果要画第二个三角形的话,可以在这三点之后,再加三行代码(3点)。所有六点代码都应包含在glBegin(GL_TRIANGLES)和glEnd()之间。在他们之间再不会有多余的点出现,也就是说,(GL_TRIANGLES)和glEnd()之间的点都是以三点为一个集合的。这同样适用于四边形。如果您知道实在绘制四边形的话,您必须在第一个四点之后,再加上四点为一个集合的点组。另一方面,多边形可以由任意个顶点,(GL_POLYGON)不在乎glBegin(GL_TRIANGLES)和glEnd()之间有多少行代码。

glVertex3f( 0.0, 1.0, 0.0 );

上顶点。

glBegin之后的第一行设置了多边形的第一个顶点,glVertex 的第一个参数是X坐标,然后依次是Y坐标和Z坐标。第一个点是上顶点,然后是左下顶点和右下顶点。glEnd()告诉OpenGL没有其他点了。这样将显示一个填充的三角形。

CKer注:这里要注意的是存在两种不同的坐标变换方式,glTranslatef(x, y, z)中的x, y, z是相对与您当前所在点的位移,但glVertex(x,y,z)是相对于glTranslatef(x,

y, z)移动后的新原点的位移。因而这里可以认为glTranslate移动的是坐标原点,glVertex中的点是相对最新的坐标原点的坐标值。

glVertex3f( -1.0, -1.0, 0.0 );

左下顶点。

glVertex3f( 1.0, -1.0, 0.0 );

右下顶点。

glEnd();

三角形绘制结束。

glTranslatef( 3.0, 0.0, 0.0 );

在屏幕的左半部分画完三角形后,我们要移到右半部分来画正方形。为此要再次使用glTranslate。这次右移,所以X坐标值为正值。因为前面左移了1.5个单位,这次要先向右移回屏幕中心(1.5个单位),再向右移动1.5个单位。总共要向右移3.0个单位。

glBegin( GL_QUADS );

开始绘制四边形。

glVertex3f( -1.0, 1.0, 0.0 );

左上顶点。

glVertex3f( 1.0, 1.0, 0.0 );

右上顶点。

glVertex3f( 1.0, -1.0, 0.0 );

右下顶点。

glVertex3f( -1.0, -1.0, 0.0 );

左下顶点。

glEnd();

四边形绘制结束。

}

本课程的源代码。

上色

上一课中我教给您三角形和四边形的绘制方法。这一课我将教您给三角形和四边形添加两种不同类型的着色方法。使用单调着色(Flat coloring)给四边形涂上固定的一种颜色。使用平滑着色(Smooth coloring)将三角形的三个顶点的不同颜色混合在一起,创建漂亮的色彩混合。

我们只要修改第二课中的NeHeWidget类中的paintGL()函数就可以了。

NeHeWidget类

(由展开。)

void NeHeWidget::paintGL()

{

glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

glLoadIdentity();

glTranslatef( -1.5, 0.0, -6.0 );

glBegin( GL_TRIANGLES );

glColor3f( 1.0, 0.0, 0.0 );

红色。

如果您还记得上节课的内容,这段代码在屏幕的左半部分绘制三角形。这一行代码是我们第一次使用命令glColor3f( r, g, b )。括号中的三个参数依次是红、绿、蓝三色分量。取值范围可以从0.0到1.0。类似于以前所讲的清除屏幕背景命令。

我们将颜色设为红色(纯红色,无绿色,无蓝色)。

glVertex3f( 0.0, 1.0, 0.0 );

上顶点。

接下来的一行代码设置三角形的第一个顶点(三角形的上顶点),并使用当前颜色(红色)来绘制。从现在开始所有的绘制的对象的颜色都是红色,直到我们将红色改变成别的什么颜色。

glColor3f( 0.0, 1.0, 0.0 );

绿色。

glVertex3f( -1.0, -1.0, 0.0 );

左下顶点。

glColor3f( 0.0, 0.0, 1.0 );

蓝色。

glVertex3f( 1.0, -1.0, 0.0 );

右下顶点。

glEnd();

glEnd()出现后,三角形将被填充。但是因为每个顶点有不同的颜色,因此看起来颜色从每个角喷出,并刚好在三角形的中心汇合,三种颜色相互混合。这就是平滑着色。

glTranslatef( 3.0, 0.0, 0.0 );

glColor3f( 0.5, 0.5, 1.0 );

一次性将颜色设置为蓝色。

现在我们绘制一个单调着色——蓝色的正方形。最重要的是要记住,设置当前色之后绘制的所有东东都是当前色的。以后您所创建的每个工程都要使用颜色。即便是在完全采用纹理贴图的时候,glColor3f仍旧可以用来调节纹理的色调。等等...,以后再说吧。

我们必须要做的事只需将颜色一次性的设为我们想采用的颜色(本例采用蓝色),然后绘制场景。每个顶点都是蓝色的,因为我们没有告诉OpenGL要改变顶点的颜色。最后的结果是.....全蓝色的正方形。再说一遍,顺时针绘制的正方形意味着我们所看见的是四边形的背面。

glBegin( GL_QUADS );

glVertex3f( -1.0, 1.0, 0.0 );

glVertex3f( 1.0, 1.0, 0.0 );

glVertex3f( 1.0, -1.0, 0.0 );

glVertex3f( -1.0, -1.0, 0.0 );

glEnd();

}

在这一课中,我试着尽量详细的解释如何为您的OpenGL多边形添加单调和平滑的着色效果的步骤。改改代码中的红绿蓝分量值,看看最后有什么样的结果。

本课程的源代码。

旋转

上一课中我教给您三角形和四边形的着色。这一课我将教您如何将这些彩色对象绕着坐标轴旋转。

其实只需在上节课的代码上增加几行就可以了。

我们将在NeHeWidget类中增加两个变量来控制这两个对象的旋转。它们是浮点类型的变量,使得我们能够非常精确地旋转对象。浮点数包含小数位置,这意味着我们无需使用1、2、3...的角度。你会发现浮点数是OpenGL编程的基础。新变量中叫做rTri的用来旋转三角形,rQuad 旋转四边形。

NeHeWidget类

(由nehewidget.h展开。)

protected:

bool fullscreen;

GLfloat rTri;

GLfloat rQuad;

};

上面就是添加的两个变量。rTri是用于三角形的角度,rQuad是用于四边形的角度。

(由展开。)

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

: QGLWidget( parent, name )

{

rTri = 0.0;

rQuad = 0.0;

fullscreen = fs;

setGeometry( 0, 0, 640, 480 );

setCaption( "NeHe's Rotation Tutorial" );

if ( fullscreen )

showFullScreen();

}

我们需要在构造函数中给rTri和rQuad赋初值,都是0.0。

void NeHeWidget::paintGL()

{

glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

glLoadIdentity();

glTranslatef( -1.5, 0.0, -6.0 );

glRotatef( rTri, 0.0, 1.0, 0.0 );

glRotatef( Angle, Xvector, Yvector, Zvector )负责让对象绕某个轴旋转。这个函数有很多用处。 Angle 通常是个变量代表对象转过的角度。Xvector,Yvector和Zvector三个参数则共同决定旋转轴的方向。比如( 1, 0, 0 )所描述的矢量经过X坐标轴的1个单位处并且方向向右。( -1, 0, 0 )所描述的矢量经过X坐标轴的1个单位处,但方向向左。

D. Michael Traub:提供了对 Xvector , Yvector 和 Zvector 的上述解释。

为了更好的理解X, Y 和 Z的旋转,我举些例子...

X轴-您正在使用一台台锯。锯片中心的轴从左至右摆放(就像OpenGL中的X轴)。尖利的锯齿绕着X轴狂转,看起来要么向上转,要么向下转。取决于锯片开始转时的方向。这与我们在OpenGL中绕着X轴旋转什么的情形是一样的。(CKer注:这会儿您要把脸蛋凑向显示器的话,保准被锯开了花 ^-^。)

Y轴-假设您正处于一个巨大的龙卷风中心,龙卷风的中心从地面指向天空(就像OpenGL中的Y轴)。垃圾和碎片围着Y轴从左向右或是从右向左狂转不止。这与我们在OpenGL中绕着Y轴旋转什么的情形是一样的。

Z轴-您从正前方看着一台风扇。风扇的中心正好朝着您(就像OpenGL中的Z轴)。风扇的叶片绕着Z轴顺时针或逆时针狂转。这与我们在OpenGL中绕着Z轴旋转什么的情形是一样的。

上面的一行代码中,如果rtri等于7,我们将三角形绕着Y轴从左向右旋转7 。您也可以改变参数的值,让三角形绕着X和Y轴同时旋转。

glBegin( GL_TRIANGLES );

glColor3f( 1.0, 0.0, 0.0 );

glVertex3f( 0.0, 1.0, 0.0 );

glColor3f( 0.0, 1.0, 0.0 );

glVertex3f( -1.0, -1.0, 0.0 );

glColor3f( 0.0, 0.0, 1.0 );

glVertex3f( 1.0, -1.0, 0.0 );

glEnd();

上面的绘制三角形的代码没有改变。在屏幕的左面画了一个彩色渐变三角形,并绕着Y轴从左向右旋转。

glLoadIdentity();

我们增加了另一个glLoadIdentity()调用。目的是为了重置模型观察矩阵。如果我们没有重置,直接调用glTranslate的话,会出现意料之外的结果。因为坐标轴已经旋转了,很可能没有朝着您所希望的方向。所以我们本来想要左右移动对象的,就可能变成上下移动了,取决于您将坐标轴旋转了多少角度。试试将glLoadIdentity() 注释掉之后,会出现什么结果。

重置模型观察矩阵之后,X、Y、Z轴都以复位,我们调用glTranslate。您会注意到这次我们只向右移了1.5单位,而不是上节课的3.0单位。因为我们重置场景的时候,焦点又回到了场景的中心(0.0)处。这样就只需向右移1.5单位就够了。

当我们移到新位置后,绕X轴旋转四边形。正方形将上下转动。

glTranslatef( 1.5, 0.0, -6.0 );

glRotatef( rQuad, 1.0, 0.0, 0.0 );

绕X轴旋转四边形。

glColor3f( 0.5, 0.5, 1.0 );

glBegin( GL_QUADS );

glVertex3f( -1.0, 1.0, 0.0 );

glVertex3f( 1.0, 1.0, 0.0 );

glVertex3f( 1.0, -1.0, 0.0 );

glVertex3f( -1.0, -1.0, 0.0 );

glEnd();

rTri += 0.2;

rQuad -= 0.15;

我们在构造函数中已经将rTri和rQuad的值设为0.0,在这里我们每绘制完一次图像,就修改一下这两个变量。两个变量的变化会使对象的旋转角度发生变化。

尝试改变下面代码中的+和-,来体会对象旋转的方向是如何改变的。并试着将0.2改成1.0。这个数字越大,物体就转的越快,这个数字越小,物体转的就越慢。

}

在这一课中,我试着尽量详细的解释如何让对象绕某个轴转动。改改代码,试着让对象绕着Z轴、X+Y轴或者所有三个轴来转动:)。

本课程的源代码。

向三维进军

在上一课的内容上作些扩展,我们现在开始生成真正的三维对象,而不是象前两节课中那样在三维世界中的二维对象。我们给三角形增加一个左侧面,一个右侧面,一个后侧面来生成一个金字塔(四棱锥)。给正方形增加左、右、上、下及背面生成一个立方体。

我们混合金字塔上的颜色,创建一个平滑着色的对象。给立方体的每一面则来个不同的颜色。

其实只需在上节课的代码上增加几行就可以了。

NeHeWidget类

(由展开。)

void NeHeWidget::paintGL()

{

glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

glLoadIdentity();

glTranslatef( -1.5, 0.0, -6.0 );

glRotatef( rTri, 0.0, 1.0, 0.0 );

有些人可能早已在上节课中的代码上尝试自行创建3D对象了。但经常有人来信问我:“我的对象怎么不会绕着其自身的轴旋转?看起来总是在满屏乱转。”要让您的对象绕自身的轴旋转,您必须让对象的中心坐标总是( 0.0, 0,0, 0,0 )。

下面的代码创建一个绕者其中心轴旋转的金字塔。金字塔的上顶点离中心一个单位,底面离中心也是一个单位。上顶点在底面的投影位于底面的中心。

注意所有的面-三角形都是逆时针次序绘制的。这点十分重要,在以后的课程中我会作出解释。现在,您只需明白要么都逆时针,要么都顺时针,但永远不要将两种次序混在一起,除非您有足够的理由必须这么做。

glBegin( GL_TRIANGLES );

glColor3f( 1.0, 0.0, 0.0 );

glVertex3f( 0.0, 1.0, 0.0 );

glColor3f( 0.0, 1.0, 0.0 );

glVertex3f( -1.0, -1.0, 1.0 );

glColor3f( 0.0, 0.0, 1.0 );

glVertex3f( 1.0, -1.0, 1.0 );

上面是我们绘制的金字塔的前侧面。因为所有的面都共享上顶点,我们将这点在所有的三角形中都设置为红色。底边上的两个顶点的颜色则是互斥的。前侧面的左下顶点是绿色的,右下顶点是蓝色的。这样相邻右侧面的左下顶点是蓝色的,右下顶点是绿色的。这样四边形的底面上的点的颜色都是间隔排列的。

还应注意到后面的三个侧面和前侧面处于同一个glBegin( GL_TRIANGLES )和glEnd()语句中间。因为我们是通过三角形来构造这个金字塔的。OpenGL知道每三个点构成一个三角形。当它画完一个三角形之后,如果还有余下的点出现,它就以为新的三角形要开始绘制了。OpenGL在这里并不会将四点画成一个四边形,而是假定新的三角形开始了。所以千万不要无意中增加任何多余的点。

glColor3f( 1.0, 0.0, 0.0 );

glVertex3f( 0.0, 1.0, 0.0 );

glColor3f( 0.0, 0.0, 1.0 );

glVertex3f( 1.0, -1.0, 1.0 );

glColor3f( 0.0, 1.0, 0.0 );

glVertex3f( 1.0, -1.0, -1.0 );

绘制右侧面。注意其底边上的两个顶点的X坐标位于中心右侧的一个单位处。顶点则位于Y轴上的一单位处,且Z坐标正好处于底边的两顶点的Z坐标中心。右侧面从上顶点开始向外侧倾斜至底边上。

这次的左下顶点用蓝色绘制,以保持与前侧面的右下顶点的一致。蓝色将从这个角向金字塔的前侧面和右侧面扩展并与其他颜色混合。

glColor3f( 1.0, 0.0, 0.0 );

glVertex3f( 0.0, 1.0, 0.0 );

glColor3f( 0.0, 1.0, 0.0 );

glVertex3f( 1.0, -1.0, -1.0 );

glColor3f( 0.0, 0.0, 1.0 );

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

后侧面。再次切换颜色。左下顶点又回到绿色,因为后侧面与右侧面共享这个角。

glColor3f( 1.0, 0.0, 0.0 );

glVertex3f( 0.0, 1.0, 0.0 );

glColor3f( 0.0, 0.0, 1.0 );

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

glColor3f( 0.0, 1.0, 0.0 );

glVertex3f( -1.0, -1.0, 1.0 );

最后画左侧面。又要切换颜色。左下顶点是蓝色,与后侧面的右下顶点相同。右下顶点是蓝色,与前侧面的左下顶点相同。

到这里金字塔就画完了。因为金字塔只绕着Y轴旋转,我们永远都看不见底面,因而没有必要添加底面。如果您觉得有经验了,尝试增加底面(正方形),并将金字塔绕X轴旋转来看看您是否作对了。确保底面四个顶点的颜色与侧面的颜色相匹配。

glEnd();

接下来开始画立方体。他由六个四边形组成。所有的四边形都以逆时针次序绘制。就是说先画右上角,然后左上角、左下角、最后右下角。您也许认为画立方体的背面的时候这个次序看起来好像顺时针,但别忘了我们从立方体的背后看背面的时候,与您现在所想的正好相反。(译者注:您是从立方体的外面来观察立方体的。)

glLoadIdentity();

glTranslatef( 1.5, 0.0, -7.0 );

注意到这次我们将立方体移地更远离屏幕了。因为立方体的大小要比金字塔大,同样移入6个单位时,立方体看起来要大的多。这是透视的缘故。越远的对象看起来越小 :) 。

glRotatef( rQuad, 1.0, 1.0, 1.0 );

glBegin( GL_QUADS );

glColor3f( 0.0, 1.0, 0.0 );

glVertex3f( 1.0, 1.0, -1.0 );

glVertex3f( -1.0, 1.0, -1.0 );

glVertex3f( -1.0, 1.0, 1.0 );

glVertex3f( 1.0, 1.0, 1.0 );

先画立方体的顶面。从中心上移一单位,注意Y坐标始终为一单位,表示这个四边形与Z轴平行。先画右上顶点,向右一单位,再屏幕向里一单位。然后左上顶点,向左一单位,再屏幕向里一单位。然后是靠近观察者的左下和右下顶点。就是屏幕往外一单位。

glColor3f( 1.0, 0.5, 0.0 );

glVertex3f( 1.0, -1.0, 1.0 );

glVertex3f( -1.0, -1.0, 1.0 );

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

glVertex3f( 1.0, -1.0, -1.0 );

底面的画法和顶面十分类似。只是Y坐标变成了-1。如果我们从立方体的下面来看立方体的话,您会注意到右上角离观察者最近,因此我们先画离观察者最近的顶点。然后是左上顶点最后才是屏幕里面的左下和右下顶点。

如果您真的不在乎绘制多边形的次序(顺时针或者逆时针)的话,您可以直接拷贝顶面的代码,将Y坐标从1改成-1,也能够工作。但一旦您进入象纹理映射这样的领域时,忽略绘制次序会导致十分怪异的结果。

glColor3f( 1.0, 0.0, 0.0 );

glVertex3f( 1.0, 1.0, 1.0 );

glVertex3f( -1.0, 1.0, 1.0 );

glVertex3f( -1.0, -1.0, 1.0 );

glVertex3f( 1.0, -1.0, 1.0 );

立方体的前面。保持Z坐标为一单位,前面正对着我们。

glColor3f( 1.0, 1.0, 0.0 );

glVertex3f( 1.0, -1.0, -1.0 );

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

glVertex3f( -1.0, 1.0, -1.0 );

glVertex3f( 1.0, 1.0, -1.0 );

立方体后面的绘制方法与前面类似。只是位于屏幕的里面。注意Z坐标现在保持-1不变。

glColor3f( 0.0, 0.0, 1.0 );

glVertex3f( -1.0, 1.0, 1.0 );

glVertex3f( -1.0, 1.0, -1.0 );

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

glVertex3f( -1.0, -1.0, 1.0 );

还剩两个面就完成了。您会注意到总有一个坐标保持不变。这一次换成了X坐标。因为我们在画左侧面。

glColor3f( 1.0, 0.0, 1.0 );

glVertex3f( 1.0, 1.0, -1.0 );

glVertex3f( 1.0, 1.0, 1.0 );

glVertex3f( 1.0, -1.0, 1.0 );

glVertex3f( 1.0, -1.0, -1.0 );

立方体的最后一个面了。X坐标保持为一单位。逆时针绘制。您愿意的话,留着这个面不画也可以,这样就是一个盒子:)

glEnd();

或者您要是有兴趣可以改变立方体所有顶点的色彩值,象金字塔那样混合颜色。您会看见一个非常漂亮的彩色立方体,各种颜色在它的各个表面流淌。

rTri += 0.2;

rQuad -= 0.15;

}

这一课又结束了。到这里您应该已经较好的掌握了在三维空间创建对象的方法。必须将OpenGL屏幕想象成一张很大的画纸,后面还带着许多透明的层。差不多就是个由大量的点组成的立方体。这些点从左至右、从上至下、从前到后的布满了这个立方体。如果您能想象的出在屏幕的深度方向,应该在设计新三维对象时没有任何问题。

如果您对三维空间的理解很困难的话,千万不要灰心!刚开始的时候,领会这些内容会很难。象立方体这样的对象是您练习的好例子。继续努力吧!

本课程的源代码。

纹理映射

学习texture map纹理映射(贴图)有很多好处。比方说您想让一颗导弹飞过屏幕。根据前几课的知识,我们最可行的办法可能是很多个多边形来构建导弹的轮廓并加上有趣的颜色。使用纹理映射,您可以使用真实的导弹图像并让它飞过屏幕。您觉得哪个更好看?照片还是一大堆三角形和四边形?使用纹理映射的好处还不止是更好看,而且您的程序运行会更快。导弹贴图可能只是一个飞过窗口的四边形。一个由多边形构建而来的导弹却很可能包括成百上千的多边形。很显然,贴图极大的节省了CPU时间。

我们要在第一课的代码上增加几行就可以了。

我们将要增加一个loadGLTextures()函数来处理有关纹理操作的。我们将在NeHeWidget类中增加三个变量xRot、yRot、zRot来处理立方体的旋转。还有一个用来存储纹理的texture[1]。

NeHeWidget类

(由nehewidget.h展开。)

protected:

void loadGLTextures();

在这个函数中我们会载入指定的图片并生成相应当纹理。

protected:

bool fullscreen;

GLfloat xRot, yRot, zRot;

GLuint texture[1];

};

上面就是添加的三个变量xRot、yRot、zRot来处理立方体在三个方向上的旋转。texture[1]用来存储一个纹理。

(由展开。)

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

: QGLWidget( parent, name )

{

xRot = yRot = zRot = 0.0;

fullscreen = fs;

setGeometry( 0, 0, 640, 480 );

setCaption( "NeHe's Texture Mapping Tutorial" );

if ( fullscreen )

showFullScreen();

}

我们需要在构造函数中给xRot、yRot、zRot赋初值,都是0.0。

void NeHeWidget::loadGLTextures()

{

QImage tex, buf;

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

载入纹理图片。这里使用了QImage类。

{

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

QImage dummy( 128, 128, 32 );

( Qt::() );

buf = dummy;

如果载入不成功,自动生成一个128*128的32位色的绿色图片。

}

tex = QGLWidget::convertToGLFormat( buf );

这里使用了QGLWidget类中提供的一个静态函数converToGLFormat(),专门用来转换图片的,具体情况请参见相应文档。

glGenTextures( 1, &texture[0] );

创建一个纹理。告诉OpenGL我们想生成一个纹理名字(如果您想载入多个纹理,加大数字)。值得注意的是,开始我们使用 GLuint texture[1] 来创建一个纹理的存储空间,您也许会认为第一个纹理就是存放在 &texture[1] 中的,但这是错的。正确的地址应该是 &texture[0] 。同样如果使用 GLuint texture[2] 的话,第二个纹理存放在 texture[1] 中。

glBindTexture( GL_TEXTURE_2D, texture[0] );

使用来自位图数据生成的典型纹理。告诉OpenGL将纹理名字texture[0]绑定到纹理目标上。2D纹理只有高度(在Y轴上)和宽度(在X轴上)。主函数将纹理名字指派给纹理数据。本例中我们告知OpenGL,&texture[0]处的内存已经可用。我们创建的纹理将存储在&texture[0]的指向的内存区域。

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

GL_RGBA, GL_UNSIGNED_BYTE, () );

这里真正的创建纹理。GL_TEXTURE_2D告诉OpenGL此纹理是一个2D纹理。数字零代表图像的详细程度,通常就由它为零去了。数字三是数据的成分数。因为图像是由红色数据,绿色数据,蓝色数据三种组分组成。 ()是纹理的宽度。()是纹理的高度。数字零是边框的值,一般就是零。GL_RGBA 告诉OpenGL图像数据由红、绿、蓝三色数据以及alpha通道数据组成,这个是由于QGLWidget类的converToGLFormat()函数的原因。 GL_UNSIGNED_BYTE

意味着组成图像的数据是无符号字节类型的。最后()告诉OpenGL纹理数据的来源。

glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );

glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );

上面的两行告诉OpenGL在显示图像时,当它比放大得原始的纹理大(GL_TEXTURE_MAG_FILTER)或缩小得比原始得纹理小(GL_TEXTURE_MIN_FILTER)时OpenGL采用的滤波方式。通常这两种情况下我都采用GL_LINEAR。这使得纹理从很远处到离屏幕很近时都平滑显示。使用GL_LINEAR需要CPU和显卡做更多的运算。如果您的机器很慢,您也许应该采用GL_NEAREST。过滤的纹理在放大的时候,看起来斑驳的很。您也可以结合这两种滤波方式。在近处时使用GL_LINEAR,远处时GL_NEAREST。

}

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

void NeHeWidget::paintGL()

{

glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

glLoadIdentity();

glTranslatef( 0.0, 0.0, -5.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 );

根据xRot、yRot、zRot的实际值来旋转正方体。

glBindTexture( GL_TEXTURE_2D, texture[0] );

选择我们使用的纹理。如果您在您的场景中使用多个纹理,您应该使用来glBindTexture(GL_TEXTURE_2D, texture[所使用纹理对应的数字]) 选择要绑定的纹理。当您想改变纹理时,应该绑定新的纹理。有一点值得指出的是,您不能在glBegin()和glEnd()之间绑定纹理,必须在glBegin()之前或glEnd()之后绑定。注意我们在上面是如何使用glBindTexture来指定和绑定纹理的。

glBegin( GL_QUADS );

为了将纹理正确的映射到四边形上,您必须将纹理的右上角映射到四边形的右上角,纹理的左上角映射到四边形的左上角,纹理的右下角映射到四边形的右下角,纹理的左下角映射到四边形的左下角。如果映射错误的话,图像显示时可能上下颠倒,侧向一边或者什么都不是。

glTexCoord2f的第一个参数是X坐标。0.0是纹理的左侧。0.5是纹理的中点,1.0是纹理的右侧。glTexCoord2f的第二个参数是Y坐标。0.0是纹理的底部。0.5是纹理的中点,1.0是纹理的顶部。

所以纹理的左上坐标是X:0.0,Y:1.0f,四边形的左上顶点是X:-1.0,Y:1.0。其余三点依此类推。

试着玩玩glTexCoord2f的X、Y坐标参数。把1.0改为0.5将只显示纹理的左半部分,把0.0改为0.5将只显示纹理的右半部分。

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 );

前面。

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 );

后面。

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 );

顶面。

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 );

底面。

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 );

右面。

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 += 0.3;

yRot += 0.2;

zRot += 0.4;

现在改变xRot、yRot、zRot的值。尝试变化每次各变量的改变值来调节立方体的旋转速度,或改变+/-号来调节立方体的旋转方向。

}

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 );

}

现在您应该比较好的理解纹理映射(贴图)了。您应该掌握了给任意四边形表面贴上您所喜爱的图像的技术。一旦您对2D纹理映射的理解感到自信的时候,试试给立方体的六个面贴上不同的纹理。

当您理解纹理坐标的概念后,纹理映射并不难理解。

本课程的源代码。

纹理滤波、光源和键盘控制

这一课我会教您如何使用三种不同的纹理滤波方式。教您如何使用键盘来移动场景中的对象,还会教您在OpenGL场景中应用简单的光照。这一课包含了很多内容,如果您对前面的课程有疑问的话,先回头复习一下。进入后面的代码之前,很好的理解基础知识十分重要。

我们要在第一课的代码上进行改动就可以了。

我们将要增加一个loadGLTextures()函数来处理有关纹理操作的。我们将增加一些变量,稍后我们对这些变量进行解释。

NeHeWidget类

(由nehewidget.h展开。)

protected:

void loadGLTextures();

在这个函数中我们会载入指定的图片并生成相应当纹理。

protected:

bool fullscreen;

GLfloat xRot, yRot, zRot;

GLfloat zoom;

GLfloat xSpeed, ySpeed;

GLuint texture[3];

GLuint filter;

bool light;

};

上面就是添加的三个变量xRot、yRot、zRot来处理立方体在三个方向上的旋转。zoom是场景深入屏幕的距离。xSpeed和ySpeed是立方体在X轴和Y轴上旋转的速度。texture[3]用来存储三个纹理。filter表明的是使用哪个纹理。light是说明现在是否使用光源。

(由展开。)

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 };

这里定义了三个数组,它们描述的是和光源有关的信息。

我们将使用两种不同的光。第一种称为环境光。环境光来自于四面八方。所有场景中的对象都处于环境光的照射中。第二种类型的光源叫做漫射光。漫射光由特定的光源产生,并在您的场景中的对象表面上产生反射。处于漫射光直接照射下的任何对象表面都变得很亮,而几乎未被照射到的区域就显得要暗一些。这样在我们所创建的木板箱的棱边上就会产生的很不错的阴影效果。

创建光源的过程和颜色的创建完全一致。前三个参数分别是RGB三色分量,最后一个是alpha通道参数。

因此,第一行有关lightAmbient的代码使我们得到的是半亮(0.5)的白色环境光。如果没有环境光,未被漫射光照到的地方会变得十分黑暗。

第二行有关lightDiffuse的代码使我们生成最亮的漫射光。所有的参数值都取成最大值1.0。它将照在我们木板箱的前面,看起来挺好。

第三行有关lightPosition的代码使我们保存光源的位置。前三个参数和glTranslate中的一样。依次分别是XYZ轴上的位移。由于我们想要光线直接照射在木箱的

正面,所以XY轴上的位移都是0.0。第三个值是Z轴上的位移。为了保证光线总在木箱的前面,所以我们将光源的位置朝着观察者(就是您哪。)挪出屏幕。我们通常将屏幕也就是显示器的屏幕玻璃所处的位置称作Z轴的0.0点。所以Z轴上的位移最后定为2.0。假如您能够看见光源的话,它就浮在您显示器的前方。当然,如果木箱不在显示器的屏幕玻璃后面的话,您也无法看见箱子。最后一个参数取为1.0。这将告诉OpenGL这里指定的坐标就是光源的位置,以后的教程中我会多加解释。

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;

fullscreen = fs;

setGeometry( 0, 0, 640, 480 );

setCaption( "NeHe's Texture, Lighting & Keyboard Tutorial" );

if ( fullscreen )

showFullScreen();

}

我们需要在构造函数中给各个变量赋初值。xRot、yRot、zRot是0.0。zoom是-5.0。xSpeed和ySpeed都是0。filter是0。light是false。

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] );

这一部分,上一章讲过了。我们这里创建了3个纹理。

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, () );

第六课中我们使用了线性滤波的纹理贴图。这需要机器有相当高的处理能力,但它们看起来很不错。这一课中,我们接着要创建的第一种纹理使用GL_NEAREST方式。从原理上讲,这种方式没有真正进行滤波。它只占用很小的处理能力,看起来也很差。唯一的好处是这样我们的工程在很快和很慢的机器上都可以正常运行。您会注意到我们在MIN和MAG时都采用了GL_NEAREST,你可以混合使用GL_NEAREST和GL_LINEAR。纹理看起来效果会好些,但我们更关心速度,所以全采用低质量贴图。MIN_FILTER在图像绘制时小于贴图的原始尺寸时采用。MAG_FILTER在图像绘制时大于贴图的原始尺寸时采用。

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, () );

这个纹理与第六课的相同,线性滤波。唯一的不同是这次放在了texture[1]中。因为这是第二个纹理。如果放在texture[0]中的话,它将覆盖前面创建的GL_NEAREST纹理。

glBindTexture( GL_TEXTURE_2D, texture[2] );

glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );

glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,

GL_LINEAR_MIPMAP_NEAREST );

这里是创建纹理的新方法。Mipmapping!您可能会注意到当图像在屏幕上变得很小的时候,很多细节将会丢失。刚才还很不错的图案变得很难看。当您告诉OpenGL创建一个 mipmapped的纹理后,OpenGL将尝试创建不同尺寸的高质量纹理。当您向屏幕绘制一个mipmapped纹理的时候,OpenGL将选择它已经创建的外观最佳的纹理(带有更多细节)来绘制,而不仅仅是缩放原先的图像(这将导致细节丢失)。

我曾经说过有办法可以绕过OpenGL对纹理宽度和高度所加的限制——64、128、256,等等。办法就是gluBuild2DMipmaps。据我的发现,您可以使用任意的位图来创建纹理。OpenGL将自动将它缩放到正常的大小。

因为是第三个纹理,我们将它存到texture[2]。这样本课中的三个纹理全都创建好了。 gluBuild2DMipmaps( GL_TEXTURE_2D, GL_RGB, (),

(), GL_RGBA, GL_UNSIGNED_BYTE, () );

这一行生成 mipmapped 纹理。我们使用三种颜色(红,绿,蓝)来生成一个2D纹理。()是位图宽度,()是位图高度,extureImage[0]->sizeY 是位图高度,GL_RGBA意味着我们依次使用RGBA色彩。GL_UNSIGNED_BYTE意味着纹理数据的单位是字节。()指向我们创建纹理所用的位图。

}

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

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] );

根据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;

将xRot和yRot的旋转值分别增加xSpeed和ySpeed个单位。xSpeed和ySpeed的值越大,立方体转得就越快。

}

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 );

glLightfv( GL_LIGHT1, GL_AMBIENT, lightAmbient );

glLightfv( GL_LIGHT1, GL_DIFFUSE, lightDiffuse );

glLightfv( GL_LIGHT1, GL_POSITION, lightPosition );

glEnable( GL_LIGHT1 );

这里开始设置光源。第一行设置环境光的发光量,光源GL_LIGHT1开始发光。这一课的开始处我们我们将环境光的发光量存放在lightAmbient数组中。现在我们就使用此数组(半亮度环境光)。

接下来我们设置漫射光的发光量。它存放在lightDiffuse数组中(全亮度白光)。

然后设置光源的位置。位置存放在lightPosition 数组中(正好位于木箱前面的中心,X-0.0,Y-0.0,Z方向移向观察者2个单位,位于屏幕外面)。

最后,我们启用一号光源。我们还没有启用GL_LIGHTING,所以您看不见任何光线。记住:只对光源进行设置、定位、甚至启用,光源都不会工作。除非我们启用GL_LIGHTING。

}

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;

按下了L键,就可以切换是否打开光源。

case Qt::Key_F:

filter += 1;;

if ( filter > 2 )

{

filter = 0;

}

updateGL();

break;

按下了F键,就可以转换一下所使用的纹理(就是变换了纹理滤波方式的纹理)。

case Qt::Key_Prior:

zoom -= 0.2;

updateGL();

break;

按下了PageUp键,将木箱移向屏幕内部。

case Qt::Key_Next:

zoom += 0.2;

updateGL();

break;

按下了PageDown键,将木箱移向屏幕外部。

case Qt::Key_Up:

xSpeed -= 0.01;

updateGL();

break;

按下了Up方向键,减少xSpeed。

case Qt::Key_Down:

xSpeed += 0.01;

updateGL();

break;

按下了Dowm方向键,增加xSpeed。

case Qt::Key_Right:

ySpeed += 0.01;

updateGL();

break;

按下了Right方向键,增加ySpeed。

case Qt::Key_Left:

ySpeed -= 0.01;

updateGL();

break;

按下了Left方向键,减少ySpeed。

case Qt::Key_F2:

fullscreen = !fullscreen;

if ( fullscreen )

{

showFullScreen();

}

else

{

showNormal();

setGeometry( 0, 0, 640, 480 );

}

update();

break;

case Qt::Key_Escape:

close();

}

}

这一课完了之后,您应该学会创建和使用这三种不同的纹理映射过滤方式。并使用键盘和场景中的对象交互。最后,您应该学会在场景中应用简单的光源,使得场景看起来更逼真。

本课程的源代码。

融合

OpenGL中的绝大多数特效都与某些类型的(色彩)融合有关。融合的定义为,将某个象素的颜色和已绘制在屏幕上与其对应的象素颜色相互结合。至于如何结合这两个颜色则依赖于颜色的alpha通道的分量值,以及/或者所使用的融合函数。alpha通常是位于颜色值末尾的第4个颜色组成分量。前面这些课我们都是用GL_RGB来指定颜色的三个分量。相应的GL_RGBA可以指定alpha分量的值。更进一步,我们可以使用glColor4f()来代替glColor3f()。

绝大多数人都认为alpha分量代表材料的透明度。这就是说,alpha值为0.0时所代表的材料是完全透明的。alpha值为1.0时所代表的材料则是完全不透明的。

融合的公式

若您对数学不感冒,而只想看看如何实现透明,请跳过这一节。若您想深入理解(色彩)融合的工作原理,这一节应该适合您吧。(CKER注:其实并不难^-^。原文中的公式如下,CKER再唠叨一下吧。其实融合的基本原理是就将要分色的图像各象素的颜色以及背景颜色均按照RGB规则各自分离之后,根据-图像的RGB颜色分量*alpha值+背景的RGB颜色分量*(1-alpha值)——这样一个简单公式来融合之后,最后将融合得到的RGB分量重新合并。)

公式如下:

(Rs Sr + Rd Dr, Gs Sg + Gd Dg, Bs Sb + Bd Db, As Sa + Ad Da)

OpenGL按照上面的公式计算这两个象素的融合结果。小写的s和r分别代表源象素和目标象素。大写的S和D则是相应的融合因子。这些决定了您如何对这些象素融合。绝大多数情况下,各颜色通道的alpha融合值大小相同,这样对源象素就有 (As, As, As, As),目标象素则有1, 1, 1, 1) - (As, As, As, As)。上面的公式就成了下面的模样:

(Rs As + Rd (1 - As), Gs As + Gd (1 - As), Bs As + Bs (1 - As), As As + Ad (1 - As))

这个公式会生成透明/半透明的效果。

OpenGL中的融合

在OpenGL中实现融合的步骤类似于我们以前提到的OpenGL过程。接着设置公式,并在绘制透明对象时关闭写深度缓存。因为我们想在半透明的图形背后绘制对象。这不是正确的混色方法,但绝大多数时候这种做法在简单的项目中都工作的很好。

Rui Martins的补充:正确的融合过程应该是先绘制全部的场景之后再绘制透明的图形。并且要按照与深度缓存相反的次序来绘制(先画最远的物体)。

考虑对两个多边形(1和2)进行alpha融合,不同的绘制次序会得到不同的结果。(这里假定多边形1离观察者最近,那么正确的过程应该先画多边形2,再画多边形1。正如您再现实中所见到的那样,从这两个透明的多边形背后照射来的光线总是先穿过多边形2,再穿过多边形1,最后才到达观察者的眼睛。)

在深度缓存启用时,您应该将透明图形按照深度进行排序,并在全部场景绘制完毕之后再绘制这些透明物体。否则您将得到不正确的结果。我知道某些时候这样做是很令人痛苦的,但这是正确的方法。

我们要在上一课的代码上进行改动就可以了。

我们将增加一些变量,稍后我们对这些变量进行解释。

NeHeWidget类

(由nehewidget.h展开。)

protected:

bool fullscreen;

GLfloat xRot, yRot, zRot;

GLfloat zoom;

GLfloat xSpeed, ySpeed;

GLuint texture[3];

GLuint filter;

bool light;

bool blend;

};

比上一课,只增加了blend这个变量,说明现在是否使用融合。

(由展开。)

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 };

这里定义了三个数组,它们描述的是和光源有关的信息。这里使用的光源和上一课一样。

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;

blend = false;

fullscreen = fs;

setGeometry( 0, 0, 640, 480 );

setCaption( "Tom Stanis & NeHe's Blending Tutorial" );

if ( fullscreen )

showFullScreen();

}

我们需要在构造函数中给各个变量赋初值。xRot、yRot、zRot是0.0。zoom是-5.0。xSpeed和ySpeed都是0。filter是0。light是false。blend是false。

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 );

glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,

GL_LINEAR_MIPMAP_NEAREST );

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

GL_RGBA, GL_UNSIGNED_BYTE, () );

}

loadGLTextures()函数就是用来载入纹理的。三个纹理的滤波方式都和上一课一样。

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::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 );

glLightfv( GL_LIGHT1, GL_AMBIENT, lightAmbient );

glLightfv( GL_LIGHT1, GL_DIFFUSE, lightDiffuse );

glLightfv( GL_LIGHT1, GL_POSITION, lightPosition );

glEnable( GL_LIGHT1 );

这里使用光源的方法和前面一样。

glColor4f( 1.0, 1.0, 1.0, 0.5 );

glBlendFunc( GL_SRC_ALPHA, GL_ONE );

上面第一行以全亮度绘制此物体,并对其进行50%的alpha融合(半透明)。当融合选项打开时,此物体将会产生50%的透明效果。上面第二行设置所采用的融合类型。

Rui Martins的补充:alpha通道的值为0.0意味着物体材质是完全透明的。1.0则意味着完全不透明。

}

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_B:

blend = !blend;

if ( blend )

{

glEnable( GL_BLEND );

glDisable( GL_DEPTH_TEST );

}

else

{

glDisable( GL_BLEND );

glEnable( GL_DEPTH_TEST );

}

updateGL();

break;

按下了B键,就可以切换是否启用融合方式。

case Qt::Key_F:

filter += 1;;

if ( filter > 2 )

{

filter = 0;

}

updateGL();

break;

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();

}

}

但是怎样才能在使用纹理贴图的时候指定融合时的颜色呢?很简单,在调整贴图模式时,文理贴图的每个象素点的颜色都是由alpha通道参数与当前地象素颜色相乘所得到的。比如,绘制的颜色是(0.5, 0.6, 0.4),我们会把颜色相乘得到(0.5,

0.6, 0.4, 0.2)(alpha参数在没有指定时,缺省为零)。

就是如此!OpenGL实现alpha融合的确很简单!

原文注 (1999年11月13日)

我(NeHe)融合代码进行了修改,以使显示的物体看起来更逼真。同时对源象素和目的象素使用alpha参数来融合,会导致物体的人造痕迹看起来很明显。

会使得物体的背面沿着侧面的地方显得更暗。基本上物体会看起来很怪异。我所用的融合方法也许不是最好的,但的确能够工作。启用光源之后,物体看起来很逼真。感谢Tom提供的原始代码,他采用的混色方法是正确的,但物体看起来并不象所期望的那样吸引人:)

代码所作的再次修改是因为在某些显卡上glDepthMask()函数存在寻址问题。这条命令在某些卡上启用或关闭深度缓冲测试时似乎不是很有效,所以我已经将启用或关闭深度缓冲测试的代码转成老式的glEnable和glDisable。

纹理贴图的alpha融合

用于纹理贴图的alpha参数可以象颜色一样从问题贴图中读取。方法如下,您需要在载入所需的材质同时取得其的alpha参数。然后在调用glTexImage2D()时使用GL_RGBA的颜色格式。

本课程的源代码。

[上一课:融合] [Qt OpenGL教程主页]

在三维空间中移动位图

欢迎进入第九课。到现在为止,您应该很好的理解OpenGL了。您已经学会了设置一个OpenGL窗口的每个细节。学会在旋转的物体上贴图并打上光线以及融合

Qt+OpenGL中文教程

本文发布于:2024-02-03 14:23:17,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170694139750610.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:纹理   使用   屏幕   颜色
留言与评论(共有 0 条评论)
   
验证码:
排行榜

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23