1. 程式人生 > >使用OpenGL ES進行高效文字渲染

使用OpenGL ES進行高效文字渲染

http://blog.jobbole.com/70468/

任何有多年客戶端開發經驗的開發者都應該知道複雜的文字渲染是怎麼工作的。至少在2010年以前,我剛開始寫libhwui的時候(這是一個基於Android2.0的2D繪畫庫),我就意識到處理文字有時會比其他方面更復雜,特別是當你嘗試用GPU在螢幕上進行繪製的時候。

文字與Android

Android上的文字渲染加速器硬體最初是由Renderscript團隊寫的,然後被很多工程師改進和優化,包括我和好友Chet Haase。在網路上,可以很容易找到很多關於怎麼使用OpenGL ES渲染文字的教程。如果覺得還不夠,可以看看關於遊戲的文章,只看關於文字渲染部分就行。

本文說不是很新奇的知識,只是對於很多開發者來說,通過本文可以從深層次上了解如何實現一個基於GPU的文字渲染系統,文章最後還介紹了一些比較容易實現的優化方法。

用OpenGL渲染文字的常用方法是計算包含所需字形的所有紋理集。這個操作通常是使用一些相當複雜的演算法進行離線操作,這樣可以在構造字形的時候更加高效。在建立這樣一個紋理集之前,首先需要知道應用程式在執行時要使用的字型,包括字型樣式、大小以及其它屬性。

在Android上,提前進行字型紋理生成不是一個實用的方案。Android上的UI工具並不能知道應用系統會使用什麼字型和字形,並且應用還可以在執行時載入自定義的字型,這是主要的限制。Android字型渲染還必須遵循以下條例:

  • 它必須在執行時建立字型快取;
  • 它必須能夠處理大量的字型;
  • 它必須可以處理大量的符號;
  • 它必須要儘可能減少字型上的資源消耗;
  • 必須執行要快速;
  • 在低端和高階機器上也能夠良好執行;
  • 能完美與其它元件結合(驅動程式或GPU)。

字型渲染器的實現

在進入底層OpenGL字型渲染器工作原理之前,我們先從應用層使用的高級別的API開始。這些API對於理解libhwui很重要。

文字API

用於佈局和繪製文字主要有4個API:

  • android.widget.TextView:一個可以處理文字佈局和渲染的檢視元件。
  • android.text.*:一個可以建立風格化文字和佈局的類集合。
  • android.graphics.Paint:用於測量文字。
  • android.graphics.Canvas:用於渲染文字。

TextView和android.text的都是在Paint和Canvas上的高階API。Android3.0以後,Paint和Canvas直接被實現在Skia之上,這是一個開源的渲染庫。SKia提供了一個很好的Freetype抽象實現,這是一個很熱門的開源字型柵格化程式。

1-BitH26buboQae4iO-FpSyg

對於Android4.4,情況變得有些複雜。Paint和Canvas都使用了一個內部的JNI API,叫做TextLayoutCache。它可以處理複雜的文字佈局(CTL)。這個API依賴Harfbuzz,一個空間開源的字形引擎。TextLayoutCache的輸入是一個字型和一個Java的UTF-16的字串,輸出是一個帶有x/y座標的字形列表。

TextLayoutCache是支援非拉丁語言的要點,比如阿拉伯語言、希伯來語、泰國語等,本文不會解釋TextLayoutCache和Harfbuzz的工作原理,但本人強烈建議讀者去學習學習CTL。如果在開發應用的時候需要支援非拉丁語言環境,那麼就要學習它了。如果你曾經參與過OpenGL渲染文字的文章中的討論,就會發現這種特殊的問題是很少見的。繪製文字比簡單排布字形更復雜。某些語言中,比如阿拉伯語是從右到左的,還有泰語甚至需要把字形排布在前一個字形的上面或者下面。

1-VvVj04gAzuTsMC_AN9RGRA

