Qt OpenGL 2D圖像文字


這次教程中,我們將學會如何使用四邊形紋理貼圖把文字顯示在屏幕上。我們將把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物體自轉。

現在就可以運行程序查看效果了!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM