David902 发表于 2020-12-23 09:14

Eye Adaptation综述及Unreal实现解析

总览

Eye Adaptation或者叫Automatic Exposure,是游戏中模拟人眼在不同光照情况下,自适应调整眼睛使得能看到更多信息的过程。看一下游戏中的一个使用范例,视频来自《神秘海域4》:


https://www.zhihu.com/video/1225717601965359104
可以看到,当德雷克从室内环境走到室外环境的过程中,外部画面的亮度调整,大大增加了景色入画的震撼。生物中,是靠视杆细胞和视锥细胞调节我们所看到的画面的明暗,视杆细胞负责夜视,视锥细胞负责亮视,视杆细胞调节速度要低于视锥细胞,所以,人们从亮到黑的调节能力要低于从黑到亮。在游戏渲染实现里,主要包括单帧中通过计算平均亮度来调整当前帧的颜色,以及帧之间如何过度。思维导图如下:


目标

我们的最终目的是为了调整画面到某个合适的亮度,使得画面不至于过暗或者过亮而丢失视觉信息。调整的方法,是用到了图像处理的log平均亮度调节法,我们需要一个调整过程来模拟眼睛的调节过程,所以不仅需要当前帧的画面亮度,也需要前一帧的画面亮度,我们通过前一帧和当前帧的平均亮度重新计算当前帧过度后的平均亮度,最后通过平均亮度来Tone操作调整整个画面的颜色。
计算平均亮度

log平均亮度的计算公式为:

https://www.zhihu.com/equation?tex=%5Coverline%7BL_w%7D+%3D+%5Cfrac%7B1%7D%7BN%7Dexp%28%5Csum%5Climits_%7Bx%2Cy%7Dlog%28%5Cdelta%2BL_w%28x%2Cy%29%29%29.
简单说就是累加每个像素亮度的$log$值然后取幂后平均,如果用CPU计算就很简单,但是由于是要对整个Scene Color计算,所以计算像素个数是非常多,并不适合使用CPU,但是对于累加这种线性计算,使用GPU计算也不容易。
基础的平均亮度计算

一种方案是分治的思想,把整个Texture分为多个Blocks,然后分别计算,也算是一种利用GPU并发性的方法。《ShaderX3 Deferred Lighting on PS 3.0 with High Dynamic Range》中提到的算法如下:


另外一种是使用Downsapmle,利用采样器硬件的插值来做平均,Downsample到一个合理的Texture大小的时候,然后就可以在一个Shader内计算平均值了。计算平均值可以在Pixel Shader中计算,也可以在Compute Shader中计算,Compute Shader中可以有更多的优化空间(详细可见Unreal的实现)。在《ShaderX3 Adaptive Glare》中有相关的实现,计算过程:


它的方法比较暴力,直接是Downsample到一个2*2的RT上,一步一步Downsample下来会得到更加好的平均值。
基于Histogram平均亮度计算

在图形处理的主流中,基于Histogram的调整是较为常见的方法。
创建Histogram

在各家API还不提供UAV之前,除了各种分治思想的计算之外,在《Efficient Histogram Generation Using Scattering on GPUs》中提到了一种方法。使用点原来代表一个像素进行点原的渲染,在vertex shader采样scene color,根据亮度找到目标Histogram的坐标,然后输出到pixel shader,这样就能blend到最终的Histogra Texture中去了,真的是巧妙。
有了Compute Shader之后,计算其实也是基于分治,好处是在Compute Shader里面对UAV的操作可以更加自如,我们可以直接将计算结果异步的存到UAV中,具体实现方案会在后面Unreal的实现中详细叙述。
在Valve的Source Engine里面实现了一种方法。执行一次比较亮度的Pass,在Pass的Pixel Shader中,使用stencil buffer来标记像素的亮度是否在我们想要的亮度范围里,然后设置好stencil test,是的只有在亮度范围内的像素才能通过test,对这次draw加上一次 occlusion
query,结果就是在亮度范围内的像素个数了。每帧亮度比较的Pass,只会测试一个亮度范围内的,多帧之后,组成一个完整的Histogram。这个方法生成的Histogram是CPU端的,然后由于occlusion
query的异步性,也不会阻塞CPU,当然occlusion
query本身也会增加Sync。下面是两次test的结果:
计算平均亮度

设定两个计算平均的百分比,找到满足大于这两个设定的百分比的第一个slot,也就是所有暗于找到的slot的slots中数值的累加要大于我们设定的两个百分比,比如,我们设定的百分比是80%,90%,那么我们需要找到第一个slot,它的slot的亮度要亮于80%的其他slot,还有一个是第一个亮于90%的其他slot。然后将这两个的slot的亮度相加取平均就是我们需要的平均亮度,当然还需要考虑权重,我们用多出来的百分比作为两者的权重。还是以80%,90%为例,如果第一个低位找到的是83%,第一个高位找到的是95%,那么他们的权重就分别为0.03与0.05。
帧之间的过度

