Qt Creator中的3D绘图及动画教程(参照NeHe)

http://blog.csdn.net/cly116/article/details/47184729

刚刚学习了Qt Creator,发现Qt提供了QtOpenGL模块,对OpenGL做了不错的封装,这使得我们可以很轻松地在Qt程序中使用OpenGL进行绘图渲染。虽然里面还是由不少专业的解释照搬原文的,但还是加入了大量自己的分析。而且Qt中写OpenGL框架上比VC简单太多了,有不少东西都封装优化好了,代码上还是由有很多区别的。当然,其中原教程没解释好的问题我都作了深入的解释,以及一些多余部分解释、代码都被我删掉简化了。

这份Qt OpenGL的3D绘图及动画教程,我基本会按照Nehe的OpenGL教程,只是将代码的实现运用到Qt Creator中,当然其中加了。

下面对Qt中OpenGL做一个简要介绍:

Qt中OpenGL主要是在QGLWidget类中完成的,而要使用QtOpenGL模块,需要在项目文件( .pro)中添加代码"QT+=opengl"。

QGLWidget类是一个用来渲染OpenGL图形的部件,提供了在Qt中显示OpenGL图形的功能。这个类使用起来很简单,只需要继承该类,然后像使用其他QWidget部件一样来使用它。QGLWidget提供了3个方便的纯虚函数,可以在子类中通过重新实现它们来执行典型的OpenGL任务:

initializeGL():设置OpenGL渲染环境,定义显示列表等。该函数只在第一次调用resizeGL()或paintGL()前被自动调用一次。

resizeGL():设置OpenGL的视口、投影等。每次部件改变大小时都会自动调用该函数。

paintGL():渲染OpenGL场景。每当部件需要更新时都会调用该函数。

(以上3个虚函数更具体的调用情况我会用另一篇文章来讲明)

也就是说,Qt中当创建并显示出一个QGLWidget子对象时,会自动依次调用initializeGL()、resizeGL()、paintGL(),完成当前场景的绘制;而当某些情况发生时,会根据情况决定是否自动调用initializeGL()、resizeGL(),一旦调用initializeGL()、resizeGL()了,会紧跟着调用paintGL()对场景进行重新绘制。

以上就是对Qt中OpenGL机制的一个简单介绍,后面的Qt OpenGL的3D绘图及动画教程,我基本会按照Nehe的OpenGL教程,只是将代码的实现运用到Qt Creator中;教程有看不懂的,大家可以给我留言或者参考Nehe的OpenGL教程 http://www.yakergong.net/nehe/

教程目录索引:

01:OpenGL窗口

02:多边形

03:添加颜色

04:旋转

05:3D空间

06:纹理映射

07:光照和键盘

08:混合透明

09:移动图像

10:3D世界

11:飘动的旗帜

12:显示列表

13:图像字体

14:图形字体

15:纹理图形字

16:雾

17:2D图像文字

18:二次几何体

19:粒子系统

20:蒙板

全部教程中需要的资源文件点此下载  http://download.csdn.net/download/cly116/8957317

第01课:创建一个OpenGL窗口 (参照NeHe)
在这个教程里,我们将在Qt Creator环境中创建OpenGL对象,它将显示一个空的OpenGL窗口,可以在窗口和全屏模式下切换,按ESC退出,它将是我们后面应用程序的基础框架。
Qt中写OpenGL与在VC上还是有不少差别的,对Qt机制不熟悉的朋友,请先大致了解下Qt的机制,再往下看教程。
 
程序运行时效果如下:
 
下面进入教程:
 
新建空的Qt项目,项目名称为myOpenGL,然后往项目中添加新的C++类,类名为MyGLWidget,基类为QGLWidget,类型信息选择“继承自QWidget”。添加完成后,打开项目文件myOpenGL.pro,将代码补全如下:
  1. TARGET = myOpenGL
  2. TEMPLATE = app
  3. HEADERS += \
  4. myglwidget.h
  5. SOURCES += \
  6. main.cpp \
  7. myglwidget.cpp
  8. QT       += core gui
  9. greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
  10. QT       += opengl
 

然后保存该文件。下面打开myglwidget.h文件,将类声明补全如下:

  1. #ifndef MYGLWIDGET_H
  2. #define MYGLWIDGET_H
  3. #include <QWidget>
  4. #include <QGLWidget>
  5. class MyGLWidget : public QGLWidget
  6. {
  7. Q_OBJECT
  8. public:
  9. explicit MyGLWidget(QWidget *parent = 0);
  10. ~MyGLWidget();
  11. protected:
  12. //对3个纯虚函数的重定义
  13. void initializeGL();
  14. void resizeGL(int w, int h);
  15. void paintGL();
  16. void keyPressEvent(QKeyEvent *event);           //处理键盘按下事件
  17. private:
  18. bool fullscreen;                                //是否全屏显示
  19. };
  20. #endif // MYGLWIDGET_H

再到myglwidget.cpp文件中先包含#include<GL/glu.h>,#include<QKeyEvent>头文件,然后添加类中函数的定义:

  1. MyGLWidget::MyGLWidget(QWidget *parent) :
  2. QGLWidget(parent)
  3. {
  4. fullscreen = false;
  5. }
  6. MyGLWidget::~MyGLWidget()
  7. {
  8. }

构造函数中只需对fullscreen初始化,析构函数暂时并不需要做什么。

下面是initializeGL()的定义:
  1. void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
  2. {
  3. glClearColor(0.0, 0.0, 0.0, 0.0);                   //黑色背景
  4. glShadeModel(GL_SMOOTH);                            //启用阴影平滑
  5. glClearDepth(1.0);                                  //设置深度缓存
  6. glEnable(GL_DEPTH_TEST);                            //启用深度测试
  7. glDepthFunc(GL_LEQUAL);                             //所作深度测试的类型
  8. glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  //告诉系统对透视进行修正
  9. }

glClearColor()函数用来设置清除屏幕时使用的颜色,4个参数分别用来设置红、绿、蓝颜色分量和Alpha值,它们的取值范围都是0.0~1.0,这里4个参数都为0.0,表示纯黑色。然后设置了阴影平滑,这样可以使色彩和光照更加精细。

接下来的三行必须做的是关于depth buffer(深度缓存)的。将深度缓存设想为屏幕后面的层。深度缓存不断地对物体进入屏幕内部有多深进行跟踪。我们本节的程序其实没有真正的使用深度缓存,但几乎所有在屏幕上显示3D场景OpenGL程序都使用深度缓存。它的排序决定那个物体先画。这样就不会将一个圆形后面的正方形画到圆形前面来。深度缓存是OpenGL十分重要的部分。最后我们希望进行最好的透视修正。这会十分轻微的影响性能,但使得透视图看起来好一点。

下面是resizeGL()的定义:
  1. void MyGLWidget::resizeGL(int w, int h)                 //重置OpenGL窗口的大小
  2. {
  3. glViewport(0, 0, (GLint)w, (GLint)h);               //重置当前的视口
  4. glMatrixMode(GL_PROJECTION);                        //选择投影矩阵
  5. glLoadIdentity();                                   //重置投影矩阵
  6. //设置视口的大小
  7. gluPerspective(45.0, (GLfloat)w/(GLfloat)h, 0.1, 100.0);
  8. glMatrixMode(GL_MODELVIEW);                         //选择模型观察矩阵
  9. glLoadIdentity();                                   //重置模型观察矩阵
  10. }

glViewport()函数用来设置视口的大小。使用glMatrixMode()设置了投影矩阵,投影矩阵用来为场景增加透视,后面使用了glLoadIdentity()重置投影矩阵,这样可以将投影矩阵恢复到初始状态。gluPerspective()用来设置透视投影矩阵,这里设置视角为45°,纵横比为窗口的纵横比,最近的位置为0.1,最远的位置为100,这两个值是场景中所能绘制的深度的临界值。可以想象,离我们眼睛比较近的东西看起来比较大,而比较远的东西看起来就比较小。最后设置并重置了模型视图矩阵。

下面是paintGL()的定义:
  1. void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
  2. {
  3. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
  4. glLoadIdentity();                                   //重置当前的模型观察矩阵
  5. }

paintGL()函数包含了所以的绘图代码,任何想在屏幕上显示的东西都将在此段代码中出现。以后每个教程中都会在这个函数增加代码,已达到绘图目的。

最后是键盘事件处理函数KeyPressEvent()的定义,由于这与OpenGL关系不大,不做过多解释:
  1. void MyGLWidget::keyPressEvent(QKeyEvent *event)
  2. {
  3. switch (event->key())
  4. {
  5. //F1为全屏和普通屏的切换键
  6. case Qt::Key_F1:
  7. fullscreen = !fullscreen;
  8. if (fullscreen)
  9. {
  10. showFullScreen();
  11. }
  12. else
  13. {
  14. showNormal();
  15. }
  16. updateGL();
  17. break;
  18. //ESC为退出键
  19. case Qt::Key_Escape:
  20. close();
  21. }
  22. }

最后再向项目中添加main.cpp文件,更改内容如下:

  1. #include <QApplication>
  2. #include "myglwidget.h"
  3. int main(int argc, char *argv[])
  4. {
  5. QApplication app(argc, argv);
  6. MyGLWidget w;
  7. w.resize(400, 300);
  8. w.show();
  9. return app.exec();
  10. }

现在就可以运行程序查看效果了!

第02课:你的第一个多边形 (参照NeHe)

这次教程中,我们将添加一个三角形和一个四边形。或许你认为这很简单,但要知道任何复杂的绘图都是从简单开始的,或者说任何复杂的模型都是可以分解成简单的图形的。所以,我们还是从简单的图形开始吧。

读完这一次教程,你还会学到如何在空间放置模型以及了解OpenGL中坐标变化。

程序运行时效果如下:


 

下面进入教程:

我们将使用GL_TRIANGLES来创建一个三角形,GL_QUADS来创建一个四边形。在第01课代码的基础上,我们只需在paintGL()函数中增加代码。

下面我将重写整个paintGL()函数,具体代码如下:

  1. void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
  2. {
  3. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
  4. glLoadIdentity();                                   //重置当前的模型观察矩阵
  5. glTranslatef(-1.5f, 0.0f, -6.0f);                   //左移1.5单位,并移入屏幕6.0单位
  6. glBegin(GL_TRIANGLES);                              //开始绘制三角形
  7. glVertex3f(0.0f, 1.0f, 0.0f);                   //上顶点
  8. glVertex3f(-1.0f, -1.0f, 0.0f);                 //左下
  9. glVertex3f(1.0f, -1.0f, 0.0f);                  //右下
  10. glEnd();                                            //三角形绘制结束
  11. glTranslatef(3.0f, 0.0f, 0.0f);                     //右移3.0单位
  12. glBegin(GL_QUADS);                                  //开始绘制四边形
  13. glVertex3f(-1.0f, 1.0f, 0.0f);                  //左上
  14. glVertex3f(1.0f, 1.0f, 0.0f);                   //右上
  15. glVertex3f(1.0f, -1.0f, 0.0f);                  //左下
  16. glVertex3f(-1.0f, -1.0f, 0.0f);                 //右下
  17. glEnd();                                            //四边形绘制结束
  18. }

当调用了glLoadIdentity()之后,我们实际上将当前点移到了屏幕中心,x轴从左到右,y轴从下到上,z轴从里到外。其中,中心右面,上面,外面的坐标值为正值。glTranslatef(x, y, z)沿着x,y和z轴移动,要注意,在glTranslatef(x, y, z)移动的时候,并不是相对屏幕中心移动,而是相对于当前所在的屏幕位置。
glBegin(GL_TRIANGLES)的意思是开始绘制三角形,glEnd()告诉OpenGL三角形已经创建好了。通常我们会需要画3个顶点,可以使用GL_TRIANGLES;而要画4个顶点时,使用GL_QUADS会更方便。最后,如果想要画更多的顶点时,可以使用GL_POLYGON。

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

glBegin()之后的第一行设置了多边形的第一个顶点,glVertex的三个参数依次是x,y和z轴坐标。glEnd()告诉OpenGL没有其他点了,这样将显示一个填充的三角形。

然后类比画出一个四边形后,就可以运行程序看效果了!

第03课:添加颜色 (参照NeHe)
这次教程中,我们将在第02课的基础上,教大家如何使用颜色。我们将一起理解两种着色模式(光滑着色与平面着色),并运用这两种模式分别给第02课的三角形和正方形着色。我们将使用平面着色给四边形着色,即给三角形涂上一种固定的颜色;使用平滑着色给三角形着色,将三角形的三个顶点的不同颜色混合在一起,创建漂亮的色彩混合。
 
程序运行时效果如下:

 
下面进入教程:
 
要对三角形和四边形进行着色,只需在第02课代码的基础上,对paintGL()函数作一定的修改。
下面我将重写整个paintGL()函数,具体代码如下:
  1. void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
  2. {
  3. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
  4. glLoadIdentity();                                   //重置当前的模型观察矩阵
  5. glTranslatef(-1.5f, 0.0f, -6.0f);                   //左移1.5单位,并移入屏幕6.0单位
  6. glBegin(GL_TRIANGLES);                              //开始绘制三角形
  7. glColor3f(1.0f, 0.0f, 0.0f);                    //设置当前色为红色
  8. glVertex3f(0.0f, 1.0f, 0.0f);                   //上顶点
  9. glColor3f(0.0f, 1.0f, 0.0f);                    //设置当前色为绿色
  10. glVertex3f(-1.0f, -1.0f, 0.0f);                 //左下
  11. glColor3f(0.0f, 0.0f, 1.0f);                    //设置当前色为蓝色
  12. glVertex3f(1.0f, -1.0f, 0.0f);                  //右下
  13. glEnd();                                            //三角形绘制结束
  14. glTranslatef(3.0f, 0.0f, 0.0f);                     //右移3.0单位
  15. glColor3f(0.5f, 0.5f, 1.0f);                        //一次性将当前色设置为蓝色
  16. glBegin(GL_QUADS);                                  //开始绘制四边形
  17. glVertex3f(-1.0f, 1.0f, 0.0f);                  //左上
  18. glVertex3f(1.0f, 1.0f, 0.0f);                   //右上
  19. glVertex3f(1.0f, -1.0f, 0.0f);                  //左下
  20. glVertex3f(-1.0f, -1.0f, 0.0f);                 //右下
  21. glEnd();                                            //四边形绘制结束
  22. }

其实与第02课相比,只是增加了4行代码而已。我们利用glColor3f(r, g, b)函数来选择颜色进行着色,该函数三个参数依次是红、绿、蓝三色分量,范围从0.0到1.0之间,类似于之前所讲的清除屏幕背景函数。当我们将颜色设为某种颜色时,接下来的代码绘制出的对象的颜色就都是对应颜色的。

对于用光滑着色的三角形,我们需要分别对于3个顶点分别选择颜色,再分别进行绘点。故我们在每次绘点之前都需要调用一次glColor3f(r, g, b)进行选色,glEnd()之后,三角形将被填充,但因为每个顶点有不同的颜色,因此看起来颜色从每个顶点喷出,并刚好在三角形的中心汇合,三种颜色相互混合,这就是平滑着色;而对于使用平面着色的四边形,我只需要在一开始就选择好颜色,直接绘制四边形即可。
还有一点值得提的是,顺时针绘制图形时,意味着我们所看见的是图形的背景,这在后面对图形有一定影响。
现在就可以运行程序查看效果了!

第04课:旋转 (参照NeHe)

这次教程中,我们将在第03课的基础上,教大家如何旋转三角形和四边形。我们将让三角形沿y轴旋转,四边形沿x轴旋转,最终我们能得到一个三角形和四边形自动旋转的场景。

程序运行时效果如下:

 

下面进入教程:

首先打开myglwidget.h文件,我们需要增加两个变量来控制这两个对象的旋转。这两个变量加在类的私有声明处,将类声明更改如下:

  1. #ifndef MYGLWIDGET_H
  2. #define MYGLWIDGET_H
  3. #include <QWidget>
  4. #include <QGLWidget>
  5. class MyGLWidget : public QGLWidget
  6. {
  7. Q_OBJECT
  8. public:
  9. explicit MyGLWidget(QWidget *parent = 0);
  10. ~MyGLWidget();
  11. protected:
  12. //对3个纯虚函数的重定义
  13. void initializeGL();
  14. void resizeGL(int w, int h);
  15. void paintGL();
  16. void keyPressEvent(QKeyEvent *event);           //处理键盘按下事件
  17. private:
  18. bool fullscreen;                                //是否全屏显示
  19. GLfloat m_rtri;                                 //控制三角形的角度
  20. GLfloat m_rquad;                                //控制四边形的角度
  21. };
  22. #endif // MYGLWIDGET_H

我们增加了两个浮点类型的变量,使得我们能够非常精确地旋转对象,你渐渐会发现浮点数是OpenGL编程的基础。新变量中叫做m_rtri的用来旋转三角形,m_rquad旋转四边形。

接下来,我们需要打开myglwidget.cpp,在构造函数中对两个新变量进行初始化,这部分很简单,不作过多解释,代码如下:

  1. MyGLWidget::MyGLWidget(QWidget *parent) :
  2. QGLWidget(parent)
  3. {
  4. fullscreen = false;
  5. m_rtri = 0.0f;
  6. m_rquad = 0.0f;
  7. }

然后进入重点的paintGL()函数了,我们只需在第03课代码的基础上,做一定的修改,就能实现三角形和四边形的旋转了。

下面我将重写整个paintGL()函数,具体代码如下:

  1. void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
  2. {
  3. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
  4. glLoadIdentity();                                   //重置当前的模型观察矩阵
  5. glTranslatef(-1.5f, 0.0f, -6.0f);                   //左移1.5单位,并移入屏幕6.0单位
  6. glRotatef(m_rtri, 0.0f, 1.0f, 0.0f);                //绕y轴旋转三角形
  7. glBegin(GL_TRIANGLES);                              //开始绘制三角形
  8. glColor3f(1.0f, 0.0f, 0.0f);                    //设置当前色为红色
  9. glVertex3f(0.0f, 1.0f, 0.0f);                   //上顶点
  10. glColor3f(0.0f, 1.0f, 0.0f);                    //设置当前色为绿色
  11. glVertex3f(-1.0f, -1.0f, 0.0f);                 //左下
  12. glColor3f(0.0f, 0.0f, 1.0f);                    //设置当前色为蓝色
  13. glVertex3f(1.0f, -1.0f, 0.0f);                  //右下
  14. glEnd();                                            //三角形绘制结束
  15. glLoadIdentity();                                   //重置模型观察矩阵
  16. glTranslatef(1.5f, 0.0f, -6.0f);                    //右移1.5单位,并移入屏幕6.0单位
  17. glRotatef(m_rquad, 1.0f, 0.0f, 0.0f);               //绕x轴旋转四边形
  18. glColor3f(0.5f, 0.5f, 1.0f);                        //一次性将当前色设置为蓝色
  19. glBegin(GL_QUADS);                                  //开始绘制四边形
  20. glVertex3f(-1.0f, 1.0f, 0.0f);                  //左上
  21. glVertex3f(1.0f, 1.0f, 0.0f);                   //右上
  22. glVertex3f(1.0f, -1.0f, 0.0f);                  //左下
  23. glVertex3f(-1.0f, -1.0f, 0.0f);                 //右下
  24. glEnd();                                            //四边形绘制结束
  25. m_rtri += 0.5f;                                     //增加三角形的旋转变量
  26. m_rquad -= 0.5f;                                    //减少四边形的旋转变量
  27. }

上面的代码绘制三角形时多了一新函数glRotatef(Angle, Xvector, Yvector, Zvector)。该函数负责让对象绕某个轴旋转,这个函数有诸多用处。Angle通常是个变量代表对象转过的角度,后三个参数则共同决定旋转轴的方向。故(1.0f, 0.0f, 0.0f)、(0.0f, 1.0f, 0.0f)、(0.0f, 0.0f, 1.0f)表示依次绕x、y、z轴旋转,参照此原理,我们也能实现四边形的旋转。

我们会发现画完三角形后,相比原来的代码多了一行glLoadIdentity(),目的是为了重置模型观察矩阵。如果我们没有重置,直接调用glTranslate的话,会发现可能没有朝着我们所希望的方向旋转,这是由于坐标轴以前已经旋转了。所以我们本来要左右移动对象的,可能就变成上下移动了。还不理解的朋友可以试着将glLoadIdentity()试注释掉之后,看会出现什么结果。

重置模型观察矩阵之后,x、y、z轴都复位,我们调用glTranslate时只向右移动了1.5单位,而不是之前的3.0单位。因为我们重置场景的时候,焦点又回到了场景的中心,这样只需右移单位即可。

最后我们通过增加m_rtri和减少m_rquad使得物体自己旋转起来,我们可以尝试改变代码中的+和-,来体会对象旋转的方向是如何改变的。并尝试着将0.5改成4.0,。这个数字越大,物体就转得越快,这个数字越小,物体转的就越慢。

至此,我们似乎已经完成了,但是运行程序时发现,三角形和四边形并没有自动旋转起来。这是由于paintGL()被调用一次之后,没有发生其他的事件使得它被自动调用。我们可以通过拉伸窗口的大小,发现三角形和四边形就动起来了,这是由于我们改变了窗口大小,调用了reszieGL()之后紧接着调用了paintGL()对场景进行重绘。显然,我们不能一直通过拉伸窗口来实现旋转,这样显得很拙,我们可以在构造函数中利用Qt的定时器事件来控制paintGL()的调用。先在myglwidget.cpp中添加头文件#include <QTimer>。构造函数代码如下:(具体initializeGL()、reszieGL()、paintGL()的调用情况请参见)

  1. MyGLWidget::MyGLWidget(QWidget *parent) :
  2. QGLWidget(parent)
  3. {
  4. fullscreen = false;
  5. m_rtri = 0.0f;
  6. m_rquad = 0.0f;
  7. QTimer *timer = new QTimer(this);                   //创建一个定时器
  8. //将定时器的计时信号与updateGL()绑定
  9. connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
  10. timer->start(10);                                   //以10ms为一个计时周期
  11. }

这里将定时器的timeout()信号与updateGL()槽绑定,每过10ms就会调用一次updateGL(),而updateGL()调用后会调用paintGL()对场景进行重绘,这样就通过对场景不停地重绘实现对象的旋转。(对Qt定时器不了解的朋友请先百度了解下其机制)
现在就可以运行程序看效果了!

第05课:3D模型 (参照NeHe)

这次教程中,我们将之前几课的基础上,教大家如何创建立体的3D模型。我们将开始生成真正的3D对象,而不是像之前那几课那样3D世界中的2D对象。我们会把之前的三角形变为立体的金字塔模型,把四边形变为立方体。

我们给三角形增加左侧面、右侧面、后侧面来生成一个金字塔。给正方形增加左、右、上、下及背面生成一个立方体。我们混合金字塔上的颜色,创建一个平滑着色的对象;给立方体的每一面来个不同的颜色。

程序运行时效果如下:

下面进入教程:

要实现3D模型,只需在第04课代码的基础上,对paintGL()函数作一定的修改。

下面我将重写整个paintGL()函数,具体代码如下:

  1. void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
  2. {
  3. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
  4. glLoadIdentity();                                   //重置当前的模型观察矩阵
  5. glTranslatef(-1.5f, 0.0f, -6.0f);                   //左移1.5单位,并移入屏幕6.0单位
  6. glRotatef(m_rtri, 0.0f, 1.0f, 0.0f);                //绕y轴旋转三角形
  7. glBegin(GL_TRIANGLES);                              //开始绘制金字塔
  8. glColor3f(1.0f, 0.0f, 0.0f);                    //红色
  9. glVertex3f(0.0f, 1.0f, 0.0f);                   //上顶点(前侧面)
  10. glColor3f(0.0f, 1.0f, 0.0f);                    //绿色
  11. glVertex3f(-1.0f, -1.0f, 1.0f);                 //左下(前侧面)
  12. glColor3f(0.0f, 0.0f, 1.0f);                    //蓝色
  13. glVertex3f(1.0f, -1.0f, 1.0f);                  //右下(前侧面)
  14. glColor3f(1.0f, 0.0f, 0.0f);                    //红色
  15. glVertex3f(0.0f, 1.0f, 0.0f);                   //上顶点(右侧面)
  16. glColor3f(0.0f, 0.0f, 1.0f);                    //蓝色
  17. glVertex3f(1.0f, -1.0f, 1.0f);                  //左下(右侧面)
  18. glColor3f(0.0f, 1.0f, 0.0f);                    //绿色
  19. glVertex3f(1.0f, -1.0f, -1.0f);                 //右下(右侧面)
  20. glColor3f(1.0f, 0.0f, 0.0f);                    //红色
  21. glVertex3f(0.0f, 1.0f, 0.0f);                   //上顶点(后侧面)
  22. glColor3f(0.0f, 1.0f, 0.0f);                    //绿色
  23. glVertex3f(1.0f, -1.0f, -1.0f);                 //左下(后侧面)
  24. glColor3f(0.0f, 0.0f, 1.0f);                    //蓝色
  25. glVertex3f(-1.0f, -1.0f, -1.0f);                //右下(后侧面)
  26. glColor3f(1.0f, 0.0f, 0.0f);                    //红色
  27. glVertex3f(0.0f, 1.0f, 0.0f);                   //上顶点(左侧面)
  28. glColor3f(0.0f, 0.0f, 1.0f);                    //蓝色
  29. glVertex3f(-1.0f, -1.0f, -1.0f);                //左下(左侧面)
  30. glColor3f(0.0f, 1.0f, 0.0f);                    //绿色
  31. glVertex3f(-1.0f, -1.0f, 1.0f);                 //右下(左侧面)
  32. glEnd();                                            //金字塔绘制结束
  33. glLoadIdentity();                                   //重置模型观察矩阵
  34. glTranslatef(1.5f, 0.0f, -6.0f);                    //右移1.5单位,并移入屏幕6.0单位
  35. glRotatef(m_rquad, 1.0f, 0.0f, 0.0f);               //绕x轴旋转四边形
  36. glBegin(GL_QUADS);                                  //开始绘制立方体
  37. glColor3f(0.0f, 1.0f, 0.0f);                    //绿色
  38. glVertex3f(1.0f, 1.0f, -1.0f);                  //右上(顶面)
  39. glVertex3f(-1.0f, 1.0f, -1.0f);                 //左上(顶面)
  40. glVertex3f(-1.0f, 1.0f, 1.0f);                  //左下(顶面)
  41. glVertex3f(1.0f, 1.0f, 1.0f);                   //右下(顶面)
  42. glColor3f(1.0f, 0.5f, 0.0f);                    //橙色
  43. glVertex3f(1.0f, -1.0f, 1.0f);                  //右上(底面)
  44. glVertex3f(-1.0f, -1.0f, 1.0f);                 //左上(底面)
  45. glVertex3f(-1.0f, -1.0f, -1.0f);                //左下(底面)
  46. glVertex3f(1.0f, -1.0f, -1.0f);                 //右下(底面)
  47. glColor3f(1.0f, 0.0f, 0.0f);                    //红色
  48. glVertex3f(1.0f, 1.0f, 1.0f);                   //右上(前面)
  49. glVertex3f(-1.0f, 1.0f, 1.0f);                  //左上(前面)
  50. glVertex3f(-1.0f, -1.0f, 1.0f);                 //左下(前面)
  51. glVertex3f(1.0f, -1.0f, 1.0f);                  //右下(前面)
  52. glColor3f(1.0f, 1.0f, 0.0f);                    //黄色
  53. glVertex3f(1.0f, -1.0f, -1.0f);                 //右上(后面)
  54. glVertex3f(-1.0f, -1.0f, -1.0f);                //左上(后面)
  55. glVertex3f(-1.0f, 1.0f, -1.0f);                 //左下(后面)
  56. glVertex3f(1.0f, 1.0f, -1.0f);                  //右下(后面)
  57. glColor3f(0.0f, 0.0f, 1.0f);                    //蓝色
  58. glVertex3f(-1.0f, 1.0f, 1.0f);                  //右上(左面)
  59. glVertex3f(-1.0f, 1.0f, -1.0f);                 //左上(左面)
  60. glVertex3f(-1.0f, -1.0f, -1.0f);                //左下(左面)
  61. glVertex3f(-1.0f, -1.0f, 1.0f);                 //右下(左面)
  62. glColor3f(1.0f, 0.0f, 1.0f);                    //紫色
  63. glVertex3f(1.0f, 1.0f, -1.0f);                  //右上(右面)
  64. glVertex3f(1.0f, 1.0f, 1.0f);                   //左上(右面)
  65. glVertex3f(1.0f, -1.0f, 1.0f);                  //左下(右面)
  66. glVertex3f(1.0f, -1.0f, -1.0f);                 //右下(右面)
  67. glEnd();                                            //立方体绘制结束
  68. m_rtri += 0.5f;                                     //增加金字体的旋转变量
  69. m_rquad -= 0.5f;                                    //减少立方体的旋转变量
  70. }

