
1. 后处理泛光效果的核心价值与设计思路1.1 泛光效果到底在解决什么问题做实时渲染的朋友应该都有这个体会场景里放再亮的自发光物体如果只做常规的色调映射画面看起来总是“平”的。一个发光灯管、一柄附魔武器、远处窗户透出的阳光这些元素在物理世界里会向周围空间溢出光晕而标准渲染管线只计算了直接光照和间接光照缺少了人眼和摄像机镜头对强光的散射响应。后处理泛光Post Processing Bloom就是用来补上这一层视觉真实感的关键环节。它的本质并不复杂把画面中亮度超过某个阈值的区域提取出来做多次模糊再把模糊结果叠加回原图。听起来简单但真正落地时会遇到一堆问题——阈值怎么定、模糊半径多大、多级模糊怎么组织、和色调映射的先后顺序怎么安排、移动端性能怎么控制。这些问题没有标准答案取决于你的项目类型、目标平台和美术风格。我见过不少项目泛光要么做得像“糊了一层油”要么亮部直接过曝成一片死白根本原因是参数没有根据实际场景的亮度分布来调而是照搬了某个教程的默认值。这篇内容我会把泛光后处理的完整实现链路拆开讲从亮度提取、降采样、模糊、上采样合成到参数调优和性能优化尽量给出可以直接参考的方案。1.2 为什么选择后处理方式而不是前向渲染内联泛光可以在前向渲染阶段对每个发光材质单独处理也可以放到后处理阶段统一处理。早期的一些项目确实用过前向内联的方式比如给自发光材质加一个额外的光晕面片或者用顶点着色器扩展几何体。这种做法的问题是光晕和场景几何强耦合遮挡关系容易出错而且每个材质都要单独配置美术维护成本极高。后处理方式的优势在于解耦。场景渲染完成后泛光作为一个全屏图像处理步骤运行它不关心光源来自哪里、是什么材质只关心最终像素的亮度值。这意味着美术只需要在材质里设置自发光强度泛光效果自动生效可以统一控制全局泛光强度、阈值、半径调整效率高不同平台可以用不同的降采样级别和模糊迭代次数适配灵活和色调映射、颜色分级等其他后处理效果可以按需组合当然后处理泛光也有代价——它需要额外的渲染目标、多次全屏采样和模糊计算对带宽和填充率有要求。但在现代图形API下通过合理的降采样策略这个开销完全可以控制在可接受范围内。1.3 整体管线设计从场景颜色到最终泛光合成一个完整的后处理泛光管线通常包含以下几个阶段场景渲染输出HDR颜色缓冲包含自发光、高光、亮部区域亮度提取根据阈值筛选出需要产生泛光的像素降采样链将亮度图逐级缩小每级记录不同频率的亮度信息模糊处理对每一级降采样图做高斯模糊或盒式模糊上采样合成从最小级别开始逐级向上合并最终得到泛光图最终合成将泛光图按强度叠加到原HDR图上再走色调映射输出这个流程的核心思想是多级模糊。如果只做一次大半径模糊采样数会爆炸而通过降采样每一级只需要处理很小的纹理模糊半径在原始分辨率下等效放大计算量大幅降低。这也是为什么几乎所有商业引擎的泛光实现都采用类似的多级降采样结构。注意泛光必须在色调映射之前处理因为色调映射会把HDR亮度压缩到LDR范围超过1.0的亮度信息会被截断泛光就失去了物理依据。正确的顺序是HDR场景 → 泛光提取与模糊 → 泛光合成 → 色调映射 → 颜色分级 → 最终输出。2. 核心细节解析与实操要点2.1 亮度阈值与软阈值曲线的设计亮度提取是泛光的第一步也是最影响最终效果的一步。最朴素的做法是硬阈值亮度大于某个值的像素保留否则置零。但硬阈值会导致泛光边缘出现明显的“切割感”亮部区域和暗部区域之间没有过渡看起来很不自然。实际项目中更常用的是软阈值曲线。它的思路是在阈值附近设置一个过渡区间亮度低于下限的完全剔除高于上限的完全保留中间区域做平滑插值。常见的实现方式有两种线性软阈值contribution saturate((brightness - threshold) / softKnee)曲线软阈值用平滑步进函数或指数函数做过渡我个人的经验是对于写实风格项目软阈值区间设置为阈值的20%到50%比较合适对于风格化项目可以适当加大过渡区间让泛光更柔和。阈值本身需要根据场景的平均亮度来定不能拍脑袋。一个实用的方法是在场景中放置一个标准白球用不同强度的光照照射观察它在什么亮度值下开始产生可见泛光以此作为阈值参考。还有一个容易被忽略的点亮度计算方式。直接用RGB最大值、平均值还是加权亮度人眼对绿色最敏感对蓝色最不敏感所以用加权亮度如0.2126R 0.7152G 0.0722B更符合感知。但如果项目有特殊需求比如想让红色泛光更明显也可以调整权重。2.2 降采样链的级数与分辨率选择降采样链的级数决定了泛光的最大影响半径。假设原始分辨率为1920×1080第一级降采样到960×540第二级480×270以此类推。每一级的模糊结果在最终合成时对应不同的泛光半径降采样级别分辨率等效泛光半径适用场景0级原始1920×1080极小半径高光锐利边缘1级960×540小半径灯泡、小光源2级480×270中半径窗户、中等发光体3级240×135大半径太阳、大面积光晕4级120×67极大半径全局氛围光级数不是越多越好。每增加一级就多一次降采样、一次模糊、一次上采样带宽和计算量都会增加。对于移动端项目通常3到4级就够了PC和主机端可以用5到6级。关键是要根据场景中发光物体的实际尺寸来定——如果场景里最大的发光体只占屏幕的十分之一那用4级降采样就足够了再多就是浪费。实操心得在调试阶段可以把每一级降采样的结果单独输出到屏幕上观察。如果某一级的模糊结果几乎全是黑色说明这一级对应的亮度信息太少可以考虑减少级数或调整阈值。2.3 模糊核的选择与分离式高斯模糊模糊是泛光效果的核心计算。理论上高斯模糊能产生最自然的过渡但二维高斯模糊的采样数是O(n²)直接做代价太高。实际项目中普遍采用分离式高斯模糊先做水平方向的一维模糊再做垂直方向的一维模糊采样数降到O(2n)。对于泛光这种对精度要求不是极端高的场景甚至可以进一步简化。很多引擎用的是盒式模糊的多次迭代来近似高斯模糊。比如做三次盒式模糊每次半径不同最终效果和高斯模糊非常接近但每次盒式模糊只需要两个采样点当前像素左右或上下各一个计算量极低。具体选择哪种方案取决于你的性能预算和画质要求分离式高斯模糊画质最好适合PC和主机端采样数通常为9到13个盒式模糊多次迭代性能最优适合移动端和VR采样数固定为2双边上采样在上采样阶段使用可以避免泛光在深度不连续处产生光晕渗漏我实测下来对于大多数项目4级降采样 每级两次盒式模糊的组合已经能提供非常好的视觉效果而且在移动端也能跑到满帧。如果追求极致画质可以把盒式模糊换成5采样或7采样的高斯近似。2.4 上采样合成与泛光强度控制上采样阶段是把降采样链中每一级的模糊结果逐级合并回去。常见的做法是从最小级别开始将其上采样后与上一级的模糊结果相加再继续向上合并直到回到原始分辨率。这个过程可以用一个权重系数来控制每一级的贡献度。这里有一个关键细节上采样时的滤波方式。如果直接用最近邻采样会出现明显的块状伪影用双线性采样会好很多但在深度边缘处仍然会有光晕渗漏。更高级的做法是双边上采样即在上采样时参考深度或亮度信息只对相似像素进行插值避免前景物体的泛光“漏”到背景上。泛光强度控制通常有两个参数全局强度和每级权重。全局强度控制最终泛光图叠加到原图时的比例每级权重控制不同半径的泛光贡献。比如想让小半径泛光更明显就提高1级和2级的权重想让大范围氛围光更强就提高4级和5级的权重。注意泛光强度不宜过高。我见过一些项目为了追求“炫酷”效果把强度拉到2.0以上结果画面亮部完全失去细节看起来像蒙了一层白雾。一般来说全局强度在0.5到1.5之间比较合理具体要看场景的亮度分布和美术风格。3. 实操过程与核心环节实现3.1 渲染目标管理与内存布局泛光后处理需要多个渲染目标来存储中间结果。以4级降采样为例至少需要1张亮度提取图原始分辨率或半分辨率4张降采样模糊图逐级减半1张最终泛光合成图原始分辨率或半分辨率这些渲染目标的格式选择很重要。如果场景是HDR的中间结果也需要用HDR格式如R11G11B10或R16G16B16A16否则亮度信息会被截断。但HDR格式的带宽开销更大所以降采样级别越低越可以考虑用更紧凑的格式。内存布局上我习惯把同一级别的水平模糊和垂直模糊结果放在两张不同的纹理里避免读写冲突。如果引擎支持可以使用纹理数组或别名渲染目标来减少内存占用和切换开销。// 伪代码渲染目标创建 struct BloomRenderTargets { Texture2D brightness; // 亮度提取结果 Texture2D downsampled[5]; // 降采样链 Texture2D blurredH[5]; // 水平模糊结果 Texture2D blurredV[5]; // 垂直模糊结果 Texture2D bloomResult; // 最终泛光图 };3.2 亮度提取着色器的完整实现亮度提取着色器负责从场景颜色中筛选出需要泛光的像素。下面是一个典型的实现// 亮度提取 float4 BrightnessExtractPS(float2 uv : TEXCOORD0) : SV_Target { float3 color sceneColor.Sample(samplerLinear, uv).rgb; // 加权亮度计算 float brightness dot(color, float3(0.2126, 0.7152, 0.0722)); // 软阈值曲线 float soft brightness - threshold softKnee; soft clamp(soft, 0.0, 2.0 * softKnee); soft soft * soft / (4.0 * softKnee 0.00001); float contribution max(soft, brightness - threshold) / max(brightness, 0.00001); return float4(color * contribution, 1.0); }这段代码的关键在于softKnee参数它控制过渡区的宽度。softKnee越大过渡越平滑但泛光范围也会略微扩大。我一般从阈值的一半开始调然后根据实际效果微调。还有一个细节是否保留颜色信息。有些项目为了性能只提取亮度值做灰度泛光最后合成时再乘以一个全局颜色。但这样会丢失不同颜色光源的泛光差异比如红色霓虹灯和蓝色霓虹灯的泛光应该有不同的色调。所以我的建议是保留RGB颜色只在亮度计算时用加权值。3.3 降采样与模糊的循环实现降采样和模糊通常在一个循环里完成。每一级的处理流程是从上一级纹理降采样到当前级对当前级做水平模糊对当前级做垂直模糊将模糊结果存入当前级的最终纹理// 伪代码降采样与模糊循环 for (int i 0; i levelCount; i) { // 降采样 if (i 0) { Downsample(brightnessTexture, downsampled[0]); } else { Downsample(downsampled[i-1], downsampled[i]); } // 水平模糊 BlurHorizontal(downsampled[i], blurredH[i], blurRadius[i]); // 垂直模糊 BlurVertical(blurredH[i], blurredV[i], blurRadius[i]); }模糊半径的选择有一个经验公式每一级的模糊半径应该大致等于该级分辨率下的一个固定像素数比如2到4个像素。这样在原始分辨率下每一级对应的等效半径就是2^i * radius形成自然的半径递增。实操心得在移动端可以把水平模糊和垂直模糊合并到一个着色器里用两个pass完成减少渲染目标切换。另外如果引擎支持计算着色器用compute shader做模糊可以利用共享内存性能提升非常明显。3.4 上采样合成与最终叠加上采样合成的目标是把所有级别的模糊结果合并成一张泛光图。常见的做法是从最小级别开始逐级向上合并// 上采样合成 float4 UpsampleCompositePS(float2 uv : TEXCOORD0) : SV_Target { float4 lower lowerLevel.Sample(samplerLinear, uv); float4 current currentLevel.Sample(samplerLinear, uv); // 按权重合并 return current lower * upsampleWeight; }upsampleWeight控制低级别大半径泛光的贡献。如果想让大范围氛围光更强就提高这个权重如果想让泛光更集中在小半径就降低它。最终叠加阶段把泛光图按全局强度加到原HDR图上float3 finalColor sceneColor bloomResult * bloomIntensity;然后再走色调映射和颜色分级。这里有一个细节泛光图是否参与色调映射。如果泛光图是在色调映射之前叠加的那它会和场景颜色一起被压缩亮部不会过曝。如果是在色调映射之后叠加泛光会直接加到LDR颜色上容易导致过曝。所以正确的做法是在色调映射之前叠加。4. 常见问题与排查技巧实录4.1 泛光效果不明显或过曝的排查思路泛光效果不符合预期通常有以下几个原因问题现象可能原因排查方法泛光几乎看不见阈值过高、强度过低、场景亮度不足降低阈值到0.5以下测试提高强度到2.0观察泛光过曝成白块阈值过低、强度过高、软阈值区间太窄提高阈值降低强度增大softKnee泛光边缘有硬切割使用了硬阈值没有软阈值过渡检查亮度提取着色器确保有softKnee泛光颜色不对亮度计算权重不对或丢失了颜色信息检查是否保留了RGB调整亮度权重泛光在物体边缘渗漏上采样没有做双边滤波引入深度或亮度参考改用双边上采样我踩过最坑的一次是场景用了自定义的色调映射曲线把亮度压缩得很厉害导致泛光提取时几乎所有像素都低于阈值。后来把泛光提取移到色调映射之前问题立刻解决。所以管线顺序一定要反复确认。4.2 性能瓶颈定位与优化手段泛光后处理的性能开销主要在三个方面带宽、填充率、采样数。定位瓶颈的方法很简单用GPU profiler逐个pass看耗时哪个pass最慢就优化哪个。常见的优化手段降低降采样级别数从5级降到4级性能提升约20%使用更紧凑的纹理格式R11G11B10比R16G16B16A16省一半带宽合并模糊pass把水平和垂直模糊合并到一个着色器减少渲染目标切换使用计算着色器利用共享内存做模糊性能提升2到3倍动态分辨率在性能紧张时降低泛光图的分辨率画质损失很小注意不要盲目追求高性能而牺牲画质。泛光是一个视觉增强效果如果因为优化导致泛光出现明显伪影或闪烁反而得不偿失。我的原则是在目标平台上稳定跑满帧的前提下尽量保证画质。4.3 不同平台和引擎的适配经验不同平台对泛光后处理的限制不同移动端带宽是最大瓶颈建议用3级降采样盒式模糊R11G11B10格式PC端可以用5到6级降采样分离式高斯模糊R16G16B16A16格式主机端介于两者之间可以用计算着色器做优化VR端对延迟极其敏感泛光半径要小避免使用大范围模糊在引擎适配方面如果用的是商业引擎通常已经内置了泛光后处理但默认参数往往不适合所有项目。我的做法是先关掉引擎自带的泛光自己实现一套这样可以完全控制参数和管线顺序。如果时间紧张也可以在引擎自带的基础上修改但要注意它的亮度提取和色调映射顺序是否合理。还有一个容易被忽略的点泛光和抗锯齿的交互。如果项目用了TAA或FXAA泛光可能会在抗锯齿之后处理导致泛光边缘出现锯齿。正确的顺序是场景渲染 → 泛光 → 抗锯齿 → 色调映射。但有些引擎的顺序是反的需要手动调整。4.4 美术风格与参数调优的配合泛光效果最终是为美术风格服务的。写实风格和风格化风格的泛光参数差异很大写实风格阈值偏高1.0到1.5软阈值区间窄泛光强度低0.5到0.8半径小强调物理准确性风格化风格阈值偏低0.5到0.8软阈值区间宽泛光强度高1.0到1.5半径大强调视觉冲击力像素风格阈值极高泛光只对极亮像素生效半径极小避免破坏像素感我通常会让美术提供几张典型场景的截图然后在引擎里实时调整参数直到美术满意为止。这个过程需要反复迭代不能指望一次调好。另外建议把泛光参数做成可配置的不同场景可以用不同的预设比如室内场景和室外场景的泛光需求完全不同。5. 进阶扩展与个人经验分享5.1 基于物理的泛光与能量守恒如果项目追求物理准确性泛光应该遵循能量守恒原则泛光只是把亮部的能量重新分配到周围像素不应该增加总能量。这意味着泛光图的亮度总和应该等于提取出的亮部能量总和。实际实现时可以通过归一化模糊核来近似满足这个条件。更高级的做法是引入镜头散射模型模拟真实镜头的光学特性。不同镜头的光晕形状不同有的呈圆形有的呈星形有的有彩虹色散。这些效果可以通过在模糊阶段使用不同的核函数来实现。不过对于大多数项目来说标准的分离式高斯模糊已经足够物理精确的泛光更多用于影视级渲染。5.2 泛光与其它后处理效果的组合泛光很少单独使用它通常和以下效果组合色调映射泛光必须在色调映射之前否则亮部信息丢失颜色分级泛光之后做颜色分级可以统一调整整体色调景深泛光和景深的顺序会影响效果通常景深在前泛光在后运动模糊泛光在运动模糊之前避免泛光被模糊拖尾这些效果的顺序没有绝对标准取决于项目需求。我的建议是先确定一个基础顺序然后在实际场景中测试观察是否有不自然的交互再逐步调整。5.3 调试工具与可视化方案调试泛光时可视化工具非常重要。我通常会实现以下几个调试视图亮度提取图只看亮度提取的结果确认阈值是否合理各级模糊图单独显示每一级降采样的模糊结果确认半径是否合适泛光图只看泛光合成结果确认强度是否合适最终合成图看最终效果确认整体是否自然这些调试视图可以通过一个快捷键切换方便快速对比。另外建议在屏幕上显示当前泛光参数阈值、强度、级数等方便调整时参考。5.4 一个实际项目的参数配置参考最后分享一个我在实际项目中使用的参数配置场景是户外写实风格目标平台是PC和主机参数值说明阈值1.2根据场景平均亮度调整软阈值区间0.6阈值的50%降采样级数51920×1080到120×67模糊半径3像素每级固定模糊迭代2次盒式近似高斯全局强度0.8叠加到原图的比例上采样权重0.7低级别贡献纹理格式R11G11B10平衡画质和带宽这套参数在目标平台上稳定跑满60帧泛光效果自然亮部细节保留良好。当然这只是一个参考具体项目还需要根据实际情况调整。最后再分享一个小技巧如果场景中有大量小发光体比如粒子特效可以单独为它们做一层低分辨率的泛光然后和主泛光图合并。这样可以在不增加主泛光级数的情况下让小发光体也有明显的泛光效果性能开销很小。