《明日之后》客户端技术复盘:如何构建末日世界的光与影?

《明日之后》作为一个3D视角的生存类手游,具备大世界、昼夜、天气、大量可采集植被、可建造房屋等多种高级特性,玩家在游戏里能明显感受到真实物理世界的光照和阴影变化。但这些效果的实现是比较复杂的,传统制作方案并不适用与这款游戏。在光照条件不固定,场景布局不固定的问题下,如何让游戏场景更接近真实物理世界?

1月20日,在网易游戏学院举办的2019网易游戏开发者峰会上,《明日之后》主程复盘了这款产品在客户端渲染和性能优化方面的技术实现。

以下为分享实录:

大家好,今天要给大家带来的是明日之后客户端技术分享。

明日之后是一个开放大世界的游戏,在渲染方面是比较复杂的。首先它是一个大世界,拥有3D视角,有昼夜天气以及大量植被,环境是可以被改变的。这些在程序员看来,就会面临光照条件不固定,场景布局不固定,面数高,DP多,Overdraw之类的问题。

光与影

带着这些问题,看一下我们是如何制作渲染方案的。

先来回顾一下传统制作方案:运用场景光照离线烘培,角色光照实时计算的方法,使场景更加真实。但这个方法并不太适合我们的游戏,因为单纯基于烘焙的方法虽然高效,但难以实现昼夜光照的交换。

所以我们当时就有几点思考:
第一,日升月落-主光源应当分离;
第二,实时GI对于手游依然遥不可及-依然需要烘焙;
第三,重新梳理光照中的各个分量,找出变与不变量。

单纯的烘焙效果是无法达到《明日之后》光照变化所需的效果的。所以我们将主光源分离出来,实时计算;将烘焙贴图的RGB存储间接光与烘焙点光;烘焙贴图的Alpha存储AO。

烘焙贴图的RGB在白天其实是看不见的,因为我们在白天时会将烘焙的RGB分量调到0。而在黑夜,烘焙贴图的RGB分量权值会调高。这样就可以做到白天没有灯光,而夜晚有灯光以及间接光的效果。

关于烘培间接光,是不存储太阳光产生的直接光照、阴影的,它只存储间接光与烘焙点光,根据时间调节直接光与间接光的权重,主要起到丰富夜间光照的作用,由于不需要存储阴影,尺寸可以小很多。

但AO就不同了,AO在白天和黑夜基本是一样的。因为AO可以让物体更“实”,有AO甚至可以接受没有阴影,烘焙时天光只产生AO,不产生光照,另外AO存储在烘焙贴图的Alpha通道。

以上基本就解决了场景烘焙基调的问题了,也就是场景静态光照的问题。

但美术发现,在这样一套光照下行走,人物不受光照影响,显得光照不够丰富,画面相对平面化。

所以我们就运用了模拟GI技术,它是一种叫Ambient Cube的技术。它会在场景里摆很多采样点,每个采样点记录6个方向的光照信息,利用烘焙器离线生成(网易自研Cloud GI)简单高效,大幅增强了夜间光照真实感与丰富度,它也可替换为SH来提升质量。

接下来就是动态点光源的问题,类似于游戏里的火把。但Ambient Cube只能实现静态点光,Deferred框架不适合移动平台。这时我们就回到点光源特性的问题,点光源有距离衰减的特性。

我们运用了Tiled Point Light这一技术,将画面切分为多个tile,利用上一帧的深度计算tile的world position,然后计算出tile贡献最大的2个点光,使得每个顶点/像素仅需计算2个点光。Tiled Point Light使得我们开销降低(iphone 5s也可使用),大大丰富了场景的光照效果。

接下来就有了这样的光照汇总

主光源实时计算
烘焙贴图的RGB存储间接光与烘焙点光
烘焙贴图的Alpha存储AO
Ambient Cube实现夜间和室内GI
Tile-based Point Light实现动态点光源
全场景阴影实时计算

提到阴影,阴影对3D视角的游戏来说几乎是不可避开的坑,Shadow Mapping在未来几年内应该还是主流方法,3D视角的阴影比2.5D视角难做很多,场景参与投影更是个大坑。

Shadow Mapping的优点是它具有非常简洁的原理,相对开销低(但也有很多坑)它具有海量的变种算法,在未来几年内应该还是主流方法,全场景阴影可能会越来越主流。如果用一句话来概括就是:如果在灯光视角看不到物体A,那么物体A就在阴影中。

但这也经常会遇到问题,其中一个比较严重的问题就是大场景的问题:3D视角游戏常常平视场景,阴影透视走样严重;使用单张超大Shadow MAP,远处精度严重浪费。

所以我们引入了Cascade Shadow Maps,使用多级Shadow Map,拆分远近物体,尽量充分利用精度,这也是LOD思想的一种应用。