也就是說,當直接或間接呼叫Canvas.drawText()函式的時候,OpenGL 渲染器不會收到你傳送的引數,而是收到一串數字、符號標識,還有x/y 座標集合。

點陣化和快取

字型渲染器的每一個繪製方法都是和字型相關的。字型用於快取個別字形符號,而字形符號又被儲存在快取結構中(快取結構可以包含不同字型的字形符號)。快取結構是持有多個緩衝區的一個重要的物件,有block集合、pixel緩衝區、OpenGL結構處理器,還有點陣緩衝區(也就是網格)。

1-qK4rIi_HDsEYPQQxFK5uPg

這個物件儲存的資料結構比較簡單:

  • 在字型渲染器中字型是儲存在一個LRU快取中的;
  • 字形符號分別儲存在對應的map字型集合中(key就是字形檔案的identifier);
  • 快取結構使用一個塊連結串列集合來記錄空間的大小;
  • 畫素緩衝區是一個uint8_t或者uint32_t型別的陣列(作alpha值和RGBA的快取);
  • 網格其實就是一個頂點陣列,帶有兩個屬性:x/y位置和u/v座標;
  • 一個GLuint的處理器。

字型渲染器對不同型別的快取結構提供了幾種快取紋理例項,也就是根據不同的大小區分,這個大小可能會根據不同裝置而有所不同,這裡這裡說的是預設的大小(快取的數量是硬編碼的):

  • 1024*512 alpha快取。
  • 2048*256 alpha快取。
  • 2028*512alpha快取。
  • 1024*512alpha快取。
  • 2048*256alpha快取。

當快取紋理物件建立之後,其對應的緩衝區不會自動分配空間,除了1024*512的alpha快取總是自動分配外,其它的都是根據需要來分配空間。

字形符號以列的形式打包在紋理中,只要字型渲染器遇到沒有快取的符號,它就會向快取紋理請求響應的型別(儲存在以上的有序列表中),然後快取該符號。

這是上述的blocks列表使用到的地方,這個列表包含了當前已分配的列和所有未分配的空間。如果字形符號和已經存在的列匹配,那該字形符號就會被加到該列的底部。

如果所有列都被佔用,從左邊的剩餘空間開闢新列。因為所有字型都是等寬的,渲染器會把每個字形的寬度弄成4畫素的倍數(預設是4畫素)。這是對列的重利用和字形打包的一個折衷,這個打包目前還不是很好,但是實現起來比較快。

所有的字形符號都儲存在一個含有1個畫素邊框的結構中,這樣在雙線過濾取樣的時候可以避免偽跡的產生。

在文字帶有縮放變形操作的渲染中,瞭解文字何時被渲染也是非常重要的。這個變形操作直接到Skia/Freetype來處理,這就意味著字形符號是在快取結構中變形儲存的。這樣可以改善渲染的質量。幸運的是,文字一般很少做縮放動畫效果,就算是使用了,也只是設計很少的字形符號。本人做過很多實驗,也沒有找到一個實際使用的場景。

還有其它關於paint的屬性會影響字形符號的柵格化和儲存的:粗體、斜體、還有X縮放(在Canvas上做矩陣變換)、字型風格以及線條寬度等。

柵格化的可選方案

事實上,還有其它的方式去在GPU上處理文字字形符號。可以直接被渲染程向量,但是這樣做開銷很大。我調查過標記距離欄位的方法,但是簡單實現的時候遇到了精度的問題(建立曲線的時候會不穩定)。

本人建議讀者可以看看Glyphy這個專案。這是一個開源庫,作者是Harfbuzz。專案在標記距離欄位技術上進行延伸,同時也解決了精度的問題。我暫時沒有花太多時間看這個專案。但是上一次在做著色器的時候,發現這種技術在Android上是被禁止使用的。

預快取技術

