天天看點

OpenGL入門學習——第九課

今天介紹關于OpenGL混合的基本知識。混合是一種常用的技巧,通常可以用來實作半透明。但其實它也是十分靈活的,你可以通過不同的設定得到不同的混合結果,産生一些有趣或者奇怪的圖象。

混合是什麼呢?混合就是把兩種顔色混在一起。具體一點,就是把某一像素位置原來的顔色和将要畫上去的顔色,通過某種方式混在一起,進而實作特殊的效果。

假設我們需要繪制這樣一個場景:透過紅色的玻璃去看綠色的物體,那麼可以先繪制綠色的物體,再繪制紅色玻璃。在繪制紅色玻璃的時候,利用“混合”功能,把将要繪制上去的紅色和原來的綠色進行混合,于是得到一種新的顔色,看上去就好像玻璃是半透明的。

要使用OpenGL的混合功能,隻需要調用:glEnable(GL_BLEND);即可。

要關閉OpenGL的混合功能,隻需要調用:glDisable(GL_BLEND);即可。

注意:隻有在RGBA模式下,才可以使用混合功能,顔色索引模式下是無法使用混合功能的。

一、源因子和目标因子

前面我們已經提到,混合需要把原來的顔色和将要畫上去的顔色找出來,經過某種方式處理後得到一種新的顔色。這裡把将要畫上去的顔色稱為“源顔色”,把原來的顔色稱為“目标顔色”。

OpenGL會把源顔色和目标顔色各自取出,并乘以一個系數(源顔色乘以的系數稱為“源因子”,目标顔色乘以的系數稱為“目标因子”),然後相加,這樣就得到了新的顔色。(也可以不是相加,新版本的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,不需要考慮越界的問題。

源因子和目标因子是可以通過glBlendFunc函數來進行設定的。glBlendFunc有兩個參數,前者表示源因子,後者表示目标因子。這兩個參數可以是多種值,下面介紹比較常用的幾種。

GL_ZERO:     表示使用0.0作為因子,實際上相當于不使用這種顔色參與混合運算。

GL_ONE:      表示使用1.0作為因子,實際上相當于完全的使用了這種顔色參與混合運算。

GL_SRC_ALPHA:表示使用源顔色的alpha值來作為因子。

GL_DST_ALPHA:表示使用目标顔色的alpha值來作為因子。

GL_ONE_MINUS_SRC_ALPHA:表示用1.0減去源顔色的alpha值來作為因子。

GL_ONE_MINUS_DST_ALPHA:表示用1.0減去目标顔色的alpha值來作為因子。

除此以外,還有GL_SRC_COLOR(把源顔色的四個分量分别作為因子的四個分量)、GL_ONE_MINUS_SRC_COLOR、GL_DST_COLOR、GL_ONE_MINUS_DST_COLOR等,前兩個在OpenGL舊版本中隻能用于設定目标因子,後兩個在OpenGL舊版本中隻能用于設定源因子。新版本的OpenGL則沒有這個限制,并且支援新的GL_CONST_COLOR(設定一種常數顔色,将其四個分量分别作為因子的四個分量)、GL_ONE_MINUS_CONST_COLOR、GL_CONST_ALPHA、GL_ONE_MINUS_CONST_ALPHA。另外還有GL_SRC_ALPHA_SATURATE。新版本的OpenGL還允許顔色的alpha值和RGB值采用不同的混合因子。但這些都不是我們現在所需要了解的。畢竟這還是入門教材,不需要整得太複雜~

舉例來說:

如果設定了glBlendFunc(GL_ONE, GL_ZERO);,則表示完全使用源顔色,完全不使用目标顔色,是以畫面效果和不使用混合的時候一緻(當然效率可能會低一點點)。如果沒有設定源因子和目标因子,則預設情況就是這樣的設定。

如果設定了glBlendFunc(GL_ZERO, GL_ONE);,則表示完全不使用源顔色,是以無論你想畫什麼,最後都不會被畫上去了。(但這并不是說這樣設定就沒有用,有些時候可能有特殊用途)

如果設定了glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);,則表示源顔色乘以自身的alpha值,目标顔色乘以1.0減去源顔色的alpha值,這樣一來,源顔色的alpha值越大,則産生的新顔色中源顔色所占比例就越大,而目标顔色所占比例則減小。這種情況下,我們可以簡單的将源顔色的alpha值了解為“不透明度”。這也是混合時最常用的方式。

如果設定了glBlendFunc(GL_ONE, GL_ONE);,則表示完全使用源顔色和目标顔色,最終的顔色實際上就是兩種顔色的簡單相加。例如紅色(1, 0, 0)和綠色(0, 1, 0)相加得到(1, 1, 0),結果為黃色。

注意:

所謂源顔色和目标顔色,是跟繪制的順序有關的。假如先繪制了一個紅色的物體,再在其上繪制綠色的物體。則綠色是源顔色,紅色是目标顔色。如果順序反過來,則紅色就是源顔色,綠色才是目标顔色。在繪制時,應該注意順序,使得繪制的源顔色與設定的源因子對應,目标顔色與設定的目标因子對應。不要被混亂的順序搞暈了。

二、二維圖形混合舉例

下面看一個簡單的例子,實作将兩種不同的顔色混合在一起。為了便于觀察,我們繪制兩個矩形:glRectf(-1, -1, 0.5, 0.5);glRectf(-0.5, -0.5, 1, 1);,這兩個矩形有一個重疊的區域,便于我們觀察混合的效果。

先來看看使用glBlendFunc(GL_ONE, GL_ZERO);的,它的結果與不使用混合時相同。

void myDisplay(void)

{

    glClear(GL_COLOR_BUFFER_BIT);

    glEnable(GL_BLEND);

    glBlendFunc(GL_ONE, GL_ZERO);

    glColor4f(1, 0, 0, 0.5);

    glRectf(-1, -1, 0.5, 0.5);

    glColor4f(0, 1, 0, 0.5);

    glRectf(-0.5, -0.5, 1, 1);

    glutSwapBuffers();

}

嘗試把glBlendFunc的參數修改為glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);以及glBlendFunc(GL_ONE, GL_ONE);,觀察效果。第一種情況下,效果與沒有使用混合時相同,後繪制的圖形會覆寫先繪制的圖形。第二種情況下,alpha被當作“不透明度”,由于被設定為0.5,是以兩個矩形看上去都是半透明的,乃至于看到黑色背景。第三種是将顔色相加,紅色和綠色相加得到黃色。

OpenGL入門學習——第九課
OpenGL入門學習——第九課
OpenGL入門學習——第九課

三、實作三維混合

也許你迫不及待的想要繪制一個三維的帶有半透明物體的場景了。但是現在恐怕還不行,還有一點是在進行三維場景的混合時必須注意的,那就是深度緩沖。

深度緩沖是這樣一段資料,它記錄了每一個像素距離觀察者有多近。在啟用深度緩沖測試的情況下,如果将要繪制的像素比原來的像素更近,則像素将被繪制。否則,像素就會被忽略掉,不進行繪制。這在繪制不透明的物體時非常有用——不管是先繪制近的物體再繪制遠的物體,還是先繪制遠的物體再繪制近的物體,或者幹脆以混亂的順序進行繪制,最後的顯示結果總是近的物體遮住遠的物體。

然而在你需要實作半透明效果時,發現一切都不是那麼美好了。如果你繪制了一個近距離的半透明物體,則它在深度緩沖區内保留了一些資訊,使得遠處的物體将無法再被繪制出來。雖然半透明的物體仍然半透明,但透過它看到的卻不是正确的内容了。

