
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 UIUGUI系统中。学习价值高通过这个方案你可以深入理解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 GetComponentRawImage(); _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 { QueueTransparent IgnoreProjectorTrue RenderTypeTransparent PreviewTypePlane } 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 }关键标签与状态设置QueueTransparent渲染队列设为透明确保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-tap5次采样的核权重分别为[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作为下一轮的输入。迭代次数与效果iteration1意味着一次水平模糊一次垂直模糊。iteration2意味着 水平-垂直-水平-垂直。你会发现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 GetComponentRawImage(); _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.shaderShader Custom/UI_GaussianBlur { Properties { [PerRendererData] _MainTex (Texture, 2D) white {} _BlurSize (Blur Size, Range(0.0, 10.0)) 1.0 } SubShader { Tags { QueueTransparent IgnoreProjectorTrue RenderTypeTransparent PreviewTypePlane CanUseSpriteAtlasTrue } 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屏幕上使用downsample2540p进行模糊视觉损失很小但性能提升极其显著。这是你必须调整的第一个参数。迭代次数Iteration的取舍iteration2水平-垂直-水平-垂直在大多数情况下已经能提供非常平滑的模糊效果。除非对品质有极端要求否则不要超过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语法与SRPURP/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质感时能自信地拿出这个五分钟就能集成到项目中的解决方案。