三个月前,鹰角办了一场公开讲座,请来了技术中心负责人Will。

那时我刚从旧金山的GDC游戏开发者大会回来,听罢这场分享,感觉其硬核程度不输GDC上的不少技术演讲。关卡设计师青青展示了早期的白盒设计,Will则介绍了Bindless渲染方案,还直接点出了“ECS框架”这类数据处理方式……听完之后,关于游戏开发技术的新知确实吸收了不少。

但Will在后台跟我透底:因为听众主要是普通玩家,内容准备得比较浅显;若将来有机会面向行业内部,他很乐意分享更具体的真实开发经验。

没想到,后续进展比预想中还要快。

上周在韩国首尔举办的Unite Seoul,是Unity开发者社区如今最重要的官方技术会议之一——Unity 7正是由Unity总裁兼CEO在这次会议上公开的。

Will受邀在此做了一场全英文演讲,题为“重构《明日方舟:终末地》的运行时渲染框架”。该场次被标记为“进阶”(Advanced),也是议程中唯一一场来自中国游戏公司的项目分享。

由于是“会场限定”,未安排线上直播或录像。现场座无虚席,演讲结束后,仍有开发者排队与Will交流。

这次演讲的内容,让我见识到了Will口中“技术干货”的真实面貌——坦白说,大部分内容听得云里雾里,许多技术名词作为多年玩家及游戏媒体从业者,从未见过。

待Will回国后,我们再次见面,结合此次演讲内容深入聊了一通。

经他补充解释,我发现这次涉及的内容,有不少正是上次聊到的、玩家也能听懂的优化技巧背后的底层技术,两者形成了很好的呼应;另一方面,因真正深入了代码层的探讨,我也对鹰角的技术体系搭建与理念有了新认识。

接下来,我尝试在上次报道基础上再往下剥一层,展示图像背后更基础的代码和架构。Will协助我重新筛选、解释了演讲内容,力求说得深入浅出。

不过这些内容依然属于非常“干”或“难懂”的范畴。一方面,我不认为自己能凭几千字,将建立在大量专业知识之上的演讲,拆成既严谨又人人易懂的科普文;另一方面,比起借助大量比喻营造似懂非懂的暧昧印象,我更希望保留原意与细节,让确实想了解鹰角技术幕后的读者获得相对精准的认知——这也算是对技术的一种尊重。

1

在终末地上线前,大家普遍知道鹰角对Unity引擎进行了深度定制化改造。高精度角色、大量动态对象、无缝箱庭以及跨平台……这些目标叠加在一起,光靠Unity通用路径根本无法满足项目的性能预算。

复杂的玩法设计和初期设定的高优化目标,决定了终末地必须突破Unity的一些通用路径。

Will此次演讲,首次系统公开了他们改了哪些部分、甚至具体怎么改的。

演讲主要提到四个部分:

其一,自研ECS数据内核,支撑每帧数万个可渲染对象的高吞吐处理;

其二,重构剔除、排序与批处理链路,让多个视图共享同一组可见性数据,再把渲染状态编码进同一个sort key,使“按状态聚簇”成为排序的天然产物,而非额外分组;

其三,将关键运行时渲染管理改为自写原生C++管线,配合CPU上手写的Render Graph,管理资源生命周期和依赖关系;

其四,底部垫一层沿用Vulkan语义的设备抽象,屏蔽平台图形API差异,把移动端和PC端拉到同一条数据导向路径上。这层封装刻意置于最底层,描述符、屏障等底层能力原样保留,上层的排序、绑定复用、屏障合并才能一路贯通至每个平台后端;在原生支持Vulkan的设备上,这一层几乎零开销。

此前文章中,我们提过终末地在移动端的一条优化底线:即便在最低端可运行设备上,也要尽量保住至少一公里的视距,而非用浓雾遮住远景。

能看到远方景物依旧轮廓清晰。

景深吃掉的性能总得从别处省出来——技术团队的办法是在工厂区周围布置山体或大型建筑,让它们自然挡住场景部分内容。玩家看到的画面更干净,被挡住的对象也不必继续绘制、消耗性能。

当时我以为这种“遮一遮”的技巧是开发中途灵机一动想到的点子,属“灵感>技术”。但Will纠正了我的理解:实际顺序是技术团队先做出这套遮挡能力,再向美术和策划推荐利用遮挡关系设计场景——这算是终末地的基建。

这套CPU软件遮挡的具体实现,大致分几步:团队先从场景中挑出几百个主要遮挡物,不使用原本完整模型,而是制作成简单遮挡片——通常只是几个quad;

运行时,CPU将这些遮挡片变换、裁剪为屏幕空间里的三角形,再按屏幕区域分组,由不同线程分别画入一张低分辨率深度图;

系统再用这张深度图判断哪些对象躲在遮挡物后面,把完全被挡对象从当帧绘制中剔除。被剔掉对象并未从场景删除,只是当帧不画,下帧重新参与判断。顺序上,遮挡测试其实是最后一关:在此之前,系统已做完视锥体测试(是否在视野内)和LOD选择(用哪档精度),且这两步与深度图构建同时进行,互不等待,正是线程间不互相等待、“不留空闲气泡”的设计。另有独立设计:十几二十个视角不再各自维护一份对象列表,而是共享同一份可见性数据,每个对象用一个32位数字记录自己在哪些视角里可见。

完成剔除后,剩余对象进入排序和批处理。团队将屏幕顺序、pipeline、mesh、材质和距离等信息编码进一个128-bit(16字节)的排序键,通过一次标准排序让相同状态对象集中在一起,相邻draw使用同一套参数,重复绑定即可整段跳过。但有一类数据躲不开:每个物体自己的位置、参数,每条draw都不一样。此处用了另一设计:不给每个物体单独建绑定表,而是全场景共用一份,指向同一个大缓冲区,每次draw只移动一个偏移量,指到自己那一小段数据,所以哪怕画几万个物体,这一档也是零分配。

最后才交由GPU正式渲染。在Unity编辑器、资源管线和工作流之下,团队自写了原生C++运行时渲染管线,以减少C#托管层与C++原生层之间的跨界开销。

这里每个环节,都涉及对Unity引擎底层架构的改动。

你可能也注意到了:在GPU图形处理能力日益增强的今天,终末地关键优化技巧之一,是在一些步骤手写代码。

Will解释说,他们并非为了手写而手写——CPU软件遮挡不算全新概念,团队参考过英特尔公开论文,行业里其他游戏也有类似思路。

但如前所述,终末地在性能优化上面对复合高压。在游戏PC与主机版本中,单个可操控角色精度最高渲染层级是8万到10万面,移动端也有4万到5万面;战斗中涉及最多同时操控4名角色;此外还有大量动态对象持续参与渲染,再叠加较大范围场景、远中近景阴影,以及跨平台观感一致性要求……林林总总,很难直接套用通用方案。

严格来说,这套CPU光栅化在数学上并不要求修改Unity底层:单独写程序,也可把几百个遮挡片画成深度图,再据此进行遮挡判断和剔除。

但用Will的话讲,这种做法代价太“贵”了——为了让终末地能在主流PC、主机和移动端旗舰机上以60帧甚至更高帧率的高画质运行,那么一帧画面完整预算就是16.66毫秒乃至更少,其中能留给CPU端遮挡和剔除的时间只有零点几毫秒。普通写法无法压进这个预算。

Will告诉我,早期他们也实验性尝试过如果尽量套用原生方案,Render Path只做简单排序等优化,那么游戏甚至难以稳定达到30帧。如今通过自研ECS、连续内存、多线程剔除、一次排序和原生C++管线,接成的这套完整架构,才达到了项目所追求的性能效用。

2

说到自研ECS,上次采访中,Will其实已解释过终末地如何使用ECS框架,来优化“大量物件同屏”情况下的画面效果。

最新「向渊行」版本仍在增加更多集成工业玩法设施种类。

ECS,即Entity Component System,一般译作“实体组件系统”。粗略来说,它让场景中每个对象不再各自背着一整套复杂逻辑行动,而是把同类数据连续摆放,再统一成批处理。

Unity为开发者提供了可选的Entities/DOTS ECS框架。ECS这类架构尤其适合处理成千上万个对象同屏的情况,但终末地还在工厂玩法上叠加了多名角色同屏战斗、无缝箱庭和极远景深——通用框架就很难压进项目性能预算。为此Will他们做了一系列相当激进的取舍:

·大约70种component ID全部在编译期写死;

· 用固定的128-bit mask描述component组合;

· 不做运行时类型注册、反射和字典查找;

· 变长数据尽量内联到定长结构;

· entity只是8字节handle;

· job直接使用原生C++和显式fence;

· 磁盘文件与运行时chunk尽量采用相同布局;

· 结构变更集中批处理,不在帧中途随意发生。

做到这地步,得到的其实就是一套专为终末地定制的ECS框架了,并由此实现了一些趋向极致的性能压榨。

比如开发团队会预先对场景进行整理和打包,尽量让磁盘上的ECS数据与运行时的chunk——即存放同一种组件组合的一批实体数据的连续内存——采用相同布局。这样加载这部分数据时,基本可通过一次memcpy(内存复制)直接搬入内存,省去逐对象解析、分配和重组。为避免误解,需强调一下这里说的只是预制ECS数据进入运行时的过程,并不等于完整场景加载只需要一次内存复制。

与此同时,正如大家所知,终末地里除了预制场景,还包含大量由玩家自由搭建的环境设施和工厂,开发组无法预先整理和打包。它们落地时会生成一批实体创建请求,再由系统通过批量结构变更写入相应chunk。入口虽然不同,但进入运行时后,两类对象会使用相同的连续内存布局、剔除、排序和渲染路径。

在保持人物、玩家自定义场景和远景都以高质量画面呈现的基础上,实现高性能优化。

可以说自研ECS就是终末地整套优化的地基。数据只有先以可预测方式排列好,后面的多线程、剔除和排序才可能快起来,也让CPU光栅化有条件被压进足够低的预算。

在PC、主机和移动端等不同设备上,终末地核心数据组织和运行时渲染架构保持一致,平台差异则留给画质、对象预算和底层后端处理。

Will展示的移动端和PC案例中,系统先通过多种剔除方式把几十万个候选对象压到数万个,再把屏幕顺序、pipeline、mesh、材质和距离等信息编码进另一组128-bit排序键。经过一次排序,相同状态对象集中在一起,系统不必在每次draw之间反复绑定材质、贴图和pipeline。这套架构把两端Render Path本身的CPU开销压到了约1毫秒——同样为避免歧义强调一下,这只是其中一个模块,排序和分批另有开销。

Will说他们在终末地这个项目上追求的是对性能优化的极致“压榨”,具体来说,就是不断检查每一笔性能开销是否真的带来了玩家能够感知的效果;如果收益不够,就继续删除、替换或重构。

虽Will无意对比,但在聊到性能压榨和技术取舍时,也提到AI已是业内技术团队普遍关注和实践的方向,鹰角技术团队同样做过不少探索。

实际上他们做过一项尝试:用AI来压缩游戏里光照数据所占用的内存,以及缩小游戏安装包体。

这项方案做了比较长一段时间,得到的压缩效果和开发工作流其实都比较理想。但最终他们发现,虽然包体和内存都变小了,运行时处理这些光照信息却会持续占用GPU,在部分中低端设备上的成本超过2毫秒,所以最终没有实装。

结合前面提到的手写代码,其实挺能体现鹰角技术部门内部所倡导的“实用主义”——手搓代码也好、AI也罢,最终都要服务于玩家能感知的体验,才会被应用到项目中。

