之前學習了使用模板緩衝來製作特殊效果,本節將繼續學習一個高級主題-混色(Blending)。通過使用混色,我們可以製作透明、半透明效果。
混色的概念
所謂混色,就是將當前要繪製的物體的顏色和顏色緩衝區中已經繪製了的物體的顏色進行混合,最終決定了當前物體的顏色。例如下面的圖中,狙擊槍的瞄準器本身是帶有藍色的,將它和後面的任務混合在一起,形成了我們看到的最終效果,這個效果裏既有瞄準器的藍色成分,也有後麪人物的像素,主要是後面人物的像素。
實際上我們通過玻璃看到外面的景象就是一種混色,有的玻璃完全透明則主要顯示外面的景象,而另一些玻璃不是完全透明則成像中包含一部分玻璃的顏色。在OpenGL中使用混色,可以實現很多效果,其中比較常見的就是透明效果。下面具體實現完全透明和半透明效果。
完全透明效果
完全透明表現的是,當前物體例如透明玻璃,將後面的像素完全展示出來,而當前物體則不必顯示。實現完全透明效果,我們通過對物體的透明度進行判斷,當小於一定閾值,例如0.1時,我們則丟棄該片元,使其後面的片元得到顯示。
首先我們加載一個草的模型,對於草這種模型,它要麼完全透明,可以透過它看到後面的物體,要麼不透明展示爲草的細節。繪製草這種模型時,我們通過往矩形塊上添加草的紋理來實現。加載了草的模型,使用深度測試一節的立方體,繪製出來的效果如下:
這裏我們看到,草模型中透明部分和不透明部分沒有得到區分,因而擋住了後面的立方體和草模型。在RGBA表達的顏色重,alpha成分一直以來,我們都是設置爲1.0,實際上這個分量表達的就是透明度。1.0表示爲完全不透明,0.0則表示完全透明。我們可以根據加載的草模型的alpha值判斷是否應該丟棄片元來實現透明效果。
加載RGBA模型,和之前一直實現的加載RGB模型,有少許不同,我們要注意兩點:
-
使用SOIL庫的時候參數要從SOIL_LOAD_RGB改爲SOIL_LOAD_RGBA
-
glTexImage2D中圖片格式和內部表示要從GL_RGB改爲GL_RGBA.
-
glTexParameteri紋理的wrap方式需要從GL_REPEAT改爲GL_CLAMP_TO_EDGE,這個主要是爲了防止由於使用GL_REPEAT時紋理邊緣部分插值導致出現我們不需要的半透明的效果
加載紋理的函數聲明爲:
static GLuint load2DTexture(const char* filename, GLint internalFormat = GL_RGB,
GLenum picFormat = GL_RGB, int loadChannels = SOIL_LOAD_RGB, GLboolean alpha=false);
完整的實現可以參考texture.h。
在代碼中加載紋理變更爲:
GLuint transparentTextId =TextureHelper::load2DTexture(
"grass.png", GL_RGBA, GL_RGBA, SOIL_LOAD_RGBA, true);
在片元着色器中,根據alpha值是否小於設定的閾值,我們決定是否丟棄片元:
#version 330 core
in vec2 TextCoord;
uniform sampler2D text;
out vec4 color;
void main()
{
vec4 textColor = texture(text, TextCoord);
if(textColor.a < 0.1) // < 0.1則丟棄片元
discard;
color = textColor;
}
這種方法實現的透明效果如下圖所示:
使用alpha值決定是否丟棄片元,我們實現的透明效果是要麼完全透明(alpha <0.1),要麼不透明(alpha >= 0.1)。實際應用中還需要使用半透明效果。
OpenGL中混色計算
混色後可以通過當前物體看到其後的物體,這裏當前物體的最終顏色是由當前物體的顏色(源的顏色 source color)和顏色緩衝區中的顏色(目的顏色 destination color)混色決定的,也就是進行相應的混合計算得到的。
要開啓混色功能需要使用:
glEnable(GL_BLEND);
混色是計算出來的,主體的公式是這樣的:
Result=source∗sfactor+destination∗dfactor(1)
公式1中source和destination表示的分別是源和目的顏色,sFactor 和dFactor分別表示源和目的顏色的計算係數。
用戶可以靈活的控制公式1的sFactor 和dFactor ,上式計算是逐個顏色分量RGBA計算的。
OpenGL提供了函數glBlendFunc用來設置上面的sfactor和dfactor,函數原型爲:
API void glBlendFunc( GLenum sfactor,
GLenum dfactor);
sfactor和dfactor用來指定源和目的顏色計算的係數,使用的是GL_ZERO, GL_ONE, GL_SRC_COLOR, GL_ONE_MINUS_SRC_COLOR等枚舉值。
例如,一個紅色和綠色方塊進行混色,效果如下圖所示:
這裏綠色(0.0,1.0,0.0,0.6)作爲源,紅色(1.0,0.0,0.0,1.0)作爲目的顏色進行混合。我們設置參數爲:
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
則進行計算的過程爲:
result=(0.0,1.0,0.0,0.6)∗(0.6,0.6,0.6,0.6)+(1.0,0.0,0.0,1.0)∗(0.4,0.4,0.4,0.4)=(0.4,0.6,0.0,0.76)[Math Processing Error]
除了glBlendFunc外,還可以使用使用glBlendFuncSeparate單獨指定RGB,Alpha的計算係數。
API void glBlendFuncSeparate(GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha);
這裏的參數同樣是GL_ZERO,GL_ONE,GL_SRC_COLOR等枚舉值。
另外,還可以通過
glBlendEquation(GLenum
mode);和glBlendEquationSeparate來指定源和目的顏色的計算方式,默認是GL_FUNC_ADD,就是公式1所示的情況。例如GL_FUNC_SUBTRACT則對應公式2:
Result=source∗sfactor−destination∗dfactor(2)[Math Processing Error]
一般我們使用的組合爲:
glBlendEquation(GL_FUNC_ADD); // 默認,無需顯式設置
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
繪製半透明效果
上面介紹了OpenGL中混色的計算,下面實現一個半透明的效果。
通過加載一個半透明的窗戶到場景,使得透過窗戶可以看到後面的場景。我們的着色器恢復到:
#version 330 core
in vec2 TextCoord;
uniform sampler2D text;
out vec4 color;
void main()
{
color = texture(text, TextCoord);
}
在場景中使用GL_RGBA等包含alpha的參數加載窗戶模型後,繪製窗戶時使用代碼:
for (std::vector<glm::vec3>::const_iterator it = windowObjs.begin();
windowObjs.end() != it; ++it)
{
model = glm::mat4();
model = glm::translate(model, *it);
glUniformMatrix4fv(glGetUniformLocation(shader.programId, "model"),
1, GL_FALSE, glm::value_ptr(model));
glDrawArrays(GL_TRIANGLES, 0, 6);
}
得到效果如下圖所示:
上面的圖中,仔細看則會發現視覺bug,前面的窗戶看不到後面的窗戶。這主要是因爲深度測試,並不關心alpha值,因此前面的窗戶由於裏觀察者更近,擋住了後面的窗戶,因此後面的窗戶沒有顯示出來。
對於這一問題,需要考慮排序問題
Transparency Sorting。
繪製包含不透明和透明場景的順序爲:
1.首先繪製不透明物體
2.對透明物體進行排序
3.按照排序後的順序,繪製透明物體。
我們這裏的解決方法是對窗戶進行由遠及近的繪製,那麼在繪製近一些的窗戶時,執行混色,混合當前顏色buffer中顏色(場景中處於後面的窗戶的顏色)和當前要繪製的窗戶顏色,則能產生正常的結果。
這裏使用的排序規則是,窗戶到觀察者的距離,藉助c++ std::map默認對鍵值進行排序的功能排序,然後使用逆向迭代器迭代繪製即可,具體實現爲:
// 繪製透明物體
// 根據到觀察者距離遠近排序 使用map的鍵的默認排序功能(鍵爲整數時從小到大)
std::map<GLfloat, glm::vec3> distanceMap;
for (std::vector<glm::vec3>::const_iterator it = windowObjs.begin();
windowObjs.end() != it; ++it)
{
float distance = glm::distance(camera.position, *it);
distanceMap[distance] = *it;
}
transparentShader.use();
glBindVertexArray(transparentVAOId);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, transparentTextId);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// 根據transparency sort 結果進行繪製
for (std::map<GLfloat, glm::vec3>::reverse_iterator it = distanceMap.rbegin();
distanceMap.rend() != it; ++it)
{
model = glm::mat4();
model = glm::translate(model, it->second);
glUniformMatrix4fv(glGetUniformLocation(shader.programId, "model"),
1, GL_FALSE, glm::value_ptr(model));
glDrawArrays(GL_TRIANGLES, 0, 6);
}
glDisable(GL_BLEND);
最終的半透明效果爲:
最後的說明
本節介紹了使用混色功能繪製透明和半透明效果。注意在加載紋理時,如果沒有將wrap方式從GL_REPEAT改爲GL_CLAMP_TO_EDGE將會得到錯誤的效果,如下圖所示: