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上,混合后的顏色通過以下公式計算,
Co=αsCs+(1−αs)Cd
以60%透明的紅色渲染到白色背景為例:
Co=(255,0,0)⋅0.6+(255,255,255)⋅(1−0.6)=(255,102,102)
也就是說,從視覺上,(255, 0, 0, 0.6)渲染到白色背景上 和 (255, 102, 102) 是同一個顏色。如果顏色以 Premultiplied Alpha 形式存儲,也就是Cs已經乘以透明度了,所以混合公式變成了:
Co=Cs′+(1−αs)Cd
為什么要 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))⋅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))⋅0.5=(127,25,0,0.55)
從上面的圖里第三個顏色是沒有 Premultiplied Alpha 的混合結果,對比第四個 Premultiplied Alpha 后顏色的結果,顯然第四個顏色更符合直覺,第三個顏色太綠了,因為綠色通道沒有乘以透明度,所以在線性插值的時候占了過大的權重。
所以 Premultiplied Alpha 最重要的意義是使得帶透明度圖片紋理可以正常的進行線性插值。這樣旋轉、縮放或者非整數的紋理坐標才能正常顯示,否則就會像上面的例子一樣,在透明像素邊緣附近產生奇怪的顏色。
紋理處理
我們使用的PNG圖片紋理,一般是不會 Premultiplied Alpha 的。游戲引擎在載入PNG紋理后會手動處理,然后再glTexImage2D傳給GPU,比如 Cocos2D-x 中的 CCImage::premultipliedAlpha:
1 void Image::premultipliedAlpha() { 2 unsigned int* fourBytes = (unsigned int*)_data; 3 for (int i = 0; i < _width * _height; i++) { 4 unsigned char* p = _data + i * 4; 5 fourBytes[i] = CC_RGB_PREMULTIPLY_ALPHA(p[0], p[1], p[2], p[3]); 6 } 7 _hasPremultipliedAlpha = true; 8 }
而GPU專用的紋理格式,比如 PVR、ETC 一般在生成紋理都是默認 Premultiplied Alpha 的,這些格式一般是GPU硬解碼,引擎用CPU處理會很慢。
總之 glTexImage2D 傳給 GPU 的紋理數據最好都是 Multiplied Alpha 的,要么在生成紋理時由紋理工具 Pre-multiplied,要么載入紋理后由游戲引擎或UI框架 Post-multiplied。