1930 字
10 分钟
后处理管线——一帧的最后 2ms

一、后处理是什么#

延迟渲染的 GBuffer Pass 和 Lighting Pass 已经算出了场景的”物理正确”颜色。但物理正确 ≠ 好看。人的眼睛不是照相机——我们感知亮度是对数级的(不是线性的)、我们会在强光下看到光晕、暗部细节会被压缩。

后处理就是把”物理正确的 HDR 数据”转换成”屏幕上好看的 LDR 像素”。

Input: SceneColor HDR (来自 Lighting Pass)
├── Bloom → 亮区光晕
├── Tone Mapping → HDR → LDR
├── Color Grading → 调色(LUT)
├── SSAO → 环境光遮蔽
├── Screen Space Reflections → 屏幕空间反射
├── Motion Blur → 运动模糊
├── DOF → 景深
├── TAA / Upscaling → 抗锯齿 / DLSS
└── UI Render → 叠加 UI
Output: Final LDR Color → Back Buffer → Present

后处理栈的顺序不是随意的。 比如 Tone Mapping 必须在 Bloom 之后(把 HDR 值映射到 LDR 后就没法做 Bloom 了),SSAO 通常放在 Tone Mapping 之前。


二、Bloom——让高亮区发光#

1 原理#

真实世界中,强光源会在我们的眼睛里(和相机镜头里)产生散射光晕。Bloom 模拟这个效果。

1. 取 Scene Color(HDR)
2. 亮度阈值 → 只保留超过阈值(如 1.0)的像素
3. 多次 Downsample(1/2 → 1/4 → 1/8 → ...)
4. 多次 Upsample + Blur → 重建模糊的 Bloom 纹理
5. Blend: FinalColor = SceneColor + Bloom * Intensity

为什么要多次上/下采样而不是一次大 Blur?

一次大 Blur 需要采样半径内所有像素——半径 64 像素的高斯 Blur 在 1080p 下每像素 4096 次采样,太贵。先用 Mini-Mipmap 链降分辨率到 1/8,再在低分辨率做 Blur(便宜),再叠加上来——这就是 Kawase BlurDual Filtering 的思路。

2 UE 中对应的#

Post Process Volume → Lens → Bloom:Intensity、Threshold、Convolution Kernel 等参数。UE 的 Bloom 默认使用多级高斯 Blur。


三、Tone Mapping——HDR 到 LDR#

1 问题#

你的 Lighting Pass 算出的 HDR 颜色范围可能是 0.0 到 100.0(甚至更高——太阳直射的高光点可能上千)。但显示器只能显示 0-255(LDR,8 位色深)。

2 方案#

最简单的(Reinhard Tone Mapping):

float3 ToneMap_Reinhard(float3 hdr) {
return hdr / (1.0 + hdr); // 把 [0, ∞) 软压缩到 [0, 1)
}

亮度低时接近线性(暗处保持细节),亮度极高时软截断(高光变成白色但不刺眼)。

UE 默认(ACES——Academy Color Encoding System):

float3 ToneMap_ACES(float3 hdr) {
// 电影行业标准——模拟电影胶片的色彩响应
float3 a = hdr * (hdr + 0.0245786f) - 0.000090537f;
float3 b = hdr * (0.983729f * hdr + 0.4329510f) + 0.238081f;
return a / b;
}

ACES 的效果是:高光保留色彩倾向(不是纯白色),暗部有柔和的对比度压缩。这个公式被几乎所有 3A 游戏和电影 VFX 使用。


四、SSAO——假环境光遮蔽,真立体感#

1 为什么不需要真 AO#

真环境光遮蔽需要算全局光照——贵。但人眼对 AO 的感知其实主要来自近处物体之间的接触阴影(如墙角、物体和地面的接缝)。屏幕空间的方法够用了。

2 SSAO 的基本思路#

对屏幕上的每个像素,取它周围一小圈随机偏移点,检查这些点在深度缓冲里是否被遮挡:

float CalcSSAO(float2 UV, float3 Pos, float3 Normal) {
float ao = 0.0;
for (int i = 0; i < SAMPLE_COUNT; i++) {
float3 samplePos = Pos + RandomDirection[i] * Radius;
float3 sampleUV = WorldToScreen(samplePos);
float sampleDepth = SceneDepth.Sample(Sampler, sampleUV.xy).r;
float realDepth = samplePos.z;
if (realDepth > sampleDepth + Bias) {
ao += 1.0; // 采样点被遮挡了
}
}
return 1.0 - ao / SAMPLE_COUNT;
}

SSAO 是 “屏幕空间” 的——只能看到深度缓冲里有信息的地方。 物体背后的东西看不到,所以远处的遮挡无法计算。但它足够让墙角、桌子腿等地方变暗,大幅提升立体感。

UE 在 SSAO 基础上演进了 GTAO(Ground Truth Ambient Occlusion)——改进了采样策略和遮挡权重,效果更接近离线烘焙的 AO。


