Unity URP 泛光 Bloom 实现:从原理到 Shader 后处理全解析
1. 光晕不是错觉Bloom 解决的其实是显示精度问题做夜景项目的时候导演反复强调霓虹灯要“亮得散开一点”。一开始我以为他在说灯牌贴图太暗后来才意识到他要的是那种经典泛光Bloom质感亮部像在发光颜色会温和地晕染到周围暗部。这套效果在真实摄影里根本不需要额外做镜头玻璃和眼球晶状体天然会散射强光夜景照片里路灯周围那圈光晕就是这么来的。可在游戏渲染里画面是数学算出来的没有镜头散射也没有视网膜残像高光区域会硬邦邦地杵在那里看着就像一块过曝的白斑。Bloom 要解决的问题说穿了是显示精度问题。HDR 渲染流程里场景中真正发光的区域像素值可以超过 1.0但显示器能呈现的亮度范围也就 0 到 1 左右。直接把超亮像素截断到 1.0画面会瞬间丢信息一盏 5 倍曝光的灯和一盏 2 倍曝光的灯最终屏幕上都变成纯白完全没有区别。Bloom 的思路不是去压缩这些超亮像素而是把溢出的那部分能量“借”到周围像素上让高光以光晕的形态扩散出去。这样既保住了亮部的主观强度又让画面过渡变得柔和这也正是人眼和相机看待强光源的真实方式。这篇内容我会从原理讲到 URP 下的完整实现适合正在从内置渲染管线迁移到 URP、或者想把手动后处理玩清楚的 Unity 开发者。你不需要有很深的图形学基础但最好知道 CommandBuffer、RenderTexture 这些基本概念。读完你就能在自己项目里挂一个可调参的 Bloom也能明白市面上那些 URP 泛光插件内部大概在折腾什么。2. 一条滤镜流水线阈值、降采样与高斯模糊怎么配合2.1 从输入到输出Bloom 的完整环节Bloom 的完整流程可以压缩成三个动作先提取画面里足够亮的像素然后把这些像素组成的区域模糊成大范围光晕最后把光晕加回原图。所有 Bloom 实现不管多复杂本质上都是这三步的排列组合。提取这步用一句话描述就是“亮的地方才准发光”。你要先定一个阈值比如 0.8画面中所有亮度超过这个值的像素被抠出来其余地方变黑。抠出来的结果是一张只有高光点、没有阴影和中间调的图通常叫 bright pass。接着是模糊但这里有个容易被忽略的细节你应该模糊的是已经抠出来的高光图而不是直接模糊完整画面。如果对整个画面做模糊暗部也会跟着晕开画面会变得雾蒙蒙。只有先提取再模糊光晕才会出现在高光周围暗部依然保持干净。最后把模糊结果按强度叠加回原图。这里的“叠加”选择很关键直接决定画面是“发光”还是“起雾”后面会专门展开。2.2 阈值提取和“亮度”较劲而不是和“颜色”较劲阈值怎么判定很多新手第一次写会犯同一个错误拿颜色通道最大值和阈值比比如max(r, g, b) 阈值。这样会提取出大量纯色高饱和像素明明暗度不高也会被误判为发光。真正合理的方式是用亮度公式把 RGB 换算成感知亮度luminance 0.2126 * r 0.7152 * g 0.0722 * b人眼对绿色最敏感红色次之蓝色最弱所以绿色通道权重最高。你拿亮度阈值筛选提取出来的区域才符合视觉直觉。另一个细节是提取时用什么函数。硬性step会让画面出现明显断层高光区域边界整齐得像刀切一样模糊后也救不回来。我习惯用平滑过渡比如saturate((luminance - threshold) / softRange)softRange 给 0.1 到 0.2提取出来的高光形状会自然柔和很多。代码里我还会顺手把超过阈值的部分剪掉再乘以掩膜也就是max(color - threshold, 0) * mask。这样做的效果是把所有高光统一摆到“零起点”上后面模糊叠加时强度只跟像素相对亮度和范围有关调参更直观。提取方式效果特征适用场景step(threshold, luminance)高光边界锐利容易脏风格化强、故意要锐边时max(color - threshold, 0)只保留超亮部分物理感更强的默认做法max(...) * smoothMask过渡柔和可控性好我推荐的大部分项目2.3 高斯模糊为什么是默认算子模糊算子有很多种均值模糊、径向模糊、运动模糊都有各自的舞台但 Bloom 的默认选择几乎总是高斯模糊。原因有两个一是高斯核的形状接近真实镜头光晕的能量分布中心贡献大、边缘衰减平滑二是高斯函数是可分离的。可分离意味着一个二维高斯卷积可以拆成两次一维卷积先水平方向再垂直方向。计算量从 O(n²) 降成 O(2n)。再加上降采样配合性能上非常划算。实际 Shader 里我常用一个 5 tap 的高斯近似用 9 个采样点的权重分布在 5 个位置上权重分布水平方向 中心0.2270270270 偏移 1.3846153846 像素左右各 0.3162162162 偏移 3.2307692308 像素左右各 0.0702702703偏移距离是经过归一化计算的不是随便填的保证三个采样点叠加后能近似覆盖 9 tap 的高斯核。直接用这个权重表即可做水平模糊时偏移只加在 X 方向垂直模糊时偏移只加在 Y 方向。2.4 降采样金字塔用分辨率换半径如果只做一次模糊光晕范围最多也就十几像素再多就要把采样偏移拉得很远高斯权重会失真性能也扛不住。所以真正的 Bloom 要用降采样金字塔。思路是这样的先把全屏画面降到一半分辨率再做模糊。此时一个 3 像素偏移在原始分辨率里相当于 6 像素模糊半径直接翻倍。再把结果降成四分之一分辨率偏移 3 像素相当于原图 12 像素。每降一级相同的采样开销换来的光晕半径就翻一倍。这就像你在地图上找一片区域的光照范围在 1:100 的地图上画一个 1 厘米的圆实际对应 1 米换到 1:500 的地图上同样画 1 厘米覆盖的却是 5 米。降采样金字塔等于手动放大了“画笔”的覆盖范围。同时性能收益非常明显半分辨率面积是原来的四分之一四分之一分辨率是十六分之一。后期处理主要在低分辨率上进行最终再升采样合并这也是移动端 Bloom 能跑起来的关键原因。2.5 合成加法和“加法”还不一样光晕提取并模糊完之后最后一步是把光晕叠回原图。最朴素的加法就是finalColor originalColor bloomColor * intensity这个实现忠实、简单也是大多数 Bloom 插件的默认方式。但“加法”不止这一种有人会用 Screen BlendfinalColor 1 - (1 - original) * (1 - bloom)这个公式会把暗部也提亮一点光晕和底色融合更自然但整体偏灰需要配合较高的阈值使用。还有 Soft Light 之类更软的算法常用于美颜类效果游戏里做 Bloom 很少用因为会削弱高光的锐度。我自己的习惯是先按默认加法做把 intensity 调到一个舒服的值然后再对比 Screen Blend看场景风格适合哪种。记住加法让高光往外“涌”Screen 混合让光“浸入”周围两者视觉性格完全不同。Bloom 的最后一个参数之所以是“强度”而不是“透明度”就是因为这个叠加不是简单的 alpha 混合而是亮度能量叠加。3. URP 里跑后处理先忘掉 OnRenderImage3.1 为什么这套 API 在 URP 中失灵了从内置渲染管线迁移过来的人第一个遇到的就是OnRenderImage不生效的问题。内置管线里后处理逻辑很简单引擎渲染完场景后调用这个回调你拿到 screen texture处理完再用Graphics.Blit写回去完事。URP 不是这么工作的。它把渲染过程拆成了一长串可编程的 RenderPass相机、阴影、不透明物体、天空盒、后处理各自是独立节点。渲染流程由 Renderer 对象统一调度脚本想在某个时机插入自己的处理必须把自己包装成ScriptableRendererFeature和ScriptableRenderPass然后注册到 Renderer 的 Feature 列表里。换句话说在内置管线里你是“被引擎调用的员工”在 URP 里你是“主动把自己塞进流水线的零件”。思路完全反过来了。3.2 RendererFeature 的“挂载点”逻辑一个 URP 后处理功能通常由两个类组成ScriptableRendererFeature负责创建设置项和 PassUnity 编辑器中你在 Renderer 组件上看到的“Add Renderer Feature”按钮加的就是这个类。ScriptableRenderPass负责真正执行渲染逻辑每一帧都会在指定时机被调用。Feature 的Create方法在添加功能时调用一次用来 new 出 Pass 对象缓存材质。AddRenderPasses每帧调用把 Pass 加入渲染队列。Pass 的Execute方法里拿到 CommandBuffer写入你要执行的 Blit 操作。有个很常见的坑有人把所有 GL 操作写在Execute里后发现画面变了但编辑器里看不到任何输出或者一进 Play 模式就闪退。这通常是因为没有在Create里创建材质也没有检查Shader.Find返回结果。URP 后处理材质必须用支持 URP Shader 的材质而且 Shader 要被包含在构建中不然Shader.Find可能返回空后续cmd.Blit全部失效。3.3 RenderPassEvent 选错时机的结果URP 有一张渲染事件表比如BeforeRenderingOpaques、AfterRenderingOpaques、BeforeRenderingPostProcessing、AfterRenderingPostProcessing。选错时机效果不会彻底失效但会出现各种诡异问题。如果你把 Bloom 放在AfterRenderingOpaques那么透明物体还没渲染透明件的自发光不会参与泛光但泛光结果会被后续透明物体盖住出现明显的层次错乱。如果放在AfterRenderingPostProcessing此时后处理栈已经做过颜色校正和 Tonemapping你的 Bloom 是在 LDR 图像上叠加的超亮像素已经被压缩过了提取阈值会极难控制画面动不动就过曝。最合理的位置是BeforeRenderingPostProcessing。Bloom 不应该和 Tonemapping 挤在一起它应该在 HDR 空间中提取高光、做模糊然后把结果交还给后处理栈做最终的颜色映射。这样 Bloom 处理的始终是原始高动态范围数据画面表现才正确。3.4 资源生命周期与 MSAA 兼容性URP 里自己分配临时 RenderTexture 有个隐蔽问题你要是直接拿cameraTargetDescriptor来分配默认会带上 MSAA 采样数和深度缓冲位。后处理基本不需要 MSAA深度缓冲也用不上。所以创建 descriptor 时必须显式设置desc.msaaSamples 1; desc.depthBufferBits 0;不关 MSAA 的话后处理 RT 会带着多重采样采样时要么得到奇怪的颜色边缘要么 Blit 回去时效率暴跌。这个我几乎在每次 URP 后处理教程里都会写但还是总有人踩到。资源释放也同样重要。Execute里创建的 RT 不要随手释放尤其不要在同一帧GetTemporary后又立刻ReleaseTemporary再拿给 CommandBuffer 用。正确做法是让 RT 活到 Pass 结束并在合适的生命周期节点统一释放。为了篇幅我这里先不做展开后面的代码里也会用相对简单的方式来处理生产环境推荐使用 RTHandle 池这个放到最后再说。4. 手写一个可运行的 Bloom脚本与 Shader 的完整拼装4.1 工程结构与 C# 骨架先建一个文件夹比如Assets/PostProcess/Bloom/里面放两个文件BloomFeature.cs和Bloom.shader。这是最直接的教学结构不引第三方库不依赖后处理栈你只要把这个 Feature 加到 URP Renderer 的 Feature 列表就能看到效果。BloomFeature.cs承担两件事定义暴露在 Inspector 上的参数以及把执行 Pass 注册进渲染流程。using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public sealed class BloomFeature : ScriptableRendererFeature { [System.Serializable] public class BloomSettings { [Range(0f, 5f)] public float threshold 1.1f; [Range(0f, 3f)] public float intensity 1.5f; [Range(1, 8)] public int iteration 4; public Color tint Color.white; } public BloomSettings settings new BloomSettings(); private BloomPass m_Pass; public override void Create() { m_Pass new BloomPass(settings); } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { renderer.EnqueuePass(m_Pass); } }BloomPass才是干活的主体。我在里面用了ListRenderTexture当作简易金字塔每层模糊结果存下来最后统一从低分辨率向高分辨率合并。using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public class BloomPass : ScriptableRenderPass { private readonly BloomFeature.BloomSettings m_Settings; private Material m_Material; private readonly ListRenderTexture m_Pyramid new ListRenderTexture(); public BloomPass(BloomFeature.BloomSettings settings) { m_Settings settings; renderPassEvent RenderPassEvent.BeforeRenderingPostProcessing; m_Material CoreUtils.CreateEngineMaterial(Shader.Find(Hidden/Custom/Bloom)); } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { if (m_Material null) { return; } CommandBuffer cmd CommandBufferPool.Get(CustomBloom); RenderTextureDescriptor baseDesc renderingData.cameraData.cameraTargetDescriptor; baseDesc.msaaSamples 1; baseDesc.depthBufferBits 0; RenderTexture source renderingData.cameraData.renderer.cameraColorTarget; RenderTextureDescriptor halfDesc baseDesc; halfDesc.width Mathf.Max(1, baseDesc.width / 2); halfDesc.height Mathf.Max(1, baseDesc.height / 2); // Pass 0提取高亮同时降到半分辨率 RenderTexture rt RenderTexture.GetTemporary(halfDesc); cmd.Blit(source, rt, m_Material, 0); // 逐层降采样 模糊 for (int i 0; i m_Settings.iteration; i) { RenderTexture blurA RenderTexture.GetTemporary(rt.descriptor); RenderTexture blurB RenderTexture.GetTemporary(rt.descriptor); cmd.Blit(rt, blurA, m_Material, 1); // 水平模糊 cmd.Blit(blurA, blurB, m_Material, 2); // 垂直模糊 m_Pyramid.Add(blurB); if (i m_Settings.iteration - 1) { RenderTextureDescriptor nextDesc rt.descriptor; nextDesc.width Mathf.Max(1, rt.width / 2); nextDesc.height Mathf.Max(1, rt.height / 2); RenderTexture next RenderTexture.GetTemporary(nextDesc); cmd.Blit(blurB, next, m_Material, 4); // 降采样 RenderTexture.ReleaseTemporary(rt); rt next; } else { RenderTexture.ReleaseTemporary(rt); } RenderTexture.ReleaseTemporary(blurA); } // 从最低分辨率开始把各层累加上去 RenderTexture accum RenderTexture.GetTemporary(m_Pyramid[m_Pyramid.Count - 1].descriptor); cmd.Blit(m_Pyramid[m_Pyramid.Count - 1], accum); cmd.SetGlobalFloat(_Intensity, 1f); cmd.SetGlobalColor(_Tint, Color.white); for (int i m_Pyramid.Count - 2; i 0; i--) { RenderTexture up RenderTexture.GetTemporary(m_Pyramid[i].descriptor); cmd.SetGlobalTexture(_BloomTex, accum); cmd.Blit(m_Pyramid[i], up, m_Material, 3); // Composite RenderTexture.ReleaseTemporary(accum); accum up; } // 最后把光晕加到原图上 RenderTexture resultRT RenderTexture.GetTemporary(baseDesc); cmd.SetGlobalTexture(_BloomTex, accum); cmd.SetGlobalFloat(_Intensity, m_Settings.intensity); cmd.SetGlobalColor(_Tint, m_Settings.tint); cmd.Blit(source, resultRT, m_Material, 3); cmd.Blit(resultRT, source); context.ExecuteCommandBuffer(cmd); cmd.Clear(); RenderTexture.ReleaseTemporary(accum); RenderTexture.ReleaseTemporary(resultRT); foreach (var tex in m_Pyramid) { RenderTexture.ReleaseTemporary(tex); } m_Pyramid.Clear(); } }这段代码没有做花哨的优化但流程是完整的提取、降采样、水平模糊、垂直模糊、金字塔合并、叠加回原图。你直接拿过去改一改参数就能跑。4.2 Shader 五个 Pass 的职责划分对应 C# 里的 Pass 索引Shader 需要五个 PassPass 索引名称源码里做了什么0Threshold提取高亮同时完成第一次半分辨率降采样1BlurH水平方向高斯模糊2BlurV垂直方向高斯模糊3Composite两路输入合并输出4Downsample2x2 平均降采样Shader 代码里最需要注意的是输入约定cmd.Blit(source, dest, material, passIndex)时URP 会自动把 source 绑定到_MainTex。另一个输入比如光晕累积图就要自己用_BloomTex指定。Pass 3 同时读两张图所以我在 C# 里每次用它之前都会SetGlobalTexture指定_BloomTex。Shader Hidden/Custom/Bloom { Properties { _MainTex (Source, 2D) white {} _BloomTex (Bloom, 2D) black {} } SubShader { Tags { RenderTypeOpaque RenderPipelineUniversalPipeline } Cull Off ZWrite Off ZTest Always HLSLINCLUDE #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; }; TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); TEXTURE2D(_BloomTex); SAMPLER(sampler_LinearClamp); half _Threshold; half _Intensity; half4 _Tint; float4 _MainTex_TexelSize; Varyings Vert(Attributes input) { Varyings o; o.positionCS float4(input.positionOS.xy, 0.0, 1.0); o.uv input.uv; return o; } ENDHLSL Pass { Name Threshold HLSLPROGRAM #pragma vertex Vert #pragma fragment FragThreshold half4 FragThreshold(Varyings input) : SV_Target { half3 col SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv).rgb; half lum Luminance(col); half softMask saturate((lum - _Threshold) / 0.1h); half3 bright max(col - _Threshold, 0) * softMask; return half4(bright, 1); } ENDHLSL } Pass { Name BlurH HLSLPROGRAM #pragma vertex Vert #pragma fragment FragBlurH half4 FragBlurH(Varyings input) : SV_Target { float2 uv input.uv; half3 sum SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv).rgb * 0.2270270270; sum SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv float2(_MainTex_TexelSize.x * 1.3846153846, 0)).rgb * 0.3162162162; sum SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv - float2(_MainTex_TexelSize.x * 1.3846153846, 0)).rgb * 0.3162162162; sum SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv float2(_MainTex_TexelSize.x * 3.2307692308, 0)).rgb * 0.0702702703; sum SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv - float2(_MainTex_TexelSize.x * 3.2307692308, 0)).rgb * 0.0702702703; return half4(sum, 1); } ENDHLSL } Pass { Name BlurV HLSLPROGRAM #pragma vertex Vert #pragma fragment FragBlurV half4 FragBlurV(Varyings input) : SV_Target { float2 uv input.uv; half3 sum SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv).rgb * 0.2270270270; sum SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv float2(0, _MainTex_TexelSize.y * 1.3846153846)).rgb * 0.3162162162; sum SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv - float2(0, _MainTex_TexelSize.y * 1.3846153846)).rgb * 0.3162162162; sum SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv float2(0, _MainTex_TexelSize.y * 3.2307692308)).rgb * 0.0702702703; sum SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv - float2(0, _MainTex_TexelSize.y * 3.2307692308)).rgb * 0.0702702703; return half4(sum, 1); } ENDHLSL } Pass { Name Composite HLSLPROGRAM #pragma vertex Vert #pragma fragment FragComposite half4 FragComposite(Varyings input) : SV_Target { half3 src SAMPLE_TEXTURE2D(_MainTex, sampler_LinearClamp, input.uv).rgb; half3 bloom SAMPLE_TEXTURE2D(_BloomTex, sampler_LinearClamp, input.uv).rgb; return half4(src bloom * _Intensity * _Tint.rgb, 1); } ENDHLSL } Pass { Name Downsample HLSLPROGRAM #pragma vertex Vert #pragma fragment FragDownsample half4 FragDownsample(Varyings input) : SV_Target { float2 texel _MainTex_TexelSize.xy; half3 sum 0; sum SAMPLE_TEXTURE2D(_MainTex, sampler_LinearClamp, input.uv float2(-texel.x, -texel.y) * 0.5).rgb; sum SAMPLE_TEXTURE2D(_MainTex, sampler_LinearClamp, input.uv float2( texel.x, -texel.y) * 0.5).rgb; sum SAMPLE_TEXTURE2D(_MainTex, sampler_LinearClamp, input.uv float2(-texel.x, texel.y) * 0.5).rgb; sum SAMPLE_TEXTURE2D(_MainTex, sampler_LinearClamp, input.uv float2( texel.x, texel.y) * 0.5).rgb; return half4(sum * 0.25, 1); } ENDHLSL } } }4.3 执行链路的逐步拆解把 C# 和 Shader 拼起来看执行链路会清晰很多。第一轮cmd.Blit(source, rt, material, 0)把摄像头颜色目标读进来用 Pass 0 提取高亮。因为rt是半分辨率这一步同时完成了第一次降采样等于高亮图也降了一级。然后进入循环。第一次循环对这张半分辨率高亮图做水平模糊和垂直模糊结果存进m_Pyramid的第一项。这就是整个泛光的最细颗粒度光晕。接着用 Pass 4 把模糊结果再降一半分辨率放入rt作为下一层的输入。第二次循环对四分之一分辨率的高亮模糊结果再做两次高斯模糊。注意这个模糊半径是在已经缩小的图上执行的实际覆盖原始像素的范围比第一次大得多。这一层会存进m_Pyramid的第二项代表更大范围的光晕。循环做到 iteration 次m_Pyramid里就有从细到粗的多层光晕。合并阶段从最小的那层开始向上逐层叠加。因为低分辨率层的尺寸比上一层小Blit到高分辨率目标时默认采样器会自动做双线性放大顺手完成了升采样视觉上就是大光晕上叠中光晕、中光晕上叠小光晕最后形成从亮部往外连续衰减的发光效果。最后一步把合并结果送到 Pass 3与原始画面做src bloom * intensity * tint完成输出。整个流程跑下来一张全屏 HDR 颜色图最终变成一张带着柔和光晕的漂亮画面。4.4 教学版的局限与生产化方向我故意把这段代码写得很直白方便理解但它离生产级还有一些距离。最明显的问题是临时 RT 的分配和释放。教学版在Execute里GetTemporary最后又ReleaseTemporary在高频率调用下可能会触发资源池抖动。URP 官方推荐的思路是用RTHandle配合RTHandleAllocator让 RT 按需复用RenderPass 结束时不需要手动释放由管线统一管理。你可以先跑通这个版本等对流程熟悉了再换成 RTHandle。另一个问题是它没有考虑多相机渲染。如果项目里有主相机、UI 相机、反射探针相机RenderFeature 会被所有适用相机执行导致 UI 也被泛光处理。正规做法要在AddRenderPasses里判断renderingData.cameraData.cameraType只对特定相机执行。还有个方向是色调映射顺序。这个版本刻意放在 Post Processing 之前适合默认管线。如果项目使用 ACES 色调映射泛光会在 Tonemapping 之前参与计算光晕颜色更自然如果用中性色调映射或自定义 LUT可能要微调强度。5. 调参实录与三个容易翻车的坑5.1 阈值、强度、迭代次数到底怎么配参数调了无数个项目之后我总结出一套相对通用的初始值你可以拿这个当起点再根据画面微调。参数推荐范围经验threshold0.9 - 1.2低于 0.9 容易整屏发光高于 1.5 基本只剩点光源intensity1.0 - 2.0想要“贵气”感再加到 2.5但注意别让高光糊成一片iteration4 - 64 层够 1080P6 层适合大屏幕和远景光晕tint默认白色夜景想偏暖可以给橙色调科幻场景可给冷蓝threshold 的设置要看你场景里 HDR 光源的强度。有些美术为了让灯更亮把自发光材质强度设到了 10 以上threshold 还在 1.0 以下结果就是全屏白茫茫。反过来场景里所有材质最高亮度只有 0.8threshold 却设到 1.2那 Bloom 就像没装一样——它根本没有内容可以提取。我的习惯是先在场景里看一眼 Camera 的 HDR buffer 峰值亮度再决定阈值。iteration 也不是越大越好。层数越多光晕最外圈越大但中间层和高光核心层的比例也会变化。你把 iteration 从 4 调到 8往往不是“更亮”而是“光晕范围更大”调整幅度一定要配合 intensity 一起看。5.2 坑一阈值设低了整个画面像起了雾这是新手最常见的翻车现场。明明只是给灯加了 Bloom结果整个画面灰蒙蒙一层细节全被吃掉像镜头没擦干净。原因不是 Bloom 本身的问题而是阈值太低画面里大量中等亮度的像素也被提取进了高光图。比如天空亮度 0.6阈值 0.5整个天空全被当作发光体模糊之后就是一层雾。解决思路有三个一是把 threshold 往上抬压住中间调二是确认场景里是否有真正的超亮光源Bloom 是给“超出显示范围”的像素服务的不是给所有亮部服务的三是检查 Tonemapping如果管线里已经做了很激进的曝光调整HDR 峰值被压缩得非常低阈值也要跟着降低否则什么都提不出来。这里有个实用技巧把 Shader 的 Pass 0 输出临时改成纯灰看看高光图里到底是什么样的。如果大面积都是灰色说明阈值太低如果只有星星点点说明阈值合理。这个 Debug 习惯能帮你省下很多猜来猜去的时间。5.3 坑二透明材质不该发光却发光Bloom 是全屏后处理它不知道物体是什么材质只认颜色亮度。所以一个水晶玻璃球明明本身不发光只是反射了太阳的强光反射区域亮度超过阈值Bloom 就会给它套上光晕看起来很违和。解决这个问题的标准做法是控制进入 Bloom 的输入源。在 URP 里可以通过 Stencil 标记或者特定 Renderer Feature 只对指定物体做“预标记”。更粗暴的做法是微调材质让反射高光不要高到超过阈值但这会影响 PBR 正确性。如果你只是做个人项目最简单的思路是接受这种“物理上不严谨但视觉上还行”的结果。很多游戏的光晕其实来自镜面反射人眼不会特别察觉。但如果你在做写实风格项目并且对美术质量有要求建议给角色和关键道具单独做一层 light buffer只有这层参与 Bloom其他物体一律不进高光提取。第三个方法是调整软阈值范围。把 softRange 从 0.1 放大到 0.3透明物体的误发光会被柔化高光边缘不会那么刺眼但不会有质的改变。真正干净的方案还是分层。5.4 坑三自定义后处理与抗锯齿的顺序错乱另一个隐蔽问题是 Bloom 和抗锯齿的先后关系。在 URP 后处理栈里FXAA 或 SMAA 通常放在最后而 Bloom 在 Tonemapping 之前。如果你把自定义 Bloom 放在AfterRenderingPostProcessing就会在 FXAA 之后做泛光模糊会把抗锯齿的成果重新弄脏画面边缘出现抖动和像素颗粒。这个问题的判断标准是泛光作用下物体边缘是否出现了彩色的“碎边”或者锯齿感被重新“激活”。如果出现了把renderPassEvent改回BeforeRenderingPostProcessing问题基本消失。还有一点要注意 MSAA。在 URP 某些版本里如果开了 MSAA自定义后处理放在 Opaque 和 Transparent 之间的 Pass 可能需要cameraTargetDescriptor.msaaSamples 1否则边缘会有紫边。这就是我在第 3.4 节强调的原因。5.5 还可以往哪里延伸跑通这个版本后你还可以继续往两个方向延伸。一个方向是给光晕加动态响应。用音频强度驱动 intensity 或 threshold音乐响起时光晕变强节奏落下时收敛适合做律动类场景。另一个方向是方向性泛光把模糊从圆对称改成径向或按风向定向爆炸、传送门、风场都能有更性格的视觉效果。我个人觉得 Bloom 最难的还不是实现而是“克制”。很多项目第一次加上 Bloom 会觉得惊艳然后越调越重最后画面腻得不行。我现在的习惯是先把 threshold 调高到“几乎不发光”的程度再一点点往下降直到刚好能看到霓虹灯边缘渗出一点光色然后停手。这个位置往往就是最耐看的位置。

