Android图像处理系列:OpenGL混合模式的使用
OpenGL一次渲染過程包含了多個階段,包括頂點著色器、圖元組裝、柵格化、片元著色器、測試和混合等,最后將結果輸出的FrameBuffer上。渲染管線最后一個階段就是混合。
混合是在繪制時,不是直接把新的顏色覆蓋在原來舊的顏色上,而是將新的顏色與舊的顏色經過一定的運算,從而產生新的顏色。新的顏色稱為源顏色,原來舊的顏色稱為目標顏色。傳統意義上的混合,是將源顏色乘以源因子,目標顏色乘以目標因子,然后相加。
在OpenGL里做顏色混合一般有兩種方式,一種是將要混合的紋理都傳入Fragment Shader,在shader里實現算法完成混合,一種就是利用OpenGL渲染管線最后的blending階段自動對源色和底色進行混合。
在Fragment Shader手動實現混合算法比較自由,我們可以自定義一些混合方法,實現一些OpenGL自帶混合模式無法實現的復雜混合算法,缺點是在部分GPU上同一個texture無法既作FBO輸出,又作紋理采樣輸入,如果底圖作為輸入傳入Fragment Shader,則當前FBO需要綁定另一個texture作為輸出,否則會出現黑色和黑塊的兼容性問題。如果混合區域覆蓋全圖,可以用FBO綁定一個空的texture作為輸出,同時原始底圖傳入Fragment Shader作為輸入;如果混合區域只占全圖的一部分,那么就需要首先復制一份底圖紋理并綁定到FBO作為輸出,同時原始底圖紋理傳入Fragment Shader做混合,這兩種不同的混合場景下,不管混合區域是全圖還是部分區域,都需要申請一塊額外的底圖大小的紋理存儲(空白或復制底圖),另外部分區域混合時還需要一次額外的渲染(復制底圖),混合所需要的空間和時間都有額外開銷。
作為對比,OpenGL渲染管線自帶的混合模式包含的混合算法是有限的,不過基本可以滿足大部分的使用場景。優點是渲染時不用將底圖作為采樣紋理輸入,定義好混合模式后,在Fragment Shader里只需要對源圖紋理進行采樣,然后由OpenGL驅動自動完成混合算法。這種方法對全圖和部分區域的混合同樣適用,都不用額外申請紋理存儲空間,渲染時不用切換FBO,只需渲染一次,渲染的效率比在Fragment Shader里手動實現混合算法要高。
本文主要介紹OpenGL渲染管線自帶的混合模式的用法和實例,同時簡要介紹一下天天P圖里用到的一些混合算法及效果,以及3D渲染時使用混合模式需要注意的一些問題。
OpenGL中的混合模式
前面提到,OpenGL渲染管線的最后階段會將源色和底色進行混合。這里的源色和底色分別指什么呢?我們可以把OpenGL的一次渲染過程形象地比作畫家拿畫筆在畫布上作畫,假如畫家拿著黃色的畫筆在紅色的畫布上作畫,最后畫出一幅綠色的圖,這里畫筆的黃色就是源色,畫布上的紅色就是底色,又叫目標色,綠色就是混合以后的結果。對應到OpenGL的一次渲染過程里,源色就是Fragment Shader處理結束后給gl_FragColor的賦值,底色就是當前FBO綁定的紋理的顏色值,混合后的結果會更新底色紋理的顏色值,就好比是紅色的畫布在用黃色的筆畫完后變成了綠色,綠色變成了畫布新的顏色。OpenGL里的混合就是將源色和底色以某種方式自動混合的技術,通常用來繪制半透明物體(不透明物體顏色直接覆蓋,無需混合)。不同的混合模式算法其實就是定義了源色和底色不同的混合比例,最后達到不同程度的混合效果。需要注意的是,物體的繪制順序可能會影響到OpenGL混合的最終處理效果。
OpenGL API提供了相關接口來開啟/關閉混合模式以及設置源色和底色混合因子,以Android Java層系統接口為例,相關調用如下:
GLES20.glEnable(GLES20.GL_BLEND); //開啟混合 GLES20.glDisable(GLES20.GL_BLEND); //關閉混合GLES20.glBlendFunc(int sfactor, int dfactor); //設置源色和目標色混合因子其中開啟和關閉混合模式的調用很簡單,在此不再贅述。下面著重介紹一下源色和目標色混合因子。OpenGL在做混合時,會把源顏色和目標顏色各乘以一個系數(源顏色乘以的系數稱為“源因子”,目標顏色乘以的系數稱為“目標因子”),然后相加得到新的顏色。
下面用數學公式來表達一下這個運算方式。假設源顏色的四個分量(指紅色,綠色,藍色,alpha值)是(Rs, Gs, Bs, As),目標顏色的四個分量是(Rd, Gd, Bd, Ad),又設源因子為(Sr, Sg, Sb, Sa),目標因子為(Dr, Dg, Db, Da)。則混合產生的新顏色可以表示為:(Rs*Sr+Rd*Dr, Gs*Sg+Gd*Dg, Bs*Sb+Bd*Db, As*Sa+Ad*Da)。如果顏色的某一分量超過了1.0,則它會被自動截取為1.0,不需要考慮越界的問題。
新版本的OpenGL可以設置運算方式,包括加、減、取兩者中較大的、取兩者中較小的、邏輯運算等,本文中不做過多討論,只介紹相加的方式。
源因子和目標因子可以通過glBlendFunc函數來進行設置。glBlendFunc有兩個參數,前者表示源因子,后者表示目標因子。這兩個參數的所有可選值如下圖所示:
| GL_DST_ALPHA | ( Ad , Ad , Ad , Ad ) |
| GL_DST_COLOR | ( Rd , Gd , Bd , Ad ) |
| GL_ONE | (1,1,1,1) |
| GL_ONE_MINUS_DST_ALPHA | (1,1,1,1) - (Ad,Ad,Ad,Ad) |
| GL_ONE_MINUS_DST_COLOR | (1,1,1,1) - (Rd,Gd,Bd,Ad) |
| GL_ONE_MINUS_SRC_ALPHA | (1,1,1,1) - (As,As,As,As) |
| GL_SRC_ALPHA | ( As , As , As , As ) |
| GL_SRC_ALPHA_SATURATE | (f,f,f,1) : f = min(As,1-Ad) |
| GL_ZERO | ( 0 , 0 , 0 , 0 ) |
我們舉個例子來說明混合顏色值是怎么算出來的。以最常用的glBlendFunc( GL_SRC_ALPHA , GL_ONE_MINUS_SRC_ALPHA )為例:
若源色為 ( 1.0 , 0.9 , 0.7 , 0.8 ),源色使用 GL_SRC_ALPHA,所以源色配比值為 ( 0.8 * 1.0 , 0.8 * 0.9 , 0.8 * 0.8 , 0.8 * 0.7 ) ,即 ( 0.8 , 0.72 , 0.64 , 0.56 );
目標色為 ( 0.6 , 0.5 , 0.4 , 0.3 ),目標色使用GL_ONE_MINUS_SRC_ALPHA,即配比比例為 1 - 0.8 = 0.2,目標色配比值為( 0.2 * 0.6 , 0.2 * 0.5 , 0.2 * 0.4 , 0.2 * 0.3 ),即 ( 0.12 , 0.1 , 0.08 , 0.06 )。
最后混合后的顏色值為 ( 0.8 , 0.72 , 0.64 , 0.56 ) + ( 0.12 , 0.1 , 0.08 , 0.06 ) = ( 0.92 , 0.82 , 0.72 , 0.62 )。
使用這種混合參數的意義也很明顯,源色的alpha值決定了結果顏色中源色和目標色的百分比。這里源色的alpha值為0.8,即結果顏色中源色占80%,目標色占20%。
OpenGL混合模式在Android平臺上的使用
在Android上使用OpenGL ES時,紋理上傳最常用的方式就是先把圖片解碼成Bitmap后調用GLUtils.texImage2D(int target, int level, Bitmap bitmap, int border)接口將Bitmap上傳至GPU顯存。
這里需要注意的是,對于有alpha通道的Bitmap,Android系統解碼API會自動執行預乘操作,即Bitmap每個像素的RGB值在解碼時會自動乘以當前像素的alpha值,也就意味著Bitmap中存儲的RGB值與原始圖片的RGB值是不同的。預乘機制為Android系統View System和Canvas繪制提供了更好的性能。
在圖片為完全不透明的情況下(像素點alpha值為255),預乘機制其實對原始圖像沒有影響,但是在半透明、漸變等情況下,預乘機制會對OpenGL混合因子的選擇產生影響。我們舉個簡單的例子,假設我們設置了OpenGL混合模式為glBlendFunc( GL_SRC_ALPHA , GL_ONE_MINUS_SRC_ALPHA ),我們希望源色的占比為alpha,即RGB_new = RGB * alpha,但是因為Bitmap在解碼時已經做了一次預乘,所以最后源色的比例實際為RGB_new = RGB * alpha * alpha,比如在白色的透明度為0.5的地方,原來的 RGB 為255,預乘機制的影響導致最終得到的結果是63.75,與期望值128.5相比會更偏向于黑色,下面是兩種結果的對比圖,第一張是正確的結果,第二張是預乘以后的結果。這也是在做天天P圖動效SDK第一個版本時遇到的坑。
了解了Bitmap的解碼預乘機制,解決這個問題的思路其實就有兩個方向了:
下面分別介紹一下這兩種方式:
Bitmap解碼時不做預乘。
在Android平臺上,解碼一個Bitmap時,BitmapFactory.Options的參數inPremultiplied控制是否預乘,這個值默認為true,如果設為false則在解碼時不做預乘。需要注意的是,如果是Android View System或者Canvas會默認此值為true進行繪制,如果Bitmap該值為false進行繪制會報RuntimeException。所以在這種情況下inPremultiplied值為false的Bitmap只能用作OpenGL上傳紋理。另外Bitmap的createBitmap和createScaledBitmap方法接受輸入Bitmap的接口,傳入的Bitmap的inPremultiplied值也必須為true,因為這些接口調用也需要繪制源Bitmap。另外inPremultiplied值的設置需要API level 19及以上才支持。
OpenGL混合時不再乘以alpha值
在沒有做預乘時,我們設置的OpenGL混合模式因子為glBlendFunc( GL_SRC_ALPHA , GL_ONE_MINUS_SRC_ALPHA ),即源色RGB值會乘以alpha值,但是因為Bitmap在解碼時已經做了預乘操作,所以源色混合因子不需要再乘以alpha值,此時我們可以設置OpenGL混合模式為glBlendFunc( ONE , GL_ONE_MINUS_SRC_ALPHA )。這種方式也是目前天天P圖Android端動效SDK渲染貼紙采用的方式。
OpenGL混合模式對三維渲染的影響
三維物體和二維圖片渲染不同的一點就是物體的遮擋關系,OpenGL渲染多個三維物體時一般情況下都需要判斷它們之間的前后關系,此時需要用到深度緩沖。
深度緩沖記錄了每一個像素距離觀察者有多近。在啟用深度測試的情況下,如果將要繪制的像素比原來的像素更近,則像素將被繪制。否則,像素就會被忽略掉,不進行繪制。這在繪制不透明的物體時非常有用——不管是先繪制近的物體再繪制遠的物體,還是先繪制遠的物體再繪制近的物體,或者干脆以混亂的順序進行繪制,最后的顯示結果總是近的物體遮住遠的物體。
然而在實現半透明效果時,我們會發現一些問題。如果我們先繪制了一個近距離的半透明物體,則它在深度緩沖區內保留了一些半透明物體的深度信息,此時再繪制遠處的不透明物體,因為不透明物體比當前深度緩沖區內的深度值遠,則會導致遠處的物體將無法再被繪制出來。雖然半透明的物體仍然半透明,但透過它卻看不到遠處的不透明物體了。
深度緩沖區可以設置為只讀或可寫,要解決以上問題,我們可以在繪制半透明物體時將深度緩沖區設置為只讀,這樣雖然半透明物體被繪制上去了,但深度緩沖區還保持在原來的狀態。如果再有一個物體需要渲染在半透明物體之后,在不透明物體之前,則它也可以被繪制(因為此時深度緩沖區中記錄的是那個不透明物體的深度)。以后再要繪制不透明物體時,只需要再 將深度緩沖區設置為可讀可寫的形式即可。如果需要繪制一個一部分半透明一部分不透明的物體怎么辦?只需要把物體分為兩個部分,一部分全是半透明的,一部分全是不透明的,分別繪制就可以了。
需要注意的是,即使使用了以上技巧,我們仍然不能隨心所欲的按照混亂順序來進行繪制。必須是先繪制不透明的物體,然后再繪制透明的物體。舉個例子,假設背景為藍色,近處有一塊紅色玻璃,中間有一個綠色物體。我們首先繪制了藍色背景,然后繪制紅色半透明玻璃,它會先和藍色背景進行混合,最后再繪制中間的綠色物體時,因為綠色物體在藍色背景前面,此時綠色物體會被繪制,但是因為它是不透明的,所以綠色物體會直接覆蓋掉紅色玻璃和藍色背景混合的效果,我們想要的綠色物體單獨與紅色玻璃混合的效果已經不能實現了。
所以總結起來,我們在繪制三維物體時,繪制順序需要首先繪制所有不透明的物體。如果兩個物體都是不透明的,則誰先誰后都沒有關系。然后,將深度緩沖區設置為只讀。接下來,繪制所有半透明的物體。如果兩個物體都是半透明的,則誰先誰后可以根據自己的意愿。不過需要注意的是,先繪制的將成為“目標顏色”,后繪制的將成為“源顏色”,所以繪制的順序將會對最后的渲染結果造成一些影響。所有物體全都繪制完成后,再將深度緩沖區設置為可讀可寫形式。OpenGL提供了一些接口來設置深度緩沖區的是否可讀寫:
GLES20.glDepthMask(false); //深度緩沖區設置為只讀 GLES20.glDepthMask(true); //深度緩沖區設置為可讀寫目前天天P圖Android端動效SDK渲染3D素材使用了開源的GamePlay引擎,目前線上的一些眼鏡類素材都有半透明的鏡片效果,透過半透明的鏡片需要能夠看到后面的鏡架等其他3D物體,所以我們目前的3D素材的混合效果就是采用了上面介紹的三維渲染的技術方案。
總結
OpenGL混合模式避免了直接在Fragment Shader中做混合時紋理空間和渲染時間的額外開銷,所以我們在開發中對于簡單的混合算法可以盡量使用OpenGL混合模式。
OpenGL混合模式的源因子和目標因子可以設置多種模式。在Android平臺上因為Bitmap解碼時預乘的影響有時需要調整源因子的混合模式。
在進行三維物體繪制和混合時,繪制的順序十分重要,不僅要考慮源因子和目標因子,還應該考慮深度緩沖區。必須先繪制所有不透明的物體,再繪制半透明的物體。在繪制半透明物體時前,還需要將深度緩沖區設置為只讀形式,否則可能出現繪制結果錯誤。
總結
以上是生活随笔為你收集整理的Android图像处理系列:OpenGL混合模式的使用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: c语言中计算机随机给出的数,用c语言产生
- 下一篇: android usb arduino,