Unity Stencil模板测试:从原理到实战,实现高级遮罩效果

📅 2026/8/12 13:09:19
Unity Stencil模板测试:从原理到实战,实现高级遮罩效果
1. 项目概述理解Stencil模板测试的核心价值在Unity的UI或者3D渲染中遮罩Mask是一个高频使用的功能。无论是制作一个滚动列表、一个圆形头像还是一个复杂的UI特效都离不开它。Unity自带的Mask组件开箱即用非常方便但当你需要实现一些非矩形遮罩、多重遮罩或者更复杂的“遮罩中的遮罩”效果时就常常会感到束手束脚。这时深入GPU渲染管线直接操作Stencil Buffer模板缓冲区就成了必须掌握的技能。很多开发者对Stencil感到畏惧觉得它深奥难懂。其实它的核心逻辑非常直观你可以把它想象成一张与屏幕分辨率相同的“模具纸”。在渲染每个物体前GPU会先拿这个物体像素的位置去对照“模具纸”上相应位置的“编号”。只有编号符合我们预设规则的像素才被允许画到屏幕上。而“编号”的写入、读取和比较规则完全由我们在Shader中通过几行Stencil代码来定义。理解了这一点你就从被Mask组件限制的“使用者”变成了可以自定义任何遮罩规则的“创造者”。本教程将从Unity内置Mask组件的实现原理拆解开始手把手带你理解Stencil的每一个参数。然后我们会彻底抛开Mask组件仅用自定义Shader复现并超越它的功能。最后我们会探索几个实战案例比如实现一个可交互的“镂空”UI、一个动态变化的波纹遮罩甚至是一个简易的镜子效果。无论你是Shader新手还是想深化图形学理解的开发者这篇“保姆级”指南都将让你对Stencil模板测试拥有透彻的掌控力。2. Stencil模板测试机制深度解析要玩转Stencil不能只停留在API调用层面必须理解其背后的渲染管线机制。这是从“会用”到“精通”的关键一步。2.1 Stencil BufferGPU的“模具图层”Stencil Buffer是一块与深度缓冲区Depth Buffer和帧缓冲区Frame Buffer大小一致的特殊内存区域通常为每个像素分配8位1字节的存储空间。这意味着每个像素可以存储一个0-255的整数值。你可以把它理解为一张单通道的、值范围为0-255的灰度图。在渲染管线中模板测试Stencil Test发生在片元着色器Fragment Shader执行之后深度测试Depth Test之前具体顺序可能因平台和设置略有不同但通常在此阶段。它的工作流程是一个经典的“读取-比较-写入”循环读取当GPU准备渲染一个片元即一个像素候选点时会从该片元屏幕坐标对应的Stencil Buffer中读取当前值。比较将这个读取到的值与我们在Shader中定义的“参考值”Reference Value按照指定的比较函数Comparison Function进行计算。裁决如果比较结果为“真”True则该片元通过模板测试可以继续后续的深度测试和颜色混合如果为“假”False则该片元被立即丢弃不再进行任何后续处理。写入无论片元是否通过测试取决于配置我们都可以定义一套“操作”Operation来更新Stencil Buffer中该位置的值为后续物体的渲染做好准备。这个机制的精妙之处在于它允许前一个渲染的物体为后一个渲染的物体“铺设”好一套像素级的可见性规则。2.2 Shader中的Stencil语法与参数全解在Unity ShaderLab语法中我们在SubShader或Pass块内使用Stencil块来定义模板测试行为。其完整结构如下Stencil { Ref [_StencilRef] // 参考值通常是一个整数或材质属性 ReadMask [_StencilReadMask] // 读取掩码默认255 WriteMask [_StencilWriteMask] // 写入掩码默认255 Comp [_StencilComp] // 比较函数 Pass [_StencilOp] // 模板测试和深度测试都通过时的操作 Fail [_StencilOpFail] // 模板测试失败时的操作 ZFail [_StencilOpZFail] // 模板测试通过但深度测试失败时的操作 }我们来逐一拆解每个参数的含义和实战中的选择逻辑Ref (Reference Value)这是比较的基准值。它可以是硬编码的整数如Ref 1更常见的做法是将其暴露为材质属性如Ref [_StencilRef]以便在材质面板或代码中动态调整。关键点这个值会和从Buffer中读取的值进行比较但它本身不是直接写入Buffer的值写入值由Pass等操作决定。ReadMask / WriteMask (读取/写入掩码)这是两个按位与操作的掩码默认值都是255二进制11111111。ReadMask在从Stencil Buffer读取值后、进行比较操作前会先执行(StencilBufferValue ReadMask)。这允许你只关心Buffer值中的特定位。例如ReadMask 12701111111会忽略最高位。WriteMask在根据Pass/Fail/ZFail操作决定要写入的新值后在真正写入Buffer前会执行(NewValue WriteMask)。这允许你保护Buffer中的某些位不被修改。常见用法当你希望用Stencil Buffer的不同位来存储不同类型的信息如一位表示遮罩区域另一位表示特殊高亮区域时掩码就非常有用。Comp (Comparison Function)比较函数。定义了如何将(StencilBufferValue ReadMask)与Ref进行比较。它是一个枚举常用选项如下Always永远通过测试。这是Mask组件中“遮罩”部分使用的模式因为它需要无条件地绘制自己并写入Stencil值。Equal相等时通过。这是Mask组件中“被遮罩内容”部分使用的模式也是实现遮罩效果最核心的模式。NotEqual不相等时通过。常用于实现“反遮罩”或“挖洞”效果。Less/Greater/LEqual/GEqual用于实现基于值的层级遮罩比如实现一个“仅显示在编号大于2的遮罩后”的效果。Pass / Fail / ZFail (Stencil Operation)操作函数。定义了在不同测试结果下如何更新Stencil Buffer中的值。它是一个枚举常用选项如下Keep保持原值不变。这是最安全的选项当你不想影响后续渲染时使用。Replace用Ref值替换当前Buffer值。这是创建遮罩区域最核心的操作。Mask组件中的遮罩图形就是用它来“盖章”的。Zero将值置为0。用于清除某个区域的遮罩标记。IncrSat/DecrSat增加1或减少1但会饱和在0和255之间。可用于实现计数类效果比如实现最多255层的嵌套遮罩。Invert按位取反。可以实现一些反转效果。一个极其重要的实操心得Pass、Fail、ZFail这三个操作是独立的。它们分别对应“片元最终通过所有测试”、“片元在模板测试阶段就被淘汰”、“片元通过了模板测试但败在了深度测试”这三种情况。这意味着即使一个物体因为模板测试没通过而不可见Fail你仍然可以通过设置Fail操作来修改Stencil Buffer这个特性可以用来实现“不可见的刻章工具”。3. 逆向工程拆解Unity Mask组件的工作原理现在我们有了Stencil的理论武器是时候回头看看Unity的Mask组件这个“黑盒”里到底发生了什么。理解它是自定义遮罩的起点。3.1 Mask组件的渲染逻辑分解当你给一个UI元素如Image添加Mask组件时Unity在背后自动为你做了两件事为Mask图形自身即带Mask组件的物体修改材质它会动态生成或修改一个材质该材质的Stencil配置大致如下Stencil { Ref [_StencilID] // 一个动态生成的唯一ID比如1 Comp Always // 总是通过测试 Pass Replace // 通过后用Ref值替换该像素的Stencil Buffer值 ReadMask 255 WriteMask 255 }这个配置的意图非常明确无条件地绘制自己并在自己覆盖的屏幕像素区域“盖上一个数字章”写入Stencil ID。这个“章”就是后续判断是否显示的依据。为所有子级物体修改材质Mask组件会遍历其所有子物体为它们的材质添加或修改Stencil配置大致如下Stencil { Ref [_StencilID] // 与父级Mask的ID相同 Comp Equal // 只有Stencil Buffer值等于这个ID时才通过测试 Pass Keep // 通过后保持Buffer值不变 ReadMask 255 WriteMask 255 }这个配置的意思是只有当像素位置的“模具编号”等于父Mask盖的“章”时才允许绘制。这样就完美实现了“只在Mask形状内显示”的效果。为什么是“子级”而不是“同层级之后”这是由渲染顺序决定的。Unity UI默认按照层级顺序从后往前渲染先渲染父级再渲染子级。因此Mask父级先渲染并写入Stencil值然后子级渲染时读取该值并进行判断。如果你试图让一个非子级的物体受Mask影响由于渲染顺序可能错乱效果就无法保证。自定义Stencil给了我们打破这个限制的可能。3.2 从Mask到自定义Shader的思维转换理解了Mask的原理我们就可以摆脱组件的束缚。自定义Shader实现遮罩的核心思路就是手动模拟上述两个步骤但拥有完全的控制权创建“模具”Shader这个Shader的唯一目的就是向Stencil Buffer写入一个特定值。它可能绘制一个可见的图形如一个圆形图片也可能绘制一个完全透明、不可见的图形仅作为“刻章”工具。其Stencil配置核心是Comp Always和Pass Replace。创建“被遮罩内容”Shader这个Shader用于绘制真正需要被裁剪的内容。其Stencil配置核心是Comp Equal或其他比较函数和Pass Keep。确保渲染顺序必须保证“模具”物体在渲染队列中先于“被遮罩内容”物体渲染。在Unity中我们可以通过设置物体的Render Queue或Shader的Queue标签来控制。注意Unity内置的UI Mask组件在编辑器中非常高效但对于大量动态Mask或复杂场景其运行时动态合批修改材质的行为可能成为性能瓶颈。自定义Shader方案虽然需要更多前期设置但运行时性能开销更稳定、更可控是高性能UI项目的常用优化手段。4. 实战从零实现自定义遮罩Shader理论说得再多不如动手写一行代码。让我们从最简单的案例开始复现一个矩形遮罩然后逐步增加复杂度。4.1 案例一复现基础矩形遮罩我们将创建两个Shader和一个材质球。第一步创建“模具”Shader (StencilMask.shader)这个Shader绘制一个矩形并写入Stencil值。我们将颜色设为白色以便观察但实践中它可以是任何颜色甚至透明。Shader Custom/StencilMask { Properties { _Color (Color, Color) (1,1,1,1) _StencilID (Stencil ID, Int) 1 // 暴露为属性的Stencil ID } SubShader { Tags { QueueGeometry-100 RenderTypeOpaque } // 队列设置得比较靠前 // 禁用深度写入防止影响后续物体深度测试根据需求可选 ZWrite Off // 模具的Stencil配置 Stencil { Ref [_StencilID] // 使用属性中的ID作为参考值 Comp Always // 总是通过测试 Pass Replace // 通过后用Ref值替换Buffer值 } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; }; struct v2f { float4 pos : SV_POSITION; }; fixed4 _Color; v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); return o; } fixed4 frag (v2f i) : SV_Target { return _Color; } ENDCG } } }关键点解析QueueGeometry-100确保了此物体在所有默认不透明物体QueueGeometry之前渲染这是为了先“盖章”。ZWrite Off是一个可选项如果你不希望这个模具物体影响深度缓冲区就关闭它。第二步创建“被遮罩内容”Shader (StencilContent.shader)这个Shader绘制的内容将只在模具区域内显示。Shader Custom/StencilContent { Properties { _MainTex (Texture, 2D) white {} _StencilID (Stencil ID, Int) 1 // 必须与模具Shader的ID一致 } SubShader { Tags { QueueGeometry RenderTypeOpaque } // 被遮罩内容的Stencil配置 Stencil { Ref [_StencilID] // 使用相同的ID Comp Equal // 仅当Buffer值等于Ref值时通过 Pass Keep // 通过后保持Buffer值不变 } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 pos : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { return tex2D(_MainTex, i.uv); } ENDCG } } }第三步在场景中应用创建一个Cube或Quad将其材质设为使用StencilMask.shader调整_StencilID属性例如设为1。创建另一个Cube或Quad将其材质设为使用StencilContent.shader确保其_StencilID也为1。运行游戏你会发现第二个物体内容只有与第一个物体模具相交的部分才会被渲染出来。移动模具物体内容的可见区域会随之变化。4.2 案例二实现圆形遮罩与多重遮罩矩形遮罩太基础我们来实现一个圆形遮罩。关键在于“模具”Shader需要能够丢弃片元只在一个圆形区域内写入Stencil值。修改“模具”Shader (StencilMaskCircle.shader)我们在片元着色器中加入圆形区域的判断。// ... 属性、SubShader标签、Stencil块与之前相同 ... Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 pos : SV_POSITION; }; fixed4 _Color; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uv v.uv; return o; } fixed4 frag (v2f i) : SV_Target { // 将UV从[0,1]映射到以中心为原点的[-0.5, 0.5] float2 centerUV i.uv - float2(0.5, 0.5); // 计算到中心点的距离 float distance length(centerUV); // 如果距离大于半径(0.5)则丢弃该片元 if (distance 0.5) discard; // 关键丢弃片元后Stencil操作也不会执行。 return _Color; } ENDCG }核心技巧discard指令会立即终止当前片元的处理。这意味着被discard的像素其Stencil操作Pass Replace也不会执行所以只有圆形区域内的像素才会被写入Stencil ID完美实现了圆形遮罩。实现多重遮罩Multi-Mask多重遮罩是指一个内容物体需要同时满足多个模具的条件才显示。例如只显示在两个圆形相交的区域。这需要利用Stencil Buffer的不同位或不同的比较函数。方法A使用不同的Stencil ID和Comp Greater创建两个圆形模具物体分别使用StencilMaskCircle.shader但设置不同的_StencilID比如ID11 ID22。创建一个内容物体使用如下Stencil配置Stencil { Ref 2 // 参考值设为2 Comp Greater // 比较函数设为“大于” Pass Keep }渲染顺序先渲染ID1的模具写入值1再渲染ID2的模具写入值2。对于内容物体只有当像素处的Stencil值大于2时才渲染。由于值1和2都不大于2所以只有两个模具都渲染过、值被更新为2的像素即最后一个模具覆盖的区域才会显示。这实际上实现的是“最后一道遮罩”并非“交集”。方法B使用位运算实现真正的“与”操作更灵活我们可以用Stencil Buffer的不同二进制位来代表不同的遮罩。假设我们用第1位值1代表遮罩A第2位值2代表遮罩B。模具A的ShaderRef 1,WriteMask 1只写第1位,Pass Replace。模具B的ShaderRef 2,WriteMask 2只写第2位,Pass Replace。内容物体的ShaderStencil { Ref 3 // 二进制 011 即第1位和第2位都为1 ReadMask 3 // 只读取低2位 Comp Equal // 只有当低2位都等于1即值等于3时才通过 Pass Keep }这样只有同时被模具A和模具B覆盖的像素其Stencil值的第1位和第2位都是1值才会是3内容物体才会在那里渲染。这种方法可以轻松扩展到更多重遮罩。4.3 案例三制作动态遮罩与交互效果静态遮罩不够酷让我们结合脚本实现一个动态变化的遮罩。例如一个逐渐扩大的圆形扫描遮罩。创建动态模具Shader (StencilMaskDynamic.shader)我们需要一个属性来控制遮罩半径。Properties { _Color (Color, Color) (1,1,1,1) _StencilID (Stencil ID, Int) 1 _Radius (Radius, Range(0, 0.7)) 0.5 // 控制半径 }在片元着色器中使用_Radius代替固定的0.5if (distance _Radius) discard;编写控制脚本 (DynamicMaskController.cs)using UnityEngine; public class DynamicMaskController : MonoBehaviour { public Material stencilMaskMaterial; // 拖入使用StencilMaskDynamic.shader的材质球 public float growthSpeed 0.5f; public float maxRadius 0.7f; private float currentRadius 0f; void Start() { if (stencilMaskMaterial ! null) { currentRadius stencilMaskMaterial.GetFloat(_Radius); } } void Update() { if (stencilMaskMaterial ! null) { currentRadius Time.deltaTime * growthSpeed; currentRadius Mathf.Clamp(currentRadius, 0f, maxRadius); stencilMaskMaterial.SetFloat(_Radius, currentRadius); } } }将这个脚本挂到场景中任意物体上并将动态模具物体的材质拖拽赋值。运行后你会看到遮罩区域从中心点逐渐扩大。同理你可以修改脚本让遮罩跟随鼠标移动将世界坐标或屏幕坐标转换到模型的UV空间实现一个“聚光灯”跟随的交互效果。实操心得材质实例化直接修改Material的属性会影响所有使用该材质的物体。如果你希望每个动态遮罩独立控制应该在脚本中使用material GetComponentRenderer().material;来获取材质实例这样修改的就是这个物体独有的材质副本不会影响其他物体。5. 高级应用与性能优化指南掌握了基础我们可以探索一些更高级、更实用的应用场景并关注性能问题。5.1 实现“镂空”与“反转遮罩”效果“镂空”效果即让内容显示在模具区域之外。这非常简单只需要将内容Shader的Comp从Equal改为NotEqual。Stencil { Ref [_StencilID] Comp NotEqual // 关键修改当Buffer值不等于Ref值时通过 Pass Keep }这样只有在模具没有“盖章”的地方内容才会被绘制出来实现了挖洞效果。5.2 在3D场景中的应用简易门户与镜子效果Stencil不仅用于UI在3D场景中同样强大。简易门户效果假设有一面墙墙上有一个洞门户我们希望在墙的后面渲染另一个场景但只在这个洞内可见。创建第一个Pass或物体渲染“墙”使用一个写入特定Stencil ID如1的Shader但在“洞”的区域使用discard。这样只有墙实体部分写入了Stencil值洞的区域保持为0。创建第二个Pass或物体渲染“门户后的场景”其Stencil配置为Ref 1,Comp NotEqual。这样门户后的场景就只会在墙的“洞”区域Stencil值不为1的地方渲染出来。简易镜子效果这是一个经典应用。镜子本身需要被渲染通常是一个简单的平面然后我们在镜子平面覆盖的区域内渲染整个场景的镜像。渲染镜子平面使用Comp Always和Pass Replace写入一个特定的Stencil ID如2。同时这个Pass应该关闭颜色写入ColorMask 0因为我们可能不想看到镜子本身的颜色或者它另有纹理。Stencil { Ref 2 Comp Always Pass Replace } ColorMask 0 // 不写入任何颜色只操作Stencil和Depth Buffer渲染镜像场景。这通常需要第二个摄像机其渲染目标是一个临时纹理Render Texture或者通过额外的Pass实现。这个渲染的Pass需要设置Stencil { Ref 2 Comp Equal Pass Keep } // 只在镜子区域内渲染同时可能还需要处理深度确保镜像物体不会和镜子平面本身产生深度冲突可能需要适当的深度偏移。5.3 性能考量与最佳实践滥用Stencil可能会带来性能开销遵循以下准则可以避免踩坑精简Stencil使用范围只在必要的物体上开启Stencil测试。对于不参与遮罩逻辑的物体确保其Shader中没有Stencil块或者设置为Comp Always默认。警惕Overdraw复杂的遮罩形状如精细的镂空会导致大量片元被discard。虽然discard避免了颜色和深度写入但片元着色器的计算已经发生这被称为过度绘制Overdraw。对于移动平台需特别注意。利用渲染队列Render Queue精确控制物体的渲染顺序是Stencil正确工作的前提。确保“模具”物体的渲染队列序号小于“内容”物体。Unity内置队列如QueueGeometry-1可以让你在标准几何体之前渲染。避免每帧修改Stencil状态在Shader中Stencil状态的切换特别是Comp和Pass的改变可能造成GPU状态切换影响合批。尽量让使用相同Stencil配置的物体材质相同以便Unity进行动态合批。WriteMask是你的朋友当场景中存在多套独立的遮罩系统时使用WriteMask将它们隔离在不同的位上可以避免相互干扰减少清理Buffer的需求。清除Stencil Buffer通常相机在每帧开始时会自动清除深度和Stencil Buffer。如果你需要在一帧中间“重置”某个区域的Stencil值可以渲染一个全屏或指定区域的、Pass Zero的物体但这会带来额外的绘制调用需权衡使用。6. 常见问题排查与调试技巧即使理解了原理实战中依然会遇到各种诡异的问题。下面是一些常见坑点和排查方法。6.1 遮罩完全无效内容全部显示或全部不显示检查渲染顺序这是最常见的原因。确保“模具”物体在渲染队列中先于“内容”物体渲染。在Scene视图的右上角将Shading Mode切换到Overdraw模式如果支持可以直观看到渲染顺序。或者使用Frame Debugger工具逐帧查看绘制调用顺序。检查Stencil ID是否匹配确认“模具”Shader写入的Ref值和“内容”Shader读取比较的Ref值是同一个数字。一个技巧是将模具的_Color设为醒目的红色先确认它确实被绘制在了正确的位置。检查Shader编译和目标平台某些Stencil操作在OpenGL ES 2.0等较旧的图形API上支持有限。确保你的Shader使用了正确的编译指令并在目标设备上进行测试。在Shader中添加#pragma target 3.0可以确保更好的特性支持。6.2 遮罩边缘出现锯齿或闪烁深度测试冲突当模具物体和内容物体处于非常接近的深度时可能会因为深度精度问题Z-fighting导致像素级别的闪烁。解决方案为模具物体添加微小的深度偏移Offset -1, -1在Pass中。确保模具物体和内容物体在深度上有轻微分离。Discard导致的边缘锯齿在使用discard实现非矩形遮罩时边缘像素可能因为MSAA多重采样抗锯齿的处理方式而产生锯齿。可以考虑在边缘实现Alpha渐变而不是硬discard但这会使得Stencil写入区域也带渐变可能不符合预期。另一种方法是使用Alpha Test (clip())但其抗锯齿行为也需注意。6.3 复杂场景中遮罩相互干扰使用WriteMask进行位隔离如前文所述为不同的遮罩系统分配不同的位如1248...并通过WriteMask限制其写入范围。主动清理Stencil区域如果一个遮罩只在某一阶段需要之后需要失效可以在该阶段结束后用一个Pass Zero且WriteMask对应的物体覆盖该区域将其Stencil值重置为0。利用相机的Clear Flags如果整个屏幕的遮罩逻辑每帧都是全新的确保相机设置中的Clear Flags包含Depth和Stencil通常是Skybox或Solid Color模式这样每帧开始Buffer都是干净的。6.4 调试工具可视化Stencil Buffer在开发过程中能够“看到”Stencil Buffer的值至关重要。你可以编写一个简单的调试Shader将Stencil值作为颜色输出。Shader Debug/StencilViewer { SubShader { Tags { QueueOverlay RenderTypeOpaque } Pass { ZTest Always // 总是通过深度测试 ZWrite Off Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct v2f { float4 pos : SV_POSITION; float4 screenPos : TEXCOORD0; }; v2f vert (appdata_base v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.screenPos ComputeScreenPos(o.pos); return o; } fixed4 frag (v2f i) : SV_Target { // 读取当前像素的Stencil值这需要平台支持并非所有情况都能直接读取 // 更通用的方法是在渲染此物体前确保Stencil测试设置为Always然后通过特殊的Pass将值“转换”到颜色。 // 这里是一个概念性代码实际实现更复杂。 // 通常使用Graphics.DrawProcedural配合CommandBuffer来捕获。 return fixed4(1,0,0,0.5); // placeholder } ENDCG } } }实际上更常用的方法是使用Unity的Frame Debugger或RenderDoc这类图形调试器直接捕获并查看Stencil Buffer的纹理这是最准确的方式。掌握Stencil模板测试就如同获得了在GPU渲染画布上直接雕刻的能力。它打破了层级和组件的限制让你能实现高度定制化的视觉逻辑。从理解Buffer的读写规则开始到逆向分析内置组件再到亲手实现从简单到复杂的遮罩效果这个过程本身就是对渲染管线一次深刻的洞察。记住关键永远在于理清“谁在何时写入什么值”以及“谁在何时依据什么条件读取”。当你遇到复杂的遮罩需求时不妨画一张渲染顺序和Stencil值变化的示意图问题往往迎刃而解。