本文繼續(xù)學(xué)習(xí)OpenGL的高級(jí)技術(shù):混合(Blending)。
1. 什么是混合
在計(jì)算機(jī)圖形學(xué)中,透明效果是實(shí)現(xiàn)逼真場(chǎng)景的重要技術(shù)之一。透明物體能夠“混合”自身顏色與背后物體的顏色,從而呈現(xiàn)出獨(dú)特的視覺(jué)效果。這種技術(shù)在 OpenGL 中被稱為 混合(Blending)。無(wú)論是玻璃窗、煙霧,還是半透明的液體,透明效果的核心都在于如何將不同物體的顏色融合在一起。
1.1 透明度:從純色到混合色
透明物體的視覺(jué)特性來(lái)源于其顏色的“混合性”。一個(gè)有色玻璃窗就是一個(gè)典型的例子:它不僅有自己的顏色,還會(huì)與背后物體的顏色混合,最終呈現(xiàn)出獨(dú)特的視覺(jué)效果。透明度由顏色的 alpha 值 決定,這是顏色向量的第四個(gè)分量。當(dāng) alpha 值為 1.0 時(shí),物體完全不透明;當(dāng) alpha 值為 0.0 時(shí),物體完全透明;而介于兩者之間的值則表示半透明。

紋理是實(shí)現(xiàn)透明效果的關(guān)鍵工具。除了常見(jiàn)的 RGB(紅、綠、藍(lán))通道外,許多紋理還包含一個(gè) alpha 通道,用于定義每個(gè)像素的透明度。例如,一個(gè)窗戶紋理的玻璃部分可能具有較低的 alpha 值(如 0.25),而窗框部分的 alpha 值為 1.0,表示完全不透明。

2. 忽略透明像素:實(shí)現(xiàn)部分不可見(jiàn)效果
并非所有紋理都需要半透明效果。例如,草葉紋理通常包含完全透明和完全不透明的區(qū)域,而沒(méi)有中間的半透明部分。為了只渲染不透明部分,我們可以利用 OpenGL 的 discard 命令丟棄透明像素。
以草葉紋理為例,展示如何忽略透明像素,下面來(lái)修改我們之前的代碼:
2.1 將草紋理顯示到場(chǎng)景中
先將草紋理放到工程中顯示出來(lái)。
2.1.1 加載紋理
之前我們封裝的 loadTexture
函數(shù)其實(shí)已經(jīng)支持加載 RGBA 格式的紋理。format = GL_RGBA;
。注意這個(gè) format
用在了 glTexImage2D
函數(shù)中。
unsigned int loadTexture(char const *path)
{
unsignedint textureID;
glGenTextures(1, &textureID);
int width, height, nrComponents;
unsignedchar *data = stbi_load(path, &width, &height, &nrComponents, 0);
if (data)
{
GLenum format;
if (nrComponents == 1)
format = GL_RED;
elseif (nrComponents == 3)
format = GL_RGB;
elseif (nrComponents == 4)
format = GL_RGBA;
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
stbi_image_free(data);
}
else
{
std::cout << "Texture failed to load at path: " << path << std::endl;
stbi_image_free(data);
}
return textureID;
}
2.1.2 修改片段著色器
現(xiàn)在加載的紋理包含 alpha 通道,是個(gè) vec4 類型了。所以,需要如下使用紋理:
// color = vec4(vec3(texture(texture1, TexCoords)), 1.0);
color = texture(texture1, TexCoords);
2.1.3 主程序修改
(1)加載草的紋理
unsigned int grassTexture = loadTexture(std::string(PROJECT_PATH + "/resource/grass.png").c_str());
(2)設(shè)置草的頂點(diǎn)數(shù)據(jù)
float transparentVertices[] = {
// Positions // Texture Coords (swapped y coordinates because texture is flipped upside down)
0.0f, 0.5f, 0.0f, 0.0f, 0.0f,
0.0f, -0.5f, 0.0f, 0.0f, 1.0f,
1.0f, -0.5f, 0.0f, 1.0f, 1.0f,
0.0f, 0.5f, 0.0f, 0.0f, 0.0f,
1.0f, -0.5f, 0.0f, 1.0f, 1.0f,
1.0f, 0.5f, 0.0f, 1.0f, 0.0f
};
(3)添加草的位置
vector<glm::vec3> vegetation;
vegetation.push_back(glm::vec3(-1.5f, 0.0f, -0.48f));
vegetation.push_back(glm::vec3( 1.5f, 0.0f, 0.51f));
vegetation.push_back(glm::vec3( 0.0f, 0.0f, 0.7f));
vegetation.push_back(glm::vec3(-0.3f, 0.0f, -2.3f));
vegetation.push_back(glm::vec3( 0.5f, 0.0f, -0.6f));
(4)創(chuàng)建草的VAO、VBO
unsigned int transparentVAO, transparentVBO;
glGenVertexArrays(1, &transparentVAO);
glGenBuffers(1, &transparentVBO);
glBindVertexArray(transparentVAO);
glBindBuffer(GL_ARRAY_BUFFER, transparentVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(transparentVertices), transparentVertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
glBindVertexArray(0);
(5)繪制草
glBindVertexArray(transparentVAO);
glBindTexture(GL_TEXTURE_2D, grassTexture);
for(GLuint i = 0; i < vegetation.size(); i++)
{
model = glm::mat4();
model = glm::translate(model, vegetation[i]);
ourShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 6);
}
glBindVertexArray(0);
2.1.4 運(yùn)行效果

2.2 忽略透明像素:實(shí)現(xiàn)草葉效果
出現(xiàn)上述顯示情況是因?yàn)镺penGL默認(rèn)是不知道如何處理alpha值的,不知道何時(shí)忽略(丟棄)它們。GLSL為我們提供了discard命令,它保證了片段不會(huì)被進(jìn)一步處理,這樣就不會(huì)進(jìn)入顏色緩沖。有了這個(gè)命令我們就可以在片段著色器中檢查一個(gè)片段是否有在一定的閾限下的alpha值,如果有,那么丟棄這個(gè)片段。
修改片段著色器如下:
void main()
{
vec4 texColor = texture(texture1, TexCoords);
if(texColor.a < 0.1)
discard;
color = texColor;
}
運(yùn)行效果:

通過(guò)這種方式,我們可以只渲染紋理中不透明的部分,而忽略透明區(qū)域,從而實(shí)現(xiàn)類似草葉的效果。
細(xì)心的朋友可能也發(fā)現(xiàn)了,上述運(yùn)行效果種,草的上方會(huì)有一條類似花屏之后的線條。這是因?yàn)椋?/span>
當(dāng)在紋理邊緣進(jìn)行采樣時(shí),OpenGL會(huì)在邊界值和下一個(gè)重復(fù)的紋理值之間進(jìn)行插值,因?yàn)槲覀儗⑵浞胖梅绞皆O(shè)置為GL_REPEAT。但由于我們使用的是透明值(alpha值),紋理圖片的上部會(huì)得到一個(gè)與底部純色值插值后的透明值。這就導(dǎo)致了一個(gè)稍微半透明的邊緣。
為了避免這種情況,當(dāng)使用帶有alpha通道的紋理時(shí),應(yīng)該將紋理環(huán)繞方式設(shè)置為GL_CLAMP_TO_EDGE:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
這樣設(shè)置后,紋理在邊緣處的采樣就不會(huì)進(jìn)行插值,而是會(huì)使用邊緣的值,從而避免出現(xiàn)半透明的邊緣。
修改后運(yùn)行效果:

3. 混合技術(shù):實(shí)現(xiàn)半透明效果
上述丟棄片段的方式,不能使我們獲得渲染半透明圖像,我們要么渲染出像素,要么完全地丟棄它。
如果需要渲染半透明物體(如玻璃窗),則需要啟用 OpenGL 的 混合功能。混合的核心思想是將當(dāng)前片段的顏色與顏色緩沖中的顏色按一定規(guī)則融合。
3.1 混合功能相關(guān)接口
啟用混合的步驟如下:
- 1. 啟用混合功能:
glEnable(GL_BLEND);
- 2. 設(shè)置混合方程:
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
混合方程參數(shù):

混合方程參數(shù)的選項(xiàng):

也可以為RGB和alpha通道各自設(shè)置不同的選項(xiàng),使用glBlendFuncSeperate
:
// 這個(gè)方程設(shè)置了RGB元素,但是只讓最終的alpha元素被源alpha值影響到。
glBlendFuncSeperate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA,GL_ONE, GL_ZERO);
還有一個(gè)接口,glBlendEquation
。上面的混合方式都是源和目標(biāo)元素相加,這個(gè)方程可以設(shè)置成相減等。

3.2 實(shí)踐:渲染半透明紋理
(1)開(kāi)啟混合
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
(2)修改片段著色器,不用discard
void main()
{
vec4 texColor = texture(texture1, TexCoords);
color = texColor;
}
(3)加載窗戶紋理
unsigned int windowTexture = loadTexture(std::string(PROJECT_PATH + "/resource/blending_transparent_window.png").c_str());
(4)渲染
這里復(fù)用之前草的頂點(diǎn)數(shù)據(jù)、VAO、VBO和位置數(shù)據(jù),只需要在繪制時(shí)綁定窗戶的紋理即可。
glBindTexture(GL_TEXTURE_2D, windowTexture);
運(yùn)行效果:

上面這個(gè)效果可以看到混合了,但是還有點(diǎn)問(wèn)題。前面的窗子,雖然半透明,但是后面的窗子還是沒(méi)有完全渲染出來(lái),被前面的窗子遮擋了。
3.3 透明物體排序
透明物體的渲染順序?qū)ψ罱K效果至關(guān)重要。由于深度緩沖無(wú)法正確處理透明像素,透明物體需要按照 從遠(yuǎn)到近 的順序渲染。否則,由于深度測(cè)試的作用,前面的透明物體可能會(huì)遮擋后面的物體。
要讓混合在多物體上有效,我們必須先繪制最遠(yuǎn)的物體,最后繪制最近的物體。
實(shí)現(xiàn)透明物體排序的步驟如下:
- 1. 計(jì)算每個(gè)物體到攝像機(jī)的距離。
- 2. 使用
std::map
存儲(chǔ),key為距離,std::map
會(huì)自動(dòng)按距離從小到大排序。
排序代碼:
std::map<float, glm::vec3> sorted;
for (unsigned int i = 0; i < vegetation.size(); i++) // windows contains all window positions
{
GLfloat distance = glm::length(cameraPos - vegetation[i]);
sorted[distance] = vegetation[i];
}
然后逆序繪制:
for(std::map<float,glm::vec3>::reverse_iterator it = sorted.rbegin(); it != sorted.rend(); ++it)
{
model = glm::mat4();
model = glm::translate(model, it->second);
ourShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 6);
}
通過(guò)這種方式,我們可以確保透明物體的正確顯示。運(yùn)行效果:

雖然這個(gè)按照它們的距離對(duì)物體進(jìn)行排序的方法在這個(gè)特定的場(chǎng)景中能夠良好工作,但它不能進(jìn)行旋轉(zhuǎn)、縮放或者進(jìn)行其他的變換,奇怪形狀的物體需要一種不同的方式,而不能簡(jiǎn)單的使用位置向量。
在場(chǎng)景中排序物體是個(gè)有難度的技術(shù),它很大程度上取決于你場(chǎng)景的類型,更不必說(shuō)會(huì)耗費(fèi)額外的處理能力了。完美地渲染帶有透明和不透明的物體的場(chǎng)景并不那么容易。有更高級(jí)的技術(shù)例如次序無(wú)關(guān)透明度(order independent transparency)。
4. 總結(jié)
透明與混合是 OpenGL 中實(shí)現(xiàn)復(fù)雜視覺(jué)效果的重要技術(shù)。
通過(guò)合理設(shè)置 alpha 通道、啟用混合功能以及對(duì)透明物體進(jìn)行排序,我們能夠渲染出逼真的透明效果。
最后,繪制物體的過(guò)程可以總結(jié)為:
(1)先繪制所有不透明物體
(2)為所有透明物體排序
(3)按順序繪制透明物體
篇幅有限,完整程序可私信我(+v:jasper_8017)獲取。
如果覺(jué)得本文對(duì)你有幫助,麻煩點(diǎn)個(gè)贊和關(guān)注唄 ~~~