为了模拟人眼的适应过程,不能直接使用当前计算的平均亮度来调节画面颜色,需要考虑之前帧的亮度。最简单的方式就是做一个线性的插值:

https://www.zhihu.com/equation?tex=%5Coverline%7BK%5E%7B%27%7D%7D_w%3D%5Coverline%7BL%7D_w%5E%7Bpre%7D%2Bc%28%5Coverline%7BL%7D_w-%5Coverline%7BL%7D_w%5E%7Bpre%7D%29.
《巫师3》的实现就是简单的线性插值,Unreal的实现是使用了指数的插值和4.25改进后的混合插值,过度更加柔和。
对画面进行Tone操作

在图形处理里面,Tone的操作可以分为Global和Local,区别就是对局部还是全局进行调整,Local的计算过于复杂,实时中多是用Global的方案。
用关键值进行Tone操作

关键值Tone操作的公式为:

https://www.zhihu.com/equation?tex=L%28x%2Cy%29%3D%5Cfrac%7Ba%7D%7B%5Coverline%7BL%7D_w%7DL_w%28x%2Cy%29.
其中a就是关键值,不同的关键值就会有不同的调整结果:


图像的暗部一般拥有比亮部更多的细节,所以,又有方案是在关键值Tone操作之后,压缩一下亮部,使用公式:

https://www.zhihu.com/equation?tex=L_d%28x%2Cy%29+%3D+%5Cfrac%7BL%28x%2Cy%29%7D%7B1%2BL%28x%2Cy%29%7D.
函数很简单就是压缩了高亮度部分,函数图像可能更直观:




为了更为细致的控制,可以用一个自定义的 https://www.zhihu.com/equation?tex=L_%7Bwhite%7D 来控制,这样可以减少更多的过曝情况:

https://www.zhihu.com/equation?tex=L_d%28x%2Cy%29+%3D+%5Cfrac%7BL%28x%2Cy%29%281%2B%5Cfrac%7BL%28x%2Cy%29%7D%7BL_%7Bwhite%7D%5E2%7D%29%7D%7B1%2BL%28x%2Cy%29%7D.
函数图像为:




Unreal4.25之前的Eye Adaptation实现

在Unreal4的很早期版本,Eye Adaptation就是其中的特性了,它的实现方案也是基于上述方案中的具体实现。
基础的平均亮度计算

基础的版本中,分为两个Pass,第一个Pass是SetUpPass,SetupPass是集成在Unreal的Downsample Pass中的,在每一次Downsample的Pass之后执行一次SetupPass,内容是计算log luminance到Alpha中参与下一次的Downsample,用最终的Downsample结果来计算平均亮度。计算平均亮度也分别用Compute Shader和Pixel Shader实现了,Compute Shader的好处是可以分块计算,效率更加高,Piexl Shader的实现就是普通的累加。
以Compute Shader为例,把已经DownSample的SceneColor按照SV_GroupThreadID分成N块,,每个Thread负责计算一块,最后将Thread之间累加起来,再求平均值。
基于Histogram的平均亮度计算

计算平均亮度的方式是与之前介绍的是一样的,主要介绍一下Unreal的Histogram的创建。Unreal是使用Compute Shader创建Histogram的,在4.25之前,Histogram的方法是只有在非手机上才可以的。
创建Histogram

Unreal的实现过程的思维导图如下:
第一个Pass,它创建了一个groupshared的HISTOGRAM_SIZE*THREADGROUP_SIZEX*THREADGROUP_SIZEY的三维float数组,也就是每一个Thread都有一个HISTOGRAM_SIZE大小的存储空间。每一个Thread将LOOP_SIZEX * LOOP_SIZEY个像素的亮度值累加到SharedHistogram中,这样每一个Thread就会有自己份的Histogram数据,计算完之后,sync。然后累加所有生成的Histogram数据,每四个生成的数据输出到一个Histogram Texture的颜色的值上(四个通道),这样就得到了ThreadGroupSizeX * ThreadGroupSizeY行的Histogram Texture。 第二个Pass,累加上一个Pass生成的Histogram Texture到一维的新Histogram Texture上。用图形表示为:


帧之间的过度

Unreal的过度是由一个指数函数来做插值的,函数图像如下:
在这个函数的基础上,它还提供了两个参数,用来控制从亮到暗和从暗到亮的速度,在本文刚开始介绍人眼的原理中提到过,这两个速度是不一样的,所以,这两个参数是必要的。
对画面的Tone操作

Unreal中的Tone操作就是单纯的基于关键值的Global的调节,其中关键值默认位1,还有一个EyeAdaptation_ExposureCompensation的参数可以用来调节a。
Unreal4.25的改进

Unreal4.25对Eye Adaptation进行了比较大的更新,从效果,美术工具上做了提升,而且还适配了手机端的Histogram的方案,手机适配这部分是中国团队完成的。
更改a的默认值

