ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Unity Shader全透明与半透明:渲染队列、混合与阴影避坑指南

Unity Shader全透明与半透明:渲染队列、混合与阴影避坑指南 第一次在 Unity 里写 Shader十有八九是从透明开始的。原因不复杂全透明和半透明效果几乎是每个项目绕不开的需求——血条掉血后的淡出、玻璃幕墙、技能指示圈、角色被墙体挡住时变虚、幽灵怪物的虚影、UI 圆角遮罩甚至数字孪生项目里那些半透明的建筑轮廓。可真动手写的时候你会发现改一行 Blend 就能出效果改错一个 ZWrite 就满屏闪烁帧数还莫名其妙掉了十几帧。我见过太多人在这里翻车明明贴图 alpha 是好的物体就是不透明两个半透明物体叠在一起后面的竟然跑到前面去模型边缘一圈黑边怎么调都去不掉。这篇文章就把 Unity Shader 里全透明与半透明这两件事从头拆一遍包括渲染队列的排序逻辑、混合因子的数学含义、AlphaTest 裁切与阴影投射的取舍、URP 与内置管线的写法差异以及我自己踩过并记下来的十几条坑。不管你刚打开 Shader 编辑器还是已经写过几个能跑但说不清原理的 Shader下面这些内容应该都能用上。1. 先分清三种透明否则代码写到一半必然混乱1.1 全透明、半透明、裁切透明在管线里根本不是一回事新手最容易犯的认知错误是把透明当成一个属性去理解。实际上在 GPU 眼里只有两条完全不同的路径。第一条路径是完全不透明渲染。像素写进颜色缓冲区同时写进深度缓冲区Z-Buffer。深度缓冲区记录了每个像素离相机多远后续像素要想覆盖它必须通过深度测试。这条路稳定、快速顺序无关画一百个不透明物体谁先谁后结果都一样。第二条路径是混合渲染。像素不会直接覆盖旧颜色而是按一个公式和已有颜色做加权求和。半透明就是走这条路。混合渲染默认不写深度或者写了会出大乱子所以它天生依赖排序——谁先画谁后画结果完全不同。而全透明这个概念在实际项目里通常指三种东西我不建议混着叫视觉上完全看不见alpha 通道为 0或者干脆不渲染。硬边裁切AlphaTest像素要么完全保留要么完全丢弃没有中间状态。树叶、铁网、破布、草叶全靠这个。半透明的极端情况alpha 接近 0 的半透明。这三种需求对应的实现方式、渲染队列、阴影行为和性能开销都不一样。我见过有人为了做一片树叶用了混合半透明结果一张叶子贴图导致整个场景排序错乱帧数腰斩。所以第一件事先想清楚你要的是哪一种。提示凡是需要边缘清晰、不需要渐变过渡、并且要投射或接收阴影的透明需求优先考虑 AlphaTest 裁切而不是 Alpha Blend。1.2 渲染队列与 RenderType顺序比混合公式更重要Unity 的渲染顺序由材质上的Queue标签决定它是一个整数数值越小越先渲染。内置的几个关键档位队列名称数值典型用途Background1000天空盒、远景Geometry2000默认绝大多数不透明物体AlphaTest2450硬边裁切物体GeometryLast2500不透明与透明之间的分界线Transparent3000半透明物体Overlay4000镜头光晕、UI 特效这个分界线很重要。2500 之前的物体先渲染并写入深度2500 之后的物体后渲染且默认不写深度。半透明物体之所以能正确显示在不透明物体前面正是因为不透明物体先画完了深度信息已经在缓冲区里半透明物体做深度测试时读到正确结果被挡住的部分自然被裁掉。RenderType是另一回事。它不参与实际排序主要作用是给着色器替换Shader Replacement做标记比如相机深度图渲染、某些后处理阶段需要把场景按类型重新画一遍。如果你希望自己的半透明物体能被深度图正确采集RenderType要写成Transparent如果是裁切物体写TransparentCutout。这两个标签经常被人忽略然后在做景深、软粒子、屏幕空间反射时出问题。还有一个经常被问到的点为什么我的半透明物体不显示八成是Queue没改还停留在 Geometry。此时它先于不透明物体渲染并写了深度后面的不透明物体直接把它盖住了。改队列是第一步改 Blend 是第二步顺序不能颠倒。2. 从零搭一个骨架Alpha 数据到底从哪来2.1 属性块设计透明度不要只依赖贴图很多人写透明 Shader直接把贴图的 alpha 拿来用然后发现没法从外部调整。工程上我一定会预留至少三个入口贴图自身的 alpha、材质颜色通道的 alpha、以及一个整体的透明度缩放系数。三者相乘才能在美术调效果和程序做动画时都有操作空间。Properties { _MainTex (基础贴图 (RGB), 2D) white {} _BaseColor (颜色 (RGBA), Color) (1,1,1,1) _AlphaScale (整体透明度, Range(0,1)) 1 _Cutoff (裁切阈值, Range(0,1)) 0.5 }这里有个容易被忽略的细节_MainTex的默认值写white而不是white{}是因为默认值后面那个花括号格式在某些版本里会被解析成固定值。稳妥写法是只写white。另外Unity 里Color类型属性在材质面板上暴露 RGBA 四个通道而Vector类型也会暴露四个分量但Color会带上颜色选择器也更容易被 HDR 相关逻辑识别。要改颜色就用Color要传方向或者自定义参数就用Vector。还有一个常被藏起来的 alpha 来源是顶点色。模型导入时如果勾选了顶点色FBX 里的 vertex color 会带进 Shader。做植被、做粒子、做程序化生成的网格时顶点色 alpha 是控制淡出的天然通道比贴图省采样比 Uniform 灵活。我的习惯是把顶点色也乘进去相当于留了一条后备通路。注意贴图导入设置里Alpha Source如果被设成None你采到的 alpha 恒为 1怎么写都不会透明。这个问题在接手别人项目时极其常见排查顺序上应该排在改 Shader 之前。2.2 最小可用的顶点片元实现下面这份是内置渲染管线下的基础版本我把它当成模板用每做一个透明需求就在上面加东西。Shader Custom/Blog/TransparentSimple { Properties { _MainTex (基础贴图 (RGB), 2D) white {} _BaseColor (颜色 (RGBA), Color) (1,1,1,1) _AlphaScale (整体透明度, Range(0,1)) 1 } SubShader { Tags { Queue Transparent RenderType Transparent IgnoreProjector True } LOD 100 Blend SrcAlpha OneMinusSrcAlpha ZWrite Off Cull Back Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float4 color : COLOR; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float4 color : COLOR; }; sampler2D _MainTex; float4 _MainTex_ST; fixed4 _BaseColor; float _AlphaScale; v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); o.color v.color; return o; } fixed4 frag (v2f i) : SV_Target { fixed4 tex tex2D(_MainTex, i.uv); fixed4 col tex * _BaseColor * i.color; col.a * _AlphaScale; return col; } ENDCG } } FallBack Transparent/VertexLit }这份代码有三处值得说。第一v2f里我保留了顶点色并传到片元虽然最终乘进去了但如果你要接光照这个通道可以换成第二套 UV。第二_MainTex_ST配合TRANSFORM_TEX宏是为了让材质的 Tiling 和 Offset 生效新手常见错误是直接tex2D(_MainTex, v.uv)结果美术在面板上调平铺完全没反应。第三FallBack Transparent/VertexLit不只是兜底它同时给这个 Shader 提供了阴影投射能力这一点在第 4 节会展开。至于LOD 100它和渲染细节层级有关一般写 100 或 200数值越大代表越精细低端设备可能跳过。现在移动平台基本不太依赖 LOD但写上无害。3. 半透明的混合方案与那笔不能不算的数学账3.1 Blend 因子的数学含义与常见组合对照Blend指令的完整形式是Blend SrcFactor DstFactor它执行的运算可以写成最终颜色 源颜色 × SrcFactor 目标颜色 × DstFactor这里的源是当前正在画的像素目标是颜色缓冲区里已经有的像素。每个 Factor 都可以取One、Zero、SrcColor、SrcAlpha、OneMinusSrcColor、OneMinusSrcAlpha、DstColor、DstAlpha等等。拿最常用的Blend SrcAlpha OneMinusSrcAlpha展开就是最终颜色 源颜色 × 源Alpha 目标颜色 × (1 - 源Alpha)透明度 0.7 时新像素占七成旧像素占三成视觉上就是一层七成不透明的膜。这就是传统 Alpha 混合。几个必须记住的组合Blend 写法展开后的效果典型用途Blend SrcAlpha OneMinusSrcAlpha标准 Alpha 混合玻璃、水面、技能范围Blend One OneMinusSrcAlpha预乘 Alpha 混合粒子、火焰边缘更干净Blend One One纯加法叠加光晕、激光、能量场Blend DstColor Zero正片叠底假阴影、贴花变暗Blend SrcAlpha One亮部叠加高光增强、发光边缘Blend Zero One源完全不参与完全不可见但保留绘制关于预乘 AlphaPremultiplied Alpha这里多说两句因为它直接决定了边缘黑边问题。普通混合公式里源颜色会先乘一次自己的 alpha。如果你的贴图在导出时 RGB 通道已经乘过 alpha 了Shader 里再乘一次边缘就会偏暗、发灰。反过来如果贴图没预乘你用了Blend One OneMinusSrcAlpha边缘就会发白、发亮。两边一定要对齐。粒子系统导出的贴图一般是预乘的而 Photoshop 直接导出的 PNG 通常不是。3.2 关掉 ZWrite 之后到底发生了什么几乎所有半透明 Shader 都会写ZWrite Off。为什么因为半透明物体如果写了深度它后面的其他半透明物体在做深度测试时会被直接拒绝导致只能看见最前面那层。把深度写入关掉后面的层才有机会继续混合上来。但代价也很直接深度缓冲区里没有半透明物体的信息所以它自己内部的面、以及它与其他半透明物体之间的前后关系完全靠渲染顺序决定。Unity 对同一队列内的物体按什么排序按渲染器包围盒中心点到摄像机的距离从远到近依次绘制。这个细节值得单独拎出来讲因为它是很多排序看起来随机问题的根源。包围盒中心是一个近似值。一个又长又扁的墙面它的包围盒中心可能离摄像机很近但实际可见像素离摄像机很远一堆散落在地上的碎片各自中心点的远近顺序和你眼睛看到的像素级遮挡关系可能完全对不上。于是就会出现经典的画面两个半透明玻璃板交叉叠放转动相机的时候哪块在前面会突然跳变一下。能不能修能但不能靠 Shader 单独解决。可选手段有三类一是手动设置Renderer.sortingOrder用脚本强行指定绘制顺序代价是失去自动适应相机角度的能力二是把大物体拆成多个小物体让包围盒中心更贴近真实几何分布三是用 Sorting Group 组件把一组对象当成一个整体参与排序。这三种方案我在项目里都用过第一种适合固定视角的展厅场景第二种适合建筑玻璃幕墙第三种适合角色身上挂一堆装饰件的场合。提示Unity 的透明排序是逐物体级别的不存在逐像素排序。想要严格正确的逐像素透明只有深度剥离、OIT 这类高级方案移动端基本不用考虑。3.3 双面半透明的正确打开方式Cull Back是默认值只渲染正面朝向相机的三角面。玻璃、树叶、窗帘这类物体两面都要看得见需要Cull Off。直接Cull Off会带来一个问题一个封闭模型的正面和背面在同一个 Pass 里绘制片元处理的先后顺序由三角面在索引缓冲里的位置决定结果就是某些角度下背面盖住了正面看起来脏兮兮的。工程上的做法是拆成两个 Pass用Cull Back和Cull Front分别渲染并在 Pass 上标注Tags { LightMode Always }之外还要靠渲染顺序保证背面先画、正面后画。因为第二个 Pass 在同一 SubShader 内默认排在后面先画背面、再画正面混合结果就稳定了。Pass { Cull Front Blend SrcAlpha OneMinusSrcAlpha ZWrite Off // 背面渲染逻辑 } Pass { Cull Back Blend SrcAlpha OneMinusSrcAlpha ZWrite Off // 正面渲染逻辑 }代价是两倍片元开销。如果物体本身很复杂、屏幕上占的面积又大这笔账要算清楚。我的经验是薄片状的物体树叶、纸片、窗帘用Cull Off单 Pass 就够因为正背面几乎重合看不出顺序问题有厚度的封闭体玻璃瓶、水箱才值得上双 Pass。4. 全透明与裁切看不见的东西也要管好阴影4.1 用 clip 裁切实现硬边透明AlphaTest 的核心就一行代码clip(tex.a - _Cutoff);clip函数接收一个标量小于 0 就丢弃当前片元等于整条像素流水线直接终止不产生颜色输出。这意味着两件事一是被裁掉的像素不参与混合边缘是硬的没有渐变二是被保留的像素依然可以写深度、依然可以被主光源的阴影渲染阶段处理。相比混合半透明裁切方式的优势非常明显它走的是不透明渲染路径排序无关、支持深度写入、能投射阴影、能被深度图采集。所以树叶、草地、铁栅栏这类东西几乎是标准答案。参数上_Cutoff一般给 0.5但这个值不是随便定的。它和贴图的 alpha 分布强相关。如果贴图边缘做了抗锯齿alpha 从 0 平滑过渡到 1 的区域比较宽这时候_Cutoff给 0.3 会让边缘更胖给 0.7 会让边缘更瘦。我通常建议美术把 alpha 做成二值加一点过渡然后_Cutoff卡在 0.4 到 0.6 之间。RenderType和Queue也要对应改掉Tags { Queue AlphaTest RenderType TransparentCutout IgnoreProjector True }队列用 2450排在所有不透明物体之后、半透明物体之前。这个位置的好处是它既能正确遮挡更远的不透明物体又不会把半透明物体挡住。还有一个和裁切相关的开关AlphaToMask On。它把 alpha 转成多重采样覆盖率掩码在开启 MSAA 的设备上能免费拿到一层抗锯齿边缘。代价是需要 MSAA 支持而且在没有 MSAA 的平台上完全无效。移动端如果开了 4x MSAA这个开关很值得加边缘会明显干净。4.2 阴影投射为什么你的透明物体没有影子默认情况下处于 Transparent 队列、ZWrite Off的半透明物体是不投射阴影的。原因在于阴影贴图的渲染阶段Unity 会用主光源的视角重新绘制一遍场景深度写入是必须的而半透明物体正好关掉了深度写入。那怎么让裁切物体投射正确形状的阴影答案是让阴影渲染阶段走一遍你的裁切逻辑。有两种做法。第一种是写一个自定义的ShadowCasterPass。这个 Pass 的LightMode标签必须是ShadowCaster并且内部要执行同样的clip裁切。这样阴影贴图里被裁掉的区域就是空洞阴影形状与视觉形状一致。Pass { Tags { LightMode ShadowCaster } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_shadowcaster struct v2f { V2F_SHADOW_CASTER; float2 uv : TEXCOORD1; }; sampler2D _MainTex; float4 _MainTex_ST; fixed _Cutoff; v2f vert(appdata_base v) { v2f o; TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) o.uv TRANSFORM_TEX(v.texcoord, _MainTex); return o; } float4 frag(v2f i) : SV_Target { fixed4 tex tex2D(_MainTex, i.uv); clip(tex.a - _Cutoff); SHADOW_CASTER_FRAGMENT(i) } ENDCG }第二种更省事直接FallBack Transparent/Cutout/VertexLit或者FallBack Legacy Shaders/Transparent/Cutout/Diffuse。FallBack 会自动引入目标 Shader 里的 ShadowCaster Pass。但要注意FallBack 引入的裁切阈值用的是它自己的_Cutoff如果你的属性名不一样就对不上号。所以更稳的做法是自定义 Pass。至于半透明物体要不要投影这是个设计取舍。物理上半透明物体确实会产生柔和的影子但实时渲染里做柔影代价很高。常见的做法是干脆不投或者用一个低模加DstColor Zero的假阴影贴片替代。我在几个项目里试过自定义 ShadowCaster 加抖动dither来模拟半透明阴影效果能看但边缘会有颗粒感而且抖动的图案在不同的阴影分辨率下表现差异很大后来放弃了。注意Queue值对阴影影响很大。如果半透明物体因为某些原因把Queue改回了 2000 并保留ZWrite Off它在阴影贴图里依然不会写入深度影子还是没有。想投影要么用裁切路径要么在 ShadowCaster Pass 里强制ZWrite On。4.3 完全不可见的三种实现方式与适用场景回到全透明最朴素的理解让物体彻底看不见。至少有三种做法它们的行为完全不同。第一种Blend Zero One。源颜色的任何分量都不参与颜色缓冲区保持原样。这个物体依然参与顶点处理、光栅化、片元着色只是结果被丢弃。GPU 开销一点没省。它适合那种需要保持渲染器激活状态但不能显示的场合比如某些依赖 Renderer 参与排序或者需要被射线检测的物体。第二种把 alpha 缩放到 0。视觉上等同第一种但如果你在 Shader 里做了裁剪或者深度写入副作用会不一样而且它依然会参与透明排序可能影响其他半透明物体的绘制顺序。第三种直接禁用渲染器。renderer.enabled false或者gameObject.SetActive(false)。这是唯一真正节省开销的做法因为整个物体从渲染列表里移除了。代价是重新启用时有激活成本而且如果这个物体承担了脚本逻辑、碰撞体、动画状态机直接关掉会有副作用。我的选择顺序是如果有脚本逻辑挂在上面只关 Renderer用renderer.enabled false如果纯粹是视觉表现用SetActive(false)。至于用 Shader 做的淡出动画那是 Transparent 队列加 alpha 插值的事属于半透明范畴别和隐藏混为一谈。顺带说一个常见的错误做法用Blend Zero One加Queue Overlay来做隐藏但保留阴影。这个思路本身有问题Overlay 队列在所有东西之后深度写入也关了阴影更不可能产生。想保留阴影又看不见本体正确路径是裁切或者单独写一个只投阴影的裁剪版模型。5. 进阶实务URP 写法差异、软淡出与性能底线5.1 内置管线与 URP 的写法对照如果你从内置管线转向 URP会发现原来的 Surface Shader 完全不能用。URP 用的是 HLSL 加上一整套自己的光照函数透明写法上有几个必须知道的差异点。第一SubShader的标签里要标注渲染管线Tags { RenderPipeline UniversalPipeline Queue Transparent RenderType Transparent }第二片元函数返回的通常是half4并且 URP 的透明混合需要在 Shader 里显式声明Blend SrcAlpha OneMinusSrcAlpha和ZWrite Off这一点和内置管线一致但由于 URP 的渲染顺序由 Renderer 配置控制材质队列仍然起作用所以队列不能乱写。第三URP 里Surface类型的透明 Shader 换成Unlit或Lit的 Shader Graph 更方便。Shader Graph 的 Graph Settings 里有Surface TypeOpaque / Transparent和Blending ModeAlpha / Premultiply / Additive / Multiply勾上 Transparent 之后会自动帮你生成对应的 Blend 指令。它的Alpha Clipping开关则对应裁切透明还能同时打开Cast Shadows。对于不熟 Shader 语法的同学Shader Graph 是更稳妥的入门路径。第四深度写入。URP 在 Shader Graph 的 Graph Settings 里有一个Depth Write选项勾选之后半透明物体也会写深度。这个选项要慎用它会让后面的半透明物体被剔除通常只在做特殊效果时打开。我自己的习惯是简单效果和移动端用 Shader Graph逻辑复杂或者需要接自定义光照的用 HLSL 手写两者可以混在同一个项目里。5.2 相机距离淡出解决穿模的实用技巧角色被墙体遮挡时变虚、第三人称镜头贴近角色时模型消失这两个需求本质上是一个根据相机距离控制透明度。这是半透明最常见的工程化应用。核心就几行float3 worldPos mul(unity_ObjectToWorld, v.vertex).xyz; float dist distance(_WorldSpaceCameraPos, worldPos); float fade saturate((dist - _FadeStart) / max(_FadeEnd - _FadeStart, 0.001)); col.a * fade;_FadeStart和_FadeEnd是两个外部参数。假设设为 1.0 和 3.0含义是距离小于 1 米时完全透明距离大于 3 米时完全不透明中间线性过渡。这里有个关键选择用顶点距离还是像素距离。上面这段在顶点着色器里算逐顶点插值开销低但对于大面积的模型比如一面墙顶点稀疏会导致过渡不均匀。要更平滑就把这段挪到片元着色器用世界坐标算像素级距离代价是每像素一次世界坐标插值和一次开方。另外用相机距离判断会有一个隐含问题相机在物体正前方和侧后方的距离可能一样但遮挡关系完全不同。真正精准的被挡住才透明需要相机到物体的射线检测或者读取深度图做比较。前者用Physics.Linecast从相机打到角色命中则降低 alpha后者在 Shader 里采样_CameraDepthTexture与自身深度比较。射线检测的方式更常用于角色因为它可以做逐部件的判断而且能配合多个遮挡点做平滑过渡。提示做距离淡出时Queue一定要设在 Transparent。另外淡出过程中物体的阴影如果还在会出现人透明了影子还在的穿帮所以要么关掉Cast Shadows要么接受这个物理上合理的表现。5.3 透明物体的性能红线与优化清单半透明是移动端最容易翻车的地方因为它是fill rate 密集型。一块 1080P 屏幕上铺满的半透明画面片元数量可能是不透明场景的三四倍而这部分开销在 Adreno 和 Mali 上非常敏感。几个我反复验证过的优化点控制屏幕占比。一个全屏半透明特效的代价可能比整个场景所有不透明物体加起来还高。安全检查的红色遮罩、全屏泛光、天气效果这些尽量用后处理做而不是用一个巨大的透明面片贴在相机前面。减少重叠层数。三层半透明叠在一起GPU 就要算三次混合。能用一层解决的绝不用两层。粒子系统尤其容易失控发射器叠发射器一秒钟能堆出十几层。优先用裁切代替混合。能裁切的场景一律用裁切它走不透明路径深度写入正常排序免费还能被遮挡剔除Occlusion Culling正确处理。半透明物体在遮挡剔除上表现很差因为它们的包围盒通常很大。控制贴图分辨率与采样次数。透明特效常常叠了很多层贴图采样移动端一个片元超过 4 次采样就要警惕。避免在透明 Shader 里做复杂光照。半透明物体本身就不需要精确光照用近似的假光照或者简单的菲涅尔边缘光就能出效果别上完整 PBR。限帧和分辨率缩放。如果项目允许透明效果密集的场景可以单独降低渲染分辨率这个在 URP 的 Render Scale 里能配。还有一条容易被忽略的Color Mask。如果某个 Pass 只关心 alpha 通道比如某些后处理遮罩可以写ColorMask A只写 alpha省掉 RGB 通道的带宽。这个技巧在移动端的高填充率场景里能省下可观的带宽。6. 踩坑实录透明效果的常见问题速查6.1 问题速查表下面这张表基本覆盖了我在实际项目里被问过和踩过的所有透明问题遇到症状先查表效率比盲改高得多。症状最可能的原因排查方向物体完全不透明alpha 没反应队列仍是 Geometry改Queue为 Transparent采到的 alpha 恒为 1贴图 Alpha Source 设置为 None改贴图导入设置半透明物体看不见被不透明物体盖住或 Blend 写法错误检查队列和 Blend 因子多个半透明物体排序跳变包围盒中心排序不精确调 sortingOrder 或拆分物体半透明物体内部面片闪烁同一物体内部排序不稳定拆 Pass 或减小几何重叠边缘一圈黑边贴图预乘与 Blend 模式不匹配换Blend One OneMinusSrcAlpha或改用未预乘贴图边缘一圈白边未预乘却用了预乘混合同上反向修正半透明物体没有阴影透明队列默认不写深度加自定义 ShadowCaster Pass半透明物体后方的物体被裁掉ZWrite 被误开检查ZWrite Off裁切边缘锯齿严重没有 MSAA 或 AlphaToMask 未开开 MSAA 与AlphaToMask On裁切物体轮廓和贴图对不上阴影 Pass 没有执行同样的 clip在 ShadowCaster 里补 clip相机靠近物体内部看到穿帮未做距离淡出或 Cull 设置不当加距离淡出逻辑透明物体在深度图里消失RenderType 标记不正确改为 Transparent / TransparentCutout6.2 几个只有真正踩过才知道的细节第一Alpha 通道被后处理毁掉。项目开了 HDR 或者用了某些后处理栈之后颜色缓冲区的 alpha 会被后处理重写导致依赖 alpha 做混合的 Shader 表现异常。如果你在 URP 里发现透明效果在开启后处理后忽然变样先怀疑这个。解决办法是让颜色混合不依赖最终缓冲区的 alpha或者把透明效果移到后处理之前。第二GPU 实例化和透明排序是矛盾的。开启 GPU Instancing 之后同一材质的多个透明物体可能被合并成一次绘制调用这会让 Unity 的逐物体排序失效。表现是原来还能勉强排序的物体突然全乱了。这种场合就要关掉 Instancing或者接受排序问题。第三ZWrite Off加ColorMask 0是一个很有用的技巧。如果你需要某个物体参与深度测试但不产生可见颜色用ColorMask 0加ZWrite On可以只写深度不写颜色用来做遮挡剔除或者遮罩区域非常合适。这个技巧在做隐藏在某个区域内才显示的效果时特别有用。第四Shader 属性面板上的_Cutoff在裁切模式下最好限制范围。如果美术把它拖到 0整个物体就凭空消失了会被误判成 bug。常见的做法是用Range(0.01, 1)而不是Range(0, 1)或者干脆加一个Toggle属性控制是否启用裁切。第五做移动端时优先测低端机。透明效果在中高端设备上跑得飞起到了低端机上可能直接掉到 20 帧。原因是 fill rate 不足混合操作越多越致命。我的习惯是在开发早期就用一台低端机做性能对照而不是等到上线前才发现问题。第六透明物体的包围盒别乱改。有些项目为了提高剔除效率会手动修改 Mesh 的 bounds结果把透明排序彻底搞乱了因为排序就是按包围盒中心算的。改剔除边界之前先确认这个物体是不是透明的。第七同一个材质在不同相机上表现不同。场景里有多个相机主相机、UI 相机、小地图相机时透明物体可能在不同相机视角下排序完全不同因为排序是逐相机进行的。做小地图的时候如果发现透明物体错位这是正常现象不是 Shader 写错了。第八Shader Graph 生成的代码不能直接改。有些同学在 Shader Graph 里做不出想要的效果就点开生成的 Shader 代码手动改结果一保存就被覆盖。正确做法是把 Graph 导出成 Shader 之后再改或者干脆手写 HLSL。我一般对需要精细控制混合的场合直接手写Shader Graph 更适合做效果原型。最后分享一个我调试透明问题的惯用流程基本能覆盖九成情况先看队列对不对再看 Blend 因子和 ZWrite然后检查贴图 alpha 是否真的传进来了接着确认排序是不是问题最后才怀疑光照和阴影。这个顺序是从必然出错的简单项到偶发的复杂项按这个顺序排一遍比漫无目的地调参数快得多。至于那些更深的东西比如深度剥离、顺序无关透明、逐像素排序属于另一个量级的复杂度除非是做高端 PC 项目否则知道有这么回事就够了。
返回列表