五、TAA(时间抗锯齿)——用时间换画质#

1 延迟渲染为什么需要 TAA#

延迟渲染不支持 MSAA。但锯齿还是要抗的。TAA 的方案是把上一帧的信息用到这一帧

TAA = 这一帧的渲染结果 × 0.5 + 上一帧(经过运动补偿后)的结果 × 0.5

2 核心机制#

float3 TAA(float2 UV, Texture2D CurrentFrame, Texture2D History, Texture2D Velocity) {
float2 velocity = Velocity.Sample(Sampler, UV).xy; // 这个像素上一帧在哪
float2 historyUV = UV - velocity; // 反推历史位置
float3 history = History.Sample(Sampler, historyUV); // 取历史颜色
float3 current = CurrentFrame.Sample(Sampler, UV); // 取当前颜色
return lerp(history, current, 0.5); // 混合(实际更复杂,有 clamp)
}

Motion Vector(运动向量) 是 TAA 的关键输入——每个像素需要知道它上一帧在屏幕的什么位置。UE 的 GBuffer 里包含 Velocity Buffer,在 GBuffer Pass 中写入。

3 TAA 的问题#

  • Ghosting(鬼影):快速移动物体后面的”拖尾”——因为历史帧的数据来自物体过去的位置,混合时没对齐
  • 模糊:混合了多帧的结果,画面偏软——很多游戏被吐槽”TAA 太糊”
  • Disocclusion 问题:被挡住的物体突然出现(如转身),上一帧没有它的历史数据,TAA 不知道怎么办

DLSS / FSR / XeSS 本质上是更智能的 TAA——用 AI 替代简单的混合逻辑,在历史帧和当前帧之间做更精细的重建。


六、Motion Blur ——运动模糊#

1 两种做法#

相机运动模糊(Camera Motion Blur): 相机的旋转/平移导致整个画面模糊。做法是用上一帧的 ViewProj 矩阵算出每个像素在上一帧的屏幕位置,在当前帧和上一帧位置之间做采样。

物体运动模糊(Per-Object Motion Blur): 单独的快速移动物体(如挥动的剑)。每个物体的 Motion Vector 不同——这在 GBuffer 的 Velocity Buffer 里已经存好了。

2 UE 的实现#

UE 的 Motion Blur 基于 Velocity Buffer 做——和 TAA 共享同一个输入。在 Velocity Buffer 指示的运动轨迹上做多次采样,取平均。


七、完整的后处理顺序与为什么#

1. SSAO / GTAO
↓ 需要深度和法线信息(GBuffer 里有)
2. Screen Space Reflections(如果需要)
↓ 利用 GBuffer 的深度和法线做光线步进
3. DOF(景深,如果需要)
↓ 基于深度做 CoC(Circle of Confusion)计算
4. Motion Blur
↓ 基于 Velocity Buffer
5. Bloom
↓ 对 HDR 数据做——必须在 Tone Map 之前
6. Tone Mapping(HDR → LDR)
↓ 颜色从线性空间映射到显示空间
7. Color Grading(LUT 调色)
↓ 在 LDR 空间做最后的色彩调整
8. TAA / Upscaling
↓ 最后抗锯齿——在 LDR 空间做
9. UI Render → Final Output

任何两个环节的顺序调换,画面效果都会不同。这不是习惯问题——是颜色空间依赖的问题。


八、在 UE 里的对应#

你在 UE 编辑器的 Post Process Volume 里调的每一个参数,都会影响上面某个环节:

Post Process 参数对应的环节
Bloom Intensity / ThresholdBloom
Exposure CompensationTone Mapping
Color Grading LUTColor Grading
Ambient Occlusion IntensitySSAO / GTAO
Motion Blur AmountMotion Blur
Depth of FieldDOF
Anti-Aliasing MethodTAA / TSR / DLSS

每个参数的背后不是魔法——是上面这些 Shader 管线在跑。


九、总结#

环节做了什么依赖
Bloom高亮区散射光晕HDR SceneColor
Tone MappingHDR → LDR 亮度映射Bloom 结果(HDR)
SSAO近处遮蔽阴影Depth + Normal
Motion Blur运动方向上的采样平均Velocity Buffer
TAA当前帧 + 历史帧混合(抗锯齿)Velocity Buffer + History
Color GradingLUT 色彩风格化Tone Map 结果

后处理是每个玩家都看得见的环节——一个 Bloom 强了弱了、Tone Mapping 换 LUT 了,都是直接体感的。理解后处理栈,才知道为什么你的 UE 画面会”发糊”(TAA)或者”过曝”(Exposure)。

后处理管线——一帧的最后 2ms
https://www.m4doka.xyz/posts/cg/cg-9-post-process/
作者
m4doka
发布于
2026-03-15
许可协议
CC BY-NC-SA 4.0