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/
教程目录索引:
全部教程中需要的资源文件点此下载 http://download.csdn.net/download/cly116/8957317
- TARGET = myOpenGL
- TEMPLATE = app
- HEADERS += \
- myglwidget.h
- SOURCES += \
- main.cpp \
- myglwidget.cpp
- QT += core gui
- greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
- QT += opengl
然后保存该文件。下面打开myglwidget.h文件,将类声明补全如下:
- #ifndef MYGLWIDGET_H
- #define MYGLWIDGET_H
- #include <QWidget>
- #include <QGLWidget>
- class MyGLWidget : public QGLWidget
- {
- Q_OBJECT
- public:
- explicit MyGLWidget(QWidget *parent = 0);
- ~MyGLWidget();
- protected:
- //对3个纯虚函数的重定义
- void initializeGL();
- void resizeGL(int w, int h);
- void paintGL();
- void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
- private:
- bool fullscreen; //是否全屏显示
- };
- #endif // MYGLWIDGET_H
再到myglwidget.cpp文件中先包含#include<GL/glu.h>,#include<QKeyEvent>头文件,然后添加类中函数的定义:
- MyGLWidget::MyGLWidget(QWidget *parent) :
- QGLWidget(parent)
- {
- fullscreen = false;
- }
- MyGLWidget::~MyGLWidget()
- {
- }
构造函数中只需对fullscreen初始化,析构函数暂时并不需要做什么。
- void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置
- {
- glClearColor(0.0, 0.0, 0.0, 0.0); //黑色背景
- glShadeModel(GL_SMOOTH); //启用阴影平滑
- glClearDepth(1.0); //设置深度缓存
- glEnable(GL_DEPTH_TEST); //启用深度测试
- glDepthFunc(GL_LEQUAL); //所作深度测试的类型
- glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告诉系统对透视进行修正
- }
glClearColor()函数用来设置清除屏幕时使用的颜色,4个参数分别用来设置红、绿、蓝颜色分量和Alpha值,它们的取值范围都是0.0~1.0,这里4个参数都为0.0,表示纯黑色。然后设置了阴影平滑,这样可以使色彩和光照更加精细。
- void MyGLWidget::resizeGL(int w, int h) //重置OpenGL窗口的大小
- {
- glViewport(0, 0, (GLint)w, (GLint)h); //重置当前的视口
- glMatrixMode(GL_PROJECTION); //选择投影矩阵
- glLoadIdentity(); //重置投影矩阵
- //设置视口的大小
- gluPerspective(45.0, (GLfloat)w/(GLfloat)h, 0.1, 100.0);
- glMatrixMode(GL_MODELVIEW); //选择模型观察矩阵
- glLoadIdentity(); //重置模型观察矩阵
- }
glViewport()函数用来设置视口的大小。使用glMatrixMode()设置了投影矩阵,投影矩阵用来为场景增加透视,后面使用了glLoadIdentity()重置投影矩阵,这样可以将投影矩阵恢复到初始状态。gluPerspective()用来设置透视投影矩阵,这里设置视角为45°,纵横比为窗口的纵横比,最近的位置为0.1,最远的位置为100,这两个值是场景中所能绘制的深度的临界值。可以想象,离我们眼睛比较近的东西看起来比较大,而比较远的东西看起来就比较小。最后设置并重置了模型视图矩阵。
- void MyGLWidget::paintGL() //从这里开始进行所以的绘制
- {
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
- glLoadIdentity(); //重置当前的模型观察矩阵
- }
paintGL()函数包含了所以的绘图代码,任何想在屏幕上显示的东西都将在此段代码中出现。以后每个教程中都会在这个函数增加代码,已达到绘图目的。
- void MyGLWidget::keyPressEvent(QKeyEvent *event)
- {
- switch (event->key())
- {
- //F1为全屏和普通屏的切换键
- case Qt::Key_F1:
- fullscreen = !fullscreen;
- if (fullscreen)
- {
- showFullScreen();
- }
- else
- {
- showNormal();
- }
- updateGL();
- break;
- //ESC为退出键
- case Qt::Key_Escape:
- close();
- }
- }
最后再向项目中添加main.cpp文件,更改内容如下:
- #include <QApplication>
- #include "myglwidget.h"
- int main(int argc, char *argv[])
- {
- QApplication app(argc, argv);
- MyGLWidget w;
- w.resize(400, 300);
- w.show();
- return app.exec();
- }
现在就可以运行程序查看效果了!
第02课:你的第一个多边形 (参照NeHe)
这次教程中,我们将添加一个三角形和一个四边形。或许你认为这很简单,但要知道任何复杂的绘图都是从简单开始的,或者说任何复杂的模型都是可以分解成简单的图形的。所以,我们还是从简单的图形开始吧。
读完这一次教程,你还会学到如何在空间放置模型以及了解OpenGL中坐标变化。
程序运行时效果如下:
下面进入教程:
我们将使用GL_TRIANGLES来创建一个三角形,GL_QUADS来创建一个四边形。在第01课代码的基础上,我们只需在paintGL()函数中增加代码。
下面我将重写整个paintGL()函数,具体代码如下:
- void MyGLWidget::paintGL() //从这里开始进行所以的绘制
- {
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
- glLoadIdentity(); //重置当前的模型观察矩阵
- glTranslatef(-1.5f, 0.0f, -6.0f); //左移1.5单位,并移入屏幕6.0单位
- glBegin(GL_TRIANGLES); //开始绘制三角形
- glVertex3f(0.0f, 1.0f, 0.0f); //上顶点
- glVertex3f(-1.0f, -1.0f, 0.0f); //左下
- glVertex3f(1.0f, -1.0f, 0.0f); //右下
- glEnd(); //三角形绘制结束
- glTranslatef(3.0f, 0.0f, 0.0f); //右移3.0单位
- glBegin(GL_QUADS); //开始绘制四边形
- glVertex3f(-1.0f, 1.0f, 0.0f); //左上
- glVertex3f(1.0f, 1.0f, 0.0f); //右上
- glVertex3f(1.0f, -1.0f, 0.0f); //左下
- glVertex3f(-1.0f, -1.0f, 0.0f); //右下
- glEnd(); //四边形绘制结束
- }
当调用了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没有其他点了,这样将显示一个填充的三角形。
然后类比画出一个四边形后,就可以运行程序看效果了!
- void MyGLWidget::paintGL() //从这里开始进行所以的绘制
- {
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
- glLoadIdentity(); //重置当前的模型观察矩阵
- glTranslatef(-1.5f, 0.0f, -6.0f); //左移1.5单位,并移入屏幕6.0单位
- glBegin(GL_TRIANGLES); //开始绘制三角形
- glColor3f(1.0f, 0.0f, 0.0f); //设置当前色为红色
- glVertex3f(0.0f, 1.0f, 0.0f); //上顶点
- glColor3f(0.0f, 1.0f, 0.0f); //设置当前色为绿色
- glVertex3f(-1.0f, -1.0f, 0.0f); //左下
- glColor3f(0.0f, 0.0f, 1.0f); //设置当前色为蓝色
- glVertex3f(1.0f, -1.0f, 0.0f); //右下
- glEnd(); //三角形绘制结束
- glTranslatef(3.0f, 0.0f, 0.0f); //右移3.0单位
- glColor3f(0.5f, 0.5f, 1.0f); //一次性将当前色设置为蓝色
- glBegin(GL_QUADS); //开始绘制四边形
- glVertex3f(-1.0f, 1.0f, 0.0f); //左上
- glVertex3f(1.0f, 1.0f, 0.0f); //右上
- glVertex3f(1.0f, -1.0f, 0.0f); //左下
- glVertex3f(-1.0f, -1.0f, 0.0f); //右下
- glEnd(); //四边形绘制结束
- }
其实与第02课相比,只是增加了4行代码而已。我们利用glColor3f(r, g, b)函数来选择颜色进行着色,该函数三个参数依次是红、绿、蓝三色分量,范围从0.0到1.0之间,类似于之前所讲的清除屏幕背景函数。当我们将颜色设为某种颜色时,接下来的代码绘制出的对象的颜色就都是对应颜色的。
第04课:旋转 (参照NeHe)
这次教程中,我们将在第03课的基础上,教大家如何旋转三角形和四边形。我们将让三角形沿y轴旋转,四边形沿x轴旋转,最终我们能得到一个三角形和四边形自动旋转的场景。
程序运行时效果如下:
下面进入教程:
首先打开myglwidget.h文件,我们需要增加两个变量来控制这两个对象的旋转。这两个变量加在类的私有声明处,将类声明更改如下:
- #ifndef MYGLWIDGET_H
- #define MYGLWIDGET_H
- #include <QWidget>
- #include <QGLWidget>
- class MyGLWidget : public QGLWidget
- {
- Q_OBJECT
- public:
- explicit MyGLWidget(QWidget *parent = 0);
- ~MyGLWidget();
- protected:
- //对3个纯虚函数的重定义
- void initializeGL();
- void resizeGL(int w, int h);
- void paintGL();
- void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
- private:
- bool fullscreen; //是否全屏显示
- GLfloat m_rtri; //控制三角形的角度
- GLfloat m_rquad; //控制四边形的角度
- };
- #endif // MYGLWIDGET_H
我们增加了两个浮点类型的变量,使得我们能够非常精确地旋转对象,你渐渐会发现浮点数是OpenGL编程的基础。新变量中叫做m_rtri的用来旋转三角形,m_rquad旋转四边形。
接下来,我们需要打开myglwidget.cpp,在构造函数中对两个新变量进行初始化,这部分很简单,不作过多解释,代码如下:
- MyGLWidget::MyGLWidget(QWidget *parent) :
- QGLWidget(parent)
- {
- fullscreen = false;
- m_rtri = 0.0f;
- m_rquad = 0.0f;
- }
然后进入重点的paintGL()函数了,我们只需在第03课代码的基础上,做一定的修改,就能实现三角形和四边形的旋转了。
下面我将重写整个paintGL()函数,具体代码如下:
- void MyGLWidget::paintGL() //从这里开始进行所以的绘制
- {
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
- glLoadIdentity(); //重置当前的模型观察矩阵
- glTranslatef(-1.5f, 0.0f, -6.0f); //左移1.5单位,并移入屏幕6.0单位
- glRotatef(m_rtri, 0.0f, 1.0f, 0.0f); //绕y轴旋转三角形
- glBegin(GL_TRIANGLES); //开始绘制三角形
- glColor3f(1.0f, 0.0f, 0.0f); //设置当前色为红色
- glVertex3f(0.0f, 1.0f, 0.0f); //上顶点
- glColor3f(0.0f, 1.0f, 0.0f); //设置当前色为绿色
- glVertex3f(-1.0f, -1.0f, 0.0f); //左下
- glColor3f(0.0f, 0.0f, 1.0f); //设置当前色为蓝色
- glVertex3f(1.0f, -1.0f, 0.0f); //右下
- glEnd(); //三角形绘制结束
- glLoadIdentity(); //重置模型观察矩阵
- glTranslatef(1.5f, 0.0f, -6.0f); //右移1.5单位,并移入屏幕6.0单位
- glRotatef(m_rquad, 1.0f, 0.0f, 0.0f); //绕x轴旋转四边形
- glColor3f(0.5f, 0.5f, 1.0f); //一次性将当前色设置为蓝色
- glBegin(GL_QUADS); //开始绘制四边形
- glVertex3f(-1.0f, 1.0f, 0.0f); //左上
- glVertex3f(1.0f, 1.0f, 0.0f); //右上
- glVertex3f(1.0f, -1.0f, 0.0f); //左下
- glVertex3f(-1.0f, -1.0f, 0.0f); //右下
- glEnd(); //四边形绘制结束
- m_rtri += 0.5f; //增加三角形的旋转变量
- m_rquad -= 0.5f; //减少四边形的旋转变量
- }
上面的代码绘制三角形时多了一新函数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()的调用情况请参见)
- MyGLWidget::MyGLWidget(QWidget *parent) :
- QGLWidget(parent)
- {
- fullscreen = false;
- m_rtri = 0.0f;
- m_rquad = 0.0f;
- QTimer *timer = new QTimer(this); //创建一个定时器
- //将定时器的计时信号与updateGL()绑定
- connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
- timer->start(10); //以10ms为一个计时周期
- }
这里将定时器的timeout()信号与updateGL()槽绑定,每过10ms就会调用一次updateGL(),而updateGL()调用后会调用paintGL()对场景进行重绘,这样就通过对场景不停地重绘实现对象的旋转。(对Qt定时器不了解的朋友请先百度了解下其机制)
现在就可以运行程序看效果了!
第05课:3D模型 (参照NeHe)
这次教程中,我们将之前几课的基础上,教大家如何创建立体的3D模型。我们将开始生成真正的3D对象,而不是像之前那几课那样3D世界中的2D对象。我们会把之前的三角形变为立体的金字塔模型,把四边形变为立方体。
我们给三角形增加左侧面、右侧面、后侧面来生成一个金字塔。给正方形增加左、右、上、下及背面生成一个立方体。我们混合金字塔上的颜色,创建一个平滑着色的对象;给立方体的每一面来个不同的颜色。
程序运行时效果如下:
下面进入教程:
要实现3D模型,只需在第04课代码的基础上,对paintGL()函数作一定的修改。
下面我将重写整个paintGL()函数,具体代码如下:
- void MyGLWidget::paintGL() //从这里开始进行所以的绘制
- {
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
- glLoadIdentity(); //重置当前的模型观察矩阵
- glTranslatef(-1.5f, 0.0f, -6.0f); //左移1.5单位,并移入屏幕6.0单位
- glRotatef(m_rtri, 0.0f, 1.0f, 0.0f); //绕y轴旋转三角形
- glBegin(GL_TRIANGLES); //开始绘制金字塔
- glColor3f(1.0f, 0.0f, 0.0f); //红色
- glVertex3f(0.0f, 1.0f, 0.0f); //上顶点(前侧面)
- glColor3f(0.0f, 1.0f, 0.0f); //绿色
- glVertex3f(-1.0f, -1.0f, 1.0f); //左下(前侧面)
- glColor3f(0.0f, 0.0f, 1.0f); //蓝色
- glVertex3f(1.0f, -1.0f, 1.0f); //右下(前侧面)
- glColor3f(1.0f, 0.0f, 0.0f); //红色
- glVertex3f(0.0f, 1.0f, 0.0f); //上顶点(右侧面)
- glColor3f(0.0f, 0.0f, 1.0f); //蓝色
- glVertex3f(1.0f, -1.0f, 1.0f); //左下(右侧面)
- glColor3f(0.0f, 1.0f, 0.0f); //绿色
- glVertex3f(1.0f, -1.0f, -1.0f); //右下(右侧面)
- glColor3f(1.0f, 0.0f, 0.0f); //红色
- glVertex3f(0.0f, 1.0f, 0.0f); //上顶点(后侧面)
- glColor3f(0.0f, 1.0f, 0.0f); //绿色
- glVertex3f(1.0f, -1.0f, -1.0f); //左下(后侧面)
- glColor3f(0.0f, 0.0f, 1.0f); //蓝色
- glVertex3f(-1.0f, -1.0f, -1.0f); //右下(后侧面)
- glColor3f(1.0f, 0.0f, 0.0f); //红色
- glVertex3f(0.0f, 1.0f, 0.0f); //上顶点(左侧面)
- glColor3f(0.0f, 0.0f, 1.0f); //蓝色
- glVertex3f(-1.0f, -1.0f, -1.0f); //左下(左侧面)
- glColor3f(0.0f, 1.0f, 0.0f); //绿色
- glVertex3f(-1.0f, -1.0f, 1.0f); //右下(左侧面)
- glEnd(); //金字塔绘制结束
- glLoadIdentity(); //重置模型观察矩阵
- glTranslatef(1.5f, 0.0f, -6.0f); //右移1.5单位,并移入屏幕6.0单位
- glRotatef(m_rquad, 1.0f, 0.0f, 0.0f); //绕x轴旋转四边形
- glBegin(GL_QUADS); //开始绘制立方体
- glColor3f(0.0f, 1.0f, 0.0f); //绿色
- glVertex3f(1.0f, 1.0f, -1.0f); //右上(顶面)
- glVertex3f(-1.0f, 1.0f, -1.0f); //左上(顶面)
- glVertex3f(-1.0f, 1.0f, 1.0f); //左下(顶面)
- glVertex3f(1.0f, 1.0f, 1.0f); //右下(顶面)
- glColor3f(1.0f, 0.5f, 0.0f); //橙色
- glVertex3f(1.0f, -1.0f, 1.0f); //右上(底面)
- glVertex3f(-1.0f, -1.0f, 1.0f); //左上(底面)
- glVertex3f(-1.0f, -1.0f, -1.0f); //左下(底面)
- glVertex3f(1.0f, -1.0f, -1.0f); //右下(底面)
- glColor3f(1.0f, 0.0f, 0.0f); //红色
- glVertex3f(1.0f, 1.0f, 1.0f); //右上(前面)
- glVertex3f(-1.0f, 1.0f, 1.0f); //左上(前面)
- glVertex3f(-1.0f, -1.0f, 1.0f); //左下(前面)
- glVertex3f(1.0f, -1.0f, 1.0f); //右下(前面)
- glColor3f(1.0f, 1.0f, 0.0f); //黄色
- glVertex3f(1.0f, -1.0f, -1.0f); //右上(后面)
- glVertex3f(-1.0f, -1.0f, -1.0f); //左上(后面)
- glVertex3f(-1.0f, 1.0f, -1.0f); //左下(后面)
- glVertex3f(1.0f, 1.0f, -1.0f); //右下(后面)
- glColor3f(0.0f, 0.0f, 1.0f); //蓝色
- glVertex3f(-1.0f, 1.0f, 1.0f); //右上(左面)
- glVertex3f(-1.0f, 1.0f, -1.0f); //左上(左面)
- glVertex3f(-1.0f, -1.0f, -1.0f); //左下(左面)
- glVertex3f(-1.0f, -1.0f, 1.0f); //右下(左面)
- glColor3f(1.0f, 0.0f, 1.0f); //紫色
- glVertex3f(1.0f, 1.0f, -1.0f); //右上(右面)
- glVertex3f(1.0f, 1.0f, 1.0f); //左上(右面)
- glVertex3f(1.0f, -1.0f, 1.0f); //左下(右面)
- glVertex3f(1.0f, -1.0f, -1.0f); //右下(右面)
- glEnd(); //立方体绘制结束
- m_rtri += 0.5f; //增加金字体的旋转变量
- m_rquad -= 0.5f; //减少立方体的旋转变量
- }
首先创建一个绕着其中心轴旋转的金字塔,金字塔的上顶点高出原点一个单位,底面中心低于原点一个单位,上顶点在底面的投影位于底面的中心。要注意的是所有的面-三角形都是逆时针次序绘制的,这点十分重要,在以后的课程中我会做出解释。现在,我们只需明白要么都逆时针,要么都顺时针,但永远不要将两种次序混在一起,除非我们有足够的理由必须这么做。
开始绘制金字塔,应注意到四个侧面处于同一glBegin(GL_TRIANGLES)和glEnd()语句之间,由于我们是用过三角形来构造这个金字塔的,OpenGL知道每三个点构成一个三角形,当它画完一个三角形之后,如果还有余下的点出现,它就以为新的三角形要开始绘制了。OpenGL在这里并不会将四个点画成一个四边形,而是假定新的三角形开始了,千万不要无意中增加任何多余的点。对于颜色的选择,我们只需对应好位置,就能取得不错的效果。
开始绘制立方体,它由六个四边形组成,所有的四边形都以逆时针次序绘制,即按照右上、左上、左下、右下的次序绘画。你也许认为画立方体的背面的时候这个次序看起来好像顺时针,但别忘了我们从立方体背后看背面的时候,与你现在所想的正好相反(我们是从立方体外面来观察立方体的)。当然,你也可以尝试用平滑着色来绘制立方体。
现在就可以运行程序查看效果了!
- #ifndef MYGLWIDGET_H
- #define MYGLWIDGET_H
- #include <QWidget>
- #include <QGLWidget>
- class MyGLWidget : public QGLWidget
- {
- Q_OBJECT
- public:
- explicit MyGLWidget(QWidget *parent = 0);
- ~MyGLWidget();
- protected:
- //对3个纯虚函数的重定义
- void initializeGL();
- void resizeGL(int w, int h);
- void paintGL();
- void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
- private:
- bool fullscreen; //是否全屏显示
- GLfloat m_xRot; //绕x轴旋转的角度
- GLfloat m_yRot; //绕y轴旋转的角度
- GLfloat m_zRot; //绕z轴旋转的角度
- QString m_FileName; //图片的路径及文件名
- GLuint m_Texture; //储存一个纹理
- };
- #endif // MYGLWIDGET_H
增加的前三个变量用来使立方体绕x、y、z轴旋转,m_FileName用于储存图片的路径及文件名,m_Texture为一个纹理分配存储空间。如果需要不止一个纹理,可以创建一个数组来储存不同的纹理。
- MyGLWidget::MyGLWidget(QWidget *parent) :
- QGLWidget(parent)
- {
- fullscreen = false;
- m_xRot = 0.0f;
- m_yRot = 0.0f;
- m_zRot = 0.0f;
- m_FileName = "D:/QtOpenGL/QtImage/Nehe.bmp"; //应根据实际存放图片的路径进行修改
- QTimer *timer = new QTimer(this); //创建一个定时器
- //将定时器的计时信号与updateGL()绑定
- connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
- timer->start(10); //以10ms为一个计时周期
- }
然后这次我们需要对initializeGL()函数作一定的修改了,具体代码如下:
- void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置
- {
- m_Texture = bindTexture(QPixmap(m_FileName)); //载入位图并转换成纹理
- glEnable(GL_TEXTURE_2D); //启用纹理映射
- glClearColor(0.0f, 0.0f, 0.0f, 0.0f); //黑色背景
- glShadeModel(GL_SMOOTH); //启用阴影平滑
- glClearDepth(1.0); //设置深度缓存
- glEnable(GL_DEPTH_TEST); //启用深度测试
- glDepthFunc(GL_LEQUAL); //所作深度测试的类型
- glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告诉系统对透视进行修正
- }
我们增加了两行代码,首先调用了Qt提供的bindTexture()函数将图片载入并转换成纹理,然后启用2D纹理映射。如果忘记启用的话,我们的对象看起来永远都是纯白色的,这明显与我们的预期大相径庭。
- void MyGLWidget::paintGL() //从这里开始进行所以的绘制
- {
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
- glLoadIdentity(); //重置模型观察矩阵
- glTranslatef(0.0f, 0.0f, -5.0f); //移入屏幕5.0单位
- glRotatef(m_xRot, 1.0f, 0.0f, 0.0f); //绕x轴旋转
- glRotatef(m_yRot, 0.0f, 1.0f, 0.0f); //绕y轴旋转
- glRotatef(m_zRot, 0.0f, 0.0f, 1.0f); //绕z轴旋转
- glBindTexture(GL_TEXTURE_2D, m_Texture); //选择纹理
- glBegin(GL_QUADS); //开始绘制立方体
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, -1.0f); //右上(顶面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, -1.0f); //左上(顶面)
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(-1.0f, 1.0f, 1.0f); //左下(顶面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(1.0f, 1.0f, 1.0f); //右下(顶面)
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, 1.0f); //右上(底面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, 1.0f); //左上(底面)
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(-1.0f, -1.0f, -1.0f); //左下(底面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(1.0f, -1.0f, -1.0f); //右下(底面)
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, 1.0f); //右上(前面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, 1.0f); //左上(前面)
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, 1.0f); //左下(前面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, 1.0f); //右下(前面)
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, -1.0f); //右上(后面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, -1.0f); //左上(后面)
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, -1.0f); //左下(后面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, -1.0f); //右下(后面)
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, 1.0f); //右上(左面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, -1.0f); //左上(左面)
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, -1.0f); //左下(左面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, 1.0f); //右下(左面)
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, -1.0f); //右上(右面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, 1.0f); //左上(右面)
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, 1.0f); //左下(右面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, -1.0f); //右下(右面)
- glEnd(); //立方体绘制结束
- m_xRot += 0.6f; //x轴旋转
- m_yRot += 0.4f; //y轴旋转
- m_zRot += 0.8f; //z轴旋转
- }
这次我们需要让对象依次绕x、y、z轴旋转,旋转多少依赖于变量m_xRot、m_yRot、m_zRot的值。下面我们调用glBindTexture()函数来选择要绑定的纹理,第2个参数表示所要绑定的纹理。当想改变纹理时,应该绑定新的纹理,要注意的是,我们不能在glBegin()和glEnd()直接绑定纹理,那样绑定的纹理时无效的。
第07课:光照和键盘控制 (参照NeHe)
这次教程中,我们将添加光照和键盘控制,它让程序看起来更美观。我将教大家如何使用键盘来移动场景中的对象,还会教大家在OpenGL场景中应用简单的光照,让我们的程序更加视觉效果更好且受我们控制。
程序运行时效果如下:
下面进入教程:
我们这次将在第06课的基础上修改代码,首先打开myglwidget.h文件,将类声明更改如下:
- #ifndef MYGLWIDGET_H
- #define MYGLWIDGET_H
- #include <QWidget>
- #include <QGLWidget>
- class MyGLWidget : public QGLWidget
- {
- Q_OBJECT
- public:
- explicit MyGLWidget(QWidget *parent = 0);
- ~MyGLWidget();
- protected:
- //对3个纯虚函数的重定义
- void initializeGL();
- void resizeGL(int w, int h);
- void paintGL();
- void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
- private:
- bool fullscreen; //是否全屏显示
- QString m_FileName; //图片的路径及文件名
- GLuint m_Texture; //储存一个纹理
- bool m_Light; //光源的开/关
- GLfloat m_xRot; //x旋转角度
- GLfloat m_yRot; //y旋转角度
- GLfloat m_xSpeed; //x旋转速度
- GLfloat m_ySpeed; //y旋转速度
- GLfloat m_Deep; //深入屏幕的距离
- };
- #endif // MYGLWIDGET_H
增加了一个布尔变量表示光源的开关,剩下的五个浮点变量用于控制对象的旋转角度,旋转速度以及距离屏幕的位置。
接下来,我们需要打开myglwidget.cpp,加上声明#include <QTimer>,在构造函数中对新增变量(除了m_Texture)进行初始化,同样不作过多解释,代码如下:
- MyGLWidget::MyGLWidget(QWidget *parent) :
- QGLWidget(parent)
- {
- fullscreen = false;
- m_FileName = "D:/QtOpenGL/QtImage/Crate.bmp"; //应根据实际存放图片的路径进行修改
- m_Light = false;
- m_xRot = 0.0f;
- m_yRot = 0.0f;
- m_xSpeed = 0.0f;
- m_ySpeed = 0.0f;
- m_Deep = -5.0f;
- QTimer *timer = new QTimer(this); //创建一个定时器
- //将定时器的计时信号与updateGL()绑定
- connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
- timer->start(10); //以10ms为一个计时周期
- }
然后,我们要来添加光照,只需要在initializeGL()函数增加几行代码,具体修改后代码如下:
- void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置
- {
- m_Texture = bindTexture(QPixmap(m_FileName)); //载入位图并转换成纹理
- glEnable(GL_TEXTURE_2D); //启用纹理映射
- glClearColor(0.0f, 0.0f, 0.0f, 0.0f); //黑色背景
- glShadeModel(GL_SMOOTH); //启用阴影平滑
- glClearDepth(1.0); //设置深度缓存
- glEnable(GL_DEPTH_TEST); //启用深度测试
- glDepthFunc(GL_LEQUAL); //所作深度测试的类型
- glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告诉系统对透视进行修正
- GLfloat LightAmbient[] = {0.5f, 0.5f, 0.5f, 1.0f}; //环境光参数
- GLfloat LightDiffuse[] = {1.0f, 1.0f, 1.0f, 1.0f}; //漫散光参数
- GLfloat LightPosition[] = {0.0f, 0.0f, 2.0f, 1.0f}; //光源位置
- glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient); //设置环境光
- glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse); //设置漫射光
- glLightfv(GL_LIGHT1, GL_POSITION, LightPosition); //设置光源位置
- glEnable(GL_LIGHT1); //启动一号光源
- }
首先我们分别定义环境光参数,漫射光参数以及光源位置。环境光来自于四面八方,所以场景中的对象都处于环境光的照射中;漫射光由特定的光源产生,并在场景中的对象表明产生反射。处于漫射光直接照射下的任何对象表面都变得很亮,而几乎未被照到的区域显得要暗一些。这样我们所创建的木板箱的棱边上就会产生很不错的阴影效果。
创建光源的过程和颜色的创建完全一致,前三个参数分别是RGB三色分量,最后一个是alpha通道参数。最后光源位置前三个参数和glTranslate中的一样,一次表示x、y、z轴上的位移,最后一个参数取为1.0f,这将告诉OpenGL这里指定的坐标就是光源的位置,以后的教程中我会多加解释。
接着开始设置光源,使得光源GL_LIGHT1开始发光,然后是设置光源位置(位于木箱原中心在z方向移向观察者2.0单位),最后我们启用一号光源。要注意的是,我们还没有启用GL_LIGHTING,所以是看不见任何光线的。记住,只对光源进行设置、定位、甚至启用,光源都不会工作,除非我们启用GL_LIGHTING。
还有是对paintGL()函数的修改,修改后具体代码如下:
- void MyGLWidget::paintGL() //从这里开始进行所以的绘制
- {
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
- glLoadIdentity(); //重置模型观察矩阵
- glTranslatef(0.0f, 0.0f, m_Deep); //移入屏幕
- glRotatef(m_xRot, 1.0f, 0.0f, 0.0f); //绕x轴旋转
- glRotatef(m_yRot, 0.0f, 1.0f, 0.0f); //绕y轴旋转
- glBindTexture(GL_TEXTURE_2D, m_Texture); //选择纹理
- glBegin(GL_QUADS); //开始绘制立方体
- glNormal3f(0.0f, 1.0f, 0.0f);
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, -1.0f); //右上(顶面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, -1.0f); //左上(顶面)
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(-1.0f, 1.0f, 1.0f); //左下(顶面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(1.0f, 1.0f, 1.0f); //右下(顶面)
- glNormal3f(0.0f, -1.0f, 0.0f);
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, 1.0f); //右上(底面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, 1.0f); //左上(底面)
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(-1.0f, -1.0f, -1.0f); //左下(底面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(1.0f, -1.0f, -1.0f); //右下(底面)
- glNormal3f(0.0f, 0.0f, 1.0f);
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, 1.0f); //右上(前面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, 1.0f); //左上(前面)
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, 1.0f); //左下(前面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, 1.0f); //右下(前面)
- glNormal3f(0.0f, 0.0f, -1.0f);
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, -1.0f); //右上(后面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, -1.0f); //左上(后面)
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, -1.0f); //左下(后面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, -1.0f); //右下(后面)
- glNormal3f(-1.0f, 0.0f, 0.0f);
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, 1.0f); //右上(左面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, -1.0f); //左上(左面)
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, -1.0f); //左下(左面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, 1.0f); //右下(左面)
- glNormal3f(1.0f, 0.0f, 0.0f);
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, -1.0f); //右上(右面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, 1.0f); //左上(右面)
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, 1.0f); //左下(右面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, -1.0f); //右下(右面)
- glEnd(); //立方体绘制结束
- m_xRot += m_xSpeed; //x轴旋转
- m_yRot += m_ySpeed; //y轴旋转
- }
除了旋转及移动上作了修改外(相信大家能看懂),多了glNormal3f()函数的调用。该函数指定一条法线,法线告诉OpenGL这个多边形的朝向,并指明多边形的正面和背面,如果没有法线,什么怪事情都可能发生:不该亮的面被照亮了,多边形的背面也被照亮了…还要注意的是,法线应指向多边形的外侧。
最后两行代码作了一定的修改,利用变量m_xSpeed、m_ySpeed来控制立方体的旋转速度。
最后当然就是键盘控制了,具体代码如下(相信大家结合注释可以很容易看懂):
- void MyGLWidget::keyPressEvent(QKeyEvent *event)
- {
- switch (event->key())
- {
- case Qt::Key_F1: //F1为全屏和普通屏的切换键
- fullscreen = !fullscreen;
- if (fullscreen)
- {
- showFullScreen();
- }
- else
- {
- showNormal();
- }
- break;
- case Qt::Key_Escape: //ESC为退出键
- close();
- break;
- case Qt::Key_L: //L为开启关闭光源的切换键
- m_Light = !m_Light;
- if (m_Light)
- {
- glEnable(GL_LIGHTING); //开启光源
- }
- else
- {
- glDisable(GL_LIGHTING); //关闭光源
- }
- break;
- case Qt::Key_PageUp: //PageUp按下使木箱移向屏幕内部
- m_Deep -= 0.1f;
- break;
- case Qt::Key_PageDown: //PageDown按下使木箱移向观察者
- m_Deep += 0.1f;
- break;
- case Qt::Key_Up: //Up按下减少m_xSpeed
- m_xSpeed -= 0.1f;
- break;
- case Qt::Key_Down: //Down按下增加m_xSpeed
- m_xSpeed += 0.1f;
- break;
- case Qt::Key_Right: //Right按下减少m_ySpeed
- m_ySpeed -= 0.1f;
- break;
- case Qt::Key_Left: //Left按下增加m_ySpeed
- m_ySpeed += 0.1f;
- break;
- }
- }
现在就可以运行程序查看效果了!
第08课:混合 (参照NeHe)
这次教程中,我们将在纹理映射的基础上加上混合,使它看起来具有透明的效果,当然解释它不是那么容易但代码并不难,希望你喜欢它。
OpenGL中的绝大多数特效都与某些类型的(色彩)混合有关。混色的定义为,将某个像素的颜色和已绘制在屏幕上与其对应的像素颜色相互结合。至于如何结合这两种颜色则依赖于颜色的alpha通道的分量值,以及所用的混色函数。Alpha通常是位于颜色值末尾的第4个颜色组成分量,一般都认为Alpha分量代表材料的透明度。也就是说,alpha值为0.0时所代表的材料是完全透明的,alpha值为1.0时所代表的材料则是完全不透明的。
在OpenGL中实现混色的步骤类似于我们以前提到的OpenGL过程,接着设置公式,并在绘制透明对象时关闭写深度缓存。因为我们想在半透明的图形背后绘制对象,这不是正确的混色方法,但绝大多数时候这种做法在简单的项目中都工作得很好。正确的混色过程应该是先绘制全部非透明场景之后,再绘制透明的图形,并且要按照与深度缓存相反的次序来绘制(先画最远的物体)。
程序运行时效果如下:
下面进入教程:
我们这次将在第07课的基础上修改代码,首先打开myglwidget.h文件,增加一个布尔变量m_Blend来记录是否开启混合,修改后代码如下:
- #ifndef MYGLWIDGET_H
- #define MYGLWIDGET_H
- #include <QWidget>
- #include <QGLWidget>
- class MyGLWidget : public QGLWidget
- {
- Q_OBJECT
- public:
- explicit MyGLWidget(QWidget *parent = 0);
- ~MyGLWidget();
- protected:
- //对3个纯虚函数的重定义
- void initializeGL();
- void resizeGL(int w, int h);
- void paintGL();
- void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
- private:
- bool fullscreen; //是否全屏显示
- QString m_FileName; //图片的路径及文件名
- GLuint m_Texture; //储存一个纹理
- bool m_Light; //光源的开/关
- bool m_Blend; //是否混合
- GLfloat m_xRot; //x旋转角度
- GLfloat m_yRot; //y旋转角度
- GLfloat m_xSpeed; //x旋转速度
- GLfloat m_ySpeed; //y旋转速度
- GLfloat m_Deep; //深入屏幕的距离
- };
- #endif // MYGLWIDGET_H
接下来打开myglwidget.cpp文件,加上声明#include <QTimer>,在构造函数中对增加变量进行初始化并更换图片,使用不同的纹理来绘画立方体,具体修改后代码如下:
- MyGLWidget::MyGLWidget(QWidget *parent) :
- QGLWidget(parent)
- {
- fullscreen = false;
- m_FileName = "D:/QtOpenGL/QtImage/Glass.bmp"; //应根据实际存放图片的路径进行修改
- m_Light = false;
- m_Blend = false;
- m_xRot = 0.0f;
- m_yRot = 0.0f;
- m_xSpeed = 0.0f;
- m_ySpeed = 0.0f;
- m_Deep = -5.0f;
- QTimer *timer = new QTimer(this); //创建一个定时器
- //将定时器的计时信号与updateGL()绑定
- connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
- timer->start(10); //以10ms为一个计时周期
- }
然后就要进入重点的混合,其他代码非常简单,并不像解释它时那么麻烦,只需要对initializeGL()作一定的修改,具体代码如下:
- void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置
- {
- m_Texture = bindTexture(QPixmap(m_FileName)); //载入位图并转换成纹理
- glEnable(GL_TEXTURE_2D); //启用纹理映射
- glClearColor(0.0f, 0.0f, 0.0f, 0.0f); //黑色背景
- glShadeModel(GL_SMOOTH); //启用阴影平滑
- glClearDepth(1.0); //设置深度缓存
- glEnable(GL_DEPTH_TEST); //启用深度测试
- glDepthFunc(GL_LEQUAL); //所作深度测试的类型
- glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告诉系统对透视进行修正
- //下面是光源部分
- GLfloat LightAmbient[] = {0.5f, 0.5f, 0.5f, 1.0f}; //环境光参数
- GLfloat LightDiffuse[] = {1.0f, 1.0f, 1.0f, 1.0f}; //漫散光参数
- GLfloat LightPosition[] = {0.0f, 0.0f, 2.0f, 1.0f}; //光源位置
- glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient); //设置环境光
- glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse); //设置漫射光
- glLightfv(GL_LIGHT1, GL_POSITION, LightPosition); //设置光源位置
- glEnable(GL_LIGHT1); //启动一号光源
- //下面是混合部分
- glColor4f(1.0f, 1.0f, 1.0f, 0.5f); //全亮度,50%Alpha混合
- glBlendFunc(GL_SRC_ALPHA, GL_ONE); //基于源像素alpah通道值得半透明混合函数
- }
增加了两行代码,第一行以全亮度绘制此物体,并对其进行50%的alpha混合(半透明),当混合选项开启时,次物体将会产生50%的透明效果。第二行设置所采用的混合类型。看,代码真的挺简单的。
最后是键盘控制的代码,具体代码如下:
- void MyGLWidget::keyPressEvent(QKeyEvent *event)
- {
- switch (event->key())
- {
- case Qt::Key_F1: //F1为全屏和普通屏的切换键
- fullscreen = !fullscreen;
- if (fullscreen)
- {
- showFullScreen();
- }
- else
- {
- showNormal();
- }
- break;
- case Qt::Key_Escape: //ESC为退出键
- close();
- break;
- case Qt::Key_B: //B为开始关闭混合而对切换键
- m_Blend = !m_Blend;
- if (m_Blend)
- {
- glEnable(GL_BLEND); //开启混合
- glDisable(GL_DEPTH_TEST); //关闭深度测试
- }
- else
- {
- glDisable(GL_BLEND); //关闭混合
- glEnable(GL_DEPTH_TEST); //打开深度测试
- }
- break;
- case Qt::Key_L: //L为开启关闭光源的切换键
- m_Light = !m_Light;
- if (m_Light)
- {
- glEnable(GL_LIGHTING); //开启光源
- }
- else
- {
- glDisable(GL_LIGHTING); //关闭光源
- }
- break;
- case Qt::Key_PageUp: //PageUp按下使木箱移向屏幕内部
- m_Deep -= 0.1f;
- break;
- case Qt::Key_PageDown: //PageDown按下使木箱移向观察者
- m_Deep += 0.1f;
- break;
- case Qt::Key_Up: //Up按下减少m_xSpeed
- m_xSpeed -= 0.1f;
- break;
- case Qt::Key_Down: //Down按下增加m_xSpeed
- m_xSpeed += 0.1f;
- break;
- case Qt::Key_Right: //Right按下减少m_ySpeed
- m_ySpeed -= 0.1f;
- break;
- case Qt::Key_Left: //Left按下增加m_ySpeed
- m_ySpeed += 0.1f;
- break;
- }
- }
当B键的控制机制与L键相似,但注意到,开启混合时还要关闭深度测试,关闭混合时还要打开深度测试,否则将发现立方体有一些面不见了!
现在就可以运行程序看效果了!
第09课:在3D空间中移动位图
想知道如何在3D空间中移动物体,想知道如何在屏幕上绘制一个图像,而让图像的背景色变为透明,希望有一个简单的动画。这次教程中将教会你所以的一切。当然,这一课是在前面几课知识的基础上创建的,请确保你已经掌握了前面几课的知识,再进入本课教程。
欢迎进入这次教程,这一课将是前面几课的综合。前面的学习中,我们学会了设置一个OpenGL窗口的每个细节,学会在旋转的物体上贴图并打上光线以及混色(透明)处理。这一课中,我们将在3D场景中移动位图,并去除位图上的黑色像素(使用混色)。接着为黑白纹理上色,最后我们将学会创建丰富的色彩,并把混合了不同色彩的纹理相互混合,得到简单的动画效果。
程序运行时效果如下:
下面进入教程:
我们这次将在第01课的基础上修改代码,其中一些与前几课重复的地方我不作过多解释。首先打开myglwdiget.h文件,将类声明更改如下:
- #ifndef MYGLWIDGET_H
- #define MYGLWIDGET_H
- #include <QWidget>
- #include <QGLWidget>
- class MyGLWidget : public QGLWidget
- {
- Q_OBJECT
- public:
- explicit MyGLWidget(QWidget *parent = 0);
- ~MyGLWidget();
- protected:
- //对3个纯虚函数的重定义
- void initializeGL();
- void resizeGL(int w, int h);
- void paintGL();
- void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
- private:
- bool fullscreen; //是否全屏显示
- QString m_FileName; //图片的路径及文件名
- GLuint m_Texture; //储存一个纹理
- bool m_Twinkle; //星星是否闪烁
- static const int num = 50; //星星的数目
- struct star{ //为星星创建的结构体
- int r, g, b; //星星的颜色
- GLfloat dist; //星星距离中心的距离
- GLfloat angle; //当前星星所处的角度
- } m_stars[num];
- GLfloat m_Deep; //星星离观察者的距离
- GLfloat m_Tilt; //星星的倾角
- GLfloat m_Spin; //星星的自转
- };
- #endif // MYGLWIDGET_H
首先是一个布尔变量m_Twinkle用来表示星星是否闪烁。然后我们创建了一个星星的结构体,结构体包含星星的颜色,离中心距离以及所处角度,并创建了一个大小为50的星星数组。最后三个浮点变量依次表示星星离观察者距离,星星的倾角,星星的自转,这三个浮点变量用于对整体视图的控制。
接下来,我们还是打开myglwidget.cpp,加上声明#include <QTimer>,在构造函数中对新增变量进行初始化,只解释小部分,希望大家结合注释可以理解,代码如下:
- MyGLWidget::MyGLWidget(QWidget *parent) :
- QGLWidget(parent)
- {
- fullscreen = false;
- m_FileName = "D:/QtOpenGL/QtImage/Star.bmp"; //应根据实际存放图片的路径进行修改
- m_Twinkle = false; //默认初始状态为不闪烁
- for (int i=0; i<num; i++) //循环初始化所有的星星
- {
- //随机获得星星颜色
- m_stars[i].r = rand() % 256;
- m_stars[i].g = rand() % 256;
- m_stars[i].b = rand() % 256;
- m_stars[i].dist = ((float)i / num) * 5.0f; //计算星星离中心的距离,最大半径为5.0
- m_stars[i].angle = 0.0f; //所以星星都从0度开始旋转
- }
- m_Deep = -15.0f; //深入屏幕15.0单位
- m_Tilt = 90.0f; //初始倾角为90度(面对观察者)
- m_Spin = 0.0f; //从0度开始自转
- QTimer *timer = new QTimer(this); //创建一个定时器
- //将定时器的计时信号与updateGL()绑定
- connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
- timer->start(10); //以10ms为一个计时周期
- }
利用循环对星星的数据进行初始化,第i 颗星星离中心的距离是将i 的值除以星星的总数,然后乘上5.0f。基本上这样使得后一颗星星比前一颗星星离中心更远一点,这样当i = 50时,就刚好达到最大半径5.0f了。然后我们选择颜色都是从0~255之间取一个随机数,为何这里不是通常的0.0f~1.0f呢?这里我们使用的颜色设置函数时glColor4ub,而不是之前的glColor4f,ub意味着参数是Unsigned Byte型的,同时这里去随机数整数似乎要比取一个浮点的随机数更容易一些。
然后我们要对initializeGL()函数作一定的修改,修改后代码如下:
- void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置
- {
- m_Texture = bindTexture(QPixmap(m_FileName));
- glEnable(GL_TEXTURE_2D);
- glClearColor(0.0, 0.0, 0.0, 0.0); //黑色背景
- glShadeModel(GL_SMOOTH); //启用阴影平滑
- glClearDepth(1.0); //设置深度缓存
- glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告诉系统对透视进行修正
- glBlendFunc(GL_SRC_ALPHA, GL_ONE); //设置混色函数取得半透明效果
- glEnable(GL_BLEND); //开启混合(混色)
- }
这里我们不打算使用深度测试,如果你使用第01课的代码的话,请确认是否已经去掉了glDepthFunc(GL_LEQUAL);和glEnable(GL_DEPTH_TEST);两行。否则,你所见到的最终效果会一团糟。这里我们使用了纹理映射,因此请你确认你已经加入了这些这一课中所没有的代码。同样要注意的是我们也开启了混合(混色),这是为了给纹理上色,产生不同颜色的星星。
还有就是最重点的paintGL()函数,我会一一作出解释,具体代码如下:
- void MyGLWidget::paintGL() //从这里开始进行所以的绘制
- {
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
- glBindTexture(GL_TEXTURE_2D, m_Texture); //选择纹理
- for (int i=0; i<num; i++)
- {
- glLoadIdentity(); //绘制每颗星星之前,重置模型观察矩阵
- glTranslatef(0.0f, 0.0f, m_Deep); //深入屏幕里面
- glRotatef(m_Tilt, 1.0f, 0.0f, 0.0f); //倾斜视角
- glRotatef(m_stars[i].angle, 0.0f, 1.0f, 0.0f); //旋转至当前所画星星的角度
- glTranslatef(m_stars[i].dist, 0.0f, 0.0f); //沿x轴正向移动
- glRotatef(-m_stars[i].angle, 0.0f, 1.0f, 0.0f); //取消当前星星的角度
- glRotatef(-m_Tilt, 1.0f, 0.0f, 0.0f); //取消视角倾斜
- if (m_Twinkle) //启动闪烁效果
- {
- //使用byte型数据值指定一个颜色
- glColor4ub(m_stars[num-i-1].r, m_stars[num-i-1].g, m_stars[num-i-1].b, 255);
- glBegin(GL_QUADS); //开始绘制纹理映射过的四边形
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, 0.0f);
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, 0.0f);
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, 0.0f);
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, 0.0f);
- glEnd(); //四边形绘制结束
- }
- glRotatef(m_Spin, 0.0f, 0.0f, 1.0f); //绕z轴旋转星星
- //使用byte型数据值指定一个颜色
- glColor4ub(m_stars[i].r, m_stars[i].g, m_stars[i].b, 255);
- glBegin(GL_QUADS); //开始绘制纹理映射过的四边形
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, 0.0f);
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, 0.0f);
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, 0.0f);
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, 0.0f);
- glEnd(); //四边形绘制结束
- m_Spin += 0.01f; //星星的自转
- m_stars[i].angle += (float)i / num; //改变星星的公转角度
- m_stars[i].dist -= 0.01f; //改变星星离中心的距离
- if (m_stars[i].dist < 0.0f) //星星到达中心了么
- {
- m_stars[i].dist += 5.0f; //往外移5.0单位
- m_stars[i].r = rand() % 256;
- m_stars[i].g = rand() % 256;
- m_stars[i].b = rand() % 256;
- }
- }
- }
首先是清屏和绑定纹理,接着进入循环,画每颗星星前当然要重置模型观察矩阵并进行视图的移动旋转,然后我们来移动星星。我们要做的第一件事是把场景沿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单位,这颗星星将重新踏上回归屏幕中心的旅程。
最后就是键盘控制部分了,具体代码如下(相信大家结合注释可以很容易看懂):
- void MyGLWidget::keyPressEvent(QKeyEvent *event)
- {
- switch (event->key())
- {
- case Qt::Key_F1: //F1为全屏和普通屏的切换键
- fullscreen = !fullscreen;
- if (fullscreen)
- {
- showFullScreen();
- }
- else
- {
- showNormal();
- }
- updateGL();
- break;
- case Qt::Key_Escape: //ESC为退出键
- close();
- break;
- case Qt::Key_T: //T为星星开启关闭闪烁的切换键
- m_Twinkle = !m_Twinkle;
- break;
- case Qt::Key_Up: //Up按下屏幕向上倾斜
- m_Tilt -= 0.5f;
- break;
- case Qt::Key_Down: //Down按下屏幕向下倾斜
- m_Tilt += 0.5f;
- break;
- case Qt::Key_PageUp: //PageUp按下缩小
- m_Deep -= 0.1f;
- break;
- case Qt::Key_PageDown: //PageDown按下放大
- m_Deep += 0.1f;
- break;
- }
- }
现在就可以运行程序查看效果了!
- #ifndef MYGLWIDGET_H
- #define MYGLWIDGET_H
- #include <QWidget>
- #include <QGLWidget>
- typedef struct tagVERTEX //创建Vertex顶点结构体
- {
- float x, y, z; //3D坐标
- float u, v; //纹理坐标
- } VERTEX;
- typedef struct tagTRIANGLE //创建Triangle三角形结构体
- {
- VERTEX vertexs[3]; //3个顶点构成一个Triangle
- } TRIANGLE;
- typedef struct tagSECTOR //创建Sector区段结构体
- {
- int numtriangles; //Sector中的三角形个数
- QVector<TRIANGLE> vTriangle; //储存三角形的向量
- } SECTOR;
- class MyGLWidget : public QGLWidget
- {
- Q_OBJECT
- public:
- explicit MyGLWidget(QWidget *parent = 0);
- ~MyGLWidget();
- protected:
- //对3个纯虚函数的重定义
- void initializeGL();
- void resizeGL(int w, int h);
- void paintGL();
- void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
- private:
- bool fullscreen; //是否全屏显示
- QString m_FileName; //图片的路径及文件名
- GLuint m_Texture; //储存一个纹理
- QString m_WorldFile; //存放世界的路径及文本名
- SECTOR m_Sector; //储存一个区段的数据
- static const float m_PIOVER180 = 0.0174532925f; //实现度和弧度直接的折算
- GLfloat m_xPos; //储存当前位置
- GLfloat m_zPos;
- GLfloat m_yRot; //视角的旋转
- GLfloat m_LookUpDown; //记录抬头和低头
- };
- #endif // MYGLWIDGET_H
可以看到我们定义了3个结构体,依次表示顶点,三角形和区段。一个区段包含一系列的多边形(三角形),三角形本质上是由三个以上顶点组合的图形,顶点就是我们最基本的分类单位了。顶点包含了OpenGL真正感兴趣的数据,我们用3D空间中的坐标值(x, y, z)以及它们的纹理坐标(u, v)来定义三角形的每个顶点。这次教程中,我们只加载了一个区段的数据,故只需一个m_Sector数据就够了(当然有兴趣的可以自己设计区段数据,多加载几个看看)。
- MyGLWidget::MyGLWidget(QWidget *parent) :
- QGLWidget(parent)
- {
- fullscreen = false;
- m_FileName = "D:/QtOpenGL/QtImage/Mud.bmp"; //应根据实际存放图片的路径进行修改
- m_WorldFile = "D:/QtOpenGL/QtImage/World.txt";
- m_Sector.numtriangles = 0;
- QFile file(m_WorldFile);
- file.open(QIODevice::ReadOnly | QIODevice::Text); //将要读入数据的文本打开
- QTextStream in(&file);
- while (!in.atEnd())
- {
- QString line[3];
- for (int i=0; i<3; i++) //循环读入3个点数据
- {
- do //读入数据并保证数据有效
- {
- line[i] = in.readLine();
- }
- while (line[i][0] == '/' || line[i] == "");
- }
- m_Sector.numtriangles++; //每成功读入3个点构成一个三角形
- TRIANGLE tempTri;
- for (int i=0; i<3; i++) //将数据储存于一个三角形中
- {
- QTextStream inLine(&line[i]);
- inLine >> tempTri.vertexs[i].x
- >> tempTri.vertexs[i].y
- >> tempTri.vertexs[i].z
- >> tempTri.vertexs[i].u
- >> tempTri.vertexs[i].v;
- }
- m_Sector.vTriangle.push_back(tempTri); //将三角形放入m_Sector中
- }
- file.close();
- m_xPos = 0.0f;
- m_zPos = 0.0f;
- m_yRot = 0.0f;
- m_LookUpDown = 0.0f;
- QTimer *timer = new QTimer(this); //创建一个定时器
- //将定时器的计时信号与updateGL()绑定
- connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
- timer->start(10); //以10ms为一个计时周期
- }
我们重点解释中间对于m_Sector的初始化,我们先将文件打开,再利用Qt的文本流一行一行的读取(为何一行一行读,大家看下存放数据的文本文件World.txt就知道了)并保证读入的数据是有效的。每当成功读入三行数据时,说明构成了一个三角形,就创建一个三角形来储存这些数据,并在最后把三角形放入m_Sector中,当然要给m_Sector的numtriangles加上一,说明多了一个三角形。最后录完数据后,关上文件。或者你会想如果有效数据行数不是3的倍数怎么办,这个问题其实已经不是我们的问题了,而且提供的数据文本存在问题,因此不必考虑。接着的数据初始化不作解释了。
- void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置
- {
- m_Texture = bindTexture(QPixmap(m_FileName));
- glEnable(GL_TEXTURE_2D);
- glClearColor(0.0, 0.0, 0.0, 0.0); //黑色背景
- glShadeModel(GL_SMOOTH); //启用阴影平滑
- glClearDepth(1.0); //设置深度缓存
- glEnable(GL_DEPTH_TEST); //启用深度测试
- glDepthFunc(GL_LEQUAL); //所作深度测试的类型
- glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告诉系统对透视进行修正
- }
- void MyGLWidget::keyPressEvent(QKeyEvent *event)
- {
- switch (event->key())
- {
- case Qt::Key_F1: //F1为全屏和普通屏的切换键
- fullscreen = !fullscreen;
- if (fullscreen)
- {
- showFullScreen();
- }
- else
- {
- showNormal();
- }
- updateGL();
- break;
- case Qt::Key_Escape: //ESC为退出键
- close();
- break;
- case Qt::Key_PageUp: //按下PageUp视角向上转
- m_LookUpDown -= 1.0f;
- if (m_LookUpDown < -90.0f)
- {
- m_LookUpDown = -90.0f;
- }
- break;
- case Qt::Key_PageDown: //按下PageDown视角向下转
- m_LookUpDown += 1.0f;
- if (m_LookUpDown > 90.0f)
- {
- m_LookUpDown = 90.0f;
- }
- break;
- case Qt::Key_Right: //Right按下向左旋转场景
- m_yRot -= 1.0f;
- break;
- case Qt::Key_Left: //Left按下向右旋转场景
- m_yRot += 1.0f;
- break;
- case Qt::Key_Up: //Up按下向前移动
- //向前移动分到x、z上的分量
- m_xPos -= (float)sin(m_yRot * m_PIOVER180) * 0.05f;
- m_zPos -= (float)cos(m_yRot * m_PIOVER180) * 0.05f;
- break;
- case Qt::Key_Down: //Down按下向后移动
- //向后移动分到x、z上的分量
- m_xPos += (float)sin(m_yRot * m_PIOVER180) * 0.05f;
- m_zPos += (float)cos(m_yRot * m_PIOVER180) * 0.05f;
- break;
- }
- }
这个实现很简单。当左右方向键按下后,旋转变量m_yRot相应的增加或减少。当前后方向键按下时,我们使用sin()和cos()函数计算具体在x和z轴方向上的位移量,使得游戏者能准确的移动。
- void MyGLWidget::paintGL() //从这里开始进行所以的绘制
- {
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
- glLoadIdentity(); //重置当前的模型观察矩阵
- GLfloat x_m, y_m, z_m, u_m, v_m; //顶点的临时x、y、z、u、v值
- GLfloat xTrans = -m_xPos; //游戏者沿x轴平移时的大小
- GLfloat zTrans = -m_zPos; //游戏者沿z轴平移时的大小
- GLfloat yTrans = -0.25f; //游戏者沿y轴略作平移,使视角准确
- GLfloat sceneroty = 360.0f - m_yRot; //游戏者的旋转
- glRotatef(m_LookUpDown, 1.0f, 0.0f, 0.0f); //抬头低头的旋转
- glRotatef(sceneroty, 0.0f, 1.0f, 0.0f); //根据游戏者正面所对方向所作的旋转
- glTranslatef(xTrans, yTrans, zTrans); //以游戏者为中心平移场景
- glBindTexture(GL_TEXTURE_2D, m_Texture); //绑定纹理
- for (int i=0; i<m_Sector.numtriangles; i++) //遍历所有的三角形
- {
- glBegin(GL_TRIANGLES); //开始绘制三角形
- glNormal3f(0.0f, 0.0f, 1.0f); //指向前面的法线
- x_m = m_Sector.vTriangle[i].vertexs[0].x;
- y_m = m_Sector.vTriangle[i].vertexs[0].y;
- z_m = m_Sector.vTriangle[i].vertexs[0].z;
- u_m = m_Sector.vTriangle[i].vertexs[0].u;
- v_m = m_Sector.vTriangle[i].vertexs[0].v;
- glTexCoord2f(u_m, v_m);
- glVertex3f(x_m, y_m, z_m);
- x_m = m_Sector.vTriangle[i].vertexs[1].x;
- y_m = m_Sector.vTriangle[i].vertexs[1].y;
- z_m = m_Sector.vTriangle[i].vertexs[1].z;
- u_m = m_Sector.vTriangle[i].vertexs[1].u;
- v_m = m_Sector.vTriangle[i].vertexs[1].v;
- glTexCoord2f(u_m, v_m);
- glVertex3f(x_m, y_m, z_m);
- x_m = m_Sector.vTriangle[i].vertexs[2].x;
- y_m = m_Sector.vTriangle[i].vertexs[2].y;
- z_m = m_Sector.vTriangle[i].vertexs[2].z;
- u_m = m_Sector.vTriangle[i].vertexs[2].u;
- v_m = m_Sector.vTriangle[i].vertexs[2].v;
- glTexCoord2f(u_m, v_m);
- glVertex3f(x_m, y_m, z_m);
- glEnd(); //三角形绘制结束
- }
- }
就正如我们之前步骤2和3所说,我们以相反的方式来平移和旋转场景,使得看上去是视角在平移和旋转,然后绑定纹理并绘制出整个场景就完成了!
第11课:旗帜效果(飘动的纹理) (参照NeHe)
这次教程中,我将教大家如何创建一个飘动的旗帜。我们所要创建的旗帜,说白了就是一个以正弦波方式运动的纹理映射图像。虽然不会很难,但效果确实很不错,希望大家能喜欢。当然这次教程是基于第06课的,希望大家确保已经掌握了前6课再进入本次教程。
程序运行时效果如下:
下面进入教程:
我们这次将在第06课的基础上修改代码,我们只会解释增加部分的代码,首先打开myglwidget.h文件,将类声明更改如下:
- #ifndef MYGLWIDGET_H
- #define MYGLWIDGET_H
- #include <QWidget>
- #include <QGLWidget>
- class MyGLWidget : public QGLWidget
- {
- Q_OBJECT
- public:
- explicit MyGLWidget(QWidget *parent = 0);
- ~MyGLWidget();
- protected:
- //对3个纯虚函数的重定义
- void initializeGL();
- void resizeGL(int w, int h);
- void paintGL();
- void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
- private:
- bool fullscreen; //是否全屏显示
- GLfloat m_xRot; //绕x轴旋转的角度
- GLfloat m_yRot; //绕y轴旋转的角度
- GLfloat m_zRot; //绕z轴旋转的角度
- QString m_FileName; //图片的路径及文件名
- GLuint m_Texture; //储存一个纹理
- float m_Points[45][45][3]; //储存网格顶点的数组
- int m_WiggleCount; //用于控制旗帜波浪运动动画
- };
- #endif // MYGLWIDGET_H
我们增加了m_Points三维数组来存放网格各顶点独立的x、y、z坐标,这里网格由45×45点形成,换句话说也就是由44格×44格的小方格子组成的。另一个新增变量m_WiggleCount用来使产生纹理波浪运动动画,每2帧一次变换波动形状看起来很不错。
接下来,我们需要打开myglwidget.cpp,加上声明#include <QtMath>,在构造函数对新增变量数据进行初始化,具体代码如下:
- MyGLWidget::MyGLWidget(QWidget *parent) :
- QGLWidget(parent)
- {
- fullscreen = false;
- m_xRot = 0.0f;
- m_yRot = 0.0f;
- m_zRot = 0.0f;
- m_FileName = "D:/QtOpenGL/QtImage/Tim.bmp"; //应根据实际存放图片的路径进行修改
- for (int x=0; x<45; x++) //初始化数组产生波浪效果(静止)
- {
- for (int y=0; y<45; y++)
- {
- m_Points[x][y][0] = float((x / 5.0f) - 4.5f);
- m_Points[x][y][1] = float((y / 5.0f) - 4.5f);
- m_Points[x][y][2] = float(sin((((x/5.0f)*40.0f)/360.0f)*3.141592654*2.0f));
- }
- }
- m_WiggleCount = 0;
- QTimer *timer = new QTimer(this); //创建一个定时器
- //将定时器的计时信号与updateGL()绑定
- connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
- timer->start(10); //以10ms为一个计时周期
- }
增加的代码就是一个循环,利用循环来添加波浪效果(只是让旗帜看起来有起伏效果,还不能达到波动动画的目的)。值得注意的是,我们在求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()函数中,请大家修改代码如下:
- void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置
- {
- m_Texture = bindTexture(QPixmap(m_FileName)); //载入位图并转换成纹理
- glEnable(GL_TEXTURE_2D); //启用纹理映射
- glClearColor(0.0, 0.0, 0.0, 0.0); //黑色背景
- glShadeModel(GL_SMOOTH); //启用阴影平滑
- glClearDepth(1.0); //设置深度缓存
- glEnable(GL_DEPTH_TEST); //启用深度测试
- glDepthFunc(GL_LEQUAL); //所作深度测试的类型
- glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告诉系统对透视进行修正
- glPolygonMode(GL_BACK, GL_FILL); //后表面完全填充
- glPolygonMode(GL_FRONT, GL_LINE); //前表面使用线条绘制
- }
最后加了两行代码,用来指定使用完全填充模式来填充多边形区域的后表面,而多边形的前表面则使用轮廓线填充,这些方式完全取决于你的个人喜好,这里我们只是为了区分前后表面罢了。
最后,我们将重写整个paintGL()函数,当然这依旧是重点,代码如下:
- void MyGLWidget::paintGL() //从这里开始进行所以的绘制
- {
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
- glLoadIdentity(); //重置当前的模型观察矩阵
- glTranslatef(0.0f, 0.0f, -15.0f); //移入屏幕15.0单位
- glRotatef(m_xRot, 1.0f, 0.0f, 0.0f); //绕x旋转
- glRotatef(m_yRot, 0.0f, 1.0f, 0.0f); //绕y旋转
- glRotatef(m_zRot, 0.0f, 0.0f, 1.0f); //绕z旋转
- glBindTexture(GL_TEXTURE_2D, m_Texture); //旋转纹理
- float flag_x1, flag_y1, flag_x2, flag_y2; //用来将纹理分割成小的四边形方便纹理映射
- glBegin(GL_QUADS);
- for (int x=0; x<44; x++)
- {
- for (int y=0; y<44; y++)
- {
- //分割纹理
- flag_x1 = float(x) / 44.0f;
- flag_y1 = float(y) / 44.0f;
- flag_x2 = float(x+1) / 44.0f;
- flag_y2 = float(y+1) / 44.0f;
- //绘制一个小的四边形
- glTexCoord2f(flag_x1, flag_y1);
- glVertex3f(m_Points[x][y][0], m_Points[x][y][1], m_Points[x][y][2]);
- glTexCoord2f(flag_x1, flag_y2);
- glVertex3f(m_Points[x][y+1][0], m_Points[x][y+1][1], m_Points[x][y+1][2]);
- glTexCoord2f(flag_x2, flag_y2);
- glVertex3f(m_Points[x+1][y+1][0], m_Points[x+1][y+1][1], m_Points[x+1][y+1][2]);
- glTexCoord2f(flag_x2, flag_y1);
- glVertex3f(m_Points[x+1][y][0], m_Points[x+1][y][1], m_Points[x+1][y][2]);
- }
- }
- glEnd();
- if (m_WiggleCount == 3) //用来变换波浪形状(每2帧一次)产生波浪动画
- {
- //利用循环使波浪值集体左移,最左侧波浪值到了最右侧
- for (int y=0; y<45; y++)
- {
- float temp = m_Points[0][y][2];
- for (int x=0; x<44; x++)
- {
- m_Points[x][y][2] = m_Points[x+1][y][2];
- }
- m_Points[44][y][2] = temp;
- }
- m_WiggleCount = 0; //计数器清零
- }
- m_WiggleCount++; //计数器加一
- m_xRot += 0.3f;
- m_yRot += 0.2f;
- m_zRot += 0.4f;
- }
我们创建了四个浮点临时变量并利用循环和除法,将纹理分割成小的四边形,使得我们能准确的对应进行纹理映射,然后画出全部的四边形拼到一起就是一个波动状态的旗帜了。
接着我们判断一下m_WiggleCount是否为2,如果是,就将波浪值m_Points[x][y][2]集体循环左移(最左侧波浪值会到最右侧)。这样我们相当于每2帧一次变化了旗帜的波动状态,看起来就是一个飘动的旗帜,不是静止的了(大家可以尝试着注释掉某一部分代码看看发生什么改变)。然后计数器清零加一什么的就不过多解释了!
现在就可以运行程序查看效果了!
- #ifndef MYGLWIDGET_H
- #define MYGLWIDGET_H
- #include <QWidget>
- #include <QGLWidget>
- class MyGLWidget : public QGLWidget
- {
- Q_OBJECT
- public:
- explicit MyGLWidget(QWidget *parent = 0);
- ~MyGLWidget();
- protected:
- //对3个纯虚函数的重定义
- void initializeGL();
- void resizeGL(int w, int h);
- void paintGL();
- void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
- private:
- void buildLists(); //初始化盒子的显示列表
- private:
- bool fullscreen; //是否全屏显示
- GLfloat m_xRot; //绕x轴旋转的角度
- GLfloat m_yRot; //绕y轴旋转的角度
- QString m_FileName; //图片的路径及文件名
- GLuint m_Texture; //储存一个纹理
- GLuint m_Box; //保存盒子的显示列表
- GLuint m_Top; //保存盒子顶部的显示列表
- };
- #endif // MYGLWIDGET_H
我们新增了两个用于显示列表的变量m_Box、m_Top,这两个变量是用于储存指向显示列表的指针。另外我们多了一个buildLists()函数,这个函数是用于初始化两个显示列表的(注意我去掉了变量m_zRot,但其实影响不大)。
- MyGLWidget::MyGLWidget(QWidget *parent) :
- QGLWidget(parent)
- {
- fullscreen = false;
- m_xRot = 0.0f;
- m_yRot = 0.0f;
- m_FileName = "D:/QtOpenGL/QtImage/Cube.bmp"; //应根据实际存放图片的路径进行修改
- QTimer *timer = new QTimer(this); //创建一个定时器
- //将定时器的计时信号与updateGL()绑定
- connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
- timer->start(10); //以10ms为一个计时周期
- }
- void MyGLWidget::buildLists() //创建盒子的显示列表
- {
- m_Box = glGenLists(2); //创建两个显示列表的空间
- glNewList(m_Box, GL_COMPILE); //开始创建第一个显示列表
- glBegin(GL_QUADS);
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, 1.0f); //右上(底面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, 1.0f); //左上(底面)
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(-1.0f, -1.0f, -1.0f); //左下(底面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(1.0f, -1.0f, -1.0f); //右下(底面)
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, 1.0f); //右上(前面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, 1.0f); //左上(前面)
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, 1.0f); //左下(前面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, 1.0f); //右下(前面)
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, -1.0f); //右上(后面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, -1.0f); //左上(后面)
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, -1.0f); //左下(后面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, -1.0f); //右下(后面)
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, 1.0f); //右上(左面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, -1.0f); //左上(左面)
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, -1.0f); //左下(左面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, 1.0f); //右下(左面)
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, -1.0f); //右上(右面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, 1.0f); //左上(右面)
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, 1.0f); //左下(右面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, -1.0f); //右下(右面)
- glEnd();
- glEndList(); //第一个显示列表结束
- m_Top = m_Box + 1; //m_Box+1得到第二个显示列表的指针
- glNewList(m_Top, GL_COMPILE); //开始创建第二个显示列表
- glBegin(GL_QUADS);
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, -1.0f); //右上(顶面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, -1.0f); //左上(顶面)
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(-1.0f, 1.0f, 1.0f); //左下(顶面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(1.0f, 1.0f, 1.0f); //右下(顶面)
- glEnd();
- glEndList();
- }
构造函数不解释了,就删掉了m_zRot的初始化。buildLists()函数中,我们会将创造盒子的代码都放在第一个显示列表里,所有创造顶部的代码都在另一个显示列表里。开始时,我们告诉OpenGL我们要建立两个显示列表,glGenLists(2)创建了两个显示列表的空间,并返回第一个列表的指针,我们把它储存在m_Box中,任何时候我们调用glCallList(m_Box)第一个显示列表就会绘制出来。
- void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置
- {
- m_Texture = bindTexture(QPixmap(m_FileName)); //载入位图并转换成纹理
- glEnable(GL_TEXTURE_2D); //启用纹理映射
- buildLists(); //创建显示列表
- glClearColor(0.0f, 0.0f, 0.0f, 0.0f); //黑色背景
- glShadeModel(GL_SMOOTH); //启用阴影平滑
- glClearDepth(1.0); //设置深度缓存
- glEnable(GL_DEPTH_TEST); //启用深度测试
- glDepthFunc(GL_LEQUAL); //所作深度测试的类型
- glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告诉系统对透视进行修正
- <pre name="code" class="cpp">
glEnable(GL_LIGHT0); //使用默认的0号灯 glEnable(GL_LIGHTING); //使用灯光 glEnable(GL_COLOR_MATERIAL); //使用颜色材质}
我们在启用纹理之后调用了buildLists()函数,创建了显示列表,注意在构造函数中调用buildLists()函数时无法生效的,Qt中使用OpenGL的时候,与内存使用相关的OpenGL函数都需要在initialize()、resize()、paintGL()中直接调用或间接调用,否则无法成功地申请空间。
- void MyGLWidget::paintGL() //从这里开始进行所以的绘制
- {
- static const GLfloat boxColor[5][3] = //盒子的颜色数组
- {
- //亮:红、橙、黄、绿、蓝
- {1.0f, 0.0f, 0.0f}, {1.0f, 0.5f, 0.0f}, {1.0f, 1.0f, 0.0f},
- {0.0f, 1.0f, 0.0f}, {0.0f, 1.0f, 1.0f}
- };
- static const GLfloat topColor[5][3] = //顶部的颜色数组
- {
- //暗:红、橙、黄、绿、蓝
- {0.5f, 0.0f, 0.0f}, {0.5f, 0.25f, 0.0f}, {0.5f, 0.5f, 0.0f},
- {0.0f, 0.5f, 0.0f}, {0.0f, 0.5f, 0.5f}
- };
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
- glBindTexture(GL_TEXTURE_2D, m_Texture); //选择纹理
- for (int y=1; y<6; y++) //循环来控制画盒子
- {
- for (int x=0; x<y; x++)
- {
- glLoadIdentity();
- //设置盒子的位置
- glTranslatef(1.4f+(float(x)*2.8f)-(float(y)*1.4f),
- ((6.0f-float(y))*2.4f)-7.0f, -20.0f);
- glRotatef(45.0f+m_xRot, 1.0f, 0.0f, 0.0f);
- glRotatef(45.0f+m_yRot, 0.0f, 1.0f, 0.0f);
- glColor3fv(boxColor[y-1]); //选择盒子颜色
- glCallList(m_Box); //绘制盒子
- glColor3fv(topColor[y-1]); //选择顶部颜色
- glCallList(m_Top); //绘制顶部
- }
- }
- }
我们一开始就定义了储存盒子和顶部颜色的数组,接着我们用双重循环来画出10个立方体,并在每次画时重置模型观察矩阵,平移和旋转到需要画出立方体位置的中心,具体如何算的我看太懂NeHe的用意就不解释了,反正这并不是重点,我们完全可以根据自己的喜好来摆放这些立方体。
- void MyGLWidget::keyPressEvent(QKeyEvent *event)
- {
- switch (event->key())
- {
- case Qt::Key_F1: //F1为全屏和普通屏的切换键
- fullscreen = !fullscreen;
- if (fullscreen)
- {
- showFullScreen();
- }
- else
- {
- showNormal();
- }
- updateGL();
- break;
- case Qt::Key_Escape: //ESC为退出键
- close();
- break;
- case Qt::Key_Left: //Left按下向左旋转
- m_yRot -= 1.0f;
- break;
- case Qt::Key_Right: //Right按下向右旋转
- m_yRot += 1.0f;
- break;
- case Qt::Key_Up: //Up按下向上旋转
- m_xRot -= 1.0f;
- break;
- case Qt::Key_Down: //Down按下向下旋转
- m_xRot += 1.0f;
- break;
- }
- }
现在就可以运行程序查看效果了!
第13课:位图字体 (参照NeHe)
这次教程中,我们将创建一些基于2D图像的字体,它们可以缩放平移,但不能旋转,并且总是面向前方,但作为基本的显示来说,我想已经足够了。
或者对于这次教程,你会觉得“在屏幕上显示文字没什么难的”,但是你真正尝试过就会知道,它确实没那么容易。你当然可以把文字写在一个图片上,再把这幅图片载入你的OpenGL程序中,打开混合选项,从而在屏幕上显示出文字。但这种做法非常耗时,而且经常图像会显得模糊。另外,除非你的图像包含一个Alpha通道,否则一旦绘制在屏幕上,那些文字就会不透明(与屏幕中的其他物体混合)。
使用位图字体比起使用图形字体(贴图)看起来不止强100倍,你可以随时改变显示在屏幕上的文字,而且用不着为它们逐个制作贴图。只需要将文字定位,再调用我们即将构建的glPrint()函数就可以在屏幕上显示文字了。
程序运行时效果如下:
下面进入教程:
我们这次将在第01课的基础上修改代码,我会对新增代码一一解释,希望大家能掌握,首先打开myglwidget.h文件,将类声明更改如下:
- #ifndef MYGLWIDGET_H
- #define MYGLWIDGET_H
- #include <QWidget>
- #include <QGLWidget>
- class MyGLWidget : public QGLWidget
- {
- Q_OBJECT
- public:
- explicit MyGLWidget(QWidget *parent = 0);
- ~MyGLWidget();
- protected:
- //对3个纯虚函数的重定义
- void initializeGL();
- void resizeGL(int w, int h);
- void paintGL();
- void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
- private:
- void buildFont(); //创建字体
- void killFont(); //删除显示列表
- void glPrint(const char *fmt, ...); //输出字符串
- private:
- bool fullscreen; //是否全屏显示
- HDC m_HDC; //储存当前设备的指针
- int m_FontSize; //控制字体的大小
- GLuint m_Base; //储存绘制字体的显示列表的开始位置
- GLfloat m_Cnt1; //字体移动计数器1
- GLfloat m_Cnt2; //字体移动计数器2
- };
- #endif // MYGLWIDGET_H
我们新增了几个变量,第一个变量m_HDC是用来储存当前设备绘制信息的一种windows数据结构,我们将会把我们自己创建的字体绑定到m_HDC上去,这样我们绘制文字时就自动采用绑定的字体了。后面几个变量的作用依次是控制字体大小、储存绘制字体的显示列表的开始位置、字体移动计数,具体的会在后面讲。
另外我们增加了三个函数,分别用于创建字体、删除显示列表、输出特定的字符串,当然最后一个glPrint()函数在前面已经提到,是个很重要的函数。
接下来,我们需要打开myglwidget.cpp,加上声明#include <QTimer>、#include <QtMath>,将构造函数和析构函数修改一下,具体代码如下:
- MyGLWidget::MyGLWidget(QWidget *parent) :
- QGLWidget(parent)
- {
- fullscreen = false;
- m_FontSize = -18;
- m_Cnt1 = 0.0f;
- m_Cnt2 = 0.0f;
- HWND hWND = (HWND)winId(); //获取当前窗口句柄
- m_HDC = GetDC(hWND); //通过窗口句柄获得HDC
- QTimer *timer = new QTimer(this); //创建一个定时器
- //将定时器的计时信号与updateGL()绑定
- connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
- timer->start(10); //以10ms为一个计时周期
- }
- MyGLWidget::~MyGLWidget()
- {
- killFont(); //删除显示列表
- }
几个普通变量的初始化我不作解释了,我们重点看m_HDC的初始化。我们要如何获得当前窗口的HDC呢?方法是我们先得到当前窗口的句柄(HWND),通过调用函数GetCD(HWND)可以获得HDC。那如何获得HWND呢?Qt中有一个winId()函数可以返回当前窗口的Id(类型为WId),我们把它强制转换为HWND类型就可以了,这样我们就可以初始化关键的m_HDC。
注意一下析构函数,在退出程序之前,我们应该确保我们分配的用于存放显示列表的空间被释放,所以我们在析构函数处调用killFont()函数删除显示列表(具体实现看下面)。
继续,我们要来定义我们新增的三个函数了,这可是重头戏,具体代码如下:
- void MyGLWidget::buildFont() //创建位图字体
- {
- HFONT font; //字体句柄
- m_Base = glGenLists(96); //创建96个显示列表
- font = CreateFont(m_FontSize, //字体高度
- 0, //字体宽度
- 0, //字体的旋转角度
- 0, //字体底线的旋转角度
- FW_BOLD, //字体的重量
- FALSE, //是否斜体
- FALSE, //是否使用下划线
- FALSE, //是否使用删除线
- ANSI_CHARSET, //设置字符集
- OUT_TT_PRECIS, //输出精度
- CLIP_DEFAULT_PRECIS, //剪裁精度
- ANTIALIASED_QUALITY, //输出质量
- FF_DONTCARE | DEFAULT_PITCH, //Family and Pitch的设置
- LPCWSTR("Courier New")); //字体名称(电脑中已装的)
- wglUseFontBitmaps(m_HDC, 32, 96, m_Base); //创建96个显示列表,绘制ASCII码为32-128的字符
- SelectObject(m_HDC, font); //选择字体
- }
- void MyGLWidget::killFont() //删除显示列表
- {
- glDeleteLists(m_Base, 96); //删除96个显示列表
- }
- void MyGLWidget::glPrint(const char *fmt, ...) //自定义输出文字函数
- {
- char text[256]; //保存字符串
- va_list ap; //指向一个变量列表的指针
- if (fmt == NULL) //如果无输入则返回
- {
- return;
- }
- va_start(ap, fmt); //分析可变参数
- vsprintf(text, fmt, ap); //把参数值写入字符串
- va_end(ap); //结束分析
- glPushAttrib(GL_LIST_BIT); //把显示列表属性压入属性堆栈
- glListBase(m_Base - 32); //设置显示列表的基础值
- glCallLists(strlen(text), GL_UNSIGNED_BYTE, text); //调用显示列表绘制字符串
- glPopAttrib(); //弹出属性堆栈
- }
首先是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()函数,不作解释,代码如下:
- void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置
- {
- glClearColor(0.0, 0.0, 0.0, 0.0); //黑色背景
- glShadeModel(GL_SMOOTH); //启用阴影平滑
- glClearDepth(1.0); //设置深度缓存
- glEnable(GL_DEPTH_TEST); //启用深度测试
- glDepthFunc(GL_LEQUAL); //所作深度测试的类型
- glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告诉系统对透视进行修正
- buildFont(); //创建字体
- }
还有,我们该进入paintGL()函数了,很简单,难的都过去了,具体代码如下:
- void MyGLWidget::paintGL() //从这里开始进行所以的绘制
- {
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
- glLoadIdentity(); //重置当前的模型观察矩阵
- glTranslatef(0.0f, 0.0f, -10.0f); //移入屏幕10.0单位
- //根据字体位置设置颜色
- glColor3f(1.0f*float(cos(m_Cnt1)), 1.0f*float(sin(m_Cnt2)),
- 1.0f-0.5f*float(cos(m_Cnt1+m_Cnt2)));
- //设置光栅化位置,即字体位置
- glRasterPos2f(-4.5f+0.5f*float(cos(m_Cnt1)), 1.92f*float(sin(m_Cnt2)));
- //输出文字到屏幕上
- glPrint("Active OpenGL Text With NeHe - %7.2f", m_Cnt1);
- m_Cnt1 += 0.051f; //增加两个计数器的值
- m_Cnt2 += 0.005f;
- }
值得注意的是,深入屏幕并不能缩小字体,只会给字体变化移动范围(这一点大家自己改改数据就知道了)。然后字体颜色设置和位置设置我觉得没必要解释了,都是数学的东西,我们主要是为了得到一个变化的效果,并不在乎它是怎么实现的。然后就是调用glPrint()函数输出文字,最后增加两个计数器的值就OK了。
最后就是键盘控制的代码了,大家自己看吧,很简单,具体代码如下:
- void MyGLWidget::keyPressEvent(QKeyEvent *event)
- {
- switch (event->key())
- {
- case Qt::Key_F1: //F1为全屏和普通屏的切换键
- fullscreen = !fullscreen;
- if (fullscreen)
- {
- showFullScreen();
- }
- else
- {
- showNormal();
- }
- updateGL();
- break;
- case Qt::Key_Escape: //ESC为退出键
- close();
- break;
- case Qt::Key_PageUp: //PageUp按下字体缩小
- m_FontSize -= 1;
- if (m_FontSize < -75)
- {
- m_FontSize = -75;
- }
- buildFont();
- break;
- case Qt::Key_PageDown: //PageDown按下字体放大
- m_FontSize += 1;
- if (m_FontSize > -5)
- {
- m_FontSize = -5;
- }
- buildFont();
- break;
- }
- }
现在就可以运行程序查看效果了!
- #ifndef MYGLWIDGET_H
- #define MYGLWIDGET_H
- #include <QWidget>
- #include <QGLWidget>
- class MyGLWidget : public QGLWidget
- {
- Q_OBJECT
- public:
- explicit MyGLWidget(QWidget *parent = 0);
- ~MyGLWidget();
- protected:
- //对3个纯虚函数的重定义
- void initializeGL();
- void resizeGL(int w, int h);
- void paintGL();
- void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
- private:
- void buildFont(); //创建字体
- void killFont(); //删除显示列表
- void glPrint(const char *fmt, ...); //输出字符串
- private:
- bool fullscreen; //是否全屏显示
- HDC m_HDC; //储存当前设备的指针
- GLYPHMETRICSFLOAT m_Gmf[256]; //记录256个字符的信息
- GLfloat m_Deep; //移入屏幕的距离
- GLuint m_Base; //储存绘制字体的显示列表的开始位置
- GLfloat m_Rot; //用于旋转字体
- };
- #endif // MYGLWIDGET_H
由于我们没有准备让轮廓字体移动,所以删掉两个计数器。接着增加m_Deep来控制移入屏幕的距离,其实就是来控制字体的放大缩小的。然后再增加m_Rot来控制字体的旋转。最后增加了GLYPHMETRICSFLOAT变量数组m_Gmf[256]用来保存256个轮廓字体显示列表中对应的每一个列表的位置和方向信息,我们通过m_Gmf[num]来选择字母。要注意的是,每个字符的宽度可以不相同,使用GLYPHMETRICS会大大简化我们的工作。
- MyGLWidget::MyGLWidget(QWidget *parent) :
- QGLWidget(parent)
- {
- fullscreen = false;
- m_Deep = -10.0f;
- m_Rot = 0.0f;
- HWND hWND = (HWND)winId(); //获取当前窗口句柄
- m_HDC = GetDC(hWND); //通过窗口句柄获得HDC
- QTimer *timer = new QTimer(this); //创建一个定时器
- //将定时器的计时信号与updateGL()绑定
- connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
- timer->start(10); //以10ms为一个计时周期
- }
继续,我们需要对buildFont()、killFont()、glPrint()三个函数作一定的修改,具体代码如下:
- void MyGLWidget::buildFont() //创建轮廓字体
- {
- HFONT font; //字体句柄
- m_Base = glGenLists(256); //创建256个显示列表
- font = CreateFont(-18, //字体高度
- 0, //字体宽度
- 0, //字体的旋转角度
- 0, //字体底线的旋转角度
- FW_BOLD, //字体的重量
- FALSE, //是否斜体
- FALSE, //是否使用下划线
- FALSE, //是否使用删除线
- ANSI_CHARSET, //设置字符集
- OUT_TT_PRECIS, //输出精度
- CLIP_DEFAULT_PRECIS, //剪裁精度
- ANTIALIASED_QUALITY, //输出质量
- FF_DONTCARE | DEFAULT_PITCH, //Family and Pitch的设置
- LPCWSTR("Comic Sans MS")); //字体名称(电脑中已装的)
- SelectObject(m_HDC, font); //选择字体
- wglUseFontOutlines(m_HDC, //当前HDC
- 0, //从ASCII码第一个字符开始
- 255, //字符数
- m_Base, //第一个显示列表的名称
- 0.0f, //字体光滑度,越小越光滑
- 0.2f, //在z方向突出的距离(字体的厚度)
- WGL_FONT_POLYGONS, //使用多边形来生成字符,每个顶点具有独立法线
- m_Gmf); //用于储存字形度量数据(高度,宽度等)
- }
- void MyGLWidget::killFont() //删除显示列表
- {
- glDeleteLists(m_Base, 256); //删除96个显示列表
- }
- void MyGLWidget::glPrint(const char *fmt, ...) //自定义输出文字函数
- {
- float length = 0;
- char text[256]; //保存字符串
- va_list ap; //指向一个变量列表的指针
- if (fmt == NULL) //如果无输入则返回
- {
- return;
- }
- va_start(ap, fmt); //分析可变参数
- vsprintf(text, fmt, ap); //把参数值写入字符串
- va_end(ap); //结束分析
- for (unsigned int i=0; i<strlen(text); i++) //计算整个字符串的长度
- {
- length += m_Gmf[(int)text[i]].gmfCellIncX;
- }
- glTranslatef(-length / 2, 0.0f, 0.0f); //左移字符串一半的长度
- glPushAttrib(GL_LIST_BIT); //把显示列表属性压入堆栈
- glListBase(m_Base); //设置显示列表的基础值
- glCallLists(strlen(text), GL_UNSIGNED_BYTE, text); //调用显示列表绘制字符串
- glPopAttrib(); //弹出属性堆栈
- }
首先是buildFont()函数。首先创建字体的方法与上一课基本一致,只是把m_FontSize换成了-18。接着,将wglUseFontBitmaps()函数替换成wglUseFontOutlines()函数,这个函数包含了8个参数前4个参数大家自己看注释,第5个参数为光滑度系数,这个值越小,字体看起来会越光滑(其实看不出明显差别)。第6个参数简单点说指的是字体的厚度,有厚度的字体才有立体感嘛,如果这个值为0.0就变成2D字体了。第7个参数告诉OpenGL用多边形来生成字符,使每个顶点都会具有独立的法线,这样加上光源后会有不错的效果(光源效果我们的代码中没有,大家可以自己加加看)。最后一个参数告诉OpenGL把创建的显示列表的度量数据(高度、宽度等)放在数组m_Gmf[]中。
- void MyGLWidget::paintGL() //从这里开始进行所以的绘制
- {
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
- glLoadIdentity(); //重置当前的模型观察矩阵
- glTranslatef(0.0f, 0.0f, m_Deep); //移入屏幕10.0单位
- glRotatef(m_Rot, 1.0f, 0.0f, 0.0f); //绕x轴旋转
- glRotatef(m_Rot*1.5f, 0.0f, 1.0f, 0.0f); //绕y轴旋转
- glRotatef(m_Rot*1.4f, 0.0f, 0.0f, 1.0f); //绕z轴旋转
- //根据字体位置设置颜色
- glColor3f(1.0f*float(cos(m_Rot/20.0f)), 1.0f*float(sin(m_Rot/25.0f)),
- 1.0f-0.5f*float(cos(m_Rot/17.0f)));
- //输出文字到屏幕上
- glPrint("NeHe - %3.2f", m_Rot/50.0f);
- m_Rot += 0.5f; //旋转变量增加
- }
最后,修改键盘控制的代码,就是按PageUp和PageDown可以放大缩小字体,具体代码如下:
- void MyGLWidget::keyPressEvent(QKeyEvent *event)
- {
- switch (event->key())
- {
- case Qt::Key_F1: //F1为全屏和普通屏的切换键
- fullscreen = !fullscreen;
- if (fullscreen)
- {
- showFullScreen();
- }
- else
- {
- showNormal();
- }
- updateGL();
- break;
- case Qt::Key_Escape: //ESC为退出键
- close();
- break;
- case Qt::Key_PageUp: //PageUp按下字体缩小
- m_Deep -= 0.2f;
- break;
- case Qt::Key_PageDown: //PageDown按下字体放大
- m_Deep += 0.2f;
- break;
- }
- }
现在就可以运行程序查看效果了!
第15课:图形字体的纹理映射 (参照NeHe)
这次教程中,我们将在第14课的基础上创建带有纹理的字体,它真的很简单。也许你想知道如何才能给字体赋予纹理贴图?我们可以使用自动纹理坐标生成器,它会自动为字体上的每一个多边形生成纹理坐标。
这次课中我们还将使用Wingdings字体来显示一个海盗旗(骷髅头和十字骨头)的标志,为此我们需要修改buildFont()函数代码。如果你想显示文字的话,就不用改动第14课中buildFont()函数的代码了,当然你也可以选择另一种字体,这都不是什么大事。
程序运行时效果如下:
下面进入教程:
我们这次将在第14课的基础上修改代码,由于有前两课代码的基础,这节课会更简单。我只解释新增的代码,首先打开myglwidget.h文件,将类声明更改如下:
- #ifndef MYGLWIDGET_H
- #define MYGLWIDGET_H
- #include <QWidget>
- #include <QGLWidget>
- class MyGLWidget : public QGLWidget
- {
- Q_OBJECT
- public:
- explicit MyGLWidget(QWidget *parent = 0);
- ~MyGLWidget();
- protected:
- //对3个纯虚函数的重定义
- void initializeGL();
- void resizeGL(int w, int h);
- void paintGL();
- void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
- private:
- void buildFont(); //创建字体
- void killFont(); //删除显示列表
- void glPrint(const char *fmt, ...); //输出字符串
- private:
- bool fullscreen; //是否全屏显示
- HDC m_HDC; //储存当前设备的指针
- GLYPHMETRICSFLOAT m_Gmf[256]; //记录256个字符的信息
- GLfloat m_Deep; //移入屏幕的距离
- GLuint m_Base; //储存绘制字体的显示列表的开始位置
- GLfloat m_Rot; //用于旋转字体
- QString m_FileName; //图片的路径及文件名
- GLuint m_Texture; //储存一个纹理
- };
- #endif // MYGLWIDGET_H
注意到唯一的变化就是最后增加了两个变量,这个两个变量相信大家已经很熟悉了,m_FileName用来保存我们将用于纹理映射的图片路径名,m_Texture用于储存纹理。
接下来我们需要打开myglwidget.cpp,先修改构造函数初始化m_FileName,不多解释了,具体代码如下:
- MyGLWidget::MyGLWidget(QWidget *parent) :
- QGLWidget(parent)
- {
- fullscreen = false;
- m_Deep = -3.0f;
- m_Rot = 0.0f;
- m_FileName = "D:/QtOpenGL/QtImage/Lights.bmp"; //应根据实际存放图片的路径进行修改
- HWND hWND = (HWND)winId(); //获取当前窗口句柄
- m_HDC = GetDC(hWND); //通过窗口句柄获得HDC
- QTimer *timer = new QTimer(this); //创建一个定时器
- //将定时器的计时信号与updateGL()绑定
- connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
- timer->start(10); //以10ms为一个计时周期
- }
继续,我们需要略微修改一下buildFont()函数,修改后代码如下:
- void MyGLWidget::buildFont() //创建位图字体
- {
- HFONT font; //字体句柄
- m_Base = glGenLists(256); //创建256个显示列表
- font = CreateFont(-18, //字体高度
- 0, //字体宽度
- 0, //字体的旋转角度
- 0, //字体底线的旋转角度
- FW_BOLD, //字体的重量
- FALSE, //是否斜体
- FALSE, //是否使用下划线
- FALSE, //是否使用删除线
- SYMBOL_CHARSET, //设置字符集
- OUT_TT_PRECIS, //输出精度
- CLIP_DEFAULT_PRECIS, //剪裁精度
- ANTIALIASED_QUALITY, //输出质量
- FF_DONTCARE | DEFAULT_PITCH, //Family and Pitch的设置
- LPCWSTR("Wingdings")); //字体名称(电脑中已装的)
- SelectObject(m_HDC, font); //选择字体
- wglUseFontOutlines(m_HDC, //当前HDC
- 0, //从ASCII码第一个字符开始
- 255, //字符数
- m_Base, //第一个显示列表的名称
- 0.1f, //字体光滑度,越小越光滑
- 0.2f, //在z方向突出的距离(字体的厚度)
- WGL_FONT_POLYGONS, //使用多边形来生成字符,每个顶点具有独立法线
- m_Gmf); //用于储存字形度量数据(高度,宽度等)
- }
注意到我们只是修改了CreateFont()函数的第9个参数(设置字符集)和最后一个参数(字体名称),修改最后一个参数好理解,因为我们要使用字体Wingdings嘛。但其实这样修改后,我们想用的Wingdings字体并不会工作。这是由于Wingdings里的字体都不是标准字符字体,我们必须告诉Windows这种字体是一种符号字体而不是一种标准字符字体,因此我们在设置字符集时,把参数改为SYMBOL_CHARSET,如此Wingdings字体就能正常工作了。
然后我们需要来修改initializeGL()函数,这是本节课的重点部分,具体代码如下:
- void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置
- {
- //自动生成纹理
- glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
- glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
- glEnable(GL_TEXTURE_GEN_S);
- glEnable(GL_TEXTURE_GEN_T);
- m_Texture = bindTexture(QPixmap(m_FileName)); //载入位图并转换成纹理
- glEnable(GL_TEXTURE_2D); //启用纹理映射
- glClearColor(0.0, 0.0, 0.0, 0.0); //黑色背景
- glShadeModel(GL_SMOOTH); //启用阴影平滑
- glClearDepth(1.0); //设置深度缓存
- glEnable(GL_DEPTH_TEST); //启用深度测试
- glDepthFunc(GL_LEQUAL); //所作深度测试的类型
- glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告诉系统对透视进行修正
- buildFont(); //创建字体
- }
注意到,我们一开始增加了四行新代码,这四行代码将为我们绘制在屏幕上的任何物体自动生成纹理坐标。函数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()函数,具体代码如下:
- void MyGLWidget::paintGL() //从这里开始进行所以的绘制
- {
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
- glLoadIdentity(); //重置当前的模型观察矩阵
- glBindTexture(GL_TEXTURE_2D, m_Texture);
- glTranslatef(1.1f*float(cos(m_Rot/16.0f)),
- 0.8f*float(sin(m_Rot/20.0f)), m_Deep); //物体移动及控制大小
- glRotatef(m_Rot, 1.0f, 0.0f, 0.0f); //绕x轴旋转
- glRotatef(m_Rot*1.2f, 0.0f, 1.0f, 0.0f); //绕y轴旋转
- glRotatef(m_Rot*1.4f, 0.0f, 0.0f, 1.0f); //绕z轴旋转
- //输出文字到屏幕上
- glPrint("N");
- m_Rot += 0.1f; //旋转变量增加
- }
首先是绑定我们已经生产的纹理,接着由于我们这次纹理不需要融合颜色,于是去掉了选择颜色的代码。然后是物体移动和旋转的代码,我也不解释了,这只是其中一种变换方式,使得能产生动画,大家完全可以自己设计平移和旋转部分的代码(如加上键盘控制等)。然后就需要来输出我们的“海盗旗”了,如果你不知道我是如何从字母“N”中得到海盗旗符号的,那就打开写字板,在字体出选择Wingdings字体。输入大写字母“N”,就会显示出海盗旗符号了。
现在可以运行程序查看效果了!
第16课:看起来很酷的雾 (参照NeHe)
这次教程中,我们将在第07课代码的基础上,为木箱的四周填上雾效果。我们将会学习三种不同的雾模式,以及怎么设置雾的颜色和雾的范围。虽然这次教程非常简单,但我们得到的雾效果确实很棒!希望大家能喜欢,当然你也可以把雾效果加到任何一个OpenGL程序中,我相信总能檫出美丽的火花!
程序运行时效果如下:
下面进入教程:
我们这次将在第07课的基础上修改代码,我只会讲解有修改的部分,希望大家先找到第07课的代码再跟着我一步步走。首先打开myglwidget.h文件,将类声明更改如下:
- #ifndef MYGLWIDGET_H
- #define MYGLWIDGET_H
- #include <QWidget>
- #include <QGLWidget>
- class MyGLWidget : public QGLWidget
- {
- Q_OBJECT
- public:
- explicit MyGLWidget(QWidget *parent = 0);
- ~MyGLWidget();
- protected:
- //对3个纯虚函数的重定义
- void initializeGL();
- void resizeGL(int w, int h);
- void paintGL();
- void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
- private:
- bool fullscreen; //是否全屏显示
- QString m_FileName; //图片的路径及文件名
- GLuint m_Texture; //储存一个纹理
- bool m_Light; //光源的开/关
- GLuint m_Fog; //雾的模式
- GLfloat m_xRot; //x旋转角度
- GLfloat m_yRot; //y旋转角度
- GLfloat m_xSpeed; //x旋转速度
- GLfloat m_ySpeed; //y旋转速度
- GLfloat m_Deep; //深入屏幕的距离
- };
- #endif // MYGLWIDGET_H
我们只是增加了一个变量m_Fog来储存当前雾的模式(我们会使用三种雾模式),方便我们后面利用键盘来控制雾模式的切换。
接下来,我们需要打开myglwidget.cpp,在构造函数中初始化新增变量,具体代码如下:
- MyGLWidget::MyGLWidget(QWidget *parent) :
- QGLWidget(parent)
- {
- fullscreen = false;
- m_FileName = "D:/QtOpenGL/QtImage/Crate.bmp"; //应根据实际存放图片的路径进行修改
- m_Light = false;
- m_Fog = 0;
- m_xRot = 0.0f;
- m_yRot = 0.0f;
- m_xSpeed = 0.0f;
- m_ySpeed = 0.0f;
- m_Deep = -5.0f;
- QTimer *timer = new QTimer(this); //创建一个定时器
- //将定时器的计时信号与updateGL()绑定
- connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
- timer->start(10); //以10ms为一个计时周期
- }
我们给m_Fog赋初始值0,表示第一种雾模式(具体是哪一种下面会讲到)。
然后我们需要来修改initializeGL()函数,雾效果的数据初始化都这里完成的,具体代码如下:
- void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置
- {
- m_Texture = bindTexture(QPixmap(m_FileName)); //载入位图并转换成纹理
- glEnable(GL_TEXTURE_2D); //启用纹理映射
- glClearColor(0.5f, 0.5f, 0.5f, 1.0f); //设置背景的颜色为雾气的颜色
- glShadeModel(GL_SMOOTH); //启用阴影平滑
- glClearDepth(1.0); //设置深度缓存
- glEnable(GL_DEPTH_TEST); //启用深度测试
- glDepthFunc(GL_LEQUAL); //所作深度测试的类型
- glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告诉系统对透视进行修正
- //光源部分
- GLfloat LightAmbient[] = {0.5f, 0.5f, 0.5f, 1.0f}; //环境光参数
- GLfloat LightDiffuse[] = {1.0f, 1.0f, 1.0f, 1.0f}; //漫散光参数
- GLfloat LightPosition[] = {0.0f, 0.0f, 2.0f, 1.0f}; //光源位置
- glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient); //设置环境光
- glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse); //设置漫射光
- glLightfv(GL_LIGHT1, GL_POSITION, LightPosition); //设置光源位置
- glEnable(GL_LIGHT1); //启动一号光源
- //雾部分
- GLfloat fogColor[] = {0.5f, 0.5f, 0.5f, 1.0f}; //雾的颜色
- glFogi(GL_FOG_MODE, GL_EXP); //设置雾气的初始模式
- glFogfv(GL_FOG_COLOR, fogColor); //设置雾的颜色
- glFogf(GL_FOG_DENSITY, 0.35); //设置雾的密度
- glHint(GL_FOG_HINT, GL_DONT_CARE); //设置系统如何计算雾气
- glFogf(GL_FOG_START, 1.0f); //雾的开始位置
- glFogf(GL_FOG_END, 5.0f); //雾的结束位置
- glEnable(GL_FOG); //启动雾效果
- }
首先我们改一下glClearColor()函数的参数,让清除屏幕的颜色与下面雾的颜色相同。我们在函数末尾加上了我们的雾效果代码,首先我们定义雾的颜色(我们定为白色雾,你完全可以根据自己的喜好修改),接着我们设置了雾气的初始模式为GL_EXP,这是m_Fog等于0时对应的模式,先别急着问为什么,下面会告诉你答案。
然后我们设置雾的密度,glFogf()函数的第二个参数越大雾会越浓,越小雾会越稀。glHint()函数用于设置修正,我们使用了GL_DONT_CARE因为我们不关心它的值。再接下去两行设置了雾的起始位置和结束位置,1.0f和5.0f均表示离屏幕的距离,我们完全可以自己根据需要修改这两个值。最后我们应用glEnable()启用了雾效果,注意没有这行是无法产生无效果的。
最后是关于键盘控制函数的修改,我们将利用它来控制雾模式的切换,具体代码如下:
- void MyGLWidget::keyPressEvent(QKeyEvent *event)
- {
- static GLuint fogMode[] = {GL_EXP, GL_EXP2, GL_LINEAR};
- switch (event->key())
- {
- case Qt::Key_F1: //F1为全屏和普通屏的切换键
- fullscreen = !fullscreen;
- if (fullscreen)
- {
- showFullScreen();
- }
- else
- {
- showNormal();
- }
- break;
- case Qt::Key_Escape: //ESC为退出键
- close();
- break;
- case Qt::Key_L: //L为开启关闭光源的切换键
- m_Light = !m_Light;
- if (m_Light)
- {
- glEnable(GL_LIGHTING); //开启光源
- }
- else
- {
- glDisable(GL_LIGHTING); //关闭光源
- }
- break;
- case Qt::Key_G: //G为雾模式的切换键
- m_Fog++;
- if (m_Fog == 3)
- {
- m_Fog = 0;
- }
- glFogi(GL_FOG_MODE, fogMode[m_Fog]);
- break;
- case Qt::Key_PageUp: //PageUp按下使木箱移向屏幕内部
- m_Deep -= 0.1f;
- break;
- case Qt::Key_PageDown: //PageDown按下使木箱移向观察者
- m_Deep += 0.1f;
- break;
- case Qt::Key_Up: //Up按下减少m_xSpeed
- m_xSpeed -= 0.1f;
- break;
- case Qt::Key_Down: //Down按下增加m_xSpeed
- m_xSpeed += 0.1f;
- break;
- case Qt::Key_Right: //Right按下减少m_ySpeed
- m_ySpeed -= 0.1f;
- break;
- case Qt::Key_Left: //Left按下增加m_ySpeed
- m_ySpeed += 0.1f;
- break;
- }
- }
注意到我们定义了一个静态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文件,将类声明更改如下:
- #ifndef MYGLWIDGET_H
- #define MYGLWIDGET_H
- #include <QWidget>
- #include <QGLWidget>
- class MyGLWidget : public QGLWidget
- {
- Q_OBJECT
- public:
- explicit MyGLWidget(QWidget *parent = 0);
- ~MyGLWidget();
- protected:
- //对3个纯虚函数的重定义
- void initializeGL();
- void resizeGL(int w, int h);
- void paintGL();
- void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
- private:
- void buildFont(); //创建字体
- void killFont(); //删除显示列表
- //输出字符串
- void glPrint(GLuint x, GLuint y, const char *string, int set);
- private:
- bool fullscreen; //是否全屏显示
- GLuint m_Base; //储存绘制字体的显示列表的开始位置
- GLfloat m_Cnt1; //字体移动计数器1
- GLfloat m_Cnt2; //字体移动计数器2
- QString m_FileName[2]; //图片的路径及文件名
- GLuint m_Texture[2]; //储存两个纹理
- };
- #endif // MYGLWIDGET_H
我们增加了变量m_Base、m_Cnt1、m_Cnt2,函数声明buildFont()、killFont(),这些和之前都讲过的作用都一样就不重复了。而m_FileName和m_Texture变为了长度为2的数组,这是因为程序中我们会用两种不同的图来建立两个不同的纹理。最后是glPrint()函数的声明,注意下它的参数和前几课不同,但作用是相同的,具体的下面会讲到。
接下来,我们需要打开myglwidget.cpp,加入声明#include <QTimer>、#include<QtMath>,将构造函数和析构函数修改如下(比较简单不具体解释了):
- MyGLWidget::MyGLWidget(QWidget *parent) :
- QGLWidget(parent)
- {
- fullscreen = false;
- m_Cnt1 = 0.0f;
- m_Cnt2 = 0.0f;
- m_FileName[0] = "D:/QtOpenGL/QtImage/Font.bmp"; //应根据实际存放图片的路径进行修改
- m_FileName[1] = "D:/QtOpenGL/QtImage/Bumps.bmp";
- QTimer *timer = new QTimer(this); //创建一个定时器
- //将定时器的计时信号与updateGL()绑定
- connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
- timer->start(10); //以10ms为一个计时周期
- }
- MyGLWidget::~MyGLWidget()
- {
- killFont(); //删除显示列表
- }
继续,我们要来定义我们增加的三个函数,同样是重头戏,具体代码如下:
- void MyGLWidget::buildFont() //创建位图字体
- {
- float cx, cy; //储存字符的x、y坐标
- m_Base = glGenLists(256); //创建256个显示列表
- glBindTexture(GL_TEXTURE_2D, m_Texture[0]); //选择字符纹理
- for (int i=0; i<256; i++) //循环256个显示列表
- {
- cx = float(i%16)/16.0f; //当前字符的x坐标
- cy = float(i/16)/16.0f; //当前字符的y坐标
- glNewList(m_Base+i, GL_COMPILE); //开始创建显示列表
- glBegin(GL_QUADS); //使用四边形显示每一个字符
- glTexCoord2f(cx, 1-cy-0.0625f);
- glVertex2i(0, 0);
- glTexCoord2f(cx+0.0625f, 1-cy-0.0625f);
- glVertex2i(16, 0);
- glTexCoord2f(cx+0.0625f, 1-cy);
- glVertex2i(16, 16);
- glTexCoord2f(cx, 1-cy);
- glVertex2i(0, 16);
- glEnd(); //四边形字符绘制完成
- glTranslated(10, 0, 0); //绘制完一个字符,向右平移10个单位
- glEndList(); //字符显示列表完成
- }
- }
- void MyGLWidget::killFont() //删除显示列表
- {
- glDeleteLists(m_Base, 256); //删除256个显示列表
- }
- void MyGLWidget::glPrint(GLuint x, GLuint y, //输入字符串
- const char *string, int set)
- {
- if (set > 1) //如果字符集大于1
- {
- set = 1; //设置其为1
- }
- glBindTexture(GL_TEXTURE_2D, m_Texture[0]); //绑定为字体纹理
- glDisable(GL_DEPTH_TEST); //禁止深度测试
- glMatrixMode(GL_PROJECTION); //选择投影矩阵
- glPushMatrix(); //保存当前的投影矩阵
- glLoadIdentity(); //重置投影矩阵
- glOrtho(0, 640, 0, 480, -1, 1); //设置正投影的可视区域
- glMatrixMode(GL_MODELVIEW); //选择模型观察矩阵
- glPushMatrix(); //保存当前的模型观察矩阵
- glLoadIdentity(); //重置模型观察矩阵
- glTranslated(x, y ,0); //把字符原点移动到(x,y)位置
- glListBase(m_Base-32+(128*set)); //选择字符集
- glCallLists(strlen(string), GL_BYTE, string); //把字符串写到屏幕
- glMatrixMode(GL_PROJECTION); //选择投影矩阵
- glPopMatrix(); //设置为保存的矩阵
- glMatrixMode(GL_MODELVIEW); //选择模型观察矩阵
- glPopMatrix(); //设置为保存
- glEnable(GL_DEPTH_TEST); //启用深度测试
- }
首先是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()函数,具体代码如下:
- void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置
- {
- m_Texture[0] = bindTexture(QPixmap(m_FileName[0])); //载入位图并转换成纹理
- m_Texture[1] = bindTexture(QPixmap(m_FileName[1]));
- glEnable(GL_TEXTURE_2D); //启用纹理映射
- glClearColor(0.0, 0.0, 0.0, 0.0); //黑色背景
- glShadeModel(GL_SMOOTH); //启用阴影平滑
- glClearDepth(1.0); //设置深度缓存
- glEnable(GL_DEPTH_TEST); //启用深度测试
- glDepthFunc(GL_LEQUAL); //所作深度测试的类型
- glBlendFunc(GL_SRC_ALPHA, GL_ONE); //设置混合因子
- glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告诉系统对透视进行修正
- buildFont(); //创建字体
- }
最开始三行载入位图转换纹理,启用纹理映射就不解释了。中间部分有小的改动,由于我们要给字体上色,所以要设置混合因子。最后调用buildFont()创建字体。
最后,我们该进入paintGL()函数,这次难度还行,具体代码如下:
- void MyGLWidget::paintGL() //从这里开始进行所以的绘制
- {
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
- glLoadIdentity(); //重置当前的模型观察矩阵
- glBindTexture(GL_TEXTURE_2D, m_Texture[1]); //设置为图像纹理
- glTranslatef(0.0f, 0.0f, -5.0f); //移入屏幕5.0单位
- glRotatef(45.0f, 0.0f, 0.0f, 1.0f); //绕z轴旋转45度
- glRotatef(m_Cnt1*30.0f, 1.0f, 1.0f, 0.0f); //绕(1,1,0)轴旋转
- glDisable(GL_BLEND); //关闭融合
- glColor3f(1.0f, 1.0f, 1.0f); //设置颜色为白色
- glBegin(GL_QUADS); //绘制纹理四边形
- glTexCoord2d(0.0f, 0.0f);
- glVertex2f(-1.0f, 1.0f);
- glTexCoord2d(1.0f, 0.0f);
- glVertex2f(1.0f, 1.0f);
- glTexCoord2d(1.0f, 1.0f);
- glVertex2f(1.0f, -1.0f);
- glTexCoord2d(0.0f, 1.0f);
- glVertex2f(-1.0f, -1.0f);
- glEnd();
- glRotatef(90.0f, 1.0f, 1.0f, 0.0); //绕(1,1,0)轴旋转90度
- glBegin(GL_QUADS); //绘制第二个四边形,与第一个垂直
- glTexCoord2d(0.0f, 0.0f);
- glVertex2f(-1.0f, 1.0f);
- glTexCoord2d(1.0f, 0.0f);
- glVertex2f(1.0f, 1.0f);
- glTexCoord2d(1.0f, 1.0f);
- glVertex2f(1.0f, -1.0f);
- glTexCoord2d(0.0f, 1.0f);
- glVertex2f(-1.0f, -1.0f);
- glEnd();
- glEnable(GL_BLEND); //启用混合
- glLoadIdentity(); //重置视口
- //根据字体位置设置颜色
- glColor3f(1.0f*float(cos(m_Cnt1)), 1.0*float(sin(m_Cnt2)),
- 1.0f-0.5f*float(cos(m_Cnt1+m_Cnt2)));
- glPrint(int((280+250*cos(m_Cnt1))),
- int(235+200*sin(m_Cnt2)), "NeHe", 0);
- glColor3f(1.0*float(sin(m_Cnt2)), 1.0f-0.5f*float(cos(m_Cnt1+m_Cnt2)),
- 1.0f*float(cos(m_Cnt1)));
- glPrint(int((280+230*cos(m_Cnt2))),
- int(235+200*sin(m_Cnt1)), "OpenGL", 1);
- glColor3f(0.0f, 0.0f, 1.0f);
- glPrint(int(240+200*cos((m_Cnt1+m_Cnt2)/5)), 2,
- "Giuseppe D'Agata", 0);
- glColor3f(1.0f, 1.0f, 1.0f);
- glPrint(int(242+200*cos((m_Cnt1+m_Cnt2)/5)), 2,
- "Giuseppe D'Agata", 0);
- m_Cnt1 += 0.01f; //增加两个计数器的值
- m_Cnt2 += 0.0081f;
- }
函数中我们先绘制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文件,将类声明更改如下:
- #ifndef MYGLWIDGET_H
- #define MYGLWIDGET_H
- #include <QWidget>
- #include <QGLWidget>
- class GLUquadric;
- class MyGLWidget : public QGLWidget
- {
- Q_OBJECT
- public:
- explicit MyGLWidget(QWidget *parent = 0);
- ~MyGLWidget();
- protected:
- //对3个纯虚函数的重定义
- void initializeGL();
- void resizeGL(int w, int h);
- void paintGL();
- void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
- private:
- void glDrawCube(); //绘制立方体
- private:
- bool fullscreen; //是否全屏显示
- QString m_FileName; //图片的路径及文件名
- GLuint m_Texture; //储存一个纹理
- bool m_Light; //光源的开/关
- GLfloat m_xRot; //x旋转角度
- GLfloat m_yRot; //y旋转角度
- GLfloat m_xSpeed; //x旋转速度
- GLfloat m_ySpeed; //y旋转速度
- GLfloat m_Deep; //深入屏幕的距离
- int m_Part1; //圆盘的起始角度
- int m_Part2; //圆盘的结束角度
- int m_P1; //增量1
- int m_P2; //增量2
- GLUquadric *m_Quadratic; //二次几何体
- GLuint m_Object; //绘制对象标示符
- };
- #endif // MYGLWIDGET_H
首先我们在类前面增加了GLUquadric的类声明。接着我们增加了6个变量,前4个变量用于控制绘制“部分圆盘”的,下面会解释。然后我们定义一个二次几何体对象指针和一个GLuint变量,二次几何体就不解释了,m_Object是配合键盘控制来完成图形之间切换的。最后我们增加了一个函数声明glDrawCube(),这个函数是用来绘制立方体的。
接下来,我们需要打开myglwidget.cpp,在构造函数中初始化新增变量(除了m_Quadratic)并修改析构函数(删除掉创建的二次几何体),很简单不多解释,具体代码如下:
- MyGLWidget::MyGLWidget(QWidget *parent) :
- QGLWidget(parent)
- {
- fullscreen = false;
- m_FileName = "D:/QtOpenGL/QtImage/Wall1.bmp"; //应根据实际存放图片的路径进行修改
- m_Light = false;
- m_xRot = 0.0f;
- m_yRot = 0.0f;
- m_xSpeed = 0.0f;
- m_ySpeed = 0.0f;
- m_Deep = -5.0f;
- m_Part1 = 0;
- m_Part2 = 0;
- m_P1 = 0;
- m_P2 = 1;
- m_Object = 0;
- QTimer *timer = new QTimer(this); //创建一个定时器
- //将定时器的计时信号与updateGL()绑定
- connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
- timer->start(10); //以10ms为一个计时周期
- }
- MyGLWidget::~MyGLWidget()
- {
- gluDeleteQuadric(m_Quadratic);
- }
继续,我们需要定义我们新增的glDrawCube()函数了,其实就是画一个纹理立方体,完全可以从第07课的paintGL()函数中复制过来,不再多作解释,代码如下:
- void MyGLWidget::glDrawCube()
- {
- glBegin(GL_QUADS); //开始绘制立方体
- glNormal3f(0.0f, 1.0f, 0.0f);
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, -1.0f); //右上(顶面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, -1.0f); //左上(顶面)
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(-1.0f, 1.0f, 1.0f); //左下(顶面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(1.0f, 1.0f, 1.0f); //右下(顶面)
- glNormal3f(0.0f, -1.0f, 0.0f);
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, 1.0f); //右上(底面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, 1.0f); //左上(底面)
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(-1.0f, -1.0f, -1.0f); //左下(底面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(1.0f, -1.0f, -1.0f); //右下(底面)
- glNormal3f(0.0f, 0.0f, 1.0f);
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, 1.0f); //右上(前面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, 1.0f); //左上(前面)
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, 1.0f); //左下(前面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, 1.0f); //右下(前面)
- glNormal3f(0.0f, 0.0f, -1.0f);
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, -1.0f); //右上(后面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, -1.0f); //左上(后面)
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, -1.0f); //左下(后面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, -1.0f); //右下(后面)
- glNormal3f(-1.0f, 0.0f, 0.0f);
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, 1.0f); //右上(左面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(-1.0f, 1.0f, -1.0f); //左上(左面)
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, -1.0f); //左下(左面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(-1.0f, -1.0f, 1.0f); //右下(左面)
- glNormal3f(1.0f, 0.0f, 0.0f);
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, -1.0f); //右上(右面)
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(1.0f, 1.0f, 1.0f); //左上(右面)
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, 1.0f); //左下(右面)
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(1.0f, -1.0f, -1.0f); //右下(右面)
- glEnd(); //立方体绘制结束
- }
然后我们需要修改一下initializeGL()函数,在其中完成对m_Quadratic的初始化,具体代码如下:
- void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置
- {
- m_Texture = bindTexture(QPixmap(m_FileName)); //载入位图并转换成纹理
- glEnable(GL_TEXTURE_2D); //启用纹理映射
- glClearColor(0.0f, 0.0f, 0.0f, 0.0f); //黑色背景
- glShadeModel(GL_SMOOTH); //启用阴影平滑
- glClearDepth(1.0); //设置深度缓存
- glEnable(GL_DEPTH_TEST); //启用深度测试
- glDepthFunc(GL_LEQUAL); //所作深度测试的类型
- glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告诉系统对透视进行修正
- m_Quadratic = gluNewQuadric(); //创建二次几何体
- gluQuadricNormals(m_Quadratic, GLU_SMOOTH); //使用平滑法线
- gluQuadricTexture(m_Quadratic, GL_TRUE); //使用纹理
- //光源部分
- GLfloat LightAmbient[] = {0.5f, 0.5f, 0.5f, 1.0f}; //环境光参数
- GLfloat LightDiffuse[] = {1.0f, 1.0f, 1.0f, 1.0f}; //漫散光参数
- GLfloat LightPosition[] = {0.0f, 0.0f, 2.0f, 1.0f}; //光源位置
- glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient); //设置环境光
- glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse); //设置漫射光
- glLightfv(GL_LIGHT1, GL_POSITION, LightPosition); //设置光源位置
- glEnable(GL_LIGHT1); //启动一号光源
- }
注意到我们增加了三行代码,首先调用gluNewQuadric()创建了一个二次几何体对象,并让m_Quadratic指向这个二次几何体。然后第二行代码将在二次曲面的表面创建平滑的法向量,这样当灯光照上去的时候将会好看些。最后我们使在二次曲面表面的纹理映射有效。
还有就是paintGL()函数了,最近几课,我们通过分过程渐渐让paintGL()函数看起来趋于简化,具体代码如下:
- void MyGLWidget::paintGL() //从这里开始进行所以的绘制
- {
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
- glLoadIdentity(); //重置模型观察矩阵
- glTranslatef(0.0f, 0.0f, m_Deep); //移入屏幕5.0单位
- glRotatef(m_xRot, 1.0f, 0.0f, 0.0f); //绕x轴旋转
- glRotatef(m_yRot, 0.0f, 1.0f, 0.0f); //绕y轴旋转
- glBindTexture(GL_TEXTURE_2D, m_Texture); //选择纹理
- switch(m_Object)
- {
- case 0: //绘制立方体
- glDrawCube();
- break;
- case 1: //绘制圆柱体
- glTranslatef(0.0f, 0.0f, -1.5f);
- gluCylinder(m_Quadratic, 1.0f, 1.0f, 3.0f, 64, 64);
- break;
- case 2: //绘制圆盘
- gluDisk(m_Quadratic, 0.5f, 1.5f, 64, 64);
- break;
- case 3: //绘制球
- gluSphere(m_Quadratic, 1.3f, 64, 64);
- break;
- case 4: //绘制圆锥
- glTranslatef(0.0f, 0.0f, -1.5f);
- gluCylinder(m_Quadratic, 1.0f, 0.0f, 3.0f, 64, 64);
- break;
- case 5: //绘制部分圆盘
- m_Part1 += m_P1;
- m_Part2 += m_P2;
- if (m_Part1 > 359)
- {
- m_P1 = 0;
- m_Part1 = 0;
- m_P2 = 1;
- m_Part2 = 0;
- }
- if (m_Part2 > 359)
- {
- m_P1 = 1;
- m_P2 = 0;
- }
- gluPartialDisk(m_Quadratic, 0.5f, 1.5f, 64, 64, m_Part1, m_Part2-m_Part1);
- break;
- }
- m_xRot += m_xSpeed; //x轴旋转
- m_yRot += m_ySpeed; //y轴旋转
- }
我们将原来的绘制立方体部分的代码换成了一个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度,我们将开始增加开始角度,这样盘子看起来就像是被逐渐地抹去一样,我们将重复这两个过程。
最后我们修改一下键盘控制函数,不多解释了,具体代码如下:
- void MyGLWidget::keyPressEvent(QKeyEvent *event)
- {
- switch (event->key())
- {
- case Qt::Key_F1: //F1为全屏和普通屏的切换键
- fullscreen = !fullscreen;
- if (fullscreen)
- {
- showFullScreen();
- }
- else
- {
- showNormal();
- }
- break;
- case Qt::Key_Escape: //ESC为退出键
- close();
- break;
- case Qt::Key_L: //L为开启关闭光源的切换键
- m_Light = !m_Light;
- if (m_Light)
- {
- glEnable(GL_LIGHTING); //开启光源
- }
- else
- {
- glDisable(GL_LIGHTING); //关闭光源
- }
- break;
- case Qt::Key_Space: //空格为物体的切换键
- m_Object++;
- if (m_Object == 6)
- {
- m_Object = 0;
- }
- break;
- case Qt::Key_PageUp: //PageUp按下使木箱移向屏幕内部
- m_Deep -= 0.1f;
- break;
- case Qt::Key_PageDown: //PageDown按下使木箱移向观察者
- m_Deep += 0.1f;
- break;
- case Qt::Key_Up: //Up按下减少m_xSpeed
- m_xSpeed -= 0.1f;
- break;
- case Qt::Key_Down: //Down按下增加m_xSpeed
- m_xSpeed += 0.1f;
- break;
- case Qt::Key_Right: //Right按下减少m_ySpeed
- m_ySpeed -= 0.1f;
- break;
- case Qt::Key_Left: //Left按下增加m_ySpeed
- m_ySpeed += 0.1f;
- break;
- }
- }
现在就可以运行程序查看效果了!
第19课:粒子系统 (参照NeHe)
这次教程中,我们将创建一个简单的粒子系统,并用它来创建一种喷射效果。利用粒子系统,我们可以实现爆炸、喷泉、流星之类的效果,听起来是不是很棒呢!
我们还会讲到一个新东西,三角形带(我的理解就是画很多三角形来组合成我们要的形状),它非常容易使用,而且当需要画很多三角形的时候,它能加快你程序的运行速度。这次教程中,我将教你该如何做一个简单的微粒程序,一旦你了解微粒程序的原理后,再创建例如:火、烟、喷泉等效果将是很轻松的事情。
程序运行时效果如下:
下面进入教程:
我们这次将在第06课代码的基础上修改代码,这次需要修改的代码量不少,希望大家耐心跟着我一步步来完成这个程序。首先打开myglwidget.h文件,将类声明更改如下:
- #ifndef MYGLWIDGET_H
- #define MYGLWIDGET_H
- #include <QWidget>
- #include <QGLWidget>
- class MyGLWidget : public QGLWidget
- {
- Q_OBJECT
- public:
- explicit MyGLWidget(QWidget *parent = 0);
- ~MyGLWidget();
- protected:
- //对3个纯虚函数的重定义
- void initializeGL();
- void resizeGL(int w, int h);
- void paintGL();
- void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
- private:
- bool fullscreen; //是否全屏显示
- QString m_FileName; //图片的路径及文件名
- GLuint m_Texture; //储存一个纹理
- static const int MAX_PARTICLES = 1000; //最大粒子数
- static const GLfloat COLORS[12][3]; //彩虹的颜色
- bool m_Rainbow; //是否为彩虹模式
- GLuint m_Color; //当前的颜色
- float m_Slowdown; //减速粒子
- float m_xSpeed; //x方向的速度
- float m_ySpeed; //y方向的速度
- float m_Deep; //移入屏幕的距离
- struct Particle //创建粒子结构体
- {
- bool active; //是否激活
- float life; //粒子生命
- float fade; //衰减速度
- float r, g, b; //粒子颜色
- float x, y, z; //位置坐标
- float xi, yi, zi; //各方向速度
- float xg, yg, zg; //各方向加速度
- } m_Particles[MAX_PARTICLES]; //存放1000个粒子的数组
- };
- #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,在构造函数中对新增变量进行初始化,具体代码如下:
- const GLfloat MyGLWidget::COLORS[][3] = //彩虹的颜色
- {
- {1.0f, 0.5f, 0.5f}, {1.0f, 0.75f, 0.5f}, {1.0f, 1.0f, 0.5f},
- {0.75f, 1.0f, 0.5f}, {0.5f, 1.0f, 0.5f}, {0.5f, 1.0f, 0.75f},
- {0.5f, 1.0f, 1.0f}, {0.5f, 0.75f, 1.0f}, {0.5f, 0.5f, 1.0f},
- {0.75f, 0.5f, 1.0f}, {1.0f, 0.5f, 1.0f}, {1.0f, 0.5f, 0.75f}
- };
- MyGLWidget::MyGLWidget(QWidget *parent) :
- QGLWidget(parent)
- {
- fullscreen = false;
- m_FileName = "D:/QtOpenGL/QtImage/Particle.bmp"; //应根据实际存放图片的路径进行修改
- m_Rainbow = true;
- m_Color = 0;
- m_Slowdown = 2.0f;
- m_xSpeed = 0.0f;
- m_ySpeed = 0.0f;
- m_Deep = -40.0f;
- for (int i=0; i<MAX_PARTICLES; i++) //循环初始化所以粒子
- {
- m_Particles[i].active = true; //使所有粒子为激活状态
- m_Particles[i].life = 1.0f; //所有粒子生命值为最大
- //随机生成衰减速率
- m_Particles[i].fade = float(rand()%100)/1000.0f+0.001;
- //粒子的颜色
- m_Particles[i].r = COLORS[int(i*(12.0f/MAX_PARTICLES))][0];
- m_Particles[i].g = COLORS[int(i*(12.0f/MAX_PARTICLES))][1];
- m_Particles[i].b = COLORS[int(i*(12.0f/MAX_PARTICLES))][2];
- //粒子的初始位置
- m_Particles[i].x = 0.0f;
- m_Particles[i].y = 0.0f;
- m_Particles[i].z = 0.0f;
- //随机生成x、y、z轴方向速度
- m_Particles[i].xi = float((rand()%50)-26.0f)*10.0f;
- m_Particles[i].yi = float((rand()%50)-25.0f)*10.0f;
- m_Particles[i].zi = float((rand()%50)-25.0f)*10.0f;
- m_Particles[i].xg = 0.0f; //设置x方向加速度为0
- m_Particles[i].yg = -0.8f; //设置y方向加速度为-0.8
- m_Particles[i].zg = 0.0f; //设置z方向加速度为0
- }
- QTimer *timer = new QTimer(this); //创建一个定时器
- //将定时器的计时信号与updateGL()绑定
- connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
- timer->start(10); //以10ms为一个计时周期
- }
注意到我们在构造函数之前对定义的静态常量数组COLORS进行初始化,一共包含12种渐变颜色,从红色到紫罗兰。进入构造函数一开始是更换纹理图片以及增加变量的初始化,这些没什么好解释的,下面我们重点看循环部分。我们利用循环来初始化每个粒子,我们让粒子变活跃(不活跃的粒子在屏幕上是不会显示的)之后,我们给它lfie。life满值是1.0f,这也给粒子完整的光亮。值得一提,把粒子的生命衰退和颜色渐暗绑到一起,效果真的很不错!
我们通过随机数来设置粒子退色的快慢,我们取0~99的随机数,然后平分1000份来得到一个很小的浮点数,最后结果加上0.001f来使fade速度值不为0。我们既然给了粒子生命,我们当然要给它其他的属性状态附上值,为了使粒子有不同的颜色,我们用i 变量乘以数组中颜色的数目(12)与MAX_PARTICLES的商,再转换成整数,利用得到的整数取对应的颜色就可以了。然后让粒子从(0, 0, 0)出发,在设定速度时,我们通过将结果乘上10.0f来创造开始时的爆炸效果,加速度就由我们统一指定初始值了。
然后,我们来略微修改initializeGL()函数,代码如下:
- void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置
- {
- m_Texture = bindTexture(QPixmap(m_FileName)); //载入位图并转换成纹理
- glEnable(GL_TEXTURE_2D); //启用纹理映射
- glClearColor(0.0f, 0.0f, 0.0f, 0.0f); //黑色背景
- glShadeModel(GL_SMOOTH); //启用阴影平滑
- glClearDepth(1.0); //设置深度缓存
- glDisable(GL_DEPTH_TEST); //禁止深度测试
- glEnable(GL_BLEND); //启用融合
- glBlendFunc(GL_SRC_ALPHA, GL_ONE); //设置融合因子
- glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告诉系统对透视进行修正
- glHint(GL_POINT_SMOOTH_HINT, GL_NICEST);
- }
我们在中间启用了融合并设置了融合因子,这是为了我们的粒子能有不同颜色。然后我们禁用了深度测试,因为如果启用深度测试的话,纹理之间会出现覆盖现象,那样画面简直一团糟。
还有,我们要进入有趣的paintGL()函数了,具体代码如下:
- void MyGLWidget::paintGL() //从这里开始进行所以的绘制
- {
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
- glLoadIdentity(); //重置模型观察矩阵
- glBindTexture(GL_TEXTURE_2D, m_Texture);
- for (int i=0; i<MAX_PARTICLES; i++) //循环所以的粒子
- {
- if (m_Particles[i].active) //如果粒子为激活的
- {
- float x = m_Particles[i].x; //x轴位置
- float y = m_Particles[i].y; //y轴位置
- float z = m_Particles[i].z + m_Deep; //z轴位置
- //设置粒子颜色
- glColor4f(m_Particles[i].r, m_Particles[i].g,
- m_Particles[i].b, m_Particles[i].life);
- glBegin(GL_TRIANGLE_STRIP); //绘制三角形带
- glTexCoord2d(1, 1);glVertex3f(x+0.5f, y+0.5f, z);
- glTexCoord2d(0, 1);glVertex3f(x-0.5f, y+0.5f, z);
- glTexCoord2d(1, 0);glVertex3f(x+0.5f, y-0.5f, z);
- glTexCoord2d(0, 0);glVertex3f(x-0.5f, y-0.5f, z);
- glEnd();
- //更新各方向坐标及速度
- m_Particles[i].x += m_Particles[i].xi/(m_Slowdown*1000);
- m_Particles[i].y += m_Particles[i].yi/(m_Slowdown*1000);
- m_Particles[i].z += m_Particles[i].zi/(m_Slowdown*1000);
- m_Particles[i].xi += m_Particles[i].xg;
- m_Particles[i].yi += m_Particles[i].yg;
- m_Particles[i].zi += m_Particles[i].zg;
- m_Particles[i].life -= m_Particles[i].fade; //减少粒子的生命值
- if (m_Particles[i].life < 0.0f) //如果粒子生命值小于0
- {
- m_Particles[i].life = 1.0f; //产生一个新粒子
- m_Particles[i].fade = float(rand()%100)/1000.0f+0.003f;
- m_Particles[i].r = colors[m_Color][0]; //设置颜色
- m_Particles[i].g = colors[m_Color][1];
- m_Particles[i].b = colors[m_Color][2];
- m_Particles[i].x = 0.0f; //粒子出现在屏幕*
- m_Particles[i].y = 0.0f;
- m_Particles[i].z = 0.0f;
- //随机生成粒子速度
- m_Particles[i].xi = m_xSpeed + float((rand()%60)-32.0f);
- m_Particles[i].yi = m_ySpeed + float((rand()%60)-30.0f);
- m_Particles[i].zi = float((rand()%60)-30.0f);
- }
- }
- }
- if (m_Rainbow) //如果为彩虹模式
- {
- m_Color++; //进行颜色的变换
- if (m_Color > 11)
- {
- m_Color = 0;
- }
- }
- }
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,这个控制了粒子大体得移动方向。最后我们给粒子分配当前的颜色就搞定循环了。
函数最后,我们判断是否为彩虹模式,如果是就改变当前的颜色,这样不同时间“重生”后的粒子就可能得到不同的颜色,从而出现彩虹效果。
最后就是键盘控制了,由于为了增加点趣味性,这次键盘控制比较“麻烦”,但是调理很清晰,具体代码如下:
- void MyGLWidget::keyPressEvent(QKeyEvent *event)
- {
- switch (event->key())
- {
- case Qt::Key_F1: //F1为全屏和普通屏的切换键
- fullscreen = !fullscreen;
- if (fullscreen)
- {
- showFullScreen();
- }
- else
- {
- showNormal();
- }
- updateGL();
- break;
- case Qt::Key_Escape: //ESC为退出键
- close();
- break;
- case Qt::Key_Tab: //Tab按下使粒子回到原点,产生爆炸
- for (int i=0; i<MAX_PARTICLES; i++)
- {
- m_Particles[i].x = 0.0f;
- m_Particles[i].y = 0.0f;
- m_Particles[i].z = 0.0f;
- //随机生成速度
- m_Particles[i].xi = float((rand()%50)-26.0f)*10.0f;
- m_Particles[i].yi = float((rand()%50)-25.0f)*10.0f;
- m_Particles[i].zi = float((rand()%50)-25.0f)*10.0f;
- }
- break;
- case Qt::Key_8: //按下8增加y方向加速度
- for (int i=0; i<MAX_PARTICLES; i++)
- {
- if (m_Particles[i].yg < 3.0f)
- {
- m_Particles[i].yg += 0.05f;
- }
- }
- break;
- case Qt::Key_2: //按下2减少y方向加速度
- for (int i=0; i<MAX_PARTICLES; i++)
- {
- if (m_Particles[i].yg > -3.0f)
- {
- m_Particles[i].yg -= 0.05f;
- }
- }
- break;
- case Qt::Key_6: //按下6增加x方向加速度
- for (int i=0; i<MAX_PARTICLES; i++)
- {
- if (m_Particles[i].xg < 3.0f)
- {
- m_Particles[i].xg += 0.05f;
- }
- }
- break;
- case Qt::Key_4: //按下4减少x方向加速度
- for (int i=0; i<MAX_PARTICLES; i++)
- {
- if (m_Particles[i].xg > -3.0f)
- {
- m_Particles[i].xg -= 0.05f;
- }
- }
- break;
- case Qt::Key_Plus: //+ 号按下加速粒子
- if (m_Slowdown > 1.0f)
- {
- m_Slowdown -= 0.05f;
- }
- break;
- case Qt::Key_Minus: //- 号按下减速粒子
- if (m_Slowdown < 3.0f)
- {
- m_Slowdown += 0.05f;
- }
- break;
- case Qt::Key_PageUp: //PageUp按下使粒子靠近屏幕
- m_Deep += 0.5f;
- break;
- case Qt::Key_PageDown: //PageDown按下使粒子远离屏幕
- m_Deep -= 0.5f;
- break;
- case Qt::Key_Return: //回车键为是否彩虹模式的切换键
- m_Rainbow = !m_Rainbow;
- break;
- case Qt::Key_Space: //空格键为颜色切换键
- m_Rainbow = false;
- m_Color++;
- if (m_Color > 11)
- {
- m_Color = 0;
- }
- break;
- case Qt::Key_Up: //Up按下增加粒子y轴正方向的速度
- if (m_ySpeed < 400.0f)
- {
- m_ySpeed += 5.0f;
- }
- break;
- case Qt::Key_Down: //Down按下减少粒子y轴正方向的速度
- if (m_ySpeed > -400.0f)
- {
- m_ySpeed -= 5.0f;
- }
- break;
- case Qt::Key_Right: //Right按下增加粒子x轴正方向的速度
- if (m_xSpeed < 400.0f)
- {
- m_xSpeed += 5.0f;
- }
- break;
- case Qt::Key_Left: //Left按下减少粒子x轴正方向的速度
- if (m_xSpeed > -400.0f)
- {
- m_xSpeed -= 5.0f;
- }
- break;
- }
- }
我感觉注释已经写得比较清楚了,就不解释太多了,具体里面的值是怎么得到的,其实就是一点点尝试,感觉效果好久用了,就这么简单!大家注意一下Tab键按下后,全部粒子会回到原点,重新从原点出发,并且我们给它们重新生成速度,方式和初始化时是相同的,这样就又产生了爆炸效果。
现在就可以运行程序查看效果了!
第20课:蒙板 (参照NeHe)
这次教程中,我们教介绍OpenGL的蒙板技术。到目前为止,我们已经学会如何使用alpha混合,把一个透明物体渲染到屏幕上了,但有时使用它看起来并不是那么的复合我们的心意。使用蒙板技术,将会使图像按照我们设定的蒙板位置精确地绘制。
直到现在,我们在把图像加载到屏幕上时都没有檫除背景色,因为这样简单高效,但是效果并不总是很好。大部分情况下,把纹理混合到屏幕,纹理不是太少就是太多。当我们使用精灵图时,我们不希望背景从精灵的缝隙中透出光来;但在显示文字时,我们又希望文字的间隙可以显示背景色。
基于上述原因,我们需要使用“掩模”。使用“掩膜”需要两个步骤,首先我们在场景上放置黑白相间的纹理,白色代表透明部分,黑色代表不透明部分。接着我们使用一种特殊的混合方式,只有在黑色部分上的纹理才会显示在场景中。
程序运行时效果如下:
下面进入教程:
我们这次将在第06课代码的基础上修改代码,总体上并不会太难,希望大家能理解蒙板技术,这技术真的很好用。首先打开myglwidget.h文件,将类声明更改如下:
- #ifndef MYGLWIDGET_H
- #define MYGLWIDGET_H
- #include <QWidget>
- #include <QGLWidget>
- class MyGLWidget : public QGLWidget
- {
- Q_OBJECT
- public:
- explicit MyGLWidget(QWidget *parent = 0);
- ~MyGLWidget();
- protected:
- //对3个纯虚函数的重定义
- void initializeGL();
- void resizeGL(int w, int h);
- void paintGL();
- void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
- private:
- bool fullscreen; //是否全屏显示
- bool m_Masking; //是否使用"<span style="font-size:12px;">掩模</span>"
- bool m_Scene; //控制绘制哪一层
- GLfloat m_Rot; //控制纹理滚动
- QString m_FileName[5]; //图片的路径及文件名
- GLuint m_Texture[5]; //储存五个纹理
- };
- #endif // MYGLWIDGET_H
我们增加了两个布尔变量m_Masking和m_Scene来控制是否开启“掩模”以及绘制哪一个场景。然后我们增加一个控制图形滚动旋转的变量m_Rot,当然要去掉之前控制旋转的变量。最后把m_FileName和m_Texture变成长度为5的数组,因为我们需要载入5个纹理。
接下来,我们打开myglwidget.cpp,在构造函数中对新增变量进行初始化,比较简单,大家参照注释理解,不多作解释,具体代码如下:
- MyGLWidget::MyGLWidget(QWidget *parent) :
- QGLWidget(parent)
- {
- fullscreen = false;
- m_Masking = true;
- m_Scene = false;
- m_FileName[0] = "D:/QtOpenGL/QtImage/Logo.bmp"; //纹理0
- m_FileName[1] = "D:/QtOpenGL/QtImage/Mask1.bmp"; //<span style="font-size:12px;">掩模</span>纹理1,作为"<span style="font-size:12px;">掩模</span>"使用
- m_FileName[2] = "D:/QtOpenGL/QtImage/Image1.bmp"; //纹理1
- m_FileName[3] = "D:/QtOpenGL/QtImage/Mask2.bmp"; //<span style="font-size:12px;">掩模</span>纹理2,作为"<span style="font-size:12px;">掩模</span>"使用
- m_FileName[4] = "D:/QtOpenGL/QtImage/Image2.bmp"; //纹理2
- QTimer *timer = new QTimer(this); //创建一个定时器
- //将定时器的计时信号与updateGL()绑定
- connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
- timer->start(10); //以10ms为一个计时周期
- }
然后,我们略微修改下initializeGL()函数,就是载入5个位图并转换成纹理,不多解释了,具体代码如下:
- void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置
- {
- //载入位图并转换成纹理
- for (int i=0; i<5; i++){
- m_Texture[i] = bindTexture(QPixmap(m_FileName[i]));
- }
- glEnable(GL_TEXTURE_2D); //启用纹理映射
- glClearColor(0.0f, 0.0f, 0.0f, 0.0f); //黑色背景
- glShadeModel(GL_SMOOTH); //启用阴影平滑
- glClearDepth(1.0); //设置深度缓存<pre name="code" class="cpp"><pre name="code" class="cpp">}
继续,我们要进入最有趣的paintGL()函数,当然这也是重点,具体代码如下:
- void MyGLWidget::paintGL() //从这里开始进行所以的绘制
- {
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
- glLoadIdentity(); //重置模型观察矩阵
- glTranslatef(0.0f, 0.0f, -2.0f); //移入屏幕2.0单位
- glBindTexture(GL_TEXTURE_2D, m_Texture[0]); //选择Logo纹理
- glBegin(GL_QUADS); //绘制纹理四边形
- glTexCoord2f(0.0f, -m_Rot+0.0f);
- glVertex3f(-1.1f, -1.1f, 0.0f);
- glTexCoord2f(3.0f, -m_Rot+0.0f);
- glVertex3f(1.1f, -1.1f, 0.0f);
- glTexCoord2f(3.0f, -m_Rot+3.0f);
- glVertex3f(1.1f, 1.1f, 0.0f);
- glTexCoord2f(0.0f, -m_Rot+3.0f);
- glVertex3f(-1.1f, 1.1f, 0.0f);
- glEnd();
- glEnable(GL_BLEND); //启用混合
- glDisable(GL_DEPTH_TEST); //禁用深度测试
- if (m_Masking) //是否启用"<span style="font-size:12px;">掩模</span>"
- {
- glBlendFunc(GL_DST_COLOR, GL_ZERO); //使用黑白"<span style="font-size:12px;">掩模</span>"
- }
- if (m_Scene)
- {
- glTranslatef(0.0f, 0.0f, -1.0f); //移入屏幕1.0单位
- glRotatef(m_Rot*360, 0.0f, 0.0f, 1.0f); //绕z轴旋转
- if (m_Masking) //"<span style="font-size:12px;">掩模</span>"是否打开
- {
- glBindTexture(GL_TEXTURE_2D, m_Texture[3]); //选择第二个"<span style="font-size:12px;">掩模</span>"纹理
- glBegin(GL_QUADS); //开始绘制四边形
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(-1.1f, -1.1f, 0.0f);
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(1.1f, -1.1f, 0.0f);
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(1.1f, 1.1f, 0.0f);
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(-1.1f, 1.1f, 0.0f);
- glEnd();
- }
- glBlendFunc(GL_ONE, GL_ONE); //把纹理2复制到屏幕上
- glBindTexture(GL_TEXTURE_2D, m_Texture[4]); //选择第二个纹理
- glBegin(GL_QUADS); //绘制四边形
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(-1.1f, -1.1f, 0.0f);
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(1.1f, -1.1f, 0.0f);
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(1.1f, 1.1f, 0.0f);
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(-1.1f, 1.1f, 0.0f);
- glEnd();
- }
- else
- {
- if (m_Masking) //"<span style="font-size:12px;">掩模</span>"是否打开
- {
- glBindTexture(GL_TEXTURE_2D, m_Texture[1]); //选择第一个"<span style="font-size:12px;">掩模</span>"纹理
- glBegin(GL_QUADS); //绘制四边形
- glTexCoord2f(m_Rot+0.0f, 0.0f);
- glVertex3f(-1.1f, -1.1f, 0.0f);
- glTexCoord2f(m_Rot+4.0f, 0.0f);
- glVertex3f(1.1f, -1.1f, 0.0f);
- glTexCoord2f(m_Rot+4.0f, 4.0f);
- glVertex3f(1.1f, 1.1f, 0.0f);
- glTexCoord2f(m_Rot+0.0f, 4.0f);
- glVertex3f(-1.1f, 1.1f, 0.0f);
- glEnd();
- }
- glBlendFunc(GL_ONE, GL_ONE); //把纹理1复制到屏幕
- glBindTexture(GL_TEXTURE_2D, m_Texture[2]); //选择第一个纹理
- glBegin(GL_QUADS); //绘制四边形
- glTexCoord2f(m_Rot+0.0f, 0.0f);
- glVertex3f(-1.1f, -1.1f, 0.0f);
- glTexCoord2f(m_Rot+4.0f, 0.0f);
- glVertex3f(1.1f, -1.1f, 0.0f);
- glTexCoord2f(m_Rot+4.0f, 4.0f);
- glVertex3f(1.1f, 1.1f, 0.0f);
- glTexCoord2f(m_Rot+0.0f, 4.0f);
- glVertex3f(-1.1f, 1.1f, 0.0f);
- glEnd();
- }
- glEnable(GL_DEPTH_TEST); //启用深度测试
- glDisable(GL_BLEND); //禁用混合
- m_Rot += 0.002f; //增加调整纹理滚动旋转变量
- if (m_Rot > 1.0f)
- {
- m_Rot -= 1.0f;
- }
- }
函数一开始,清除背景色,重置矩阵,把物体移入屏幕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键作为切换键,很简单不多解释了,具体代码如下:
- void MyGLWidget::keyPressEvent(QKeyEvent *event)
- {
- switch (event->key())
- {
- case Qt::Key_F1: //F1为全屏和普通屏的切换键
- fullscreen = !fullscreen;
- if (fullscreen)
- {
- showFullScreen();
- }
- else
- {
- showNormal();
- }
- updateGL();
- break;
- case Qt::Key_Escape: //ESC为退出键
- close();
- break;
- case Qt::Key_Space: //空格为场景(图层)的切换键
- m_Scene = !m_Scene;
- break;
- case Qt::Key_M: //M为是否"掩膜"的切换键
- m_Masking = !m_Masking;
- break;
- }
- }
现在就可以运行程序查看效果了!
一点内容的补充:上面我们提到当调用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方向。