原帖地址:http://ogldev.atspace.co.uk/www/tutorial04/tutorial04.html
本章開始學習shader的使用,以前大家常使用OpenGL固定管線來做一些程序,shader相對來說使用較少,而現代gpu編程,shader應用少不了,雖然使用shader編程,代碼多一點,但是卻更靈活。
OpenGL的shader管線框圖如下,注意,少了tessellation的部分,而OpenGL中的tessellation和d3d11中的是一樣的,只是每個階段名字叫的不一樣而已,后面教程中會提到。
第一步是vertex processor(vs),第二步是geometry processor(gs),第三步是clip操作(還應該包括PA, primitive assembly),最后是光柵化以及fragment processor(ps)。
可以看下D3D11教程的介紹,來和opengl的管線比較一下,其實除了名字,本質都是一樣的,因為都是相同的硬件。
http://www.cnblogs.com/mikewolf2002/archive/2012/03/24/2415141.html
1. 頂點處理(vertex processor),該階段主要是對每個頂點執行shader操作,頂點數量在draw函數中指定。頂點shader中沒有任何體元語義的內容,僅是針對頂點的操作,比如坐標空間變化,紋理坐標變化等等。每個頂點都必須執行頂點shader,不能跳過該階段,執行完頂點shader后,頂點進入下一個階段。
2. 幾何處理(geometry processor),在該階段,頂點的鄰接關系以及體元語義都被傳入shader,在幾何shader中,不僅僅處理頂點本身,還要考慮很多附加的信息。幾何shader甚至能改變輸出體元語義類型,比如輸入體元是一系列單獨的點(point list體元語義),而輸出體元則是三角形或者兩個三角形組成的四邊形等等, 甚至我們還能在幾何shader中輸入多個頂點,對於每個頂點輸出不同語義的體元。
3. clipper階段,或者稱作裁剪階段,這是一個固定管線模塊,它將用裁剪空間的6個面對體元進行裁剪操作,裁剪空間外的部分將會被移去,這時可能會產生新的頂點和新的三角形,用戶也可以定義自己的裁剪平面進行裁剪操作,裁剪后的三角形將會被傳到光柵化階段。[注:在clipper之前,會有PA階段,就是頂點shader或幾何shader處理過的頂點被重新裝配成三角形]
4. 光柵化階段和片元操作階段,clipper后的三角形會先被光柵化,產生很多的fragment(片元),接着執行片元shader,在片元shader中,可能會裝入紋理,從而產生最終的像素顏色。 【fragment可以理解為帶sample、深度信息的像素】
不同於D3D11的管線,OpenGL的頂點shader、幾何shader以及片元shader都是可選的,如果沒有指定它們,則會執行缺省的功能。
shader管理類似於創建C/C++程序,首先寫shader代碼,把代碼放在一個文本文件或者一個字符串中,然后編譯該shader代碼,把編譯后的shader代碼放到各個shader對象中,接着把shader對象鏈接到程序中,最后把shader送到GPU中去。
鏈接shader時候,driver可能會優化shader代碼,比如你的頂點shader可能會輸出一個法向的參數,但后面的片元shader並沒有使用它,可能driver就會進行優化操作,去掉該參數的輸出,從而加快頂點shader的執行。
在opengl中編寫shader程序,主要有以下步驟:
首先創建一個shader程序對象,然后分別創建相應的shader對象,比如頂點shader對象, 片元shader對象,並把它們連接到shader程序對象。在創建shader對象時候,需要裝入shader源碼,編譯shader,鏈接到shader程序對象幾個步驟。
下面是主要的代碼:
GLuint ShaderProgram = glCreateProgram();
我們首先創建一個shader程序對象,並把所有shader都鏈接到這個shader程序對象。
GLuint ShaderObj = glCreateShader(ShaderType);
我們用上面的函數創建2個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之前,我們首先要通過glShaderSource函數指定shader的源代碼,該函數可以通過字符指針數組(實際上是二維指針const GLchar ** ,每個元素都是一個字符指針,指向相應的源代碼)指定多個shader源代碼。該函數第一個參數是shader對象,第二個參數是個整數,指定字符指針數組中元素的個數,即多少個源代碼,第三個參數為字符指針數組地址,第四個參數是個整數指針數組,和shader字符指針數組對應,它指定每個shader源代碼的字符數量。為了使程序簡單,我們在glShaderSource中,字符指針數組元素只有一個,即只有一個slot來放源代碼。
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);
}
通常編譯shader的時候可能會碰到各種錯誤,這時候我們可以通過上面的代碼得到編譯狀態並打印出錯誤信息,便於調試shader代碼。
glAttachShader(ShaderProgram, ShaderObj);
最終把編譯的shader對象和shader程序對象綁定起來。
glLinkProgram(ShaderProgram);
綁定之后是鏈接操作,鏈接操作之后,我們可以通過函數glDeleteShader釋放中間shader對象。
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鏈接時候是否有錯誤。注意檢測shader鏈接錯誤的代碼和檢測shader編譯的代碼有些不同(最大的不同就是使用不一樣的函數)。
glValidateProgram(ShaderProgram);
驗證shader程序對象的有效性。這兒可能大家有點疑惑:既然前面已經鏈接成功,干嘛還要再次在驗證有效性?其實它們之間是有點區別的,鏈接檢測基於shader綁定,而驗證有效性則是驗證程序能否在現在的管線上執行。在復雜的應用中,有多個shader,多個狀態,在每個draw之前,最好都做一次驗證有效性的操作。在我們程序中,只做了一次驗證。當然,為了減少驗證的開銷,我們可以只在debug階段進行驗證操作,而最終的release階段不需要這些操作,從而減少程序開銷,提高性能。
glUseProgram(ShaderProgram);
最終,我們通過上面這個函數把鏈接好的程序對象送到shader管線。這個shader將對隨后的所有draw有效,除非你用另一個shader程序對象代替它或者通過設置glUseProgram(NULL)禁止它(此時會打開固定管線功能)。如果你只指定一個shader對象,比如只有vertex shader,那么fragment shader則使用固定管線的功能。
前面我們已經了解了shader管理的功能,下面看看頂點和片元shader的代碼: (分別包含在 'pVS' 和 'pFS' 變量中,在我修改的代碼中,會分別放在color.vs和color.ps中,並通過一個gclFile類來讀寫shader代碼文件,這樣便於調試shader代碼)。
#version 400
這告訴編譯器使用4.0版本的GLSL,如果編譯器不支持,則會產生一個錯誤信息。
layout (location = 0) in vec3 Position;
這個聲明指定頂點shader中頂點的屬性值是一個三維坐標向量,它的屬性名字是Position,對於GPU中每個執行頂點shader的頂點,該屬性值都有效,即意味着頂點buffer中的值被解釋為位置。 layout (location = 0)在頂點buffer和頂點屬性名字之間創建了一個綁定關系。
location指定該屬性在頂點buffer中的位置,特別是頂點包含多個屬性時候(比如位置、法向、紋理坐標等等),我們必須要讓編譯器知道,頂點的那個屬性和頂點buffer中的那個位置對應起來,通常有2種方法實現這個,一個就像我們在shader中指定的location = 0,在這種情況下,在cpp源代碼中我們通過硬編碼的方式,指定shader屬性,比如glVertexAttributePointer(0),另外一種情況下,我們可以在shader代碼中簡單聲明in vec3 Position,在應用程序中,通過函數glGetAttribLocation在運行時查找屬性位置,這時我們要利用glGetAttribLocation的返回值,而不是硬編碼。在本篇教程程序中,我們采用第一種方式,對於復雜的應用,可以采用第二種方式,讓編譯器決定屬性索引,然后在運行時查詢它,這樣可以方便把多個shader源文件集成在一起,而不必用它們匹配我們自己定義的緩沖layout。
void main()
我們能夠把多個shader對象鏈接在一起形成最終的shader,但是對每種類型shader(VS,GS,FS)來說,代碼中只能有一個main函數,這個函數作為該shader的執行入口點,例如,我們能夠創建一個shader庫文件,其中包含計算光照的一些函數,然后把它鏈接到沒有main函數的shader中去。
gl_Position = vec4(0.5 * Position.x, 0.5 * Position.y, Position.z, 1.0);
這兒,我們通過硬編碼來改變頂點位置,x和y的值減半,z值保持不變。'gl_Position'是一個內置的特殊變量,用來存儲頂點在齊次裁剪空間的坐標。光柵化模塊將會查找這個變量的值,用它作為屏幕空間的值(其實不准確,視口變換在PA里面做的,所以vs里面的點並不是對應屏幕空間)。x和y減半意味着渲染出的三角形是上篇教程中三角形的1/4,注意:這兒我們設置w=1.0,這是很重要的,否則的話,渲染結果可能並不正確。
把物體從三維投影到二維,需要兩個階段:第一個階段,把所有的頂點乘以投影矩陣,之后GPU在光柵化前會執行透視除法,就是x、y、z分量的值等於各自的值除以w的值,而w的值為1,透視除法是GPU的固定模塊完成,通常是在PA里面。本教程中,我們直接設置w=1.0,透視除法后的值和沒除前一樣。
如果程序運行正常,將會有三個頂點(-0.5, -0.5), (0.5, -0.5) 和(0.0, 0.5)到達光柵化模塊。本程序中clipper不需要做任何事情,因為我們所有的頂點都是在歸一化的裁剪空間盒子內,之后,頂點的值被映射到屏幕空間(視口變化操作),並在光柵化中模塊對三角形體元執行光柵化操作,光柵化后的每個片元隨后會執行片元shader操作。
下面的shader是片元shader的代碼:
out vec4 FragColor;
片元shader的功能就是輸出片元最終的顏色值,在片元shader里面,也可以放棄一些片元(像素),或者改變它們的z值(注意:這將影響硬件early z的功能)。輸出顏色最終通過宣布的out變量FragColor來完成,FragColor向量的四個分量分別表示R,G,B,A值,該值最終被寫入framebuffer。
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
上篇教程中,沒有shader操作,最終屏幕畫的是默認的白色,現在我們指定紅色,最終將會在屏幕上畫一個紅色的三角形。
程序執行后效果如下: