在片元着色器结束,到帧缓存输出之间,有一个逐片元操作,有很多种测试,如图
Pixel Ownership Test :控制像素的使用权限,只能在game窗口和scene窗口显示,其他位置都会被剔除掉
Scissor Test :裁剪测试,在game和scene窗口内,自己可以再次定义渲染范围
Alpha Test :透明度测试
Stencil Test :模板测试
Depth Tset :深度测试
Blending:透明度混合
Stencil Test在Alpha Test之后Depth Test之前,模板测试和深度测试一样,它也可能会丢弃片元。被保留的片元会进入深度测试。模板测试是根据模板缓冲(Stencil Buffer)中的值来判断的,模板缓冲中为每个片元(像素)分配一个8位的数值,默认为0,取值范围[0, 255]
我们可以修改模板缓冲中的一些值为1,然后只渲染值为1对应的像素,就得到右图的结果
语法格式
Stencil
{
Ref referenceValue // 参考值 默认值为 0
ReadMask readMask // 读掩码
WriteMask writeMask // 写掩码
Comp comparisonFunction // 比较的方法 默认为 Always
Pass stencilOperation // 通过模板测试时的处理方法 默认为 keep
Fail stencilOperation // 没有通过模板测试时的处理方法 默认为 keep
ZFail stencilOperation // 通过模板测试却没有通过深度测试时的处理方法 默认为 keep
}
从逻辑上理解
//referenceValue是当前片元的参考值,stencilBufferValue是模板缓冲区的值
if(referenceValue & readMask comparisonFunction stencilBufferValue & readMask)
通过像素
else
抛弃像素
指定上面的比较方法 comparisonFunction
比较方法 | 描述 |
---|---|
Greater | 相当于“>”操作,即仅当左边>右边,模板测试通过,渲染像素 |
GEqual | 相当于“>=”操作,即仅当左边>=右边,模板测试通过,渲染像素 |
Less | 相当于“<”操作,即仅当左边<右边,模板测试通过,渲染像素 |
LEqual | 相当于“<=”操作,即仅当左边<=右边,模板测试通过,渲染像素 |
Equal | 相当于“=”操作,即仅当左边=右边,模板测试通过,渲染像素 |
NotEqual | 相当于“!=”操作,即仅当左边!=右边,模板测试通过,渲染像素 |
Always | 不管公式两边为何值,模板测试总是通过,渲染像素 |
Never | 不敢公式两边为何值,模板测试总是失败 ,像素被抛弃 |
指定上面的更新操作 stencilOperation
更新值 | 描述 |
---|---|
Keep | 保留当前缓冲中的内容,即stencilBufferValue不变 |
Zero | 将0写入缓冲,即stencilBufferValue值变为0 |
Replace | 将参考值写入缓冲,即将referenceValue赋值给stencilBufferValue |
IncrSat | stencilBufferValue加1,如果stencilBufferValue超过255了,那么保留为255,即不大于255 |
DecrSat | stencilBufferValue减1,如果stencilBufferValue超过为0,那么保留为0,即不小于0 |
Invert | 将当前模板缓冲值(stencilBufferValue)按位取反 |
IncrWrap | 当前缓冲的值加1,如果缓冲值超过255了,那么变成0,然后继续自增 |
DecrWrap | 当前缓冲的值减1,如果缓冲值已经为0,那么变成255,然后继续自减 |
这里由两部分组成,一个是前面透明的Mask(蒙板),另一个是Mask后面的所有Object。为了先渲染Mask,我们需要控制Mask的Render Queue小于Object的Render Queue,然后在Mask的Shader中修改模板缓冲中的值,当渲染Object时,只有它的参考值Ref和模板缓冲值相同才能通过模板测试
Shader "MyCustom/StencilMask"
{
Properties
{
_StencilRef ("StencilRef", Int) = 0
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry-1"}
ZWrite Off
ColorMask 0
Stencil
{
Ref [_StencilRef] // [0, 255]
Comp always // 模板测试总是通过
Pass replace // Ref值替换掉stencil buffer中的值
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return 0;
}
ENDCG
}
}
}
注意这里需要关闭深度写入,不能让Mask影响后面物体的渲染,使用ColorMask 0,不写入颜色,将Mask变成透明的
这里的渲染队列设置为Geometry-1(1999),是为了先渲染Mask,因为物体默认的渲染队列是Geometry(2000),在面板上修改参考值Ref为1,这样Mask覆盖的区域模板测试总是通过,并且会修改模板值为1,其他部分还是0,如图
Shader "MyCustom/StencilObject"
{
Properties
{
_Color("Color",Color) = (1, 1, 1, 1)
_StencilRef("StencilRef", Int) = 0
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry"}
Stencil
{
Ref [_StencilRef] // [0, 255]
Comp Equal // Ref值和stencil buff值相等时通过模板测试
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
float4 _Color;
fixed4 frag (v2f i) : SV_Target
{
return _Color;
}
ENDCG
}
}
}
对于Object,修改面板上的Ref为1,这样当模板缓冲区中的值为1(即处于Mask覆盖区域)才能通过模板测试,渲染出来
最终效果
立方体不同面显示不同场景,实现原理和上面的3D卡牌是一样的,每个面都有一个Mask,赋予不同的Ref值,场景中的物体只要Ref值和Mask一样,就能在这个Mask上显示,详情可以参考这个项目,地址
这里要实现透过Mask,看到墙后面的物体,原理和上面一样的,只是中间加了个墙作为遮挡,上面用到的两个Shader也不用修改
Mask
Wall,Wall需要新建一个材质,和Object使用相同的Shader,Ref值改成0
Object
Mask覆盖的区域模板值改为1,其他区域还是0
渲染Wall的时候,在Mask覆盖区域 0 != 1,模板测试未通过,其他区域,0 == 0,模板测试通过
渲染Object时,只有在Mask覆盖区域 1 == 1,模板测试通过
使用模板测试实现的描边,当物体有重叠时,在物体相连处并没有描边 ,它不是基于模型做的描边
Shader "MyCustom/StencilOutline"
{
Properties
{
_StencilRef ("StencilRef", Int) = 0
[Enum(UnityEngine.Rendering.CompareFunction)]_StencilComp ("StencilComp", Int) = 8
[Enum(UnityEngine.Rendering.StencilOp)]_StencilOp ("StencilOp", Int) = 2
_EdgeColor("Edge Color", Color) = (1, 1, 1, 1)
_EdgeWidth("Edge Width", Float) = 0.1
}
SubShader
{
Tags { "RenderType"="Opaque"}
Stencil
{
Ref [_StencilRef]
Comp [_StencilComp]
Pass [_StencilOp]
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
float3 worldNormal : TEXCOORD0;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 worldNormal = normalize(i.worldNormal);
float3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
float lambert = max(dot(worldNormal, worldLight), 0.0);
float3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
return float4(lambert + ambient, 1);
}
ENDCG
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
float _EdgeWidth;
float4 _EdgeColor;
v2f vert (appdata v)
{
v2f o;
o.vertex = v.vertex + normalize(float4(v.normal, 0)) * _EdgeWidth;
o.vertex = UnityObjectToClipPos(o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return _EdgeColor;
}
ENDCG
}
}
}
这里比较方法和更新操作使用了枚举,比较函数Equal,比较操作是模板值加1,Stencil在Pass外面,两个Pass都会执行模板测试
第一个Pass使用兰伯特渲染物体,模板缓冲中默认值是0,与0相等,通过模板测试,并将物体覆盖区域的模板值加1
第二个Pass顶点沿法线方向外扩,描边的部分Ref值与0相等,通过模板测试,描边内的物体模板值已经变成1了,与0不相等,没通过模板测试,所以只渲染描边的部分
红球的设置,先渲染红球,并把红球覆盖区域的模板值改为1
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry" }
Stencil
{
Ref 1 // 0-255
Comp always // 总是通过测试
Pass replace // 用1替换stencil buffer的值
}
绿球的设置,后渲染绿球,只有在相交的部分,Ref值和模板值相同,模板测试通过
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry+1"}
Stencil
{
Ref 1 // 0-255
Comp Equal // Ref和stencil buffer值相等时通过
Pass keep // stencil 和 z test都通过,保持模板值不变
}
【技术美术百人计划】图形 3.1 深度与模板测试 传送门效果示例