系列文章前言
1.1 系列定位
本系列逐层拆解实时渲染管线(Real-Time Rendering Pipeline)。前两篇走的是"几何与光照":延迟渲染解决"怎么同时点亮很多光源",阴影贴图解决"光照怎么落到物体上还有影子"。
OK,言归正传。这一篇进入最后一段——后处理(Post-Processing)。三维场景已渲染成一张二维图像,剩下的工作都在像素上做。看似收尾杂活,实为整条管线里最被低估的一段:画面"好不好看",最后的判断几乎都发生在这几个 Pass 里。
1.2 本篇要讲清的一件事
后处理里有三样东西被讲得极多,却很少被讲成一条链:Bloom(泛光)、色调映射(Tone Mapping)、抗锯齿(Anti-Aliasing)。
它们在教程里通常是三个并列的"效果开关",但按管线的执行顺序看,它们其实是一次连续的能量搬运:
- 场景渲染输出近似无限动态范围的线性光,Bloom 把高光溢出这一真实现象加回来——加法;
- 显示器动态范围有限,色调映射把无限压进有限——减法,且观感不能塌;
- 前两步都在空间与时间上引入新的高频,抗锯齿收拾自己留下的烂摊子——采样。
我把它叫作"能量的漏斗"。整篇文章都挂在这条主线上,而不是把三样东西各自孤立地讲一遍。
先划一条边界:本文只讨论后处理链中的这三段,以及它们彼此的相互影响;完整的后处理清单(景深、运动模糊、色彩分级等)、各家引擎的后处理图配置、具体游戏的画质调优建议,都不在本文范围内(理由见 2.3 节)。
所以本篇要回答的是三个"为什么",而不是三个"怎么用":
- 高光为什么会溢出,Bloom 凭什么可以"把真的加回来"?
- 无限动态范围压进有限范围,为什么不能直接线性截断?
- 抗锯齿到底在跟什么作对,为什么没有一种方法能全部解决?
1.3 全系列内容总览
- 【实时渲染管线探析】(一) 延迟渲染:从原理到实现的性能权衡
- 【实时渲染管线探析】(二) 阴影贴图:从 PCF 到 VSM 的演进
- 【实时渲染管线探析】(三) 后处理链:Bloom、色调映射与抗锯齿 ← 本篇
- 【实时渲染管线探析】(四) PBR 与 IBL:从渲染方程到工业实践
- 【实时渲染管线探析】(五) 可见性与剔除:从视锥到 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 叠加"这条被多篇文章写错的官方口径。
背景铺垫
2.1 后处理为什么值得单独讲
一个容易被忽略的事实是:后处理是唯一一段"看不见几何"的渲染。
OK,前面的所有环节都在处理三维信息——顶点、三角形、法线、材质。到了后处理,输入是一张已经光栅化完的图像(加上若干辅助缓冲区),输出还是图像。这意味着它的成本模型和前面完全不同:它不随场景复杂度增长,只随分辨率和带宽增长。1080p 升到 4K,几何量不变,后处理的填充率翻了四倍。
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²。
落差就在这里:场景侧是"没有上限的线性光",显示侧是"有明确上限的编码值"。这中间必须有人做取舍,而取舍必然有损——这正是下一章之后所有讨论的前提。
再看一个真实世界里高动态范围的例子——这张全景由多张不同曝光合成:
同一场景,HDR 呈现与 LDR 呈现的差别是这样的:
2.3 本文的范围边界
本文只讨论后处理链中的 Bloom、色调映射、抗锯齿这三段,以及它们之间的相互影响。以下内容不在范围内:
- 完整的后处理清单(景深、运动模糊、色差、颗粒、色彩分级)——只在说明先后顺序时提及位置,不展开算法
- 各家引擎的完整后处理图配置——版本差异大,属工程配置而非算法原理
- 具体游戏的画质调优建议——本文给取舍依据,不给"这台机器该开什么"
- 主机硬件的 AA 实现细节——只在讨论 MSAA 时点到代价
- 光线追踪降噪——另一个完整话题
性能数据只引用论文、厂商官方文档与官方技术章提供的实测,社区横评只用于交叉印证量级,并会标明来源性质。
2.4 术语对齐
后文统一使用简称,先对齐一轮,避免中英混用造成歧义。
几个容易混的,这里单独说清:
- 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 常被当作"让画面高级一点"的后期特效,但它的原型是真实光学系统里的事——强光在镜头与眼睛内部散射,亮的物体周围因此出现一圈光晕。下面三张不是渲染结果,是相机拍出来的。
三张照片放在一起看,能读出这类光斑的一个共同特征:它总是围绕光源出现,且形状与光源本身无关。
好了,我们正式开始。第三张是这里最有说服力的一张。加装遮光罩后光晕明显减弱,说明这部分光并非场景里真实存在的亮光,而是镜筒内壁的杂散反射造成的。换句话说:光晕是成像系统的响应,不是被摄物本身的属性。
这直接决定了 Bloom 的定位——它不是"凭空造一个好看的效果",而是把一个被渲染过程遗漏掉的真实响应补回来。
同一类“成像系统的固有响应”还有别的形式——高对比边缘的彩色镶边也是镜头带来的,被摄物本身并没有这种颜色:
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.3 PSF 的形状从哪来:光圈决定核形状
好了,PSF 不是任意的。在几何光学近似下,焦外点光源成像的形状就是光圈孔的形状(Aperture Shape)——圆形光圈得到圆形光斑,多边形光圈得到多边形光斑,这就是摄影里说的散景(Bokeh)。
下面两张照片可以直接对照:焦外高光点呈现出规整的多边形,形状由镜头的叶片数决定。
光圈形状不只决定散景,也决定景深过渡——引擎里的深度虚化是这样的:
光圈不只决定散景形状,也决定焦外过渡:
工程后果很直接:想要"物理正确"的 Bloom,卷积核应来自光圈形状,而不是随手挑一个高斯核。 圆形光圈的散焦斑(Circle of Confusion)在几何上是均匀圆盘,其卷积结果是"圆盘模糊"而非高斯模糊;只有考虑衍射时才会趋近带环的艾里斑(Airy Disk)。
言归正传。到这里已经能看出一个常见的偷懒:很多实现直接用一个高斯核代替 PSF。这不算错——高斯在视觉上足够接近,而且可分离(Separable),成本低得多。但它同时也意味着"物理正确"只是一种近似,这是后面讨论能量守恒时要注意的前提。
散景的形状直接暴露出光圈孔的形状:
把光圈换成心形,焦外的光点就变成心形——这是“PSF 形状 = 光圈形状”最直白的证据:
真实镜头的光圈叶片是这样排布的:
另一类让点扩散开的是色差——不同波长的光折射率不同,焦点也就不重合:
放大看,这种镶边是有方向的:
3.4 从 PSF 到可实现:分离卷积把代价降下来
卷积的代价可以用可分离与分层来摊薄,引擎里的混合模糊就是这样:
如果老老实实按 $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 的收益主要不在"亮",而在"边界过渡"。没有 Bloom 时,高亮物体与暗背景的交界是一条硬边;真实照片里这条边是渐变的——人眼对它极其敏感,这就是去掉 Bloom 后画面立刻显"假"的原因。
引擎里 Bloom 的另一组真实输出:
最后一个顺序问题,很多人踩过坑: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
三条可引用的量化结论:
- 分离卷积把二维卷积代价从 $O(d^2)$ 降到 $O(2d)$,$50\times50$ 核由约 2500 次读取降到约 100 次。
- Bloom 可在 1/3–1/4 分辨率计算,填充率节省 9–16 倍,观感几乎无损。
- 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 则整图发灰。
下面我们进入正题。两条路线的差异不只是"少一个参数",而是处理哲学不同:阈值是"先判断再保留",乘系数是"全都保留但整体压到很小"——后者连续、无可调拐点,代价是 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 文档描述为"一单位宽的线性过渡带"。
阈值路线的参数在引擎里是这样起作用的:
4.3 Unreal 的 Threshold = -1:一个被误读的参数
Unreal 的 Bloom 文档里有一句常被引用但很少被解释的话:
将 Threshold 设为 -1 表示所有像素同等贡献(physically correct),且由于省掉了阈值 Pass 而更快。
这句话需要拆成两层来读。
第一层是语义:Threshold 设为 -1 之后,阈值判断被关掉,所有像素都进入 Bloom 层。这实际上就走到了上面那条"全程乘系数"的路线上。
下面开始。第二层是为什么更快:阈值判断本身需要一个 Pass 来算出每个像素的权重并写入中间缓冲。关掉它之后这个 Pass 被整个省掉,带宽与 ALU 都省下来——在 Unreal 官方文档里,这是一个明确标出的性能收益。
所以"物理正确"这四个字在这里的含义是:没有人为的亮度筛选,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$ 是加权重归一化。
最容易漏掉的是那个除法。 只加权、不归一化,等于给每个采样点乘了一个小于 1 的权重,整张图会整体变暗;除以权重和,加权平均才回到正确的亮度量级。Jimenez 在原作者页的技术问答里专门强调过,中文文章里几乎都没写。
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 阶段的输出差异,可以直接看出来。
亮部提取这一步在引擎里有一张直接的视觉凭据:
4.7 一个绕不开的取舍:能量守恒
现在可以回答本章开头那个疑问了。
普通 Bloom 不只是"让亮的更亮",它还会增加画面总能量。 Unreal 官方文档的表述是:Standard Bloom 不是能量守恒的(not conservative),会导致整图变亮——卷积结果叠加回原图时若不做归一化,总能量必然增加。
先来看看现象。真正的能量守恒做法要换一个模型:把 Bloom 理解为从高光区搬到周围的那部分能量,而不是额外加上的亮度。这就要求卷积核满足归一化条件(权值和为 1),并且叠加时的系数与"搬走了多少"相配。
Unreal 的 Bloom Convolution 走的就是后一条路。官方文档说明它用卷积核纹理来建模相机与眼内光的散射与衍射,是能量守恒的卷积,底层用 FFT 加速。代价是算力——所以官方定位是"用于过场与高端硬件"。
下一章就顺着这条线,把工业实现的几种形态拆开看。
三、Bloom 的实现分野:从高斯到 FFT 卷积
5.1 先给全景:四种形态
到这里,Bloom 的"要做一次卷积"已经清楚,但在工程上"怎么做这次卷积"有四种主流形态,它们的取舍差别很大。先给一张总表,后面分节展开。
引擎把 Bloom 的尺度组织也做成了可选档位:
| 形态 | 做法 | 质量 | 相对成本 | 内存 | 能量守恒 |
|---|---|---|---|---|---|
| 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),一次采样可取到两个纹素的加权平均。
金字塔分层的实际分配,引擎里是这样展示的:
5.3 形态二:mip 金字塔上的 13-tap 与 tent filter
COD AW 的路线在"怎么做这一次模糊"上给出了一个成本与质量的平衡点,也是目前最被广泛抄写的一套。
降采样用 13-tap。 每级 mip 取 13 个位置加权平均,权重兼顾覆盖率与稳定性。13 不是随口定的——其分布让每个输出纹素均匀覆盖 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 完成归一化。
上采样与模糊半径的关系,可以直接看引擎的输出:
tent filter 的关键在于半径随 mip 级放大。 Jimenez 把半径固定在 UV 空间,而每级 mip 分辨率减半,对应的实际像素半径就逐级翻倍:低级只影响邻近像素,高级影响很大一片。若干级串起来,结果在数学上逼近高斯,成本却远低于一次大核高斯。 这正是它"看着太简单"却成为主流的原因——简单性匹配了"多级叠加"的结构。
5.4 形态三:Kawase 与 Dual filtering
先来看看结论。Kawase 的思路是放弃"算得准",只要"看起来够用":每个输出像素只取四个角的采样求平均。中心区域靠双线性插值补上,天然形成大范围模糊;要更大范围就多迭代几次。
Unity URP 的 Kawase 模式就是它:固定尺寸纹理上做 4 角采样,官方给出的适用场景是"低分辨率下更快、省内存"。
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)都是归一化因子。
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)。所以做一次大核卷积可以拆成三步:正变换、逐点相乘、逆变换。
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 的演进按时间串起来,能看清每一步是为什么而发生。
同一套分层机制,换一个视角看它的层级结构:
| 阶段 | 时间 | 关键变化 | 动机 |
|---|---|---|---|
| 分离卷积 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 的正午天空,输出完全一样——都是纯白。动态范围里最亮的那一段被整体剁掉了,而这恰恰是人眼最敏感、观感上最有价值的区域(高光层次、发光体的质感)。
这不是"损失一点精度",而是信息的结构性丢失:被剁掉的部分再也无法在后续任何步骤中恢复。
下面这张是典型的"HDR 输入"——室内与窗外的亮度差了三到四个数量级。如果直接线性映射,窗外的部分会变成一片惨白,室内则一团黑。
色调映射的直接结果,可以看这张实拍:
先看一张未做色调映射的 HDR 画面——高光处已经溢出:
直接截断成 LDR 之后,高光细节被削平:
曝光多加一档,高光就整片糊死:
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)。
开始正文。这条曲线的形状决定了画面的性格。三个区域各司其职:toe(暗部) 决定阴影里的细节保留多少;中段 通常保持近似线性,因为它承担了大部分可见内容;shoulder(高光) 决定高光区被压得多柔和——压得越陡,高光越早变白。
6.3 场景参考与显示参考:两种不同的"真实"
上一步引出一个更基础的问题:为什么不能"忠实还原"?
答案是:"忠实"这个词在两端指的不是同一件事。
场景参考(Scene-referred)图像描述的是物理光量,每个像素值对应一个可测量的辐射亮度。显示参考(Display-referred)图像描述的是显示设备上的编码值,每个像素值对应一个设备响应。前者有物理意义,后者有设备意义。
正餐开始。物理学角度最"忠实"的做法,是把场景亮度原样搬到显示器上。但前面算过了——两者的动态范围差着十几个数量级,做不到。于是必须做取舍,而取舍就意味着放弃"物理忠实",转向"感知忠实"。
感知忠实的目标可以概括为:让观察者在显示器上看到的相对亮度关系,尽可能接近他在真实场景中的感受。 注意这里的关键词是"感受",不是"数值"。
6.4 一个具体的工程问题:中灰会被显示成白
NVIDIA 的开发者文章《Preparing for Real HDR》里给了一个很扎实的例子,值得完整复述,因为它把"为什么经典算子不够用"讲得很具体。
问题在于:经典算子(最典型是 Reinhard)的输入只有一个——图像里的最大亮度,它不知道输出设备实际能到多亮。
依然是国际惯例,于是会出现这种事:把算子的 $[0,1]$ 输出直接映射到 1000 尼特显示器上,画面里的"中灰"(比如沥青路面)本应对应几十尼特,现在被显示成几百尼特——它看起来和普通 SDR 显示器上的白色一样亮。
这就是为什么工业界的做法最终转向了与显示设备能力绑定的映射,而不是纯图像自适应的映射。这也是本章后面要讲的曝光(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 体系正是这个思路。
曝光对画面的作用,可以用一组连续的帧直观看到:
反过来减三档,画面整体压暗但高光层次还在:
同一场景不同曝光时长的系列,可以直观读出“档”的含义:
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 与 Bloom 放在一起,引擎侧的完整效果是这样的:
一张实际做过色调映射的照片是这样:
同一思路的另一条落地方式是曝光融合——把多张不同曝光的照片合成出一张:
五、算子谱系:Reinhard / Hable / ACES / AgX
7.1 先给全景
色调映射算子不是一个一个独立发明的,它们背后是几种不同的出发点。按出发点归类,主流算子可以分成四支。
| 算子 | 出发点 | 关键参数 | 高光表现 | 色彩倾向 |
|---|---|---|---|---|
| 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 算子的落地效果——下面是同一张 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 等),它是配参数用的曝光前缩放,改它等于改整体曝光——它不是色调映射的一部分,这一点在对照不同版本的代码时要留意。
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"。 上面的式子是对 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),目的是在压缩过程中保持色相不变。
- 工作空间上,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 的中段过渡极平滑,反而容易让颜色发闷、缺少层次。
社区(Blender 5.0 实拍的对比讨论)给出的选型经验值得记录:
darktable 的文档里还有一条规则值得抄下来:永远不要同时使用两个 tone mapper。 这句话背后是同一个道理——色调映射是非线性的,串联两次等于把曲线叠了两遍,结果不可预期。
7.6 四条曲线叠在一起看
把四条曲线画在同一张图上,差异一目了然。为了可比,这里统一把输入亮度按相同曝光缩放。
从图上能读出三件事:
- 中段的斜率决定了对比度观感。 ACES 的中段最陡,所以画面最"有对比";Reinhard 最平,所以最"发灰"。
- toe 的形态决定了暗部细节。 Hable 的 toe 抬得最高,暗部细节最清楚。
- 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 通常只在特定视角或特定场景下出现。
色调映射之后通常还会接一步色彩调整:
7.8 演进脉络
| 阶段 | 时间 | 提出者/来源 | 关键贡献 |
|---|---|---|---|
| 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 中配置。它模拟的是人眼从暗室走到亮室外的适应过程:场景整体变亮时,逐渐降低曝光;变暗时,逐渐提高曝光。
关键参数有两个:
Apply Physical Camera Exposure(物理相机曝光):启用后用快门速度(Shutter Speed)、ISO 与光圈(Aperture)三个物理量计算曝光,而不是直接给一个 EV 值。这让美术可以用"摄影语言"调画面。Bias(曝光补偿):在对数尺度上微调曝光,等价于摄影里的 EV 补偿。这是最常被美术使用的参数。
真实相机处理这件事的办法是包围曝光——同一场景连拍多档,再挑一档或做合成:
8.2 EV100 体系:把曝光变成可解释的量
Unreal 在 4.25 引入了 EV100 体系,并配套了 HDR 可视化工具(ShowFlag.VisualizeHDR 1)。这个改进值得单讲,因为它把"调曝光"从经验试错变成了可读的诊断。
EV100 的含义是:以 ISO 100 为基准,当前场景亮度对应的曝光值。Unreal 的可视化里:
- 蓝线表示目标 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 模式下所有参数都参与色调映射,几个关键量:
- White level:设定参考白的亮度。它决定了"多亮算白"。
- MaxCLL(Maximum Content Light Level):内容中单帧最亮像素的峰值亮度。
- MaxFALL(Maximum Frame-Average Light Level):内容中单帧的平均亮度上限。
后两个参数的意义在于:它们让显示设备知道"这片内容最亮会到多少",从而可以按内容做局部调光策略。Gamma 模式则牺牲亮度范围换取"10 bit 全用于色深"。
引擎里的 HDR 相关开关集中在环境设置这一层:
8.4 色域:HDR 多出来的那片颜色
色域(Color Gamut)描述的是"一个系统能表示哪些颜色",通常画在 CIE 1931 色度图上。
下图对比了几套常见标准覆盖的范围。
BT.2020 比它们更宽——它是为 UHD 时代设计的,覆盖了 DCI-P3 之外更大的一块。
这件事对色调映射的直接影响是:HDR 内容转到 SDR 时,多出来的那片颜色必须被处理掉。 官方文档给出的两种做法是:
- Clip(硬裁切):超出 BT.709 边界的颜色直接压到边界上。快,但饱和色会出现可见的色相跳变。
- Compress(渐进压缩):把超出的部分平滑地压缩到边界内。慢,但色相连续性更好。
亮度/对比/饱和三者的作用,可以分开看:
这与前面讲的"线性截断 vs 曲线映射"是同一类取舍——都是"快速丢弃"与"平滑压缩"之间的选择,只不过一个发生在亮度维度,另一个发生在色度维度。
8.5 一张 HDR 管线的整体对照
把前面的内容收进一张表,看清 SDR 与 HDR 管线在各环节的差异。
HDR 管线里还有一步常被忽略的环节——色彩分级(3D LUT):
| 环节 | SDR 管线 | HDR 管线 |
|---|---|---|
| 工作色域 | BT.709 | BT.2020(HDR10)/ 依标准 |
| 传递函数 | γ2.4 | PQ 或 HLG |
| 位深 | 8 bit | 10 / 12 bit |
| 色调映射参与度 | 固定曲线 | 所有参数都参与(White level / MaxCLL / MaxFALL) |
| 色域处理 | 无需(同域) | 需 Clip 或 Compress |
LUT 本身是一张被摊平的三维查找表,长这样:
七、抗锯齿的靶区:三类别名与 Nyquist 视角
9.1 走样的本质:采样率不够
抗锯齿的教材通常从 Nyquist 采样定理讲起,这里也这么开。
采样定理(Nyquist–Shannon Sampling Theorem) 说的是:要把一个信号完整还原,采样频率必须至少是信号最高频率的两倍。低于这个下限,高频成分就会"折叠"到低频上去,形成混叠(Aliasing)。
$$ f_s > 2 f_{\max} $$
其中:
- $f_s$ 是采样频率。
- $f_{\max}$ 是信号中的最高频率。
- $2 f_{\max}$ 称为 Nyquist 频率。
先看没有抗锯齿时的样子——这是走样最直观的形态:
走样在几何边缘上的表现是这样的:
马里兰大学 CMSC 427 的课程讲义给出的解决方向有三条,正好对应抗锯齿的三条技术路线:
- 过采样(Oversampling)——提高采样率。对应 SSAA。
- 预滤波(Pre-filtering)——在采样前把高频滤掉。对应 mipmap。
- 随机采样(Stochastic Sampling)——把规则采样换成随机分布。对应抖动与 TAA。
采样率不够的走样在真实摄影里随处可见——拍一块 LED 屏,传感器像素栅格与屏幕像素栅格拍频,就出现摩尔纹:
9.2 渲染里的三种别名
渲染中的走样不只一种。把靶区分清,才知道每种方法在打什么。
| 别名类型 | 成因 | 表现 | 对应手段 |
|---|---|---|---|
| 几何别名(Geometric) | 几何边缘处可见性跳变 | 锯齿状的边缘 | MSAA / SSAA / CSAA |
| 着色别名(Shading) | 高频着色(高光、细节纹理) | 高光闪烁、纹理噪点 | SSAA / 后处理 AA / TAA |
| 时间别名(Temporal) | 帧间采样位置固定 | 静止闪烁、运动中的爬行 | TAA / 超分 |
这个三分法是本文认为最有价值的一个视角,原因是:它解释了为什么没有一种 AA 能解决全部问题。
- MSAA 只采样可见性,所以它对几何别名有效,对着色别名完全无效——因为着色只做了一次。
- 后处理 AA 在已经渲染好的图像上做边缘检测与软化,能改善几何边缘的观感,但对已经丢失的高频着色信息无能为力。
- TAA 通过多帧累积把两类空间别名都摊薄,但引入了时间维度特有的新问题:鬼影。
纹理走样最经典的形态是摩尔纹——它是采样率不足的直接产物:
摩尔纹是三种别名里最容易被忽略的那一种:
9.3 mipmap 已经解决了什么,还剩什么
一个常见困惑是:既然有 mipmap 了,为什么还会有纹理走样?
CMSC 425 的课程材料把这个边界划得很清楚:mipmap 解决的是纹理走样,不解决几何走样。 这两件事的成因不同。
- 纹理走样来自纹理坐标在屏幕空间的收缩——一个像素覆盖了很多纹素。mipmap 通过预滤波(选合适层级的、已降采样的纹理)解决它,这正是前面说的"预滤波"路线。
- 几何走样来自可见性的二元跳变——一个像素要么在三角形内,要么在外,没有中间状态。mipmap 无法解决,因为它根本不涉及几何边缘。
顺带说一句,这也解释了为什么"把纹理质量调到最高"对锯齿边缘毫无帮助。
9.4 方法的完整清单与归类
按产生的时期与机制,抗锯齿方法可以归成四代。
| 代数 | 方法 | 机制 | 靶区 |
|---|---|---|---|
| 第一代:超采样 | 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= MSAAr.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 打开后的实际差别:
MSAA 的核心是把“覆盖”与“着色”分开:
说白了,但这条设计也直接决定了它的局限:因为它只采样可见性,所以它对着色别名完全无效。 高光闪烁、纹理噪点这些高频着色问题,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 略高。
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 在引擎里的表现——先看 FXAA:
同样属后处理路线,SMAA 的边缘处理更精细:
10.4 效果图:几何边缘的采样密度
下面我们开始。这里本该放一张实机的"开 AA / 关 AA"对照截图。但本文在素材检索阶段对 Wikimedia Commons 做了三轮检索(关键词分别为 antialiasing comparison、supersampling antialiasing、Mandelbrot antialiasing samples),确认没有长边 ≥1280 且许可干净的实机对照图——现有素材此前记录的最大尺寸为 264×286(本次复核发现同类素材还有更大的,最大 819×431,但均未达 1280)。
因此这一格以自绘原理图为主,并在此明确声明:主图为示意图,不是实机截图。下面另补入两张真实的抗锯齿对照素材作为佐证(尺寸偏小,仅作原理级对照)。
把采样密度再往上推一档:
把 MSAA 档位降到 2×,边缘质量的变化很直观:
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 |
两组数字值得分别解读。渲染成本只上升约 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 打开后:
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 的一个不得不承认的性质:它的画质依赖帧率。 帧率越高,帧间位移越小,重投影误差越小。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)。这条式子的含义是:如果历史颜色与当前邻域的颜色差异过大,就把它拉回来。
但邻域钳制有一个内在矛盾,前面引用的技术文章把它点得很准:钳制过于激进,会把本该累积的子像素细节一起丢掉,结果就是模糊。 所以钳制的松紧是一个必须调的取舍——太松有鬼影,太紧有模糊。
对于薄物体与粒子,问题更严重:它们在邻域里占比很小,min/max 包围盒本身就不稳定,钳制可能完全失效。针对这类情况,工程上还有两件工具:
- 速度加权混合:运动矢量幅度大时降低历史权重。社区实现的默认历史权重约 0.9,速度加权让它在快速运动时降到更低,从而减少拖影。
- 基于移动性的抗鬼影:Unreal 的
r.TemporalAA.Quality = 2(默认值)就是"开启输入滤波并带基于移动性的抗鬼影"。
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,等于把两套时序滤波串联——每一层都在低通,结果是明显糊掉。
超采样是最“暴力”的解法,代价也最直接:
与之相对的是重建式超分——用更少的采样推断出高分辨率:
超采样档位之间也有差别:
另外两个事实也值得记下:
- 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 成本对照表
11.8 演进脉络
| 阶段 | 机制突破 | 代表方法 |
|---|---|---|
| 超采样 | 提高采样率 | 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 回到那条主线
把三章合起来看,后处理链的形态就清楚了。它是一条能量的漏斗:
- 加: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.