字形符號快取是一定要做的。如果做預快取的話,效果會更好。因為libhwui是一個延遲的渲染器(和Skia的快速模式正好相反),所有螢幕上出現的字形都是一幀一幀開始的。在一系列的顯示操作(批處理和合並操作)中,字型渲染器需要儘可能多地快取字形符號。

使用預快取技術的主要優勢在於,可以完全或者最小化紋理載入的時間。紋理載入操作是消耗非常大的,它會推延CPU或者GPU。甚至在幀渲染過程中,改變紋理還會在GPU體系結構帶來更多記憶體的壓力。

ImaginationTech的PowerVRml SGX GPUs使用了延遲疊加技術架構,可以提供很多有趣的特性。但如果在渲染幀時需要修改紋理,會強制要求驅動程式對紋理進行復制。因為字型結構相當大,如果不好好處理紋理載入的話,很容易就記憶體耗盡了。

這樣的場景確實發生在Google Play的一個應用中。這個APP是一個簡單的計算器,僅使用一些數學符號和數字進行簡單的繪製按鈕。字型渲染器在某的時候甚至渲染不出第一幀。因為按鈕是連續進行繪製的,每一個按鈕都會觸發一個紋理載入,然後複製整個字型快取。系統根本沒有這麼多記憶體去儲存這麼多快取的備份。

清空快取

因為用作字形快取的紋理是非常大的,它們有時會被系統回收再利用,以便為其它程式更多的RAM。

當用戶隱藏當前的應用時,系統給應用傳送一條訊息要求釋放盡可能多的記憶體。很明顯,這就需要銷燬最大的字形快取結構。在Android中,這個大快取結構就是所有字形的快取。除了預設第一個建立的以外(1024*512的預設快取)。

紋理結構在沒有儲存空間的時會被清空。字型渲染器使用LRU演算法對素有字型進行記錄,僅僅是記錄而已。如果需要,就會根據最近最少使用的紋理來清除記憶體。目前沒有提供這個操作,但是它確實是一個不錯的優化策略。

批處理和合並操作

Android4.3引入的繪製批處理和合並操作是一項重要的優化,徹底減少了大量往OpenGL驅動傳送指令的問題。

為了進行合併操作,字型渲染器在進行多種繪製呼叫的時候會快取文字,每個快取紋理都會擁有一個客戶端的2048 quads的陣列(1 quad = 1 glyph)。當呼叫lilbhwui中的一個文字繪製API時,字型渲染器獲取合適的網格為每個字形符號進行位置和u/v座標的繪製。網格在批處理的末端被髮送到GPU上(由延遲顯示系統決定)。或者當一個quad的緩衝區滿了的時候,可能會出現多網格渲染同一個字串的情況——一個字元快取佔用一個網格。

這個優化過程很容易實現,對顯示效果幫助也很大。因為字型渲染器使用多快取結構,所以在一個字串的渲染過程彙總,可能字形符號會來自不同的紋理。如果沒有批處理好合並操作的話,每個繪製呼叫都要傳遞給GPU。字型渲染器就需要不斷切換不同的快取結構,這樣會帶來很大的消耗。

在測試字型渲染器的時候,我已經在一個測試App中發現了這個問題。這個App只是簡單地用不同的樣式和大小渲染一句“hello world”。其中字母“o”被儲存在不同的紋理中,和其它的字元不一樣。這種情況導致字型渲染器開始時只繪製了“hell”,然後渲染“o”,然後再渲染“w”,然後在渲染“o”,接著才是“rld”。這5個繪製呼叫和5個紋理進行繫結連線後,只有其中兩個是實際需要的,現在渲染器先繪製“hell w rld”,然後在一起繪製兩個“o”,這就是批處理和合並操作的好處了。

優化紋理載入

之前提到過字型渲染在更新快取紋理的時候(記錄每個紋理中的髒資料塊)會盡可能載入少一點資料。但是很不幸,這個方法還是有兩個限制。

