轉自網摘 源地址己找不到。沒示例程序。
增加了兩個文件,showline.c, showtext.c。分別為第二個和第三個示例程序的main函數相關部分。
在ctbuf.h和textarea.h最開頭部分增加了一句#include <stdlib.h>
附件中一共有三個示例程序:
第一個,飄動的“曹”字旗。代碼為:flag.c, GLee.c, GLee.h
第二個,帶緩沖的顯示文字。代碼為:showline.c, ctbuf.c, ctbuf.h, GLee.c, GLee.h
第三個,顯示歌詞。代碼為:showtext.c, ctbuf.c, ctbuf.h, textarea.c, textarea.h, GLee.c, GLee.h
其中,GLee.h和GLee.c可以從網上下載,因此這里並沒有放到附件中。在編譯的時候應該將這兩個文件和其它代碼文件一起編譯。
本課我們來談談如何顯示文字。
OpenGL並沒有直接提供顯示文字的功能,並且,OpenGL也沒有自帶專門的字庫。因此,要顯示文字,就必須依賴操作系統所提供的功能了。
各種流行的圖形操作系統,例如Windows系統和Linux系統,都提供了一些功能,以便能夠在OpenGL程序中方便的顯示文字。
最常見的方法就是,我們給出一個字符,給出一個顯示列表編號,然后操作系統由把繪制這個字符的OpenGL命令裝到指定的顯示列表中。當需要繪制字符的時候,我們只需要調用這個顯示列表即可。
不過,Windows系統和Linux系統,產生這個顯示列表的方法是不同的(雖然大同小異)。作為我個人,只在Windows系統中編程,沒有使用Linux系統的相關經驗,所以本課我們僅針對Windows系統。
OpenGL版的“Hello, World!”
寫完了本課,我的感受是:顯示文字很簡單,顯示文字很復雜。看似簡單的功能,背后卻隱藏了深不可測的玄機。
呵呵,別一開始就被嚇住了,讓我們先從“Hello, World!”開始。
前面已經說過了,要顯示字符,就需要通過操作系統,把繪制字符的動作裝到顯示列表中,然后我們調用顯示列表即可繪制字符。
假如我們要顯示的文字全部是ASCII字符,則總共只有0到127這128種可能,因此可以預先把所有的字符分別裝到對應的顯示列表中,然后在需要時調用這些顯示列表。
Windows系統中,可以使用wglUseFontBitmaps函數來批量的產生顯示字符用的顯示列表。函數有四個參數:
第一個參數是HDC,學過Windows GDI的朋友應該會熟悉這個。如果沒有學過,那也沒關系,只要知道調用wglGetCurrentDC函數,就可以得到一個HDC了。具體的情況可以看下面的代碼。
第二個參數表示第一個要產生的字符,因為我們要產生0到127的字符的顯示列表,所以這里填0。
第三個參數表示要產生字符的總個數,因為我們要產生0到127的字符的顯示列表,總共有128個字符,所以這里填128。
第四個參數表示第一個字符所對應顯示列表的編號。假如這里填1000,則第一個字符的繪制命令將被裝到第1000號顯示列表,第二個字符的繪制命令將被裝到第1001號顯示列表,依次類推。我們可以先用glGenLists申請128個連續的顯示列表編號,然后把第一個顯示列表編號填在這里。
還要說明一下,因為wglUseFontBitmaps是Windows系統特有的函數,所以在使用前需要加入頭文件:#include <windows.h>。
現在讓我們來看具體的代碼:
#include <windows.h> // ASCII字符總共只有0到127,一共128種字符 #define MAX_CHAR 128 void drawString(const char* str) { static int isFirstCall = 1; static GLuint lists; if( isFirstCall ) { // 如果是第一次調用,執行初始化 // 為每一個ASCII字符產生一個顯示列表 isFirstCall = 0; // 申請MAX_CHAR個連續的顯示列表編號 lists = glGenLists(MAX_CHAR); // 把每個字符的繪制命令都裝到對應的顯示列表中 wglUseFontBitmaps(wglGetCurrentDC(), 0, MAX_CHAR, lists); } // 調用每個字符對應的顯示列表,繪制每個字符 for(; *str!='\0'; ++str) glCallList(lists + *str); }
顯示列表一旦產生就一直存在(除非調用glDeleteLists銷毀),所以我們只需要在第一次調用的時候初始化,以后就可以很方便的調用這些顯示列表來繪制字符了。
繪制字符的時候,可以先用glColor*等指定顏色,然后用glRasterPos*指定位置,最后調用顯示列表來繪制。
void display(void) { glClear(GL_COLOR_BUFFER_BIT); glColor3f(1.0f, 0.0f, 0.0f); glRasterPos2f(0.0f, 0.0f); drawString("Hello, World!"); glutSwapBuffers(); }
效果如圖:
指定字體
在產生顯示列表前,Windows允許選擇字體。
我做了一個selectFont函數來實現它,大家可以看看代碼。
void selectFont(int size, int charset, const char* face) { HFONT hFont = CreateFontA(size, 0, 0, 0, FW_MEDIUM, 0, 0, 0, charset, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, face); HFONT hOldFont = (HFONT)SelectObject(wglGetCurrentDC(), hFont); DeleteObject(hOldFont); } void display(void) { selectFont(48, ANSI_CHARSET, "Comic Sans MS"); glClear(GL_COLOR_BUFFER_BIT); glColor3f(1.0f, 0.0f, 0.0f); glRasterPos2f(0.0f, 0.0f); drawString("Hello, World!"); glutSwapBuffers(); }
最主要的部分就在於那個參數超多的CreateFont函數,學過Windows GDI的朋友應該不會陌生。沒有學過GDI的朋友,有興趣的話可以自己翻翻MSDN文檔。這里我並不准備仔細講這些參數了,下面的內容還多着呢:(
如果需要在自己的程序中選擇字體的話,把selectFont函數抄下來,在調用glutCreateWindow之后、在調用 wglUseFontBitmaps之前使用selectFont函數即可指定字體。函數的三個參數分別表示了字體大小、字符集(英文字體可以用 ANSI_CHARSET,簡體中文字體可以用GB2312_CHARSET,繁體中文字體可以用CHINESEBIG5_CHARSET,對於中文的 Windows系統,也可以直接用DEFAULT_CHARSET表示默認字符集)、字體名稱。
效果如圖:
顯示中文
原則上,顯示中文和顯示英文並無不同,同樣是把要顯示的字符做成顯示列表,然后進行調用。
但是有一個問題,英文字母很少,最多只有幾百個,為每個字母創建一個顯示列表,沒有問題。但是漢字有非常多個,如果每個漢字都產生一個顯示列表,這是不切實際的。
我們不能在初始化時就為每個字符建立一個顯示列表,那就只有在每次繪制字符時創建它了。當我們需要繪制一個字符時,創建對應的顯示列表,等繪制完畢后,再將它銷毀。
這里還經常涉及到中文亂碼的問題,我對這個問題也不甚了解,但是網上流傳的版本中,使用了MultiByteToWideChar這個函數的,基本上都沒有出現亂碼,所以我也准備用這個函數:)
通常我們在C語言里面使用的字符串,如果中英文混合的話,例如“this is 中文字符.”,則英文字符只占用一個字節,而中文字符則占用兩個字節。用 MultiByteToWideChar函數,可以轉化為所有的字符都占兩個字節(同時解決了前面所說的亂碼問題:))。
轉化的代碼如下:
// 計算字符的個數 // 如果是雙字節字符的(比如中文字符),兩個字節才算一個字符 // 否則一個字節算一個字符 len = 0; for(i=0; str[i]!='\0'; ++i) { if( IsDBCSLeadByte(str[i]) ) ++i; ++len; } // 將混合字符轉化為寬字符 wstring = (wchar_t*)malloc((len+1) * sizeof(wchar_t)); MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, str, -1, wstring, len); wstring[len] = L'\0'; // 用完后記得釋放內存 free(wstring); // 加上前面所講到的wglUseFontBitmaps函數,即可顯示中文字符了。 void drawCNString(const char* str) { int len, i; wchar_t* wstring; HDC hDC = wglGetCurrentDC(); GLuint list = glGenLists(1); // 計算字符的個數 // 如果是雙字節字符的(比如中文字符),兩個字節才算一個字符 // 否則一個字節算一個字符 len = 0; for(i=0; str[i]!='\0'; ++i) { if( IsDBCSLeadByte(str[i]) ) ++i; ++len; } // 將混合字符轉化為寬字符 wstring = (wchar_t*)malloc((len+1) * sizeof(wchar_t)); MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, str, -1, wstring, len); wstring[len] = L'\0'; // 逐個輸出字符 for(i=0; i<len; ++i) { wglUseFontBitmapsW(hDC, wstring[i], 1, list); glCallList(list); } // 回收所有臨時資源 free(wstring); glDeleteLists(list, 1); }
注意我用了wglUseFontBitmapsW函數,而不是 wglUseFontBitmaps。wglUseFontBitmapsW是wglUseFontBitmaps函數的寬字符版本,它認為字符都占兩個字節。因為這里使用了MultiByteToWideChar,每個字符其實是占兩個字節的,所以應該用wglUseFontBitmapsW。
void display(void) { glClear(GL_COLOR_BUFFER_BIT); selectFont(48, ANSI_CHARSET, "Comic Sans MS"); glColor3f(1.0f, 0.0f, 0.0f); glRasterPos2f(-0.7f, 0.4f); drawString("Hello, World!"); selectFont(48, GB2312_CHARSET, "楷體_GB2312"); glColor3f(1.0f, 1.0f, 0.0f); glRasterPos2f(-0.7f, -0.1f); drawCNString("當代的中國漢字"); selectFont(48, DEFAULT_CHARSET, "華文仿宋"); glColor3f(0.0f, 1.0f, 0.0f); glRasterPos2f(-0.7f, -0.6f); drawCNString("傳統的中國漢字"); glutSwapBuffers(); }
效果如圖:
紋理字體
把文字放到紋理中有很多好處,例如,可以任意修改字符的大小(而不必重新指定字體)。
對一面飄動的旗幟使用帶有文字的紋理,則文字也會隨着飄動。這個技術在“三國志”系列游戲中經常用到,比如關羽的部隊,旗幟上就飄着個“關”字,張飛的部 隊,旗幟上就飄着個“張”字,曹操的大營,旗幟上就飄着個“曹”字。三國人物何其多,不可能為每種姓氏都單獨制作一面旗幟紋理,如果能夠把文字放到紋理上,則可以解決這個問題。(參見后面的例子:繪制一面“曹”字旗)
如何把文字放到紋理中呢?自然的想法就是:“如果前面所用的顯示列表,可以直接往紋理里面繪制,那就好了”。不過,“繪制到紋理”這種技術要涉及的內容可不少,足夠我們專門拿一課的篇幅來講解了。這里我們不是直接繪制到紋理,而是用簡單一點的辦法:先把漢字繪制出來,成為像素,然后用glCopyTexImage2D把像素復制為紋理。
glCopyTexImage2D與glTexImage2D的用法是類似的,不過前者是直接把繪制好的像素復制到紋理中,后者是從內存傳送數據到紋理中。要使用到的代碼大致如下:
// 先把文字繪制好 glRasterPos2f(XXX, XXX); drawCNString("關"); // 分配紋理編號 glGenTextures(1, &texID); // 指定為當前紋理 glBindTexture(GL_TEXTURE_2D, texID); // 把像素作為紋理數據 // 將屏幕(0, 0) 到 (64, 64)的矩形區域的像素復制到紋理中 glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 64, 64, 0); // 設置紋理參數 glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER, GL_LINEAR);
然后,我們就可以像使用普通的紋理一樣來做了。繪制各種物體時,指定合適的紋理坐標即可。
有一個細節問題需要特別注意。大家看上面的代碼,指定文字顯示的位置,寫的是glRasterPos2f(XXX, XXX);這里來講講如何計算這個顯示坐標。
讓我們首先從計算文字的大小談起。大家知道即使是同一字號的同一個文字,大小也可能是不同的,英文字母尤其如此,有的字體中大寫字母O和小寫字母l是一樣寬的(比如Courier New),有的字體中大寫字母O比較寬,而小寫字母l比較窄(比如Times New Roman),漢字通常比英文字母要寬。
為了計算文字的寬度,Windows專門提供了一個函數GetCharABCWidths,它計算一系列連續字符的ABC寬度。所謂ABC寬度,包括了 a, b, c三個量,a表示字符左邊的空白寬度,b表示字符實際的寬度,c表示字符右邊的空白寬度,三個寬度值相加得到整個字符所占寬度。如果只需要得 到總的寬度,可以使用GetCharWidth32函數。如果要支持漢字,應該使用寬字符版本,即GetCharABCWidthsW和 GetCharWidth32W。在使用前需要用MultiByteToWideChar函數,將通常的字符串轉化為寬字符串,就像前面的 wglUseFontBitmapsW那樣。
解決了寬度,我們再來看看高度。本來,在指定字體的時候指定大小為s的話,所有的字符高度都為s,只有寬度不同。但是,如果我們使用glRasterPos2i(-1, -1)從最左下角開始顯示字符的話,其實是不能得到完整的字符的:(。我們知道英文字母在寫的時候可以分上中下三欄,這時繪制出來只有上、中兩欄是可見的,下面一欄則不見了,字母g尤其明顯。見下圖:
所以,需要把繪制的位置往上移一點,具體來說就是移動下面一欄的高度。這個高度是多少像素呢?這個我也不知道有什么好辦法來計算,根據我的經驗,移動整個字符高度的八分之一是比較合適的。例如字符大小為24,則移動3個像素。
還要注意,OpenGL 2.0以前的版本,通常要求紋理的大小必須是2的整數次方,因此我們應該設置字體的高度為2的整數次方,例如16, 32, 64,這樣用起來就會比較方便。
現在讓我們整理一下思路。首先要做的是將字符串轉化為寬字符的形式,以便使用wglUseFontBitmapsW和GetCharWidth32W函數。 然后設置字體大小,接下來計算字體寬度,計算實際繪制的位置。然后產生顯示列表,利用顯示列表繪制字符,銷毀顯示列表。最后分配一個紋理編號,把字符像素復制到紋理中。
呵呵,內容已經不少了,讓我們來看看代碼。
#define FONT_SIZE 64 #define TEXTURE_SIZE FONT_SIZE GLuint drawChar_To_Texture(const char* s) { wchar_t w; HDC hDC = wglGetCurrentDC(); // 選擇字體字號、顏色 // 不指定字體名字,操作系統提供默認字體 // 設置顏色為白色 selectFont(FONT_SIZE, DEFAULT_CHARSET, ""); glColor3f(1.0f, 1.0f, 1.0f); // 轉化為寬字符 MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, s, 2, &w, 1); // 計算繪制的位置 { int width, x, y; GetCharWidth32W(hDC, w, w, &width); // 取得字符的寬度 x = (TEXTURE_SIZE - width) / 2; y = FONT_SIZE / 8; glWindowPos2iARB(x, y); // 一個擴展函數 } // 繪制字符 // 繪制前應該將各種可能影響字符顏色的效果關閉 // 以保證能夠繪制出白色的字符 { GLuint list = glGenLists(1); glDisable(GL_DEPTH_TEST); glDisable(GL_LIGHTING); glDisable(GL_FOG); glDisable(GL_TEXTURE_2D); wglUseFontBitmaps(hDC, w, 1, list); glCallList(list); glDeleteLists(list, 1); } // 復制字符像素到紋理 // 注意紋理的格式 // 不使用通常的GL_RGBA,而使用GL_LUMINANCE4 // 因為字符本來只有一種顏色,使用GL_RGBA浪費了存儲空間 // GL_RGBA可能占16位或者32位,而GL_LUMINANCE4只占4位 { GLuint texID; glGenTextures(1, &texID); glBindTexture(GL_TEXTURE_2D, texID); glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE4, 0, 0, TEXTURE_SIZE, TEXTURE_SIZE, 0); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); return texID; } }
為了方便,我使用了glWindowPos2iARB這個擴展函數來指定繪制的位置。如果某個系統中OpenGL沒有支持這個擴展,則需要使用較多的代碼來 實現類似的功能。為了方便的調用這個擴展,我使用了GLEE。詳細的情形可以看本教程第十四課,最后的那一個例子。GL_ARB_window_pos擴展在OpenGL 1.3版本中已經成為標准的一部分,而幾乎所有現在還能用的顯卡在正確安裝驅動后都至少支持OpenGL 1.4,所以不必擔心不支持的問題。
另外,占用的空間也是需要考慮的問題。通常,我們的紋理都是用GL_RGBA格式,OpenGL會保存紋理中每個像素的紅、綠、藍、 alpha四個值,通常,一個像素就需要16或32個二進制位才能保存,也就是2個字節或者4個字節才保存一個像素。我們的字符只有“繪制”和“不繪制” 兩種狀態,因此一個二進制位就足夠了,前面用16個或32個,浪費了大量的空間。緩解的辦法就是使用GL_LUMINANCE4這種格式,它不單獨保存 紅、綠、藍顏色,而是把這三種顏色合起來稱為“亮度”,紋理中只保存這種亮度,一個像素只用四個二進制位保存亮度,比原來的16個、32個要節省不少。注意這種格式不會保存alpha值,如果要從紋理中取alpha值的話,總是返回1.0。
應用紋理字體的實例:飄動的旗幟
(提示:這一段需要一些數學知識)
有了紋理,只要我們繪制一個正方形,適當的設置紋理坐標,就可以輕松的顯示紋理圖象了(參見第十一課),因為這里紋理圖象實際上就是字符,所以我們也就顯示出了字符。並且,隨着正方形大小的變化,字符的大小也會隨着變化。
直接貼上紋理,太簡單了。現在我們來點挑戰性的:畫一個飄動的曹操軍旗幟。效果如下圖,很酷吧?呵呵。
效果是不錯,不過它也不是那么容易完成的,接下來我們一點一點的講解。
為了完成上面的效果,我們需要具備以下的知識:
1. 用多個四邊形(實際上是矩形)連接起來,制作飄動的效果
2. 使用光照,計算法線向量
3. 把紋理融合進去
因為要使用光照,法線向量是不可少的。這里我們通過不共線的三個點來得到三個點所在平面的法線向量。
從數學的角度看,原理很簡單。三個點v1, v2, v3,可以用v2減v1,v3減v1,得到從v1到v2和從v1到v3的向量s1和s2。然后向量s1和s2進行叉乘,得到垂直於s1和s2所在平面的向量,即法線向量。
為了方便使用,應該把法線向量縮放至單位長度,這個也很簡單,計算向量的模,然后向量的每個分量都除以這個模即可。
#include <math.h> // 設置法線向量 // 三個不在同一直線上的點可以確定一個平面 // 先計算這個平面的法線向量,然后指定到OpenGL void setNormal(const GLfloat v1[3], const GLfloat v2[3], const GLfloat v3[3]) { // 首先根據三個點坐標,相減計算出兩個向量 const GLfloat s1[] = {v2[0]-v1[0], v2[1]-v1[1], v2[2]-v1[2]}; const GLfloat s2[] = {v3[0]-v1[0], v3[1]-v1[1], v3[2]-v1[2]}; // 兩個向量叉乘得到法線向量的方向 GLfloat n[] = { s1[1]*s2[2] - s1[2]*s2[1], s1[2]*s2[0] - s1[0]*s2[2], s1[0]*s2[1] - s1[1]*s2[0] }; // 把法線向量縮放至單位長度 GLfloat abs = sqrt(n[0]*n[0] + n[1]*n[1] + n[2]*n[2]); n[0] /= abs; n[1] /= abs; n[2] /= abs; // 指定到OpenGL glNormal3fv(n); }
細心的朋友可能會想到這樣一個問題:明明繪制文字的時候使用的是白色,放到紋理中也是白色,那個“曹”字是如何顯示為黃色的呢?
這就要說到紋理的使用方法了。大家在看了第十一課“紋理的使用入門”以后,難免認為紋理就是用一幅圖片上的像素顏色來替換原來的顏色。其實這只是紋理最簡單的一種用法,它還可以有其它更復雜但是實用的用法。
這里我們必須提到一個函數:glTexEnv*。從OpenGL 1.0到OpenGL 1.5,每個OpenGL版本都對這個函數進行了修改,如今它的功能已經變的非常強大(但同時也非常復雜,如果要全部講解,只怕又要花費一整課的篇幅了)。
最簡單的用法就是:
它指定紋理的使用方式為“代替”,即用紋理中的顏色代替原來的顏色。
我們這里使用另一種用法:
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_BLEND);
glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, color);
其中第二行指定紋理的使用方式為“混合”,它與OpenGL的混合功能類似,但源因子和目標因子是固定的,無法手工指定。最終產生的顏色為:紋理的顏色*常量顏色 + (1.0-紋理顏色)*原來的顏色。常量顏色是由第三行代碼指定為黃色。
因為我們的紋理里面裝的是文字,只有黑、白兩種顏色。如果紋理中某個位置是黑色,套用上面的公式,發現結果就是原來的顏色,沒有變化;如果紋理中某個位置是白色,套用上面的公式,發現結果就是常量顏色。所以,文字的顏色就由常量顏色決定。我們指定常量顏色,也就指定了文字的顏色。
主要的知識就是這些了,結合前面課程講過的視圖變換(設置觀察點)、光照(設置光源、材質),以及動畫,飄動的旗幟就算制作完成。
呵呵,代碼已經比較龐大了,限於篇幅,完整的版本這里就不發上來了,不過附件里面有一份源代碼flag.c。
緩沖機制
走出做完旗幟的喜悅后,讓我們回到二維文字的問題上來。
前面說到因為漢字的數目眾多,無法在初始化時就為每個漢字都產生一個顯示列表。不過,如果每次顯示漢字時都重新產生顯示列表,效率上也說不過去。一個好的辦法就是,把經常使用的漢字的顯示列表保存起來,當需要顯示漢字時,如果這個漢字的顯示列表已經保存,則不再需要重新產生。如果有很多的漢字都需要產生顯示列表,占用容量過多,則刪除一部分最近沒有使用的顯示列表,以便釋放出一些空間來容納新的顯示列表。
學過操作系統原理的朋友應該想起來了,沒錯,這與內存置換的算法是一樣的。內存速度快但是容量小,硬盤(虛擬內存)速度慢但是容量大,需要找到一種機制,使性能盡可能的達到最高。這就是內存置換算法。
常見的內存置換算法有好幾種,這里我們選擇一種簡單的。那就是隨機選擇一個顯示列表並且刪除,空出一個位置用來裝新的顯示列表。
還要說一下,我們不再直接用顯示列表來顯示漢字了,改用紋理。因為紋理更加靈活,而且根據實驗,紋理比顯示列表更快。一個顯示列表只能保存一個字符,但是紋理只要足夠大,則可以保存很多的字符。假設字符的高度是32,則寬度不超過32,如果紋理是256*256的話,就可以保存8行8列,總共64個漢字。
我們要做的功能:
1. 緩沖機制的初始化
2. 緩沖機制的退出
3. 根據一個文字字符,返回對應的紋理坐標。如果字符本身不在紋理中,則應該先把字符加入到紋理中(如果紋理已經裝不下了,則先刪除一個),然后返回紋理坐標。
要改進緩沖機制的性能,則應該使用更高效的置換算法,不過這個已經遠超出OpenGL的范圍了。大家如果有空也可以看看linux源碼什么的,應該會找到好的置換算法。
即使我們使用最簡單的置換算法,完整的代碼仍然有將近200行,其實這些都是算法基本功了,跟OpenGL關系並不太大。仍然是由於篇幅限制,僅在附件中給 出,就不貼在這里了。文件名為ctbuf.h和ctbuf.c,在使用的時候把這兩個文件都加入到工程中,並調用ctbuf.h中聲明的函數即可。
這里我們僅僅給出調用部分的代碼。
#include "ctbuf.h" void display(void) { static int isFirstCall = 1; if( isFirstCall ) { isFirstCall = 0; ctbuf_init(32, 256, "黑體"); } glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glEnable(GL_TEXTURE_2D); glPushMatrix(); glTranslatef(-1.0f, 0.0f, 0.0f); ctbuf_drawString("美好明天就要到來", 0.1f, 0.15f); glTranslatef(0.0f, -0.15f, 0.0f); ctbuf_drawString("Best is yet to come", 0.1f, 0.15f); glPopMatrix(); glutSwapBuffers(); }
注意這里我們是用紋理來實現字符顯示的,因此文字的大小會隨着窗口大小而變化。最初的Hello, World程序就不會有這樣的效果,因為它的字體硬性的規定了大小,不如紋理來得靈活。
有了緩沖機制,顯示文字的速度會比沒有緩沖時快很多,這樣我們也可以考慮顯示大段的文字了。
基本上,前面的ctbuf_drawString函數已經可以快速的顯示一個較長的字符串,但是它有兩個缺點。
第一個缺點是不會換行,一直橫向顯示到底。
第二個缺點是即使字符在屏幕以外,也會嘗試在緩沖中查找這個字符,如果沒找到,還會重新生成這個字符。
讓我們先來看看第一個問題,換行。所謂換行其實就是把光標移動到下一行的開頭,如果知道每一行開頭的位置的話,只需要很短的代碼就可以實現。
不過,OpenGL顯示文字的時候並不會保存每一行開頭的位置,所以這個需要我們自己動手來做。
第二個問題是關於性能的,如果字符本身不會顯示出來,那么為它產生顯示列表和紋理就是一種浪費,如果為了容納它的顯示列表或者紋理,而把緩沖區中其它有用的字符的顯示列表或者紋理給刪除了,那就更加得不償失。
所以,判斷字符是否會顯示也是很重要的。像我們的瀏覽器,如果顯示一個巨大的網頁,其實它也只繪制最必要的部分。
為了解決上面兩個問題,我們再單獨的編寫一個模塊。初始化的時候指定顯示區域的大小、每行多少個字符、每列多少個字符,在模塊內部判斷是否需要換行,以及判斷每個文字是否真的需要顯示。
呃,小小的感慨一下,為什么每當我做好一份代碼,就發現它實在太長,長到我不想貼出來呢?唉……
先看看圖:
注意觀察就可以發現,歌詞分為多行,只有必要的行才會顯示,不會從頭到尾的顯示出來。
代碼中主要是算法和C語言基本功,跟OpenGL關系並不大。還是照舊,把主要的代碼放到附件里,文件名為textarea.h和textarea.c,使用時要與前面的ctbuf.h和ctbuf.c一起使用。
這里僅給出調用部分的代碼。
const char* g_string = "《合金裝備》(Metal Gear Solid)結尾曲歌詞\n" // 歌詞很多很長 "因為。。。。。。。。 \n" "美好即將到來\n"; textarea_t* p_textarea = NULL; void display(void) { static int isFirstCall = 1; if( isFirstCall ) { isFirstCall = 0; ctbuf_init(24, 256, "隸書"); p_textarea = ta_create(-0.7f, -0.5f, 0.7f, 0.5f, 20, 10, g_string); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); } glClear(GL_COLOR_BUFFER_BIT); // 顯示歌詞文字 glEnable(GL_TEXTURE_2D); ta_display(p_textarea); // 用半透明的效果顯示一個方框 // 這個框是實際需要顯示的范圍 glEnable(GL_BLEND); glDisable(GL_TEXTURE_2D); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glColor4f(1.0f, 1.0f, 1.0f, 0.5f); glRectf(-0.7f, -0.5f, 0.7f, 0.5f); glDisable(GL_BLEND); // 顯示一些幫助信息 glEnable(GL_TEXTURE_2D); glPushMatrix(); glTranslatef(-1.0f, 0.9f, 0.0f); ctbuf_drawString("歌詞顯示程序", 0.1f, 0.1f); glTranslatef(0.0f, -0.1f, 0.0f); ctbuf_drawString("按W/S鍵實現上、下翻頁", 0.1f, 0.1f); glTranslatef(0.0f, -0.1f, 0.0f); ctbuf_drawString("按ESC退出", 0.1f, 0.1f); glPopMatrix(); glutSwapBuffers(); }
其實上面我們所講那么多,只講了一類字體,即像素字體,此外還有輪廓字體。所以,這個看似已經很長的課程,其實只講了“顯示文字”這個課題的一半。估計大家已經看不下去了,其實我也寫不下去了。好長……
那么,本課就到這里吧。有種虎頭蛇尾的感覺:(
小結
本課的內容不可謂不多。列表如下:
1. 以Hello, World開始,說明英文字符(ASCII字符)是如何繪制的。
2. 給出了一個設置字體的函數selectFont。
3. 講了如何顯示中文字符。
4. 講了如何把字符保存到紋理中。
5. 給出了一個大的例子,繪制一面“曹”字旗。(附件flag.c)
6. 講解了緩沖機制,其實跟內存的置換算法原理是一樣的。我們給出了一個最簡單的緩沖實現,采用隨機的置換算法。(做成了模塊,附件ctbuf.h,ctbuf.c,調用的例子在本課正文中可以找到)
7. 通過緩沖機制,實現顯示大段的文字。主要是注意換行的處理,還有就是只顯示必要的行。(做成了模塊,附件textarea.h,textarea.c,調用的例子在本課正文中可以找到)
最后兩個模塊雖然是以附件形式給出的,但是原理我想我已經說清楚了,並且這些內容跟OpenGL關系並不大,主要還是相關專業的知識,或者C語言基本功。主要是讓大家弄清楚原理,附件代碼只是作為參考用。
說說我的感受:顯示文字很簡單,顯示文字很復雜。除了最基本的顯示列表、紋理等OpenGL常識外,更多的會涉及到數學、數據結構與算法、操作系統等各個領域。一個大型的程序通常都要實現一些文字特殊效果,僅僅是調用幾個顯示列表當然是不行的,需要大量的相關知識來支撐。
本課的門檻突然提高,搞得我都不知道這還算不算是“入門教程”了,希望各位不要退縮哦。祝大家愉快。