1. 程式人生 > >OpenGL學習筆記二(著色器)

OpenGL學習筆記二(著色器)

OpenGL的著色器語言(GLSL)

在說OpenGL的著色器語言之前先來介紹一下著色器到底是個什麼東西。

在學unity3d的時候就聽說有人說能寫shader和做圖形渲染優化的人都是大神,當時沒學過著色器一聽就感覺不明覺厲啊,先送上膝蓋再說。現在學了著色器當然還是感覺一臉懵逼Orz。

這時就該祭出無上法典《計算機圖形學》,沒錯就是這本書,看不懂shader、學不會shader還是我們沒有打好基礎,因為shader是應用在GPU上的語言,如果不瞭解GPU的工作流程,我們如何學會shader呢。

著色器(Shader)是執行在GPU上的小程式。這些小程式為圖形渲染管線的某個特定部分而執行。從基本意義上來說,著色器只是一種把輸入轉化為輸出的程式。著色器也是一種非常獨立的程式,因為它們之間不能相互通訊;它們之間唯一的溝通只有通過輸入和輸出。

Shaders 是進行三維圖形學程式設計的先進方法,從某種意義上來說 Shader 的出現是圖形學中的一種”退步”,因為在這之前所有的功能都直接由固定管線提供,而開發人員只需要為其指定引數(如光照屬性、旋轉角度等),但是由於 Shader 的出現這些功能現在都需要開發者自己通過 Shader 實現。儘管如此,這種可程式設計效能夠提供給開發者更多的靈活性和創造性。

目前主流的著色器語言有GLSL(OpenGL Shading Language)是在 OpenGL 對應的著色器語言。相對地,DirectX 對應的著色器語言是 High-Level Shading Language(HLSL)。本篇說的OpenGL的著色器語言GLSL。

著色器是使用一種叫GLSL的類C語言寫成的。GLSL是為圖形計算量身定製的,它包含一些針對向量和矩陣操作的有用特性。

OpenGL的可程式設計管線如下圖所示:

這裡寫圖片描述

在我上一篇OpenGL學習筆記(三角形) 就用到了最基本的著色器語言,也差不多是最簡單的著色器語言,頂點著色器(Vertex Shader)和片段著色器(Fragment Shader)。就OpenGL而言著色器一般有三種著色器頂點著色器、片元著色器、幾何著色器。

頂點處理器負責對傳入渲染管線的每個頂點執行頂點著色器中的內容,(傳入的頂點的數量由繪製函式確定),頂點著色器並不關心所要渲染的基本圖元的拓撲結構。此外,你不能在頂點處理器中丟棄任何一個頂點。每個頂點都只被頂點處理器處理一次,在經過矩陣變換之後繼續進入接下來的流水線。

下一個階段是幾何處理器,組成圖元所需要的頂點以及其鄰接關係都會被提供給著色器。這使得著色器能夠考慮除頂點本身之外的其他資訊。除此之外,幾何處理器也可以將在繪製函式中確定的拓撲關係修改成另外一種拓撲關係。例如你可以通過建立一個頂點列表來生成兩個三角形(如一個正方形).除此之外,你也可以在每次呼叫幾何著色器的時候對一個頂點進行多次引用,通過這樣的方式我們可以按照我們在幾何著色器中選定的拓撲結構來生成多個圖元。

渲染管線中的下一個階段是裁剪,這是一個單一功能的固定功能單元——它通過我們前面課程中見過的規範化盒子對圖元進行裁剪。同時它還通過近裁剪面和遠裁剪面對其進行裁剪。同時他也支援使用者自定義裁剪面對場景進行裁剪。未被裁剪掉的頂點會變換到螢幕座標系之下,之後通過光柵化將頂點按照拓撲結構渲染到螢幕上。例如,如果我們要繪製一個三角形那就意味著要找出位於三角形內部的所有點。對於這樣每一個點,在光柵化過程中都會呼叫片段處理器對其進行處理。在片段處理器中我們可以通過對紋理進行取樣或者使用其他技術來確定畫素的顏色。

頂點著色器、片段著色器、幾何著色器這三個可程式設計階段是可選擇的,如果我們不向其繫結 Shader 程式就會執行預設的固定管線的函式。

概念不能再說了不然頭都暈了,下面直接上程式碼。

著色器的開頭總是要宣告版本,接著是輸入和輸出變數、uniform和main函式。每個著色器的入口點都是main函式,在這個函式中我們處理所有的輸入變數,並將結果輸出到輸出變數中。

