天天看點

圖檔Premultiplied Alpha到底是幹嘛用的

Premultiplied Alpha 這個概念做遊戲開發的人都不會不知道。Xcode 的工程選項裡有一項 Compress PNG Files,會對 PNG 進行 Premultiplied Alpha,Texture Packer 中也有Premultiplied Alpha 的選項。那麼問題來了,Premultiplied Alpha 是什麼呢?我被這個問題困惑了很久,之前搜到過 Nvidia的這篇文章,其實說的很清楚,隻是當時有很多相關概念沒搞清楚,是以沒看懂。直到前幾天讀《Real Time Rendering》時終于搞懂了。

Alpha Blending

要搞清楚這個問題,先得了解Alpha通道的工作原理,如果你已經了解可以直接跳過。

最常見的像素表示格式是RGBA8888即 (r, g, b, a),每個通道8位,0-255。例如紅色60%透明度就是 (255, 0, 0, 153),為了表示友善alpha通道一般記成正規化後的0-1的浮點數,也就是 (255, 0, 0, 0.6)。而 Premultiplied Alpha 則是把RGB通道乘以透明度也就是 (r * a, g * a, b * a, a),50%透明紅色就變成了(153, 0, 0, 0.6)。

透明通道在渲染的時候通過 Alpha Blending 産生作用,如果一個透明度為 as 的顔色 Cs 渲染到顔色 Cd上,混合後的顔色通過以下公式計算,

$$C_o = \alpha_s C_s + (1 - \alpha_s) C_d$$

以60%透明的紅色渲染到白色背景為例:

$$C_o = (255, 0, 0) \cdot 0.6 + (255, 255, 255) \cdot (1 - 0.6) = (255, 102, 102)$$

也就是說,從視覺上,(255, 0, 0, 0.6)渲染到白色背景上 和 (255, 102, 102) 是同一個顔色。如果顔色以 Premultiplied Alpha 形式存儲,也就是Cs已經乘以透明度了,是以混合公式變成了: $$C_o = {C_s}' + (1 - \alpha_s) C_d$$

為什麼要 Premultiplied Alpha 呢?

Premultiplied Alpha 後的像素格式變得不直覺,因為在畫圖的時候都是先從調色闆中選出一個RGB顔色,再單獨設定透明度,如果RGB乘以透明度就搞不清楚原色是什麼了。從前面的 Alpha Blending 公式可以看出,Premultiplied Alpha 之後,混合的時候可以少一次乘法,這可以提高一些效率,但這并不是最主要的原因。最主要的原因是:

沒有 Premultiplied Alpha 的紋理無法進行 Texture Filtering(除非使用最近鄰插值)。

以最常見的 filtering 方式線性插值為例,一個寬2px高1px的圖檔,左邊的像素是紅色,右邊是綠色10%透明度,如果把這個圖檔縮放到1x1的大小,那麼縮放後1像素的顔色就是左右兩個像素線性插值的結果,也就是把兩個像素各個通道加起來除以2。如果使用沒有 Premultiplied Alpha 的顔色進行插值,那麼結果就是:

$$((255, 0, 0, 1) + (0, 255, 0, 0.1)) \cdot 0.5 = (127, 127, 0, 0.55)$$

如果綠色 Premultiplied Alpha,也就是 (0, 255 * 0.1, 0, 0.1),和紅色混合後:

$$((255, 0, 0, 1) + (0, 25, 0, 0.1)) \cdot 0.5 = (127, 25, 0, 0.55)$$

圖檔Premultiplied Alpha到底是幹嘛用的

從上面的圖裡第三個顔色是沒有 Premultiplied Alpha 的混合結果,對比第四個 Premultiplied Alpha 後顔色的結果,顯然第四個顔色更符合直覺,第三個顔色太綠了,因為綠色通道沒有乘以透明度,是以線上性插值的時候占了過大的權重。

是以 Premultiplied Alpha 最重要的意義是使得帶透明度圖檔紋理可以正常的進行線性插值。這樣旋轉、縮放或者非整數的紋理坐标才能正常顯示,否則就會像上面的例子一樣,在透明像素邊緣附近産生奇怪的顔色。

紋理處理

我們使用的PNG圖檔紋理,一般是不會 Premultiplied Alpha 的。遊戲引擎在載入PNG紋理後回手動處理,然後再glTexImage2D傳給GPU,比如 Cocos2D-x 中的 CCImage::premultipliedAlpha:

cpp              void Image::premultipliedAlpha() {
    unsigned int* fourBytes = (unsigned int*)_data;
    for (int i = 0; i < _width * _height; i++) {
        unsigned char* p = _data + i * 4;
        fourBytes[i] = CC_RGB_PREMULTIPLY_ALPHA(p[0], p[1], p[2], p[3]);
    }  
    _hasPremultipliedAlpha = true;
}
           

而GPU專用的紋理格式,比如 PVR、ETC 一般在生成紋理都是預設 Premultiplied Alpha 的,這些格式一般是GPU硬解碼,引擎用CPU處理會很慢。

總之 glTexImage2D 傳給 GPU 的紋理資料最好都是 Multiplied Alpha 的,要麼在生成紋理時由紋理工具 Pre-multiplied,要麼載入紋理後由遊戲引擎或UI架構 Post-multiplied。

原文位址:http://boundary.cc/2015/07/why-premultiplied-alpha/