2020年12月7日-10日,由腾讯游戏学院举办的第四届腾讯游戏开发者大会(Tencent Game Developers Conference,简称TGDC)在线上举行。腾讯互动娱乐《天涯明月刀》手游引擎技术负责人刘冰啸以“用技术诠释国风浪漫的归去来——《天涯明月刀》手游开发历程”为主题发表演讲。
《天涯明月刀》手游是一款基于真实的物理渲染的国风MMORPG游戏,延续了天刀端游的画面表现力。在手游平台性能受限的环境下实现这样的效果,甚至超越端游的表现,背后的一切是怎么发生的呢?
以下是演讲内容,有删减:
大家好,我是刘冰啸,来自腾讯《天涯明月刀》手游项目组,担任手游引擎技术负责人。这次给大家带来《天涯明月刀》手游的开发历程,包括其中几项关键点的技术决策和开发经历。
《天涯明月刀》手游的技术落地与优化
看一下《天涯明月刀》手游的先进技术是如何在天刀里面落地的。
手游引擎开发的几个重要要素,首先就是画质,我们可以进行一些选择,希望突出远景还是近景;希望画面表现优先于室内还是优先于室外;整个画面效果是鲜艳还是更加自然;在光与影之间做取舍,是优先光线表达,还是希望优先影子的效果。
第二是帧率。对于《天涯明月刀》手游这种强交互的MMORPG动作类游戏,我们需求一个高且稳定的帧率。通常在PC开发的时候,我们只需要顾虑两个方面,先达成画质的效果表现,然后通过优化来不断地提高帧率,帧率达成一个目标点之后,再想办法引入更好的画质表现,两者不停地迭代,来提升整体画面的效果和优化表现。
而对于手游来说,我们引入了第三个维度,功耗。功耗对于手机来说是非常重要的一个维度,它不仅会产生发热,还会影响到游玩的单次时间。
在这三个维度上,对其中任何一个维度的增加,都会影响到其他另外两个维度的表达。对于《天涯明月刀》手游的优化来说,我们开发引擎组采用了各种各样的技术栈,来去获取更好的优化效果。
比如对多线程渲染引擎进行了优化,把大部分可以并行的工作——动画、布料,都放到其他的线程中去运行。使用了ISPC来自动生成NeonSIMD的代码,可以更好地提升CPU的执行效率。在GPU和CPU的遮挡剔除中,我们使用了不同粒度的遮挡剔除算法,首先启用了Vulkan API,然后通过Vulkan API 对GPU的内存进行更好的粒度上的管理。
我们并且实现了GPU driven的渲染管线,用compute来去辅助优化各项其他的渲染技术。在画面上,我们使用了PBR的材质,以及真实的物理单位的lighting,获得更好的真实的HDR的画面效果。
在《天涯明月刀》手游的每次测试中,我们都会进入一个重要的优化或者功能。
第一次测试中,我们对unity 进行了多线程的框架改动,把渲染线程和提交线程从主线程中剥离出来。因为在手游的开发环境里面,一个主要线程的持续工作会带来手机芯片的功率提升,而它的功率提升会带来更多的发热。
第二次测试中,引入了Vulkan API。Vulkan API相对于GLES来说,有着更好、更轻量级的调用。经过测试,我们可以做到在提交线程上获得30%的CPU的效率提升。因为Vulkan API是更加自由的开发方式,可以在其中进行各种各样的优化。例如对一些比较重的Vulkan API操作,Descriptor Set的绑定,layout的绑定,都可以采用一些Cache的方式来去做。这么灵活的Vulkan API,使得我们在做开发的时候可以获得更好的解法。
第三次测试中,我们引入了GPU Driven的技术。通过GPU Driven技术,我们可以把大部分CPU上的工作转移到GPU上去运行,不仅提升了GPU的效率,也减少了从GPU到CPU之间的各种传递的带宽。这项技术我们用在了地形上、植被上,在家园里面也运用了这些效果。
另外在第三次测试里面,我们修改并提升了整体的光照表达,引入了自动曝光,提升了Tonemapping效果,解决了由于真实物理单位引入之后,在不同的光照环境下lighting体现的一些细节颜色丢失问题。我们重新对sky lighting进行了定义,使得整个场景的室外表现更加丰富和具有对比度。
采用GPU Driven技术对渲染技术进行优化
这里我主要讲一下,在第三次测试中,我们采用GPU Driven技术来对渲染技术进行优化的一些要点。
在开发过程中,我们经历了多线程的优化,发现了做手机平台的一个优化甜区——手机的多核,比我们在端游时代开发的时候更早的进入了多核时代,现在安卓手机很多都是四加四的多核架构。在这些架构下,都是以能让开发者更多的使用这些辅助的计算单元来提升你的计算效率。
对于我们引擎组来讲,我们采用了两个方向。第一个方向是尽可能的剥离主线程上的计算,通过dispatch到小核上、dispatch到其他线程上来提升它的计算效率。另外一个方向,把计算转入GPU compute。
为什么GPU compute是如此重要呢?它是现代渲染框架的一个基石。大家都了解,除了GPU进行光栅化处理部分之外,compute能够完成大量的GPU渲染流程上的操作,包括计算光照、计算材质等等。
另外,compute的渲染语言完全能和GPU本身的硬件对应起来。例如里面的Local Data Storage机制、ThreadGroup和Thread利用率的概念,都能很好地在compute语言上表现出来。
第二点是使用compute能够发挥Vulkan更大的潜力。因为我们可以使用Vulkan对GPU的同步行为做出非常好的控制,而compute作为一个独立的单元,可以把compute计算很好的和GPU的其他计算并行起来。例如compute可以和一个带宽优先的,例如shadow pass进行并行。
另外,compute是一个单独的Queue,对比vs和ps的整套pipeline来说,它是一个非常简化的单元,非常容易到处去摆放。
第三点,采用compute的话,我们可以给GPU Driven和Bindless打开更广阔的优化空间,甚至可以使用Async compute方式来更进一步的并行compute和GPU的单元。
对于《天涯明月刀手游》来说,打开compute的关键突破点,就是GPU Driven的地形系统。
看一下使用compute作为GPU Driven的一些研究方向。
在最近的几年里,GPU Driven是一个相对来说比较热门的研发方向,育碧有一篇文章,GPU Driven的渲染管线,主要是讲《刺客信条:大革命》的开发。EA的寒霜引擎也提到了GPU Driven的pipeline。2018年FarCry5也实现了相关的功能,2019年育碧的机动车Fusion也完成了相关的一些实现。但是在手机的这个领域上,目前还没有一些技术能够真正在在线的产品中体现出来。
什么是GPU驱动的渲染管线?首先是GPU掌握实际的渲染控制,可以提供更细致的渲染力度。例如做渲染剔除的时候,CPU只能控制在object level,使用Object Bounding来去做剔除,在GPU这个层面上可以做到更进一步的控制。我们可以在Mesh cluster级别上通过切分Mesh来获得更好的力度控制。
它的另外一个好处是,不需要GPU和CPU之间的数据来回传递,在理想情况下,GPU Driven甚至都可以使用一个Drawcall来绘制完整个场景。当然这个需要compute shader的支持,以及indirect drawing相关API的提供。在Vulkan 1.0的情况下,我们是都可以拿到相关的支持。
GPU Driven流程
看一下在《天涯明月刀》手游里面,GPU地形实施之后的结果。
首先介绍一下CPU地形常用的算法。我们上一个版本里面的地形算法是CPU端的Geometry Clipmap算法,它采用的裁剪剔除方式是视锥的裁剪剔除方式,对比GPU Driven来说,少了depth剔除,计算LOD的方式是根据距离计算的。而GPU Driven是可以根据距离和地形块的复杂度来计算。
送入clipmap方式的顶点处,因为有两个pass,其中一个要通过Virtual Texture来使用,这样的话它需要21万*2的顶点数。而在GPU Driven情况下,因为获得比较好的剔除效果,它只需要8万6的顶点数。
看一下最后的GPU时间。在GPU Driven的情况下,我们在iPhone8P上可以获得2.9毫秒的时间开销,这比通过CPU的方式下节省了近四分之一的成本。而在CPU端获得收益更大,我们提交线程和渲染线程,每个都可以获得五毫秒以上的收益。
GPU Driven的流程来讲的话,主要包含GPU Driven和Virtual Texture两个算法。这两个算法的实现有互相的交叉,出于简化,我们只讲一下GPU Driven相关流程上的一些算法。
首先是一个深度的mips生成,这部分算法是在compute里面实现的。其次在compute里面实现GPU遮挡剔除,使用上一步制作的深度缓冲buffer。GPU的遮挡剔除主要是通过计算你送入的patche的尺寸,看它处在哪一级的深度缓冲上,来对比这一级深度缓冲的深度then compare和你的深度and your depth,来决定是否被depth剔除掉。
通过这个流程我们可以获得可视的patche数目,根据可视的patche数目,我们仍然通过compute用来做indirect的Arguments的buffer来生成出来。
第四步就是把这些准备好的indirect buffer给绘制出去,这个就是整个地形GPU Driven的渲染流程。在这里面有一个非常重要的优化,也是在compute上可以达成的,这个优化主要是利用compute里面的Thread shared memory的方式来做。这种优化减少了CPU到GPU之间的多次的dispatch,减少了binding memory pingpong操作。
在IMD的一篇文章里,也会有更好的效果,它能应用的范围主要在于对你的buffer做filter。例如我们经常在渲染管线中提到的Bloom/高斯模糊、自动曝光,这些对于区域进行filter的操作都可以采用这些方法来获得优化。
GPU Driven怎么应用在《天涯明月刀手游》的地形体系里?
首先我们会做第一次的dispatch,这个dispatch会产生16*16的线程组,每组128条线程,把这128条线程读入深度放入mips里。第二步通过同步的方式,把上一级的mips相邻的四个点取出来,合并选择最深的单位,写到第二级的mips里,依次类推,完成四级的写出。在第二个dispatch里,我们用同样的方法,dispatch一个线程组,128条线程,把后面 32*16的mips写入完成。
在GPU Driven的地形系统里面,我们仍然有另外一个机制的优化,这个机制是LOD体系。在Farcry的实现里面,它主要采用的是CPU 四叉树的方式来组织LOD的patches,根据距离更新四叉树上的节点,在选择CPU 四叉树上节点,根据相机的距离选择这些节点,送入GPU进行indirect buffer和GPU Culling。
而对于《天涯明月刀》手游来说,我们把这些patch信息先通过offline的方式bake出来,在每一个相机发生位置的时候,去更新这些bake出来的信息,根据这些信息去对改变的patch做过滤,生成新的indirect buffer。
看一下差异,首先第一步,我们读到了所有的patches属性,第二步我们根据视锥裁剪,获得视锥裁剪之后,第三步我们再用HiZ产生一次depth裁剪。完成这三步之后,就可以生成Indirect Draw Arguments的buffer来去dispatch出去。
在我们完成GPU Driven地形之后,我们引擎组会把这些所有的流程重新梳理一遍,再根据梳理出来的流程,去选择可以应用GPU Driven的其他渲染模块。其中比较重要的一个渲染模块就是场景的植被管理。
有经验的渲染程序可能直接会意识到这一点,其实草的geometry和地形的patch、或者是sector的管理是一个非常类似的概念,它们都有LOD,有不同的geometry的表现。我们绘制这些geometry LOD的时候,最好的方法是通过Multi Draw Instanced Indirect的方案来去做。
另外一点是这些草的Texture。每种类型草的Texture,对应在地形上,更像是一个地形的Virtual Texture机制,我们也可以用bindless的方式去做绑定。可是在Vulkan 1.0的平台情况下,我们这两个API,Multi Draw Instanced Indirect或者是bindless,都没办法获得更好的支持。所以在《天涯明月刀》手游的实现里,我们只能采用将草的每种类型完成一次GPU Driven的culling和Draw Indirect Buffer的生成。
另外在《天涯明月刀手游》里面,比较适合GPU Driven的场景是家园的渲染。家园的玩法在游戏里,主要可以让玩家尽可能多的定制。我们整个家园的地形、地表、墙壁、物件、地板等等,它是需求非常多的geometry类型。第二,它的区域相对来说比较小,只有128米*128米的自定义空间,而在这种小的自定义空间情况下,遮挡剔除是必须要做的非常好的一种技术。
从这两点看起来,GPU Driven非常适合应用在家园渲染的情况。问题就在于,家园里面的这些物件其实和草的类型一样,都非常依赖于Multi Draw Instanced Indirect和bindless这两种API的实现。对于《天涯明月刀》手游来说,我们只能退而求其次,利用地形步骤算出来的HiZ的buffer做遮挡剔除。
我们送入一套做遮挡剔除buffer的内容,通过CPU从readback的方案来获得这些buffer的遮挡剔除的结果,在CPU端组织尽量多的instance对象。即便采用这种方式,在家园渲染情况下也获得了比较好的渲染效率。
在完成了compute的基础机制上,我们在其上面也做了各种其他的尝试,其中有一条是,完成了在ASTC和PVRTC的GPU实时压缩。这个也是通过GPU compute来实现的,这个功能可以用在角色的妆容系统上。
对《天涯明月刀》手游来说,整个角色的妆容系统需要完成多个Feature的绘制,如果不能很好的去做,baking到一张贴图上,在实时渲染的情况下它会产生更多的开销。实时baking到贴图上,我们还希望它能够尽量去做压缩来减少内存的使用。我们测试了一些compute compression的效果,在PSNR和效率上都能获得比较好的表现。
另外,我们尝试对于VirtualTexture的压缩。前面提到地形的VirtualTexture技术需要更新大量的地形块数据,越多的地形块数据才能使你更新的频率变低。在《天涯明月刀》手游里,我们使用2048*2048三张不经压缩的材质,通过调整贴图的尺寸和贴图数量以及压缩方式,基本上能把大张贴图的时间控制在四毫秒以下。
这个时间其实已经可以达到使用的效果,从远处来看,其实地表材质很难观察到差异性。下图是压缩前后的两张对比。但是这种算法,对比一下它的细节表现,我们仍然可以看到,在开启压缩的情况下,它其实是有一些Blocking的瑕疵,这样的瑕疵在游戏的画面品质情况下是不能接受的,所以这种方案只能把它放弃掉。
另外一个compute应用的领域,我们尝试了Cluster deferred。Cluster deferred主要应用于家园室内场景,首先它是一个封闭的空间,它仍然和之前家园的环境一样有大量的动态的物体、动态的光照效果。
在下图可以看到,家园物体里面有大概55盏灯,从左上角的一张蓝色的背景图来看,这个区域最多是8盏灯以上的照亮。在这个技术方案实现之后,我们发现它仍然解决不了几个问题;
第一个问题就是,deferred本身的固有问题,它的带宽问题,设计更好的GBuffer搁置也需要去应用一些API,例如subpass,或者一些更好的pass combine的操作。第二是在于材质的复杂度,一个deferred的材质和forward材质,在整个大世界的使用是很难做兼容融合的,这也增大了shader的复杂度或者是工作量。
这种方向也许对未来是一个比较好的技术点,但是在现有的架构下,我认为还是不够成熟,这套方案仍然也是放弃掉的。
《天涯明月刀》手游compute技术落地
来看我们应用compute技术落地的情况。
我认为有几个决策是非常重要的。第一,需要有一个非常好的基础,这个基础在于从多线程开始,我们意识到整个计算体系应该不同的去往compute方向,或者是往其他线程方向去使用,通过减少主线程的开销成本,去提升整个游戏的性能效率。
我们让比较好的技术功底的同事完成了GPU Driven地形的突破,在这个基础之上,继续使用GPU Driven的技术解决了一些场景植被的问题,以及家园的渲染效率问题。我们compute还解决了自动曝光等等相关的问题。
第三点,在于技术落地和产品需求之间的要求。有很多产品技术可能比较先进,但是它缺乏能达到产品需求的质量品质,我们对于这种技术也只能忍痛割爱。
Q&A
性能优化时候有什么技术难点?听说你们能以更低的规格实现更高标准,是怎么做到的?
我们在实现的过程中,没有特别去顾虑高规格和低规格,我们一般是设定一个标准,在这个标准之上会去找一些平衡。举个例子,对于我前面提到的光照或者是阴影的表达,我们认为阴影的表达是弱于光照效果的,所以我们仍然采用了比较传统的用lightmap来bake shadow的方式,并没有使用实时的阴影在大世界里面。
所以我觉得可能这更像是一个平衡的问题,这要基于你的游戏本身去承受的技术点,优先选择面,最终达成一个适合游戏要求的技术的方式。
像《天涯明月刀》这类的经典IP 从端游移植到手游,从引擎技术角度,如何更好的还原端游品质?
这个问题要从两方面来去回答。一方面是说我们需要去还原端游的哪些特点,第一,要更加注重远景的表现;第二,要有更好的角色的表现。我们有些特定的技术点的确也做到了比较好的还原,例如说云海效果。
基于以上的这些,要从端游还原到手游上的一些画面技术点,我们会去制定它的关键的技术要素,例如说我们仍然延续了端游的PBR的管线,但是我们使用了比端游更好的、基于物理光照单位的一个lighting方式,这样会使我们手游在暗光情况下的表现非常好,甚至在某种程度上优于端游的表现。
,