本文是OpenGL 4.0 Shading Language Cookbook的学习笔记。
前面我们展示了使用几何着色器生成与输入图元不同的图元。除此之外,几何着色器还可以为之后的渲染管线生成附加信息。几何着色器可以访问图元的所有顶点,可以很方便地基于整个图元的信息进行计算处理。
本例使用几何着色器计算图元的附加信息,然后在片段着色器中使用这些附加信息来绘制模型的线框。
下图使用这一技术渲染模型的线框。线框使用几何着色器计算出的信息绘制而成。
有很多技术可以在着色后的表面上绘制线框。我们这里使用的技术来自NVIDIA在2007年出版的白皮书。我们使用几何着色器可以做到在一遍处理中同时绘制线框和模型。在这里,我们还对线框进行了简单的抗锯齿处理,最后得到的结果十分不错。
我们计算片段到最近的三角形边的距离,当这个距离在一个固定范围内,我们对这个片段着色时混合上边的颜色。否则,进行正常的着色。
我们使用下面的方法计算片段到边的距离。在几何着色器中,我们计算每个顶点到对边的距离(也就是多边形的高)。即下图中的ha,hb和hc。
我们可以利用三角形的内角通过余弦函数来计算ha,hb和hc。比如,对于ha,我们可以使用下图的方法计算。
hb和hc也可以使用同样的方法计算得到。(需要注意
计算出三角形的高之后,我们可以创建一个叫做edge-distance的向量。这个向量的x成分表示到a边的距离,y成分表示到b边的距离,z成分表示到c边的距离。OpenGL会自动对这些成分进行插值。对于位于顶点A处的片段,它的这个向量为(ha,0,0),对于B点处的片段,它的这个向量为(0,hb,0),对于C点处的片段,它的向量为(0,0,hc)。对于位于三角形中的片段,它的向量是经过插值后的数据,每个分量表示到各边的距离。
我们在屏幕空间计算这些数据。让OpenGL对这些数据进行线性插值。
在片段着色器,我们查找片段到三条边的最短距离,如果这个距离小于线段的宽度,我们就将片段颜色混合上线段颜色。在这里,我们使用smoothstep函数来对线段进行一定程度的反走样。
实现
我们需要设置视口矩阵(uniform变量ViewporMatrix),在几何着色器中,我们用它来计算顶点在屏幕空间下的坐标。
下面这些Uniform变量和线框相关:
- Line.Width:线框的线段宽度。
- Line.Color:线框的颜色。
我们需要采取下面的步骤为网格创建线框:
1. 使用下面的代码作为顶点着色器:
#version 400
layout (location = 0 ) in vec3 VertexPosition;
layout (location = 1 ) in vec3 VertexNormal;
out vec3 VNormal;
out vec3 VPosition;
uniform mat4 ModelViewMatrix;
uniform mat3 NormalMatrix;
uniform mat4 ProjectionMatrix;
uniform mat4 MVP;
void main()
{
VNormal = normalize( NormalMatrix * VertexNormal);
VPosition = vec3(ModelViewMatrix * vec4(VertexPosition,1.0));
gl_Position = MVP * vec4(VertexPosition,1.0);
}
2. 使用下面的代码作为几何着色器:
#version 400
layout( triangles_adjacency ) in;
layout( triangle_strip, max_vertices = 15 ) out;
out vec3 GNormal;
out vec3 GPosition;
// Which output primitives are silhouette edges
flat out bool GIsEdge;
in vec3 VNormal[]; // Normal in camera coords.
in vec3 VPosition[]; // Position in camera coords.
uniform float EdgeWidth;
// Width of sil. edge in clip cds.
uniform float PctExtend; // Percentage to extend quad
bool isFrontFacing( vec3 a, vec3 b, vec3 c )
{
return ((a.x * b.y - b.x * a.y) +
(b.x * c.y - c.x * b.y)
+ (c.x * a.y - a.x * c.y)) > 0;
}
void emitEdgeQuad( vec3 e0, vec3 e1 )
{
vec2 ext = PctExtend * (e1.xy - e0.xy);
vec2 v = normalize(e1.xy – e0.xy);
vec2 n = vec2(-v.y, v.x) * EdgeWidth;
// Emit the quad
GIsEdge = true;//This is part of the sil. edge
gl_Position = vec4( e0.xy - ext, e0.z, 1.0);
EmitVertex();
gl_Position = vec4( e0.xy - n - ext, e0.z, 1.0 );
EmitVertex();
gl_Position = vec4( e1.xy + ext, e1.z, 1.0 );
EmitVertex();
gl_Position = vec4( e1.xy - n + ext, e1.z, 1.0 );
EmitVertex();
EndPrimitive();
}
void main()
{
vec3 p0 = gl_in[0].gl_Position.xyz / gl_in[0].gl_Position.w;
vec3 p1 = gl_in[1].gl_Position.xyz / gl_in[1].gl_Position.w;
vec3 p2 = gl_in[2].gl_Position.xyz / gl_in[2].gl_Position.w;
vec3 p3 = gl_in[3].gl_Position.xyz / gl_in[3].gl_Position.w;
vec3 p4 = gl_in[4].gl_Position.xyz / gl_in[4].gl_Position.w;
vec3 p5 = gl_in[5].gl_Position.xyz / gl_in[5].gl_Position.w;
if( isFrontFacing(p0, p2, p4) ) {
if( ! isFrontFacing(p0,p1,p2) )
emitEdgeQuad(p0,p2);
if( ! isFrontFacing(p2,p3,p4) )
emitEdgeQuad(p2,p4);
if( ! isFrontFacing(p4,p5,p0) )
emitEdgeQuad(p4,p0);
}
// Output the original triangle
GIsEdge = false;
// This triangle is not part of an edge.
GNormal = VNormal[0];
GPosition = VPosition[0];
gl_Position = gl_in[0].gl_Position;
EmitVertex();
GNormal = VNormal[2];
GPosition = VPosition[2];
gl_Position = gl_in[2].gl_Position;
EmitVertex();
GNormal = VNormal[4];
GPosition = VPosition[4];
gl_Position = gl_in[4].gl_Position;
EmitVertex();
EndPrimitive();
}
3. 使用下面的代码作为片段着色器:
#version 400
//*** Light and material uniforms go here ****
uniform vec4 LineColor; // The sil. edge color
in vec3 GPosition; // Position in camera coords
in vec3 GNormal; // Normal in camera coords.
flat in bool GIsEdge;
// Whether or not we're drawing an edge
layout( location = 0 ) out vec4 FragColor;
vec3 toonShade( )
{
// *** 使用之前文章中的卡通着色代码 ***
}
void main()
{
//If we're drawing an edge, use constant color,
//otherwise, shade the poly.
if( GIsEdge ) {
FragColor = LineColor;
} else {
FragColor = vec4( toonShade(), 1.0 );
}
}
原理
顶点着色器将位置和法线变化到相机空间后输出给几何着色器。gl_Position被设置为剪切坐标系下的位置坐标。我们将在几何着色器中使用它来确定屏幕空间坐标。
我们使用下面的代码定义几何着色器的输入和输出信息。
layout( triangles ) in;
layout( triangle_strip, max_vertices = 3 ) out;
我们并不改变三角形的几何结构,输入和输出的三角形图元是完全相同的。
我们的几何着色器有三个输出变量GNormal,GPosition和GEdgeDistance。前两个是相机坐标系下的法线和位置坐标。第三个存储了顶点到各边的距离,我们使用noperspective限定符定义它。
noperspective out vec3 GEdgeDistance;
noperspective限定符用来指明变量使用线性插值,而不是默认的透视校正插值。前面提到,我们是在屏幕空间进行计算,所以不应该使用透视校正插值。
在main函数中,我们将顶点的位置坐标使用视口矩阵从剪切空间变换到屏幕空间。(需要注意,由于剪切空间坐标是齐次坐标,我们需要将坐标除以w成分。)
接着,我们使用余弦定理计算出ha,hb和hc。
得到三个高后,我们首先设置第一个顶点的GEdgeDistance,保持变量GNormal,GPosition和gl_Position不变,然后调用EmitVertex函数输出顶点。对于三角形的其它两个顶点进行类似处理,然后调用EndPrimitive函数结束图元定义。
在片段着色器,我们对着色模型进行计算,并将计算结果存储在变量color中。在这一阶段,变量GEdgeDistance包含了片段到三角形各边的距离。我们将三个分量中最小的那个存储在变量d中。最后,我们使用smoothstep函数计算线框和着色模型计算出的颜色的混合比例。
float mixVal = smoothstep( Line.Width – 1, Line.Width + 1, d );
如果片段到三角形各边的最小距离小于Line.Width-1,那么smoothstep函数返回0,如果大于Line.Width,那么smoothstep函数返回1。如果在它们之间,返回一个平滑的过渡值。对于位于线框上的片段,我们得到0值,在线框外的片段,我们得到1值。在线框周围2像素返回内,我们得到一个在0到1之间的过渡值。我们使用得到的值对着色模型计算出的颜色和线框颜色进行混合。
最后,片段颜色使用mixVal作为混合参数混合得到。
其它
这一技术可以产生非常不错的效果,同时只有很少的缺点。它没有改变图元的几何结构,只是利用图元信息计算出附加的信息来完成线框的绘制。
这里使用的着色器代码可以不经修改地用在其它OpenGL程序上。它可以很方便地用来调试网格渲染的问题。
还可以通过两遍处理实现这一效果,第一遍渲染模型,第二遍处理渲进行多边形偏移(通过调用glPolygonOffset函数)后渲染线框。但这一技术需要找得到合适的多边形偏移值。关于这一技术,可以参考Real Time Rendering, third edition, by T Akenine-Moller, E Haines, and N Hoffman, AK Peters, 2008 第11.4.2章节。