1 理解 Stencil Buffer
引例
stencil 是印刷工业中的版面模子,模子上抠出需要的图案,然后将模子盖在要被印刷的材质上,对洞涂或喷绘颜色。
如果将屏幕上所有像素想象成一串连续的 0 组成的矩形,那么 stencil buffer 的作用就是将某些 0 变为 1,2,3 … 255,在每个 pass 中可以决定只渲染某个特定 stencil 值的像素并抛弃对其他非该值像素的操作,就像一块模板一样扣住了所有像素,并只对当前 stencil 值的洞洞进行喷绘。

- 左图为颜色缓冲区中的一张图,在模板缓冲区中我们会给这张图的每一个片元分配一个 0-255 的数字(8 位,默认为 0)
- 中、右图可以看到,我们修改了一些 0 为 1,通过自定义的一些准则,如输出模板缓冲区中 1 对应的片元的颜色;0 的不输出,最后通过模板测试的结果就如右图所示。
模板测试效果举例
- ①传送门效果:可以看到左边传送门内的景象正是右侧的场景

- ②Minions 讲解的一些效果,例如 3D 卡牌效果、侦探镜效果等
-

③每个正方体面显示不同场景(每个面作为蒙版来显示场景)
《笼中窥梦》游戏场景:

对于上述的例子总结一下,这些效果基本可以归结为三层组成
以②中的图 4 传送门为例子,三层分别对应:门外场景、门内场景、门
也就是说可以理解为:包括两层物体/场景、和一层遮罩
2 什么是模板测试
从渲染管线理解

在像素处理阶段的合并阶段(Merger)进行各种测试:
合并阶段是可以配置但不可编程的(对应图中为黄色背景)

流程:
像素所有权测试→裁剪测试→透明度测试→模板测试→深度测试→透明度混合
PixelOwnershipTest(像素所有权测试):
简单来说就是控制当前屏幕像素的使用权限
举例:比如 unity 引擎中仅渲染 scene 和 game 窗口,即只对 scene 和 game 窗口部分的像素具有使用权限
ScissorTest(裁剪测试):
在渲染窗口再定义要渲染哪一部分,默认全部渲染,可以自己控制。
和裁剪空间一起理解,也就是只渲染能看到的部分
举例:只渲染窗口的左下角部分
AlphaTest(透明度测试)
提前设置一个透明度阈值
只能实现不透明效果和全透明效果
举例:设置透明度 a 为 0.5,如果片元大于这个值就通过测试,如果小于 0.5 就剔除掉
StencilTest(模板测试)
DepthTest(深度测试)
Blending(透明度混合)
可以实现半透明效果
完成接下来的其他一系列操作后,我们会将合格的片元/像素输出到帧缓冲区(FrameBuffer),最后渲染到屏幕上。
从逻辑上理解

理解:
referenceValue:当前模板缓冲片元的参考值(0~255)
&readMask:与读掩码做一个“与”操作
stencilBufferValue:模板缓冲区里的值,初始为 0
中间comparisonFunction,就是做一个比较
结果:
如果通过,这个片元就进入下一个阶段
未通过/抛弃,停止并且不会进入下一个阶段,也就是说不会进入颜色缓冲区
总结:就是通过一定条件来判断这个片元/片元属性执行保留还是抛弃的操作
从书面概念上理解
模板缓冲区 Stencil buffer
- 模板缓冲区与颜色缓冲区和深度缓冲区类似,模板缓冲区可以为屏幕上的每一个像素点保存一个无符号整数值(通常为 8 位 int,0-255)。
- 这个值的意义根据程序的具体应用而定。
模板测试
- 渲染过程中,可以用这个值与预先设定好的参考值作(ReferenceValue)比较,根据结果来决定是否更新相应的像素点的颜色值。
- 这个比较的过程就称为模板测试。
- 模板测试在透明度测试之后,深度测试之前。
- 如果模板测试通过,相应的像素点更新,否则不更新。
3 使用方法
语法表示/结构解释
[[unity3d.com)](https://docs.unity3d.com/cn/2022.3/Manual/SL-Stencil.html|ShaderLab 命令:模板 - Unity 手册 (unity3d.com)]]

- Ref:当前片元的参考值(0-255)referenceValue
- ReadMask:读掩码
- WriteMask:写掩码
- Comp:比较操作函数
- Pass:测试通过,之后进行操作(StencilOperation,后边有详细讲解)
- Fail:测试未通过,也会进行一个操作
- ZFail: 模板测试通过,深度测试未通过,也可以进行一个操作
默认值如下:
注:Unity 中模板缓冲区默认是 0, 即 Ref 0
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Stencil { Ref 0 Comp Equal Pass keep Fail keep ZFail IncrWrap }
|
ComparisonFunction
我们可以根据需求配置

StencilOperation 更新值
有不同的更新操作,根据自己的需求进行配置

4 总结
最重要 (用来比较的)两个值:
当前模板缓冲区值(StencilBufferValue)、模板参考值(ReferenceValue)
模板测试主要就是对这两个值进行特定的比较操作,例如 Never、Always、Equal 等,具体参考上文的表格
模板测试后要对模板缓冲区的值进行更新操作,例如 Keep,Replace 等,具体参考上文表格
模板测试之后可以根据结果对模板缓冲区做不同的更新操作,例如模板测试成功操作 Pass、模板测试失败操作 Fail、深度测试失败操作 ZFail、还有正对正面和背面精确更新操作 Passback,Passfront,Failback 等…
属性中使用一个内置的枚举,这样就可以在外边自己选择可配置的属性了

5 应用

[3条消息) Unity Shader: 理解Stencil buffer并将它用于一些实战案例(描边,多边形填充,反射区域限定,阴影体shadow volume阴影渲染)_liu_if_else的博客-CSDN博客 Unity Shader: 理解Stencil buffer并将它用于一些实战案例(描边,多边形填充,反射区域限定,阴影体shadow volume阴影渲染)_liu_if_else的博客-CSDN博客]]
描边
[[05 描边#2 模板测试描边]]
卡牌效果
ID 都为 0 时,即 unity 默认的显示效果:

Mask 和 Texture 的 ID 都设置为 1:


思路:蒙版 Ref[_ID]
设置为 1,Texture 的 Ref[_ID]
也设置为 1,这样可以显示与蒙版重叠的部分,其余部分都剔除。

以下为案例 shader,重点在于 Stencil 部分的设置,其余部分就是按正常 shader 来写,对于 3d 模型计算一下光照
①蒙版 Mask 的 shader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| Shader "Unlit/StencilMask" { Properties { _ID ("Mask ID", Int) = 1 } SubShader { Tags { "RenderType"="Opaque" "Queue" = "Geometry+1" } ColorMask 0 Zwrite off Stencil { Ref[_ID] Comp always Pass replace } LOD 100
Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag
#include "UnityCG.cginc"
struct appdata { float4 vertex : POSITION; };
struct v2f { float4 pos : SV_POSITION; };
v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); return o; }
fixed4 frag (v2f i) : SV_Target { return float4(1,1,1,1); } ENDCG } } }
|
②被遮挡物体的 shader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| Shader "Unlit/TextureMasked" { Properties { _MainTex("BaseColor",2D) = "white"{} _ID ("Mask ID", Int) = 1 } SubShader { Tags { "RenderType"="Opaque" "Queue" = "Geometry+2" } Stencil { Ref[_ID] Comp equal } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; o.uv = v.uv; o.pos = UnityObjectToClipPos (v.vertex); return o; } fixed4 frag (v2f i) : SV_Target { float4 BaseColor = tex2D(_MainTex, i.uv); return BaseColor; } ENDCG } } FallBack "Diffuse" }
|
③完善卡牌效果
分离前后 Mask:
MainTex 的 A 通道是一个 Mask,将正反面分离以定制正反面的不同效果,比如单独加 Fresnel


盒子不同面显示不同场景

- 和卡牌效果类似,一个用蒙版遮罩的物体,盒子每个面使用一个蒙版遮罩
- 同样利用默认的值为 0 来做,只是面多了,蒙版和里边显示的物体也多了,ID 依次为 1、2、3、4
- 总结:一个蒙版对应一个物体,他们使用相同的 ID,出来的效果就是:每个面显示的盒子内部物体不同
多边形填充
通过 stencil 值对几何体交叉区域进行判定与渲染。
第一个 pass 渲染一个几何体,不论任何情况都通过测试并对它所覆盖的像素区域 stencil 值加 1,后三个 pass 分别只对 stencil 值为 2,3,4 的区域进行渲染。

fold1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
| Shader "Unlit/PolygonsBeta" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 100
CGINCLUDE #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; };
struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; };
sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; } ENDCG
Pass { Stencil { Ref 0 Comp always Pass IncrWrap Fail keep ZFail IncrWrap }
CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fog
fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); UNITY_APPLY_FOG(i.fogCoord, col); return fixed4(0,0,0,0); } ENDCG } Pass { Stencil { Ref 2 Comp Equal Pass keep Fail keep ZFail keep } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fog #include "UnityCG.cginc"
fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); UNITY_APPLY_FOG(i.fogCoord, col); return fixed4(0.2,0.2,0.2,1); } ENDCG }
Pass { Stencil { Ref 3 Comp equal Pass keep Fail keep ZFail keep } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fog #include "UnityCG.cginc"
fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); UNITY_APPLY_FOG(i.fogCoord, col); return fixed4(0.6,0.6,0.6,1); } ENDCG }
Pass { Stencil { Ref 4 Comp equal Pass keep Fail keep ZFail keep } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fog #include "UnityCG.cginc"
fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); UNITY_APPLY_FOG(i.fogCoord, col); return fixed4(1,1,1,1); } ENDCG } } }
|
反射区域限定
此用法主要是辅助一个反射 shader,可以比较简单的模拟出一个镜面效果。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
| Shader "Unlit/TwoPassReflection" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" "Queue"="Geometry" } LOD 100
Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog #include "UnityCG.cginc"
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; };
struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; };
sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; } fixed4 frag (v2f i) : SV_Target { // sample the texture fixed4 col = tex2D(_MainTex, i.uv); // apply fog UNITY_APPLY_FOG(i.fogCoord, col); return col; } ENDCG }
Pass { Stencil { Ref 1 //0-255 Comp Equal //default:always Pass keep //default:keep Fail keep //default:keep ZFail keep //default:keep } ZTest Always CGPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog #include "UnityCG.cginc"
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float4 normal: NORMAL; };
struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; };
sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; v.vertex.xyz=reflect(v.vertex.xyz,float3(-1.0f,0.0f,0.0f)); v.vertex.xyz=reflect(v.vertex.xyz,float3(0.0f,1.0f,0.0f)); v.vertex.x+=1.5f; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; } fixed4 frag (v2f i) : SV_Target { // sample the texture fixed4 col = tex2D(_MainTex, i.uv); // apply fog UNITY_APPLY_FOG(i.fogCoord, col); return col; } ENDCG } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| Shader "Unlit/Mirror" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" "Queue"="Geometry-1" } LOD 100
Stencil { Ref 0 //0-255 Comp always //default:always Pass IncrSat //default:keep Fail keep //default:keep ZFail keep //default:keep }
Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog #include "UnityCG.cginc"
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; };
struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; };
sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; } fixed4 frag (v2f i) : SV_Target { // sample the texture fixed4 col = tex2D(_MainTex, i.uv); // apply fog UNITY_APPLY_FOG(i.fogCoord, col); return fixed4(0.2f,0.2f,0.2f,1.0f); } ENDCG } } }
|
说明
在 TwoPassReflection. shader 中,第一个 pass 正常渲染模型,第二个 pass 对顶点进行了一个简单的反射,并将 ZTest 设为 always,然后将一个 quad 放入本体和倒影之间,它的效果是这样的:

图 5:使用 TwoPassReflection. shader,无 mirror. shader
倒影超出了想要的范围。解决这一问题,在 quad 上使用 mirror. shader 将 quad 覆盖的像素 stencil 值改为 1,并在 TwoPassReflection 第二个 pass 中约定只在 stencil 值为 1 的区域中渲染。
效果

图 6:quad 使用 mirror. shader
这里有个前提是 mirror 必须在倒影之前渲染以先将反射区域的 stencil 值标记好。
阴影体 shadow volume 阴影渲染
说明
shadow volume 阴影体算法是将‘遮光体’遮挡光源后产生的阴影实例为一个几何体,对在该阴影几何体的渲染过程中找出应该渲染阴影效果的像素。

图 7:圆柱阴影体
检测手段有几种,本案例 shader 采用的是 Depth Fail,也叫 Carmack’s reverse 方法的思路。它的思想与步骤如下:
1,在一般物体渲染后,渲染阴影体,第一个 pass cull front,渲染内侧,在 stencil 测试阶段如果发现深度测试失败,说明该像素在阴影体内部表面或阴影体外部表面与视角之间有发生遮挡,将该像素 stencil 值加 1。
2,第二个 pass cull back,渲染外侧,如果有深度测试失败,则说明该像素在阴影体外部表面与视角之间有发生遮挡,将该像素 stencil 值减 1。
3,经过两个 pass 的 stencil 操作,只有在阴影体内部的物体且它遮挡住阴影体内部表面的部分的 stencil 值为 1。对阴影体内 stencil 值为 1 的像素进行渲染。
本文中的 shader 只为展示 stencil buffer 在此技术中的角色,缺乏正确的阴影体网格或它的动态生成手段,粗暴的用 Unity 默认几何体中的圆柱体模拟一个阴影体,并且算法中也没有考虑被阴影覆盖的物体自身的阴影体的问题以及其他细节问题。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
| Shader "Unlit/SV_DepthFailBeta" { Properties { } SubShader { Tags { "RenderType"="Opaque" "Queue"="Geometry+1"} //在渲染所有阴影体内物体后再渲染阴影体 LOD 100 CGINCLUDE //三个pass内着色器内容相同 #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; };
struct v2f { UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; };
v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); UNITY_TRANSFER_FOG(o,o.vertex); return o; } fixed4 frag (v2f i) : SV_Target { // apply fog UNITY_APPLY_FOG(i.fogCoord, col); return fixed4(0.3,0.3,0.3,1); //影子颜色 } ENDCG
Pass { Cull Front //阴影体内侧像素Z测试失败,stencil值加1 Stencil { Ref 0 //0-255 Comp always //default:always Pass keep //default:keep Fail keep //default:keep ZFail IncrWrap //default:keep }
ColorMask 0 //关闭color buffer写入 CGPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog ENDCG } Pass { Cull Back //阴影体外侧像素Z测试失败,stencil值减1 Stencil { Ref 0 //0-255 Comp always //default:always Pass keep //default:keep Fail keep //default:keep ZFail DecrWrap //default:keep } ColorMask 0 //关闭color buffer写入 CGPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog ENDCG }
Pass { Cull Back //经过前两个pass,stencil值为1的值为在此阴影体内被阴影覆盖的像素 Stencil { Ref 1 //0-255 Comp equal //default:always Pass keep //default:keep Fail keep //default:keep ZFail keep //default:keep } CGPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog ENDCG } } }
|
效果

图 8:使用 SV_DepthFailBeta. shader