news 2026/10/5 9:43:41

UGUI背景高斯模糊简易方案:快照缓存与Blit迭代实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
UGUI背景高斯模糊简易方案:快照缓存与Blit迭代实现

做游戏UI的朋友大概率都遇到过这种需求:背包、商城、设置这种弹窗打开时,背后的场景要糊成一片,好让玩家把目光全放在弹窗上。这两年我经手的几个Unity项目里,UGUI高斯模糊背景基本成了标配需求。这篇文章想分享的是一个不依赖第三方插件、不需要换管线、代码量也不大的简易方案:先用相机缓存一帧低分辨率画面,再用高斯模糊Shader做多次Blit,最后显示在UGUI的RawImage上。不管你是刚接触Unity的新人,还是已经用UGUI写过不少界面的老手,这套思路都值得参考,因为它不仅能跑通,还能帮你避开我当年踩过的那些坑。

1. 为什么UGUI很难直接做背景模糊

1.1 一个看似简单、但路走不通的思路

很多第一次做这个功能的人会想:Canvas上挂一个模糊组件,把整个UI区域模糊掉不就行了?

实际做下去就会发现,UGUI的UI元素是直接绘制到屏幕空间的,它是一个个独立的矩形,渲染时不会像场景物体那样有“后处理”的插入点。UI元素根本拿不到“自己背后已经画了哪些像素”这个信息。你给某个Image加一个模糊Shader,它只能模糊自身的那张贴图,没办法把楼下那层场景、按钮、头像框统统揉进去。

这就导致网上搜“UGUI高斯模糊”时,方案五花八门,但很多其实都在绕弯路:有人把整个Canvas塞进RenderTexture,有人用第三方后处理插件,还有人干脆让美术出两张预烘焙模糊图。这些方案要么改动太大,要么卡在特定渲染管线里,很难直接抄进项目。

1.2 核心矛盾:UGUI是屏幕空间叠加层

要理解这个问题的根源,得先搞清UGUI的渲染位置。

UGUI的Canvas有三种渲染模式。默认的Screen Space - Overlay模式是直接把UI绘制在屏幕坐标上,它不经过3D相机的渲染流程。也就是说,你想对屏幕做后处理模糊,相机那边只处理到了3D场景,压根碰不到Overlay Canvas上的UI像素。即使你给主相机加了高斯模糊后处理,弹窗背后的按钮、文字、面板依然清晰得扎眼。

这也是为什么很多“UGUI模糊”教程最后做出来的效果,其实是“场景模糊 + 半透明黑底遮罩”,UI本身并没有跟着虚化。如果你的游戏主界面本来就很干净,这种效果够用;但如果你想要“整个背景UI全部糊掉,只留当前弹窗”,就必须把UI的渲染方式重新分层。

1.3 两条可行路线:快照式 vs 实时式

在实际项目里,UGUI背景模糊最终落地的方案基本可以分成两类。

第一类是快照式。弹窗打开时,先抓取一帧当前画面,对这张图做高斯模糊,把模糊结果当作RawImage的纹理显示在弹窗底部。这种做法最省性能,弹窗打开后背景是静止的,但绝大多数UI弹窗(背包、商城、角色面板)并不需要背景实时动起来,静态模糊图完全够看。

第二类是实时式。每一帧都把场景渲染到RenderTexture,然后立即做一次模糊再上屏,背景会跟着场景一起动。视觉效果确实无缝,但代价是每帧都要跑几次全屏Blit加几次后处理采样,移动端很容易发热掉帧。

这篇文章主推快照式,理由很简单:它才是真正意义上的“简易解决方案”。你在弹窗打开的那一瞬间做一次模糊,后面就不用再管它了。除了内存里多一张RT,几乎没有持续性能开销。

2. 高斯模糊要有“成本意识”:原理与参数

2.1 从二维高斯核到一维分离滤波

高斯模糊的原理,简单说就是每个像素取周围一圈像素的加权平均值,权重服从高斯分布。离中心越近的像素权重越大,离得越远权重越小,所以模糊后的画面既柔和又不会出现生硬的边缘线条。