一個典型的著色器有下面的結構:

#version version_number
in type in_variable_name;
in type in_variable_name;

out type out_variable_name;

uniform type uniform_name;

int main()
{
  // 處理輸入並進行一些圖形操作
  ...
  // 輸出處理過的結果到輸出變數
  out_variable_name = weird_stuff_we_processed;
}

當我們特別談論到頂點著色器的時候,每個輸入變數也叫頂點屬性(Vertex Attribute)。我們能宣告的頂點屬性是有上限的,它一般由硬體來決定。OpenGL確保至少有16個包含4分量的頂點屬性可用,但是有些硬體或許允許更多的頂點屬性,你可以查詢GL_MAX_VERTEX_ATTRIBS來獲取具體的上限:

int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;

和其他程式語言一樣,GLSL有資料型別可以來指定變數的種類。GLSL中包含C等其它語言大部分的預設基礎資料型別:int、float、double、uint和bool。GLSL也有兩種容器型別,分別是向量(Vector)和矩陣(Matrix)。

這裡寫圖片描述

大多數時候我們使用vecn,因為float足夠滿足大多數要求了。

一個向量的分量可以通過vec.x這種方式獲取,這裡x是指這個向量的第一個分量。你可以分別使用.x、.y、.z和.w來獲取它們的第1、2、3、4個分量。GLSL也允許你對顏色使用rgba,或是對紋理座標使用stpq訪問相同的分量。

向量這一資料型別也允許一些有趣而靈活的分量選擇方式,叫做重組(Swizzling)。重組允許這樣的語法:

vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;

你可以使用上面4個字母任意組合來建立一個和原來向量一樣長的(同類型)新向量,只要原來向量有那些分量即可;然而,你不允許在一個vec2向量中去獲取.z元素。我們也可以把一個向量作為一個引數傳給不同的向量建構函式,以減少需求引數的數量:

vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);

向量是一種靈活的資料型別,我們可以把用在各種輸入和輸出上。

雖然著色器是各自獨立的小程式,但是它們都是一個整體的一部分,出於這樣的原因,我們希望每個著色器都有輸入和輸出,這樣才能進行資料交流和傳遞。GLSL定義了in和out關鍵字專門來實現這個目的。每個著色器使用這兩個關鍵字設定輸入和輸出,只要一個輸出變數與下一個著色器階段的輸入匹配,它就會傳遞下去。但在頂點和片段著色器中會有點不同。

頂點著色器應該接收的是一種特殊形式的輸入,否則就會效率低下。頂點著色器的輸入特殊在,它從頂點資料中直接接收輸入。為了定義頂點資料該如何管理,我們使用location這一元資料指定輸入變數,這樣我們才可以在CPU上配置頂點屬性。我們已經在前面的教程看過這個了,layout (location = 0)。頂點著色器需要為它的輸入提供一個額外的layout標識,這樣我們才能把它連結到頂點資料。

你也可以忽略layout (location = 0)識別符號,通過在OpenGL程式碼中使用glGetAttribLocation查詢屬性位置值(Location),但是我更喜歡在著色器中設定它們,這樣會更容易理解而且節省你(和OpenGL)的工作量。

另一個例外是片段著色器,它需要一個vec4顏色輸出變數,因為片段著色器需要生成一個最終輸出的顏色。如果你在片段著色器沒有定義輸出顏色,OpenGL會把你的物體渲染為黑色(或白色)。

所以,如果我們打算從一個著色器向另一個著色器傳送資料,我們必須在傳送方著色器中宣告一個輸出,在接收方著色器中宣告一個類似的輸入。當型別和名字都一樣的時候,OpenGL就會把兩個變數連結到一起,它們之間就能傳送資料了(這是在連結程式物件時完成的)。

頂點著色器

#version 330 core
layout (location = 0) in vec3 aPos; // 位置變數的屬性位置值為0

out vec4 vertexColor; // 為片段著色器指定一個顏色輸出

void main()
{
    gl_Position = vec4(aPos, 1.0); // 注意我們如何把一個vec3作為vec4的構造器的引數
    vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把輸出變數設定為暗紅色
}

片段著色器

#version 330 core
out vec4 FragColor;

in vec4 vertexColor; // 從頂點著色器傳來的輸入變數(名稱相同、型別相同)

void main()
{
    FragColor = vertexColor;
}