背景知識:編程
從這個課程開始,每個咱們執行的效果和技術,都會用到着色器。着色器是當前開發3D圖形的流行方式。從某種程度來講,這是一個退步,由於大多數3d功能函數都提供了固定功能管線,僅僅須要開發人員配置一些參數(好比光線屬性,旋轉值等),而如今必須都得由開發人員制定(經過着色器),然而,這種可編程性卻能帶來更多的靈活和創新。數組
可視化的OpenGl可編程管線以下圖:app
【頂點處理器】負責各個頂點着色器的執行,以及每一個頂點經過管線(數量取決於用於draw call的參數)。頂點着色器對須要渲染什麼圖形是一無所知的。另外,頂點處理器中你不能丟棄頂點。每個頂點剛好一次進入頂點過程,經過變換進入管線的下一階段。編輯器
下一階段是【幾何處理器】。在這一階段數據是做爲完整的圖元信息(即它的全部頂點)和相鄰頂點提供給着色器的。這使技術必須考慮到除頂點自己以外的更多額外信息。幾何着色器還能輸出與以前draw call中選擇的不一樣的拓撲結構。例如,你能夠提供給一個點的列表,而後根據這些點生成2個三角形(好比一個正方形,這個技術叫廣告牌)。此外,你能夠給每一個幾何着色器傳多個頂點,根據你選擇的拓撲產成多種幾何圖元。函數
管線的下一階段是【剪裁】。這是一個有明確任務的固定功能--在以前咱們教程中看到的標準盒子中,剪裁圖元。它會在近Z面和遠Z面之間剪裁它們。也能夠申請自定義剪裁面,有不那麼作的剪髮。在此階段中,留下來的頂點的位置會被映射到屏幕空間座標,接着光柵化根據它們的拓撲結構渲染它們到屏幕。例如,若是是三角形,這意味着把三角形內部全部的點找出來。對於每一個點光柵化調用【片斷處理器】。在這裏你能用紋理採樣或其餘技術肯定像素的顏色。佈局
這三個可編程的階段(頂點,幾何,片斷處理器)都是可選的。若是你沒有對它們綁定着色器,那麼某些默認的函數功能會被執行。測試
着色器管理程序跟C/C++程序的建立方式很是相似。首先你得寫一段着色器代碼,並使其可被你的程序獲取。這步能夠經過在源程序代碼裏簡單的包含一個字符串數組文本,或者把一個額外的文本文件加載進來(而後一個字符串數組中)。接着你就能把着色器代碼逐個翻譯成着色器對象。以後你能夠把着色器連接到一個單一的程序,然後把它加載進GPU。連接着色器使驅動有機會根據着色器之間的關係裁剪、優化它們。例如,你可能會有一個頂點着色器發起了法向量,匹配一個片斷着色器忽視了法向量。在這種狀況下,驅動中的GLSL編譯器會移除着色器中法向量相關函數以便可以更快地執行頂點着色器。若是以後連接到另外一程序的着色器又與一個用了法向量的片斷着色器匹配,則會產生一個不一樣的頂點着色器。優化
代碼實踐:ui
GLuint ShaderProgram = glCreateProgram();
咱們經過建立一個程序對象開始配置一個這個過程。咱們會把全部着色器一塊兒連接進這個對象中。操作系統
GLuint ShaderObj = glCreateShader(ShaderType);
咱們用上面的調用建立了2額着色器對象。一個是GL_VERTEX_SHADER類型的着色器,另外一個是GL_FRAGMENT_SHADER類型的。對於二者來講,指定着色器源程序和編譯着色器的過程是同樣的。
const GLchar* p[1];
p[0] = pShaderText;
GLint Lengths[1];
Lengths[0]= strlen(pShaderText);
glShaderSource(ShaderObj, 1, p, Lengths);
在編譯着色器對象以前咱們必需要指定它的源代碼。函數glShaderSource把着色器對象當作一個參數以及在指定源程序上提供了靈活性。源程序能夠分散在幾個字符數組中,而你須要提供一個指針數組指向這些字符數組,同時還須要一個包含每一個對應的字符數組的長度的整型數組。簡單起見咱們用只有一個字符串的數組做爲整個着色器的源程序,同時對於指針和長度咱們也僅僅用了一個元素。
glCompileShader(ShaderObj);
編譯着色器卻是至關容易...
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);
最終,咱們把編譯好的着色器鏈接到程序對象上。這很是像在makefile中爲連接過程指定對象列表。因爲咱們在這沒有makefile因此咱們模擬它的編程行爲。只有被鏈接的對象參與到連接過程。
glLinkProgram(ShaderProgram);
在編譯了全部着色器對象以及把它們鏈接到程序後,最終咱們能夠連接它了。注意連接了程序後你能夠調glDetachShader和glDeleteShader丟棄中間着色器對象。OpenGL驅動在大多數對象產生時維持着一個引用計數。若是一個着色器被建立後立刻又刪除,那麼驅動會丟棄它,可是若是它被鏈接到一個程序了,調用glDeleteShader只會標記爲刪除,你須要調用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);
}
注意咱們檢查程序相關錯誤(好比鏈接錯誤)跟着色器程序錯誤有所不一樣。咱們用glGetProgramiv代替glGetShderiv,用glGetProgramInfoLog代替glGetShaderInfoLog.
glValidateProgram(ShaderProgram);
你能夠問問你本身爲何咱們在程序成功連接後還須要驗證它。差異在於連接檢查的是着色器間的錯誤,而上面的調用檢查在當前管線狀態中程序是否能運行。在一個更復雜的應用中,多種着色器以及各類狀態在發送變化,最好在每個draw call前驗證一下。在咱們的簡單app中咱們就檢查了一次。固然了,你可能只是但願在開發過程當中多作這種檢查,而在最終的產品中要避免這種開銷。
glUseProgram(ShaderProgram);
最後,用上面這個調用把已連接到程序中的着色器設置到管線狀態中。如今這個程序會一直爲全部draw calls起做用,知道你用另外一個替換它或者調用glUseProgram(NULL)明確地禁止它使用(同時開啓固定功能管線)。若是你建立了一個只包含一個類型的着色器程序,其餘階段操做系統會用默認固定管線。
咱們已經完成了涉及到OpenGL着色器管理的調用的實踐。剩下的課程涉及到頂點着色器和片斷着色器的內容(源碼在'pVS'和'pFS'中)。
#version 330
這告訴編譯器咱們在針對GLSL的3.3版本。若是編輯器不支持它會發出一個錯誤。
layout (location = 0) in vec3 Position;
這段聲明出如今頂點着色器中。它聲明瞭一個3元素浮點向量的特定頂點屬性,在着色器中被認爲是一個「位置」。「特定頂點」意味着每次在GPU中調用着色器,都會從緩衝中提供一個新的頂點屬性。聲明的第一小節,layout(location = 0),創建了屬性名和緩衝中屬性數據的綁定。在諸如咱們頂點包含不少屬性(位置、法向量、紋理座標等等)的時候,這是必須的。咱們必須讓編譯器知道緩衝中的哪個頂點屬性被映射成着色器程序中的聲明屬性。這時有兩個方法。第一是像咱們目前作的那樣,明確地設置它的索引(爲0)。在這種狀況下咱們在應用中寫死一段代碼(像咱們以前在調用glVertexAttributePointer時第一個參數作的那樣);第二是不要它了(只在着色器中簡單的聲明'in vec3 Position')接着用glGetAttribLocation在運行時詢問location值。在這種狀況下咱們須要給glVertexAttributePointer一個返回值,而不是寫死一段代碼值。在這裏咱們選擇了簡單的方式,可是對於更加複雜的應用最好讓編譯器在運行時判斷、詢問屬性索引。當遇到不少沒有佈局好的代碼時,整合起來會更簡單。
void main()
你能經過把各類着色器對象連接到一塊兒來建立你的着色器。然而,只能有一個主函數做爲每一個着色器(頂點、集合、片斷)的入口。例如,你能夠用幾個函數建立一個輕量級的庫,帶着你提供的着色器連接它,但這些都不是「main」函數。
gl_Position = vec4(0.5 * Position.x, 0.5 * Position.y, Position.z, 1.0);
這裏咱們把傳來的頂點位置作一個寫死的變換。咱們讓x和y值減半,留着y值不變。 'gl_Position'是一個內置變量用以提供包含同類(x,y,z,w份量)頂點位置。光柵化會尋找這個變量,而後把它用做在屏幕空間中的座標(在一些轉換以後)。把x和y縮減到一半意味着咱們只能看到以前課程中四分之一大小的三角形。獲取從3D到2D的投影其實是由2個獨立的階段中完成的。首先,你須要把你全部的頂點乘以投影矩陣(咱們再其餘課程中會開發),而後GPU會在點屬性到達光柵器以前對它們自動執行「透視分離」。這意味着它經過w份量選項把gl_Position的其餘選項分離開。在這個課程中咱們還未在頂點着色器中作任何投影變換,但卻不能阻止透視分離運行。不論如何,gl_Position這個從頂點着色器輸出的值都會被HW用w份量分割。咱們須要記住這點,不然咱們拿不到咱們期待的結果。爲了規避透視分離的影響,咱們能夠設w爲1.0。1.0分離不會影響位置向量中的其餘份量,而依然留在咱們的標準盒子裏。
若是一切正常工做,3個值爲 (-0.5, -0.5), (0.5, -0.5) 和 (0.0, 0.5)的頂點將會到達光柵器。由於全部的頂點恰好都在標準盒子裏,因此裁剪就不用作任何事了。 這些值被映射成屏幕空間座標,而後光柵器開始跑遍全部三角形內部的點。對於每個點,片斷着色器都會執行。下面的代碼來自片斷着色器。
out vec4 FragColor;
片斷着色器的工做經常是肯定片斷(像素)的顏色。此外,片斷着色器能夠一併丟棄像素,或者改變它的Z值(會影響到隨後Z測試的結果)。輸出顏色是經過聲明上面的變量來作的。4個份量表明R,G,B,A。你設置進變量的值,會被光柵器接收到,最終寫入幀緩衝。
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
在以前幾個課程中沒有片斷着色器,因此全部東西都默認用白色來繪製。在這裏咱們設置片斷顏色爲紅色。
譯者總結:
1.OpenGL渲染管線的順序是:
頂點着色器--幾何着色器--剪裁--光柵化(包含片斷着色器)
2.建立並使用一個着色器的完整過程是:
1.建立一個着色器運行程序對象; GLuint ShaderProgram = glCreateProgram();
2.寫一段相似於C/C++的着色器代碼:
3.建立一個着色器對象(能夠是頂點或片斷),把代碼賦予到對象上; glShaderSource(ShaderObj, 1, p, Lengths);
4.編譯着色器對象; glCompileShader(ShaderObj);
5.把着色器對象連接到着色器運行程序對象中; glAttachShader(ShaderProgram, ShaderObj);
6.把着色器設置到管線狀態中。 glUseProgram(ShaderProgram);
3.錯誤檢測:
一種是用於捕獲着色器程序的語法錯誤,如glGetShaderiv、glGetShaderInfoLog;
另外一種用於捕獲運行程序的邏輯錯誤,如glGetProgramiv、glGetProgramInfoLog,這種相似於assert。
4.頂點着色器的做用:輸入頂點數據,輸入幾何圖元。經過設置頂點着色器中的指令,實現投影變換、透視分離等。 幾何着色器的做用:輸出拓撲結構。 片斷着色器的做用:輸入幾何圖元,肯定像素顏色。