PSSM是一种比较流行的做法,但《明日之后》使用的则是以相机为中心建立多级嵌套的Shadow Map。它使外层Shadow Map可以缓存,隔帧更新,减少DP,相机在小范围内移动时,可以完全不更新外层,DP开销接近0,实践上每帧均摊DP只有十来个。但它也有缺点,远处的阴影质量不如PSSM,难以实现超远距离的阴影。

接下来是植被问题:场景中有大量植被,大量Overdraw,阴影也存在被重复计算的问题。所以我们用Screen Space Shadow,使绘制场景时不计算阴影,最后一个Pass利用深度重构像素的世界坐标,并计算阴影,减少阴影的Overdraw。这样能减少GPU Time,从16.8ms减少到14.3ms。阴影也不是承在方向光上,而是承在最终颜色上,会让被光面变得更黑。

关于渲染这一块还想分享的是雨雪渲染。雨雪实际上是使用椎体包围相机,椎体行播放多层纹理动画来实现的。我们将近处和远处的雨都分开三个通道存在贴图里,做一个层次变化,在椎体上播放。

关于湿身效果非常有趣,它是通过调节PBR的金属度/粗糙度来模拟湿身效果,在PBR框架下无额外开销,所以不用白不用。

关于方案就讲到这,接下来讲讲优化方面是怎么做的。

优化

首先是渲染效率的问题,3D大场景的DP数非常恐怖,移动设备CPU/GPU都很弱,对DP数量非常敏感。我们的思路是:剔除不必要的DP,合并零碎的DP,优化单DP效率。

首先我们做了遮挡剔除,在视锥剔除的基础上,进一步剔除被遮挡的物体。最适合手游的遮挡剔除方案是PVS(Potentially visible set),它将相机可达空间切分为多个Cell,光线追踪计算每个Cell可看见哪些模型,使用bitset等数据结构保存可见性信息。运行时,我们就根据相机位置找到对应的Cell可见性信息,用于剔除不可见模型。

接下来进行合批。相同材质的物体会被合批,以一个DP绘制出来,合批发生在加载线程,生成额外顶点数据。有相同材质的要求,就意味着物体之间需要共享贴图,才能用一个指令绘制出来。

这样会遇到很多问题:
美术需要把多个模型的贴图合并到一张(随缘合);
每个模型的UV都需要重新调节一下(很麻烦);
后续增加模型.修改模型.删除模型,都需要调整UV;
维护成本高;
只要修改一个模型贴图,就会产生大贴图的Patch体积;
跨场景共享模型会造成内存浪费。

针对这些问题,我们制定了改进方案:离线仅预计算合批策略;贴图合并改为加载时进行。针对场景结构设计一种贪心算法,自动搜索哪些模型应当被合批,计算合批信息,仅需保存贴图的合并信息,合并操作留到运行时再做。

运行时创建被合批贴图时,在内存里将多张压缩贴图合并一张Atlas,ASTC/ETC2/PVR都是Block-based的压缩算法,按Block拷贝即可合并,合并的时间开销很小,和I/O相比可以忽略不计。引擎加载模型时,查询贴图是否被合批,如果被合批,则根据贴图的合批信息调整自己的UV即可。

最终的合批效果比人肉做可能还会好一些,美术也不需要花费大量时间去做合批,修改贴图不再会导致巨大的Patch。

除此之外,游戏中需要大量植被,植被虽做了LOD,但面数依旧很高。我们的思路是将它们渲染到Render Target,再以Billboard面片方式批量绘制。

为什么不离线做呢?
第一个原因是因为不想增加包体;
其次一些动态的信息可能需要渲染到RT里;
最后的成果是新手场景面数从20万减少到17万,新手场景GPU Time从14.3ms减少到12.3ms,并且因为生效距离远,所以不太容易看出瑕疵。

预算机制

最后讲一块,预算机制。

启发点首先是因为计算能力是有限的,我们需要对任务建立重要度分级。所以我们需要建立资源消耗和计算能力的闭环,从以前的”来几个,处理几个”,转变为”能处理几个,处理几个”的负反馈机制。然后当消耗达到预算上限时,延迟/放弃低优先级任务,或者换出低优先级资源。

这里我举两个例子。
第一:CPU预算,CPU资源是有限的,为了达到30fps,每帧预算只有33ms,那么我们可以使用预算管理异步回调。

第二:内存预算,NeoX中的纹理预算系统。我们可以通过严格控制纹理的内存的使用量,根据纹理对于场景的贡献度打分.排序,将分值低的纹理切换到低分辨率的Mipmap来实现对纹理内存的严格管控。

结语:
光影表现对新游戏而言非常重要,实时化是个大趋势,手游的技术路线并不完全与端游的老路一致,没有银弹,希望大家勇于探索新技术。

本文来自网易游戏学院,本文观点不代表GameLook立场,转载请联系原作者。

关注微信