事情總在變化, opengl迎來了3.3以及4.1的進化, 相信今后的擴充也會朝著這個方向. 對于字體渲染方面, 也并不是什么壞事. 今后有時間再寫篇關于3.3和4.1的全屏字體渲染的新方案, 仍然是結合freetype2的, 相信隨著freetype2的進步, 和對它的逐步認識, 應該會比現有方案更簡單高效... 現在最最最重要的事是...睡覺!!!
對于此文, 大家僅做參考吧.
經過多次修改測試,字體問題終于有了個比較完美的解決方法了,貼出來亮亮~~
此法可以說完全是“紅寶書”(即《OpenGL編程指南》)所賜, 此篇也不過是一些實踐心得和我自己對字體顯示方法的一些體會罷了。
下面就來介紹這個所謂的“終極解決方案”,對于待解決的各種問題,都有著多種可供選擇的方案,就讓我來邊比較邊描述吧:
- 渲染方式和幀數
不管是不是OpenGL平臺, 在每個3D平臺中, 點陣字體無非兩個用處: 要么做效果,要么做提示。效果就是標題文字、按鈕之類的,我們一般稱之為banner,titile,caption的東西; 提示就是指一些有動態更新要求的文字,如控制信息提示, 調試模式下的對象名稱、坐標等, 還有就是交互場合,比如聊天。
兩種應用需求有所不同,但不管是哪種,在OpenGL中我能找到的直接支持字體的,只有三種方法,選擇他們的標準只有一個——速度:
● glBindTexture, 紋理貼圖,連文字帶背景做好一張大圖, 按需地選取各個文字子圖像,再貼到相應位置的矩形上。貼圖能夠實現的文字效果最多,你可以把文字紋理映射到空間任意位置的巨型上,可以隨意的旋轉縮放和變形。在不要求大量動態更新文字內容的地方,可以選用此方法。大部分的小型3D游戲,都采用了這樣的方式顯示文字,速度夠快,能實現所有的變換效果。
不足之處是:
很難實現多顏色混合顯示的文字,因為為紋理設置顏色需要的步驟十分繁瑣,需要反復切換和設置紋理函數和像素傳輸轉換函數,難免影響性能;
文字內容不能靈活的更換, 除非你打算用很多碎小的紋理來拼湊文章;但隨著碎小圖片的增多,頂點的和紋理對象也大量增加,需要大量額外的片段處理和過濾操作,會明顯拖慢處理流水線,在要求顯示大量動態文本的場合下力不從心。不過好在OpenGL在處理紋理對象時多數情況是使用硬件實現的,速度不會慢太多,但也絕對不夠塊(你可能玩過這樣的3D游戲:圖像效果場景規模都一般,可鼠標速度慢得難以忍受,出現這種情況,九成的原因是頂點片元過多造成的,單次場景同時顯示的紋理片段過碎過多,都會成倍地同時增加頂點和像素片元,拖慢速度,鼠標有時間響應,卻沒時間畫出來);
還有就是變換拉伸后,紋理字體會出現模糊的現象,有些人建議打開Anisotropic Filtering(各向異性過濾)開關, 利用反走樣解決,但效果似乎也不穩定,在轉角過大、近距離或光線角度太偏的情況下,效果就越來越差了,我想這是紋理映射的通病吧,不可能就一張圖你從哪里看都一樣的清晰啊,也有人用多等級的紋理和Mipmap解決,本人沒試驗過(比較麻煩)所以沒什么發現權。
● glDrawPixels,像素繪制,任何紋理能夠支持的圖像格式,它都能支持,縮放也很簡單,也可通過設置像素傳輸和像素封裝函數實現一些其他的效果。
缺點是:
他同紋理一樣,很難靈活設置顏色;
只能在光柵上繪制,若需要各種變換效果,還要開辟額外的輔助緩沖和紋理對象;
而最大最大的問題就是速度! 像素在顯示之前的處理動作是沒有經過加速的,也就是說不管你有沒有把他編譯到顯示列表,像素的轉換傳輸等動作每次都照做不誤,它不同于紋理對象中的像素,多數OpenGL實現沒有對它開辟專屬的顯存區域(這種說法有待考證,但實際測試中效率確實很差,編程指南中有特定篇幅介紹了如何提高像素繪制的效率,但即使犧牲一切資源來保證效率,實測效果仍然很難讓人滿意)。
所以,雖然 glDrawPixels似乎是三種方法中最簡單有效的, 可實際運行起來卻是三種方法中最慢的!所以如果你要繪制大量點陣字,又想保證幀數的話,寧愿去考慮紋理貼圖,也不要在這個函數上花太多心思。
● glBitmap,位圖,如果你想在你的3D引擎里添加一個控制臺,這個是唯一的選擇,96個可打印字符做成位圖映射到索引為0x20~0x7F的顯示列表,供隨時調用。就算直接用glBitmap也來的及,對幀數的影響也不算大, 三種方法中它的速度最能讓人滿意, 且能通過設置光柵顏色靈活改變位圖字體的顏色。想象一下,如果你的控制臺里的warning error 普通的log message和user command分別使用了不同的顏色顯示,而為實現這個既酷又實用的效果,所付出的代價僅僅是在設置光柵前加個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),使存儲字模所需的空間降至原來的一半。
- 字庫和編碼映射
除了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節點都是個相當有難度的工作。
- 定制自己的字體文件
哎……這也是被逼無奈,如果你夢想著自己的圖行引擎能有全功能的中文支持(顯示、輸入),你必須一再考慮速度的問題!因為中文實在是太多了……而且萬把字符一會要查表一會要轉換圖像一會又要排布文字,各個環節都不像西文那樣方便直接, 都需要額外的繁瑣的計算!如果你還要些特效,你一定會比我更吝嗇速度。
實踐證明,使用了定制點陣字體文件的方式后,不使用顯示列表而是實時從內存取得字模再逐個glBitmap,其效率幾乎可以和使用了顯示列表的內嵌Freetype2的字體系統媲美。至于怎么建立自己的字體文件嘛,我的意見是:怎么方便怎么建,讀著方便,用這方便就OK了,因為像這樣的位圖數據生成文件后數據是很“稀疏”的,很容易壓縮和解壓,所以空間上不必太擔心(我自己做的24×24點陣字體文件,連帶額外數據只有4MB多一點)。
其他的就沒什么可說的了,要注意的只有三點:你需要一個有序的code-index表,為什么要有序?因為代碼域很長而實際的可顯示碼點很稀少,在一個有序的靜態表中二分查找是不二之選;你還需要為每個字模數據建立一個字模信息記錄,記錄啥?寬width、高height、列步進長度advance、行字節數pitch、字模數據指針等; 還有就是字模數據,如果你想更塊一些,讓每行像素的字節數擴充到4的倍數,浪費些空間可以再換些速度。
到目前為止我們基本完成了下面的要求:
1. 速度快,永遠不能放棄對它的追求!
2. 省內存,CPU內存要省,GPU內存更要一省再省!
3. 美觀,字是拿來看的,辛勤勞動不能僅因一個難看而被淪為劣質產品。
4. 簡單,方法要簡單通用!這個好像差點事.....
5. 支持海量中文,在新一輪的‘文字改革’到來之前,這永遠是個艱巨的任務!
http://tsuui.is-programmer.com/posts/4252.html