资讯详情

资讯详情

Unity半透明剪影效果:ZTest Greater双Pass与Stencil方案实战

1. 半透明剪影效果的核心价值与场景拆解半透明剪影在Unity项目里算是一个看起来简单、做起来坑多的典型需求。我第一次接触这个效果是在做一个潜行类玩法原型的时候当时想让敌人在墙体后面呈现出一个模糊的轮廓玩家能隐约判断位置但又不能完全看清。后来发现这个需求在战术射击、恐怖解谜、RTS战争迷雾、甚至UI角色展示里都反复出现本质上就是一套遮挡关系可视化的方案。所谓半透明剪影核心诉求是当角色或物体被其他几何体遮挡时仍然能以某种半透明的、通常带颜色的轮廓形式显示在遮挡物之上。它和普通的X光透视不一样X光是把遮挡物变透明而剪影是把被遮挡物提到最前面来渲染同时保留遮挡的空间感。这个区别很关键直接决定了技术选型。适合读这篇内容的人大概分三类一是刚接触Shader Graph或手写Shader、想找一个完整案例练手的Unity开发者二是项目里明确有遮挡提示需求、需要快速落地方案的技术美术三是想理解ZTest、Stencil、RenderQueue这些渲染状态到底怎么配合使用的进阶学习者。不管你是哪一类我都会把每一步的参数为什么这么设讲清楚而不是丢一段代码让你自己猜。2. 技术方案选型与底层原理拆解2.1 为什么不用简单的透明材质很多人第一反应是给角色换一个半透明材质把Alpha调到0.3就完事了。实测下来这条路走不通原因有两个。第一普通半透明材质在渲染时会关闭深度写入ZWrite Off导致角色自身的各个面互相穿插、排序混乱看起来像一团糊掉的色块。第二也是最致命的半透明材质依然受深度测试ZTest LEqual约束被墙挡住的部分根本不会显示你看到的还是被裁掉的结果。所以真正的剪影效果必须解决一个矛盾既要让被遮挡的部分显示出来又要保持角色自身的正确遮挡关系。这就引出了两种主流思路。2.2 方案一双Pass ZTest Greater这是最经典也最可控的做法。给角色挂两个Pass第一个Pass用正常的ZTest LEqual渲染角色本体第二个Pass用ZTest Greater只渲染被遮挡的部分并输出半透明颜色。ZTest Greater的含义是只通过深度值比深度缓冲更大的片元翻译成人话就是只画那些被前面东西挡住的像素。这个逻辑非常优雅因为遮挡判断完全交给GPU的深度测试硬件来做不需要任何额外的射线检测或脚本计算。这个方案的优点是精确、性能好、和场景遮挡天然同步。缺点是需要处理两个Pass的渲染顺序以及半透明部分和本体之间的颜色叠加问题。2.3 方案二Stencil模板缓冲方案如果项目里已经有一套基于Stencil的渲染管线比如很多URP项目会用Stencil做描边、遮罩那用Stencil来做剪影会更统一。思路是遮挡物先写入一个Stencil标记角色渲染时读取这个标记命中标记的像素才输出剪影颜色。Stencil方案的好处是可控性极强你可以精确指定哪些物体参与遮挡计算比如只让墙壁遮挡、不让地板遮挡。但代价是需要改造遮挡物的材质如果场景里遮挡物种类很多维护成本会上升。而且Stencil是逐像素的位操作理解门槛比ZTest高一些。2.4 两种方案的对比与选择建议对比维度ZTest Greater双PassStencil模板方案实现复杂度低单Shader搞定中需改造遮挡物遮挡精确度依赖深度缓冲精确依赖标记可自定义性能开销多一个Pass的绘制多一次Stencil读写选择性遮挡不支持全场景生效支持可指定物体适用管线内置/URP/HDRP均可需管线支持Stencil维护成本低中高我的建议是如果你的需求就是被任何东西挡住都显示剪影直接用ZTest Greater方案五分钟能跑起来。如果需要只被特定层级的物体遮挡才显示或者项目里已经有Stencil基础设施再考虑方案二。下面我主要围绕方案一展开因为它是性价比最高的落地路径。3. 手写Shader实现完整流程3.1 创建Shader文件与基础结构在Project窗口右键Create Shader Unlit Shader命名成Silhouette。我习惯用Unlit而不是Surface Shader因为剪影不需要光照计算Unlit更轻量、更可控。打开后先把默认内容清掉搭出双Pass的骨架。Shader Custom/Silhouette { Properties { _MainTex (Main Texture, 2D) white {} _SilhouetteColor (Silhouette Color, Color) (1, 0.5, 0, 0.5) _SilhouetteIntensity (Silhouette Intensity, Range(0, 3)) 1.5 } SubShader { Tags { Queue Transparent RenderType Transparent } // Pass 1: 本体正常渲染 Pass { ZTest LEqual ZWrite On // ... 本体渲染逻辑 } // Pass 2: 剪影渲染 Pass { ZTest Greater ZWrite Off Blend SrcAlpha OneMinusSrcAlpha // ... 剪影渲染逻辑 } } }这里有几个关键点要说明。Queue设为Transparent是为了保证角色在不透明物体之后渲染这样深度缓冲里已经有墙壁的深度信息了ZTest Greater才能正确判断哪些像素被挡住。ZWrite在剪影Pass里必须关掉否则剪影会写入深度影响后续其他半透明物体的排序。3.2 本体Pass的编写要点本体Pass其实就是一个标准的纹理采样加颜色输出但要注意Alpha的处理。如果你希望本体是完全不透明的就把输出的Alpha设为1如果本体本身也带一点透明那就要处理好混合模式。Pass { ZTest LEqual ZWrite On Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc sampler2D _MainTex; float4 _MainTex_ST; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; 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 { fixed4 col tex2D(_MainTex, i.uv); return col; } ENDCG }3.3 剪影Pass的核心逻辑剪影Pass是整个效果的关键。它用ZTest Greater只渲染被遮挡的像素然后输出一个带透明度的纯色。这里有个细节如果你直接用纯色输出剪影会是一个实心的色块看起来比较生硬。更好的做法是结合菲涅尔效应让剪影边缘亮、中间暗形成一种发光轮廓的感觉。Pass { ZTest Greater ZWrite Off Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc float4 _SilhouetteColor; float _SilhouetteIntensity; struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 viewDir : TEXCOORD1; }; v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.worldNormal UnityObjectToWorldNormal(v.normal); o.viewDir normalize(WorldSpaceViewDir(v.vertex)); return o; } fixed4 frag (v2f i) : SV_Target { float fresnel 1.0 - saturate(dot(normalize(i.worldNormal), normalize(i.viewDir))); fresnel pow(fresnel, 2.0); float alpha _SilhouetteColor.a * fresnel * _SilhouetteIntensity; return fixed4(_SilhouetteColor.rgb, alpha); } ENDCG }菲涅尔的计算逻辑是法线和视线方向越垂直点积越小说明这个面越侧对摄像机也就是轮廓边缘fresnel值越大。pow(fresnel, 2.0)是为了让边缘过渡更锐利一些这个指数你可以根据美术需求调2到4之间比较常见。3.4 参数调节与视觉效果打磨把Shader挂到材质上之后你会看到几个可调参数。_SilhouetteColor的Alpha控制剪影整体透明度建议从0.5开始试。_SilhouetteIntensity是强度倍率用来补偿菲涅尔导致的整体偏暗1.5到2.5之间比较合适。实测下来有个坑如果角色模型的面数很低菲涅尔效果会显得很块状因为法线变化不够平滑。这时候要么让美术加面数要么把fresnel的pow指数降到1.0让过渡更柔和。另外如果角色有多个材质槽记得每个材质都要换成这个Shader否则会出现部分身体有剪影、部分没有的割裂感。4. 渲染顺序与深度问题的实战排查4.1 为什么剪影会盖住不该盖的东西最常见的bug是剪影不仅显示在墙壁后面还显示在了角色自己前面。比如角色的手臂在身体前面结果手臂也被渲染成了剪影。这是因为ZTest Greater判断的是比深度缓冲大而角色本体在Pass 1已经写入了深度Pass 2渲染时手臂的深度和身体前表面的深度比较如果手臂在身体后面它就会被判定为被遮挡从而错误地显示剪影。解决这个问题的标准做法是在Pass 2里也做一次自身深度比较但方向相反。更简单的办法是给角色加一个专门的遮挡层让Pass 2只和场景遮挡物比较。不过最省事的方案是接受这个瑕疵因为大多数情况下角色自身的穿插并不明显而且剪影本身是半透明的视觉上不会太突兀。4.2 RenderQueue的排序陷阱Unity的渲染顺序由Queue值决定数值越小越先渲染。不透明物体默认是2000透明是3000。我们的Shader设成Transparent3000意味着它在所有不透明物体之后渲染这是对的。但如果场景里有其他半透明物体比如玻璃、水面它们的Queue也是3000排序就取决于距离摄像机的远近。我踩过的一个坑是角色站在一块半透明玻璃后面结果剪影和玻璃的渲染顺序随机闪烁。原因是两者Queue相同Unity按包围盒中心距离排序而角色和玻璃的距离关系在移动中会变化。解决办法是把剪影的Queue设成3001强制它在普通半透明物体之后渲染。代价是剪影会盖在所有半透明物体上但对于遮挡提示这个需求来说这个取舍是合理的。4.3 深度缓冲精度导致的边缘闪烁在远距离或者大场景里你可能会看到剪影边缘有闪烁的噪点。这是深度缓冲精度不足导致的Z-Fighting。Unity默认的深度缓冲是24位在摄像机远裁剪面设得很大时比如1000近处的精度会下降。缓解办法有三个一是把摄像机的远裁剪面调小到实际需要的范围比如200二是开启摄像机的Reversed Z选项URP默认开启三是在Shader里给ZTest加一个小的偏移比如ZTest Greater [0.001]但这个偏移量需要反复测试太大又会导致剪影穿透。5. 性能优化与多平台适配经验5.1 移动端的Overdraw控制双Pass意味着角色被绘制了两次Overdraw直接翻倍。在移动端如果屏幕上有十几个带剪影的角色GPU填充率很容易成为瓶颈。我实测过一个场景20个剪影角色在中端手机上会让帧率从60掉到35左右。优化手段有几个。第一给剪影Pass加LOD控制距离远的角色直接关掉剪影Pass。第二用Shader LOD或者Keyword变体让低配设备走简化版去掉菲涅尔计算。第三如果剪影只是用来做提示可以考虑降低剪影Pass的渲染分辨率不过这个需要改管线成本较高。5.2 批处理与材质实例化每个角色的剪影颜色可能不同比如敌人红色、队友绿色如果每个颜色都创建一个材质就会打断合批。正确做法是用MaterialPropertyBlock来设置颜色这样所有角色可以共享同一个材质GPU Instancing也能生效。void SetSilhouetteColor(Renderer renderer, Color color) { MaterialPropertyBlock block new MaterialPropertyBlock(); renderer.GetPropertyBlock(block); block.SetColor(_SilhouetteColor, color); renderer.SetPropertyBlock(block); }注意MaterialPropertyBlock只对相同Shader的材质有效而且它不会创建新的材质实例所以不会增加Draw Call。这个技巧在需要大量同类型剪影的场景里非常实用。5.3 不同渲染管线的适配差异内置管线直接用上面的Shader就能跑。URP需要把Shader改成URP的写法主要是Include换成URP的库并且Tag里要加RenderPipeline UniversalPipeline。HDRP差异更大因为HDRP的深度测试和混合模式有自己的封装建议直接用Shader Graph搭节点逻辑和手写Shader是一样的。URP下还有一个注意点URP的深度纹理_CameraDepthTexture默认可能没开启如果你在Shader里采样深度纹理做额外判断需要在URP Asset里勾选Depth Texture选项。不过我们这套方案用的是硬件ZTest不依赖深度纹理所以没这个问题。6. 常见问题速查与避坑清单6.1 剪影完全不显示先检查三个地方。第一材质的Queue是不是Transparent如果是OpaqueZTest Greater永远不成立。第二场景里有没有遮挡物如果角色前面什么都没有自然没有剪影。第三Shader的Pass 2有没有被意外注释掉或者编译报错导致整个Shader回退到粉色。6.2 剪影颜色发灰、不够亮这是半透明混合的正常现象。因为剪影是叠加在遮挡物之上的如果遮挡物本身颜色很深剪影的Alpha又低混合结果就会偏暗。解决办法是提高_SilhouetteIntensity或者把混合模式改成Blend One One叠加模式让剪影颜色直接加上去。叠加模式在暗色场景里效果很好但在亮色场景里容易过曝需要权衡。6.3 剪影边缘有锯齿半透明物体的边缘锯齿比不透明物体更明显因为MSAA对半透明像素的处理有限。如果项目开了后处理可以加一个FXAA或SMAA。如果没开后处理可以在Shader里用clip加一个alpha阈值把极低alpha的像素直接丢弃这样边缘会干净一些但会牺牲一点柔和度。6.4 角色死亡后剪影还在这是逻辑层的问题不是Shader的问题。角色销毁时如果只销毁了GameObject但材质是共享的不会有残留。但如果用了对象池角色被回收后Renderer还开着剪影就会留在原地。记得在回收时把Renderer的enabled设为false或者把材质换回默认材质。问题现象最可能原因快速修复剪影完全不显示Queue不是Transparent改Queue为3000剪影盖住本体Pass 2的ZTest写错确认是Greater不是Always边缘闪烁深度精度不足调小远裁剪面颜色发灰Alpha太低或混合模式提高Intensity或改叠加移动端掉帧Overdraw过高加LOD或距离裁剪多角色颜色不生效材质实例化打断合批改用MaterialPropertyBlock6.5 一个容易被忽略的细节法线方向菲涅尔计算依赖法线如果模型的法线是反的比如从外部软件导入时没处理好剪影会变成中间亮、边缘暗完全反过来的效果。遇到这种情况在Shader里把法线取反就行或者在导入设置里勾选Recalculate Normals。这个坑我在导入某个第三方模型时踩过排查了半小时才发现是法线问题。7. 效果扩展与进阶玩法7.1 加一个呼吸脉冲动画静态剪影看久了会腻加一个随时间变化的透明度脉冲会更有生命力。在Shader里用_Time.y驱动一个sin函数乘到alpha上就行。float pulse 0.8 0.2 * sin(_Time.y * 3.0); float alpha _SilhouetteColor.a * fresnel * _SilhouetteIntensity * pulse;这个改动很小但视觉提升很明显特别适合用来标记重要目标。频率3.0大概是一秒一个周期太快会显得焦躁太慢又没存在感2到4之间比较舒服。7.2 被遮挡时显示、不被遮挡时隐藏的渐变有时候你希望剪影只在被遮挡时出现而且出现和消失要有过渡不要硬切。这需要在Shader里判断当前像素是否被遮挡但硬件ZTest是二值的没有中间状态。变通做法是用两个Pass的alpha做插值或者用脚本检测遮挡状态然后通过MaterialPropertyBlock平滑地改_SilhouetteColor.a。脚本方案更灵活但需要每帧做射线检测性能开销要评估。7.3 和描边效果的组合剪影和描边经常一起用。描边负责勾勒角色轮廓剪影负责在被遮挡时提示位置。两者可以共存在同一个Shader里用不同的Pass实现。注意描边Pass通常用Cull Front加顶点外扩和剪影Pass的ZTest Greater不冲突但渲染顺序要安排好一般描边在前、剪影在后。7.4 在UI上的应用这套思路也可以搬到UI上。比如背包里的角色头像被其他UI元素遮挡时显示半透明剪影。不过UI的渲染走的是Canvas不经过常规的3D渲染管线ZTest不适用。UI上要用Mask组件或者自定义的UI Shader来实现类似效果原理不同但目标一致。8. 我在实际项目中的几点体会这套剪影方案我从原型阶段一直用到上线中间改过好几版。最大的体会是不要一上来就追求完美效果先把ZTest Greater这个核心跑通看到剪影能正确显示在被遮挡的位置再去打磨菲涅尔、颜色、动画这些细节。很多新手卡在参数调优上其实是因为基础逻辑还没验证。另一个体会是关于性能的。剪影效果在PC上几乎无感但在移动端一定要做距离裁剪。我的做法是给每个剪影角色挂一个小脚本每0.2秒检测一次和摄像机的距离超过阈值就关掉剪影Pass。0.2秒的检测间隔足够不会造成明显的延迟感但能省下大量CPU开销。最后分享一个小技巧如果美术觉得菲涅尔边缘太硬可以在fresnel计算后加一个smoothstep把过渡区间拉宽。比如smoothstep(0.2, 0.8, fresnel)这样边缘会有一个柔和的渐变带看起来更像光晕而不是色块。这个参数没有标准值完全看美术的审美多试几组就能找到感觉。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →