之前學習了使用模闆緩沖來制作特殊效果,本節将繼續學習一個進階主題-混色(Blending)。通過使用混色,我們可以制作透明、半透明效果。
混色的概念
所謂混色,就是将目前要繪制的物體的顔色和顔色緩沖區中已經繪制了的物體的顔色進行混合,最終決定了目前物體的顔色。例如下面的圖中,狙擊槍的瞄準器本身是帶有藍色的,将它和後面的任務混合在一起,形成了我們看到的最終效果,這個效果裡既有瞄準器的藍色成分,也有後面人物的像素,主要是後面人物的像素。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICO4ITM0QDMyIDMygDM2EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
實際上我們通過玻璃看到外面的景象就是一種混色,有的玻璃完全透明則主要顯示外面的景象,而另一些玻璃不是完全透明則成像中包含一部分玻璃的顔色。在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則丢棄片元
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"),
, GL_FALSE, glm::value_ptr(model));
glDrawArrays(GL_TRIANGLES, , );
}
得到效果如下圖所示:
上面的圖中,仔細看則會發現視覺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"),
, GL_FALSE, glm::value_ptr(model));
glDrawArrays(GL_TRIANGLES, , );
}
glDisable(GL_BLEND);
最終的半透明效果為:
最後的說明
本節介紹了使用混色功能繪制透明和半透明效果。注意在加載紋理時,如果沒有将wrap方式從GL_REPEAT改為GL_CLAMP_TO_EDGE将會得到錯誤的效果,如下圖所示: