Chapter 65: Post-Processing Pipelines
后处理管线处理一帧中已经形成的图像信号。读完本章后,读者应能追踪一帧从 HDR scene color 到 final backbuffer 的路径,判断 tone mapping、Bloom、景深、UI composite 和显示色彩空间各自应处在什么位置,并能用资源状态、pass 数量、采样次数和带宽证据排查画面问题与性能问题。
本章贯穿一个实时渲染器中的单帧:主场景先渲染到 RGBA16F 的 HDR scene color,同时保留 depth、velocity、exposure buffer 和若干历史纹理;后处理阶段生成 Bloom、景深、色调映射、LUT 色彩校正、UI 合成,最后写入 swapchain backbuffer。这个例子覆盖大多数游戏、可视化和实时预览工具中的后处理路径。
后处理的主结论是:它是一条图像信号处理链。链条的输入纹理、颜色空间、执行顺序、临时资源生命周期和最终输出格式共同决定视觉结果。把某个效果看成“屏幕上叠一层滤镜”会丢失关键约束;真正需要追踪的是每个 pass 读取哪张纹理、写入哪张纹理、在哪个分辨率下运行、是否依赖上一帧历史,以及输出值是否仍处在线性 HDR 空间。
工程上最稳定的做法是把后处理当成 frame graph 的一组 pass 来设计。每个 pass 声明输入、输出、格式、分辨率、采样模式、是否可降级和调试输出。这样才能把 Bloom 过曝、UI 颜色不一致、景深边缘漏光、TAA ghosting、移动平台带宽过高这类现象放回同一条管线中复盘。
65.1 后处理管线的组成与执行时机
后处理管线从主渲染结果开始,而主渲染结果通常是一张 HDR scene color。HDR scene color 存储场景光照、材质、透明物体或部分透明层合成后的结果,常见格式包括 RGBA16F、R11G11B10F 和更高精度的调试格式。它保留超过显示白点的亮度,给 exposure、Bloom 和 tone mapping 留出数值空间。
一帧后处理常见输入可以按用途分成五类。第一类是颜色输入,例如 HDR scene color、上采样后的反射或体积光结果。第二类是几何辅助输入,例如 depth、normal、motion vector、stencil 或 object id。第三类是时间输入,例如 history color、previous exposure、previous velocity 或 temporal accumulation buffer。第四类是控制输入,例如 exposure、white point、focus distance、quality tier 和调试开关。第五类是最终输出目标,例如 offscreen final color、UI composite target 和 swapchain backbuffer。
下面的图只表达一条典型路径。实际项目会把 TAA、FSR/DLSS/XeSS、透明合成、字幕和平台 compositor 插入不同位置,但核心判断仍然围绕颜色空间、资源读写和执行顺序展开。
图中的关键路径是 HDR Scene Color → Pre-Tonemap Composite → Tone Mapping → Color Grading → UI Composite → Final Backbuffer。Bloom 通常从 tone mapping 前的 HDR 图像提取高亮,因为阈值、模糊和上采样混合需要使用线性光照强度。UI 常放在 tone mapping 后合成,因为大部分 UI 资源以 SDR 贴图、字体 atlas 和屏幕空间颜色定义为基础。HDR UI 应独立定义显示映射策略,直接沿用 SDR 合成假设会造成亮度和白点错位。
执行时机的第一条约束来自颜色空间。tone mapping 前的 pass 应在 scene-linear 或项目定义的工作色彩空间中运行;tone mapping 后的 pass 应接受已经压缩到显示范围的颜色。Bloom、镜头眩光、自动曝光和景深通常读取 HDR 或线性颜色。色彩校正 LUT 和胶片颗粒常接近输出阶段。UI、鼠标指针、调试文字和平台 overlay 常在显示映射之后处理。
执行时机的第二条约束来自 depth 和 velocity 的可用性。景深、motion blur、TAA 和某些屏幕空间效果需要 depth 或 velocity。depth 在后处理阶段通常以 shader resource view 读取;在显式 API 中,这意味着 depth attachment 的写入阶段已经结束,并且资源状态要转到只读采样状态。velocity buffer 需要和当前帧的投影矩阵、上一帧矩阵、skinned mesh 更新和 jitter 策略一致;任何一个输入错位都会把 temporal pass 的错误放大成拖影或边缘闪烁。
执行时机的第三条约束来自 backbuffer。swapchain backbuffer 是呈现引擎接收的最终图像目标,它的格式、颜色空间和呈现模式由平台决定。后处理链可以先写入一个 offscreen final color,再在最后一次 pass 中转换到 backbuffer。这样做让调试、截图、动态分辨率和多平台输出更稳定。直接把多个效果写进 backbuffer 会让 resource barrier、调试输出和 UI 合成顺序变得脆弱。
一个可复用的资源视角是把后处理看成“读旧纹理、写新纹理”的 ping-pong 链。每个 pass 读取上一阶段输出,写入新的临时 render target。模糊类效果会在横向和纵向 pass 之间切换两张纹理。Bloom 会在 downsample chain 与 upsample chain 之间维护多级 mip 或多张半分辨率纹理。frame graph 的职责是根据依赖关系分配资源、插入 barrier、复用生命周期不重叠的临时纹理,并在调试模式下保留可观察中间结果。
定位后处理顺序错误时,先看当前问题出现在 tone mapping 前还是 tone mapping 后。Bloom 亮度异常、景深边缘漏光、motion blur 拖影通常发生在 tone mapping 前或 temporal pass 内。UI 过亮、截图颜色和窗口颜色不一致、HDR 显示发灰通常发生在 tone mapping 后、LUT 后或 backbuffer color space 阶段。把问题归入前半段或后半段,能显著缩小排查范围。
65.2 色调映射(Tone Mapping)与色彩校正
色调映射(Tone Mapping)把场景线性 HDR 亮度压缩到目标显示范围。它的输入是 scene-referred color,也就是由灯光、材质和曝光模型产生的场景颜色;它的输出是 display-referred color,也就是面向显示设备、截图或视频编码的颜色。色调映射解决的问题是显示设备动态范围有限,而渲染场景中可能同时出现太阳、高光、暗部、发光材质和室内间接光。
exposure 是 tone mapping 前的亮度缩放。自动曝光通常从 luminance histogram、log average luminance 或分块亮度统计得到当前帧目标曝光,再通过 temporal smoothing 平滑变化。曝光改变的是 HDR scene color 进入 tone mapping curve 前的尺度;它会影响 Bloom 阈值、白点压缩和暗部噪声可见度。项目应明确 Bloom 阈值使用曝光前数值还是曝光后数值,否则昼夜切换时 Bloom 会出现不稳定的强弱跳变。
white point 是把高亮压缩到显示白附近的控制点。它作为 tone mapping curve 的亮度边界参与压缩,单独把它当颜色校正旋钮会打乱高亮层次。white point 过低会把大量高亮压进白色,画面失去材质层次;white point 过高会让 SDR 输出显得灰、暗或缺乏对比。调试 white point 时应同时观察高亮渐变、肤色、云层、金属反射和 UI 叠加后的整体亮度。
常见 tone mapping curve 包括 Reinhard、filmic、ACES 风格 curve 和项目自定义 curve。ACES 在影视和色彩管理中有完整的 transform 体系,实时渲染中常用的是近似 filmic curve 或基于 ACES look 的工程化曲线;真正的 ACES 输出变换涉及更完整的色彩空间和显示设备映射。工程文档中应把“ACES 风格 curve”和完整 ACES 色彩管理区分清楚,ACES Core Transforms 提供了可回溯的参考实现入口。
下面的 HLSL 片段展示一个简化的后处理组合。它用于说明 exposure、tone mapping、LUT 和输出编码的相对位置,不承担复刻某个标准的任务。
float3 ApplyExposure(float3 sceneColor, float exposure)
{
return sceneColor * exposure;
}
float3 FilmicCurve(float3 color)
{
float3 x = max(color, 0.0.xxx);
float3 a = x * (2.51 * x + 0.03);
float3 b = x * (2.43 * x + 0.59) + 0.14;
return saturate(a / b);
}
float3 PostToneMap(float3 sceneColor, float exposure)
{
float3 exposed = ApplyExposure(sceneColor, exposure);
float3 displayLinear = FilmicCurve(exposed);
float3 graded = ApplyColorGradingLut(displayLinear);
return LinearToOutputTransfer(graded);
}
代码中的 ApplyExposure 仍然工作在线性 HDR 值上;FilmicCurve 把亮度压缩到显示范围;ApplyColorGradingLut 接收已经进入显示侧的线性颜色;LinearToOutputTransfer 决定写入 backbuffer 前的传递函数。这个顺序提供了一个调试基准:如果 LUT 前后的灰阶已经错位,问题在 tone mapping 或 LUT 输入范围;如果窗口显示和截图不一致,问题更可能在 output transfer、swapchain format 或平台截图路径。
色彩校正通常由矩阵、曲线和 LUT 组成。矩阵适合做白平衡、颜色空间转换和简单饱和度控制。曲线适合做对比度、暗部提升和高光 roll-off。3D LUT 适合表达复杂 look,例如电影色调、冷暖分区和艺术指导风格。LUT 的输入范围必须和生成 LUT 时的假设一致;一个面向 display-linear 的 LUT 直接读取 HDR scene color,会把大部分数值挤到查找表边界。
显示色彩空间是后处理链的最后一层约束。Windows 的 DXGI_COLOR_SPACE_TYPE 列举了 sRGB、scRGB、BT.2020、PQ 和 HLG 等输出定义,Microsoft DXGI 文档 展示了这些枚举如何描述 range、transfer 和 primaries。Vulkan 的 VkColorSpaceKHR 则描述 presentation engine 接收的 swapchain color space,Vulkan reference page 列出了 sRGB、Display P3、BT.2020 和 HDR10 ST2084 等值。工程上要把 tone mapping 输出、backbuffer format、swapchain color space 和显示器模式放在同一张检查表里。
排查 tone mapping 与色彩校正问题时,顺序应固定。先输出曝光前后的 luminance debug view,确认 HDR 值范围;再关闭 Bloom、景深、胶片颗粒等干扰 pass,只保留 tone mapping;随后切换 neutral LUT,确认 LUT 输入范围;最后检查 swapchain format、颜色空间和 OS HDR 开关。这个顺序把场景光照、显示映射、艺术 LUT 和平台输出分开,能减少把色彩问题误归因到 shader 材质的概率。
65.3 模糊/Bloom/景深等常见效果实现
模糊类后处理的共同点是邻域采样。一个输出像素会读取周围多个输入像素,采样半径、权重、分辨率和边界处理决定视觉结果与带宽成本。模糊不应只按“效果名称”分类;更有用的分类是采样范围、输入纹理、是否使用深度、是否需要时间历史,以及输出是否参与后续 tone mapping。
Bloom 的典型路径是 threshold、downsample、blur 或 filter、upsample blend。threshold 从 HDR scene color 中提取高亮区域;downsample chain 把高亮图像逐级降分辨率;每一级执行小半径 filter;upsample 时把低频光晕逐级叠回高分辨率。这个结构把大半径模糊转化成多级低分辨率处理,降低采样次数和带宽,同时保留大范围扩散感。
Bloom 阈值需要和 exposure 策略绑定。固定阈值作用在曝光前 HDR 值上,会让发光材质在不同曝光下保持物理尺度;固定阈值作用在曝光后值上,会让视觉亮度更接近最终画面。两种做法都能成立,但项目要固定选择,并给美术调参工具显示同一条数值语义。混用阈值空间会导致室内外切换时高光忽强忽弱。
separable blur 把二维卷积分解成横向 pass 与纵向 pass。一次半径为 r 的二维采样需要约 (2r + 1) * (2r + 1) 次读取;分离后只需要 2 * (2r + 1) 次读取。这个变化直接降低 texture fetch 数量,也让中间结果可以写入半分辨率纹理。代价是它只适合可分离的核,例如高斯类模糊;某些形状化散景、镜头污渍和星芒效果需要专门的核或预计算纹理。
景深(Depth of Field)把焦点外区域扩散成模糊盘。它的核心输入是 depth 和相机参数,核心中间量是 circle of confusion(CoC)。CoC 表示当前像素相对焦平面的模糊半径。近景虚化和远景虚化常需要分别处理,因为近处物体的模糊会覆盖背景,远处背景的模糊通常受前景边缘遮挡。
下面的简化 HLSL 片段展示 CoC 的计算位置。它只表达管线关系,实际项目要根据相机模型、深度线性化方式和艺术控制项调整公式。
float ComputeCoc(float linearDepth, float focusDistance, float focusRange, float maxRadius)
{
float distanceFromFocus = abs(linearDepth - focusDistance);
float normalized = saturate(distanceFromFocus / max(focusRange, 0.001));
return normalized * maxRadius;
}
float4 DepthOfFieldPass(float2 uv)
{
float depth = LinearizeDepth(DepthTexture.Sample(PointSampler, uv).r);
float coc = ComputeCoc(depth, FocusDistance, FocusRange, MaxCocRadius);
float3 blurred = GatherWeightedColor(SceneColor, uv, coc);
return float4(blurred, coc);
}
代码中的关键点是 DepthTexture 和 SceneColor 的同步语义。depth 必须和当前 SceneColor 对应同一帧、同一 jitter 状态和同一分辨率映射。动态分辨率、TAA jitter、透明物体和 alpha-tested 几何都会改变边缘语义。景深边缘漏光通常来自 depth 与 color 的覆盖关系不一致,或者 near/far blur 合成时没有处理前景遮挡。
motion blur 与景深类似,都把一个像素扩展到邻域采样,但 motion blur 的采样方向来自 velocity。velocity buffer 表达当前像素从上一帧到当前帧的屏幕位移。相机运动、物体运动、skinning、粒子和透明物体可能有不同的 velocity 生成策略。motion blur 的失败信号是物体边缘拖影、HUD 被拖动、镜头切换时整屏糊掉;这些现象应先检查 velocity 的写入对象、reset 策略和 UI 合成位置。
胶片颗粒、vignette、chromatic aberration 和锐化通常位于 tone mapping 后。它们的目标是塑造最终显示图像,作用范围不进入 HDR 光照计算。锐化如果放在上采样或 TAA 后,需要同时观察边缘振铃、文字可读性和噪声放大。chromatic aberration 会改变不同颜色通道的采样位置,调试时应提供强度为零的 neutral path,保证截图和 QA 能快速确认它是否参与问题。
常见后处理效果之间存在顺序依赖。Bloom 通常在 tone mapping 前合成,景深可以在 Bloom 前或后,取决于项目希望发光体虚化后扩散,还是先扩散再被景深处理。motion blur 常在 tone mapping 前或 temporal resolve 附近处理,以便使用线性颜色和 velocity。色彩校正通常接近输出端。UI 叠加一般位于这些镜头效果之后;当 UI 也要参与景深或 motion blur 时,应为 UI 提供单独 mask 和明确的 pass 位置。
65.4 构建可配置后处理架构
可配置后处理架构的目标是把效果链从硬编码顺序变成可检查的 pass 图。frame graph 是一种常见组织方式:每个 pass 声明读取资源、写入资源、执行阶段、分辨率比例、格式和依赖,系统根据这些声明分配临时资源、插入同步、决定执行顺序,并为调试工具提供 pass 名称和中间纹理。
一个后处理 pass descriptor 至少要包含四类信息。资源信息描述输入输出纹理、采样器、格式和 resource state。执行信息描述 shader、pipeline state、thread group 或 full-screen draw。质量信息描述分辨率比例、采样次数、半径、是否启用 temporal reuse。调试信息描述 pass 名称、debug view、开关和可热更新参数。缺少其中任一类,效果都很难稳定扩展到多平台和多质量等级。
下面的伪代码表达一个 Bloom pass 组的声明方式。它省略了具体 API 绑定,只保留 frame graph 需要理解的依赖关系。
struct PostPassDesc {
const char* name;
TextureHandle input;
TextureHandle output;
float resolutionScale;
PixelFormat format;
ShaderHandle shader;
bool allowAsyncCompute;
bool exposeDebugView;
};
void BuildBloomPasses(FrameGraph& graph, TextureHandle hdrSceneColor)
{
TextureHandle bright = graph.CreateTexture("BloomBright", HalfRes, RGBA16F);
graph.AddFullscreenPass({
"BloomThreshold",
hdrSceneColor,
bright,
0.5f,
RGBA16F,
LoadShader("bloom_threshold"),
false,
true
});
TextureHandle lowMip = graph.CreateTexture("BloomLowMip", QuarterRes, RGBA16F);
graph.AddFullscreenPass({
"BloomDownsample",
bright,
lowMip,
0.25f,
RGBA16F,
LoadShader("bloom_downsample"),
true,
true
});
}
这段声明让架构获得三条信息:BloomThreshold 依赖 HDR scene color,BloomDownsample 依赖 threshold 输出,两者都可以暴露 debug view。真实引擎会继续声明多级 downsample、upsample 和最终 composite。frame graph 可以发现 BloomBright 在 upsample 结束后生命周期结束,从而把它的内存复用于后续 vignette、DOF intermediate 或截图缩略图。
resource lifetime 是后处理架构能否扩展的关键。大量后处理 pass 会创建临时纹理,如果每个效果独占资源,内存占用会随效果数量线性增长。frame graph 可以根据最后一次读写时间做 aliasing,让生命周期不重叠的临时纹理复用同一块底层内存。显式 API 还需要 resource barrier 或 layout transition,确保一个 pass 的 render target 输出在下一个 pass 中以 shader resource 方式读取。
quality tier 应绑定到资源规模和采样规模,而不仅是开关。低档 Bloom 可以减少 mip 层数、使用更低分辨率、降低上采样核;低档景深可以降低最大 CoC 半径、关闭 near blur 或降低散景质量;低档 motion blur 可以限制 sample count 和最大 velocity。把 tier 写成 pass 参数,能让同一套效果在桌面、移动、VR 和云端串流中共享架构。
debug toggle 要能回答具体问题。Bloom debug view 应展示 threshold mask、每级 mip 和最终 composite 权重。景深 debug view 应展示 linear depth、CoC、near/far mask 和合成结果。tone mapping debug view 应展示 exposure、luminance histogram、LUT neutral path 和输出 transfer 前后的灰阶。调试输出的价值在于缩短定位路径,截图按钮数量本身不构成有效调试能力。
hot reload 适合修改 shader、常量、LUT、曲线和效果参数。它要求 pass descriptor 和资源布局保持稳定,shader 编译失败时保留上一版可运行 pipeline。修改资源格式、增加新输入纹理、改变 thread group 大小或改变 async compute 依赖时,热更新系统应触发更严格的重建和校验。这样能保护运行中的渲染器,防止一个局部 shader 改动破坏整条后处理链。
跨 API 架构要把平台差异限制在 backend 层。Direct3D、Vulkan、Metal 和 WebGPU 对 resource state、render pass、bind group 或 descriptor 的表达不同,但后处理 pass 的逻辑依赖相同:读取纹理、写入纹理、采样、合成、转到 backbuffer。架构层应使用统一的 texture handle、pass name、format 和 usage 描述;backend 层负责把这些描述映射到具体 API 的 barrier、view、sampler 和 pipeline state。
65.5 效果预算与性能调整方法
后处理性能主要受分辨率、采样次数、pass count、render target 带宽、同步和历史复用影响。它常表现为 GPU 时间增加、移动平台功耗上升、VR 帧期限超时、动态分辨率频繁下降或截图路径卡顿。预算分析应从“每个 pass 读写多少像素和多少字节”开始,再看 shader ALU、texture fetch、barrier 和 async compute overlap。
最简单的带宽估算是 pixels * bytesPerPixel * readWriteCount。一张 2560×1440 的 RGBA16F 纹理约有 3.69 百万像素,每个像素 8 字节。一个读取一次并写入一次的全分辨率 pass,理论读写量约为 3.69M * 8 * 2 = 59MB,还未计算缓存行为、多次采样、tile flush、resolve 和额外历史纹理。多 pass 后处理的成本很容易被 render target 带宽主导。
分辨率是第一预算杠杆。Bloom、ambient glare、vignette、部分 blur 和部分 volumetric composite 可以在半分辨率或四分之一分辨率运行。景深、motion blur 和 TAA 与边缘、velocity、depth 对齐关系更强,降分辨率需要配合 depth-aware upsample 或 bilateral filter。锐化、LUT、tone mapping 和 UI composite 通常保持全分辨率,因为它们直接影响最终像素和文字边缘。
采样次数是第二预算杠杆。大半径 blur、景深散景、motion blur 和高质量 Bloom 都会扩大采样范围。优化顺序应先降低采样所在分辨率,再使用 separable 或 mip chain 结构,随后减少最大半径或 sample count,最后再考虑更激进的 temporal reuse。直接压低采样数会优先暴露 banding、闪烁、圆形散景破碎和边缘噪声。
pass count 是第三预算杠杆。多个全屏 pass 之间每次写回 render target 都会产生带宽和同步成本。tone mapping、LUT、vignette、grain 和 simple color transform 可以合并到一个 full-screen pass。Bloom threshold 和 downsample 有时可以合并到第一层降采样。景深、motion blur 和 TAA 若共享 velocity、depth 和历史纹理,也应统一调度,减少重复读取。
temporal reuse 能把高成本采样分摊到多帧。TAA、Bloom 稳定化、volumetric history、SSR history 和景深历史都属于这一类。它的成本是历史纹理、motion vector、reprojection、history rejection 和 ghosting 处理。任何 temporal reuse 都需要 reset 规则:镜头切换、FOV 变化、分辨率变化、曝光突变、场景切换和 UI 覆盖变化都可能让上一帧历史失效。
平台等级决定预算边界。桌面高端 GPU 可以承受多级 HDR Bloom、全分辨率 color grading 和复杂 DOF;移动 tile-based GPU 对 render target 写回、格式带宽和 tile memory 压力更敏感;VR 对延迟和帧稳定性更严格,后处理应保护 per-eye 分辨率、畸变前后顺序和 UI 可读性;云端串流还要考虑编码前锐化、码率噪声和截图/录制分支。
下表给出一组可迁移的调整维度。它用于排查时排序,不代表固定配方。
| 现象 | 先检查对象 | 常用调整动作 | 需要观察的副作用 |
|---|---|---|---|
| GPU 后处理时间过高 | 全分辨率 pass 和 RGBA16F 写回 | 合并 tone mapping/LUT/grain,降低 Bloom 或 blur 分辨率 | 边缘清晰度、光晕稳定性 |
| Bloom 过亮或跳变 | threshold 空间、exposure 顺序 | 固定阈值语义,显示 threshold debug view | 昼夜切换、高亮材质层次 |
| 景深边缘漏光 | depth、CoC、near/far mask | 使用 depth-aware 合成,限制 near blur 半径 | 前景轮廓、透明物体边缘 |
| motion blur 拖影 | velocity buffer、history reset | 限制最大 velocity,镜头切换清空历史 | 快速运动物体、HUD |
| HDR 输出发灰 | tone mapping 输出、swapchain color space | 检查 output transfer 和 backbuffer 设置 | 截图、窗口显示、录屏 |
性能调试要建立固定顺序。第一步,打开 GPU timestamp,得到每个后处理 pass 的耗时。第二步,记录每个 pass 的分辨率、格式、输入数量和输出数量。第三步,关闭单个效果,确认视觉收益与耗时之间的比例。第四步,把候选 pass 降到半分辨率或合并到相邻 pass,观察画面差异。第五步,检查 resource barrier、layout transition 和 async compute overlap,确认 GPU 时间是否被同步等待拉长。
一个后处理效果进入正式质量档前,应同时通过视觉检查和资源检查。视觉检查关注亮度范围、边缘、时间稳定性、UI 可读性和截图一致性。资源检查关注 pass 数量、临时纹理总尺寸、格式、读写次数、历史纹理和平台 fallback。只有两组检查都成立,效果才算真正接入管线;单独看最终截图容易漏掉高刷新率、HDR 显示和移动平台上的问题。
最小自检任务
给定一帧 2560×1440 的实时渲染输出:主场景写入 RGBA16F HDR scene color,另有 depth、velocity 和 exposure buffer。后处理需求包括 Bloom、景深、tone mapping、3D LUT、UI composite 和 final backbuffer。当前问题是:UI 在 HDR 显示上过亮,Bloom 在室内外切换时强弱跳变,GPU 后处理耗时从 1.8ms 增加到 4.6ms。请设计一条后处理执行顺序,并说明排查这三个问题的步骤。
答案要点
合理顺序可以是:HDR scene color 进入 temporal 或稳定化阶段;Bloom 从 tone mapping 前的 HDR 或曝光后 HDR 中提取 threshold,并执行 downsample 与 upsample;景深读取当前帧 scene color、depth 和 CoC 参数,在 tone mapping 前完成镜头效果;Bloom 与景深结果合成到 pre-tonemap composite;tone mapping 使用 exposure 和 white point 输出 display-linear color;3D LUT 做色彩校正;UI 在 tone mapping 和 LUT 后合成;最后执行 output transfer 并写入 final backbuffer。
UI 过亮应从 tone mapping 后半段排查。先关闭 Bloom、景深和颗粒等干扰 pass,只保留 tone mapping、LUT 和 UI composite。随后确认 UI 资源是否按 SDR 颜色定义进入,检查 UI 是否被错误送入 HDR exposure 或 tone mapping。最后检查 backbuffer format、swapchain color space、OS HDR 状态和截图路径,确认 UI 合成值与最终输出传递函数一致。
Bloom 跳变应从 threshold 空间和 exposure 顺序排查。先输出 threshold mask,观察室内外切换时进入 Bloom 的像素集合是否突变。随后确认阈值作用在曝光前 HDR 值还是曝光后 HDR 值,并让工具面板显示同一套数值语义。若使用 temporal smoothing,还要检查镜头切换和曝光突变时的历史清理策略。
GPU 耗时增加应按分辨率、pass count、采样次数和带宽排查。先用 timestamp 找出最慢 pass,再列出每个 pass 的分辨率、格式、读写纹理数量和采样次数。Bloom 可优先检查 downsample 层数、upsample 核和 RGBA16F 写回次数;景深可检查 CoC 半径、near/far 分支和 gather sample count;tone mapping、LUT、vignette、grain 等输出阶段效果可合并到一次 full-screen pass。每次调整后同时观察高亮层次、边缘漏光、文字清晰度和时间稳定性。
本章知识点总结
- 后处理定位:后处理是一条从 HDR scene color 到 final backbuffer 的图像信号链,核心约束来自输入纹理、颜色空间、执行顺序和输出格式。
- HDR 输入:HDR scene color 保留超过显示白点的亮度,为 exposure、Bloom 和 tone mapping 提供可调数值空间。
- 执行时机:Bloom、景深和 motion blur 多数依赖 tone mapping 前的线性颜色、depth、velocity 或历史纹理。
- UI 合成:SDR UI 通常放在 tone mapping 与 LUT 之后合成,HDR UI 需要单独定义显示映射策略。
- 资源状态:显式 API 中后处理 pass 需要清楚表达 render target 输出、shader resource 读取和 backbuffer 写入之间的状态转换。
- Tone Mapping:tone mapping 把场景线性 HDR 亮度压缩到目标显示范围,exposure 和 white point 决定高亮与暗部的分布。
- 色彩校正:矩阵、曲线和 3D LUT 必须匹配输入范围与工作色彩空间,否则会造成灰阶错位和颜色截断。
- Bloom 链路:Bloom 通过 threshold、downsample、filter 和 upsample blend 把高亮扩散成低频光晕,阈值语义要和 exposure 策略一致。
- 景深输入:景深依赖 depth、CoC 和当前 scene color 的一致性,边缘问题通常来自覆盖关系、分辨率映射或 near/far 合成策略。
- Frame Graph:可配置后处理架构应让每个 pass 声明输入、输出、格式、分辨率、质量档和 debug view。
- 性能预算:后处理预算优先按分辨率、采样次数、pass count、带宽、temporal reuse 和平台等级拆解。
- 排查顺序:先把问题归入 tone mapping 前或后,再用 debug view、timestamp、资源表和平台输出设置确认原因。