在官方blog里面对这一部分说了很多,刚开始我看的一头雾水,后来才明白,就是把a的默认值改成了0.18,在我看的论文中,默认值都推荐用0.18,解释那么多,我就不听了。与之相关的更改是把Low Precent和High Percent的默认值从80%,98.3%改到了10%,90%,这两个值就是用来计算平均亮度的百分比。这两个更改使得平均亮度和Tone的操作更加科学。
镜片透射比的更改

这个部分是我上面没有提到的,Unreal有一套基于物理的计算曝光的参数,这个是为了模拟相机的曝光过程,这个计算发生在我们的Eye Adaptation之前,列几个公式就明白了,

https://www.zhihu.com/equation?tex=B+%3D+Exposure+%2A+L.
其中B就是我们需要的亮度,L是辐射亮度,Exposure可以由以下公式计算:

https://www.zhihu.com/equation?tex=Exposure+%3D+%5Cfrac%7B1%7D%7B%281.2%2A2%5E%7BEV100%7D%29%7D.
其中的EV100又可以用以下公式计算:

https://www.zhihu.com/equation?tex=EV100+%3D+log_2%28%5Cfrac%7BN%5E2%7D%7Bt%7D+%2A+%5Cfrac%7B100%7D%7BS%7D%29.
其中N是光圈大小,t是快门速度,S是ISO,这些参数玩过相机的人应该都清楚,这些参数都可以在Unreal的相机设置中设置。这次的更改中增加了一个参数用来让用户确定是否使用相机的这些参数来计算曝光。在计算Exposure的公式中,有一个1.2,这次的更新就是去掉了这个1.2,增加了一个参数来控制这个数值。
新增了曝光Mask和曝光补偿曲线及其他工具更新

这是一个工具上的提升,曝光Mask控制的是亮度计算的权重,曝光补偿曲线就是控制曝光补偿的。还有其他用于调试的工具更新。
帧过度的曲线调整

在4.25中,调整了过度曲线,使用了前半段线性后半段指数函数的过度曲线。这个曲线从官方文档中并未有基于物理的解释,只是从观感上更好了。


新增手机的适配

这个部分是才是最大的更新,官方文档居然只用几句话带过。手机端的Tone操作和过度是跟其他平台一致的,主要的内容是计算平均亮度包括Basic EyeAdaptation和基于Histogram的算法,都根据手机平台做出了优化的方案。
Baisc EyeAdaptation使用的方法就是分治的思路来累加,累加的源也是经过DownSample过的,具体就不详细说了。主要说一下,基于Histogram的优化。对比一下其他平台的:


手机平台改为用RWBuffer来存储输出,问了Epic中国的同学,说是RWTexture在iOS下无法做atomic操作,冷知识加一。在读取SceneColor的时候,使用bilinear采样,减少读取次数。累加过程与之前不同,之前是将某个Group输出到Texture的某一行,这里是先将Array里面的数据累加好放到一个位置,然后将数据跨Group与其他的Group计算结果用InterlockedAdd加起来。也就是结果就已经是最终的Histogram了,这样,就少了一次Reduce的Pass。其中还有一些细节,优化的很巧妙,包括对Metal,mali等机型的特别优化,必须给Epic中国团队赞。
总结

在玩一些写实风格的游戏的时候,遇到从建筑物出来或者进入建筑物的时候,这不仅仅是观感上的差别,对游戏玩法也是有影响的。所以,好EyeAdaptation是必要的,对游戏的品质是有很大的影响的,在通盘了解之后,可以根据项目需求,有更加合适的使用和定制。


引用
Akenine-Mo, Tomas, Eric Haines, and Naty Hoffman. "Real-time rendering." (2018).
Reinhard, Erik, et al. "Photographic tone reproduction for digital images." Proceedings of the 29th annual conference on Computer graphics and interactive techniques. 2002.
Scheuermann, Thorsten, and Justin Hensley. "Efficient histogram generation using scattering on GPUs." Proceedings of the 2007 symposium on Interactive 3D graphics and games. 2007.
Calver, Dean. "Deferred Lighting on PS 3.0 with High Dynamic Range." ShaderX3, Charles River Media (2004): 97-105.
Sousa, Tiago. "Adaptive glare." Shader X3: Advanced Rendering with DirectX and OpenGL (2005): 349-355.
https://mynameismjp.wordpress.com/2011/08/10/average-luminance-compute-shader/
https://astralcode.blogspot.com/2017/10/reverse-engineering-rendering-of.html
https://www.slideshare.net/fcarucci/HDR-Meets-Black-And-White-2-2006
https://www.unrealengine.com/en-US/tech-blog/how-epic-games-is-handling-auto-exposure-in-4-25?lang=en-US
https://docs.unrealengine.com/en-US/Engine/Rendering/PostProcessEffects/AutomaticExposure/index.html
页: [1]
查看完整版本: Eye Adaptation综述及Unreal实现解析