3.1 模板测试和深度测试 - 知乎
一篇写的很详细的文章。
模板测试发生在片元着色器之后,是一种用于决定片元是否可见的手段,可以将模板测试理解为特殊的深度测试,深度测试中的深度值是由物体本身的位置决定,但模板测试中的 "深度值" 是由我们自己定义的,所以我们在利用模板测试实现某些效果时,和深度测试的逻辑很类似。同样也存在一个和深度缓冲区一样的模板缓冲区。
stencil
{
Ref 1
ReadMask 1
writeMask 1
Comp Always
Pass Keep
Fail Keep
ZFail Keep
}
1.1 Ref: 当前片元的模板参考值,默认值0。
1.2 ReadMask: 读取时与Ref值进行按位&与操作,默认值255。
1.3 WriteMask:写入时与Ref值进行按位&与操作,默认值255。
1.4 Comp:与ZTest类似,比较Ref值与模板缓冲区内的值,默认值always。
1.5 Pass:与ZWrite类似,对于通过深度测试与模板测试的值如何处理,默认值keep。
1.6 Fail:当模板测试未能通过时,应该怎么办,默认值keep。
1.7 ZFail: 当模板测试通过,而深度测试未通过怎么办,默认值keep。
其中Comp使用的值我们称之为比较操作值,其包含的值有
值 | Rendering.CompareFunction 枚举中的对应整数值 | 功能 |
---|---|---|
Never |
1 | 从不渲染像素。 |
Less |
2 | 在参考值小于模板缓冲区中的当前值时渲染像素。 |
Equal |
3 | 在参考值等于模板缓冲区中的当前值时渲染像素。 |
LEqual |
4 | 在参考值小于或等于模板缓冲区中的当前值时渲染像素。 |
Greater |
5 | 在参考值大于模板缓冲区中的当前值时渲染像素。 |
NotEqual |
6 | 在参考值与模板缓冲区中的当前值不同时渲染像素。 |
GEqual |
7 | 在参考值大于或等于模板缓冲区中的当前值时渲染像素。 |
Always |
8 | 始终渲染像素。 |
后三个Pass、Fail、ZFail使用的值称之为模板操作值,其包含的值有:
值 | Rendering.StencilOp 枚举中的对应整数值 | 功能 |
---|---|---|
Keep |
0 | 保持模板缓冲区的当前内容。 |
Zero |
1 | 将 0 写入模板缓冲区。 |
Replace |
2 | 将参考值写入缓冲区。 |
IncrSat |
3 | 递增缓冲区中的当前值。如果该值已经是 255,则保持为 255。 |
DecrSat |
4 | 递减缓冲区中的当前值。如果该值已经是 0,则保持为 0。 |
Invert |
5 | 将缓冲区中当前值的所有位求反。 |
IncrWrap |
7 | 递增缓冲区中的当前值。如果该值已经是 255,则变为 0。 |
DecrWrap |
8 | 递减缓冲区中的当前值。如果该值已经是 0,则变为 255。 |
Rendering.StencilOp 为C#中的写法。
可以看到,模板测试和深度测试相比自由度更高,可自定义的配置也更多。
这里面涉及到如何处理好深度和模板的关系。而且还有一个小坑。就是真正写入模板的并不是外边框本身,而是一个和外边框位置及其接近的Quad。
如图所示:
Quad写入模板同时关闭深度,外边框正常渲染。
Quad的Shader:
Shader "Custom/Test1"
{
Properties
{
}
SubShader
{
Pass
{
stencil
{
Ref 1
//让边框的模板值写入缓冲区
Comp Always
Pass Replace
}
ColorMask 0
//防止遮挡物体
Zwrite off
}
}
}
墙面Shader:
Shader "Custom/Test2"
{
Properties
{
//用于控制纹理的整体颜色表现
_Color("Color",Color)=(1,1,1,1)
_MainTex("基础纹理",2D)="white"{}
_Specular("Specular",Color)=(1,1,1,1)
_Gloss("Gloss",Range(0,256))=20
}
SubShader
{
stencil
{
Ref 1
Comp Equal
}
Tags
{
"Queue"="Overlay"
}
//Pass常规渲染,不用关注
Pass
{
Tags
{
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "UnityLightingCommon.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Specular;
fixed _Gloss;
struct a2v
{
float4 vertex:POSITION;
float3 normal:NORMAL;
float4 texcoord:TEXCOORD0;
};
struct v2f
{
float4 pos:SV_POSITION;
float3 worldNormal :TEXCOORD0;
float3 worldPos:TEXCOORD1;
float2 uv:TEXCOORD2;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(v2f i):SV_Target
{
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
//纹理采样
fixed3 texResult = tex2D(_MainTex, i.uv) * _Color;
fixed3 diffuse = _LightColor0 * texResult
* saturate(dot(worldLight, worldNormal));
fixed3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
fixed3 halfDir = normalize(viewDir + worldLight);
fixed3 specular = _LightColor0.rgb * _Specular
* pow(saturate(dot(worldNormal, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1);
}
ENDCG
}
}
}
还有一种是笼中窥梦游戏中使用的,用一个立方体做模板测试,每一个面显示不同的内容,那个做法一样,每个不同的面给一个不同的模板值即可,对应内部的物体模板和对应面保持一致即可。
关于深度测试的语法什么的就不举例了,这里主要探讨一下深度测试中一些问题。
首先便是深度缓冲区中存储的并不是线性深度值,原因总结就是投影矩阵的问题,当然如果你想要线性的深度值,完全可以变回到视图空间。具体更多请参考篇首链接。
然后便是深度冲突的问题。两个三角面过于接近,深度缓冲区精度不够导致两个三角面互相争夺深度写入,从而出现的闪烁问题。,三角面离得越远,越有可能发生,原因就是深度缓冲区在z值非常大的时候没有很高的精度。
解决深度冲突的方法有很多,比如尽量避免两个物体过于接近等等,还有一种便是深度偏移命令。
深度偏移语法:
Offset -1,1
每一个片元的深度值都会增加如下所示的偏移量:offset = (m * factor) + (r * units)。factor为offset的第一个数值,units为offset的第二个数值,m是多边形的深度的斜率,多边形越接近与近裁剪面平行,m就越接近0,r是一个常量。
也就是说深度偏移命令是通过给两个相似的深度加上不同的偏移解决的深度偏移。而offset越小,片元的深度便越靠近摄像机,越大便越远离摄像机。
那么根据上面的例子,我们对于砖块模型来说就应该这样写:
Offset -1,-1
对于方块模型应该这样写:
Offset 1,1
结果:
当然深度偏移并不是万能的,我们上面提到过深度值是非线性的,而且越远占比越小,那么近处的物体加上偏移和远处的物体加上偏移带来的影响也是不同的。同时如果我们一次性加的偏移过多,会出现砖块模型遮住砖块之前的模型,尽管这两个模型并没有出现深度冲突。
参考文章:Shader Depth Offset [Polygon Offset]_xak的博客-CSDN博客_depth offset