1. 程式人生 > >[WebGL入門]二十九,透明混色

[WebGL入門]二十九,透明混色


混色

上次介紹了紋理引數和它的設定方法,這次暫時不說紋理,來介紹一下混色。

混合(blend)大家應該知道了,那麼混合什麼呢,WebGL中把顏色混合在一起就稱之為混色。

跟遮擋剔除和深度測試等一樣,混色預設也是無效的,需要先把它設定為有效才能使用。但是,進行混色,把顏色混合在一起又能實現什麼呢?

實際上,這次的主要目的是透明混色,也就是需要通過混色來進行半透明處理。其他的,雖然通過顏色之間的混合可以衍生出的各種各樣的效果,但是本次只是理解一下混色的基礎,怎麼處理混色可以得到半透明的效果,只需要先理解這一點就夠了,內容可能稍微有些複雜,不過也不用去考慮太多了。

混色到底是什麼

先來說明一下混色的概念。所謂混色,就是剛才敘述的那樣,把顏色混合在一起。那麼用來混合的顏色是什麼顏色呢?頂點的顏色?還是紋理的顏色?答案既不是頂點顏色也不是紋理 顏色。而是源顏色(即將要繪製的顏色)和目標顏色(已經繪製的顏色)。

WebGL在渲染東西之前,必須進行context的初始化,將顏色和深度清除。持續迴圈的處理中,需要每次執行clear函式,將context的顏色初始化。這時候,要通過clearColor函式指定一種顏色將context塗抹一遍,這也是所謂的目標顏色中的顏色,也就是已經畫上去的顏色了。而接下來要渲染什麼的話,要進行繪製,繪製的顏色就是源顏色。比如說,已經在context上渲染了一些東西,這個已經渲染了的顏色就是源顏色,而接下來要進行渲染的顏色就是目標顏色。

設定混色為有效,就是讓源顏色和目標顏色這兩種顏色以某種方式進行混色。兩種顏色以怎樣的方式混合,有很多選擇分支可以選擇的。將這些選擇分支進行復雜的組合,可以得到多彩的的效果。

混合係數

混合的時候的混合方式是有非常多的種類的,然後將這些方式進行各種各樣的組合,最終決定了context上的顏色。

說實話,這些是非常難理解的一個領域。想一下子全部理解肯定會遇到些非常難的知識點,所以不需要勉強自己全部理解,先知道一下混色的選擇分支就可以了。

想要使用混色,需要先設定為有效,這時使用我們已經很熟悉的enable函式進行設定。

>設定混色為有效的程式碼

gl.enable(gl.BLEND);

好了,簡單吧。向enable函式中傳入的引數是一個內建常量gl.BLEND,這樣就可以設定混色為有效了。想設定為無效的時候,使用disable函式就可以了。

將混色設定為有效之後,接下來就是混合係數的設定了。

*混合係數的名稱可能不是統一的叫法,但是本站的文章,混色相關的設定都統稱為混合係數了。

混合係數,對源顏色和目標顏色是可以分別設定的,設定的函式為blendFunc函式,這個函式接收兩個引數,第一個引數是源顏色的混合係數,第二個引數是目標顏色的混合係數。

>blendFunc函式

gl.blendFunc(sourceFactor, destinationFactor);
上述的source就是源顏色,destination就是目標顏色。接著看,可以指定的混合係數在下表中。

>混合係數一覽

引數名値・式
gl.ZERO(0, 0, 0, 0)
gl.ONE(1, 1, 1, 1)
gl.SRC_COLOR(Rs, Gs, Bs, As)
gl.DST_COLOR(Rd, Gd, Bd, Ad)
gl.ONE_MINUS_SRC_COLOR(1, 1, 1, 1) - (Rs, Gs, Bs, As)
gl.ONE_MINUS_DST_COLOR(1, 1, 1, 1) - (Rd, Gd, Bd, Ad)
gl.SRC_ALPHA(As, As, As, As)
gl.DST_ALPHA(Ad, Ad, Ad, Ad)
gl.ONE_MINUS_SRC_ALPHA(1, 1, 1, 1) - (As, As, As, As)
gl.ONE_MINUS_DST_ALPHA(1, 1, 1, 1) - (Ad, Ad, Ad, Ad)
gl.CONSTANT_COLOR(Rc, Gc, Bc, Ac)
gl.ONE_MINUS_CONSTANT_COLOR(1, 1, 1, 1) - (Rc, Gc, Bc, Ac)
gl.CONSTANT_ALPHA(Ac, Ac, Ac, Ac)
gl.ONE_MINUS_CONSTANT_ALPHA(1, 1, 1, 1) - (Ac, Ac, Ac, Ac)
gl.SRC_ALPHA_SATURATE(f, f, f, 1) f = min(As, 1 - Ad)
種類相當多吧,乍一看估計都不知道這是些什麼東西,現在就來詳細看一下。