要解決以上問題,需要在繪制半透明物體時将深度緩沖區設定為隻讀,這樣一來,雖然半透明物體被繪制上去了,深度緩沖區還保持在原來的狀态。如果再有一個物體出現在半透明物體之後,在不透明物體之前,則它也可以被繪制(因為此時深度緩沖區中記錄的是那個不透明物體的深度)。以後再要繪制不透明物體時,隻需要再将深度緩沖區設定為可讀可寫的形式即可。嗯?你問我怎麼繪制一個一部分半透明一部分不透明的物體?這個好辦,隻需要把物體分為兩個部分,一部分全是半透明的,一部分全是不透明的,分别繪制就可以了。

即使使用了以上技巧,我們仍然不能随心所欲的按照混亂順序來進行繪制。必須是先繪制不透明的物體,然後繪制透明的物體。否則,假設背景為藍色,近處一塊紅色玻璃,中間一個綠色物體。如果先繪制紅色半透明玻璃的話,它先和藍色背景進行混合,則以後繪制中間的綠色物體時,想單獨與紅色玻璃混合已經不能實作了。

總結起來,繪制順序就是:首先繪制所有不透明的物體。如果兩個物體都是不透明的,則誰先誰後都沒有關系。然後,将深度緩沖區設定為隻讀。接下來,繪制所有半透明的物體。如果兩個物體都是半透明的,則誰先誰後隻需要根據自己的意願(注意了,先繪制的将成為“目标顔色”,後繪制的将成為“源顔色”,是以繪制的順序将會對結果造成一些影響)。最後,将深度緩沖區設定為可讀可寫形式。

調用glDepthMask(GL_FALSE);可将深度緩沖區設定為隻讀形式。調用glDepthMask(GL_TRUE);可将深度緩沖區設定為可讀可寫形式。

一些網上的教程,包括大名鼎鼎的NeHe教程,都在使用三維混合時直接将深度緩沖區禁用,即調用glDisable(GL_DEPTH_TEST);。這樣做并不正确。如果先繪制一個不透明的物體,再在其背後繪制半透明物體,本來後面的半透明物體将不會被顯示(被不透明的物體遮住了),但如果禁用深度緩沖,則它仍然将會顯示,并進行混合。NeHe提到某些顯示卡在使用glDepthMask函數時可能存在一些問題,但可能是由于我的閱曆有限,并沒有發現這樣的情況。

那麼,實際的示範一下吧。我們來繪制一些半透明和不透明的球體。假設有三個球體,一個紅色不透明的,一個綠色半透明的,一個藍色半透明的。紅色最遠,綠色在中間,藍色最近。根據前面所講述的内容,紅色不透明球體必須首先繪制,而綠色和藍色則可以随意修改順序。這裡為了示範不注意設定深度緩沖的危害,我們故意先繪制最近的藍色球體,再繪制綠色球體。

為了讓這些球體有一點立體感,我們使用光照。在(1, 1, -1)處設定一個白色的光源。代碼如下:

void setLight(void)

{

    static const GLfloat light_position[] = {1.0f, 1.0f, -1.0f, 1.0f};

    static const GLfloat light_ambient[]  = {0.2f, 0.2f, 0.2f, 1.0f};

    static const GLfloat light_diffuse[]  = {1.0f, 1.0f, 1.0f, 1.0f};

    static const GLfloat light_specular[] = {1.0f, 1.0f, 1.0f, 1.0f};

    glLightfv(GL_LIGHT0, GL_POSITION, light_position);

    glLightfv(GL_LIGHT0, GL_AMBIENT,  light_ambient);

    glLightfv(GL_LIGHT0, GL_DIFFUSE,  light_diffuse);

    glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular);

    glEnable(GL_LIGHT0);

    glEnable(GL_LIGHTING);

    glEnable(GL_DEPTH_TEST);

}

每一個球體顔色不同。是以它們的材質也都不同。這裡用一個函數來設定材質。

void setMatirial(const GLfloat mat_diffuse[4], GLfloat mat_shininess)

