1. 程式人生 > >OpenGL核心技術之混合技術

OpenGL核心技術之混合技術

筆者介紹:姜雪偉IT公司技術合夥人,IT高階講師,CSDN社群專家,特邀編輯,暢銷書作者,國家專利發明人;已出版書籍:《手把手教你架構3D遊戲引擎》電子工業出版社和《實戰核心技術詳解》電子工業出版社等。

本章給讀者介紹關於混合技術的實現,混合在遊戲中經常使用,它在引擎中的實現主要是分為三種:透明,半透明,次序無關透明度,本篇博文主要是圍繞它們進行。

在OpenGL中,物體透明技術通常被叫做混合(Blending)。一個物體的透明度,被定義為它的顏色的alpha值。alpha顏色值是一個顏色
向量的第四個元素。美術在製作的遊戲圖片顏色,主要是由rgba四位組成的,顏色的最後一位就是我們說的alpha通道,它主要是決定材質
的透明度的。
先說透明的材質處理,做過3D遊戲的開發者都比較熟悉,在3D場景編輯器中經常需要在地面上刷一些草,這些草的圖片製作是帶有
alpha通道的,效果如下所示:


程式的處理方式就是把背景去掉,把草顯示出來,所以,當向場景中新增像這樣的紋理時,我們不希望看到一個方塊影象,
而是隻顯示實際的紋理畫素,剩下的部分可以被看穿。我們要忽略(丟棄)紋理透明部分的畫素,不必將這些片段儲存到顏色
緩衝中。接下來要做的事情就是載入帶有Alpha通道的紋理圖片,在這裡我們使用了SOIL庫, SOIL 是一個用於向OpenGL中
載入紋理的小型C語言庫。下載地址:http://www.lonesock.net/soil.html,SOIL庫提供的載入函式如下:
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個顏色元素,而不僅僅是RGB元素:
void main()
{
    // color = vec4(vec3(texture(texture1, TexCoords)), 1.0);
    color = texture(texture1, TexCoords);
}

透明材質就載入完成了,接下來就是對草進行擺放了,程式碼段如下所示:
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));

一個單獨的四邊形被貼上草的紋理,這並不能完美的表現出真實的草,但是比起載入複雜的模型還是要高效很多,利用一些小技巧,


比如在同一個地方新增多個不同朝向的草,還是能獲得比較好的效果的。


由於草紋理被新增到四邊形物體上,我們需要再次建立另一個VAO,向裡面填充VBO,以及設定合理的頂點屬性指標。


在我們繪製完地面和兩個立方體後,我們就來繪製草葉:
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);

執行程式得到的效果如下所示:


出現這種情況是因為OpenGL預設是不知道如何處理alpha值的,不知道何時忽略(丟棄)它們。我們不得不手動做這件事。
幸運的是這很簡單,感謝著色器,GLSL為我們提供了discard命令,它保證了片段不會被進一步處理,這樣就不會進入顏色緩衝。
有了這個命令我們就可以在片段著色器中檢查一個片段是否有在一定的閾限下的alpha值,如果有,那麼丟棄這個片段,就好像
它不存在一樣:
#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;
}

在這兒我們檢查被取樣紋理顏色包含著一個低於0.1這個閾限的alpha值,如果有,就丟棄這個片段。這個片段著色器能夠保證我們只渲染哪些不是完全透明的片段。現在我們來看看效果:


下面把Shader指令碼的頂點著色器給讀者展示如下:

#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec2 texCoords;

out vec2 TexCoords;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    gl_Position = projection * view * model * vec4(position, 1.0f);
    TexCoords = texCoords;
}

片段著色器程式碼如下所示:
#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;
}

其次介紹半透明處理,以上Shader完成了透明材質的渲染,其實這種方法在材質渲染中經常使用,可以把不需要的顏色放棄掉,這種方式不適合渲染半透明的圖片,也沒有用到Blend混合模式。為了渲染出不同的透明度級別,需要開啟混合(Blending),開啟混合功能函式如下:

glEnable(GL_BLEND);
開啟混合後,我們還需要告訴OpenGL它該如何混合。

OpenGL以下面的方程進行混合:


  • C¯source:源顏色向量。這是來自紋理的本來的顏色向量。
  • C¯destination:目標顏色向量。這是儲存在顏色緩衝中當前位置的顏色向量。
  • Fsource:源因子。設定了對源顏色的alpha值影響。
  • Fdestination:目標因子。設定了對目標顏色的alpha影響。
片段著色器執行完成並且所有的測試都通過以後,混合方程才能自由執行片段的顏色輸出,當前它在顏色緩衝中(前面片段的顏色在當前片段之前儲存)。源和目標顏色會自動被OpenGL設定,而源和目標因子可以讓我們自由設定。我們來看一個簡單的例子:


我們有兩個方塊,我們希望在紅色方塊上繪製綠色方塊。紅色方塊會成為目標顏色(它會先進入顏色緩衝),我們將在紅色方塊上繪製綠色方塊。


那麼問題來了:我們怎樣來設定因子呢?我們起碼要把綠色方塊乘以它的alpha值,所以我們打算把FsrcFsrc設定為源顏色向量的alpha值:0.6。接著,讓目標方塊的濃度等於剩下的alpha值。如果最終的顏色中綠色方塊的濃度為60%,我們就把紅色的濃度設為40%(1.0 – 0.6)。所以我們把Fdestination 設定為1減去源顏色向量的alpha值。方程將變成:

最終方塊結合部分包含了60%的綠色和40%的紅色,得到一種髒兮兮的顏色:


最後的顏色被儲存到顏色緩衝中,取代先前的顏色。

這個方案不錯,但我們怎樣告訴OpenGL來使用這樣的因子呢?恰好有一個叫做glBlendFunc的函式。

void glBlendFunc(GLenum sfactor, GLenum dfactor)接收兩個引數,來設定源(source)和目標(destination)因子。OpenGL為

我們定義了很多選項,我們把最常用的列在下面。注意,顏色常數向量C¯constant可以用glBlendColor函式分開來設定。


為從兩個方塊獲得混合結果,我們打算把源顏色的alphaalpha給源因子,1alpha1−alpha給目標因子,調整到glBlendFunc

後就像這樣:

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
也可以為RGB和alpha通道各自設定不同的選項,使用glBlendFuncSeperate
glBlendFuncSeperate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA,GL_ONE, GL_ZERO);

這個方程就像我們之前設定的那樣,設定了RGB元素,但是隻讓最終的alpha元素被源alpha值影響到。

OpenGL給了我們更多的自由,我們可以改變方程源和目標部分的操作符。現在,源和目標元素已經相加了。如果我們願意的話,

我們還可以把它們相減。

void glBlendEquation(GLenum mode)允許我們設定這個操作,有3種可行的選項:

  • GL_FUNC_ADD:預設的,彼此元素相加:
  • GL_FUNC_SUBTRACT:彼此元素相減: 
  • GL_FUNC_REVERSE_SUBTRACT:彼此元素相減,但順序相反:

通常我們可以簡單地省略glBlendEquation因為GL_FUNC_ADD在大多數時候就是我們想要的,但是如果你如果你真想嘗試

努力打破主流常規,其他的方程或許符合你的要求。現在我們知道OpenGL如何處理混合,是時候把我們的知識運用起來了,

我們來新增幾個半透明窗子。首先,初始化時我們需要開啟混合,設定合適和混合方程:

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

由於我們開啟了混合,就不需要丟棄片段了,所以我們把片段著色器設定為原來的那個版本:
#version 330 core
in vec2 TexCoords;

out vec4 color;

uniform sampler2D texture1;

void main()
{
    color = texture(texture1, TexCoords);
}

它根據alpha值,把當前片段的顏色和顏色緩衝中的顏色進行混合。因為窗子的玻璃部分的紋理是半透明的,我們應該可以

透過玻璃看到整個場景。


如果你仔細看看,就會注意到有些不對勁。前面的窗子透明部分阻塞了後面的。為什麼會這樣?

原因是深度測試在與混合的一同工作時出現了點狀況。當寫入深度緩衝的時候,深度測試不關心片段是否有透明度,

所以透明部分被寫入深度緩衝,就和其他值沒什麼區別。結果是整個四邊形的窗子被檢查時都忽視了透明度。即便透明部

分應該顯示出後面的窗子,深度緩衝還是丟棄了它們。

所以我們不能簡簡單單地去渲染窗子,我們期待著深度緩衝為我們解決這所有問題;這也正是混合之處程式碼不怎麼好

看的原因。為保證前面窗子顯示了它後面的窗子,我們必須首先繪製後面的窗子。這意味著我們必須手工調整窗子的順序,

從遠到近地逐個渲染。

這裡要注意:對於全透明物體,比如草葉,我們選擇簡單的丟棄透明畫素而不是混合,這樣就減少了令我們頭疼的問題(沒有深度測試題)。

下面介紹如何按照順序渲染物體,要讓混合在多物體上有效,我們必須先繪製最遠的物體,最後繪製最近的物體。

普通的無混合物體仍然可以使用深度緩衝正常繪製,所以不必給它們排序。我們一定要保證它們在透明物體前繪製好。

當無透明度物體和透明物體一起繪製的時候,通常要遵循以下原則:

先繪製所有不透明物體。 為所有透明物體排序。 按順序繪製透明物體。 一種排序透明物體的方式是,獲取一個物體

到觀察者透檢視的距離。這可以通過獲取攝像機的位置向量和物體的位置向量來得到。接著我們就可以把它和相應的位置

向量一起儲存到一個map資料結構(STL庫)中。map會自動基於它的鍵排序它的值,所以當我們把它們的距離作為鍵添

加到所有位置中後,它們就自動按照距離值排序了:

std::map<float, glm::vec3> sorted;
for (GLuint i = 0; i < windows.size(); i++) // windows contains all window positions
{
    GLfloat distance = glm::length(camera.Position - windows[i]);
    sorted[distance] = windows[i];
}

最後產生了一個容器物件,基於它們距離從低到高儲存了每個窗子的位置。

隨後當渲染的時候,我們逆序獲取到每個map的值(從遠到近),然後以正確的繪製相應的窗子:

for(std::map<float,glm::vec3>::reverse_iterator it = sorted.rbegin(); it != sorted.rend(); ++it)
{
    model = glm::mat4();
    model = glm::translate(model, it->second);
    glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
    glDrawArrays(GL_TRIANGLES, 0, 6);
}

我們從map得來一個逆序的迭代器,迭代出每個逆序的條目,然後把每個窗子的四邊形平移到相應的位置。這個相對

簡單的方法對透明物體進行了排序,修正了前面的問題,現在場景看起來像這樣:


雖然這個按照它們的距離對物體進行排序的方法在這個特定的場景中能夠良好工作,但它不能進行旋轉、縮放或者

進行其他的變換,奇怪形狀的物體需要一種不同的方式,而不能簡單的使用位置向量。

在場景中排序物體是個有難度的技術,它很大程度上取決於你場景的型別,更不必說會耗費額外的處理能力了。

完美地渲染帶有透明和不透明的物體的場景並不那麼容易。

接下介紹技術次序無關透明度,為了解決上述提到的問題,我們使用更高階的更高階的技術更高階的技術次序無關

透明度。下面介紹該技術的實現,我們的渲染主要分兩步:

第一步:渲染填充連結串列;

第二步:渲染排序+blend;

 先看第一步,我們使用片段著色器來填充連結串列:

#version 420 core

layout (early_fragment_tests) in;

layout (binding = 0, r32ui) uniform uimage2D head_pointer_image;
layout (binding = 1, rgba32ui) uniform writeonly uimageBuffer list_buffer;

layout (binding = 0, offset = 0) uniform atomic_uint list_counter;

layout (location = 0) out vec4 color;

in vec4 surface_color;

uniform vec3 light_position = vec3(40.0, 20.0, 100.0);

