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