OpenGL點陣字體繪製終極解決方案

http://www.360doc.com/content/12/0406/11/1016783_201355491.shtml

事情總在變化, opengl迎來了3.3以及4.1的進化, 相信今後的擴充也會朝着這個方向. 對於字體渲染方面, 也並不是什麼壞事. 今後有時間再寫篇關於3.3和4.1的全屏字體渲染的新方案, 仍然是結合freetype2的, 相信隨着freetype2的進步, 和對它的逐步認識, 應該會比現有方案更簡單高效... 現在最最最重要的事是...睡覺!!!

對於此文, 大家僅做參考吧.


經過多次修改測試,字體問題終於有了個比較完美的解決方法了,貼出來亮亮~~

此法可以說完全是“紅寶書”(即《OpenGL編程指南》)所賜,此篇也不過是一些實踐心得和我自己對字體顯示方法的一些體會罷了。

下面就來介紹這個所謂的“終極解決方案”,對於待解決的各種問題,都有着多種可供選擇的方案,就讓我來邊比較邊描述吧:

  1. 渲染方式和幀數

不管是不是OpenGL平臺, 在每個3D平臺中,  點陣字體無非兩個用處: 要麼做效果,要麼做提示。效果就是標題文字、按鈕之類的,我們一般稱之爲banner,titile,caption的東西;提示就是指一些有動態更新要求的文字,如控制信息提示,  調試模式下的對象名稱、座標等, 還有就是交互場合,比如聊天。

兩種應用需求有所不同,但不管是哪種,在OpenGL中我能找到的直接支持字體的,只有三種方法,選擇他們的標準只有一個——速度:

● glBindTexture, 紋理貼圖,連文字帶背景做好一張大圖,  按需地選取各個文字子圖像,再貼到相應位置的矩形上。貼圖能夠實現的文字效果最多,你可以把文字紋理映射到空間任意位置的巨型上,可以隨意的旋轉縮放和變形。在不要求大量動態更新文字內容的地方,可以選用此方法。大部分的小型3D遊戲,都採用了這樣的方式顯示文字,速度夠快,能實現所有的變換效果。

不足之處是:

很難實現多顏色混合顯示的文字,因爲爲紋理設置顏色需要的步驟十分繁瑣,需要反覆切換和設置紋理函數和像素傳輸轉換函數,難免影響性能;

文字內容不能靈活的更換, 除非你打算用很多碎小的紋理來拼湊文章;但隨着碎小圖片的增多,頂點的和紋理對象也大量增加,需要大量額外的片段處理和過濾操作,會明顯拖慢處理流水線,在要求顯示大量動態文本的場合下力不從心。不過好在OpenGL在處理紋理對象時多數情況是使用硬件實現的,速度不會慢太多,但也絕對不夠塊(你可能玩過這樣的3D遊戲:圖像效果場景規模都一般,可鼠標速度慢得難以忍受,出現這種情況,九成的原因是頂點片元過多造成的,單次場景同時顯示的紋理片段過碎過多,都會成倍地同時增加頂點和像素片元,拖慢速度,鼠標有時間響應,卻沒時間畫出來);

還有就是變換拉伸後,紋理字體會出現模糊的現象,有些人建議打開Anisotropic Filtering(各向異性過濾)開關,利用反走樣解決,但效果似乎也不穩定,在轉角過大、近距離或光線角度太偏的情況下,效果就越來越差了,我想這是紋理映射的通病吧,不可能就一張圖你從哪裏 看都一樣的清晰啊,也有人用多等級的紋理和Mipmap解決,本人沒試驗過(比較麻煩)所以沒什麼發現權。

● glDrawPixels,像素繪製,任何紋理能夠支持的圖像格式,它都能支持,縮放也很簡單,也可通過設置像素傳輸和像素封裝函數實現一些其他的效果。

缺點是:

他同紋理一樣,很難靈活設置顏色; 

只能在光柵上繪製,若需要各種變換效果,還要開闢額外的輔助緩衝和紋理對象;

而最大最大的問題就是速度! 像素在顯示之前的處理動作是沒有經過加速的,也就是說不管你有沒有把他編譯到顯示列表,像素的轉換傳輸等動作每次都照做不誤,它不同於紋理對象中的像素,多數OpenGL實現沒有對它開闢專屬的顯存區域(這種說法有待考證,但實際測試中效率確實很差,編程指南中有特定篇幅介紹瞭如何提高像素繪製的效率,但即使犧牲一切資源來保證效率,實測效果仍然很難讓人滿意)。