相关新闻

首篇Agent Harness综述:模型之外,Agent拼什么?TaoToken统一Key视角下的Harness工程拆解

首篇Agent Harness综述:模型之外,Agent拼什么?TaoToken统一Key视角下的Harness工程拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 16:19:29 阅读更多 →
LangChain模型配置实战:从参数调优到云端本地切换全攻略

LangChain模型配置实战:从参数调优到云端本地切换全攻略

聊到 LangChain,我自己的体感是:跑通一个简单 Chain 很容易,真正让人卡住的反而是平时不太注意的模型配置。很多人拉着 PromptTemplate 调了半天,发现问题根本不在 Prompt,而是你代码里那个llm对象没有配对。模型才真正…

2026/10/18 16:18:29 阅读更多 →
如何用 AI + OpenSpec 驱动团队迭代开发:把 Codex auth.json 改到 TaoToken

如何用 AI + OpenSpec 驱动团队迭代开发:把 Codex auth.json 改到 TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 16:18:29 阅读更多 →

最新新闻

uview-plus密码切换失效的根因排查与正确修复方案

uview-plus密码切换失效的根因排查与正确修复方案

上周给小程序做登录页,密码框要求支持明文密文切换。组件用的uview-plus,照着文档写了几行代码,结果右上角那个小眼睛点了完全没反应,真机上试了好几次,要么纹丝不动,要么闪一下又弹回密文状态。在项目群里…

2026/10/18 17:04:56 阅读更多 →
多传感器融合三条路线深度对比:前融合、特征级与决策级的信息损耗与工程实践

多传感器融合三条路线深度对比:前融合、特征级与决策级的信息损耗与工程实践

我先把话说在前面:多传感器融合这个主题,市面上讲“算法公式”的教程很多,但真正把前融合、特征级融合、决策级融合这三条路线放在同一个项目里拉通对比、还跑过实车数据的人,不多。这篇文章我不会从教科书定义开始,而…

2026/10/18 17:04:56 阅读更多 →
USB不只是采集画面还能输出显示:从UVC到DP Alt Mode的方案与避坑指南

USB不只是采集画面还能输出显示:从UVC到DP Alt Mode的方案与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 17:04:56 阅读更多 →
数字IC社招UVM面试高频题拆解与工程实践回答策略

数字IC社招UVM面试高频题拆解与工程实践回答策略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 17:04:56 阅读更多 →
开发板待机功耗超标排查:从芯片睡眠到系统级漏电归因

开发板待机功耗超标排查:从芯片睡眠到系统级漏电归因

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 17:04:56 阅读更多 →
ROS2小车网页遥控:WebSocket桥接实现低延迟物理控制

ROS2小车网页遥控:WebSocket桥接实现低延迟物理控制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 17:03:56 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:55:02 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →