
做过Unity渲染的同学应该都绕不开一个场景给树叶、栅栏、草地或者头发做材质时材质本身其实是一张带Alpha通道的贴图我们希望让贴图中半透明白以外的区域被彻底裁掉只剩下要显示的那部分。这就是逐片元阶段里的透明度测试Alpha Test在做的事情。在Unity URPUniversal Render Pipeline通用渲染管线里透明度测试的写法、性能特征和内建管线差异不小许多新手照着旧CG代码抄会出各种问题。这篇博文我从“透明度测试在渲染流水线里到底处在哪个环节”讲起再到URP下从零写一个可用的Alpha Test Shader最后记录实际项目里踩过的坑和排查方法希望对正在跟URP较劲的你有点帮助。1. 先弄清楚透明度测试发生在渲染管线的哪个环节1.1 渲染流水线的宏观阶段划分渲染流水线可以粗分成三个阶段应用程序阶段CPU提交绘制命令、几何阶段顶点变换、裁剪、投影、逐片元阶段光栅化后对每个像素/片元进行着色、深度测试、写入帧缓冲。GPU最终运算的最小单元是“片元”它由几何阶段插值而来一个三角形覆盖到的每个像素位置都可能产生一个或多个片元。逐片元阶段就是决定这些片元最终“去留”和“颜色”的地方。透明度测试发生在逐片元阶段但严格说它是在片元着色器Fragment Shader里通过指令主动砍掉不满足条件的片元而不是像深度测试/模板测试那样由固定功能单元自动完成。这意味着我们写的Shader代码在这个位置拥有完全的决策权读到一个Alpha值判断它要不要通过不通过就丢弃discard/clip。这个动作发生在片元着色器输出颜色之前也发生在深度测试写入之前顺序上很靠前。为什么强调“顺序”因为丢弃片元会影响一系列后续操作包括Early-Z、模板写入、深度写入、颜色混合牵一发而动全身。1.2 逐片元阶段里到底做了什么决策一个片元进入逐片元阶段的典型流程大致是先经过剪裁测试Scissor Test然后可能做Early深度测试Early-Z再做透明度测试程序员自己写片元着色器求出颜色和深度再做深度测试、模板测试、混合最后写入帧缓冲。不同GPU会把某些测试优化到提前执行但逻辑上透明度测试的位置就是“片元着色器处理中”的一个分支。当Shader中调用clip(alpha - cutoff)时等于告诉GPU这个片元你不用继续算了直接扔了吧。需要留意的是透明度测试丢弃的是该片元的所有数据包括子像素覆盖率、深度样本、颜色这意味着该像素不会触发后续的混合操作。所以它跟透明度混合有本质区别透明度混合是“合并颜色”片元并没有消失透明度测试是“物理删除”片元的几何影响彻底没戏了。直观理解透明度测试就像用剪纸刀把贴图裁成镂空形状而透明度混合更像是把半透明玻璃叠在画面上。1.3 为什么是“测试”而不是“混合”Unity内建管线里的TagQueue和RenderType常用于区分这两种透明处理但不少人把“AlphaTest”和“Alpha Blend”混为一谈这在URP下调整渲染顺序时会踩坑。透明度测试因为最终会丢弃部分片元通常依然属于“不透明”或“裁剪”类别RenderQueue一般放在AlphaTest约2450Blend保持关闭状态。透明度混合则属于Transparent队列3000需要打开Blend SrcAlpha OneMinusSrcAlpha之类的混合因子。做成对比表更直观项目透明度测试Alpha Test透明度混合Alpha Blend片元去向不满足Alpha条件会被直接丢弃片元始终存在只参与颜色混合混合模式关闭Blend通常不透明必须开启Blend指定混合因子渲染队列AlphaTest约2450Transparent约3000深度写入有经常写深度通常关闭深度写入排序需求无需从后往前排序大部分情况必须排序否则半透明穿插错误效果硬边缘镂空柔和半透渐变很多人追求“软边缘”容易误以为只要把片元颜色的Alpha输出成灰度就能得到平滑过渡但实际上不开启透明度混合时任何Alpha都会被二值化处理。要么保留要么丢弃不存在中间值。这是理解透明度测试最关键的一条线。2. 透明度测试的底层原理Alpha值、clip与隐藏的GPU开销2.1 片元的Alpha从哪来怎么算Alpha值本质上是纹理采样结果里RGBA的A通道。美术在制作树叶、铁丝网、草地贴图时通常把需要展示的形态画在RGB通道把裁掉区域的Alpha设置为0或接近0。Shader里就是一行half4 albedo tex2D(_MainTex, uv);然后取albedo.a。如果是程序化生成材质Alpha也可以通过噪声、距离场运算或顶点色继承而来但最终给到clip函数的只有一个标量。需要注意的是URP默认的Lit/Unlit Shader里通常没有透明度测试分支你要么在Surface Options里勾选Alpha Clip要么自己写HLSL里手动调用clip。自写的好处是明确可控不会被URP的宏展开绕晕。同样重要的是Alpha值的精度在贴图压缩后可能变化。比如使用DXT5格式的带Alpha纹理实际还原出的Alpha通道会出现原始值间的插值或压缩误差切割阈值选太靠近0.5时边缘很容易出现碎屑点。压缩格式对Alpha的精度影响往往被忽视这也是后续排查边缘“麻点”的重要思路。2.2 clip指令的本质剪切掉不符合条件的片元在HLSL里clip(x)等价于“如果x小于0则丢弃该片元”。我见过不少写法是if (albedo.a _Cutoff) discard;这在语义上完全没问题但底层编译器往往会把它转化为类似的clip指令避免动态分支带来的低效率。推荐直接写clip(albedo.a - _Cutoff);因为这条指令在多数GPU上是一个可预测的“丢弃/保留”操作不会被额外拆成两个分支路径。_Cutoff是裁剪阈值。当albedo.a - _Cutoff 0时片元保留否则丢弃。如果我们把阈值设成0.5那么Alpha低于0.5的片元全部被丢弃。美术常在贴图中处理“渐变羽化边缘”想通过调节Alpha做半透明过渡但这种效果在Alpha Test下会直接变成硬边。阈值越低保留的片元越多镂空范围越大但边缘也会更碎。一般树叶用0.30.5能获得比较饱满的形态。2.3 早点知道Early-Z的影响更好现代GPU普遍支持Early-Z优化也就是在片元着色器执行前先用光栅化得到的深度值和当前深度缓冲做一次预深度测试如果能确定为被遮挡就跳过片元着色器以省时间。但这个优化有一个大前提在片元着色器里不修改深度值且没有在程序里做出与深度相关的丢弃如透明度测试。由于clip会随机丢弃部分片元GPU无法确保Early-Z统计的覆盖率准确部分硬件可能被迫禁用Early-Z导致每个被绘制的片元都要完完整整跑一遍片元着色器性能压力明显上升。这是很多项目把大范围草叶材质做剪裁后帧率骤降的原因之一。所以在URP里做大量透明度测试时要格外注意Overdraw优化和Shader复杂度。片元着色器里没必要做复杂光照计算的就别做因为那些后来被丢弃的片元也在白付钱。有一些曲线救国方案比如在几何阶段做裁减把树叶模型做得更细而不是全依赖片元裁剪。这也解释了为什么手游项目里草垛模型常用“十字交叉面片AlphaTest”而不是复杂巨量面片。2.4 曾经的内建AlphaTest Pass与URP的差异老Unity内建管线的Shader通常会写SubShader { Tags { QueueAlphaTest RenderTypeTransparentCutout } Pass { AlphaTest Greater 0.5 // ... } }这里的AlphaTest Greater 0.5是固定管线的写法URP已经不再推荐。URP是基于可编程管线的统一交给片元着色器里的clip完成。如果直接把老的透明测试Shader搬到URP常常会发现标签、宏和着色路径对不上报错或效果错乱。URP下的实现核心是在Shader中声明Tags { QueueAlphaTest RenderTypeTransparentCutout }在HLSL片元函数中调用clip(i.uv ...)使用URP提供的宏和光照函数时注意在SurfaceData结构里传递alpha同时勾选使用AlphaClip相关宏如果只是想要一个带裁剪的Unlit材质URP里甚至可以不开Lit、不用贴图直接颜色输出加上clip非常轻量。反过来如果想要这个裁剪材质参与光照则必须走URP的Lit流程然后通过_Cutoff控制阈值这时就不能自己随意覆盖Alpha分支容易和URP内置的AlphaClip宏冲突。3. 在URP里从头写一个透明度测试Shader3.1 准备工作URP环境与Shader模板选择先确保项目里装好URP并且Graphic Settings里已经指定URP Asset。新建Shader时Unity 6的菜单里可选的URP模板有Lit、Unlit、Simple Lit等也可以直接创建一个空Shader文件参考官方Package里的样例重构。我习惯先创建一个Unlit Shader因为透明度测试的核心逻辑与光照无关更容易看到重点。新建完Shader后我们还需要一张带Alpha通道的测试贴图最稳妥的做法是在Photoshop里画一张带黑白Alpha的图导出TGA或PNG。如果不想花时间做美术资源用Unity的ProBuilder或者简单Quad配合内置的“Default-Particle”这类自带Alpha纹理也能凑合但效果没针对性。接下来进入代码环节我会给一个精简但完整的URP Unlit Shader带透明度测试、阴影投影和双面可选功能。3.2 Shader完整代码带AlphaTest的叶子材质直接贴上我常用的版本这个Shader适用于树叶、铁网、单面布料等不需要光照的透明裁剪效果Shader Custom/URPAlphaTestUnlit { Properties { _MainTex (Texture, 2D) white {} _Color (Tint, Color) (1,1,1,1) _Cutoff (Alpha Cutoff, Range(0,1)) 0.5 [Toggle] _IsDoubleSided (Double Sided, Float) 0 } SubShader { Tags { RenderPipeline UniversalPipeline RenderType TransparentCutout Queue AlphaTest UniversalMaterialType Unlit } Pass { Name ForwardUnlitAlphaTest Tags { LightMode UniversalForward } Cull Off HLSLPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile _ _SHADOWS_SOFT #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE #pragma multi_compile_fragment _ _SHADOWS_SOFT #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionHCS : SV_POSITION; float2 uv : TEXCOORD0; }; TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); CBUFFER_START(UnityPerMaterial) float4 _MainTex_ST; half4 _Color; half _Cutoff; float _IsDoubleSided; CBUFFER_END Varyings vert(Attributes input) { Varyings output; VertexPositionInputs posInputs GetVertexPositionInputs(input.positionOS.xyz); output.positionHCS posInputs.positionCS; output.uv TRANSFORM_TEX(input.uv, _MainTex); return output; } half4 frag(Varyings input) : SV_Target { half4 albedo SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv) * _Color; clip(albedo.a - _Cutoff); return albedo; } ENDHLSL } Pass { Name ShadowCaster Tags { LightMode ShadowCaster } ZWrite On ZTest LEqual ColorMask 0 HLSLPROGRAM #pragma vertex vert_shadow #pragma fragment frag_shadow #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionHCS : SV_POSITION; float2 uv : TEXCOORD0; }; TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); CBUFFER_START(UnityPerMaterial) float4 _MainTex_ST; half4 _Color; half _Cutoff; CBUFFER_END Varyings vert_shadow(Attributes input) { Varyings output; VertexPositionInputs posInputs GetVertexPositionInputs(input.positionOS.xyz); output.positionHCS posInputs.positionCS; output.uv TRANSFORM_TEX(input.uv, _MainTex); return output; } half4 frag_shadow(Varyings input) : SV_Target { half4 albedo SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv) * _Color; clip(albedo.a - _Cutoff); return 0; } ENDHLSL } } FallBack Off }这版Shader同时包含Forward和ShadowCaster两个Pass。ShadowCaster的作用是让透明裁切的物体能正确投射阴影不做这步的话树叶在地面上可能只剩整片矩形阴影非常出戏。至于代码里的#pragma multi_compile是顺手处理URP阴影宏后面细说。3.3 逐行拆解为什么要用clip、为什么设置Queue和RenderTypeShader中clip(albedo.a - _Cutoff);是核心。当alpha值减去cuitoff得到负数时GPU会直接把这个片元丢弃不再输出任何颜色。这也是“透明度测试”命名的由来。我一直建议把这段代码放在片元着色器最前面、采样纹理之后不要在它前面做额外光照计算否则扔掉的片元都等于白白浪费了GPU时间。Queue AlphaTest的作用是让这个物体在大多数不透明物体之后、半透明物体之前渲染。这样做的好处是它既能作为不透明物体写入深度挡住后面真正的不透明物体又不需要像半透明物体那样排渲染顺序。当然实际URP SRPScriptable Render Pipeline对RenderQueue是直接依据Transparency Sort等机制排序的但Queue标签仍然是Shader与Culling/Depth交互的重要标记。RenderType TransparentCutout严格说对URP的渲染本身影响不大但它会被很多后处理或抓取效果读取。比如Unity的Post Processing深度效果会参考RenderType来决定是否处理该物体一些自定义渲染特性也依赖它做分类。如果你做的是风格化项目建议保持一致不要随意改动这个Tag。Cull Off是可选设置用来让树叶正反面都能显示。根据美术资产不同有的树叶模型本身是双面片状需要Cull Off。但要注意双面会导致片元着色器执行翻倍而且在阴影投射里双面也可能带来半透物体投影过大问题。所以代码里我用了一个开关实际使用要按资源来调不要无脑开。3.4 配合URP的SRP Batcher与CBuffer上面代码里采用了CBUFFER_START(UnityPerMaterial)包装所有材质属性。SRP Batcher要求所有UnityEngine.Material属性必须放在名为UnityPerMaterial的CBuffer里且Shader中不能有在SRP Batcher规则外的非CBuffer变量访问。如果写的是URP Lit那样的大ShaderURP已经在内部帮你封装了大部分但在自写Shader里必须注意这点否则性能会打折扣特别是场景里出现大量需要独立材质的树叶时。另外我发现很多人在自写Shader时容易忘掉给_MainTex_ST声明。这个变量是服务于平铺和偏移Tiling/Offset的Unity会把它和材质属性绑定。如果没有TEXTURE2DSAMPLER的宏方式而是直接用sampler2D在URP里依然能用但不是SoAStructure of Arrays布局下的最佳实践。为了匹配新的API和SRP Batcher建议统一使用URP的采样宏。3.5 阴影作为透明对象时的处理透明度测试物体如果只写ForwardPass投射阴影时会按“整个模型的不透明正方体”来生成阴影典型的错误表现是地面出现一整块树冠矩形阴影而不是镂空树叶的阴影。解决办法就是上面写的ShadowCaster Pass并且在里面采样同一张贴图、同样执行clip。这样在渲染阴影贴图时阴影深度信息里也会正确抠掉Alpha小于阈值的部分。需要注意的是ShadowCaster Pass中几乎不需要光照结果通常是一个极简的深度输出。但如果你使用URP的GetVertexPositionInputs和阴影相关的宏可能还要在Attributes中传入“正常化的法线”等。实际项目中树叶这类模型可以不做法线计算只需位置和UV代码更省。另外还有一个小细节如果物体启用了“Receive Shadows”那它本身作为被遮挡方接收阴影时URP会在Forward Pass里根据主光阴影贴图计算结果。我的示例里的#pragma multi_compile就是为了让GetMainLight能正确读取阴影数据。如果完全不需要接收阴影可以去掉相关宏减少变体数量。4. 关键细节与坑为什么别人写的树叶总是怪怪的4.1 Cull Off与双面剔除的取舍许多树叶和草面是单面片状它们的背面本来就不可见打开Cull Back默认可以省一半工作量。但树枝叶片会因为视角旋转而穿帮两面都能看到时又必须Cull Off。这里推荐一个经验如果模型是双面交叉的草片Cull Off如果是单面朝向的板片保持Cull Back。开启Cull Off后每个像素会执行两次片元着色正面和背面各一次如果着色器里还有光照、阴影性能开销直接翻倍。处于同一棵树的多个纹理页Atlas包含不同朝向叶片时最好每个材质单独设置双面属性而不是全部统一。另外开启Cull Off后阴影投射Pass同样会双面都写深度这可能导致阴影贴图里叶片密度和实体形态不一致纹理解析度不够时容易产生奇怪的条纹。遇到这类问题可以单独在ShadowCaster Pass强制Cull Back任其正面渲染。4.2 边缘硬锯齿与平台差异MSAA、SSAA透明度测试天生产生硬边这个“边”完全依赖几何覆盖率和纹理采样结果没有抗锯齿处理时放大后边缘会非常粗糙像狗啃的一样。常说的Solution有几种一是用MipMapAlpha贴图预过滤让边缘Alpha下降配合低Cutoff形成一定程度的“伪柔化”二是升级为带噪声扰动的Alpha Test对uv或clip阈值做轻微扰动让边缘的阶梯感变成随机颗粒感掩盖锯齿三是直接改用透明度混合但排序和深度问题接踵而至。平台差异还体现在纹理采样上。PC上常见的Bilinear采样和移动端部分硬件的定点/线性过滤存在细微差别同一张贴图同一Cutoff值在安卓和PC上可能呈现完全不同的镂空轮廓。我遇到过真机上图样模糊一圈而编辑器里正常的案例。排查时除了看压缩格式还要确认纹理的“Alpha Is Transparency”和“Generate Mip Maps”是否打开。注意只要缩放过MIP Level切换会让Cutoff需要微调。4.3 被透明测试遮挡的物体“消失”问题透明度测试片元被丢弃后该像素的深度不会被写入。这意味着当一个镂空物体位于另一个物体前方时后方物体本应显示的部分仍然显示。从渲染效果看是对的但有些人误以为“被裁掉的区域应该显示背景可是没写深度导致后方的半透明物体排序会出问题”。这里要区分出不透明物体和半透明物体后面的不透明物体正常写入深度镂空物体的空区域透过去当然能看到它们没问题。但如果后方有半透明物体渲染顺序可能需要特殊处理。另一个典型bug是开启了深度预写Depth Prepass后透明测试物体的深度在Prepass阶段已被写入如果Prepass的实现里没有采样纹理做clip那么整个物体的完整几何就会挡在最前面后面的物体被错误遮住。为了绕过URP的Depth Prepass Pass里也必须包含同样的Alpha Test逻辑。URP的Lit Shader内部做了这件事但自写Shader如果不加一个DepthOnlyPass就需要在设置里关掉Opaque Depth Prepass或者保证Prepass按“实体裁剪”执行。4.4 与阴影接受/投射、后处理深度的交互透明度测试物体的深度数据本身是“离散的”因为被裁剪像素没有深度。在某些后处理效果如拟真景深、体积雾、AO中它们会使用深度缓冲和法线缓冲而镂空区域没有对应深度样本后处理算法可能会将深度拉平造成边缘模糊或黑圈。例如Screen Space Ambient Occlusion做SSAO时会把镂空区域的深度解释为背景深度叶子边缘形成一个“伪阴影”。这些后处理大多发生在屏幕空间只依赖深度纹理和颜色纹理很难精确区分真实场景的结构所以美术上经常要反复调后处理强度。接受阴影方面如果树叶是Unlit且不接受阴影可以在Forward Pass不开阴影采样宏可以节省不少GPU周期。但如果树叶表面希望保留一定“明部/暗部”过渡而又不想用Lit可以在片元着色器里简单计算法线与方向光的方向点乘作为亮度系数乘到颜色上。这个做法虽然不够物理但用来表现叶子受光也不失为一种轻量手段。4.5 性能clip指令的频率与Overdraw控制很多移动端GPU对分支和discard并不友好。虽然clip是硬件指令但它意味着片元着色器不能过早断言是否有效可能会使一些GPU的Early-Z失效。因此对于大面积的草和树叶一个常用的优化是减少“无效片元”的数量让纹理边缘尽量贴合模型轮廓。模型上多几块面片比用一大张四边面去裁剪更划算。在URP项目中也可以通过Frame Debugger查看Overdraw和Draw Call。如果发现AlphaTest物体渲染耗时很高先看片元着色器里除了clip还做了什么再去检查纹理是否带MipMap和压缩。不要一上来就怀疑是透明度测试的问题有时只是纹理采样采样倍数太高比如各向异性过滤拉满带来了额外开销。5. 常见问题速查表与实测记录5.1 问题表格与解决思路现象可能原因解决思路透明区域显示为黑色纹理未导入Alpha通道或通道未标记为透明检查纹理类型和Alpha Is Transparency选项整个物体被丢弃一片Clip阈值过高拉低Cutoff到0.20.5边缘出现亮色或白色光边边缘Alpha残留或压缩伪影调整纹理边缘Alpha重新导出调整阈值物体投射矩形阴影缺少ShadowCaster Pass代码里增加带clip的ShadowCaster Pass双面叶子背面展示时反而穿帮Cull模式错误根据模型实际面朝向设置Cull叶子面向镜头时出现大量重叠闪烁深度冲突/排序错误检查Queue与ZWrite必要时设置ZWrite Off但不建议真机比编辑器边缘碎纹理压缩后Alpha变化使用ASTC或ETC2并微调Cutoff测试不同mipmap水平星光照时透明度测试物体不接收阴影缺阴影宏或光照路径没走引入URP阴影相关multi_compile并在shader中调用GetMainLightAlpha Test后无法被后处理正确识别RenderType标签不匹配确认RenderTypeTransparentCutout和相关深度处理兼容5.2 实测同一模型在不同平台的滚动条差异我用一个由2万面组成的十字交叉草叶模型在PCNVIDIA RTX 3060与华为MatePad 11上测试同贴图和Cutoff0.5。PC端帧率稳定边缘轮廓清晰。在HarmonyOS平板上则出现明显的边缘抖动和局部碎点排查后发现是ETC2的Alpha压缩损失了部分细节而PC上使用BC7则几乎无感知。将Cutoff降为0.45后碎点数量减少但还是能看出边缘有些毛糙。最终方案是把草叶纹理的Alpha区域边缘做了约2像素的羽化并开启“Alpha Is Transparency”才在真机上获得可接受的结果。这说明“相同Shader在不同平台表现不一致”不完全属于代码问题纹理格式的质量差异同样不可忽略。此外MipMap在草地下沉时会急剧降低Alpha分辨率。如果你在远处看到草丛像一堆垃圾噪点大概率是MipMap生成的Alpha边缘不够干净可以尝试调整MipMap的偏置或关闭MipMap但会增加带宽。5.3 调试透明测试的利器RenderDoc与帧调试器大多数时候渲染问题不显眼但排查起来很费劲。建议安装RenderDoc插件直接在Editor里就能抓帧。RenderDoc可以查看每个Pass的输入输出、Shader当前的常量缓冲区、纹理采样结果和深度缓冲能清楚地看到clip丢弃前后的像素状态变化。尤其在怀疑阴影Pass或Depth Prepass时RenderDoc能直接让我看到Shadow Map生成过程中哪些片元被丢弃了、哪些因为在阈值以下没写入深度。Unity自身的Frame Debugger对URP支持很差尤其在查看逐Pass的话有很多优化后的信息丢失。但配合RenderDoc的“Mesh Pipeline”面板可以看到顶点着色器输出的裁剪区域和光栅化覆盖对于判断是不是“裁剪变成了全屏四边形”这类问题非常有帮助。加上RenderDoc内置的API级调试能直观对比编辑器与真机行为的差异。如果设备不支持RenderDoc抓帧可以用Adreno Profiler或者XCode的Metal Debugger看纹理采样和统计信息但移动端帧调试毕竟麻烦最好在项目能跑Editor时先解决大部分问题再拿到真机上复测。结尾一点个人体会透明度测试在图形API里属于古老又基础的功能但它和URP的各种新管线特性碰撞后细节问题比想象的更多。我个人的经验是写透明裁剪Shader时先别急着堆功能把“采样Alpha、clip、阴影投射、DepthOnly、SRP Batcher兼容”这五件事想清楚后续材质就能少折腾一半。特别是URP的Depth Prepass如果你渲染顺序乱镂空区域经常出现意料之外的遮挡这块一定要提前想好。最后分享一个可以立刻用上的小技巧当你的树叶边缘出现明显锯齿时不一定要马上换材质方案。试着在片元着色器里给UV加一个由世界坐标计算出来的噪声扰动让阈值在一个极小范围内随机变化。这样硬边会散成颗粒边缘视觉上要比直线锯齿顺眼不少。当然这个方法会增加少量像素运算胜在纯Shader内解决不需要额外美术资源。如果你正好卡在透明裁剪的边缘质量上建议先试这一招。