所以,雖然 glDrawPixels似乎是三種方法中最簡單有效的, 可實際運行起來卻是三種方法中最慢的!所以如果你要繪製大量點陣字,又想保證幀數的話,寧願去考慮紋理貼圖,也不要在這個函數上花太多心思。

● glBitmap,位圖,如果你想在你的3D引擎裏添加一個控制檯,這個是唯一的選擇,96個可打印字符做成位圖映射到索引爲0x20~0x7F的顯示列 表,供隨時調用。就算直接用glBitmap也來的及,對幀數的影響也不算大,  三種方法中它的速度最能讓人滿意, 且能通過設置光柵顏色靈活改變位圖字體的顏色。想象一下,如果你的控制檯裏的warningerror 普通的log message和usercommand分別使用了不同的顏色顯示,而爲實現這個既酷又實用的效果,所付出的代價僅僅是在設置光柵前加個glColor這麼簡單而已。

缺點:

只能在光柵上繪製,若要縮放旋轉之類的變換,需要額外的處理工序,但由於其本身的速度優勢,這些工序一般不會對幀數有太大的影響;

另外由於位圖只有黑白單色,無法表示灰度,鋸齒問題嚴重,如果只顯示英文字體還好,一旦要顯示中文,文字效果很差,實在是褻瀆中華文化!當然如果你知道怎麼在OpenGL裏實現一個和ClearType類似的技術,那另當別論。

 

以往對於全屏字體渲染,glBitmap一直是我心中的痛,難以割捨它的高速,又無法忍受它的效果, 直到前一段在讀編程指南時,無意間發現了一種利用glBitmap顯示反鋸齒字體的技巧。當時反覆讀了幾次,貌似明白了上面的意思,拿到機器上試了試,果然天才, 很好地解決了鋸齒的問題,相見恨晚,感嘆讀書太不認真,怎麼早沒發現!!  下面簡單描述一下這個方法:

對於一副256灰度圖像,每個像素使用了一個字節表示0~255個灰度,而位圖只有一位0或1,乍一看不太可能,但位圖可以靈活設置顏色的特點,成了突破口。既然位圖在設置光柵前可以使用glColor爲光柵指定"當前光柵顏色",不僅如此,我們還可以指定顏色的alpha值,從而繪製明暗相間的彩色位圖,瞭解了?

把一個反鋸齒的灰度字體圖像分爲多幅位圖,假設分爲4張位圖,第一張:使灰度1~63的相應點置1,其他點置0;第二張:64~127的置1,其他置0...以此類推, 灰階每上升64的點都集中到同一張位圖上。然後,打開混合,使用4次glBitmap調用繪製出來,每次繪製前將光柵顏色設置成與圖像對應階段的灰度,像下面這樣: 

GLfloat curColor[4] = { r, g, b, a*0.25f}; //假設當前顏色爲 (r,g,b,a)

for (int i=0; i<4; ++i) {

   glColor4fv(curColor);

   glRasterPosiv(curPos);

   glBitmap(w,h,0,0, 0,0, bitmap[i]);

   //當前alpha增幅0.25, 4次增至1.0

   curColor[3] +=a*0.25f;

}

就相當於讓一張256灰階的位圖降低到5灰階。這麼做的效果如何呢?

下圖是我在glut這種超慢框架下的測試的:

中間的截圖是用glDrawPixels在打開freetype2的autohinting選項下渲染的256灰階字體, 上下兩張截圖都是使用glBitmap繪製的,沒有打開autohintng,上面的是3副位圖(4灰階)/字,下面的是4副位圖/字。 glDrawPixels是使用了顯示列表繪製全屏1003個漢字的,已經累成14FPS了,而glBitmap是沒用顯示列表的,同樣1003字一屏, 在glut下也能達到50FPS以上!近乎完美!

(窗口分辨率是960x600)


 同時,由於每個像素變成了4個bit表示(4張圖每張1bit),使存儲字模所需的空間降至原來的一半。

 

  1. 字庫和編碼映射

除了glDrawPixels,每一種方法都有應用它的理由,但不管你用哪一種,要克服的最大困難除了渲染速度,就是字庫問題了!讀取字庫建議使用FreeType2這個開源目, 它支持當今幾乎所有流行格式的字體文件,我們可以選擇它來作爲字體導入的工具,當然也可以把它link到你的程序中,實時的載入ttf字體並按需生成字模 圖像。解決字庫的讀取問題,FreeType2絕對是上上之選,就這麼簡單~

