OpenGL 各個shader的作用和區別
OpenGL 各個shader的作用和區別
penGL4.0釋出了Tessellation shader(Control + Evaluation shader)。到OpenGL4.* 為止,現在OpenGL已經支援了5種不同型別的shader。
1.Vertex Shader,簡稱VS
2.TESS Control Shader (D3D11 叫Hull shader),簡稱TCS
3.TESS Evaluation Shader (D3D叫Domain shader),簡稱TES
4.Geometry Shader ,簡稱GS
5.Fragment Shader(D3D叫Pixel Shader),簡稱PS
我將根據他們的“主要”輸入與輸出,以及其基本功能進行對比。這樣我們就可以很好的瞭解他們在OpenGL Pipeline裡面的作用和區別。
以下順序是他們在OpenGL Pipeline裡面的執行順序。
1.Vertex Shader
輸入:頂點座標(Position),該座標值是由glVertex* 或者是glDraw*傳入的。
輸出:頂點座標,這個是經過幾何變換後的座標。
功能:簡單的說就是把輸入的頂點座標乘以(一系列)幾何變換矩陣。每輸入一個頂點(也就是glVertex*每呼叫一次),Vertex shader都會被呼叫一次。Vertex Shader只知道處理頂點,它不知道這些頂點是做什麼用的,也就是不知道這些頂點將來會被裝配成什麼圖元。(因為Vertex shader後面才會有圖元裝配的過程)
當然,VS還可以接收顏色,紋理座標,霧座標等屬性,並在內部對他們做一點點變化,然後再輸出。
2.TESS Control Shader
輸入:Patch,一個Patch可以看成是多個頂點的集合。它包括每個頂點的屬性(座標,顏色,紋理座標等等)。使用者可以指定一個patch裡面要包含幾個頂點。同時,一個patch還可以用自己的屬性,該屬性被它內部的所有頂點共有,即這些頂點只有一套patch屬性,而不是每個頂點擁有一個自己的patch屬性。(懂了嗎?)
輸出:Patch , gl_TessLevelOuter , gl_TessLevelInner。
功能:TCS會根據需求把Patch自己的屬性以及它內部的頂點屬性做一些修改。然後輸出Patch。當然,它也可以不做任何修改,直接傳給後面的shader。我們知道Tessellation的作用就是把一個圖元分割成很多圖元,比如把一個三角形分割成很多更小的三角形。因此,在分割的時候我們得要知道這個三角形的每個邊要被分割成多少段,然後在三角形內部,我們還要怎麼繼續分割,這兩個紫色的內容就是儲存在 gl_TessLevelOuter 和gl_TessLevelInner。TCS可以根據需要設定這兩個值。
所以,TCS的主要作用是設定Patch以及它內部頂點的屬性。同時也是最重要的,設定圖元接下來被細分的度。(TCS不做分割動作)
下面貼了個三角形分割的圖片。
注意:用TCS的話,glBegin函式的引數必須是GL_PATCHES,而不是以前那種傳統的圖元(點,線,三角形等)。 glPatchParameteri可以指定每個Patch包含幾個頂點。在VS與TCS直接有個圖元裝配的過程,它就是把VS輸出的頂點封裝一個Patch,然後傳給TCS。
3.TESS Evaluation Shader
輸入:一系列頂點。這些頂點是三角形被分割後產生的新頂點。下面是每個TES程式都必須有的一段程式碼:
layout( triangles, fractional_odd_spacing, ccw ) in;
它表示TES的輸入是三角形(當然你也可以寫成其他型別的圖元),至於 fractional_odd_spacing,和ccw是什麼意思,大家看spec吧,很簡單,我怕我解釋不清楚而誤解大家。最後的那個“in”進一步說明了這是TES的輸入。
輸出:也是一系列頂點。
功能:其實在TCS與TES之間有個過程叫Tessellation Primitive Generator(簡稱TGP),它首先會去檢視TES的輸入是什麼,哦,它要三角形。那麼,TGP就會把TCS傳入的Patch內部的頂點看成是若干個三角形(注意Patch內部的頂點不一定只有三個)。然後,TGP每次從當前Patch裡面取出三個頂點做一個三角形的分割,直到Patch裡面的頂點全部被取出。
每個三角形具體怎麼被分割呢?
其實,gl_TessLevelOuter 和gl_TessLevelInner會被傳入給TGP。它們的作用就被體現出來。這就是為什麼我前面說的TCS不做分割,只計算分割的度。(注意TGP不是shader,它只是pipeline裡面的一個狀態而已)
現在開始講TES的功能吧。其實TGP傳入的頂點的座標值並不是世界座標值,而是一個三角形內部的座標表示形式,大家看到上面的圖了吧,三角形頂點上有座標的,TGP然後根據這個座標去計算內部新成立的頂點在該區域性座標系內部的座標。因此,TES就是要把每個頂點的區域性座標變換成世界座標,以及把頂點相應屬性(顏色,紋理座標等)轉換成真正且有效的屬性值。每處理一個頂點就輸出一個頂點。
注意:TES並不知道這些頂點會被組成什麼圖元,它只要求TGP把patch內部的頂點當成什麼圖元去分割。TES和VS一樣,輸入是頂點,輸出也是頂點。在TES後面有個圖元裝配的過程,它會根據TES的輸入(看上面的那行程式碼),轉換成相應的圖元。這裡圖元裝配器會把TES輸出的頂點裝配成一個一個的三角形。
4.Geometry Shader
輸入:一個圖元
輸出:一個或者多個圖元
功能:無論是否有VS,TCS或者TES,在GS前面都會有一個圖元裝配的過程,也就是說,傳給GS的是圖元,而不是頂點。
GS可以做什麼呢?
根據新圖元生成新一個或者多個圖元。下面兩會程式碼也是每個GS都必須有的:
layout( triangles ) in;
layout( triangle_strip, max_vertices = 3 ) out;
它表示GS輸入的圖元是三角形。如果當前圖元不是三角形,該GS將不會被呼叫。該GS的輸出是三角形帶,每個三角形帶最多包括三個頂點。其實這個GS的輸入和輸出可以看成是一樣,沒做太多的事情。呵呵。如果最大頂點數超過3,那麼GS實際上是把多個三角形拼成了一個三角形帶。具體怎麼拼,根據需要自己去寫程式吧。
具體GS能夠做什麼呢?
舉個簡單的例子吧。我們可以用GS畫Bezier曲線。Bezier曲線是有幾個控制點生成的,我們可以把這些控制點假裝當成圖元(點,線,線帶)傳個GS。然後GS在內部通過Bezier曲線演算法算出跟多的Bezier曲線上面的頂點。算出來的點當成線帶輸出,這樣就由控制點計算出了新圖元(Bezier曲線)。
前言:Shader Model 4給我們帶來了Geometry Shader這個玩意兒。其實這個東西早就在一些3D動畫製作軟體中存在了,比如Maya 8。我參考了以前DX10的哪一篇Preview與Csustan.edu的一篇比較詳盡的教材向大家展示了Geometry Shader的用途和特點。說實話,目前關於這個Geometry Shader的資料真的是很少,Wikipedia上也只有薄薄的幾行而已。
Shader Model 4與Unified GPU的特性著實讓大家心馳神往,無限長度的指令、統一結構,讓GPU的通用計算特性越來越強。目前在Realtime Rendering領域中雖然說Geometry Shader還沒有真正得到使用,但是NVIDIA的心思是很顯而易見的:將已經非常成熟的離線動畫製作中的技術用於效能日益提高的GPU上。NVIDIA宣稱Geforce8系列GPU可以使用Softimage|XSI的Shader,這不僅僅是一個Compiler的實現,更加明顯的是一種利用GPU實現離線渲染畫質的未來趨勢。也許未來我們將可以看到以實時速度光線跟蹤渲染出的近乎於電影一般畫質的遊戲場景,這已經不是幻想,而是現實。讓我們先Geometry Shader(一下簡稱GS,Vertex Shader和Pixal Shader類似)究竟是怎麼一回事吧。
Where Is The Geometry Shader
簡而言之,GS位於VS與PS之間,可以完成許多模型層面上的工作諸如LOD。以往這些工作都是在CPU上完成的,佔用了寶貴的CPU迴圈 —— CPU可是很繁忙的東西,遊戲邏輯、音樂、輸入接受都是靠它,卻無法提高多少效能,CPU的平行計算效能是遠遠無法和GPU相比的。
What Does the Geometry Shader Do
我們最先看到GS的時候都有一個錯覺,認為它是和VS功能差不多的一個單元,其實不然。GS的輸入物件和輸出的物件是沒有任何關係的,點Point可以產生三角形Triangle,三角形可以組成三角形條帶Triangle Strip。但是GS所接受的圖元Primitive和以前使用的不同,它只接受“可調整”的圖元。這些圖元被一個一個的輸入GS,經過加工後再一個一個的傳送到管線的下一個流程中。
What Is The Adjacency Primtive
上文我們提高GS所接受的原料與傳統的不同,在OpenGL中,我們定義了新的圖元型別,它們是:
• GL_LINES_ADJACENCY_EXT
• GL_LINE_STRIP_ADJACENCY_EXT
• GL_TRIANGLES_ADJACENCY_EXT
• GL_TRIANGLE_STRIP_ADJECENCY_EXT
我們可以在glBegin()、glDrawElements()等API中將它們作為新的引數使用。下面解釋一下它們各自有什麼特點。
Line with Adjacency:每一個由4N個頂點組成,N是線段的數目。真正繪製的是#1與#2,#0與#3提供調整資訊。圖左上。
LIne Strip with Adjacency:每一個由N+3個頂點組成,N的意義同上。線段其實是在#1與#2,#2與#3,一直到#N與#N+1這些個頂點之間繪製的。圖右上。
Triangle with Adjacency:每一個由6N個頂點組成,N指的是三角形的數目。#0 #2 #4定義了原始的三角形,而#1 #3 #5定義了修正三角形Ajacent Triangle。
Triangle Strip with Adjacency:每一個由4N+2個三角形組成,N的意義同上。#2 #4 #6 #8定義了原始三角形條帶,而#1 #3 #5定義修正三角形群。
What's New In OpenGL
從GLEW 1.36與GLEE 5.21開始整合了關於GeometryShader的相關拓展。先貼出使用GS的程式碼我們再來陳述。
GLuint dl = glGenLists( 1 );
glNewList( dl, GL_COMPILE );
. . .
program = glCreateProgram();
. . .
glProgramParameteriEXT( program, GL_GEOMETRY_INPUT_TYPE_EXT, inputGeometryType);
glProgramParameteriEXT( program, GL_GEOMETRY_OUTPUT_TYPE_EXT, outputGeometryType);
glProgramParameteriEXT(program, GL_GEOMETRY_VERTICES_OUT_EXT, 101);
glLinkProgram( program );
glUseProgram( program );
. . .
glEndList( );
應該是很眼熟,極其類似於使用VS/FS。根據NVIDIA OpenGL Extension Specifications中的說明,被glCreateShader()接受的新列舉量是:
GEOMETRY_SHADER_EXT
新增加的函式有:
void ProgramParameteriEXT(uint program, enum pname, int value);
被上述函式接受的列舉量包括:
GEOMETRY_VERTICES_OUT_EXT
GEOMETRY_INPUT_TYPE_EXT
GEOMETRY_OUTPUT_TYPE_EXT
被glBegin()、glDrawElements()等API接受的列舉量包括:
LINES_ADJACENCY_EXT
LINE_STRIP_ADJACENCY_EXT
TRIANGLES_ADJACENCY_EXT
TRIANGLE_STRIP_ADJACENCY_EXT
更加詳細的說明定義請參閱NVIDIA OpenGL Extension Specifications。
在上面的範例程式碼中,我們可以很容易知道使用GS的過程:首先新建一個Program的HANDLE,然後呼叫glProgramParameteriEXT傳入輸入與輸出圖元的具體型別和輸出的圖元個數,必須呼叫2次,然後連結、啟用。GL_GEOMETRY_INPUT_TYPE_EXT後的inputGeometryType可以是以下幾種列舉量:
GL_POINTS
GL_LINES
GL_LINES_ADJACENCY_EXT
GL_TRIANGLES
GL_TRIANGLES_ADJACENCY_EXT
這是很直觀的呼叫。但是要注意,GS能夠輸出的物件決定於輸入的物件型別:1、倘若輸出GL_LINES,則必須輸入GL_LINES、GL_LINE_STRIP、GL_LINE_LOOP;2、倘若輸出GL_LINES_ADJACENCY_EXT,則必須輸入GL_LINES_ADJACENCY_EXT或者GL_LINE_STRIP_ADJACENCY_EXT;3、倘若輸出GL_TRIANGLES,則必須輸入GL_TRIANGLES、GL_TRIANGLE_STRIP、GL_TRIANGLE_FAN;4、倘若輸出GL_TRIANGLES_ADJACENCY_EXT,則必須輸入GL_TRIANGLES_ADJACENCY_EXT或者GL_TRIANGLE_STRIP_ADJACENCY_EXT。
GL_GEOMETRY_OUTPUT_TYPE_EXT後的outputGeometryType可以是下面幾種列舉量:
GL_POINTS
GL_LINE_STRIP
GL_TRIANGLE_STRIP
What's New In GLSL
首先我們要知道,GS在VS之後,如果GS需要VS計算過後的資料,則需要在兩個Shader中宣告"varying in"型變數。PS在GS之後,同理,如果PS需要使用GS計算的資料,那麼需要在兩個Shader中定義"varying out"型變數。GS和VS、PS一樣也可以訪問Uniform型常量,而且GS可以訪問所有OpenGL的內建Uniform,比如ModelView矩陣。只要適合,你甚至可以在GS中完成變換。
我們知道了GS位於VS之後,下面講述如何在這兩個Shader中進行互動。如果我們使用了GS,那麼必須也使用VS。GS使用一切VS計算寫入的Uniform,包括gl_Position、gl_Normal、gl_FrontColor等,我們需要知道,VS無法修改內建Uniform的數值比如gl_Vertex,但是可以任意的寫入Uniform比如gl_Position。
gl_PositionIn[#]
gl_NormalIn[#]
gl_TexCoordIn[ ][#]
gl_FrontColorIn[#]
gl_BackColorIn[#]
gl_PointSizeIn[#]
gl_LayerIn[#]
gl_PrimitiveIDIn[#]
陣列符號中的"#"一般應該由gl_VerticesIn這個const int型別所決定。gl_VerticesIn這個數值是在連結確定的,具體的數值含義是,標識輸入的圖元型別的最大維度,具體如下:
GL_POINTS 1
GL_LINES 2
GL_LINES_ADJACENCY_EXT 4
GL_TRIANGLES 3
GL_TRIANGLES_ADJACENCY_EXT 6
我們可以很清楚的看到每一個圖元由多少個頂點組成。
Several Examples
Bezier Line
利用Bezier的基本原理,輸入幾個控制點獲得平滑的樣條曲線。程式碼如下:
/*
GeometryInput gl_lines_adjacency
GeometryOutput gl_line_strip
Vertex bezier.vert
Geometry bezier.geom
Fragment bezier.frag
Program Bezier FpNum <2. 10. 50.>
LineWidth 3.
LinesAdjacency [0. 0. 0.] [1. 1. 1.] [2. 1. 2.] [3. -1. 0.]
*/
#version 120
#extension GL_EXT_geometry_shader4: enable
uniform float FpNum;
void main()
{
int num = int( FpNum+ 0.99 );
float dt = 1. / float(num);
float t = 0.;
for( int i = 0; i <= num; i++ ) {
float omt = 1. - t;
float omt2 = omt * omt;
float omt3 = omt * omt2;
float t2 = t * t;
float t3 = t * t2;
vec4 xyzw= omt3 * gl_PositionIn[0].xyzw +
3. * t * omt2 * gl_PositionIn[1].xyzw +
3. * t2 * omt* gl_PositionIn[2].xyzw +
t3 * gl_PositionIn[3].xyzw;
gl_Position= xyzw;
EmitVertex();
t += dt;
}
}
通過傳入不同的FpNum,我們可以控制樣條曲線的精度。在這裡我們直接寫入gl_Position,並沒有乘以gl_ModelViewProjectionMatrix,因為在VS中我們已經做過裁減了,而且,在裁減空間與在世界空間中插值的精度相同。(Big Big Big Question :真的麼?在Ken Perlin的那本《TEXTURING & MODELING A Procedural Approach third edition》中特地提到過Pixar RenderMan是在世界空間中插值的,比螢幕插值精確。)
Sphere Subdivision
球體分割,將一個大三角形逐步分割成許多小三角形,最終成為一個球面。示意圖如下:
程式碼如下。
/*
#version 120
#extension GL_EXT_geometry_shader4: enable*/\
uniform float FpLevel;
varying float LightIntensity;
vec3 V0, V01, V02;
void ProduceVertex( float s, float t )
{
const vec3 lightPos= vec3( 0., 10., 0. );
vec3 v = V0 + s*V01 + t*V02;
v = normalize(v);
vec3 n = v;
vec3 tnorm = normalize(gl_NormalMatrix*n); //the transformed normal
vec4 ECposition = gl_ModelViewMatrix * vec4( (Radius*v), 1. );
LightIntensity = dot( normalize(lightPos-ECposition.xyz), tnorm);
LightIntensity = abs( LightIntensity);
LightIntensity *= 1.5;
gl_Position = gl_ProjectionMatrix * ECposition;
EmitVertex();
}
void
main()
{
V01 = ( gl_PositionIn[1] - gl_PositionIn[0] ).xyz;
V02 = ( gl_PositionIn[2] - gl_PositionIn[0] ).xyz;
V0 = gl_PositionIn[0].xyz;
int level = int( FpLevel );
int numLayers = 1 << level;
float dt = 1. / float( numLayers );
float t_top = 1.;
for( int it = 0; it < numLayers; it++ )
{
float t_bot = t_top - dt;
float smax_top = 1. - t_top;
float smax_bot = 1. - t_bot;
int nums = it + 1;
float ds_top = smax_top / float( nums - 1 );
float ds_bot = smax_bot / float( nums );
float s_top = 0.;
float s_bot = 0.;
for( int is = 0; is < nums; is++ )
{
ProduceVertex( s_bot, t_bot );
ProduceVertex( s_top, t_top );
s_top += ds_top;
s_bot += ds_bot;
}
ProduceVertex( s_bot, t_bot );
EndPrimitive();
t_top = t_bot;
t_bot -= dt;
}
}
結果如下:
傳入的Level控制了迭代次數,當Level = 3時本質上level = 1<<3 = 8。
Object Silhouette
利用GS,給模型描邊。程式碼如下:
/*
GeometryInput gl_triangles_adjacency
GeometryOutput gl_line_strip
Vertex silh.vert
Geometry silh.geom
Fragment silh.frag
Program Silhouette Color { 0. 1. 0. }
*/
#version 120
#extension GL_EXT_geometry_shader4: enable
void main()
{
vec3 V0 = gl_PositionIn[0].xyz;
vec3 V1 = gl_PositionIn[1].xyz;
vec3 V2 = gl_PositionIn[2].xyz;
vec3 V3 = gl_PositionIn[3].xyz;
vec3 V4 = gl_PositionIn[4].xyz;
vec3 V5 = gl_PositionIn[5].xyz;
vec3 N042 = cross( V4-V0, V2-V0 );
vec3 N021 = cross( V2-V0, V1-V0 );
vec3 N243 = cross( V4-V2, V3-V2 );
vec3 N405 = cross( V0-V4, V5-V4 );
if( dot( N042, N021 ) < 0. )
N021 = vec3(0.,0.,0.) - N021;
if( dot( N042, N243 ) < 0. )
N243 = vec3(0.,0.,0.) - N243;
if( dot( N042, N405 ) < 0. )
N405 = vec3(0.,0.,0.) - N405;
if( N042.z * N021.z < 0. )
{
gl_Position = gl_ProjectionMatrix* vec4( V0, 1. );
EmitVertex();
gl_Position = gl_ProjectionMatrix* vec4( V2, 1. );
EmitVertex();
EndPrimitive();
}
if( N042.z * N243.z < 0. )
{
gl_Position= gl_ProjectionMatrix* vec4( V2, 1. );
EmitVertex();
gl_Position= gl_ProjectionMatrix* vec4( V4, 1. );
EmitVertex();
EndPrimitive();
}
if( N042.z * N405.z < 0. )
{
gl_Position= gl_ProjectionMatrix* vec4( V4, 1. );
EmitVertex();
gl_Position= gl_ProjectionMatrix* vec4( V0, 1. );
EmitVertex();
EndPrimitive();
}
}
從上面的3個例子我們可以看出GS的強大功能,不僅僅可以修改模型本生,更可以實現幾何層面處理。