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的强大功能,不仅仅可以修改模型本生,更可以实现几何层面处理。