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
- #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()
- {
- }
下面是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); //告訴系統對透視進行修正
- }
- 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(); //重置模型觀察矩陣
- }
- void MyGLWidget::paintGL() //從這里開始進行所以的繪制
- {
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度緩存
- glLoadIdentity(); //重置當前的模型觀察矩陣
- }
- 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(); //四邊形繪制結束
- }
第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
- 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); //告訴系統對透視進行修正
- }
- 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軸旋轉
- }
第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
- 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為一個計時周期
- }
- 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;
- }
- }
- 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(); //三角形繪制結束
- }
- }
第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
- 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();
- }
- 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">
- 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); //繪制頂部
- }
- }
- }
- 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
- 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(); //彈出屬性堆棧
- }
然后,我們修改一下paintGL()函數,與之前的代碼很神似,只是更改了旋轉和平移部分的代碼,最后讓旋轉變量增加,不過多解釋了,具體代碼如下:
- 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方向。