首先创建一个绕着其中心轴旋转的金字塔,金字塔的上顶点高出原点一个单位,底面中心低于原点一个单位,上顶点在底面的投影位于底面的中心。要注意的是所有的面-三角形都是逆时针次序绘制的,这点十分重要,在以后的课程中我会做出解释。现在,我们只需明白要么都逆时针,要么都顺时针,但永远不要将两种次序混在一起,除非我们有足够的理由必须这么做。

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

开始绘制立方体,它由六个四边形组成,所有的四边形都以逆时针次序绘制,即按照右上、左上、左下、右下的次序绘画。你也许认为画立方体的背面的时候这个次序看起来好像顺时针,但别忘了我们从立方体背后看背面的时候,与你现在所想的正好相反(我们是从立方体外面来观察立方体的)。当然,你也可以尝试用平滑着色来绘制立方体。

现在就可以运行程序查看效果了!

第06课:纹理映射 (参照NeHe)
这次教程中,我教会大家如何把纹理映射到立方体的六个面上。学习texture map(纹理映射)有诸多好处。比如说想让一颗导弹飞过屏幕。根据前几课的知识,我们最可行的办法可能是很多个多边形来构建导弹的轮廓并加上有趣的颜色。而使用纹理映射,我们可以使用真实的导弹图像并让它飞过屏幕。你觉得哪个更好看?使用纹理映射的好处还不止是更好看,而且程序的运行会更快。导弹贴图可能只是一个飞过窗口的四边形,而一个导弹却需要成百上千的多边形组成,很明显,纹理映射极大的节省了CPU的时间。
 
程序运行时效果如下:
 
下面进入教程:
 
我们这次将在第01课得到的基础框架上开始添加代码,首先打开myglwidget.h文件,我们需要增加一些变量,将类声明更改如下:
  1. #ifndef MYGLWIDGET_H
  2. #define MYGLWIDGET_H
  3. #include <QWidget>
  4. #include <QGLWidget>
  5. class MyGLWidget : public QGLWidget
  6. {
  7. Q_OBJECT
  8. public:
  9. explicit MyGLWidget(QWidget *parent = 0);
  10. ~MyGLWidget();
  11. protected:
  12. //对3个纯虚函数的重定义
  13. void initializeGL();
  14. void resizeGL(int w, int h);
  15. void paintGL();
  16. void keyPressEvent(QKeyEvent *event);           //处理键盘按下事件
  17. private:
  18. bool fullscreen;                                //是否全屏显示
  19. GLfloat m_xRot;                                 //绕x轴旋转的角度
  20. GLfloat m_yRot;                                 //绕y轴旋转的角度
  21. GLfloat m_zRot;                                 //绕z轴旋转的角度
  22. QString m_FileName;                             //图片的路径及文件名
  23. GLuint m_Texture;                               //储存一个纹理
  24. };
  25. #endif // MYGLWIDGET_H

增加的前三个变量用来使立方体绕x、y、z轴旋转,m_FileName用于储存图片的路径及文件名,m_Texture为一个纹理分配存储空间。如果需要不止一个纹理,可以创建一个数组来储存不同的纹理。

 
接下来,我们需要打开myglwidget.cpp,加上声明#include <QTimer>,在构造函数中对新增变量(除了m_Texture)进行初始化,同样不作过多解释,代码如下:
  1. MyGLWidget::MyGLWidget(QWidget *parent) :
  2. QGLWidget(parent)
  3. {
  4. fullscreen = false;
  5. m_xRot = 0.0f;
  6. m_yRot = 0.0f;
  7. m_zRot = 0.0f;
  8. m_FileName = "D:/QtOpenGL/QtImage/Nehe.bmp";        //应根据实际存放图片的路径进行修改
  9. QTimer *timer = new QTimer(this);                   //创建一个定时器
  10. //将定时器的计时信号与updateGL()绑定
  11. connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
  12. timer->start(10);                                   //以10ms为一个计时周期
  13. }

然后这次我们需要对initializeGL()函数作一定的修改了,具体代码如下:

  1. void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
  2. {
  3. m_Texture = bindTexture(QPixmap(m_FileName));       //载入位图并转换成纹理
  4. glEnable(GL_TEXTURE_2D);                            //启用纹理映射
  5. glClearColor(0.0f, 0.0f, 0.0f, 0.0f);               //黑色背景
  6. glShadeModel(GL_SMOOTH);                            //启用阴影平滑
  7. glClearDepth(1.0);                                  //设置深度缓存
  8. glEnable(GL_DEPTH_TEST);                            //启用深度测试
  9. glDepthFunc(GL_LEQUAL);                             //所作深度测试的类型
  10. glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  //告诉系统对透视进行修正
  11. }

我们增加了两行代码,首先调用了Qt提供的bindTexture()函数将图片载入并转换成纹理,然后启用2D纹理映射。如果忘记启用的话,我们的对象看起来永远都是纯白色的,这明显与我们的预期大相径庭。

 
最后我们该开始绘制贴图过的立方体了,paintGL()函数具体代码如下:
  1. void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
  2. {
  3. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
  4. glLoadIdentity();                                   //重置模型观察矩阵
  5. glTranslatef(0.0f, 0.0f, -5.0f);                    //移入屏幕5.0单位
  6. glRotatef(m_xRot, 1.0f, 0.0f, 0.0f);                //绕x轴旋转
  7. glRotatef(m_yRot, 0.0f, 1.0f, 0.0f);                //绕y轴旋转
  8. glRotatef(m_zRot, 0.0f, 0.0f, 1.0f);                //绕z轴旋转
  9. glBindTexture(GL_TEXTURE_2D, m_Texture);            //选择纹理
  10. glBegin(GL_QUADS);                                  //开始绘制立方体
  11. glTexCoord2f(1.0f, 1.0f);
  12. glVertex3f(1.0f, 1.0f, -1.0f);                  //右上(顶面)
  13. glTexCoord2f(0.0f, 1.0f);
  14. glVertex3f(-1.0f, 1.0f, -1.0f);                 //左上(顶面)
  15. glTexCoord2f(0.0f, 0.0f);
  16. glVertex3f(-1.0f, 1.0f, 1.0f);                  //左下(顶面)
  17. glTexCoord2f(1.0f, 0.0f);
  18. glVertex3f(1.0f, 1.0f, 1.0f);                   //右下(顶面)
  19. glTexCoord2f(0.0f, 0.0f);
  20. glVertex3f(1.0f, -1.0f, 1.0f);                  //右上(底面)
  21. glTexCoord2f(1.0f, 0.0f);
  22. glVertex3f(-1.0f, -1.0f, 1.0f);                 //左上(底面)
  23. glTexCoord2f(1.0f, 1.0f);
  24. glVertex3f(-1.0f, -1.0f, -1.0f);                //左下(底面)
  25. glTexCoord2f(0.0f, 1.0f);
  26. glVertex3f(1.0f, -1.0f, -1.0f);                 //右下(底面)
  27. glTexCoord2f(1.0f, 1.0f);
  28. glVertex3f(1.0f, 1.0f, 1.0f);                   //右上(前面)
  29. glTexCoord2f(0.0f, 1.0f);
  30. glVertex3f(-1.0f, 1.0f, 1.0f);                  //左上(前面)
  31. glTexCoord2f(0.0f, 0.0f);
  32. glVertex3f(-1.0f, -1.0f, 1.0f);                 //左下(前面)
  33. glTexCoord2f(1.0f, 0.0f);
  34. glVertex3f(1.0f, -1.0f, 1.0f);                  //右下(前面)
  35. glTexCoord2f(0.0f, 0.0f);
  36. glVertex3f(1.0f, -1.0f, -1.0f);                 //右上(后面)
  37. glTexCoord2f(1.0f, 0.0f);
  38. glVertex3f(-1.0f, -1.0f, -1.0f);                //左上(后面)
  39. glTexCoord2f(1.0f, 1.0f);
  40. glVertex3f(-1.0f, 1.0f, -1.0f);                 //左下(后面)
  41. glTexCoord2f(0.0f, 1.0f);
  42. glVertex3f(1.0f, 1.0f, -1.0f);                  //右下(后面)
  43. glTexCoord2f(1.0f, 1.0f);
  44. glVertex3f(-1.0f, 1.0f, 1.0f);                  //右上(左面)
  45. glTexCoord2f(0.0f, 1.0f);
  46. glVertex3f(-1.0f, 1.0f, -1.0f);                 //左上(左面)
  47. glTexCoord2f(0.0f, 0.0f);
  48. glVertex3f(-1.0f, -1.0f, -1.0f);                //左下(左面)
  49. glTexCoord2f(1.0f, 0.0f);
  50. glVertex3f(-1.0f, -1.0f, 1.0f);                 //右下(左面)
  51. glTexCoord2f(1.0f, 1.0f);
  52. glVertex3f(1.0f, 1.0f, -1.0f);                  //右上(右面)
  53. glTexCoord2f(0.0f, 1.0f);
  54. glVertex3f(1.0f, 1.0f, 1.0f);                   //左上(右面)
  55. glTexCoord2f(0.0f, 0.0f);
  56. glVertex3f(1.0f, -1.0f, 1.0f);                  //左下(右面)
  57. glTexCoord2f(1.0f, 0.0f);
  58. glVertex3f(1.0f, -1.0f, -1.0f);                 //右下(右面)
  59. glEnd();                                            //立方体绘制结束
  60. m_xRot += 0.6f;                                     //x轴旋转
  61. m_yRot += 0.4f;                                     //y轴旋转
  62. m_zRot += 0.8f;                                     //z轴旋转
  63. }

这次我们需要让对象依次绕x、y、z轴旋转,旋转多少依赖于变量m_xRot、m_yRot、m_zRot的值。下面我们调用glBindTexture()函数来选择要绑定的纹理,第2个参数表示所要绑定的纹理。当想改变纹理时,应该绑定新的纹理,要注意的是,我们不能在glBegin()和glEnd()直接绑定纹理,那样绑定的纹理时无效的。

为了将纹理正确地映射到四边形上,我们需要将纹理的四个角对应映射到四边形的四个角上。如果映射错误的话,图像显示时可能上下颠倒,侧向一边或者什么都不是。glTexCoord2f的两个参数分别表示x、y坐标,范围从0.0f到1.0f。
最后我们让m_xRot、m_yRot、m_zRot的值增加,大家可以尝试变化每次个变量的改变值来调节立方体的旋转速度,或改变+/-号来调节立方体的旋转方向。
现在就可以运行程序查看效果了!

第07课:光照和键盘控制 (参照NeHe)

这次教程中,我们将添加光照和键盘控制,它让程序看起来更美观。我将教大家如何使用键盘来移动场景中的对象,还会教大家在OpenGL场景中应用简单的光照,让我们的程序更加视觉效果更好且受我们控制。

程序运行时效果如下:

下面进入教程:

我们这次将在第06课的基础上修改代码,首先打开myglwidget.h文件,将类声明更改如下:

  1. #ifndef MYGLWIDGET_H
  2. #define MYGLWIDGET_H
  3. #include <QWidget>
  4. #include <QGLWidget>
  5. class MyGLWidget : public QGLWidget
  6. {
  7. Q_OBJECT
  8. public:
  9. explicit MyGLWidget(QWidget *parent = 0);
  10. ~MyGLWidget();
  11. protected:
  12. //对3个纯虚函数的重定义
  13. void initializeGL();
  14. void resizeGL(int w, int h);
  15. void paintGL();
  16. void keyPressEvent(QKeyEvent *event);           //处理键盘按下事件
  17. private:
  18. bool fullscreen;                                //是否全屏显示
  19. QString m_FileName;                             //图片的路径及文件名
  20. GLuint m_Texture;                               //储存一个纹理
  21. bool m_Light;                                   //光源的开/关
  22. GLfloat m_xRot;                                 //x旋转角度
  23. GLfloat m_yRot;                                 //y旋转角度
  24. GLfloat m_xSpeed;                               //x旋转速度
  25. GLfloat m_ySpeed;                               //y旋转速度
  26. GLfloat m_Deep;                                 //深入屏幕的距离
  27. };
  28. #endif // MYGLWIDGET_H

增加了一个布尔变量表示光源的开关,剩下的五个浮点变量用于控制对象的旋转角度,旋转速度以及距离屏幕的位置。

接下来,我们需要打开myglwidget.cpp,加上声明#include <QTimer>,在构造函数中对新增变量(除了m_Texture)进行初始化,同样不作过多解释,代码如下:

  1. MyGLWidget::MyGLWidget(QWidget *parent) :
  2. QGLWidget(parent)
  3. {
  4. fullscreen = false;
  5. m_FileName = "D:/QtOpenGL/QtImage/Crate.bmp";        //应根据实际存放图片的路径进行修改
  6. m_Light = false;
  7. m_xRot = 0.0f;
  8. m_yRot = 0.0f;
  9. m_xSpeed = 0.0f;
  10. m_ySpeed = 0.0f;
  11. m_Deep = -5.0f;
  12. QTimer *timer = new QTimer(this);                   //创建一个定时器
  13. //将定时器的计时信号与updateGL()绑定
  14. connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
  15. timer->start(10);                                   //以10ms为一个计时周期
  16. }

然后,我们要来添加光照,只需要在initializeGL()函数增加几行代码,具体修改后代码如下:

  1. void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
  2. {
  3. m_Texture = bindTexture(QPixmap(m_FileName));       //载入位图并转换成纹理
  4. glEnable(GL_TEXTURE_2D);                            //启用纹理映射
  5. glClearColor(0.0f, 0.0f, 0.0f, 0.0f);               //黑色背景
  6. glShadeModel(GL_SMOOTH);                            //启用阴影平滑
  7. glClearDepth(1.0);                                  //设置深度缓存
  8. glEnable(GL_DEPTH_TEST);                            //启用深度测试
  9. glDepthFunc(GL_LEQUAL);                             //所作深度测试的类型
  10. glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  //告诉系统对透视进行修正
  11. GLfloat LightAmbient[] = {0.5f, 0.5f, 0.5f, 1.0f};  //环境光参数
  12. GLfloat LightDiffuse[] = {1.0f, 1.0f, 1.0f, 1.0f};  //漫散光参数
  13. GLfloat LightPosition[] = {0.0f, 0.0f, 2.0f, 1.0f}; //光源位置
  14. glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient);     //设置环境光
  15. glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse);     //设置漫射光
  16. glLightfv(GL_LIGHT1, GL_POSITION, LightPosition);   //设置光源位置
  17. glEnable(GL_LIGHT1);                                //启动一号光源
  18. }

首先我们分别定义环境光参数,漫射光参数以及光源位置。环境光来自于四面八方,所以场景中的对象都处于环境光的照射中;漫射光由特定的光源产生,并在场景中的对象表明产生反射。处于漫射光直接照射下的任何对象表面都变得很亮,而几乎未被照到的区域显得要暗一些。这样我们所创建的木板箱的棱边上就会产生很不错的阴影效果。

创建光源的过程和颜色的创建完全一致,前三个参数分别是RGB三色分量,最后一个是alpha通道参数。最后光源位置前三个参数和glTranslate中的一样,一次表示x、y、z轴上的位移,最后一个参数取为1.0f,这将告诉OpenGL这里指定的坐标就是光源的位置,以后的教程中我会多加解释。

接着开始设置光源,使得光源GL_LIGHT1开始发光,然后是设置光源位置(位于木箱原中心在z方向移向观察者2.0单位),最后我们启用一号光源。要注意的是,我们还没有启用GL_LIGHTING,所以是看不见任何光线的。记住,只对光源进行设置、定位、甚至启用,光源都不会工作,除非我们启用GL_LIGHTING。

还有是对paintGL()函数的修改,修改后具体代码如下:

  1. void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
  2. {
  3. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
  4. glLoadIdentity();                                   //重置模型观察矩阵
  5. glTranslatef(0.0f, 0.0f, m_Deep);                   //移入屏幕
  6. glRotatef(m_xRot, 1.0f, 0.0f, 0.0f);                //绕x轴旋转
  7. glRotatef(m_yRot, 0.0f, 1.0f, 0.0f);                //绕y轴旋转
  8. glBindTexture(GL_TEXTURE_2D, m_Texture);            //选择纹理
  9. glBegin(GL_QUADS);                                  //开始绘制立方体
  10. glNormal3f(0.0f, 1.0f, 0.0f);
  11. glTexCoord2f(1.0f, 1.0f);
  12. glVertex3f(1.0f, 1.0f, -1.0f);                  //右上(顶面)
  13. glTexCoord2f(0.0f, 1.0f);
  14. glVertex3f(-1.0f, 1.0f, -1.0f);                 //左上(顶面)
  15. glTexCoord2f(0.0f, 0.0f);
  16. glVertex3f(-1.0f, 1.0f, 1.0f);                  //左下(顶面)
  17. glTexCoord2f(1.0f, 0.0f);
  18. glVertex3f(1.0f, 1.0f, 1.0f);                   //右下(顶面)
  19. glNormal3f(0.0f, -1.0f, 0.0f);
  20. glTexCoord2f(0.0f, 0.0f);
  21. glVertex3f(1.0f, -1.0f, 1.0f);                  //右上(底面)
  22. glTexCoord2f(1.0f, 0.0f);
  23. glVertex3f(-1.0f, -1.0f, 1.0f);                 //左上(底面)
  24. glTexCoord2f(1.0f, 1.0f);
  25. glVertex3f(-1.0f, -1.0f, -1.0f);                //左下(底面)
  26. glTexCoord2f(0.0f, 1.0f);
  27. glVertex3f(1.0f, -1.0f, -1.0f);                 //右下(底面)
  28. glNormal3f(0.0f, 0.0f, 1.0f);
  29. glTexCoord2f(1.0f, 1.0f);
  30. glVertex3f(1.0f, 1.0f, 1.0f);                   //右上(前面)
  31. glTexCoord2f(0.0f, 1.0f);
  32. glVertex3f(-1.0f, 1.0f, 1.0f);                  //左上(前面)
  33. glTexCoord2f(0.0f, 0.0f);
  34. glVertex3f(-1.0f, -1.0f, 1.0f);                 //左下(前面)
  35. glTexCoord2f(1.0f, 0.0f);
  36. glVertex3f(1.0f, -1.0f, 1.0f);                  //右下(前面)
  37. glNormal3f(0.0f, 0.0f, -1.0f);
  38. glTexCoord2f(0.0f, 0.0f);
  39. glVertex3f(1.0f, -1.0f, -1.0f);                 //右上(后面)
  40. glTexCoord2f(1.0f, 0.0f);
  41. glVertex3f(-1.0f, -1.0f, -1.0f);                //左上(后面)
  42. glTexCoord2f(1.0f, 1.0f);
  43. glVertex3f(-1.0f, 1.0f, -1.0f);                 //左下(后面)
  44. glTexCoord2f(0.0f, 1.0f);
  45. glVertex3f(1.0f, 1.0f, -1.0f);                  //右下(后面)
  46. glNormal3f(-1.0f, 0.0f, 0.0f);
  47. glTexCoord2f(1.0f, 1.0f);
  48. glVertex3f(-1.0f, 1.0f, 1.0f);                  //右上(左面)
  49. glTexCoord2f(0.0f, 1.0f);
  50. glVertex3f(-1.0f, 1.0f, -1.0f);                 //左上(左面)
  51. glTexCoord2f(0.0f, 0.0f);
  52. glVertex3f(-1.0f, -1.0f, -1.0f);                //左下(左面)
  53. glTexCoord2f(1.0f, 0.0f);
  54. glVertex3f(-1.0f, -1.0f, 1.0f);                 //右下(左面)
  55. glNormal3f(1.0f, 0.0f, 0.0f);
  56. glTexCoord2f(1.0f, 1.0f);
  57. glVertex3f(1.0f, 1.0f, -1.0f);                  //右上(右面)
  58. glTexCoord2f(0.0f, 1.0f);
  59. glVertex3f(1.0f, 1.0f, 1.0f);                   //左上(右面)
  60. glTexCoord2f(0.0f, 0.0f);
  61. glVertex3f(1.0f, -1.0f, 1.0f);                  //左下(右面)
  62. glTexCoord2f(1.0f, 0.0f);
  63. glVertex3f(1.0f, -1.0f, -1.0f);                 //右下(右面)
  64. glEnd();                                            //立方体绘制结束
  65. m_xRot += m_xSpeed;                                 //x轴旋转
  66. m_yRot += m_ySpeed;                                 //y轴旋转
  67. }

除了旋转及移动上作了修改外(相信大家能看懂),多了glNormal3f()函数的调用。该函数指定一条法线,法线告诉OpenGL这个多边形的朝向,并指明多边形的正面和背面,如果没有法线,什么怪事情都可能发生:不该亮的面被照亮了,多边形的背面也被照亮了…还要注意的是,法线应指向多边形的外侧。
最后两行代码作了一定的修改,利用变量m_xSpeed、m_ySpeed来控制立方体的旋转速度。

最后当然就是键盘控制了,具体代码如下(相信大家结合注释可以很容易看懂):

  1. void MyGLWidget::keyPressEvent(QKeyEvent *event)
  2. {
  3. switch (event->key())
  4. {
  5. case Qt::Key_F1:                                    //F1为全屏和普通屏的切换键
  6. fullscreen = !fullscreen;
  7. if (fullscreen)
  8. {
  9. showFullScreen();
  10. }
  11. else
  12. {
  13. showNormal();
  14. }
  15. break;
  16. case Qt::Key_Escape:                                //ESC为退出键
  17. close();
  18. break;
  19. case Qt::Key_L:                                     //L为开启关闭光源的切换键
  20. m_Light = !m_Light;
  21. if (m_Light)
  22. {
  23. glEnable(GL_LIGHTING);                      //开启光源
  24. }
  25. else
  26. {
  27. glDisable(GL_LIGHTING);                     //关闭光源
  28. }
  29. break;
  30. case Qt::Key_PageUp:                                //PageUp按下使木箱移向屏幕内部
  31. m_Deep -= 0.1f;
  32. break;
  33. case Qt::Key_PageDown:                              //PageDown按下使木箱移向观察者
  34. m_Deep += 0.1f;
  35. break;
  36. case Qt::Key_Up:                                    //Up按下减少m_xSpeed
  37. m_xSpeed -= 0.1f;
  38. break;
  39. case Qt::Key_Down:                                  //Down按下增加m_xSpeed
  40. m_xSpeed += 0.1f;
  41. break;
  42. case Qt::Key_Right:                                 //Right按下减少m_ySpeed
  43. m_ySpeed -= 0.1f;
  44. break;
  45. case Qt::Key_Left:                                  //Left按下增加m_ySpeed
  46. m_ySpeed += 0.1f;
  47. break;
  48. }
  49. }

现在就可以运行程序查看效果了!

第08课:混合 (参照NeHe)

这次教程中,我们将在纹理映射的基础上加上混合,使它看起来具有透明的效果,当然解释它不是那么容易但代码并不难,希望你喜欢它。

OpenGL中的绝大多数特效都与某些类型的(色彩)混合有关。混色的定义为,将某个像素的颜色和已绘制在屏幕上与其对应的像素颜色相互结合。至于如何结合这两种颜色则依赖于颜色的alpha通道的分量值,以及所用的混色函数。Alpha通常是位于颜色值末尾的第4个颜色组成分量,一般都认为Alpha分量代表材料的透明度。也就是说,alpha值为0.0时所代表的材料是完全透明的,alpha值为1.0时所代表的材料则是完全不透明的。

在OpenGL中实现混色的步骤类似于我们以前提到的OpenGL过程,接着设置公式,并在绘制透明对象时关闭写深度缓存。因为我们想在半透明的图形背后绘制对象,这不是正确的混色方法,但绝大多数时候这种做法在简单的项目中都工作得很好。正确的混色过程应该是先绘制全部非透明场景之后,再绘制透明的图形,并且要按照与深度缓存相反的次序来绘制(先画最远的物体)。

程序运行时效果如下:

下面进入教程:

我们这次将在第07课的基础上修改代码,首先打开myglwidget.h文件,增加一个布尔变量m_Blend来记录是否开启混合,修改后代码如下:

  1. #ifndef MYGLWIDGET_H
  2. #define MYGLWIDGET_H
  3. #include <QWidget>
  4. #include <QGLWidget>
  5. class MyGLWidget : public QGLWidget
  6. {
  7. Q_OBJECT
  8. public:
  9. explicit MyGLWidget(QWidget *parent = 0);
  10. ~MyGLWidget();
  11. protected:
  12. //对3个纯虚函数的重定义
  13. void initializeGL();
  14. void resizeGL(int w, int h);
  15. void paintGL();
  16. void keyPressEvent(QKeyEvent *event);           //处理键盘按下事件
  17. private:
  18. bool fullscreen;                                //是否全屏显示
  19. QString m_FileName;                             //图片的路径及文件名
  20. GLuint m_Texture;                               //储存一个纹理
  21. bool m_Light;                                   //光源的开/关
  22. bool m_Blend;                                   //是否混合
  23. GLfloat m_xRot;                                 //x旋转角度
  24. GLfloat m_yRot;                                 //y旋转角度
  25. GLfloat m_xSpeed;                               //x旋转速度
  26. GLfloat m_ySpeed;                               //y旋转速度
  27. GLfloat m_Deep;                                 //深入屏幕的距离
  28. };
  29. #endif // MYGLWIDGET_H

接下来打开myglwidget.cpp文件,加上声明#include <QTimer>,在构造函数中对增加变量进行初始化并更换图片,使用不同的纹理来绘画立方体,具体修改后代码如下:

  1. MyGLWidget::MyGLWidget(QWidget *parent) :
  2. QGLWidget(parent)
  3. {
  4. fullscreen = false;
  5. m_FileName = "D:/QtOpenGL/QtImage/Glass.bmp";        //应根据实际存放图片的路径进行修改
  6. m_Light = false;
  7. m_Blend = false;
  8. m_xRot = 0.0f;
  9. m_yRot = 0.0f;
  10. m_xSpeed = 0.0f;
  11. m_ySpeed = 0.0f;
  12. m_Deep = -5.0f;
  13. QTimer *timer = new QTimer(this);                   //创建一个定时器
  14. //将定时器的计时信号与updateGL()绑定
  15. connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
  16. timer->start(10);                                   //以10ms为一个计时周期
  17. }

然后就要进入重点的混合,其他代码非常简单,并不像解释它时那么麻烦,只需要对initializeGL()作一定的修改,具体代码如下:

  1. void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
  2. {
  3. m_Texture = bindTexture(QPixmap(m_FileName));       //载入位图并转换成纹理
  4. glEnable(GL_TEXTURE_2D);                            //启用纹理映射
  5. glClearColor(0.0f, 0.0f, 0.0f, 0.0f);               //黑色背景
  6. glShadeModel(GL_SMOOTH);                            //启用阴影平滑
  7. glClearDepth(1.0);                                  //设置深度缓存
  8. glEnable(GL_DEPTH_TEST);                            //启用深度测试
  9. glDepthFunc(GL_LEQUAL);                             //所作深度测试的类型
  10. glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  //告诉系统对透视进行修正
  11. //下面是光源部分
  12. GLfloat LightAmbient[] = {0.5f, 0.5f, 0.5f, 1.0f};  //环境光参数
  13. GLfloat LightDiffuse[] = {1.0f, 1.0f, 1.0f, 1.0f};  //漫散光参数
  14. GLfloat LightPosition[] = {0.0f, 0.0f, 2.0f, 1.0f}; //光源位置
  15. glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient);     //设置环境光
  16. glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse);     //设置漫射光
  17. glLightfv(GL_LIGHT1, GL_POSITION, LightPosition);   //设置光源位置
  18. glEnable(GL_LIGHT1);                                //启动一号光源
  19. //下面是混合部分
  20. glColor4f(1.0f, 1.0f, 1.0f, 0.5f);                  //全亮度,50%Alpha混合
  21. glBlendFunc(GL_SRC_ALPHA, GL_ONE);                  //基于源像素alpah通道值得半透明混合函数
  22. }

增加了两行代码,第一行以全亮度绘制此物体,并对其进行50%的alpha混合(半透明),当混合选项开启时,次物体将会产生50%的透明效果。第二行设置所采用的混合类型。看,代码真的挺简单的。

最后是键盘控制的代码,具体代码如下:

  1. void MyGLWidget::keyPressEvent(QKeyEvent *event)
  2. {
  3. switch (event->key())
  4. {
  5. case Qt::Key_F1:                                    //F1为全屏和普通屏的切换键
  6. fullscreen = !fullscreen;
  7. if (fullscreen)
  8. {
  9. showFullScreen();
  10. }
  11. else
  12. {
  13. showNormal();
  14. }
  15. break;
  16. case Qt::Key_Escape:                                //ESC为退出键
  17. close();
  18. break;
  19. case Qt::Key_B:                                     //B为开始关闭混合而对切换键
  20. m_Blend = !m_Blend;
  21. if (m_Blend)
  22. {
  23. glEnable(GL_BLEND);                         //开启混合
  24. glDisable(GL_DEPTH_TEST);                   //关闭深度测试
  25. }
  26. else
  27. {
  28. glDisable(GL_BLEND);                        //关闭混合
  29. glEnable(GL_DEPTH_TEST);                    //打开深度测试
  30. }
  31. break;
  32. case Qt::Key_L:                                     //L为开启关闭光源的切换键
  33. m_Light = !m_Light;
  34. if (m_Light)
  35. {
  36. glEnable(GL_LIGHTING);                      //开启光源
  37. }
  38. else
  39. {
  40. glDisable(GL_LIGHTING);                     //关闭光源
  41. }
  42. break;
  43. case Qt::Key_PageUp:                                //PageUp按下使木箱移向屏幕内部
  44. m_Deep -= 0.1f;
  45. break;
  46. case Qt::Key_PageDown:                              //PageDown按下使木箱移向观察者
  47. m_Deep += 0.1f;
  48. break;
  49. case Qt::Key_Up:                                    //Up按下减少m_xSpeed
  50. m_xSpeed -= 0.1f;
  51. break;
  52. case Qt::Key_Down:                                  //Down按下增加m_xSpeed
  53. m_xSpeed += 0.1f;
  54. break;
  55. case Qt::Key_Right:                                 //Right按下减少m_ySpeed
  56. m_ySpeed -= 0.1f;
  57. break;
  58. case Qt::Key_Left:                                  //Left按下增加m_ySpeed
  59. m_ySpeed += 0.1f;
  60. break;
  61. }
  62. }

当B键的控制机制与L键相似,但注意到,开启混合时还要关闭深度测试,关闭混合时还要打开深度测试,否则将发现立方体有一些面不见了!

现在就可以运行程序看效果了!

第09课:在3D空间中移动位图

想知道如何在3D空间中移动物体,想知道如何在屏幕上绘制一个图像,而让图像的背景色变为透明,希望有一个简单的动画。这次教程中将教会你所以的一切。当然,这一课是在前面几课知识的基础上创建的,请确保你已经掌握了前面几课的知识,再进入本课教程。

欢迎进入这次教程,这一课将是前面几课的综合。前面的学习中,我们学会了设置一个OpenGL窗口的每个细节,学会在旋转的物体上贴图并打上光线以及混色(透明)处理。这一课中,我们将在3D场景中移动位图,并去除位图上的黑色像素(使用混色)。接着为黑白纹理上色,最后我们将学会创建丰富的色彩,并把混合了不同色彩的纹理相互混合,得到简单的动画效果。

程序运行时效果如下:

下面进入教程:

我们这次将在第01课的基础上修改代码,其中一些与前几课重复的地方我不作过多解释。首先打开myglwdiget.h文件,将类声明更改如下:

  1. #ifndef MYGLWIDGET_H
  2. #define MYGLWIDGET_H
  3. #include <QWidget>
  4. #include <QGLWidget>
  5. class MyGLWidget : public QGLWidget
  6. {
  7. Q_OBJECT
  8. public:
  9. explicit MyGLWidget(QWidget *parent = 0);
  10. ~MyGLWidget();
  11. protected:
  12. //对3个纯虚函数的重定义
  13. void initializeGL();
  14. void resizeGL(int w, int h);
  15. void paintGL();
  16. void keyPressEvent(QKeyEvent *event);           //处理键盘按下事件
  17. private:
  18. bool fullscreen;                                //是否全屏显示
  19. QString m_FileName;                             //图片的路径及文件名
  20. GLuint m_Texture;                               //储存一个纹理
  21. bool m_Twinkle;                                 //星星是否闪烁
  22. static const int num = 50;                      //星星的数目
  23. struct star{                                    //为星星创建的结构体
  24. int r, g, b;                                //星星的颜色
  25. GLfloat dist;                               //星星距离中心的距离
  26. GLfloat angle;                              //当前星星所处的角度
  27. } m_stars[num];
  28. GLfloat m_Deep;                                 //星星离观察者的距离
  29. GLfloat m_Tilt;                                 //星星的倾角
  30. GLfloat m_Spin;                                 //星星的自转
  31. };
  32. #endif // MYGLWIDGET_H

首先是一个布尔变量m_Twinkle用来表示星星是否闪烁。然后我们创建了一个星星的结构体,结构体包含星星的颜色,离中心距离以及所处角度,并创建了一个大小为50的星星数组。最后三个浮点变量依次表示星星离观察者距离,星星的倾角,星星的自转,这三个浮点变量用于对整体视图的控制。

接下来,我们还是打开myglwidget.cpp,加上声明#include <QTimer>,在构造函数中对新增变量进行初始化,只解释小部分,希望大家结合注释可以理解,代码如下:

  1. MyGLWidget::MyGLWidget(QWidget *parent) :
  2. QGLWidget(parent)
  3. {
  4. fullscreen = false;
  5. m_FileName = "D:/QtOpenGL/QtImage/Star.bmp";        //应根据实际存放图片的路径进行修改
  6. m_Twinkle = false;                                  //默认初始状态为不闪烁
  7. for (int i=0; i<num; i++)                           //循环初始化所有的星星
  8. {
  9. //随机获得星星颜色
  10. m_stars[i].r = rand() % 256;
  11. m_stars[i].g = rand() % 256;
  12. m_stars[i].b = rand() % 256;
  13. m_stars[i].dist = ((float)i / num) * 5.0f;      //计算星星离中心的距离,最大半径为5.0
  14. m_stars[i].angle = 0.0f;                        //所以星星都从0度开始旋转
  15. }
  16. m_Deep = -15.0f;                                    //深入屏幕15.0单位
  17. m_Tilt = 90.0f;                                     //初始倾角为90度(面对观察者)
  18. m_Spin = 0.0f;                                      //从0度开始自转
  19. QTimer *timer = new QTimer(this);                   //创建一个定时器
  20. //将定时器的计时信号与updateGL()绑定
  21. connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
  22. timer->start(10);                                   //以10ms为一个计时周期
  23. }

利用循环对星星的数据进行初始化,第i 颗星星离中心的距离是将i 的值除以星星的总数,然后乘上5.0f。基本上这样使得后一颗星星比前一颗星星离中心更远一点,这样当i = 50时,就刚好达到最大半径5.0f了。然后我们选择颜色都是从0~255之间取一个随机数,为何这里不是通常的0.0f~1.0f呢?这里我们使用的颜色设置函数时glColor4ub,而不是之前的glColor4f,ub意味着参数是Unsigned Byte型的,同时这里去随机数整数似乎要比取一个浮点的随机数更容易一些。

然后我们要对initializeGL()函数作一定的修改,修改后代码如下:

  1. void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
  2. {
  3. m_Texture = bindTexture(QPixmap(m_FileName));
  4. glEnable(GL_TEXTURE_2D);
  5. glClearColor(0.0, 0.0, 0.0, 0.0);                   //黑色背景
  6. glShadeModel(GL_SMOOTH);                            //启用阴影平滑
  7. glClearDepth(1.0);                                  //设置深度缓存
  8. glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  //告诉系统对透视进行修正
  9. glBlendFunc(GL_SRC_ALPHA, GL_ONE);                  //设置混色函数取得半透明效果
  10. glEnable(GL_BLEND);                                 //开启混合(混色)
  11. }

这里我们不打算使用深度测试,如果你使用第01课的代码的话,请确认是否已经去掉了glDepthFunc(GL_LEQUAL);和glEnable(GL_DEPTH_TEST);两行。否则,你所见到的最终效果会一团糟。这里我们使用了纹理映射,因此请你确认你已经加入了这些这一课中所没有的代码。同样要注意的是我们也开启了混合(混色),这是为了给纹理上色,产生不同颜色的星星。

还有就是最重点的paintGL()函数,我会一一作出解释,具体代码如下:

  1. void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
  2. {
  3. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
  4. glBindTexture(GL_TEXTURE_2D, m_Texture);            //选择纹理
  5. for (int i=0; i<num; i++)
  6. {
  7. glLoadIdentity();                               //绘制每颗星星之前,重置模型观察矩阵
  8. glTranslatef(0.0f, 0.0f, m_Deep);               //深入屏幕里面
  9. glRotatef(m_Tilt, 1.0f, 0.0f, 0.0f);            //倾斜视角
  10. glRotatef(m_stars[i].angle, 0.0f, 1.0f, 0.0f);  //旋转至当前所画星星的角度
  11. glTranslatef(m_stars[i].dist, 0.0f, 0.0f);      //沿x轴正向移动
  12. glRotatef(-m_stars[i].angle, 0.0f, 1.0f, 0.0f); //取消当前星星的角度
  13. glRotatef(-m_Tilt, 1.0f, 0.0f, 0.0f);           //取消视角倾斜
  14. if (m_Twinkle)                                  //启动闪烁效果
  15. {
  16. //使用byte型数据值指定一个颜色
  17. glColor4ub(m_stars[num-i-1].r, m_stars[num-i-1].g, m_stars[num-i-1].b, 255);
  18. glBegin(GL_QUADS);                          //开始绘制纹理映射过的四边形
  19. glTexCoord2f(0.0f, 0.0f);
  20. glVertex3f(-1.0f, -1.0f, 0.0f);
  21. glTexCoord2f(1.0f, 0.0f);
  22. glVertex3f(1.0f, -1.0f, 0.0f);
  23. glTexCoord2f(1.0f, 1.0f);
  24. glVertex3f(1.0f, 1.0f, 0.0f);
  25. glTexCoord2f(0.0f, 1.0f);
  26. glVertex3f(-1.0f, 1.0f, 0.0f);
  27. glEnd();                                    //四边形绘制结束
  28. }
  29. glRotatef(m_Spin, 0.0f, 0.0f, 1.0f);            //绕z轴旋转星星
  30. //使用byte型数据值指定一个颜色
  31. glColor4ub(m_stars[i].r, m_stars[i].g, m_stars[i].b, 255);
  32. glBegin(GL_QUADS);                          //开始绘制纹理映射过的四边形
  33. glTexCoord2f(0.0f, 0.0f);
  34. glVertex3f(-1.0f, -1.0f, 0.0f);
  35. glTexCoord2f(1.0f, 0.0f);
  36. glVertex3f(1.0f, -1.0f, 0.0f);
  37. glTexCoord2f(1.0f, 1.0f);
  38. glVertex3f(1.0f, 1.0f, 0.0f);
  39. glTexCoord2f(0.0f, 1.0f);
  40. glVertex3f(-1.0f, 1.0f, 0.0f);
  41. glEnd();                                    //四边形绘制结束
  42. m_Spin += 0.01f;                            //星星的自转
  43. m_stars[i].angle += (float)i / num;         //改变星星的公转角度
  44. m_stars[i].dist -= 0.01f;                   //改变星星离中心的距离
  45. if (m_stars[i].dist < 0.0f)                 //星星到达中心了么
  46. {
  47. m_stars[i].dist += 5.0f;                //往外移5.0单位
  48. m_stars[i].r = rand() % 256;
  49. m_stars[i].g = rand() % 256;
  50. m_stars[i].b = rand() % 256;
  51. }
  52. }
  53. }

首先是清屏和绑定纹理,接着进入循环,画每颗星星前当然要重置模型观察矩阵并进行视图的移动旋转,然后我们来移动星星。我们要做的第一件事是把场景沿y轴旋转。如果我们旋转90度的话,x轴就不再是从左到右的了,它将从里到外穿出屏幕。第二行代码沿x轴移动一个正值,通常这样代表移向了屏幕的右侧,但由于我们绕y轴旋转了坐标系,x轴的正向可以使任意方向。因此,当我们沿x轴正向移动时,可能向左、向右、向前、向后。

接着的代码带一点小技巧。我们绘制的星星实际上是一个平面的纹理,现在我们在屏幕中心画了个平面的四边形然后贴上纹理,这看起来很不错。但是当我们绕着y轴转上个90度的话,纹理在屏幕上就只剩下右侧和左侧的两条边朝着我们了,看起来就是一条细线,这不并不是我们所想要的,我们希望星星永远正面朝着我们。因此,在绘制星星之前,我们通过逆序旋转来抵消之前对星星所作的任何旋转,当然旋转的角度就要加上- 号了。

然后到了if 条件从句,如果m_Twinkle为TRUE,我们在屏幕上先画一次不旋转的星星,当我们画第i颗星星时,将采用第num-i-1颗星星的颜色使得颜色不同。由于开启了m_Twinkle,每颗星星最后会被绘制两遍,两遍绘制的星星颜色相互融合,会产生很棒的效果,看起来比原来亮了许多。值得注意的是,给纹理上色是件很容易的事,尽管纹理本身是黑白的,纹理将变成我们在绘制它之前选定的任意颜色。if 条件从句后,我们要绘制第二遍的星星,和前面不同的是,这一遍的星星肯定会被绘制,并且这次的星星绕着z轴旋转(星星的自转)。

后面的代码代表星星的运动,我们增加m_Spin的值来控制星星自转,然后将每颗星星的公转角度增加 i/num这使得离中心更远的星星转得更快,最后减少每颗星星离屏幕中心的距离,这样看起来星星们好像被不断地吸入屏幕的中心。

最后几行是检查星星是否已经碰到了屏幕中心。当星星碰到屏幕中心时,我们为它赋上新颜色,然后往外移5.0单位,这颗星星将重新踏上回归屏幕中心的旅程。

最后就是键盘控制部分了,具体代码如下(相信大家结合注释可以很容易看懂):

  1. void MyGLWidget::keyPressEvent(QKeyEvent *event)
  2. {
  3. switch (event->key())
  4. {
  5. case Qt::Key_F1:                                //F1为全屏和普通屏的切换键
  6. fullscreen = !fullscreen;
  7. if (fullscreen)
  8. {
  9. showFullScreen();
  10. }
  11. else
  12. {
  13. showNormal();
  14. }
  15. updateGL();
  16. break;
  17. case Qt::Key_Escape:                            //ESC为退出键
  18. close();
  19. break;
  20. case Qt::Key_T:                                 //T为星星开启关闭闪烁的切换键
  21. m_Twinkle = !m_Twinkle;
  22. break;
  23. case Qt::Key_Up:                                //Up按下屏幕向上倾斜
  24. m_Tilt -= 0.5f;
  25. break;
  26. case Qt::Key_Down:                              //Down按下屏幕向下倾斜
  27. m_Tilt += 0.5f;
  28. break;
  29. case Qt::Key_PageUp:                            //PageUp按下缩小
  30. m_Deep -= 0.1f;
  31. break;
  32. case Qt::Key_PageDown:                          //PageDown按下放大
  33. m_Deep += 0.1f;
  34. break;
  35. }
  36. }

现在就可以运行程序查看效果了!

第10课:加载3D世界,并在其中漫游 (参照NeHe)
这次教程中,我将教大家如何加载一个3D世界,并在3D世界中漫游。这相较于我们只能创造一个旋转的立方体或一群星星时有很大的进步了,当然这节课代码难度不低,但也不会很难,只要你跟着我慢慢一步一步来。
一个3D世界当然不像我们之前那样,只要几个对象就搞定了,因此,我们会选择将3D环境用数据来表达,并存放在一个文本中。随着环境复杂度的上升,这个工作得难度也会随之上升。出于这个原因,我们必须将数据归类,使其具有更多的可操作性风格。后面程序中,我们会把3D世界看作是区段(sector)的集合。一个区段可以是一个房间、一个立方体或者任意一个闭合的空间。
 
程序运行时效果如下:
 
 
下面进入教程:
 
我们这次将在第01课的基础上修改代码,其中一些与前几课重复的地方我不作过多解释。首先打开myglwidget.h文件,将类声明更改如下:
  1. #ifndef MYGLWIDGET_H
  2. #define MYGLWIDGET_H
  3. #include <QWidget>
  4. #include <QGLWidget>
  5. typedef struct tagVERTEX                            //创建Vertex顶点结构体
  6. {
  7. float x, y, z;                                  //3D坐标
  8. float u, v;                                     //纹理坐标
  9. } VERTEX;
  10. typedef struct tagTRIANGLE                          //创建Triangle三角形结构体
  11. {
  12. VERTEX vertexs[3];                              //3个顶点构成一个Triangle
  13. } TRIANGLE;
  14. typedef struct tagSECTOR                            //创建Sector区段结构体
  15. {
  16. int numtriangles;                               //Sector中的三角形个数
  17. QVector<TRIANGLE> vTriangle;                            //储存三角形的向量
  18. } SECTOR;
  19. class MyGLWidget : public QGLWidget
  20. {
  21. Q_OBJECT
  22. public:
  23. explicit MyGLWidget(QWidget *parent = 0);
  24. ~MyGLWidget();
  25. protected:
  26. //对3个纯虚函数的重定义
  27. void initializeGL();
  28. void resizeGL(int w, int h);
  29. void paintGL();
  30. void keyPressEvent(QKeyEvent *event);           //处理键盘按下事件
  31. private:
  32. bool fullscreen;                                //是否全屏显示
  33. QString m_FileName;                             //图片的路径及文件名
  34. GLuint m_Texture;                               //储存一个纹理
  35. QString m_WorldFile;                            //存放世界的路径及文本名
  36. SECTOR m_Sector;                                //储存一个区段的数据
  37. static const float m_PIOVER180 = 0.0174532925f; //实现度和弧度直接的折算
  38. GLfloat m_xPos;                                 //储存当前位置
  39. GLfloat m_zPos;
  40. GLfloat m_yRot;                                 //视角的旋转
  41. GLfloat m_LookUpDown;                           //记录抬头和低头
  42. };
  43. #endif // MYGLWIDGET_H

可以看到我们定义了3个结构体,依次表示顶点,三角形和区段。一个区段包含一系列的多边形(三角形),三角形本质上是由三个以上顶点组合的图形,顶点就是我们最基本的分类单位了。顶点包含了OpenGL真正感兴趣的数据,我们用3D空间中的坐标值(x, y, z)以及它们的纹理坐标(u, v)来定义三角形的每个顶点。这次教程中,我们只加载了一个区段的数据,故只需一个m_Sector数据就够了(当然有兴趣的可以自己设计区段数据,多加载几个看看)。

其他增加的变量,m_PIOVER180就是一个度数和弧度制的折算因子,m_xPos、m_zPos用于记录游戏者的位置,m_yRot用于记录游戏者视角的旋转,m_LookUpDown用于控制游戏者的仰视俯视,简单点说就是抬头低头啦。
 
接下来,我们需要打开myglwidget.cpp,加上声明#include <QTimer>、#include <QTextStream>、#include <QtMath>,在构造函数中对数据进行初始化,具体代码如下:
  1. MyGLWidget::MyGLWidget(QWidget *parent) :
  2. QGLWidget(parent)
  3. {
  4. fullscreen = false;
  5. m_FileName = "D:/QtOpenGL/QtImage/Mud.bmp";         //应根据实际存放图片的路径进行修改
  6. m_WorldFile = "D:/QtOpenGL/QtImage/World.txt";
  7. m_Sector.numtriangles = 0;
  8. QFile file(m_WorldFile);
  9. file.open(QIODevice::ReadOnly | QIODevice::Text);   //将要读入数据的文本打开
  10. QTextStream in(&file);
  11. while (!in.atEnd())
  12. {
  13. QString line[3];
  14. for (int i=0; i<3; i++)                         //循环读入3个点数据
  15. {
  16. do                                          //读入数据并保证数据有效
  17. {
  18. line[i] = in.readLine();
  19. }
  20. while (line[i][0] == '/' || line[i] == "");
  21. }
  22. m_Sector.numtriangles++;                        //每成功读入3个点构成一个三角形
  23. TRIANGLE tempTri;
  24. for (int i=0; i<3; i++)                         //将数据储存于一个三角形中
  25. {
  26. QTextStream inLine(&line[i]);
  27. inLine >> tempTri.vertexs[i].x
  28. >> tempTri.vertexs[i].y
  29. >> tempTri.vertexs[i].z
  30. >> tempTri.vertexs[i].u
  31. >> tempTri.vertexs[i].v;
  32. }
  33. m_Sector.vTriangle.push_back(tempTri);          //将三角形放入m_Sector中
  34. }
  35. file.close();
  36. m_xPos = 0.0f;
  37. m_zPos = 0.0f;
  38. m_yRot = 0.0f;
  39. m_LookUpDown = 0.0f;
  40. QTimer *timer = new QTimer(this);                   //创建一个定时器
  41. //将定时器的计时信号与updateGL()绑定
  42. connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
  43. timer->start(10);                                   //以10ms为一个计时周期
  44. }

我们重点解释中间对于m_Sector的初始化,我们先将文件打开,再利用Qt的文本流一行一行的读取(为何一行一行读,大家看下存放数据的文本文件World.txt就知道了)并保证读入的数据是有效的。每当成功读入三行数据时,说明构成了一个三角形,就创建一个三角形来储存这些数据,并在最后把三角形放入m_Sector中,当然要给m_Sector的numtriangles加上一,说明多了一个三角形。最后录完数据后,关上文件。或者你会想如果有效数据行数不是3的倍数怎么办,这个问题其实已经不是我们的问题了,而且提供的数据文本存在问题,因此不必考虑。接着的数据初始化不作解释了。

 
然后在initializeGL()函数中,请大家修改代码如下(不解释):
  1. void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
  2. {
  3. m_Texture = bindTexture(QPixmap(m_FileName));
  4. glEnable(GL_TEXTURE_2D);
  5. glClearColor(0.0, 0.0, 0.0, 0.0);                   //黑色背景
  6. glShadeModel(GL_SMOOTH);                            //启用阴影平滑
  7. glClearDepth(1.0);                                  //设置深度缓存
  8. glEnable(GL_DEPTH_TEST);                            //启用深度测试
  9. glDepthFunc(GL_LEQUAL);                             //所作深度测试的类型
  10. glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  //告诉系统对透视进行修正
  11. }
任何一个不错的的3D引擎都会允许用户在这个世界中游走和遍历,我们的这个也一样,实现这个功能当然要通过键盘控制。具体实现的途径有一种是直接移动镜头并绘制以镜头为中心的3D环境,但这样会很慢并且不易用代码实现,我们的解决方法如下:
根据用户的指令旋转并变换视角位置。
围绕原点,以与视角相反的旋转方向来旋转世界(让人产生视角旋转的错觉)。
以与视角平移方向相反的方向来平移世界(让人产生视角移动的错觉)。
这样实现起来就简单多了。下面我们先通过键盘控制,来实现平移并旋转视角。
  1. void MyGLWidget::keyPressEvent(QKeyEvent *event)
  2. {
  3. switch (event->key())
  4. {
  5. case Qt::Key_F1:                                    //F1为全屏和普通屏的切换键
  6. fullscreen = !fullscreen;
  7. if (fullscreen)
  8. {
  9. showFullScreen();
  10. }
  11. else
  12. {
  13. showNormal();
  14. }
  15. updateGL();
  16. break;
  17. case Qt::Key_Escape:                                //ESC为退出键
  18. close();
  19. break;
  20. case Qt::Key_PageUp:                                //按下PageUp视角向上转
  21. m_LookUpDown -= 1.0f;
  22. if (m_LookUpDown < -90.0f)
  23. {
  24. m_LookUpDown = -90.0f;
  25. }
  26. break;
  27. case Qt::Key_PageDown:                              //按下PageDown视角向下转
  28. m_LookUpDown += 1.0f;
  29. if (m_LookUpDown > 90.0f)
  30. {
  31. m_LookUpDown = 90.0f;
  32. }
  33. break;
  34. case Qt::Key_Right:                                 //Right按下向左旋转场景
  35. m_yRot -= 1.0f;
  36. break;
  37. case Qt::Key_Left:                                  //Left按下向右旋转场景
  38. m_yRot += 1.0f;
  39. break;
  40. case Qt::Key_Up:                                    //Up按下向前移动
  41. //向前移动分到x、z上的分量
  42. m_xPos -= (float)sin(m_yRot * m_PIOVER180) * 0.05f;
  43. m_zPos -= (float)cos(m_yRot * m_PIOVER180) * 0.05f;
  44. break;
  45. case Qt::Key_Down:                                  //Down按下向后移动
  46. //向后移动分到x、z上的分量
  47. m_xPos += (float)sin(m_yRot * m_PIOVER180) * 0.05f;
  48. m_zPos += (float)cos(m_yRot * m_PIOVER180) * 0.05f;
  49. break;
  50. }
  51. }

