问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

ToneMapping技术详解:从基础概念到UE5中的工程实现

创作时间:
作者:
@小白创作中心

ToneMapping技术详解:从基础概念到UE5中的工程实现

引用
CSDN
1.
https://blog.csdn.net/weixin_43762386/article/details/142989481

ToneMapping是计算机图形学中一个重要的技术概念,主要用于解决高动态范围(HDR)图像在低动态范围(LDR)显示设备上的映射问题。本文将从HDR和LDR的基本概念出发,深入探讨ToneMapping的原理、发展历程,以及在UE5游戏引擎中的具体实现方式。

ToneMapping到底是什么?

要回答这个问题,首先需要了解HDR和LDR的概念。

目前市面上大部分入门级的显示设备都继承了256个亮度层级的规格,这样的显示器就是LDR显示器,LDR也就是只有256个亮度层级的色彩范围。一般来说,在渲染的rt里,LDR意味着每个像素的每个通道,取值范围是[0, 1]

但是,256个亮度层级对于记录现实世界的画面,是完全不够用的。举例来说,大白天的太阳直视亮度如果记为4000,那么此时,房间内通过窗户照亮的区域,亮度可能只有80,房间内阴影区域可能亮度仅为0.55。假如同一个画面里,可以透过窗户直接看到太阳,且也能看到房间内的大部分区域,那么,如果要把0.55-4000都放在同一个画面上,亮度普遍低于100的室内区域,在256个亮度层级中,仅能使用(256*100/4000)=6.4个层级,从视觉上来开房间内的区域就是一片黑。如果你想把256个层级中的大部分都用在室内,那么窗户外的画面可能几乎都被截断在最高亮度层级,也就是窗户外一片曝白。

这张图就是一个很好的例子,车窗外的天空一片曝白看不清细节,而车内的仪表盘等位置又过于黑。这种情况的根本原因就是256个亮度层级并不够真实世界的亮度范围去分配。

为了真实表现出画面中各个区域的亮度,就有了HDR技术。HDR设备有远超256的亮度层级,对应的像素值的取值范围也就可以大于1。由于实际应用中,像素值大部分情况,极值都在100一下,远小于HDR的最大取值,因此可以近似的认为,HDR像素的取值范围就是[0, +∞)

实际上,现在的游戏为了还原真实光照,开发了PBR流程,而PBR渲染的原本未作后处理的输出,都是HDR的。然而,现实是HDR设备还远没有那么普及,且市场上大量所谓的HDR设备,其亮度层级也仅仅是略多于256,远不足以满足接近[0, +∞)的HDR画面效果。因此,我们不得不思考,如何将HDR的画面显示在LDR的设备上。

不论是简单截断到01,还是按照画面最大值归一化,都会遇到前文提到的窗户向外看的问题。因此,我们需要一种更好、更聪明,更符合视觉感受的技术,来将HDR的画面映射到LDR设备上。

从数学上讲,就是将[0, +∞)的输入,映射到[0, 1]的输出上。这就是ToneMapping。

ToneMapping VS Gamma Correction

可能有些人和我一样,一开始傻傻分不清ToneMapping和Gamma Correction,实际工程中也经常出现ToneMapping和Gamma校正在同一drawcall中实现的情况,也就让新手更加混乱了。这里一次性讲清楚。

从数学上讲,ToneMapping是将[0, +∞)的输入,映射到[0, 1]的输出上,Gamma校正是将[0, 1]的输入,映射到[0, 1]的输出上。输入的取值范围有根本性的不同。

从目的上讲,ToneMapping的目的是将HDR的画面映射到LDR的显示器上。Gamma矫正则是为了应对人眼在显示器最大亮度范围内,对于亮度的感受并不线性的问题。常说的0.18灰直观的反应了这个问题,在肉眼的感受中,0.18的亮度是在感受上介于显示器最亮于最暗的中间值。

总之,ToneMapping是将远超显示器亮度层级的画面映射到显示器能够显示的亮度层级内,Gamma校正是更好的分配显示器已有的层级,让肉眼在显示器的亮度范围内,对明暗区域的感受更加均匀。某种意义上来说,二者是上下游的关系,ToneMapping的输出一般可以作为Gamma校正的输入,为了方便也为了性能,实际工程中经常在一个drawcall中一次口气做完ToneMapping和Gamma校正。

ToneMapping的发展史

戳这里,已经有知乎大佬生动的讲完了,我觉得过于完美,就不洗稿了。

总之,要知道的是,ToneMapping说到底就是一条曲线,一条将[0, +∞)的输入,映射到[0, 1]上,且尽可能保留画面细节信息的曲线。而现在最酷最潮的ToneMapping曲线就是ACES,它的简略版长这样:

UE5里的ToneMapping

UE5采纳的就是ACESToneMapping。如果你现在去翻UE5的Shader代码,你就会发现,怎么代码里完全找不到上述公式?

答案很简单,UE5采纳的是更复杂的ACES版本,我们上文展示的仅仅只是抖音省流版的精简公式。

实际上,UE5使用的ACES版本要复杂的多,且出于性能考虑,将原教旨主义的——将[0, +∞)的输入,映射到[0, 1]的输出上——的ToneMapping,和很多其他功能整合在了一起,并通过一个3DTexture加速。

那么先别急,因为UE5的算法在工程上非常严谨,而工程上严谨就意味着非常繁琐。在正式开始讲解UE5的ToneMapping前,你还需要先理解一个概念——Color Space!

