混合
在OpenGL中爬橡,物體透明技術(shù)通常被叫做混合(Blending)治唤。透明的物體(或物體的一部分)非純色而是混合色,這種顏色來自于不同濃度的自身顏色和它后面的物體顏色糙申。一個(gè)有色玻璃窗就是一種透明物體肝劲,玻璃有自身的顏色,但是最終的顏色包含了所有玻璃后面的顏色郭宝。這也正是混合這名稱的出處,因?yàn)槲覀儗⒍喾N(來自于不同物體)顏色混合為一個(gè)顏色掷漱,透明使得我們可以看穿物體粘室。
透明物體可以是完全透明(它使顏色完全穿透)或者半透明的(它使顏色穿透的同時(shí)也顯示自身顏色)。一個(gè)物體的透明度卜范,被定義為它的顏色的alpha值衔统。alpha顏色值是一個(gè)顏色向量的第四個(gè)元素,你可能已經(jīng)看到很多了海雪。在這個(gè)教程前锦爵,我們一直把這個(gè)元素設(shè)置為1.0,這樣物體的透明度就是0.0奥裸,同樣的险掀,當(dāng)alpha值是0.0時(shí)就表示物體是完全透明的,alpha值為0.5時(shí)表示物體的顏色由50%的自身的顏色和50%的后面的顏色組成湾宙。
我們之前所使用的紋理都是由3個(gè)顏色元素組成的:紅樟氢、綠冈绊、藍(lán),但是有些紋理同樣有一個(gè)內(nèi)嵌的aloha通道埠啃,它為每個(gè)紋理像素(Texel)包含著一個(gè)alpha值死宣。這個(gè)alpha值告訴我們紋理的哪個(gè)部分有透明度,以及這個(gè)透明度有多少碴开。例如毅该,下面的窗子紋理的玻璃部分的alpha值為0.25(它的顏色是完全紅色,但是由于它有75的透明度潦牛,它會(huì)很大程度上反映出網(wǎng)站的背景色眶掌,看起來就不那么紅了),角落部分alpha是0.0(表示完全透明)罢绽。
我們很快就會(huì)把這個(gè)窗子紋理加到場(chǎng)景中畏线,但是首先,我們將討論一點(diǎn)簡單的技術(shù)來實(shí)現(xiàn)紋理的半透明寝殴,也就是完全透明和完全不透明明垢。
忽略片段
有些圖像并不關(guān)心半透明度,但也想基于紋理的顏色值顯示一部分痊银。例如,創(chuàng)建像草這種物體你不需要花費(fèi)很大力氣贞绳,通常把一個(gè)草的紋理貼到2D四邊形上致稀,然后把這個(gè)四邊形放置到你的場(chǎng)景中《兜ィ可是萎攒,草并不是像2D四邊形這樣的形狀,而只需要顯示草紋理的一部分而忽略其他部分矛绘。
下面的紋理正是這樣的紋理耍休,它既有完全不透明的部分(alpha值為1.0)也有完全透明的部分(alpha值為0.0),而沒有半透明的部分货矮。你可以看到?jīng)]有草的部分羊精,圖片顯示了網(wǎng)站的背景色,而不是它自身的那部分顏色次屠。
所以园匹,當(dāng)向場(chǎng)景中添加像這樣的紋理時(shí)雳刺,我們不希望看到一個(gè)方塊圖像,而是只顯示實(shí)際的紋理像素裸违,剩下的部分可以被看穿掖桦。我們要忽略(丟棄)紋理透明部分的像素,不必將這些片段儲(chǔ)存到顏色緩沖中供汛。在此之前枪汪,我們還要學(xué)一下如何加載一個(gè)帶有透明像素的紋理。
加載帶有alpha值的紋理我們需要告訴SOIL怔昨,去加載RGBA元素圖像雀久,而不再是RGB元素的。SOIL能以RGBA的方式加載大多數(shù)沒有alpha值的紋理趁舀,它會(huì)將這些像素的alpha值設(shè)為了1.0(完全不透明)赖捌。
unsigned char * image = SOIL_load_image(path, &width, &height, 0, SOIL_LOAD_RGBA);
不要忘記還要改變OpenGL生成的紋理:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);
保證你在片段著色器中獲取了紋理的所有4個(gè)顏色元素,而不僅僅是RGB元素:
void main ()
{
// color = vec4(vec3(texture(texture1, TexCoords)), 1.0);
color = texture (texture1, TexCoords);
}
現(xiàn)在我們知道了如何加載透明紋理矮烹,是時(shí)候試試在深度測(cè)試教程里那個(gè)場(chǎng)景中添加幾根草了越庇。
我們創(chuàng)建一個(gè)std::vector,并向里面添加幾個(gè)glm::vec3變量奉狈,來表示草的位置:
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));
一個(gè)單獨(dú)的四邊形被貼上草的紋理卤唉,這并不能完美的表現(xiàn)出真實(shí)的草,但是比起加載復(fù)雜的模型還是要高效很多仁期,利用一些小技巧,比如在同一個(gè)地方添加多個(gè)不同朝向的草熬的,還是能獲得比較好的效果的悦析。
由于草紋理被添加到四邊形物體上亭螟,我們需要再次創(chuàng)建另一個(gè)VAO预烙,向里面填充VBO翘县,以及設(shè)置合理的頂點(diǎn)屬性指針锈麸。在我們繪制完地面和兩個(gè)立方體后忘伞,我們就來繪制草葉:
glBindVertexArray (vegetationVAO);
glBindTexture (GL_TEXTURE_2D, grassTexture);
for (GLuint i = 0; i < vegetation.size (); i++)
{
model = glm::mat4 ();
model = glm::translate (model, vegetation[i]);
glUniformMatrix4fv (modelLoc, 1, GL_FALSE, glm::value_ptr (model));
glDrawArrays (GL_TRIANGLES, 0, 6);
}
glBindVertexArray (0);
運(yùn)行程序你將看到:
出現(xiàn)這種情況是因?yàn)镺penGL默認(rèn)是不知道如何處理alpha值的翘魄,不知道何時(shí)忽略(丟棄)它們暑竟。我們不得不手動(dòng)做這件事但荤。幸運(yùn)的是這很簡單,感謝著色器化借,GLSL為我們提供了discard命令铐炫,它保證了片段不會(huì)被進(jìn)一步處理倒信,這樣就不會(huì)進(jìn)入顏色緩沖鳖悠。有了這個(gè)命令我們就可以在片段著色器中檢查一個(gè)片段是否有在一定的閾限下的alpha值,如果有卡辰,那么丟棄這個(gè)片段九妈,就好像它不存在一樣:
#version 330 core
in vec2 TexCoords;
out vec4 color;
uniform sampler2D texture1;
void main ()
{
vec4 texColor = texture (texture1, TexCoords);
if (texColor.a < 0.1)
discard;
color = texColor;
}
在這兒我們檢查被采樣紋理顏色包含著一個(gè)低于0.1這個(gè)閾限的alpha值宴树,如果有森渐,就丟棄這個(gè)片段。這個(gè)片段著色器能夠保證我們只渲染哪些不是完全透明的片段∧推耄現(xiàn)在我們來看看效果:
需要注意的是埠况,當(dāng)采樣紋理邊緣的時(shí)候,OpenGL在邊界值和下一個(gè)重復(fù)的紋理的值之間進(jìn)行插值(因?yàn)槲覀儼阉姆胖梅绞皆O(shè)置成了GL_REPEAT)喜命。這樣就行了壁榕,但是由于我們使用的是透明值牌里,紋理圖片的上部獲得了它的透明值是與底邊的純色值進(jìn)行插值的牡辽。結(jié)果就是一個(gè)有點(diǎn)半透明的邊催享,你可以從我們的紋理四邊形的四周看到。為了防止它的出現(xiàn)攀涵,當(dāng)你使用alpha紋理的時(shí)候要把紋理環(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);
你可以在這里得到源碼以故。
項(xiàng)目代碼在這怒详。
混合
上述丟棄片段的方式昆烁,不能使我們獲得渲染半透明圖像静尼,我們要么渲染出像素鼠渺,要么完全地丟棄它拦盹。為了渲染出不同的透明度級(jí)別,我們需要開啟混合(Blending)池磁。像大多數(shù)OpenGL的功能一樣华临,我們可以開啟GL_BLEND來啟用混合功能:
glEnable(GL_BLEND);
開啟混合后雅潭,我們還需要告訴OpenGL它該如何混合扶供。OpenGL以下面的方程進(jìn)行混合:
Cˉresult = Cˉsource ? Fsource + Cˉdestination ? Fdestination
- Cˉsource:源顏色向量椿浓。這是來自紋理的本來的顏色向量提岔。
- Cˉdestination:目標(biāo)顏色向量碱蒙。這是儲(chǔ)存在顏色緩沖中當(dāng)前位置的顏色向量。
- Fsource:源因子喷兼。設(shè)置了對(duì)源顏色的alpha值影響褒搔。
- Fdestination:目標(biāo)因子星瘾。設(shè)置了對(duì)目標(biāo)顏色的alpha影響琳状。
片段著色器運(yùn)行完成并且所有的測(cè)試都通過以后念逞,混合方程才能自由執(zhí)行片段的顏色輸出翎承,當(dāng)前它在顏色緩沖中(前面片段的顏色在當(dāng)前片段之前儲(chǔ)存)叨咖。源和目標(biāo)顏色會(huì)自動(dòng)被OpenGL設(shè)置甸各,而源和目標(biāo)因子可以讓我們自由設(shè)置。我們來看一個(gè)簡單的例子:
我們有兩個(gè)方塊儒恋,我們希望在紅色方塊上繪制綠色方塊涂邀。紅色方塊會(huì)成為目標(biāo)顏色(它會(huì)先進(jìn)入顏色緩沖),我們將在紅色方塊上繪制綠色方塊驹止。
那么問題來了:我們?cè)鯓觼碓O(shè)置因子呢?我們起碼要把綠色方塊乘以它的alpha值墓捻,所以我們打算把Fsource設(shè)置為源顏色向量的alpha值:0.6撤卢。接著,讓目標(biāo)方塊的濃度等于剩下的alpha值渡紫。如果最終的顏色中綠色方塊的濃度為60%惕澎,我們就把紅色的濃度設(shè)為40%(1.0 – 0.6)唧喉。所以我們把Fdestination設(shè)置為1減去源顏色向量的alpha值。方程將變成:
最終方塊結(jié)合部分包含了60%的綠色和40%的紅色梯找,得到一種臟兮兮的顏色:
最后的顏色被儲(chǔ)存到顏色緩沖中唆阿,取代先前的顏色。
這個(gè)方案不錯(cuò)锈锤,但我們?cè)鯓痈嬖VOpenGL來使用這樣的因子呢驯鳖?恰好有一個(gè)叫做glBlendFunc的函數(shù)闲询。
void glBlendFunc(GLenum sfactor, GLenum dfactor)接收兩個(gè)參數(shù),來設(shè)置源(source)和目標(biāo)(destination)因子浅辙。OpenGL為我們定義了很多選項(xiàng)扭弧,我們把最常用的列在下面。注意记舆,顏色常數(shù)向量Cˉconstant可以用glBlendColor函數(shù)分開來設(shè)置御蒲。
為從兩個(gè)方塊獲得混合結(jié)果,我們打算把源顏色的alpha給源因子严望,1-alpha給目標(biāo)因子拨匆。調(diào)整到glBlendFunc之后就像這樣:
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
也可以為RGB和alpha通道各自設(shè)置不同的選項(xiàng)宏赘,使用glBlendFuncSeperate:
glBlendFuncSeperate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA,GL_ONE, GL_ZERO);
這個(gè)方程就像我們之前設(shè)置的那樣脐往,設(shè)置了RGB元素,但是只讓最終的alpha元素被源顏色的alpha值影響到(GL_ONE)誊辉。
OpenGL給了我們更多的自由蛙紫,我們可以改變方程源和目標(biāo)部分的操作符⊙涠荆現(xiàn)在抄肖,源和目標(biāo)元素已經(jīng)相加了幌甘。如果我們?cè)敢獾脑捒裕覀冞€可以把它們相減十酣。
void glBlendEquation(GLenum mode)允許我們?cè)O(shè)置這個(gè)操作搓彻,有3種可行的選項(xiàng):
- GL_FUNC_ADD:默認(rèn)的,彼此元素相加:Cˉresult = Src + Dst.
- GL_FUNC_SUBTRACT:彼此元素相減: Cˉresult = Src – Dst.
- GL_FUNC_REVERSE_SUBTRACT:彼此元素相減,但順序相反:Cˉresult = Dst – Src.
通常我們可以簡單地省略glBlendEquation因?yàn)镚L_FUNC_ADD在大多數(shù)時(shí)候就是我們想要的是己,但是如果你如果你真想嘗試努力打破主流常規(guī),其他的方程或許符合你的要求牢贸。
渲染半透明紋理
現(xiàn)在我們知道OpenGL如何處理混合,是時(shí)候把我們的知識(shí)運(yùn)用起來了,我們來添加幾個(gè)半透明窗子耳峦。我們會(huì)使用本教程開始時(shí)用的那個(gè)場(chǎng)景冠句,但是不再渲染草紋理聚唐,取而代之的是來自教程開始處半透明窗子紋理臀蛛。
首先舔琅,初始化時(shí)我們需要開啟混合虚循,設(shè)置合適和混合方程:
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
由于我們開啟了混合奢入,就不需要丟棄片段了筝闹,所以我們把片段著色器設(shè)置為原來的那個(gè)版本:
#version 330 core
in vec2 TexCoords;
out vec4 color;
uniform sampler2D texture1;
void main()
{
color = texture(texture1, TexCoords);
}
這一次(無論OpenGL什么時(shí)候去渲染一個(gè)片段),它都根據(jù)alpha值腥光,把當(dāng)前片段的顏色和顏色緩沖中的顏色進(jìn)行混合关顷。因?yàn)榇白拥牟AР糠值募y理是半透明的,我們應(yīng)該可以透過玻璃看到整個(gè)場(chǎng)景武福。
如果你仔細(xì)看看议双,就會(huì)注意到有些不對(duì)勁。前面的窗子透明部分阻塞了后面的捉片。為什么會(huì)這樣平痰?
原因是深度測(cè)試在與混合的一同工作時(shí)出現(xiàn)了點(diǎn)狀況。當(dāng)寫入深度緩沖的時(shí)候界睁,深度測(cè)試不關(guān)心片段是否有透明度,所以透明部分被寫入深度緩沖兵拢,就和其他值沒什么區(qū)別翻斟。結(jié)果是整個(gè)四邊形的窗子被檢查時(shí)都忽視了透明度。即便透明部分應(yīng)該顯示出后面的窗子说铃,深度緩沖還是丟棄了它們访惜。
所以我們不能簡簡單單地去渲染窗子,我們期待著深度緩沖為我們解決這所有問題腻扇;這也正是混合之處代碼不怎么好看的原因债热。為保證前面窗子顯示了它后面的窗子,我們必須首先繪制后面的窗子幼苛。這意味著我們必須手工調(diào)整窗子的順序窒篱,從遠(yuǎn)到近地逐個(gè)渲染。
對(duì)于全透明物體,比如草葉墙杯,我們選擇簡單的丟棄透明像素而不是混合配并,這樣就減少了令我們頭疼的問題(沒有深度測(cè)試問題)。
別打亂順序
要讓混合在多物體上有效高镐,我們必須先繪制最遠(yuǎn)的物體溉旋,最后繪制最近的物體。普通的無混合物體仍然可以使用深度緩沖正常繪制嫉髓,所以不必給它們排序观腊。我們一定要保證它們?cè)谕该魑矬w前繪制好。當(dāng)無透明度物體和透明物體一起繪制的時(shí)候算行,通常要遵循以下原則:
先繪制所有不透明物體梧油。 為所有透明物體排序。 按順序繪制透明物體纱意。
** 一種排序透明物體的方式是婶溯,獲取一個(gè)物體到觀察者透視圖的距離。**這可以通過獲取攝像機(jī)的位置向量和物體的位置向量來得到偷霉。接著我們就可以把它和相應(yīng)的位置向量一起儲(chǔ)存到一個(gè)map數(shù)據(jù)結(jié)構(gòu)(STL庫)中迄委。map會(huì)自動(dòng)基于它的鍵排序它的值,所以當(dāng)我們把它們的距離作為鍵添加到所有位置中后类少,它們就自動(dòng)按照距離值排序了:
map<float, glm::vec3> sorted; // 距離叙身,位置向量
for (auto pos : windows)
{
GLfloat distance = glm::length (camera.Position - pos); // 攝像機(jī)位置與窗口的距離,map會(huì)按照距離從小到大排序硫狞。
sorted[distance] = pos;
}
最后產(chǎn)生了一個(gè)容器對(duì)象信轿,基于距離從低到高儲(chǔ)存了每個(gè)窗子的位置。
隨后當(dāng)渲染的時(shí)候残吩,我們逆序獲取到每個(gè)map的值(從遠(yuǎn)到近)财忽,然后以正確的繪制相應(yīng)的窗子:
for (map<float, glm::vec3>::reverse_iterator it = sorted.rbegin (); it != sorted.rend (); ++it) // 逆序獲取每個(gè)map的值(從遠(yuǎn)到近)
{
model = glm::mat4 ();
model = glm::translate (model, it->second);
glUniformMatrix4fv (glGetUniformLocation (windowShader.Program, "model"), 1, GL_FALSE, glm::value_ptr (model));
glDrawArrays (GL_TRIANGLES, 0, 6);
}
我們從map得來一個(gè)逆序的迭代器,迭代出每個(gè)逆序的條目泣侮,然后把每個(gè)窗子的四邊形平移到相應(yīng)的位置即彪。這個(gè)相對(duì)簡單的方法對(duì)透明物體進(jìn)行了排序,修正了前面的問題活尊,現(xiàn)在場(chǎng)景看起來像這樣:
你可以從這里得到完整的帶有排序的源碼隶校。
完整項(xiàng)目代碼在[這里[(https://www.jianguoyun.com/p/DScoVDMQ-5L3BRicqxY)。
雖然這個(gè)按照它們的距離對(duì)物體進(jìn)行排序的方法在這個(gè)特定的場(chǎng)景中能夠良好工作蛹锰,但它不能進(jìn)行旋轉(zhuǎn)深胳、縮放或者進(jìn)行其他的變換,奇怪形狀的物體需要一種不同的方式铜犬,而不能簡單的使用位置向量舞终。
在場(chǎng)景中排序物體是個(gè)有難度的技術(shù)轻庆,它很大程度上取決于你場(chǎng)景的類型,更不必說會(huì)耗費(fèi)額外的處理能力了权埠。完美地渲染帶有透明和不透明的物體的場(chǎng)景并不那么容易榨了。有更高級(jí)的技術(shù)例如次序無關(guān)透明度(order independent transparency),但是這超出了本教程的范圍∪帘危現(xiàn)在你不得不采用普通的混合你的物體龙屉,但是如果你小心謹(jǐn)慎,并知道這個(gè)局限满俗,你仍可以得到頗為合適的混合實(shí)現(xiàn)转捕。