Unity URP半透明UI模糊背景:Dual Blur算法与Render Texture实战
2026/8/9 2:49:22 网站建设 项目流程

1. 项目概述与核心价值

在Unity URP项目中,想要实现一个既美观又性能可控的半透明UI模糊背景效果,是很多开发者,尤其是追求高品质视觉体验的移动端或PC端项目开发者,经常遇到的一个“痒点”。这个需求听起来简单——不就是让UI后面的东西变模糊吗?但真做起来,你会发现从简单的Image加个Blur Material,到考虑性能开销、不同分辨率适配、以及如何与URP的后处理管线和谐共处,中间隔着不少坑。我自己在几个商业项目里反复折腾过这个效果,从最初粗暴的全屏后处理,到后来优化出的基于Render Texture的局部模糊方案,积累了不少实战经验。今天,我就结合2025年Unity URP的最新特性(比如RTHandle的普及、Blitter API的优化),来手把手带你实现一个快速、高效、且易于集成的半透明UI模糊效果方案。无论你是UI特效新手,还是想优化现有方案的老手,这篇指南都会给你清晰的路径和可落地的代码。

简单来说,我们要实现的是:当打开一个弹窗或面板时,其背景(即被该UI遮挡的游戏画面)会呈现一种类似iOS或Windows 11的“亚克力”或“毛玻璃”质感——半透明且带有模糊。这不仅能提升UI的视觉层次感和沉浸感,还能引导用户聚焦于当前弹出的内容。传统的全屏后处理模糊开销太大,而简单的Shader模糊又可能效果生硬。我们的目标是找到一个平衡点。

2. 核心思路与方案选型:为什么是“双重模糊”与Render Texture?

在动手写代码之前,我们先花点时间厘清思路。实现UI背景模糊,主流方案无外乎三种,每种都有其明显的优缺点,选择哪种完全取决于你的项目需求和性能预算。

2.1 方案对比与决策

方案一:全屏后处理模糊这是最“懒人”但也最“昂贵”的方法。直接在URP的Renderer Features里加一个全屏的模糊后处理(比如Kawase Blur或Dual Blur)。所有被渲染到屏幕上的东西都会变模糊。

  • 优点:实现简单,效果统一,UI本身不需要特殊处理。
  • 缺点:性能开销巨大,因为它模糊了整个屏幕,包括那些不需要模糊的区域。更致命的是,它会让UI上需要清晰显示的文字和图标也变得模糊,通常需要额外的Mask或Stencil操作来排除,增加了复杂度。对于移动端或性能敏感的项目,基本可以第一时间排除这个方案。

方案二:基于Shader的UI材质模糊给UI Image组件直接赋予一个自定义Shader,这个Shader尝试对屏幕纹理进行采样和模糊。这通常需要传递相机渲染的纹理(_CameraColorTexture)给UI Shader。

  • 优点:理论上开销可控,只影响使用了该材质的UI元素。
  • 缺点:在URP中,_CameraColorTexture的获取和使用比内置管线更复杂,且受限于URP的渲染顺序,容易出现采样到错误内容(比如采样到了UI自身)的情况,稳定性不佳。此外,模糊范围难以精确控制(只能模糊该UI矩形区域背后的内容)。

方案三:基于Render Texture的局部模糊(我们选择的方案)这是目前综合来看最稳健、可控性最强的方案。其核心流程分为四步:

  1. 捕获:在UI渲染之前,使用一个专用的摄像机,只拍摄我们需要模糊的那部分场景(即未来会被UI遮挡的区域),将其渲染到一张Render Texture上。
  2. 模糊:对这张Render Texture应用一个高效的模糊算法(如Dual Blur),生成一张模糊后的纹理。
  3. 应用:将模糊后的纹理,作为一张普通的图片,设置为UI面板的背景。
  4. 混合:通过调整UI背景材质的透明度,实现半透明效果。
  • 优点
    • 性能可控:只模糊需要的区域,渲染开销与模糊区域大小成正比。
    • 效果精确:模糊区域与UI面板完美对齐,不会影响其他UI元素。
    • 灵活性高:可以轻松控制模糊强度、迭代次数、半透明程度。
    • 兼容性好:与URP管线、多相机、Post Processing等兼容性良好。
  • 缺点:实现步骤稍多,需要管理额外的摄像机和Render Texture。

实操心得:在经历了前两种方案的种种坑之后,我几乎在所有新项目中都坚定地选择了方案三。它的额外复杂度是值得的,因为它提供了最佳的“性能/效果”比,并且行为可预测,在项目后期优化时不会成为难以处理的“黑盒”。

2.2 关键技术点解析:Dual Blur(双重模糊)算法

既然选择了Render Texture方案,模糊算法的效率就直接决定了性能。高斯模糊效果虽好,但采样次数多,性能差。我们选择业界广泛用于实时模糊的Dual Blur(也叫双模糊或Kawase Blur的优化变种)算法。

它的核心思想是“降采样与升采样”:

  1. 降采样(DownSample):将原始纹理不断缩小(例如每次缩小一半),在每次缩小的过程中进行一种特殊的模糊(通常是取像素及其周围固定偏移点的平均值)。这个过程会快速损失高频细节(即锐利边缘),实现初步模糊。
  2. 升采样(UpSample):将经过多次降采样后得到的最小、最模糊的纹理,再逐步放大回原始尺寸。在每次放大的过程中,再次进行模糊操作。这一步能平滑因为降采样产生的锯齿,并融合不同层级的模糊信息,最终得到一个非常平滑的模糊结果。

相比传统高斯模糊需要对每个像素采样周围N*N个像素,Dual Blur通过金字塔式的处理,用更少的采样次数(通常是固定次数的降采样和升采样)达到了视觉上可接受的高质量模糊,特别适合实时渲染。

// Dual Blur 核心函数示例(片段着色器部分) half4 DualBlurPass (Varyings input, sampler2D sourceTex, float2 texelSize, int iteration, bool isDownSample) { float2 uv = input.uv; half4 sum = 0; // 一个经典的Kawase Blur核 float2 offset = texelSize * (iteration + 0.5); // 偏移量随迭代次数增大 sum += tex2D(sourceTex, uv + float2(-offset.x, -offset.y)); sum += tex2D(sourceTex, uv + float2( offset.x, -offset.y)); sum += tex2D(sourceTex, uv + float2(-offset.x, offset.y)); sum += tex2D(sourceTex, uv + float2( offset.x, offset.y)); sum *= 0.25; // 取平均 // 如果是降采样,我们通常直接返回这个模糊后的结果(纹理尺寸会减半) // 如果是升采样,则需要与上一级(更模糊)的纹理进行混合,这里简化表示 return sum; }

在Unity中,我们不会直接手写整个渲染循环,而是利用Command BufferBlitter配合Shader,通过多次Blit操作来串联降采样和升采样的过程。

3. 完整实现步骤:从零搭建模糊背景系统

理论讲完了,我们开始动手。我会按照一个清晰的步骤,带你创建所有必要的文件,并解释每一行关键代码的作用。

3.1 第一步:创建模糊Shader与材质

首先,我们需要一个执行Dual Blur算法的Shader。

  1. 在Project窗口中,右键创建Shader > Unlit Shader,命名为DualBlur
  2. 用下面的代码完全替换新建的Shader文件内容。这是一个简化但功能完整的Dual Blur Shader,包含降采样和升采样两个Pass。
