標籤:

如何使用 OpenGL 來進行文字繪製的 signed distance field ?

技術是Valve提出的

http://www.valvesoftware.com/publications/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf


這個問題需要的背景知識其實蠻多的,不過 signed distance field 這個東西確實是一種非常精巧的視覺表示 (visual representation),它的出發點和實現的方法都非常有意思。

只是理解這個我們要從 GPU 說起..

GPU 作為計算機裡面的圖形單元,我一旦提起你可能想到就是

其實在這麼霸氣的外表下面,GPU 主要只干兩個事情:

  • 把輸入進來的 3D 模型每個頂點的投影位置算好

  • 把紋理貼上去。

當然,這裡面還有很多光照,alpha test, stencil 一類這裡就不扯遠了。

總之呢,你看到的各種光陸怪離的遊戲場景,其實都是這樣產生的。

仔細看的,可以看到其實都是一個個三角形構成的網格 ( Mesh ), 然後貼上對應的紋理圖。

那麼問題來了, 假如我要在場景中添加一段文字呢, 你會怎麼做?

最直接的做法,既然 GPU 這麼喜歡畫三角形, 我就把文字搞成一個三角形組成的 mesh, 然後當成一般的模型渲染不就行啦, 就像下圖一樣:

完美!!, 對不對

等等, 你有沒有數過上面 a 字母中有多少個三角形, 而這個僅僅是一個字母而已!

這種方法有個巨大的問題:由於 mesh 本身由三角形構成,而字元,特別是西文字元和數字,其中的曲線非常多。用三角形去構建弧線就變成了一件非常坑爹的事情,三角形用得少,字母看起來會非常難看,而三角形用的多,場景複雜度會爆炸式上升,直接的結果就是,幀率下降,遊戲沒法玩。。

比如我大歐家的 O

一直到 169 個 三角形,才像點樣子,如果要渲染個 5000 字的讀後感 ..

好吧.. 我們再找其他方法:

既然沒法用模型來做,那我們換個思路,GPU 不是也很喜歡貼圖嘛,那我直接把字體生成一個貼圖,直接貼不就得啦?

左邊就是把所有的字母生成到一個貼圖裡面,這樣如果渲染一個字母,只需要把這部分貼圖貼到兩個三角形上就好啦。現在一下子一個字母只需要兩個三角形了啊。

完美!!

其實這個方法是一個非常普遍的 GPU 渲染文字的方法, 基本可以滿足你絕大部分需求,想詳細了解可以看看 Glyph Atlas 相關的話題。

呃, 說好的 signed distance field 呢....

我們來仔細看看這兩個方法,熟悉計算機圖形學可能馬上就發現, 第一個方法是矢量模型 (放大,縮小,旋轉)都不會有問題。 而第二個方法, 由於字體已經渲染到了貼圖中, 屬於光柵模型(rasterized image).

光柵模型一個最大的問題是,一旦需要做放大,縮小,變形旋轉的時候,只能靠插值來解決 (interpolation). 而插值這種靠猜的方法, 後果就是會帶來邊緣模糊。

而在遊戲裡面,特別是場景裡面的文字, 經常可以各個角度來看,貼圖拉伸完全不能避免, 於是和上圖一樣,第二種方法的結果看起來會..呃..非常難看。

那麼有沒有辦法,既能有光柵模型的性能優勢,又能有矢量模型任意拉伸和縮放的優勢呢。。

好吧,現在輪到 signed distance field 出場了。

我們來考慮下, 數據存在貼圖裡面的問題最大的問題其實在於:貼圖最終被貼到模型上面的時候, 一旦拉伸都會通過插值器, 然後插值器就會在圖片拉伸的區域寫入插入的值,而這個計算出來的值,並不是一定是貼圖裡面物體在拉伸的時候會真實出現的值。

比如我們來看這個典型的 bilinear 插值, 處於 (x,y) 點其實是周圍 4 個點按照距離算出來的,如果圖像並非是一個線性漸變的, 比如 這個 (x1, y2) , ( x2, y2) 其實構成的是一個邊緣, 那麼 (x,y) 計算出的值其實只是一個近似值,於是模糊就產生了.

那我們能不能找到一種數據格式,這種數據可以表示圖形的所有信息,同時通過插值器的時候,插值器插入的值,恰好就是真實的值呢?

答案就是 signed distance field.

我們在貼圖裡面,不再存儲紋理的像素數據,而是存儲每一個點到邊緣的距離,就像下面一樣:

假設白色的是筆劃,那麼整個貼圖裡面每一個像素存儲的都是這個像素到最近筆劃邊緣的距離。大家有沒有注意到:這個距離值不管是在 x 方向還是在 y 方向, 本來就是線性的,於是經過 bilinear 插值以後,插入的值,也是真實的距離值

