Shader Graph实现自定义Bloom:从原理到性能优化的完整指南

📅 2026/7/18 9:04:58
Shader Graph实现自定义Bloom:从原理到性能优化的完整指南
1. 项目概述为什么我们需要在URP/HDRP中自己动手做Bloom在Unity的通用渲染管线URP和高清渲染管线HDRP中Post Processing后处理堆栈已经内置了非常强大且高质量的Bloom泛光效果。那么为什么我们还需要费劲地用Shader Graph去重新实现一个呢这个问题在我接手一个风格化渲染的独立游戏项目时变得尤为突出。项目要求Bloom效果不仅要能产生光晕还需要能根据物体的特定材质属性比如自发光贴图的某个通道来动态控制泛光的强度和颜色甚至需要将Bloom效果与一些自定义的屏幕扭曲效果进行混合。内置的Bloom虽然质量高但作为一个“黑盒”其可定制性在面对这些非标准需求时就显得捉襟见肘了。这正是Shader Graph的价值所在。它允许我们以可视化的方式深入到后处理效果的底层逻辑从屏幕缓冲区的采样开始一步步构建出我们想要的视觉效果。通过Shader Graph实现Bloom你获得的不只是一个效果而是一套完整的、可任意拆解和重组的光效流水线。你可以精确控制高亮区域的提取阈值、模糊的迭代次数与采样权重、颜色叠加的方式甚至可以将Bloom Mask泛光遮罩与游戏逻辑如角色技能、场景交互绑定。这对于追求独特艺术风格、需要高度定制化后期效果或者希望深入理解图像处理原理的开发者来说是一个不可或缺的技能。简单来说用Shader Graph做Bloom核心目标是将效果的控制权从“能用”提升到“精控”。接下来我将以一个可调节的、基于Shader Graph的Bloom效果为例拆解其从原理到实现的完整过程并分享其中那些官方文档里不会写的“坑”和技巧。2. 核心原理拆解Bloom效果是如何“算”出来的在动手连接节点之前我们必须先搞清楚Bloom效果的算法骨架。一个标准的Bloom实现通常遵循一个经典的“提取-模糊-合成”三步流水线。理解每一步的数学和视觉意图是后续在Shader Graph中灵活调整的关键。2.1 高亮区域提取决定“什么在发光”Bloom的第一步是从渲染完成的场景图像中分离出那些足够亮、应该产生光晕的部分。我们不会让整个画面都泛光那样会失去对比度显得画面“发灰”。实现原理 我们通常对屏幕图像进行一次“亮度提取”操作。最常见的方法是计算每个像素的RGB值转换到亮度Luminance的公式例如Luminance 0.2126*R 0.7152*G 0.0722*B。这个权重基于人眼对绿光最敏感、对蓝光最不敏感的特性。得到亮度值后我们用一个阈值Threshold进行过滤。只有亮度高于此阈值的像素才会被保留低于阈值的则置为黑色。公式可以简化为Highlight max(Luminance - Threshold, 0)。但直接这样做的边缘会很生硬因此通常会配合一个柔化阈值Soft Threshold或使用平滑函数让高亮区域的边缘有一个平滑的过渡。在Shader Graph中的对应思路 我们将从Scene Color节点获取屏幕图像通过一系列乘法和加法节点模拟亮度计算然后与一个Threshold属性进行比较和钳制Clamp最终输出一张只有高亮区域的单色或彩色纹理。2.2 模糊处理制造“光晕”的扩散感提取出的高亮区域是一张锐利的、边缘清晰的图。Bloom的“光晕”感正是通过对这张高亮图进行多次模糊处理来实现的。模糊的本质是让每个像素的颜色向其周围像素“扩散”。高斯模糊Gaussian Blur是最常用的方法 它不是一个简单的均匀模糊而是根据距离按照高斯分布正态分布的权重来混合周围像素的颜色。距离中心像素越远的像素权重越低。这能产生非常平滑、自然的模糊效果。由于性能考虑我们通常使用“分离式高斯模糊”。它将一个二维的高斯模糊核分解为两个一维的模糊操作先进行一次水平方向的模糊再在结果上进行一次垂直方向的模糊。这样假设模糊核大小为N计算复杂度从N²降低到了2N性能提升巨大。在Shader Graph中的实现挑战 Shader Graph本身没有提供一键式的高斯模糊节点。我们需要手动构建一个模糊链。这通常意味着创建一个可调节的Blur Offset模糊偏移量和Iterations迭代次数属性。使用Sample Texture 2D节点通过多次采样每次采样时对UV坐标进行微小的偏移UV float2(offset, 0)用于水平UV float2(0, offset)用于垂直并赋予不同的权重来模拟模糊核。为了达到较强的模糊效果我们往往需要多次如4次、8次执行这个分离式模糊过程将上一次模糊的结果作为下一次的输入。这需要在Shader Graph中妥善组织节点或者借助Sub Graph来封装可重用的模糊单元。2.3 合成叠加将光晕“加回”原画面经过模糊处理后我们得到了一张柔和、扩散的高亮纹理这就是光晕层。最后一步是将这个光晕层以某种方式与原始的场景颜色进行混合。最常见的合成方式是“加法混合Additive Blending” 即Final Color Original Scene Color Bloom Texture * Intensity。这里的Intensity是一个强度系数用于控制泛光的整体亮度。加法混合能很好地模拟光的叠加效果让发光区域显得更亮、更通透。更高级的合成方式屏幕混合Screen BlendFinal Color 1 - (1 - Original) * (1 - Bloom)。这种混合方式能防止颜色过曝超过1.0在某些HDR工作流中可能更合适但视觉上通常比加法混合更柔和。颜色控制我们可以对Bloom纹理进行色调调整Tint例如让所有泛光带上一点暖黄色模拟黄昏的太阳光晕。在Shader Graph中的操作 这一步相对直观。我们将原始的Scene Color与处理后的Bloom纹理通过一个Add或自定义的混合节点进行结合并通过一个Intensity属性控制混合强度。最终输出到Fragment节点的Base Color如果这是整个后处理的最终输出或一个临时的渲染纹理。理解了这三步核心原理我们就有了在Shader Graph中搭建效果的“设计图”。接下来我们将进入具体的实现环节。3. Shader Graph 实战一步步构建可调节Bloom这里我们将在URP环境下进行创建。HDRP的流程基本一致只是部分节点名称和渲染管线接口可能略有不同。我们将创建一个Unlit Shader Graph因为它最适合用于屏幕后处理效果。3.1 创建Shader Graph与基础设置创建资源在Project窗口中右键 - Create - Shader - Unlit Shader Graph。命名为“SG_CustomBloom”。关键图形设置在Graph Inspector中如果没打开在Shader Graph窗口左上角点击“Graph Inspector”按钮将“Graph Type”设置为Sub Graph或Sprite Unlit等等对于全屏后处理我们通常先创建一个Sub Graph作为Bloom的核心算法再创建一个主Shader Graph来调用它。但为了教程连贯我们先在一个主图中实现。更推荐的方法是先做Sub Graph。实际上更清晰的流程是创建一个主Shader Graph如“FullscreenBloom”用于最终合成并创建一个Sub Graph如“SG_GaussianBlur”封装模糊逻辑。这里我们先按主图实现。添加必需属性在Blackboard中创建以下Property这些都是我们后期需要调节的参数_Threshold(Vector1): 高亮提取阈值默认值0.8。_SoftThreshold(Vector1): 柔化阈值范围默认值0.1。用于平滑过渡。_Intensity(Vector1): Bloom强度默认值1.0。_TintColor(Color): 泛光色调默认白色。_BlurOffset(Vector1): 单次模糊的采样偏移距离默认值0.005。这个值需要根据屏幕分辨率调整。_Iterations(Vector1): 模糊迭代次数默认值4。整数类型但Shader Graph中通常用Float我们后面会取整。3.2 实现高亮提取Highlight Extraction获取屏幕颜色添加一个Scene Color节点。这是URP提供的节点可以直接采样当前渲染的屏幕图像。计算亮度添加一个Dot Product节点。将Scene Color的RGB输出连接到Dot Product的A端口。创建一个Vector3常量值为(0.2126, 0.7152, 0.0722)连接到Dot Product的B端口。Dot Product节点的输出就是该像素的近似亮度值Luminance。应用阈值与柔化添加一个Subtract节点将上一步的亮度值减去_Threshold属性。添加一个Saturate节点其本质是Clamp(0, 1)但我们需要一个平滑过渡。这里采用一个常见的技巧// 伪代码用于理解节点连接 half brightness dot(sceneColor.rgb, half3(0.2126, 0.7152, 0.0722)); half knee _SoftThreshold * 0.5; // 柔化区间的一半 half soft brightness - _Threshold knee; soft clamp(soft, 0, _SoftThreshold*2); // 钳制到柔化范围 soft soft * soft / (4 * knee 1e-5); // 二次曲线平滑 half highlight max(brightness - _Threshold, soft); // 最终高亮值在Shader Graph中我们需要用多个Multiply、Add、Subtract、Divide和Power节点来构建上述公式。虽然看起来复杂但连接完成后可以封装成一个Sub Graph名为“SG_HighlightExtract”方便复用。重建彩色高亮图最终的高亮图不应该是黑白的。我们将计算出的highlight值一个0-1的标量与原始的Scene Color相乘Add一个Multiply节点A端口接Scene ColorB端口接上一步计算出的highlight标量。输出就是彩色的高亮纹理。实操心得阈值和柔化阈值的调节需要配合场景的整体亮度。在明亮的室外场景阈值可能需要调高如1.2以避免过多物体泛光在昏暗的室内阈值可以很低如0.5让微弱的灯光也能产生光晕。_SoftThreshold通常设为_Threshold的10%-25%能获得比较自然的过渡。3.3 构建高斯模糊子图Sub Graph这是最复杂的一步。我们将创建一个可重用的高斯模糊Sub Graph。创建Sub Graph在Project窗口右键 - Create - Shader - Sub Graph。命名为“SG_GaussianBlur”。定义接口Inputs: 添加Texture2D输入命名为InputTexSamplerState输入命名为SamplerVector2输入命名为UVFloat输入命名为BlurOffset。Outputs: 添加Vector4输出命名为OutputColor。实现分离式高斯模糊逻辑高斯模糊需要一组权重和偏移。一个经典的5-tap采样5次高斯核的权重近似为[0.227, 0.194, 0.121, 0.054, 0.016]对应的偏移是[0, ±1, ±2] * offset。在Sub Graph中我们需要进行两次这样的操作。水平模糊// 伪代码逻辑 Vector4 color SampleTexture(InputTex, Sampler, UV) * 0.227; color SampleTexture(InputTex, Sampler, UV float2(BlurOffset, 0)) * 0.194; color SampleTexture(InputTex, Sampler, UV - float2(BlurOffset, 0)) * 0.194; color SampleTexture(InputTex, Sampler, UV float2(BlurOffset*2, 0)) * 0.121; color SampleTexture(InputTex, Sampler, UV - float2(BlurOffset*2, 0)) * 0.121; // ... 可以继续增加更多tap以获得更平滑模糊但性能开销也增大在Shader Graph中你需要添加多个Sample Texture 2D节点将它们的UV输入分别设置为UV、UV (BlurOffset, 0)等然后将采样结果乘以对应的权重常量最后用一个Add节点将所有结果相加。垂直模糊将水平模糊的结果作为一个虚拟的纹理作为输入重复上述过程但偏移方向改为(0, BlurOffset)。在主图中调用模糊回到主Shader Graph将高亮提取的输出颜色连接到我们自定义的SG_GaussianBlur子图的InputTex。将Tiling And Offset节点的UV输出或直接用UV节点连接到子图的UV。将_BlurOffset属性连接到子图的BlurOffset。为了实现多次迭代我们需要将模糊子图串联起来。最简单但低效的方法是复制粘贴多个子图节点将前一个的OutputColor连接到后一个的InputTex。迭代次数由_Iterations控制但Shader Graph不支持动态循环我们通常固定一个较大的迭代次数比如4次然后用一个Branch节点根据_Iterations来选择使用第几次迭代的结果。更高效的方法是在C#脚本中动态生成材质属性并控制渲染次数但这超出了纯Shader Graph的范围。注意事项模糊偏移量_BlurOffset的值非常敏感。它通常与屏幕分辨率成反比。一个经验公式是BlurOffset ≈ 1.0 / _ScreenParams.x水平方向或1.0 / _ScreenParams.y垂直方向。_ScreenParams是一个内置变量包含了屏幕宽度和高度。你可以创建一个Vector2参数_BlurRadius然后在节点中计算offset _BlurRadius / _ScreenParams.xy这样调节起来更直观。3.4 最终合成与输出色调调整将多次模糊后的最终Bloom纹理与_TintColor属性相乘实现颜色过滤。强度控制再将上一步的结果与_Intensity属性相乘。叠加到原画面添加一个Add节点。将原始的Scene Color连接到A端口将经过色调和强度调整的Bloom纹理连接到B端口。输出将Add节点的结果连接到Fragment节点的Base Color如果你的Shader是用于全屏Blit或Emission如果作为某个物体的材质。对于全屏后处理我们通常创建一个使用此Shader的材质然后通过脚本如CommandBuffer或URP的RenderFeature在相机渲染完成后用这个材质将整个屏幕绘制一次。至此一个基础的可调节Bloom效果在Shader Graph中就搭建完成了。你可以通过调节_Threshold、_Intensity、_BlurOffset等参数实时在材质面板上看到效果变化。4. 性能优化与进阶技巧用Shader Graph实现的Bloom虽然灵活但性能开销需要密切关注尤其是模糊操作。4.1 性能优化策略降低分辨率进行模糊Downsample这是最有效的优化手段。我们不需要在全分辨率下进行昂贵的多次模糊。通常的做法是先将高亮提取的结果渲染到一张原图1/2或1/4大小的渲染纹理Render Texture中。在这张低分辨率纹理上进行模糊操作。最后将模糊结果上采样Upsample到全分辨率再与原始场景颜色合成。这样做模糊操作的像素处理量减少了3/41/2下采样或15/161/4下采样性能提升巨大而视觉损失在合理的模糊强度下几乎不可察觉。在Shader Graph中实现下采样可以通过调整Sample Texture 2D节点的UV缩放或者更规范地在C#脚本端创建不同尺寸的Render Texture来实现。控制迭代次数与采样数_Iterations模糊次数和每次模糊的采样点数Tap数是性能的主要消耗点。对于移动平台可以从迭代2次、每遍5-tap模糊开始测试。PC平台可以适当增加到4次迭代、7-tap或9-tap模糊以获得更平滑的效果。使用双线性滤波Bilinear Filtering在创建用于Bloom的Render Texture时确保其过滤模式设置为Bilinear。这样在模糊和下/上采样时硬件纹理过滤能提供一定的平滑效果有时可以减少模糊迭代次数。4.2 进阶效果拓展散景Bokeh效果可以通过在模糊过程中使用一个自定义的形状如六边形、圆形的采样核而不是标准的高斯核来模拟相机镜头的散景光斑。这需要在模糊子图中修改采样偏移和权重分布。基于深度的Bloom有时我们不希望远景或某些特定物体如UI产生Bloom。可以结合Camera Depth Texture。在提取高亮时先采样深度图通过比较深度值来决定是否提取该像素的高亮信息。这能做出景深般的Bloom效果让光晕更聚焦于前景物体。Bloom Mask贴图除了亮度我们还可以用一张额外的遮罩贴图Mask Texture来控制Bloom的强度。例如让场景中特定的发光符文或魔法阵区域产生更强烈的Bloom。只需将遮罩贴图的采样值乘到高亮提取的结果上即可。与辉光Glow通道结合在制作科幻或霓虹风格场景时通常会在模型材质中直接输出一个“Glow”或“Emission”通道到渲染纹理如使用URP的Renderer Features自定义渲染目标。我们的Bloom Shader可以直接采样这个Glow通道作为高亮源而不是从场景颜色中提取这样控制更加精确和高效。5. 常见问题与调试实录在实际操作中你肯定会遇到各种问题。以下是我踩过的一些坑和解决方法问题1Bloom效果闪烁或抖动。原因最常见的原因是_BlurOffset值在每帧计算时依赖于像_Time这样的变量或者UV计算有精度问题。更隐蔽的原因是下采样的Render Texture尺寸不是偶数导致采样位置在整数像素间来回跳动。解决确保用于下采样的Render Texture的宽高是偶数。检查模糊计算中的UV偏移确保是稳定的。避免使用frac、sin(_Time)等会产生周期性变化的函数直接控制核心偏移量。可以考虑对Bloom纹理使用Clamp或Repeat的包裹模式但Clamp通常更安全。问题2Bloom边缘有黑色或颜色不正常的硬边。原因高亮提取的阈值设置得太高或者柔化过渡Soft Threshold没做好导致高亮区域与非高亮区域边界对比过于强烈。在模糊后这个硬边被扩散开形成了黑圈或光晕断层。解决适当降低_Threshold提高_SoftThreshold让提取区域边缘更平滑。检查亮度计算公式是否正确确保没有因为权重错误导致某些颜色通道被过度抑制。尝试在模糊前对提取的高亮图进行一次轻微的“侵蚀”Erode或“膨胀”Dilate操作可通过额外的采样实现来平滑其形状。问题3性能开销巨大游戏帧率下降明显。原因模糊迭代次数过多或在下采样前就在全分辨率下进行了模糊。解决务必使用下采样。这是性能优化的第一步也是最重要的一步。逐步减少_Iterations。很多时候2-3次迭代配合下采样效果已经足够好。在Unity编辑器的Stats窗口或Profiler的Rendering部分查看SetPass Calls和Batches确认Bloom绘制命令的消耗。确保你的全屏绘制只发生一次。问题4如何将这个Shader Graph应用到相机上纯Shader Graph方案局限单个Shader Graph文件无法直接作为后处理效果挂在相机上。它需要被一个材质球使用。标准实现流程将上面完成的Shader Graph保存它会生成一个.shader文件。创建一个新的材质球使用这个.shader。编写一个C#脚本继承自MonoBehaviour。在OnRenderImage方法中在Built-in RP中或使用URP的RenderFeature推荐。在URP中创建一个Scriptable Renderer Feature在其AddRenderPasses方法中配置一个ScriptableRenderPass。在这个Pass中使用CommandBuffer.Blit或CoreUtils.DrawFullScreen方法用你创建的Bloom材质来绘制全屏四边形。将这个Renderer Feature添加到URP Asset的Renderer列表中。这样每个使用该Render的相机都会应用你的Bloom效果。这个过程涉及一定的脚本编写但它将Shader Graph的计算结果与Unity的渲染管线完美衔接是生产环境中的标准做法。虽然步骤稍多但一旦配置完成所有参数都可以通过材质球或脚本进行动态调节非常灵活。通过这一整套从原理到实现再到优化和问题排查的流程你应该已经掌握了在URP/HDRP中用Shader Graph打造自定义Bloom效果的核心能力。这不仅仅是实现了一个效果更重要的是你拥有了对“光”进行像素级编程控制的工具和思路。下次当你需要实现某种特殊的镜头光晕、能量护盾辉光或者风格化的发光特效时这套方法可以随时被你拆解、重组和扩展。