news 2026/9/26 4:52:54

Unity内置渲染管线屏幕高斯模糊后处理:Shader实现与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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# 端代码大致流程是:

  1. 申请一个临时 RTrtTemp,分辨率与屏幕一致。
  2. Graphics.Blit(source, rtTemp, blurMaterial, 0)—— 执行第一个 Pass(水平模糊)。
  3. Graphics.Blit(rtTemp, destination, blurMaterial, 1)—— 执行第二个 Pass(垂直模糊)。
  4. 释放临时 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~_Weight4(5 卷积核) // 也可以直接用静态数组,但对参数化调整不够灵活 // 偏移步长:一个像素的大小 * 模糊强度 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 个采样点的卷积核,中心权重为_Weight0,1 倍偏移为_Weight1,2 倍偏移为_Weight2,3 倍偏移为_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 的原始权重,而实际卷积时,除了中心的i=0之外,每个i会被采样两次(左边一次、右边一次),所以总权重等于中心权重加上两侧权重的两倍。这个细节常常被忽略,但一旦做错,画面就会偏亮或偏暗,肉眼很难察觉但直方图能看出来。

3.3 参数计算与选择逻辑

用 σ = 1.0,半径 3 来算一组权重示例:

  • temp[0] = exp(0) = 1
  • temp[1] = exp(-1/(2*1)) = exp(-0.5) ≈ 0.6065
  • temp[2] = exp(-4/(2*1)) = exp(-2) ≈ 0.1353
  • temp[3] = exp(-9/(2*1)) = exp(-4.5) ≈ 0.0111
  • sum = 1 + 0.6065 + 0.1353 + 0.0111 ≈ 1.7529
  • normalizedSum = 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,命名BlurMaterial,Shader 选择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 模式的 Canvas,UI 是后处理的“后面”渲染的,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 没有正确赋值。这里有一个排查思路清单:

  1. 确认 Camera 上挂了脚本,且脚本OnEnable里Shader.Find成功没有返回 null。如果 Shader 名拼错,会静默失败。
  2. 确认 Material 的 Shader 确实是Custom/ScreenBlurGaussian。直接在材质球上点击 Shader 下拉框检查。
  3. 确认OnRenderImage里blurMaterial非空。在OnRenderImage第一行打印日志看有没有执行。
  4. 如果你的 Camera 组件的Target Texture不为空,OnRenderImage的行为会有所不同,destination参数会变成那个 RenderTexture,有时候会让人误以为“没生效”。
  5. 编辑器下未进入播放模式且没有勾选ExecuteInEditMode,那么在 Scene 视图看不到效果,但 Game 视图也不一定看得到,因为编辑预览模式下相机渲染是实时的。保险起见先在 Play 模式里测试。
  6. 如果你用了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、Quest),RT 分辨率要和单眼分辨率匹配,不要盲目把降采样调太低,否则会导致画面抖动。
  • 注意避免每帧动态分配 RenderTexture,GetTemporary是对象池方案,但如果每次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 可能会被合入相机画面。解决方式有三种:

  1. 使用两个相机:一个渲染 UI,一个渲染 3D 场景,UI 相机在后处理相机之后单独输出。
  2. 在OnRenderImage中,把 UI 层通过Canvas.targetTexture单独渲染出来,再在最终合成时叠加。
  3. 在 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实际效果完全不同,美术不知道原理就会一路乱试。

代码并不复杂,复杂的是你在不同场景下做出正确取舍。希望这篇文章能让你的下一次屏幕模糊实现少走一些弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 4:51:35

FreeRouting开源PCB自动布线工具:从原理到实战的完整指南

1. 为什么我要认真聊聊FreeRouting这个开源布线工具画PCB这件事&#xff0c;说到底是电子工程师的日常。但凡是做过板子的人都知道&#xff0c;布局布线是整个流程里最磨人的环节——尤其是当你面对一块几百个网络的板子&#xff0c;手动一根根拉线&#xff0c;眼睛都快看花了。…

作者头像 李华
网站建设 2026/9/26 4:51:06

域名注册和域名解析要分清:网站搭建全流程与打不开问题排查

做网站这些年&#xff0c;我见过最多一类问题就是&#xff1a;域名也买了、代码也上传了、服务器也搭好了&#xff0c;但打开浏览器输入域名就是打不开、报无法访问的错。仔细一查&#xff0c;十有八九是把域名注册和域名解析这两件事搞混了。注册是花钱把名字“买下来”&#…

作者头像 李华
网站建设 2026/9/26 4:50:24

前端安全面试核心考点:XSS、CSRF、CSP与JWT防御实战

1. 前端安全面试的底层逻辑拆解1.1 为什么前端安全成了面试必考题早些年面前端&#xff0c;安全这块顶多问一句“你知道XSS吗”&#xff0c;候选人回一句“跨站脚本攻击&#xff0c;要转义”&#xff0c;面试官点点头就过去了。现在完全不是这个节奏。我最近帮团队筛简历、做技…

作者头像 李华
网站建设 2026/9/26 4:49:38

宠物咖啡馆系统实战:SpringBoot+Vue+MyBatis+MySQL

这个项目是去年朋友开宠物咖啡馆时拉我一起做的。他的店不算小&#xff0c;两层楼&#xff0c;一层饮品区加零售货架&#xff0c;二层是宠物互动区和寄养间&#xff0c;周末高峰期一天要接待上百拨客人。开业不到两个月&#xff0c;纸质本子和Excel已经扛不住了&#xff1a;点单…

作者头像 李华
网站建设 2026/9/26 4:49:38

SpringBoot+Vue前后端分离实战:高校饮食推荐系统设计与推荐算法实现

1. 项目整体设计与模块拆解高校学生饮食推荐系统&#xff0c;听起来名字有点长&#xff0c;但本质就是一个典型的“信息管理系统 推荐逻辑”的课程设计/毕业设计项目。我拿到这套SpringBoot后端 Vue前端 MySQL数据库的源码时&#xff0c;第一反应是&#xff1a;这不就是现在…

作者头像 李华
网站建设 2026/9/26 4:49:28

GEE非商业版配额制落地:4月27日前必须完成的申请与调整

你打开 Google Earth Engine 代码编辑器&#xff0c;右上角或者 Cloud Console 的配额页面多半已经挂了一条提醒&#xff1a;非商业版正式引入计算配额制度&#xff0c;分区申请需要在 4 月 27 日前完成。第一眼看到时我以为是普通的产品更新&#xff0c;仔细读完才发现这次是动…

作者头像 李华