一、后处理是什么
延迟渲染的 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 → 叠加 UIOutput: 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 Blur 或 Dual 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.52 核心机制
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 Buffer5. 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 / Threshold | Bloom |
| Exposure Compensation | Tone Mapping |
| Color Grading LUT | Color Grading |
| Ambient Occlusion Intensity | SSAO / GTAO |
| Motion Blur Amount | Motion Blur |
| Depth of Field | DOF |
| Anti-Aliasing Method | TAA / TSR / DLSS |
每个参数的背后不是魔法——是上面这些 Shader 管线在跑。
九、总结
| 环节 | 做了什么 | 依赖 |
|---|---|---|
| Bloom | 高亮区散射光晕 | HDR SceneColor |
| Tone Mapping | HDR → LDR 亮度映射 | Bloom 结果(HDR) |
| SSAO | 近处遮蔽阴影 | Depth + Normal |
| Motion Blur | 运动方向上的采样平均 | Velocity Buffer |
| TAA | 当前帧 + 历史帧混合(抗锯齿) | Velocity Buffer + History |
| Color Grading | LUT 色彩风格化 | Tone Map 结果 |
后处理是每个玩家都看得见的环节——一个 Bloom 强了弱了、Tone Mapping 换 LUT 了,都是直接体感的。理解后处理栈,才知道为什么你的 UE 画面会”发糊”(TAA)或者”过曝”(Exposure)。