Color Space

戳这里,这是我看到的对ColorSpace讲解最为清晰直观的视频,大家直接看视频就好了,因为色域这东西本来就不好用文字解释,还是看视频直观。我这里就直接不解释了。

需要记住的就是,XYZ坐标系是所有色域的超集,任何一个色域,都是在XYZ坐标系下找到三个点来确定rgb,并配合一个白点确定何为白色,由此划定出来的XYZ坐标系的子集。因为所有的变换都是仿射变换,所以各个色域的坐标点之间,可以通过线性代数中的矩阵相乘来相互转换。

具体到ACES,它自己就定义了两个色彩空间——AP0和AP1

如图可以看到,这两个色彩空间各自给出了自己的Red、Green、Blue点的XYZ坐标系坐标,二者共用一个白点所以也就给了一个White Point坐标。

如前文所说,某一色域下的颜色值,可以通过矩阵乘法转换到其他坐标系下的值。那么怎么计算这个转换矩阵呢?这里就要介绍一个方便的工具网站。

输入RGB和WhitePoint,即可计算出这个色域到XYZ坐标系的正反转换矩阵,不同色域通过XYZ坐标系作为中介,矩阵累乘即可完成转换。

好了,现在我们可以继续聊UE5的ToneMapping了

再探UE5里的ToneMapping

UE官方文档对UE5的ToneMapping介绍比较简洁笼统。

UE5的ToneMapping控制完全通过Post Process Volume(PPV)控制。UE5给出了5个参数来控制ToneMapping的曲线。这里我就不详细解释了,一来官方文档又具体的对比例子,二来仅仅从数学上,解释意义并不大。还是需要TA或者美术来为了视觉效果定制。

UE暴露这几个参数就意味着UE5允许开发者主动对ToneMapping的曲线进行一定程度的定制,而不是定死了使用原版的ACES。

UE5在shader源码里记录了ACES和其他版本的ToneMapping技术对应的参数

如果你想使用原版的ACES,那么就按照这个参数设置。挺乐的,不知道为什么,UE官方把配置写在了Shader代码的注释里,也没写官方文档,这给谁看啊,美术肯定看不到啊。

在运行时,ToneMapping被分为了两部分实现。

第一步:计算一个3D Texture的查找表

UE5的ToneMapping首先会通过一个单独的drawcall计算一张查找表,这个查找表的输入是log空间的颜色值。整个查找表是32X32X32的3D Texutre。

我们先讲为什么能这么做,再讲为什么要这么做。

首先,我们说过,ToneMapping本质就是一条将[0, +∞)的输入,映射到[0, 1]上的曲线。你会发现,这个曲线的计算结果只和输入有关,而不会收到画面信息的影响。换句话说,不论周围的像素如何,同一颜色的像素输入给某一ToneMapping,输出结果也是唯一的。因此,我们完全可以预计算一张查找表,这张查找表记录了所有输入像素值到输出像素值的计算结果,在实际对画面像素ToneMapping的时候,只需要用像素查找这张表的对应值即可。

那为什么要这么做?原因很简单:节省性能。首先,ToneMapping计算并不便宜,需要涉及从原本的色彩空间转换到ACES AP1空间再转回去,完整的ACESTonemapping也有大量的多项式乘除,UE5还开放了PPV中的参数控制ToneMapping曲线,使得计算更加复杂。如果我们对画面像素实时计算,那么2K的分辨率就需要对25601440≈3.6M个像素进行这一套计算。但如果只是建立一个3232*32的查找表,那就只需要对32K个像素进行ToneMapping计算!少了两个数量级!

更何况,我们说了,ToneMapping的曲线与画面输入无关,只受到PPV控制参数的影响,那就意味着我们并不需要每一帧都生成一次查找表,只需要在PPV参数变化的时候重新生成即可!节省!太节省了!

事实上,UE将一大堆只与像素颜色值有关的计算全部集成到了这张查找表里。如果你去翻UE shader,你会发现,float4 CombineLUTsCommon(float2 InUV, uint InLayerIndex) 函数是这个CombineLUTS Drawcall的主体函数,而这个函数里集成了WhiteBalance、BlueCorrect、ColorCorrection、ToneMapping以及GammaCorrection。也就意味着,这些操作都不需要每帧做一次了,只需要在查找表上sample一下即可。

第二步:将ToneMapping绘制到画面

这一步的drawcall就是实际绘制到最终画面了。

还是一样,本着能省则省的原则,UE5将许多操作集成到了这个Drawcall中。事实上,这个Drawcall处理了许多模拟真实相机的计算操作。它的主体函数是float4 TonemapCommonPS(…) ,包含了相机瑕疵模拟、Bloom、vignette、局部曝光、Sharpen、胶片质感模拟、应用ToneMapping等操作。可以看到这些操作都是与画面信息相关的操作,对于同一像素值可能有不同的计算结果,因此没法事先生成LUT。

总结

ToneMapping说简单也简单,说到底就是一条将[0, +∞)的输入,映射到[0, 1],且尽可能保留画面细节信息的曲线;说复杂也复杂,因为高质量的ToneMapping需要严格的色域计算,且在工程中,为了节约性能,往往会建立LUT加速计算,且和一大堆其他输入域与输出域相似的计算结合到一起。像在UE中修改ToneMapping功能,难得可能不是算法设计,而是在复杂的工程结构中正确定位ToneMapping代码并理清楚上下文吧。

哈哈~

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号