简介面向Unity开发者的一份URP后处理实验工程聚焦Kawase模糊在通用渲染管线中的实现方式适合已了解RendererFeature基本概念、希望动手验证多Pass渲染的中级学习者。工程演示如何借助CommandBuffer完成多次绘制、按需增减模糊迭代次数并将中间结果存入临时纹理配合毛玻璃演示场景可直观体会降采样层级与模糊半径的关系。压缩包共66个文件约133KB以asset场景与管线配置、preset预设、mat材质、shader与shadergraph着色器、C#脚本为主另含工程设置、README说明与演示截图体量轻巧但结构完整、可直接运行。已有865人学习下载。读者能获得可导入URP工程的后处理范例、多Pass命令缓冲的调用方式以及Kawase模糊的着色器与参数组织思路便于改造成背景虚化、UI毛玻璃等效果。1. 屏幕毛玻璃不该拖垮帧率做 UI 背景毛玻璃或者角色受伤时的屏幕模糊很多人第一反应是套一个高斯模糊后处理。等真在 Unity URP 里跑起来才发现效果是有了手机上一测帧率直接掉一截。这份 urp_kawase_blur 走的是另一条路——用 Kawase Blur 这种采样次数少、天然分级的算法把整套逻辑封进一个 RendererFeature挂在任意 URP 相机上就能开模糊。它本身是个渲染功能测试工程Shader、RenderPass、参数面板和示例场景都摊开摆在一起没有额外依赖。适合两类人刚接触 URP 自定义渲染、想照着抄一个能跑通的 RendererFeature 的以及已经写了模糊、但卡在采样数和分辨率缩放上的。下面按我拆这份工程的顺序讲。2. 工程结构与选型为什么是 Kawase 而不是高斯拿到一个渲染工程我习惯先看目录再看管线。目录告诉你作者把哪些东西当成了一等公民管线配置告诉你这段代码介入的是渲染流程的哪一环。这个工程的目录结构很干净三类文件分得很清楚Feature、Pass、Shader没有多余的 Manager 和单例。2.1 目录结构与资源组成不同人整理的测试工程目录名字会有出入但认准这三类文件就够了。下面是我按照它实际包含的内容整理出的结构后面所有章节都会落到这些文件上urp_kawase_blur/ ├─ Assets/ │ ├─ KawaseBlur/ │ │ ├─ Runtime/ │ │ │ ├─ KawaseBlurFeature.cs # RendererFeature 入口挂在 Renderer Data 上 │ │ │ ├─ KawaseBlurPass.cs # ScriptableRenderPass降/升采样循环 │ │ │ └─ KawaseBlurSettings.cs # 可序列化参数容器 │ │ ├─ Shaders/ │ │ │ ├─ DualKawase.shader # 降采样 / 升采样 / 合成三个 Pass │ │ │ └─ KawaseCommon.hlsl # 顶点函数与公共采样宏 │ │ └─ Materials/ │ │ └─ KawaseBlur.mat # 引用 DualKawase.shader │ ├─ Scenes/ │ │ └─ BlurTest.unity # 示例场景含 UI 面板与测试模型 │ └─ Settings/ │ ├─ URP-Performance.asset # URP Asset │ └─ URP-Renderer.asset # Universal Renderer Data文件职责改动频率KawaseBlurFeature.cs生命周期入口Create()建 PassAddRenderPasses()入队低KawaseBlurPass.cs真正干活的渲染逻辑管临时 RT 和迭代高KawaseBlurSettings.cs参数序列化Inspector 里能直接调中DualKawase.shader采样核决定模糊质量与性能中这套拆分的好处是改效果不动 C#改流程不动 Shader。我在项目里见过把采样核写进 C# 里拼字符串的版本调一次参数要重新编译一次那才叫遭罪。2.2 用 RendererFeature 而不是 PostProcessVolumeURP 里做全局后处理有两条路一条是自定义ScriptableRendererFeatureScriptableRenderPass另一条是塞进 Volume 框架走PostProcessVolume。这个工程选的是前者原因很实际。Volume 那套要求你实现VolumeComponent和PostProcessPass还要跟 URP 内置的后处理栈抢执行顺序稍微调错一点就会跟 Bloom、Color Grading 打架。而 RendererFeature 是官方给的扩展点renderPassEvent一个字段就能决定这段代码插在渲染流程的哪一步跟内置后处理互不干扰。对于「我就想要一个模糊」这个需求Feature 是最短路径。再看算法本身。高斯模糊想做半径 8 像素的效果二维核需要采 17×17 次利用可分离性也要 1717 次采样次数随半径线性甚至平方增长。Kawase Blur 的思路完全不同它不追求数学上的精确高斯分布而是通过反复的降采样 升采样在每一级里只用 4 次采样就能造出接近高斯的柔化效果而且天然带分级信息。维度可分离高斯双 Kawase大半径采样次数随半径线性增长每级固定 4 次是否天然分级否是降采样自带 mip大半径质量均匀、干净轻微带状可通过偏移抖动缓解适用场景静态图、离线实时相机后处理、UI 毛玻璃提示Kawase 不是「高斯的下位替代」。半径小的时候两者肉眼几乎没差别半径大到 30 像素以上Kawase 的采样开销优势才真正拉开。选它是因为你的画面在动而不是因为它便宜。2.3 一条模糊请求从进入到返回的完整链路把工程跑起来之后我用 Frame Debugger 顺着看了一遍整条链路的顺序是这样的URP 渲染完不透明物体结果写进相机 Color Target相机调用AddRenderPassesFeature 判断材质是否有效、相机类型是否合法然后EnqueuePassOnCameraSetup里根据相机描述符算出迭代次数和临时 RT 尺寸Execute里连续 Blit先逐级降到 1/2、1/4、1/8再逐级升回去最后一级结果写回相机颜色后面的透明物体和后处理继续在模糊后的画面上叠加。这里有个容易被忽略的细节第 5 步写回的是相机自己的 Color Target。如果你把renderPassEvent设在了透明物体渲染之后那透明物体包括大部分 UI就不会被糊到设在了之后又会让后面的东西全糊。选择哪个事件值本质上是在回答「我要模糊的是哪些物体」这个后面第 4 章会展开。3. 核心模块拆解双通道降采样与升采样这一章是整个工程的重头戏。Shader 决定糊出来好不好看C# 决定这块糊得对不对、贵不贵。我按「先看像素怎么采、再看 RT 怎么走、最后看参数怎么进」的顺序拆。3.1 Shader 里的采样核4 次采样等效 3×3 均值双 Kawase 的降采样 Pass 只做一件事——在四个对角方向上各采一次取平均。因为 GPU 的双线性采样本身就在 2×2 纹素内做了插值四个对角采样点加起来等效于一个 3×3 的均值核但只花了 4 次采样。Shader Hidden/UrpKawaseBlur/DualKawase { Properties { _MainTex (Source, 2D) white {} } SubShader { Tags { RenderPipeline UniversalPipeline } Cull Off ZWrite Off ZTest Always // 全屏 Blit 的固定搭配 HLSLINCLUDE #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); float4 _MainTex_TexelSize; // x 1/width, y 1/height float _Offset; // 采样偏移单位是像素逐级递增 float _Blend; // 与原画面混合的权重 struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; }; Varyings Vert(Attributes v) { Varyings o; o.positionCS TransformObjectToHClip(v.positionOS.xyz); o.uv v.uv; return o; } ENDHLSL // Pass 0降采样四点对角采样 Pass { Name KawaseDown HLSLPROGRAM #pragma vertex Vert #pragma fragment FragDown float4 FragDown(Varyings i) : SV_Target { float2 texel _MainTex_TexelSize.xy * _Offset; float4 sum SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv float2( texel.x, texel.y)); sum SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv float2(-texel.x, texel.y)); sum SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv float2( texel.x, -texel.y)); sum SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv float2(-texel.x, -texel.y)); return sum * 0.25; } ENDHLSL } // Pass 1升采样把上一级结果叠回来 Pass { Name KawaseUp HLSLPROGRAM #pragma vertex Vert #pragma fragment FragUp float4 FragUp(Varyings i) : SV_Target { float2 texel _MainTex_TexelSize.xy * _Offset; float4 sum SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv float2( texel.x, 0)); sum SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv float2(-texel.x, 0)); sum SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv float2( 0, texel.y)); sum SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv float2( 0, -texel.y)); return sum * 0.25; } ENDHLSL } // Pass 2合成把模糊结果按强度混回原图 Pass { Name KawaseComposite HLSLPROGRAM #pragma vertex Vert #pragma fragment FragComposite sampler2D _SourceTex; // C# 侧通过 cmd.SetGlobalTexture 传入 float4 FragComposite(Varyings i) : SV_Target { float4 blurred SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv); float4 src tex2D(_SourceTex, i.uv); return lerp(src, blurred, _Blend); } ENDHLSL } } }_Offset是这个采样核里唯一需要动态喂的参数。降采样时它的含义是「在对角方向上离中心多远」值越大单次采样的覆盖范围越广升采样时它用来做采样点抖动让叠回来的结果更接近高斯曲线而不是一串台阶。_MainTex_TexelSize由 Unity 在每次 Blit 前自动写入但要注意它的正负号在部分图形 API 下渲染纹理是上下翻转的y会是负数。所以别写死假设_MainTex_TexelSize.y 0直接用float2整体乘就行这正是上面代码里float2 texel _MainTex_TexelSize.xy * _Offset;这个写法的原因。合成 Pass 里的_SourceTex是我加的一个常见扩展点如果你想让模糊强度做成滑块就需要原图和模糊图共存靠_Blend插值。这个工程默认走的是「直接覆盖」blendWithSource打开后才走这个 Pass。3.2 C# 渲染流程临时 RT 的分配、复用与释放Pass 里最容易翻车的地方不是采样是 RT 的生命周期。分配了不释放跑几分钟就爆显存每帧重新new CommandBufferGC 会在 Profiler 里画出漂亮的锯齿。using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public class KawaseBlurPass : ScriptableRenderPass { private const string k_Tag Kawase Blur; private readonly ProfilingSampler m_Sampler new ProfilingSampler(k_Tag); // RT 的 id 缓存起来避免每帧 Shader.PropertyToID 产生字符串哈希开销 private readonly Listint m_RTs new Listint(16); private readonly KawaseBlurSettings m_Settings; private Material m_Material; private RenderTargetIdentifier m_Source; private int m_Iterations; public KawaseBlurPass(KawaseBlurSettings settings, Material material) { m_Settings settings; m_Material material; } public void SetSource(RenderTargetIdentifier source) m_Source source; public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData data) { // 明确告诉 URP这一帧要读相机颜色保证 Color Target 已就绪 ConfigureInput(ScriptableRenderPassInput.Color); var desc data.cameraData.cameraTargetDescriptor; // 迭代次数随分辨率缩放1080p 为基准 // 不然 4K 屏上同样的 iteration 会糊成一坨 float scale Mathf.Max(1f, desc.height / 1080f); m_Iterations Mathf.Clamp(Mathf.RoundToInt(m_Settings.iteration * scale), 0, 8); } public override void Execute(ScriptableRenderContext context, ref RenderingData data) { if (m_Material null || m_Iterations 0) return; CommandBuffer cmd CommandBufferPool.Get(); using (new ProfilingScope(cmd, m_Sampler)) { int w data.cameraData.cameraTargetDescriptor.width; int h data.cameraData.cameraTargetDescriptor.height; m_RTs.Clear(); RenderTargetIdentifier last m_Source; // ---- 降采样分辨率逐级减半偏移逐级递增 ---- for (int i 0; i m_Iterations; i) { w Mathf.Max(w / 2, 1); h Mathf.Max(h / 2, 1); int id Shader.PropertyToID(_KawaseDown_ i); cmd.GetTemporaryRT(id, w, h, 0, FilterMode.Bilinear, RenderTextureFormat.Default); cmd.SetGlobalFloat(_Offset, m_Settings.offset i * m_Settings.offsetStep); cmd.Blit(last, id, m_Material, 0); // pass 0 Down last id; m_RTs.Add(id); } // ---- 升采样从最小一级往上叠最终回到全分辨率 ---- for (int i m_RTs.Count - 2; i 0; i--) { cmd.SetGlobalFloat(_Offset, m_Settings.offset i * m_Settings.offsetStep); cmd.Blit(last, m_RTs[i], m_Material, 1); // pass 1 Up last m_RTs[i]; } // ---- 写回相机颜色 ---- if (m_Settings.blendWithSource) { m_Material.SetFloat(_Blend, m_Settings.intensity); cmd.Blit(last, m_Source, m_Material, 2); } else { cmd.Blit(last, m_Source); } // 必须成对释放否则临时 RT 会一直挂在显存里 foreach (int id in m_RTs) cmd.ReleaseTemporaryRT(id); } context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } }逐段说明几处关键逻辑OnCameraSetup里做的不是渲染是「算账」。cameraTargetDescriptor给你的是相机当前 Color Target 的完整描述宽高决定临时 RT 的尺寸msaaSamples和depthBufferBits则是要减掉的开销——模糊不需要 MSAA也不需要深度把这两个字段清零能省下可观的带宽。迭代次数按height / 1080缩放解决的是「在小窗口里调好的参数全屏看完全不一样」这个经典问题。Execute里的两个 for 循环是这个算法的主体。第一个循环每次都把宽高折半同时_Offset加上offsetStep这意味着降采样层级越深采样点覆盖的原始像素范围越大——这正是 Kawase 能用固定采样次数造出大半径模糊的原因。第二个循环从最深层往回走每次把上一级的结果叠到当前级的 RT 上注意这里直接 Blit 进已有的m_RTs[i]不需要重新分配。最后写回m_Source这一段有个隐患要提醒目标就是源同一个 RT 不能同时读写。cmd.Blit内部会做临时切换多数情况下安全但如果你的图形 API 或 URP 版本比较严格可能出现画面撕裂或闪烁。稳妥的做法是多申请一张全分辨率 RT 做中转代价是多一次读写。我在真机上遇到过这个翻车从那以后凡是要写回相机颜色都强制走中转 RT。3.3 参数封装Settings 与 Feature 的职责边界参数容器的设计直接决定这个工具好不好用。这个工程把参数拆成了「影响效果的」和「影响流程的」两类前者放在 Settings 里给策划调后者只在 Feature 里生效。using UnityEngine; using UnityEngine.Rendering.Universal; [System.Serializable] public class KawaseBlurSettings { [Header(效果)] [Range(0, 8)] public int iteration 3; // 降采样级数越大越糊也越贵 [Range(0.5f, 4f)] public float offset 1.0f; // 首级采样偏移像素 [Range(0f, 2f)] public float offsetStep 1.0f; // 每级偏移增量 [Range(0f, 1f)] public float intensity 1f; // 与原画面的混合权重 public bool blendWithSource false; [Header(流程)] public RenderPassEvent renderPassEvent RenderPassEvent.BeforeRenderingTransparents; public Material blurMaterial; }iteration是性价比最高的旋钮。从 2 调到 4画质提升肉眼可见从 4 调到 8画质几乎没变化开销却翻倍。我在实际项目里一般定在 3 到 5 之间超过 6 就该考虑是不是该用降分辨率渲染了。offset和offsetStep决定模糊的「手感」。offset太小模糊会带明显的方形块状感太大画面会丢失整体结构看起来像隔着毛玻璃看。这两个值的比例如实反映了作者对视觉的取舍改的时候建议一次只动一个。Feature 这一层则很薄只负责创建 Pass、判断该不该入队using UnityEngine; using UnityEngine.Rendering.Universal; public class KawaseBlurFeature : ScriptableRendererFeature { public KawaseBlurSettings settings new KawaseBlurSettings(); private KawaseBlurPass m_Pass; public override void Create() { m_Pass new KawaseBlurPass(settings, settings.blurMaterial); m_Pass.renderPassEvent settings.renderPassEvent; } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData data) { if (settings.blurMaterial null) return; // 场景视图和材质预览窗口别糊不然编辑器里没法干活 var type data.cameraData.cameraType; if (type CameraType.Preview || type CameraType.Reflection) return; m_Pass.SetSource(renderer.cameraColorTarget); renderer.EnqueuePass(m_Pass); } }cameraColorTarget这个属性在较新的 URP 里已经标记为过时替代品是cameraColorTargetHandle返回的是RTHandle。如果你把这份工程移植到新版本这一行大概率要改改完记得同步调整SetSource的参数类型。这类 API 迁移是 URP 项目升级时最常见的返工点血泪经验是别等到打包前一周才升版本。4. 跑起来从打开工程到看见第一帧模糊理论讲完落到操作。这一章按我实际跑通的顺序写每一步都标了失败时该看哪里。4.1 环境准备与工程导入新建或打开一个使用 URP 模板的 Unity 工程把这个资源包里的KawaseBlur文件夹整体拖进Assets。拖完先别急着开场景检查两件事Shader 有没有报编译错误材质球上的 Shader 引用有没有断。# 用命令行做一次静默编译检查适合接 CI Unity -batchmode -quit -projectPath ./urp_kawase_blur \ -logFile ./build/shader_compile.log这条命令的价值在于它会把 Shader 编译错误写进日志文件而不弹窗。团队协作时我一般把它挂成提交前检查比人肉点开 Unity 看控制台靠谱。-quit保证编译完就退出-logFile指定输出路径路径不存在会直接失败所以要先建目录。导入后确认 URP Asset 已经生效Project Settings 里 Graphics 的 Scriptable Render Pipeline Settings 不为空Quality 里各等级的 Render Pipeline Asset 也指向同一个 URP Asset。这一步漏了的话场景能打开、模型能显示但 Feature 永远不会被调用——属于典型的「代码没问题配置没生效」。4.2 挂载 RendererFeature 与事件值选择找到Settings/URP-Renderer.asset在 Inspector 底部点 Add Renderer Feature选KawaseBlurFeature。挂上之后把KawaseBlur.mat拖进Blur Material字段Iteration先给 3其他保持默认。然后是Render Pass Event这个字段决定模糊作用在哪些物体上事件值模糊范围典型用途BeforeRenderingTransparents仅不透明物体景深、背景虚化BeforeRenderingPostProcessing不透明 透明屏幕受伤特效、整体柔化AfterRenderingPostProcessing全部含后处理结果刻意的「隔着玻璃」观感第一次跑建议先用BeforeRenderingPostProcessing这样场景里的模型和半透明特效都能被糊到效果最直观。如果放上去发现完全没变化先看 Frame Debugger 里有没有出现名为 Kawase Blur 的绘制事件没有就是配置问题有就是 Shader 或 RT 的问题两者的排查方向完全不同。4.3 用 Frame Debugger 和参数面板做一次验证点开 Window Analysis Frame DebuggerEnable 之后逐条展开绘制调用。你要找的是这样一组事件先出现若干次分辨率递减的 KawaseDown再出现等量的 KawaseUp最后回到相机分辨率。如果 KawaseDown 只出现一次就结束了说明iteration被算成了 1回头检查OnCameraSetup里的缩放逻辑是不是被 1080 除出了 0。参数调试的推荐顺序是先把iteration定下来决定模糊的「硬度」再调offset决定颗粒感最后用intensity收口。反过来调会一直在两个变量之间反复横跳很难收敛。4.4 运行时开关与性能采样工程里通常还会带一个简单的开关脚本方便对比开与不开的帧率差异using UnityEngine; using UnityEngine.Rendering.Universal; public class BlurSwitch : MonoBehaviour { [SerializeField] private UniversalAdditionalCameraData cameraData; [SerializeField] private KawaseBlurFeature feature; // Inspector 里拖资源引用 private void Update() { // 按 B 键切换做性能对比时最省事 if (Input.GetKeyDown(KeyCode.B) feature ! null) feature.SetActive(!feature.isActive); } }SetActive之后AddRenderPasses不会被调用Pass 自然不入队开销归零比去改材质球的开关干净。UniversalAdditionalCameraData在这里其实用不到如果你的开关只控制 Feature把它删掉即可——我保留它是因为常在这块加相机级别的判断比如只在主相机上开模糊。性能采样的时候别只看总体帧率。Profiler 里切到 Rendering 模块看Kawase Blur这一项占多少毫秒再对比关掉时的差距。手机上这个值超过 2ms 就该考虑降迭代次数或者把分辨率降到 0.5 倍再升回来。5. 避坑与排查五种「看起来像玄学」的故障渲染问题最气人的地方在于同样的代码在别人机器上跑得好好的到你这就是一片黑。下面五条是我在这类工程上真实踩过的按「现象 → 原因 → 解决」整理。5.1 完全没效果Frame Debugger 里也找不到 Pass现象场景正常显示就是一点都不糊Frame Debugger 里搜不到 Kawase 字样。原因三种可能一是 Feature 挂错了 Renderer Data工程里有多个 Renderer相机用的是另一个二是blurMaterial字段空着AddRenderPasses里被提前 return三是相机是 Overlay 类型而 Feature 只挂了 Base 相机的 Renderer。解决先在AddRenderPasses里加一行Debug.Log(data.cameraData.cameraType / (settings.blurMaterial ! null));跑一帧看日志。九成情况下日志里会出现你没想到的相机类型。5.2 模糊生效了但 UI 也跟着糊成一团现象3D 场景糊得很漂亮HUD 和按钮也一起变模糊了文字完全没法看。原因renderPassEvent设成了BeforeRenderingPostProcessing或更靠后而 UI 是在相机之后单独渲染的属于 Overlay 相机的合成内容被一起处理了。解决把事件值改回BeforeRenderingTransparents只糊不透明物体或者给 UI 单独开一个 Overlay 相机让主相机的 Feature 不作用于它。用 Canvas 的 Screen Space - Camera 模式时尤其要注意这层关系。5.3 画面边缘出现黑边或彩色漏光现象屏幕四角发暗或者模糊后边缘出现一圈奇怪的色带。原因采样时 UV 越界。_Offset逐级递增到最后一级时采样点已经跑出了 [0,1] 范围采样器默认的 wrap 模式会把对边的像素拉过来形成漏光。解决把采样器的寻址模式改成 Clamp。UML 里可以在 Shader 中显式声明SAMPLER(sampler_ClampLinear)并在材质上配置也可以在 C# 侧给临时 RT 建一个RenderTexture并设wrapMode TextureWrapMode.Clamp。这是最容易被忽略的一条因为它只在大半径时暴露。5.4 开模糊之后整体画面变暗或发灰现象不开模糊时画面正常一开就整体偏暗颜色也没那么饱和了。原因临时 RT 的格式和相机的 Color Target 格式不一致。相机在 Linear 色彩空间下用的是 sRGB 格式而你用RenderTextureFormat.Default申请的 RT 可能是线性格式一来一回做了两次错误的色彩转换。解决别用固定的RenderTextureFormat.Default从cameraTargetDescriptor.colorFormat里取原样传给GetTemporaryRT。这是我在一个项目里排查了整整一下午才定位到的问题当时还以为是 Shader 里的lerp权重出了问题。5.5 编辑器里很流畅打包到手机上掉帧严重现象PC 上跑 200 帧安卓真机上开模糊直接掉到 30 以下。原因移动端 GPU 对带宽极其敏感。全分辨率 RT 的读写、msaaSamples没清零、每帧重新申请临时 RT这三点叠加起来带宽会被吃干净。解决一是确认OnCameraSetup里把msaaSamples和depthBufferBits清零了二是把降采样的起始分辨率直接设成屏幕的一半反正最后都要降到 1/8从一半开始只是少一级三是检查m_RTs有没有在每次 Execute 后清空List.Clear()只清元素不释放 RT必须配ReleaseTemporaryRT。注意移动端上ProfilingSampler是有开销的发布包里记得把 ProfilingScope 相关代码剥掉或者用#if DEVELOPMENT_BUILD包起来。6. 二次开发把它改成你自己的模糊工具链测试工程能跑通只是起点真正要落地一般还得往前推三步。第一步把参数接到 Volume 上。现在参数锁死在 Renderer Data 里场景美术改一次要重新打开资产。常见做法是新建一个KawaseBlurVolume : VolumeComponent把iteration、offset、intensity声明成ClampedIntParameter和ClampedFloatParameter然后在 Pass 里通过VolumeManager.instance.stack.GetComponentKawaseBlurVolume()取值。这样同一份 Feature 可以在不同场景、不同后处理 Profile 下呈现完全不同的模糊强度。第二步加上遮罩。全局模糊用得多局部模糊用得更多——比如只糊屏幕中心以外、只糊某个物体背后。思路是额外传一张 mask 图在合成 Pass 里用lerp(src, blurred, mask.r * _Blend)替代现在的统一插值。mask 可以由另一个相机渲染到 RT也可以直接给一张静态贴图。第三步做分辨率降级。与其在OnCameraSetup里按 1080 硬除不如把降采样起始分辨率做成一个 0 到 1 的系数低端机设 0.5、高端机设 1.0模糊半径靠offset补偿回来。这样同一套参数在中低端设备上也不会崩。改造点涉及文件改动量风险Volume 参数化Settings、Pass中需要处理 Volume 为空时的兜底遮罩模糊Shader Pass 2、Pass中mask 的 UV 对齐容易错分辨率降级Pass.OnCameraSetup小缩放系数要同步调 offset写回中转 RTPass.Execute小多一次全屏读写改完怎么验证我的习惯是准备三个固定场景一个高对比度棋盘格看采样核有没有问题、一个纯色渐变看有没有色带、一个带文字 UI 的界面看字会不会糊。每次改完跑一遍这三个基本能兜住大部分回归。有一回我图省事直接在主工程里改了采样核没做备份结果升采样那级的偏移量算错整个画面糊成一片灰回滚 Git 发现上一次提交是三天前。从那以后凡是动 Shader 的采样参数我都强制先把原始工程复制一份单独放改完在副本里对着棋盘格验证通过再合回主工程。渲染这东西肉眼真的不可靠还是得靠那几个不会说话的标准场景。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?