ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Unity渲染管线深度与模板测试:从原理到实战的Shader编程指南

2026/8/12 19:46:44 拓冰建站 浏览量
Unity渲染管线深度与模板测试:从原理到实战的Shader编程指南 1. 项目概述理解渲染管线中的“守门员”在Unity Shader的编写过程中我们常常醉心于如何用复杂的数学公式和纹理采样创造出炫酷的视觉效果比如流光溢彩的材质、动态变化的表面。但一个稳定、正确且高效的渲染结果其基石往往不是最花哨的那部分而是渲染管线中两个默默无闻却至关重要的“守门员”——模板测试Stencil Test和深度测试Depth Test。很多开发者尤其是刚接触Shader编程的朋友可能会觉得这两个概念有些晦涩或者认为它们只是引擎底层自动处理的事情与自己关系不大。实际上一旦你开始制作诸如角色描边、水面折射、遮挡剔除如X-Ray效果、UI遮罩或者复杂的多Pass渲染效果时不理解这两个测试机制几乎寸步难行写出的Shader要么效果错误要么性能低下。简单来说你可以把渲染过程想象成在一张画布上作画。深度测试决定了“谁画在前面谁画在后面”它解决了物体在三维空间中的前后遮挡关系。而模板测试则像是一把自定义的“刻刀”或“印章”它允许你基于屏幕上已有的像素信息模板值精确地控制接下来哪些像素可以被绘制哪些区域必须被保留或禁止绘制。这两个测试发生在片元着色器Fragment Shader执行之后颜色写入帧缓冲Frame Buffer之前是像素能否最终“上屏”的最后一道关卡。我最初接触这两个概念时也走过不少弯路。比如想做一个简单的角色外发光描边结果发现描边穿过了墙壁或者想实现一个区域性的UI高亮却总是控制不好显示范围。后来才明白这些问题本质上都是因为没有正确配置深度和模板状态。本文将结合我多年的实战经验从原理到应用彻底拆解Unity Shader中的模板测试与深度测试让你不仅能理解它们是如何工作的更能掌握如何灵活运用它们来解决实际开发中的棘手问题。2. 深度测试三维世界的秩序维护者2.1 深度测试的核心原理与缓冲区深度测试顾名思义就是测试当前准备绘制的像素片元的深度值即距离摄像机的远近与已经存储在深度缓冲区Depth Buffer或Z-Buffer中对应位置的深度值之间的关系。它的唯一使命就是解决遮挡问题离相机更近的物体会遮挡住更远的物体。深度缓冲区是一张与屏幕分辨率一致的二维纹理每个像素存储一个深度值。这个值通常是经过透视除法后的标准化设备坐标NDC的z分量范围在[0, 1]之间在DirectX风格的API中0为最近1为最远OpenGL风格则可能相反Unity默认使用Reverse-Z等优化策略但逻辑一致。当一个片元产生后GPU会进行以下操作将片元的深度值经过顶点着色器变换和插值后得到与深度缓冲区中同一像素位置的现有深度值进行比较。根据预设的比较函数如Less, Greater, Equal等决定当前片元是否通过测试。如果通过测试当前片元的深度值可能会写入深度缓冲区取决于深度写入是否开启覆盖旧值同时片元的颜色值会被送入后续的混合阶段或直接写入颜色缓冲区。如果未通过测试则该片元被直接丢弃不会产生任何后续影响。这个机制保证了无论物体以何种顺序提交渲染最终屏幕上显示的永远是离相机最近的那一层表面从而正确呈现三维空间感。注意深度测试是逐像素进行的并且通常发生在片元着色器执行之后Early-Z除外。这意味着即使一个片元在片元着色器中进行了大量复杂计算如果最终深度测试失败这些计算就白费了。因此良好的渲染排序和利用Early-Z技术对性能优化至关重要。2.2 Unity中的深度状态配置在Unity ShaderLab中我们通过ZWrite和ZTest指令来控制深度测试和写入。2.2.1 ZWrite深度写入开关ZWrite On // 默认状态通过深度测试后将片元深度写入深度缓冲区 ZWrite Off // 关闭深度写入片元深度值不会更新深度缓冲区ZWrite On是大多数不透明物体的标准配置。ZWrite Off则常用于半透明物体、后期处理特效或一些特殊的全屏绘制。例如绘制UI时我们通常希望UI元素之间按照渲染队列排序进行混合而不希望它们写入深度干扰场景物体的遮挡关系所以UI Shader常关闭深度写入。2.2.2 ZTest深度比较函数ZTest Less // 默认值。当前片元深度 缓冲区深度时通过离相机更近 ZTest Greater // 当前片元深度 缓冲区深度时通过离相机更远 ZTest LEqual // 当前片元深度 缓冲区深度时通过 ZTest GEqual // 当前片元深度 缓冲区深度时通过 ZTest Equal // 当前片元深度 缓冲区深度时通过 ZTest NotEqual // 当前片元深度 ! 缓冲区深度时通过 ZTest Always // 总是通过测试无视深度值ZTest Less是最常见的配置确保只有更近的物体才能覆盖更远的物体。ZTest Always则完全绕过深度测试常用于绘制始终在最前面的物体如某些全屏遮罩或后期效果。ZTest Greater可以用于实现“仅在物体背后绘制”的效果比如一些X-Ray透视效果。实操心得渲染队列与深度测试的配合Unity的渲染队列RenderQueue是一个非常重要的概念它宏观上决定了物体被绘制的顺序。不透明物体如Geometry队列值2500通常按照从前往后Far to Near的顺序渲染以利用深度测试提前丢弃被遮挡的片元Early-Z。而半透明物体如Transparent队列值2500则按照从后往前Back to Front的顺序渲染因为需要基于颜色混合来模拟透光效果。理解你的Shader属于哪个队列并据此设置正确的ZWrite和ZTest是避免出现渲染错误的第一步。一个常见的错误是一个本应半透明的物体设置了ZWrite On会导致它后面的其他半透明物体无法被正确渲染。2.3 深度测试的经典应用场景场景一角色描边轮廓光单纯在模型基础上放大一圈绘制描边会因为深度测试而穿墙。标准做法是使用两个Pass第一个Pass描边使用顶点偏移将模型沿法线方向挤出渲染纯色。这个Pass的ZTest设置为Greater或LEqual取决于想要的效果并且ZWrite通常设为Off。这样描边只会在模型本体或更近的物体背后绘制而不会遮挡住模型本身或前面的物体。第二个Pass本体正常渲染模型材质使用默认的ZTest Less和ZWrite On。 这样就能得到完美的、不穿墙的描边效果。场景二水面与折射效果水面通常需要渲染水下的场景折射。一种实现方式是先正常渲染水下场景。渲染水面本身。水面的Shader可能会用一个Pass其ZTest设为LEqual因为水面可能和岸边有交叉并且开启深度写入。如果需要更精确的折射可能会用到 GrabPass 抓取屏幕图像并结合深度图进行扰动。这时对深度测试和写入的控制就更为精细以确保折射图像只出现在水面以下且被水面遮挡的区域。场景三遮挡透视X-Ray效果当角色被墙壁遮挡时我们希望墙壁变为半透明或显示一个轮廓。这可以通过渲染两个版本的墙壁来实现正常渲染不透明的墙壁。当检测到角色被遮挡时用另一个Shader再次渲染墙壁。这个Shader使用ZTest Greater并且可能使用不同的颜色如红色半透明或仅渲染轮廓。这样这个“高亮”版本的墙壁就只会在正常墙壁深度值更小的后面即被遮挡的部分显示出来。3. 模板测试像素级的精密模具如果说深度测试是依据“远近”这个单一维度来筛选像素那么模板测试则提供了一个8位的“模具缓冲区”Stencil Buffer允许我们进行任意复杂的像素级绘制逻辑控制。它是实现很多高级渲染特效的钥匙。3.1 模板缓冲区与测试流程模板缓冲区同样是一张与屏幕分辨率一致的8位整数纹理每个像素的值范围是0-255。模板测试的流程比深度测试更灵活包含三个核心操作比较Compare、操作Pass/Fail/ZFail操作。整个过程可以用一个完整的决策树来描述读取参考值Ref我们在Shader中设定一个整型的参考值ReferenceValue。读取掩码ReadMask一个与参考值和缓冲区值进行按位与操作的掩码用于选择参与比较的位。通常默认为2550xFF即所有位都参与。读取缓冲区值从模板缓冲区中读取当前像素位置现有的值。比较将Ref ReadMask与缓冲区值 ReadMask进行比较比较函数由Comp指定如Always, Never, Equal, NotEqual, Less, Greater等。根据测试结果与深度测试结果执行操作模板测试失败执行Fail操作。模板测试通过但深度测试失败执行ZFail操作。模板测试和深度测试都通过执行Pass操作。可执行的操作包括Keep保持模板缓冲区当前值不变。Zero将值设置为0。Replace将模板缓冲区值设置为参考值Ref。IncrSat增加1但不超过最大值255。DecrSat减少1但不低于最小值0。Invert按位取反。IncrWrap/DecrWrap循环增减超过255回绕到0反之亦然。3.2 Unity ShaderLab中的模板语法在SubShader或Pass中使用Stencil块来配置Stencil { Ref 2 // 参考值整数 ReadMask 255 // 读掩码默认255 WriteMask 255 // 写掩码默认255只有被此掩码覆盖的位才会被写入操作影响 Comp Always // 比较函数 Pass Keep // 两者都通过时的操作 Fail Keep // 模板测试失败时的操作 ZFail Keep // 模板通过但深度失败时的操作 }关键参数解析Ref这是你设定的“模具”的值。你可以为不同的物体或不同的Pass设置不同的Ref值来代表不同的“标记”。Comp比较函数。Always意味着总是通过测试这是最常用的“写入模板”模式。Equal则常用于“读取模板”模式只在与Ref值相等的区域绘制。Pass/Fail/ZFail这三个操作定义了在不同测试结果下如何更新模板缓冲区。Replace操作是核心用于将Ref值“盖章”到缓冲区。实操心得WriteMask的妙用WriteMask允许你只更新模板缓冲区的特定位。例如你可以用高4位存储一种信息如物体类型低4位存储另一种信息如状态。通过设置WriteMask为0x0F只写低4位你可以在不破坏高4位信息的情况下更新状态位。这在实现复杂的状态机式渲染逻辑时非常有用。3.3 模板测试的进阶应用模式模式一区域遮罩绘制如UI技能范围这是模板测试最直观的应用。假设你要在游戏地面上绘制一个扇形的技能释放范围指示器。第一步绘制模具。用一个Pass或一个单独的简单物体先绘制这个扇形区域。这个Shader的Stencil配置为Ref 1, Comp Always, Pass Replace。这意味着无论怎样只要这个Pass的像素被绘制就会把对应像素的模板缓冲区值设置为1。这个Pass的渲染本身可能是不可见的颜色写入关闭ColorMask 0或者是一个简单的单色。第二步绘制效果。绘制实际的效果如半透明的高亮地面、动态纹理。这个Shader的Stencil配置为Ref 1, Comp Equal, Pass Keep。这意味着只有模板缓冲区值等于1的像素即第一步绘制的扇形区域内这个Pass才会执行片元着色器并输出颜色。区域外的像素直接在此阶段被模板测试丢弃。 这样就实现了效果被严格限制在指定形状区域内边界清晰锐利性能也好因为区域外的像素早早就被丢弃了。模式二多物体复杂交集/并集效果假设你要实现一个特效只在角色A和角色B同时所在的屏幕区域显示。用Ref 1和Pass Replace绘制角色A的轮廓区域。用Ref 2和Pass Replace绘制角色B的轮廓区域。但这里有个问题直接Replace会覆盖掉A的标记。更高级的做法是使用位操作。为A分配模板值1二进制00000001为B分配模板值2二进制00000010。绘制A时使用Pass IncrSat或自定义位运算写入。绘制B时同样操作。最终两个角色重叠区域的模板值会变成300000011。绘制特效时设置Comp EqualRef 3。这样特效就只出现在重叠区域了。通过设计不同的Ref值和操作可以实现并集任一角色所在区域、差集在A但不在B的区域等复杂逻辑。模式三镜面反射与平面反射实现镜子效果是模板测试的经典案例。第一步清空镜子区域的模板值。通常镜子是一个平面四边形。首先用Ref 1和Pass Replace绘制这个四边形但只写入模板不写入颜色和深度ColorMask 0,ZWrite Off。这标记了镜子所在的屏幕区域。第二步渲染反射内容。我们需要渲染场景在镜子中的倒影。此时设置Ref 1,Comp Equal。这样反射的绘制被限制在第一步标记的镜子区域内。同时需要结合摄像机变换渲染反射相机视图和可能用到的ZTest Greater因为反射物在镜子“后面”。第三步渲染镜子表面。最后渲染镜子本身的材质如带轻微反射纹理的玻璃。这个Pass可以正常进行深度测试并可能与反射内容进行Alpha混合同时可以设置ZWrite On来确保镜子表面遮挡其后的物体。4. 深度与模板的综合实战一个完整的特效案例让我们通过一个具体的案例——“能量护盾受到攻击时的局部涟漪效果”——来串联深度和模板测试的应用。需求是一个半透明的球形护盾当被子弹击中时击中点会产生一个扩散的涟漪这个涟漪必须只在护盾表面显示不能穿透护盾显示在内部或背后的物体上也不能显示在护盾前面的物体上。4.1 方案设计与Shader结构我们将使用一个多Pass的Shader来实现护盾本体和涟漪效果。Shader整体结构规划Pass 0: 护盾本体Pass。渲染基本的半透明护盾颜色处理深度和模板的初始状态。Pass 1: 涟漪模具Pass。根据传入的击中点位置和涟漪进度在护盾表面对应的屏幕区域“盖章”写入特定的模板标记。这个Pass不输出颜色。Pass 2: 涟漪效果Pass。只在被标记的模板区域绘制动态的涟漪纹理和颜色。4.2 核心Shader代码解析与注释以下是精简后的核心ShaderLab代码框架并附有详细注释Shader Custom/EnergyShield { Properties { _ShieldColor (护盾颜色, Color) (0.3, 0.6, 1.0, 0.5) _ShieldTex (护盾纹理, 2D) white {} _RippleTex (涟漪纹理, 2D) white {} _HitPoint (击中点世界坐标, Vector) (0,0,0,0) // w分量用作涟漪进度(0-1) _RippleColor (涟漪颜色, Color) (1,0.2,0.1,1) _RippleWidth (涟漪宽度, Range(0, 0.5)) 0.1 } SubShader { Tags { QueueTransparent RenderTypeTransparent IgnoreProjectorTrue } // Pass 0: 护盾本体 Pass { Name SHIELD_BASE Tags { LightModeForwardBase } Blend SrcAlpha OneMinusSrcAlpha // 标准Alpha混合 ZWrite Off // 半透明物体关闭深度写入避免遮挡后续半透明物体 ZTest LEqual // 默认深度测试护盾应该被前面的不透明物体遮挡 CGPROGRAM // ... 顶点/片元着色器代码计算护盾基础颜色和透明度 ENDCG } // Pass 1: 使用模板标记涟漪区域 Pass { Name RIPPLE_STENCIL // 这个Pass只操作模板缓冲区不输出任何颜色和深度 ColorMask 0 // 关闭所有颜色通道写入不产生视觉输出 ZWrite Off // 关闭深度写入不影响深度缓冲区 ZTest Always // 绕过深度测试确保无论护盾前后都能标记到正确屏幕位置但依赖顶点计算 Stencil { Ref 128 // 使用一个特定的值128作为涟漪区域的标记 Comp Always // 总是通过测试 Pass Replace // 通过测试的像素将其模板值替换为128 // Fail和ZFail保持Keep因为此Pass的ZTest是Always所以ZFail不会发生 } CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; float3 worldPos : TEXCOORD0; float3 worldNormal : TEXCOORD1; }; float4 _HitPoint; // xyz: 世界坐标击中点 w: 进度 v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.worldPos mul(unity_ObjectToWorld, v.vertex).xyz; o.worldNormal UnityObjectToWorldNormal(v.normal); return o; } fixed4 frag (v2f i) : SV_Target { // 关键计算判断当前片元是否在涟漪影响范围内 float3 hitDir i.worldPos - _HitPoint.xyz; float distanceToHit length(hitDir); float progress _HitPoint.w; // 从0到1的涟漪扩散进度 // 计算涟漪半径随进度扩大 float rippleRadius progress * 2.0; // 假设最大半径2单位 // 判断如果片元在涟漪半径范围内则通过此Pass触发模板Replace操作 // 使用平滑步进来产生柔和的边界避免锯齿 float rippleFactor smoothstep(rippleRadius, rippleRadius - _RippleWidth, distanceToHit); // 注意这里没有颜色输出但片元是否被丢弃决定了模板操作是否执行 // 如果片元在涟漪区域外我们可以手动裁剪(clip)掉它 clip(rippleFactor - 0.001); // 当rippleFactor接近0时裁剪 return 0; // 颜色输出被ColorMask 0屏蔽了这里返回任意值均可 } ENDCG } // Pass 2: 在标记区域绘制涟漪效果 Pass { Name RIPPLE_DRAW Blend One One // 使用叠加混合让涟漪高亮 ZWrite Off ZTest LEqual // 涟漪效果本身也需要进行深度测试确保它不会画在护盾前面的物体上 Stencil { Ref 128 // 参考值与缓冲区值比较 Comp Equal // 仅当模板缓冲区值等于128时才通过测试并绘制 Pass Keep // 绘制后保持模板值不变以便后续帧或其他效果使用 Fail Keep ZFail Keep } CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc // ... 属性与结构体定义同Pass1或根据需要调整 sampler2D _RippleTex; float4 _RippleTex_ST; float4 _RippleColor; float4 _HitPoint; float _RippleWidth; v2f vert (appdata v) { /* 与Pass1的vert类似计算世界坐标等 */ } fixed4 frag (v2f i) : SV_Target { // 再次计算与击中点的距离和进度 float3 hitDir i.worldPos - _HitPoint.xyz; float distanceToHit length(hitDir); float progress _HitPoint.w; float rippleRadius progress * 2.0; // 计算UV基于距离和角度生成涟漪纹理坐标 float2 rippleUV; rippleUV.x distanceToHit / (rippleRadius 0.001); // 径向UV // 计算角度可以使用atan2或预先计算的方向 rippleUV.y (atan2(hitDir.z, hitDir.x) / UNITY_PI) * 0.5 0.5; // 角度映射到[0,1] // 采样涟漪纹理通常是一个环状渐变的纹理 fixed4 rippleTex tex2D(_RippleTex, rippleUV * _RippleTex_ST.xy _RippleTex_ST.zw); // 根据距离计算衰减使涟漪从中心向外减弱 float falloff 1.0 - smoothstep(rippleRadius - _RippleWidth, rippleRadius, distanceToHit); // 结合纹理、颜色和衰减得到最终颜色 fixed4 finalColor _RippleColor * rippleTex.r * falloff * progress; // progress也用于整体淡入淡出 return finalColor; } ENDCG } } FallBack Diffuse }4.3 实现要点与避坑指南渲染顺序至关重要这三个Pass必须按顺序执行。SHIELD_BASE先绘制确立护盾在深度缓冲区中的位置。RIPPLE_STENCIL接着运行它在护盾所在的屏幕空间位置“盖章”。RIPPLE_DRAW最后运行只在上一步“盖章”的区域绘制。如果顺序乱了模板标记和效果绘制可能发生在错误的时间或空间。Pass 1的顶点计算精度RIPPLE_STENCILPass虽然ZTest是Always但其顶点着色器中的顶点变换UnityObjectToClipPos和世界坐标计算必须与SHIELD_BASEPass完全一致。否则计算出的屏幕位置和世界位置会有偏差导致模板标记的区域与护盾实际显示的区域不重合出现涟漪错位或泄露。模板值的生命周期管理在这个例子中我们用了固定的Ref值128。在复杂场景中如果多个护盾实例或多种特效共用模板缓冲区需要精心规划Ref值的分配避免冲突。通常每个需要独立模板控制的物体或效果组应该使用不同的Ref值或者在绘制完成后将模板值重置例如在护盾消失时用一个额外的Pass将对应区域的模板值设回0。性能考量RIPPLE_STENCILPass开启了ZTest Always且关闭了深度写入这意味着它会在所有像素上执行片元着色器尽管有clip裁剪。如果护盾模型面数很高且涟漪区域很小就会产生大量被clip丢弃的无效计算Overdraw。优化方法包括将涟漪区域的计算尽可能移到顶点着色器或几何着色器进行粗略剔除或者使用Command Buffer配合RenderTarget来更高效地管理模板/遮罩纹理。5. 常见问题、性能陷阱与调试技巧即使理解了原理在实际使用深度和模板测试时依然会遇到各种诡异的问题。下面是我总结的一些常见坑点和解决思路。5.1 深度测试相关疑难杂症问题1半透明物体渲染顺序错乱出现“穿帮”或深度冲突Z-fighting。现象两个半透明物体交错时颜色混合结果不正确或者交界处出现闪烁的像素。原因分析半透明渲染依赖从后往前的顺序进行Alpha混合。如果顺序错误后面的物体先绘制再绘制前面的物体混合公式会导致颜色权重出错。深度冲突则是由于两个片元的深度值过于接近浮点数精度不足以区分前后。解决方案严格排序确保所有半透明物体按照其包围盒中心到摄像机的距离从远到近排序。Unity的Transparent队列虽然有一定排序但对于复杂嵌套物体可能不够有时需要手动拆分渲染队列或使用多个SubShader。关闭深度写入半透明物体Shader务必设置ZWrite Off。写入深度会阻止后续更远的半透明片元通过深度测试破坏混合顺序。解决Z-fighting轻微调整物体的位置或缩放对于共面的情况如贴花Decal可以使用很小的深度偏移Offset指令例如Offset 0, -1让贴花稍微嵌入表面。问题2使用GrabPass做折射/扰动效果时抓取到的背景不对。现象水面折射扭曲了它背后的天空盒却漏掉了它前面的物体。原因分析GrabPass抓取的是当前帧在当前物体绘制之前的颜色缓冲区内容。如果水面前面有物体该物体可能还没有被绘制取决于渲染队列或者水的深度测试设置导致它绘制在了前面物体的后面。解决方案仔细调整渲染队列。确保水物体在所有不透明物体之后、但在其他可能遮挡它的半透明物体之前渲染。同时水的Shader应使用正确的ZTest通常LEqual和ZWrite通常Off。更高级的做法是使用延迟渲染或自定义渲染纹理来获取完整的场景深度和颜色信息。5.2 模板测试相关疑难杂症问题1模板效果不显示或显示不全。现象按照教程写了模板Shader但效果完全没出现或者只出现了一部分。排查步骤调试三板斧检查模板值将Ref值设为一个非常显眼的数字如255并将比较函数Comp设为AlwaysPass设为Replace。同时给这个Pass一个鲜艳的纯色输出如红色。运行游戏如果能看到大面积红色说明模板写入基本通路是好的。如果看不到检查模型是否被裁剪、摄像机是否看到、Shader是否被正确应用。检查比较函数在第一步的基础上将Comp改回你需要的如EqualRef保持255。如果效果消失说明当前缓冲区值不是255。你需要检查是哪个Pass在之前修改了模板值。可能是引擎的其他默认操作或者是场景中其他物体的Shader。检查写入掩码和操作确认WriteMask是255全写并且Pass/Fail/ZFail操作符合预期。一个常见的错误是在标记模具的Pass中因为深度测试失败ZFail导致模板操作没有执行而开发者默认了ZFail Keep。这时需要根据情况调整ZTest或ZFail的操作。问题2多物体模板互相干扰。现象场景中有多个使用模板测试的物体它们的效果互相覆盖或错乱。原因分析所有物体共享同一个8位模板缓冲区。如果它们使用了相同或冲突的Ref值和操作逻辑就会互相覆盖。解决方案分区规划为不同类型的模板用途分配不同的Ref值范围。例如UI遮罩用1-10游戏特效用11-100特殊场景交互用101-200。使用位操作如前文所述利用ReadMask和WriteMask将8位模板缓冲区当作多个标志位来使用。例如用第0位表示“是否在区域A”第1位表示“是否在区域B”。这样可以通过位与、位或|操作实现复杂的组合逻辑。及时清理如果一个模板效果是临时的如技能范围圈在效果结束时应该有一个Pass或通过脚本控制渲染将对应区域的模板值重置为0。可以使用Comp Always和Pass Zero操作。5.3 性能优化要点Early-Z与深度测试对于不透明物体尽量保证它们按照从近到远的顺序提交渲染Unity的渲染队列在一定程度上帮忙管理。这能让GPU的Early-Z优化最大限度地提前丢弃被遮挡的片元节省片元着色器的计算开销。避免不透明物体Shader使用ZTest Always或ZTest Greater除非有特殊需求。模板测试的Overdraw模板测试本身是廉价的但驱动它的渲染操作可能带来Overdraw。例如为了标记一个复杂形状的区域你可能会绘制一个覆盖全屏的四边形然后在片元着色器中用clip裁剪。这会导致大量片元被计算后又丢弃。优化方法是使用更精确的几何体来近似目标形状减少覆盖面积。将裁剪逻辑尽可能提前到顶点着色器或者使用几何着色器/曲面细分来动态生成几何体。考虑使用额外的摄像机或RenderTexture来预先渲染模板掩码虽然增加Draw Call但可能减少总体片元计算量。减少状态切换在同一个渲染批次内频繁切换深度/模板测试状态ZWrite,ZTest,Stencil会破坏合批增加Draw Call。尽量将状态相同的物体材料放在一起渲染。5.4 实用调试技巧可视化深度/模板缓冲区在Unity编辑器中你可以通过Frame Debugger窗口在渲染事件列表中查看每一步之后的深度和模板缓冲区。这是最强大的调试工具。此外可以编写一个简单的后期处理Shader将深度或模板缓冲区作为纹理采样并显示到屏幕上直观地看到每个像素的值。使用简单几何体验证当模板逻辑复杂时先用一个简单的Quad四边形来测试你的模板写入和读取逻辑是否正确。Quad简单明了容易判断其覆盖的屏幕区域。分步验证不要试图一次性写完一个复杂的多Pass模板Shader。先写一个Pass验证它能否正确写入模板。再写第二个Pass验证它能否正确读取并显示。最后再添加复杂的视觉效果计算。留意平台差异虽然Unity尽力抽象但不同图形API如OpenGL ES, Metal, Vulkan对深度和模板测试的某些边缘情况处理可能有细微差别。尤其是在移动平台务必进行真机测试。一个在编辑器DirectX模式下运行良好的特效可能在iOS的Metal上出现问题。