本篇談另一個圖像處理的問題:兩個圖層都有alpha時的混色,差不多是自己寫繪圖軟體的圖層群組功能了。
這次兩位電子妖精就不出場了。
符號說明:
大寫字母A,B,C,S表示記憶體內的一張圖。
後面的小寫字母c,a表示顏色分量,c代表把r,g,b都按照相同的公式計算,a是alpha(不透明度)。
一般繪圖軟體的RGBA值是0~255,而D3D和OpenGL的shader裡是用0~1的小數,本篇也是用0~1。
如果下層是主畫面,每個像素alpha都是1,只有上層有透明度,那混色公式是很常見的那樣:
AcAa + Sc(1-Aa)式中的c代表把rgb都用這個公式計算,如下
ArAa + Sr(1-Aa)
AgAa + Sg(1-Aa)
AbAa + Sb(1-Aa)
Aa越大則A佔的比例越大,反之則S的成分越大。
把每個像素都套用這個式子計算。
這個式子暫且稱為「無alpha版本」,下面的說明會用到。
可是如果用framebuffer object做兩階段繪圖,下層不是主畫面,上下層都有透明度,那計算式是怎麼樣呢?
思路如下
左:把A用無alpha版本跟S疊合,再把B用無alpha版本跟S疊合。
右:把B與A用「有alpha版本」疊合產生C,再用無alpha版本把C疊到S上面。
兩個方法最後主畫面的像素要一樣,先計算左邊的方法,再把兩者比對求出有alpha版本的公式。
左邊是把上面的混色公式再疊上B
BcBa + [AcAa + Sc(1-Aa)](1-Ba)
=
BcBa + AcAa(1-Ba) + Sc(1-Aa)(1-Ba) --<1>
右邊C疊上S的步驟是這樣
CcCa + Sc(1-Ca) --<2>
求出Cc、Ca,讓<2>的結果跟<1>一樣
兩式中有出現Sc的項,<1>是Sc(1-Aa)(1-Ba),<2>是Sc(1-Ca),可發現Ca即等於Aa和Ba做screen運算。
Ca = screen(Aa,Ba) = 1-(1-Aa)(1-Ba) =
Ba+Aa(1-Ba)再比對剩下的項,<1>式的BcBa + AcAa(1-Ba)和<2>式的CcCa
把兩者都除以Ca
Cc=[BcBa + AcAa(1-Ba)] / Ca這就是「有alpha版本」的混色公式。
如果要在D3D和OpenGL實作這個公式,問題就來了,目前即使最新的D3D和OpenGL,混色這一步也還不能寫shader。
上層c*參數1 + 下層c*參數2
上層a*參數1 + 下層a*參數2
D3D11的device->CreateBlendState和OpenGL的glBlendFunc能做的是設定裡面的參數1和參數2,而且只能在預先定義的幾種裡面選一個,不能造出這個型式以外的算式。
無alpha版本很容易,設參數1=GL_SRC_ALPHA,參數2=GL_ONE_MINUS_SRC_ALPHA。
如果是有alpha版本,Ca=Ba+Aa(1-Ba)做得出來,設參數1=GL_ONE,參數2=GL_ONE_MINUS_SRC_ALPHA。
但是Cc的算式多了一點步驟,這樣就做不到了。
終於要講到本篇的標題:premultiply alpha了。
這是把RGB預先乘以alpha,若原始圖檔是(Ar,Ag,Ab,Aa),讀取時做一點計算讓存在記憶體裡的是(ArAa,AgAa,AbAa,Aa),因為Aa的範圍是0~1,把RGB乘上一個0~1的數會≦原來的值。
印象中有聽過這個方法,有一次就試試看把算式中的c都乘上alpha,發現問題迎刃而解。
令Ap=AcAa,記憶體裡的像素變成(Ap,Aa)、(Bp,Ba)、(Cp,Ca),S因為alpha固定是1.0所以Sp=Sc
把上面公式裡的c×a都換成p
無alpha版本變成
Ap + Sp(1-Aa)
左圖變成
Bp + [Ap + Sp(1-Aa)](1-Ba)
=Bp + Ap(1-Ba) + Sp[1-screen(Aa,Ba)]
右圖是
Cp + Sp(1-Ca)
比對後求出
Cp = Bp+Ap(1-Ba),與無alpha版公式相同
Ca = screen(Aa,Ba) =
Ba+Aa(1-Ba)公式變得很簡單,可以用glBlendFunc湊出來了,這樣算下來才知道為什麼會有人想出premultiply alpha的方法。這方法其實很早以前就有,早期可用來簡化像素計算,現在可能因為電腦變快,能應付比較多計算就很少用了。
於是把艾莉兒改造一下,讓她讀取圖檔後立刻把所有像素做premultiply alpha,glBlendFunc和shader裡讀取貼圖的部分也配合改算式。
不過以上計算數值範圍都是0~1,記憶體裡實際是0~255,所以premultiply alpha的算式是「原來的RGB值×alpha/255」。
另外,我除了普通的混色模式以外還有用到add、multiply、screen三種,有了premultiply alpha以後這些公式也簡化了,此篇先不介紹,以後可能另寫一篇介紹各種混色模式。
還有個問題,如果圖不是RGBA格式,而是DXT等壓縮過的格式,那就不能讀取後修改RGB值了。(PNG、JPG讀取時會解碼,但DXT可以給顯卡直接利用,會保持壓縮的狀態存在主記憶體和顯示記憶體)
目前我還沒用到這類格式,先不考慮這個問題,以後用到的話想到的做法是把premultiply alpha移到shader來做,用一個uniform變數告訴shader貼圖是主記憶體傳過去的還是顯卡裡產生的,主記憶體傳過去的才在shader裡乘以alpha。
跟前一篇貼圖坐標一樣,這個問題也是做紙娃娃系統時遇到的。其實只要用到framebuffer object應該就會碰到這個問題,紙娃娃系統是我第一個用到framebuffer object的功能。
我這裡常常要用OpenGL和shader寫出繪圖軟體裡的功能,不知道其他人做遊戲的情況如何,有沒有要寫影像處理演算法。