首先,OpenGL ES2.0不允許隨意上傳一個矩形區域。glTextSubImage2D 會讓你指定矩形的x/y座標和寬高來更新矩形裡面的紋理。並且它會把矩形的寬當做記憶體裡的資料幅度,這個可以通過建立一個合適大小的CPU緩衝區來解決,但是也需要事先知道這個矩形的到底有多大。

有一個很好的折衷,就是載入包含髒資料塊(矩形)的最小畫素帶。因為這個畫素帶和紋理一樣寬,這樣就可以節省空間。比每次都要更新整個紋理效果好得多。

第二個問題是紋理載入屬於非同步呼叫,這樣可能造成相當長的CPU延遲(甚至可能會達到1毫秒,依賴紋理的大小、驅動程式還有GPU)。像之前說的那樣,如果使用預快取應該是沒有問題的。但是如果使用的是“重字型”的場景,或者是區域化語言的場景的話(較多的使用字形符號比如中文),那麼問題就還是會出現的。

令人欣慰的是,OpenGL3.0為這兩個問題提供瞭解決方案,這樣就可以直接使用一個畫素儲存的屬性來載入資料矩形了。GL_UNPACK_ROW_LENGTH這個屬性指定了記憶體源資料的寬度。需要注意的是,這個屬性會影響到當前OpenGL上下文的全域性狀態。

載入紋理時,CPU延遲可以通過使用畫素緩衝物件(PBOs)來避免。就像所有OpenGL裡的緩衝區物件一樣PBO會駐留在GPU中,但也可以對映到記憶體中。PBOs有很多有趣的屬性,但是我們關心的是一個在主存中取消對映關係後還可以進行非同步載入紋理的屬性,此時操作佇列變成:

glMapBufferRange → write glyphs to buffer → glUnmapBuffer → glPixelStorei(GL_UNPACK_ROW_LENGTH) → glTexSubImage2D

呼叫glTexSubImage2D可以立即返回,而不用阻塞渲染器,字型渲染器可以在記憶體中對映整個緩衝區,而且似乎不會出現問題。這對於快取紋理的更新操作是一個不錯的方案。

這兩種OpenGL ES3.0的優化方法會出現在Android4.4中。

陰影效果

一般文字在渲染的時候都會帶有陰影效果,這是一個相當耗費資源的操作。在臨近的字形符號可以進行相互模糊操作之後,字型渲染器不再進行獨立的預模糊操作。有很多中方法可以實現模糊化,但是為了在同一幀中把這些調配操作和紋理取樣操作最小化,陰影效果會被簡單儲存為紋理,在多幀切換的時候可以儲存。

因為應用程式可以輕易地拖垮GPU,所以我們還是得依靠CPU來對文字進行模糊化。最簡單和高效的方式就是使用Renderscript的C++ API,只需要簡單幾行程式碼就可以實現核心功能。最簡單的方法是在初始化Renderscript的時候指定RS_INIT_LOW_LATENCY標記來強制執行在CPU上。

未來的優化操作

有一個優化方法我希望可以在我離開Android團隊之前實現。文字預快取、非同步和部分紋理更新都是一些重要的優化操作。但是柵格化文字元號一直都是一個很耗費資源的操作,在systrace可以很容易看到(啟用gfs標識然後看precacheText事件)。

對預快取的一個簡單的優化方式就是,把這個操作放到另一個工作執行緒去執行,把柵格化操作放到後臺。這個技術已經被用到一些複雜的路徑柵格化操作中,但是沒有新增到OpenGL架構之中。

改進批處理和合並操作也是一個可能的優化方式,用於繪製文字的顏色一般是被髮送到一個fragment陰影統一操作。這樣可以減少傳送到GPU的頂點資料,但副作用會產生很多不需要的批處理指令:一個批處理操作只能包含一種文字顏色。如果文字顏色也儲存為頂點屬性,那麼就可以網GPU傳遞更少的資料。

原始碼

