這次教程中,我們將學會如何使用四邊形紋理貼圖把文字顯示在屏幕上。我們將把256個不同的文字從一個256×256的紋理圖像中一個個提取出來,接着創建一個輸出函數來創建任意我們希望的文字。
還記得在第一篇字體教程中我提到使用紋理在屏幕上繪制文字嗎?通常當你使用紋理繪制文字時你會調用你最喜歡的圖像處理程序,選擇一種字體,然后輸入你想顯示的文字或段落,然后保存下來位圖並把它作為紋理讀入到你的程序里,問題是這對一個需要很多文字或者文字在不停變化的程序來說,這么做效率並不高。這次教程中我們只使用一個紋理來顯示任意256個不同的字符。
程序運行時效果如下:
下面進入教程:
由於相較於之前幾課字體教程的代碼改動較大,我們將直接在第01課的基礎上修改代碼,我會一一解釋新增的代碼,首先myglwidget.h文件,將類聲明更改如下:
1 #ifndef MYGLWIDGET_H 2 #define MYGLWIDGET_H
3
4 #include <QWidget>
5 #include <QGLWidget>
6
7 class MyGLWidget : public QGLWidget 8 { 9 Q_OBJECT 10 public: 11 explicit MyGLWidget(QWidget *parent = 0); 12 ~MyGLWidget(); 13
14 protected: 15 //對3個純虛函數的重定義
16 void initializeGL(); 17 void resizeGL(int w, int h); 18 void paintGL(); 19
20 void keyPressEvent(QKeyEvent *event); //處理鍵盤按下事件
21
22 private: 23 void buildFont(); //創建字體
24 void killFont(); //刪除顯示列表 25 //輸出字符串
26 void glPrint(GLuint x, GLuint y, const char *string, int set); 27
28 private: 29 bool fullscreen; //是否全屏顯示
30
31 GLuint m_Base; //儲存繪制字體的顯示列表的開始位置
32 GLfloat m_Cnt1; //字體移動計數器1
33 GLfloat m_Cnt2; //字體移動計數器2
34
35 QString m_FileName[2]; //圖片的路徑及文件名
36 GLuint m_Texture[2]; //儲存兩個紋理
37 }; 38
39 #endif // MYGLWIDGET_H
我們增加了變量m_Base、m_Cnt1、m_Cnt2,函數聲明buildFont()、killFont(),這些和之前都講過的作用都一樣就不重復了。而m_FileName和m_Texture變為了長度為2的數組,這是因為程序中我們會用兩種不同的圖來建立兩個不同的紋理。最后是glPrint()函數的聲明,注意下它的參數和前幾課不同,但作用是相同的,具體的下面會講到。
接下來,我們需要打開myglwidget.cpp,加入聲明#include <QTimer>、#include<QtMath>,將構造函數和析構函數修改如下(比較簡單不具體解釋了):
1 MyGLWidget::MyGLWidget(QWidget *parent) : 2 QGLWidget(parent) 3 { 4 fullscreen = false; 5 m_Cnt1 = 0.0f; 6 m_Cnt2 = 0.0f; 7 m_FileName[0] = "D:/QtOpenGL/QtImage/Font.bmp"; //應根據實際存放圖片的路徑進行修改
8 m_FileName[1] = "D:/QtOpenGL/QtImage/Bumps.bmp"; 9
10 QTimer *timer = new QTimer(this); //創建一個定時器 11 //將定時器的計時信號與updateGL()綁定
12 connect(timer, SIGNAL(timeout()), this, SLOT(updateGL())); 13 timer->start(10); //以10ms為一個計時周期
14 }
1 MyGLWidget::~MyGLWidget() 2 { 3 killFont(); //刪除顯示列表
4 }
繼續,我們要來定義我們增加的三個函數,同樣是重頭戲,具體代碼如下:
1 void MyGLWidget::buildFont() //創建位圖字體
2 { 3 float cx, cy; //儲存字符的x、y坐標
4 m_Base = glGenLists(256); //創建256個顯示列表
5 glBindTexture(GL_TEXTURE_2D, m_Texture[0]); //選擇字符紋理
6
7 for (int i=0; i<256; i++) //循環256個顯示列表
8 { 9 cx = float(i%16)/16.0f; //當前字符的x坐標
10 cy = float(i/16)/16.0f; //當前字符的y坐標
11
12 glNewList(m_Base+i, GL_COMPILE); //開始創建顯示列表
13 glBegin(GL_QUADS); //使用四邊形顯示每一個字符
14 glTexCoord2f(cx, 1-cy-0.0625f); 15 glVertex2i(0, 0); 16 glTexCoord2f(cx+0.0625f, 1-cy-0.0625f); 17 glVertex2i(16, 0); 18 glTexCoord2f(cx+0.0625f, 1-cy); 19 glVertex2i(16, 16); 20 glTexCoord2f(cx, 1-cy); 21 glVertex2i(0, 16); 22 glEnd(); //四邊形字符繪制完成
23 glTranslated(10, 0, 0); //繪制完一個字符,向右平移10個單位
24 glEndList(); //字符顯示列表完成
25 } 26 }
1 void MyGLWidget::killFont() //刪除顯示列表
2 { 3 glDeleteLists(m_Base, 256); //刪除256個顯示列表
4 }
1 void MyGLWidget::glPrint(GLuint x, GLuint y, //輸入字符串
2 const char *string, int set) 3 { 4 if (set > 1) //如果字符集大於1
5 { 6 set = 1; //設置其為1
7 } 8
9 glBindTexture(GL_TEXTURE_2D, m_Texture[0]); //綁定為字體紋理
10 glDisable(GL_DEPTH_TEST); //禁止深度測試
11 glMatrixMode(GL_PROJECTION); //選擇投影矩陣
12 glPushMatrix(); //保存當前的投影矩陣
13 glLoadIdentity(); //重置投影矩陣
14 glOrtho(0, 640, 0, 480, -1, 1); //設置正投影的可視區域
15 glMatrixMode(GL_MODELVIEW); //選擇模型觀察矩陣
16 glPushMatrix(); //保存當前的模型觀察矩陣
17 glLoadIdentity(); //重置模型觀察矩陣
18
19 glTranslated(x, y ,0); //把字符原點移動到(x,y)位置
20 glListBase(m_Base-32+(128*set)); //選擇字符集
21 glCallLists(strlen(string), GL_BYTE, string); //把字符串寫到屏幕
22 glMatrixMode(GL_PROJECTION); //選擇投影矩陣
23 glPopMatrix(); //設置為保存的矩陣
24 glMatrixMode(GL_MODELVIEW); //選擇模型觀察矩陣
25 glPopMatrix(); //設置為保存
26 glEnable(GL_DEPTH_TEST); //啟用深度測試
27 }
首先是buildFont()函數。我們先是定義兩個臨時變量來儲存字體紋理中每個字的位置,cx儲存水平方向位置,cy儲存豎直方向位置。接着我們告訴OpenGL我們要建立256個顯示列表,變量m_Base指向第一個顯示列表,然后選擇我們的字體紋理。現在我們開始循環,來創建所以256個字符,並存在顯示列表里。一開始我們計算得到cx、cy,對16取余和除以16是由於一行是16個字符,最后都除以16.0f是按16個字符把紋理寬度高度均為1.0分成16份。
后面就開始創建顯示列表了,繪制四邊形對應紋理時,+或-0.0625f是指一個字符的高或寬,還有由於紋理坐標(0, 0)是在左下角,所以glTexCoord2f(x, y)的第二參數是1-cy、1-cy-0.0625而不是cy、cy+0.0625(比如說cx、cy同時為0,那它對應的字符紋理左下角坐標就應是(0.0, 1-0.0f-0.0625f)了,希望大家能明白)。要注意的是,我們使用glVertex2i()而不是glVertex3f(),我們的字體是2D字體,所以不需要z值。因為我們使用的是正交投影,我們不需要移進屏幕,在一個正交投影平面繪圖你所需要的是指定x、y坐標。又因為我們的屏幕是以像素形式從0到639(寬),從0到479(高),因此我們既不需要用浮點數也不需要負數。
畫完四邊形后,我們右移了10個像素,至於紋理有病。如果我們不平移,文字將會重疊。有由於我們的字體太窄太瘦,我們不想右移16個像素那么多,如果那樣的話,每個字符之間將有很大的間隔,只移動10個像素是個不錯的選擇。
接着是killFont()函數。它很簡單,就是調用glDeleteLists()函數從m_Base開始刪除256個顯示列表。
最后是glPrint()函數。首先我們判斷一下set字符集,如果大於1,就將set置0。這是由於我們的字體紋理中只有兩種字體,第一種是普通的,第二種是斜體,如果選擇的字符集有誤,我們就把它設為默認的普通字符集。接着我們再次選擇字體紋理,我們這么做事防止我們在決定往屏幕上輸出文字前選擇了別的紋理,導致出錯。然后我們禁用了深度測試,我們這么做事因為混合的效果會更好。如果我們不禁用深度測試,文字可能會被什么東西擋住,或者得不到正確的混合效果。當然,如果你不打算混合文字(那樣文字周圍的黑色區域就不會顯示),你就可以啟用深度測試。
下面幾行十分重要!我們選擇投影矩陣,然后調用glPushMatrix()函數,保存當前投影矩陣(其實就是把投影矩陣壓入堆棧)。保存投影矩陣后,我們重置矩陣並調用glOrtho()設置正交投影屏幕,第一和第三個參數表示屏幕的底邊和左邊,第二和第四個參數表示屏幕的上邊和右邊。由於我們不需要用到深度測試,所以我們將第五和第六個參數設為-1和1。我們再選擇模型觀察矩陣,用glPushMatrix()保存當前設置。然后我們重置模型觀察矩陣以便在正交投影視點下工作。
現在我們可以繪制文字了,我們從移動到繪制文字的位置開始。我們使用glTranslated()而不是glTranslatef(),因為我們處理的是像素,所以浮點數沒有意義。接着我們用glListBase()來選擇字符集,如果我們想使用第二個字符集,我們在當前的顯示列表基數上加上128,通過加128,我們跳過了前128個字符。而減去32是因為我們的字符集是從“空格”開始的,即ASCII碼第33個字符開始,故我們減去32,告訴OpenGL跳過前面32個字符。然后我們使用glCallLists()繪制文字,這個之前解釋過,不再解釋了。
最后我們要做的是恢復透視視圖。我們選擇投影矩陣並用glPopMatrix()恢復我們先前glPushMatrix()保存的設置,接着選擇模型觀察矩陣做相同的工作。你或許會問,難道不用按相反順序彈出矩陣嗎?不用,這是用於投影矩陣和模型觀察矩陣的堆棧並不是同一個(這樣說其實並不准確,不過道理是差不多的),所以無論選擇哪個矩陣先彈出都沒有問題。值得注意的是,如果你把代碼中的最后兩句glMatrixMode()調換位置,運行程序時你是看不到圖像紋理的只能看到文字,這是由於我們最后選擇的矩陣是GL_PROJECTION,而我們繪制圖像紋理是在GL_MODEWIEW上繪制的,所以你看不到圖像紋理。當然解決辦法就是,在恢復了投影矩陣后,開始深度測試之前,再次調用glMatrix()選擇模型觀察矩陣GL_MODEVIEW。那為什么我們能看到文字呢?這是由於做了平面正交投影后,在任何矩陣所繪制的東西都是在平面繪制的,OpenGL自動會把它們投影到屏幕上,所以總能看到文字的。函數最后我們啟用了深度測試,如果你沒有在上面的代碼中關閉深度測試,就不需要這行。
然后我們修改一下initializeGL()函數,具體代碼如下:
1 void MyGLWidget::initializeGL() //此處開始對OpenGL進行所以設置
2 { 3 m_Texture[0] = bindTexture(QPixmap(m_FileName[0])); //載入位圖並轉換成紋理
4 m_Texture[1] = bindTexture(QPixmap(m_FileName[1])); 5 glEnable(GL_TEXTURE_2D); //啟用紋理映射
6
7 glClearColor(0.0, 0.0, 0.0, 0.0); //黑色背景
8 glShadeModel(GL_SMOOTH); //啟用陰影平滑
9 glClearDepth(1.0); //設置深度緩存
10 glEnable(GL_DEPTH_TEST); //啟用深度測試
11 glDepthFunc(GL_LEQUAL); //所作深度測試的類型
12 glBlendFunc(GL_SRC_ALPHA, GL_ONE); //設置混合因子
13 glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告訴系統對透視進行修正
14
15 buildFont(); //創建字體
16 }
最開始三行載入位圖轉換紋理,啟用紋理映射就不解釋了。中間部分有小的改動,由於我們要給字體上色,所以要設置混合因子。最后調用buildFont()創建字體。
最后,我們該進入paintGL()函數,這次難度還行,具體代碼如下:
1 void MyGLWidget::paintGL() //從這里開始進行所以的繪制
2 { 3 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度緩存
4 glLoadIdentity(); //重置當前的模型觀察矩陣
5
6 glBindTexture(GL_TEXTURE_2D, m_Texture[1]); //設置為圖像紋理
7 glTranslatef(0.0f, 0.0f, -5.0f); //移入屏幕5.0單位
8 glRotatef(45.0f, 0.0f, 0.0f, 1.0f); //繞z軸旋轉45度
9 glRotatef(m_Cnt1*30.0f, 1.0f, 1.0f, 0.0f); //繞(1,1,0)軸旋轉
10 glDisable(GL_BLEND); //關閉融合
11 glColor3f(1.0f, 1.0f, 1.0f); //設置顏色為白色
12 glBegin(GL_QUADS); //繪制紋理四邊形
13 glTexCoord2d(0.0f, 0.0f); 14 glVertex2f(-1.0f, 1.0f); 15 glTexCoord2d(1.0f, 0.0f); 16 glVertex2f(1.0f, 1.0f); 17 glTexCoord2d(1.0f, 1.0f); 18 glVertex2f(1.0f, -1.0f); 19 glTexCoord2d(0.0f, 1.0f); 20 glVertex2f(-1.0f, -1.0f); 21 glEnd(); 22
23 glRotatef(90.0f, 1.0f, 1.0f, 0.0); //繞(1,1,0)軸旋轉90度
24 glBegin(GL_QUADS); //繪制第二個四邊形,與第一個垂直
25 glTexCoord2d(0.0f, 0.0f); 26 glVertex2f(-1.0f, 1.0f); 27 glTexCoord2d(1.0f, 0.0f); 28 glVertex2f(1.0f, 1.0f); 29 glTexCoord2d(1.0f, 1.0f); 30 glVertex2f(1.0f, -1.0f); 31 glTexCoord2d(0.0f, 1.0f); 32 glVertex2f(-1.0f, -1.0f); 33 glEnd(); 34
35 glEnable(GL_BLEND); //啟用混合
36 glLoadIdentity(); //重置視口 37 //根據字體位置設置顏色
38 glColor3f(1.0f*float(cos(m_Cnt1)), 1.0*float(sin(m_Cnt2)), 39 1.0f-0.5f*float(cos(m_Cnt1+m_Cnt2))); 40 glPrint(int((280+250*cos(m_Cnt1))), 41 int(235+200*sin(m_Cnt2)), "NeHe", 0); 42 glColor3f(1.0*float(sin(m_Cnt2)), 1.0f-0.5f*float(cos(m_Cnt1+m_Cnt2)), 43 1.0f*float(cos(m_Cnt1))); 44 glPrint(int((280+230*cos(m_Cnt2))), 45 int(235+200*sin(m_Cnt1)), "OpenGL", 1); 46 glColor3f(0.0f, 0.0f, 1.0f); 47 glPrint(int(240+200*cos((m_Cnt1+m_Cnt2)/5)), 2, 48 "Giuseppe D'Agata", 0); 49 glColor3f(1.0f, 1.0f, 1.0f); 50 glPrint(int(242+200*cos((m_Cnt1+m_Cnt2)/5)), 2, 51 "Giuseppe D'Agata", 0); 52
53 m_Cnt1 += 0.01f; //增加兩個計數器的值
54 m_Cnt2 += 0.0081f; 55 }
函數中我們先繪制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物體自轉。
現在就可以運行程序查看效果了!