Geometry Shader Concepts & Examples
前言: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的强大功能,不仅仅可以修改模型本生,更可以实现几何层面处理。
Addition
你最起码需要使用glew 1.4库,一块Geforce 8的显卡才可以使用GS。我只有7300与6200,所以无法亲自体验了,有兴趣的朋友不妨瞧瞧看。考试期间忙里偷闲看了一些东西,不过现在越来越发现自己没有多少创造力了,只知道实现别人的,还实在没有功力自己来开拓。