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