void main(void)
{
    uint index;
    uint old_head;
    uvec4 item;

    index = atomicCounterIncrement(list_counter);

    old_head = imageAtomicExchange(head_pointer_image, ivec2(gl_FragCoord.xy), uint(index));

    item.x = old_head;
    item.y = packUnorm4x8(surface_color);
    item.z = floatBitsToUint(gl_FragCoord.z);
	item.w = 255 / 4;

    imageStore(list_buffer, int(index), item);

    //color = surface_color;
	discard;
}

同時把頂點著色器程式碼也給讀者展示一下:
#version 330

layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;

uniform mat4 model_matrix;
uniform mat4 view_matrix;
uniform mat4 projection_matrix;

uniform float minAlpha = 0.5f;

out vec4 surface_color;

void main(void)
{
    vec3 color = normal;
    if (color.r < 0) { color.r = -color.r; }
    if (color.g < 0) { color.g = -color.g; }
    if (color.b < 0) { color.b = -color.b; }
	vec3 normalized = normalize(color);
	float variance = (normalized.r - normalized.g) * (normalized.r - normalized.g);
	variance += (normalized.g - normalized.b) * (normalized.g - normalized.b);
	variance += (normalized.b - normalized.r) * (normalized.b - normalized.r);
	variance = variance / 2.0f;// range from 0.0f - 1.0f
	float a = (0.75f - minAlpha) * (1.0f - variance) + minAlpha;
    surface_color = vec4(normalized, a);

    gl_Position = projection_matrix * view_matrix * model_matrix * vec4(position, 1.0f);
}

下面開始第二步驟的操作是渲染排序+blend,片段著色器程式碼如下所示:
#version 420 core
// The per-pixel image containing the head pointers
layout (binding = 0, r32ui) uniform uimage2D head_pointer_image;
// Buffer containing linked lists of fragments
layout (binding = 1, rgba32ui) uniform uimageBuffer list_buffer;

// This is the output color
layout (location = 0) out vec4 color;

// This is the maximum number of overlapping fragments allowed
#define MAX_FRAGMENTS 40

// Temporary array used for sorting fragments
uvec4 fragment_list[MAX_FRAGMENTS];

void main(void)
{
    uint current_index;
    uint fragment_count = 0;

    current_index = imageLoad(head_pointer_image, ivec2(gl_FragCoord).xy).x;

    while (current_index != 0 && fragment_count < MAX_FRAGMENTS)
    {
        uvec4 fragment = imageLoad(list_buffer, int(current_index));
        fragment_list[fragment_count] = fragment;
        current_index = fragment.x;
        fragment_count++;
    }

	if (fragment_count > 1)
	{
		for (uint i = 0; i < fragment_count - 1; i++)
		{
			uint p = i;
			uint depth1 = (fragment_list[p].z);
			for (uint j = i + 1; j < fragment_count; j++)
			{
				uint depth2 = (fragment_list[j].z);
				if (depth1 < depth2)
				{
					p = j; depth1 = depth2;
				}
			}
			if (p != i)
			{
				uvec4 tmp = fragment_list[p];
				fragment_list[p] = fragment_list[i];
				fragment_list[i] = tmp;
			}
		}
	}

    vec4 final_color = vec4(0.0);

    for (uint i = 0; i < fragment_count; i++)
    {
        vec4 modulator = unpackUnorm4x8(fragment_list[i].y);
        //final_color = mix(final_color, modulator, modulator.a);
		final_color = final_color * (1.0f - modulator.a) + modulator * modulator.a;
    }

    color = final_color;
    // color = vec4(float(fragment_count) / float(MAX_FRAGMENTS));
}

附帶著頂點著色器程式碼如下所示:
#version 420 core

in vec3 position;

uniform mat4 model_matrix;
uniform mat4 view_matrix;
uniform mat4 projection_matrix;

void main(void)
{
    gl_Position = projection_matrix * view_matrix * model_matrix * vec4(position, 1.0f);
    //gl_Position = vec4(position, 1.0f);
}

實現的效果圖如下所示:


如果沒有使用次序無關透明技術實現的效果圖如下所示:


總結:

關於在3D遊戲中的混合技術主要用於處理透明,半透明以及在解決渲染次序問題使用的次序無關透明度技術,希望對讀者有所幫助。。。。。。