实时渲染管线探析 · 第 三 篇

后处理链:Bloom、色调映射与抗锯齿

2026-09-1019,251 字166 张配图本文由@溯光 出品,转载请注明出处
题图
题图焦外点光源形成的散景(Bokeh):一种真实存在的"溢出光"@ Wikimedia Commons · Clem Onojeghuo(CC0)· https://commons.wikimedia.org/wiki/File:Street_bokeh_(Unsplash).jpg

系列文章前言

1.1 系列定位

本系列逐层拆解实时渲染管线(Real-Time Rendering Pipeline)。前两篇走的是"几何与光照":延迟渲染解决"怎么同时点亮很多光源",阴影贴图解决"光照怎么落到物体上还有影子"。

OK,言归正传。这一篇进入最后一段——后处理(Post-Processing)。三维场景已渲染成一张二维图像,剩下的工作都在像素上做。看似收尾杂活,实为整条管线里最被低估的一段:画面"好不好看",最后的判断几乎都发生在这几个 Pass 里。

系列五篇在渲染管线中的位置
图 3-1本系列五篇在渲染管线中的位置,本篇落在最后的后处理段@ 本系列绘制

1.2 本篇要讲清的一件事

后处理里有三样东西被讲得极多,却很少被讲成一条链:Bloom(泛光)、色调映射(Tone Mapping)、抗锯齿(Anti-Aliasing)。

它们在教程里通常是三个并列的"效果开关",但按管线的执行顺序看,它们其实是一次连续的能量搬运:

  • 场景渲染输出近似无限动态范围的线性光,Bloom 把高光溢出这一真实现象加回来——加法;
  • 显示器动态范围有限,色调映射把无限压进有限——减法,且观感不能塌;
  • 前两步都在空间与时间上引入新的高频,抗锯齿收拾自己留下的烂摊子——采样。

我把它叫作"能量的漏斗"。整篇文章都挂在这条主线上,而不是把三样东西各自孤立地讲一遍。

先划一条边界:本文只讨论后处理链中的这三段,以及它们彼此的相互影响;完整的后处理清单(景深、运动模糊、色彩分级等)、各家引擎的后处理图配置、具体游戏的画质调优建议,都不在本文范围内(理由见 2.3 节)。

后处理链的能量漏斗
图 3-2本文的主线:后处理链是一次连续的能量搬运——加回溢出、压进显示、收拾采样@ 本系列绘制

所以本篇要回答的是三个"为什么",而不是三个"怎么用":

  1. 高光为什么会溢出,Bloom 凭什么可以"把真的加回来"?
  2. 无限动态范围压进有限范围,为什么不能直接线性截断?
  3. 抗锯齿到底在跟什么作对,为什么没有一种方法能全部解决?

1.3 全系列内容总览

  1. 【实时渲染管线探析】(一) 延迟渲染:从原理到实现的性能权衡
  2. 【实时渲染管线探析】(二) 阴影贴图:从 PCF 到 VSM 的演进
  3. 【实时渲染管线探析】(三) 后处理链:Bloom、色调映射与抗锯齿 ← 本篇
  4. 【实时渲染管线探析】(四) PBR 与 IBL:从渲染方程到工业实践
  5. 【实时渲染管线探析】(五) 可见性与剔除:从视锥到 GPU-Driven

1.4 相关资源链接

  • 上一篇:阴影贴图:从 PCF 到 VSM 的演进(见系列目录)
  • 下一篇:PBR 与 IBL:从渲染方程到工业实践(见系列目录)
  • Real-Time Rendering 官方网站(本系列常查的工具书站点):https://www.realtimerendering.com/

1.5 与已有中文文章的差异

OK,下面我们开始。动笔前我把同主题的中文文章过了一遍。它们质量不差,但有一个共同的结构性特征:把后处理写成效果手册——每个效果一段参数说明加一张调参前对比图,读者学会的是"拖动哪个滑条",而不是"为什么这里必须做这件事"。

差异列出来,方便你判断这篇是否值得读:

已有文章 切入角度 未覆盖的部分
《Bloom(泛光): 让画面"发光"的魔法》 科普向,阈值 + 高斯模糊 无源码、无参数,未触及能量守恒
CSDN《屏幕后处理 5.0: 讨论双重模糊的 Bloom》 高斯→Kawase→双模糊递进 未给 1/(1+luma) 萤火虫抑制与加权重归一化
LearnOpenGL CN《泛光》 OpenGL 手把手(bright pass + ping-pong 高斯) 无 mip chain / tent filter / 能量讨论,强度纯经验值
掘金《Three.js UnrealBloomPass 分析》 拆解 UnrealBloomPass 源码流程 未覆盖 dual filtering、Karis 金字塔、FFT 卷积
CSDN《色调映射: 当无限的光被塞进有限的画布》 以"压缩哲学"串 Reinhard/Filmic/ACES 未涉及 AgX、PQ/HLG 管线
博客园《Tone Mapping(简洁直观的 Tonemapping 介绍)》 算子逐段代码讲解 未覆盖 ACES 2 与色域映射,无对比数据
CSDN《渲染的抗锯齿终极指南》 8 种 AA 方案铺开 + MSAA 工作流程 缺统一频谱视角,MSAA 带宽代价无量化
CSDN《抗锯齿技术如何影响帧率》 帧率对照表 参数名与官方不符,超分与 AA 的关系表述有误

这几篇的原文地址如下,都值得一读:

  • https://blog.csdn.net/qq_33060405/article/details/161434078
  • https://blog.csdn.net/qq_41835314/article/details/127986418
  • https://learnopengl-cn.github.io/05%20Advanced%20Lighting/07%20Bloom/
  • https://juejin.cn/post/7541304587762073646
  • https://blog.csdn.net/qq_33060405/article/details/164171456
  • https://www.cnblogs.com/dragon2012/p/14674417.html
  • https://blog.csdn.net/PixelWander/article/details/155637810
  • https://blog.csdn.net/codeink/article/details/155934603

本文相对它们的差异集中在五处:COD AW 的萤火虫抑制与加权重归一化、"阈值"与"乘系数"两条 Bloom 路线的分歧、tone curve 曲线族的并排对照、MSAA 的 Resolve 成本量化、以及"超分内置 AA 不应与 TAA 叠加"这条被多篇文章写错的官方口径。

本文与已有中文文章的内容覆盖对照
图 3-3本文与已有中文文章在内容覆盖上的差异@ 本系列绘制
夜间点光源的散景
图 3-4夜间点光源的散景:暗背景下溢出光更醒目,这是 Bloom 试图复原的现象@ Wikimedia Commons · Geoff Peters(CC BY 2.0)· https://commons.wikimedia.org/wiki/File:Light_bokeh_at_night_(14769384586).jpg

背景铺垫

2.1 后处理为什么值得单独讲

一个容易被忽略的事实是:后处理是唯一一段"看不见几何"的渲染。

OK,前面的所有环节都在处理三维信息——顶点、三角形、法线、材质。到了后处理,输入是一张已经光栅化完的图像(加上若干辅助缓冲区),输出还是图像。这意味着它的成本模型和前面完全不同:它不随场景复杂度增长,只随分辨率和带宽增长。1080p 升到 4K,几何量不变,后处理的填充率翻了四倍。

后处理链三个阶段的输入输出值域
图 3-5后处理链三段的值域去向:Bloom 加回溢出光,色调映射压进显示范围,抗锯齿修改采样分布@ 本系列绘制

2.2 场景的动态范围与显示器的动态范围,差多少

先给一组量级,不然后面讲"压缩"没有参照。

真实世界的动态范围跨度极大:月光下的景物约 0.001 cd/m²,直视太阳可达 1.6×10⁹ cd/m²——跨越 12 个数量级。摄影上习惯用"档"(Stop)衡量,一档即亮度翻倍;人眼单次注视能分辨约 10–14 档。

显示器侧呢?SDR 峰值亮度约 100–300 cd/m²,对比度标称 1000:1 到 3000:1(约 10–11.5 档);HDR10 消费级峰值 1000 cd/m²(良好黑位下约 14 档),高端的到 4000 cd/m²。

场景动态范围与显示动态范围的差距
图 3-6从真实世界到显示器的动态范围落差:这是色调映射存在的根本理由@ 本系列绘制
动态范围的刻度尺对照
图 3-7动态范围刻度尺对照@ 本系列绘制

落差就在这里:场景侧是"没有上限的线性光",显示侧是"有明确上限的编码值"。这中间必须有人做取舍,而取舍必然有损——这正是下一章之后所有讨论的前提。

大动态范围的夜景
图 3-8大动态范围的夜晚场景:高光点与深暗部同时存在,正是色调映射要处理的输入@ Wikimedia Commons · Bhullargraphic(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File:Vibrant_raindrops_on_glass_with_a_blurred_urban_bokeh_light_background.jpg

再看一个真实世界里高动态范围的例子——这张全景由多张不同曝光合成:

真实世界的高动态范围场景:同一画面里高光与暗部跨越十余档曝光
图 3-9真实世界的高动态范围场景:同一画面里高光与暗部跨越十余档曝光@ Diego Delso(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File:Anfiteatro_de_Pula,_Croacia,_2017-04-17,_DD_49-57_HDR_PAN.jpg

同一场景,HDR 呈现与 LDR 呈现的差别是这样的:

同场景 HDR 与 LDR 对照:高光与暗部能同时保留的余地,正是后处理要压缩的那段动态范围
图 3-10同场景 HDR 与 LDR 对照:高光与暗部能同时保留的余地,正是后处理要压缩的那段动态范围@ Wikimedia Commons · Igreja_Nossa_Senhora_de_Fatima_(JPEG-HDR).jpg : Axel Jacobs (Photographer) derivative work: Hdruser ( talk )(CC BY-SA 2.0)· https://commons.wikimedia.org/wiki/File%3AIgreja%20Nossa%20Senhora%20de%20Fatima%20%28HDR%20LDR%20comparison%29.jpg

2.3 本文的范围边界

本文只讨论后处理链中的 Bloom、色调映射、抗锯齿这三段,以及它们之间的相互影响。以下内容不在范围内:

  • 完整的后处理清单(景深、运动模糊、色差、颗粒、色彩分级)——只在说明先后顺序时提及位置,不展开算法
  • 各家引擎的完整后处理图配置——版本差异大,属工程配置而非算法原理
  • 具体游戏的画质调优建议——本文给取舍依据,不给"这台机器该开什么"
  • 主机硬件的 AA 实现细节——只在讨论 MSAA 时点到代价
  • 光线追踪降噪——另一个完整话题

性能数据只引用论文、厂商官方文档与官方技术章提供的实测,社区横评只用于交叉印证量级,并会标明来源性质。

本文的讨论范围
图 3-11本文的讨论范围:后处理链的三段主线,以及明确排除的四类话题@ 本系列绘制
后处理链中所有效果的执行顺序
图 3-12后处理链的完整执行顺序@ 本系列绘制

2.4 术语对齐

后文统一使用简称,先对齐一轮,避免中英混用造成歧义。

关键术语中英对照
图 3-13关键术语的中英对照与本系列简称约定@ 本系列绘制

几个容易混的,这里单独说清:

  • Bloom(泛光) 与 Glow(辉光):同一类"高光外溢",本文统一叫 Bloom(GPU Gems 早期用 Glow)。
  • HDR:本文指值域概念(线性、可超过 1.0),不指"HDR10 显示器"这一显示标准。
  • AA(抗锯齿) 与 Super Resolution(超分):严格区分。超分是"低分辨率渲染 + 上采样",自带 AA 但不是 AA 本身。
  • Tone Mapping(色调映射) 与 Color Grading(色彩分级):前者管值域映射,后者管风格倾向。

2.5 一个贯穿全篇的问题

带着这个问题读下去会更有收获:如果 Bloom 只是把亮的地方弄得更亮,那为什么"物理正确"的 Bloom 反而看起来比"调高强度的 Bloom"更收敛?

答案在第二、三章:真实的溢出光遵守能量守恒(Energy Conservation)——它把能量从高光区搬到周围,而非凭空创造;大多数"调强度"的实现做的是后者。

第二个问题:既然 TAA 的采样数量远超 MSAA,为什么主流引擎仍保留 MSAA 选项? 答案在第八章——两者解决的不是同一类别名。

一、Bloom:把真实的光晕加回来

3.1 先看现象:光晕不是特效,是相机的缺陷

先把顺序摆正。Bloom 常被当作"让画面高级一点"的后期特效,但它的原型是真实光学系统里的事——强光在镜头与眼睛内部散射,亮的物体周围因此出现一圈光晕。下面三张不是渲染结果,是相机拍出来的。

强光源造成的镜头散射
图 3-14强光源进入镜头后,在镜组内部多次反射与散射形成的光斑@ Wikimedia Commons · Amolven(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File:A_Lens_flare.jpg
柔和的镜头光晕
图 3-15逆光水面的柔和光晕:高光能量向周围低频扩散@ Wikimedia Commons · W.carter(CC0)· https://commons.wikimedia.org/wiki/File:Dreamy_lens_flare_over_water.jpg

三张照片放在一起看,能读出这类光斑的一个共同特征:它总是围绕光源出现,且形状与光源本身无关。

有无遮光罩的对照
图 3-16同一场景加装遮光罩前后的对照:光晕主要来自镜头内部而非被摄物@ Wikimedia Commons · Ggia(CC BY-SA 3.0)· https://commons.wikimedia.org/wiki/File:20100706_lens_flare_example_comparison_lens_hood.jpg

好了,我们正式开始。第三张是这里最有说服力的一张。加装遮光罩后光晕明显减弱,说明这部分光并非场景里真实存在的亮光,而是镜筒内壁的杂散反射造成的。换句话说:光晕是成像系统的响应,不是被摄物本身的属性。

这直接决定了 Bloom 的定位——它不是"凭空造一个好看的效果",而是把一个被渲染过程遗漏掉的真实响应补回来。

同一类“成像系统的固有响应”还有别的形式——高对比边缘的彩色镶边也是镜头带来的,被摄物本身并没有这种颜色:

色差:高对比边缘出现的彩色镶边同样是镜头而非被摄物的属性
图 3-17色差:高对比边缘出现的彩色镶边同样是镜头而非被摄物的属性@ Wikimedia Commons · Wilfredor(CC0)· https://commons.wikimedia.org/wiki/File%3AChromatic%20aberration%20Wine.png

3.2 原理:这一切就是一个卷积

把上面的现象数学化,就是点扩散函数(Point Spread Function, PSF)。

理想成像系统下,一个点光源应该在像面上成一个个点。真实的镜头做不到:一个点会扩散成一小块分布,这块分布的形状就是 PSF。于是整张图像的成像结果,可以写成理想图像与 PSF 的卷积:

$$ I_{\text{captured}}(x,y) = I_{\text{ideal}}(x,y) * \text{PSF}(x,y) $$

其中:

  • $I_{\text{ideal}}$ 是理想成像结果,即渲染器直接算出的那张图。
  • $\text{PSF}$ 是点扩散函数,描述"一个点会散成什么样"。
  • $*$ 表示卷积(Convolution)。

这条式子是整个 Bloom 章的骨架:渲染器给出的是 $I_{\text{ideal}}$,真实相机给出的是右边的结果。渲染要"像照片",就得补上这一步卷积。

点扩散函数与卷积的作用
图 3-18点扩散函数把理想像面上的点扩散成一块能量分布,这就是卷积核的物理来源@ 本系列绘制
圆盘 PSF 与高斯 PSF 的形状对照
图 3-19圆盘 PSF 与高斯 PSF 的形状差异@ 本系列绘制

真实镜头为什么做不到“一个点成一个点”?像差是原因之一——球差让镜片边缘的光线焦点与中心不重合:

球差实拍:镜片边缘与中心的光线焦点不重合,一个点因此在像面上被扩散成一块
图 3-20球差实拍:镜片边缘与中心的光线焦点不重合,一个点因此在像面上被扩散成一块@ Wikimedia Commons · Adam Gabrys(CC BY-SA 3.0)· https://commons.wikimedia.org/wiki/File%3ALens%20spherical%20aberration%20RL.jpg

3.3 PSF 的形状从哪来:光圈决定核形状

好了,PSF 不是任意的。在几何光学近似下,焦外点光源成像的形状就是光圈孔的形状(Aperture Shape)——圆形光圈得到圆形光斑,多边形光圈得到多边形光斑,这就是摄影里说的散景(Bokeh)。

下面两张照片可以直接对照:焦外高光点呈现出规整的多边形,形状由镜头的叶片数决定。

光圈叶片形状与散景形态
图 3-21焦外高光点的形状由光圈叶片数决定,图中是典型的多边形散景@ Wikimedia Commons · Javier Perez Montes(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File:Bokeh_Highlights_-_AF-Nikkor_50mm_f1.8_-_01.jpg
不同光圈下的散景变化
图 3-22同一场景改变光圈值后的散景变化:孔径越大,扩散范围越广@ Wikimedia Commons · Dietmar Rabich(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File:Blendenvariationen_--_2024_--_6808.jpg
光圈形状到卷积核的映射
图 3-23几何光学近似下,光圈孔形状直接对应卷积核形状@ 本系列绘制
圆盘模糊与高斯模糊的观感差异
图 3-24圆盘模糊与高斯模糊的观感差异@ 本系列绘制

光圈形状不只决定散景,也决定景深过渡——引擎里的深度虚化是这样的:

引擎中的景深(DoF)虚化:光圈越大,焦外过渡带越窄、散景越明显
图 3-25引擎中的景深(DoF)虚化:光圈越大,焦外过渡带越窄、散景越明显@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html

光圈不只决定散景形状,也决定焦外过渡:

焦外过渡(DoF near):光圈越大,焦外虚化越强
图 3-26焦外过渡(DoF near):光圈越大,焦外虚化越强@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html

工程后果很直接:想要"物理正确"的 Bloom,卷积核应来自光圈形状,而不是随手挑一个高斯核。 圆形光圈的散焦斑(Circle of Confusion)在几何上是均匀圆盘,其卷积结果是"圆盘模糊"而非高斯模糊;只有考虑衍射时才会趋近带环的艾里斑(Airy Disk)。

言归正传。到这里已经能看出一个常见的偷懒:很多实现直接用一个高斯核代替 PSF。这不算错——高斯在视觉上足够接近,而且可分离(Separable),成本低得多。但它同时也意味着"物理正确"只是一种近似,这是后面讨论能量守恒时要注意的前提。

散景的形状直接暴露出光圈孔的形状:

多边形散景:焦外高光点呈规整多边形,边数由光圈叶片数决定
图 3-27多边形散景:焦外高光点呈规整多边形,边数由光圈叶片数决定@ Wikimedia Commons · Roni Amin(CC BY 2.0)· https://commons.wikimedia.org/wiki/File%3ABokeh%20shapes.jpg

把光圈换成心形,焦外的光点就变成心形——这是“PSF 形状 = 光圈形状”最直白的证据:

心形散景:加装心形遮罩后焦外高光点随之变成心形,PSF 形状由光圈孔直接决定
图 3-28心形散景:加装心形遮罩后焦外高光点随之变成心形,PSF 形状由光圈孔直接决定@ Wikimedia Commons · Iouri Goussev(CC BY-SA 2.0)· https://commons.wikimedia.org/wiki/File%3AHeart-shaped%20bokeh.jpg

真实镜头的光圈叶片是这样排布的:

镜头的光圈叶片:叶片数量与排列决定了孔径呈现的多边形形状
图 3-29镜头的光圈叶片:叶片数量与排列决定了孔径呈现的多边形形状@ Wikimedia Commons · Petar Milošević(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File%3AOlympus%20Zuiko%20OM%2050%20mm%20lens.jpg

另一类让点扩散开的是色差——不同波长的光折射率不同,焦点也就不重合:

色差实拍(微距):不同波长焦点不重合,高对比边缘出现彩色镶边
图 3-30色差实拍(微距):不同波长焦点不重合,高对比边缘出现彩色镶边@ Wikimedia Commons · Acagastya(CC0)· https://commons.wikimedia.org/wiki/File%3AChromatic%20aberration%20in%20a%20macro%20photo.jpg

放大看,这种镶边是有方向的:

色差细节:边缘一侧偏蓝紫、另一侧偏黄绿,方向取决于镜片的色散特性
图 3-31色差细节:边缘一侧偏蓝紫、另一侧偏黄绿,方向取决于镜片的色散特性@ Wikimedia Commons · Jkk(CC BY-SA 3.0)· https://commons.wikimedia.org/wiki/File%3AChromatic%20aberration%20with%20detail.jpg

3.4 从 PSF 到可实现:分离卷积把代价降下来

卷积的代价可以用可分离与分层来摊薄,引擎里的混合模糊就是这样:

混合模糊(mixed blur):用多级模糊近似大核卷积
图 3-32混合模糊(mixed blur):用多级模糊近似大核卷积@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html

如果老老实实按 $I * \text{PSF}$ 做二维卷积,代价是核面积的平方级。GPU Gems 第 21 章《Real-Time Glow》给出的第一个关键结论就是这个:

二维可分离卷积把代价从 $d^2$ 降到 $2d$。

举例:一个 $50 \times 50$ 的核,直接卷积每个像素需要 2500 次纹理读取;拆成"先水平 50 次、再垂直 50 次"两步之后,总共约 100 次读取。这是 25 倍的差距。原文给出的量化是"50×50 的核只需约 100 次读取"。

//-----------------------------------【Bloom 的分离式模糊】-----------------------------------
// 描述:把二维卷积拆成水平/垂直两遍,代价从 d^2 降到 2d
//------------------------------------------------------------------------------------------
//【1】先做降采样:Bloom 不需要全分辨率,1/3~1/4 尺寸在观感上几乎无损
//【2】水平方向做一维卷积
//【3】垂直方向做一维卷积
float3 BloomSeparable(Texture2D src, float2 uv, float2 texelSize)
{
    //【1】降采样:GPU Gems 建议 1/3~1/4 分辨率,配合双线性采样可抑制闪烁
    float3 c = DownsampleBilinear(src, uv, texelSize);

    //【2】水平一维卷积(核可分离,故只沿 x 方向取权重)
    c = Convolve1D(src, uv, float2(texelSize.x, 0), KERNEL_WEIGHTS);

    //【3】垂直一维卷积
    c = Convolve1D(src, uv, float2(0, texelSize.y), KERNEL_WEIGHTS);

    return c;
}

说白了,下面对分离式模糊特别说明两点。第一,降采样不是可选优化,而是算法的组成部分——GPU Gems 明确指出 Bloom 可在 1/3 到 1/4 分辨率上做,观感几乎无损,填充率却省下 9 到 16 倍。第二,降采样必须用双线性过滤,否则采样率下降会让原始图像的高频混叠进来,加剧闪烁(Shimmering)。

完整实现参考 GPU Gems 第 21 章:https://developer.nvidia.com/gpugems/gpugems/part-iv-image-processing/chapter-21-real-time-glow

3.5 效果图:有与没有的区别

把上面两步接起来,先做一次亮部提取(Bright Pass),再卷积,最后叠加回原图,就得到经典的 Bloom。下面是"有"与"没有"的观感对照示意。

有与没有 Bloom 的观感差异
图 3-33无 Bloom 时高光边界生硬收在几何边缘;有 Bloom 后能量向周围扩散,明暗过渡自然@ 本系列绘制

引擎里的开关对照最能说明问题:

引擎中的 Bloom 效果:开启后高亮区域向外溢出光晕
图 3-34引擎中的 Bloom 效果:开启后高亮区域向外溢出光晕@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html

这里最容易被忽略的一点:Bloom 的收益主要不在"亮",而在"边界过渡"。没有 Bloom 时,高亮物体与暗背景的交界是一条硬边;真实照片里这条边是渐变的——人眼对它极其敏感,这就是去掉 Bloom 后画面立刻显"假"的原因。

Bloom 在完整后处理链中的位置
图 3-35Bloom 在后处理链中的位置:它需要 HDR 输入,且必须在色调映射之前@ 本系列绘制
顺序错误的后果
图 3-36顺序颠倒的后果:阈值失效、高光过曝@ 本系列绘制

引擎里 Bloom 的另一组真实输出:

引擎中的 Bloom(glow):高亮区域向周围扩散出光晕
图 3-37引擎中的 Bloom(glow):高亮区域向周围扩散出光晕@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html

最后一个顺序问题,很多人踩过坑:Bloom 必须在色调映射之前做。

下面我们开始。依然是先来看看现象。原因是 Bloom 的亮部提取依赖"值大于阈值"这个判断,而这个判断只有在 HDR 线性空间才有意义。如果先做色调映射,值域已经被压到 $[0,1]$,所有像素都被压到了接近上限的位置,阈值就失去了区分能力——更糟的是,此时再叠加会直接导致高光过曝(Clipping)。第 6.1 节的线性截断讨论会把这个顺序问题讲得更透。

3.6 源码链接与本节的量化结论

本节涉及的实现与数据出处:

  • GPU Gems Ch.21《Real-Time Glow》——分离卷积与低分辨率渲染:https://developer.nvidia.com/gpugems/gpugems/part-iv-image-processing/chapter-21-real-time-glow
  • NVIDIA GameWorks Bloom 示例(quarter-size FBO + 亮度平方化 + 高斯 H/V 两步):https://docs.nvidia.com/gameworks/content/gameworkslibrary/graphicssamples/opengl_samples/bloomsample.htm
  • Unity 高动态范围手册(HDR 对 Bloom 的前置要求):https://docs.unity.cn/cn/tuanjiemanual/Manual/HDR.html

三条可引用的量化结论:

  1. 分离卷积把二维卷积代价从 $O(d^2)$ 降到 $O(2d)$,$50\times50$ 核由约 2500 次读取降到约 100 次。
  2. Bloom 可在 1/3–1/4 分辨率计算,填充率节省 9–16 倍,观感几乎无损。
  3. Bloom 的输入必须是 HDR 线性空间;顺序错误会导致高光过曝。

二、Bloom 的两条路线:阈值与乘系数

4.1 一个被忽略的分歧

绝大多数 Bloom 教程都会教你这一步:把亮度低于阈值的像素抹掉,只保留最亮的部分。写出来大致是这样:

// 伪代码:常见的"阈值"路线
float luma = dot(color.rgb, float3(0.2126, 0.7152, 0.0722));
color.rgb *= smoothstep(threshold, threshold + knee, luma);

这段代码看起来天经地义,以至于没人问一句:如果阈值本身就选不对呢?

2014 年 SIGGRAPH 的 Advances in Real-Time Rendering 课程上,Jorge Jimenez 给出了一个反例。他在《Next Generation Post Processing in Call of Duty: Advanced Warfare》里明确说:COD Advanced Warfare 的 Bloom 不使用亮度阈值。 理由有两条:

其一,PBR 管线下的动态范围已经足够高,阈值在这里成了一个强行切断信号的人为分界;其二,阈值会带来一个不好调的过渡带,参数敏感。

替代方案是把整个模糊后的 Bloom 层乘一个很低的系数——约 0.04——然后直接叠加。效果上,只有原本很亮的像素才在叠加后明显发光,而中等亮度的像素贡献极小、几乎看不出来。这个系数的可用区间不宽:低于 0.02 基本看不出效果,高于 0.08 则整图发灰。

阈值路线与乘系数路线
图 3-38两条路线的分歧:阈值路线先做硬性筛选,乘系数路线保留全部亮度并整体压低@ 本系列绘制
乘系数路线的系数取值对照
图 3-39乘系数路线的系数取值对照@ 本系列绘制

下面我们进入正题。两条路线的差异不只是"少一个参数",而是处理哲学不同:阈值是"先判断再保留",乘系数是"全都保留但整体压到很小"——后者连续、无可调拐点,代价是 Bloom 层里始终含有全部场景能量。

4.2 阈值路线的细节:软拐点(Soft Knee)

问题在于:如果严格按"高于阈值保留、低于阈值丢弃"来做,阈值附近会出现一条可见的硬边——亮度刚过阈值的像素突然有了 Bloom,亮度差一点的完全没有。这条边在画面里表现为一圈突兀的光晕边界。

工业上的解决办法是加一个软拐点:在阈值上下各留一段过渡带,在这个带内线性(或平滑)地从 0 渐变到 1。

$$ w(\ell) = \text{smoothstep}(t,\ t + k,\ \ell) $$

其中:

  • $\ell$ 是像素亮度,通常按 Rec.709 权重 $\ell = 0.2126R + 0.7152G + 0.0722B$ 计算。
  • $t$ 是阈值(Threshold),Unreal 中以亮度单位计。
  • $k$ 是软拐点宽度(Knee),Unreal 文档描述为"一单位宽的线性过渡带"。
硬阈值与软拐点的曲线对照
图 3-40硬阈值在阈值附近产生可见边界,软拐点把突变摊成一段过渡@ 本系列绘制
阈值与拐点的参数空间
图 3-41阈值与拐点的参数空间@ 本系列绘制

阈值路线的参数在引擎里是这样起作用的:

引擎中的 glow 阈值(threshold):只有超过阈值的亮度才进入 Bloom
图 3-42引擎中的 glow 阈值(threshold):只有超过阈值的亮度才进入 Bloom@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html

4.3 Unreal 的 Threshold = -1:一个被误读的参数

Unreal 的 Bloom 文档里有一句常被引用但很少被解释的话:

将 Threshold 设为 -1 表示所有像素同等贡献(physically correct),且由于省掉了阈值 Pass 而更快。

这句话需要拆成两层来读。

第一层是语义:Threshold 设为 -1 之后,阈值判断被关掉,所有像素都进入 Bloom 层。这实际上就走到了上面那条"全程乘系数"的路线上。

下面开始。第二层是为什么更快:阈值判断本身需要一个 Pass 来算出每个像素的权重并写入中间缓冲。关掉它之后这个 Pass 被整个省掉,带宽与 ALU 都省下来——在 Unreal 官方文档里,这是一个明确标出的性能收益。

Unreal Threshold 取值的三种状态
图 3-43Unreal 中 Threshold 的三种状态:正值(带过渡带)、-1(全部像素参与,物理正确且省一个 Pass)@ 本系列绘制

所以"物理正确"这四个字在这里的含义是:没有人为的亮度筛选,Bloom 层的能量分布忠实反映场景的高光分布。它并不是说这样做"更真",而是说这样做没有引入额外的调参自由度。

4.4 乘系数路线的细节:萤火虫抑制与加权重归一化

COD AW 的方案虽然去掉了阈值,但引入了一个新麻烦:萤火虫(Firefly)。

下面进入正题。萤火虫(Firefly)指画面里个别像素亮度极端突出——比周围高好几个数量级,来源是高光镜面反射、粒子与单像素过曝点。这些点在 Bloom 层里会成为极亮的孤立斑点,降采样后仍显眼,像画面里飞着一只亮虫子。

Jimenez 给出的处理是按亮度反比加权:

$$ w = \frac{1}{1 + \ell}, \qquad \text{result} = \frac{\sum_i c_i w_i}{\sum_i w_i} $$

其中:

  • $\ell$ 是该采样点的亮度。
  • $w$ 是权重,亮度越高权重越小,因此极端亮的点被压制。
  • 后面的除法 $\sum c_i w_i / \sum w_i$ 是加权重归一化。
萤火虫抑制与加权重归一化
图 3-44左:未抑制时极端亮的孤立像素在降采样后成为显眼斑点;右:按 1/(1+luma) 加权并重归一化后消失@ 本系列绘制
萤火虫的典型来源
图 3-45萤火虫的典型来源@ 本系列绘制

最容易漏掉的是那个除法。 只加权、不归一化,等于给每个采样点乘了一个小于 1 的权重,整张图会整体变暗;除以权重和,加权平均才回到正确的亮度量级。Jimenez 在原作者页的技术问答里专门强调过,中文文章里几乎都没写。

加权平均的重归一化
图 3-46只加权不归一化会让整体亮度漂移,除以权重和才能保持能量量级@ 本系列绘制

4.5 代码:金字塔降采样中的加权

下面这段是上面公式的直接落地。它出现在降采样阶段,每一步都要做加权与归一化。

//-----------------------------------【Bloom 降采样:萤火虫抑制】-----------------------------
// 描述:COD AW 方案的核心——按亮度反比加权,并做加权重归一化
//------------------------------------------------------------------------------------------
//【1】取 4 个对角采样点(或 13-tap 采样组)
//【2】按 1/(1+luma) 计算每个点的权重,亮度过高的点被压制
//【3】加权求和后除以权重和,避免整体亮度漂移
float3 DownsampleWeighted(Texture2D src, float2 uv, float2 texelSize)
{
    float3 sum = 0;
    float  wsum = 0;

    //【1】四个对角采样
    [unroll]
    for (int i = 0; i < 4; ++i)
    {
        float3 c = src.SampleLevel(samplerLinear, uv + OFFSETS[i] * texelSize, 0).rgb;

        //【2】亮度反比加权:luma 越大权重越小,抑制 firefly
        float luma = dot(c, float3(0.2126, 0.7152, 0.0722));
        float w = 1.0 / (1.0 + luma);

        sum  += c * w;
        wsum += w;
    }

    //【3】关键:必须除以权重和,否则整张图会整体变暗
    return sum / max(wsum, 1e-5);
}

下面对这段代码特别说明一点:关键是"这个除法存在"。删掉它再用"调高 Bloom 强度"补偿的写法很常见,但那会把萤火虫重新放大回来,等于白做。luma 用 Rec.709 权重,与阈值路线一致。

完整实现可参考 Jimenez 的原演讲材料:https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare/

4.6 亮部提取的输出对照

两条路线在 Bright Pass 阶段的输出差异,可以直接看出来。

两条路线 Bright Pass 的输出对照
图 3-47阈值路线下中等亮度区域被完全剔除;乘系数路线下全部保留但整体很暗@ 本系列绘制

亮部提取这一步在引擎里有一张直接的视觉凭据:

引擎中的 glow map:亮部提取后的结果,只有高亮区域被保留
图 3-48引擎中的 glow map:亮部提取后的结果,只有高亮区域被保留@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html

4.7 一个绕不开的取舍:能量守恒

现在可以回答本章开头那个疑问了。

普通 Bloom 不只是"让亮的更亮",它还会增加画面总能量。 Unreal 官方文档的表述是:Standard Bloom 不是能量守恒的(not conservative),会导致整图变亮——卷积结果叠加回原图时若不做归一化,总能量必然增加。

先来看看现象。真正的能量守恒做法要换一个模型:把 Bloom 理解为从高光区搬到周围的那部分能量,而不是额外加上的亮度。这就要求卷积核满足归一化条件(权值和为 1),并且叠加时的系数与"搬走了多少"相配。

能量守恒与非守恒的 Bloom
图 3-49非守恒 Bloom 凭空增加总能量(整图变亮);守恒 Bloom 把能量从高光搬向周围,总量不变@ 本系列绘制

Unreal 的 Bloom Convolution 走的就是后一条路。官方文档说明它用卷积核纹理来建模相机与眼内光的散射与衍射,是能量守恒的卷积,底层用 FFT 加速。代价是算力——所以官方定位是"用于过场与高端硬件"。

下一章就顺着这条线,把工业实现的几种形态拆开看。

三、Bloom 的实现分野:从高斯到 FFT 卷积

5.1 先给全景:四种形态

到这里,Bloom 的"要做一次卷积"已经清楚,但在工程上"怎么做这次卷积"有四种主流形态,它们的取舍差别很大。先给一张总表,后面分节展开。

Bloom 四种实现形态的横向对比
图 3-50Bloom 四种实现形态的对比:质量、成本、内存占用与能量特性@ 本系列绘制

引擎把 Bloom 的尺度组织也做成了可选档位:

引擎中的 glow scale 模式:不同尺度档位下的光晕范围
图 3-51引擎中的 glow scale 模式:不同尺度档位下的光晕范围@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html
四种形态的采样方式
图 3-52四种形态的采样方式对照@ 本系列绘制
形态 做法 质量 相对成本 内存 能量守恒
Gaussian 降采样后逐级高斯模糊(可分离) 最佳 较高 中 否
Kawase 固定尺寸纹理上做 4 角采样,多级迭代 较低 低(低分辨率下最优) 低 否
Dual Kawase 式降采样 + 加权重升采样 中 低(高分辨率下可能比 Kawase 更快) 中 否
Convolution 卷积核纹理 + FFT 最高(可建模真实 PSF) 高 高 是

5.2 形态一:高斯金字塔

最经典的形态。做法是把原图逐级降采样成一条 mip 链,每级做一次可分离高斯模糊,然后把所有级叠加起来。

Unreal 的 Standard Bloom 就是这条路线:UE4/UE5 使用 Blur1 到 Blur5,分辨率从 1/2 逐级到 1/32。作为对比,UE3 时代是 1/4、1/8、1/16 三级——级数增加是为了让光晕的低频扩展更平滑,代价是这几级各自的模糊 Pass。

Unity URP 的高斯模式更精细:BlurH 是 9-tap 高斯,BlurV 是 5-tap 双线性等效高斯。"5-tap 等效 9-tap"是关键技巧——纹理采样自带双线性插值(Bilinear Filtering),一次采样可取到两个纹素的加权平均。

URP 的 9-tap 水平模糊与 5-tap 双线性等效垂直模糊
图 3-53URP 的 BlurH 是 9-tap 高斯,BlurV 用双线性采样把它折成 5-tap,省掉 4 次采样@ 本系列绘制
Unreal 与 URP 的 mip 级数对照
图 3-54Unreal 与 URP 的 mip 级数对照@ 本系列绘制

金字塔分层的实际分配,引擎里是这样展示的:

引擎中的 glow layers:Bloom 在多级尺度上的分层叠加
图 3-55引擎中的 glow layers:Bloom 在多级尺度上的分层叠加@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html

5.3 形态二:mip 金字塔上的 13-tap 与 tent filter

COD AW 的路线在"怎么做这一次模糊"上给出了一个成本与质量的平衡点,也是目前最被广泛抄写的一套。

降采样用 13-tap。 每级 mip 取 13 个位置加权平均,权重兼顾覆盖率与稳定性。13 不是随口定的——其分布让每个输出纹素均匀覆盖 4 个源纹素的范围,避免漏采。

13-tap 降采样的采样点分布
图 3-5613-tap 降采样的采样点分布:中心 4 点加两组交错环绕,保证覆盖率与均匀性@ 本系列绘制

升采样用 3×3 tent filter。 tent filter 就是三角形核,权重按到中心的距离线性衰减:

$$ K_{\text{tent}} = \frac{1}{16} \begin{bmatrix} 1 & 2 & 1 \\ 2 & 4 & 2 \\ 1 & 2 & 1 \end{bmatrix} $$

其中每个元素是权重,整核之和为 16,除以 16 完成归一化。

3×3 tent filter 的权重分布
图 3-573×3 tent filter:中心权重 4,边中 2,角 1,总权重 16@ 本系列绘制
tent 半径随 mip 级放大
图 3-58tent 半径随 mip 级放大@ 本系列绘制

上采样与模糊半径的关系,可以直接看引擎的输出:

引擎中的 glow blur:上采样阶段的模糊半径对光晕边缘的影响
图 3-59引擎中的 glow blur:上采样阶段的模糊半径对光晕边缘的影响@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html

tent filter 的关键在于半径随 mip 级放大。 Jimenez 把半径固定在 UV 空间,而每级 mip 分辨率减半,对应的实际像素半径就逐级翻倍:低级只影响邻近像素,高级影响很大一片。若干级串起来,结果在数学上逼近高斯,成本却远低于一次大核高斯。 这正是它"看着太简单"却成为主流的原因——简单性匹配了"多级叠加"的结构。

5.4 形态三:Kawase 与 Dual filtering

先来看看结论。Kawase 的思路是放弃"算得准",只要"看起来够用":每个输出像素只取四个角的采样求平均。中心区域靠双线性插值补上,天然形成大范围模糊;要更大范围就多迭代几次。

Unity URP 的 Kawase 模式就是它:固定尺寸纹理上做 4 角采样,官方给出的适用场景是"低分辨率下更快、省内存"。

Kawase 的 4 角采样
图 3-60Kawase 每次只取四个角的采样,靠双线性插值填补中心@ 本系列绘制

Dual filtering 是 Kawase 的改进版,核心改动在采样权重的设计上。Unity URP 的实现:

  • DualDownsample:$(1/8)\times(4\cdot c_0 + \text{四角})$
  • DualUpsample:$(1/12)\times(2\times\text{对角四采样} + \text{四轴向采样})$

其中 $c_0$ 是中心像素。两个式子的系数(1/8 与 1/12)都是归一化因子。

Dual filtering 的降采样与升采样权重
图 3-61DualDownsample 与 DualUpsample 的采样点与权重分配@ 本系列绘制
Dual 与 Kawase 的采样点对照
图 3-62Dual 与 Kawase 的采样点差异@ 本系列绘制

Unity 官方对三种模式的取舍有明确说法,这里值得原文转述其要点:

  • Gaussian:下采样高斯 + 双线性/双三次上采样,质量最佳。
  • Dual:Kawase 式 blur + 下/上采样,比 Gaussian 快但质量差;高分辨率下可能反而比 Kawase 更快。
  • Kawase:固定尺寸纹理,低分辨率下更快、省内存。

最后那条"高分辨率下 Dual 可能比 Kawase 更快"值得留意:三种形态之间没有全局最优,选择依赖目标分辨率——这正是 URP 把它们做成可切换选项的原因。

5.5 形态四:FFT 卷积

前面三种形态都在做同一件事的近似:用有限次采样逼近一次大核卷积。FFT 卷积走的是完全不同的路——把卷积搬到频域去做。

按卷积定理,空间域的卷积等于频域的逐点相乘:

$$ I * K \;\longleftrightarrow\; \mathcal{F}(I) \cdot \mathcal{F}(K) $$

其中 $\mathcal{F}$ 表示傅里叶变换(Fourier Transform)。所以做一次大核卷积可以拆成三步:正变换、逐点相乘、逆变换。

FFT 卷积 Bloom 的三步
图 3-63FFT 卷积:正变换 → 频域逐点相乘 → 逆变换@ 本系列绘制

Unreal 的 Bloom Convolution 就是这条路。官方文档说明它"用卷积核纹理建模相机与眼内光的散射与衍射",是能量守恒的卷积,且"底层使用 FFT 进行加速"。

意义在于:卷积核不再受"可分离"与"采样次数"限制。一张真实的镜头 PSF(比如带六边形散景与衍射环的核)可以直接作为核纹理喂进去,物理上远比其他三种准确——这就是前面说的"把光圈形状还给 Bloom"。代价是需要频域变换、按分辨率分配的临时缓冲与核纹理显存,所以官方定位在"过场与高端硬件"。

5.6 代码:核对卷积核的归一化

不管用哪种形态,有一件事必须做对——卷积核归一化。这是"能量守恒"在实现层面的最低要求。

//-----------------------------------【Bloom 卷积核校验】-------------------------------------
// 描述:核不归一化 = Bloom 会让整图变亮;这里给出运行时自检
//------------------------------------------------------------------------------------------
//【1】累加核内全部权重
//【2】与 1.0 比较,超出容差即告警
//【3】归一化后再用于卷积
float NormalizeKernel(inout float weights[MAX_TAPS])
{
    //【1】求和
    float sum = 0;
    [unroll]
    for (int i = 0; i < MAX_TAPS; ++i) sum += weights[i];

    //【2】自检:偏离 1.0 超过 1e-3 说明核设计有问题
    if (abs(sum - 1.0) > 1e-3)
        printf("Bloom kernel not normalized: sum=%.6f (expect 1.0)", sum);

    //【3】归一化:保证 Bloom 只搬运能量,不创造能量
    [unroll]
    for (int j = 0; j < MAX_TAPS; ++j) weights[j] /= sum;

    return sum;
}

我们正式开始。下面对这段特别说明一点:它的价值在于把"能量守恒"从口头约定变成可执行断言。 Standard Bloom 不守恒是对的(叠加系数本就不为 1);但一旦换成卷积形态并声称守恒,核归一化就是必须成立的硬条件。

5.7 演进脉络

把 Bloom 的演进按时间串起来,能看清每一步是为什么而发生。

Bloom 的演进脉络
图 3-64Bloom 的演进脉络:从分离卷积到金字塔滤波再到频域卷积@ 本系列绘制

同一套分层机制,换一个视角看它的层级结构:

引擎中的 glow 层级结构:各层尺度与强度的对应关系
图 3-65引擎中的 glow 层级结构:各层尺度与强度的对应关系@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html
阶段 时间 关键变化 动机
分离卷积 Glow GPU Gems,2004 二维卷积拆成两遍一维 把 $O(d^2)$ 降到 $O(2d)$
低分辨率渲染 同期 在 1/3–1/4 分辨率上做 Bloom 填充率节省 9–16 倍
金字塔滤波 COD AW,2014 13-tap 降采样 + 3×3 tent 升采样 + 萤火虫抑制 时域稳定性与鲁棒性
多模式并存 URP / HDRP Gaussian / Kawase / Dual 三选一 按分辨率与内存约束取舍
频域卷积 Unreal Bloom Convolution FFT + 卷积核纹理 能量守恒 + 可建模真实 PSF

年份口径说明:GPU Gems 第 21 章的《Real-Time Glow》成文于 2004 年;COD AW 的材料来自 SIGGRAPH 2014 课程;Unreal 的 Convolution Bloom 与 Unity URP 的多模式是后续版本引入,具体版本随引擎迭代变化,故表中不标年。

四、色调映射:为什么不能线性截断

6.1 先看一个数值:线性截断会丢掉什么

假设场景渲染出来的线性值是 0 到 1000 尼特,显示器能表达 0 到 200 尼特。最朴素的做法是线性截断(Clamp):超过 200 的全部当 200。

结果会怎样?一个亮度 205 的灯泡和一个亮度 900 的正午天空,输出完全一样——都是纯白。动态范围里最亮的那一段被整体剁掉了,而这恰恰是人眼最敏感、观感上最有价值的区域(高光层次、发光体的质感)。

这不是"损失一点精度",而是信息的结构性丢失:被剁掉的部分再也无法在后续任何步骤中恢复。

线性截断与曲线映射的差异
图 3-66线性截断把 200 以上的全部压成纯白;曲线映射把高光压缩成渐变的层次@ 本系列绘制
截断与曲线映射的信息保留
图 3-67截断与曲线映射的信息保留对照@ 本系列绘制

下面这张是典型的"HDR 输入"——室内与窗外的亮度差了三到四个数量级。如果直接线性映射,窗外的部分会变成一片惨白,室内则一团黑。

室内与室外亮度差异极大的 HDR 场景
图 3-68典型的 HDR 场景:室内外亮度差数个数量级,正是色调映射要处理的输入@ Wikimedia Commons · Sebastian Nibisz(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File:HDRI_Sample_Scene_Window.jpg

色调映射的直接结果,可以看这张实拍:

色调映射后的实拍结果:高光被压回可显示范围,暗部细节同时保留
图 3-69色调映射后的实拍结果:高光被压回可显示范围,暗部细节同时保留@ Guido Radig(CC BY 3.0)· https://commons.wikimedia.org/wiki/File:Darmstadt_-_Mathildenh%C3%B6he_(tone-mapping).jpg

先看一张未做色调映射的 HDR 画面——高光处已经溢出:

未色调映射的 HDR 画面:高光远超显示范围,线性截断会丢层次
图 3-70未色调映射的 HDR 画面:高光远超显示范围,线性截断会丢层次@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/HDR

直接截断成 LDR 之后,高光细节被削平:

线性截断的结果:超出 1.0 的部分被直接削掉,高光成片死白
图 3-71线性截断的结果:超出 1.0 的部分被直接削掉,高光成片死白@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/HDR

曝光多加一档,高光就整片糊死:

+1EV 过曝实拍:亮度超出上限的部分被截断成同一片纯白,层次全部丢失
图 3-72+1EV 过曝实拍:亮度超出上限的部分被截断成同一片纯白,层次全部丢失@ Wikimedia Commons · Kapitel(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File%3AEV%2B1%20%28test%29.jpg

6.2 色调映射的定义:一条作用在亮度上的曲线

剑桥大学 AGIP 课程的讲义给了色调映射一个很清爽的定义:它通常表现为一条一维曲线,把场景参考值映射到显示参考值。

$$ L_d = f(L_s) $$

其中:

  • $L_s$ 是场景亮度(Scene-referred),线性、无上限。
  • $L_d$ 是显示亮度(Display-referred),线性、限制在 $[0, L_{\text{max}}]$。
  • $f$ 就是色调映射算子(Tone Mapping Operator, TMO)。
色调映射曲线的三段结构
图 3-73色调映射曲线的典型三段:暗部 toe 提亮、中段近似线性、高光 shoulder 压缩@ 本系列绘制
色调映射在管线中的位置
图 3-74色调映射在管线中的位置@ 本系列绘制

开始正文。这条曲线的形状决定了画面的性格。三个区域各司其职:toe(暗部) 决定阴影里的细节保留多少;中段 通常保持近似线性,因为它承担了大部分可见内容;shoulder(高光) 决定高光区被压得多柔和——压得越陡,高光越早变白。

6.3 场景参考与显示参考:两种不同的"真实"

上一步引出一个更基础的问题:为什么不能"忠实还原"?

答案是:"忠实"这个词在两端指的不是同一件事。

场景参考(Scene-referred)图像描述的是物理光量,每个像素值对应一个可测量的辐射亮度。显示参考(Display-referred)图像描述的是显示设备上的编码值,每个像素值对应一个设备响应。前者有物理意义,后者有设备意义。

正餐开始。物理学角度最"忠实"的做法,是把场景亮度原样搬到显示器上。但前面算过了——两者的动态范围差着十几个数量级,做不到。于是必须做取舍,而取舍就意味着放弃"物理忠实",转向"感知忠实"。

场景参考与显示参考的转换
图 3-75从场景参考到显示参考:一条有损的、以感知为目标的映射@ 本系列绘制

感知忠实的目标可以概括为:让观察者在显示器上看到的相对亮度关系,尽可能接近他在真实场景中的感受。 注意这里的关键词是"感受",不是"数值"。

6.4 一个具体的工程问题:中灰会被显示成白

NVIDIA 的开发者文章《Preparing for Real HDR》里给了一个很扎实的例子,值得完整复述,因为它把"为什么经典算子不够用"讲得很具体。

问题在于:经典算子(最典型是 Reinhard)的输入只有一个——图像里的最大亮度,它不知道输出设备实际能到多亮。

依然是国际惯例,于是会出现这种事:把算子的 $[0,1]$ 输出直接映射到 1000 尼特显示器上,画面里的"中灰"(比如沥青路面)本应对应几十尼特,现在被显示成几百尼特——它看起来和普通 SDR 显示器上的白色一样亮。

中灰在错误映射下被显示成白
图 3-76只知最大亮度、不知设备亮度,会导致中灰被抬到"白"的观感位置@ 本系列绘制
中灰锚点的三种放置
图 3-77中灰锚点的三种放置@ 本系列绘制
曝光补偿的对数刻度
图 3-78曝光补偿的对数刻度@ 本系列绘制

这就是为什么工业界的做法最终转向了与显示设备能力绑定的映射,而不是纯图像自适应的映射。这也是本章后面要讲的曝光(Exposure) 为什么必须参与进来的原因——曝光就是"这个设备该把中灰放在哪个亮度上"的答案。

6.5 曝光与档(Stop)

摄影的曝光概念提供了一套对数尺度:一档(Stop,即 EV)代表亮度翻倍。

$$ \text{EV} = \log_2 \frac{L}{L_{\text{ref}}} $$

其中:

  • $L$ 是当前亮度。
  • $L_{\text{ref}}$ 是参考亮度(相机里通常取中灰对应的亮度)。
  • $\text{EV}$ 是相对于参考的档数,正数表示更亮。

用对数尺度的好处是把乘法变成加法:调整曝光是加减几档,而不是乘除系数。Unreal 4.25 之后的 EV100 体系正是这个思路。

亮度与 EV 档数的对数关系
图 3-79亮度与 EV 档数的对数关系:每翻一倍亮度,档数加一@ 本系列绘制

曝光对画面的作用,可以用一组连续的帧直观看到:

不同曝光下的同一场景:曝光决定进入色调映射的动态范围
图 3-80不同曝光下的同一场景:曝光决定进入色调映射的动态范围@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/HDR

反过来减三档,画面整体压暗但高光层次还在:

−3EV 欠曝实拍:同一场景压暗三档,高光信息得以保留,代价是暗部进入噪声区
图 3-81−3EV 欠曝实拍:同一场景压暗三档,高光信息得以保留,代价是暗部进入噪声区@ Wikimedia Commons · Kapitel(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File%3AEV-3%20%28test%29.jpg

同一场景不同曝光时长的系列,可以直观读出“档”的含义:

不同曝光时长的系列对照:曝光时长每延长一倍即增加一档
图 3-82不同曝光时长的系列对照:曝光时长每延长一倍即增加一档@ Wikimedia Commons · Markus Mayer(CC BY-SA 3.0)· https://commons.wikimedia.org/wiki/File%3AEinfluss%20der%20Zeit%20auf%20die%20Belichtung.jpg

6.6 代码:一个可用但会过曝的映射

先给一个"能用但不够好"的实现,作为后面算子谱系的对照基线。

//-----------------------------------【色调映射:线性截断基线】-------------------------------
// 描述:本文的对照基线——把 HDR 线性值按曝光缩放后直接截断
//------------------------------------------------------------------------------------------
//【1】按曝光缩放:exposure 通常来自自动曝光或物理相机参数
//【2】截断到 [0,1]:这一步就是"线性截断"
//【3】编码到 sRGB:注意这一步是显示编码,不是色调映射的一部分
float3 ToneMapClamp(float3 hdrColor, float exposure)
{
    //【1】曝光:等价于调整"中灰落在哪个尼特"
    float3 c = hdrColor * exposure;

    //【2】线性截断:超过 1.0 的全部变成 1.0,高光层次全部丢失
    c = saturate(c);

    //【3】sRGB 编码(近似 gamma 2.2)
    return pow(c, 1.0 / 2.2);
}

依然是惯例,先看看下面对这段特别说明一点:saturate 那一行就是全部问题的来源——它把"高光压缩"简化成了"高光丢弃",后面所有算子的作用本质上都是在替代它。注意曝光与色调映射是两件事,这里写在一个函数里只为对照;工程上曝光通常在更早阶段完成,因为 Bloom 的阈值也依赖曝光后的值。

6.7 效果图:压缩之后的观感

同样的场景用曲线映射之后,高光区会呈现出层次——这是色调映射最直观的收益。

HDR 照片压缩到 SDR 之后的观感
图 3-83一张 HDR 实拍:高光与暗部被同时保留,这是色调映射追求的目标观感@ Wikimedia Commons · Vipulvaibhav5(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File:High_Dynamic_Range_photograph_of_Linga_Temple.jpg
HDR 与 LDR 的色调映射前后对比
图 3-84色调映射的前后对照:左为相机自动曝光(LDR)的结果,右为同一场景 HDR 经 Mantiuk 算子映射后的结果——高光与暗部层次被同时保留@ Wikimedia Commons · Axel Jacobs / Hdruser(CC BY-SA 2.0)· https://commons.wikimedia.org/wiki/File:Igreja_Nossa_Senhora_de_Fatima_(HDR_LDR_comparison).jpg
四种色调映射算子的同场景对比
图 3-85同一 HDR 场景经四种算子映射的结果(顺时针:Reinhard 摄影色调复现 / Fattal 梯度域压缩 / Mantiuk 感知对比度框架 / Reinhard 感光细胞生理启发)——同一输入的"取舍"可以差这么多@ Wikimedia Commons · Axel Jacobs(摄影)/ Hdruser(衍生)(CC BY-SA 2.0)· https://commons.wikimedia.org/wiki/File:Tone_Mapping_Methods.jpg

把 HDR 与 Bloom 放在一起,引擎侧的完整效果是这样的:

引擎中的 HDR 与 Bloom 联动:高动态范围加上光晕后的画面
图 3-86引擎中的 HDR 与 Bloom 联动:高动态范围加上光晕后的画面@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html

一张实际做过色调映射的照片是这样:

色调映射实景:高光与暗部同时保留,观感比线性截断自然得多
图 3-87色调映射实景:高光与暗部同时保留,观感比线性截断自然得多@ Wikimedia Commons · JeanNeef(CC BY-SA 3.0)· https://commons.wikimedia.org/wiki/File%3AKirche%20Falkenhain%20HDR%20tone%20mapping.jpg

同一思路的另一条落地方式是曝光融合——把多张不同曝光的照片合成出一张:

曝光融合:多档曝光合成一张,本质是把动态范围压缩进显示区间
图 3-88曝光融合:多档曝光合成一张,本质是把动态范围压缩进显示区间@ Wikimedia Commons · Slaunger(CC BY-SA 3.0)· https://commons.wikimedia.org/wiki/File%3AM%C3%B8nsted%20kalkgruber%20exposure%20fused%202014-07-18.jpg

五、算子谱系:Reinhard / Hable / ACES / AgX

7.1 先给全景

色调映射算子不是一个一个独立发明的,它们背后是几种不同的出发点。按出发点归类,主流算子可以分成四支。

四种色调映射算子的对比
图 3-89四种主流色调映射算子的对比:公式、参数、高光表现与色相倾向@ 本系列绘制
算子 出发点 关键参数 高光表现 色彩倾向
Reinhard 摄影曝光 白点 $L_{\text{white}}$ 整体偏灰、高光弱 逐通道应用会去饱和
Hable / Filmic 胶片响应 A–F 六参数 + 白点 W 可控的 toe/shoulder 可控,中性偏暖
ACES Filmic 电影工业标准 无(拟合式) S 形、过渡平滑 高光保留较多饱和度
AgX 高能量输入 视实现 高光克制、不糊 偏"粉彩",不毁色

评价维度说明:高光表现指最亮区域的层次与压缩曲线形状;色彩倾向指在中高亮度区对饱和度的处理方式。这张表的依据是各算子的原始定义与官方文档,不是主观画质评分。

7.2 Reinhard:最简单的那个

Reinhard 算子的形式简单到可以一行写完:

$$ L_d = \frac{L'}{1 + L'} $$

其中:

  • $L'$ 是经过曝光缩放后的亮度。
  • 分母的 $1 + L'$ 保证了当 $L' \to \infty$ 时 $L_d \to 1$,天然不会超过 1.0。

OK,言归正传。它的优点是清晰:单调、无参数(基础版)、值域自动归一。缺点是曲线在暗部压得太平——L' = 0.1 时输出只有 0.09,几乎原样通过;而在高光端又压得太狠,L' = 10 时输出 0.909,L' = 100 时输出 0.990——从 10 到 100 差了 10 倍亮度,输出只差 0.08。高光几乎没有层次。

Reinhard 曲线
图 3-90Reinhard 曲线:形式最简、值域自动归一,但高光端压缩过狠、层次丢失@ 本系列绘制

Reinhard 算子的落地效果——下面是同一张 HDR 图压缩后的结果:

Reinhard 色调映射后的结果:高光被压缩、暗部保留
图 3-91Reinhard 色调映射后的结果:高光被压缩、暗部保留@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/HDR

改进版引入一个白点($L_{\text{white}}$),让指定亮度映射到纯白:

$$ L_d = \frac{L'\left(1 + \frac{L'}{L_{\text{white}}^2}\right)}{1 + L'} $$

这样一来,$L_{\text{white}}$ 以下的区域不会被过早压到白,高光层次因此改善。

还有一个容易踩的坑:Reinhard 应该作用在亮度上,而不是逐 RGB 通道。 逐通道应用会导致暗部严重去饱和——因为三个通道各自被压缩,通道间的比例被改变,颜色就"洗白"了。这一点在下文 7.6 节会细讲。

7.3 Hable / Filmic:把胶片的响应曲线参数化

John Hable 在 GDC 2010 的《Uncharted 2: HDR Lighting》里给出了一条参数化的胶片曲线,也就是社区常说的 Uncharted 2 Filmic。

它的形式是一组有理式,用 A–F 六个参数分别控制 toe、线性中段和 shoulder:

$$ f(x) = \frac{(x(ax+b))}{(x(cx+d)+e)} + f $$

其中:

  • $A, B$ 主要影响 toe(暗部抬起)。
  • $C, D, E$ 影响 shoulder(高光压缩)。
  • $F$ 是整体偏移。

实际使用时还要做一次归一化:先用一个给定的白点 $W$ 求 $f(W)$,再把结果除以它,使得 $W$ 映射到 1.0。这个白点参数就是 whiteScale。

//-----------------------------------【色调映射:Hable / Filmic】-----------------------------
// 描述:GDC 2010 Uncharted 2 的胶片曲线,A-F 六参数控制 toe/线性/shoulder
//------------------------------------------------------------------------------------------
//【1】曲线主体:一组有理式,分母非线性
//【2】归一化:用白点 W 求出 whiteScale,使其映射到 1.0
//【3】最终结果 = 曲线值 × whiteScale
float3 HableFilmic(float3 x)
{
    const float A = 0.15, B = 0.50, C = 0.10, D = 0.20, E = 0.02, F = 0.30;
    return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F;
}

float3 ToneMapFilmic(float3 color, float whitePoint)
{
    //【1】曲线主体
    float3 curr = HableFilmic(color * 2.0);

    //【2】归一化:让白点映射到 1.0
    float3 whiteScale = 1.0 / HableFilmic(whitePoint);

    //【3】输出
    return curr * whiteScale;
}

下面对这段特别说明两点。第一,那一步归一化不能省——省掉它,整张图会偏暗,因为曲线本身没有归一到 $[0,1]$。第二,注意 color * 2.0 这个系数在社区流传的版本里五花八门(有 1.0、2.0、2.2 等),它是配参数用的曝光前缩放,改它等于改整体曝光——它不是色调映射的一部分,这一点在对照不同版本的代码时要留意。

Hable / Filmic 曲线
图 3-92Hable 曲线:A–F 六参数分别作用于 toe、中段与 shoulder,形状可调@ 本系列绘制

7.4 ACES:从电影工业来的标准

OK,下面我们开始。ACES 全称 Academy Color Encoding System,是电影行业的一套色彩管理与色调映射体系。它的完整结构比前面两个复杂得多,但作为"算子"被游戏引擎广泛采用的,是一个简化的拟合式。

最常被引用的拟合版本来自 Krzysztof Narkowicz:

$$ f(x) = \frac{x(2.51x + 0.03)}{x(2.43x + 0.59) + 0.14} $$

其形状是一条 S 形曲线:相比 Reinhard,它的 toe 与 shoulder 压缩更温和,高光区的感知饱和度保留更多。这是它被 Unity 与 Unreal 广泛采用的原因。

ACES Filmic 拟合曲线
图 3-93ACES Filmic 的拟合曲线:S 形,高光区感知饱和度保留较多@ 本系列绘制

但要注意区分"拟合式"与"官方 ACES"。 上面的式子是对 ACES 观感的近似,不是 ACES 标准本身。官方的 ACES 体系是这样的结构:

  • ACES 1 定义 RRT(Reference Rendering Transform)+ ODT(Output Device Transform) 两段;1.1 起二者被拼接为统一的 Output Transform,以简化色调映射曲线的对应关系。
  • ACES 2 保留 Output Transform 作为总称,并把 Rendering Transform 拆成三步:Tone scale(J)→ Chroma Compression(M)→ Gamut Compression(J&M)。
  • 这三步在一个叫 JMh 的空间里进行,该空间基于 Hellwig 2022 的简化色貌模型(Color Appearance Model),目的是在压缩过程中保持色相不变。
ACES 体系的结构演进
图 3-94ACES 的结构:RRT + ODT,以及 ACES 2 拆出的 Tone scale / Chroma / Gamut 三步@ 本系列绘制
ACES 的 AP0 与 AP1 工作空间
图 3-95AP0 与 AP1 工作空间的关系@ 本系列绘制
  • 工作空间上,ACES2065-1(AP0) 是超宽色域的交换空间,ACEScg(AP1) 是渲染用的工作空间。
  • LMT(Look Modification Transform) 与色域压缩是独立环节,不混在 Output Transform 里。

RRT 的设计目标被官方描述为"让场景参考图像在显示器上观感愉悦"——注意,是"愉悦"不是"准确"。这句话很关键,它说明 ACES 从一开始就是感知导向的,而不是物理导向的。

7.5 AgX:为高能量输入而生

AgX 是较新的一支,现在是 Blender 的默认视图变换。它的设计出发点很具体:比 ACES 更优雅地处理高能量输入——强光源、火焰、自发光体。

OK,它要解决的问题是 ACES 在高光处的一个已知短板:强色偏(Hue Shift)。当亮度极高的饱和色被压缩时,ACES 会让色相往某个方向跑(典型是往黄色或白色偏),而 AgX 通过更克制的处理避免了这一点。

代价是观感。社区实测的普遍反馈是:AgX 最克制、不毁色,但整体偏"粉彩"(Pastel),对比不如 ACES 强;而 ACES 2.0 的中段过渡极平滑,反而容易让颜色发闷、缺少层次。

AgX 曲线与高光处理
图 3-96AgX 曲线:高能量区压缩更克制,避免 ACES 在高光处的强色偏@ 本系列绘制
ACES 与 AgX 的高光色相走向
图 3-97ACES 与 AgX 的高光色相走向@ 本系列绘制

社区(Blender 5.0 实拍的对比讨论)给出的选型经验值得记录:

darktable 的文档里还有一条规则值得抄下来:永远不要同时使用两个 tone mapper。 这句话背后是同一个道理——色调映射是非线性的,串联两次等于把曲线叠了两遍,结果不可预期。

7.6 四条曲线叠在一起看

把四条曲线画在同一张图上,差异一目了然。为了可比,这里统一把输入亮度按相同曝光缩放。

四条色调映射曲线叠画
图 3-98四条曲线的并排对照:Reinhard 高光最平、ACES 最 S 形、AgX 高光最克制@ 本系列绘制

从图上能读出三件事:

  1. 中段的斜率决定了对比度观感。 ACES 的中段最陡,所以画面最"有对比";Reinhard 最平,所以最"发灰"。
  2. toe 的形态决定了暗部细节。 Hable 的 toe 抬得最高,暗部细节最清楚。
  3. shoulder 的宽度决定了高光层次。 AgX 的 shoulder 最宽,高光过渡最长。

7.7 一个必须讲清的实现细节:作用在亮度还是通道

这是本文认为最值得强调的实现细节之一。

色调映射应该作用在亮度上,而不是逐 RGB 通道。

好了,我们正式开始。原因在于:逐通道应用会改变通道之间的比例关系,而通道比例就是颜色本身。当一个亮橙色的像素 $(R, G, B) = (1.0,\ 0.5,\ 0.1)$ 被逐通道压缩时,$R$ 通道最先饱和到接近 1,而 $B$ 通道还在低位——于是 $(0.95,\ 0.48,\ 0.10)$ 变成 $(1.0,\ 0.6,\ 0.2)$ 这类结果,橙色被"洗"向黄色或白色。这就是去饱和(Desaturation) 的成因。

正确的做法是:先算亮度,对亮度做映射,再把 RGB 按映射前后亮度的比值缩放。

//-----------------------------------【色调映射:亮度域应用】---------------------------------
// 描述:只在亮度上应用曲线,再按比例还原 RGB,避免逐通道去饱和
//------------------------------------------------------------------------------------------
//【1】求亮度:用 Rec.709 权重
//【2】对亮度应用曲线,得到新的亮度
//【3】用比值缩放 RGB,保持通道比例(即保持色相与饱和度)
float3 ToneMapLuminance(float3 color, float exposure)
{
    //【1】亮度
    float luma = dot(color, float3(0.2126, 0.7152, 0.0722));
    float lumaExposed = luma * exposure;

    //【2】只在亮度上做映射
    float lumaMapped = ReinhardLuminance(lumaExposed);

    //【3】按比值缩放:色彩比例不变,因此不会去饱和
    float scale = (luma > 1e-5) ? (lumaMapped / lumaExposed) : 0.0;
    return color * exposure * scale;
}

下面对这段特别说明一点:分母的 luma > 1e-5 判断是必要的。 纯黑像素的亮度接近 0,直接做除法会得到 NaN 并污染整个画面。这个细节在截图之外很难发现,因为 NaN 通常只在特定视角或特定场景下出现。

逐通道与亮度域映射的去饱和对照
图 3-99逐通道映射改变了通道比例,导致高光去饱和;亮度域映射保持比例不变@ 本系列绘制
去饱和在色度图上的轨迹
图 3-100去饱和在色度图上的轨迹@ 本系列绘制

色调映射之后通常还会接一步色彩调整:

色彩调整(adjustments):在色调映射之后对亮度、对比、饱和度做修正
图 3-101色彩调整(adjustments):在色调映射之后对亮度、对比、饱和度做修正@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html

7.8 演进脉络

色调映射的演进时间线
图 3-102色调映射的演进脉络:从摄影曝光到电影标准再到高能量处理@ 本系列绘制
阶段 时间 提出者/来源 关键贡献
Reinhard 2002 Reinhard et al. 首个被广泛采用的实用算子
Filmic / Hable GDC 2010 John Hable(Naughty Dog) 把胶片响应参数化,可调 toe/shoulder
ACES 2014 起 Academy(AMPAS) 电影工业标准,含 RRT/ODT 与工作空间体系
ACES Filmic 拟合 2015 Krzysztof Narkowicz 简化式,被引擎广泛采用
AgX 近年 社区(现为 Blender 默认) 针对高能量输入,克制高光色偏

好了,年份口径说明:Reinhard 算子的原始论文发表于 2002 年;Hable 的演讲在 GDC 2010;ACES 体系自 2014 年起陆续发布(后续有 1.1、2.0 等版本迭代);Narkowicz 的拟合式发布于 2015 年;AgX 的具体发表年份随实现演进,故不单标年。

六、曝光与色域:映射之外的两件事

8.1 曝光为什么必须参与

前一章留了一个伏笔:Reinhard 这类算子只知道"图像里最亮是多少",不知道"显示器能到多亮"。解决这个问题需要引入一个与设备绑定的量——曝光(Exposure)。

思路很直接:曝光决定中灰(Middle Gray)落在哪个亮度上。一旦中灰的位置定了,剩下的映射就只是围绕这个锚点展开。

Unreal 的 Auto Exposure(社区俗称"眼适应",Eye Adaptation)就是这套机制的工程实现,在 Post Process Volume 中配置。它模拟的是人眼从暗室走到亮室外的适应过程:场景整体变亮时,逐渐降低曝光;变暗时,逐渐提高曝光。

自动曝光与眼适应
图 3-103自动曝光:随场景亮度调整曝光,模拟人眼的适应过程@ 本系列绘制
曝光在 Bloom 之前的原因
图 3-104曝光必须早于 Bloom@ 本系列绘制

关键参数有两个:

  • Apply Physical Camera Exposure(物理相机曝光):启用后用快门速度(Shutter Speed)、ISO 与光圈(Aperture)三个物理量计算曝光,而不是直接给一个 EV 值。这让美术可以用"摄影语言"调画面。
  • Bias(曝光补偿):在对数尺度上微调曝光,等价于摄影里的 EV 补偿。这是最常被美术使用的参数。

真实相机处理这件事的办法是包围曝光——同一场景连拍多档,再挑一档或做合成:

曝光包围条:同一场景的欠曝、正常、过曝三档并排,动态范围的取舍一目了然
图 3-105曝光包围条:同一场景的欠曝、正常、过曝三档并排,动态范围的取舍一目了然@ Wikimedia Commons · The original uploader was Imroy at English Wikipedia .(CC BY-SA 2.5)· https://commons.wikimedia.org/wiki/File%3AHDR%20example%20-%20exposure.jpeg

8.2 EV100 体系:把曝光变成可解释的量

Unreal 在 4.25 引入了 EV100 体系,并配套了 HDR 可视化工具(ShowFlag.VisualizeHDR 1)。这个改进值得单讲,因为它把"调曝光"从经验试错变成了可读的诊断。

EV100 的含义是:以 ISO 100 为基准,当前场景亮度对应的曝光值。Unreal 的可视化里:

  • 蓝线表示目标 EV100(自动曝光算出来的目标值)
  • 直方图显示画面亮度分布
  • 两者的相对位置说明当前曝光是偏亮还是偏暗,以及亮度分布是否超出了显示能力
EV100 可视化
图 3-106EV100 可视化:目标 EV100 与亮度直方图的叠加,用于判断曝光是否合理@ 本系列绘制

顺带一提,Unreal 自 4.15 起默认启用符合 ACES 标准的 filmic tonemapper——这也说明曝光与色调映射在工程上是配套设计的,不是两个独立的开关。

8.3 HDR 显示标准:管线差异在哪

前面讨论的"显示范围"一直是个笼统概念。实际工程中它取决于走哪套显示标准:

标准 色域 传递函数 位深
SDR BT.709 γ2.4 8 bit
HDR10 BT.2020 PQ 10 bit
Dolby Vision BT.2020 PQ 12 bit

三处差异各自意味着什么:

PQ 与 γ2.4 传递函数曲线
图 3-107PQ 与 γ2.4 的传递函数曲线:PQ 在暗部与高光都分配了更多编码精度@ 本系列绘制
HLG 与 PQ 的曲线差异
图 3-108HLG 与 PQ 的曲线差异@ 本系列绘制
位深与可见色带的关系
图 3-109位深与可见色带的关系@ 本系列绘制

PQ 模式下所有参数都参与色调映射,几个关键量:

  • White level:设定参考白的亮度。它决定了"多亮算白"。
  • MaxCLL(Maximum Content Light Level):内容中单帧最亮像素的峰值亮度。
  • MaxFALL(Maximum Frame-Average Light Level):内容中单帧的平均亮度上限。

后两个参数的意义在于:它们让显示设备知道"这片内容最亮会到多少",从而可以按内容做局部调光策略。Gamma 模式则牺牲亮度范围换取"10 bit 全用于色深"。

引擎里的 HDR 相关开关集中在环境设置这一层:

引擎的 HDR 设置:色调映射、白点与曝光在这一层统一配置
图 3-110引擎的 HDR 设置:色调映射、白点与曝光在这一层统一配置@ Godot 官方文档(CC BY 4.0) · Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html

8.4 色域:HDR 多出来的那片颜色

色域(Color Gamut)描述的是"一个系统能表示哪些颜色",通常画在 CIE 1931 色度图上。

下图对比了几套常见标准覆盖的范围。

色域对比:NTSC / BT.709 / P3
图 3-111CIE 1931 色度图上的色域对比:NTSC 1953、BT.709 与 DCI-P3 的覆盖范围@ Wikimedia Commons · Myndex(CC BY 4.0)· https://commons.wikimedia.org/wiki/File:CIE1931xy_gamut_comparison_CC_v06_NTSC_709_P3.png

BT.2020 比它们更宽——它是为 UHD 时代设计的,覆盖了 DCI-P3 之外更大的一块。

BT.2020 与 sRGB 的色域差异
图 3-112BT.2020 与 sRGB 的色域边界对比:HDR 多出来的那片颜色@ Wikimedia Commons · Myndex(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File:CIE1931xy_gamut_comparison_of_sRGB_P3_Rec2020.svg

这件事对色调映射的直接影响是:HDR 内容转到 SDR 时,多出来的那片颜色必须被处理掉。 官方文档给出的两种做法是:

  • Clip(硬裁切):超出 BT.709 边界的颜色直接压到边界上。快,但饱和色会出现可见的色相跳变。
  • Compress(渐进压缩):把超出的部分平滑地压缩到边界内。慢,但色相连续性更好。
色域映射:Clip 与 Compress
图 3-113色域映射的两种做法:Clip 直接裁切(快但有色相跳变);Compress 渐进压缩(保持色相连续)@ 本系列绘制
Clip 与 Compress 的直观对照
图 3-114Clip 与 Compress 的直观对照@ 本系列绘制

亮度/对比/饱和三者的作用,可以分开看:

亮度—对比—饱和度(BCS)调整:三者分别作用于不同的视觉维度
图 3-115亮度—对比—饱和度(BCS)调整:三者分别作用于不同的视觉维度@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html

这与前面讲的"线性截断 vs 曲线映射"是同一类取舍——都是"快速丢弃"与"平滑压缩"之间的选择,只不过一个发生在亮度维度,另一个发生在色度维度。

8.5 一张 HDR 管线的整体对照

把前面的内容收进一张表,看清 SDR 与 HDR 管线在各环节的差异。

SDR 与 HDR 管线的差异
图 3-116SDR 与 HDR 管线的逐环节对照:色域、传递函数、位深与色调映射的参与方式@ 本系列绘制

HDR 管线里还有一步常被忽略的环节——色彩分级(3D LUT):

引擎中的 3D LUT 色彩分级:色调映射之后再做一次色彩整形
图 3-117引擎中的 3D LUT 色彩分级:色调映射之后再做一次色彩整形@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html
环节 SDR 管线 HDR 管线
工作色域 BT.709 BT.2020(HDR10)/ 依标准
传递函数 γ2.4 PQ 或 HLG
位深 8 bit 10 / 12 bit
色调映射参与度 固定曲线 所有参数都参与(White level / MaxCLL / MaxFALL)
色域处理 无需(同域) 需 Clip 或 Compress

LUT 本身是一张被摊平的三维查找表,长这样:

3D LUT 摊平后的模板:三维查找表按格铺开成一条长条,运行时按 RGB 三轴索引
图 3-1183D LUT 摊平后的模板:三维查找表按格铺开成一条长条,运行时按 RGB 三轴索引@ Godot 官方文档(CC BY 4.0) · Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html

七、抗锯齿的靶区:三类别名与 Nyquist 视角

9.1 走样的本质:采样率不够

抗锯齿的教材通常从 Nyquist 采样定理讲起,这里也这么开。

采样定理(Nyquist–Shannon Sampling Theorem) 说的是:要把一个信号完整还原,采样频率必须至少是信号最高频率的两倍。低于这个下限,高频成分就会"折叠"到低频上去,形成混叠(Aliasing)。

$$ f_s > 2 f_{\max} $$

其中:

  • $f_s$ 是采样频率。
  • $f_{\max}$ 是信号中的最高频率。
  • $2 f_{\max}$ 称为 Nyquist 频率。
采样率不足导致混叠
图 3-119采样率不足时高频折叠为低频:样本点重建出的波形与原信号完全不符@ 本系列绘制
空间频率与采样点密度
图 3-120空间频率与采样点密度@ 本系列绘制

先看没有抗锯齿时的样子——这是走样最直观的形态:

未启用抗锯齿:几何边缘出现明显的阶梯状锯齿
图 3-121未启用抗锯齿:几何边缘出现明显的阶梯状锯齿@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/3d_antialiasing.html

走样在几何边缘上的表现是这样的:

几何走样:低采样率下边缘出现明显的阶梯状锯齿
图 3-122几何走样:低采样率下边缘出现明显的阶梯状锯齿@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-OpenGL/Anti-Aliasing

马里兰大学 CMSC 427 的课程讲义给出的解决方向有三条,正好对应抗锯齿的三条技术路线:

  1. 过采样(Oversampling)——提高采样率。对应 SSAA。
  2. 预滤波(Pre-filtering)——在采样前把高频滤掉。对应 mipmap。
  3. 随机采样(Stochastic Sampling)——把规则采样换成随机分布。对应抖动与 TAA。

采样率不够的走样在真实摄影里随处可见——拍一块 LED 屏,传感器像素栅格与屏幕像素栅格拍频,就出现摩尔纹:

屏幕摩尔纹:传感器采样率与屏幕像素栅格不匹配产生的拍频花纹,与渲染里采样不足是同一件事
图 3-123屏幕摩尔纹:传感器采样率与屏幕像素栅格不匹配产生的拍频花纹,与渲染里采样不足是同一件事@ Wikimedia Commons · 4084470 0.smil(CC BY 4.0)· https://commons.wikimedia.org/wiki/File%3AMoir%C3%A9%20pattern%20on%20LED%20Screen%20at%20Kunshannan%20Railway%20Station%20in%20Suzhou%2C%20Jiangsu%2C%20China%2020210528.jpg

9.2 渲染里的三种别名

渲染中的走样不只一种。把靶区分清,才知道每种方法在打什么。

别名类型 成因 表现 对应手段
几何别名(Geometric) 几何边缘处可见性跳变 锯齿状的边缘 MSAA / SSAA / CSAA
着色别名(Shading) 高频着色(高光、细节纹理) 高光闪烁、纹理噪点 SSAA / 后处理 AA / TAA
时间别名(Temporal) 帧间采样位置固定 静止闪烁、运动中的爬行 TAA / 超分
三种别名与各自的靶区
图 3-124三种别名:几何、着色、时间,各自对应不同的抗锯齿手段@ 本系列绘制
同一画面上三种别名的共存区域
图 3-125三种别名在同一画面上的共存@ 本系列绘制
mipmap 已解决与未解决的范围
图 3-126mipmap 的预滤波作用范围@ 本系列绘制

这个三分法是本文认为最有价值的一个视角,原因是:它解释了为什么没有一种 AA 能解决全部问题。

  • MSAA 只采样可见性,所以它对几何别名有效,对着色别名完全无效——因为着色只做了一次。
  • 后处理 AA 在已经渲染好的图像上做边缘检测与软化,能改善几何边缘的观感,但对已经丢失的高频着色信息无能为力。
  • TAA 通过多帧累积把两类空间别名都摊薄,但引入了时间维度特有的新问题:鬼影。
几何走样的成因
图 3-127几何走样:边缘处一个像素的可见性在几何内外跳变,采样只能二选一@ 本系列绘制
着色走样的成因
图 3-128着色走样:单个像素内的高光变化被一次着色采样漏掉,表现为闪烁的高光点@ 本系列绘制
时间走样的成因
图 3-129时间走样:采样位置逐帧固定,导致闪烁与边缘爬行@ 本系列绘制

纹理走样最经典的形态是摩尔纹——它是采样率不足的直接产物:

摩尔纹:屏幕像素与纹理频率不一致时出现的干涉条纹
图 3-130摩尔纹:屏幕像素与纹理频率不一致时出现的干涉条纹@ Brocken Inaglory(CC BY-SA 3.0)· https://commons.wikimedia.org/wiki/File:Moir%C3%A9_fringes_IMG_3712.jpg

摩尔纹是三种别名里最容易被忽略的那一种:

摩尔纹走样:图案频率接近采样频率时,会出现原图并不存在的低频花纹
图 3-131摩尔纹走样:图案频率接近采样频率时,会出现原图并不存在的低频花纹@ Wikimedia Commons · Pixelmaniac pictures ( Leave a reply )(CC0)· https://commons.wikimedia.org/wiki/File%3AMoir%C3%A9%20pattern%20with%20screen.jpg

9.3 mipmap 已经解决了什么,还剩什么

一个常见困惑是:既然有 mipmap 了,为什么还会有纹理走样?

CMSC 425 的课程材料把这个边界划得很清楚:mipmap 解决的是纹理走样,不解决几何走样。 这两件事的成因不同。

  • 纹理走样来自纹理坐标在屏幕空间的收缩——一个像素覆盖了很多纹素。mipmap 通过预滤波(选合适层级的、已降采样的纹理)解决它,这正是前面说的"预滤波"路线。
  • 几何走样来自可见性的二元跳变——一个像素要么在三角形内,要么在外,没有中间状态。mipmap 无法解决,因为它根本不涉及几何边缘。
mipmap 的作用边界
图 3-132mipmap 通过预滤波解决了纹理走样,但几何边缘的可见性跳变它管不到@ 本系列绘制

顺带说一句,这也解释了为什么"把纹理质量调到最高"对锯齿边缘毫无帮助。

9.4 方法的完整清单与归类

按产生的时期与机制,抗锯齿方法可以归成四代。

抗锯齿方法的完整清单与归类
图 3-133抗锯齿方法按代数归类:超采样 → 硬件多重采样 → 后处理 → 时序@ 本系列绘制
代数 方法 机制 靶区
第一代:超采样 SSAA、Downscaling 高分辨率渲染后降采样 几何 + 着色
第二代:硬件多重采样 MSAA、CSAA、SGSSAA、TXAA 多采样可见性,单次着色 几何(SGSSAA 兼及着色)
第三代:后处理 FXAA、MLAA、SMAA、CMAA、AXAA 边缘检测 + 形态学软化 几何(近似)
第四代:时序 TAA、TSSAA、TSR、TAAU 子像素抖动 + 历史累积 几何 + 着色 + 时间
交叉:超分 DLSS、FSR、XeSS 低分辨率渲染 + 时序上采样 全部(内置)

言归正传。Valve 的开发者社区文档给出了各方法的 Performance Impact 等级,可以作为量级参照:SSAA 为 Very High,MSAA 为 Moderate 到 High,SGSSAA 为 High 到 Very High。同一份文档还解释了延迟渲染下 MSAA 实现复杂、收益下降——这正是 Black Mesa、Apex Legends 等作品移除 MSAA 的原因,也与本系列第一篇讲的 G-Buffer 直接相关。

9.5 一个被多篇文章写错的地方

写到这里必须纠正一个流传很广的说法。不少中文文章把引擎里的 AA 参数写成"TXAA 的 Jitter Amount、Temporal Sample Count",并声称这是 Unreal 的官方参数。

Unreal 的官方控制台变量是这些:

  • r.AntiAliasingMethod:0 = Off、1 = FXAA、2 = TAA、3 = MSAA
  • r.TemporalAA.Quality:0 = 关闭输入滤波、1 = 开启、2 = 开启并带基于移动性的抗鬼影(默认)
  • r.FXAA.Quality:0 = Console、1/2/3 = PC Medium 的 3/5/8 采样、4 = PC Low 的 12 采样(默认)、5 = PC Extreme

另外 Unreal 官方文档给出的 AA 方法可用性矩阵里有一条常被忽略:MSAA 只支持 Mobile 前向渲染器与桌面/主机的前向渲染器,延迟渲染器不支持。 这与第一条系列文章讲的延迟渲染 G-Buffer 带宽问题是同一件事的两面。

TXAA 是 NVIDIA 早期的时序抗锯齿方案,与 Unreal 的 TAA 不是同一个东西。把参数名混在一起写,会让读者在引擎里根本找不到对应项。

9.6 本节参考资料与量化结论

  • Unreal《Anti-Aliasing and Upscaling》官方文档:https://dev.epicgames.com/documentation/en-us/unreal-engine/anti-aliasing-and-upscaling-in-unreal-engine
  • Unreal《Temporal Super Resolution》官方文档:https://dev.epicgames.com/documentation/unreal-engine/temporal-super-resolution-in-unreal-engine
  • Valve Developer Community《Anti-aliasing》:https://developer.valvesoftware.com/wiki/Anti-aliasing
  • UMD CMSC 427《Sampling and Aliasing》讲义:http://www.cs.umd.edu/class/fall2009/cmsc427/Lects/chapt11-sampling-alias-6.pdf
  • CS425《Visual Appearance: Antialiasing and Transparency》讲义:https://fmiranda.me/courses/cs425-slides/14-visual-appearance.pdf
  • 北京大学《可视计算与交互概论》反走样章节:https://vcl-pku.github.io/vci-book/getting-started/anti-aliasing/aa-principle.html
  • 《Decoupled Coverage Anti-Aliasing》(ACM):https://dl.acm.org/doi/pdf/10.1145/2790060.2790068
  • 《Surface Based Anti-Aliasing》(Salvi 2012):https://www.sci-hub.st/storage/2024/3672/03c9b9b905dca1fcedac86e978af6f4b/salvi2012.pdf
结论 数据
Nyquist 下限 $f_s > 2 f_{\max}$,低于则混叠
抗锯齿的三条路线 过采样 / 预滤波 / 随机采样
别名分类 几何、着色、时间三类,靶区互不重叠
mipmap 的作用边界 解决纹理走样,不解决几何走样
Valve 给出的开销等级 SSAA Very High;MSAA Moderate–High;SGSSAA High–Very High
Unreal 的 MSAA 支持范围 仅 Mobile 前向与桌面/主机前向,延迟渲染不支持

八、空间方法:MSAA 与后处理 AA 的取舍

10.1 MSAA 的核心:解耦可见性与着色

MSAA(Multisample Anti-Aliasing)是硬件时代最重要的一个 AA 方案,它的设计思想可以用一句话概括:

在每个像素的多个位置上采样可见性,但只着色一次。

这是 ACM 论文《Decoupled Coverage Anti-Aliasing》里对 MSAA 的精确定位。它带来的收益很直接:几何边缘的覆盖率被以 1/N 的精度算出来,而着色成本与不开 AA 时完全相同。

MSAA 的可见性与着色解耦
图 3-134MSAA 的核心理念:多次采样可见性(覆盖率),着色仍然只做一次@ 本系列绘制
MSAA 的采样点图案
图 3-135MSAA 的采样点图案:4x 为旋转网格,8x 为更密的旋转网格@ 本系列绘制

MSAA 打开后的实际差别:

引擎中的 MSAA 4x:几何边缘的锯齿被多采样抹平
图 3-136引擎中的 MSAA 4x:几何边缘的锯齿被多采样抹平@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/3d_antialiasing.html

MSAA 的核心是把“覆盖”与“着色”分开:

MSAA 原理:每个像素存多个覆盖样本,着色仍按每像素一次
图 3-137MSAA 原理:每个像素存多个覆盖样本,着色仍按每像素一次@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-OpenGL/Anti-Aliasing

说白了,但这条设计也直接决定了它的局限:因为它只采样可见性,所以它对着色别名完全无效。 高光闪烁、纹理噪点这些高频着色问题,MSAA 一点办法都没有。Unity 官方文档对此的表述是:MSAA 最擅长三角面边缘,但不修复 shader 走样(高光/纹理走样)。

Godot 的官方文档更是把这条写进了对照表:MSAA 不影响 shader 走样,也不影响 alpha scissor 材质——因为后者是像素级的裁剪,不是几何边缘。

10.2 CSAA:用覆盖率采样换带宽

MSAA 的代价主要不在算力,而在带宽:4x MSAA 意味着颜色缓冲、深度缓冲、模板缓冲的读写量都按 4 倍计。CSAA(Coverage Sampled Anti-Aliasing)的思路是保留覆盖率精度但不放大着色与存储。

NVIDIA 的 CSAA 教程给出的官方结论是:CSAA 引入"覆盖率采样(Coverage Sample)"这一新采样类型,画质接近 8x/16x MSAA,而性能开销仅比标准(通常 4x)MSAA 略高。

CSAA 的覆盖率采样
图 3-138CSAA:深度与覆盖率分离存储,用更少的内存代价获得更高的边缘精度@ 本系列绘制

10.3 后处理 AA:FXAA / SMAA / CMAA

后处理 AA 走的是完全不同的路:它不碰几何,只在已经渲染好的图像上做边缘检测与软化。

FXAA(Fast Approximate Anti-Aliasing)是这一类里最省的。Unity 的官方文档说得很直白:FXAA 是最省的开销技术,适合移动端与不支持运动矢量的平台。它的代价是模糊——因为它无法区分"真边缘"和"看起来像边缘的纹理细节"。

SMAA(Subpixel Morphological Anti-Aliasing)是改进版,用更精确的形态学分析与图案匹配来减少误判。Unity 官方对它的定位是"质量高于 FXAA 但更慢"。

CMAA(Conservative Morphological Anti-Aliasing)走的是保守路线。Intel 的官方文章给出了可对比的数据(对比 SS 参考的 PSNR):

方法 PSNR
None(无 AA) 35.28 dB
FXAA 36.75 dB
CMAA 2.0 ES 36.63 dB

这组数字很能说明问题:CMAA 的画质接近 FXAA,而它的设计目标是更保守地处理边缘、减少纹理细节的损失。 论文《AXAA》给出的对比是:AXAA 比 CMAA 快至多 2.3 倍、比 SMAA 1x 快 2.8 倍。

后处理 AA 的边缘检测与软化流程
图 3-139后处理 AA 的典型流程:亮度边缘检测 → 边缘方向判断 → 沿边缘方向做混合@ 本系列绘制
边缘检测在各区域的响应
图 3-140边缘检测在平坦区域、纹理细节与真边缘上的响应差异@ 本系列绘制

后处理 AA 在引擎里的表现——先看 FXAA:

引擎中的 FXAA:仅靠边缘检测与模糊抹平锯齿
图 3-141引擎中的 FXAA:仅靠边缘检测与模糊抹平锯齿@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/3d_antialiasing.html

同样属后处理路线,SMAA 的边缘处理更精细:

引擎中的 SMAA:基于形态学边缘检测的后处理抗锯齿
图 3-142引擎中的 SMAA:基于形态学边缘检测的后处理抗锯齿@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/3d_antialiasing.html

10.4 效果图:几何边缘的采样密度

下面我们开始。这里本该放一张实机的"开 AA / 关 AA"对照截图。但本文在素材检索阶段对 Wikimedia Commons 做了三轮检索(关键词分别为 antialiasing comparison、supersampling antialiasing、Mandelbrot antialiasing samples),确认没有长边 ≥1280 且许可干净的实机对照图——现有素材此前记录的最大尺寸为 264×286(本次复核发现同类素材还有更大的,最大 819×431,但均未达 1280)。

因此这一格以自绘原理图为主,并在此明确声明:主图为示意图,不是实机截图。下面另补入两张真实的抗锯齿对照素材作为佐证(尺寸偏小,仅作原理级对照)。

几何边缘在 1 采样与 8 采样下的可见性差异
图 3-143同一几何边缘在单采样与 8 采样下的可见性差异:右侧的覆盖率被以 1/8 精度表达@ 本系列绘制

把采样密度再往上推一档:

引擎中的 MSAA 8x:更高采样密度下的几何边缘
图 3-144引擎中的 MSAA 8x:更高采样密度下的几何边缘@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/3d_antialiasing.html
开/关抗锯齿的真实对比
图 3-145真实素材对照:左为关闭抗锯齿、右为开启抗锯齿的同一几何体(Menger 海绵)——锯齿与平滑的差别一目了然@ Wikimedia Commons · PantheraLeo1359531(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File:Menger_mit_und_ohne_Antialiasing_27082019.png
超采样抗锯齿的采样密度演示
图 3-146真实素材对照:超采样的采样密度演示,边缘覆盖率随采样数提升而更精确@ Wikimedia Commons · Rannomspp(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File:Supers%C3%A4mplimine.png

把 MSAA 档位降到 2×,边缘质量的变化很直观:

引擎中的 MSAA 2×:采样密度降低后边缘仍不如 4×/8× 干净
图 3-147引擎中的 MSAA 2×:采样密度降低后边缘仍不如 4×/8× 干净@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/3d_antialiasing.html

10.5 MSAA 的代价:一组官方数据

"MSAA 有带宽代价"是个常见说法,但很少看到具体数字。Meta 关于 Quest 平台的官方 MSAA 分析给出了一组难得的实测(双眼前提、GPU level 4):

项目 关闭 MSAA 开启 MSAA 4x
渲染成本 10.449 ms 11.497 ms
Resolve 成本 0.313 ms 0.626 ms
MSAA 的渲染与 Resolve 成本
图 3-148MSAA 4x 的成本构成:渲染成本上升约 10%,而 Resolve 成本翻了一倍@ 本系列绘制
MSAA 覆盖率到最终颜色的 Resolve
图 3-149MSAA 的覆盖率 Resolve@ 本系列绘制

两组数字值得分别解读。渲染成本只上升约 10%——这印证了 MSAA"着色只做一次"的效率。但 Resolve 成本翻了一倍(0.313 → 0.626 ms),因为把多采样缓冲解析成单采样图像这一步,工作量正比于采样数。

在移动端这种总帧预算只有十几毫秒的场景里,Resolve 那一倍的增长不是可以忽略的。这也解释了为什么移动平台更倾向于 FXAA 这类后处理方案。

10.6 量化结论汇总

结论 数据 出处
MSAA 的核心机制 多次采样可见性,单次着色 Decoupled Coverage AA(ACM)
MSAA 的能力边界 不修复 shader 走样与 alpha scissor 材质 Unity / Godot 官方文档
CSAA 的收益 画质接近 8x/16x MSAA,开销仅略高于 4x MSAA NVIDIA CSAA 教程
FXAA / CMAA 的 PSNR 35.28(无)/ 36.75(FXAA)/ 36.63(CMAA2 ES) Intel 官方
AXAA 的相对速度 比 CMAA 快至多 2.3×,比 SMAA 1x 快 2.8× ACM 论文
MSAA 4x 的渲染成本 10.449 → 11.497 ms(约 +10%) Meta Quest 官方
MSAA 4x 的 Resolve 成本 0.313 → 0.626 ms(翻倍) Meta Quest 官方

10.7 本节参考资料

  • Unity URP《Anti-aliasing》官方文档:https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@14.0/manual/anti-aliasing.html
  • Unity Post-Processing《Anti-aliasing》:https://docs.unity3d.com/Packages/com.unity.postprocessing@3.5/manual/Anti-aliasing.html
  • Godot 4《3D 抗锯齿》官方文档:https://docs.godotengine.org/zh-tw/4.x/tutorials/3d/3d_antialiasing.html
  • Godot 3D Anti-Aliasing Demo(MIT 许可):https://store.godotengine.org/asset/godot-foundation/anti-aliasing-3d-demo/
  • NVIDIA《Coverage Sampled Antialiasing (CSAA)》教程:https://developer.nvidia.com/w/SDK/9.5/Samples/DEMOS/Direct3D9/src/CSAATutorial/docs/CSAATutorial.pdf
  • Intel《Conservative Morphological Anti-Aliasing 2.0》:https://www.intel.sg/content/www/cn/zh/develop/articles/conservative-morphological-anti-aliasing-20.html
  • Meta《Multisample Anti-Aliasing Analysis for Meta Quest》:https://developers.meta.com/horizon/documentation/native/android/mobile-msaa-analysis/
  • 《AXAA: Adaptive approXimate Anti-Aliasing》(ACM):https://dl.acm.org/doi/pdf/10.1145/2945078.2945129

九、时间方法:TAA 与超分

11.1 TAA 的核心:给投影矩阵加抖动

TAA(Temporal Anti-Aliasing)绕开了空间采样数量的限制,换了一个维度——时间。

下面我们进入正题。《A Survey of Temporal Antialiasing Techniques》给出的机制描述是:给相机投影矩阵加入子像素的抖动偏移(Jitter Offset),让每一帧的采样位置都略有不同;然后把历史帧对齐、累积,用多帧的样本共同逼近更高采样密度的结果。

关键的一点是:多数实现每帧每像素仍然只渲染单采样。 所以 TAA 的算力代价很低,它的"采样数"是靠时间维度堆出来的,不是靠单帧堆出来的。

$$ \text{Jitter}_n = \frac{(h_n(x) - 0.5,\ h_n(y) - 0.5)}{W} $$

其中:

  • $h_n$ 是第 $n$ 帧使用的低差异序列(如 Halton 序列)的第 $n$ 项。
  • $W$ 是画面宽度,把抖动换算到 NDC 空间。
  • 减 0.5 是为了把采样点从纹素中心偏移到子像素位置。
TAA 的子像素抖动序列
图 3-150TAA 的 jitter 序列:每帧偏移到不同的子像素位置,多帧累积后覆盖更多采样点@ 本系列绘制

时间方法的效果——同一场景下 TAA 打开后:

引擎中的 TAA:跨帧抖动累积后的稳定边缘
图 3-151引擎中的 TAA:跨帧抖动累积后的稳定边缘@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/3d_antialiasing.html

11.2 历史缓冲与重投影

只有抖动还不够——如果每帧各自独立,画面会剧烈闪烁。TAA 的做法是把上一帧的结果重投影(Reprojection)到当前帧的坐标系,再与当前帧混合。

重投影依赖运动矢量(Motion Vector):每个像素记录"这一点在上一帧位于哪里"。有了运动矢量,就可以用上一帧的历史颜色作为本帧的初始估计,然后按权重混合:

$$ C_{\text{out}} = (1 - \alpha)\, C_{\text{current}} + \alpha\, C_{\text{history}} $$

其中:

  • $C_{\text{current}}$ 是当前帧渲染出来的颜色。
  • $C_{\text{history}}$ 是从上一帧重投影过来的颜色。
  • $\alpha$ 是历史权重,通常在 0.9 附近——权重越高,越平滑,但鬼影越明显。
TAA 的历史缓冲与重投影
图 3-152TAA 的重投影:用运动矢量把历史帧对齐到当前坐标系,再按权重混合@ 本系列绘制
历史权重对收敛速度的影响
图 3-153历史权重对收敛速度与鬼影的影响@ 本系列绘制
TAA 对不同频率成分的影响
图 3-154TAA 对不同频率成分的影响:高频被持续削弱@ 本系列绘制

下面开始。这也是 TAA 的一个不得不承认的性质:它的画质依赖帧率。 帧率越高,帧间位移越小,重投影误差越小。Digital Foundry 的专题文章专门指出过这一点,并说明 TAA 的软化与鬼影会随分辨率与观看距离被放大。

11.3 鬼影:TAA 的固有代价

鬼影(Ghosting) 是 TAA 最典型的缺陷:运动的物体在身后拖出残影。

成因很直接:历史缓冲里记录的是旧位置的颜色。当物体移动后,把旧颜色按权重混进来,就留下了"过去的影子"。

工业上的标准缓解手段是邻域钳制(Neighborhood Clamping):把历史颜色限制在当前像素邻域的颜色范围(min/max 的包围盒)之内。

$$ C_{\text{history}}^{\text{clamped}} = \text{clamp}\!\left(C_{\text{history}},\ \min_{N}(C),\ \max_{N}(C)\right) $$

其中 $N$ 表示当前像素的邻域(通常是 3×3)。这条式子的含义是:如果历史颜色与当前邻域的颜色差异过大,就把它拉回来。

TAA 的鬼影成因
图 3-155鬼影的成因:历史缓冲中的旧颜色被混合进新位置@ 本系列绘制
邻域钳制的作用
图 3-156邻域钳制:把历史颜色限制在当前邻域的颜色范围内,抑制鬼影@ 本系列绘制

但邻域钳制有一个内在矛盾,前面引用的技术文章把它点得很准:钳制过于激进,会把本该累积的子像素细节一起丢掉,结果就是模糊。 所以钳制的松紧是一个必须调的取舍——太松有鬼影,太紧有模糊。

对于薄物体与粒子,问题更严重:它们在邻域里占比很小,min/max 包围盒本身就不稳定,钳制可能完全失效。针对这类情况,工程上还有两件工具:

  • 速度加权混合:运动矢量幅度大时降低历史权重。社区实现的默认历史权重约 0.9,速度加权让它在快速运动时降到更低,从而减少拖影。
  • 基于移动性的抗鬼影:Unreal 的 r.TemporalAA.Quality = 2(默认值)就是"开启输入滤波并带基于移动性的抗鬼影"。
速度加权混合
图 3-157速度加权:运动矢量越大,历史权重越低,从而减少快速运动中的拖影@ 本系列绘制

11.4 代码:一段带钳制的历史混合

//-----------------------------------【TAA 历史混合】-----------------------------------------
// 描述:重投影 + 邻域钳制 + 速度加权,TAA 的核心三步
//------------------------------------------------------------------------------------------
//【1】用运动矢量把历史帧重投影到当前像素
//【2】邻域钳制:把历史颜色限制在当前 3x3 邻域的颜色范围内
//【3】速度加权:运动越快,历史权重越低
float3 TemporalResolve(Texture2D historyTex, float2 uv, float2 motion,
                       float3 currentColor, float3 nMin, float3 nMax)
{
    //【1】重投影:当前位置减去运动矢量,即上一帧的对应位置
    float2 prevUV = uv - motion;
    float3 history = historyTex.SampleLevel(samplerLinear, prevUV, 0).rgb;

    //【2】邻域钳制:超出邻域范围的历史颜色被拉回边界,抑制鬼影
    history = clamp(history, nMin, nMax);

    //【3】速度加权:motion 越长,历史权重越低,避免运动拖影
    float speed = length(motion) * screenSize;
    float alpha = lerp(0.9, 0.0, saturate(speed / SPEED_THRESHOLD));

    return lerp(currentColor, history, alpha);
}

下面进入正题。下面对这段特别说明两点。第一,prevUV 是减而不是加——运动矢量记录的是"当前像素在上一帧的位置偏移",方向容易搞反,搞反的表现是画面出现与运动方向相反的拖影。第二,alpha 的默认值 0.9 意味着每帧只混入 10% 的新信息,这也是 TAA 出现软化观感的根本原因:它本质上是一个低通滤波。

11.5 超分与 AA 的关系:一个必须澄清的误解

近几年 DLSS、FSR、XeSS 这些超分技术普及之后,出现了一个广泛的误解:"开了 DLSS 之后还要不要再开 TAA?"

官方口径很明确:不要叠加。

  • Intel 的 XeSS API 开发指南写明:XeSS 是时序摊销的超采样/上采样技术,直接替代 TAA(drops in place of TAA);当 XeSS 被关闭时,AA 模式应恢复到先前的设置。
  • NVIDIA 的 RTX UI 开发指南说明 DLSS 套件包含 DLAA(Deep Learning Anti-Aliasing) 与 Super Resolution,目标是"better than native IQ"。
  • 社区侧的总结更直接:DLSS/FSR/XeSS 已内置 AA,不要再叠加 TAA,否则双重时序处理会导致过度模糊。

原因在于这些超分技术本身就是时序累积器:它们已经在做抖动、重投影、历史混合。再叠一层 TAA,等于把两套时序滤波串联——每一层都在低通,结果是明显糊掉。

超分与 TAA 的关系
图 3-158超分器内置时序 AA;与 TAA 叠加等于串联两级时序滤波,导致过度模糊@ 本系列绘制

超采样是最“暴力”的解法,代价也最直接:

引擎中的 SSAA 4x:以 4 倍像素量渲染再降采样
图 3-159引擎中的 SSAA 4x:以 4 倍像素量渲染再降采样@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/3d_antialiasing.html

与之相对的是重建式超分——用更少的采样推断出高分辨率:

引擎中的 FSR2:低分辨率渲染后重建为高分辨率
图 3-160引擎中的 FSR2:低分辨率渲染后重建为高分辨率@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/3d_antialiasing.html

超采样档位之间也有差别:

引擎中的 SSAA 2.25×:以更高像素量渲染再降采样
图 3-161引擎中的 SSAA 2.25×:以更高像素量渲染再降采样@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/3d_antialiasing.html

另外两个事实也值得记下:

  • DLSS 4 引入了多帧生成(Multi Frame Generation)与 Transformer 模型;DLSS 4.5 带来动态 MFG 与第二代 Transformer。
  • XeSS 仅支持 1080p 及以上分辨率,并提供 Off / Performance / Balanced / Quality / Ultra Quality 五档。

11.6 应用层的实测数据

Digital Foundry 2024 年的专题《Temporal anti-aliasing: a blessing or a curse?》给出了目前可查的最系统的实测:

场景 数据
RTX 3070 跑 Crysis 3,原生 1080p 近 200 fps(非 CPU 瓶颈)
同上,4x SSAA 降至 70 fps
同上,8x SSAA 仅 19 fps
8800 GTX 上 8x MSAA 跑 Oblivion 损失约 35% 性能
《Deus Ex: Mankind Divided》中 TAA 的帧时间占比 约 3%

这组数字把整章的取舍讲清楚了:SSAA 的质量最高,但成本高到不可用(8x 把 200 fps 打到 19 fps,接近十分之一);MSAA 成本居中(约 35%);TAA 几乎免费(约 3%)。所以今天的主流选择必然是 TAA 或超分——不是因为它们画质更好,而是因为它们的成本在可接受范围。

社区汇总的帧率影响对照(按"社区实测"性质引用,口径不一,只用于量级参考):

方法 帧率影响
SSAA 2x −50% ~ −75%
MSAA 4x −20% ~ −40%
TAA −8% ~ −15%
SMAA −3% ~ −8%
FXAA −2% ~ −7%
DLSS / FSR / XeSS(Quality) 约 +30% ~ +50% 增益

11.7 成本对照表

抗锯齿方法的成本对照
图 3-162抗锯齿方法在质量、成本、副作用与硬件前提四个维度上的对照@ 本系列绘制
MSAA 与 TAA 的靶区互补
图 3-163MSAA 与 TAA 的靶区互补@ 本系列绘制

11.8 演进脉络

抗锯齿方法的演进时间线
图 3-164抗锯齿的演进脉络:从超采样到硬件多重采样、后处理、时序,再到超分@ 本系列绘制
阶段 机制突破 代表方法
超采样 提高采样率 SSAA
硬件多重采样 解耦可见性与着色 MSAA、CSAA
后处理 在成图上做形态学处理 FXAA、MLAA、SMAA、CMAA
时序 抖动 + 历史累积,摊销到时间维度 TAA、TSSAA
超分 时序上采样,内置 AA DLSS、FSR、XeSS、TSR

11.9 本节的量化结论

结论 数据
TAA 的单帧采样数 多数实现每帧每像素单采样
历史权重默认值 约 0.9
Unreal 抗鬼影默认档 r.TemporalAA.Quality = 2
8x SSAA 的代价 Crysis 3 原生 200 fps → 19 fps
8x MSAA 的代价 Oblivion 损失约 35%
TAA 的帧时间占比 约 3%(Deus Ex: Mankind Divided)
Unreal TSR 的官方对比 4K 原生 57.50 ms → 1080p 上采样 33.37 ms
TSR 的成本特性 随渲染分辨率缩放,分辨率越低开销越高
XeSS 的分辨率下限 1080p

11.10 本节参考资料

  • 《A Survey of Temporal Antialiasing Techniques》:http://behindthepixels.io/assets/files/TemporalAA.pdf
  • Digital Foundry《Temporal anti-aliasing: a blessing or a curse?》:https://www.digitalfoundry.net/articles/digitalfoundry-2024-temporal-anti-aliasing-a-blessing-or-a-curse
  • Unreal 论坛 TSR feedback thread:https://forums.unrealengine.com/t/tsr-feedback-thread/883977
  • Intel XeSS API Developer Guide(PDF):https://cdrdv2-public.intel.com/746849/intel-xe-super-sampling-xess-api-developer-guide-septupdate-v6.pdf
  • NVIDIA RTX UI Developer Guidelines(Streamline):https://media.githubusercontent.com/media/NVIDIA-RTX/Streamline/refs/heads/main/docs/RTX%20UI%20Developer%20Guidelines.pdf
  • NVIDIA DLSS 技术页:https://www.nvidia.com/zh-tw/geforce/technologies/dlss/
  • AMD FidelityFX Super Resolution:https://www.amd.com/en/products/graphics/technologies/fidelityfx/super-resolution.html
  • NVIDIA GTA V 画质与性能指南:https://www.nvidia.com/en-us/geforce/news/grand-theft-auto-v-pc-graphics-and-performance-guide/
  • NVIDIA Titanfall 2 图形与性能指南:https://www.nvidia.com/en-us/geforce/guides/gfecnt/titanfall-2-graphics-performance-and-tweaking-guide/
  • Babylon.js TAA Rendering Pipeline:https://doc.babylonjs.com/features/featuresDeepDive/postProcesses/TAARenderingPipeline
  • TAA 机制与鬼影讨论:https://www.mysimulator.uk/content/articles/temporal-anti-aliasing.html
  • DirectX 12 TAA 实现说明:https://www.cosmiclearn.com/dx12/taa.php
  • 抗锯齿模式与性能对照(社区汇总):https://www.switchbladegaming.com/game-settings/anti-aliasing-guide/
  • FXAA / TAA / MSAA / DLAA 说明(社区汇总):https://gamerhardware.org/what-is-anti-aliasing-gaming/

十、结语:漏斗收口,以及三个仍然开着的口子

12.1 回到那条主线

把三章合起来看,后处理链的形态就清楚了。它是一条能量的漏斗:

后处理链的能量漏斗(收口版)
图 3-165后处理链的能量漏斗收口:加法(Bloom)→ 减法(色调映射)→ 采样修正(抗锯齿)@ 本系列绘制
  • 加:Bloom 把真实光学系统的响应补回画面,这一步是唯一一次"增加"。
  • 减:色调映射把无上限的场景亮度压进有限的显示范围,这一步必然有损。
  • 修:抗锯齿处理前两步留下的采样痕迹,它不改变能量,只改变样本的分布。

先来看看现象。三者串起来的因果是:Bloom 需要 HDR 才能做阈值判断,色调映射必须站在 Bloom 之后,而抗锯齿则要处理这两步产生的所有空间与时间高频。 把它们当三个独立开关来理解,就会在顺序和参数上反复踩坑。

12.2 三个仍然开着的口子

第一,能量守恒的 Bloom 在实时场景里仍然昂贵。 Unreal 的 FFT 卷积方案被官方定位在"过场与高端硬件",这说明"物理正确的"和"实时的"目前还没能完全重合。

第二,TAA 的软化与鬼影没有彻底的解法。 邻域钳制的松紧矛盾是结构性的——松了有鬼影,紧了会模糊。今天主流的缓解手段(速度加权、抗模糊滤波)都是在两者之间找平衡,而不是消除其中一端。

第三,AA 参数在不同引擎里仍然没有统一语义。 本文第九章专门纠正过 Unreal 的 cvar 命名问题——这类混乱在跨引擎讨论中会持续制造误解,而它本可以避免。

12.3 选型建议

如果只记三条:

12.4 下一篇预告与系列进度

下一篇进入 《PBR 与 IBL:从渲染方程到工业实践》。本篇讨论的是"图像已经渲染出来之后"的事,下一篇要回到"图像怎么被算出来"——渲染方程怎么在实时约束下被拆成可以执行的形式,以及 IBL 如何把一张环境贴图变成有物理意义的照明。

系列的完整进度:

至此,规划中的五篇已完成三篇交付,各篇主题与完成情况如下表:

序 标题 状态
(一) 延迟渲染:从原理到实现的性能权衡 已交付
(二) 阴影贴图:从 PCF 到 VSM 的演进 已交付
(三) 后处理链:Bloom、色调映射与抗锯齿 本篇
(四) PBR 与 IBL:从渲染方程到工业实践 已发布
(五) 可见性与剔除:从视锥到 GPU-Driven 已发布

本系列将汇编成电子书,每篇发布后随内容深化更新知识地图与索引。


本篇图片来源台账

本文使用的外部图像均来自 A 级源(Wikimedia Commons、LearnOpenGL 与 Godot 官方文档),许可与作者如下,原始页面可直接点开核对:

文件 来源 作者 许可 原始页面
src_bokeh_street.jpg Wikimedia Commons Clem Onojeghuo CC0 https://commons.wikimedia.org/wiki/File:Street_bokeh_(Unsplash).jpg
src_bokeh_night.jpg Wikimedia Commons Geoff Peters CC BY 2.0 https://commons.wikimedia.org/wiki/File:Light_bokeh_at_night_(14769384586).jpg
src_bokeh_nikkor.jpg Wikimedia Commons Javier Perez Montes CC BY-SA 4.0 https://commons.wikimedia.org/wiki/File:Bokeh_Highlights_-AF-Nikkor_50mm_f1.8-_01.jpg
src_bokeh_blenden.jpg Wikimedia Commons Dietmar Rabich CC BY-SA 4.0 https://commons.wikimedia.org/wiki/File:Blendenvariationen_--2024--_6808.jpg
src_raindrops.jpg Wikimedia Commons Bhullargraphic CC BY-SA 4.0 https://commons.wikimedia.org/wiki/File:Vibrant_raindrops_on_glass_with_a_blurred_urban_bokeh_light_background.jpg
src_lensflare_flare.jpg Wikimedia Commons Amolven CC BY-SA 4.0 https://commons.wikimedia.org/wiki/File:A_Lens_flare.jpg
src_lensflare_water.jpg Wikimedia Commons W.carter CC0 https://commons.wikimedia.org/wiki/File:Dreamy_lens_flare_over_water.jpg
src_lensflare_hood.jpg Wikimedia Commons Ggia CC BY-SA 3.0 https://commons.wikimedia.org/wiki/File:20100706_lens_flare_example_comparison_lens_hood.jpg
src_hdri_window.jpg Wikimedia Commons Sebastian Nibisz CC BY-SA 4.0 https://commons.wikimedia.org/wiki/File:HDRI_Sample_Scene_Window.jpg
src_linga_temple.jpg Wikimedia Commons Vipulvaibhav5 CC BY-SA 4.0 https://commons.wikimedia.org/wiki/File:High_Dynamic_Range_photograph_of_Linga_Temple.jpg
src_gamut_ntsc709p3.png Wikimedia Commons Myndex CC BY 4.0 https://commons.wikimedia.org/wiki/File:CIE1931xy_gamut_comparison_CC_v06_NTSC_709_P3.png
src_gamut_rec2020.png Wikimedia Commons Myndex CC BY-SA 4.0 https://commons.wikimedia.org/wiki/File:CIE1931xy_gamut_comparison_of_sRGB_P3_Rec2020.svg
src_tonemap_hdr_ldr.jpg Wikimedia Commons Axel Jacobs(摄影)/ Hdruser(衍生) CC BY-SA 2.0 https://commons.wikimedia.org/wiki/File:Igreja_Nossa_Senhora_de_Fatima_(HDR_LDR_comparison).jpg
src_tonemap_methods.jpg Wikimedia Commons Axel Jacobs(摄影)/ Hdruser(衍生) CC BY-SA 2.0 https://commons.wikimedia.org/wiki/File:Tone_Mapping_Methods.jpg
src_aa_menger.png Wikimedia Commons PantheraLeo1359531 CC BY-SA 4.0 https://commons.wikimedia.org/wiki/File:Menger_mit_und_ohne_Antialiasing_27082019.png
src_aa_supersampling.png Wikimedia Commons Rannomspp CC BY-SA 4.0 https://commons.wikimedia.org/wiki/File:Supers%C3%A4mplimine.png
src_godot_dof_far.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html
src_godot_glow_bloom.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html
src_godot_glow_threshold.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html
src_godot_glow_map.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html
src_godot_glow_scale.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html
src_godot_glow_layers2.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html
src_godot_glow_blur.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html
src_godot_glow_layers.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html
src_lo_hdr_exposure.png LearnOpenGL(CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/HDR
src_godot_hdr_bloom.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html
src_lo_hdr_reinhard.png LearnOpenGL(CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/HDR
src_godot_lut.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html
src_godot_aa_none.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/3d_antialiasing.html
src_godot_aa_msaa4x.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/3d_antialiasing.html
src_godot_aa_fxaa.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/3d_antialiasing.html
src_godot_aa_smaa.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/3d_antialiasing.html
src_godot_aa_msaa8x.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/3d_antialiasing.html
src_godot_aa_taa.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/3d_antialiasing.html
src_godot_aa_ssaa4x.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/3d_antialiasing.html
src_godot_aa_fsr2.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/3d_antialiasing.html
src_hdr_pula.jpg Wikimedia Commons Diego Delso CC BY-SA 4.0 https://commons.wikimedia.org/wiki/File:Anfiteatro_de_Pula,_Croacia,_2017-04-17,_DD_49-57_HDR_PAN.jpg
src_tonemap_darmstadt.jpg Wikimedia Commons Guido Radig CC BY 3.0 https://commons.wikimedia.org/wiki/File:Darmstadt_-Mathildenh%C3%B6he(tone-mapping).jpg
src_moire_fringes.jpg Wikimedia Commons Brocken Inaglory CC BY-SA 3.0 https://commons.wikimedia.org/wiki/File:Moir%C3%A9_fringes_IMG_3712.jpg
src_godot_glow1.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html
src_godot_dof_near.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html
src_godot_mixed_blur.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html
src_lo_hdr_image.png LearnOpenGL(CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/HDR
src_lo_hdr_clamped.png LearnOpenGL(CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/HDR
src_godot_adjustments.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html
src_godot_adjustments_bcs.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html
src_lo_aa_aliasing.png LearnOpenGL(CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-OpenGL/Anti-Aliasing
src_lo_aa_multisampled.png LearnOpenGL(CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-OpenGL/Anti-Aliasing
src_godot_aa_msaa2x.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/3d_antialiasing.html
src_godot_aa_ssaa225.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/3d_antialiasing.html
src_spherical_aberration.jpg Wikimedia Commons Adam Gabrys CC BY-SA 3.0 https://commons.wikimedia.org/wiki/File%3ALens%20spherical%20aberration%20RL.jpg
src_bokeh_shapes.jpg Wikimedia Commons Roni Amin CC BY 2.0 https://commons.wikimedia.org/wiki/File%3ABokeh%20shapes.jpg
src_bokeh_heart.jpg Wikimedia Commons Iouri Goussev CC BY-SA 2.0 https://commons.wikimedia.org/wiki/File%3AHeart-shaped%20bokeh.jpg
src_lens_zuiko.jpg Wikimedia Commons Petar Milošević CC BY-SA 4.0 https://commons.wikimedia.org/wiki/File%3AOlympus%20Zuiko%20OM%2050%20mm%20lens.jpg
src_ca_macro.jpg Wikimedia Commons Acagastya CC0 https://commons.wikimedia.org/wiki/File%3AChromatic%20aberration%20in%20a%20macro%20photo.jpg
src_ca_detail.jpg Wikimedia Commons Jkk CC BY-SA 3.0 https://commons.wikimedia.org/wiki/File%3AChromatic%20aberration%20with%20detail.jpg
src_ca_wine.png Wikimedia Commons Wilfredor CC0 https://commons.wikimedia.org/wiki/File%3AChromatic%20aberration%20Wine.png
src_hdr_ldr_compare.jpg Wikimedia Commons Igreja_Nossa_Senhora_de_Fatima_(JPEG-HDR).jpg : Axel Jacobs (Photographer) derivative work: Hdruser ( talk ) CC BY-SA 2.0 https://commons.wikimedia.org/wiki/File%3AIgreja%20Nossa%20Senhora%20de%20Fatima%20%28HDR%20LDR%20comparison%29.jpg
src_ev_plus1.jpg Wikimedia Commons Kapitel CC BY-SA 4.0 https://commons.wikimedia.org/wiki/File%3AEV%2B1%20%28test%29.jpg
src_ev_minus3.jpg Wikimedia Commons Kapitel CC BY-SA 4.0 https://commons.wikimedia.org/wiki/File%3AEV-3%20%28test%29.jpg
src_exposure_time.jpg Wikimedia Commons Markus Mayer CC BY-SA 3.0 https://commons.wikimedia.org/wiki/File%3AEinfluss%20der%20Zeit%20auf%20die%20Belichtung.jpg
src_tonemap_church.jpg Wikimedia Commons JeanNeef CC BY-SA 3.0 https://commons.wikimedia.org/wiki/File%3AKirche%20Falkenhain%20HDR%20tone%20mapping.jpg
src_exposure_fused.jpg Wikimedia Commons Slaunger CC BY-SA 3.0 https://commons.wikimedia.org/wiki/File%3AM%C3%B8nsted%20kalkgruber%20exposure%20fused%202014-07-18.jpg
src_exposure_strip.jpg Wikimedia Commons The original uploader was Imroy at English Wikipedia . CC BY-SA 2.5 https://commons.wikimedia.org/wiki/File%3AHDR%20example%20-%20exposure.jpeg
src_godot_env_hdr.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html
src_godot_lut_template.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html
src_moire_screen.jpg Wikimedia Commons 4084470 0.smil CC BY 4.0 https://commons.wikimedia.org/wiki/File%3AMoir%C3%A9%20pattern%20on%20LED%20Screen%20at%20Kunshannan%20Railway%20Station%20in%20Suzhou%2C%20Jiangsu%2C%20China%2020210528.jpg
src_moire_screen2.jpg Wikimedia Commons Pixelmaniac pictures ( Leave a reply ) CC0 https://commons.wikimedia.org/wiki/File%3AMoir%C3%A9%20pattern%20with%20screen.jpg

其余示意图均为本系列绘制,版权归本系列所有。

本篇引用的主要资料

  • Jorge Jimenez, Next Generation Post Processing in Call of Duty: Advanced Warfare, SIGGRAPH 2014 — https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare/
  • SIGGRAPH 2014 Advances in Real-Time Rendering 课程主页 — https://advances.realtimerendering.com/s2014/index.html
  • GPU Gems, Ch.21 Real-Time Glow — https://developer.nvidia.com/gpugems/gpugems/part-iv-image-processing/chapter-21-real-time-glow
  • Unreal Engine, Bloom — https://dev.epicgames.com/documentation/en-us/unreal-engine/bloom-in-unreal-engine
  • Unreal Engine, Anti-Aliasing and Upscaling — https://dev.epicgames.com/documentation/en-us/unreal-engine/anti-aliasing-and-upscaling-in-unreal-engine
  • Unreal Engine, Temporal Super Resolution — https://dev.epicgames.com/documentation/unreal-engine/temporal-super-resolution-in-unreal-engine
  • Unreal Engine, Color Grading and the Filmic Tonemapper — https://dev.epicgames.com/documentation/en-us/unreal-engine/color-grading-and-the-filmic-tonemapper-in-unreal-engine
  • Unreal Engine, Auto Exposure — https://dev.epicgames.com/documentation/unreal-engine/auto-exposure-in-unreal-engine
  • Unity URP, Bloom.shader 源码 — https://github.com/Unity-Technologies/Graphics/blob/master/Packages/com.unity.render-pipelines.universal/Shaders/PostProcessing/Bloom.shader
  • Unity URP, BloomFilterMode API 文档 — https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@17.4/api/UnityEngine.Rendering.Universal.BloomFilterMode.html
  • Unity HDRP, Tonemapping — https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@16.0/manual/Post-Processing-Tonemapping.html
  • Unity HDRP, High Dynamic Range Output — https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@14.0/manual/HDR-Output.html
  • Godot, 3D 抗锯齿 — https://docs.godotengine.org/zh-tw/4.x/tutorials/3d/3d_antialiasing.html
  • Valve Developer Community, Anti-aliasing — https://developer.valvesoftware.com/wiki/Anti-aliasing
  • ACES Documentation, Output Transforms — https://docs.acescentral.com/system-components/output-transforms/
  • ACES Documentation, AMF Implementation Guide — https://docs.acescentral.com/amf/guides/implementation/
  • ACES Documentation, ACEScg — https://docs.acescentral.com/encodings/acescg/
  • NVIDIA, Preparing for Real HDR — https://developer.nvidia.com/preparing-real-hdr
  • NVIDIA CSAA Tutorial(PDF) — https://developer.nvidia.com/w/SDK/9.5/Samples/DEMOS/Direct3D9/src/CSAATutorial/docs/CSAATutorial.pdf
  • NVIDIA GameWorks Bloom Sample — https://docs.nvidia.com/gameworks/content/gameworkslibrary/graphicssamples/opengl_samples/bloomsample.htm
  • Intel, Conservative Morphological Anti-Aliasing 2.0 — https://www.intel.sg/content/www/cn/zh/develop/articles/conservative-morphological-anti-aliasing-20.html
  • Intel XeSS API Developer Guide(PDF) — https://cdrdv2-public.intel.com/746849/intel-xe-super-sampling-xess-api-developer-guide-septupdate-v6.pdf
  • Meta, Multisample Anti-Aliasing Analysis for Meta Quest — https://developers.meta.com/horizon/documentation/native/android/mobile-msaa-analysis/
  • A Survey of Temporal Antialiasing Techniques — http://behindthepixels.io/assets/files/TemporalAA.pdf
  • Decoupled Coverage Anti-Aliasing(ACM) — https://dl.acm.org/doi/pdf/10.1145/2790060.2790068
  • AXAA: Adaptive approXimate Anti-Aliasing(ACM) — https://dl.acm.org/doi/pdf/10.1145/2945078.2945129
  • Surface Based Anti-Aliasing(Salvi 2012) — https://www.sci-hub.st/storage/2024/3672/03c9b9b905dca1fcedac86e978af6f4b/salvi2012.pdf
  • UMD CMSC 427, Sampling and Aliasing — http://www.cs.umd.edu/class/fall2009/cmsc427/Lects/chapt11-sampling-alias-6.pdf
  • Cambridge AGIP, High dynamic range and tone mapping(PDF) — https://www.cl.cam.ac.uk/teaching/2122/AGIP/07_HDR_and_tone_mapping_1pp.pdf
  • Digital Foundry, Temporal anti-aliasing: a blessing or a curse? — https://www.digitalfoundry.net/articles/digitalfoundry-2024-temporal-anti-aliasing-a-blessing-or-a-curse
  • AgX 参考实现与设计说明 — https://github.com/EaryChow/AgX/blob/main/README.md
  • darktable AgX 文档 — https://darktable.info/en/darkroom/basics/agx/
  • dashVulkan, HDR + Bloom: Karis Pyramid + ACES — https://borisbat.github.io/dasVulkan/tutorials/11_hdr.html
  • Bloom 实现的分 pass 帧耗预算(社区) — https://github.com/benpm/sgct/blob/master/BLOOM_IMPLEMENTATION_SUMMARY.md
  • 移动端后处理实战与 benchmark(社区) — https://dredyson.com/my-sc-post-effects-pack-built-in-urp-journey-what-i-learned-after-6-months-of-real-world-testing-vr-troubleshooting-and-performance-optimization/

  • https://dev.epicgames.com/documentation/en-us/unreal-engine/python-api/class/PostProcessSettings

  • https://dev.epicgames.com/documentation/en-us/unreal-engine/python-api/class/CameraExposureSettings
  • https://link.unrealengine.com/ko/tech-blog/how-epic-games-is-handling-auto-exposure-in-4-25
  • https://www.mpegflow.com/topics/color/hdr-to-sdr-conversion
  • https://www.mpegflow.com/topics/color/tone-mapping
  • https://images.apple.com/final-cut-pro/docs/HDR_WideColor.pdf
  • https://arxiv.org/html/2410.19839
  • https://z-fx.nl/ColorspACES.pdf
  • https://chemecse.io/assets/UPennHDRRenderingTalk.pdf
  • https://excel.fit.vutbr.cz/submissions/2024/038/38_poster.pdf
  • https://www.fab.com/listings/8ac93617-31b7-42f7-ad80-b917b828083c
  • https://github.com/luluco250/FXShaders/blob/master/Shaders/AdaptiveTonemapper.fx
  • https://gist.github.com/SpineyPete/ebf9619f009318536c6da48209894fed
  • https://github.com/expenses/bloom
  • https://github.com/EnviralDesign/TD-13TapBloom
  • https://github.com/SketchNI/obs-composite-blur
  • https://learnopengl.com/advanced-lighting/bloom
  • https://commons.wikimedia.org/wiki/File:Tone_Mapping_Methods.jpg
  • https://commons.wikimedia.org/wiki/File:Anti-aliasing.jpg
  • https://commons.wikimedia.org/wiki/File:Menger_mit_und_ohne_Antialiasing_27082019.png
  • https://commons.wikimedia.org/wiki/File:Antialiasing_comparaison.gif
  • https://commons.wikimedia.org/wiki/File:Mandelbrot-spiral-antialiased-4-samples.png
  • https://commons.wikimedia.org/wiki/File:Mandelbrot-spiral-antialiased-25-samples.png
  • https://commons.wikimedia.org/wiki/File:Supersampling-by-antialiasing.gif
  • https://commons.wikimedia.org/wiki/File:Anti-aliasing_demo.svg
  • https://commons.wikimedia.org/wiki/File:CIE1931xy_gamut_comparison.svg
  • https://commons.wikimedia.org/wiki/File:CIExy1931.svg
  • https://commons.wikimedia.org/wiki/File:Visible_spectrum_390-710_nm_linear_Rec._2020.svg
  • https://commons.wikimedia.org/wiki/Category:Quality_photographs_of_bokeh

  • https://www.thecoreitech.com/gaming-computers/dlss-vs-fsr-vs-xess/

  • https://discourse.threejs.org/t/tone-mapping-overview/75204/16
  • https://stefangroenewoud.artstation.com/blog/2Ap3z/day-23-deep-dive-tone-mapping
  • https://github.com/imazen/zentone/issues/14
  • https://lilys.ai/es/notes/unreal-engine-5-20260221/unreal-engine-look-explained-fix
  • https://habr.com/en/articles/957852/
  • https://blog.csdn.net/qq_41835314/article/details/129238947 With Best Wishes.

相关链接 - 上一篇:【实时渲染管线探析】(一) 延迟渲染:从原理到实现的性能权衡 - 上一篇:【实时渲染管线探析】(二) 阴影贴图:从 PCF 到 VSM 的演进 - 下一篇(预告):【实时渲染管线探析】(四) PBR 与 IBL:从渲染方程到工业实践

With Best Wishes.