这个实现很简单。当左右方向键按下后,旋转变量m_yRot相应的增加或减少。当前后方向键按下时,我们使用sin()和cos()函数计算具体在x和z轴方向上的位移量,使得游戏者能准确的移动。

 
现在我们已经具备了一切所需的数据,可以开始进行步骤2和3了,当然我们也将进入重点的paintGL()函数。虽然重点,但代码并不难,具体代码如下:
  1. void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
  2. {
  3. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
  4. glLoadIdentity();                                   //重置当前的模型观察矩阵
  5. GLfloat x_m, y_m, z_m, u_m, v_m;                    //顶点的临时x、y、z、u、v值
  6. GLfloat xTrans = -m_xPos;                           //游戏者沿x轴平移时的大小
  7. GLfloat zTrans = -m_zPos;                           //游戏者沿z轴平移时的大小
  8. GLfloat yTrans = -0.25f;                            //游戏者沿y轴略作平移,使视角准确
  9. GLfloat sceneroty = 360.0f - m_yRot;                //游戏者的旋转
  10. glRotatef(m_LookUpDown, 1.0f, 0.0f, 0.0f);          //抬头低头的旋转
  11. glRotatef(sceneroty, 0.0f, 1.0f, 0.0f);             //根据游戏者正面所对方向所作的旋转
  12. glTranslatef(xTrans, yTrans, zTrans);               //以游戏者为中心平移场景
  13. glBindTexture(GL_TEXTURE_2D, m_Texture);            //绑定纹理
  14. for (int i=0; i<m_Sector.numtriangles; i++)         //遍历所有的三角形
  15. {
  16. glBegin(GL_TRIANGLES);                          //开始绘制三角形
  17. glNormal3f(0.0f, 0.0f, 1.0f);               //指向前面的法线
  18. x_m = m_Sector.vTriangle[i].vertexs[0].x;
  19. y_m = m_Sector.vTriangle[i].vertexs[0].y;
  20. z_m = m_Sector.vTriangle[i].vertexs[0].z;
  21. u_m = m_Sector.vTriangle[i].vertexs[0].u;
  22. v_m = m_Sector.vTriangle[i].vertexs[0].v;
  23. glTexCoord2f(u_m, v_m);
  24. glVertex3f(x_m, y_m, z_m);
  25. x_m = m_Sector.vTriangle[i].vertexs[1].x;
  26. y_m = m_Sector.vTriangle[i].vertexs[1].y;
  27. z_m = m_Sector.vTriangle[i].vertexs[1].z;
  28. u_m = m_Sector.vTriangle[i].vertexs[1].u;
  29. v_m = m_Sector.vTriangle[i].vertexs[1].v;
  30. glTexCoord2f(u_m, v_m);
  31. glVertex3f(x_m, y_m, z_m);
  32. x_m = m_Sector.vTriangle[i].vertexs[2].x;
  33. y_m = m_Sector.vTriangle[i].vertexs[2].y;
  34. z_m = m_Sector.vTriangle[i].vertexs[2].z;
  35. u_m = m_Sector.vTriangle[i].vertexs[2].u;
  36. v_m = m_Sector.vTriangle[i].vertexs[2].v;
  37. glTexCoord2f(u_m, v_m);
  38. glVertex3f(x_m, y_m, z_m);
  39. glEnd();                                        //三角形绘制结束
  40. }
  41. }

就正如我们之前步骤2和3所说,我们以相反的方式来平移和旋转场景,使得看上去是视角在平移和旋转,然后绑定纹理并绘制出整个场景就完成了!

现在就可以运行程序查看效果了!
 
PS:NeHe教程中有关于移动中视角轻微上下摆动的设定,在这个教程中被我删掉了,由于我发现这个效果并不明显,有兴趣的朋友请看http://www.yakergong.net/nehe/

第11课:旗帜效果(飘动的纹理) (参照NeHe)

这次教程中,我将教大家如何创建一个飘动的旗帜。我们所要创建的旗帜,说白了就是一个以正弦波方式运动的纹理映射图像。虽然不会很难,但效果确实很不错,希望大家能喜欢。当然这次教程是基于第06课的,希望大家确保已经掌握了前6课再进入本次教程。

程序运行时效果如下:

下面进入教程:

我们这次将在第06课的基础上修改代码,我们只会解释增加部分的代码,首先打开myglwidget.h文件,将类声明更改如下:

  1. #ifndef MYGLWIDGET_H
  2. #define MYGLWIDGET_H
  3. #include <QWidget>
  4. #include <QGLWidget>
  5. class MyGLWidget : public QGLWidget
  6. {
  7. Q_OBJECT
  8. public:
  9. explicit MyGLWidget(QWidget *parent = 0);
  10. ~MyGLWidget();
  11. protected:
  12. //对3个纯虚函数的重定义
  13. void initializeGL();
  14. void resizeGL(int w, int h);
  15. void paintGL();
  16. void keyPressEvent(QKeyEvent *event);           //处理键盘按下事件
  17. private:
  18. bool fullscreen;                                //是否全屏显示
  19. GLfloat m_xRot;                                 //绕x轴旋转的角度
  20. GLfloat m_yRot;                                 //绕y轴旋转的角度
  21. GLfloat m_zRot;                                 //绕z轴旋转的角度
  22. QString m_FileName;                             //图片的路径及文件名
  23. GLuint m_Texture;                               //储存一个纹理
  24. float m_Points[45][45][3];                      //储存网格顶点的数组
  25. int m_WiggleCount;                              //用于控制旗帜波浪运动动画
  26. };
  27. #endif // MYGLWIDGET_H

我们增加了m_Points三维数组来存放网格各顶点独立的x、y、z坐标,这里网格由45×45点形成,换句话说也就是由44格×44格的小方格子组成的。另一个新增变量m_WiggleCount用来使产生纹理波浪运动动画,每2帧一次变换波动形状看起来很不错。

接下来,我们需要打开myglwidget.cpp,加上声明#include <QtMath>,在构造函数对新增变量数据进行初始化,具体代码如下:

  1. MyGLWidget::MyGLWidget(QWidget *parent) :
  2. QGLWidget(parent)
  3. {
  4. fullscreen = false;
  5. m_xRot = 0.0f;
  6. m_yRot = 0.0f;
  7. m_zRot = 0.0f;
  8. m_FileName = "D:/QtOpenGL/QtImage/Tim.bmp";         //应根据实际存放图片的路径进行修改
  9. for (int x=0; x<45; x++)                            //初始化数组产生波浪效果(静止)
  10. {
  11. for (int y=0; y<45; y++)
  12. {
  13. m_Points[x][y][0] = float((x / 5.0f) - 4.5f);
  14. m_Points[x][y][1] = float((y / 5.0f) - 4.5f);
  15. m_Points[x][y][2] = float(sin((((x/5.0f)*40.0f)/360.0f)*3.141592654*2.0f));
  16. }
  17. }
  18. m_WiggleCount = 0;
  19. QTimer *timer = new QTimer(this);                   //创建一个定时器
  20. //将定时器的计时信号与updateGL()绑定
  21. connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
  22. timer->start(10);                                   //以10ms为一个计时周期
  23. }

增加的代码就是一个循环,利用循环来添加波浪效果(只是让旗帜看起来有起伏效果,还不能达到波动动画的目的)。值得注意的是,我们在求m_Points[x][y][0]和m_Points[x][y][1]时,都是用x、y除以5.0f,如果除以5的话,由于整数除法取整,会导致画面出现锯齿效果,这显然不是我们想要的。最后减去4.5f这样使得计算结果落在区间[-4.5, 4.5],也就让我们的波浪可以“居中”了。点m_Points[x][y][2]最后的值就是一个sin()函数计算的结果(因为我们模拟的是正弦波运动),×8.0f是求相应角度(360度平分到45个点就是8度一个点了),最后角度转换到弧度制我就不多做解释了。

然后在initializeGL()函数中,请大家修改代码如下:

  1. void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
  2. {
  3. m_Texture = bindTexture(QPixmap(m_FileName));       //载入位图并转换成纹理
  4. glEnable(GL_TEXTURE_2D);                            //启用纹理映射
  5. glClearColor(0.0, 0.0, 0.0, 0.0);                   //黑色背景
  6. glShadeModel(GL_SMOOTH);                            //启用阴影平滑
  7. glClearDepth(1.0);                                  //设置深度缓存
  8. glEnable(GL_DEPTH_TEST);                            //启用深度测试
  9. glDepthFunc(GL_LEQUAL);                             //所作深度测试的类型
  10. glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  //告诉系统对透视进行修正
  11. glPolygonMode(GL_BACK, GL_FILL);                    //后表面完全填充
  12. glPolygonMode(GL_FRONT, GL_LINE);                   //前表面使用线条绘制
  13. }

最后加了两行代码,用来指定使用完全填充模式来填充多边形区域的后表面,而多边形的前表面则使用轮廓线填充,这些方式完全取决于你的个人喜好,这里我们只是为了区分前后表面罢了。

最后,我们将重写整个paintGL()函数,当然这依旧是重点,代码如下:

  1. void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
  2. {
  3. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
  4. glLoadIdentity();                                   //重置当前的模型观察矩阵
  5. glTranslatef(0.0f, 0.0f, -15.0f);                   //移入屏幕15.0单位
  6. glRotatef(m_xRot, 1.0f, 0.0f, 0.0f);                //绕x旋转
  7. glRotatef(m_yRot, 0.0f, 1.0f, 0.0f);                //绕y旋转
  8. glRotatef(m_zRot, 0.0f, 0.0f, 1.0f);                //绕z旋转
  9. glBindTexture(GL_TEXTURE_2D, m_Texture);            //旋转纹理
  10. float flag_x1, flag_y1, flag_x2, flag_y2;           //用来将纹理分割成小的四边形方便纹理映射
  11. glBegin(GL_QUADS);
  12. for (int x=0; x<44; x++)
  13. {
  14. for (int y=0; y<44; y++)
  15. {
  16. //分割纹理
  17. flag_x1 = float(x) / 44.0f;
  18. flag_y1 = float(y) / 44.0f;
  19. flag_x2 = float(x+1) / 44.0f;
  20. flag_y2 = float(y+1) / 44.0f;
  21. //绘制一个小的四边形
  22. glTexCoord2f(flag_x1, flag_y1);
  23. glVertex3f(m_Points[x][y][0], m_Points[x][y][1], m_Points[x][y][2]);
  24. glTexCoord2f(flag_x1, flag_y2);
  25. glVertex3f(m_Points[x][y+1][0], m_Points[x][y+1][1], m_Points[x][y+1][2]);
  26. glTexCoord2f(flag_x2, flag_y2);
  27. glVertex3f(m_Points[x+1][y+1][0], m_Points[x+1][y+1][1], m_Points[x+1][y+1][2]);
  28. glTexCoord2f(flag_x2, flag_y1);
  29. glVertex3f(m_Points[x+1][y][0], m_Points[x+1][y][1], m_Points[x+1][y][2]);
  30. }
  31. }
  32. glEnd();
  33. if (m_WiggleCount == 3)                             //用来变换波浪形状(每2帧一次)产生波浪动画
  34. {
  35. //利用循环使波浪值集体左移,最左侧波浪值到了最右侧
  36. for (int y=0; y<45; y++)
  37. {
  38. float temp = m_Points[0][y][2];
  39. for (int x=0; x<44; x++)
  40. {
  41. m_Points[x][y][2] = m_Points[x+1][y][2];
  42. }
  43. m_Points[44][y][2] = temp;
  44. }
  45. m_WiggleCount = 0;                              //计数器清零
  46. }
  47. m_WiggleCount++;                                    //计数器加一
  48. m_xRot += 0.3f;
  49. m_yRot += 0.2f;
  50. m_zRot += 0.4f;
  51. }

我们创建了四个浮点临时变量并利用循环和除法,将纹理分割成小的四边形,使得我们能准确的对应进行纹理映射,然后画出全部的四边形拼到一起就是一个波动状态的旗帜了。

接着我们判断一下m_WiggleCount是否为2,如果是,就将波浪值m_Points[x][y][2]集体循环左移(最左侧波浪值会到最右侧)。这样我们相当于每2帧一次变化了旗帜的波动状态,看起来就是一个飘动的旗帜,不是静止的了(大家可以尝试着注释掉某一部分代码看看发生什么改变)。然后计数器清零加一什么的就不过多解释了!

现在就可以运行程序查看效果了!

第12课:显示列表 (参照NeHe)
想知道如何加速我们的OpenGL程序么?这次教程中,我将告诉你如何使用OpenGL的显示列表,它通过预编译OpenGL命令来加速我们的程序,并可以为我们省去很多重复的代码,听起来是不是很棒呢!
当我们在制作游戏里的小行星场景时,每一层至少需要两个行星,你可以用OpenGL中的多边形来构造每一个行星。但要知道每次把行星画到屏幕上都是很麻烦的,当我们面临复杂的场景时,要靠代码的绘画方式一个个画出所有的行星,这对于绝大多数人来说都是一个噩梦。那么,解决办法是什么呢?用显示列表,我们只需要一次性建立物体,可以贴图,用颜色,想怎么弄就怎么弄。然后给显示列表一个名字,比如给小行星的显示列表命名为“asteroid”。现在,任何时候,我们想在屏幕上画出行星,我们只需要调好位置后,调用glCallList(asteroid),之前做好的小行星就会立刻显示在屏幕上了。由于小行星已经在显示列表里建造好了,OpenGL不会再计算如何构造它。它已经在内存中建造好了,这将大大降低CPU的使用,让你的程序跑得更快。
 
程序运行时效果如下:
 
下面进入教程:
 
我们这次同样将在第06课的基础上修改代码,我们只会解释增加部分的代码,首先打开myglwidget.h文件,将类声明更改如下:
  1. #ifndef MYGLWIDGET_H
  2. #define MYGLWIDGET_H
  3. #include <QWidget>
  4. #include <QGLWidget>
  5. class MyGLWidget : public QGLWidget
  6. {
  7. Q_OBJECT
  8. public:
  9. explicit MyGLWidget(QWidget *parent = 0);
  10. ~MyGLWidget();
  11. protected:
  12. //对3个纯虚函数的重定义
  13. void initializeGL();
  14. void resizeGL(int w, int h);
  15. void paintGL();
  16. void keyPressEvent(QKeyEvent *event);           //处理键盘按下事件
  17. private:
  18. void buildLists();                              //初始化盒子的显示列表
  19. private:
  20. bool fullscreen;                                //是否全屏显示
  21. GLfloat m_xRot;                                 //绕x轴旋转的角度
  22. GLfloat m_yRot;                                 //绕y轴旋转的角度
  23. QString m_FileName;                             //图片的路径及文件名
  24. GLuint m_Texture;                               //储存一个纹理
  25. GLuint m_Box;                                   //保存盒子的显示列表
  26. GLuint m_Top;                                   //保存盒子顶部的显示列表
  27. };
  28. #endif // MYGLWIDGET_H

我们新增了两个用于显示列表的变量m_Box、m_Top,这两个变量是用于储存指向显示列表的指针。另外我们多了一个buildLists()函数,这个函数是用于初始化两个显示列表的(注意我去掉了变量m_zRot,但其实影响不大)。

 
接下来,我们需要打开myglwidget.cpp,修改构造函数同时添加buildLists()函数的定义,具体代码如下:
  1. MyGLWidget::MyGLWidget(QWidget *parent) :
  2. QGLWidget(parent)
  3. {
  4. fullscreen = false;
  5. m_xRot = 0.0f;
  6. m_yRot = 0.0f;
  7. m_FileName = "D:/QtOpenGL/QtImage/Cube.bmp";        //应根据实际存放图片的路径进行修改
  8. QTimer *timer = new QTimer(this);                   //创建一个定时器
  9. //将定时器的计时信号与updateGL()绑定
  10. connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
  11. timer->start(10);                                   //以10ms为一个计时周期
  12. }
 
  1. void MyGLWidget::buildLists()                           //创建盒子的显示列表
  2. {
  3. m_Box = glGenLists(2);                              //创建两个显示列表的空间
  4. glNewList(m_Box, GL_COMPILE);                       //开始创建第一个显示列表
  5. glBegin(GL_QUADS);
  6. glTexCoord2f(0.0f, 0.0f);
  7. glVertex3f(1.0f, -1.0f, 1.0f);              //右上(底面)
  8. glTexCoord2f(1.0f, 0.0f);
  9. glVertex3f(-1.0f, -1.0f, 1.0f);             //左上(底面)
  10. glTexCoord2f(1.0f, 1.0f);
  11. glVertex3f(-1.0f, -1.0f, -1.0f);            //左下(底面)
  12. glTexCoord2f(0.0f, 1.0f);
  13. glVertex3f(1.0f, -1.0f, -1.0f);             //右下(底面)
  14. glTexCoord2f(1.0f, 1.0f);
  15. glVertex3f(1.0f, 1.0f, 1.0f);               //右上(前面)
  16. glTexCoord2f(0.0f, 1.0f);
  17. glVertex3f(-1.0f, 1.0f, 1.0f);              //左上(前面)
  18. glTexCoord2f(0.0f, 0.0f);
  19. glVertex3f(-1.0f, -1.0f, 1.0f);             //左下(前面)
  20. glTexCoord2f(1.0f, 0.0f);
  21. glVertex3f(1.0f, -1.0f, 1.0f);              //右下(前面)
  22. glTexCoord2f(0.0f, 0.0f);
  23. glVertex3f(1.0f, -1.0f, -1.0f);             //右上(后面)
  24. glTexCoord2f(1.0f, 0.0f);
  25. glVertex3f(-1.0f, -1.0f, -1.0f);            //左上(后面)
  26. glTexCoord2f(1.0f, 1.0f);
  27. glVertex3f(-1.0f, 1.0f, -1.0f);             //左下(后面)
  28. glTexCoord2f(0.0f, 1.0f);
  29. glVertex3f(1.0f, 1.0f, -1.0f);              //右下(后面)
  30. glTexCoord2f(1.0f, 1.0f);
  31. glVertex3f(-1.0f, 1.0f, 1.0f);              //右上(左面)
  32. glTexCoord2f(0.0f, 1.0f);
  33. glVertex3f(-1.0f, 1.0f, -1.0f);             //左上(左面)
  34. glTexCoord2f(0.0f, 0.0f);
  35. glVertex3f(-1.0f, -1.0f, -1.0f);            //左下(左面)
  36. glTexCoord2f(1.0f, 0.0f);
  37. glVertex3f(-1.0f, -1.0f, 1.0f);             //右下(左面)
  38. glTexCoord2f(1.0f, 1.0f);
  39. glVertex3f(1.0f, 1.0f, -1.0f);              //右上(右面)
  40. glTexCoord2f(0.0f, 1.0f);
  41. glVertex3f(1.0f, 1.0f, 1.0f);               //左上(右面)
  42. glTexCoord2f(0.0f, 0.0f);
  43. glVertex3f(1.0f, -1.0f, 1.0f);              //左下(右面)
  44. glTexCoord2f(1.0f, 0.0f);
  45. glVertex3f(1.0f, -1.0f, -1.0f);             //右下(右面)
  46. glEnd();
  47. glEndList();                                        //第一个显示列表结束
  48. m_Top = m_Box + 1;                                  //m_Box+1得到第二个显示列表的指针
  49. glNewList(m_Top, GL_COMPILE);                       //开始创建第二个显示列表
  50. glBegin(GL_QUADS);
  51. glTexCoord2f(1.0f, 1.0f);
  52. glVertex3f(1.0f, 1.0f, -1.0f);              //右上(顶面)
  53. glTexCoord2f(0.0f, 1.0f);
  54. glVertex3f(-1.0f, 1.0f, -1.0f);             //左上(顶面)
  55. glTexCoord2f(0.0f, 0.0f);
  56. glVertex3f(-1.0f, 1.0f, 1.0f);              //左下(顶面)
  57. glTexCoord2f(1.0f, 0.0f);
  58. glVertex3f(1.0f, 1.0f, 1.0f);               //右下(顶面)
  59. glEnd();
  60. glEndList();
  61. }

构造函数不解释了,就删掉了m_zRot的初始化。buildLists()函数中,我们会将创造盒子的代码都放在第一个显示列表里,所有创造顶部的代码都在另一个显示列表里。开始时,我们告诉OpenGL我们要建立两个显示列表,glGenLists(2)创建了两个显示列表的空间,并返回第一个列表的指针,我们把它储存在m_Box中,任何时候我们调用glCallList(m_Box)第一个显示列表就会绘制出来。

接下来我们开始构造第一个显示列表。我们已经申请了两个显示列表的空间了,并且有m_Box指针指向第一个显示列表,所以我们需要做的是告诉OpenGL要建立什么类型的显示列表。我们用glNewList()命令来做这件事情,注意到m_Box是第一个参数,这表示OpenGL将把列表储存到m_Box所指向的内存空间。而第二个参数GL_COMPILE告诉OpenGL我们想预先在内存中构造这个列表,这样每次画的时候就不必重新计算怎么构造物体了。
GL_COMPILE类似于编程。在我们写程序的时候,把它装载到编译器里,我们每次运行程序都需要重新编译。而如果它已经编译成了.exe文件,那么每次我们只需要点击那个.exe文件就可以运行它了,不需要编译。当OpenGL编译过显示列表后,就不需要再每次显示的时候重新编译它了。这就是为什么用显示列表可以加快速度。
下面我们就画了一个没有顶部的盒子,它不会出现在屏幕上,只会储存在显示列表里。我们可以在glNewList()和glEndList()中间加上任何你想加上的代码,可以设置颜色,贴图等等。但是,如果是你想在绘画过程发生改变的代码就不能加进去,这是由于显示列表一旦建立,就不能改变它。比如我们想绘制不同颜色的物体,所以我们加上了glColor3ub(rand()%255, rand()%255, rand()%255),但因为显示列表只会建立一次,所以每次画出来的物体都是同一种颜色,也就是说在储存进列表时glColor3ub的三个参数值就固定下来了。
然后我们用glEndList()命令告诉OpenGL我们已经完成了一个显示列表。在glNewList()和glEndList()之间的任何东西就是显示列表的一部分。接着,我们来建立第二个显示列表,在上一个显示列表的指针上加一,就得到了第二个显示列表的指针,并储存在m_Top中。最后同样建立这个显示列表就不解释了。
 
