前言: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的強大功能,不僅僅可以修改模型本生,更可以實現幾何層面處理。
ps:需要使用glew 1.4庫