Cg 编程/Unity/物体轮廓
本教程介绍一种顶点变换,它将每个顶点沿着其表面法线向量移动,以创建物体的更大轮廓。虽然这种顶点变换非常简单,但要创建合理的轮廓渲染需要使用模板缓冲区,本文也会对此进行讨论。
创建物体周围轮廓的一种方法是通过沿着其表面法线向量移动顶点的来扩大物体。给定物体坐标中的位置和法线向量,这在顶点着色器中相当简单
uniform float _Thickness;
float4 vert(float4 vertex : POSITION, float3 normal : NORMAL) : SV_POSITION {
return mul(UNITY_MATRIX_MVP, float4(normal, 0.0) * _Thickness + vertex);
}
这个顶点着色器将表面法线向量 normal
转换为 float4
向量,然后将其乘以用户指定的统一 _Thickness
(以允许调整轮廓的厚度),并将其添加到 vertex
中顶点的坐标。所有这些操作都在物体坐标中进行;因此,我们必须通过乘以 UNITY_MATRIX_MVP
转换为裁剪坐标,然后返回坐标。
关于这种特定顶点变换,没有太多可说的 - 除了它最适合光滑表面。具有硬边(即表面法线向量不连续)的表面效果不佳。有时使用 Cull Off
来渲染正面和背面会有所帮助,但总的来说,这种方法在硬边上效果不佳。
为了使轮廓呈现统一的绿色,我们可以使用类似这样的简单片段着色器
float4 frag(void) : COLOR {
return float4(0.0, 1.0, 0.0, 1.0);
}
渲染这种轮廓更具挑战性的部分是避免轮廓物体被轮廓遮挡:由于我们已经扩大了物体来创建轮廓,因此更大的轮廓通常会遮挡我们要轮廓的物体。另一方面,轮廓应该遮挡背景中的其他物体,并且应该被前景中的物体遮挡,即它应该像任何其他不透明物体一样,除了它在我们要轮廓的物体前面时。
如果我们假设我们首先渲染一个要轮廓的物体的普通版本,然后渲染轮廓,我们可以这样描述挑战:轮廓只应该为不被轮廓物体覆盖的像素进行光栅化。这样,挑战听起来像可以用模板测试解决,因为模板测试用于将光栅化限制到帧缓冲区的特定部分;在本例中,限制到不被我们要轮廓的物体覆盖的帧缓冲区部分。
我们的策略是在模板缓冲区中用 1 标记所有被我们要轮廓的物体覆盖的像素。之后,在渲染轮廓时,我们可以使用模板测试来仅在未标记的像素中光栅化轮廓。
为了用 1 在模板缓冲区中标记所有被物体覆盖的像素,我们可以在 Pass
块中使用此 ShaderLab 语法,该块位于 CGPROGRAM
之前
Stencil {
Ref 1
Comp Always
Pass Replace
}
Stencil
关键字指定我们希望激活模板测试,这很必要,因为写入模板缓冲区在技术上是模板测试的一部分。Ref 1
设置模板测试的参考值;在本例中,是我们想要写入模板缓冲区的的值。
Comp Always
指定我们希望与所有片段一起工作,无论模板缓冲区中像素的值是什么。一般来说,Comp
指定对模板缓冲区中像素值的比较。如果比较失败,则丢弃片段,并且不会光栅化像素。Always
指定始终通过的比较,即没有片段被丢弃。
Pass Replace
指定如果比较通过,则应将 Replace
"操作" 应用于模板缓冲区,即应将模板缓冲区中像素的值替换为使用 Ref
指定的参考值。
一个完整的通道来渲染黑色物体,同时用 1 在所有被物体覆盖的像素中标记模板缓冲区,可以看起来像这样
Pass {
Stencil {
Ref 1
Comp Always
Pass Replace
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 vert(float4 vertex : POSITION) : SV_POSITION
{
return mul(UNITY_MATRIX_MVP, vertex);
}
float4 frag(void) : COLOR {
return float4(0.0, 0.0, 0.0, 1.0);
}
ENDCG
}
我们方法的第二部分是仅在模板缓冲区未用 1 标记的地方渲染轮廓。这种模板测试可以这样指定
Stencil {
Ref 1
Comp NotEqual
Pass Keep
}
同样,Ref 1
设置模板测试的参考值,但在此情况下,该值用于比较。
Comp NotEqual
指定对模板缓冲区中像素值的比较,该比较仅在值不等于参考值时才通过。如果比较失败(即,如果模板缓冲区中的值为 1),则丢弃片段,并且不会光栅化像素,这就是我们希望轮廓做到的,这样它就不会遮挡我们要轮廓的物体。
Pass Keep
指定如果比较通过,则应将 Keep
"操作" 应用于模板缓冲区,即我们不更改模板缓冲区,而只是保留模板缓冲区中已有的任何值。(这很重要,因为网格的多个三角形可能会覆盖同一个像素。)
此模板测试应用于上述沿着表面法线向量移动了顶点的轮廓的着色器。
将所有内容放在一起并为 _Thickness
统一变量定义属性块,我们有以下着色器
Shader "OutlinedObject" {
Properties {
_Thickness ("Thickness", Float) = 0.1
}
SubShader {
Pass {
Stencil {
Ref 1
Comp Always
Pass Replace
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 vert(float4 vertex : POSITION) : SV_POSITION
{
return mul(UNITY_MATRIX_MVP, vertex);
}
float4 frag(void) : COLOR {
return float4(0.0, 0.0, 0.0, 1.0);
}
ENDCG
}
Pass {
Cull Off
Stencil {
Ref 1
Comp NotEqual
Pass Keep
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
uniform float _Thickness;
float4 vert(float4 vertex : POSITION, float3 normal : NORMAL) : SV_POSITION {
return mul(UNITY_MATRIX_MVP, vertex + normal * _Thickness);
}
float4 frag(void) : COLOR {
return float4(0.0, 1.0, 0.0, 1.0);
}
ENDCG
}
}
}
此着色器为统一黑色物体渲染不透明的绿色轮廓。但是,您可以轻松地替换顶点和片段着色器以渲染其他颜色的透明轮廓或阴影物体 - 只要您保留模板测试,并且第二个通道中的轮廓大于第一个通道中的物体即可。
如果您仍想了解更多信息
- 关于模板测试如何在 OpenGL 管道中发挥作用,您应该阅读 “每片段操作”部分.
- 关于渲染轮廓的其他方法,您应该阅读 “卡通着色”部分.
- 关于模板测试的规范,您应该阅读 Unity 关于 “ShaderLab:模板” 的文档。