1. 程式人生 > 其它 >opengl——著色器基礎

opengl——著色器基礎

技術標籤:opengl

Background

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

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

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

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

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

著色器程式的建立與C/C++程式的建立相似。首先你需要編寫著色器程式文字並使之對你的程式可見,我們可以直接將shader程式文字存放在一個字串陣列中並將其包含你的原始碼中或者從外部文字檔案中匯入(依然轉化成字串陣列)即可。其次我們需要將Shader源程式逐個編譯成 Shader 物件。之後可以將編譯好的 Shader 物件連結到一個單獨的程式物件中並將其載入到 GPU。對 Shader 程式進行連結使得驅動程式能夠根據他們之間的關係對 Shader 程式進行裁剪和優化。例如,例如,你可以為一個傳出法線資訊的頂點著色器匹配一個不使用法線的片元著色器。這樣的話驅動程式中的 GLSL 編譯器就會將與法線有關的操作移除來提高頂點著色器的執行效率。如果同樣是這個頂點著色器但是和另外一個需要使用法線資訊的片元著色器連結到另外一個程式物件上,則頂點著色器中與法線相關的操作就不會被剔除。

Code Walkthru

GLuint ShaderProgram = glCreateProgram();

我們通過建立一個shader程式物件來開始我們的著色器工程,我們將會把所有的shader程式都連結到這個sahder程式物件上。

GLuint ShaderObj = glCreateShader(ShaderType);

通過呼叫上面的函式我們建立了兩個 Shader 物件,其中一個 Shader 物件的型別為 GL_VERTEX_SHADER,另一個為 GL_FRAGMENT_SHADER。這兩個型別的 Shader物件的指定 Shader 源程式和編譯 Shader 程式的過程是一樣的。

const GLchar* p[1];
p[0] = pShaderText;
GLint Lengths[1];
Lengths[0]= strlen(pShaderText);
glShadersource(ShaderObj, 1, p, Lengths);

在對 Shader 物件進行編譯之前我們必須為其指定 Shader 源程式,glShadersource 函式需要一個 Shader 物件作為引數,這個函式為 Shader 源程式的指定提供了一種很靈活的方法。源程式可以分佈在多個字串陣列中,你需要提供這些陣列的指標陣列以及一個用於存放對應陣列長度的整數陣列。為了簡單起見,整個著色器程式碼我們僅使用一個字串陣列,源程式指標陣列和和長度陣列都只有一個元素。glShadersource(ShaderObj, 1, p, Lengths)的第二個引數是這兩個陣列的元素個數。

glCompileShader(ShaderObj);

編譯shader程式。

GLint success;
glGetShaderiv(ShaderObj, GL_COMPILE_STATUS, &success);
if (!success) {
    GLchar InfoLog[1024];
    glGetShaderInfoLog(ShaderObj, sizeof(InfoLog), NULL,InfoLog);
    fprintf(stderr, "Error compiling shader type %d:'%s'n", ShaderType, InfoLog);
}

很多時候,你會遇到一些編譯錯誤。上面的程式碼能獲得編譯狀態並且顯示編譯器遇到的錯誤。

glAttachShader(ShaderProgram, ShaderObj);

最後,我們將編譯之後的 Shader 物件附加到程式物件上,這和在 Makefile 中連結一個物件連結串列類似。因為我們這兒沒有 Makefile 所以通過程式設計來實現這種功能。只有被附加到程式物件上的 Shader 物件才會參與連結過程。

glLinkProgram(ShaderProgram);

在編譯好所有 Shader 物件以及將他們附加到程式物件之後我們就可以進行連結操作。注意在連結程式物件之後你可以通過為每個 Shader 物件呼叫 glDetachShader 和 glDeleteShader 方法來刪除其中的 Shader 物件。OpenGL驅動程式會為其生成的大部分物件維持一個引用計數。如果我們在建立一個 Shader 物件之後又將其刪除則驅動程式會將這個 Shader 物件剔除掉,但是如果我們將 Shader 物件附加到程式物件之後呼叫 glDeleteShader 函式則只會將 Shader 物件標記為刪除部分,你需要呼叫 glDetachShader 函式其引用計數才會下降到0之後才會被移除。

glGetProgramiv(ShaderProgram, GL_LINK_STATUS,&Success);
if (Success == 0) {
    glGetProgramInfoLog(ShaderProgram, sizeof(ErrorLog), NULL,ErrorLog);
    fprintf(stderr, "Error linking shader program:'%s'n", ErrorLog);
}

需要注意的是我們檢查程式相關的錯誤(如連結錯誤)與檢查 Shader 相關錯誤有些許不同。我們使用 glGetShaderiv 函式和 glGetShaderInfoLog 函式代替 glGetShaderiv 函式和 glGetShaderInfoLog 函式。

glValidateProgram(ShaderProgram);

你也許會問既然我們都已經成功的連結了程式物件為什麼還需要對其進行驗證呢。它們之間的區別是連結主要檢查基於著色器組合的錯誤,而上面呼叫的函式則是驗證基於當前的管線狀態程式是否能夠成功執行。在一個有多個shader程式和很多狀態變化的複雜程式中,在每次繪製之前都進行驗證是更加明智的。在我們這個簡單的程式中我們僅僅對其呼叫了一次。當然你也可以僅僅在開發過程中進行這樣的驗證而避免在最終產品中增加這個不必要的開銷。

