手游中的氛围想要渲染得好,光照是重中之重。游族网络的手游大作《盗墓笔记》,包含大量的地下多光源渲染场景以及地上丰富唯美的光影效果,他们是如何处理好多光源光照渲染,增加打灯数量的同时降低Drawcall的?游族网络技术美术总监袁晟在Unity线上技术大会中,为大家揭秘。
以下是演讲实录,有节选:
大家好!感谢大家今天来参加我的技术分享。我叫袁晟,是一名技术美术,来自上海游族网络。今天要给大家分享的题目是我们在《盗墓笔记》这个项目中使用的光影技术。
我的分享大概分为这样几个部分。首先介绍一下项目的需求和挑战,然后介绍一下项目中所使用的光照技术,最后是小节。
《盗墓笔记》这个项目是在2018年提出的,flag是MMORPG、次世代、大世界,以及手游。当时美术同学给我们提出的需求是这样的:首先它有地上场景,大家可以看到以下两幅画面,画中有非常多的光影效果,比如说太阳穿过树叶,还有一些镜头的光影效果。美术给的关键词一个是现代与唯美感的大世界,还有一个就是氛围。
其次是地下场景,大家知道《盗墓笔记》这个项目中的地下场景是非常多的,所以室内的环境也很重要。美术这边对于室内环境的要求是具有真实感,但是并不要求是一个非常大的奇观。
在渲染品质上,他们给了两个竞品,一个是Bungie的《命运》,另一个是《守望先锋》(见下图)。其实两款产品都不是手游项目,都是PC项目。所以我认为美术同学的意思是希望光影是尽量真实的,同时,它的材质可能在真实的基础上需要一定的风格化。
《命运》
《守望先锋》
所以,最后根据美术的需求,我大概做了这样一些总结。
产品需求方面场景包括地上和地下,场景尺寸除了主城地上的那些部分,其他地下的场景可能不会太大。
画面风格这块需要是写实材质的,但是材质略带一些风格化。
材质表现这块应该是PBS的材质,题材可能包括现代、古典以及自然。
我们认为最大的挑战是美术一开始提出来的词“氛围”,因为这个词比较抽象,所以我试图使用一些技术词汇进行了转变。我们需要光源环境、体积光、体积雾以及一系列的后处理来营造“氛围”,因此我们也需要一个能够提供相应处理的工具。
在技术选型这边,因为这个项目是两年以前立项的,当时Unity的版本是2017和2018,我们选择了一个相对更新的Unity 2018。Unity 2018给我们提供的功能,比如SRP、Vulkan等,我们也会具体考虑是否适合使用,后面我会具体地讲SRP。在API这边,因为两年以前根据渠道商提供给我们的设备分布情况,几乎没有ESR的情况,大概只有8%,所以我们OpenGLES这块定的是3.0。
其实我们一开始是非常希望能够使用Vulkan的,但是当时比较纠结,最后还是没有在这个项目中使用。
我们来聊一下光照。
对我们来说,挑战最大的部分还是室内和夜晚的照明。为什么?因为我们在手机游戏产品中可能往往使用的是一个方向光,用Lightmap(光照贴图)的形式来做。但是在室内和夜晚的照明场景中,主光是分开的。这一是因为没有一个像太阳这样的统一光源,二是因为它的主光是按照区域划分的。比如下面这张图中间的光来自于上面一个方形的空隙,这个空隙就成为它的室外灯光的主光,边上的火把又成为这个柱子的主光。
第三个就是在这样一个场景中可能我们需要使用不同类型的辅助光源来烘托气氛,比如点光、面光,或者是聚光灯,因为地下场景总体上还是比较黑的。这个时候,如果我们使用一个方向光就很容易把整个场景打亮,所以我们相对而言会更多地依靠间接光照,这样就不会让画面显得太脏或者是太闷。
这边是《古墓丽影》的一张游戏截图,可以看到它的画面是非常有层次感的,远处的火堆和一些室外的日光洒下来,环境中有非常棒的空气感,而且整个场景是没有死黑的部分的,非常有层次感,非常通透。这是我们美术希望能够实现的一个效果。
《古墓丽影》
讲完需求和挑战,我们看一下比较传统的光照技术。
这边先简单介绍一下,一般来说比较传统的光照有是前向和延时两种。前向渲染方向,Unity提供了两种解决方案,一种是BuiltIn RP,一种是LWRP(2019版本开始更名为URP通用渲染管线)。
目前,场景复杂度对于前向渲染中光照的影响非常大,它的复杂度是灯光数量×物件数量,所以如果使用BuiltIn RP drawcall就会非常高。当然,Unity提供另外一种URP,它把多个灯的信息同时传到一个pass里面去,所以它可以在一个pass里面把光照全部算完,它的drawcall就降到物体的数量,但是其实这部分光照信息仍然要在Shader中进行计算的,所以这部分的算力还是由GPU承担了。
另外一种是延时渲染,Unity提供的是HDRP(高清渲染管线),该功能目前还是没有办法在手机上使用的。最下面我写了两个问号,现在还是有一些方式、一些手段允许我们使用one-pass的deffered,这种方式可能需要使用Metal或者是Vulkan这样比较新的API才能实现。它可以让你的MRT产生的G-Buffer只留在tile memory上,而不是要写到shared memory里。所以,这就是我们非常纠结的一点,因为我们项目一开始就没有选择使用Vulkan,所以这个方案基本上早期就被否了。
我们看一下在《盗墓笔记》中使用的光照方案。
我们的静态物体基本上使用的是Lightmap进行光照,动态物体和角色我们使用的是Light Probe,植被使用的是Light Probe+Directional AO。
这边可以看到这样一个场景,里面有非常多的光照,主光通过天窗投下来,来自于室外,它的场景里还有非常多的壁灯和其他的辅助光源。由于灯非常多,一般来说我们需要使用延时渲染去做,主要是在主机或者是PC上面。但是通过我们的方法,目前在手机中能跑到60帧以上。
这里面演示的是角色在不同环境里移动的时候受到环境的影响,比如说这个地方偏蓝,它可以受到自然光的影响。
再往前走,前面有一个稍微亮点的地方,角色会变亮,角色的受光方向基本上也是和环境一致的。这个是比较类似于延时渲染或者是实时光照的情况,但是基本上完全是烘焙的。
这是另外一个场景,是《盗墓笔记》中比较经典的地下场景。
我们可以看到这种地下场景一般是比较暗的,我们不希望这些暗都是死黑的,所以使用了间接光照给它进行打亮的。这里面我使用了一个手电筒,它是一个动态光,其他的光都是完全烘焙的。像这些地方周围的环境对于Normal和高的表现是比较友好的。
像这样一个场景我们虽然没有使用实时光照,但是它的光照效果跟使用实时光照的效果是非常接近的。
再来看一下性能的情况。首先这边看到在刚才的场景中这边的Batch大概是173,因为这个场景相对比较复杂,还有一些NPC在场景当中。
这样一个镜头从上往下看基本上所有的东西都会加入到计算当中,加入到drawcall当中,这个时候的Batch大概是180。
这个场景相对比较简单,里面没有NPC,道具比较少,而且草都是用Instance去绘制的,所以Batch又降得比较低,只有109。
另外一个镜头,这个场景看得比较远,它是150以下,146。
所以说整个绘制过程中其实没有用到实时光,drawcall也是完全压制住了。
我们使用的光照技术是AHD光照,顾名思义,A就是Ambient,环境光的意思,H是Highlight,高光的颜色,它里面指的是主光的颜色,D就是Directional,指的是主光的方向。AHD相当于把一个像素的入射光的光照拆分为两项:一个是Ambient项,一个是Directional项。而Directional 项由两个参数表示,一个是主光的颜色,第二个是主光的方向。然后我们把主光的颜色和方向从完整的入射光里面去除掉,剩下的部分我们都称为Ambient项。
比如说除了主光以外的一些辅助光源或者是间接光照,我们都是称为Ambient Light。
由此,我们把这样一个入射的光照拆成这样一个表达形式。这边的I(n)的意思是一个像素的入射光照,它拆成了Ca,Ca表示的是环境的光照颜色,这边的Cd表示的是主光的颜色,这边的cosin就是一个基本的光照系数。
这种形式把比较复杂的光照变成了一个相对比较简单的形式,同时,由于拿到了主光的方向和颜色,所以我们可以在此基础上进行高光的计算。
AHD并非是一个新技术,它只是比较小众。2013年的时候当时的Last of us和2016年的Call of Duty都使用了这个技术。
Last of us(2013年)
Call of Duty(2016年)
使用AHD来做的Lightmap,如下图,这张是实时光照的情况,上面有一些法线,所以表现了比较多的细节。如果我们把这块石头进行Lightmap烘焙,它就会比较平。这是因为我们一般的Lightmap是没有办法把Normal以及高光信息烘焙到Lightmap上的。
如果是使用了AHD的Lightmap,它就能把高光、法线所有的PBR用到的数据都能完整地用Lightmap记录,然后还原出来。如下图。
AHD光照它有什么问题呢?首先我们知道它分了三项,也就是Ambient的颜色和主光的颜色,这两张是HDR的,还有主光的方向,这张是LDR的。总共有三张Lightmap,这个我们肯定不能接受,所以常见的AHD是需要进行压缩的。
我们把这三项,两张HDR、一张LDR压缩成右边的这样一个系数,就是标量和方向是一张LDR,还有一张Iz(一张HDR),这两张是可以合成一张的,最后变成两张贴图。
Iz一般是什么?我们的Lightmap是怎么计算的?
它是把顶点光照作为法线,把顶点的法线带入进行计算的。所以这边的z项表示的是顶点法线,而不是一般我们认为的像素法线。所以,它可以把整个这项变成这样一个形式:Ca+max(0,z (dot) d)Cd。可以看到Ca和Cd还是原来的,只是中间这一项变了。这个Iz就可以用传统Lightmap的颜色表示了。所以,这个对传统的Lightmap工具比较友好。
另外,我们看这边的系数是什么意思?
我们有另外一个假设,我们认为环境的颜色和Lightmap得到的颜色是正比关系,也就是说它的色相是不会变的,所以我们可以把环境的颜色的luminous求出来和Lightmap的luminous求出来进行一个比值。我只要记录这个比值,就可以从Lightmap的颜色拿到环境项的颜色。所以,一般来说我们会对AHD进行这样的压缩去保存。
我们知道怎么做以后,后面就是怎么在Unity中去实现。
我们知道Unity中已经有一个东西叫Directional Lightmap。它的设计初衷并不是为了实现AHD的,但是它也能够帮助我们实现这些东西。根据解码,我们发现需要的几个最重要的参数在Lightmap中已经有了,比如说Iz,系数,方向。Directional Lightmap的w项我们是不需要的,后面可以把它优化掉。所以,整个Directional Lightmap是可以直接用来作AHD计算的。
后面比较简单,我们只需要进行一些加减乘除就可以简单地拿到它最后的结果。
最后的效果是这样的,我们可以通过这张Lightmap的颜色以及一张Directional Lightmap,QQ买号不需要任何实时光照就可以计算出漫反射、高光等。因为我们是PBR的渲染,所以我们还需要AO和IBL,合成为最终的结果。
这个是基于Lightmap的做法,当然这个还比较简单。
第二部分是动态物体,由于动态物体没有办法使用Lightmap,我们需要使用的是Light Probe。目前,Light Probe是使用球谐来保存的,它能完整地计算Directional和Ambient的信息,但是无法计算高光。所以如果我们能从Light Probe中把AHD三项完整地拿出来,就可以和场景一样进行计算。
从下图我们可以看到最终的效果是,当角色在场景中自然走动的时候,他会受到环境的影响,比如从亮走到暗,或者是从暗走到光照以下,它都会因为环境的变化、高光方向以及光照方向相应地进行转变。
为了拿到一个完整的SH9的信息,我们是这样记录的。通常Radiance我们会投影到球谐的式子中,大概是这样一个形式。我们记录的系数是这边L项,我们把这个称为Row SH,假如说这个地方只需要使用一个二阶的球谐,一共要记录9个float 3,也就是9个颜色。
但是在Unity的Light Probe中保存的式子是不一样的,它是这样一个形式。它比我们需要的形式多了这样一些系数,比如π分之一,还有这边的A。它是什么意思呢?
因为Unity最后记录的是一个Lambert表面的Radiance数据,所以说对于Lambert表面的BRDF是π分之一,后面的A是cosin在球谐上的投影,投影的具体形式是这个样子的。
所以,如果同样使用一个二阶的SH保存光照信息,它所存下来的系数是这样的形式,也就是π分之一,AxL,它比我们希望的形式多了两个系数。当然它的数据同样是存在9个颜色数据里面的。
举个例子,假如说我们是一个一阶的,中间一个求,Unity会保存的形式相当于这样一段,我这边画的橙色的一段。而我们需要的可能是中间黄色的一段。我们把Unity存的东西称为缩放过的球谐系数。
Unity这样处理明显是希望通过离线计算把一些不需要的乘法先在离线环境中计算掉,这样在实时计算的时候就可以比较简单地往下做,而不需要再计算这些东西。
Unity把这些东西在球谐的9个系数上进行乘法,完成以后就可以再往下传到Shader里面去。
这边9个float 3一共27个数据,而Unity的传法是7个float4的数据,所以一共28个槽,这边还多一个。
这边有一些小的点是什么呢?大家可以看到在这边第六项其实是cosin平方减1,把它展开的话这边多了一个常数项。所以把这个常数项和邻接的合并到一起,因为邻接也是个常数。它把这个-1扔过去,剩下的cosin项就留在原来的地方。
所以,这边B通道乘的是cosin部分,而A通道是邻接的加上刚才的常数。
在Shader中使用就非常简单了,把Normal乘以刚才的球谐系数,就可以得到光照结果。
当然,它只能拿到Diffuse和Ambient项,所以,我们接下来还要继续处理。我们需要从它那边得到完整的AHD。想要得到完整的AHD,首先就要先把缩放的球谐变换回原始的球谐,把这些系数都给拆掉。
第二步我们需要解出主光的方向。首先可以把球谐函数看作一个球面函数的和。我们的目标是要从这个球面函数得到它的最大值,假如说我是一个一阶的球谐,也就是一个常数项和三个系数组成的,把后面经过整理变成一个点积。我们的目标是让这个项变成最大值。我的做法就是让这个点积等于1,因为cosin最大值是1。这边就很容易得到Omega的方向,就是这样一个式子。
因为球谐中保存的是单位向量,所以我们接下来还需要计算出方向光的模。这边因为计算比较复杂,时间有限,我就不在这个地方展开了。我们可以参考ppsloan的论文里面对这个进行完整的求解,它的模等于17分之16π。
http://www.ppsloan.org/publications/StupidSH36.pdf
拿到这个系数以后,我们后面就可以求后面的两项:一个是A项,就是Ambient的颜色和一个C项,也就是主光颜色。我们这边使用的方法是最小二分法,我们需要把AHD的展开式,C×L项+A×LA项,这个就是AHD的形式。剪掉后面的LE就是我们从Unity给我们烘的球谐中得出RowSH的那一项。所以,我们给它相减,求方差,再对它进行偏导求极值、求误差最小的情况,就可以通过这种方法求到它的A和C的预估。
这边有一个小的点,我们这边使用的是white,因为球谐一共有9个颜色,所以我其实有两种方法。第一个是RGB三个通道分别求三个不同的方向,三个不同的主光颜色。但是这个方法就会要求我在后面的实时计算中进行三次的光照计算,这显然不太划算,所以我们用了另外一种,对每个颜色进行luminous计算,求它的明度,对它的明度求最大方向以及最大方向的颜色。
这边为什么有两个L呢?这个地方是为了后面要球谐投影的展开。这边很容易理解,因为它是一个方向的,所以这个方向在球谐上的投影,当然这里面还带了一个cosin。
因为它是一个Ambient项,Ambient项是个常数,所以它只有一个系数,2根号π。所以这边就把它进行球谐的投影,方向在球谐上的投影我们记住是大写的D。Ambient项在球谐上的投影我们记住是大写的A。Unity给我们提供的RowSH我们记住是大写的E。这样在球谐上投影以后我们就可以最终通过求解这个方程知道C和A等于多少。
后面的计算就相对比较简单,只需要把C和A算出来就可以了。
最复杂的部分已经结束了。后面就是要把Light Probe在场景里面进行布置。我们这里是使用Houdini进行布置的。我们把场景的碰撞体导到Houdini里面,用Houdini对它进行计算。
这边看到的黑的点是Houdini进行完的球谐的Light Probe的位置。
使用Collision Box为Houdini的输入
再把这个东西通过Houdini的工具HDA,插入到Unity里面去。这样我们就可以使用这个工具直接在Unity里面进行计算。
这个是计算的结果,这些点最后都会转化成Light Probe的球。
我们为什么使用Houdini做这个东西?
当然第一个原因是因为自动化比较简单,第二个原因是,大家如果用过球谐的共振,就会发现如果点的分布是均匀的话经常会在墙壁或者是门洞这些地方出现漏光,而实际上当出现这种情况的时候我们需要美术在门洞或者墙壁的地方增加细分,然后去把这个漏光修掉。但是如果使用Houdini的话就比较容易,因为这个完整的碰撞体拿到以后,我们知道哪里是墙,哪里是门洞,所以我们就可以在相应的位置上改变它分布的力度,就比较容易去处理这个问题。
最后一部分就是Directional AO,比如植被或者是表面拓扑比较复杂的模型。如果我们使用Lightmap对此类模型进行计算,非常耗Lightmap的像素,一般来说没有办法很好地表现。所以我们最终还是使用的Light Probe对它进行的光照。但是,这个地方如果只使用LightProbe,它容易变得比较平,因为LightProbe的采样只有一个点,所以这个地方就使用了另外一个方法,Directional AO,进行额外的计算,让它产生一个正确的自阴影。
这是Directional AO(简称DAO)的一个效果。这边只显示DAO的黑白。大家可以看到这个场景的灯光是来自于某个特定的方向,所以这棵树的受光面白一点,被遮挡的地方黑一点。当我们旋转这棵树的时候,这个光照是会实时地发生变化的,它永远是被光遮挡的地方会变成黑色。
整个计算相对来说比较简单,并不需要我们额外的用传统的方法去存AO或者是Shadowmap。
我们求解DAO的思路比较简单,因为DAO是一个球面函数,所以我们可以把这个球面函数也投影到球谐上。DAO是个高频信息,所以我们需要使用高阶的SH,我们这边使用的是三阶的。我们把SH的系数拿到以后保存到这个模型的顶点色上,然后在实时运算的时候把这些信息拿回来进行还原。
这个是最后的效果。左边是没有DAO的效果,大家可以看到比较平,右边是有DAO的效果,所以可以看到会有一个自阴影的明暗的状态。
无 Directional AO
Directional AO
最后我就简单总结一下我们这个技术的优点和缺点。
首先,使用了AHD以后我们比较大的优点是可以通过离线的方式把光照信息完整地保存到Lightmap和Light Probe上。这样运行的时候我们只需要计算一次高光就可以了。同时,我们的drawcall就会降为物体的数量,与灯光的数量无关。我们可以把间接光照的信息完整地保留下来,并且在实时烘焙的时候将它还原。我们可以使用一些比较复杂的灯光,比如说面光、体积光或者其他一些东西。
缺点,我这边简单写了两个。一个是Lightmap需要两张:一张是标准的Lightmap,一张是Directional,需要Light Probe。另外一个它是需要依赖烘焙,无法实时编辑,这是它比较大的一个缺点。还有一个缺点就是我的灯光位置不能移动,也因为它是烘焙的。
我们在这个项目中使用这套技术最大的好处是,它不限制美术在创作中对灯光的使用,美术可以在任何场景中任意地发挥。下图这样的场景大概打了几十个灯,比较复杂的场景中美术打了300多个灯。这些灯在我们的实时计算量上都是看不见的,因为都是离线计算。同时,我们可以把间接光照拿回来的,所以相对来说比较廉价。从效果来说也基本上和使用实时光照是类似的。
好的,我的分享就到此为止。谢谢大家。