Unity后处理局部化:双相机与Stencil模板保护背景UI 事情发生在上周项目里要做一道蓄力冲击波波头接触到的地面、敌人、碎屑要被轻微模糊并带一点扭曲但空中的背景贴图和头顶的UI必须保持原样。我图省事直接在场景里放了一个Global Volume把Blur强度拉到0.5结果是整个画面都糊了连UI文字都成了马赛克。把Canvas改成Screen Space - Overlay之后UI倒是好了背景贴图依然躲不掉。这时候我才意识到Unity Post-Processing的默认行为就是“一锅端”想让后处理只影响局部并且背景图不受影响方案不能靠碰运气。这篇文章就是针对这个需求整理的完整解法。我会从后处理的生效机制讲起再分别给出三套可落地的设置方法双相机渲染分离、UI Canvas模式逃离、以及URP里基于Stencil的像素级局部后处理。最后附一份我实际排查踩坑时的症状对照表。内容覆盖Built-in管线和URP适合已经会用Post Processing基本功能、但被“全屏特效”困住的人。1. 后处理为什么天生是“全屏杀手”先看机制再谈设置1.1 后处理在渲染链路中的位置决定了它的“全屏”属性在深入设置之前必须先把Unity后处理的运行机制搞清楚。无论是Built-in管线下的Post Processing Stack V2还是URP里的Volume框架后处理的本质都是相机把场景渲染成一张完整图像之后在输出到屏幕之前再对这张“整图”连续执行若干个全屏Shader Pass。这条链路和UI的渲染是分开的理解这一点很关键。你的主相机渲染完场景得到一个Color Buffer后处理在这个Buffer上做模糊、调色、扭曲处理完之后Unity才把最终结果交给屏幕。所以从设计上后处理根本不知道画面里哪个物体是“背景山”、哪个物体是“角色”、哪个区域是“UI面板”它眼里只有一张矩形的像素图。很多人看到Post-process Volume组件上有Layer Mask选项就以为可以指定“只让某些物体被后处理”。这是一个非常常见的误解。那个Layer Mask的实际作用是让相机只接收特定Layer上的Volume组件。也就是说它控制的是“这个相机要不要读取这个调色参数”而不是“这个后处理效果只作用在画面里的某类物体上”。哪怕你把Volume的Layer设得再精准只要它被相机读取了效果仍然是铺满整张画面的。1.2 先把“局部”拆成三种含义再对号入座在实际项目里“只影响局部且背景图不受影响”这句话至少有三种完全不同的需求解决方案天差地别。我建议你先停下来确认自己要的是哪一种再决定走哪条技术路线。第一种只是UI层不能受影响。UI本身默认走独立的Overlay渲染流程后处理根本不碰它。如果你遇到UI被模糊了通常是Canvas的Render Mode被改成了Screen Space - Camera或World Space这种需求最简单的解决方案就是改回Overlay或者给UI单独分配一个不挂后处理组件的相机。第二种场景里有一张背景图Sprite、Quad、3D面片它不需要被后处理但画面中的某个局部特效例如冲击波扫过的地面、角色身上的护盾需要被模糊或扭曲。这种需求下背景图和前景特效都在同一个相机里渲染后处理一定会对整张图生效必须通过“多相机分层渲染”或“Stencil模板裁剪”把两部分拆开。第三种同一个完整场景里前景角色站在背景图前面两者有真实的远近遮挡关系但只允许角色被后处理背景必须绝对干净。这种情况最复杂双相机分层很难处理好遮挡关系需要走到Stencil模板细化方案。我后来的项目需求实际上属于第二种偏第三种前景有冲击波背景山要保持清晰UI要完全不参与。所以下面三个方案我按“从简单到复杂、从够用到底层可控”的顺序来写你直接跳到符合自己情况的那一章就可以了。2. 平面分层双相机 RenderTexture 把画面拆成两层如果你只是想让场景里特定的几个物体被后处理而背景层完全不受影响双相机加RenderTexture是最直觉、也最容易调通的方案。它不需要写Shader不需要深入理解Stencil靠Unity编辑器里的配置就能完成。2.1 思路把“要特效”和“不要特效”拆成两路画面这个方案的架构非常直白主相机负责渲染背景层不挂任何后处理组件特效相机只渲染需要后处理的物体开启后处理输出到RenderTexture最后把这张RT用RawImage显示到UI上或者用另一个相机把它叠回场景中。用一套更生活化的比喻后处理像给照片加滤镜你不能让滤镜只作用于照片里的一棵树索性直接拍两张照片——一张拍干净的背景一张只拍那棵树并加上滤镜——最后把两张照片合成。背景那张“底片”从头到尾都没被碰过自然干净。这个方案最大的好处是主相机完全没有后处理任何模糊、泛光、颜色校正都不可能再污染背景而RT上的效果因为没有与主场景做深度交互也不会出现复杂遮挡问题。适合粒子特效、护盾、时停冻结、局部扭曲这类“本来就该漂浮在画面上层”的效果。2.2 从Layer设置到相机输出的完整配置步骤第一步先在Project Settings里创建两个Layer比如叫Background和FXTarget。这一步别偷懒直接在Chinese或任意已有层上凑合后面Culling Mask会变得很难维护。第二步在主相机上做两件事把Culling Mask里FXTarget这一层取消勾选确认相机上没有挂Post-process Layer或Volume组件。主相机的Clear Flags我建议设成Solid Color颜色按项目需要来但不要用Skybox否则后续合成时层与层之间容易出现颜色错位。第三步创建一个新相机作为特效相机命名为FX Camera。它的Culling Mask只勾选FXTargetClear Flags设成Solid Color颜色设成黑色的同时把Alpha设为0。然后给这个相机挂上Post-process Layer组件再在场景里创建一个带Volume的物体把Volume所在的Layer设置成与Post-process Layer指定的层一致并勾选Is Global或者给Volume加上一个能覆盖住特效区域的Collider。第四步在Project窗口右键创建Render Texture命名为FXRenderTexture然后在特效相机的Target Texture里把它拖进去。此时特效相机不会直接输出到屏幕而是把渲染结果写入这张RT。第五步在需要显示特效的位置创建一张RawImage把FXRenderTexture拖到RawImage的Texture字段取消勾选Raycast Target防止它挡住按钮点击。因为RawImage属于UI层用的是Overlay渲染主相机的后处理天然与它无关这等于完成了最终的图像合成。2.3 容易出现破绽的四个细节细节一两个相机的FOV、位置、旋转、Clipping Planes必须完全一致。我一开始图省事没有同步特效相机的Near/Far值结果特效物体和背景的缩放关系完全对不上看起来像特效悬浮在错误的位置。建议写个小工具脚本在Update里把主相机的参数同步给特效相机避免美术在编辑器里手调出错。细节二RT的Alpha通道很容易被后处理“吃”掉。后处理Shader在做Blit时如果不显式保留Alpha最终输出到RawImage上会出现一个黑色矩形底。解决手段有两个一是RT格式选ARGB32或ARGBHalf二是后处理Shader里最后返回颜色时手动把Alpha设为原值。如果你用的是Post Processing Stack V2自带的模糊效果在Build-in管线里通常还需要挂一个FinalPass Shader否则Alpha会丢失这个坑比较隐蔽排查时优先怀疑。细节三RawImage叠加在UI上之后不要让它挡住鼠标事件。RawImage默认是Graphic会拦截UI射线检测哪怕下面是空的按钮也可能点不中。务必取消它的Raycast Target或者在Canvas的GraphicRaycaster上做过滤。细节四特效相机的性能代价比你想象的小但也要控制范围。它只渲染FXTarget层里的物体如果这层里只有几个特效MeshDraw Call非常少但如果美术把所有特效都丢在同一层把背景里几十个粒子系统也放进去特效相机的渲染压力就会上来。建议这层只放“必须被后处理的物体”其他特效不进这层。2.4 这套方案的软肋在哪里双相机方案的软肋是特效相机渲染出来的RT本质上是一张“贴图”它没有和主相机场景里的深度发生真实关系。你可以把这张RawImage放在UI层级所以它看起来像前景但你没办法让背景正确地挡住它、或者让它被背景中的山体遮挡一部分。如果你只是要一个悬浮的特效这没问题如果你要“角色站在山后面山挡住角色同时角色身上的模糊效果只作用于角色”双相机方案就绕不过去。这个软肋出现后我自然而然转向了后面的Stencil方案。不过在那之前先解决另一个更常见的需求UI背景图不受后处理影响。3. UI/背景图脱困从Canvas Render Mode入手的简单解法如果你的“背景图”是UGUI里的Image那么恭喜你这可能是最容易解决的一类问题。你需要的不是复杂的相机分层而是搞清楚Canvas的Render Mode。3.1 三种Render Mode在后处理链路下的真实行为UGUI的Canvas有三种Render Mode它们与后处理的关系差异非常大。我直接整理成表格方便你对照Canvas Render Mode是否受相机后处理影响说明Screen Space - Overlay不受影响UI在场景和后处理全部结束后绘制主相机画面对它无意义最安全Screen Space - Camera受指定相机的后处理影响UI会被当成该相机画面的一部分在相机后处理流程中一起被模糊/调色World Space受世界场景影响UI被当作3D物体放入场景不仅受后处理还受光照和阴影影响我在项目里遇到“UI和背景图被模糊”的时候第一反应就是检查Canvas。如果Canvas是Screen Space - Camera并且指定了主相机那主相机上的Post-process Layer会毫无保留地处理掉所有UI。把Render Mode改回Screen Space - OverlayUI立刻恢复正常。这就是最简单的“背景图不受影响”的解法。3.2 如果项目UI框架强制要求Screen Space - Camera怎么办实际项目里UI框架可能为了做特效、或配合相机位移必须把Canvas挂在相机上。这时候你不能简单地改模式否则整个UI框架都得重构。我的做法是给UI单独分配一个UI相机UI Canvas的Render Mode继续用Screen Space - Camera但指定这个单独的UI相机不挂Post-process Layer也不进入主相机的后处理渲染链。具体配置创建一个UICameraDepth设为比主相机高Clear Flags设Depth OnlyCulling Mask只勾UI层。UI Canvas的事件相机也填这个UICamera。这样主相机在渲染场景时虽然会处理场景后处理但UI画面来自另一个没有后处理的相机最终通过相机Depth叠加显示在最上层因此不受影响。这个方法需要注意UICamera的Viewport Rect和主相机保持一致否则多屏或分辨率切换时UI位置会偏。我经历过一次UI在竖屏下跑到屏幕外的问题排查到最后就是UICamera的Viewport Rect没有跟随主相机。3.3 如果背景图是场景里的Sprite或Quad如果“背景图”不是一个UI Image而是摆在世界空间里的Sprite、Quad、3D面片那它和任何普通物体没有区别后处理一定会作用在它上面。Volume的Is Global参数、Collider范围、Volume Layer都不会改变这一点——再次强调后处理只管整张图像。这种背景图只有一个解决方案把它从特效相机的Culling Mask里排除。如果它和需要后处理的物体在同一个相机里就必须拆相机或者用Stencil。我建议直接跳到下一章别在Volume参数上浪费时间。4. 像素级精确URP Render Feature Stencil 做局部后处理前两套方案都有明显的边界双相机分层无法处理真实遮挡UI相机只能解决UI。如果你的需求是“同一个主相机画面里前景角色被后处理背景图绝对干净同时角色和背景的遮挡关系还正确”那必须用Stencil模板缓冲。4.1 什么时候必须上Stencil简单说后处理是全屏Pass但Shader可以在像素写入前做一次模板测试模板测试只允许“预先被标记过的像素”通过。于是你可以先让指定层物体往Stencil Buffer里写入一个标记值再让后处理Pass只处理这个标记值对应的像素。背景像素因为没有被标记在GPU跑到Fragment Shader之前就被丢弃了最终画面保持不变。这和双相机方案的区别在于Stencil方案是在同一张画面里做裁剪角色的深度、遮挡、和背景的相对关系完全保留。角色站在山后时山遮住的部分在屏幕像素上本来就没有被写入模板标记后处理自然也不会作用到被遮挡区域。这正好解决了方案二的硬伤。4.2 两段式机制先打标记再按标记裁剪第一阶段你需要一个“渲染对象”的Pass把指定Layer对象渲染到相机目标上并在渲染的同时写入Stencil值。URP里的Render Objects Renderer Feature就能干这件事。它的本质是“额外渲染一遍特定层物体”你可以在配置里指定Layer、覆盖材质以及写入Stencil的操作。第二阶段让一个全屏后处理Pass执行模糊或扭曲但这个Pass的Shader需要开启Stencil比较只处理模板值等于指定Ref的像素。由于全屏后处理用的是相机颜色目标而颜色目标与模板缓冲是同一份Depth/Stencil Buffer所以模板数据可以正常读取后处理Shader就能做到“像素级局部”。提醒一句即使模板测试能提前丢弃大部分像素全屏三角形依然会被光栅化一整个屏幕Fragment Shader毕竟不需要执行这部分开销已经在硬件层面被省掉了。模板测试发生在Rasterizer之前性能上的收益比在Shader里做if判断高得多。4.3 使用URP 14以上版本的Full Screen Pass最省事如果你用的Unity版本比较新URP已经内置了Full Screen Pass Renderer Feature不用自己写ScriptableRendererFeature。操作步骤是第一步创建一个带Stencil测试的Shader。这个Shader最终作为后处理材质被Full Screen Pass调用。Stencil部分可以直接写在SubShader下Stencil { Ref [_StencilRef] Comp Equal }第二步在URP Renderer上添加一个Render Objects Renderer Feature把LayerMask设为需要后处理的那一层。在Stencil设置里把Stencil操作改成ReplaceRef设为1。这样该层物体渲染时会往模板缓冲写入1。第三步再添加一个Full Screen Pass Renderer Feature指定刚才创建的材质并把材质的_StencilRef参数设为1。全屏Pass在输出画面时会检测模板值只有值为1的像素才执行后处理逻辑。我把这一步放在前面是因为配置最简单适合核心项目正在用URP 14的人。老版本Unity没有这个Feature那就用下面这套通用的自定义RendererFeature。4.4 通用方案自己写ScriptableRendererFeature如果你用的是URP 12或更早版本或者想把逻辑握在自己手里可以参考我下面这套最小实现。它由两部分组成一部分在管线里插入全屏Blit另一部分是在Shader里开启模板测试。先看C#部分using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public class StencilBlitRendererFeature : ScriptableRendererFeature { public Material blitMaterial; [Range(0, 255)] public int stencilRef 1; public RenderPassEvent passEvent RenderPassEvent.AfterRenderingPostProcessing; private StencilBlitPass pass; public override void Create() { pass new StencilBlitPass(passEvent, blitMaterial, stencilRef); } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { if (blitMaterial ! null) renderer.EnqueuePass(pass); } private class StencilBlitPass : ScriptableRenderPass { private readonly Material material; private readonly int stencilRef; private RenderTargetIdentifier source; private int tempRTID; public StencilBlitPass(RenderPassEvent evt, Material mat, int stencilRef) { renderPassEvent evt; material mat; this.stencilRef stencilRef; tempRTID Shader.PropertyToID(_StencilBlitRT); } public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) { source renderingData.cameraData.renderer.cameraColorTarget; } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { var cmd CommandBufferPool.Get(StencilBlitPass); var desc renderingData.cameraData.cameraTargetDescriptor; cmd.GetTemporaryRT(tempRTID, desc); cmd.SetGlobalInt(_StencilRef, stencilRef); Blit(cmd, source, tempRTID, material, 0); Blit(cmd, tempRTID, source); cmd.ReleaseTemporaryRT(tempRTID); context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } } }这段代码的逻辑是把当前相机画面先Blit到一张临时RT在Blit时使用自带模板测试的材质再把处理后的临时RT Blit回原相机目标。这里需要注意临时RT不需要额外分配Depth/Stencil因为模板数据仍然在原始相机目标的Depth/Stencil Buffer里最终Blit回source时Stencil测试会正常读取。Shader部分我写了一个最简的模板测试壳它本身没有任何视觉效果只是用来验证“哪些像素能通过测试”。你把自己的模糊、扭曲逻辑替换到Frag函数里就行Shader Hidden/StencilLocalPostProcess { SubShader { Tags { RenderTypeOpaque RenderPipelineUniversalPipeline } ZTest Always ZWrite Off Cull Off Stencil { Ref [_StencilRef] Comp Equal Pass Keep Fail Keep } Pass { HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; }; Varyings Vert(Attributes input) { Varyings output; output.positionCS TransformObjectToHClip(input.positionOS.xyz); output.uv input.uv; return output; } half4 Frag(Varyings input) : SV_Target { // 这里放真正要做后处理的采样逻辑例如模糊、扭曲、描边 return SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv); } ENDHLSL } } }这个Shader里的Stencil块是核心。Comp Equal意味着“只有模板值等于_StencilRef的像素才会继续执行Fragment Shader”其他像素在光栅化阶段就被卡掉了。所以即使_Frag函数里写的是简单采样背景像素也已经完全保持原样。在URP Renderer上把这个Feature挂上去Material选刚才创建的材质Stencil Ref设成与Render Objects里写入的标记值一致就能看到效果了。4.5 Stencil方案最容易翻车的三个地方第一个坑Render Objects写入标记值时用增量操作而不是Replace。如果你把Stencil操作设置成IncrementWrap而不是Replace而且场景中同一像素有多个物体叠加模板值可能会超过判断值导致后处理漏掉一部分区域。我建议一律用Replace并指定固定Ref值。第二个坑后处理使用的资源里Depth/Stencil格式不能被丢弃。URP如果开启了Opaque Depth Texture或者Depth Priming某些情况下模板信息可能不是以你预期的方式保留。如果发现后处理完全没生效先在URP Asset里关闭Depth Priming测试一次。这是我实际项目里排查最久的一个坑。第三个坑全屏Shader的Stencil Ref参数忘了通过SetGlobalInt传到材质。我的代码里在Execute时做了cmd.SetGlobalInt如果你自己在Urp Asset里配Full Screen Pass需要检查Material实例上是否有_StencilRef属性或者用RendererFeature里的参数覆盖它。5. 排错清单与选型遇到问题先查这几处无论你采用双相机还是Stencil方案实际项目里都会有“设置了但没生效”“生效了但影响范围不对”的情况。下面是我整理的症状排查表基本能覆盖后处理局部化的大多数故障。症状常见原因解决动作后处理完全没生效Post-process Layer的Volume Layer与实际Volume所在Layer不匹配把Volume所在的GameObject放到指定Layer并确认Collider或Is Global生效UI被一起模糊了Canvas Render Mode不是Overlay改成Screen Space - Overlay或使用独立UICamera背景图依然被模糊但UI正常背景是场景物体后处理必然全屏改用双相机RT或Stencil方案RawImage显示黑底RT Alpha被后处理覆盖RT格式改ARGB32/ARGBHalfShader里保留Alpha特效相机输出的RT是黑屏相机没有正确渲染FXTarget层或没有Volume检查Culling Mask、Target Texture和Volume的Layer双相机画面位置错位两个相机FOV/位置不同步写脚本同步两个相机参数并保证Viewport Rect一致Stencil后处理对背景也生效Render Objects没写入模板或模板Ref不匹配用Frame Debugger检查渲染顺序和Stencil设置局部后处理边缘生硬模板标记只有0/1没有过渡使用模板值做边缘过渡或者在后处理Shader里基于UV做平滑衰减排查时我习惯用Frame Debugger逐帧看两个Render Pass第一个Pass有没有成功执行替换模板操作第二个Pass是否有Stencil Test的标记。Frame Debugger只读Pass名所以记得给每个Shader Pass起有意义的名字不然多个后处理Pass混在一起会眼花缭乱。如果你还没确定用哪个方案这里是我经过多轮项目试错后沉淀出的选型逻辑可以直接套用只要UI不受影响改Canvas为Overlay或者独立UICamera10分钟搞定。特效是漂浮层不和场景深度交互双相机加RawImage逻辑最简单美术可维护性也最高。特效需要和场景发生真实遮挡角色在背景前、背景后又清晰上Stencil方案虽然要写一点Shader和RendererFeature但效果下限高。你是商业项目且Unity版本较新优先用URP内置Full Screen Pass Renderer Feature少写代码少踩自定义Feature的坑。我个人现在遇到这类需求已经形成了一套标准流程先移动Canvas的Render Mode做验证再决定要不要上双相机最后才评估Stencil。因为很多策划口中的“背景图”其实指的就是UI背景这个最简单。真正需要Stencil的场景往往是角色特效和场景交互紧密、或者需要做动态区域后处理的时候。希望这份踩坑记录能帮你少走两天弯路。