一. Intro
在PBR渲染中,除了已被大家深入分析了很多遍的PBR材质属性(Surface Appearance)外,合理的光源强度和后处理也是不可或缺的部分。这里结合工作中的一些实践经验,讨论一下后处理中另一个关键环节——自动曝光在移动平台上的实现。
本文讨论一种不采样backbuffer进行自动曝光的方法,以及在项目实践过程中遇到的问题和坑,希望能对遇到此问题的各位产生一些帮助和提示。
二.自动曝光方案及其在移动平台中遇到的问题
2.1 自动曝光是个啥,为什么要上这个特性?
[ 图1 ]
自动曝光是在不改变光源强度本身的前提下,根据当前镜头拍摄到的场景光照强度,自动调整镜头的曝光值以及其他参数,使得成像结果不至于过爆(Overexposed,图1右)或者过黑(Underexposed,图1左)的一种操作。
这一过程就隐藏在日常生活中:拿出一部手机打开拍照功能,对着晴朗的天空拍一张。之后,迅速将镜头对准屋内一个昏暗的角落,你会发现屏幕中的影像先是一片漆黑,过了一两秒以后逐渐变亮。此时再拍照,会发现照片的亮度和对着外面照的亮度并不会差很多,至少不会像刚才那样一片漆黑。手机这种自动适应拍照环境并调整曝光强度(可能还有其他参数)让画面的亮度看起来“合适”的过程就是自动曝光。
[ 图2 ]
很多非移动平台的3A作品为了模拟真实的光照和人眼适应强光的效果,都会加入自动曝光功能。FPS品类的例如Call of Duty,Division;竞速类的例如近几代的极品飞车和Forza Horizon;ACT类的刺客信条、鬼泣等都有非常明显的自动曝光feature。以上图中这个 Division 2 的场景为例子(https://www.youtube.com/watch?v=EMkZ7sOoRSw, 00:50到01:00)。角色相机在进入一个相对较黑的屋内之前,看屋里几乎无法分辨室内的任何东西。走到屋内以后,自动曝光功能将镜头的曝光值提起来了,使得玩家可以看清屋内的物体。同样的光照环境,变的只有相机属性,却会对场景物体的辨识度产生很大影响。这是自动曝光作用的一个典型案例。
不加证明地总结自动曝光特性的一些好处:
允许场景的光源对比度更大,动态程度更高,从而提高光强的真实度
提高场景中暗部的辨识度,这一点对于某些游戏品类很重要
可以间接的减少美术同学的“补间接光”的操作
可见,若希望在移动平台上将真实感渲染品质往端游和主机的品质push一下的话,产品渲染管线中的自动曝光功能大概率是不可缺少的。尤其是笔者接触较多的FPS品类,它们的场景漫游摄像机会在光照环境变化剧烈的室内外频繁切换,这时自动曝光的作用就显得格外突出。
2.2当前技术复盘
回到游戏渲染中的技术来,一起看看现在工程中的做法。以最常见的Unreal 4与 Unity来说,他们的自动曝光处理流程可以大体概括为:
取当前帧的图像到一个renderTexture(RT),获得当前帧场景的光照信息。
一般,还需要对当前帧进行降采样(缩小画面),节省分析时候的性能开销。这一步可以与后处理管线中的其他处理步骤合并。
通过各种方法,例如多点采样、compute shader等手段,根据降采样buffer中的数据,得到这一帧画面中场景的光照信息。
根据这些信息,以及各家的算法,做自动曝光的处理。
处理曝光部分,除了最基本的将曝光值提高或者降低以外(相当于最后输出时候将光强乘了一个系数),还有可能结合其他图形处理技术对画面进行优化,例如UE4会分析当前帧的直方图,然后做一些类似直方图均衡的操作。
[ 图3 ]
上述操作基本上都是针对单一一帧内容进行的操作。除了静态的处理,自动曝光还需要考虑动态的一些东西。例如从暗到亮或者反过来的速度,以模拟眼睛的明适应和暗适应。在图3中,SpeedUp / Down 控制的就是明暗变化的速度。
2.3 技术痛点与期冀
根据上述的复盘,我们发现当前的自动曝光方案均需要读取当前渲染环境的屏幕空间亮度才能继续后续的运算。意味着需要至少 swap 一次 RT 才能完成这一操作(如果有不占带宽的方法求不吝赐教),这对于2019-2020的中低端机来说还是太费了。以2019年下半年的标准,像 iphone6 / oppo r11 这类的中配机一般仅允许有一次RT的切换,个人是感觉这一次切换不如给shadowmap来的值得。而对于像 oppo A33,米4、荣耀畅玩 5X 这些2019年的低配机型来说,连保证全场景PBR输入的带宽都困难,更不要说什么切RT了。因此,我们认为传统方案中需要读backbuffer到RT这种方案,天生会成为中低配机的痛点。
基于这种分析,结合笔者当时项目的特点,我们给出本文中自动曝光方案的要求和期望:
不能用额外RT,不能来回切RT
更不太可能碰compute shader
能够满足最基本的自动曝光功能,即根据摄像机可视区域的平均亮度,将输出到tonemapping的亮度乘一个系数使他保持一个相对合理的亮度。
高中低配使用同一套方案,不搞高配一套低配一套。不能因为机型问题产生fallback、美术高低配不同的工作量以及因此带来的大小包问题。这是项目本身的要求和属性及以往项目的经验教训决定的。
下面将根据这些要求聊一聊本文的自动曝光方案。
三.方案详解
3.1落地效果
[ 图4 ]
这一方案去年下半年的时候就已经合进笔者参与的 FPS 项目 《使命召唤手游》(Call of Duty : Mobile ,CODM)里了,不过当时并没有经过玩家和市场验证,并不敢冒然分享可能不靠谱的东西。目前,该项目已经有若干张图使用了该方法,已有外国玩家录制了地图视频,反响还不错(https://www.youtube.com/watch?v=OE1-BeUudGs)。
如图4所示,玩家从光照较弱的洞内走向光照充足的洞外,自动曝光自动将画面亮度压了下来使得场景的亮度一直保持在一个相对合理的亮度。自动曝光的效果通过视频展示更为明显,推荐各位通过玩家录制的游玩视频或者亲自下载应用体验效果~
3.2思路
上一节分析中我们提到,现有方案的关键痛点是取屏幕空间亮度这个操作在中低配手机上开销太大。因此,我们需要从这里入手另辟蹊径。实际上,只要获得摄像机当前视角下,观察到的物体平均亮度,就可以继续传统自动曝光处理的剩余工作。因此,我们需要一种非常廉价的方法,替代当前这种使用 rendertexture 获得平均亮度的方法。
除了屏幕空间实际看到的像素点以外,还有什么方法可以获得场景某一处的亮度呢?很幸运,我们在 CODM 现有的渲染管线中找到了一个非常合适的候选人——为动态物体提供间接光照的 Lightprobe。Unity 的 Lightprobe 是以四面体网格存储的场景间接漫反射光照(GI)的球谐函数系数(SH),当这个 renderer 处于GI SH网格中时,renderer 可以通过采样 SH 四边形网格并通过插值获得 renderer 当前位置的间接光SH值。思考这个SH的值可以发现,若我们在场景中的某一个位置对一个方向采样 SH,得到的数值就是这一点采样方向上半球内场景亮度的 cosine 积分。这与我们想要的场景某一位置看到的平均亮度有非常强的相关性。同时,采 SH 网格开销一般是远远小于 swap buffer 再把 rendertexture 返回到内存中的。因此我们认为使用lightprobe的数据是有可能实现低成本自动曝光的。
经过一些实验和思考,我们认为这个思路是比较靠谱的。只要把采屏幕亮度这一步,替换成采场景摄像机位置视线上的SH就可以搞定了。拿到场景的SH之后的操作按照传统自动曝光的操作进行处理即可。本文也将主要围绕如何高性能取到镜头之内场景的平均亮度,而忽略取到亮度之后处理曝光值部分的讨论。
3.3 将SH到场景平均亮度的转换
一旦思路有了,验证这个方法的手段非常简单。但是一旦亲自尝试,就会发现直接使用SH信息根本不稳定。我们还需要对采集SH的位置以及SH信息本身进行一定的分析和计算才能得到可用的场景平均亮度。
[ 图5 ]
首先,一起回顾一下GI SH的数值含义。如图5左侧所示,若从某一点向一个方向采样场景GI的 SH值,我们会得到什么?只要对lightprobe原理以及球谐函数表达GI这套东西熟悉的话,就知道它得到的是采样点这一方向对半球内radiance乘cosine的积分。简单的来说,就是采样方向半球可见区域里面“亮度”的cosine加权平均,方向越接近采样方向,权重越大;反之,半球内垂直于采样方向上的radiance权重几乎为0。若从数值上分析,我们可以发现这个方向上的SH值,大约等价于这个方向上一个球面角之内radiance的平均值。这个夹角的半角用α表示,如图5右侧蓝色区域所示。我们忽略掉半球积分内占总权重非常小的那一部分灰色区域,粗略认为SH的值大约等于这个立体角半角α的锥形之内场景radiance的平均值,即蓝色范围内场景的平均亮度。
经过不少实验和摸索,在实际落地中我们将α取经验值:52度。在这个度数下,蓝色区域的积分值展总半球内cosine加权积分值的85%以上。有了这层等价关系,我们就可以将SH值、相机方向、相机FOV值联系在一起。当我们在场景中某一点向一个方向上采样得到了一个SH,我们就认为这个SH值是一个朝着采样方向,且FOV为104度的摄像机拍摄到图像的平均亮度。
这种等价关系引入了本方法的第一个数值偏差,即那部分灰色的区域以及蓝色部分的加权问题。假设在灰色的区域内场景突然出现了一个特别亮的物体,则这一部分区域会把整个SH的值拉高一点,使得相机走过这一区域时候的曝光值降低(亮度平均值高于实际场景值)。经过一些测试,我们认为这个角度下是可以满足大部分的使用情形的,问题不大。请各位读者在实践中留意这一部分数值偏差。
3.4 加上相机的FOV
我们已经将SH值与场景平均亮度之间建立了数值联系,但是这还不够解决问题。
[ 图6 ]
实现过自动曝光或者有摄影经验的朋友一定知道,无论是实时渲染的自动曝光,还是数码相机的自动测光(图6),都不是简单粗暴地对当前画面内的所有区域的亮度做平均,因为画面中心的亮度往往更重要。对于数码相机,测光的方法只会选取画面中心区域的一个点或者几个点进行测量,而不是对全画幅进行数值平均。实时渲染中的自动曝光算法也有类似的操作。
[ 图7 ]
以图7为例子,若要对上图中的场景进行自动曝光处理,则需要统计下图中中心区域的亮度。基于此,我们可以得出一个结论,自动曝光需要统计的场景亮度立体角总是会小于摄像机的FOV。也因此,我们会遇到两个比较严重的问题:
第一,在上一节的等价关系中,我们认为GI SH的值约等于FOV 104度的摄像机的亮度平均值。而游戏中的相机FOV几乎不可能总是104度。一般的FPS FOV在30度到90度之间,还需要考虑开瞄准镜时,FOV急剧变化的问题。
第二,假如相机的 FOV确实是104度,我们实际用来测光的立体角肯定比这个值要小。如果直接使用GI SH的值,统计出来的范围比相机实际看到的范围要大。
这两个问题说明,实际落地时不能像大多数读者想的那样直接在拿相机的位置和方向去采SH就完了。前期研发时的测试也证明,不做任何处理直接拿出来的SH有比较大的误差,尤其是在窗口、门口这种内外亮度较大的位置上,这些位置的亮度几乎一定是错的。
怎么办呢?笔者认为可以通过修改SH的采样位置来修正这两个问题。本文中的方法,将SH的采样点在相机的视线矢量方向上向前移动一段距离,使得SH的采样数据能够基本吻合常见的FOV值。
[ 图8 ]
我们一起来看一下怎么移动采样点。图8引入一个新的半角θ表示相机用来统计屏幕亮度所用的FOV,注意这个值一般都比相机本身的FOV要小。我们不妨认为,相机需要统计的屏幕亮度值全部由图8左侧的灰色虚拟物体提供,摄像机看到的这个物体的亮度就是自动曝光想要的值。那么,物体的亮度怎么获得呢?只要让SH等价的那个104度立体角的锥切着物体的两侧正好包住物体就好了,如图8蓝色线区域所示。我们将SH采样点A往灰色物体的方向上移动,使得蓝色线正好盖住物体。在这种情况下,根据刚才的约等于关系,我们可以认为A点采样的SH值,大约等于灰色物体的平均亮度,即自动曝光平均亮度。
移动的这段距离,实际上就是线段AO的长度。倘若我们知道虚拟物体到摄像机的距离,又知道α的角度,便可通过简单运算得出AO与CA的距离,在此不表。
引入灰色虚拟物体的这种假设,在落地的时候会遇到一些必须要处理的坑:
如何拿到CO之间的距离?
这一数据是保证本方案可行性的关键数据。笔者的项目里因为场景具有相对高效的碰撞盒,因此拿到场景大致的外包盒并不会太难也不会影响效率。同时,这个采样也不用每帧都做,因此性能上还好。如果项目因为某种原因拿不到CO的距离,使用一个固定值也能达到一定的效果。但是诸如穿墙、距离过近的情况会有问题。看自己的项目情况吧。
CO的距离无限大怎么办?
在笔者的项目中,除非是开了狙击镜,否则会强制要求AO的距离控制在0-8个场景单位之间。基于上文的假设以及项目数据,在这种假设下CO的距离可以达到20个单位左右,完全可以满足项目要求。同时我们也调整过AO最远允许的距离,发现调大以后并没有什么卵用。因此为了保证求交结果的稳定,我们将AO的值限制在了8以内。当然,这个数值可能需要根据项目的情况进行调整。
灰色物体离摄像机太近怎么办?
数值上来说确实无解,不过在实际测试过程中并没有产生太大的问题。
灰色物体的法线如果不正对这摄像机怎么办?(站在地上看地面)
实际落地中并没有对此做过多的处理。倘若是地面的话,我们会将测量点沿着地面法线方向向上抬高一点点来补偿这个问题,但是这个操作的收效甚微,几乎可以忽略。
3.5 落地
基于上面几小节讨论的内容,总结一下这种自动曝光的方法步骤:
根据相机的FOV、位置、方向,确定采集GI SH的大概位置。
根据3.3节中的偏移,确定采样SH的精确位置。
采样GI SH,得到当前时刻可视范围内的场景亮度。注意,这个操作不是每帧做的,大概一秒5-10帧足矣。
根据亮度确定曝光值是该提高还是降低,确定一个目标曝光值。
根据一些与时间相关的参数,将曝光值向目标曝光值调整。
将曝光值传给后处理链,得到最终的曝光效果。
在落地过程中还需要注意一些事情:
注意场景 GI SH的有效范围
一般来说为了节省资源和内存,我们只会在摄像机可能经过的区域布置SH网格。自动曝光的加入无疑扩大了这个网格的范围。也因为这个原因,这个方案更适合一些封闭的小场景。对于时下流行的开放大世界,则必须借助非常规的 lightprobe 结构实现自动曝光。对于此要么增加SH网格的范围,或者可以考虑像笔者一样使用一些hack。例如站在地面向天上看的情况,一般情况SH网格并不会给天空方向很多的资源,这就导致采集到的SH并不能代表角色头顶上方的SH值。遇到这种情况,笔者是将SH采样值的位置强制限制在相机上方一定高度之内。事实证明这种方法很有效。另外还有一种情况就是从室内看窗外,如果窗外的SH不能代表窗外的亮度,则曝光就会错误。然而如果是不能走到的窗口,外面一般是没有SH网格的,这就产生了错误。笔者建议这部分使用一些自动化工具来检查,靠人检查是不太行的。
GI SH一定要能代表场景的实际亮度
这是笔者在实践中遇到的最大问题!Lightprobe可能会因为各种原因无法表达场景的实际亮度,本文中的自动曝光方法遇到这种情况就会错误的压低曝光值。这一点就需要美术同事一起努力避免。想要完全避免这部分问题,最好的方法还是完全设置一套独立的SH网格来做自动曝光,当然也需要更多工作量。
四.未来的工作
(1)未来能够改进的点
虽然当下的方法已经上线了,但是不免还是有一些遗憾。目前觉得最想继续提高的,就是在这个思路下支持动态物体。可以考虑将能产生强烈影响光照的物体抽象成一个球体,然后再将发光强度project到SH上从而影响曝光值。
另外,就是这个方法在类 volume lightmap (UE4那套)的落地。其实只要能搞到与场景亮度一致的GI SH,这套方法都可以用。不过取 VLM 的效率有可能不如取 lightprobe 高,同时还有可能有精度上的问题。
(2)有关自动曝光生产的一些经验
实现自动曝光的技术特性只是光照强度更加PBR化的一小步。回到游戏产品的生产过程中,笔者结合产品经验以及与团队中美术老板们的讨论,在这里给出一些与自动曝光对项目产生的影响,这些东西只是实践产生的一些经验和忠告。当然,它们可能并不适合所有的项目:
自动曝光功能会改变场景中光源在布光中的作用,会非常强烈的影响场景美术以及灯光师的场景打光思路和流程。例如一间暗室的窗口,有自动曝光时可以当局部场景的主光源,没有的话就需要美术额外补一些光做出类似窗口过爆的效果。
自动曝光功能会让场景布光强度更合理,间接地让渲染结果更“PBR”。我们观察到开启自动曝光后,场景美术在布光时对比度会变得更大胆,不像原来那么“平”。这更符合真实光照环境中的情况。
美术同学要适应带有自动曝光特性的场景,并且根据自动曝光的“脾气”制定一些布光策略。
自动曝光必须对美术所见即所得,否则会变成场景和灯光师的噩梦。
渲染管线中有没有自动曝光,最好能早点确定好,否则会给场景生产造成混乱和返工。