Qt Creator中的3D绘图及动画教程(参照NeHe)
Qt Creator中的3D繪圖及動畫教程(參照NeHe)
http://blog.csdn.net/cly116/article/details/47184729
剛剛學習了Qt Creator,發現Qt提供了QtOpenGL模塊,對OpenGL做了不錯的封裝,這使得我們可以很輕松地在Qt程序中使用OpenGL進行繪圖渲染。雖然里面還是由不少專業的解釋照搬原文的,但還是加入了大量自己的分析。而且Qt中寫OpenGL框架上比VC簡單太多了,有不少東西都封裝優化好了,代碼上還是由有很多區別的。當然,其中原教程沒解釋好的問題我都作了深入的解釋,以及一些多余部分解釋、代碼都被我刪掉簡化了。
這份Qt OpenGL的3D繪圖及動畫教程,我基本會按照Nehe的OpenGL教程,只是將代碼的實現運用到Qt Creator中,當然其中加了。
下面對Qt中OpenGL做一個簡要介紹:
Qt中OpenGL主要是在QGLWidget類中完成的,而要使用QtOpenGL模塊,需要在項目文件( .pro)中添加代碼"QT+=opengl"。
QGLWidget類是一個用來渲染OpenGL圖形的部件,提供了在Qt中顯示OpenGL圖形的功能。這個類使用起來很簡單,只需要繼承該類,然后像使用其他QWidget部件一樣來使用它。QGLWidget提供了3個方便的純虛函數,可以在子類中通過重新實現它們來執行典型的OpenGL任務:
initializeGL():設置OpenGL渲染環境,定義顯示列表等。該函數只在第一次調用resizeGL()或paintGL()前被自動調用一次。
resizeGL():設置OpenGL的視口、投影等。每次部件改變大小時都會自動調用該函數。
paintGL():渲染OpenGL場景。每當部件需要更新時都會調用該函數。
(以上3個虛函數更具體的調用情況我會用另一篇文章來講明)
也就是說,Qt中當創建并顯示出一個QGLWidget子對象時,會自動依次調用initializeGL()、resizeGL()、paintGL(),完成當前場景的繪制;而當某些情況發生時,會根據情況決定是否自動調用initializeGL()、resizeGL(),一旦調用initializeGL()、resizeGL()了,會緊跟著調用paintGL()對場景進行重新繪制。
以上就是對Qt中OpenGL機制的一個簡單介紹,后面的Qt OpenGL的3D繪圖及動畫教程,我基本會按照Nehe的OpenGL教程,只是將代碼的實現運用到Qt Creator中;教程有看不懂的,大家可以給我留言或者參考Nehe的OpenGL教程http://www.yakergong.net/nehe/
教程目錄索引:
01:OpenGL窗口
02:多邊形
03:添加顏色
04:旋轉
05:3D空間
06:紋理映射
07:光照和鍵盤
08:混合透明
09:移動圖像
10:3D世界
11:飄動的旗幟
12:顯示列表
13:圖像字體
14:圖形字體
15:紋理圖形字
16:霧
17:2D圖像文字
18:二次幾何體
19:粒子系統
20:蒙板
全部教程中需要的資源文件點此下載 http://download.csdn.net/download/cly116/8957317
第01課:創建一個OpenGL窗口(參照NeHe)
在這個教程里,我們將在Qt Creator環境中創建OpenGL對象,它將顯示一個空的OpenGL窗口,可以在窗口和全屏模式下切換,按ESC退出,它將是我們后面應用程序的基礎框架。
Qt中寫OpenGL與在VC上還是有不少差別的,對Qt機制不熟悉的朋友,請先大致了解下Qt的機制,再往下看教程。
程序運行時效果如下:
下面進入教程:
新建空的Qt項目,項目名稱為myOpenGL,然后往項目中添加新的C++類,類名為MyGLWidget,基類為QGLWidget,類型信息選擇“繼承自QWidget”。添加完成后,打開項目文件myOpenGL.pro,將代碼補全如下:
TARGET=myOpenGL
TEMPLATE=app
HEADERS+=
myglwidget.h
SOURCES+=
main.cpp
myglwidget.cpp
QT+=coregui
greaterThan(QT_MAJOR_VERSION,4):QT+=widgets
QT+=opengl
然后保存該文件。下面打開myglwidget.h文件,將類聲明補全如下:
#ifndefMYGLWIDGET_H
#defineMYGLWIDGET_H
#include<QWidget>
#include<QGLWidget>
classMyGLWidget:publicQGLWidget
{
Q_OBJECT
public:
explicitMyGLWidget(QWidget*parent=0);
~MyGLWidget();
protected:
//對3個純虛函數的重定義
voidinitializeGL();
voidresizeGL(intw,inth);
voidpaintGL();
voidkeyPressEvent(QKeyEvent*event);//處理鍵盤按下事件
private:
boolfullscreen;//是否全屏顯示
};
#endif//MYGLWIDGET_H
再到myglwidget.cpp文件中先包含#include<GL/glu.h>,#include<QKeyEvent>頭文件,然后添加類中函數的定義:
MyGLWidget::MyGLWidget(QWidget*parent):
QGLWidget(parent)
{
fullscreen=false;
}
MyGLWidget::~MyGLWidget()
{
}
構造函數中只需對fullscreen初始化,析構函數暫時并不需要做什么。
下面是initializeGL()的定義:
voidMyGLWidget::initializeGL()//此處開始對OpenGL進行所以設置
{
glClearColor(0.0,0.0,0.0,0.0);//黑色背景
glShadeModel(GL_SMOOTH);//啟用陰影平滑
glClearDepth(1.0);//設置深度緩存
glEnable(GL_DEPTH_TEST);//啟用深度測試
glDepthFunc(GL_LEQUAL);//所作深度測試的類型
glHint(GL_PERSPECTIVE_CORRECTION_HINT,GL_NICEST);//告訴系統對透視進行修正
}
glClearColor()函數用來設置清除屏幕時使用的顏色,4個參數分別用來設置紅、綠、藍顏色分量和Alpha值,它們的取值范圍都是0.0~1.0,這里4個參數都為0.0,表示純黑色。然后設置了陰影平滑,這樣可以使色彩和光照更加精細。
接下來的三行必須做的是關于depth buffer(深度緩存)的。將深度緩存設想為屏幕后面的層。深度緩存不斷地對物體進入屏幕內部有多深進行跟蹤。我們本節的程序其實沒有真正的使用深度緩存,但幾乎所有在屏幕上顯示3D場景OpenGL程序都使用深度緩存。它的排序決定那個物體先畫。這樣就不會將一個圓形后面的正方形畫到圓形前面來。深度緩存是OpenGL十分重要的部分。最后我們希望進行最好的透視修正。這會十分輕微的影響性能,但使得透視圖看起來好一點。
下面是resizeGL()的定義:
voidMyGLWidget::resizeGL(intw,inth)//重置OpenGL窗口的大小
{
glViewport(0,0,(GLint)w,(GLint)h);//重置當前的視口
glMatrixMode(GL_PROJECTION);//選擇投影矩陣
glLoadIdentity();//重置投影矩陣
//設置視口的大小
gluPerspective(45.0,(GLfloat)w/(GLfloat)h,0.1,100.0);
glMatrixMode(GL_MODELVIEW);//選擇模型觀察矩陣
glLoadIdentity();//重置模型觀察矩陣
}
glViewport()函數用來設置視口的大小。使用glMatrixMode()設置了投影矩陣,投影矩陣用來為場景增加透視,后面使用了glLoadIdentity()重置投影矩陣,這樣可以將投影矩陣恢復到初始狀態。gluPerspective()用來設置透視投影矩陣,這里設置視角為45°,縱橫比為窗口的縱橫比,最近的位置為0.1,最遠的位置為100,這兩個值是場景中所能繪制的深度的臨界值。可以想象,離我們眼睛比較近的東西看起來比較大,而比較遠的東西看起來就比較小。最后設置并重置了模型視圖矩陣。
下面是paintGL()的定義:
voidMyGLWidget::paintGL()//從這里開始進行所以的繪制
{
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);//清除屏幕和深度緩存
glLoadIdentity();//重置當前的模型觀察矩陣
}
paintGL()函數包含了所以的繪圖代碼,任何想在屏幕上顯示的東西都將在此段代碼中出現。以后每個教程中都會在這個函數增加代碼,已達到繪圖目的。
最后是鍵盤事件處理函數KeyPressEvent()的定義,由于這與OpenGL關系不大,不做過多解釋:
voidMyGLWidget::keyPressEvent(QKeyEvent*event)
{
switch(event->key())
{
//F1為全屏和普通屏的切換鍵
caseQt::Key_F1:
fullscreen=!fullscreen;
if(fullscreen)
{
showFullScreen();
}
else
{
showNormal();
}
updateGL();
break;
//ESC為退出鍵
caseQt::Key_Escape:
close();
}
}
最后再向項目中添加main.cpp文件,更改內容如下:
#include<QApplication>
#include"myglwidget.h"
intmain(intargc,char*argv[])
{
QApplicationapp(argc,argv);
MyGLWidgetw;
w.resize(400,300);
w.show();
returnapp.exec();
}
現在就可以運行程序查看效果了!
第02課:你的第一個多邊形(參照NeHe)
這次教程中,我們將添加一個三角形和一個四邊形。或許你認為這很簡單,但要知道任何復雜的繪圖都是從簡單開始的,或者說任何復雜的模型都是可以分解成簡單的圖形的。所以,我們還是從簡單的圖形開始吧。
讀完這一次教程,你還會學到如何在空間放置模型以及了解OpenGL中坐標變化。
程序運行時效果如下:
下面進入教程:
我們將使用GL_TRIANGLES來創建一個三角形,GL_QUADS來創建一個四邊形。在第01課代碼的基礎上,我們只需在paintGL()函數中增加代碼。
下面我將重寫整個paintGL()函數,具體代碼如下:
voidMyGLWidget::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沒有其他點了,這樣將顯示一個填充的三角形。
然后類比畫出一個四邊形后,就可以運行程序看效果了!
第03課:添加顏色(參照NeHe)
這次教程中,我們將在第02課的基礎上,教大家如何使用顏色。我們將一起理解兩種著色模式(光滑著色與平面著色),并運用這兩種模式分別給第02課的三角形和正方形著色。我們將使用平面著色給四邊形著色,即給三角形涂上一種固定的顏色;使用平滑著色給三角形著色,將三角形的三個頂點的不同顏色混合在一起,創建漂亮的色彩混合。
程序運行時效果如下:
下面進入教程:
要對三角形和四邊形進行著色,只需在第02課代碼的基礎上,對paintGL()函數作一定的修改。
下面我將重寫整個paintGL()函數,具體代碼如下:
voidMyGLWidget::paintGL()//從這里開始進行所以的繪制
{
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);//清除屏幕和深度緩存
glLoadIdentity();//重置當前的模型觀察矩陣
glTranslatef(-1.5f,0.0f,-6.0f);//左移1.5單位,并移入屏幕6.0單位
glBegin(GL_TRIANGLES);//開始繪制三角形
glColor3f(1.0f,0.0f,0.0f);//設置當前色為紅色
glVertex3f(0.0f,1.0f,0.0f);//上頂點
glColor3f(0.0f,1.0f,0.0f);//設置當前色為綠色
glVertex3f(-1.0f,-1.0f,0.0f);//左下
glColor3f(0.0f,0.0f,1.0f);//設置當前色為藍色
glVertex3f(1.0f,-1.0f,0.0f);//右下
glEnd();//三角形繪制結束
glTranslatef(3.0f,0.0f,0.0f);//右移3.0單位
glColor3f(0.5f,0.5f,1.0f);//一次性將當前色設置為藍色
glBegin(GL_QUADS);//開始繪制四邊形
glVertex3f(-1.0f,1.0f,0.0f);//左上
glVertex3f(1.0f,1.0f,0.0f);//右上
glVertex3f(1.0f,-1.0f,0.0f);//左下
glVertex3f(-1.0f,-1.0f,0.0f);//右下
glEnd();//四邊形繪制結束
}
其實與第02課相比,只是增加了4行代碼而已。我們利用glColor3f(r, g, b)函數來選擇顏色進行著色,該函數三個參數依次是紅、綠、藍三色分量,范圍從0.0到1.0之間,類似于之前所講的清除屏幕背景函數。當我們將顏色設為某種顏色時,接下來的代碼繪制出的對象的顏色就都是對應顏色的。
對于用光滑著色的三角形,我們需要分別對于3個頂點分別選擇顏色,再分別進行繪點。故我們在每次繪點之前都需要調用一次glColor3f(r, g, b)進行選色,glEnd()之后,三角形將被填充,但因為每個頂點有不同的顏色,因此看起來顏色從每個頂點噴出,并剛好在三角形的中心匯合,三種顏色相互混合,這就是平滑著色;而對于使用平面著色的四邊形,我只需要在一開始就選擇好顏色,直接繪制四邊形即可。
還有一點值得提的是,順時針繪制圖形時,意味著我們所看見的是圖形的背景,這在后面對圖形有一定影響。
現在就可以運行程序查看效果了!
第04課:旋轉(參照NeHe)
這次教程中,我們將在第03課的基礎上,教大家如何旋轉三角形和四邊形。我們將讓三角形沿y軸旋轉,四邊形沿x軸旋轉,最終我們能得到一個三角形和四邊形自動旋轉的場景。
程序運行時效果如下:
下面進入教程:
首先打開myglwidget.h文件,我們需要增加兩個變量來控制這兩個對象的旋轉。這兩個變量加在類的私有聲明處,將類聲明更改如下:
#ifndefMYGLWIDGET_H
#defineMYGLWIDGET_H
#include<QWidget>
#include<QGLWidget>
classMyGLWidget:publicQGLWidget
{
Q_OBJECT
public:
explicitMyGLWidget(QWidget*parent=0);
~MyGLWidget();
protected:
//對3個純虛函數的重定義
voidinitializeGL();
voidresizeGL(intw,inth);
voidpaintGL();
voidkeyPressEvent(QKeyEvent*event);//處理鍵盤按下事件
private:
boolfullscreen;//是否全屏顯示
GLfloatm_rtri;//控制三角形的角度
GLfloatm_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()函數,具體代碼如下:
voidMyGLWidget::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=newQTimer(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()函數,具體代碼如下:
voidMyGLWidget::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在這里并不會將四個點畫成一個四邊形,而是假定新的三角形開始了,千萬不要無意中增加任何多余的點。對于顏色的選擇,我們只需對應好位置,就能取得不錯的效果。
開始繪制立方體,它由六個四邊形組成,所有的四邊形都以逆時針次序繪制,即按照右上、左上、左下、右下的次序繪畫。你也許認為畫立方體的背面的時候這個次序看起來好像順時針,但別忘了我們從立方體背后看背面的時候,與你現在所想的正好相反(我們是從立方體外面來觀察立方體的)。當然,你也可以嘗試用平滑著色來繪制立方體。
現在就可以運行程序查看效果了!
第06課:紋理映射(參照NeHe)
這次教程中,我教會大家如何把紋理映射到立方體的六個面上。學習texture map(紋理映射)有諸多好處。比如說想讓一顆導彈飛過屏幕。根據前幾課的知識,我們最可行的辦法可能是很多個多邊形來構建導彈的輪廓并加上有趣的顏色。而使用紋理映射,我們可以使用真實的導彈圖像并讓它飛過屏幕。你覺得哪個更好看?使用紋理映射的好處還不止是更好看,而且程序的運行會更快。導彈貼圖可能只是一個飛過窗口的四邊形,而一個導彈卻需要成百上千的多邊形組成,很明顯,紋理映射極大的節省了CPU的時間。
程序運行時效果如下:
下面進入教程:
我們這次將在第01課得到的基礎框架上開始添加代碼,首先打開myglwidget.h文件,我們需要增加一些變量,將類聲明更改如下:
#ifndefMYGLWIDGET_H
#defineMYGLWIDGET_H
#include<QWidget>
#include<QGLWidget>
classMyGLWidget:publicQGLWidget
{
Q_OBJECT
public:
explicitMyGLWidget(QWidget*parent=0);
~MyGLWidget();
protected:
//對3個純虛函數的重定義
voidinitializeGL();
voidresizeGL(intw,inth);
voidpaintGL();
voidkeyPressEvent(QKeyEvent*event);//處理鍵盤按下事件
private:
boolfullscreen;//是否全屏顯示
GLfloatm_xRot;//繞x軸旋轉的角度
GLfloatm_yRot;//繞y軸旋轉的角度
GLfloatm_zRot;//繞z軸旋轉的角度
QStringm_FileName;//圖片的路徑及文件名
GLuintm_Texture;//儲存一個紋理
};
#endif//MYGLWIDGET_H
增加的前三個變量用來使立方體繞x、y、z軸旋轉,m_FileName用于儲存圖片的路徑及文件名,m_Texture為一個紋理分配存儲空間。如果需要不止一個紋理,可以創建一個數組來儲存不同的紋理。
接下來,我們需要打開myglwidget.cpp,加上聲明#include <QTimer>,在構造函數中對新增變量(除了m_Texture)進行初始化,同樣不作過多解釋,代碼如下:
MyGLWidget::MyGLWidget(QWidget*parent):
QGLWidget(parent)
{
fullscreen=false;
m_xRot=0.0f;
m_yRot=0.0f;
m_zRot=0.0f;
m_FileName="D:/QtOpenGL/QtImage/Nehe.bmp";//應根據實際存放圖片的路徑進行修改
QTimer*timer=newQTimer(this);//創建一個定時器
//將定時器的計時信號與updateGL()綁定
connect(timer,SIGNAL(timeout()),this,SLOT(updateGL()));
timer->start(10);//以10ms為一個計時周期
}
然后這次我們需要對initializeGL()函數作一定的修改了,具體代碼如下:
voidMyGLWidget::initializeGL()//此處開始對OpenGL進行所以設置
{
m_Texture=bindTexture(QPixmap(m_FileName));//載入位圖并轉換成紋理
glEnable(GL_TEXTURE_2D);//啟用紋理映射
glClearColor(0.0f,0.0f,0.0f,0.0f);//黑色背景
glShadeModel(GL_SMOOTH);//啟用陰影平滑
glClearDepth(1.0);//設置深度緩存
glEnable(GL_DEPTH_TEST);//啟用深度測試
glDepthFunc(GL_LEQUAL);//所作深度測試的類型
glHint(GL_PERSPECTIVE_CORRECTION_HINT,GL_NICEST);//告訴系統對透視進行修正
}
我們增加了兩行代碼,首先調用了Qt提供的bindTexture()函數將圖片載入并轉換成紋理,然后啟用2D紋理映射。如果忘記啟用的話,我們的對象看起來永遠都是純白色的,這明顯與我們的預期大相徑庭。
最后我們該開始繪制貼圖過的立方體了,paintGL()函數具體代碼如下:
voidMyGLWidget::paintGL()//從這里開始進行所以的繪制
{
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);//清除屏幕和深度緩存
glLoadIdentity();//重置模型觀察矩陣
glTranslatef(0.0f,0.0f,-5.0f);//移入屏幕5.0單位
glRotatef(m_xRot,1.0f,0.0f,0.0f);//繞x軸旋轉
glRotatef(m_yRot,0.0f,1.0f,0.0f);//繞y軸旋轉
glRotatef(m_zRot,0.0f,0.0f,1.0f);//繞z軸旋轉
glBindTexture(GL_TEXTURE_2D,m_Texture);//選擇紋理
glBegin(GL_QUADS);//開始繪制立方體
glTexCoord2f(1.0f,1.0f);
glVertex3f(1.0f,1.0f,-1.0f);//右上(頂面)
glTexCoord2f(0.0f,1.0f);
glVertex3f(-1.0f,1.0f,-1.0f);//左上(頂面)
glTexCoord2f(0.0f,0.0f);
glVertex3f(-1.0f,1.0f,1.0f);//左下(頂面)
glTexCoord2f(1.0f,0.0f);
glVertex3f(1.0f,1.0f,1.0f);//右下(頂面)
glTexCoord2f(0.0f,0.0f);
glVertex3f(1.0f,-1.0f,1.0f);//右上(底面)
glTexCoord2f(1.0f,0.0f);
glVertex3f(-1.0f,-1.0f,1.0f);//左上(底面)
glTexCoord2f(1.0f,1.0f);
glVertex3f(-1.0f,-1.0f,-1.0f);//左下(底面)
glTexCoord2f(0.0f,1.0f);
glVertex3f(1.0f,-1.0f,-1.0f);//右下(底面)
glTexCoord2f(1.0f,1.0f);
glVertex3f(1.0f,1.0f,1.0f);//右上(前面)
glTexCoord2f(0.0f,1.0f);
glVertex3f(-1.0f,1.0f,1.0f);//左上(前面)
glTexCoord2f(0.0f,0.0f);
glVertex3f(-1.0f,-1.0f,1.0f);//左下(前面)
glTexCoord2f(1.0f,0.0f);
glVertex3f(1.0f,-1.0f,1.0f);//右下(前面)
glTexCoord2f(0.0f,0.0f);
glVertex3f(1.0f,-1.0f,-1.0f);//右上(后面)
glTexCoord2f(1.0f,0.0f);
glVertex3f(-1.0f,-1.0f,-1.0f);//左上(后面)
glTexCoord2f(1.0f,1.0f);
glVertex3f(-1.0f,1.0f,-1.0f);//左下(后面)
glTexCoord2f(0.0f,1.0f);
glVertex3f(1.0f,1.0f,-1.0f);//右下(后面)
glTexCoord2f(1.0f,1.0f);
glVertex3f(-1.0f,1.0f,1.0f);//右上(左面)
glTexCoord2f(0.0f,1.0f);
glVertex3f(-1.0f,1.0f,-1.0f);//左上(左面)
glTexCoord2f(0.0f,0.0f);
glVertex3f(-1.0f,-1.0f,-1.0f);//左下(左面)
glTexCoord2f(1.0f,0.0f);
glVertex3f(-1.0f,-1.0f,1.0f);//右下(左面)
glTexCoord2f(1.0f,1.0f);
glVertex3f(1.0f,1.0f,-1.0f);//右上(右面)
glTexCoord2f(0.0f,1.0f);
glVertex3f(1.0f,1.0f,1.0f);//左上(右面)
glTexCoord2f(0.0f,0.0f);
glVertex3f(1.0f,-1.0f,1.0f);//左下(右面)
glTexCoord2f(1.0f,0.0f);
glVertex3f(1.0f,-1.0f,-1.0f);//右下(右面)
glEnd();//立方體繪制結束
m_xRot+=0.6f;//x軸旋轉
m_yRot+=0.4f;//y軸旋轉
m_zRot+=0.8f;//z軸旋轉
}
這次我們需要讓對象依次繞x、y、z軸旋轉,旋轉多少依賴于變量m_xRot、m_yRot、m_zRot的值。下面我們調用glBindTexture()函數來選擇要綁定的紋理,第2個參數表示所要綁定的紋理。當想改變紋理時,應該綁定新的紋理,要注意的是,我們不能在glBegin()和glEnd()直接綁定紋理,那樣綁定的紋理時無效的。
為了將紋理正確地映射到四邊形上,我們需要將紋理的四個角對應映射到四邊形的四個角上。如果映射錯誤的話,圖像顯示時可能上下顛倒,側向一邊或者什么都不是。glTexCoord2f的兩個參數分別表示x、y坐標,范圍從0.0f到1.0f。
最后我們讓m_xRot、m_yRot、m_zRot的值增加,大家可以嘗試變化每次個變量的改變值來調節立方體的旋轉速度,或改變+/-號來調節立方體的旋轉方向。
現在就可以運行程序查看效果了!
第07課:光照和鍵盤控制(參照NeHe)
這次教程中,我們將添加光照和鍵盤控制,它讓程序看起來更美觀。我將教大家如何使用鍵盤來移動場景中的對象,還會教大家在OpenGL場景中應用簡單的光照,讓我們的程序更加視覺效果更好且受我們控制。
程序運行時效果如下:
下面進入教程:
我們這次將在第06課的基礎上修改代碼,首先打開myglwidget.h文件,將類聲明更改如下:
#ifndefMYGLWIDGET_H
#defineMYGLWIDGET_H
#include<QWidget>
#include<QGLWidget>
classMyGLWidget:publicQGLWidget
{
Q_OBJECT
public:
explicitMyGLWidget(QWidget*parent=0);
~MyGLWidget();
protected:
//對3個純虛函數的重定義
voidinitializeGL();
voidresizeGL(intw,inth);
voidpaintGL();
voidkeyPressEvent(QKeyEvent*event);//處理鍵盤按下事件
private:
boolfullscreen;//是否全屏顯示
QStringm_FileName;//圖片的路徑及文件名
GLuintm_Texture;//儲存一個紋理
boolm_Light;//光源的開/關
GLfloatm_xRot;//x旋轉角度
GLfloatm_yRot;//y旋轉角度
GLfloatm_xSpeed;//x旋轉速度
GLfloatm_ySpeed;//y旋轉速度
GLfloatm_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=newQTimer(this);//創建一個定時器
//將定時器的計時信號與updateGL()綁定
connect(timer,SIGNAL(timeout()),this,SLOT(updateGL()));
timer->start(10);//以10ms為一個計時周期
}
然后,我們要來添加光照,只需要在initializeGL()函數增加幾行代碼,具體修改后代碼如下:
voidMyGLWidget::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);//告訴系統對透視進行修正
GLfloatLightAmbient[]={0.5f,0.5f,0.5f,1.0f};//環境光參數
GLfloatLightDiffuse[]={1.0f,1.0f,1.0f,1.0f};//漫散光參數
GLfloatLightPosition[]={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()函數的修改,修改后具體代碼如下:
voidMyGLWidget::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來控制立方體的旋轉速度。
最后當然就是鍵盤控制了,具體代碼如下(相信大家結合注釋可以很容易看懂):
voidMyGLWidget::keyPressEvent(QKeyEvent*event)
{
switch(event->key())
{
caseQt::Key_F1://F1為全屏和普通屏的切換鍵
fullscreen=!fullscreen;
if(fullscreen)
{
showFullScreen();
}
else
{
showNormal();
}
break;
caseQt::Key_Escape://ESC為退出鍵
close();
break;
caseQt::Key_L://L為開啟關閉光源的切換鍵
m_Light=!m_Light;
if(m_Light)
{
glEnable(GL_LIGHTING);//開啟光源
}
else
{
glDisable(GL_LIGHTING);//關閉光源
}
break;
caseQt::Key_PageUp://PageUp按下使木箱移向屏幕內部
m_Deep-=0.1f;
break;
caseQt::Key_PageDown://PageDown按下使木箱移向觀察者
m_Deep+=0.1f;
break;
caseQt::Key_Up://Up按下減少m_xSpeed
m_xSpeed-=0.1f;
break;
caseQt::Key_Down://Down按下增加m_xSpeed
m_xSpeed+=0.1f;
break;
caseQt::Key_Right://Right按下減少m_ySpeed
m_ySpeed-=0.1f;
break;
caseQt::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來記錄是否開啟混合,修改后代碼如下:
#ifndefMYGLWIDGET_H
#defineMYGLWIDGET_H
#include<QWidget>
#include<QGLWidget>
classMyGLWidget:publicQGLWidget
{
Q_OBJECT
public:
explicitMyGLWidget(QWidget*parent=0);
~MyGLWidget();
protected:
//對3個純虛函數的重定義
voidinitializeGL();
voidresizeGL(intw,inth);
voidpaintGL();
voidkeyPressEvent(QKeyEvent*event);//處理鍵盤按下事件
private:
boolfullscreen;//是否全屏顯示
QStringm_FileName;//圖片的路徑及文件名
GLuintm_Texture;//儲存一個紋理
boolm_Light;//光源的開/關
boolm_Blend;//是否混合
GLfloatm_xRot;//x旋轉角度
GLfloatm_yRot;//y旋轉角度
GLfloatm_xSpeed;//x旋轉速度
GLfloatm_ySpeed;//y旋轉速度
GLfloatm_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=newQTimer(this);//創建一個定時器
//將定時器的計時信號與updateGL()綁定
connect(timer,SIGNAL(timeout()),this,SLOT(updateGL()));
timer->start(10);//以10ms為一個計時周期
}
然后就要進入重點的混合,其他代碼非常簡單,并不像解釋它時那么麻煩,只需要對initializeGL()作一定的修改,具體代碼如下:
voidMyGLWidget::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);//告訴系統對透視進行修正
//下面是光源部分
GLfloatLightAmbient[]={0.5f,0.5f,0.5f,1.0f};//環境光參數
GLfloatLightDiffuse[]={1.0f,1.0f,1.0f,1.0f};//漫散光參數
GLfloatLightPosition[]={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%的透明效果。第二行設置所采用的混合類型。看,代碼真的挺簡單的。
最后是鍵盤控制的代碼,具體代碼如下:
voidMyGLWidget::keyPressEvent(QKeyEvent*event)
{
switch(event->key())
{
caseQt::Key_F1://F1為全屏和普通屏的切換鍵
fullscreen=!fullscreen;
if(fullscreen)
{
showFullScreen();
}
else
{
showNormal();
}
break;
caseQt::Key_Escape://ESC為退出鍵
close();
break;
caseQt::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;
caseQt::Key_L://L為開啟關閉光源的切換鍵
m_Light=!m_Light;
if(m_Light)
{
glEnable(GL_LIGHTING);//開啟光源
}
else
{
glDisable(GL_LIGHTING);//關閉光源
}
break;
caseQt::Key_PageUp://PageUp按下使木箱移向屏幕內部
m_Deep-=0.1f;
break;
caseQt::Key_PageDown://PageDown按下使木箱移向觀察者
m_Deep+=0.1f;
break;
caseQt::Key_Up://Up按下減少m_xSpeed
m_xSpeed-=0.1f;
break;
caseQt::Key_Down://Down按下增加m_xSpeed
m_xSpeed+=0.1f;
break;
caseQt::Key_Right://Right按下減少m_ySpeed
m_ySpeed-=0.1f;
break;
caseQt::Key_Left://Left按下增加m_ySpeed
m_ySpeed+=0.1f;
break;
}
}
當B鍵的控制機制與L鍵相似,但注意到,開啟混合時還要關閉深度測試,關閉混合時還要打開深度測試,否則將發現立方體有一些面不見了!
現在就可以運行程序看效果了!
第09課:在3D空間中移動位圖
想知道如何在3D空間中移動物體,想知道如何在屏幕上繪制一個圖像,而讓圖像的背景色變為透明,希望有一個簡單的動畫。這次教程中將教會你所以的一切。當然,這一課是在前面幾課知識的基礎上創建的,請確保你已經掌握了前面幾課的知識,再進入本課教程。
歡迎進入這次教程,這一課將是前面幾課的綜合。前面的學習中,我們學會了設置一個OpenGL窗口的每個細節,學會在旋轉的物體上貼圖并打上光線以及混色(透明)處理。這一課中,我們將在3D場景中移動位圖,并去除位圖上的黑色像素(使用混色)。接著為黑白紋理上色,最后我們將學會創建豐富的色彩,并把混合了不同色彩的紋理相互混合,得到簡單的動畫效果。
程序運行時效果如下:
下面進入教程:
我們這次將在第01課的基礎上修改代碼,其中一些與前幾課重復的地方我不作過多解釋。首先打開myglwdiget.h文件,將類聲明更改如下:
#ifndefMYGLWIDGET_H
#defineMYGLWIDGET_H
#include<QWidget>
#include<QGLWidget>
classMyGLWidget:publicQGLWidget
{
Q_OBJECT
public:
explicitMyGLWidget(QWidget*parent=0);
~MyGLWidget();
protected:
//對3個純虛函數的重定義
voidinitializeGL();
voidresizeGL(intw,inth);
voidpaintGL();
voidkeyPressEvent(QKeyEvent*event);//處理鍵盤按下事件
private:
boolfullscreen;//是否全屏顯示
QStringm_FileName;//圖片的路徑及文件名
GLuintm_Texture;//儲存一個紋理
boolm_Twinkle;//星星是否閃爍
staticconstintnum=50;//星星的數目
structstar{//為星星創建的結構體
intr,g,b;//星星的顏色
GLfloatdist;//星星距離中心的距離
GLfloatangle;//當前星星所處的角度
}m_stars[num];
GLfloatm_Deep;//星星離觀察者的距離
GLfloatm_Tilt;//星星的傾角
GLfloatm_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(inti=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=newQTimer(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()函數作一定的修改,修改后代碼如下:
voidMyGLWidget::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()函數,我會一一作出解釋,具體代碼如下:
voidMyGLWidget::paintGL()//從這里開始進行所以的繪制
{
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);//清除屏幕和深度緩存
glBindTexture(GL_TEXTURE_2D,m_Texture);//選擇紋理
for(inti=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單位,這顆星星將重新踏上回歸屏幕中心的旅程。
最后就是鍵盤控制部分了,具體代碼如下(相信大家結合注釋可以很容易看懂):
voidMyGLWidget::keyPressEvent(QKeyEvent*event)
{
switch(event->key())
{
caseQt::Key_F1://F1為全屏和普通屏的切換鍵
fullscreen=!fullscreen;
if(fullscreen)
{
showFullScreen();
}
else
{
showNormal();
}
updateGL();
break;
caseQt::Key_Escape://ESC為退出鍵
close();
break;
caseQt::Key_T://T為星星開啟關閉閃爍的切換鍵
m_Twinkle=!m_Twinkle;
break;
caseQt::Key_Up://Up按下屏幕向上傾斜
m_Tilt-=0.5f;
break;
caseQt::Key_Down://Down按下屏幕向下傾斜
m_Tilt+=0.5f;
break;
caseQt::Key_PageUp://PageUp按下縮小
m_Deep-=0.1f;
break;
caseQt::Key_PageDown://PageDown按下放大
m_Deep+=0.1f;
break;
}
}
現在就可以運行程序查看效果了!
第10課:加載3D世界,并在其中漫游(參照NeHe)
這次教程中,我將教大家如何加載一個3D世界,并在3D世界中漫游。這相較于我們只能創造一個旋轉的立方體或一群星星時有很大的進步了,當然這節課代碼難度不低,但也不會很難,只要你跟著我慢慢一步一步來。
一個3D世界當然不像我們之前那樣,只要幾個對象就搞定了,因此,我們會選擇將3D環境用數據來表達,并存放在一個文本中。隨著環境復雜度的上升,這個工作得難度也會隨之上升。出于這個原因,我們必須將數據歸類,使其具有更多的可操作性風格。后面程序中,我們會把3D世界看作是區段(sector)的集合。一個區段可以是一個房間、一個立方體或者任意一個閉合的空間。
程序運行時效果如下:
下面進入教程:
我們這次將在第01課的基礎上修改代碼,其中一些與前幾課重復的地方我不作過多解釋。首先打開myglwidget.h文件,將類聲明更改如下:
#ifndefMYGLWIDGET_H
#defineMYGLWIDGET_H
#include<QWidget>
#include<QGLWidget>
typedefstructtagVERTEX//創建Vertex頂點結構體
{
floatx,y,z;//3D坐標
floatu,v;//紋理坐標
}VERTEX;
typedefstructtagTRIANGLE//創建Triangle三角形結構體
{
VERTEXvertexs[3];//3個頂點構成一個Triangle
}TRIANGLE;
typedefstructtagSECTOR//創建Sector區段結構體
{
intnumtriangles;//Sector中的三角形個數
QVector<TRIANGLE>vTriangle;//儲存三角形的向量
}SECTOR;
classMyGLWidget:publicQGLWidget
{
Q_OBJECT
public:
explicitMyGLWidget(QWidget*parent=0);
~MyGLWidget();
protected:
//對3個純虛函數的重定義
voidinitializeGL();
voidresizeGL(intw,inth);
voidpaintGL();
voidkeyPressEvent(QKeyEvent*event);//處理鍵盤按下事件
private:
boolfullscreen;//是否全屏顯示
QStringm_FileName;//圖片的路徑及文件名
GLuintm_Texture;//儲存一個紋理
QStringm_WorldFile;//存放世界的路徑及文本名
SECTORm_Sector;//儲存一個區段的數據
staticconstfloatm_PIOVER180=0.0174532925f;//實現度和弧度直接的折算
GLfloatm_xPos;//儲存當前位置
GLfloatm_zPos;
GLfloatm_yRot;//視角的旋轉
GLfloatm_LookUpDown;//記錄抬頭和低頭
};
#endif//MYGLWIDGET_H
可以看到我們定義了3個結構體,依次表示頂點,三角形和區段。一個區段包含一系列的多邊形(三角形),三角形本質上是由三個以上頂點組合的圖形,頂點就是我們最基本的分類單位了。頂點包含了OpenGL真正感興趣的數據,我們用3D空間中的坐標值(x, y, z)以及它們的紋理坐標(u, v)來定義三角形的每個頂點。這次教程中,我們只加載了一個區段的數據,故只需一個m_Sector數據就夠了(當然有興趣的可以自己設計區段數據,多加載幾個看看)。
其他增加的變量,m_PIOVER180就是一個度數和弧度制的折算因子,m_xPos、m_zPos用于記錄游戲者的位置,m_yRot用于記錄游戲者視角的旋轉,m_LookUpDown用于控制游戲者的仰視俯視,簡單點說就是抬頭低頭啦。
接下來,我們需要打開myglwidget.cpp,加上聲明#include <QTimer>、#include <QTextStream>、#include <QtMath>,在構造函數中對數據進行初始化,具體代碼如下:
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;
QFilefile(m_WorldFile);
file.open(QIODevice::ReadOnly|QIODevice::Text);//將要讀入數據的文本打開
QTextStreamin(&file);
while(!in.atEnd())
{
QStringline[3];
for(inti=0;i<3;i++)//循環讀入3個點數據
{
do//讀入數據并保證數據有效
{
line[i]=in.readLine();
}
while(line[i][0]=='/'||line[i]=="");
}
m_Sector.numtriangles++;//每成功讀入3個點構成一個三角形
TRIANGLEtempTri;
for(inti=0;i<3;i++)//將數據儲存于一個三角形中
{
QTextStreaminLine(&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=newQTimer(this);//創建一個定時器
//將定時器的計時信號與updateGL()綁定
connect(timer,SIGNAL(timeout()),this,SLOT(updateGL()));
timer->start(10);//以10ms為一個計時周期
}
我們重點解釋中間對于m_Sector的初始化,我們先將文件打開,再利用Qt的文本流一行一行的讀取(為何一行一行讀,大家看下存放數據的文本文件World.txt就知道了)并保證讀入的數據是有效的。每當成功讀入三行數據時,說明構成了一個三角形,就創建一個三角形來儲存這些數據,并在最后把三角形放入m_Sector中,當然要給m_Sector的numtriangles加上一,說明多了一個三角形。最后錄完數據后,關上文件。或者你會想如果有效數據行數不是3的倍數怎么辦,這個問題其實已經不是我們的問題了,而且提供的數據文本存在問題,因此不必考慮。接著的數據初始化不作解釋了。
然后在initializeGL()函數中,請大家修改代碼如下(不解釋):
voidMyGLWidget::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);//告訴系統對透視進行修正
}
任何一個不錯的的3D引擎都會允許用戶在這個世界中游走和遍歷,我們的這個也一樣,實現這個功能當然要通過鍵盤控制。具體實現的途徑有一種是直接移動鏡頭并繪制以鏡頭為中心的3D環境,但這樣會很慢并且不易用代碼實現,我們的解決方法如下:
根據用戶的指令旋轉并變換視角位置。
圍繞原點,以與視角相反的旋轉方向來旋轉世界(讓人產生視角旋轉的錯覺)。
以與視角平移方向相反的方向來平移世界(讓人產生視角移動的錯覺)。
這樣實現起來就簡單多了。下面我們先通過鍵盤控制,來實現平移并旋轉視角。
voidMyGLWidget::keyPressEvent(QKeyEvent*event)
{
switch(event->key())
{
caseQt::Key_F1://F1為全屏和普通屏的切換鍵
fullscreen=!fullscreen;
if(fullscreen)
{
showFullScreen();
}
else
{
showNormal();
}
updateGL();
break;
caseQt::Key_Escape://ESC為退出鍵
close();
break;
caseQt::Key_PageUp://按下PageUp視角向上轉
m_LookUpDown-=1.0f;
if(m_LookUpDown<-90.0f)
{
m_LookUpDown=-90.0f;
}
break;
caseQt::Key_PageDown://按下PageDown視角向下轉
m_LookUpDown+=1.0f;
if(m_LookUpDown>90.0f)
{
m_LookUpDown=90.0f;
}
break;
caseQt::Key_Right://Right按下向左旋轉場景
m_yRot-=1.0f;
break;
caseQt::Key_Left://Left按下向右旋轉場景
m_yRot+=1.0f;
break;
caseQt::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;
caseQt::Key_Down://Down按下向后移動
//向后移動分到x、z上的分量
m_xPos+=(float)sin(m_yRot*m_PIOVER180)*0.05f;
m_zPos+=(float)cos(m_yRot*m_PIOVER180)*0.05f;
break;
}
}
這個實現很簡單。當左右方向鍵按下后,旋轉變量m_yRot相應的增加或減少。當前后方向鍵按下時,我們使用sin()和cos()函數計算具體在x和z軸方向上的位移量,使得游戲者能準確的移動。
現在我們已經具備了一切所需的數據,可以開始進行步驟2和3了,當然我們也將進入重點的paintGL()函數。雖然重點,但代碼并不難,具體代碼如下:
voidMyGLWidget::paintGL()//從這里開始進行所以的繪制
{
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);//清除屏幕和深度緩存
glLoadIdentity();//重置當前的模型觀察矩陣
GLfloatx_m,y_m,z_m,u_m,v_m;//頂點的臨時x、y、z、u、v值
GLfloatxTrans=-m_xPos;//游戲者沿x軸平移時的大小
GLfloatzTrans=-m_zPos;//游戲者沿z軸平移時的大小
GLfloatyTrans=-0.25f;//游戲者沿y軸略作平移,使視角準確
GLfloatsceneroty=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(inti=0;i<m_Sector.numtriangles;i++)//遍歷所有的三角形
{
glBegin(GL_TRIANGLES);//開始繪制三角形
glNormal3f(0.0f,0.0f,1.0f);//指向前面的法線
x_m=m_Sector.vTriangle[i].vertexs[0].x;
y_m=m_Sector.vTriangle[i].vertexs[0].y;
z_m=m_Sector.vTriangle[i].vertexs[0].z;
u_m=m_Sector.vTriangle[i].vertexs[0].u;
v_m=m_Sector.vTriangle[i].vertexs[0].v;
glTexCoord2f(u_m,v_m);
glVertex3f(x_m,y_m,z_m);
x_m=m_Sector.vTriangle[i].vertexs[1].x;
y_m=m_Sector.vTriangle[i].vertexs[1].y;
z_m=m_Sector.vTriangle[i].vertexs[1].z;
u_m=m_Sector.vTriangle[i].vertexs[1].u;
v_m=m_Sector.vTriangle[i].vertexs[1].v;
glTexCoord2f(u_m,v_m);
glVertex3f(x_m,y_m,z_m);
x_m=m_Sector.vTriangle[i].vertexs[2].x;
y_m=m_Sector.vTriangle[i].vertexs[2].y;
z_m=m_Sector.vTriangle[i].vertexs[2].z;
u_m=m_Sector.vTriangle[i].vertexs[2].u;
v_m=m_Sector.vTriangle[i].vertexs[2].v;
glTexCoord2f(u_m,v_m);
glVertex3f(x_m,y_m,z_m);
glEnd();//三角形繪制結束
}
}
就正如我們之前步驟2和3所說,我們以相反的方式來平移和旋轉場景,使得看上去是視角在平移和旋轉,然后綁定紋理并繪制出整個場景就完成了!
現在就可以運行程序查看效果了!
PS:NeHe教程中有關于移動中視角輕微上下擺動的設定,在這個教程中被我刪掉了,由于我發現這個效果并不明顯,有興趣的朋友請看http://www.yakergong.net/nehe/
第11課:旗幟效果(飄動的紋理)(參照NeHe)
這次教程中,我將教大家如何創建一個飄動的旗幟。我們所要創建的旗幟,說白了就是一個以正弦波方式運動的紋理映射圖像。雖然不會很難,但效果確實很不錯,希望大家能喜歡。當然這次教程是基于第06課的,希望大家確保已經掌握了前6課再進入本次教程。
程序運行時效果如下:
下面進入教程:
我們這次將在第06課的基礎上修改代碼,我們只會解釋增加部分的代碼,首先打開myglwidget.h文件,將類聲明更改如下:
#ifndefMYGLWIDGET_H
#defineMYGLWIDGET_H
#include<QWidget>
#include<QGLWidget>
classMyGLWidget:publicQGLWidget
{
Q_OBJECT
public:
explicitMyGLWidget(QWidget*parent=0);
~MyGLWidget();
protected:
//對3個純虛函數的重定義
voidinitializeGL();
voidresizeGL(intw,inth);
voidpaintGL();
voidkeyPressEvent(QKeyEvent*event);//處理鍵盤按下事件
private:
boolfullscreen;//是否全屏顯示
GLfloatm_xRot;//繞x軸旋轉的角度
GLfloatm_yRot;//繞y軸旋轉的角度
GLfloatm_zRot;//繞z軸旋轉的角度
QStringm_FileName;//圖片的路徑及文件名
GLuintm_Texture;//儲存一個紋理
floatm_Points[45][45][3];//儲存網格頂點的數組
intm_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(intx=0;x<45;x++)//初始化數組產生波浪效果(靜止)
{
for(inty=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=newQTimer(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()函數中,請大家修改代碼如下:
voidMyGLWidget::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()函數,當然這依舊是重點,代碼如下:
voidMyGLWidget::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);//旋轉紋理
floatflag_x1,flag_y1,flag_x2,flag_y2;//用來將紋理分割成小的四邊形方便紋理映射
glBegin(GL_QUADS);
for(intx=0;x<44;x++)
{
for(inty=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(inty=0;y<45;y++)
{
floattemp=m_Points[0][y][2];
for(intx=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幀一次變化了旗幟的波動狀態,看起來就是一個飄動的旗幟,不是靜止的了(大家可以嘗試著注釋掉某一部分代碼看看發生什么改變)。然后計數器清零加一什么的就不過多解釋了!
現在就可以運行程序查看效果了!
第12課:顯示列表(參照NeHe)
想知道如何加速我們的OpenGL程序么?這次教程中,我將告訴你如何使用OpenGL的顯示列表,它通過預編譯OpenGL命令來加速我們的程序,并可以為我們省去很多重復的代碼,聽起來是不是很棒呢!
當我們在制作游戲里的小行星場景時,每一層至少需要兩個行星,你可以用OpenGL中的多邊形來構造每一個行星。但要知道每次把行星畫到屏幕上都是很麻煩的,當我們面臨復雜的場景時,要靠代碼的繪畫方式一個個畫出所有的行星,這對于絕大多數人來說都是一個噩夢。那么,解決辦法是什么呢?用顯示列表,我們只需要一次性建立物體,可以貼圖,用顏色,想怎么弄就怎么弄。然后給顯示列表一個名字,比如給小行星的顯示列表命名為“asteroid”。現在,任何時候,我們想在屏幕上畫出行星,我們只需要調好位置后,調用glCallList(asteroid),之前做好的小行星就會立刻顯示在屏幕上了。由于小行星已經在顯示列表里建造好了,OpenGL不會再計算如何構造它。它已經在內存中建造好了,這將大大降低CPU的使用,讓你的程序跑得更快。
程序運行時效果如下:
下面進入教程:
我們這次同樣將在第06課的基礎上修改代碼,我們只會解釋增加部分的代碼,首先打開myglwidget.h文件,將類聲明更改如下:
#ifndefMYGLWIDGET_H
#defineMYGLWIDGET_H
#include<QWidget>
#include<QGLWidget>
classMyGLWidget:publicQGLWidget
{
Q_OBJECT
public:
explicitMyGLWidget(QWidget*parent=0);
~MyGLWidget();
protected:
//對3個純虛函數的重定義
voidinitializeGL();
voidresizeGL(intw,inth);
voidpaintGL();
voidkeyPressEvent(QKeyEvent*event);//處理鍵盤按下事件
private:
voidbuildLists();//初始化盒子的顯示列表
private:
boolfullscreen;//是否全屏顯示
GLfloatm_xRot;//繞x軸旋轉的角度
GLfloatm_yRot;//繞y軸旋轉的角度
QStringm_FileName;//圖片的路徑及文件名
GLuintm_Texture;//儲存一個紋理
GLuintm_Box;//保存盒子的顯示列表
GLuintm_Top;//保存盒子頂部的顯示列表
};
#endif//MYGLWIDGET_H
我們新增了兩個用于顯示列表的變量m_Box、m_Top,這兩個變量是用于儲存指向顯示列表的指針。另外我們多了一個buildLists()函數,這個函數是用于初始化兩個顯示列表的(注意我去掉了變量m_zRot,但其實影響不大)。
接下來,我們需要打開myglwidget.cpp,修改構造函數同時添加buildLists()函數的定義,具體代碼如下:
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=newQTimer(this);//創建一個定時器
//將定時器的計時信號與updateGL()綁定
connect(timer,SIGNAL(timeout()),this,SLOT(updateGL()));
timer->start(10);//以10ms為一個計時周期
}
voidMyGLWidget::buildLists()//創建盒子的顯示列表
{
m_Box=glGenLists(2);//創建兩個顯示列表的空間
glNewList(m_Box,GL_COMPILE);//開始創建第一個顯示列表
glBegin(GL_QUADS);
glTexCoord2f(0.0f,0.0f);
glVertex3f(1.0f,-1.0f,1.0f);//右上(底面)
glTexCoord2f(1.0f,0.0f);
glVertex3f(-1.0f,-1.0f,1.0f);//左上(底面)
glTexCoord2f(1.0f,1.0f);
glVertex3f(-1.0f,-1.0f,-1.0f);//左下(底面)
glTexCoord2f(0.0f,1.0f);
glVertex3f(1.0f,-1.0f,-1.0f);//右下(底面)
glTexCoord2f(1.0f,1.0f);
glVertex3f(1.0f,1.0f,1.0f);//右上(前面)
glTexCoord2f(0.0f,1.0f);
glVertex3f(-1.0f,1.0f,1.0f);//左上(前面)
glTexCoord2f(0.0f,0.0f);
glVertex3f(-1.0f,-1.0f,1.0f);//左下(前面)
glTexCoord2f(1.0f,0.0f);
glVertex3f(1.0f,-1.0f,1.0f);//右下(前面)
glTexCoord2f(0.0f,0.0f);
glVertex3f(1.0f,-1.0f,-1.0f);//右上(后面)
glTexCoord2f(1.0f,0.0f);
glVertex3f(-1.0f,-1.0f,-1.0f);//左上(后面)
glTexCoord2f(1.0f,1.0f);
glVertex3f(-1.0f,1.0f,-1.0f);//左下(后面)
glTexCoord2f(0.0f,1.0f);
glVertex3f(1.0f,1.0f,-1.0f);//右下(后面)
glTexCoord2f(1.0f,1.0f);
glVertex3f(-1.0f,1.0f,1.0f);//右上(左面)
glTexCoord2f(0.0f,1.0f);
glVertex3f(-1.0f,1.0f,-1.0f);//左上(左面)
glTexCoord2f(0.0f,0.0f);
glVertex3f(-1.0f,-1.0f,-1.0f);//左下(左面)
glTexCoord2f(1.0f,0.0f);
glVertex3f(-1.0f,-1.0f,1.0f);//右下(左面)
glTexCoord2f(1.0f,1.0f);
glVertex3f(1.0f,1.0f,-1.0f);//右上(右面)
glTexCoord2f(0.0f,1.0f);
glVertex3f(1.0f,1.0f,1.0f);//左上(右面)
glTexCoord2f(0.0f,0.0f);
glVertex3f(1.0f,-1.0f,1.0f);//左下(右面)
glTexCoord2f(1.0f,0.0f);
glVertex3f(1.0f,-1.0f,-1.0f);//右下(右面)
glEnd();
glEndList();//第一個顯示列表結束
m_Top=m_Box+1;//m_Box+1得到第二個顯示列表的指針
glNewList(m_Top,GL_COMPILE);//開始創建第二個顯示列表
glBegin(GL_QUADS);
glTexCoord2f(1.0f,1.0f);
glVertex3f(1.0f,1.0f,-1.0f);//右上(頂面)
glTexCoord2f(0.0f,1.0f);
glVertex3f(-1.0f,1.0f,-1.0f);//左上(頂面)
glTexCoord2f(0.0f,0.0f);
glVertex3f(-1.0f,1.0f,1.0f);//左下(頂面)
glTexCoord2f(1.0f,0.0f);
glVertex3f(1.0f,1.0f,1.0f);//右下(頂面)
glEnd();
glEndList();
}
構造函數不解釋了,就刪掉了m_zRot的初始化。buildLists()函數中,我們會將創造盒子的代碼都放在第一個顯示列表里,所有創造頂部的代碼都在另一個顯示列表里。開始時,我們告訴OpenGL我們要建立兩個顯示列表,glGenLists(2)創建了兩個顯示列表的空間,并返回第一個列表的指針,我們把它儲存在m_Box中,任何時候我們調用glCallList(m_Box)第一個顯示列表就會繪制出來。
接下來我們開始構造第一個顯示列表。我們已經申請了兩個顯示列表的空間了,并且有m_Box指針指向第一個顯示列表,所以我們需要做的是告訴OpenGL要建立什么類型的顯示列表。我們用glNewList()命令來做這件事情,注意到m_Box是第一個參數,這表示OpenGL將把列表儲存到m_Box所指向的內存空間。而第二個參數GL_COMPILE告訴OpenGL我們想預先在內存中構造這個列表,這樣每次畫的時候就不必重新計算怎么構造物體了。
GL_COMPILE類似于編程。在我們寫程序的時候,把它裝載到編譯器里,我們每次運行程序都需要重新編譯。而如果它已經編譯成了.exe文件,那么每次我們只需要點擊那個.exe文件就可以運行它了,不需要編譯。當OpenGL編譯過顯示列表后,就不需要再每次顯示的時候重新編譯它了。這就是為什么用顯示列表可以加快速度。
下面我們就畫了一個沒有頂部的盒子,它不會出現在屏幕上,只會儲存在顯示列表里。我們可以在glNewList()和glEndList()中間加上任何你想加上的代碼,可以設置顏色,貼圖等等。但是,如果是你想在繪畫過程發生改變的代碼就不能加進去,這是由于顯示列表一旦建立,就不能改變它。比如我們想繪制不同顏色的物體,所以我們加上了glColor3ub(rand()%255,rand()%255,rand()%255),但因為顯示列表只會建立一次,所以每次畫出來的物體都是同一種顏色,也就是說在儲存進列表時glColor3ub的三個參數值就固定下來了。
然后我們用glEndList()命令告訴OpenGL我們已經完成了一個顯示列表。在glNewList()和glEndList()之間的任何東西就是顯示列表的一部分。接著,我們來建立第二個顯示列表,在上一個顯示列表的指針上加一,就得到了第二個顯示列表的指針,并儲存在m_Top中。最后同樣建立這個顯示列表就不解釋了。
然后在initializeGL()函數中,將代碼修改如下:
voidMyGLWidget::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);//告訴系統對透視進行修正
<prename="code"class="cpp">
glEnable(GL_LIGHT0); //使用默認的0號燈 glEnable(GL_LIGHTING); //使用燈光 glEnable(GL_COLOR_MATERIAL); //使用顏色材質}
我們在啟用紋理之后調用了buildLists()函數,創建了顯示列表,注意在構造函數中調用buildLists()函數時無法生效的,Qt中使用OpenGL的時候,與內存使用相關的OpenGL函數都需要在initialize()、resize()、paintGL()中直接調用或間接調用,否則無法成功地申請空間。
最后三行使的燈光有效,LIGHT一般是顯卡中預先定義過的。最后一行的GL_COLOR_MATERIAL使得我們可以用顏色來貼紋理。如果沒有這行代碼,紋理將始終保持原來的顏色,glColor3f(r, g,b)就沒有用了。
還有就是paintGL()函數,這次看起來就簡單許多了(麻煩的我們已經通過參數列表搞定了),具體代碼如下:
voidMyGLWidget::paintGL()//從這里開始進行所以的繪制
{
staticconstGLfloatboxColor[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}
};
staticconstGLfloattopColor[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(inty=1;y<6;y++)//循環來控制畫盒子
{
for(intx=0;x<y;x++)
{
glLoadIdentity();
//設置盒子的位置
glTranslatef(1.4f+(float(x)*2.8f)-(float(y)*1.4f),
((6.0f-float(y))*2.4f)-7.0f,-20.0f);
glRotatef(45.0f+m_xRot,1.0f,0.0f,0.0f);
glRotatef(45.0f+m_yRot,0.0f,1.0f,0.0f);
glColor3fv(boxColor[y-1]);//選擇盒子顏色
glCallList(m_Box);//繪制盒子
glColor3fv(topColor[y-1]);//選擇頂部顏色
glCallList(m_Top);//繪制頂部
}
}
}
我們一開始就定義了儲存盒子和頂部顏色的數組,接著我們用雙重循環來畫出10個立方體,并在每次畫時重置模型觀察矩陣,平移和旋轉到需要畫出立方體位置的中心,具體如何算的我看太懂NeHe的用意就不解釋了,反正這并不是重點,我們完全可以根據自己的喜好來擺放這些立方體。
然后我們選擇顏色,接著按前面所講,調用glCallList()就可以畫出我們要的立方體了。相比于之前的幾課,顯示列表讓我們的paintGL()函數看起來簡單了許多。
最后是鍵盤控制的,比較簡單我就不過多解釋了,具體代碼如下:
voidMyGLWidget::keyPressEvent(QKeyEvent*event)
{
switch(event->key())
{
caseQt::Key_F1://F1為全屏和普通屏的切換鍵
fullscreen=!fullscreen;
if(fullscreen)
{
showFullScreen();
}
else
{
showNormal();
}
updateGL();
break;
caseQt::Key_Escape://ESC為退出鍵
close();
break;
caseQt::Key_Left://Left按下向左旋轉
m_yRot-=1.0f;
break;
caseQt::Key_Right://Right按下向右旋轉
m_yRot+=1.0f;
break;
caseQt::Key_Up://Up按下向上旋轉
m_xRot-=1.0f;
break;
caseQt::Key_Down://Down按下向下旋轉
m_xRot+=1.0f;
break;
}
}
現在就可以運行程序查看效果了!
第13課:位圖字體(參照NeHe)
這次教程中,我們將創建一些基于2D圖像的字體,它們可以縮放平移,但不能旋轉,并且總是面向前方,但作為基本的顯示來說,我想已經足夠了。
或者對于這次教程,你會覺得“在屏幕上顯示文字沒什么難的”,但是你真正嘗試過就會知道,它確實沒那么容易。你當然可以把文字寫在一個圖片上,再把這幅圖片載入你的OpenGL程序中,打開混合選項,從而在屏幕上顯示出文字。但這種做法非常耗時,而且經常圖像會顯得模糊。另外,除非你的圖像包含一個Alpha通道,否則一旦繪制在屏幕上,那些文字就會不透明(與屏幕中的其他物體混合)。
使用位圖字體比起使用圖形字體(貼圖)看起來不止強100倍,你可以隨時改變顯示在屏幕上的文字,而且用不著為它們逐個制作貼圖。只需要將文字定位,再調用我們即將構建的glPrint()函數就可以在屏幕上顯示文字了。
程序運行時效果如下:
下面進入教程:
我們這次將在第01課的基礎上修改代碼,我會對新增代碼一一解釋,希望大家能掌握,首先打開myglwidget.h文件,將類聲明更改如下:
#ifndefMYGLWIDGET_H
#defineMYGLWIDGET_H
#include<QWidget>
#include<QGLWidget>
classMyGLWidget:publicQGLWidget
{
Q_OBJECT
public:
explicitMyGLWidget(QWidget*parent=0);
~MyGLWidget();
protected:
//對3個純虛函數的重定義
voidinitializeGL();
voidresizeGL(intw,inth);
voidpaintGL();
voidkeyPressEvent(QKeyEvent*event);//處理鍵盤按下事件
private:
voidbuildFont();//創建字體
voidkillFont();//刪除顯示列表
voidglPrint(constchar*fmt,...);//輸出字符串
private:
boolfullscreen;//是否全屏顯示
HDCm_HDC;//儲存當前設備的指針
intm_FontSize;//控制字體的大小
GLuintm_Base;//儲存繪制字體的顯示列表的開始位置
GLfloatm_Cnt1;//字體移動計數器1
GLfloatm_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;
HWNDhWND=(HWND)winId();//獲取當前窗口句柄
m_HDC=GetDC(hWND);//通過窗口句柄獲得HDC
QTimer*timer=newQTimer(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()函數刪除顯示列表(具體實現看下面)。
繼續,我們要來定義我們新增的三個函數了,這可是重頭戲,具體代碼如下:
voidMyGLWidget::buildFont()//創建位圖字體
{
HFONTfont;//字體句柄
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,//FamilyandPitch的設置
LPCWSTR("CourierNew"));//字體名稱(電腦中已裝的)
wglUseFontBitmaps(m_HDC,32,96,m_Base);//創建96個顯示列表,繪制ASCII碼為32-128的字符
SelectObject(m_HDC,font);//選擇字體
}
voidMyGLWidget::killFont()//刪除顯示列表
{
glDeleteLists(m_Base,96);//刪除96個顯示列表
}
voidMyGLWidget::glPrint(constchar*fmt,...)//自定義輸出文字函數
{
chartext[256];//保存字符串
va_listap;//指向一個變量列表的指針
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各個參數請點擊此處),最后一個參數是字體類型,我們可以使用我們電腦已安裝的任何字體,在WindowsFonts目錄可查看電腦已安裝的字體。
然后我們從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()函數,不作解釋,代碼如下:
voidMyGLWidget::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()函數了,很簡單,難的都過去了,具體代碼如下:
voidMyGLWidget::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("ActiveOpenGLTextWithNeHe-%7.2f",m_Cnt1);
m_Cnt1+=0.051f;//增加兩個計數器的值
m_Cnt2+=0.005f;
}
值得注意的是,深入屏幕并不能縮小字體,只會給字體變化移動范圍(這一點大家自己改改數據就知道了)。然后字體顏色設置和位置設置我覺得沒必要解釋了,都是數學的東西,我們主要是為了得到一個變化的效果,并不在乎它是怎么實現的。然后就是調用glPrint()函數輸出文字,最后增加兩個計數器的值就OK了。
最后就是鍵盤控制的代碼了,大家自己看吧,很簡單,具體代碼如下:
voidMyGLWidget::keyPressEvent(QKeyEvent*event)
{
switch(event->key())
{
caseQt::Key_F1://F1為全屏和普通屏的切換鍵
fullscreen=!fullscreen;
if(fullscreen)
{
showFullScreen();
}
else
{
showNormal();
}
updateGL();
break;
caseQt::Key_Escape://ESC為退出鍵
close();
break;
caseQt::Key_PageUp://PageUp按下字體縮小
m_FontSize-=1;
if(m_FontSize<-75)
{
m_FontSize=-75;
}
buildFont();
break;
caseQt::Key_PageDown://PageDown按下字體放大
m_FontSize+=1;
if(m_FontSize>-5)
{
m_FontSize=-5;
}
buildFont();
break;
}
}
現在就可以運行程序查看效果了!
第14課:輪廓字體(參照NeHe)
這次教程中,我將教大家繪制3D的輪廓字體,當然肯定不是貼圖方式了,它們可像一般的3D模型一樣進行旋轉,放縮。
創建輪廓字體的方法與13課位圖的位圖字體類似,但輪廓字體要酷得多!輪廓字體可以在屏幕中以3D方式旋轉,而且輪廓字體還可以有一定的厚度,而不再是平面的2D字符了。使用輪廓字體,我們可以將計算機中的任何字體轉換為OpenGL的3D字體,是不是聽起來很誘人呢?
程序運行時效果如下:
下面進入教程:
我們這次將在第13課的基礎上修改代碼,由于有13課代碼的基礎,這節課會簡單許多。我只解釋新增的代碼,首先打開myglwidget.h文件,將類聲明更改如下:
#ifndefMYGLWIDGET_H
#defineMYGLWIDGET_H
#include<QWidget>
#include<QGLWidget>
classMyGLWidget:publicQGLWidget
{
Q_OBJECT
public:
explicitMyGLWidget(QWidget*parent=0);
~MyGLWidget();
protected:
//對3個純虛函數的重定義
voidinitializeGL();
voidresizeGL(intw,inth);
voidpaintGL();
voidkeyPressEvent(QKeyEvent*event);//處理鍵盤按下事件
private:
voidbuildFont();//創建字體
voidkillFont();//刪除顯示列表
voidglPrint(constchar*fmt,...);//輸出字符串
private:
boolfullscreen;//是否全屏顯示
HDCm_HDC;//儲存當前設備的指針
GLYPHMETRICSFLOATm_Gmf[256];//記錄256個字符的信息
GLfloatm_Deep;//移入屏幕的距離
GLuintm_Base;//儲存繪制字體的顯示列表的開始位置
GLfloatm_Rot;//用于旋轉字體
};
#endif//MYGLWIDGET_H
由于我們沒有準備讓輪廓字體移動,所以刪掉兩個計數器。接著增加m_Deep來控制移入屏幕的距離,其實就是來控制字體的放大縮小的。然后再增加m_Rot來控制字體的旋轉。最后增加了GLYPHMETRICSFLOAT變量數組m_Gmf[256]用來保存256個輪廓字體顯示列表中對應的每一個列表的位置和方向信息,我們通過m_Gmf[num]來選擇字母。要注意的是,每個字符的寬度可以不相同,使用GLYPHMETRICS會大大簡化我們的工作。
接下來,我們需要打開myglwidget.cpp,先修改構造函數,不多解析了,具體代碼如下:
MyGLWidget::MyGLWidget(QWidget*parent):
QGLWidget(parent)
{
fullscreen=false;
m_Deep=-10.0f;
m_Rot=0.0f;
HWNDhWND=(HWND)winId();//獲取當前窗口句柄
m_HDC=GetDC(hWND);//通過窗口句柄獲得HDC
QTimer*timer=newQTimer(this);//創建一個定時器
//將定時器的計時信號與updateGL()綁定
connect(timer,SIGNAL(timeout()),this,SLOT(updateGL()));
timer->start(10);//以10ms為一個計時周期
}
繼續,我們需要對buildFont()、killFont()、glPrint()三個函數作一定的修改,具體代碼如下:
voidMyGLWidget::buildFont()//創建輪廓字體
{
HFONTfont;//字體句柄
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,//FamilyandPitch的設置
LPCWSTR("ComicSansMS"));//字體名稱(電腦中已裝的)
SelectObject(m_HDC,font);//選擇字體
wglUseFontOutlines(m_HDC,//當前HDC
0,//從ASCII碼第一個字符開始
255,//字符數
m_Base,//第一個顯示列表的名稱
0.0f,//字體光滑度,越小越光滑
0.2f,//在z方向突出的距離(字體的厚度)
WGL_FONT_POLYGONS,//使用多邊形來生成字符,每個頂點具有獨立法線
m_Gmf);//用于儲存字形度量數據(高度,寬度等)
}
voidMyGLWidget::killFont()//刪除顯示列表
{
glDeleteLists(m_Base,256);//刪除96個顯示列表
}
voidMyGLWidget::glPrint(constchar*fmt,...)//自定義輸出文字函數
{
floatlength=0;
chartext[256];//保存字符串
va_listap;//指向一個變量列表的指針
if(fmt==NULL)//如果無輸入則返回
{
return;
}
va_start(ap,fmt);//分析可變參數
vsprintf(text,fmt,ap);//把參數值寫入字符串
va_end(ap);//結束分析
for(unsignedinti=0;i<strlen(text);i++)//計算整個字符串的長度
{
length+=m_Gmf[(int)text[i]].gmfCellIncX;
}
glTranslatef(-length/2,0.0f,0.0f);//左移字符串一半的長度
glPushAttrib(GL_LIST_BIT);//把顯示列表屬性壓入堆棧
glListBase(m_Base);//設置顯示列表的基礎值
glCallLists(strlen(text),GL_UNSIGNED_BYTE,text);//調用顯示列表繪制字符串
glPopAttrib();//彈出屬性堆棧
}
首先是buildFont()函數。首先創建字體的方法與上一課基本一致,只是把m_FontSize換成了-18。接著,將wglUseFontBitmaps()函數替換成wglUseFontOutlines()函數,這個函數包含了8個參數前4個參數大家自己看注釋,第5個參數為光滑度系數,這個值越小,字體看起來會越光滑(其實看不出明顯差別)。第6個參數簡單點說指的是字體的厚度,有厚度的字體才有立體感嘛,如果這個值為0.0就變成2D字體了。第7個參數告訴OpenGL用多邊形來生成字符,使每個頂點都會具有獨立的法線,這樣加上光源后會有不錯的效果(光源效果我們的代碼中沒有,大家可以自己加加看)。最后一個參數告訴OpenGL把創建的顯示列表的度量數據(高度、寬度等)放在數組m_Gmf[]中。
然后是killFont()函數。它很簡單,就是調用glDeleteLists()函數從m_Base開始刪除256個顯示列表。
最后是glPrint()函數。我們只是在原來的基礎上加了一些代碼,我們首先看到我們增加了一個浮點變量length用來統計整個字符串的寬度。接著我們利用循環,在循環中,由于我們已經將每個字符的度量值儲存在m_Gmf[]中,我們利用m_Gmf[text[i]].gmfCellIncX來獲得每個字符的寬度,累加起來就得到字符串的總寬度。然后,我們把視圖原點左移length/2的距離,這樣就保證字符串總是在屏幕的中心了。最后,把glListBase(m_Base-32)換成glListBase(m_Base),這是因為我們這次是從ASCII碼第一個字符開始創建的顯示列表。
然后,我們修改一下paintGL()函數,與之前的代碼很神似,只是更改了旋轉和平移部分的代碼,最后讓旋轉變量增加,不過多解釋了,具體代碼如下:
voidMyGLWidget::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可以放大縮小字體,具體代碼如下:
voidMyGLWidget::keyPressEvent(QKeyEvent*event)
{
switch(event->key())
{
caseQt::Key_F1://F1為全屏和普通屏的切換鍵
fullscreen=!fullscreen;
if(fullscreen)
{
showFullScreen();
}
else
{
showNormal();
}
updateGL();
break;
caseQt::Key_Escape://ESC為退出鍵
close();
break;
caseQt::Key_PageUp://PageUp按下字體縮小
m_Deep-=0.2f;
break;
caseQt::Key_PageDown://PageDown按下字體放大
m_Deep+=0.2f;
break;
}
}
現在就可以運行程序查看效果了!
第15課:圖形字體的紋理映射(參照NeHe)
這次教程中,我們將在第14課的基礎上創建帶有紋理的字體,它真的很簡單。也許你想知道如何才能給字體賦予紋理貼圖?我們可以使用自動紋理坐標生成器,它會自動為字體上的每一個多邊形生成紋理坐標。
這次課中我們還將使用Wingdings字體來顯示一個海盜旗(骷髏頭和十字骨頭)的標志,為此我們需要修改buildFont()函數代碼。如果你想顯示文字的話,就不用改動第14課中buildFont()函數的代碼了,當然你也可以選擇另一種字體,這都不是什么大事。
程序運行時效果如下:
下面進入教程:
我們這次將在第14課的基礎上修改代碼,由于有前兩課代碼的基礎,這節課會更簡單。我只解釋新增的代碼,首先打開myglwidget.h文件,將類聲明更改如下:
#ifndefMYGLWIDGET_H
#defineMYGLWIDGET_H
#include<QWidget>
#include<QGLWidget>
classMyGLWidget:publicQGLWidget
{
Q_OBJECT
public:
explicitMyGLWidget(QWidget*parent=0);
~MyGLWidget();
protected:
//對3個純虛函數的重定義
voidinitializeGL();
voidresizeGL(intw,inth);
voidpaintGL();
voidkeyPressEvent(QKeyEvent*event);//處理鍵盤按下事件
private:
voidbuildFont();//創建字體
voidkillFont();//刪除顯示列表
voidglPrint(constchar*fmt,...);//輸出字符串
private:
boolfullscreen;//是否全屏顯示
HDCm_HDC;//儲存當前設備的指針
GLYPHMETRICSFLOATm_Gmf[256];//記錄256個字符的信息
GLfloatm_Deep;//移入屏幕的距離
GLuintm_Base;//儲存繪制字體的顯示列表的開始位置
GLfloatm_Rot;//用于旋轉字體
QStringm_FileName;//圖片的路徑及文件名
GLuintm_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";//應根據實際存放圖片的路徑進行修改
HWNDhWND=(HWND)winId();//獲取當前窗口句柄
m_HDC=GetDC(hWND);//通過窗口句柄獲得HDC
QTimer*timer=newQTimer(this);//創建一個定時器
//將定時器的計時信號與updateGL()綁定
connect(timer,SIGNAL(timeout()),this,SLOT(updateGL()));
timer->start(10);//以10ms為一個計時周期
}
繼續,我們需要略微修改一下buildFont()函數,修改后代碼如下:
voidMyGLWidget::buildFont()//創建位圖字體
{
HFONTfont;//字體句柄
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,//FamilyandPitch的設置
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()函數,這是本節課的重點部分,具體代碼如下:
voidMyGLWidget::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()函數,具體代碼如下:
voidMyGLWidget::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文件,將類聲明更改如下:
#ifndefMYGLWIDGET_H
#defineMYGLWIDGET_H
#include<QWidget>
#include<QGLWidget>
classMyGLWidget:publicQGLWidget
{
Q_OBJECT
public:
explicitMyGLWidget(QWidget*parent=0);
~MyGLWidget();
protected:
//對3個純虛函數的重定義
voidinitializeGL();
voidresizeGL(intw,inth);
voidpaintGL();
voidkeyPressEvent(QKeyEvent*event);//處理鍵盤按下事件
private:
boolfullscreen;//是否全屏顯示
QStringm_FileName;//圖片的路徑及文件名
GLuintm_Texture;//儲存一個紋理
boolm_Light;//光源的開/關
GLuintm_Fog;//霧的模式
GLfloatm_xRot;//x旋轉角度
GLfloatm_yRot;//y旋轉角度
GLfloatm_xSpeed;//x旋轉速度
GLfloatm_ySpeed;//y旋轉速度
GLfloatm_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=newQTimer(this);//創建一個定時器
//將定時器的計時信號與updateGL()綁定
connect(timer,SIGNAL(timeout()),this,SLOT(updateGL()));
timer->start(10);//以10ms為一個計時周期
}
我們給m_Fog賦初始值0,表示第一種霧模式(具體是哪一種下面會講到)。
然后我們需要來修改initializeGL()函數,霧效果的數據初始化都這里完成的,具體代碼如下:
voidMyGLWidget::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);//告訴系統對透視進行修正
//光源部分
GLfloatLightAmbient[]={0.5f,0.5f,0.5f,1.0f};//環境光參數
GLfloatLightDiffuse[]={1.0f,1.0f,1.0f,1.0f};//漫散光參數
GLfloatLightPosition[]={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);//啟動一號光源
//霧部分
GLfloatfogColor[]={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()啟用了霧效果,注意沒有這行是無法產生無效果的。
最后是關于鍵盤控制函數的修改,我們將利用它來控制霧模式的切換,具體代碼如下:
voidMyGLWidget::keyPressEvent(QKeyEvent*event)
{
staticGLuintfogMode[]={GL_EXP,GL_EXP2,GL_LINEAR};
switch(event->key())
{
caseQt::Key_F1://F1為全屏和普通屏的切換鍵
fullscreen=!fullscreen;
if(fullscreen)
{
showFullScreen();
}
else
{
showNormal();
}
break;
caseQt::Key_Escape://ESC為退出鍵
close();
break;
caseQt::Key_L://L為開啟關閉光源的切換鍵
m_Light=!m_Light;
if(m_Light)
{
glEnable(GL_LIGHTING);//開啟光源
}
else
{
glDisable(GL_LIGHTING);//關閉光源
}
break;
caseQt::Key_G://G為霧模式的切換鍵
m_Fog++;
if(m_Fog==3)
{
m_Fog=0;
}
glFogi(GL_FOG_MODE,fogMode[m_Fog]);
break;
caseQt::Key_PageUp://PageUp按下使木箱移向屏幕內部
m_Deep-=0.1f;
break;
caseQt::Key_PageDown://PageDown按下使木箱移向觀察者
m_Deep+=0.1f;
break;
caseQt::Key_Up://Up按下減少m_xSpeed
m_xSpeed-=0.1f;
break;
caseQt::Key_Down://Down按下增加m_xSpeed
m_xSpeed+=0.1f;
break;
caseQt::Key_Right://Right按下減少m_ySpeed
m_ySpeed-=0.1f;
break;
caseQt::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文件,將類聲明更改如下:
#ifndefMYGLWIDGET_H
#defineMYGLWIDGET_H
#include<QWidget>
#include<QGLWidget>
classMyGLWidget:publicQGLWidget
{
Q_OBJECT
public:
explicitMyGLWidget(QWidget*parent=0);
~MyGLWidget();
protected:
//對3個純虛函數的重定義
voidinitializeGL();
voidresizeGL(intw,inth);
voidpaintGL();
voidkeyPressEvent(QKeyEvent*event);//處理鍵盤按下事件
private:
voidbuildFont();//創建字體
voidkillFont();//刪除顯示列表
//輸出字符串
voidglPrint(GLuintx,GLuinty,constchar*string,intset);
private:
boolfullscreen;//是否全屏顯示
GLuintm_Base;//儲存繪制字體的顯示列表的開始位置
GLfloatm_Cnt1;//字體移動計數器1
GLfloatm_Cnt2;//字體移動計數器2
QStringm_FileName[2];//圖片的路徑及文件名
GLuintm_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=newQTimer(this);//創建一個定時器
//將定時器的計時信號與updateGL()綁定
connect(timer,SIGNAL(timeout()),this,SLOT(updateGL()));
timer->start(10);//以10ms為一個計時周期
}
[cpp]view plaincopy
MyGLWidget::~MyGLWidget()
{
killFont();//刪除顯示列表
}
繼續,我們要來定義我們增加的三個函數,同樣是重頭戲,具體代碼如下:
voidMyGLWidget::buildFont()//創建位圖字體
{
floatcx,cy;//儲存字符的x、y坐標
m_Base=glGenLists(256);//創建256個顯示列表
glBindTexture(GL_TEXTURE_2D,m_Texture[0]);//選擇字符紋理
for(inti=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();//字符顯示列表完成
}
}
voidMyGLWidget::killFont()//刪除顯示列表
{
glDeleteLists(m_Base,256);//刪除256個顯示列表
}
voidMyGLWidget::glPrint(GLuintx,GLuinty,//輸入字符串
constchar*string,intset)
{
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()函數,具體代碼如下:
voidMyGLWidget::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()函數,這次難度還行,具體代碼如下:
voidMyGLWidget::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,
"GiuseppeD'Agata",0);
glColor3f(1.0f,1.0f,1.0f);
glPrint(int(242+200*cos((m_Cnt1+m_Cnt2)/5)),2,
"GiuseppeD'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文件,將類聲明更改如下:
#ifndefMYGLWIDGET_H
#defineMYGLWIDGET_H
#include<QWidget>
#include<QGLWidget>
classGLUquadric;
classMyGLWidget:publicQGLWidget
{
Q_OBJECT
public:
explicitMyGLWidget(QWidget*parent=0);
~MyGLWidget();
protected:
//對3個純虛函數的重定義
voidinitializeGL();
voidresizeGL(intw,inth);
voidpaintGL();
voidkeyPressEvent(QKeyEvent*event);//處理鍵盤按下事件
private:
voidglDrawCube();//繪制立方體
private:
boolfullscreen;//是否全屏顯示
QStringm_FileName;//圖片的路徑及文件名
GLuintm_Texture;//儲存一個紋理
boolm_Light;//光源的開/關
GLfloatm_xRot;//x旋轉角度
GLfloatm_yRot;//y旋轉角度
GLfloatm_xSpeed;//x旋轉速度
GLfloatm_ySpeed;//y旋轉速度
GLfloatm_Deep;//深入屏幕的距離
intm_Part1;//圓盤的起始角度
intm_Part2;//圓盤的結束角度
intm_P1;//增量1
intm_P2;//增量2
GLUquadric*m_Quadratic;//二次幾何體
GLuintm_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=newQTimer(this);//創建一個定時器
//將定時器的計時信號與updateGL()綁定
connect(timer,SIGNAL(timeout()),this,SLOT(updateGL()));
timer->start(10);//以10ms為一個計時周期
}
MyGLWidget::~MyGLWidget()
{
gluDeleteQuadric(m_Quadratic);
}
繼續,我們需要定義我們新增的glDrawCube()函數了,其實就是畫一個紋理立方體,完全可以從第07課的paintGL()函數中復制過來,不再多作解釋,代碼如下:
voidMyGLWidget::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的初始化,具體代碼如下:
voidMyGLWidget::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);//使用紋理
//光源部分
GLfloatLightAmbient[]={0.5f,0.5f,0.5f,1.0f};//環境光參數
GLfloatLightDiffuse[]={1.0f,1.0f,1.0f,1.0f};//漫散光參數
GLfloatLightPosition[]={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()函數看起來趨于簡化,具體代碼如下:
voidMyGLWidget::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)
{
case0://繪制立方體
glDrawCube();
break;
case1://繪制圓柱體
glTranslatef(0.0f,0.0f,-1.5f);
gluCylinder(m_Quadratic,1.0f,1.0f,3.0f,64,64);
break;
case2://繪制圓盤
gluDisk(m_Quadratic,0.5f,1.5f,64,64);
break;
case3://繪制球
gluSphere(m_Quadratic,1.3f,64,64);
break;
case4://繪制圓錐
glTranslatef(0.0f,0.0f,-1.5f);
gluCylinder(m_Quadratic,1.0f,0.0f,3.0f,64,64);
break;
case5://繪制部分圓盤
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度,我們將開始增加開始角度,這樣盤子看起來就像是被逐漸地抹去一樣,我們將重復這兩個過程。
最后我們修改一下鍵盤控制函數,不多解釋了,具體代碼如下:
voidMyGLWidget::keyPressEvent(QKeyEvent*event)
{
switch(event->key())
{
caseQt::Key_F1://F1為全屏和普通屏的切換鍵
fullscreen=!fullscreen;
if(fullscreen)
{
showFullScreen();
}
else
{
showNormal();
}
break;
caseQt::Key_Escape://ESC為退出鍵
close();
break;
caseQt::Key_L://L為開啟關閉光源的切換鍵
m_Light=!m_Light;
if(m_Light)
{
glEnable(GL_LIGHTING);//開啟光源
}
else
{
glDisable(GL_LIGHTING);//關閉光源
}
break;
caseQt::Key_Space://空格為物體的切換鍵
m_Object++;
if(m_Object==6)
{
m_Object=0;
}
break;
caseQt::Key_PageUp://PageUp按下使木箱移向屏幕內部
m_Deep-=0.1f;
break;
caseQt::Key_PageDown://PageDown按下使木箱移向觀察者
m_Deep+=0.1f;
break;
caseQt::Key_Up://Up按下減少m_xSpeed
m_xSpeed-=0.1f;
break;
caseQt::Key_Down://Down按下增加m_xSpeed
m_xSpeed+=0.1f;
break;
caseQt::Key_Right://Right按下減少m_ySpeed
m_ySpeed-=0.1f;
break;
caseQt::Key_Left://Left按下增加m_ySpeed
m_ySpeed+=0.1f;
break;
}
}
現在就可以運行程序查看效果了!
第19課:粒子系統(參照NeHe)
這次教程中,我們將創建一個簡單的粒子系統,并用它來創建一種噴射效果。利用粒子系統,我們可以實現爆炸、噴泉、流星之類的效果,聽起來是不是很棒呢!
我們還會講到一個新東西,三角形帶(我的理解就是畫很多三角形來組合成我們要的形狀),它非常容易使用,而且當需要畫很多三角形的時候,它能加快你程序的運行速度。這次教程中,我將教你該如何做一個簡單的微粒程序,一旦你了解微粒程序的原理后,再創建例如:火、煙、噴泉等效果將是很輕松的事情。
程序運行時效果如下:
下面進入教程:
我們這次將在第06課代碼的基礎上修改代碼,這次需要修改的代碼量不少,希望大家耐心跟著我一步步來完成這個程序。首先打開myglwidget.h文件,將類聲明更改如下:
#ifndefMYGLWIDGET_H
#defineMYGLWIDGET_H
#include<QWidget>
#include<QGLWidget>
classMyGLWidget:publicQGLWidget
{
Q_OBJECT
public:
explicitMyGLWidget(QWidget*parent=0);
~MyGLWidget();
protected:
//對3個純虛函數的重定義
voidinitializeGL();
voidresizeGL(intw,inth);
voidpaintGL();
voidkeyPressEvent(QKeyEvent*event);//處理鍵盤按下事件
private:
boolfullscreen;//是否全屏顯示
QStringm_FileName;//圖片的路徑及文件名
GLuintm_Texture;//儲存一個紋理
staticconstintMAX_PARTICLES=1000;//最大粒子數
staticconstGLfloatCOLORS[12][3];//彩虹的顏色
boolm_Rainbow;//是否為彩虹模式
GLuintm_Color;//當前的顏色
floatm_Slowdown;//減速粒子
floatm_xSpeed;//x方向的速度
floatm_ySpeed;//y方向的速度
floatm_Deep;//移入屏幕的距離
structParticle//創建粒子結構體
{
boolactive;//是否激活
floatlife;//粒子生命
floatfade;//衰減速度
floatr,g,b;//粒子顏色
floatx,y,z;//位置坐標
floatxi,yi,zi;//各方向速度
floatxg,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,在構造函數中對新增變量進行初始化,具體代碼如下:
constGLfloatMyGLWidget::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(inti=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=newQTimer(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()函數,代碼如下:
voidMyGLWidget::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()函數了,具體代碼如下:
voidMyGLWidget::paintGL()//從這里開始進行所以的繪制
{
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);//清除屏幕和深度緩存
glLoadIdentity();//重置模型觀察矩陣
glBindTexture(GL_TEXTURE_2D,m_Texture);
for(inti=0;i<MAX_PARTICLES;i++)//循環所以的粒子
{
if(m_Particles[i].active)//如果粒子為激活的
{
floatx=m_Particles[i].x;//x軸位置
floaty=m_Particles[i].y;//y軸位置
floatz=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,這個控制了粒子大體得移動方向。最后我們給粒子分配當前的顏色就搞定循環了。
函數最后,我們判斷是否為彩虹模式,如果是就改變當前的顏色,這樣不同時間“重生”后的粒子就可能得到不同的顏色,從而出現彩虹效果。
最后就是鍵盤控制了,由于為了增加點趣味性,這次鍵盤控制比較“麻煩”,但是調理很清晰,具體代碼如下:
voidMyGLWidget::keyPressEvent(QKeyEvent*event)
{
switch(event->key())
{
caseQt::Key_F1://F1為全屏和普通屏的切換鍵
fullscreen=!fullscreen;
if(fullscreen)
{
showFullScreen();
}
else
{
showNormal();
}
updateGL();
break;
caseQt::Key_Escape://ESC為退出鍵
close();
break;
caseQt::Key_Tab://Tab按下使粒子回到原點,產生爆炸
for(inti=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;
caseQt::Key_8://按下8增加y方向加速度
for(inti=0;i<MAX_PARTICLES;i++)
{
if(m_Particles[i].yg<3.0f)
{
m_Particles[i].yg+=0.05f;
}
}
break;
caseQt::Key_2://按下2減少y方向加速度
for(inti=0;i<MAX_PARTICLES;i++)
{
if(m_Particles[i].yg>-3.0f)
{
m_Particles[i].yg-=0.05f;
}
}
break;
caseQt::Key_6://按下6增加x方向加速度
for(inti=0;i<MAX_PARTICLES;i++)
{
if(m_Particles[i].xg<3.0f)
{
m_Particles[i].xg+=0.05f;
}
}
break;
caseQt::Key_4://按下4減少x方向加速度
for(inti=0;i<MAX_PARTICLES;i++)
{
if(m_Particles[i].xg>-3.0f)
{
m_Particles[i].xg-=0.05f;
}
}
break;
caseQt::Key_Plus://+號按下加速粒子
if(m_Slowdown>1.0f)
{
m_Slowdown-=0.05f;
}
break;
caseQt::Key_Minus://-號按下減速粒子
if(m_Slowdown<3.0f)
{
m_Slowdown+=0.05f;
}
break;
caseQt::Key_PageUp://PageUp按下使粒子靠近屏幕
m_Deep+=0.5f;
break;
caseQt::Key_PageDown://PageDown按下使粒子遠離屏幕
m_Deep-=0.5f;
break;
caseQt::Key_Return://回車鍵為是否彩虹模式的切換鍵
m_Rainbow=!m_Rainbow;
break;
caseQt::Key_Space://空格鍵為顏色切換鍵
m_Rainbow=false;
m_Color++;
if(m_Color>11)
{
m_Color=0;
}
break;
caseQt::Key_Up://Up按下增加粒子y軸正方向的速度
if(m_ySpeed<400.0f)
{
m_ySpeed+=5.0f;
}
break;
caseQt::Key_Down://Down按下減少粒子y軸正方向的速度
if(m_ySpeed>-400.0f)
{
m_ySpeed-=5.0f;
}
break;
caseQt::Key_Right://Right按下增加粒子x軸正方向的速度
if(m_xSpeed<400.0f)
{
m_xSpeed+=5.0f;
}
break;
caseQt::Key_Left://Left按下減少粒子x軸正方向的速度
if(m_xSpeed>-400.0f)
{
m_xSpeed-=5.0f;
}
break;
}
}
我感覺注釋已經寫得比較清楚了,就不解釋太多了,具體里面的值是怎么得到的,其實就是一點點嘗試,感覺效果好久用了,就這么簡單!大家注意一下Tab鍵按下后,全部粒子會回到原點,重新從原點出發,并且我們給它們重新生成速度,方式和初始化時是相同的,這樣就又產生了爆炸效果。
現在就可以運行程序查看效果了!
第20課:蒙板(參照NeHe)
這次教程中,我們教介紹OpenGL的蒙板技術。到目前為止,我們已經學會如何使用alpha混合,把一個透明物體渲染到屏幕上了,但有時使用它看起來并不是那么的復合我們的心意。使用蒙板技術,將會使圖像按照我們設定的蒙板位置精確地繪制。
直到現在,我們在把圖像加載到屏幕上時都沒有檫除背景色,因為這樣簡單高效,但是效果并不總是很好。大部分情況下,把紋理混合到屏幕,紋理不是太少就是太多。當我們使用精靈圖時,我們不希望背景從精靈的縫隙中透出光來;但在顯示文字時,我們又希望文字的間隙可以顯示背景色。
基于上述原因,我們需要使用“掩模”。使用“掩膜”需要兩個步驟,首先我們在場景上放置黑白相間的紋理,白色代表透明部分,黑色代表不透明部分。接著我們使用一種特殊的混合方式,只有在黑色部分上的紋理才會顯示在場景中。
程序運行時效果如下:
下面進入教程:
我們這次將在第06課代碼的基礎上修改代碼,總體上并不會太難,希望大家能理解蒙板技術,這技術真的很好用。首先打開myglwidget.h文件,將類聲明更改如下:
#ifndefMYGLWIDGET_H
#defineMYGLWIDGET_H
#include<QWidget>
#include<QGLWidget>
classMyGLWidget:publicQGLWidget
{
Q_OBJECT
public:
explicitMyGLWidget(QWidget*parent=0);
~MyGLWidget();
protected:
//對3個純虛函數的重定義
voidinitializeGL();
voidresizeGL(intw,inth);
voidpaintGL();
voidkeyPressEvent(QKeyEvent*event);//處理鍵盤按下事件
private:
boolfullscreen;//是否全屏顯示
boolm_Masking;//是否使用"<spanstyle="font-size:12px;">掩模</span>"
boolm_Scene;//控制繪制哪一層
GLfloatm_Rot;//控制紋理滾動
QStringm_FileName[5];//圖片的路徑及文件名
GLuintm_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";//<spanstyle="font-size:12px;">掩模</span>紋理1,作為"<spanstyle="font-size:12px;">掩模</span>"使用
m_FileName[2]="D:/QtOpenGL/QtImage/Image1.bmp";//紋理1
m_FileName[3]="D:/QtOpenGL/QtImage/Mask2.bmp";//<spanstyle="font-size:12px;">掩模</span>紋理2,作為"<spanstyle="font-size:12px;">掩模</span>"使用
m_FileName[4]="D:/QtOpenGL/QtImage/Image2.bmp";//紋理2
QTimer*timer=newQTimer(this);//創建一個定時器
//將定時器的計時信號與updateGL()綁定
connect(timer,SIGNAL(timeout()),this,SLOT(updateGL()));
timer->start(10);//以10ms為一個計時周期
}
然后,我們略微修改下initializeGL()函數,就是載入5個位圖并轉換成紋理,不多解釋了,具體代碼如下:
voidMyGLWidget::initializeGL()//此處開始對OpenGL進行所以設置
{
//載入位圖并轉換成紋理
for(inti=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);//設置深度緩存<prename="code"class="cpp"><prename="code"class="cpp">}
繼續,我們要進入最有趣的paintGL()函數,當然這也是重點,具體代碼如下:
voidMyGLWidget::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)//是否啟用"<spanstyle="font-size:12px;">掩模</span>"
{
glBlendFunc(GL_DST_COLOR,GL_ZERO);//使用黑白"<spanstyle="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)//"<spanstyle="font-size:12px;">掩模</span>"是否打開
{
glBindTexture(GL_TEXTURE_2D,m_Texture[3]);//選擇第二個"<spanstyle="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)//"<spanstyle="font-size:12px;">掩模</span>"是否打開
{
glBindTexture(GL_TEXTURE_2D,m_Texture[1]);//選擇第一個"<spanstyle="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鍵作為切換鍵,很簡單不多解釋了,具體代碼如下:
voidMyGLWidget::keyPressEvent(QKeyEvent*event)
{
switch(event->key())
{
caseQt::Key_F1://F1為全屏和普通屏的切換鍵
fullscreen=!fullscreen;
if(fullscreen)
{
showFullScreen();
}
else
{
showNormal();
}
updateGL();
break;
caseQt::Key_Escape://ESC為退出鍵
close();
break;
caseQt::Key_Space://空格為場景(圖層)的切換鍵
m_Scene=!m_Scene;
break;
caseQt::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方向。
總結
以上是生活随笔為你收集整理的Qt Creator中的3D绘图及动画教程(参照NeHe)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++11中的std::function
- 下一篇: Polygon Cruncher减边用法