1. 项目概述:为什么我们需要弹窗背景虚化?
在Unity里做UI,尤其是弹窗,你是不是总觉得少了点“高级感”?一个平平无奇的半透明黑色遮罩,配上弹窗内容,虽然功能上没问题,但视觉上总显得廉价。回想一下你手机里的那些主流App,无论是系统设置还是社交软件,当弹窗出现时,背后的内容并非简单地变暗,而是呈现出一种柔和、朦胧的虚化效果。这种效果,就是高斯模糊。
高斯模糊能瞬间提升UI的视觉层次和沉浸感。它模拟了相机镜头失焦的物理现象,将用户的注意力自然地聚焦在前景的弹窗上,同时背景信息并未被粗暴地遮挡,而是以一种优雅的方式退居幕后。这对于需要临时打断用户操作、又不希望完全切断与背景联系的场景(如设置面板、确认对话框、奖励展示)来说,是绝佳的选择。
过去,在移动设备上实现实时高斯模糊是个性能挑战。但随着硬件性能的提升和渲染技术的优化,如今在主流设备上,一个优化得当的高斯模糊Shader完全可以做到流畅运行。这个项目,就是要带你绕开复杂的后处理管线,用最直接、最高效的方式,为你的UI弹窗实现一个即插即用的背景虚化效果。我们不会涉及庞大的后处理框架(如URP的Volume),而是聚焦于一个独立的、可附着在UI RawImage上的Shader,让你在5分钟内看到效果。
2. 核心思路与方案选型:为什么是屏幕抓取+Shader?
要实现弹窗背景的模糊,核心问题就一个:模糊谁?答案显然是模糊弹窗即将覆盖住的那部分屏幕区域。在Unity中,有几种常见的思路:
- 后处理全局模糊:使用像Unity Post Processing Stack或URP内置的Bloom/Gaussian Blur效果。这种方法会模糊整个屏幕,包括你的弹窗本身,这显然不是我们想要的。我们需要的是仅模糊弹窗背后的区域。
- 双摄像机渲染:设置两个摄像机,一个主摄像机渲染正常场景,另一个UI摄像机只渲染弹窗。然后通过脚本抓取主摄像机的渲染纹理(RenderTexture),对其应用模糊后,作为UI摄像机的背景。这种方法更灵活,但设置稍显复杂,涉及摄像机管理和渲染纹理的传递。
- 屏幕抓取与Shader局部模糊:这是我们本次采用的方案。其核心流程分为三步:
- 抓取:在弹窗即将激活的瞬间,使用
Camera.main.targetTexture或通过OnRenderImage等方法,将当前游戏视图(不含UI)渲染到一张临时的RenderTexture中。 - 传递:将这张包含背景场景的RenderTexture传递给弹窗背景板(一个全屏的RawImage组件)。
- 模糊:在这个RawImage上使用我们自定义的Shader,对传入的纹理进行高斯模糊采样,仅在这个UI元素上显示出模糊效果。
- 抓取:在弹窗即将激活的瞬间,使用
为什么选择方案3?
- 轻量精准:它只影响我们指定的UI元素,不会干扰其他UI或场景对象的渲染。
- 性能可控:模糊计算的复杂度(迭代次数、采样半径)完全由我们控制,可以根据目标设备灵活调整。
- 易于集成:不需要改动现有的渲染管线,几乎可以无缝集成到任何Unity UI(UGUI)系统中。
- 学习价值高:通过这个方案,你可以深入理解Shader中图像处理的基本方法、RenderTexture的运用,以及UGUI与Shader的配合。
接下来,我们将把这个思路拆解成可执行的代码和Shader。
2.1 工具与资源准备
在开始写代码之前,确保你的Unity项目已经就绪。我们不需要任何特殊的插件。
- Unity版本:2019.4 LTS或更新版本均可。本项目使用内置渲染管线,在URP中原理相通,但Shader语法和部分API需要调整。
- 创建必要文件:
- 在
Assets下创建一个Scripts文件夹,用于存放C#脚本。 - 在
Assets下创建一个Shaders文件夹,用于存放我们的Shader文件。 - 在
Shaders文件夹内,右键 -> Create -> Shader -> Unlit Shader,命名为UI_GaussianBlur。 - 在
Scripts文件夹内,右键 -> Create -> C# Script,命名为BlurBackground。
- 在
3. 核心组件解析:C#脚本如何抓取屏幕?
BlurBackground.cs脚本是整个效果的驱动器,它负责在恰当的时机捕捉屏幕内容,并将其传递给Shader。我们将这个脚本挂载到弹窗的背景板(一个全屏的RawImage)上。
3.1 脚本结构与初始化
using UnityEngine; using UnityEngine.UI; [RequireComponent(typeof(RawImage))] public class BlurBackground : MonoBehaviour { private RawImage _rawImage; private RenderTexture _captureTexture; private RenderTexture _blurredTexture; private Camera _mainCamera; [Header("模糊参数")] [Range(1, 6)] public int downsample = 2; [Range(0.1f, 10.0f)] public float blurSize = 3.0f; [Range(1, 10)] public int iteration = 3; private Material _blurMaterial; public Shader blurShader; void Awake() { _rawImage = GetComponent<RawImage>(); _mainCamera = Camera.main; if (blurShader == null) blurShader = Shader.Find("Custom/UI_GaussianBlur"); if (blurShader != null && blurShader.isSupported) { _blurMaterial = new Material(blurShader); } else { Debug.LogError("模糊Shader未找到或不支持,将使用默认UI材质。"); enabled = false; return; } } }代码解析与注意事项:
RequireComponent(typeof(RawImage)):确保脚本所在的GameObject上一定有RawImage组件,因为我们最终要把模糊后的图像显示在这里。_captureTexture:用于临时存储从主摄像机抓取到的原始屏幕图像。_blurredTexture:用于存储经过Shader处理后的模糊图像。- 参数说明:
downsample:降采样系数。这是性能优化的关键。值为2意味着我们先创建一个宽高为屏幕1/2的RenderTexture来进行模糊计算。像素数变为1/4,极大地减少了Shader需要处理的片段数量,对性能提升巨大,且适当降采样带来的轻微“像素感”反而能增强模糊效果。blurSize:模糊采样半径,直接影响模糊的强度。iteration:迭代次数。高斯模糊通常通过多次(迭代)应用一个较小的模糊核来实现,迭代次数越多,模糊效果越平滑,性能消耗也线性增加。
- Shader查找:在
Awake中尝试根据路径查找Shader。最佳实践是在Inspector面板上直接拖拽赋值,避免运行时查找失败。
3.2 屏幕抓取与模糊执行时机
抓取屏幕的最佳时机是在弹窗即将显示,但尚未被渲染到同一帧之前。我们可以使用Canvas的WillRenderCanvases事件,或者在弹窗激活的OnEnable方法中,使用Coroutine配合WaitForEndOfFrame来确保抓取的是最终合成的场景画面。
这里我们采用OnEnable配合协程的方式,因为它逻辑清晰,且能完美确保抓取时机。
void OnEnable() { if (_blurMaterial == null || _mainCamera == null) return; // 使用协程,在帧结束时抓取屏幕 StartCoroutine(CaptureAndBlurRoutine()); } System.Collections.IEnumerator CaptureAndBlurRoutine() { // 等待当前帧所有渲染完成 yield return new WaitForEndOfFrame(); CaptureScreen(); ApplyBlur(); ApplyToRawImage(); }关键点:WaitForEndOfFrame这个指令让协程暂停,直到当前帧中所有摄像机的渲染和GUI绘制都已完成。此时抓屏,得到的就是纯粹的、不包含当前弹窗UI的游戏场景画面,这正是我们需要的背景。
3.3 CaptureScreen:如何高效抓取?
private void CaptureScreen() { // 如果临时纹理不存在或尺寸不对,则创建它 if (_captureTexture == null || _captureTexture.width != Screen.width / downsample || _captureTexture.height != Screen.height / downsample) { if (_captureTexture != null) RenderTexture.ReleaseTemporary(_captureTexture); _captureTexture = RenderTexture.GetTemporary(Screen.width / downsample, Screen.height / downsample, 0); _captureTexture.filterMode = FilterMode.Bilinear; } // 设置主摄像机的目标纹理为我们创建的临时纹理,并立即渲染一帧 _mainCamera.targetTexture = _captureTexture; _mainCamera.Render(); _mainCamera.targetTexture = null; // 重要!渲染完后立即重置,否则主相机不会再渲染到屏幕 }操作意图与避坑指南:
- 使用
RenderTexture.GetTemporary:这是Unity推荐的管理短期RenderTexture的方式。它从一个全局缓存池中分配纹理,避免频繁new和Destroy带来的GC(垃圾回收)压力。务必在不用时(如OnDisable中)调用RenderTexture.ReleaseTemporary进行释放。 - 动态调整纹理尺寸:检查纹理尺寸是否匹配当前屏幕尺寸除以降采样系数。这确保了在游戏运行时分辨率改变(如设备旋转)后,模糊效果依然正确。
filterMode = FilterMode.Bilinear:设置纹理的过滤模式为双线性过滤,这样在后续Shader采样时,能获得更平滑的过渡,尤其是在降采样后。Camera.Render():手动调用一次相机渲染,将场景渲染到_captureTexture中。这是一个同步调用。- 重置
targetTexture:这是极易遗漏且会导致严重错误的一步。如果不将_mainCamera.targetTexture设回null,主摄像机之后的所有渲染都会输出到这张临时纹理上,导致游戏画面变黑。务必在Render()之后立刻重置。
4. Shader核心:高斯模糊算法实现
现在,我们有了包含背景场景的_captureTexture。接下来,我们需要一个Shader来处理它,实现高斯模糊。在UI_GaussianBlur.shader中,我们将实现一个双Pass(两次处理)的高斯模糊。
4.1 Shader属性与变量声明
Shader "Custom/UI_GaussianBlur" { Properties { _MainTex ("Texture", 2D) = "white" {} _BlurSize ("Blur Size", Range(0.0, 10.0)) = 1.0 } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" } Cull Off ZWrite Off ZTest Always Blend SrcAlpha OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag_horizontal #include "UnityCG.cginc" // ... 后续代码 ENDCG } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag_vertical #include "UnityCG.cginc" // ... 后续代码 ENDCG } } FallBack "UI/Default" }关键标签与状态设置:
"Queue"="Transparent":渲染队列设为透明,确保UI在正确层级渲染。ZWrite Off:关闭深度写入,UI元素通常不需要参与深度测试。ZTest Always:深度测试设为永远通过,确保它不会被场景中的物体遮挡。Blend SrcAlpha OneMinusSrcAlpha:启用Alpha混合,这是UI透明叠加的标准混合模式。- 两个Pass:第一个Pass进行水平方向的模糊,第二个Pass在第一个Pass的结果上进行垂直方向的模糊。两次一维模糊叠加,等效于一次二维高斯模糊,但采样次数从N²降低到2N,性能好得多(这就是所谓的“Separable Gaussian Blur”)。
4.2 顶点着色器与数据结构
顶点着色器是标准的,负责将模型顶点从对象空间转换到裁剪空间。
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; }4.3 片段着色器:实现一维高斯核卷积
这是Shader的精华部分。我们使用一个5次采样(中心点+左右各两个偏移点)的近似高斯核。高斯核的权重是预先计算好的常数。
// 水平模糊片段着色器 fixed4 frag_horizontal (v2f i) : SV_Target { // 高斯核权重 (近似值,和为1) float weight[3] = {0.4026, 0.2442, 0.0545}; // 中心,偏移1,偏移2的权重 // 注意:实际采样时,左右对称点共享同一个权重,所以中心权重是weight[0],第一个偏移点权重是weight[1]*2,第二个是weight[2]*2。 fixed4 col = tex2D(_MainTex, i.uv) * weight[0]; float2 texelSize = _BlurSize * _MainTex_TexelSize.xy; // _MainTex_TexelSize是每个像素的UV大小 // 水平方向左右各采样两次 col += tex2D(_MainTex, i.uv + float2(texelSize.x * 1.0, 0.0)) * weight[1]; col += tex2D(_MainTex, i.uv - float2(texelSize.x * 1.0, 0.0)) * weight[1]; col += tex2D(_MainTex, i.uv + float2(texelSize.x * 2.0, 0.0)) * weight[2]; col += tex2D(_MainTex, i.uv - float2(texelSize.x * 2.0, 0.0)) * weight[2]; return col; }原理与计算过程详解:
- 高斯核:我们使用了一个5-tap(5次采样)的核,权重分别为[0.0545, 0.2442, 0.4026, 0.2442, 0.0545]。这些权重之和为1,保证了模糊不会改变图像的整体亮度。在代码中,我们利用对称性,只存储了三个权重(中心、第一偏移、第二偏移),采样时正负偏移共用同一个权重值。
_MainTex_TexelSize:这是一个Unity提供的内置变量(需要在Properties块外声明uniform float4 _MainTex_TexelSize;)。它是一个float4,其x和y分量分别代表主纹理每个像素在U和V方向上的尺寸(即1/纹理宽度 和 1/纹理高度)。这是进行像素级偏移的标准做法。texelSize * _BlurSize:_BlurSize是我们的可调参数。用它乘以基础像素单位,就得到了最终的采样步长。_BlurSize越大,采样点离中心越远,模糊效果越强。- 采样累加:最终颜色是中心像素及其左右各两个偏移点像素的颜色,按高斯权重加权平均后的结果。
垂直模糊的片段着色器frag_vertical与此完全对称,只需将偏移方向从float2(texelSize.x * n, 0.0)改为float2(0.0, texelSize.y * n)即可。
4.4 在C#脚本中完成多Pass模糊
回到BlurBackground.cs脚本,我们需要实现ApplyBlur方法,利用刚才的Shader,对抓取的纹理进行多次迭代的水平和垂直模糊。
private void ApplyBlur() { if (_blurMaterial == null || _captureTexture == null) return; // 创建用于存储中间结果的临时RT int rtW = _captureTexture.width; int rtH = _captureTexture.height; // 获取第一个临时RT(用于第一次水平模糊) RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0); buffer0.filterMode = FilterMode.Bilinear; // 设置Shader参数 _blurMaterial.SetFloat("_BlurSize", blurSize); // 第一次处理:水平模糊 _captureTexture -> buffer0 Graphics.Blit(_captureTexture, buffer0, _blurMaterial, 0); // 使用Shader的第一个Pass // 进行迭代模糊 for (int i = 1; i < iteration; i++) { RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0); buffer1.filterMode = FilterMode.Bilinear; // 垂直模糊 buffer0 -> buffer1 Graphics.Blit(buffer0, buffer1, _blurMaterial, 1); // 使用Shader的第二个Pass // 释放上一轮的buffer0,准备下一轮 RenderTexture.ReleaseTemporary(buffer0); buffer0 = buffer1; // 如果不是最后一次迭代,需要再来一次水平模糊 if (i < iteration - 1) { buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0); buffer1.filterMode = FilterMode.Bilinear; Graphics.Blit(buffer0, buffer1, _blurMaterial, 0); RenderTexture.ReleaseTemporary(buffer0); buffer0 = buffer1; } } // 最终结果存储在buffer0中 _blurredTexture = buffer0; // 注意:_blurredTexture现在指向的是buffer0,我们稍后需要释放它,但不是在此时。 }Graphics.Blit详解:这是Unity中在CPU端调用Shader进行图像处理的核心函数。Graphics.Blit(源纹理, 目标纹理, 材质, 材质Pass索引)。Pass索引从0开始。我们通过交替使用Pass 0(水平)和Pass 1(垂直),实现了多次迭代的模糊。
性能与内存管理心得:
- 乒乓缓冲(Ping-Pong Buffer):上述代码中
buffer0和buffer1的交替使用就是典型的乒乓缓冲技术。我们只需要两个临时RT,就能完成任意次数的迭代,极大地节省了内存。每次迭代后,释放旧的输入RT,将输出RT作为下一轮的输入。 - 迭代次数与效果:
iteration=1意味着一次水平模糊+一次垂直模糊。iteration=2意味着 水平->垂直->水平->垂直。你会发现,iteration从1增加到2,效果提升非常明显;但从3增加到4,肉眼差异就很小了,但性能消耗却翻倍了。通常,迭代次数设为2或3是性价比最高的选择。
5. 完整代码整合与场景设置
5.1 完整的BlurBackground.cs脚本
将上述所有部分整合,并补充资源释放和参数更新逻辑。
using UnityEngine; using UnityEngine.UI; [RequireComponent(typeof(RawImage))] public class BlurBackground : MonoBehaviour { private RawImage _rawImage; private RenderTexture _captureTexture; private RenderTexture _blurredTexture; private Camera _mainCamera; [Header("模糊参数")] [Range(1, 6)] public int downsample = 2; [Range(0.1f, 10.0f)] public float blurSize = 3.0f; [Range(1, 10)] public int iteration = 3; private Material _blurMaterial; public Shader blurShader; void Awake() { _rawImage = GetComponent<RawImage>(); _mainCamera = Camera.main; if (blurShader == null) blurShader = Shader.Find("Custom/UI_GaussianBlur"); if (blurShader != null && blurShader.isSupported) { _blurMaterial = new Material(blurShader); _rawImage.material = _blurMaterial; // 将材质赋给RawImage } else { Debug.LogError("模糊Shader未找到或不支持,将使用默认UI材质。"); enabled = false; return; } } void OnEnable() { if (_blurMaterial == null || _mainCamera == null) return; StartCoroutine(CaptureAndBlurRoutine()); } void OnDisable() { // 禁用时释放所有临时纹理 if (_captureTexture != null) { RenderTexture.ReleaseTemporary(_captureTexture); _captureTexture = null; } if (_blurredTexture != null) { RenderTexture.ReleaseTemporary(_blurredTexture); _blurredTexture = null; } // 重置RawImage的纹理,避免显示残留 if (_rawImage != null) _rawImage.texture = null; } System.Collections.IEnumerator CaptureAndBlurRoutine() { yield return new WaitForEndOfFrame(); CaptureScreen(); ApplyBlur(); ApplyToRawImage(); } private void CaptureScreen() { if (_captureTexture == null || _captureTexture.width != Screen.width / downsample || _captureTexture.height != Screen.height / downsample) { if (_captureTexture != null) RenderTexture.ReleaseTemporary(_captureTexture); _captureTexture = RenderTexture.GetTemporary(Screen.width / downsample, Screen.height / downsample, 0); _captureTexture.filterMode = FilterMode.Bilinear; } _mainCamera.targetTexture = _captureTexture; _mainCamera.Render(); _mainCamera.targetTexture = null; } private void ApplyBlur() { if (_blurMaterial == null || _captureTexture == null) return; int rtW = _captureTexture.width; int rtH = _captureTexture.height; RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0); buffer0.filterMode = FilterMode.Bilinear; _blurMaterial.SetFloat("_BlurSize", blurSize); Graphics.Blit(_captureTexture, buffer0, _blurMaterial, 0); for (int i = 1; i < iteration; i++) { RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0); buffer1.filterMode = FilterMode.Bilinear; Graphics.Blit(buffer0, buffer1, _blurMaterial, 1); RenderTexture.ReleaseTemporary(buffer0); buffer0 = buffer1; if (i < iteration - 1) { buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0); buffer1.filterMode = FilterMode.Bilinear; Graphics.Blit(buffer0, buffer1, _blurMaterial, 0); RenderTexture.ReleaseTemporary(buffer0); buffer0 = buffer1; } } // 释放旧的_blurredTexture(如果存在) if (_blurredTexture != null) RenderTexture.ReleaseTemporary(_blurredTexture); _blurredTexture = buffer0; } private void ApplyToRawImage() { if (_rawImage != null && _blurredTexture != null) { _rawImage.texture = _blurredTexture; } } // 提供一个方法,用于动态更新模糊参数(例如通过Slider调节) public void UpdateBlurParams(int newDownsample, float newBlurSize, int newIteration) { if (newDownsample != downsample || Mathf.Abs(newBlurSize - blurSize) > 0.01f || newIteration != iteration) { downsample = newDownsample; blurSize = newBlurSize; iteration = newIteration; // 参数改变后,重新执行模糊 if (gameObject.activeInHierarchy) { StopAllCoroutines(); StartCoroutine(CaptureAndBlurRoutine()); } } } }5.2 完整的UI_GaussianBlur.shader
Shader "Custom/UI_GaussianBlur" { Properties { [PerRendererData] _MainTex ("Texture", 2D) = "white" {} _BlurSize ("Blur Size", Range(0.0, 10.0)) = 1.0 } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" } Cull Off ZWrite Off ZTest Always Blend SrcAlpha OneMinusSrcAlpha Pass { Name "HORIZONTAL_BLUR" CGPROGRAM #pragma vertex vert #pragma fragment frag_horizontal #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_TexelSize; float _BlurSize; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o; } fixed4 frag_horizontal (v2f i) : SV_Target { // 5x5 高斯核近似权重 (对称) float weight[3] = {0.4026, 0.2442, 0.0545}; float2 texelSize = _BlurSize * _MainTex_TexelSize.xy; fixed4 col = tex2D(_MainTex, i.uv) * weight[0]; col += tex2D(_MainTex, i.uv + float2(texelSize.x * 1.0, 0.0)) * weight[1]; col += tex2D(_MainTex, i.uv - float2(texelSize.x * 1.0, 0.0)) * weight[1]; col += tex2D(_MainTex, i.uv + float2(texelSize.x * 2.0, 0.0)) * weight[2]; col += tex2D(_MainTex, i.uv - float2(texelSize.x * 2.0, 0.0)) * weight[2]; return col; } ENDCG } Pass { Name "VERTICAL_BLUR" CGPROGRAM #pragma vertex vert #pragma fragment frag_vertical #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_TexelSize; float _BlurSize; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o; } fixed4 frag_vertical (v2f i) : SV_Target { float weight[3] = {0.4026, 0.2442, 0.0545}; float2 texelSize = _BlurSize * _MainTex_TexelSize.xy; fixed4 col = tex2D(_MainTex, i.uv) * weight[0]; col += tex2D(_MainTex, i.uv + float2(0.0, texelSize.y * 1.0)) * weight[1]; col += tex2D(_MainTex, i.uv - float2(0.0, texelSize.y * 1.0)) * weight[1]; col += tex2D(_MainTex, i.uv + float2(0.0, texelSize.y * 2.0)) * weight[2]; col += tex2D(_MainTex, i.uv - float2(0.0, texelSize.y * 2.0)) * weight[2]; return col; } ENDCG } } FallBack "UI/Default" }5.3 在Unity编辑器中设置
- 创建UI:创建一个Canvas,在Canvas下创建一个Panel作为弹窗。在弹窗Panel下,创建一个全屏的
RawImage作为背景板,将其锚点(Anchors)拉伸到全屏,颜色可以设为半透明白色(如#FFFFFF80)。 - 挂载脚本:将
BlurBackground脚本拖到RawImage游戏对象上。 - 赋值Shader:在
BlurBackground组件的Inspector面板上,将Blur Shader字段赋值为我们创建的Custom/UI_GaussianBlur。 - 控制弹窗:通过设置弹窗Panel的
SetActive(true/false)来控制显示和隐藏。当Panel激活时,BlurBackground.OnEnable()会自动触发,完成背景虚化。
6. 性能优化与常见问题排查
6.1 性能调优实战心得
实时模糊是相对耗时的操作,尤其是在移动设备上。以下是经过项目验证的优化策略:
- 降采样(Downsample)是第一利器:如代码所示,将抓屏和模糊计算的分辨率降低到屏幕的1/2、1/3甚至1/4,性能提升是指数级的(像素数变为1/4, 1/9, 1/16)。在1080p屏幕上,使用
downsample=2(540p)进行模糊,视觉损失很小,但性能提升极其显著。这是你必须调整的第一个参数。 - 迭代次数(Iteration)的取舍:
iteration=2(水平->垂直->水平->垂直)在大多数情况下已经能提供非常平滑的模糊效果。除非对品质有极端要求,否则不要超过3。在脚本中暴露这个参数,方便根据不同设备(如高端PC vs. 低端手机)进行配置。 - 模糊半径(Blur Size)与性能:
blurSize直接影响Shader中采样的步长。值越大,采样点离中心越远,性能消耗不变(因为采样次数固定),但过大的值可能导致采样点超出纹理边界,需要处理好边缘(我们的简单Shader没有做clamp,在实际复杂场景中可能需要处理)。通常3-5之间效果较好。 - 避免每帧更新:这个效果只在弹窗打开时计算一次。千万不要在
Update()里每帧都去抓屏和模糊!我们的OnEnable+协程方案是完美的。 - 使用RenderTexture.ReleaseTemporary:务必在
OnDisable中释放所有临时RT。内存泄漏是移动设备崩溃的常见原因。
6.2 常见问题与解决方案速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 弹窗出现后,整个游戏画面变黑 | 未重置主摄像机的targetTexture。主摄像机持续渲染到临时RT,而非屏幕。 | 在CaptureScreen()方法中,_mainCamera.Render()之后,立即设置_mainCamera.targetTexture = null; |
| 模糊效果闪烁或抖动 | 每帧都在重新计算模糊。可能是脚本被错误地放在了Update中调用,或者弹窗被频繁开关。 | 确保模糊计算只在弹窗激活时执行一次(OnEnable)。检查是否有其他代码在控制弹窗的显隐。 |
| 模糊区域出现黑色或粉色条纹 | 1. Shader中采样坐标超出了纹理范围。 2. RenderTexture未正确初始化(深度缓冲等格式问题)。 | 1. 在Shader中,对采样UV使用clamp或确保纹理Wrap Mode为Clamp。2. 检查 RenderTexture.GetTemporary的参数,特别是深度缓冲区位数。对于纯颜色模糊,深度可以设为0。 |
| 模糊效果非常“方块化”或像素感重 | 降采样系数downsample设置过大(如4或6),导致用于模糊的纹理分辨率过低。 | 降低downsample值(尝试2或3)。或者,在模糊计算后,将低分辨率的结果上采样显示到全屏RawImage时,确保RawImage的Filter Mode不是Point。 |
| 在UI滚动或动画时,模糊背景不更新 | 设计如此。我们的方案是“静态快照”,只在弹窗打开时抓取一帧。如果背景是动态的(如滚动列表),模糊背景会显得停滞。 | 对于动态背景,需要更复杂的方案,例如使用第二个摄像机专门渲染背景层到RT,并每帧更新模糊。但这会显著增加性能开销。需评估是否必要。 |
| 在URP/HDRP中无效或报错 | 内置渲染管线的Shader语法与SRP(URP/HDRP)不兼容。Camera.main.targetTexture和Graphics.Blit在SRP中用法有变。 | 1. 将Shader转换为HLSL,并使用Blit命令时指定Pass。2. 在URP中,考虑使用 Renderer Features或Blit到RenderTexture的方案。这是另一个专题,但核心分离模糊的思想不变。 |
| 编辑器下运行正常,打包后无效 | Shader没有被打包进项目。或者Shader在目标平台不被支持。 | 1. 检查Shader的“Always Included Shaders”列表,或确保其被场景/资源引用。 2. 在 Awake中检查blurShader.isSupported,并做好回退处理。 |
6.3 进阶扩展思路
当你掌握了基础版本后,可以尝试以下扩展来提升效果或应对更复杂的需求:
- 动态模糊强度:结合动画系统(Animation或DOTween),在弹窗打开时,让
blurSize从0逐渐增加到目标值,实现一个平滑的模糊动画,体验更佳。 - 与UI遮罩结合:如果你的弹窗不是全屏矩形,而是有圆角或异形,可以结合
Mask或RectMask2D组件,让模糊背景只显示在弹窗形状内部。 - 多层级模糊:对于复杂的UI,可以分层级应用不同强度的模糊。例如,最底层的场景模糊强一些,上层的功能面板模糊弱一些,营造更深的景深感。
- 性能监控:在开发阶段,使用Unity Profiler监控
Camera.Render和Graphics.Blit的耗时。特别是在低端设备上,确保一帧内完成模糊计算,避免卡顿。
这个从屏幕抓取到Shader模糊的完整方案,不仅实现了一个漂亮的UI效果,更是一个理解Unity渲染流程、纹理操作和图像处理Shader的绝佳实践。它剥离了复杂框架的包装,直击核心原理,希望这份详细的拆解能让你在下次需要提升UI质感时,能自信地拿出这个五分钟就能集成到项目中的解决方案。