如果想詳細地看看字型渲染器的實現,可以瀏覽libhwui的GitHub,可以從FontRender.cpp開始,因為很多驚喜都在這裡發生,它的支援類可以在font或者sub目錄找到。對了,PixelBuffer.cpp這個檔案也不錯,可以看看。這就是一個畫素緩衝區的抽象實現,可以用於CPU(uint8_t型別的陣列)或者GPU緩衝區(PBO)。

最後的話

本文只是對Android的字型渲染器進行簡單介紹,還有很多實現的細節沒有考慮到,或者很多問題以後會說明,所以有什麼問題可以儘管向我提問。


相關推薦

使用OpenGL ES進行高效文字渲染

http://blog.jobbole.com/70468/ 任何有多年客戶端開發經驗的開發者都應該知道複雜的文字渲染是怎麼工作的。至少在2010年以前,我剛開始寫libhwui的時候(這是一個基於Android2.0的2D繪畫庫),我就意識到處理文字有時會比其他方面更

OpenGL ESOpenGL ES 1.X的渲染管線

本文圖片和內容來自 <Android 3D 遊戲開發技術寶典> OpenGL ES 是OpenGL三維圖形API的子集,主要針對手機等嵌入式裝置。 OpenGL ES主要分為兩個版本 一個是Ope

Android 中使用OpenGL ES進行2D開發(紋理Texture使用)

OpenGL紋理是一種點陣圖,可以把它貼上到OpenGL物體的表面上。比如可以獲取一張郵票的影象貼上到正方形中,使正方形看起來像一張郵票。要使郵票保持合適的方向,以便影象井然有序地排列,則必須獲得形狀的每個頂點並在正方形上標記出來,以便郵票和正方形的形狀保持一致。在Open

FFmpeg In Android - H264碼流解碼/OpenGL ES渲染

主要思路是FFmpeg解碼H264得到一張yuv420p圖片後,傳遞給opengl es在著色器內部做圖片轉換yuv->rgb,然後通過紋理貼圖的方式渲染出來.這種方式的效率更高.核心程式碼如下: #include "common.h" #include "gl_util.h"

OpenGL ES 3.0 渲染管線介紹

一、前言 OpenGL 1.x 系列採用的還是固定功能管線。 從 OpenGL ES 2.0 開始採用了可程式設計圖形管線。 而 OpenGL ES 3.0 相容了 2.0,並加入了很多 2.0 不具備的功能。 Android 4.3 之後開始支援 Open

opengl學習之路三十九,文字渲染

當你在圖形計算領域冒險到了一定階段以後你可能會想使用OpenGL來繪製文字。然而,可能與你想象的並不一樣,使用像OpenGL這樣的底層庫來把文字渲染到螢幕上並不是一件簡單的事情。如果你只需要繪製128種不同的字元(Character),那麼事情可能會簡單一些。

OpenGL ES 多執行緒和多屏渲染

“內容歸納” 應用程式和驅動程式之間的傳輸完成之前,阻塞型操作有: 1、上傳資料的圖形API呼叫; 2、顯示卡驅動程式中著色器編譯; 一、什麼情況下使用: 多執行緒渲染最適合於編譯著色器或上傳資料至顯示卡驅動器時CPU資源有限的應用程式。原因有2: 主執行緒

一個命令對文字進行高效排序

導讀 在Linux下,有時候需要對文字內容進行排序,例如按照字典順序排序,按照數字排序或者按照特定列排序等等。今天我們就藉助一個命令-sort來滿足我們對文字排序的需求。 按照字典順序排序 假如有文字內容test1.txt如下(偷偷問一句:

Android音視訊-視訊採集(OpenGL ES渲染

上面的都是基於Android的高階應用層API來實現的音視訊的採集和編碼,下面我們要開啟攝像頭通過OpenGL ES底層native程式碼來渲染視訊畫面。 簡介 總體的思路是從攝像頭採集到視訊的資料,然後傳遞給底層的OpenGL ES來渲染顯示到上層

深度剖析OpenGL ES中的多執行緒和多視窗渲染技術

移動裝置中的CPU和GPU已經變得很強大,到處都是配備一個或多個高解析度螢幕的裝置,需要使用帶有圖形驅動器的複雜互動也日益增加。在這篇部落格文章中,我將討論多執行緒和多視窗渲染對開發人員來講意味著什麼,同時我將介紹將這些技術應用您設計當中的條件和時機。 什麼是多執行緒渲

OpenGL ES渲染管線

渲染管線(graphics pipeline) 在 OpenGL ES 1.0 版本中,支援固定管線,而 OpenGL ES 2.0 版本不再支援固定管線,只支援可程式設計管線。什麼是管線?什麼又是固定管線和可程式設計管線?管線(pipeline)也稱渲染管線

通俗易懂的 OpenGL ES 3.0(二)渲染三角形

前言 學習了OpenGL有一段時間,在繪製出屬於自己的三角形之前,會接觸許多理論上的知識。用簡單的方式寫下自己對OpenGL的一些見解。望大家取其精華去其糟粕 最終效果:改變背景色,並且繪製渲染一個暗紅色的三角形 必備知識 OpenGL需要我們至少設定一個

OpenGL ES總結(四)OpenGL 渲染視訊畫面

前一篇介紹是渲染一張圖片,今天是在MediaPlayer播放過程中,渲染視訊,看下Agenda: 與渲染圖片的區別 建立SurfaceTexture 設定shader(著色器) 建立紋理座標 UV座標介紹 UV紋理座標設定與貼圖規則是什麼? 視訊播放

opengl es 渲染方式與 紋理座標設定 ,OpenGL ES 模型檢視之縮放操作

OpenGl ES關於渲染方式有以下兩種: RENDERMODE_CONTINUOUSLY和RENDERMODE_WHEN_DIRTY。 預設渲染方式為RENDERMODE_CONTINUOUSLY,這兩種渲染的含義是: RENDERMODE_CONTINUOUSL

在iOS上使用OpenGL ES渲染YUV

1)建立OpenGL context [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];2)layer設定成不透明 _eaglLayer = (CAEAGLLayer*) self.layer; _eaglLayer.opaque = Y

OpenGL ES 渲染和簡單的濾鏡效果

--- Update 12.25 --- glPixelStorei 實際上OpenGL也支援使用了這種“對齊”方式的畫素資料。只要通過glPixelStore修改“畫素儲存時對齊的方式”就可以了。像這樣: int alignment = 4; glPixelStore

OpenGL/OpenGL ES 渲染管線理解

OpenGL渲染管線 (1) (2) CS構架: OpenGL可以看做是為執行OpenGL程式的應用(client)和顯示卡(server)提供一個軟體介面。 資料由client上傳至server需要CPU參與,而資料在Server中傳輸完

Android OpenGL ES 簡明開發教程_材質渲染

該系列文章均轉載自 由於原文好像無法開啟,正好自己有記錄,所以正好分享出來,其中也對一些API作了解釋。 前面討論瞭如何給 3D 圖形染色,更一般的情況是使用點陣圖來給 Mesh 上色(渲染材質)。主要步驟如下: 建立 Bitmap 物件 使用材

還在使用OpenGL ES渲染,你Out了,趕緊來擁抱Vulkan吧~

背景介紹 Vulkan是Khronos組織制定的“下一代”開放的圖形顯示API。是與DirectX12能夠匹敵的GPU API標準。 Vulkan是基於AMD的Mantle API演化而來,眼下Vulkan 1.0標準已經完畢並正式公佈。下圖是Vulkan的效果: 上一代的OpenGL|ES並不會被遺棄。

opengl es入門---常見代碼解析

字符串數組 chm 視口 posit detail 編寫 組件 eat 包含著 轉自:http://blog.csdn.net/wangyuchun_799/article/details/7736928,尊重原創! 3.1創建渲染緩沖區 GLuint m