然后在initializeGL()函数中,将代码修改如下:
  1. void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
  2. {
  3. m_Texture = bindTexture(QPixmap(m_FileName));       //载入位图并转换成纹理
  4. glEnable(GL_TEXTURE_2D);                            //启用纹理映射
  5. buildLists();                                       //创建显示列表
  6. glClearColor(0.0f, 0.0f, 0.0f, 0.0f);               //黑色背景
  7. glShadeModel(GL_SMOOTH);                            //启用阴影平滑
  8. glClearDepth(1.0);                                  //设置深度缓存
  9. glEnable(GL_DEPTH_TEST);                            //启用深度测试
  10. glDepthFunc(GL_LEQUAL);                             //所作深度测试的类型
  11. glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  //告诉系统对透视进行修正
  12. <pre name="code" class="cpp">

glEnable(GL_LIGHT0); //使用默认的0号灯 glEnable(GL_LIGHTING); //使用灯光 glEnable(GL_COLOR_MATERIAL); //使用颜色材质}


我们在启用纹理之后调用了buildLists()函数,创建了显示列表,注意在构造函数中调用buildLists()函数时无法生效的,Qt中使用OpenGL的时候,与内存使用相关的OpenGL函数都需要在initialize()、resize()、paintGL()中直接调用或间接调用,否则无法成功地申请空间。

最后三行使的灯光有效,LIGHT一般是显卡中预先定义过的。最后一行的GL_COLOR_MATERIAL使得我们可以用颜色来贴纹理。如果没有这行代码,纹理将始终保持原来的颜色,glColor3f(r, g,b)就没有用了。
还有就是paintGL()函数,这次看起来就简单许多了(麻烦的我们已经通过参数列表搞定了),具体代码如下:
  1. void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
  2. {
  3. static const GLfloat boxColor[5][3] =               //盒子的颜色数组
  4. {
  5. //亮:红、橙、黄、绿、蓝
  6. {1.0f, 0.0f, 0.0f}, {1.0f, 0.5f, 0.0f}, {1.0f, 1.0f, 0.0f},
  7. {0.0f, 1.0f, 0.0f}, {0.0f, 1.0f, 1.0f}
  8. };
  9. static const GLfloat topColor[5][3] =               //顶部的颜色数组
  10. {
  11. //暗:红、橙、黄、绿、蓝
  12. {0.5f, 0.0f, 0.0f}, {0.5f, 0.25f, 0.0f}, {0.5f, 0.5f, 0.0f},
  13. {0.0f, 0.5f, 0.0f}, {0.0f, 0.5f, 0.5f}
  14. };
  15. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
  16. glBindTexture(GL_TEXTURE_2D, m_Texture);            //选择纹理
  17. for (int y=1; y<6; y++)                             //循环来控制画盒子
  18. {
  19. for (int x=0; x<y; x++)
  20. {
  21. glLoadIdentity();
  22. //设置盒子的位置
  23. glTranslatef(1.4f+(float(x)*2.8f)-(float(y)*1.4f),
  24. ((6.0f-float(y))*2.4f)-7.0f, -20.0f);
  25. glRotatef(45.0f+m_xRot, 1.0f, 0.0f, 0.0f);
  26. glRotatef(45.0f+m_yRot, 0.0f, 1.0f, 0.0f);
  27. glColor3fv(boxColor[y-1]);                  //选择盒子颜色
  28. glCallList(m_Box);                          //绘制盒子
  29. glColor3fv(topColor[y-1]);                  //选择顶部颜色
  30. glCallList(m_Top);                          //绘制顶部
  31. }
  32. }
  33. }

我们一开始就定义了储存盒子和顶部颜色的数组,接着我们用双重循环来画出10个立方体,并在每次画时重置模型观察矩阵,平移和旋转到需要画出立方体位置的中心,具体如何算的我看太懂NeHe的用意就不解释了,反正这并不是重点,我们完全可以根据自己的喜好来摆放这些立方体。

然后我们选择颜色,接着按前面所讲,调用glCallList()就可以画出我们要的立方体了。相比于之前的几课,显示列表让我们的paintGL()函数看起来简单了许多。
 
最后是键盘控制的,比较简单我就不过多解释了,具体代码如下:
  1. void MyGLWidget::keyPressEvent(QKeyEvent *event)
  2. {
  3. switch (event->key())
  4. {
  5. case Qt::Key_F1:                                    //F1为全屏和普通屏的切换键
  6. fullscreen = !fullscreen;
  7. if (fullscreen)
  8. {
  9. showFullScreen();
  10. }
  11. else
  12. {
  13. showNormal();
  14. }
  15. updateGL();
  16. break;
  17. case Qt::Key_Escape:                                //ESC为退出键
  18. close();
  19. break;
  20. case Qt::Key_Left:                                  //Left按下向左旋转
  21. m_yRot -= 1.0f;
  22. break;
  23. case Qt::Key_Right:                                 //Right按下向右旋转
  24. m_yRot += 1.0f;
  25. break;
  26. case Qt::Key_Up:                                    //Up按下向上旋转
  27. m_xRot -= 1.0f;
  28. break;
  29. case Qt::Key_Down:                                  //Down按下向下旋转
  30. m_xRot += 1.0f;
  31. break;
  32. }
  33. }

现在就可以运行程序查看效果了!

第13课:位图字体 (参照NeHe)

这次教程中,我们将创建一些基于2D图像的字体,它们可以缩放平移,但不能旋转,并且总是面向前方,但作为基本的显示来说,我想已经足够了。

或者对于这次教程,你会觉得“在屏幕上显示文字没什么难的”,但是你真正尝试过就会知道,它确实没那么容易。你当然可以把文字写在一个图片上,再把这幅图片载入你的OpenGL程序中,打开混合选项,从而在屏幕上显示出文字。但这种做法非常耗时,而且经常图像会显得模糊。另外,除非你的图像包含一个Alpha通道,否则一旦绘制在屏幕上,那些文字就会不透明(与屏幕中的其他物体混合)。

使用位图字体比起使用图形字体(贴图)看起来不止强100倍,你可以随时改变显示在屏幕上的文字,而且用不着为它们逐个制作贴图。只需要将文字定位,再调用我们即将构建的glPrint()函数就可以在屏幕上显示文字了。

程序运行时效果如下:

下面进入教程:

我们这次将在第01课的基础上修改代码,我会对新增代码一一解释,希望大家能掌握,首先打开myglwidget.h文件,将类声明更改如下:

  1. #ifndef MYGLWIDGET_H
  2. #define MYGLWIDGET_H
  3. #include <QWidget>
  4. #include <QGLWidget>
  5. class MyGLWidget : public QGLWidget
  6. {
  7. Q_OBJECT
  8. public:
  9. explicit MyGLWidget(QWidget *parent = 0);
  10. ~MyGLWidget();
  11. protected:
  12. //对3个纯虚函数的重定义
  13. void initializeGL();
  14. void resizeGL(int w, int h);
  15. void paintGL();
  16. void keyPressEvent(QKeyEvent *event);           //处理键盘按下事件
  17. private:
  18. void buildFont();                               //创建字体
  19. void killFont();                                //删除显示列表
  20. void glPrint(const char *fmt, ...);             //输出字符串
  21. private:
  22. bool fullscreen;                                //是否全屏显示
  23. HDC m_HDC;                                      //储存当前设备的指针
  24. int m_FontSize;                                 //控制字体的大小
  25. GLuint m_Base;                                  //储存绘制字体的显示列表的开始位置
  26. GLfloat m_Cnt1;                                 //字体移动计数器1
  27. GLfloat m_Cnt2;                                 //字体移动计数器2
  28. };
  29. #endif // MYGLWIDGET_H

我们新增了几个变量,第一个变量m_HDC是用来储存当前设备绘制信息的一种windows数据结构,我们将会把我们自己创建的字体绑定到m_HDC上去,这样我们绘制文字时就自动采用绑定的字体了。后面几个变量的作用依次是控制字体大小、储存绘制字体的显示列表的开始位置、字体移动计数,具体的会在后面讲。

另外我们增加了三个函数,分别用于创建字体、删除显示列表、输出特定的字符串,当然最后一个glPrint()函数在前面已经提到,是个很重要的函数。

接下来,我们需要打开myglwidget.cpp,加上声明#include <QTimer>、#include <QtMath>,将构造函数和析构函数修改一下,具体代码如下:

  1. MyGLWidget::MyGLWidget(QWidget *parent) :
  2. QGLWidget(parent)
  3. {
  4. fullscreen = false;
  5. m_FontSize = -18;
  6. m_Cnt1 = 0.0f;
  7. m_Cnt2 = 0.0f;
  8. HWND hWND = (HWND)winId();                          //获取当前窗口句柄
  9. m_HDC = GetDC(hWND);                                //通过窗口句柄获得HDC
  10. QTimer *timer = new QTimer(this);                   //创建一个定时器
  11. //将定时器的计时信号与updateGL()绑定
  12. connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
  13. timer->start(10);                                   //以10ms为一个计时周期
  14. }
  1. MyGLWidget::~MyGLWidget()
  2. {
  3. killFont();                                         //删除显示列表
  4. }

几个普通变量的初始化我不作解释了,我们重点看m_HDC的初始化。我们要如何获得当前窗口的HDC呢?方法是我们先得到当前窗口的句柄(HWND),通过调用函数GetCD(HWND)可以获得HDC。那如何获得HWND呢?Qt中有一个winId()函数可以返回当前窗口的Id(类型为WId),我们把它强制转换为HWND类型就可以了,这样我们就可以初始化关键的m_HDC。

注意一下析构函数,在退出程序之前,我们应该确保我们分配的用于存放显示列表的空间被释放,所以我们在析构函数处调用killFont()函数删除显示列表(具体实现看下面)。

继续,我们要来定义我们新增的三个函数了,这可是重头戏,具体代码如下:

  1. void MyGLWidget::buildFont()                            //创建位图字体
  2. {
  3. HFONT font;                                         //字体句柄
  4. m_Base = glGenLists(96);                            //创建96个显示列表
  5. font = CreateFont(m_FontSize,                       //字体高度
  6. 0,                                //字体宽度
  7. 0,                                //字体的旋转角度
  8. 0,                                //字体底线的旋转角度
  9. FW_BOLD,                          //字体的重量
  10. FALSE,                            //是否斜体
  11. FALSE,                            //是否使用下划线
  12. FALSE,                            //是否使用删除线
  13. ANSI_CHARSET,                     //设置字符集
  14. OUT_TT_PRECIS,                    //输出精度
  15. CLIP_DEFAULT_PRECIS,              //剪裁精度
  16. ANTIALIASED_QUALITY,              //输出质量
  17. FF_DONTCARE | DEFAULT_PITCH,      //Family and Pitch的设置
  18. LPCWSTR("Courier New"));          //字体名称(电脑中已装的)
  19. wglUseFontBitmaps(m_HDC, 32, 96, m_Base);           //创建96个显示列表,绘制ASCII码为32-128的字符
  20. SelectObject(m_HDC, font);                          //选择字体
  21. }
  1. void MyGLWidget::killFont()                             //删除显示列表
  2. {
  3. glDeleteLists(m_Base, 96);                          //删除96个显示列表
  4. }
  1. void MyGLWidget::glPrint(const char *fmt, ...)          //自定义输出文字函数
  2. {
  3. char text[256];                                     //保存字符串
  4. va_list ap;                                         //指向一个变量列表的指针
  5. if (fmt == NULL)                                    //如果无输入则返回
  6. {
  7. return;
  8. }
  9. va_start(ap, fmt);                                  //分析可变参数
  10. vsprintf(text, fmt, ap);                        //把参数值写入字符串
  11. va_end(ap);                                         //结束分析
  12. glPushAttrib(GL_LIST_BIT);                          //把显示列表属性压入属性堆栈
  13. glListBase(m_Base - 32);                            //设置显示列表的基础值
  14. glCallLists(strlen(text), GL_UNSIGNED_BYTE, text);  //调用显示列表绘制字符串
  15. glPopAttrib();                                      //弹出属性堆栈
  16. }

首先是buildFont()函数。我们先定义了字体句柄变量(HFONT),用来存放我们将要创建和使用的字体。接着我们在定义m_Base的同时使用glGenLists(96)创建了一组共96个显示列表。然后我们调用Windows的API函数CreateFont()来创建我们自己的字体,前13个参数的意义大家请参考注释,我觉得没必要一个个解释了(有兴趣了解CreateFont各个参数请点击此处),最后一个参数是字体类型,我们可以使用我们电脑已安装的任何字体,在Windows\Fonts目录可查看电脑已安装的字体。

然后我们从ASCII码第32个字符(空格)开始建立96个显示列表。如果你愿意,也可以建立所有256个字符,只要确保使用glGenLists建立256个显示列表就可以了。最后我们将font对象指针选入HDC,如此就完成了字体的创建及绑定。

然后是killFont()函数。它很简单,就是调用glDeleteLists()函数从m_Base开始删除96个显示列表。

最后是glPrint()函数。首先第一行我们创建一个大小为256个字符的字符数组,将用来保存我们要输出的字符串。第二行我们创建了一个指向一个变量列表的指针,我们在传递字符串的同时也传递了这个变量列表。然后是排除字符串为空的情况。接着的三行代码将文字中的所有符号转换为它们的字符编号,最终文字和转换的符号被储存在字符串text中。然后我们将GL_LIST_BIT压入属性堆栈,它会防止glListBase影响到我们的程序中的其它显示列表。

glListBase(m_Base-32)是告诉OpenGL去哪找对应字符的显示列表,由于每个字符对应一个显示列表,通过m_Base设置一个起点,OpenGL就知道到哪去找到正确的显示列表。减去32是因为我们没有构造前32个显示列表,那么久跳过它们就好了。于是,我们不得不通过从m_Base的值减去32来让OpenGL知道这一点。

现在OpenGL知道字母的存放位置了,我们就可以让它在屏幕上显示文字了。glCallLists()函数能同时将多个显示列表的内容显示在屏幕上,第一个参数是要显示在屏幕上的字符串长度,第二个参数告诉OpenGL将字符串当作一个无符号数组处理,它们的值都介于0到255之间,第三个参数通过传递text来告诉OpenGL显示的具体内容。最后,我们将GL_LIST_BIT属性弹出堆栈,恢复到我们使用glListBase(m_Base-32)之前的状态。

也许你想知道为什么字符不会彼此重叠堆积在一起。那是因为每个字符的显示列表都知道字符的右边缘在哪里,在写完一个字符后,OpenGL自动移动到刚刚写过的字符的右边,再写下一个字或画下一个物体时就会从最后的位置开始,也就是最后一个字符的右边。

然后我们修改一下initializeGL()函数,不作解释,代码如下:

  1. void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
  2. {
  3. glClearColor(0.0, 0.0, 0.0, 0.0);                   //黑色背景
  4. glShadeModel(GL_SMOOTH);                            //启用阴影平滑
  5. glClearDepth(1.0);                                  //设置深度缓存
  6. glEnable(GL_DEPTH_TEST);                            //启用深度测试
  7. glDepthFunc(GL_LEQUAL);                             //所作深度测试的类型
  8. glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  //告诉系统对透视进行修正
  9. buildFont();                                        //创建字体
  10. }

还有,我们该进入paintGL()函数了,很简单,难的都过去了,具体代码如下:

  1. void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
  2. {
  3. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
  4. glLoadIdentity();                                   //重置当前的模型观察矩阵
  5. glTranslatef(0.0f, 0.0f, -10.0f);                   //移入屏幕10.0单位
  6. //根据字体位置设置颜色
  7. glColor3f(1.0f*float(cos(m_Cnt1)), 1.0f*float(sin(m_Cnt2)),
  8. 1.0f-0.5f*float(cos(m_Cnt1+m_Cnt2)));
  9. //设置光栅化位置,即字体位置
  10. glRasterPos2f(-4.5f+0.5f*float(cos(m_Cnt1)), 1.92f*float(sin(m_Cnt2)));
  11. //输出文字到屏幕上
  12. glPrint("Active OpenGL Text With NeHe - %7.2f", m_Cnt1);
  13. m_Cnt1 += 0.051f;                                   //增加两个计数器的值
  14. m_Cnt2 += 0.005f;
  15. }

值得注意的是,深入屏幕并不能缩小字体,只会给字体变化移动范围(这一点大家自己改改数据就知道了)。然后字体颜色设置和位置设置我觉得没必要解释了,都是数学的东西,我们主要是为了得到一个变化的效果,并不在乎它是怎么实现的。然后就是调用glPrint()函数输出文字,最后增加两个计数器的值就OK了。

最后就是键盘控制的代码了,大家自己看吧,很简单,具体代码如下:

  1. void MyGLWidget::keyPressEvent(QKeyEvent *event)
  2. {
  3. switch (event->key())
  4. {
  5. case Qt::Key_F1:                                    //F1为全屏和普通屏的切换键
  6. fullscreen = !fullscreen;
  7. if (fullscreen)
  8. {
  9. showFullScreen();
  10. }
  11. else
  12. {
  13. showNormal();
  14. }
  15. updateGL();
  16. break;
  17. case Qt::Key_Escape:                                //ESC为退出键
  18. close();
  19. break;
  20. case Qt::Key_PageUp:                                //PageUp按下字体缩小
  21. m_FontSize -= 1;
  22. if (m_FontSize < -75)
  23. {
  24. m_FontSize = -75;
  25. }
  26. buildFont();
  27. break;
  28. case Qt::Key_PageDown:                              //PageDown按下字体放大
  29. m_FontSize += 1;
  30. if (m_FontSize > -5)
  31. {
  32. m_FontSize = -5;
  33. }
  34. buildFont();
  35. break;
  36. }
  37. }

现在就可以运行程序查看效果了!

第14课:轮廓字体 (参照NeHe)
这次教程中,我将教大家绘制3D的轮廓字体,当然肯定不是贴图方式了,它们可像一般的3D模型一样进行旋转,放缩。
创建轮廓字体的方法与13课位图的位图字体类似,但轮廓字体要酷得多!轮廓字体可以在屏幕中以3D方式旋转,而且轮廓字体还可以有一定的厚度,而不再是平面的2D字符了。使用轮廓字体,我们可以将计算机中的任何字体转换为OpenGL的3D字体,是不是听起来很诱人呢?
 
程序运行时效果如下:
 
 
下面进入教程:
 
我们这次将在第13课的基础上修改代码,由于有13课代码的基础,这节课会简单许多。我只解释新增的代码,首先打开myglwidget.h文件,将类声明更改如下:
  1. #ifndef MYGLWIDGET_H
  2. #define MYGLWIDGET_H
  3. #include <QWidget>
  4. #include <QGLWidget>
  5. class MyGLWidget : public QGLWidget
  6. {
  7. Q_OBJECT
  8. public:
  9. explicit MyGLWidget(QWidget *parent = 0);
  10. ~MyGLWidget();
  11. protected:
  12. //对3个纯虚函数的重定义
  13. void initializeGL();
  14. void resizeGL(int w, int h);
  15. void paintGL();
  16. void keyPressEvent(QKeyEvent *event);           //处理键盘按下事件
  17. private:
  18. void buildFont();                               //创建字体
  19. void killFont();                                //删除显示列表
  20. void glPrint(const char *fmt, ...);             //输出字符串
  21. private:
  22. bool fullscreen;                                //是否全屏显示
  23. HDC m_HDC;                                      //储存当前设备的指针
  24. GLYPHMETRICSFLOAT m_Gmf[256];                   //记录256个字符的信息
  25. GLfloat m_Deep;                                 //移入屏幕的距离
  26. GLuint m_Base;                                  //储存绘制字体的显示列表的开始位置
  27. GLfloat m_Rot;                                  //用于旋转字体
  28. };
  29. #endif // MYGLWIDGET_H

由于我们没有准备让轮廓字体移动,所以删掉两个计数器。接着增加m_Deep来控制移入屏幕的距离,其实就是来控制字体的放大缩小的。然后再增加m_Rot来控制字体的旋转。最后增加了GLYPHMETRICSFLOAT变量数组m_Gmf[256]用来保存256个轮廓字体显示列表中对应的每一个列表的位置和方向信息,我们通过m_Gmf[num]来选择字母。要注意的是,每个字符的宽度可以不相同,使用GLYPHMETRICS会大大简化我们的工作。

 
接下来,我们需要打开myglwidget.cpp,先修改构造函数,不多解析了,具体代码如下:
  1. MyGLWidget::MyGLWidget(QWidget *parent) :
  2. QGLWidget(parent)
  3. {
  4. fullscreen = false;
  5. m_Deep = -10.0f;
  6. m_Rot = 0.0f;
  7. HWND hWND = (HWND)winId();                          //获取当前窗口句柄
  8. m_HDC = GetDC(hWND);                                //通过窗口句柄获得HDC
  9. QTimer *timer = new QTimer(this);                   //创建一个定时器
  10. //将定时器的计时信号与updateGL()绑定
  11. connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
  12. timer->start(10);                                   //以10ms为一个计时周期
  13. }

继续,我们需要对buildFont()、killFont()、glPrint()三个函数作一定的修改,具体代码如下:

  1. void MyGLWidget::buildFont()                            //创建轮廓字体
  2. {
  3. HFONT font;                                         //字体句柄
  4. m_Base = glGenLists(256);                           //创建256个显示列表
  5. font = CreateFont(-18,                              //字体高度
  6. 0,                                //字体宽度
  7. 0,                                //字体的旋转角度
  8. 0,                                //字体底线的旋转角度
  9. FW_BOLD,                          //字体的重量
  10. FALSE,                            //是否斜体
  11. FALSE,                            //是否使用下划线
  12. FALSE,                            //是否使用删除线
  13. ANSI_CHARSET,                     //设置字符集
  14. OUT_TT_PRECIS,                    //输出精度
  15. CLIP_DEFAULT_PRECIS,              //剪裁精度
  16. ANTIALIASED_QUALITY,              //输出质量
  17. FF_DONTCARE | DEFAULT_PITCH,      //Family and Pitch的设置
  18. LPCWSTR("Comic Sans MS"));        //字体名称(电脑中已装的)
  19. SelectObject(m_HDC, font);                          //选择字体
  20. wglUseFontOutlines(m_HDC,                           //当前HDC
  21. 0,                               //从ASCII码第一个字符开始
  22. 255,                             //字符数
  23. m_Base,                          //第一个显示列表的名称
  24. 0.0f,                            //字体光滑度,越小越光滑
  25. 0.2f,                            //在z方向突出的距离(字体的厚度)
  26. WGL_FONT_POLYGONS,               //使用多边形来生成字符,每个顶点具有独立法线
  27. m_Gmf);                          //用于储存字形度量数据(高度,宽度等)
  28. }
  1. void MyGLWidget::killFont()                             //删除显示列表
  2. {
  3. glDeleteLists(m_Base, 256);                          //删除96个显示列表
  4. }
  1. void MyGLWidget::glPrint(const char *fmt, ...)          //自定义输出文字函数
  2. {
  3. float length = 0;
  4. char text[256];                                     //保存字符串
  5. va_list ap;                                         //指向一个变量列表的指针
  6. if (fmt == NULL)                                    //如果无输入则返回
  7. {
  8. return;
  9. }
  10. va_start(ap, fmt);                                  //分析可变参数
  11. vsprintf(text, fmt, ap);                        //把参数值写入字符串
  12. va_end(ap);                                         //结束分析
  13. for (unsigned int i=0; i<strlen(text); i++)                  //计算整个字符串的长度
  14. {
  15. length += m_Gmf[(int)text[i]].gmfCellIncX;
  16. }
  17. glTranslatef(-length / 2, 0.0f, 0.0f);              //左移字符串一半的长度
  18. glPushAttrib(GL_LIST_BIT);                          //把显示列表属性压入堆栈
  19. glListBase(m_Base);                                 //设置显示列表的基础值
  20. glCallLists(strlen(text), GL_UNSIGNED_BYTE, text);  //调用显示列表绘制字符串
  21. glPopAttrib();                                      //弹出属性堆栈
  22. }

首先是buildFont()函数。首先创建字体的方法与上一课基本一致,只是把m_FontSize换成了-18。接着,将wglUseFontBitmaps()函数替换成wglUseFontOutlines()函数,这个函数包含了8个参数前4个参数大家自己看注释,第5个参数为光滑度系数,这个值越小,字体看起来会越光滑(其实看不出明显差别)。第6个参数简单点说指的是字体的厚度,有厚度的字体才有立体感嘛,如果这个值为0.0就变成2D字体了。第7个参数告诉OpenGL用多边形来生成字符,使每个顶点都会具有独立的法线,这样加上光源后会有不错的效果(光源效果我们的代码中没有,大家可以自己加加看)。最后一个参数告诉OpenGL把创建的显示列表的度量数据(高度、宽度等)放在数组m_Gmf[]中。

然后是killFont()函数。它很简单,就是调用glDeleteLists()函数从m_Base开始删除256个显示列表。
最后是glPrint()函数。我们只是在原来的基础上加了一些代码,我们首先看到我们增加了一个浮点变量length用来统计整个字符串的宽度。接着我们利用循环,在循环中,由于我们已经将每个字符的度量值储存在m_Gmf[]中,我们利用m_Gmf[text[i]].gmfCellIncX来获得每个字符的宽度,累加起来就得到字符串的总宽度。然后,我们把视图原点左移length/2的距离,这样就保证字符串总是在屏幕的中心了。最后,把glListBase(m_Base-32)换成glListBase(m_Base),这是因为我们这次是从ASCII码第一个字符开始创建的显示列表。
然后,我们修改一下paintGL()函数,与之前的代码很神似,只是更改了旋转和平移部分的代码,最后让旋转变量增加,不过多解释了,具体代码如下:
  1. void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
  2. {
  3. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
  4. glLoadIdentity();                                   //重置当前的模型观察矩阵
  5. glTranslatef(0.0f, 0.0f, m_Deep);                   //移入屏幕10.0单位
  6. glRotatef(m_Rot, 1.0f, 0.0f, 0.0f);                 //绕x轴旋转
  7. glRotatef(m_Rot*1.5f, 0.0f, 1.0f, 0.0f);            //绕y轴旋转
  8. glRotatef(m_Rot*1.4f, 0.0f, 0.0f, 1.0f);            //绕z轴旋转
  9. //根据字体位置设置颜色
  10. glColor3f(1.0f*float(cos(m_Rot/20.0f)), 1.0f*float(sin(m_Rot/25.0f)),
  11. 1.0f-0.5f*float(cos(m_Rot/17.0f)));
  12. //输出文字到屏幕上
  13. glPrint("NeHe - %3.2f", m_Rot/50.0f);
  14. m_Rot += 0.5f;                                      //旋转变量增加
  15. }

最后,修改键盘控制的代码,就是按PageUp和PageDown可以放大缩小字体,具体代码如下:

  1. void MyGLWidget::keyPressEvent(QKeyEvent *event)
  2. {
  3. switch (event->key())
  4. {
  5. case Qt::Key_F1:                                    //F1为全屏和普通屏的切换键
  6. fullscreen = !fullscreen;
  7. if (fullscreen)
  8. {
  9. showFullScreen();
  10. }
  11. else
  12. {
  13. showNormal();
  14. }
  15. updateGL();
  16. break;
  17. case Qt::Key_Escape:                                //ESC为退出键
  18. close();
  19. break;
  20. case Qt::Key_PageUp:                                //PageUp按下字体缩小
  21. m_Deep -= 0.2f;
  22. break;
  23. case Qt::Key_PageDown:                              //PageDown按下字体放大
  24. m_Deep += 0.2f;
  25. break;
  26. }
  27. }

现在就可以运行程序查看效果了!

第15课:图形字体的纹理映射 (参照NeHe)

这次教程中,我们将在第14课的基础上创建带有纹理的字体,它真的很简单。也许你想知道如何才能给字体赋予纹理贴图?我们可以使用自动纹理坐标生成器,它会自动为字体上的每一个多边形生成纹理坐标。

这次课中我们还将使用Wingdings字体来显示一个海盗旗(骷髅头和十字骨头)的标志,为此我们需要修改buildFont()函数代码。如果你想显示文字的话,就不用改动第14课中buildFont()函数的代码了,当然你也可以选择另一种字体,这都不是什么大事。

程序运行时效果如下:

下面进入教程:

我们这次将在第14课的基础上修改代码,由于有前两课代码的基础,这节课会更简单。我只解释新增的代码,首先打开myglwidget.h文件,将类声明更改如下:

  1. #ifndef MYGLWIDGET_H
  2. #define MYGLWIDGET_H
  3. #include <QWidget>
  4. #include <QGLWidget>
  5. class MyGLWidget : public QGLWidget
  6. {
  7. Q_OBJECT
  8. public:
  9. explicit MyGLWidget(QWidget *parent = 0);
  10. ~MyGLWidget();
  11. protected:
  12. //对3个纯虚函数的重定义
  13. void initializeGL();
  14. void resizeGL(int w, int h);
  15. void paintGL();
  16. void keyPressEvent(QKeyEvent *event);           //处理键盘按下事件
  17. private:
  18. void buildFont();                               //创建字体
  19. void killFont();                                //删除显示列表
  20. void glPrint(const char *fmt, ...);             //输出字符串
  21. private:
  22. bool fullscreen;                                //是否全屏显示
  23. HDC m_HDC;                                      //储存当前设备的指针
  24. GLYPHMETRICSFLOAT m_Gmf[256];                   //记录256个字符的信息
  25. GLfloat m_Deep;                                 //移入屏幕的距离
  26. GLuint m_Base;                                  //储存绘制字体的显示列表的开始位置
  27. GLfloat m_Rot;                                  //用于旋转字体
  28. QString m_FileName;                             //图片的路径及文件名
  29. GLuint m_Texture;                               //储存一个纹理
  30. };
  31. #endif // MYGLWIDGET_H

注意到唯一的变化就是最后增加了两个变量,这个两个变量相信大家已经很熟悉了,m_FileName用来保存我们将用于纹理映射的图片路径名,m_Texture用于储存纹理。

接下来我们需要打开myglwidget.cpp,先修改构造函数初始化m_FileName,不多解释了,具体代码如下:

  1. MyGLWidget::MyGLWidget(QWidget *parent) :
  2. QGLWidget(parent)
  3. {
  4. fullscreen = false;
  5. m_Deep = -3.0f;
  6. m_Rot = 0.0f;
  7. m_FileName = "D:/QtOpenGL/QtImage/Lights.bmp";        //应根据实际存放图片的路径进行修改
  8. HWND hWND = (HWND)winId();                          //获取当前窗口句柄
  9. m_HDC = GetDC(hWND);                                //通过窗口句柄获得HDC
  10. QTimer *timer = new QTimer(this);                   //创建一个定时器
  11. //将定时器的计时信号与updateGL()绑定
  12. connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
  13. timer->start(10);                                   //以10ms为一个计时周期
  14. }

继续,我们需要略微修改一下buildFont()函数,修改后代码如下:

  1. void MyGLWidget::buildFont()                            //创建位图字体
  2. {
  3. HFONT font;                                         //字体句柄
  4. m_Base = glGenLists(256);                           //创建256个显示列表
  5. font = CreateFont(-18,                              //字体高度
  6. 0,                                //字体宽度
  7. 0,                                //字体的旋转角度
  8. 0,                                //字体底线的旋转角度
  9. FW_BOLD,                          //字体的重量
  10. FALSE,                            //是否斜体
  11. FALSE,                            //是否使用下划线
  12. FALSE,                            //是否使用删除线
  13. SYMBOL_CHARSET,                   //设置字符集
  14. OUT_TT_PRECIS,                    //输出精度
  15. CLIP_DEFAULT_PRECIS,              //剪裁精度
  16. ANTIALIASED_QUALITY,              //输出质量
  17. FF_DONTCARE | DEFAULT_PITCH,      //Family and Pitch的设置
  18. LPCWSTR("Wingdings"));            //字体名称(电脑中已装的)
  19. SelectObject(m_HDC, font);                          //选择字体
  20. wglUseFontOutlines(m_HDC,                           //当前HDC
  21. 0,                               //从ASCII码第一个字符开始
  22. 255,                             //字符数
  23. m_Base,                          //第一个显示列表的名称
  24. 0.1f,                            //字体光滑度,越小越光滑
  25. 0.2f,                            //在z方向突出的距离(字体的厚度)
  26. WGL_FONT_POLYGONS,               //使用多边形来生成字符,每个顶点具有独立法线
  27. m_Gmf);                            //用于储存字形度量数据(高度,宽度等)
  28. }

注意到我们只是修改了CreateFont()函数的第9个参数(设置字符集)和最后一个参数(字体名称),修改最后一个参数好理解,因为我们要使用字体Wingdings嘛。但其实这样修改后,我们想用的Wingdings字体并不会工作。这是由于Wingdings里的字体都不是标准字符字体,我们必须告诉Windows这种字体是一种符号字体而不是一种标准字符字体,因此我们在设置字符集时,把参数改为SYMBOL_CHARSET,如此Wingdings字体就能正常工作了。

然后我们需要来修改initializeGL()函数,这是本节课的重点部分,具体代码如下:

  1. void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
  2. {
  3. //自动生成纹理
  4. glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
  5. glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
  6. glEnable(GL_TEXTURE_GEN_S);
  7. glEnable(GL_TEXTURE_GEN_T);
  8. m_Texture = bindTexture(QPixmap(m_FileName));       //载入位图并转换成纹理
  9. glEnable(GL_TEXTURE_2D);                            //启用纹理映射
  10. glClearColor(0.0, 0.0, 0.0, 0.0);                   //黑色背景
  11. glShadeModel(GL_SMOOTH);                            //启用阴影平滑
  12. glClearDepth(1.0);                                  //设置深度缓存
  13. glEnable(GL_DEPTH_TEST);                            //启用深度测试
  14. glDepthFunc(GL_LEQUAL);                             //所作深度测试的类型
  15. glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  //告诉系统对透视进行修正
  16. buildFont();                                        //创建字体
  17. }

注意到,我们一开始增加了四行新代码,这四行代码将为我们绘制在屏幕上的任何物体自动生成纹理坐标。函数glTexGen非常强大,而且复杂,在这里我们没法完全讲清楚。我们只需要知道GL_S和GL_T是纹理坐标就可以了。默认状态下,它被设置为提取物体此刻在屏幕上的x坐标和y坐标,并把它们装换为顶点坐标。我们运行程序时会发现到物体在z平面没有纹理,只显示一些斑纹,而正面和反面都被赋予了纹理,这些都是由glTexGen函数产生的。

GL_TEXTURE_GEN_MODE允许我们选择我们想在S和T纹理坐标上使用的纹理映射模式,我们有三种选择:GL_EYE_LINEAR - 会使纹理固定在屏幕上,它不会移动,物体将被赋予处于它通过的地区的那一块纹理;GL_OBJECT_LINEAR - 纹理被固定于屏幕上运动的物体上;GL_SPHERE_MAP - 创建一种有金属质感的物体(大家可以变化着试试,效果都很不错)。

当然下面增加的两行用于生成纹理和启用纹理映射,和第06课的代码一样的,不解释了。

最后,我们来修改一下paintGL()函数,具体代码如下:

  1. void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
  2. {
  3. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
  4. glLoadIdentity();                                   //重置当前的模型观察矩阵
  5. glBindTexture(GL_TEXTURE_2D, m_Texture);
  6. glTranslatef(1.1f*float(cos(m_Rot/16.0f)),
  7. 0.8f*float(sin(m_Rot/20.0f)), m_Deep); //物体移动及控制大小
  8. glRotatef(m_Rot, 1.0f, 0.0f, 0.0f);                 //绕x轴旋转
  9. glRotatef(m_Rot*1.2f, 0.0f, 1.0f, 0.0f);            //绕y轴旋转
  10. glRotatef(m_Rot*1.4f, 0.0f, 0.0f, 1.0f);            //绕z轴旋转
  11. //输出文字到屏幕上
  12. glPrint("N");
  13. m_Rot += 0.1f;                                      //旋转变量增加
  14. }

首先是绑定我们已经生产的纹理,接着由于我们这次纹理不需要融合颜色,于是去掉了选择颜色的代码。然后是物体移动和旋转的代码,我也不解释了,这只是其中一种变换方式,使得能产生动画,大家完全可以自己设计平移和旋转部分的代码(如加上键盘控制等)。然后就需要来输出我们的“海盗旗”了,如果你不知道我是如何从字母“N”中得到海盗旗符号的,那就打开写字板,在字体出选择Wingdings字体。输入大写字母“N”,就会显示出海盗旗符号了。

现在可以运行程序查看效果了!

第16课:看起来很酷的雾 (参照NeHe)

这次教程中,我们将在第07课代码的基础上,为木箱的四周填上雾效果。我们将会学习三种不同的雾模式,以及怎么设置雾的颜色和雾的范围。虽然这次教程非常简单,但我们得到的雾效果确实很棒!希望大家能喜欢,当然你也可以把雾效果加到任何一个OpenGL程序中,我相信总能檫出美丽的火花!

程序运行时效果如下:

下面进入教程:

我们这次将在第07课的基础上修改代码,我只会讲解有修改的部分,希望大家先找到第07课的代码再跟着我一步步走。首先打开myglwidget.h文件,将类声明更改如下:

  1. #ifndef MYGLWIDGET_H
  2. #define MYGLWIDGET_H
  3. #include <QWidget>
  4. #include <QGLWidget>
  5. class MyGLWidget : public QGLWidget
  6. {
  7. Q_OBJECT
  8. public:
  9. explicit MyGLWidget(QWidget *parent = 0);
  10. ~MyGLWidget();
  11. protected:
  12. //对3个纯虚函数的重定义
  13. void initializeGL();
  14. void resizeGL(int w, int h);
  15. void paintGL();
  16. void keyPressEvent(QKeyEvent *event);           //处理键盘按下事件
  17. private:
  18. bool fullscreen;                                //是否全屏显示
  19. QString m_FileName;                             //图片的路径及文件名
  20. GLuint m_Texture;                               //储存一个纹理
  21. bool m_Light;                                   //光源的开/关
  22. GLuint m_Fog;                                   //雾的模式
  23. GLfloat m_xRot;                                 //x旋转角度
  24. GLfloat m_yRot;                                 //y旋转角度
  25. GLfloat m_xSpeed;                               //x旋转速度
  26. GLfloat m_ySpeed;                               //y旋转速度
  27. GLfloat m_Deep;                                 //深入屏幕的距离
  28. };
  29. #endif // MYGLWIDGET_H

我们只是增加了一个变量m_Fog来储存当前雾的模式(我们会使用三种雾模式),方便我们后面利用键盘来控制雾模式的切换。

接下来,我们需要打开myglwidget.cpp,在构造函数中初始化新增变量,具体代码如下:

  1. MyGLWidget::MyGLWidget(QWidget *parent) :
  2. QGLWidget(parent)
  3. {
  4. fullscreen = false;
  5. m_FileName = "D:/QtOpenGL/QtImage/Crate.bmp";        //应根据实际存放图片的路径进行修改
  6. m_Light = false;
  7. m_Fog = 0;
  8. m_xRot = 0.0f;
  9. m_yRot = 0.0f;
  10. m_xSpeed = 0.0f;
  11. m_ySpeed = 0.0f;
  12. m_Deep = -5.0f;
  13. QTimer *timer = new QTimer(this);                   //创建一个定时器
  14. //将定时器的计时信号与updateGL()绑定
  15. connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
  16. timer->start(10);                                   //以10ms为一个计时周期
  17. }

我们给m_Fog赋初始值0,表示第一种雾模式(具体是哪一种下面会讲到)。

然后我们需要来修改initializeGL()函数,雾效果的数据初始化都这里完成的,具体代码如下:

  1. void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
  2. {
  3. m_Texture = bindTexture(QPixmap(m_FileName));       //载入位图并转换成纹理
  4. glEnable(GL_TEXTURE_2D);                            //启用纹理映射
  5. glClearColor(0.5f, 0.5f, 0.5f, 1.0f);               //设置背景的颜色为雾气的颜色
  6. glShadeModel(GL_SMOOTH);                            //启用阴影平滑
  7. glClearDepth(1.0);                                  //设置深度缓存
  8. glEnable(GL_DEPTH_TEST);                            //启用深度测试
  9. glDepthFunc(GL_LEQUAL);                             //所作深度测试的类型
  10. glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  //告诉系统对透视进行修正
  11. //光源部分
  12. GLfloat LightAmbient[] = {0.5f, 0.5f, 0.5f, 1.0f};  //环境光参数
  13. GLfloat LightDiffuse[] = {1.0f, 1.0f, 1.0f, 1.0f};  //漫散光参数
  14. GLfloat LightPosition[] = {0.0f, 0.0f, 2.0f, 1.0f}; //光源位置
  15. glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient);     //设置环境光
  16. glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse);     //设置漫射光
  17. glLightfv(GL_LIGHT1, GL_POSITION, LightPosition);   //设置光源位置
  18. glEnable(GL_LIGHT1);                                //启动一号光源
  19. //雾部分
  20. GLfloat fogColor[] = {0.5f, 0.5f, 0.5f, 1.0f};      //雾的颜色
  21. glFogi(GL_FOG_MODE, GL_EXP);                        //设置雾气的初始模式
  22. glFogfv(GL_FOG_COLOR, fogColor);                    //设置雾的颜色
  23. glFogf(GL_FOG_DENSITY, 0.35);                       //设置雾的密度
  24. glHint(GL_FOG_HINT, GL_DONT_CARE);                  //设置系统如何计算雾气
  25. glFogf(GL_FOG_START, 1.0f);                         //雾的开始位置
  26. glFogf(GL_FOG_END, 5.0f);                           //雾的结束位置
  27. glEnable(GL_FOG);                                   //启动雾效果
  28. }

首先我们改一下glClearColor()函数的参数,让清除屏幕的颜色与下面雾的颜色相同。我们在函数末尾加上了我们的雾效果代码,首先我们定义雾的颜色(我们定为白色雾,你完全可以根据自己的喜好修改),接着我们设置了雾气的初始模式为GL_EXP,这是m_Fog等于0时对应的模式,先别急着问为什么,下面会告诉你答案。

然后我们设置雾的密度,glFogf()函数的第二个参数越大雾会越浓,越小雾会越稀。glHint()函数用于设置修正,我们使用了GL_DONT_CARE因为我们不关心它的值。再接下去两行设置了雾的起始位置和结束位置,1.0f和5.0f均表示离屏幕的距离,我们完全可以自己根据需要修改这两个值。最后我们应用glEnable()启用了雾效果,注意没有这行是无法产生无效果的。

最后是关于键盘控制函数的修改,我们将利用它来控制雾模式的切换,具体代码如下:

  1. void MyGLWidget::keyPressEvent(QKeyEvent *event)
  2. {
  3. static GLuint fogMode[] = {GL_EXP, GL_EXP2, GL_LINEAR};
  4. switch (event->key())
  5. {
  6. case Qt::Key_F1:                                    //F1为全屏和普通屏的切换键
  7. fullscreen = !fullscreen;
  8. if (fullscreen)
  9. {
  10. showFullScreen();
  11. }
  12. else
  13. {
  14. showNormal();
  15. }
  16. break;
  17. case Qt::Key_Escape:                                //ESC为退出键
  18. close();
  19. break;
  20. case Qt::Key_L:                                     //L为开启关闭光源的切换键
  21. m_Light = !m_Light;
  22. if (m_Light)
  23. {
  24. glEnable(GL_LIGHTING);                      //开启光源
  25. }
  26. else
  27. {
  28. glDisable(GL_LIGHTING);                     //关闭光源
  29. }
  30. break;
  31. case Qt::Key_G:                                     //G为雾模式的切换键
  32. m_Fog++;
  33. if (m_Fog == 3)
  34. {
  35. m_Fog = 0;
  36. }
  37. glFogi(GL_FOG_MODE, fogMode[m_Fog]);
  38. break;
  39. case Qt::Key_PageUp:                                //PageUp按下使木箱移向屏幕内部
  40. m_Deep -= 0.1f;
  41. break;
  42. case Qt::Key_PageDown:                              //PageDown按下使木箱移向观察者
  43. m_Deep += 0.1f;
  44. break;
  45. case Qt::Key_Up:                                    //Up按下减少m_xSpeed
  46. m_xSpeed -= 0.1f;
  47. break;
  48. case Qt::Key_Down:                                  //Down按下增加m_xSpeed
  49. m_xSpeed += 0.1f;
  50. break;
  51. case Qt::Key_Right:                                 //Right按下减少m_ySpeed
  52. m_ySpeed -= 0.1f;
  53. break;
  54. case Qt::Key_Left:                                  //Left按下增加m_ySpeed
  55. m_ySpeed += 0.1f;
  56. break;
  57. }
  58. }

注意到我们定义了一个静态GLuint数组fogMode[]来储存我们要切换的雾模式GL_EXP、GL_EXP2、GL_LINEAR三种模式。GL_EXP - 充满整个屏幕的只是基本渲染的雾,并不是特别像雾;GL_EXP2 - 比GL_EXP更进一步,它也是充满整个屏幕,但它使屏幕看起来更有深度;GL_LINEAR - 最好的渲染模式,物体淡入淡出的效果更自然(我们可以通过切换键比较看看效果就知道了)。由于GL_EXP放在fogMode[0]处,故m_Fog为0时对应的模式是GL_EXP。

每次按下G键,我们就让m_Fog加一,如果加后等于3,就让它重新回到0,然后调用glFogi()函数重新选择雾模式。

现在就可以运行程序查看效果了!

第17课:2D图像文字 (参照NeHe)

这次教程中,我们将学会如何使用四边形纹理贴图把文字显示在屏幕上。我们将把256个不同的文字从一个256×256的纹理图像中一个个提取出来,接着创建一个输出函数来创建任意我们希望的文字。

还记得在第一篇字体教程中我提到使用纹理在屏幕上绘制文字吗?通常当你使用纹理绘制文字时你会调用你最喜欢的图像处理程序,选择一种字体,然后输入你想显示的文字或段落,然后保存下来位图并把它作为纹理读入到你的程序里,问题是这对一个需要很多文字或者文字在不停变化的程序来说,这么做效率并不高。这次教程中我们只使用一个纹理来显示任意256个不同的字符。

程序运行时效果如下:

下面进入教程:

由于相较于之前几课字体教程的代码改动较大,我们将直接在第01课的基础上修改代码,我会一一解释新增的代码,首先myglwidget.h文件,将类声明更改如下:

  1. #ifndef MYGLWIDGET_H
  2. #define MYGLWIDGET_H
  3. #include <QWidget>
  4. #include <QGLWidget>
  5. class MyGLWidget : public QGLWidget
  6. {
  7. Q_OBJECT
  8. public:
  9. explicit MyGLWidget(QWidget *parent = 0);
  10. ~MyGLWidget();
  11. protected:
  12. //对3个纯虚函数的重定义
  13. void initializeGL();
  14. void resizeGL(int w, int h);
  15. void paintGL();
  16. void keyPressEvent(QKeyEvent *event);           //处理键盘按下事件
  17. private:
  18. void buildFont();                               //创建字体
  19. void killFont();                                //删除显示列表
  20. //输出字符串
  21. void glPrint(GLuint x, GLuint y, const char *string, int set);
  22. private:
  23. bool fullscreen;                                //是否全屏显示
  24. GLuint m_Base;                                  //储存绘制字体的显示列表的开始位置
  25. GLfloat m_Cnt1;                                 //字体移动计数器1
  26. GLfloat m_Cnt2;                                 //字体移动计数器2
  27. QString m_FileName[2];                          //图片的路径及文件名
  28. GLuint m_Texture[2];                            //储存两个纹理
  29. };
  30. #endif // MYGLWIDGET_H

我们增加了变量m_Base、m_Cnt1、m_Cnt2,函数声明buildFont()、killFont(),这些和之前都讲过的作用都一样就不重复了。而m_FileName和m_Texture变为了长度为2的数组,这是因为程序中我们会用两种不同的图来建立两个不同的纹理。最后是glPrint()函数的声明,注意下它的参数和前几课不同,但作用是相同的,具体的下面会讲到。

接下来,我们需要打开myglwidget.cpp,加入声明#include <QTimer>、#include<QtMath>,将构造函数和析构函数修改如下(比较简单不具体解释了):

  1. MyGLWidget::MyGLWidget(QWidget *parent) :
  2. QGLWidget(parent)
  3. {
  4. fullscreen = false;
  5. m_Cnt1 = 0.0f;
  6. m_Cnt2 = 0.0f;
  7. m_FileName[0] = "D:/QtOpenGL/QtImage/Font.bmp";        //应根据实际存放图片的路径进行修改
  8. m_FileName[1] = "D:/QtOpenGL/QtImage/Bumps.bmp";
  9. QTimer *timer = new QTimer(this);                   //创建一个定时器
  10. //将定时器的计时信号与updateGL()绑定
  11. connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
  12. timer->start(10);                                   //以10ms为一个计时周期
  13. }
  1. MyGLWidget::~MyGLWidget()
  2. {
  3. killFont();                                         //删除显示列表
  4. }

继续,我们要来定义我们增加的三个函数,同样是重头戏,具体代码如下:

  1. void MyGLWidget::buildFont()                            //创建位图字体
  2. {
  3. float cx, cy;                                       //储存字符的x、y坐标
  4. m_Base = glGenLists(256);                           //创建256个显示列表
  5. glBindTexture(GL_TEXTURE_2D, m_Texture[0]);         //选择字符纹理
  6. for (int i=0; i<256; i++)                           //循环256个显示列表
  7. {
  8. cx = float(i%16)/16.0f;                         //当前字符的x坐标
  9. cy = float(i/16)/16.0f;                         //当前字符的y坐标
  10. glNewList(m_Base+i, GL_COMPILE);                //开始创建显示列表
  11. glBegin(GL_QUADS);                          //使用四边形显示每一个字符
  12. glTexCoord2f(cx, 1-cy-0.0625f);
  13. glVertex2i(0, 0);
  14. glTexCoord2f(cx+0.0625f, 1-cy-0.0625f);
  15. glVertex2i(16, 0);
  16. glTexCoord2f(cx+0.0625f, 1-cy);
  17. glVertex2i(16, 16);
  18. glTexCoord2f(cx, 1-cy);
  19. glVertex2i(0, 16);
  20. glEnd();                                    //四边形字符绘制完成
  21. glTranslated(10, 0, 0);                     //绘制完一个字符,向右平移10个单位
  22. glEndList();                                    //字符显示列表完成
  23. }
  24. }
  1. void MyGLWidget::killFont()                             //删除显示列表
  2. {
  3. glDeleteLists(m_Base, 256);                         //删除256个显示列表
  4. }
  1. void MyGLWidget::glPrint(GLuint x, GLuint y,            //输入字符串
  2. const char *string, int set)
  3. {
  4. if (set > 1)                                        //如果字符集大于1
  5. {
  6. set = 1;                                        //设置其为1
  7. }
  8. glBindTexture(GL_TEXTURE_2D, m_Texture[0]);         //绑定为字体纹理
  9. glDisable(GL_DEPTH_TEST);                           //禁止深度测试
  10. glMatrixMode(GL_PROJECTION);                        //选择投影矩阵
  11. glPushMatrix();                                     //保存当前的投影矩阵
  12. glLoadIdentity();                                   //重置投影矩阵
  13. glOrtho(0, 640, 0, 480, -1, 1);                     //设置正投影的可视区域
  14. glMatrixMode(GL_MODELVIEW);                         //选择模型观察矩阵
  15. glPushMatrix();                                     //保存当前的模型观察矩阵
  16. glLoadIdentity();                                   //重置模型观察矩阵
  17. glTranslated(x, y ,0);                              //把字符原点移动到(x,y)位置
  18. glListBase(m_Base-32+(128*set));                    //选择字符集
  19. glCallLists(strlen(string), GL_BYTE, string);       //把字符串写到屏幕
  20. glMatrixMode(GL_PROJECTION);                         //选择投影矩阵
  21. glPopMatrix();                                      //设置为保存的矩阵
  22. glMatrixMode(GL_MODELVIEW);                        //选择模型观察矩阵
  23. glPopMatrix();                                      //设置为保存
  24. glEnable(GL_DEPTH_TEST);                            //启用深度测试
  25. }

首先是buildFont()函数。我们先是定义两个临时变量来储存字体纹理中每个字的位置,cx储存水平方向位置,cy储存竖直方向位置。接着我们告诉OpenGL我们要建立256个显示列表,变量m_Base指向第一个显示列表,然后选择我们的字体纹理。现在我们开始循环,来创建所以256个字符,并存在显示列表里。一开始我们计算得到cx、cy,对16取余和除以16是由于一行是16个字符,最后都除以16.0f是按16个字符把纹理宽度高度均为1.0分成16份。

后面就开始创建显示列表了,绘制四边形对应纹理时,+或-0.0625f是指一个字符的高或宽,还有由于纹理坐标(0, 0)是在左下角,所以glTexCoord2f(x, y)的第二参数是1-cy、1-cy-0.0625而不是cy、cy+0.0625(比如说cx、cy同时为0,那它对应的字符纹理左下角坐标就应是(0.0, 1-0.0f-0.0625f)了,希望大家能明白)。要注意的是,我们使用glVertex2i()而不是glVertex3f(),我们的字体是2D字体,所以不需要z值。因为我们使用的是正交投影,我们不需要移进屏幕,在一个正交投影平面绘图你所需要的是指定x、y坐标。又因为我们的屏幕是以像素形式从0到639(宽),从0到479(高),因此我们既不需要用浮点数也不需要负数。

画完四边形后,我们右移了10个像素,至于纹理有病。如果我们不平移,文字将会重叠。有由于我们的字体太窄太瘦,我们不想右移16个像素那么多,如果那样的话,每个字符之间将有很大的间隔,只移动10个像素是个不错的选择。

接着是killFont()函数。它很简单,就是调用glDeleteLists()函数从m_Base开始删除256个显示列表。

最后是glPrint()函数。首先我们判断一下set字符集,如果大于1,就将set置0。这是由于我们的字体纹理中只有两种字体,第一种是普通的,第二种是斜体,如果选择的字符集有误,我们就把它设为默认的普通字符集。接着我们再次选择字体纹理,我们这么做事防止我们在决定往屏幕上输出文字前选择了别的纹理,导致出错。然后我们禁用了深度测试,我们这么做事因为混合的效果会更好。如果我们不禁用深度测试,文字可能会被什么东西挡住,或者得不到正确的混合效果。当然,如果你不打算混合文字(那样文字周围的黑色区域就不会显示),你就可以启用深度测试。

下面几行十分重要!我们选择投影矩阵,然后调用glPushMatrix()函数,保存当前投影矩阵(其实就是把投影矩阵压入堆栈)。保存投影矩阵后,我们重置矩阵并调用glOrtho()设置正交投影屏幕,第一和第三个参数表示屏幕的底边和左边,第二和第四个参数表示屏幕的上边和右边。由于我们不需要用到深度测试,所以我们将第五和第六个参数设为-1和1。我们再选择模型观察矩阵,用glPushMatrix()保存当前设置。然后我们重置模型观察矩阵以便在正交投影视点下工作。

现在我们可以绘制文字了,我们从移动到绘制文字的位置开始。我们使用glTranslated()而不是glTranslatef(),因为我们处理的是像素,所以浮点数没有意义。接着我们用glListBase()来选择字符集,如果我们想使用第二个字符集,我们在当前的显示列表基数上加上128,通过加128,我们跳过了前128个字符。而减去32是因为我们的字符集是从“空格”开始的,即ASCII码第33个字符开始,故我们减去32,告诉OpenGL跳过前面32个字符。然后我们使用glCallLists()绘制文字,这个之前解释过,不再解释了。

最后我们要做的是恢复透视视图。我们选择投影矩阵并用glPopMatrix()恢复我们先前glPushMatrix()保存的设置,接着选择模型观察矩阵做相同的工作。你或许会问,难道不用按相反顺序弹出矩阵吗?不用,这是用于投影矩阵和模型观察矩阵的堆栈并不是同一个(这样说其实并不准确,不过道理是差不多的),所以无论选择哪个矩阵先弹出都没有问题。值得注意的是,如果你把代码中的最后两句glMatrixMode()调换位置,运行程序时你是看不到图像纹理的只能看到文字,这是由于我们最后选择的矩阵是GL_PROJECTION,而我们绘制图像纹理是在GL_MODEWIEW上绘制的,所以你看不到图像纹理。当然解决办法就是,在恢复了投影矩阵后,开始深度测试之前,再次调用glMatrix()选择模型观察矩阵GL_MODEVIEW。那为什么我们能看到文字呢?这是由于做了平面正交投影后,在任何矩阵所绘制的东西都是在平面绘制的,OpenGL自动会把它们投影到屏幕上,所以总能看到文字的。函数最后我们启用了深度测试,如果你没有在上面的代码中关闭深度测试,就不需要这行。

然后我们修改一下initializeGL()函数,具体代码如下:

  1. void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
  2. {
  3. m_Texture[0] = bindTexture(QPixmap(m_FileName[0])); //载入位图并转换成纹理
  4. m_Texture[1] = bindTexture(QPixmap(m_FileName[1]));
  5. glEnable(GL_TEXTURE_2D);                            //启用纹理映射
  6. glClearColor(0.0, 0.0, 0.0, 0.0);                   //黑色背景
  7. glShadeModel(GL_SMOOTH);                            //启用阴影平滑
  8. glClearDepth(1.0);                                  //设置深度缓存
  9. glEnable(GL_DEPTH_TEST);                            //启用深度测试
  10. glDepthFunc(GL_LEQUAL);                             //所作深度测试的类型
  11. glBlendFunc(GL_SRC_ALPHA, GL_ONE);                  //设置混合因子
  12. glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  //告诉系统对透视进行修正
  13. buildFont();                                        //创建字体
  14. }

最开始三行载入位图转换纹理,启用纹理映射就不解释了。中间部分有小的改动,由于我们要给字体上色,所以要设置混合因子。最后调用buildFont()创建字体。

最后,我们该进入paintGL()函数,这次难度还行,具体代码如下:

  1. void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
  2. {
  3. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
  4. glLoadIdentity();                                   //重置当前的模型观察矩阵
  5. glBindTexture(GL_TEXTURE_2D, m_Texture[1]);         //设置为图像纹理
  6. glTranslatef(0.0f, 0.0f, -5.0f);                    //移入屏幕5.0单位
  7. glRotatef(45.0f, 0.0f, 0.0f, 1.0f);                 //绕z轴旋转45度
  8. glRotatef(m_Cnt1*30.0f, 1.0f, 1.0f, 0.0f);          //绕(1,1,0)轴旋转
  9. glDisable(GL_BLEND);                                //关闭融合
  10. glColor3f(1.0f, 1.0f, 1.0f);                        //设置颜色为白色
  11. glBegin(GL_QUADS);                                  //绘制纹理四边形
  12. glTexCoord2d(0.0f, 0.0f);
  13. glVertex2f(-1.0f, 1.0f);
  14. glTexCoord2d(1.0f, 0.0f);
  15. glVertex2f(1.0f, 1.0f);
  16. glTexCoord2d(1.0f, 1.0f);
  17. glVertex2f(1.0f, -1.0f);
  18. glTexCoord2d(0.0f, 1.0f);
  19. glVertex2f(-1.0f, -1.0f);
  20. glEnd();
  21. glRotatef(90.0f, 1.0f, 1.0f, 0.0);                  //绕(1,1,0)轴旋转90度
  22. glBegin(GL_QUADS);                                  //绘制第二个四边形,与第一个垂直
  23. glTexCoord2d(0.0f, 0.0f);
  24. glVertex2f(-1.0f, 1.0f);
  25. glTexCoord2d(1.0f, 0.0f);
  26. glVertex2f(1.0f, 1.0f);
  27. glTexCoord2d(1.0f, 1.0f);
  28. glVertex2f(1.0f, -1.0f);
  29. glTexCoord2d(0.0f, 1.0f);
  30. glVertex2f(-1.0f, -1.0f);
  31. glEnd();
  32. glEnable(GL_BLEND);                                 //启用混合
  33. glLoadIdentity();                                   //重置视口
  34. //根据字体位置设置颜色
  35. glColor3f(1.0f*float(cos(m_Cnt1)), 1.0*float(sin(m_Cnt2)),
  36. 1.0f-0.5f*float(cos(m_Cnt1+m_Cnt2)));
  37. glPrint(int((280+250*cos(m_Cnt1))),
  38. int(235+200*sin(m_Cnt2)), "NeHe", 0);
  39. glColor3f(1.0*float(sin(m_Cnt2)), 1.0f-0.5f*float(cos(m_Cnt1+m_Cnt2)),
  40. 1.0f*float(cos(m_Cnt1)));
  41. glPrint(int((280+230*cos(m_Cnt2))),
  42. int(235+200*sin(m_Cnt1)), "OpenGL", 1);
  43. glColor3f(0.0f, 0.0f, 1.0f);
  44. glPrint(int(240+200*cos((m_Cnt1+m_Cnt2)/5)), 2,
  45. "Giuseppe D'Agata", 0);
  46. glColor3f(1.0f, 1.0f, 1.0f);
  47. glPrint(int(242+200*cos((m_Cnt1+m_Cnt2)/5)), 2,
  48. "Giuseppe D'Agata", 0);
  49. m_Cnt1 += 0.01f;                                   //增加两个计数器的值
  50. m_Cnt2 += 0.0081f;
  51. }

函数中我们先绘制3D物体最后绘制文字,这样文字将显示在3D物体上面,而不会被3D物体遮住。我们之所以加入一个3D物体是为了演示透视投影和正交投影可同时使用。首先我们选择纹理,为了看见3D物体,我们往屏幕内移动5个单位。我们绕z轴旋转45度,这将使我们的四边形顺时针旋转45度,让我们的四边形看起来更像砖石而不是矩形,接着我们让物体同时绕x、y轴旋转m_Cnt1*30度,这使我们的物体像在一个点上旋转的钻石那样旋转。然后我们关闭混合,设置颜色为亮白,绘制第一个纹理映射的四边形。再绕x、y轴旋转90度,画另一个四边形,第二个四边形从第一个四边形中间切过去,来形成一个好看的形状。