如果直接用二维高斯核做卷积,GPU上每个像素要采样周围一个方形区域里的很多个点,成本很高。好在高斯核有一个非常实用的数学性质:二维高斯核可以拆成两个一维高斯核。先在水平方向做一维模糊,再在垂直方向做一维模糊,最终效果和直接做二维卷积几乎一样,但计算量从O(n^2)降到了O(2n)。

所以你会看到几乎所有GPU高斯模糊Shader都是两个Pass:第一个Pass做水平采样,第二个Pass做垂直采样。中间用临时RenderTexture接力。

2.2 用降采样和迭代把模糊半径做大

模糊效果强不强的关键,是采样半径够不够大。但GPU上采样半径越大,每个像素要读取的纹理次数就越多,性能会指数级变差。

一个非常划算的做法就是降采样。先把图片缩小到1/2、甚至1/4分辨率,再在这个低分辨率图上做模糊,最后放大到原始尺寸。分辨率降低之后,一次采样在原始画面里对应的范围就变大了,模糊半径等效放大一倍甚至更多。这也是为什么所有后处理模糊方案里,降采样都是标配。

还有一个词叫迭代。一次5Tap的高斯模糊只能把像素信息扩散到周围几个像素,扩散半径有限。如果对同一张图连续做两三次模糊,扩散范围会一层层往外滚,最终效果比一次性用大核模糊更柔和,性能反而更可控。

我常用的起点参数是:降采样倍数downsample=2,迭代次数iterations=3,采样扩散距离blurSpread=1.2到1.5。这个组合在移动端能跑,视觉效果也基本达到弹窗背景的常见要求。想要更糊就加iterations,想省性能就把downsample调到4。

2.3 采样优化:5次采样顶9次

刚才说的5Tap采样,是指每Pass只采集5个像素点,按照高斯权重加权平均。这其实是一个经典的GPU优化技巧。

本来一个9点的高斯模糊要采样9个纹理点,但GPU纹理采样器支持双线性插值,一次采样就能获取相邻两个像素的加权平均结果。利用这个特性,可以把两个相邻采样点合并成一个位于中间位置的采样点,采样次数直接从9次降到5次,视觉差异几乎看不出来。

经典的权重和偏移量如下:

  • 中心点:偏移0,权重0.227027
  • 两侧第一对采样点:偏移1.3846个像素,权重0.316216
  • 两侧第二对采样点:偏移3.2308个像素,权重0.070270

这五个权重加起来约等于1,而且正好是一组按高斯曲线离散化后的近似值。Shader里可以直接抄这组数,效果比那种盲目取一大圈像素求平均的坏方案细腻得多。

3. 简易方案完整落地:截图缓存 + Blit模糊 + RawImage

3.1 第一步:搭好UI层级,把模糊层放对位置

方案的大前提是弹窗Canvas仍然使用默认的Screen Space - Overlay模式,这样弹窗本身不会被主相机渲染进缓存RT,抓屏时就不需要额外隐藏弹窗。

实际场景中,你在弹窗Canvas下先建一个全屏RawImage,命名为BlurBG,放在层级最底部。RawImage的RectTransform用Anchor拉伸铺满全屏,也就是Min和Max都设成(0,0)-(1,1),四边Offset全为0。

然后记住一个重要操作:把RawImage上的Raycast Target勾掉。不然这个全屏图会把整个界面变成一个大号按钮,下面所有UI的点击事件全部被吃掉。

弹窗里的其他内容放在这个RawImage之后创建,渲染顺序自然就在模糊层之上。如果项目同时用多个Canvas,检查sortingOrder,确保弹窗Canvas排在主界面上面。

3.2 第二步:写模糊Shader

模糊Shader是整个方案的性能核心。下面这个Shader是我一直在用的,两个Pass分别是水平模糊和垂直模糊,可调参数只有三个:采样扩散距离、降采样倍数(由脚本控制)、迭代次数(由脚本控制)。

Shader "Custom/UI/GaussianBlur" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _BlurSize ("Blur Size", Range(0, 8)) = 1 } SubShader { ZTest Always Cull Off ZWrite Off CGINCLUDE #include "UnityCG.cginc" sampler2D _MainTex; float4 _MainTex_TexelSize; float _BlurSize; struct v2f { float4 pos : SV_POSITION; half2 uv[5] : TEXCOORD0; }; v2f vertBlur (appdata_img v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); half2 uv = v.texcoord; half2 d = _MainTex_TexelSize.xy * _BlurSize; o.uv[0] = uv; o.uv[1] = uv + d * 1.3846153846; o.uv[2] = uv - d * 1.3846153846; o.uv[3] = uv + d * 3.2307692308; o.uv[4] = uv - d * 3.2307692308; return o; } fixed4 fragBlur (v2f i) : SV_Target { fixed4 c = tex2D(_MainTex, i.uv[0]) * 0.2270270270; c += tex2D(_MainTex, i.uv[1]) * 0.3162162162; c += tex2D(_MainTex, i.uv[2]) * 0.3162162162; c += tex2D(_MainTex, i.uv[3]) * 0.0702702703; c += tex2D(_MainTex, i.uv[4]) * 0.0702702703; return c; } ENDCG Pass { NAME "BLUR_H" CGPROGRAM #pragma vertex vertBlur #pragma fragment fragBlur ENDCG } Pass { NAME "BLUR_V" CGPROGRAM #pragma vertex vertBlur #pragma fragment fragBlur ENDCG } } }

注意SubShader开头三行:ZTest Always Cull Off ZWrite Off。后处理Blit时,我们不在意深度测试结果,也不希望把RT写入深度缓冲。加这三行能避免很多奇怪的渲染问题。

还有个细节:_MainTex_TexelSize是Unity自动传入的源纹理单像素尺寸。水平Pass里靠它算出每个像素的UV偏移量,垂直Pass同理。所以这个Shader无论处理多大分辨率的RT,模糊的物理范围都能保持正确。

3.3 第三步:用OnRenderImage缓存屏幕画面

内置渲染管线下,最稳的抓屏时机是OnRenderImage。在这个回调里,相机的画面已经渲染完毕,我们可以把低分辨率拷贝存一份,再正常把画面输出到屏幕。

挂在主相机的脚本长这样:

using UnityEngine; public class ScreenCache : MonoBehaviour { [SerializeField] private int cacheDownsample = 2; private RenderTexture cacheRT; public RenderTexture GetCacheTexture() { return cacheRT; } private void OnRenderImage(RenderTexture source, RenderTexture destination) { int w = Mathf.Max(2, source.width / cacheDownsample); int h = Mathf.Max(2, source.height / cacheDownsample); if (cacheRT == null || cacheRT.width != w || cacheRT.height != h) { if (cacheRT != null) { RenderTexture.ReleaseTemporary(cacheRT); } cacheRT = RenderTexture.GetTemporary(w, h, 0, RenderTextureFormat.Default); } Graphics.Blit(source, cacheRT); Graphics.Blit(source, destination); } private void OnDestroy() { if (cacheRT != null) { RenderTexture.ReleaseTemporary(cacheRT); cacheRT = null; } } }

为什么这个方法比临时把相机targetTexture切走更稳?

因为OnRenderImage只是截获“相机已经渲染好的中间结果”,你不会打断相机的正常渲染流程,也不会出现一帧黑屏。缓存RT的分辨率固定为主画面的四分之一,内存和带宽开销都很小。弹窗打开时直接拿这张缓存RT做模糊即可,不需要再拉高分辨率画面。

如果你已经有一套自定义后处理链,也可以把这份缓存拷贝逻辑合并进已有的OnRenderImage,只要保证最后一行Graphics.Blit(source, destination)不丢就行。

3.4 第四步:弹窗里做模糊并赋给RawImage

核心的模糊管理器脚本如下。它处理三件事:创建临时RT、迭代做高斯模糊、把结果交给RawImage。

using UnityEngine; using UnityEngine.UI; public class UGUIGaussianBlur : MonoBehaviour { [Header("UI绑定")] [SerializeField] private RawImage blurImage; [SerializeField] private ScreenCache screenCache; [SerializeField] private Shader blurShader; [Header("模糊参数")] [Range(1, 8)] public int downsample = 2; [Range(1, 8)] public int iterations = 3; [Range(0f, 4f)] public float blurSpread = 1.2f; private Material blurMaterial; private RenderTexture sourceRT; private RenderTexture tempRT; private void Awake() { if (blurImage == null) blurImage = GetComponent<RawImage>(); if (screenCache == null) screenCache = FindObjectOfType<ScreenCache>(); if (blurShader == null) blurShader = Shader.Find("Custom/UI/GaussianBlur"); blurMaterial = new Material(blurShader); blurImage.raycastTarget = false; } public void ApplyBlur() { if (blurMaterial == null || screenCache == null) { Debug.LogWarning("UGUIGaussianBlur: 缺少必要引用"); return; } RenderTexture cacheRT = screenCache.GetCacheTexture(); if (cacheRT == null) return; int w = Mathf.Max(2, cacheRT.width / downsample); int h = Mathf.Max(2, cacheRT.height / downsample); sourceRT = RenderTexture.GetTemporary(w, h, 0, RenderTextureFormat.Default); tempRT = RenderTexture.GetTemporary(w, h, 0, RenderTextureFormat.Default); blurMaterial.SetFloat("_BlurSize", blurSpread); Graphics.Blit(cacheRT, sourceRT); for (int i = 0; i < iterations; i++) { Graphics.Blit(sourceRT, tempRT, blurMaterial, 0); Graphics.Blit(tempRT, sourceRT, blurMaterial, 1); } blurImage.texture = sourceRT; } public void ClearBlur() { if (blurImage != null) blurImage.texture = null; if (sourceRT != null) { RenderTexture.ReleaseTemporary(sourceRT); sourceRT = null; } if (tempRT != null) { RenderTexture.ReleaseTemporary(tempRT); tempRT = null; } } private void OnDestroy() { ClearBlur(); if (blurMaterial != null) Destroy(blurMaterial); } }

注意一个细节:我先用Graphics.Blit(cacheRT, sourceRT)把缓存图拷进一个与模糊迭代尺寸一致的RT,这样后续迭代中的水平Pass和垂直Pass不会互相污染。两个临时RT轮换用来回,迭代结束后sourceRT里就是最终模糊结果。

模糊后的纹理直接赋给RawImage的texture字段。弹窗关闭时调用ClearBlur()释放临时RT,并清掉RawImage的纹理引用,避免Unity里常见的“引用不存在的RenderTexture”报错。

3.5 调用方式和可复用流程

调用顺序极其简单。弹窗打开时先执行模糊,再显示弹窗Canvas即可。

public void ShowDialog() { blurPanel.gameObject.SetActive(true); blurBackground.ApplyBlur(); dialogCanvas.gameObject.SetActive(true); } public void CloseDialog() { blurBackground.ClearBlur(); blurPanel.gameObject.SetActive(false); dialogCanvas.gameObject.SetActive(false); }

如果你是Screen Space - Overlay模式,上面的顺序完全没问题。因为Overlay UI不参与主相机渲染,抓屏时根本不包含弹窗本身。

但如果你因为某些原因把弹窗Canvas切成了Screen Space - Camera模式,那抓屏前必须先隐藏弹窗Canvas,否则弹窗自己会被抓进那张背景图里,出现套娃效果。这种情况我建议先把Canvas模式改回Overlay,或者专门拆一个独立相机处理弹窗层,不要让弹窗和场景共用同一个后处理流。

如果想要弹窗打开时有一点点淡入动画,RawImage的CanvasGroup做个alpha渐变即可,模糊RT本身不需要重新生成。

4. URP、HDRP与进阶玩法

4.1 URP里取代OnRenderImage的方案

Unity在URP里已经把OnRenderImage这条路堵死了。想沿用这套快照式模糊思路,得改用ScriptableRendererFeature加ScriptableRenderPass。

核心思路是:在相机渲染的后处理阶段,把当前相机画面拷贝到一张临时RT,按照同样的迭代Blit方式做模糊,最后把模糊结果写入一个静态可访问的RenderTexture。UGUI那边,RawImage每帧从静态变量里读取纹理并显示。

using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public class BlurBackgroundFeature : ScriptableRendererFeature { [System.Serializable] public class Settings { public RenderPassEvent renderEvent = RenderPassEvent.BeforeRenderingPostProcessing; public int downsample = 2; public int iterations = 3; public float blurSpread = 1.2f; } public Settings settings = new Settings(); private BlurPass blurPass; public override void Create() { blurPass = new BlurPass(settings); } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { renderingData.cameraData.camera.TryGetComponent<Camera>(out Camera cam); // 按项目需求决定哪些相机参与模糊,这里简化为主相机 if (renderingData.cameraData.camera != Camera.main) return; renderer.EnqueuePass(blurPass); } private class BlurPass : ScriptableRenderPass { private Settings settings; private Material material; private RenderTexture blurResult; public static RenderTexture BlurResult; public BlurPass(Settings settings) { this.settings = settings; renderPassEvent = settings.renderEvent; material = new Material(Shader.Find("Custom/UI/GaussianBlur")); } public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) { // 在URP中你需要在此处申请临时RT,具体版本间API略有差异 } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { CommandBuffer cmd = CommandBufferPool.Get("BlurBackground"); // 1. 获取当前相机的color target // 2. 拷贝到临时RT // 3. 迭代模糊 // 4. 最终结果写入blurResult // 5. 把blurResult赋值给静态字段BlurResult context.ExecuteCommandBuffer(cmd); cmd.Clear(); CommandBufferPool.Release(cmd); } } }

这段代码是骨架,不同URP版本里获取cameraColorTarget的方式有差异,Unity 2021之后普遍用renderer.cameraColorTarget,但有些版本会要求你在OnCameraSetup里做一次ConfigureTarget。具体细节我建议以你安装的URP包版本官方ScriptableRenderPass示例为准。

RenderPassEvent选BeforeRenderingPostProcessing,能让你的模糊效果在Bloom、Color Grading之前生效,画面更自然。如果项目本身没有其他后处理,选AfterRenderingOpaques也行,但尽量避开默认的AfterRenderingTransparents,因为那时UI已经被画上去了,会污染抓屏内容。

4.2 让主界面UI也一起模糊的几种方式

快照式方案天然只模糊3D场景,因为Overlay UI不经过相机。如果你的主界面本身就是一套World Space或Screen Space - Camera模式的UI,那它会被渲染进相机画面,自然也会被模糊。

最好的解法,是把主界面UI从Overlay改成World Space模式。具体做法是单独建一个世界空间的Canvas,放在相机前方固定距离,让它的坐标跟着相机移动。这样OnRenderImage里抓到的画面就同时包含了3D场景和主界面UI。弹窗继续用Overlay Canvas,抓屏时不会把弹窗自己装进去。

第二种办法是用两个相机分层合成。一个相机专门渲染3D场景,另一个相机专门渲染主界面UI层,两张渲染结果先合并到同一张RT,再对此RT做模糊。这么做视觉效果最精细,能单独控制每层是否需要模糊,但代码量和调试成本都上来了,不太符合“简易”的定位。

还有一种通用做法是CommandBuffer截帧。挂在相机上,在UI渲染前后各插入一次拷贝指令,把UI层的画面单独捞出来和场景层合并后再模糊。这个方案能应对各种复杂Canvas结构,但需要比较扎实的CommandBuffer基础,适合项目已经有自定义渲染管线时用。

4.3 动态模糊、拖动模糊、局部模糊这类花样怎么做

快照式模糊虽然只生成一次,但可以做成低成本动态。

弹窗打开后,每隔几秒重新调用一次ApplyBlur(),背景就会随着场景变化缓慢刷新,看起来像实时模糊,性能压力又比每帧刷新小得多。如果你知道弹窗背后有重要动画正在播放,可以设一个计时器,在动画播放期间按60帧刷新,动画结束后立即停止刷新。

拖动模糊的玩法类似,在拖拽事件的回调里重新执行模糊。比如切牌、翻页这类交互,拖动过程让背景持续模糊,松手后定格。

局部模糊可以通过Mask实现。你在RawImage外面套一层圆形或圆角矩形的Mask,模糊背景只在特定区域可见,其余部分保持原始清晰度。这种效果比较适合角色展示、道具详情这类需要局部聚焦的界面。

这些扩展方向本质上都在“模糊算法”和“UI呈现”两个维度上做文章。把底层Shader和RT流转逻辑搞清楚,玩法可以随便加。

5. 常见问题与排错实录

5.1 模糊层挡点击怎么办

症状很典型:弹窗打开后,底下所有按钮全部点不动,连弹窗自身的按钮都觉得“有点迟钝”。

不用怀疑,就是那层全屏RawImage的问题。RawImage继承自MaskableGraphic,默认Raycast Target是开启的,它把整块屏幕都变成了可点击区域,阻挡了所有下层UI的事件。

处理方式是在RawImage的Inspector里把Raycast Target勾掉,或者在代码里设置blurImage.raycastTarget = false。我习惯在Awake里统一设置,这样即使美术在编辑器里忘了勾,运行时也能兜底。

如果模糊层下面还有其他需要保持点击交互的UI,比如地图上的一堆标记,那最好用CanvasGroup控制blocksRaycasts,把模糊层单独放在一个独立的Canvas中,通过开关这个Canvas来切换交互状态。

5.2 黑屏、画面抖动、模糊太弱

黑屏问题先检查Shader。最常见的是Shader.Find("Custom/UI/GaussianBlur")找不到对应Shader,Material创建失败,所有Blit全部白执行。解决方案:在Inspector里把Shader拖到序列化字段上,用[SerializeField] private Shader blurShader来引用,比字符串查找靠谱。

第二常见的是RT尺寸为0。如果在某些极端分辨率下Screen.width / downsample算出来是0,GetTemporary会返回一张无效RT,RawImage显示黑屏。用Mathf.Max(2, ...)兜底基本能解决。

画面抖动通常是因为抓屏时机不对。如果你在Update里临时把相机targetTexture切走再调camera.Render(),那一帧屏幕必然会闪一下。换成OnRenderImage缓存方案后,抖动的根因就消失了。

模糊太弱这个问题最好调。优先加iterations,从3加到5,效果立竿见影。如果还不够,把downsample从2改成3或4,分辨率越低,一次模糊扩散的物理范围越大。最后再动blurSpread,它只是对采样偏移的缩放,单纯调它很难出现“糊到妈都不认识”的效果。

5.3 移动端与WebGL的注意事项

移动端做这个效果,最关心的就是发热和掉帧。OnRenderImage本身在绝大多数移动设备上能用,但个别GPU驱动有兼容问题,建议在目标真机上多测几款机型。

RT格式尽量用RenderTextureFormat.Default,不要随手用DefaultHDR。HDR格式在高光区域确实更细腻,但对移动端带宽是额外负担,而一个UI模糊背景根本不需要那么高的动态范围。

参数策略上,移动端我推荐downsample=4,iterations=2。画面会偏“油画感”一点,但模糊背景本来就是退到后景的装饰,没人会贴在屏幕上找细节。等到正式发布包,再根据性能报告微调。

WebGL平台要特别关注RT的生命周期。浏览器切换标签页之后,GPU资源可能被回收,WebGL上下文也可能重建。所有RT都要在OnEnable或第一次使用时重新检查,发现IsCreated()返回false就重新生成。

另外,WebGL下尽量别频繁GetTemporary和ReleaseTemporary同一尺寸的RT,内存碎片会让页面越来越卡。像ScreenCache这种常驻缓存RT,该缓存就缓存,不要在生命周期里反复横跳。

5.4 多相机与多次渲染的兼容性

如果你的项目不止一个相机,比如主摄像机加角色展示相机、UI相机,那要特别注意ScreenCache挂在哪个相机上。

OnRenderImage里的source只代表“当前正在执行这个脚本的那个相机”的输出,并不是最终混合到屏幕上的合成结果。如果UI相机后渲染、角色相机先渲染,UI相机最终覆盖了画面,那你就应该把ScreenCache挂到UI相机上,同时把角色相机的内容想办法先合并进这个相机的渲染结果里。

一个比较偷懒的做法是:让角色相机把渲染结果输出到一张RT,再作为贴图放在UI相机画面中的一个RawImage上,最后由UI相机的ScreenCache统一抓屏。这样整个画面的最终形态就都在一个相机里了,模糊逻辑不用变。

但归根结底,UGUI背景模糊这个需求,在这个简易方案里最怕的就是“多个相机各自渲染、彼此不统一”。所以架构上最好先保证:所有需要被模糊的内容,都汇入同一个相机的最终画面。

最后说点实际做下来的体会。最早我在项目里用的是临时接管targetTexture的做法,虽然能出效果,但遇到多相机、Camera模式Canvas这种坑非常容易翻车。后来换成OnRenderImage缓存低分辨率帧,问题一下子少了大半。这套方案虽然定位是简易,但核心思路是通用的:抓一帧、降采样、迭代模糊、交给RawImage。“抓得住帧,别去碰相机本身”,这句话基本就是所有UGUI模糊方案的关键。你项目里如果已经有一套自定义后处理流程,完全可以把上面的模糊Shader直接插进去复用。要是你也在折腾UGUI背景模糊,希望这篇分享能帮你少走几步弯路。

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

从零搭建AI工程能力:手写神经网络与反向传播实战

1. 从零搭建AI工程能力&#xff1a;为什么我劝你别一上来就调包这两年“AI工程”这个词被炒得火热&#xff0c;招聘网站上挂着“AI工程师”的岗位薪资一个比一个高&#xff0c;培训班也铺天盖地地宣传“三个月转型AI”。但我带过几个新人、也帮朋友面试过不少候选人之后&#x…

作者头像 李华
网站建设 2026/10/5 9:41:03

Hindsight:基于历史反馈的大模型可观测性工程实践

1. 项目概述&#xff1a;hindsight 不是“事后诸葛亮”&#xff0c;而是一个可落地的 AI 工具链设计范式 “hindsight”这个词在日常语境里常被译作“后见之明”或“事后诸葛亮”&#xff0c;但放在当前 AI 开发实践里&#xff0c;它早已脱离了贬义色彩&#xff0c;演变成一种 …

作者头像 李华
网站建设 2026/10/5 9:40:48

从零构建AI工程能力:数据、训练、评估与部署全链路实战指南

1. 从零搭建AI工程能力&#xff1a;为什么“会调包”远远不够很多人对AI工程的理解停留在“装个环境、跑个Demo、调个API”这个层面。我刚开始接触这个领域时也是这么想的——直到第一次把模型推到真实业务场景里&#xff0c;才发现问题根本不是模型能不能跑通&#xff0c;而是…

作者头像 李华
网站建设 2026/10/5 9:40:33

Keras实战:波士顿房价回归预测全流程解析

简介&#xff1a;这份PDF教程聚焦深度学习中的回归问题&#xff0c;以波士顿房价预测为完整案例&#xff0c;面向希望用Keras在Python中开展项目实战的机器学习初学者与进阶开发者。内容从任务描述、14项特征含义讲起&#xff0c;逐步演示如何加载数据、使用StandardScaler做尺…

作者头像 李华
网站建设 2026/10/5 9:39:56

openrig 配置管理:统一管理 Claude Code 与 Codex 多模型环境

1. openrig 到底是个什么东西第一次看到 openrig 这个名字&#xff0c;很多人会以为是某个硬件外设或者开源机械臂项目。实际上&#xff0c;结合它周边的关键词——Claude Code、Codex、YAML、Node.js——可以判断出&#xff0c;openrig 是一个围绕 AI 编程助手&#xff08;尤其…

作者头像 李华
网站建设 2026/10/5 9:39:41

SPP、空洞卷积与ASPP的本质区别与选型指南

1. 这不是“又一个卷积技巧”——SPP、空洞卷积与ASPP的本质是空间感知的三重解法你打开一篇语义分割论文&#xff0c;十有八九会在backbone之后看到这几个缩写&#xff1a;SPP、ASPP、dilated convolution。它们常被笼统归为“多尺度特征融合”或“扩大感受野”的手段&#xf…

作者头像 李华