glUseProgram(ShaderProgram);

最後,我們呼叫上面的函式將連結之後的 Shader 程式物件新增到渲染管線中。除非我們使用其他的 Shader 程式物件來替換當前的程式物件或者通過呼叫 glUseProgram(NULL)顯式的禁用它的使用(並且啟用固定管線),否則這個 Shader 程式物件會對每次的繪製都會產生效果。如果你建立的 Shader 程式物件只包含一種型別的 Shader 程式,那麼其他階段的操作會預設的呼叫固定管線中的功能。
我們已經介紹了 OpenGL 中與 Shader 程式管理相關的函式,本教程中剩下的就是與頂點著色器和片元著色器相關的內容了(包含在“pVS”和“pFS”變數中)。

 #version 330

這告訴編譯器我們的 Shader 程式是針對3.3版本的 GLSL,如果編譯器不支援這個版本則會報錯。

layout (location = 0) in vec3 Position;

這條語句出現在頂點著色器中,他聲明瞭一個指定為頂點屬性的float型別三維向量,這個向量在shader中被表示為‘Position’。‘頂點屬性’意味著 GPU 中的 Shader 程式每呼叫一次,頂點緩衝區都會為其提供一個新的頂點資料。語句中的第一部分——layout (location = 0)將屬性名稱與緩衝區中的屬性進行繫結。這在我們的頂點中包含多個屬性(位置、法線、紋理座標等)時顯得尤為重要。我們必須要告訴編譯器頂點緩衝區中的頂點屬性與 Shader 中宣告的屬性的對映關係。有兩種方法可以實現這個功能,首先我們可以顯式的對其進行設定就和我們在這兒設定的一樣(設定為0),這種情況下我們在應用程式中使用硬編碼(正如我們呼叫 glVertexAttributePointer 函式時的第一個引數一樣);或者我們直接不管他(直接在 Shader 中宣告‘in vec3 Position’),之後在應用程式執行過程中通過呼叫 glGetAttribLocation 獲取其地址,在這種情況下我們需要將返回的地址傳遞給 glVertexAttributePointer 而不是使用硬編碼值。在這裡我們選擇比較簡單的方式來實現,但是對於比較複雜的應用程式在執行時確定屬性索引會更加好。這使得在不用將多個 Shader 源程式調整到一個緩衝區佈局的情況下更容易將其整合。

void main()

我們可以通過將多個 Shader 物件連結到一起來建立你自己的著色器,但是在每個著色器階段(VS,GS,FS)只能有一個 main 函式作為著色器的入口點。例如你可以通過多個函式建立一個燈光庫,並且將它連結到你所提供的 Shader 程式物件上,其中一個函式名為 main 函式。

gl_Position = vec4(0.5 * Position.x, 0.5 *Position.y, Position.z, 1.0);

在這裡我們通過編碼對傳入的頂點位置進行變換,我們將頂點的 X、Y 分量的值減半而保持 Z 方向值不變,gl-Position 是一個特殊的內建變數,他能夠存放齊次(包含X,Y,Z和W分量)頂點座標。在光柵化過程中系統會尋找這個變數並使用它作為頂點在螢幕上的位置(需要經過一些矩陣變換)。將頂點的X,Y分量減半意味著我們將會看到一個面積只有前面教程中的四分之一的三角形。需要注意的是我們將W分量設定為1.0,這對於三角形的正確顯示是非常重要的,實現從3D到2D的投影變換實際上是在兩個不同的階段實現的,首先你需要讓所有的頂點都乘上投影矩陣(我們將在後面的教程中對此進行介紹),之後 GPU 在對其進行光柵化之前自動對位置屬性(Position)執行透視分割。這意味著 gl-Position 中的所有分量都會除以W分量。在本節的頂點著色器中我們並沒有進行任何與投影有關的操作,但是我們不能禁用透視分割階段。不論我們從頂點著色器中輸出 gl_Position 中的任何值都會被除以其W分量。為了得到我們所期望的結果我們需要記住這一點。為了避免透視分割對結果產生影響我們將W分量設定為1.0.除以1.0並不會影響 Position 向量中的其他分量,並使其依舊處於規範化盒子中。
如果所有部分都正確工作,那麼這三個頂點(-0.5, -0.5), (0.5, -0.5) 和(0.0, 0.5)會進入光柵化階段。由於所有點都正好處於規範化盒子之中,所以裁剪器並不需要做任何事。這些值會被對映到螢幕座標系中,之後光柵化階段開始遍歷處於三角形內部的所有點。對於三角形中的每個點都會對其執行片元著色器,下面的程式碼就來自於片元著色器。

out vec4 FragColor;

一般情況下片元著色器的作用就是確定片元的顏色,除此之外,片元著色器也完全可以丟棄片元或則改變其Z值(Z值的改變會對之後的深度測試產生影響)。輸出顏色是通過宣告上面的變數實現,四個分量分別表示R,G,B和A(alpha)。被寫入到這個變數中的值會被光柵化程式接受並最終寫入到幀快取中。

FragColor = vec4(1.0, 0.0, 0.0, 1.0);

在前面的教程中由於我們並沒有使用片元著色器,所以所有的物體都被繪製成預設的白色。這裡我們將顏色設定為紅色。

Operation Result

在這裡插入圖片描述
學習內容來源:https://www.bootwiki.com/opengl/opengl-modern-opengl-tutorial.html