在绘制完有纹理贴图的四边形后,我们开启混合并绘制文字,下面的根据文字选择颜色,打印“NeHe”、“OpenGL”就不解释了。我们来看打印“Giuseppe D'Agata”时,我们用深蓝色和亮白色两次绘制(作者的名字),并在x方向上平移2个像素,这样创造出一种亮白色字附带深蓝色阴影的效果,感觉真的很棒啊!要注意的是,这里必须打开混合,如果没有打开是不会出现这样的效果的(大家可以注释掉glEnable(GL_BLEND)看看,我就不解释了),甚至其它两个字符串也变得糟糕透了。最后一件事是以不同的速率递增我们的计数器,这使得文字移动,3D物体自转。

现在就可以运行程序查看效果了!

第18课:二次几何体 (参照NeHe)

这次教程中,我将介绍二次几何体。利用二次几何体,我们可以很容易创建球、圆盘、圆柱和圆锥。

我们先介绍一下二次几何体GLUquadric(NeHe教程用的是GLUquadricObj,源代码中GLUquadricObj是GLUquadric的别名),其实它本质上是一个二次方程,即a1x^2 + a2y^2 + a3z^2 + a4xy + a5yz + a6zx + a7x + a8y + a9z + a10 = 0。要知道,任何一个空间规则曲面(包括平面)都是可以用二次方程表示出来的,因此OpenGL利用二次几何体来实现一些函数,帮助用户更简单的绘画出常用的空间曲面。

程序运行时效果如下:

下面进入教程:

我们将在第07课的基础上修改代码,我只会对新增代码作解释,首先打开myglwidget.h文件,将类声明更改如下:

  1. #ifndef MYGLWIDGET_H
  2. #define MYGLWIDGET_H
  3. #include <QWidget>
  4. #include <QGLWidget>
  5. class GLUquadric;
  6. class MyGLWidget : public QGLWidget
  7. {
  8. Q_OBJECT
  9. public:
  10. explicit MyGLWidget(QWidget *parent = 0);
  11. ~MyGLWidget();
  12. protected:
  13. //对3个纯虚函数的重定义
  14. void initializeGL();
  15. void resizeGL(int w, int h);
  16. void paintGL();
  17. void keyPressEvent(QKeyEvent *event);           //处理键盘按下事件
  18. private:
  19. void glDrawCube();                              //绘制立方体
  20. private:
  21. bool fullscreen;                                //是否全屏显示
  22. QString m_FileName;                             //图片的路径及文件名
  23. GLuint m_Texture;                               //储存一个纹理
  24. bool m_Light;                                   //光源的开/关
  25. GLfloat m_xRot;                                 //x旋转角度
  26. GLfloat m_yRot;                                 //y旋转角度
  27. GLfloat m_xSpeed;                               //x旋转速度
  28. GLfloat m_ySpeed;                               //y旋转速度
  29. GLfloat m_Deep;                                 //深入屏幕的距离
  30. int m_Part1;                                    //圆盘的起始角度
  31. int m_Part2;                                    //圆盘的结束角度
  32. int m_P1;                                       //增量1
  33. int m_P2;                                       //增量2
  34. GLUquadric *m_Quadratic;                        //二次几何体
  35. GLuint m_Object;                                //绘制对象标示符
  36. };
  37. #endif // MYGLWIDGET_H

首先我们在类前面增加了GLUquadric的类声明。接着我们增加了6个变量,前4个变量用于控制绘制“部分圆盘”的,下面会解释。然后我们定义一个二次几何体对象指针和一个GLuint变量,二次几何体就不解释了,m_Object是配合键盘控制来完成图形之间切换的。最后我们增加了一个函数声明glDrawCube(),这个函数是用来绘制立方体的。

接下来,我们需要打开myglwidget.cpp,在构造函数中初始化新增变量(除了m_Quadratic)并修改析构函数(删除掉创建的二次几何体),很简单不多解释,具体代码如下:

  1. MyGLWidget::MyGLWidget(QWidget *parent) :
  2. QGLWidget(parent)
  3. {
  4. fullscreen = false;
  5. m_FileName = "D:/QtOpenGL/QtImage/Wall1.bmp";       //应根据实际存放图片的路径进行修改
  6. m_Light = false;
  7. m_xRot = 0.0f;
  8. m_yRot = 0.0f;
  9. m_xSpeed = 0.0f;
  10. m_ySpeed = 0.0f;
  11. m_Deep = -5.0f;
  12. m_Part1 = 0;
  13. m_Part2 = 0;
  14. m_P1 = 0;
  15. m_P2 = 1;
  16. m_Object = 0;
  17. QTimer *timer = new QTimer(this);                   //创建一个定时器
  18. //将定时器的计时信号与updateGL()绑定
  19. connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
  20. timer->start(10);                                   //以10ms为一个计时周期
  21. }
  1. MyGLWidget::~MyGLWidget()
  2. {
  3. gluDeleteQuadric(m_Quadratic);
  4. }

继续,我们需要定义我们新增的glDrawCube()函数了,其实就是画一个纹理立方体,完全可以从第07课的paintGL()函数中复制过来,不再多作解释,代码如下:

  1. void MyGLWidget::glDrawCube()
  2. {
  3. glBegin(GL_QUADS);                                  //开始绘制立方体
  4. glNormal3f(0.0f, 1.0f, 0.0f);
  5. glTexCoord2f(1.0f, 1.0f);
  6. glVertex3f(1.0f, 1.0f, -1.0f);                  //右上(顶面)
  7. glTexCoord2f(0.0f, 1.0f);
  8. glVertex3f(-1.0f, 1.0f, -1.0f);                 //左上(顶面)
  9. glTexCoord2f(0.0f, 0.0f);
  10. glVertex3f(-1.0f, 1.0f, 1.0f);                  //左下(顶面)
  11. glTexCoord2f(1.0f, 0.0f);
  12. glVertex3f(1.0f, 1.0f, 1.0f);                   //右下(顶面)
  13. glNormal3f(0.0f, -1.0f, 0.0f);
  14. glTexCoord2f(0.0f, 0.0f);
  15. glVertex3f(1.0f, -1.0f, 1.0f);                  //右上(底面)
  16. glTexCoord2f(1.0f, 0.0f);
  17. glVertex3f(-1.0f, -1.0f, 1.0f);                 //左上(底面)
  18. glTexCoord2f(1.0f, 1.0f);
  19. glVertex3f(-1.0f, -1.0f, -1.0f);                //左下(底面)
  20. glTexCoord2f(0.0f, 1.0f);
  21. glVertex3f(1.0f, -1.0f, -1.0f);                 //右下(底面)
  22. glNormal3f(0.0f, 0.0f, 1.0f);
  23. glTexCoord2f(1.0f, 1.0f);
  24. glVertex3f(1.0f, 1.0f, 1.0f);                   //右上(前面)
  25. glTexCoord2f(0.0f, 1.0f);
  26. glVertex3f(-1.0f, 1.0f, 1.0f);                  //左上(前面)
  27. glTexCoord2f(0.0f, 0.0f);
  28. glVertex3f(-1.0f, -1.0f, 1.0f);                 //左下(前面)
  29. glTexCoord2f(1.0f, 0.0f);
  30. glVertex3f(1.0f, -1.0f, 1.0f);                  //右下(前面)
  31. glNormal3f(0.0f, 0.0f, -1.0f);
  32. glTexCoord2f(0.0f, 0.0f);
  33. glVertex3f(1.0f, -1.0f, -1.0f);                 //右上(后面)
  34. glTexCoord2f(1.0f, 0.0f);
  35. glVertex3f(-1.0f, -1.0f, -1.0f);                //左上(后面)
  36. glTexCoord2f(1.0f, 1.0f);
  37. glVertex3f(-1.0f, 1.0f, -1.0f);                 //左下(后面)
  38. glTexCoord2f(0.0f, 1.0f);
  39. glVertex3f(1.0f, 1.0f, -1.0f);                  //右下(后面)
  40. glNormal3f(-1.0f, 0.0f, 0.0f);
  41. glTexCoord2f(1.0f, 1.0f);
  42. glVertex3f(-1.0f, 1.0f, 1.0f);                  //右上(左面)
  43. glTexCoord2f(0.0f, 1.0f);
  44. glVertex3f(-1.0f, 1.0f, -1.0f);                 //左上(左面)
  45. glTexCoord2f(0.0f, 0.0f);
  46. glVertex3f(-1.0f, -1.0f, -1.0f);                //左下(左面)
  47. glTexCoord2f(1.0f, 0.0f);
  48. glVertex3f(-1.0f, -1.0f, 1.0f);                 //右下(左面)
  49. glNormal3f(1.0f, 0.0f, 0.0f);
  50. glTexCoord2f(1.0f, 1.0f);
  51. glVertex3f(1.0f, 1.0f, -1.0f);                  //右上(右面)
  52. glTexCoord2f(0.0f, 1.0f);
  53. glVertex3f(1.0f, 1.0f, 1.0f);                   //左上(右面)
  54. glTexCoord2f(0.0f, 0.0f);
  55. glVertex3f(1.0f, -1.0f, 1.0f);                  //左下(右面)
  56. glTexCoord2f(1.0f, 0.0f);
  57. glVertex3f(1.0f, -1.0f, -1.0f);                 //右下(右面)
  58. glEnd();                                            //立方体绘制结束
  59. }

然后我们需要修改一下initializeGL()函数,在其中完成对m_Quadratic的初始化,具体代码如下:

  1. void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
  2. {
  3. m_Texture = bindTexture(QPixmap(m_FileName));       //载入位图并转换成纹理
  4. glEnable(GL_TEXTURE_2D);                            //启用纹理映射
  5. glClearColor(0.0f, 0.0f, 0.0f, 0.0f);               //黑色背景
  6. glShadeModel(GL_SMOOTH);                            //启用阴影平滑
  7. glClearDepth(1.0);                                  //设置深度缓存
  8. glEnable(GL_DEPTH_TEST);                            //启用深度测试
  9. glDepthFunc(GL_LEQUAL);                             //所作深度测试的类型
  10. glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  //告诉系统对透视进行修正
  11. m_Quadratic = gluNewQuadric();                        //创建二次几何体
  12. gluQuadricNormals(m_Quadratic, GLU_SMOOTH);           //使用平滑法线
  13. gluQuadricTexture(m_Quadratic, GL_TRUE);              //使用纹理
  14. //光源部分
  15. GLfloat LightAmbient[] = {0.5f, 0.5f, 0.5f, 1.0f};  //环境光参数
  16. GLfloat LightDiffuse[] = {1.0f, 1.0f, 1.0f, 1.0f};  //漫散光参数
  17. GLfloat LightPosition[] = {0.0f, 0.0f, 2.0f, 1.0f}; //光源位置
  18. glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient);     //设置环境光
  19. glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse);     //设置漫射光
  20. glLightfv(GL_LIGHT1, GL_POSITION, LightPosition);   //设置光源位置
  21. glEnable(GL_LIGHT1);                                //启动一号光源
  22. }

注意到我们增加了三行代码,首先调用gluNewQuadric()创建了一个二次几何体对象,并让m_Quadratic指向这个二次几何体。然后第二行代码将在二次曲面的表面创建平滑的法向量,这样当灯光照上去的时候将会好看些。最后我们使在二次曲面表面的纹理映射有效。

还有就是paintGL()函数了,最近几课,我们通过分过程渐渐让paintGL()函数看起来趋于简化,具体代码如下:

  1. void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
  2. {
  3. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
  4. glLoadIdentity();                                   //重置模型观察矩阵
  5. glTranslatef(0.0f, 0.0f, m_Deep);                    //移入屏幕5.0单位
  6. glRotatef(m_xRot, 1.0f, 0.0f, 0.0f);                //绕x轴旋转
  7. glRotatef(m_yRot, 0.0f, 1.0f, 0.0f);                //绕y轴旋转
  8. glBindTexture(GL_TEXTURE_2D, m_Texture);            //选择纹理
  9. switch(m_Object)
  10. {
  11. case 0:                                             //绘制立方体
  12. glDrawCube();
  13. break;
  14. case 1:                                             //绘制圆柱体
  15. glTranslatef(0.0f, 0.0f, -1.5f);
  16. gluCylinder(m_Quadratic, 1.0f, 1.0f, 3.0f, 64, 64);
  17. break;
  18. case 2:                                             //绘制圆盘
  19. gluDisk(m_Quadratic, 0.5f, 1.5f, 64, 64);
  20. break;
  21. case 3:                                             //绘制球
  22. gluSphere(m_Quadratic, 1.3f, 64, 64);
  23. break;
  24. case 4:                                             //绘制圆锥
  25. glTranslatef(0.0f, 0.0f, -1.5f);
  26. gluCylinder(m_Quadratic, 1.0f, 0.0f, 3.0f, 64, 64);
  27. break;
  28. case 5:                                             //绘制部分圆盘
  29. m_Part1 += m_P1;
  30. m_Part2 += m_P2;
  31. if (m_Part1 > 359)
  32. {
  33. m_P1 = 0;
  34. m_Part1 = 0;
  35. m_P2 = 1;
  36. m_Part2 = 0;
  37. }
  38. if (m_Part2 > 359)
  39. {
  40. m_P1 = 1;
  41. m_P2 = 0;
  42. }
  43. gluPartialDisk(m_Quadratic, 0.5f, 1.5f, 64, 64, m_Part1, m_Part2-m_Part1);
  44. break;
  45. }
  46. m_xRot += m_xSpeed;                                 //x轴旋转
  47. m_yRot += m_ySpeed;                                 //y轴旋转
  48. }

我们将原来的绘制立方体部分的代码换成了一个switch()语句,我们利用m_Object来确定画哪一种物体(具体哪个值对应哪个,请大家参照注释)。我们后面讨论绘制这些物体调用的函数时,会忽略第一个参数m_Quadratic,这个参数将被除立方体外的所有对象使用。由于前面已经解释过二次几何体的实质,我们在讨论下面函数的参数时将忽略它。

我们创建的第2个对象是一个圆柱体:参数2是圆柱的底面半径;参数3是圆柱的顶面半径;参数4是圆柱的高度(表面我们也可以绘制圆台的);参数5是纬线(环绕z轴有多少细分);参数6是经线(沿着z轴有多少细分)。细分越多该对象就越细致,其实我们可以用gluCylinder来绘制多棱柱的,只要把参数5和参数6换成对应的棱数就行了。

第3个对象是一个CD一样的盘子:参数2是盘子的内圆半径,该参数可以为0.0,则表示在盘子中间没孔,内圆半径越大孔越大;参数3表示外圆半径,这个参数必须比内圆半径大;参数4是组成该盘子切片的数量;参数5是组成盘子的环的数量,环很像唱片上的轨迹。同样,把参数4改成边数,同样可以得到带孔(不带孔)的多边形。

第4个对象是球:参数2是球的半径;和圆柱一样,参数3是纬线;参数4是经线。细分越多球看起来就越平滑。

第5个对象是圆锥:其实和绘制圆柱是一样的,只是把顶面半径设置为0.0,这样顶面就成了一个点。同样参考上面说的方法可以绘制多棱锥。

第6个对象将被gluPartialDisk()函数创建。相比于gluDisk()函数,gluPartialDisk()多了两个新参数。参数6是我们想要绘制的分部盘子的开始角度,参数6是旋转角,也就是转过的调度。我们将要增加旋转角,这将引起盘子沿顺时针方向缓慢的被绘制在屏幕上。一旦旋转角达到360度,我们将开始增加开始角度,这样盘子看起来就像是被逐渐地抹去一样,我们将重复这两个过程。

最后我们修改一下键盘控制函数,不多解释了,具体代码如下:

  1. void MyGLWidget::keyPressEvent(QKeyEvent *event)
  2. {
  3. switch (event->key())
  4. {
  5. case Qt::Key_F1:                                    //F1为全屏和普通屏的切换键
  6. fullscreen = !fullscreen;
  7. if (fullscreen)
  8. {
  9. showFullScreen();
  10. }
  11. else
  12. {
  13. showNormal();
  14. }
  15. break;
  16. case Qt::Key_Escape:                                //ESC为退出键
  17. close();
  18. break;
  19. case Qt::Key_L:                                     //L为开启关闭光源的切换键
  20. m_Light = !m_Light;
  21. if (m_Light)
  22. {
  23. glEnable(GL_LIGHTING);                      //开启光源
  24. }
  25. else
  26. {
  27. glDisable(GL_LIGHTING);                     //关闭光源
  28. }
  29. break;
  30. case Qt::Key_Space:                                 //空格为物体的切换键
  31. m_Object++;
  32. if (m_Object == 6)
  33. {
  34. m_Object = 0;
  35. }
  36. break;
  37. case Qt::Key_PageUp:                                //PageUp按下使木箱移向屏幕内部
  38. m_Deep -= 0.1f;
  39. break;
  40. case Qt::Key_PageDown:                              //PageDown按下使木箱移向观察者
  41. m_Deep += 0.1f;
  42. break;
  43. case Qt::Key_Up:                                    //Up按下减少m_xSpeed
  44. m_xSpeed -= 0.1f;
  45. break;
  46. case Qt::Key_Down:                                  //Down按下增加m_xSpeed
  47. m_xSpeed += 0.1f;
  48. break;
  49. case Qt::Key_Right:                                 //Right按下减少m_ySpeed
  50. m_ySpeed -= 0.1f;
  51. break;
  52. case Qt::Key_Left:                                  //Left按下增加m_ySpeed
  53. m_ySpeed += 0.1f;
  54. break;
  55. }
  56. }

现在就可以运行程序查看效果了!

第19课:粒子系统 (参照NeHe)

这次教程中,我们将创建一个简单的粒子系统,并用它来创建一种喷射效果。利用粒子系统,我们可以实现爆炸、喷泉、流星之类的效果,听起来是不是很棒呢!

我们还会讲到一个新东西,三角形带(我的理解就是画很多三角形来组合成我们要的形状),它非常容易使用,而且当需要画很多三角形的时候,它能加快你程序的运行速度。这次教程中,我将教你该如何做一个简单的微粒程序,一旦你了解微粒程序的原理后,再创建例如:火、烟、喷泉等效果将是很轻松的事情。

程序运行时效果如下:

下面进入教程:

我们这次将在第06课代码的基础上修改代码,这次需要修改的代码量不少,希望大家耐心跟着我一步步来完成这个程序。首先打开myglwidget.h文件,将类声明更改如下:

  1. #ifndef MYGLWIDGET_H
  2. #define MYGLWIDGET_H
  3. #include <QWidget>
  4. #include <QGLWidget>
  5. class MyGLWidget : public QGLWidget
  6. {
  7. Q_OBJECT
  8. public:
  9. explicit MyGLWidget(QWidget *parent = 0);
  10. ~MyGLWidget();
  11. protected:
  12. //对3个纯虚函数的重定义
  13. void initializeGL();
  14. void resizeGL(int w, int h);
  15. void paintGL();
  16. void keyPressEvent(QKeyEvent *event);           //处理键盘按下事件
  17. private:
  18. bool fullscreen;                                //是否全屏显示
  19. QString m_FileName;                             //图片的路径及文件名
  20. GLuint m_Texture;                               //储存一个纹理
  21. static const int MAX_PARTICLES = 1000;          //最大粒子数
  22. static const GLfloat COLORS[12][3];             //彩虹的颜色
  23. bool m_Rainbow;                                 //是否为彩虹模式
  24. GLuint m_Color;                                 //当前的颜色
  25. float m_Slowdown;                               //减速粒子
  26. float m_xSpeed;                                 //x方向的速度
  27. float m_ySpeed;                                 //y方向的速度
  28. float m_Deep;                                   //移入屏幕的距离
  29. struct Particle                                 //创建粒子结构体
  30. {
  31. bool active;                                //是否激活
  32. float life;                                 //粒子生命
  33. float fade;                                 //衰减速度
  34. float r, g, b;                              //粒子颜色
  35. float x, y, z;                              //位置坐标
  36. float xi, yi, zi;                           //各方向速度
  37. float xg, yg, zg;                           //各方向加速度
  38. } m_Particles[MAX_PARTICLES];                   //存放1000个粒子的数组
  39. };
  40. #endif // MYGLWIDGET_H

首先我们定义了一个静态整形常量MAX_PARTICLES来存放粒子的最大数目,和一个静态GLfloat常量数组来存放彩虹的颜色。接着是一个布尔变量m_Rainbow来表示当前模式是否为彩虹模式,然后是GLuint变量m_Color来表示当前的粒子的颜色,它将在控制粒子颜色在彩虹颜色数组中切换。粒子颜色会与纹理融合,我们用纹理而不用电的重要原因是,点的速度慢,而且挺麻烦的,其次纹理很酷,也好控制。

下面四行是定义了四个浮点变量。m_Slowdown控制粒子移动的快慢,数值越高移动越快,数值越低移动越慢,粒子的速度将影响它们在屏幕上移动的距离,要注意速度慢的粒子不会移动很远就会消失。m_xSpeed和m_ySpeed控制尾部的方向,m_xSpeed为正时粒子将会向右移动,负时则向左移动,m_ySpeed为正时粒子将会向上移动,负时则向下移动,m_xSpeed和m_ySpeed有助于在我们想要的方向上移动粒子。最后是变量m_Deep,我们用该变量移入移除我们的屏幕,在粒子系统中,有时当接近你时,可以看见更多美妙的图像。

最后我们定义了结构体Particle,用来描述某一粒子的状态属性。我们用布尔变量active开始,如果为true,我们的粒子为活跃的;如果为false则粒子为死的,此时我们就不绘制它。变量life和fade来控制粒子显示多久以及显示时候的亮度,随着life数值的降低fade的数值也相应减低,这将导致一些粒子比其他粒子燃烧的时间长。后面是记录粒子颜色,位置,速度,加速度等状态属性的变量,作用我想大家会点高中物理都能明白的,最后我们创建一个长度为MAX_PARTICLES的结构体数组。

接下来,我们打开myglwidget.cpp,在构造函数中对新增变量进行初始化,具体代码如下:

  1. const GLfloat MyGLWidget::COLORS[][3] =                 //彩虹的颜色
  2. {
  3. {1.0f, 0.5f, 0.5f}, {1.0f, 0.75f, 0.5f}, {1.0f, 1.0f, 0.5f},
  4. {0.75f, 1.0f, 0.5f}, {0.5f, 1.0f, 0.5f}, {0.5f, 1.0f, 0.75f},
  5. {0.5f, 1.0f, 1.0f}, {0.5f, 0.75f, 1.0f}, {0.5f, 0.5f, 1.0f},
  6. {0.75f, 0.5f, 1.0f}, {1.0f, 0.5f, 1.0f}, {1.0f, 0.5f, 0.75f}
  7. };
  8. MyGLWidget::MyGLWidget(QWidget *parent) :
  9. QGLWidget(parent)
  10. {
  11. fullscreen = false;
  12. m_FileName = "D:/QtOpenGL/QtImage/Particle.bmp";    //应根据实际存放图片的路径进行修改
  13. m_Rainbow = true;
  14. m_Color = 0;
  15. m_Slowdown = 2.0f;
  16. m_xSpeed = 0.0f;
  17. m_ySpeed = 0.0f;
  18. m_Deep = -40.0f;
  19. for (int i=0; i<MAX_PARTICLES; i++)                 //循环初始化所以粒子
  20. {
  21. m_Particles[i].active = true;                   //使所有粒子为激活状态
  22. m_Particles[i].life = 1.0f;                     //所有粒子生命值为最大
  23. //随机生成衰减速率
  24. m_Particles[i].fade = float(rand()%100)/1000.0f+0.001;
  25. //粒子的颜色
  26. m_Particles[i].r = COLORS[int(i*(12.0f/MAX_PARTICLES))][0];
  27. m_Particles[i].g = COLORS[int(i*(12.0f/MAX_PARTICLES))][1];
  28. m_Particles[i].b = COLORS[int(i*(12.0f/MAX_PARTICLES))][2];
  29. //粒子的初始位置
  30. m_Particles[i].x = 0.0f;
  31. m_Particles[i].y = 0.0f;
  32. m_Particles[i].z = 0.0f;
  33. //随机生成x、y、z轴方向速度
  34. m_Particles[i].xi = float((rand()%50)-26.0f)*10.0f;
  35. m_Particles[i].yi = float((rand()%50)-25.0f)*10.0f;
  36. m_Particles[i].zi = float((rand()%50)-25.0f)*10.0f;
  37. m_Particles[i].xg = 0.0f;                       //设置x方向加速度为0
  38. m_Particles[i].yg = -0.8f;                      //设置y方向加速度为-0.8
  39. m_Particles[i].zg = 0.0f;                       //设置z方向加速度为0
  40. }
  41. QTimer *timer = new QTimer(this);                   //创建一个定时器
  42. //将定时器的计时信号与updateGL()绑定
  43. connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
  44. timer->start(10);                                   //以10ms为一个计时周期
  45. }

注意到我们在构造函数之前对定义的静态常量数组COLORS进行初始化,一共包含12种渐变颜色,从红色到紫罗兰。进入构造函数一开始是更换纹理图片以及增加变量的初始化,这些没什么好解释的,下面我们重点看循环部分。我们利用循环来初始化每个粒子,我们让粒子变活跃(不活跃的粒子在屏幕上是不会显示的)之后,我们给它lfie。life满值是1.0f,这也给粒子完整的光亮。值得一提,把粒子的生命衰退和颜色渐暗绑到一起,效果真的很不错!

我们通过随机数来设置粒子退色的快慢,我们取0~99的随机数,然后平分1000份来得到一个很小的浮点数,最后结果加上0.001f来使fade速度值不为0。我们既然给了粒子生命,我们当然要给它其他的属性状态附上值,为了使粒子有不同的颜色,我们用i 变量乘以数组中颜色的数目(12)与MAX_PARTICLES的商,再转换成整数,利用得到的整数取对应的颜色就可以了。然后让粒子从(0, 0, 0)出发,在设定速度时,我们通过将结果乘上10.0f来创造开始时的爆炸效果,加速度就由我们统一指定初始值了。

然后,我们来略微修改initializeGL()函数,代码如下:

  1. void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
  2. {
  3. m_Texture = bindTexture(QPixmap(m_FileName));       //载入位图并转换成纹理
  4. glEnable(GL_TEXTURE_2D);                            //启用纹理映射
  5. glClearColor(0.0f, 0.0f, 0.0f, 0.0f);               //黑色背景
  6. glShadeModel(GL_SMOOTH);                            //启用阴影平滑
  7. glClearDepth(1.0);                                  //设置深度缓存
  8. glDisable(GL_DEPTH_TEST);                           //禁止深度测试
  9. glEnable(GL_BLEND);                                 //启用融合
  10. glBlendFunc(GL_SRC_ALPHA, GL_ONE);                  //设置融合因子
  11. glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  //告诉系统对透视进行修正
  12. glHint(GL_POINT_SMOOTH_HINT, GL_NICEST);
  13. }

我们在中间启用了融合并设置了融合因子,这是为了我们的粒子能有不同颜色。然后我们禁用了深度测试,因为如果启用深度测试的话,纹理之间会出现覆盖现象,那样画面简直一团糟。

还有,我们要进入有趣的paintGL()函数了,具体代码如下:

  1. void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
  2. {
  3. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
  4. glLoadIdentity();                                   //重置模型观察矩阵
  5. glBindTexture(GL_TEXTURE_2D, m_Texture);
  6. for (int i=0; i<MAX_PARTICLES; i++)                 //循环所以的粒子
  7. {
  8. if (m_Particles[i].active)                      //如果粒子为激活的
  9. {
  10. float x = m_Particles[i].x;                 //x轴位置
  11. float y = m_Particles[i].y;                 //y轴位置
  12. float z = m_Particles[i].z + m_Deep;        //z轴位置
  13. //设置粒子颜色
  14. glColor4f(m_Particles[i].r, m_Particles[i].g,
  15. m_Particles[i].b, m_Particles[i].life);
  16. glBegin(GL_TRIANGLE_STRIP);                 //绘制三角形带
  17. glTexCoord2d(1, 1);glVertex3f(x+0.5f, y+0.5f, z);
  18. glTexCoord2d(0, 1);glVertex3f(x-0.5f, y+0.5f, z);
  19. glTexCoord2d(1, 0);glVertex3f(x+0.5f, y-0.5f, z);
  20. glTexCoord2d(0, 0);glVertex3f(x-0.5f, y-0.5f, z);
  21. glEnd();
  22. //更新各方向坐标及速度
  23. m_Particles[i].x += m_Particles[i].xi/(m_Slowdown*1000);
  24. m_Particles[i].y += m_Particles[i].yi/(m_Slowdown*1000);
  25. m_Particles[i].z += m_Particles[i].zi/(m_Slowdown*1000);
  26. m_Particles[i].xi += m_Particles[i].xg;
  27. m_Particles[i].yi += m_Particles[i].yg;
  28. m_Particles[i].zi += m_Particles[i].zg;
  29. m_Particles[i].life -= m_Particles[i].fade; //减少粒子的生命值
  30. if (m_Particles[i].life < 0.0f)             //如果粒子生命值小于0
  31. {
  32. m_Particles[i].life = 1.0f;             //产生一个新粒子
  33. m_Particles[i].fade = float(rand()%100)/1000.0f+0.003f;
  34. m_Particles[i].r = colors[m_Color][0];  //设置颜色
  35. m_Particles[i].g = colors[m_Color][1];
  36. m_Particles[i].b = colors[m_Color][2];
  37. m_Particles[i].x = 0.0f;                //粒子出现在屏幕中央
  38. m_Particles[i].y = 0.0f;
  39. m_Particles[i].z = 0.0f;
  40. //随机生成粒子速度
  41. m_Particles[i].xi = m_xSpeed + float((rand()%60)-32.0f);
  42. m_Particles[i].yi = m_ySpeed + float((rand()%60)-30.0f);
  43. m_Particles[i].zi = float((rand()%60)-30.0f);
  44. }
  45. }
  46. }
  47. if (m_Rainbow)                                      //如果为彩虹模式
  48. {
  49. m_Color++;                                      //进行颜色的变换
  50. if (m_Color > 11)
  51. {
  52. m_Color = 0;
  53. }
  54. }
  55. }

paintGL()函数中,我们在循环中没有重置模型观察矩阵,因为我们并没有使用过glRotate和glTranslate函数,我们在画粒子位置的时候,计算出相应坐标,用glVertex3f()函数来代替glTranslate函数,这样在我们画粒子的时候就不会改变模型观察矩阵了。

然后我们建立一个循环,在循环中更新绘制每一个粒子。首先检查粒子是否活跃,如果不活跃则不被更新(在这个程序中,它们始终都是活跃的)。接着定义三个临时变量存放粒子的x、y、z值,设置粒子颜色,然后就来绘制它了,我们用一个三角形带来代替四边形这样使程序运行快一点(一般情况是这样,关于三角形带点此有相关文章)。

接下来我们来移动粒子。首先我们取得当前粒子的x位置,然后把x运动速度加上粒子被减速1000倍后的值。所以如果粒子在x轴(0)上屏幕中心的位置,x轴速度(xi)为+10,而m_Slowdown为1,我们可以以10/(1*1000)或0.01f速度移向右边。如果,m_slowDown值到2我们的速度就只有0.005f了。这也是为什么yong10.0f乘开始值来叫像素移动快速,制造一个爆发效果。然后我们要根据加速度更新我们粒子的速度,根据衰退速度更新我们粒子的生命。

最后我们检查粒子是否还活着(生命值大于0),如果粒子烧尽,我们会使它恢复,我们给它满值生命和新的衰退速度。当然我们也重新设定粒子回到屏幕中心,然后重新随机生成速度。要注意,我们没有将移动速度乘10,我们这次不想要一个爆发效果,而要比较慢地移动粒子;然后我们要相应的加上m_xSpeed和m_ySpeed,这个控制了粒子大体得移动方向。最后我们给粒子分配当前的颜色就搞定循环了。

函数最后,我们判断是否为彩虹模式,如果是就改变当前的颜色,这样不同时间“重生”后的粒子就可能得到不同的颜色,从而出现彩虹效果。

最后就是键盘控制了,由于为了增加点趣味性,这次键盘控制比较“麻烦”,但是调理很清晰,具体代码如下:

  1. void MyGLWidget::keyPressEvent(QKeyEvent *event)
  2. {
  3. switch (event->key())
  4. {
  5. case Qt::Key_F1:                                    //F1为全屏和普通屏的切换键
  6. fullscreen = !fullscreen;
  7. if (fullscreen)
  8. {
  9. showFullScreen();
  10. }
  11. else
  12. {
  13. showNormal();
  14. }
  15. updateGL();
  16. break;
  17. case Qt::Key_Escape:                                //ESC为退出键
  18. close();
  19. break;
  20. case Qt::Key_Tab:                                   //Tab按下使粒子回到原点,产生爆炸
  21. for (int i=0; i<MAX_PARTICLES; i++)
  22. {
  23. m_Particles[i].x = 0.0f;
  24. m_Particles[i].y = 0.0f;
  25. m_Particles[i].z = 0.0f;
  26. //随机生成速度
  27. m_Particles[i].xi = float((rand()%50)-26.0f)*10.0f;
  28. m_Particles[i].yi = float((rand()%50)-25.0f)*10.0f;
  29. m_Particles[i].zi = float((rand()%50)-25.0f)*10.0f;
  30. }
  31. break;
  32. case Qt::Key_8:                                     //按下8增加y方向加速度
  33. for (int i=0; i<MAX_PARTICLES; i++)
  34. {
  35. if (m_Particles[i].yg < 3.0f)
  36. {
  37. m_Particles[i].yg += 0.05f;
  38. }
  39. }
  40. break;
  41. case Qt::Key_2:                                     //按下2减少y方向加速度
  42. for (int i=0; i<MAX_PARTICLES; i++)
  43. {
  44. if (m_Particles[i].yg > -3.0f)
  45. {
  46. m_Particles[i].yg -= 0.05f;
  47. }
  48. }
  49. break;
  50. case Qt::Key_6:                                     //按下6增加x方向加速度
  51. for (int i=0; i<MAX_PARTICLES; i++)
  52. {
  53. if (m_Particles[i].xg < 3.0f)
  54. {
  55. m_Particles[i].xg += 0.05f;
  56. }
  57. }
  58. break;
  59. case Qt::Key_4:                                     //按下4减少x方向加速度
  60. for (int i=0; i<MAX_PARTICLES; i++)
  61. {
  62. if (m_Particles[i].xg > -3.0f)
  63. {
  64. m_Particles[i].xg -= 0.05f;
  65. }
  66. }
  67. break;
  68. case Qt::Key_Plus:                                  //+ 号按下加速粒子
  69. if (m_Slowdown > 1.0f)
  70. {
  71. m_Slowdown -= 0.05f;
  72. }
  73. break;
  74. case Qt::Key_Minus:                                 //- 号按下减速粒子
  75. if (m_Slowdown < 3.0f)
  76. {
  77. m_Slowdown += 0.05f;
  78. }
  79. break;
  80. case Qt::Key_PageUp:                                //PageUp按下使粒子靠近屏幕
  81. m_Deep += 0.5f;
  82. break;
  83. case Qt::Key_PageDown:                              //PageDown按下使粒子远离屏幕
  84. m_Deep -= 0.5f;
  85. break;
  86. case Qt::Key_Return:                                //回车键为是否彩虹模式的切换键
  87. m_Rainbow = !m_Rainbow;
  88. break;
  89. case Qt::Key_Space:                                 //空格键为颜色切换键
  90. m_Rainbow = false;
  91. m_Color++;
  92. if (m_Color > 11)
  93. {
  94. m_Color = 0;
  95. }
  96. break;
  97. case Qt::Key_Up:                                    //Up按下增加粒子y轴正方向的速度
  98. if (m_ySpeed < 400.0f)
  99. {
  100. m_ySpeed += 5.0f;
  101. }
  102. break;
  103. case Qt::Key_Down:                                  //Down按下减少粒子y轴正方向的速度
  104. if (m_ySpeed > -400.0f)
  105. {
  106. m_ySpeed -= 5.0f;
  107. }
  108. break;
  109. case Qt::Key_Right:                                 //Right按下增加粒子x轴正方向的速度
  110. if (m_xSpeed < 400.0f)
  111. {
  112. m_xSpeed += 5.0f;
  113. }
  114. break;
  115. case Qt::Key_Left:                                  //Left按下减少粒子x轴正方向的速度
  116. if (m_xSpeed > -400.0f)
  117. {
  118. m_xSpeed -= 5.0f;
  119. }
  120. break;
  121. }
  122. }

我感觉注释已经写得比较清楚了,就不解释太多了,具体里面的值是怎么得到的,其实就是一点点尝试,感觉效果好久用了,就这么简单!大家注意一下Tab键按下后,全部粒子会回到原点,重新从原点出发,并且我们给它们重新生成速度,方式和初始化时是相同的,这样就又产生了爆炸效果。

现在就可以运行程序查看效果了!

第20课:蒙板 (参照NeHe)

这次教程中,我们教介绍OpenGL的蒙板技术。到目前为止,我们已经学会如何使用alpha混合,把一个透明物体渲染到屏幕上了,但有时使用它看起来并不是那么的复合我们的心意。使用蒙板技术,将会使图像按照我们设定的蒙板位置精确地绘制。

直到现在,我们在把图像加载到屏幕上时都没有檫除背景色,因为这样简单高效,但是效果并不总是很好。大部分情况下,把纹理混合到屏幕,纹理不是太少就是太多。当我们使用精灵图时,我们不希望背景从精灵的缝隙中透出光来;但在显示文字时,我们又希望文字的间隙可以显示背景色。

基于上述原因,我们需要使用“掩模”。使用“掩膜”需要两个步骤,首先我们在场景上放置黑白相间的纹理,白色代表透明部分,黑色代表不透明部分。接着我们使用一种特殊的混合方式,只有在黑色部分上的纹理才会显示在场景中。

程序运行时效果如下:

下面进入教程:

我们这次将在第06课代码的基础上修改代码,总体上并不会太难,希望大家能理解蒙板技术,这技术真的很好用。首先打开myglwidget.h文件,将类声明更改如下:

  1. #ifndef MYGLWIDGET_H
  2. #define MYGLWIDGET_H
  3. #include <QWidget>
  4. #include <QGLWidget>
  5. class MyGLWidget : public QGLWidget
  6. {
  7. Q_OBJECT
  8. public:
  9. explicit MyGLWidget(QWidget *parent = 0);
  10. ~MyGLWidget();
  11. protected:
  12. //对3个纯虚函数的重定义
  13. void initializeGL();
  14. void resizeGL(int w, int h);
  15. void paintGL();
  16. void keyPressEvent(QKeyEvent *event);           //处理键盘按下事件
  17. private:
  18. bool fullscreen;                                //是否全屏显示
  19. bool m_Masking;                                 //是否使用"<span style="font-size:12px;">掩模</span>"
  20. bool m_Scene;                                   //控制绘制哪一层
  21. GLfloat m_Rot;                                  //控制纹理滚动
  22. QString m_FileName[5];                          //图片的路径及文件名
  23. GLuint m_Texture[5];                            //储存五个纹理
  24. };
  25. #endif // MYGLWIDGET_H

我们增加了两个布尔变量m_Masking和m_Scene来控制是否开启“掩模”以及绘制哪一个场景。然后我们增加一个控制图形滚动旋转的变量m_Rot,当然要去掉之前控制旋转的变量。最后把m_FileName和m_Texture变成长度为5的数组,因为我们需要载入5个纹理。

接下来,我们打开myglwidget.cpp,在构造函数中对新增变量进行初始化,比较简单,大家参照注释理解,不多作解释,具体代码如下:

  1. MyGLWidget::MyGLWidget(QWidget *parent) :
  2. QGLWidget(parent)
  3. {
  4. fullscreen = false;
  5. m_Masking = true;
  6. m_Scene = false;
  7. m_FileName[0] = "D:/QtOpenGL/QtImage/Logo.bmp";     //纹理0
  8. m_FileName[1] = "D:/QtOpenGL/QtImage/Mask1.bmp";    //<span style="font-size:12px;">掩模</span>纹理1,作为"<span style="font-size:12px;">掩模</span>"使用
  9. m_FileName[2] = "D:/QtOpenGL/QtImage/Image1.bmp";   //纹理1
  10. m_FileName[3] = "D:/QtOpenGL/QtImage/Mask2.bmp";    //<span style="font-size:12px;">掩模</span>纹理2,作为"<span style="font-size:12px;">掩模</span>"使用
  11. m_FileName[4] = "D:/QtOpenGL/QtImage/Image2.bmp";   //纹理2
  12. QTimer *timer = new QTimer(this);                   //创建一个定时器
  13. //将定时器的计时信号与updateGL()绑定
  14. connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
  15. timer->start(10);                                   //以10ms为一个计时周期
  16. }

然后,我们略微修改下initializeGL()函数,就是载入5个位图并转换成纹理,不多解释了,具体代码如下:

  1. void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
  2. {
  3. //载入位图并转换成纹理
  4. for (int i=0; i<5; i++){
  5. m_Texture[i] = bindTexture(QPixmap(m_FileName[i]));
  6. }
  7. glEnable(GL_TEXTURE_2D);                            //启用纹理映射
  8. glClearColor(0.0f, 0.0f, 0.0f, 0.0f);               //黑色背景
  9. glShadeModel(GL_SMOOTH);                            //启用阴影平滑
  10. glClearDepth(1.0);                                  //设置深度缓存<pre name="code" class="cpp"><pre name="code" class="cpp">}


继续,我们要进入最有趣的paintGL()函数,当然这也是重点,具体代码如下:

  1. void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
  2. {
  3. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
  4. glLoadIdentity();                                   //重置模型观察矩阵
  5. glTranslatef(0.0f, 0.0f, -2.0f);                    //移入屏幕2.0单位
  6. glBindTexture(GL_TEXTURE_2D, m_Texture[0]);         //选择Logo纹理
  7. glBegin(GL_QUADS);                                  //绘制纹理四边形
  8. glTexCoord2f(0.0f, -m_Rot+0.0f);
  9. glVertex3f(-1.1f, -1.1f, 0.0f);
  10. glTexCoord2f(3.0f, -m_Rot+0.0f);
  11. glVertex3f(1.1f, -1.1f, 0.0f);
  12. glTexCoord2f(3.0f, -m_Rot+3.0f);
  13. glVertex3f(1.1f, 1.1f, 0.0f);
  14. glTexCoord2f(0.0f, -m_Rot+3.0f);
  15. glVertex3f(-1.1f, 1.1f, 0.0f);
  16. glEnd();
  17. glEnable(GL_BLEND);                                 //启用混合
  18. glDisable(GL_DEPTH_TEST);                           //禁用深度测试
  19. if (m_Masking)                                      //是否启用"<span style="font-size:12px;">掩模</span>"
  20. {
  21. glBlendFunc(GL_DST_COLOR, GL_ZERO);             //使用黑白"<span style="font-size:12px;">掩模</span>"
  22. }
  23. if (m_Scene)
  24. {
  25. glTranslatef(0.0f, 0.0f, -1.0f);                //移入屏幕1.0单位
  26. glRotatef(m_Rot*360, 0.0f, 0.0f, 1.0f);         //绕z轴旋转
  27. if (m_Masking)                                  //"<span style="font-size:12px;">掩模</span>"是否打开
  28. {
  29. glBindTexture(GL_TEXTURE_2D, m_Texture[3]); //选择第二个"<span style="font-size:12px;">掩模</span>"纹理
  30. glBegin(GL_QUADS);                          //开始绘制四边形
  31. glTexCoord2f(0.0f, 0.0f);
  32. glVertex3f(-1.1f, -1.1f, 0.0f);
  33. glTexCoord2f(1.0f, 0.0f);
  34. glVertex3f(1.1f, -1.1f, 0.0f);
  35. glTexCoord2f(1.0f, 1.0f);
  36. glVertex3f(1.1f, 1.1f, 0.0f);
  37. glTexCoord2f(0.0f, 1.0f);
  38. glVertex3f(-1.1f, 1.1f, 0.0f);
  39. glEnd();
  40. }
  41. glBlendFunc(GL_ONE, GL_ONE);                    //把纹理2复制到屏幕上
  42. glBindTexture(GL_TEXTURE_2D, m_Texture[4]);     //选择第二个纹理
  43. glBegin(GL_QUADS);                              //绘制四边形
  44. glTexCoord2f(0.0f, 0.0f);
  45. glVertex3f(-1.1f, -1.1f, 0.0f);
  46. glTexCoord2f(1.0f, 0.0f);
  47. glVertex3f(1.1f, -1.1f, 0.0f);
  48. glTexCoord2f(1.0f, 1.0f);
  49. glVertex3f(1.1f, 1.1f, 0.0f);
  50. glTexCoord2f(0.0f, 1.0f);
  51. glVertex3f(-1.1f, 1.1f, 0.0f);
  52. glEnd();
  53. }
  54. else
  55. {
  56. if (m_Masking)                                  //"<span style="font-size:12px;">掩模</span>"是否打开
  57. {
  58. glBindTexture(GL_TEXTURE_2D, m_Texture[1]); //选择第一个"<span style="font-size:12px;">掩模</span>"纹理
  59. glBegin(GL_QUADS);                          //绘制四边形
  60. glTexCoord2f(m_Rot+0.0f, 0.0f);
  61. glVertex3f(-1.1f, -1.1f, 0.0f);
  62. glTexCoord2f(m_Rot+4.0f, 0.0f);
  63. glVertex3f(1.1f, -1.1f, 0.0f);
  64. glTexCoord2f(m_Rot+4.0f, 4.0f);
  65. glVertex3f(1.1f, 1.1f, 0.0f);
  66. glTexCoord2f(m_Rot+0.0f, 4.0f);
  67. glVertex3f(-1.1f, 1.1f, 0.0f);
  68. glEnd();
  69. }
  70. glBlendFunc(GL_ONE, GL_ONE);                    //把纹理1复制到屏幕
  71. glBindTexture(GL_TEXTURE_2D, m_Texture[2]);     //选择第一个纹理
  72. glBegin(GL_QUADS);                              //绘制四边形
  73. glTexCoord2f(m_Rot+0.0f, 0.0f);
  74. glVertex3f(-1.1f, -1.1f, 0.0f);
  75. glTexCoord2f(m_Rot+4.0f, 0.0f);
  76. glVertex3f(1.1f, -1.1f, 0.0f);
  77. glTexCoord2f(m_Rot+4.0f, 4.0f);
  78. glVertex3f(1.1f, 1.1f, 0.0f);
  79. glTexCoord2f(m_Rot+0.0f, 4.0f);
  80. glVertex3f(-1.1f, 1.1f, 0.0f);
  81. glEnd();
  82. }
  83. glEnable(GL_DEPTH_TEST);                            //启用深度测试
  84. glDisable(GL_BLEND);                                //禁用混合
  85. m_Rot += 0.002f;                                    //增加调整纹理滚动旋转变量
  86. if (m_Rot > 1.0f)
  87. {
  88. m_Rot -= 1.0f;
  89. }
  90. }

函数一开始,清除背景色,重置矩阵,把物体移入屏幕2.0单位。接着我们选择logo纹理,绘制纹理四边形,注意到我们调用glTexCoord选择纹理坐标时,有的数是大于1.0的,这时候OpenGL默认截取小数部分进行处理,这样就可以得到无缝的循环纹理(具体效果大家看上面的图或自己运行程序时再看)。然后我们启用混合并禁用深度测试。

接着我们需要根据m_Masking的值设置是否使用“掩模”,如果是,我们需要设置相应的混合因子。一个“掩模”只是一幅绘制到屏幕的纹理图片,但只有黑色和白色,白色的部分代表透明,黑色的部分代表不透明。我们设置的混合因子GL_DST_COLOR、GL_ZERO使得任何纹理(OpenGL并不知道这是不是“掩模”)黑色的部分会变为黑色,白色的部分会保持原来的颜色,就是变成透明,透过了原来的颜色。

然后我们检查是绘制哪一个场景(图层),true绘制第二层,false绘制第一层。true时先开始绘制第二层,为了不使得它看起来太大,我们把它移入屏幕1.0单位,并把它按m_Rot的值绕z轴旋转。接着我们检查m_Marking的值,如果为true,我们就把“掩模”绘制到屏幕上,当我们完成这个操作时,将会看到一个镂空的纹理出现在屏幕上。然后我们变换混合因子GL_ONE、GL_ONE,这次我们告诉OpenGL把任何黑色部分对应的像素复制到屏幕,这样看起来纹理就像被镂空一样贴在屏幕上。要注意的是,我在变换了混合因子后才选择的纹理。如果我们没有使用 “掩模”,我们的图像将与屏幕颜色融合。

下面我绘制第一层与第二层的绘制基本相同,不多解释了。最后我们启用深度测试,禁用混合,然后增加m_Rot变量,如果大于1.0,把它的值减去1.0。

最后我们修改一下键盘控制函数,就是加上了空格和M键作为切换键,很简单不多解释了,具体代码如下:

  1. void MyGLWidget::keyPressEvent(QKeyEvent *event)
  2. {
  3. switch (event->key())
  4. {
  5. case Qt::Key_F1:                                    //F1为全屏和普通屏的切换键
  6. fullscreen = !fullscreen;
  7. if (fullscreen)
  8. {
  9. showFullScreen();
  10. }
  11. else
  12. {
  13. showNormal();
  14. }
  15. updateGL();
  16. break;
  17. case Qt::Key_Escape:                                //ESC为退出键
  18. close();
  19. break;
  20. case Qt::Key_Space:                                 //空格为场景(图层)的切换键
  21. m_Scene = !m_Scene;
  22. break;
  23. case Qt::Key_M:                                     //M为是否"掩膜"的切换键
  24. m_Masking = !m_Masking;
  25. break;
  26. }
  27. }

现在就可以运行程序查看效果了!

一点内容的补充:上面我们提到当调用glTexCoord选择纹理坐标时,如果大于1.0,OpenGL默认截取小数部分进行处理。其实这只是OpenGL默认的处理模式:GL_REPEAT。对于纹理坐标大于1.0,OpenGL有以下几种处理模式:

GL_CLAMP - 截取

GL_REPEAT - 重复(OpenGL默认的模式)

GL_MIRRORED_REPEAT - 镜像重复

GL_CLAMP_TO_EDGE - 忽略边框截取

GL_CLAMP_TO_BORDER - 带边框的截取

我们可以利用glTexParameter函数来进行模式的转换,如:x方向的转换为glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP),变换模式只需更改第三个参数。而第二参数代表方向,GL_TEXTURE_WRAP_S代表x方向,GL_TEXTURE_WRAP_T代表y方向,GL_TEXTURE_WRAP_R代表z方向。

Qt Creator中的3D绘图及动画教程(参照NeHe)的更多相关文章

  1. qt creator中使用qwt插件

    前提:我用mingw编译的qwt. 将qwt插件集成到qt designer非常easy.仅仅要把qwt编译的qwt_designer_plugin.dll复制到C:\Qt\Qt5.3.1\5.3\m ...

  2. Qt在VS2013或Qt Creator 中的控制台输出方式设置

    首先值得注意的是:在写程序的时候,项目保存路径不要涉及到中文,否则容易出错! 一.Qt在VS2013中的控制台输出方式: 注意:这里是而不是Qt Application. 然后直接点击finish即可 ...

  3. Qt Creator中,include路径包含过程(或如何找到对应的头文件)

    Qt Creator中,include路径包含过程(或如何找到对应的头文件) 利用Qt Creator开发程序时,需要包含利用#include来添加头文件. 大家都知道,#include <&g ...

  4. qt creator中编辑Makefile的设置

    在qt creator中编辑Makefile时的Tab键总是不能识别,需要这样设置

  5. 如何在Qt Creator中导入图标资源

    本文主要描述如何在Qt Creator中创建资源文件,并的打入导入图标文件. 查看图标资源文件时,可以在项目的工程文件上鼠标单击右键-Open With-资源编辑器,效果如下图所示: 在项目的工程文件 ...

  6. Qt Creator中如何选择某个子项目为启动项目

    Qt Creator中的子目录项目类似于Visual Studio中的Solution(解决方案),可以用来管理多个子项目.但是在Qt Creator IDE中由不能像Visual Studio中那样 ...

  7. Qt Creator中使用qss对界面美化没有作用(效果)的问题

    最近在研究qt界面开发,发现使用qss对界面进行美化后效果不错,要比mfc效率高很多,美化效果也很出色.但是在使用qss文件对界面控件进行美化的过程中遇到了个很奇葩的问题,困惑了我好久,今晚又遇到了, ...

  8. Qt Creator 中的段落 注释的 快捷方法【转载】

    原文网址:http://jingyan.baidu.com/article/d5c4b52bc2bd1dda560dc5bb.html 作为一名合格的程序员,漂漂亮亮的注释是必须的!!怎么在Qt Cr ...

  9. Qt Creator 中的插件Plugin, 区分说明。。。

    Qt Creator 中可以创建 三中类型的插件Plugin: 1.用的最多的,派生自QGenericPlugin类: 在新建Library,   Plugin类型工程中,新建. 调用使用QPlugi ...

随机推荐

  1. P3368 【模板】树状数组 2(区间增减,单点查询)

    P3368 [模板]树状数组 2 题目描述 如题,已知一个数列,你需要进行下面两种操作: 1.将某区间每一个数数加上x 2.求出某一个数的和 输入输出格式 输入格式: 第一行包含两个整数N.M,分别表 ...

  2. [BJOI2017]树的难题 点分治,线段树合并

    [BJOI2017]树的难题 LG传送门 点分治+线段树合并. 我不会写单调队列,所以就写了好写的线段树. 考虑对于每一个分治中心,把出边按颜色排序,这样就能把颜色相同的子树放在一起处理.用一棵动态开 ...

  3. pager-taglib分页注意事项

    必须先导包,尤其是 jsp 这种工具类和标签库的

  4. Windows环境下php开启GD库的方法

    一.GD库是什么? GD库是php处理图形的扩展库,GD库提供了一系列用来处理图片的API,使用GD库可以处理图片,或者生成图片,也可以给图片加水印.在网站上GD库通常用来生成缩略图,或者用来对图片加 ...

  5. java 通过内存映射文件来提高IO读取文件性能

    MappedByteBuffer out = new RandomAccessFile("src/demo20/test.dat", "rw"). getCha ...

  6. java class file

    目录 什么是java类文件 幻数 主次版本号 常量池数和常量池 this_class super_class 接口数量和接口 字段数和字段 方法数和方法 以下内容主要还是参考<Inside JV ...

  7. Linux系统进程管理

    Linux系统进程管理 什么是进程 进程是已启动的可执行程序的运行实例,进程有以下组成部分: 分配内存, 已分配内存的地址空间 安全属性, 进程的运行身份和权限 进程代码, 运行一个或多个的线程 进程 ...

  8. 算法笔记(c++)--求一个数的所有质数因子

    算法笔记(c++)--求一个数的所有质数因子 先贴题目: 这题不难,恶心在理解上面.最后看评论知道了怎么回事: 2*2*3*3*5=180 按照这逻辑的话应该输入的数由一系列质数相乘出来,所以每次找到 ...

  9. 从零开始的Python爬虫速成指南

    序 本文主要内容:以最短的时间写一个最简单的爬虫,可以抓取论坛的帖子标题和帖子内容. 本文受众:没写过爬虫的萌新. 入门 0.准备工作 需要准备的东西: Python.scrapy.一个IDE或者随便 ...

  10. 英文Datasheet没那么难读

    话说学好数理化,走遍天下都不怕.可是在这个所谓的全球化时代,真要走遍天下的话,数理化还真未必比得上一门外语.作为技术人员,可以看到的是目前多数前沿的产品和技术多来自发达的欧美等国家,而英语目前才是真正 ...