
因为之前做的一个效果用到了以前没怎么用过的模板测试(stencil),所以系统性的了解了一下,并以此文留档记录。
至少单从使用上来说,模板测试这个东西并不复杂,完整的代码也就是有六七个可配置项,常用的又只有两三个。所以轻松愉快的过掉这节吧- -
先看一下模板测试能用来干什么- -本人身为一个原神玩家,当时渊下宫地图刚出的时候,那个入口的门的效果让我觉得很有意思。

1-1
这个效果是,从那个红色标识的门洞范围内能看到对面山壁的入口,但从其他地方看(比如右图的青色区域)就只是山壁,而且在角色穿过门洞前,从旁边绕过去也是无法穿过山壁的。
虽然不清楚mhy具体怎么实现的这个效果,但这种视觉效果利用模板测试是可以实现的。而入口能不能穿过则应该是以“角色从正面穿过门洞”为判断条件,在脚本里控制了山壁碰撞体的启用。
我在引擎里还原了个最基本效果如下,本质上和原神这个效果是一码事。

1-2
看网格就能发现,球的模型其实还完整的存在于那里,只不过在门框范围外的部分没有被渲染。而在这个例子中,决定是否渲染的便是模板值(默认是0)。
对于屏幕上的每个像素,其保存的数据除了用于直接让我们看到效果的RGBA色值外,其他常见的还有深度值和模板值等。前者用于判断屏幕上一点距离相机的远近来处理遮挡剔除等,后者用于实现这种“部分渲染”的特殊效果。

1-3
观察上图还可以发现,这个区域实际上利用了一个方片模型来实现。实际上要形容的话,这种效果的模板测试其实就是利用模型的剪影(外轮廓)做了一个图层蒙版区域。
从渲染流程上来说,调整材质的Render Queue保证先渲染这个方片(写入指定模板值),然后渲染球。在渲染球的时候,如果模板值是0则不渲染,否则渲染,就是这么个简单的条件分支逻辑。
这里先放上上面的剪影片和球的shader代码,关于Stencil的配置看不懂可以继续往下- -
剪影片
//URP
Shader "StencilPlane"
{
Properties
{
_Ref ("模板值", int) = 1
}
SubShader
{
Tags
{
"RenderPipeline" = "UniversalPipeline"
"RenderType" = "Transparent"
"Queue" = "Background"
}
Pass
{
Stencil
{
Ref [_Ref]
Pass Replace
}
Cull off
ZWrite off
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct a2v
{
half4 posOS : POSITION;
};
struct v2f
{
half4 posCS : SV_POSITION;
};
v2f vert (a2v i)
{
v2f o;
o.posCS = TransformObjectToHClip(i.posOS.xyz);
return o;
}
half4 frag (v2f i) : SV_Target
{
return 0;
}
ENDHLSL
}
}
FallBack "Hidden/Universal Render Pipeline/FallbackError"
} 球
//URP
Shader "StencilLambert"
{
Properties
{
_Ref ("模板值", int) = 1
}
SubShader
{
Tags
{
"RenderPipeline" = "UniversalPipeline"
"RenderType" = "Opaque"
"Queue" = "Geometry"
}
Pass
{
Stencil
{
Ref [_Ref]
Comp Equal
Pass Replace
}
Cull back
ZWrite on
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
struct a2v
{
half4 posOS : POSITION;
half3 nDirOS : NORMAL;
};
struct v2f
{
half4 posCS : SV_POSITION;
half3 nDirWS : TEXCOORD0;
};
v2f vert (a2v i)
{
v2f o;
//坐标
o.posCS = TransformObjectToHClip(i.posOS.xyz);
//向量
o.nDirWS = TransformObjectToWorldNormal(i.nDirOS);
return o;
}
half4 frag (v2f i) : SV_Target
{
half3 nDirWS = normalize(i.nDirWS);
half3 lDirWS = normalize(GetMainLight().direction);
half lambert = saturate(dot(nDirWS, lDirWS));
return half4(lambert.rrr, 1.0);
}
ENDHLSL
}
}
FallBack "Hidden/Universal Render Pipeline/FallbackError"
} U3Dshader中的模板测试实际上就是下面这么一个代码块,其位置放在shader的Pass或SubShader代码块中。
Stencil
{
Ref 参与比较的模板值(0-255)
Comp 当前模板值与Ref的大小关系
Pass 如果满足Comp条件要执行的操作
Fail 如果不满足Comp条件要执行的操作
ZFail 如果不满足Comp并且没通过深度测试要执行的操作
ReadMask //一般用不上
WriteMask //一般用不上
} 其中的各项只能填一些整数或现成的枚举值。也就是说,模板测试可配置但不可编程(你无法自定义判断模板值的方法以及分支中的方法)。其中,内置的各种枚举值与其含义对照如下。
Comp(实际上深度测试ZTest的可配置枚举值也是下面这些)
Greater 当前模板值大于Ref
GEqual 大于等于
Less 小于
LEqual 小于等于
Equal 等于
NotEqual 不等于
Always 相当于布尔值true
Never 相当于布尔值false
Pass / Fail / ZFail
Keep 保持当前模板值不变
Zero 改变当前模板值为0
Replace 用Ref替换当前模板值
IncrSat 当前模板值+1后写入,如果超过255则写入255(截断)
IncrWrap 当前模板值+1后写入,如果超过255则写入0(循环)
DecrSat 当前模板值-1后写入,如果小于0则写入0(截断)
DecrWarp 当前模板值-1后写入,如果小于0则写入255(循环)
Invert 当前模板值二进制按位取反后写入