Unity内置渲染管线屏幕高斯模糊后处理:Shader实现与避坑指南
刚看到这个问题的时候我第一反应是“这不就是个后处理嘛网上模板一大把”。但真到自己动手在Unity 内置渲染管线Built-in Render Pipeline里把屏幕模糊 Shader 调顺溜还是有不少容易踩的细节。尤其这几天群里好几个朋友都在问“为什么模糊之后边缘有黑边”“为什么 Unity 试玩版水印也被模糊了”“URP 里能用这套代码吗”我觉得值得把这件事从头到尾捋一遍写一篇真正能直接照着做的文章。这篇文章不只给代码还会解释为什么用双 Pass 高斯模糊、为什么权重要这么算、为什么移动端要降分辨率以及我在实际项目里遇到的各种坑。适合正在做 UI 弹窗背景模糊、游戏暂停菜单、全局特效叠加、或者单纯想在 Built-in 管线里实现“景深/聚焦”效果的朋友参考。1. 需求拆解与方案选型思路1.1 屏幕模糊在 Built-in 管线里的核心实现手段在 Unity 内置渲染管线中屏幕后处理最经典的入口是OnRenderImage(RenderTexture source, RenderTexture destination)。这个函数在相机渲染完所有不透明和透明物体之后、把图像显示到屏幕之前被调用。你可以在这里拿到整张屏幕的像素数据通过 Material 进行 Shader 采样再输出到目标 RenderTexture 或直接作为最终的屏幕图像。这个机制决定了屏幕模糊的本质对屏幕图像进行多次纹理采样并按照一定权重混合颜色。常见的模糊算法有均值模糊Box Blur对周围像素简单求平均速度快但噪点感强泛用性差。高斯模糊Gaussian Blur按高斯分布分配权重效果平滑自然是工业界最常用的方案。径向模糊Radial Blur围绕屏幕中心做拉伸模糊多用于“速度感”“冲击感”特效。Kawase 模糊利用交错降采样与双线性插值在移动端性能极好质量略逊于高斯。考虑到 Built-in 管线里没有现成的后处理栈Post Processing Stack 虽然存在但它更多服务于泛光、色调映射等模糊通常是自定义需求我最终选定可分离的高斯模糊作为主方案。原因很简单二维高斯核可以拆成水平方向和垂直方向两次一维卷积计算量从 O(n²) 降到 O(2n)在 Shader 里就是两个 Pass 的事刻画均匀效果也足够柔和。1.2 为什么选择 Built-in 而不是 URP/HDRP虽然 URP 已经成为新项目的主流选择但存量项目里 Built-in 管线依然占很大比例。很多老项目、外包单、Unity 2020 甚至 2019 时代的工程就是建立在 Built-in 之上。另外大学生做课程设计、独立开发者做小游戏 Demo很多还是在 Built-in 里开发。URP 里可以用ScriptableRenderPass做自定义后处理但代码结构和 Built-in 完全不同Blit 方式也变了。HDRP 直接用 Volume 组件那套东西更复杂不适合想快速看到效果的场景。这篇文章聚焦 Built-in是因为它足够底层、足够直观——理解了这一套再去迁移到 URP 你也能很快找到对应 API。另外还有一点Built-in 管线在移动端做后处理时RenderTexture 的使用姿势和内存管理比 URP 更“原始”踩过的坑也更典型学会了这套你在任何渲染管线里写后处理都会更从容。2. 核心知识铺垫高斯模糊的数学原理与渐进式采样2.1 高斯核与权重计算高斯模糊的权重由一维高斯函数决定G(x) (1 / (sqrt(2π) * σ)) * exp(-x² / (2σ²))其中 σ 是标准差控制模糊的扩散程度。σ 越小中心像素权重越大模糊越弱σ 越大周围像素权重越大模糊越强。在实际 Shader 里我们不会真的对每个像素周围无限范围采样而是取一个半径。比如当 σ 1.0 时大致采样 5 个点-2, -1, 0, 1, 2就能覆盖超过 95% 的权重σ 1.5 时取 7 个点比较稳妥。我习惯在 C# 端预先计算好权重数组传给 Shader而不用 Shader 内部每次实时计算 e 的幂次因为 GPU 上 exp 运算的开销相对高而且 GPU 上循环逻辑也受硬件限制如部分移动端 GPU 对动态循环支持不好。权重归一化一定不能省否则画面会整体变暗或变亮。注意这里权重计算一定以“中心像素”为参考点做对称计算并且最终需要做归一化。归一化方式就是把所有权重相加再分别除以总和保证整幅图像亮度不变。2.2 双 Pass 可分离模糊二维高斯核是“可分离”的意思是G(x, y) G(x) * G(y)所以一个二维高斯模糊可以拆成两步先对所有像素做水平方向模糊得到中间结果再对中间结果做垂直方向模糊得到最终结果。在 Unity 后处理里这个“中间结果”就是一个临时 RenderTexture。C# 端代码大致流程是申请一个临时 RTrtTemp分辨率与屏幕一致。Graphics.Blit(source, rtTemp, blurMaterial, 0)—— 执行第一个 Pass水平模糊。Graphics.Blit(rtTemp, destination, blurMaterial, 1)—— 执行第二个 Pass垂直模糊。释放临时 RT。如果要做多次迭代iterations 1就把 source 和 destination 反复互换类似乒乓渲染。2.3 降采样采样优化一次性做 7x7 甚至 9x9 的高斯模糊在 GPU 上是几百次采样对移动端压力非常大。主流优化方案是每次模糊前先对 RenderTexture 做降采样假设降为原来 1/2 或 1/4。在低分辨率图像上做模糊采样。最后再把模糊结果放大回原分辨率Blit 时自动完成。因为模糊本来就是一个低频滤波操作丢失的高频细节对最终视觉影响微乎其微。这个思路是“屏幕后处理性能优化的黄金法则”。3. 完整实现从 Shader 到 C# 脚本3.1 后处理 Shader 完整代码解析下面是我在 Built-in 管线里实测可用的屏幕模糊 Shader。分为两个 Pass第一个 Pass 做水平模糊第二个 Pass 做垂直模糊。为了兼顾移动端性能加上了_DownSampleValue来控制采样距离缩放。Shader Custom/ScreenBlurGaussian { Properties { _MainTex (Texture, 2D) white {} _BlurSize (Blur Size, Range(0, 4)) 1 _Iteration (Iteration, Int) 2 } SubShader { // 后处理需要关闭深度写入且通常不需要深度测试 Cull Off ZWrite Off ZTest Always Pass { Name Horizontal Blur CGPROGRAM #pragma vertex vert #pragma fragment frag_horizontal #include UnityCG.cginc sampler2D _MainTex; float4 _MainTex_TexelSize; float _BlurSize; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv v.uv; return o; } fixed4 frag_horizontal (v2f i) : SV_Target { fixed4 col 0; // 水平方向左右偏移 // 权重数组由 C# 端传入这里使用 _Weight0~_Weight45 卷积核 // 也可以直接用静态数组但对参数化调整不够灵活 // 偏移步长一个像素的大小 * 模糊强度 float offset _MainTex_TexelSize.x * _BlurSize; // 中心像素 col tex2D(_MainTex, i.uv) * _Weight0; // 对称采样左右两边 col tex2D(_MainTex, i.uv float2(offset, 0)) * _Weight1; col tex2D(_MainTex, i.uv - float2(offset, 0)) * _Weight1; col tex2D(_MainTex, i.uv float2(offset * 2, 0)) * _Weight2; col tex2D(_MainTex, i.uv - float2(offset * 2, 0)) * _Weight2; col tex2D(_MainTex, i.uv float2(offset * 3, 0)) * _Weight3; col tex2D(_MainTex, i.uv - float2(offset * 3, 0)) * _Weight3; return col; } ENDCG } Pass { Name Vertical Blur CGPROGRAM #pragma vertex vert #pragma fragment frag_vertical #include UnityCG.cginc sampler2D _MainTex; float4 _MainTex_TexelSize; float _BlurSize; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv v.uv; return o; } fixed4 frag_vertical (v2f i) : SV_Target { fixed4 col 0; float offset _MainTex_TexelSize.y * _BlurSize; // 中心像素 col tex2D(_MainTex, i.uv) * _Weight0; // 对称采样上下两边 col tex2D(_MainTex, i.uv float2(0, offset)) * _Weight1; col tex2D(_MainTex, i.uv - float2(0, offset)) * _Weight1; col tex2D(_MainTex, i.uv float2(0, offset * 2)) * _Weight2; col tex2D(_MainTex, i.uv - float2(0, offset * 2)) * _Weight2; col tex2D(_MainTex, i.uv float2(0, offset * 3)) * _Weight3; col tex2D(_MainTex, i.uv - float2(0, offset * 3)) * _Weight3; return col; } ENDCG } } }这段代码里用到了_Weight0到_Weight3四个权重值。对于 7 个采样点的卷积核中心权重为_Weight01 倍偏移为_Weight12 倍偏移为_Weight23 倍偏移为_Weight3。关于_MainTex_TexelSize这是 Unity 自动提供的变量表示纹理每个 texel 的尺寸。如果纹理是 1920x1080那么_MainTex_TexelSize.x 1/1920_MainTex_TexelSize.y 1/1080。用这个变量计算屏幕采样偏移可以保证无论分辨率怎么变模糊的物理距离以屏幕像素为单位是稳定的。3.2 C# 端后处理脚本完整实现using UnityEngine; [ExecuteInEditMode] [RequireComponent(typeof(Camera))] public class ScreenBlurEffect : MonoBehaviour { public Shader blurShader; public Material blurMaterial; [Range(0.5f, 2.0f)] public float downSample 1.0f; [Range(0.1f, 4.0f)] public float blurSize 1.0f; [Range(1, 6)] public int iterations 2; [Range(0.1f, 3.0f)] public float sigma 1.0f; // 用于存储计算出的权重 private float[] weights; private void OnValidate() { if (blurMaterial null blurShader ! null) { blurMaterial new Material(blurShader); blurMaterial.hideFlags HideFlags.HideAndDontSave; } BakeWeights(); UpdateMaterialProperties(); } private void OnEnable() { if (blurShader null) blurShader Shader.Find(Custom/ScreenBlurGaussian); if (blurMaterial null) { blurMaterial new Material(blurShader); blurMaterial.hideFlags HideFlags.HideAndDontSave; } BakeWeights(); UpdateMaterialProperties(); } private void OnRenderImage(RenderTexture source, RenderTexture destination) { if (blurMaterial null || blurShader null) { Graphics.Blit(source, destination); return; } BakeWeights(); UpdateMaterialProperties(); int width Mathf.Max(1, Mathf.RoundToInt(source.width / downSample)); int height Mathf.Max(1, Mathf.RoundToInt(source.height / downSample)); RenderTextureFormat rtFormat RenderTextureFormat.Default; // 申请临时 RT注意分辨率可以比屏幕小 RenderTexture temp1 RenderTexture.GetTemporary(width, height, 0, rtFormat); RenderTexture temp2 RenderTexture.GetTemporary(width, height, 0, rtFormat); // 先把屏幕图像缩放拷贝到低分辨率 RT 上 Graphics.Blit(source, temp1); // 乒乓迭代 for (int i 0; i iterations; i) { // 水平模糊temp1 - temp2 Graphics.Blit(temp1, temp2, blurMaterial, 0); // 垂直模糊temp2 - temp1 Graphics.Blit(temp2, temp1, blurMaterial, 1); } // 最后将低分辨率模糊结果放大回原分辨率 Graphics.Blit(temp1, destination); RenderTexture.ReleaseTemporary(temp1); RenderTexture.ReleaseTemporary(temp2); } private void BakeWeights() { // 使用 sigma 计算高斯卷积核 // 我们这里选择半径为 3即 7 个采样点 int radius 3; // 构建临时权重数组 float[] temp new float[radius 1]; float sum 0; float twoSigmaSquare 2.0f * sigma * sigma; for (int i 0; i radius; i) { temp[i] Mathf.Exp(-(i * i) / twoSigmaSquare); sum temp[i]; } // 对称部分加权 float normalizedSum sum * 2 - temp[0]; // 中心权重只算一次 weights new float[radius 1]; for (int i 0; i radius; i) { // 中心权重只加一次非中心权重需要乘以 2 if (i 0) weights[i] temp[i] / normalizedSum; else weights[i] temp[i] / normalizedSum; } } private void UpdateMaterialProperties() { if (blurMaterial null) return; blurMaterial.SetFloat(_BlurSize, blurSize); if (weights ! null weights.Length 4) { blurMaterial.SetFloat(_Weight0, weights[0]); blurMaterial.SetFloat(_Weight1, weights[1]); blurMaterial.SetFloat(_Weight2, weights[2]); blurMaterial.SetFloat(_Weight3, weights[3]); } // 如果是移动端或者需要省带宽可以降低 RenderTexture 精度 // 在 OnRenderImage 中使用 RenderTextureFormat 相关参数但在 Material 层面 // 如 Shader 中有 _HalfResolution 开关也可在此设置 } private void OnDisable() { if (blurMaterial ! null) { DestroyImmediate(blurMaterial); } } }这里有几个关键点值得展开说明第一OnValidate和OnEnable都调用了BakeWeights和UpdateMaterialProperties。这是为了在编辑器里滑动参数时即使没有进入播放模式也能在 Scene 视图的 Game 画面里实时看到模糊效果变化。ExecuteInEditMode让这个组件在编辑器下就能运行OnRenderImage对调参非常方便。第二RenderTexture.GetTemporary的使用。很多人会直接用new RenderTexture但在后处理里这么写会造成每帧创建、每帧 GC 的问题严重时会导致 GC Alloc 飙升和帧率抖动。GetTemporary内部有对象池用完ReleaseTemporary归还是标准做法。第三权重归一化。看了BakeWeights这段你会发现归一化公式是normalizedSum sum * 2 - temp[0]。原因是高斯核是对称的我只需要计算半径 0 到 3 的原始权重而实际卷积时除了中心的i0之外每个i会被采样两次左边一次、右边一次所以总权重等于中心权重加上两侧权重的两倍。这个细节常常被忽略但一旦做错画面就会偏亮或偏暗肉眼很难察觉但直方图能看出来。3.3 参数计算与选择逻辑用 σ 1.0半径 3 来算一组权重示例temp[0] exp(0) 1temp[1] exp(-1/(2*1)) exp(-0.5) ≈ 0.6065temp[2] exp(-4/(2*1)) exp(-2) ≈ 0.1353temp[3] exp(-9/(2*1)) exp(-4.5) ≈ 0.0111sum 1 0.6065 0.1353 0.0111 ≈ 1.7529normalizedSum 1.7529 * 2 - 1 2.5058权重归一化后_Weight0 1 / 2.5058 ≈ 0.399_Weight1 0.6065 / 2.5058 ≈ 0.242_Weight2 0.1353 / 2.5058 ≈ 0.054_Weight3 0.0111 / 2.5058 ≈ 0.0044可以看出距离中心 3 个像素的权重已经非常小5 个像素的权重更是可以忽略。这就是为什么 7 个采样点足够覆盖大多数情况。如果 σ 增大到 2.0那么分布更平缓权重会分布得更均匀模糊效果更强但采样点同样是 7 个时边缘会产生“截断”感——因为哈希模式的原始权重之外被强行归零。这就是为什么我建议 σ 不超过 3.0否则要么增加半径要么接受轻微瑕疵。4. 实操过程搭建场景与调试4.1 最小复现步骤说了这么多理论下面我在 Unity 里完整演示一遍实际操作流程。第一步创建一个空场景添加一个主相机默认就有。在相机的Inspector面板里把Allow MSAA关掉。为什么因为在 Built-in 管线里后处理在 MSAA 之后进行但OnRenderImage拿到的 source 纹理往往是不含 MSAA 样本的如果你开着 MSAA边缘锯齿仍然明显而模糊本身会掩盖锯齿所以一般做后处理时建议关闭或降低 MSAA。第二步在场景里放几个颜色对比强烈的物体比如纯色 Cube 加一个高对比贴图的地面这样可以直观看到模糊效果。第三步创建 Shader 文件和 C# 脚本重命名分别为ScreenBlurGaussian.shader和ScreenBlurEffect.cs内容使用上面的代码。第四步创建材质在 Project 窗口点击Create - Material命名BlurMaterialShader 选择Custom/ScreenBlurGaussian。第五步把ScreenBlurEffect.cs挂到 Camera 上点击播放或者在编辑模式下就能在 Game 视图看到模糊效果。如果一切顺利你会看到整个画面被模糊掉了。这时候调试参数就变得非常重要。4.2 参数调节建议与效果预览对照我推荐一组常用的初始参数Down Sample 2即降采样到 1/4 分辨率移动端性能敏感可改到 4。Iterations 2做两次水平垂直效果细腻。Blur Size 1.0采样偏移的基础倍数。Sigma 1.0权重分布的基本柔和感。这套参数在 1080p 分辨率下PC 端基本无压力移动端中端机型也能跑到 60 FPS前提是降采样到 2 以上。调参时注意观察三个屏幕区域高对比边缘看有没有黑边或白边、大面积纯色区域看有没有噪点感、文字/UI 区域如果 UI 不需要模糊需要额外处理后面会讲。4.3 UI 层是否要被模糊一个非常高频的问题我只想模糊 3D 场景但 UI 层Canvas不想被模糊怎么做这里要分情况。如果你用的是 Screen Space Overlay 模式的 CanvasUI 是后处理的“后面”渲染的OnRenderImage拿到的 source 里不包含 UI模糊只会作用在 3D 画面上最终 UI 仍然锐利。如果你的 Canvas 用了 Screen Space Camera 模式并且 Canvas 的 Sort Order 在某个相机之后那它可能已经被合入 source就会被一起模糊。如果你希望 UI 的一部分比如弹窗背景是模糊的而 UI 的其余部分不模糊那就不能用这种全局后处理方案了。这时候更推荐用“UI 专用模糊”把相机画面抓到一个 RenderTexture赋给 UI 的 RawImage再对 RawImage 区域的局部使用另一种模糊 Shader。这个方法我在很多手游的公告弹窗、暂停面板里都见过也就是“局部屏幕模糊”。提示全局后处理模糊是“所见即所得”的你用 Game 视图看到的模糊效果就是最终成品效果。如果最终要打包成小游戏比如微信小游戏需要特别注意 RenderTexture 格式和内存占用这个后面说。5. 常见问题与排查技巧实录5.1 模糊没有生效我该检查什么最可能的原因是OnRenderImage没有被调用或者 Material 没有正确赋值。这里有一个排查思路清单确认 Camera 上挂了脚本且脚本OnEnable里Shader.Find成功没有返回 null。如果 Shader 名拼错会静默失败。确认 Material 的 Shader 确实是Custom/ScreenBlurGaussian。直接在材质球上点击 Shader 下拉框检查。确认OnRenderImage里blurMaterial非空。在OnRenderImage第一行打印日志看有没有执行。如果你的 Camera 组件的Target Texture不为空OnRenderImage的行为会有所不同destination参数会变成那个 RenderTexture有时候会让人误以为“没生效”。编辑器下未进入播放模式且没有勾选ExecuteInEditMode那么在 Scene 视图看不到效果但 Game 视图也不一定看得到因为编辑预览模式下相机渲染是实时的。保险起见先在 Play 模式里测试。如果你用了CommandBuffer或别的相机后处理插件比如老版本的 Post Processing Stack可能会发生 OnRenderImage 互相覆盖的情况需要排查是否有别的脚本也在处理OnRenderImage。5.2 模糊之后出现黑边或白边这通常和 UV 边缘采样有关。屏幕空间后处理时边缘像素的采样会超出 0~1 范围默认的纹理采样方式是 Clamp超出边缘时会一直取边缘像素颜色这不会导致黑边。但如果你把 RenderTexture 的 wrap mode 设置成了 Repeat边缘就会“回绕”出现不可预测的颜色。另一种常见来源是多个迭代之后边缘信息被拉长。如果迭代次数多且_BlurSize大边缘处会形成一圈半透明的“光晕”。这时需要做边缘保护常见做法是限制采样 UV 范围在 [0, 1] 之内超出则不采样。使用tex2Dlod配合Clamp。在迭代之间对边缘做裁边处理牺牲一点无缝感。还有一个隐藏原因Gamma 空间和线性空间处理不一致。如果项目在 Gamma 色彩空间下用这套 Shader采样权重呈线性变化但如果项目在线性空间下Shader 输出的颜色还需要做一次 sRGB 转换。如果不手动处理边缘会显得过亮。5.3 移动端卡顿与内存优化技巧移动端跑模糊最大的问题是带宽。屏幕上很多像素每个像素要采样多次GPU 的纹理单元很快会成为瓶颈。我实测过一个中端 Android 机骁龙 778G不降采样跑 720p 的高斯模糊迭代 3 次帧率明显下跌到 30FPS 以下。降采样到 1/4 后同样迭代 3 次可以稳定 50-60FPS。所以在移动端我的建议是降采样至少 2 倍。迭代次数 1-2 次足够。使用RenderTextureFormat.RGB111110Float而不是ARGB32可以减少带宽。如果 XR 平台如 Pico 4、QuestRT 分辨率要和单眼分辨率匹配不要盲目把降采样调太低否则会导致画面抖动。注意避免每帧动态分配 RenderTextureGetTemporary是对象池方案但如果每次GetTemporary的参数都不同比如传感器分辨率变化对象池会频繁扩容也会有开销。5.4 为什么我看到的模糊是“方块状”的这种块状感一般来自降采样后没有做平滑放大。Graphics.Blit默认使用的FilterMode是双线性Bilinear理论上放大时应该平滑。如果你在 Shader 里写了sampler2D而没有设置采样器状态或者纹理没有正确设置filterMode FilterMode.Bilinear就会出现像素感。检查RenderTexture.filterMode是否设为Bilinear。另外当迭代次数较多时如果 RT 分辨率太低比如降到 1/8模糊后的图像再放大回去就会出现很明显的低分辨率模糊感也就是“方块状”。这不算 bug而是降采样的固有代价。需要平衡性能与画质。5.5 场景中有 UI 被模糊怎么办上面提过用Screen Space Overlay的 Canvas 不会被后处理影响到。但是当你把 Canvas 切成Screen Space Camera并把它的Plane Distance设得很近时UI 可能会被合入相机画面。解决方式有三种使用两个相机一个渲染 UI一个渲染 3D 场景UI 相机在后处理相机之后单独输出。在OnRenderImage中把 UI 层通过Canvas.targetTexture单独渲染出来再在最终合成时叠加。在 Shader 里做一个_BlurMask纹理用 mask 控制哪些区域模糊哪些区域保持清晰。这个方案最灵活适合“弹窗周围模糊、弹窗内部不模糊”的场景。我自己在实际项目里用的是第三种mask 用另一台相机渲染 UI 形状或者美术直接画一张 UI 遮罩贴图。缺点是 Canvas 如果有动态内容mask 和 UI 的同步比较麻烦。6. 进阶扩展Kawase 模糊与 CommandBuffer 方案6.1 Kawase 模糊思路如果要在移动端追求极致性能很多人会选择 Kawase 模糊而不是高斯模糊。Kawase 模糊是“交错降采样 多个小核混叠”的思路。它的做法是把图像缩小到 1/2采样中心像素和四个对角线像素然后逐步放大每一步都做一次同样的采样。因为每次只做 5 次采样比 7 次高斯采样更轻而且多次迭代叠加效果非常柔和。换成 Kawase 方案后C# 端逻辑基本不变只是 Shader 内部 Pass 变成“带间距的四方向采样 加权平均”。如果你的项目对性能极致敏感我建议研究一下网上也有不少成熟的开源实现。6.2 CommandBuffer 方案OnRenderImage虽然简单但也有局限它只能处理整张屏幕无法细粒度控制执行时机也无法和多个后处理效果优雅排队。如果你要做链式后处理模糊 泛光 色调映射用CommandBuffer会更工程化。CommandBuffer 方案核心是把 Blit 命令按顺序插入到相机渲染管线中比如在AfterRendering事件里插入多个 Blit。代码结构更“现代”也方便复用。缺点是代码量更大调试也更复杂。对于新手我会建议先掌握OnRenderImage等确实需要多效果链式叠加时再升级。6.3 与小游戏平台的兼容性微信小游戏、抖音小游戏这些平台由于浏览器环境限制RenderTexture 的使用有一些特殊要求。OnRenderImage本身可以被支持但需要注意RenderTextureFormat需使用 Mobile 兼容类型ARGB32通常最保险。尽量少的 GetTemporary 数量防止内存碎片化。有些小游戏平台在高分屏下对 RenderTexture 精度有要求降采样倍数不能太高否则文字会糊。这一块细节很多但理解了 Built-in 里的这套后处理流程换到小游戏平台只是调整参数的事。7. 总结一下我实际项目里的心得屏幕模糊这个需求看起来就是个十几行的 Shader 加几十行的 C#但真正在生产环境里落地牵扯到渲染管线、内存、平台、UI 层级、色彩空间方方面面。我个人的体会是先想清楚你要模糊的区域是“全屏”“半屏”还是“UI 局部”再决定技术方案。全屏大背景直接用本文的方案局部弹窗建议用 RenderTexture RawImage 方案动态 UI 遮罩建议用 Mask 方案千万不要一上来就全局后处理否则后期会被 UI 适配坑死。另外一个经验是调参一定要用真正的目标分辨率去调。编辑器里 720p 的 Game 视图看着很顺眼发布到 2K 屏手机上可能糊得没法看。我在做一款 3D 解谜手游时就吃过这个亏编辑器下调好的模糊强度打包到真机上浓得看不清按钮最后只能改用 URP 的动态分辨率加上按屏幕宽高比动态调整_BlurSize才解决。最后再分享一个小技巧如果团队里有美术同学可以把BlurSize、DownSample、Iterations交给美术去调但一定要给一个“推荐参数区间”的文档。因为这些参数是相互耦合的BlurSize 4配合Iterations 1和BlurSize 1配合Iterations 4实际效果完全不同美术不知道原理就会一路乱试。代码并不复杂复杂的是你在不同场景下做出正确取舍。希望这篇文章能让你的下一次屏幕模糊实现少走一些弯路。