{

    static const GLfloat mat_specular[] = {0.0f, 0.0f, 0.0f, 1.0f};

    static const GLfloat mat_emission[] = {0.0f, 0.0f, 0.0f, 1.0f};

    glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, mat_diffuse);

    glMaterialfv(GL_FRONT, GL_SPECULAR,  mat_specular);

    glMaterialfv(GL_FRONT, GL_EMISSION,  mat_emission);

    glMaterialf (GL_FRONT, GL_SHININESS, mat_shininess);

}

有了這兩個函數,我們就可以根據前面的知識寫出整個程式代碼了。這裡隻給出了繪制的部分,其它部分大家可以自行完成。

void myDisplay(void)

{

    // 定義一些材質顔色

    const static GLfloat red_color[] = {1.0f, 0.0f, 0.0f, 1.0f};

    const static GLfloat green_color[] = {0.0f, 1.0f, 0.0f, 0.3333f};

    const static GLfloat blue_color[] = {0.0f, 0.0f, 1.0f, 0.5f};

    // 清除螢幕

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // 啟動混合并設定混合因子

    glEnable(GL_BLEND);

    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    // 設定光源

    setLight();

    // 以(0, 0, 0.5)為中心,繪制一個半徑為.3的不透明紅色球體(離觀察者最遠)

    setMatirial(red_color, 30.0);

    glPushMatrix();

    glTranslatef(0.0f, 0.0f, 0.5f);

    glutSolidSphere(0.3, 30, 30);

    glPopMatrix();

    // 下面将繪制半透明物體了,是以将深度緩沖設定為隻讀

    glDepthMask(GL_FALSE);

    // 以(0.2, 0, -0.5)為中心,繪制一個半徑為.2的半透明藍色球體(離觀察者最近)

    setMatirial(blue_color, 30.0);

    glPushMatrix();

    glTranslatef(0.2f, 0.0f, -0.5f);

    glutSolidSphere(0.2, 30, 30);

    glPopMatrix();

    // 以(0.1, 0, 0)為中心,繪制一個半徑為.15的半透明綠色球體(在前兩個球體之間)

    setMatirial(green_color, 30.0);

    glPushMatrix();

    glTranslatef(0.1, 0, 0);

    glutSolidSphere(0.15, 30, 30);

    glPopMatrix();

    // 完成半透明物體的繪制,将深度緩沖區恢複為可讀可寫的形式

    glDepthMask(GL_TRUE);

    glutSwapBuffers();

}

大家也可以将上面兩處glDepthMask删去,結果會看到最近的藍色球雖然是半透明的,但它的背後直接就是紅色球了,中間的綠色球沒有被正确繪制。

OpenGL入門學習——第九課
OpenGL入門學習——第九課

小結:

本課介紹了OpenGL混合功能的相關知識。

混合就是在繪制時,不是直接把新的顔色覆寫在原來舊的顔色上,而是将新的顔色與舊的顔色經過一定的運算,進而産生新的顔色。新的顔色稱為源顔色,原來舊的顔色稱為目标顔色。傳統意義上的混合,是将源顔色乘以源因子,目标顔色乘以目标因子,然後相加。

源因子和目标因子是可以設定的。源因子和目标因子設定的不同直接導緻混合結果的不同。将源顔色的alpha值作為源因子,用1.0減去源顔色alpha值作為目标因子,是一種常用的方式。這時候,源顔色的alpha值相當于“不透明度”的作用。利用這一特點可以繪制出一些半透明的物體。

在進行混合時,繪制的順序十分重要。因為在繪制時,正要繪制上去的是源顔色,原來存在的是目标顔色,是以先繪制的物體就成為目标顔色,後來繪制的則成為源顔色。繪制的順序要考慮清楚,将目标顔色和設定的目标因子相對應,源顔色和設定的源因子相對應。

在進行三維混合時,不僅要考慮源因子和目标因子,還應該考慮深度緩沖區。必須先繪制所有不透明的物體,再繪制半透明的物體。在繪制半透明物體時前,還需要将深度緩沖區設定為隻讀形式,否則可能出現畫面錯誤。

繼續閱讀