Shader "Hidden/DualBlur" { Properties { _MainTex ("Texture", 2D) = "white" {} _Offset ("Offset", Float) = 1 _IsDownSample ("Is Down Sample", Int) = 1 // 1表示降采样,0表示升采样 } SubShader { Tags { "RenderType"="Opaque" } LOD 100 ZWrite Off ZTest Always Cull Off Pass // Pass 0: 降采样 (DownSample) { Name "DownSample" CGPROGRAM #pragma vertex vert #pragma fragment frag_downsample #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; float4 _MainTex_TexelSize; // Unity提供的纹理像素尺寸信息 float _Offset; int _IsDownSample; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag_downsample (v2f i) : SV_Target { // Kawase Blur 核,使用固定偏移 float2 texelSize = _MainTex_TexelSize.xy; float2 offset = texelSize * _Offset; fixed4 col = 0; col += tex2D(_MainTex, i.uv + float2(-offset.x, -offset.y)); col += tex2D(_MainTex, i.uv + float2( offset.x, -offset.y)); col += tex2D(_MainTex, i.uv + float2(-offset.x, offset.y)); col += tex2D(_MainTex, i.uv + float2( offset.x, offset.y)); col *= 0.25; return col; } ENDCG } Pass // Pass 1: 升采样 (UpSample) { Name "UpSample" CGPROGRAM #pragma vertex vert #pragma fragment frag_upsample #include "UnityCG.cginc" // 结构体和vert函数与上面相同,省略... v2f vert (appdata v) { ... } fixed4 frag_upsample (v2f i) : SV_Target { // 升采样时,我们使用一个更大的偏移,并混合更多样本以获得平滑过渡 float2 texelSize = _MainTex_TexelSize.xy; float2 offset = texelSize * _Offset * 2.0; // 升采样偏移通常更大 fixed4 col = 0; col += tex2D(_MainTex, i.uv + float2(-offset.x, -offset.y)) * 0.25; col += tex2D(_MainTex, i.uv + float2( offset.x, -offset.y)) * 0.25; col += tex2D(_MainTex, i.uv + float2(-offset.x, offset.y)) * 0.25; col += tex2D(_MainTex, i.uv + float2( offset.x, offset.y)) * 0.25; // 也可以选择与原始纹理或上一级纹理混合,这里为简化直接返回 return col; } ENDCG } } }
  1. 创建材质:在Project窗口右键该Shader,选择Create > Material,会自动生成使用此Shader的材质,命名为Mat_DualBlur

注意:这个Shader是一个简化版,实际生产环境中,降采样和升采样可能需要多Pass迭代,并且会使用RenderTexture的Mipmap或者通过多个临时RT来传递数据。我们后续会在C#脚本中组织这个迭代过程。

3.2 第二步:编写C#脚本管理模糊渲染

这是系统的核心控制器。我们创建一个名为UIBlurBackgroundController的C#脚本。

using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public class UIBlurBackgroundController : MonoBehaviour { [Header("模糊效果设置")] [Tooltip("模糊迭代次数,次数越高越模糊,性能开销越大")] [Range(1, 6)] public int blurIterations = 3; [Tooltip("每次模糊的扩散偏移系数")] [Range(0.5f, 3.0f)] public float blurOffset = 1.0f; [Tooltip("降采样比例,2表示长宽各缩小一半")] public int downSample = 2; [Header("渲染目标")] public RenderTexture finalBlurTexture; // 最终输出的模糊纹理 public Material blurMaterial; // 上一步创建的DualBlur材质 // 内部使用的临时RenderTexture private RenderTexture tempRT1; private RenderTexture tempRT2; private Camera targetCamera; void Start() { // 假设这个脚本挂载在需要模糊背景的UI Canvas或面板上 // 我们需要一个摄像机来拍摄背景 CreateBlurCamera(); // 初始化RenderTexture UpdateRenderTexture(); } void OnEnable() { // 订阅URP的渲染事件,在相机渲染完场景后、渲染UI前执行我们的模糊操作 RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering; } void OnDisable() { RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering; ReleaseRT(); } void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera) { // 确保只对主相机或我们指定的相机进行处理 if (camera != Camera.main && camera != targetCamera) return; // 如果最终纹理尚未创建或尺寸不对,则创建 if (finalBlurTexture == null || finalBlurTexture.width != Screen.width / downSample || finalBlurTexture.height != Screen.height / downSample) { UpdateRenderTexture(); } // 执行模糊渲染流程 RenderBlur(camera); } void CreateBlurCamera() { GameObject camObj = new GameObject("BlurCaptureCamera"); camObj.transform.SetParent(transform); camObj.transform.localPosition = Vector3.zero; targetCamera = camObj.AddComponent<Camera>(); targetCamera.CopyFrom(Camera.main); // 复制主相机参数 targetCamera.depth = Camera.main.depth - 1; // 确保它在主相机之前渲染 targetCamera.cullingMask = ~(1 << LayerMask.NameToLayer("UI")); // 不渲染UI层 targetCamera.clearFlags = CameraClearFlags.Depth; targetCamera.targetTexture = finalBlurTexture; targetCamera.enabled = false; // 我们不希望它自动渲染,由我们手动控制 } void UpdateRenderTexture() { ReleaseRT(); int width = Screen.width / downSample; int height = Screen.height / downSample; // 创建最终输出纹理 finalBlurTexture = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32); finalBlurTexture.filterMode = FilterMode.Bilinear; finalBlurTexture.Create(); // 创建两个临时纹理用于模糊过程中的交换 tempRT1 = RenderTexture.GetTemporary(width, height, 0, RenderTextureFormat.ARGB32); tempRT2 = RenderTexture.GetTemporary(width, height, 0, RenderTextureFormat.ARGB32); } void RenderBlur(Camera sourceCamera) { if (blurMaterial == null || finalBlurTexture == null) return; // 1. 用目标相机拍摄场景到tempRT1 targetCamera.targetTexture = tempRT1; targetCamera.Render(); // 2. 应用Dual Blur算法 RenderTexture currentSource = tempRT1; RenderTexture currentDest = tempRT2; // 降采样+模糊迭代 blurMaterial.SetInt("_IsDownSample", 1); for (int i = 0; i < blurIterations; i++) { blurMaterial.SetFloat("_Offset", blurOffset * (i + 1)); Graphics.Blit(currentSource, currentDest, blurMaterial, 0); // 使用Shader的Pass 0 (DownSample) // 交换纹理,为下一次迭代做准备 Swap(ref currentSource, ref currentDest); } // 升采样+模糊迭代 blurMaterial.SetInt("_IsDownSample", 0); for (int i = blurIterations - 1; i >= 0; i--) { blurMaterial.SetFloat("_Offset", blurOffset * (i + 1)); Graphics.Blit(currentSource, currentDest, blurMaterial, 1); // 使用Shader的Pass 1 (UpSample) Swap(ref currentSource, ref currentDest); } // 3. 将最终结果复制到finalBlurTexture Graphics.Blit(currentSource, finalBlurTexture); // 4. 将finalBlurTexture赋值给UI背景Image的材质 ApplyBlurTextureToUI(); } void ApplyBlurTextureToUI() { // 这里需要你根据UI结构来设置。 // 例如,如果你的模糊背景是一个RawImage: // GetComponent<RawImage>().texture = finalBlurTexture; // 或者通过MaterialPropertyBlock设置: MeshRenderer mr = GetComponent<MeshRenderer>(); // 如果是UI Effect等 if (mr != null) { mr.material.SetTexture("_MainTex", finalBlurTexture); } } void Swap(ref RenderTexture a, ref RenderTexture b) { RenderTexture temp = a; a = b; b = temp; } void ReleaseRT() { if (finalBlurTexture != null) finalBlurTexture.Release(); if (tempRT1 != null) RenderTexture.ReleaseTemporary(tempRT1); if (tempRT2 != null) RenderTexture.ReleaseTemporary(tempRT2); finalBlurTexture = null; tempRT1 = tempRT2 = null; } // 提供一个方法,在UI激活/打开时调用 public void EnableBlur(bool enable) { this.enabled = enable; if (enable) { UpdateRenderTexture(); } else { ReleaseRT(); } } }

这个脚本做了以下几件关键事情:

  • 事件驱动:通过订阅RenderPipelineManager.beginCameraRendering,在正确的渲染时机执行模糊操作。
  • 动态管理RT:根据屏幕分辨率和降采样参数动态创建和释放Render Texture,避免内存浪费。
  • 组织模糊流程:清晰地组织了“捕获场景 -> 降采样迭代 -> 升采样迭代 -> 输出结果”的完整管线。
  • 提供控制接口EnableBlur方法方便在UI打开/关闭时启用或禁用模糊效果,节省性能。

3.3 第三步:在URP中配置与UI集成

  1. 配置材质:将之前创建的Mat_DualBlur材质拖拽到脚本的Blur Material字段上。
  2. 创建UI背景:在你的UI面板上,创建一个RawImage组件作为背景层。将UIBlurBackgroundController脚本挂载到这个GameObject上。
  3. 脚本关联:在ApplyBlurTextureToUI方法中,取消注释RawImage相关的代码,或者根据你的UI组件类型进行修改,确保脚本能将生成的模糊纹理赋值给正确的UI元素。
  4. 调整参数:在Inspector中调整blurIterations(模糊次数,3-4次通常足够)、blurOffset(模糊强度)和downSample(初始降采样,2或4能大幅提升性能),实时查看效果变化。

3.4 第四步:实现半透明效果

模糊背景已经有了,如何实现半透明?这其实非常简单,不需要修改模糊算法本身。

  1. 修改UI背景材质:为你的UI背景(RawImageImage)使用一个支持透明度混合的Shader。Unity UI默认的UI/Default就支持。
  2. 调整颜色透明度:直接修改UI背景元素的Color属性,降低其Alpha通道值(例如,设置为0.7)。这样,模糊的背景纹理就会与底层内容进行Alpha混合,实现半透明效果。
  3. 进阶控制:你甚至可以写一个简单的Shader,让模糊纹理的透明度可以非线性变化,或者与顶点颜色、屏幕位置等因素关联,实现更动态的效果。
// 在UIBlurBackgroundController中,可以添加一个控制透明度的属性 [Range(0, 1)] public float backgroundAlpha = 0.8f; void ApplyBlurTextureToUI() { RawImage ri = GetComponent<RawImage>(); if (ri != null) { ri.texture = finalBlurTexture; Color c = ri.color; c.a = backgroundAlpha; ri.color = c; // 动态设置透明度 } }

4. 性能优化与进阶技巧

一个基础版本跑起来后,我们必须要考虑性能。特别是在移动设备上,每多一次全屏Draw Call都是昂贵的。

4.1 核心性能优化策略

  1. 降低采样分辨率:这是最有效的优化。通过downSample参数(如设为4),将捕获和模糊的纹理长宽各缩小至1/4,像素数减少到1/16,后续所有模糊操作的填充率压力骤降。在1080p屏幕上,一个4倍降采样的540p纹理进行模糊,视觉损失很小,但性能提升巨大。
  2. 控制迭代次数blurIterations每增加1,意味着至少多2次全纹理的绘制(一次降一次升)。在移动端,尝试从2次迭代开始,如果效果不足,再考虑增加到3次。通常3次迭代已经能获得非常柔和的模糊效果。
  3. 使用RTHandle系统(2025年URP最佳实践):上述示例使用了传统的RenderTexture。在新版URP中,强烈建议使用RTHandle系统来管理Render Texture。它能自动处理动态分辨率缩放(DLSS/FSR)、不同相机的不同分辨率需求,并更高效地重用纹理内存。
    // 伪代码示例:使用RTHandle private RTHandle m_BlurRTHandle; void AllocateRT() { var desc = new RenderTextureDescriptor(Screen.width/downSample, Screen.height/downSample, RenderTextureFormat.ARGB32); desc.msaaSamples = 1; RenderingUtils.ReAllocateIfNeeded(ref m_BlurRTHandle, desc, FilterMode.Bilinear, TextureWrapMode.Clamp, name: "_BlurTexture"); } // 在Blit时,使用RTHandle的rt属性作为Texture Graphics.Blit(source, m_BlurRTHandle.rt, material);
  4. 按需渲染:不要在每一帧都更新模糊纹理。只有当UI打开、关闭或背景场景发生显著变化时才需要更新。可以通过脚本控制EnableBlur方法,或者添加一个“脏标记”系统,只有当相机位置、旋转或场景中特定物体移动时才触发重新渲染模糊纹理。

4.2 进阶效果与问题排查

常见问题1:模糊区域边缘有接缝或不对齐

  • 原因:捕获模糊的相机视口(Viewport Rect)或投影矩阵与UI屏幕空间位置不匹配。
  • 解决:确保你的targetCamerarect与UI面板的屏幕矩形(可通过RectTransformUtility.CalculateRelativeRectTransformBounds计算)完全一致。可能需要将捕获相机的渲染模式设置为ScreenSpace - Camera,并将其projectionMatrix设置为与主相机一致的正交投影。

常见问题2:模糊效果闪烁或抖动

  • 原因:可能是每帧分配的临时RT尺寸有细微差异,或者是模糊迭代中使用了时间相关的参数(我们示例中没有)。
  • 解决:确保UpdateRenderTexture只在屏幕尺寸真正改变时调用(如Screen.width/height变化)。检查Shader中是否错误引入了_Time等变量。

常见问题3:性能Profiler显示开销依然很高

  • 排查:在Unity Profiler的Rendering区域,查看RenderTexture.SetDraw Mesh的次数。如果模糊迭代次数多,这里会非常密集。
  • 优化
    • 尝试使用更高效的模糊算法变种,如Box Blur(性能最好,质量稍差)或Dual Kawase Blur(质量好,性能中等)。
    • 考虑将模糊操作放在较低的渲染频率下执行,比如每2帧或3帧更新一次模糊纹理,对于动态不高的背景足够了。
    • 对于静态UI(如设置菜单),可以只在一开始计算一次模糊纹理并缓存起来,直到关闭UI。

进阶效果:动态模糊强度你可以将blurOffsetblurIterations与UI的动画(如打开、关闭的缩放动画)关联起来,实现模糊强度从0到目标值的平滑过渡,视觉体验会更佳。

// 在UI动画协程中 float duration = 0.3f; float timer = 0; int targetIterations = 3; while(timer < duration) { timer += Time.deltaTime; float t = timer / duration; // 使用缓动函数 blurIterations = (int)Mathf.Lerp(0, targetIterations, EaseOutCubic(t)); yield return null; }

进阶效果:分层模糊对于复杂的UI,你可能希望不同层级的UI背景有不同的模糊强度。这可以通过为每一层UI单独配置一个UIBlurBackgroundController并设置不同的参数来实现。注意性能叠加,不宜过多。

5. 2025年URP新特性与未来展望

随着Unity版本的迭代,URP提供了更多工具来简化这类效果的制作。

  1. Blitter API:URP提供了Blitter类,它比Graphics.Blit更高效,并且能自动处理UV翻转和多种纹理格式,是未来进行全屏后处理或类似我们这种多Pass纹理处理的首选。
  2. Fullscreen Shader Graph:对于不喜欢写CG/HLSL代码的开发者,可以尝试使用Shader Graph的Fullscreen Shader功能来构建模糊逻辑。通过自定义Renderer Feature,将Fullscreen Pass插入到渲染管线中。这种方式可视化程度高,但对于复杂的多Pass、多RT的Dual Blur流程,用节点连线的复杂度可能高于代码。
  3. Render Graph(试验性):这是URP面向未来的渲染管线抽象层,能自动管理渲染资源(如RTHandle)的生命周期和依赖关系。当你的项目升级到支持Render Graph的URP版本后,将模糊渲染流程迁移到Render Graph框架下,能获得最佳的性能和内存管理。

实现一个高性能的半透明UI模糊效果,关键在于理解“按需渲染”和“分层处理”的思想。避免为全屏买单,只为你真正需要的那一小块区域付出计算资源。本文提供的基于Render Texture和Dual Blur的方案,经过多个项目验证,是一个在效果、性能和实现复杂度之间取得良好平衡的实用方案。从搭建基础框架开始,逐步集成文中的优化技巧,你就能在项目中游刃有余地使用这个提升UI质感的神器了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询