其實你有沒有意識到,本質上signed distance field 可以讓你在紋理裡面(光柵圖裡面)存一個圖像的矢量表達。同時還利用插值器幫你做了矢量變換,這個才是這個方法最巧妙的地方。

最後生成的紋理長這個樣子:

右邊就是 signed distance field, 看起來像是邊緣在發光一樣,其實是因為距離邊緣越遠,數值越小,看起來越暗。。

貼出來的圖就是很霸氣的這樣啊(借個論文中的圖):

是不是感覺好清楚 ^^

PS: signed distance field 不僅僅可以用在字體上面,也可以用在很多的符號和形狀上面,下面這個就是一個例子:

對於 signed distance map 生成有很多種方法,有興趣的可以看看

GPU Gems 3 - Chapter 34. Signed Distance Fields Using Single-Pass GPU Scan Conversion of Tetrahedra

Real-time texture-mapped vector glyphs (PDF Download Available)

然後不想動手的, 開源庫在這裡。。

Distance field fonts · libgdx/libgdx Wiki · GitHub

最後:這種方法其實不適合於精確渲染字體,背後的原因大家可以自己思考一下 ..


Valve提的是用SDF做矢量圖的方法,我才是第一個直截了當用它做字體的 KlayGE中的字體系統


  • 預先按照合適的解析度算好矢量場,做成貼圖資源,載入進去。
  • 渲染文字的時候,把字母的bounding box作為幾何體塞進去,設置合適的該字母的貼圖坐標offset。
  • 在fragment shader裡面,訪問這個貼圖,得到當前字母、當前屏幕像素的edge distance。
  • 如果想要做帶方向的效果,比如「向左下方拖影」,那麼就需要設置合適的tangent方向、表面法向量,以便在屏幕空間得到表面空間、貼圖空間的正確方向。

這種東西原理上並不難,主要難度在於如何把顯示API包裝成便於嵌入到你工作流中的形式。


首先聲明答非所問,之前就久仰SDF font大名,最近項目階段性完成終於有時間來研究一下了,沒想到這居然都是10年前的技術了,真是深感路途遙遙啊。

之所以考慮使用SDF Font主要是因為目前在Unity上做文字描邊實在是太過消耗頂點數,而過大的頂點buffer會直接影響整個動態界面的更新效率(主要的使用場景是道具界面和HUD界面),而SDF其中的一個優點便是可以直接通過shader繪製出文字描邊,而不需要重複多遍的繪製文字。

不過經過測試之後目前我是大概率打算放棄SDF Font。原因只有一個,就是SDF Font對於小號字的表現效果太差,多少算小號呢?如果你的文字是32×32的,那麼基本上32號以下的字體都可以算是小號字,這基本覆蓋了UI字體的全部字型大小,我大膽猜想這也是為什麼這項技術出現10來都還沒有流行(至少我覺得)起來的原因。

為什麼會這樣呢?首先SDF就像 @羅志宇 回答中所說本質上是在紋理中存儲Vector,這樣經由線性插值採樣可以適應很高的upscaling而不出現採樣鋸齒。但是這並不能解決downscale的問題,採樣問題可以通過mipmap解決(雖說這直接導致貼圖體積增加1/3但也是必須的),但是光柵化的鋸齒是沒法處理的,造成的結果就是一條斜線上粗細不均,各條線段粗細不均,移動時有明顯變化。

為了能夠解決這種問題必須給渲染出來的文字留出一定的虛化比例用以採樣,就像文字圖集上所顯示的文字那樣,經過簡單測試後我目前採取了下面這個演算法,算是能比較好的處理小字的情況,但是跟普通的ttf渲染出來的相比效果還是差了一點,而且這樣有點擔心手機上運行的效率問題,如何結合後續的outline效果也還暫時沒想明白。可能後續還是要做出來實際測試一下看

half font = tex2D(_MainTex, i.texcoord.xy).a;
half text = step(0.4, font) * 0.5 + saturate(font - 0.4);

剛剛接觸SDF一天,看法可能有諸多錯誤,放在這裡就算拋磚引玉了,如有錯誤希望不吝指教。


我來個學術的辦法,ft提取字模,然後三角化,然後shader。


signed distance field 貼圖需要離線生成,對於中文遊戲,一個字型檔不要太大(我記得要最少要10幾20幾M的貼圖),況且可能用多個字體庫的。 有沒有可能實時的解決方案?


推薦閱讀:

在 OpenGL 中應用查找表(LUT)技術時,如何避免紋理數據自動歸一化導致的圖像異常?
不同GLSL版本之間的區別?
OpenGL ES2 對象樹繪製與 VBO組織問題?
現代OpenGL是怎麼繪製曲面的?
請問OpenGL的的頂點數據中position必須與color一一對應嗎?

TAG:OpenGL |