
1. 项目概述为什么Bloom是渲染的“点睛之笔”在Unity里做渲染尤其是做那些需要氛围感的项目比如赛博朋克夜景、奇幻魔法光效或者写实向的黄昏场景你绕不开一个东西Bloom也就是我们常说的“泛光”或“辉光”效果。这东西听起来挺玄乎但说白了它就是模拟现实世界中高亮光源比如灯泡、太阳光线溢出、在相机镜头或人眼中产生的一种柔和光晕。没有Bloom你的光源就是一个生硬的、像素级的亮块加上Bloom它立刻就有了体积感、空气感和情绪感画面一下子就“活”了。我见过太多新手项目模型、贴图、灯光都做得不错但总感觉画面“干巴巴”的或者像游戏引擎里的默认场景缺的就是这层后期处理的“滤镜”。Bloom Effect正是后期处理Post-Processing栈里最核心、最出效果的一环。它不单单是让亮的地方更亮而是通过一套算法让亮度信息在画面中“扩散”和“混合”创造出那种光线弥漫、朦胧梦幻的视觉感受。无论是追求极致真实的3A大作还是风格化的独立游戏Bloom都是提升画面表现力的标配。所以今天我们不聊那些大而全的教程就深挖一下在Unity中实现一个高质量、可定制的Bloom效果到底有哪些门道。我会从最基础的原理讲起带你手把手实现一个简化版再深入URP/HDRP管线中的高级配置最后分享一堆我踩过坑才总结出来的调参经验和性能优化技巧。无论你是刚接触Shader的萌新还是想优化现有项目画面的老手相信都能找到对你有用的东西。2. Bloom效果的核心原理与算法拆解要玩转Bloom不能只停留在调滑块上得知道它背后是怎么算的。知其然更要知其所以然这样出了问题你才知道从哪儿下手调。2.1 亮度阈值与高频信息提取Bloom不是对整个画面进行模糊。如果直接模糊全屏那整个画面都会变得糊成一团丢失所有细节。Bloom的第一步也是最重要的一步是亮度提取Luminance Extraction。我们需要一个阈值Threshold来告诉系统“只有比这个亮度更亮的部分才值得产生Bloom效果。” 这个阈值通常是一个颜色值或亮度值。在Shader中我们计算每个像素的亮度Luminance常见的公式是Luminance dot(color.rgb, float3(0.2126, 0.7152, 0.0722))。这个权重系数是基于人眼对绿光最敏感、红光次之、蓝光最不敏感的特性来的。然后我们将像素颜色与阈值进行比较。传统做法是简单的if (luminance threshold)但这样会产生生硬的边界。现代实现更常用的是软阈值Soft Threshold。它定义了一个过渡区间在这个区间内贡献度从0平滑过渡到1。公式通常类似float contribution saturate((luminance - threshold) / (softThreshold))。这样提取出来的高亮区域边缘是柔和的后续模糊出来的光晕也会更自然。实操心得阈值设置非常关键。设得太低比如0.5可能连一些白色反光都算进去导致Bloom范围过大画面“发白”或“起雾”。设得太高比如3.0可能只有极少数超亮光源如太阳、爆炸中心才会生效。我的经验是对于大多数HDR场景从1.0开始尝试然后根据画面中最亮物体的表现来微调。记住Bloom是用来强化“光源”感的不是用来给所有白色物体镶边的。2.2 高斯模糊与多级下采样Downsampling提取出高亮区域我们称之为“Bloom图”后下一步就是让它“扩散”开形成光晕。这个扩散过程本质上就是模糊Blur。最经典的模糊算法是高斯模糊Gaussian Blur。它的原理是用一个符合高斯分布正态分布的权重核Kernel对图像中每个像素及其周围的像素进行加权平均。权重核中心权重最高越往边缘权重越低。一个标准的5x5高斯核需要对每个像素进行25次纹理采样和计算在全分辨率下直接做性能开销是巨大的。因此所有高效的Bloom实现都采用了一个关键技术多级下采样Mipmap Chain 或 Pyramid。它的流程是这样的将提取到的高亮图渲染到一个比原图小一半长宽各除以2的渲染目标Render Target上。这个过程叫下采样Downsample。对这个半分辨率图进行一次模糊操作通常是两次一维的高斯模糊先水平后垂直即Separable Gaussian Blur性能更好。再将这个模糊后的图下采样到四分之一分辨率再进行一次模糊。重复此过程通常进行4到6次得到一系列分辨率递减、模糊程度递增的图像形成一个图像金字塔。为什么要这么做首先性能。在四分之一分辨率下做模糊需要处理的像素数只有原来的十六分之一性能提升是数量级的。其次质量。通过多级下采样和模糊我们模拟了光线在空气中多次散射、衰减的过程。低分辨率层代表大范围、柔和的底层光晕高分辨率层代表靠近光源的、较锐利的光晕边缘。最后将它们混合就能得到层次丰富、过渡自然的Bloom效果。2.3 混合Blending与色调映射Tone Mapping适配我们有了一个多层的Bloom金字塔最后一步就是把它加回到原始画面上。但这里不是简单的FinalColor SceneColor BloomColor。首先Bloom颜色通常是高动态范围HDR的可能非常亮。直接相加会导致混合区域严重过曝变成一片没有细节的纯白色。因此我们需要一个强度Intensity参数来控制Bloom的整体贡献度。有时还会提供一个色彩滤镜Color Filter参数让你给Bloom光晕染上一点颜色比如暖色调或冷色调以匹配场景氛围。其次混合模式也有讲究。简单加法是最常见的但有些实现会使用屏幕混合Screen Blend模式FinalColor 1 - (1-SceneColor) * (1-BloomColor)。这种模式能避免在已经很亮的区域过度增亮有时能产生更柔和的效果。Unity的Post Processing v2包中就提供了混合模式的选项。最后也是新手最容易忽略的一点Bloom必须在线性颜色空间Linear Color Space下计算并且要考虑色调映射Tone Mapping。如果你在Gamma空间做Bloom亮度计算和混合都会出错。而色调映射如ACES Filmic是一个将HDR值压缩到显示器能显示的LDR范围的曲线。一个强烈的Bloom值在经过一个对比度很强的色调映射曲线后可能会被压得过于暗淡或产生色偏。因此高级的Bloom实现有时会提供选项让Bloom在色调映射之前或之后进行混合这会产生不同的视觉风格。Unity URP/HDRP的Bloom效果通常已经正确处理了颜色空间和色调映射的交互但如果你是自己写Shader这一点必须牢记。3. 在Unity URP中实现与配置Bloom效果理解了原理我们来看看在当下最流行的Universal Render Pipeline (URP) 中如何实际使用和配置Bloom。URP内置了高质量的Bloom效果我们无需从零造轮子但要知道怎么把它用好。3.1 启用与基础配置首先确保你的项目使用的是URP。然后你需要一个Volume组件来承载后处理效果。在场景中创建一个空物体添加Volume组件。在它的Profile里点击Add Override选择Bloom。核心参数解析Threshold阈值 刚才原理部分讲过的亮度阈值。URP的Bloom默认使用了更科学的色彩阈值Color Threshold。它不是一个单值而是一个颜色。系统会分别比较像素的R、G、B通道与阈值颜色的对应通道只有至少一个通道超过阈值该像素才会被纳入Bloom计算。这允许你对不同颜色的光设定不同的敏感度。通常保持默认1,1,1,1即可你可以整体调节强度。Intensity强度 Bloom效果的总体亮度乘数。这是你最常调节的参数。从0.5开始慢慢往上加直到光晕可见但又不“糊脸”为止。写实场景通常较低0.2-0.8风格化或科幻场景可以调高1.5-3.0。Scatter散射 这个参数控制Bloom光晕的“扩散”程度。值越低如0.1光晕更紧贴光源显得锐利值越高如0.9光晕扩散范围更大更柔和、梦幻。它本质上影响了模糊核的大小或下采样的权重。Tint色调 给Bloom光晕整体染上一层颜色。默认是白色1,1,1。你可以尝试微妙的暖色如1.0, 0.95, 0.9来模拟夕阳或用冷色0.9, 0.95, 1.0来表现月光或科技感。Clamp钳制 一个防止极端亮斑的安全措施。它将Bloom图中单个像素的最大亮度限制在这个值避免因某个像素过亮比如数值爆炸导致整个Bloom效果失控变成全屏白光。一般不需要动除非你遇到了奇怪的过曝问题。3.2 高级参数与性能权衡URP的Bloom提供了一些高级选项用于质量微调和性能优化。High Quality Filtering高质量过滤 勾选后在下采样和上采样过程中会使用双线性或三线性过滤减少因分辨率降低而产生的锯齿感特别是针对移动的、高对比度的光源边缘质量更好但稍有性能开销。对于PC和主机平台建议开启。Skip Iterations跳过迭代 这是一个重要的性能控制参数。它定义了从第几级开始进行Bloom的模糊混合。默认是1意味着从第一次下采样后半分辨率的纹理开始进行模糊和叠加。如果设为2则会跳过第一级直接从四分之一分辨率开始。跳过的级数越多性能越好但Bloom效果的精细度会下降特别是小光源周围锐利的辉光边缘可能会消失。在移动设备上为了性能经常设置为2甚至3。Dirt Texture Dirt Intensity污渍纹理与强度 这是一个增强镜头感的功能。你可以导入一张带有灰尘、指纹、划痕的黑白纹理图Dirt Texture。Bloom效果会乘以这张纹理的亮度值从而在光晕上产生不均匀的、类似真实相机镜头污渍的效果极大增强电影感。强度控制这种效果的明显程度。3.3 针对不同场景的配置策略配置Bloom没有万能公式必须结合场景内容。室内小场景如房间、洞穴 光源通常是灯、火把等点光源。阈值可以设得稍高1.5-2.5确保只有这些明确的光源发光。强度适中0.3-0.7散射可以低一些0.2-0.4让光晕更集中避免“雾霾”感充斥整个密闭空间。户外大场景如平原、山脉 主要光源是太阳和天空盒。阈值要设低0.8-1.2因为太阳的亮度极高同时要捕捉天空的明亮部分。强度可以大胆一些0.8-1.5散射也可以高0.6-0.8让阳光和天光柔和地弥漫开来。这里有个关键技巧单独为太阳Directional Light设置一个极高的亮度比如5-10并确保它的颜色是HDR值超过1,1,1这样它才能被Bloom阈值捕捉到产生强烈的光晕。霓虹灯/科幻风格 追求强烈的、色彩鲜艳的发光效果。阈值可以很低0.5-1.0让所有带亮色的物体都参与发光。强度可以拉得很高2.0-5.0色调可以赋予夸张的色彩如品红、青色。同时可以尝试使用或制作一张风格化的Dirt Texture来增加科技感的镜头炫光。踩坑实录我曾经在一个移动端项目里Bloom导致低端机帧数骤降。排查后发现罪魁祸首是场景里有很多细小但高亮的粒子特效如火星。每个粒子都触发了Bloom计算导致下采样纹理里充满了高频噪声模糊计算量巨大。解决方案有两个一是提高Bloom阈值让这些小粒子不被提取二是修改粒子Shader让其输出到Bloom缓冲区的亮度大大降低通过一个自定义的渲染层或Shader变体。这提醒我们美术资源的亮度设计需要和后处理效果联动考虑。4. 自定义Bloom Shader开发入门虽然URP内置的Bloom已经很强大了但有时你有特殊需求比如想要完全控制模糊算法、实现一些风格化效果如六边形光斑、条纹炫光或者需要极致的性能优化这时候就需要自己动手写Shader了。别怕我们来实现一个最基础的版本。4.1 创建后处理渲染器特性Render Feature在URP中自定义后处理效果通常通过ScriptableRendererFeature和ScriptableRenderPass来实现。我们创建一个CustomBloomFeature。using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public class CustomBloomFeature : ScriptableRendererFeature { [System.Serializable] public class Settings { public Material material; // 承载Bloom算法的材质 public float threshold 1.0f; public float intensity 1.0f; public int downSample 2; // 下采样次数 public RenderPassEvent renderPassEvent RenderPassEvent.BeforeRenderingPostProcessing; } public Settings settings new Settings(); private CustomBloomPass _renderPass; public override void Create() { _renderPass new CustomBloomPass(settings); } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { if (settings.material null) return; renderer.EnqueuePass(_renderPass); } }4.2 实现渲染通道Render Pass核心逻辑在CustomBloomPass中。我们需要申请临时渲染目标RT并执行多步绘制。class CustomBloomPass : ScriptableRenderPass { private Material _material; private float _threshold; private float _intensity; private int _iterations; private RenderTargetIdentifier _source; // 相机原始颜色纹理 private RenderTargetIdentifier _destination; // 最终目标纹理通常是相机帧缓冲 private int[] _bloomRT; // 用于存储下采样链的RT ID数组 public CustomBloomPass(CustomBloomFeature.Settings settings) { _material settings.material; _threshold settings.threshold; _intensity settings.intensity; _iterations settings.downSample; _bloomRT new int[_iterations]; for (int i 0; i _iterations; i) { _bloomRT[i] Shader.PropertyToID(_BloomRT i); } renderPassEvent settings.renderPassEvent; } public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) { RenderTextureDescriptor descriptor renderingData.cameraData.cameraTargetDescriptor; descriptor.msaaSamples 1; // Bloom通常在抗锯齿之后进行 descriptor.depthBufferBits 0; // 不需要深度 // 为每一级下采样创建RT for (int i 0; i _iterations; i) { descriptor.width Mathf.Max(1, descriptor.width 1); descriptor.height Mathf.Max(1, descriptor.height 1); cmd.GetTemporaryRT(_bloomRT[i], descriptor, FilterMode.Bilinear); } } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { if (_material null) return; CommandBuffer cmd CommandBufferPool.Get(Custom Bloom); var source renderingData.cameraData.renderer.cameraColorTarget; // 第一步亮度提取到第一个RT _material.SetFloat(_Threshold, _threshold); Blit(cmd, source, _bloomRT[0], _material, 0); // 假设材质第0个Pass是提取 // 第二步构建下采样与模糊金字塔 for (int i 1; i _iterations; i) { // 从上一级RT下采样并模糊到当前级RT Blit(cmd, _bloomRT[i-1], _bloomRT[i], _material, 1); // 假设第1个Pass是下采样模糊 } // 第三步上采样并混合这里简化实际应从最小RT开始向上混合 for (int i _iterations - 2; i 0; i--) { // 将当前级RT与上一级更模糊的RT混合 cmd.SetGlobalTexture(_BloomTex, _bloomRT[i1]); Blit(cmd, _bloomRT[i], _bloomRT[i], _material, 2); // 假设第2个Pass是上采样混合 } // 第四步将最终的Bloom图加回原画面 _material.SetFloat(_Intensity, _intensity); cmd.SetGlobalTexture(_BloomTex, _bloomRT[0]); Blit(cmd, source, source, _material, 3); // 假设第3个Pass是最终合成 context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } public override void OnCameraCleanup(CommandBuffer cmd) { // 释放所有临时RT for (int i 0; i _iterations; i) { cmd.ReleaseTemporaryRT(_bloomRT[i]); } } }4.3 编写Bloom Shader最后是Shader部分。这里展示一个极度简化的多Pass Shader框架。Shader Hidden/Custom/Bloom { Properties { _MainTex (Texture, 2D) white {} _Threshold (Threshold, Float) 1.0 _Intensity (Intensity, Float) 1.0 } SubShader { Cull Off ZWrite Off ZTest Always // Pass 0: 亮度提取 Pass { CGPROGRAM #pragma vertex vert_img // 使用内置的全屏四边形顶点着色器 #pragma fragment frag_extract #include UnityCG.cginc sampler2D _MainTex; float _Threshold; float4 frag_extract (v2f_img i) : SV_Target { float4 col tex2D(_MainTex, i.uv); float luminance dot(col.rgb, float3(0.2126, 0.7152, 0.0722)); // 简单阈值提取 float bloomFactor step(_Threshold, luminance); return col * bloomFactor; } ENDCG } // Pass 1: 下采样与模糊简化版仅做4-tap双线性下采样模拟模糊 Pass { CGPROGRAM #pragma vertex vert_img #pragma fragment frag_downsample_blur #include UnityCG.cginc sampler2D _MainTex; float4 _MainTex_TexelSize; // 纹素大小用于偏移采样 float4 frag_downsample_blur (v2f_img i) : SV_Target { float2 texel _MainTex_TexelSize.xy; // 简单的4采样模糊实际应用应使用分离高斯模糊 float4 a tex2D(_MainTex, i.uv float2(-texel.x, -texel.y) * 0.5); float4 b tex2D(_MainTex, i.uv float2( texel.x, -texel.y) * 0.5); float4 c tex2D(_MainTex, i.uv float2(-texel.x, texel.y) * 0.5); float4 d tex2D(_MainTex, i.uv float2( texel.x, texel.y) * 0.5); return (a b c d) * 0.25; } ENDCG } // Pass 2 3 略... 实际需要上采样混合和最终合成的Pass } }这个自定义版本非常基础缺少软阈值、高质量模糊、色调映射适配等。但它清晰地展示了Bloom的完整管线提取 - 下采样模糊金字塔 - 上采样混合 - 合成。你可以在此基础上实现更复杂的算法比如Kawase Blur一种高性能的近似高斯模糊、Dual Filtering Bloom或者加入刚才提到的色调映射前/后混合选项。5. 性能优化与疑难问题排查Bloom效果虽好但用不好就是帧率杀手。尤其在移动平台和低端PC上必须精打细算。5.1 性能优化关键点分辨率是头号敌人 Bloom处理的是全屏纹理。分辨率从1080p降到720p像素数减少约55%性能提升立竿见影。URP的Render Scale设置Universal Render Pipeline Asset - Quality - Render Scale可以全局降低渲染分辨率对性能提升极大但会影响画面清晰度。更精细的做法是单独降低Bloom的渲染分辨率。在URP的Bloom组件中Skip Iterations参数就是干这个的。每跳过一次迭代Bloom的起始分辨率就降低一倍性能提升接近一倍。在移动端从Skip 1改为Skip 2是常见的优化手段。迭代次数与模糊半径 下采样/模糊的迭代次数URP中由内部算法控制自定义Shader中由你决定直接影响效果质量和性能。通常4-6次迭代足以满足大多数需求。每增加一次迭代就多一倍的低分辨率纹理和模糊计算。模糊半径由Scatter参数或Shader中的采样偏移控制越大每次模糊的采样次数越多。在自定义Shader中使用分离高斯模糊Separable Gaussian Blur将一个二维NxN的模糊核拆解成水平N次采样和垂直N次采样能将复杂度从O(N²)降到O(2N)是必用的优化。精确控制渲染目标 在自定义Render Pass中要确保创建的临时RT格式正确通常RenderTextureFormat.DefaultHDR即可并且尺寸精确。在OnCameraCleanup中必须及时释放避免内存泄漏。使用CommandBuffer.GetTemporaryRT和ReleaseTemporaryRT来管理生命周期。基于设备的动态调整 最好的优化是“按需分配”。可以在游戏启动时检测设备等级通过SystemInfo.graphicsDeviceType,SystemInfo.graphicsMemorySize等动态调整Bloom的启用状态、迭代次数、分辨率等参数。甚至可以准备两套后处理Volume Profile在高低配设备间切换。5.2 常见问题与解决方案速查表问题现象可能原因排查与解决方案画面整体发白、起雾Bloom阈值过低或强度过高。1. 提高Threshold值确保只有真正的光源被提取。2. 大幅降低Intensity值从0.2开始尝试。3. 检查场景中是否有大量纯白色或高亮物体考虑调整它们的材质亮度或排除在Bloom层外。光晕边缘有锯齿闪烁下采样过程中过滤质量不足或光源在屏幕上移动过快。1. 在URP中勾选High Quality Filtering。2. 在自定义Shader中确保下采样时使用双线性或三线性过滤tex2D默认就是双线性。3. 对于移动的UI或粒子可以考虑使用基于时间的抖动Temporal Filtering来平滑但这比较复杂。特定颜色如红色光晕异常颜色阈值设置不当或色调映射导致色偏。1. 检查URP Bloom的Threshold是否是颜色值确保R、G、B通道平衡。默认(1,1,1)最安全。2. 尝试关闭或切换色调映射Tone Mapping模式看是否是ACES等强对比曲线压坏了Bloom色彩。性能消耗巨大帧率下降明显Bloom计算分辨率过高迭代次数太多或场景中高频高亮像素过多。1. 增加Skip Iterations。2. 检查是否在低配移动设备上开启了Bloom考虑动态关闭。3. 使用渲染统计工具如Unity Profiler的GPU模块查看Bloom Pass的耗时定位瓶颈。4. 优化美术资源减少使用极亮HDR值3的粒子或自发光材质。Bloom效果在透明物体后不正确Bloom是在所有不透明物体渲染后、透明物体渲染前执行的。透明物体如UI、粒子没有被Bloom处理。这是预期行为。如果需要UI也产生Bloom需要将UI渲染到另一个相机或使用特殊的渲染层顺序。通常Bloom主要作用于场景中的3D光源。自定义Bloom Shader全屏粉色/紫色Shader编译错误或材质球丢失。1. 检查Shader代码是否有语法错误确保所有Pass和Properties正确定义。2. 在CustomBloomFeature的Settings中确保Material字段已正确赋值。3. 查看编辑器Console窗口是否有Shader编译错误提示。5.3 调试技巧可视化中间步骤调试Bloom这类屏幕后处理效果一个非常有效的方法是可视化中间渲染纹理。在自定义Render Pass中你可以将任何一步的RT通过Blit到一张调试用的RenderTexture或者直接替换相机的最终输出来观察亮度提取图、每一级下采样图的样子。这能帮你快速判断是阈值设错了还是模糊算法有问题或是混合权重不对。在URP中虽然不能直接修改内置效果但你可以通过Frame Debugger工具在渲染事件列表中找到“Bloom”相关的DrawCall观察其输入输出纹理对理解内部流程也大有裨益。最后记住Bloom是一种“调味料”不是“主菜”。它的目的是服务于整体画面氛围切忌喧宾夺主。最好的Bloom效果是让玩家感觉不到它的存在但拿掉后又会觉得画面少了灵魂。多参考优秀的游戏和电影培养自己对光感的审美再结合今天讲的技术细节你就能在Unity里驾驭好这股“光”的力量了。