1. 后处理到底在哪一步介入渲染先把这个链路想清楚1.1 一帧画面的“最后一公里”很多刚接触Unity后处理的同学会把后处理理解成类似修图软件里的“滤镜层”觉得拿一个滤镜盖住画面就行。方向没错但如果只停留在这种理解后面一定会踩坑。后处理实际上是整帧场景完成光栅化之后、画面正式输出之前连续执行的多步像素级操作。通俗点说场景里的模型、光照、雾效、透明物体都已经绘制到一张颜色纹理上后处理才开始接手对这张已经成型的画面逐像素做二次加工。这个机制的底层实现并不复杂但理解它对后续调试非常重要。每个后处理效果本质上都是一次全屏四边形渲染GPU把上一阶段生成的渲染纹理当作输入跑一遍像素着色器结果写入另一张临时渲染纹理再作为下一个效果的输入如此串联下去。你会发现效果之间的排列顺序会强烈影响最终观感因为前一个效果的输出颜色会直接作为后一个效果的计算基础。同样一组参数Bloom放在色调映射之前或之后高光区域的扩散方向、层次感觉完全不同。所以我一直建议后处理参数不要单独看一定要结合它在效果栈里的位置来判断。每一层后处理都会占用一张临时渲染纹理和一次全屏的读写开销层数越多带宽消耗越大。很多项目出现帧率问题第一反应是模型面数太高但打开帧调试器一看往往是后处理叠加了七八层全屏Pass带宽跑满了。这部分内容我会在性能章节展开先把这个基本认识立住后面所有排查思路才有落脚点。1.2 内置管线和URP的执行时机差别内置渲染管线和URP在执行后处理的方式上差别很大很多从旧项目迁移到URP的开发者最先遇到的不适应就是“我以前挂个脚本就有后处理现在怎么没反应了”。内置管线里传统写法是在相机上挂脚本用OnRenderImage在整帧渲染结束之后自动回调开发者不需要关心具体在哪一步画面要送出去之前它就会跑。而URP是流水线架构相机的渲染被拆成很多个Pass后处理必须作为额外的Pass主动插入到流水线里由RenderFeature驱动。正是这个差异导致了一批看起来很像的“灵异事件”。比如在URP下调后处理Scene视图里能看到效果Game视图里却正常两个视图表现不一致再比如编辑器里一切正常打包后效果整个消失。大多数情况都是RenderFeature没有正确命中目标相机或者Volume组件的遮罩层级没对上导致后处理配置没有作用到实际渲染的那台相机上。URP下做后处理一定要建立一套清晰的RenderPass注册与相机关联的意识不能指望像内置管线那样随手挂个脚本就完事。1.3 认清渲染纹理的流转路径如果要在项目里加一个介于“主相机输出”和“最终输出”之间的自定义效果建议按三步来梳理先确认输入纹理是哪个相机的Color Target再确认效果输出到哪张临时纹理最后确认管线里下一个消费这张纹理的是谁。把这三步走通很多黑屏、效果错位的问题能提前规避一大半。我见过不少人在URP里写自定义后处理随手复制一个别人的Blit RenderFeature跑起来效果不对就开始调着色器参数调半天也没用。实际上问题根本不在着色器而是目标RenderTarget用错了画面写入了一个错误的位置后续根本拿不到这个结果。所以我个人的习惯是在项目里给后处理链路画一张非常简单的数据流草图不需要多正式哪怕只是写在纸上相机RT出发经过效果A再到效果B最终到屏幕。有了这张图每次新增效果时唯一的疑问就是它该卡在哪两个环节之间而不是在无数张临时纹理里盲目猜测。2. 内置效果逐个过功能、适用场景、还有那些绕不开的坑2.1 色彩校正与色调映射决定画面“底子”的第一步内置后处理栈里我建议先理清色彩校正和色调映射。它们负责把场景从物理上正确的高动态范围亮度映射到显示器能展示的低动态范围同时定义整幅画面的色彩倾向。很多新手遇到画面过曝或者死黑第一反应是去压灯光、改材质其实问题往往出在色调映射曲线上。Unity的后处理栈提供了多种色调映射模式最常见的是Neutral、ACES和Filmic。Neutral变化幅度小不容易把颜色拉偏适合写实项目ACES更贴近电影工业的色彩转换流程高光舒张、暗部压缩的特点明显但处理不好会让画面偏灰Filmic是偏风格化的选择暗部过渡更柔和高光收敛也更快很多追求短片质感的项目会用。我的建议是新建项目后先把色调映射当成“固定底模”来对待而不是每换一个场景就重新拉一遍。先在光照最简单的灰盒环境里定好曝光和色调映射再往场景里填内容。这样不同材质在不同场景里呈现的一致性会高很多不然每块材质球都在不同光照下单独拉最后拼在一起时颜色对不上只能靠猜。2.2 Bloom、镜头光晕与辉光最容易“过量”的效果Bloom在手游项目里几乎成了标配但也是最容易被滥用的效果。新手最典型的问题是把亮度阈值打得很低、强度拉得很高结果整个画面像隔着一层薄雾亮部糊成一片暗部也跟着泛白。Bloom的参数虽然多核心其实就两个阈值和扩散半径。阈值决定什么亮度的像素参与发光建议把它保持在一个比较高的位置只让光源、高光材质这些真正的亮部参与扩散半径决定模糊范围理性控制在需要光感溢出的区域就好。Bloom是用来模拟亮部溢出到周边区域的物理现象不是把整张画面无差别地揉一遍。镜头光晕则是另一回事。它会在光源位置拉出星芒或者光斑常用于太阳、路灯、霓虹灯这类点光源画面。实际项目里光晕的形态跟镜头光圈结构、焦段直接相关不是随便几个数值就能做出真实感。如果追求写实需要根据镜头焦段匹配星芒数量、拉伸方向和旋转角度如果只是做风格化适当地夸张可以但要确保光斑集中在高光附近不要在画面的暗部区域大量浮现。2.3 景深、运动模糊、渐晕、色差情绪表达的主要手段后处理里最有“电影感”的往往是这几个细节效果。景深让焦点以外的区域产生弥散模糊用来引导视线运动模糊模拟快门时间内的位移赛车和动作游戏很常用渐晕在四角压暗制造聚焦感色差则模拟镜头RGB通道的微小错位在复古和惊悚题材里很常见。单看效果都不难难在搭配使用。景深和运动模糊都大量依赖深度信息和运动向量场景一旦出现快速运动采样数不足时就会出现抖动或边缘撕裂。性能方面它们同样不便宜景深需要降采样和多次模糊运动模糊要对当前帧做多轮重投影取样。移动端建议谨慎开启即使要用也要把最大模糊半径调小、采样数控制在合理范围。色差也有个小坑强度一旦过高在UI文字和高频纹理上会出现非常刺眼的红蓝边。除非你明确要做屏幕边缘色散的风格否则强度不建议超过0.5。效果主要开销风格适配移动端建议色彩校正/色调映射低通用所有项目必开Bloom中高光、梦幻、科幻降分辨率、阈值拉高景深高电影、叙事、特写慎用或做简化版运动模糊高竞速、动作、过场桌面端为主渐晕低恐怖、硬照、聚焦随意色差低复古、故障、氛围少量使用3. 想做出差异化三类自定义后处理值得会写3.1 为什么内置包缺少“模糊”和“边缘检测”熟悉后处理栈的人会注意到它提供了大量氛围类效果但就是没有单独的“模糊”和“描边/边缘检测”效果。原因很实际模糊在游戏里几乎没有通用形态。UI背景的半透明模糊是一种做法摄像机景深的焦外模糊是另一种做法全屏故障风格又是完全不同的思路。内置团队没打算用一个方案覆盖所有使用场景这部分的决策权就留给了开发者。但项目里真的经常需要这些效果。弹窗背后的模糊背景、切断画面时的扫描线、模拟水下视线的扭曲、风格化卡通的描边……这类效果光靠调参数是实现不出来的只有自己写Shader才能拿到真正的控制权。我建议每个做渲染方向的开发者都至少掌握一个通用后处理Shader的读写能力不用很复杂但要知道每个环节在做什么。3.2 一个通用后处理Shader的最小骨架自定义后处理Shader并不神秘核心就两步采样当前屏幕纹理在uv坐标上做计算输出颜色。下面这个是最小可运行的骨架适用于Built-in管线的OnRenderImage也可以在URP里通过Blit调用。我项目里很多效果都是在这个基础上改出来的。Shader Custom/ScreenEffectBase { Properties { _MainTex (Source, 2D) white {} _Intensity (Intensity, Range(0, 5)) 1 } SubShader { Cull Off ZWrite Off ZTest Always Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc sampler2D _MainTex; float _Intensity; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv v.uv; return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col tex2D(_MainTex, i.uv); col.rgb * _Intensity; return col; } ENDCG } } }这段Shader里有三行值得单独拎出来讲Cull Off、ZWrite Off、ZTest Always。后处理本身是全屏覆盖不需要背面剔除、不需要写深度更不需要参与深度测试。不少新手写后处理效果不对最后发现就是这三行的设置没跟上导致全屏四边形只渲染了部分区域或者被深度缓冲挡掉了本该显示的内容。写自定义后处理Shader先对照一下这三行能省很多排查时间。3.3 把自定义效果接进URPRenderFeature与BlitURP里接入自定义后处理的方式和内置管线不同需要写一个ScriptableRenderPass负责执行Blit再写一个RenderFeature把Pass注册到渲染管线。下面这段代码是精简版保留了最核心的逻辑方便理解整体流程。public class SimpleBlitPass : ScriptableRenderPass { private Material _material; private RenderTargetIdentifier _source; public SimpleBlitPass(Material mat) { _material mat; renderPassEvent RenderPassEvent.AfterRenderingPostProcessing; } public void Setup(RenderTargetIdentifier source) { _source source; } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { CommandBuffer cmd CommandBufferPool.Get(SimpleBlit); Blit(cmd, _source, BuiltinRenderTextureType.CameraTarget, _material, 0); context.ExecuteCommandBuffer(cmd); cmd.Clear(); CommandBufferPool.Release(cmd); } }实际项目里渲染时机的选择非常关键。renderPassEvent设置在BeforeRenderingPostProcessing效果就会先于内置后处理执行设置在AfterRenderingPostProcessing则在内置效果之后执行顺序不同会直接影响最终画面。另一个容易忽略的坑是URP多相机的情况。每个相机的渲染流程独立RenderFeature会在每台相机的管线里各自执行一遍。如果只想让主相机启用后处理一定要在RenderFeature里添加判断否则叠加相机会莫名其妙多出一层效果让人误以为Shader写错了。4. 参数调优把“刺眼新手村”调到“电影感”4.1 色调映射曲线的常见误用刚接触调色的开发者很容易犯一个反直觉的错把色调映射当成调亮度的旋钮结果越调越灰。色调映射的核心价值是在把不同曝光层级压缩到可显示范围时尽量保留亮部与暗部的层次而不是把画面整体调亮或调暗。曝光控制才负责画面明暗色调映射负责的是“压缩曲线长什么样”。我一般按三步调。第一步关闭所有和颜色无关的后处理效果只保留色调映射先看整个画面的动态范围是否落在可接受区间。第二步打开曝光控制让平均亮度稳定在参考值附近。第三步再回头微调色调映射的曲线形状。实际做下来你会发现很多画面“脏”的原因不是曲线不对而是曝光没有稳定下来。曝光飘忽不定曲线再怎么调都没有意义。4.2 Bloom的“阈值-半径-权重”组合思路调Bloom的关键不是反复拉强度而是搞清楚“哪些像素该发光、发到什么程度”。我常用一个很笨但有效的方法先在灰度模式下观察画面把阈值调高让绝大部分画面变黑只留下光源和高光区域是亮的然后逐步降低阈值直到亮部区域的扩散刚好覆盖到想要的范围最后才调整半径和强度。这样做能避开两个典型问题一是阈值太高只有太阳和最强灯光能发光其他材质完全没有光感二是阈值太低整幅画面浮上一层白色光晕观感极差。还有一个经常被忽略的源头场景里若有大量自发光材质需要先确认它们的自发光强度本身是否合理。很多不自然的Bloom根本原因不在Bloom参数而在材质把自发光调到了几十上百阈值再高也压不住。4.3 对比度、饱和度与色相偏置先定风格底模我的习惯是先定风格底模再谈细调。顺序大致是先确定整体对比度方向是高反差硬朗风格还是低反差柔和风格再确定饱和度基调是偏高饱和还是灰调最后通过色轮或色调分离给暗部、中间调、亮部注入不同的色相偏移。这三步走完项目的视觉基调基本就立住了。这里有个很实用的经验不要在完全空白的场景里做风格底模也不要在材质都没定的白模场景里做。最好的时机是场景里已经有至少五六种有代表性的材质、环境光和主光都基本确定时再动手调整风格底模。否则后处理调出来的感觉大概率会和后续新增材质的反光特性冲突最后还得推翻重调。这个时序问题看似小实际能省下一大批返工时间。5. 性能难点后处理才是真正的带宽杀手5.1 先打开帧调试器看看每一步花了多少钱后处理性能优化的第一步永远不是改参数而是量化每一层效果到底消耗了多少带宽和填充率。Unity的Frame Debugger可以逐级查看每个后处理Pass的执行情况RenderDoc则能进一步查看每张临时渲染纹理的分辨率和格式。先用这些工具摸清成本再决定要不要砍效果、降分辨率或合并Pass。一个常见的典型场景是4K分辨率屏幕上Bloom如果按全分辨率执行一次采样就是几千万像素几次降采样叠加带宽消耗非常惊人。这也是为什么成熟的后处理库在设计Bloom时都会先降一半分辨率逐级抽稀最后再上采样回来。你看着只是分辨率砍了一半实际带宽节省是数量级的。5.2 降分辨率、临时纹理复用与Pass合并具体优化手段里性价比最高的是三件事。第一降分辨率。Bloom、景深、运动模糊这类模糊型效果完全可以在半分辨率甚至四分之一分辨率下计算视觉差异通常极小。很多后处理栈里专门提供了HalfRes标志目的就是让开发者用精度换带宽。第二临时纹理复用。每多分配一张临时RTGPU就要多一次分配和回收尽量使用固定池子里的纹理避免频繁分配。第三合并Pass。如果两个效果作用在同一块uv区域能在同一个Shader里算完就不要拆成两次全屏渲染。我在项目里经常把模糊和色差合并到一个Shader里处理或者把暗角和色调曲线放在同一个片元着色器末尾。合并之后单个Shader变复杂了但全屏读写次数减少了一次带宽直接省掉四分之一到三分之一。对于移动端来说这是质的差别。5.3 移动端、桌面端和主机端的预算差异不同平台的GPU架构差异很大不能拿同一套参数到处跑。桌面端GPU带宽充裕后处理全开也未必有问题移动端带宽非常受限尤其是基于Tile的GPU架构一半以上能耗都花在读取和写入渲染纹理上。在移动端我遵循的优先级是色调映射必开渐晕随意Bloom视情况开半分辨率景深和运动模糊尽量降低成本或直接关闭。分辨率越高后处理的全屏Pass成本越高。手机屏幕现在动不动就是2K甚至更高一个后处理效果每秒跑几十次全屏遍历带宽消耗很可观。做移动端时后处理的临时纹理分辨率最好跟随“内部渲染分辨率”不要直接跟着屏幕物理分辨率走。即便只保持在内部渲染分辨率的四分之三性能也会宽裕很多。这个细节在真机上跑一下就能感受到明显差距。6. 排查链路黑屏、白闪、过曝时按什么顺序找问题6.1 黑屏先分清是Shader问题还是管线问题后处理最常见的故障就是黑屏但成因五花八门。我的排查顺序基本固定第一步把后处理脚本或RenderFeature临时禁用画面恢复说明问题出在后处理链路第二步打开帧调试器看最后一个正常输出的Pass是哪一步黑屏往往发生在某一次Blit的目标纹理写错或者目标RT从未被清除第三步检查Shader里是否写了ZWrite On或ZTest LEqual这些设置会意外遮住全屏四边形最后确认输入纹理的格式与Shader采样器的格式匹配有些平台对半浮点纹理的采样非常敏感结果直接就黑了。如果是Built-in迁移到URP的项目还需要多检查一个地方相机上是否还挂着旧的后处理脚本。两套管线脚本混用最常见的结果是跑完一个后处理又被URP的Volume再跑一次两重效果叠加时某些叠加逻辑会把画面直接变黑。这种问题不看代码很难想到排查效率最高的方式就是把相机上的脚本逐个禁用看到底是哪一层叠加出了问题。6.2 白闪与过曝大概率卡在HDR和色调映射白闪通常和HDR纹理有关。如果一个效果在高动态范围下的输出亮度值过大但后续色调映射没有及时处理或者目标纹理本身不支持超过1的浮点值那么超范围的颜色会被直接截断成纯白。排查时先看帧缓冲格式再用调试器确认峰值亮度最后再决定是修改效果Shader的亮度输出还是调整色调映射参数。这个顺序不要反先确认格式再调参数否则很可能在错误方向上浪费时间。另一种常见的白闪来自快速运动场景下的采样崩溃。运动模糊和景深在极端uv坐标下可能采样到已经释放的临时纹理数据数值直接变成NaN或Inf。这种白闪往往不是亮度溢出而是一种数值异常。临时解决办法是在后处理Shader输出前加一个clamp把颜色限制在安全范围避免NaN传染到后续Pass然后再回头排查运动向量的采样逻辑。6.3 平台差异编辑器正常、真机出问题时的自查顺序“编辑器里一切正常打包到真机效果丢失或异常”这类问题几乎是后处理领域的高频投诉。我的自查顺序是先检查平台相关的着色器变体是否被打包进去后处理用到的Keyword和Shader变体是否在Always Included Shaders列表里。很多后处理Shader只有在特定平台编译时才会生成所需变体打包时被自动剔除效果自然消失。其次是纹理格式差异。桌面GPU默认支持A16B16G16R16F这类半浮点纹理但某些移动GPU或老款主机支持策略不同。不做格式兼容的话后处理在真机上会以完全不同的精度执行观感差异巨大。最稳妥的做法是在后处理纹理的导入配置中强制开启sRGB读写标记并在Shader里统一在线性空间处理避免不同平台各自隐式转换一遍导致最终颜色完全对不上。最后再说一个我自己的习惯项目里始终给后处理留一个Debug开关一键切换整体后处理是否启用方便随时对比“有”和“没有”的差异。核心参数也尽量做成运行时调整直接在场景里通过调试面板改参数而不是每次改完数值都要切回编辑器等待编译。先把链路跑通再谈定制效果和风格化Shader出图的稳定性和可控性都会比逐个拉滑块高很多。