Unity默认管线屏幕后处理伪焦散实现方案

发布时间:2026/10/2 19:39:53
Unity默认管线屏幕后处理伪焦散实现方案 1. 项目缘起与核心思路拆解1.1 为什么要在默认管线下做伪焦散焦散这个词听起来很唬人其实说白了就是光线穿过水面或者玻璃之后在物体表面形成的那种晃动的亮斑。真实感强的焦散一般靠光子映射或者屏幕空间光线追踪来做但这两条路在Unity默认渲染管线里都不好走——前者需要引擎层面的支持后者对硬件有硬性门槛。我接手这个需求的时候项目已经锁定在默认管线用的是内置的Standard Shader团队也没有计划升级到URP或者HDRP。在这种约束下最务实的做法就是走屏幕后处理这条路。屏幕后处理做焦散的核心逻辑并不复杂在相机渲染完场景之后拿一张深度图或者一张场景法线图在屏幕空间里用噪声纹理去扰动采样坐标再把扰动后的结果叠加到原图上。这样做出来的效果虽然不是物理正确的但在视觉上足够骗过眼睛而且性能开销可控移动端也能跑。我试过在骁龙865的设备上跑这套方案1080P分辨率下额外开销大概在1.2毫秒左右对于非写实风格的项目来说完全可以接受。这个方案适合谁呢如果你在做风格化项目、独立游戏、或者对性能敏感的手游又不想折腾管线升级那这套思路值得参考。它不需要你重写Shader也不需要额外的渲染Pass只要你会写一点屏幕后处理的基础代码就能上手。1.2 整体方案选型与对比在动手之前我对比了三种常见做法。第一种是直接用粒子系统模拟焦散光斑优点是简单直观缺点是光斑不会随视角变化贴在物体表面像贴纸一样假。第二种是用Projector投影把一张焦散纹理投射到场景里效果比粒子好一些但投影在曲面上会拉伸变形而且不支持屏幕空间的扰动。第三种就是屏幕后处理它的优势在于焦散图案是跟着屏幕走的视角变化时扰动效果自然而且可以结合深度图做遮挡判断避免焦散出现在不该出现的地方。我最终选了屏幕后处理还有一个原因是它和现有项目的兼容性最好。项目里已经有一套后处理框架我只需要在现有的后处理栈里加一个Pass就行不用改动场景里的任何物体。这种非侵入式的做法在团队协作里特别重要美术不需要重新摆放物体程序也不需要改渲染逻辑大家各司其职。提示如果你的项目已经用了Post Processing Stack v2可以直接写一个自定义的PostProcessEffect省去自己管理RenderTexture的麻烦。但如果项目用的是自己写的后处理框架那就需要手动管理Blit和临时纹理。1.3 核心原理一句话说清屏幕后处理做焦散的原理本质上就是“用噪声扰动UV再叠加颜色”。具体来说在片元着色器里我们先用屏幕坐标采样一张噪声纹理得到一个扰动值然后用这个扰动值去偏移当前像素的采样坐标最后把偏移后的采样结果和原图做叠加或者屏幕混合。为了让焦散看起来有流动感噪声纹理的采样坐标会随时间滚动滚动的速度和方向可以调模拟水流或者光线折射的变化。这里有个关键点扰动不能直接加在屏幕坐标上否则整个画面都会扭曲。正确的做法是只对焦散图案本身做扰动也就是说我们先根据屏幕坐标和深度信息判断哪些区域需要显示焦散然后只在这些区域里应用扰动。深度信息的作用是让焦散只出现在靠近水面的物体上比如池底、墙壁、地面而天空或者远处的物体就不应该被焦散影响。2. 核心细节解析与实操要点2.1 深度图与法线图的选择做屏幕后处理焦散深度图是必须的法线图是可选的。深度图用来判断像素距离相机的远近从而控制焦散的强度和范围。比如我们可以设定一个深度区间只有在这个区间内的像素才会被焦散影响这样就能避免焦散出现在太远或者太近的地方。Unity默认管线下获取深度图有两种方式一种是让相机生成一张深度纹理另一种是用Camera.depthTextureMode开启深度模式。我一般用后者因为它在后处理里更灵活。法线图的作用是让焦散根据表面朝向产生变化。比如朝上的表面地面、池底焦散强一些朝侧面的表面墙壁焦散弱一些。这样看起来更自然因为真实世界里焦散在水平面上的表现确实比垂直面更明显。获取法线图需要额外渲染一遍场景开销比深度图大所以如果项目对性能要求很高可以只用法线图做可选增强默认关闭。// 开启深度纹理和法线纹理 void OnEnable() { GetComponentCamera().depthTextureMode DepthTextureMode.Depth | DepthTextureMode.DepthNormals; }这段代码放在相机脚本的OnEnable里确保相机在渲染时生成深度和法线信息。注意DepthNormals模式会把深度和法线打包到一张纹理里采样的时候需要解码具体解码方式在Unity文档里有说明这里不展开。2.2 噪声纹理的选取与滚动逻辑噪声纹理的选择直接决定了焦散的形态。我试过几种方案Perlin噪声太柔和焦散看起来像一团雾Worley噪声细胞噪声比较适合因为它有清晰的边界看起来像光斑的轮廓还有一种做法是用两张噪声纹理叠加一张控制大形态一张控制小细节效果更丰富。我最终用的是Worley噪声在Photoshop里生成了一张512x512的灰度图然后在Shader里用frac函数做平铺。滚动逻辑是这样的在片元着色器里用_Time.y乘以一个速度系数加到噪声纹理的UV上。速度系数不要太大0.05到0.15之间比较合适太快了像在抽搐太慢了又看不出流动。方向可以只滚一个轴也可以两个轴都滚两个轴都滚的话建议给不同的速度这样图案不会呈现明显的对角线运动。float2 noiseUV i.uv * _NoiseTiling float2(_Time.y * _SpeedX, _Time.y * _SpeedY); float noise tex2D(_NoiseTex, noiseUV).r;这段代码里_NoiseTiling控制噪声的平铺密度值越大光斑越密。我一般设在3到8之间具体看场景比例。_SpeedX和_SpeedY分别控制两个轴的滚动速度建议一个设0.1另一个设0.07这样运动看起来更自然。2.3 焦散颜色的叠加方式焦散的颜色叠加方式决定了最终画面的观感。常见的做法有三种加法叠加、屏幕混合、柔光混合。加法叠加会让焦散区域变亮适合表现阳光透过水面形成的亮斑屏幕混合也是变亮但比加法柔和一些不会那么容易过曝柔光混合则会根据底色调整亮的地方更亮暗的地方稍微提亮适合表现比较微弱的焦散。我一般用屏幕混合因为它在亮部和暗部都有不错的表现而且不容易把画面搞得太“脏”。具体实现是用1 - (1 - src) * (1 - dst)这个公式在Shader里就是1 - (1 - colA) * (1 - colB)。如果你想让焦散更强烈可以在混合之前给焦散颜色乘一个强度系数这个系数建议做成可调参数方便美术在编辑器里实时调整。注意焦散颜色不要用纯白色纯白叠加后容易过曝看起来像画面被洗过一样。建议用偏暖的淡黄色或者偏冷的淡蓝色具体看场景的光照氛围。我一般用float3(1.0, 0.95, 0.8)这种微微偏暖的颜色。2.4 深度衰减与边缘过渡深度衰减是让焦散看起来自然的关键。如果不做衰减焦散会均匀地铺满整个屏幕看起来像一层贴纸。正确的做法是根据像素深度计算一个衰减系数距离相机越远焦散越弱直到完全消失。衰减曲线可以用线性也可以用指数我一般用smoothstep因为它两端过渡比较平滑。float depth LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv)); float fade 1 - smoothstep(_FadeStart, _FadeEnd, depth);这段代码里_FadeStart和_FadeEnd控制衰减的起始和结束距离。比如你想让焦散在5米到15米之间逐渐消失就把_FadeStart设成5_FadeEnd设成15。注意LinearEyeDepth会把深度纹理里的非线性深度转换成线性距离单位是米这样参数设置起来更直观。边缘过渡还有一个细节焦散图案的边缘不要是硬边否则看起来像剪纸。可以在噪声采样之后再用一次smoothstep把噪声值映射到0到1之间并且让过渡区间宽一些。这样焦散光斑的边缘就会柔和很多看起来更像光线自然衰减的效果。3. 实操过程与核心环节实现3.1 后处理脚本的搭建先建一个C#脚本挂在相机上负责在OnRenderImage里调用Shader。脚本的核心逻辑是创建材质、设置参数、执行Blit。这里要注意材质的创建和销毁时机不要在每帧都new Material那样会产生大量GC。正确的做法是在OnEnable里创建材质在OnDisable里销毁。[ExecuteInEditMode] public class CausticsPostProcess : MonoBehaviour { public Shader causticsShader; public Texture noiseTex; public Color causticsColor new Color(1.0f, 0.95f, 0.8f, 1.0f); public float intensity 1.0f; public float tiling 5.0f; public float speedX 0.1f; public float speedY 0.07f; public float fadeStart 5.0f; public float fadeEnd 15.0f; private Material mat; void OnEnable() { if (causticsShader null) { causticsShader Shader.Find(Hidden/CausticsPostProcess); } mat new Material(causticsShader); mat.hideFlags HideFlags.HideAndDontSave; GetComponentCamera().depthTextureMode | DepthTextureMode.Depth; } void OnDisable() { if (mat ! null) { DestroyImmediate(mat); } } void OnRenderImage(RenderTexture src, RenderTexture dst) { if (mat null) { Graphics.Blit(src, dst); return; } mat.SetTexture(_NoiseTex, noiseTex); mat.SetColor(_CausticsColor, causticsColor); mat.SetFloat(_Intensity, intensity); mat.SetFloat(_Tiling, tiling); mat.SetFloat(_SpeedX, speedX); mat.SetFloat(_SpeedY, speedY); mat.SetFloat(_FadeStart, fadeStart); mat.SetFloat(_FadeEnd, fadeEnd); Graphics.Blit(src, dst, mat); } }这段脚本里[ExecuteInEditMode]让它在编辑器里也能实时预览方便调参。hideFlags设置成HideAndDontSave是为了避免材质在场景切换时被意外销毁。depthTextureMode里加了Depth确保深度纹理可用。3.2 Shader的完整实现Shader部分我写了一个Unlit的屏幕后处理Shader核心是片元着色器里的采样和混合。下面给出关键代码省略了标准的顶点着色器和属性声明。sampler2D _MainTex; sampler2D _CameraDepthTexture; sampler2D _NoiseTex; float4 _MainTex_TexelSize; float4 _CausticsColor; float _Intensity; float _Tiling; float _SpeedX; float _SpeedY; float _FadeStart; float _FadeEnd; fixed4 frag(v2f i) : SV_Target { fixed4 sceneCol tex2D(_MainTex, i.uv); float depth LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv)); float fade 1 - smoothstep(_FadeStart, _FadeEnd, depth); if (fade 0.001) { return sceneCol; } float2 noiseUV i.uv * _Tiling float2(_Time.y * _SpeedX, _Time.y * _SpeedY); float noise tex2D(_NoiseTex, noiseUV).r; noise smoothstep(0.3, 0.7, noise); float3 caustics _CausticsColor.rgb * noise * _Intensity * fade; float3 finalCol 1 - (1 - sceneCol.rgb) * (1 - caustics); return fixed4(finalCol, sceneCol.a); }这段代码里SAMPLE_DEPTH_TEXTURE是Unity提供的宏兼容不同平台。smoothstep(0.3, 0.7, noise)把噪声值重新映射让光斑边缘更柔和。fade控制深度衰减_Intensity控制整体强度。最后的屏幕混合公式是1 - (1 - a) * (1 - b)这是标准的Screen Blend。3.3 参数调试与效果微调参数调试是这套方案里最耗时间的环节。我一般按这个顺序调先调_Tiling确定光斑的密度再调_SpeedX和_SpeedY确定流动速度然后调_FadeStart和_FadeEnd确定焦散的影响范围最后调_Intensity和_CausticsColor确定亮度和色调。_Tiling的调试有个小技巧先在场景里放一个平面把相机对准它然后慢慢增大_Tiling直到光斑密度看起来和参考图接近。_SpeedX和_SpeedY建议一个设0.1另一个设0.07这样运动方向不会太单一。_FadeStart和_FadeEnd要根据场景比例来如果场景里物体距离相机大概10米那_FadeStart设5、_FadeEnd设15比较合适。实操心得调试的时候把_Intensity先设成0调好其他参数后再慢慢加上去。这样你能更清楚地看到焦散图案本身的形态不会被亮度干扰判断。3.4 性能优化与移动端适配移动端跑这套方案需要注意几点。第一噪声纹理不要太大512x512足够了再大就是浪费带宽。第二深度纹理的精度可以降一档用DepthTextureMode.Depth而不是DepthNormals省掉法线编码解码的开销。第三Shader里的分支要尽量少if (fade 0.001)这种提前返回在移动端GPU上是有代价的如果目标设备比较老可以去掉这个分支让GPU统一计算。我实测下来在骁龙865上1080P分辨率这套后处理的开销大概是1.2毫秒。如果去掉深度衰减开销能降到0.8毫秒左右。如果项目对性能极其敏感还可以把后处理的分辨率降一半用RenderTexture的halfRes模式这样开销能再降一半但焦散图案会稍微模糊一些看你能不能接受。优化手段性能收益画质损失降低噪声纹理分辨率小小去掉法线图中中去掉深度衰减中大半分辨率后处理大中去掉提前返回分支小无4. 常见问题与排查技巧实录4.1 焦散不显示或者显示异常最常见的问题是焦散完全不显示。排查顺序是这样的先检查相机有没有开启深度纹理depthTextureMode里有没有Depth再检查Shader里有没有正确声明_CameraDepthTexture然后检查_Intensity是不是被设成了0最后检查噪声纹理有没有赋值。我遇到过好几次都是因为噪声纹理忘了拖到脚本的槽里Shader里采样了一张默认的白图结果噪声值全是1焦散就变成了一片纯色。还有一种情况是焦散显示出来了但只在屏幕的一部分区域显示。这通常是深度衰减参数设得不对_FadeStart和_FadeEnd的范围太窄导致只有特定距离的物体才有焦散。把这两个值调大一些覆盖整个场景的深度范围问题就解决了。4.2 焦散边缘出现硬边或者锯齿硬边和锯齿一般是因为噪声纹理的采样精度不够或者smoothstep的过渡区间太窄。解决办法有两个一是把噪声纹理的Filter Mode设成Bilinear不要用Point二是把smoothstep的区间拉宽比如从0.3, 0.7改成0.2, 0.8。如果还有锯齿可以在采样噪声之前先对UV做一次模糊或者用tex2Dlod手动指定mipmap级别。注意如果项目开启了抗锯齿MSAA屏幕后处理可能会和MSAA冲突导致边缘出现奇怪的伪影。解决办法是在后处理之前先Resolve MSAA或者直接关掉MSAA用后处理抗锯齿代替。4.3 焦散在透明物体上表现异常透明物体不写深度所以深度图里透明物体所在的位置会显示它背后的深度。这会导致焦散出现在透明物体的背后看起来像穿透了一样。解决办法是在Shader里判断当前像素的透明度如果透明度低于某个阈值就不应用焦散。但屏幕后处理拿不到透明度信息所以这个判断做不了。更实际的做法是调整深度衰减参数让焦散只出现在不透明的物体上或者手动在场景里把透明物体排除在焦散影响范围之外。4.4 性能突然下降或者帧率波动性能突然下降通常是因为后处理的RenderTexture没有正确释放导致显存泄漏。检查一下OnRenderImage里有没有创建临时的RenderTexture但没有Release。另外如果项目里同时有多个后处理效果它们之间的Blit次数会累加每次Blit都是一次全屏采样开销不小。可以考虑把多个后处理合并到一个Shader里减少Blit次数。问题现象可能原因排查方法焦散完全不显示深度纹理未开启检查depthTextureMode焦散只在局部显示深度衰减范围太窄调大FadeStart和FadeEnd边缘硬边锯齿噪声采样精度不够改Filter Mode为Bilinear透明物体穿透透明物体不写深度调整衰减范围或排除透明物体帧率波动RenderTexture泄漏检查Release调用4.5 编辑器里正常但打包后失效这个问题我踩过好几次。最常见的原因是Shader没有被打包进Build里因为它是动态加载的Unity的静态分析可能漏掉它。解决办法是在Project Settings的Graphics里把Shader加到Always Included Shaders列表里或者在脚本里用Shader.Find之前先确保Shader在Resources文件夹里。另一个原因是移动端平台不支持某些Shader特性比如LinearEyeDepth在GLES2上可能有问题需要加平台判断或者降级方案。实操心得打包前一定要在目标平台上真机跑一遍编辑器里的表现和真机经常不一样。特别是深度纹理的精度和格式不同平台差异很大。我一般会在真机上用Frame Debugger看一下深度纹理的实际格式确认和预期一致。4.6 焦散颜色和场景光照不匹配焦散颜色如果和场景光照氛围不搭看起来会很突兀。比如场景是冷色调的夜晚焦散却是暖黄色的就会像贴上去的一样。解决办法是把焦散颜色和场景的主光源颜色关联起来在脚本里动态设置_CausticsColor。或者干脆用中性色让焦散只负责提亮不负责染色这样适配性更强。我一般会准备几套预设颜色白天用暖黄夜晚用冷蓝黄昏用橙红美术根据场景切换就行。这套方案我在三个项目里用过从PC到移动端都跑过整体稳定性不错。最大的体会是参数调试比代码实现更花时间因为焦散这种东西很主观不同的人对“自然”的定义不一样。我的建议是先把基础效果跑通然后让美术在编辑器里实时调程序只负责把参数暴露出来。这样效率最高也最容易出效果。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询