首先,看上面的[值・式]一欄中的值和式子,全都是四個元素,這些純粹是顏色的各個要素,就是說從左到右依次為RGBA。還有大寫的英文字母R、G等開頭的部分,指的就是RGBA的哪個元素,接在後面的小寫字母s和d的部分,就分別是[ s = source ]・[ d = destination ]。可是最後還出現了字母c是吧,這個是[ c = constant ],暫時先不用去管它,只需要注意s和d這些混合係數就行了。

其實,通過混合係數的名字,大致也能知道這些係數的意思。比如最開始出現的gl.ZERO,就是各個元素都是0的意思,下面出現的gl.ONE也一樣,就是各個元素都是1的意思。

WebGL的混色設定為有效的時候,像下面這樣執行blendFunc函式的話,是和混合係數的預設值相同的。

>設定和WebGL的預設值相同的程式碼

gl.blendFunc(gl.ONE, gl.ZERO);
那麼,這麼設定之後,實際上顏色的計算是怎樣進行的呢。

第一個引數是指定[源顏色]如何處理,比如源顏色是(0.5, 0.5, 0.5, 1.0),將這些值都乘上1,結果還是(0.5, 0.5, 0.5, 1.0),沒有任何變化。

第二個引數是指定[目標顏色],也就是context的顏色如何處理,第二個引數指定了gl.ZERO,那無論context上是什麼顏色,最後都會變成0。

最終在context上繪製的顏色,就是下面這個式子計算出的結果。

描畫色 = 描畫元の色 * sourceFactor + 描畫先の色 * destinationFactor
這麼看結果就很直白了。預設的設定,就是隻輸出源顏色端的顏色(source),而目標顏色端的顏色無論是什麼,最終都是源顏色的顏色。

就是這樣,指定某個混合係數,就能對源顏色和目標顏色進行一些處理。通過適當的混合係數,就可以實現透明混色了。

實現透明混色

實際問題來了,指定哪個混合係數才能實現透明混色呢。透明混色,就是使用顏色的透明部分(alpha值)進行混合處理。顏色的alpha值是一個可以控制顏色的特殊係數。因為是通過顏色的透明部分來混合顏色,所以叫做透明混色。

首先,進行透明混色的前提,假設context上的原有顏色(初始化的時候)是不透明的藍色,不透明的藍色用RGBA來表示就是(0.0, 0.0, 1.0, 1.0)。

然後,繪製紅色的多邊形的時候,如果把這個紅色的多邊形的不透明度設定為70%(1.0, 0.0, 0.0, 0.7)進行繪製的話,要怎麼做呢?

藍色的context上繪製透明度為70%的紅色多邊形的話,就會變成略微發紅的紫色。如果用數值來表示的話,就是下面這樣。

源顏色端(多邊形) + 目標顏色端(context) = 最終的輸出顏色

(1.0, 0.0, 0.0) * sFactor + (0.0, 0.0, 1.0) * dFactor = (0.7, 0.0, 0.3)

 * sFactor +  * dFactor = 

那各個混合係數指定為什麼合適呢。因為是要做透明混色,所以指定和透明值無關的混合係數的話就沒什麼意義了。光是這樣就可以篩除大量的選擇分支了。而且,仔細看源顏色的顏色的話,顏色成分是0.7倍,目標顏色的顏色成分是0.3倍。匯出這種結果到底要怎麼設定混合係數呢。

換句話說,源顏色端的多邊形的透明度為0.7,這麼說就容易懂了吧。但是無論說的多明白也是沒用的,還是看一下具體寫法。

首先,多邊形的顏色,直接與多邊形的顏色的透明度相乘就可以了,因為多邊形的顏色的透明度為0.7,只需要單純的相乘,如下。

源顏色的顏色計算:
sFactor = 源顏色の透明度(0.7) [ gl.SRC_ALPHA ](1.0, 0.0, 0.0) * sFactor(0.7, 0.7, 0.7) = (0.7, 0.0, 0.0)
接著是目標顏色,和剛才不同的是,B稱為的1.0最終也需要變成0.3,這樣的話,使用從1減去源顏色的透明度得到的值就行了。
目標顏色的計算:
dFactor = 1.0 - 源顏色端的透明度(0.7) = (0.3) [ gl.ONE_MINUS_SRC_ALPHA ](0.0, 0.0, 1.0) * dFactor(0.3, 0.3, 0.3) = (0.0, 0.0, 0.3)

這樣匯出的源顏色和目標顏色,加上之前算出的結果,就是最終在context上輸出的顏色。

最終輸出的顏色:
(0.7, 0.0, 0.0) + (0.0, 0.0, 0.3) = (0.7, 0.0, 0.3)
雖然感覺是繞了個大圈,但是這樣就是正確的透明混色的計算。sFactor是直接和源顏色的透明度相乘,所以需要指定混色系數為gl.SRC_ALPHA。dFactor是乘與1減去源顏色的透明度,所以使用的混合係數是gl.ONE_MINUS_SRC_ALPHA。綜合一下,為了實現透明混色,混合係數就像下面這樣設定。

>指定透明混色

gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

對比下文章開頭的混合係數一覽表,就應該能明白為什麼這麼設定就能正確得到透明混色了。

程式

那麼接著來看具體的程式要如何來寫吧。這次要渲染兩個四角多邊形,第一個多邊形,使用紋理且不使用混色。第二個多邊形不使用紋理但是使用混色。

為了讓demo可以自由改變透明度,使用HTML5中的新加的input標籤,型別是range。

>range的例子

input type range

這個input標籤里加上id,然後在javascript中使用,讓透明度可以任意的變化。另外,range的最小值是0,最大值是100,然後除於100之後傳給著色器,這樣透明度就是通常的0-1了。

這次要同時渲染使用紋理的多邊形和不使用紋理的多邊形,所以要通知著色器是否使用紋理。著色器內,和javascript一樣,可以使用if,來分別處理。看下這次demo的著色器的程式碼吧,首先是頂點著色器。

>頂點著色器demo

attribute vec3  position;
attribute vec4  color;
attribute vec2  textureCoord;
uniform   mat4  mvpMatrix;
uniform   float vertexAlpha;
varying   vec4  vColor;
varying   vec2  vTextureCoord;

void main(void){
    vColor        = vec4(color.rgb, color.a * vertexAlpha);
    vTextureCoord = textureCoord;
    gl_Position   = mvpMatrix * vec4(position, 1.0);
}
這次的attribute變數要接收頂點的[ 位置 ]・[ 顏色 ]・[ 紋理座標 ]這三個屬性,這個簡單吧。而uniform變數接收[ 座標變換矩陣 ]・[ 從range獲取的透明度 ]等兩個屬性。

傳入片段著色器的varying變數也有兩個,分別是顏色屬性和紋理座標屬性。這裡要重點看的是設定vColor的部分。
>變數vColor的設定部分

vColor = vec4(color.rgb, color.a * vertexAlpha);
使用頂點顏色和從range獲取的透明度,來得到傳入片段著色器的資訊。vec4這個變數型別,可以靈活的儲存xyzw,rgba等這樣的資訊,所以利用這種變數來儲存頂點的透明度與標籤設定的透明度相乘得到的最後的透明度。應該也不是特別難吧,想想目前為止我們已經做的事情,也算是比較簡單了吧。

接著看片段著色器吧。

>片段著色器的demo

precision mediump float;

uniform sampler2D texture;
uniform int       useTexture;
varying vec4      vColor;
varying vec2      vTextureCoord;

void main(void){
    vec4 destColor = vec4(0.0);
    if(bool(useTexture)){
        vec4 smpColor = texture2D(texture, vTextureCoord);
        destColor = vColor * smpColor;
    }else{
        destColor = vColor;
    }
    gl_FragColor = destColor;
}

這裡看起來有些複雜,其實做的事情並不那麼複雜。首先用了兩個uniform變數。一個是( sampler2D texture )關於紋理的資訊、另一個( int useTexture )表示是否使用紋理。變數useTexture是int型,也就是說傳入的是個整數值,然後在著色器中再變換成布林型。

具體做了些什麼呢,看片段著色器的main函式內部。首先聲明瞭一個變數destColor,用來儲存最終輸出的顏色。然後下面用if來判斷下該做什麼。這裡使用了bool這個內建函式,來將整數行數值變換成布林型,根據變換的結果來決定是否使用紋理的顏色。為什麼要特意把整型變為布林型呢,因為uniform變數沒有辦法直接使用布林型,從javascript向著色器中傳遞資料的時候用的是uniform4fv和uniform1i等型別,而像uniform1b這樣的型別是不存在的,所以沒有辦法直接向著色器傳遞布林型。所以就利用uniform1i傳遞整型資料,然後在著色器中變換成布林型使用。※不管怎麼說吧,雖然特意進行了布林型變換,但是隻利用整數行比較也是可以區分的,具體怎麼寫就看自己的喜好了,這次只是為了告訴大家著色器中變數型別是可以變換的。

看下主程式

雖然文章有點兒長了,還得繼續看看javascript部分。主程式中,進行了適當的attribute和uniform相關的處理之後,將深度測試和紋理設定為有效。這次只是單純的繪製平面多邊形,所以頂點剔除則保持無效。
在持續迴圈當中,根據條件決定進行什麼渲染處理,這部分使用下面的函式。

>函式 blend_type

// ブレンドタイプを設定する関數
function blend_type(prm){
    switch(prm){
        // 透過処理
        case 0:
            gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
            break;
        // 加算合成
        case 1:
            gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
            break;
        default:
            break;
    }
}
這個函式根據接收的整數值引數,進行不同的混色方法。這次的demo除了一般的透明混色外,還使用了加算合成這樣比較有代表性的混色手法。
函式blend_type的引數為0的時候進行透明混色處理,引數為1的時候進行加算合成處理。進行透明混色的時候混合係數就是剛才說明的那樣設定。加算合成的合成係數沒有詳細說明,參照一下blendFunc函式的引數就知道了。
混合引數設定完之後,接著就從input標籤中獲取與頂點顏色相乘的透明度。

>獲取透明度

// エレメントからアルファ成分を取得
var vertexAlpha = parseFloat(elmRange.value / 100);
這裡使用的parseFloat函式的作用是將數值變成浮點型。因為獲取到的數值是0~100,這樣做是將數值範圍控制在0~1。
接著是繪製多邊形模型,如上所述,這次渲染兩個多邊形模型。首先,繪製距離鏡頭較遠的多邊形,使用紋理且不使用混色。
>繪製第一個多邊形
// 模型座標變換的生成
m.identity(mMatrix);
m.translate(mMatrix, [0.25, 0.25, -0.25], mMatrix);
m.rotate(mMatrix, rad, [0, 1, 0], mMatrix);
m.multiply(tmpMatrix, mMatrix, mvpMatrix);

// 繫結紋理
gl.bindTexture(gl.TEXTURE_2D, texture);

// 設定混色無效
gl.disable(gl.BLEND);

// uniform變數的註冊及繪製
gl.uniformMatrix4fv(uniLocation[0], false, mvpMatrix);
gl.uniform1f(uniLocation[1], 1.0);
gl.uniform1i(uniLocation[2], 0);
gl.uniform1i(uniLocation[3], true);
gl.drawElements(gl.TRIANGLES, index.length, gl.UNSIGNED_SHORT, 0);
這裡注意一下uniform相關的處理。uniformLocation的索引0是座標變換矩陣,索引1是在著色器中與頂點的透明度相乘的透明度值,索引2是為了註冊紋理使用的,索引3是是否使用紋理。
接著,繪製不使用紋理,而混色有效的多邊形。
>繪製第二個多邊形
// 模型座標變換的生成
m.identity(mMatrix);
m.translate(mMatrix, [-0.25, -0.25, 0.25], mMatrix);
m.rotate(mMatrix, rad, [0, 0, 1], mMatrix);
m.multiply(tmpMatrix, mMatrix, mvpMatrix);

// 解除紋理的繫結
gl.bindTexture(gl.TEXTURE_2D, null);

// 設定混色有效
gl.enable(gl.BLEND);

// uniform變數的註冊和繪製
gl.uniformMatrix4fv(uniLocation[0], false, mvpMatrix);
gl.uniform1f(uniLocation[1], vertexAlpha);
gl.uniform1i(uniLocation[2], 0);
gl.uniform1i(uniLocation[3], false);
gl.drawElements(gl.TRIANGLES, index.length, gl.UNSIGNED_SHORT, 0);

座標變換矩陣要比渲染第一個多邊形的時候靠近一些。並且,將從input標籤中獲取的透明度傳入著色器。


總結

透明混色雖然已經介紹完了,但是一開始想要徹底理解是很費力的。根據自己的經驗,只要理解之後,接下來就是單純的四則運算了。自由組合混合係數就能實現各種效果。這次的demo雖然只是進行了透明處理和加成合成,但是類似顏色反轉等特殊的合成也都是可以的,深入研究之後可能就能明白了。
另外,不光是透明混色,關於混色的處理也有很多限制,這些下次會詳細介紹。