當然, 如果你只想支持普通的96個可打印字符,除了glDrawPixels,其他兩種方式隨便用——想要效果就用glBindTexture、想要簡單方便就 glBitmap,然後關掉瀏覽器、合上參考書,最多半個小時你的字體問題就有着落了! 可如果你想要支持中文??龐大的字庫體積是你不得不考慮的另一個問題,何爲龐大?讓我們簡單地算下:

GB2312編碼包含7445個字符,其中漢字6000多個,GBK編碼下僅漢字就有20902個,最新國家標準GB18030-2005,總共 76546個字符, 而目前的Unicode字符集,已經增至超過10萬個字符,雖然現在還沒有哪個unicode字庫能支持到這麼多字符(難道真的有?),但至少20000 個還是有的!而這些字符都是分散在編碼空間中的,就是說編碼是不連續的,不能使用連續的顯示列表索引作簡單的映射(即使連續,這麼龐大的數目,就算顯示列表沒有上限,它所佔據的顯存空間也相當可觀),因此不得不爲‘字符編碼’到‘字模索引/列表索引’建立查找表。

最猛的做法是,在內存平鋪整張表,字模全部存入內存,一步索引到字模,生成顯示列表,下次再繪製字模時只需索引到顯示列表而不必去取字模。這樣做好像也沒 什麼問題,沒什麼問題?如果真的沒問題就不會是最猛的了——對於GB2312和GBK這種"小型"多字節編碼就需要盡1MB的空間,對於unicode最 少最少需要近4MB的空間,而在這個大表裏,八成以上的內容是普通人這輩子都用不上的,而每刷新一幀,你的每個要顯示的字符都要重複查表一次,在這樣大的空間中頻繁查表,產生頁交換的可能非常的大,速度不慢纔怪,絕對不比你每次調用freetype實時轉換灰階來的快,而且還很浪費。

我建議的方法是利用std::map!當然如果你有自己的紅黑樹類和allocator也可以自己做一個map,效率上可能更勝一籌。map的作用是把字 模信息映射到字符編碼,動態的載入我們僅有可能用到的那幾千個字模信息,這樣既節省了空間(省點是點),又比較高效。另外,這裏不必專門爲map設定空間 限制,map在到達一定大小後(大約7000個節點)或每過一段時間後將查找表clear掉就可以了,除非你要在程序裏顯示《說文解字》全篇,否則要讓 map增大到5000節點都是個相當有難度的工作。

 

  1. 定製自己的字體文件

哎……這也是被逼無奈,如果你夢想着自己的圖行引擎能有全功能的中文支持(顯示、輸入),你必須一再考慮速度的問題!因爲中文實在是太多了……而且萬把字 符一會要查表一會要轉換圖像一會又要排布文字,各個環節都不像西文那樣方便直接, 都需要額外的繁瑣的計算!如果你還要些特效,你一定會比我更吝嗇速度。

實踐證明,使用了定製點陣字體文件的方式後,不使用顯示列表而是實時從內存取得字模再逐個glBitmap,其效率幾乎可以和使用了顯示列表的內嵌 Freetype2的字體系統媲美。至於怎麼建立自己的字體文件嘛,我的意見是:怎麼方便怎麼建,讀着方便,用這方便就OK了,因爲像這樣的位圖數據生成 文件後數據是很“稀疏”的,很容易壓縮和解壓,所以空間上不必太擔心(我自己做的24×24點陣字體文件,連帶額外數據只有4MB多一點)。

其他的就沒什麼可說的了,要注意的只有三點:你需要一個有序的code-index表,爲什麼要有序?因爲代碼域很長而實際的可顯示碼點很稀少,在一個有序的靜態表中二分查找是不二之選;你還需要爲每個字模數據建立一個字模信息記錄,記錄啥?寬width、高height、列步進長度advance、行字 節數pitch、字模數據指針等; 還有就是字模數據,如果你想更塊一些,讓每行像素的字節數擴充到4的倍數,浪費些空間可以再換些速度。

 

到目前爲止我們基本完成了下面的要求:

1. 速度快,永遠不能放棄對它的追求!

2. 省內存,CPU內存要省,GPU內存更要一省再省!

3. 美觀,字是拿來看的,辛勤勞動不能僅因一個難看而被淪爲劣質產品。

4. 簡單,方法要簡單通用!這個好像差點事.....

5. 支持海量中文,在新一輪的‘文字改革’到來之前,這永遠是個艱鉅的任務! 而我們做到……一半了!!  不容易啊!!

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章