3

不论是从这次演讲内容还是采访交流中,都能感受到Will很在意自己讲的“干货”是不是足够“干”。他说自己作为技术人员将心比心,也总会希望自己特意去听的讲座里多些真东西。

此前Unity曾多次邀请鹰角做技术分享,但终末地当时还未上线,团队内部认为先让大家看到产品形态和实际效果,再讲技术如何实现,才是更踏实的顺序,所以都婉拒了;现在,玩家和同行都有了实机内容可以对照,对于其背后深度定制后有了真正好奇和猜想,这些技术分享才变得更有意义。

Will回忆说,演讲结束后,还有个仍是相关专业学生的韩国听讲者找到他,说自己毕业后想从事“二次元相关渲染”,中国公司在这个领域做得很厉害,但在韩国甚至很难找到对应项目或工作机会。

Will在会场内与听众交流。

在我看来,这次演讲特别之处,就在于国内游戏产品其实很少有机会像这样,在某一个技术领域内被业内公认为“标杆”,去国际上做一些足够具体、具有创新性的技术分享,构建外界对于国内游戏行业的新印象。

这一切都有自身含金量,但依然容易被归入“内容和商业模式创新”。

包括在玩家层面,大家对中国厂商“技术强”的理解,通常也是“做得快、做得多、能把东西装进手机”。

中国游戏行业并不缺优秀工程师。移动端性能、网络架构、长期运营和大规模内容生产等领域,国内团队很早就处理过许多前沿问题。但在相当长时间里,这些能力未必附着在一款能由团队自己定义和表达的产品上——有些被掩盖在外包和协作项目背后,有些又因产品本身缺少辨识度,很难作为代表走到台前。

今年GDC上有不少来自中国厂商的演讲,但如前所述,如今被加上“游戏节”后缀的GDC意图吸引更基础开发者甚至普通玩家参与,所以现场讲座大部分内容比较浅显,整体比起学术论坛更像是游戏展会了。

但终末地这次带出去讲的,都属于大型项目最底层工程能力:内存里的数据布局、任务依赖与无锁并行、一次排序怎样减少状态切换、不同图形API如何被收进同一层抽象。这些是中国开发者在过去很少以自己的项目、自己的口径,在海外技术会议上得到关注的内容。

回头来看,鹰角建立至今也不到十年,在技术层获得如此快速发展还是非常令人感叹。

这种变化和“中国制造”在其他工业领域走过的路有些相似。最先被注意到的总是卷得飞起的产能和成本,后来才是工艺、标准、研发,以及属于自己的产品。只有当内容、品牌和技术同时长到一定程度,制造它的方法才会成为别人愿意研究的对象。

结语

很大程度上来讲,这也是我去和Will交流、以及写下这篇文章的理由。

国产游戏行业近两年发展很快、变化也很大,作为玩家、作为媒体,我们也终于可以更大方更自豪地谈论“我们的游戏产业”,以及用更加平衡、而非过去那种充满羡慕甚至接近于朝圣的心态,去和其他国家开发者们做交流。

也正因如此,我更加感受到作为游戏媒体,理应去学习和了解更多的专业知识与行业见闻,才能匹配得上这种进步。

终末地在「向渊行」版本更新后,也推出了由项目团队进行幕后创作分享的视频。由对应岗位成员,从相对专业角度分享美术、关卡与地图、玩法、剧情方面的创作故事,其中也会融入一些技术向科普,能看到玩家们对于这些内容其实也是感兴趣的。

我相信,接下来也会有更多国内厂商像鹰角一样,愿意围绕着自己产品的技术强项来做对外分享——尽管比起美术、音乐、叙事、玩法策划等内容创作故事,技术分享对于普通玩家的吸引力和宣传价值在现阶段明显要弱一些,要讲明白也更费劲。但这样的付出对于行业长远健康发展而言,无疑有着深远意义。