米哈游弋振中:从手机走向主机,《原神》主机版渲染技术分享
在今年中国Unity线上技术大会<游戏专场>上,米哈游技术总监弋振中分享了,从手机走向主机-《原神》主机版渲染技术。以下是分享实录:
大家好!欢迎参加这次分享!今天我演讲的主题是从手机走向主机,《原神》主机版渲染技术分享。首先我简单介绍一下《原神》的游戏,《原神》是开放世界的RPG游戏,有独特的二次元画风,是跨平台的,而且长期运营,长期更新。
稍微介绍一下自己,我叫弋振中,大概有十多年的主机游戏开发经验,毕业之后加入了Ubisoft Shanghai,2012年去了加州的Zindagi Games,为当时还没有上市的PS4做一款独占的游戏。然后去了纽约的Avalanche Studios,参与制作了《正当防卫》的3和4。回国之前的最后一站是在西雅图的微软Xbox。2019年初回到上海,加入了米哈游,组建了研发团队,目前负责《原神》的主机平台开发。这是我做过的一些项目。
今天的内容安排大致是这个样子,首先我会介绍一下《原神》主机平台的基本情况,然后按照我们开发的时候改造渲染管线的思路,选择部分的技术点进行更深入的分享。希望从渲染的角度,让大家对于我们如何将《原神》带到主机平台有大致的了解。最后是一点我个人的开发体会。
首先,Unity是我们游戏里面使用的引擎。Unity是一个灵活度很高的引擎,代码风格很简洁,所以我们能够更方便地定制化开发《原神》的渲染管线。Unity中国的技术支持很配合我们,在此对他们表示感谢!
PS4作为游戏的主机,硬件架构可以说是为游戏开发量身打造的,主机开发过程当中大量的精力是花费在如何更好地利用硬件特性上,也积累了不少我们认为还可以的技术实现。不过因为索尼NDA的原因,今天的分享就不涉及相关的内容,也不涉及底层的优化。前面也提到今天的分享,主要是针对渲染管线的,所以也不涉及CPU和其他的模板。
下面是主机渲染管线的简介,首先我们有非常强大的引擎团队,在Unity上面为《原神》进行了深度的开发。主机平台和手机平台采用了不同的渲染管线,但是游戏的基调是一致的,都是基于PBR的风格化渲染。《原神》的主机平台开发起步时间稍晚于手机平台,在平台管线搭建好以后就进入了同步开发的节奏。资源的制作、功能的开发多方面都需要兼顾到多个平台的情况。
基于PBR,是为了让整个大世界的光影效果保持统一,因为我们的光影都是实时计算的,有24小时的循环,有动态的天气系统。PBR能够确保不会在不同的光照条件下出现脱离预期的渲染效果。作为风格化的游戏,也需要根据美术的需求修改不同的材质。
《原神》在PS4上的分辨率是这样设置的,PS4 pro上面是原生的4K分辨率,在PS4的base,我们是把1440P作为我们的渲染分辨率,最后是输出到1080P上面,这样我们得到的最终后面会更加清晰,也会更加锐利。作为《原神》为主机开发的功能,大量使用了compute shader,compute shader有很多很好的特性,而且在支持Async compute管线的平台上,我们还能够进一步隐藏开销。
《原神》的风格化渲染是非常独特的,因此美术对于图形功能的要求也和写实类游戏不一样,尤其是光影效果,大家可以看到脏、黑、死、焦、噪这些词都频繁地出现了美术和程序的沟通当中。
下面我们提到的所有的技术,都经历了很长的磨合期,有一些甚至还在磨合当中。在经历了反复的打磨和修改,直到美术对最终效果满意,程序对最终实现方案的性能也满意的情况下,我们才会大规模铺开制作。
接下来再介绍具体的功能之前,我跟大家讲一下在PS4平台刚开始开发的时候,我们面临的一个状况。首先是我们有一个已经针对手机做大量开发的Unity引擎,这就意味着简单的切换平台就想让游戏能够在PS4上跑起来,这是不可能的了。很多项目之前做的改动,在实现的时候也没有考虑到主机平台的特性。再加上各种计划为PS4开发的图形和游戏的功能,这都意味着大量的工作量。
另外还有TRC、索尼账号等等一系列PS4独有的问题需要解决,工作量和工作难度都非常大,然而我们能够给主机开发的资源又很有限。一开始的主机团队就我一个人,光杆司令。为了全球同步上市,留给我们的开发时间大概只有一年半,一年半的时间要让一个平台从无到有,还要达到一定的品质,这期间还要准备ChinaJoy、TGS等展会,能够顺利地完成这一切,真的是非常感谢《原神》的整个开发团队的努力,非常的不容易。
下面讲一下我们在渲染管线做改动的时候一些思路,因为开发的时间很紧,所以在选择技术改造点的时候,遵循下面几个原则。
第一个是关于功能的选择,我们首先排除掉开发周期长的,需要过多地前期研究工作的功能,因为我们没有时间。
然后根据游戏的美术风格,我们选择一些对于画质的帮助更大的地方去做提升。另外因为时间不多,所以我们希望新加的功能能够更多地发挥作用,所以最好是能够在相互之间产生互动,这样会显得画面更加系统化,得到的画质提升也会有1+1>2的效果。
下面就是我们做一些技术点的解析。首先我们从场景的光影方面选择了几个技术点,主要是侧重一些方法,会稍微涉及到一点点的优化思路。第一个是关于方向光的阴影,《原神》的大量游戏时间是在室外,室外方向光的阴影质量非常重要。一方面近处的阴影细节需要更加细腻,才显得画面更干净。另外一方面是阴影覆盖范围需要足够大,因为游戏的可视距离非常远。《原神》的阴影范围是800米。大家可以看一下这张贴图,即使在远处墙壁上一小片的绿植产生的阴影,在放大之后都能够看到树叶的轮廓。这个地方放大之后,能够看到树叶轮廓,而且非常稳定。整体上来说,我们对于《原神》的方向光阴影的质量是比较满意的。
我们阴影的技术还算是比较常规,使用了Cascaded shadow map 加上基于Poisson disc的soft shadow,我们游戏没有使用通常的4级cascades,而是用了8级,这属于大力出奇迹的方式。大力出奇迹带来了更好的阴影效果,当然也带来了更多的性能开销。更多的drawcall会带来CPU开销,更多的cascades也会带来GPU的开销。我们把质量提升上去之后,会想办法来解决性能问题,那我们怎么去做的呢?
首先在CPU端,我们做了一个shadow cache,8级cascades前4级我们每帧都更新,后面4级是采用轮流更新的方式,确保每8帧所有的cascades都能至少更新一次。每一帧的话,我们只更新5级cascades。
主要的工作量其实在GPU端,用了8级cascades以后,我们的screen space shadow map的开销长期是大于2毫秒的,在某些情况之下能够超过2.5毫秒。GPU比4级cascades的情况下,爆涨了0.5到0.8毫秒。
我们的软阴影采用的是泊松分布的采样,而且每个像素会去做一个旋转,来消除重复的pattern,这一整套的操作都是很重量级的。但是我们仔细想一想,真的需要对每个像素都要做这么多操作吗?所以我们的优化思路是尽量只在必要的地方做软阴影计算,我们会生成一张Mask贴图,在贴图里面标出阴影、半影和非影片区。阴影区和非阴影区只需要直接返回0和1就好了,只有在半影区才会去计算软阴影,通过这种方式,我们的GPU开销大致减少了30%左右。甚至比采用4级cascades还要再快一些。
大家可以看一下这张图,图里面被红色标注的区域,就是我们的半影区,这个是需要我们去做软阴影处理的区域。其他的区域,就是在阴影区域或者是非阴影区域,我们直接返回0和1就好了。大家可以看出来,绝大部分的像素都可以去掉软阴影计算这个繁琐的步骤。
这张神奇的Mask贴图是怎么生成的呢?这张Mask贴图的分辨率是屏幕分辨率的1/4×1/4,也就是说一个Mask值对应的是一个4×4的block。然后我们对4×4的block里面的每一个像素,来判断它是不是在阴影中,最后汇总成一个阴影、半影和非阴影的三个状态,保存到Mask贴图里。这样我们能够得到一个准确的半影信息,但是它不够快,所以我们做了进一步的优化,只选择4×4这个block里面很少的几个像素,来判断是不是在阴影当中。
这几个像素的判断结果,就代表了整个block的信息,显然这样会出现一些误差,因为我们是拿几个少数几个像素的结果来代表整个block,所以我们把这样计算得到了Mask贴图做了模糊处理,让半影的区域稍微扩散出去。整个Mask贴图的生成,包括模糊处理大概的开销是在0.3毫秒左右。大家可以看一下对比图。
优化出来的效果非常好,肉眼可以说是看不出任何的区别。这样优化完之后,我们的GPU开销时间大概稳定在1.3到1.7毫秒。
把阴影搞好以后,下面我们来看看AO(Ambient Occlusion环境光遮蔽)。大家可以考虑一种情况,就是人物和场景的物体都已经处在山或建筑物的阴影当中,这个时候人物和物体的投影跟山和建筑的投影是融为一体的。这种情况之下,画面缺乏对比,人和物体就会显得浮空。为了解决这个问题,我们在游戏里面采用了多种的AO技术,针对不同的情景生成不同的AO。
首先我们使用了HBAO,这是一个比较常规的实现,能够提供一些比较细节的AO效果。同时我们对静态物体和动态物体分别采用了AO Volume和Capsule AO这两种技术。大家可以看一下这是HBAO开关的对比图,效果还是很明显的。
下面这个是AO Volume的开关情况,大家可以重点看一下我们在红圈里面的区域。椅子对地面产生了柔和的投影,在我们打开AO volume的情况下。
和HBAO相比,AO Volume能够产生更大范围的AO。它可以针对类似桌子或者椅子产生大面积AO。因为技术原理和性能的限制,HBAO是没办法产生这种效果的。AO Volume这个时候就体现很好的补充,要实现AO volume,首先我们是在离线的时候对需要产生AO volume的物体做一个遮挡信息的计算。这个计算是在物体的本地空间(Local space)去做的,生成的遮挡信息我们保存下来,在运行的时候注入到volume texture中去使用。这个技术在2012年GDC关于《InFamous 2》的讲座上有提到过,大家有兴趣可以去看一下。
下面是关于Capsule AO的对比图,大家可以重点看一下屏风和地面,被我们红色的圈给圈出来的区域。大家可以看到相邻在屏风和地面,能够产生出能够反映体形和人影的投影。而且如果在游戏中大家去观察的话,随之相邻动作的改变,阴影的形状也会随之产生变化。
我们前面提到AO Volume主要是针对静态物体的,因为遮挡信息是通过离线计算的方式保存下来。像角色这种带骨骼动画的,是不能采用这种方式的,因为形状是不停地在发生变化。Capsule AO的做法就是用一些胶囊体包裹住人物的四肢和躯干,这些胶囊体和角色的骨骼动画绑定进行同步更新。然后这些胶囊体会被用来做遮挡计算,计算的时候我们把它分为无方向的环境遮挡计算,以及带方向的遮挡信息计算。带方向的遮挡信息计算采用的方向是主光源方向和法线进行混合之后的得到的虚拟遮挡方向。通过这种方式,角色可以同时在周围的墙和地面等投出多个阴影。
下面是一个关于AO的优化技巧,《原神》的AO都是在1/2×1/2分辨率的RT(Render Texture)上去做计算。为了保证画面的干净,我们对AO还做了一个模糊处理(blur)。然后再Upsample一个全分辨率的贴图上面去。所有的模糊处理和Upsample pass,我们都用了一个Bilateral filter,确保不会有无效的AO渗透到周围的区域。
从前面的描述可以看出来,模糊处理和Upsample加起来一共有三个pass,这就意味着AO需要被读取和写入多次。而且你如果你了解Bilateral Upsample的话,大家可以知道相邻的像素之间有很多的计算其实都是重复的,所以我们采用的优化方式是将所有的计算都放到一个compute pass里面去做。然后通过LDS来保存blur的中间值,通过同时输出四个像素的方式,来重用相邻像素的计算。最终我们还可以通过async compute pipe把性能开销进一步降低。
关于我们的Local Light,我们在游戏里面采用了Clustered deferred lighting。我们支持是视野内同时出现最多1024盏灯。大概的做法是我们将屏幕分成64×64像素的tile,然后每一个tile在深度的方向上面继续分为16级的clusters。通过这两个图,可以大概看出我们能够支持多少灯。
这张图是一个游戏里面的截图,是一个典型的通过Local Light的阴影提升画面的情况,多个不同的Local Light,它们的照明范围是交错存在的,然后角色也投下多个不同的阴影朝不同的方向,画面就显得细节很丰富。
我们怎么做的呢?我们的Local Light 阴影系统支持接近100盏灯的实时阴影,理论上我们可以支持更多的,不过这已经很够用了。阴影的分辨率是根据优先级和距离进行动态调整,最终的阴影是通过烘焙的静态场景阴影和实时生成的动态场景阴影结合得到的。
游戏里面有很多的Local Light,如果每一个Local Light都去烘焙它的shadow texture的话,会占用的硬盘空间非常大。而且因为是深度贴图,所以不能够随便使用BCn的压缩,那样瑕疵会非常风险,所以需要一个好的算法来对于烘焙的shadow texture做一个压缩。这个压缩需要在精度损失足够低的同时,还要保持压缩率足够高,同时我们的解压开销要非常小才行。
我们开发的这个系统是在离线制作的时候,对于shadow texture做一个压缩,尽量地去保持精度,运行的时候解压的速度也非常快,用compute shader去解压的情况,1K×1K的shadow texture,我们解压只需要0.05毫秒,可以说非常非常快。
那压缩率和压缩质量呢?我们先介绍一下压缩的算法思路。首先我们对于shadow texture按照一个2×2的block来进行编码,每4个深度值,我们用32bit来保存。如果想要降低精度损失,可以选择高精度压缩,这种情况之下每个block的大小变成64bit。编码的方式有两种,一种是基于深度平面方程的方式,或者是通过压缩的浮点数方式。编码完成之后,还要进一步通过一个quad tree来合并编码以后的数据,进一步提高压缩率。quad tree是每个tile要保存一个,而每个tile又包含了16×16个block,大家可以看到下面的三个图,从左到右分别是没有压缩的深度贴图,中间是我们的平面方程编码的视图,最右边是我们quad tree 0到4级的深度视图,黑的地方是深度为0的区域。我们参考了Li Bo在2019年Siggraph上面的讲座,大家有兴趣可以去看一下。
压缩比:在一个典型的室内场景默认精度压缩比是在20:1到30:1左右。如果开启高精度模式压缩的话,大概默认精度压缩到40%到70%。阴影贴图的压缩是非常必要的,可以帮我们容量下降一个数量级。
大家可以看一个对比,这是默认精度压缩,能够看到红圈里面有一些瑕疵。这个实际上是我们找到的可以说是最差的一个情况。这是高精度压缩,基本上看不出任何瑕疵来。这个是默认精度压缩。如果把高精度压缩的图和不压缩的情况做对比的话,其实肉眼是看不出什么差别,所以没有放这个图。
刚才的图是一个2K×2K的shadow texture,大小如果不压缩的话是在8MB,默认精度压缩大小变成了274.4KB,压缩率是29.85。如果替换成高精度压缩,就是肉眼看不出差距的压缩,贴图大小变成了583.5KB,这种情况的压缩率还是有14左右,所以还是相当不错的。
在搞好了Local Light以后,我们接下来为游戏添加了体积雾,体积雾是可以接受Local Light的照明影响,在灯的影响范围内形成一圈光晕,可以极大地提升画面的体积感。大家可以看到图里面近处的灯笼周围会有一圈泛光。包括画面远处的建筑物,因为笼罩在灯光下会使得周围的体积雾也被照亮,而显得有一丝的朦胧。
这个图是一个更有意思的情况。如果我们给Local Light加一个projection texture,也就是我们通过这个贴图来控制Local Light光照的形状,就像右边这样,体积雾也会产生相应的变化。
我们的体积雾的计算是基于物理的。我们也支持通过不同的参数让体积雾在大世界里面的不同区域有不同的表现。体积雾支持Local Light,这个在前面已经展示过了。为了让体积雾更加稳定,画面更加细腻,我们给体积雾添加了Temporal filter,进行了多帧的混合。整体的GPU开销,也控制的不错,在PS4 Pro下面大致在1毫秒甚至更少。
大致的实现是这样的:首先是基于相机空间,我们把view frustum分成很多的voxel,这些voxel跟我们前面提到的clustered deferred lighting的clusters是对齐的,这样方便我们在后面对Local Light做scattering计算的时候进行一个加速。
前面提到的体积雾参数和Local Light的信息,都会被注入到这些voxel里面去,然后我们通过Ray marching的方式去计算体积雾。在这个时候,Local Light的信息就自然而然被考虑进去了。
有了体积雾,我们不得不提到God Ray效果,首先大家可以先看看游戏里面God Ray的表现。对于方向光进行遮挡就可以产生God Ray的效果,我们的做法是有一个单独的pass来生成God Ray,然后是在1/2×1/2分辨率下面。God Ray也是通过Ray marching的方式去生成的,我们会去采样shadow map,但是最多会采样5级的cascades。
God Ray生成完之后,我们会提供美术一些可以调整的参数,然后将God Ray的结果叠加到体积雾上面去。它在使用上面,并不是一个物理上正确的东西,但是它的效果是能够让美术满意的。
了解体积雾的人可能会有一个问题,为什么要单独使用一个pass呢?体积雾本身就可以产生God Ray。这个就是一个很好的技术和美术磨合的例子,我们有体积雾直接生成的God Ray,在游戏里面实际效果其实不能够让美术满意,原因有两点。第一个是分辨率不够,因为体积雾的分辨率是靠Voxel,而我们的Voxel是不会划分的特别精细的。第二是因为体积雾生成的God Ray强度是完全依赖于体积雾的浓度。要想得到很明显的God Ray,就需要雾的浓度提的非常高。雾的浓度一旦提高了,画面就会显得不通透,太脏,这就是回到前面提到的两组词里面,这是美术不能够接受的。所以我们是采用了单独的pass去生成God Ray,这样可以得到更锐利、更清晰的效果,美术调整也更灵活。美术想要什么,我们就给他做什么。
下面给大家看看对比图,大家就能更好地体会到我说的是什么意思。这个God Ray就是通过体积雾生成的,包括整个画面的表现。这张是我们游戏里面现在使用的方式,就是单独的pass去生成,做一个对比。
大家可以看到第一张God Ray不是很明显,而且画面雾的浓度非常高。而第二张图,God Ray会更清晰一些,而且整个画面是更加干净、更加通透,这就是美术想要的效果。
接下来是IBL(Image Based Lighting)系统,大家先看一下演示视频。图中左边的是Reflection probe(反射探针),右边是Ambient probe,随着24小时的变化,我们的Reflection probe和Ambient probe的内容也会跟着变化。
我们先看一下左边的Reflection probe,Reflection probe是用来给场景提供反射信息的。因为游戏的光影不断变化,我们是不能够简单地为反射探针烘焙一张环境贴图作为反射信息之用。所以对于每一个Reflection probe,我们是烘焙了一个mini GBuffer。产生在游戏当中,根据当时的光照条件去实时生成环境贴图,美术可以在游戏里面摆很多个这样的Reflection probe,只要他们需要。
然后在运行的时候,我们会去更新场景的Reflection probe的cubemap。整个过程我们大致分为三步,第一步是Relight,第二步是Convolve和Compress。我们使用Compute Shader去同时处理六个面,然后分帧进行,同时只处理一个probe,不停地做循环。
第一个步骤,就是Relight步骤,大家可以通过图能看出来,就是一个简单的把当前的光照环境用来照亮mini GBuffer,得到环境贴图的过程。然后生成的环境贴图,需要经过Convolve这一步,得到mipmap的正确信息。最后这个贴图需要再通过一个Compute Shader的做法,压缩成BC6H的格式,然后送到渲染管线里面去使用。大致是这么三步的过程。
下面是我们的Ambient probe。Ambient probe也是实时生成的。我们在做完Relight以后,Reflection probe是包含了当前的整个光照信息,我们可以从中提取出当前的Ambient的信息,并且把它转化成一个3阶的SH(Spherical Harmonic)系数保存下来。
这个提取的过程,在我们把Reflection probe处理完成以后会自动进行,也是同时使用Compute Shader来处理六个面。
这么看下来,我们整个系统算是完成了,但实际上里面有很多地方是可以改进的。第一个是Relight是没有阴影的,因为单靠mini GBuffer我们是没有办法在Relight pass生成阴影,这样会导致一个很大的问题。就是在Relight完成得到的环境贴图是漏光的,本来应该处于阴影当中的地面也会变得非常明亮。
通过这样的环境贴图算出的环境光(ambient)也会出现有问题的情况,那怎么解决呢?我们的做法是,我们把24小时的shadow都烘焙下来,就是隔一段时间我们烘焙一下,把shadow转化成一个shadow SH保存起来。在运行的时候简单通过当前的时间对shadow SH进行插值,用来压暗Relight以后的结果。
这样得到的效果是出乎意料的好,而且我们需要保存的数据非常的少。因为shadow SH很糊,所以我们做插值也没有什么大的问题。
同样的方式,我们还可以把Local Light的信息也保存下来,作为Local Light的SH在Relight的时候也加上去,这样可以得到非常好、非常廉价的一个Local Light 反弹的效果。
大家可以看一下对比,这张图是没有添加shadow SH,这张是添加的,大家可以看到没添加的情况之下,屋檐和地面都莫名其妙的亮,添加之后就能够看出来是在阴影当中了,所以效果是很明显的。
下面是我们把Local Light SH加进去的情况对比。这是没有添加的,大家注意看一下画面右上角那片屋檐下面暗的区域,这是没有添加Local Light SH的情况。这是添加了的。那块区域被照亮了。
我们现在已经解决了漏光的问题,并且添加了Local Light SH。接下来是一个室内室外光照环境不一致带来的问题,因为室内和室外的光照环境往往是很不一样的。如果不加区分的话,室内外的环境光(ambient)混在一起,得到的效果就很容易让人家觉得不对劲。
我们是把Reflection probe分成室内、室外两种,然后美术通过摆放一个室内环境用的网格(interior mesh)来标记受室内光影响的像素。Ambient probe也会相应地为室内、室外生成不同的环境光。
下面看一下对比,这是没有开的情况,如果不区分的话,室内跟室外一样都会受天光的影响而变得很蓝,然后做了室内(interior)标记,室内的像素就能够正确地反应出室内的光照条件,会显得更黄一些。
而且我们还做了一个过度的处理,就是在门口这个区域当室内光照和室外光照环境切换的时候,不会出现一个因为明显的光照差异不一样而产生的硬边的效果。
下面这个就是室内环境用的网格(interior mesh)生成的Mask标记图,红色区域就是室内的区域,大家可以看一下对照关系。
除了通过Reflection probe得到的反射,我们还有Screen space reflection来提供实时的反射信息。SSR在PS4 Pro上面的GPU开销大概是在1.5毫秒左右,我们对SSR也加了一个Temporal filter,通过当前帧的SSR信息和历史信息混合起来,来提高SSR计算结果的稳定性,让画面也更平滑一些。
为了得到更多的反射信息,我们为SSR生成了Hi-Z的buffer,我们可以让每条射线通过Hi-Z最多能够跟踪的距离达到整个屏幕。
下面是一个SSR(Screen space reflection)效果开关的对比图,在比较光滑的地板上效果尤其明显。
从前面的截图大家也能够看到,在没有SSR的情况之下,我们还有Reflection probe,它也是可以提供场景的反射信息。我们是使用了一个Deferred reflection pass来计算Reflection和Ambient信息。在计算Reflection的同时,我们把AO信息也考虑进去,这样可以有效地降低漏光。
接下来是我们的最后一个技术点,HDR Display。这里面的HDR Display包含了两个方面,一个方面是指亮度,需要使用PQ ST2084的EOTF,最高是能够让画面亮度达到10000 nits。另外一方面就是在色彩空间这边,我们需要支持Rec.2020色彩空间,Rec.2020色彩空间和现在普遍的电视机使用的Rec.709相比,它可以显示的色彩范围要大得多。大家可以看看在CIE 1931色度图里面的覆盖范围对比。Rec.2020色彩空间覆盖范围大概能达到75.8%,相比之下Rec.709只能在35.9%的样子。
在这里我们称使用了ST2084和Rec.2020色彩空间的渲染管线为HDR管线,而使用的Rec.709色彩空间的非HDR管线,我们把他们叫做SDR管线。关于HDR Display很多基础的信息,有很多人都已经讲过在这里面就不细讲了。下面主要讲一下,为了让这个技术放到《原神》里面去,我们做了哪些调整。
这张图是《原神》的SDR和HDR的管线对比图,和SDR管线相比,HDR管线没有了tone mapping,color grading变成了HDR的color grading。而代替tone mapping的是RRT+ODT(reference rendering transform + output display transform)的组合,这就是很多人熟悉的ACES调色。
另外,UI在HDR下面,也是单独画到一张RT的。然后再跟场景做一个合并,这是因为UI的亮度处理方式跟场景是不太一样的。在图上大家看到RRT+ODT是灰掉的,我们待会会再来细讲这个事情。
《原神》从1.2开始,会在PS4上面支持HDR10的模式,然后替换SDR的Color grading是我们的HDR Color grading,美术会在Davinci这种软件上面去做HDR校色。然后通过我们的脚本输出HDR的Look-Up-Table(LUT)。
在运行的时候,白平衡HDR的Color grading,还有我们的Color expansion这些操作,都会在一个compute pass里面,输出到一张Color的Look-Up-Table里面去。因为这个Look-Up-Table很小,所以尽管前面提到的这些都是需要大量的计算操作,但实际上开销是很小的,大概不到0.05毫秒。
前面管线图的RRT+ODT部分是灰掉的,虽然这个是一个主流的调色方式,但是我们并没有采用它,因为和我们的游戏风格不太搭。所以尽管主流,我们还是放弃了。
为了保证在低亮度范围内的画面和SDR版本的游戏一致,我们将HDR的渲染画面和tone mapping处理之后的画面做了一个基于亮度的混合,然后在亮度不高的地方,就尽量保证了filmic tonemapping的关于toe部分的处理。
在低亮度范围内一致性的问题,这里面涉及到OOTF的事情。前面我们提到在HDR上面是用了BT1886作为一个EOTF曲线,然后在设备处理游戏输出的时候会用到,相应的游戏在输出信号给电视机的时候也会增加一条曲线,这个就是OETF。然后在HDR的管线里面,实际上就是一条gamma曲线。
但是这里面存在一个问题,就是1886的gamma是2.4,但是SDR的OETF是一个分段函数,大致上可以看作是gamma 2.2,这就出现了一个问题,也就是在PPT上写的问题。OETF处理完的颜色,经过EOTF,它得到的并不是它本身,就产生了一个误差。
但是在HDR下面,因为我们OETF和EOTF是被很好的定义了的情况,所以他们是互逆的,于是这个颜色在经过这两个处理之后能够得到原来的颜色,所以在HDR下面没有这么一个问题。
但是大家已经习惯了在HDR下面有这么一个误差的画面,所以为了模拟这个误差,我们是在HDR管线里面添加了OOTF,把差异给补上去了。
做完这些,是不是HDR就彻底没问题了呢?并不是。这还有一个大坑,叫做Hue Shift。什么解释一下什么是Hue Shift?举个例子,大家可以看一下游戏里面火焰制作的示意图,我们通过一个灰度图,就是下面像馒头一样的东西,加一个噪声贴图,然后进行扰动,得到了火焰扰动的纹理,然后我们用橘色去染色。最后我们把火焰的整体亮度往上提,这个时候就得到大家可以看到的见证奇迹的时刻,在SDR下面因为有tone mapping的原因,tone mapping在亮部是有一条曲线的,会让亮度增加逐渐变慢,于是橘色的R通道跟G通道的差异本来是很大,但是随着亮度的增加,tone mapping曲线介入了,R通道的增长就变慢,G通道就逐渐赶了上来,于是就产生了Hue Shift,画面渐渐开始发黄,于是就得到了后面看到的SDR下面火焰的效果。
但是这个是美术想要的一个效果,只不过他是通过这种神奇的方式得到的。但是问题来了:因为在HDR下面,我们是没有tone mapping的,Hue Shift是不会发生的。它叫做Hue Preserving。R通道跟G通道的比例关系是得到一个保持的,所以能够得到亮度非常高的橘色。但是在视觉上,不会让人家觉得很明亮,因为没有黄色。
那怎么去修改呢?一种常用的方法,也是很多大作都用的方法,叫做黑体辐射。这是一个基于物理的算法,美术去指定温度,根据温度计算出应该是什么颜色。通过这种方式,我们是需要修改美术的资源。不过我们没有采用这种方式,为什么?一方面是因为HDR 这个功能是在很后期才加入的,我们不能够让美术去大量修改已有的资源。
另外游戏并不是写实类的游戏,我们的火可以是各种颜色,而且还有很多其他的特效是随着元素反应去转换颜色的。所以我们自己搞了一个方法,我们在shader里面去模拟了Hue Shift,并且把模拟放到了color grading pass里面去,合并到Look-Up-Table的计算中。
这样的好处是,首先我们是不需要修改任何的效果,得到的效果非常满意,我们不仅仅是让火焰特效在HDR下面的效果跟SDR几乎一致。而且因为通过引入tone mapping的方式模拟了Hue Shift,所以我们前面提到的在非HDR的亮度部分的画面一致性的问题,也被顺手解决掉了,不需要跟tone mapping处理之后的画面做混合。而且这个操作,也是在生成Look-Up-Table的时候去做的,所以它的性能增加是可以忽略不计的。
技术介绍我们就到这里介绍了,下面是我一些个人的总结和感想。
首先是全球玩家对《原神》主机版的接受程度之高是出乎我们的预料,非常诚惶诚恐,也非常的感慨。从零开始,我把PS4版做起来,看着它逐渐地变化,就像看着自己的孩子逐渐长大一样。随着PS版越做越完善,很多的小伙伴就逐渐加入了开发队伍中,于是PS4版变成了大家的孩子,大家努力让它变得更好,最后就怀着忐忑的心情送出来跟全球玩家见面。没想到现在这么受欢迎,所以是一件非常有成就感的事情。而且很开心,能够和这么好的团队一起做这个项目,后面我们也会继续努,不断地改进,也不断地去优化,不断地出更好的内容给大家。
就像前面提到的,《原神》的主机版开发是第一次尝试,时间、资源和人才都很缺乏。通过一年多的开发,我们积累了很多的经验,尤其是如何把写实的渲染技术跟风格化游戏结合的经验,非常的宝贵!随着新的主机平台的到来,我们又面临了一大波的技术升级。不过和在美国的时候相比,国内主机开发的从业人员太少了,希望大家能够多多交流,和我们探讨一下技术,交流一下开发经验。
下面是我们HR拜托的一个硬广的时间,希望大家踊跃扫描二维码。就我个人而言,在米哈游做游戏、做技术是一个非常不错的选择,我们也很热忱地邀请大家加入,尤其是加入主机团队,我们需要所有岗位的大佬,一起来做次世代的游戏。
最后是感谢!首先要感谢原神引擎团队的所有成员,也感谢所有为《原神》主机开发出了力的大佬们!其次还要感谢主机团队特别成员,每天被大家撸来撸去的lulu。最后是特别感谢陈文礼、Terry liu,以及各位Sony全球的技术支持专家,感谢你们在《原神》开发期间对项目的大力支持!
我这次的分享就结束了,感谢大家!