刚从首尔回来,我就拽着《明日方舟:终末地》技术负责人Will补了一堂代码课。起因是他在Unite Seoul那场座位爆满的闭门演讲——全英文、无录像、全场唯一中国游戏公司项目,被标成“进阶”,聊的是他们怎么把Unity的底层翻了个底朝天。我本以为这种分享会高冷得像编译器说明书,结果Will坐下来第一句就把我逗乐了:“那次给玩家讲的太浅了,这次才算真家伙。”
上次我写过,终末地为了在低端手机上保住一公里视距,团队故意在山和建筑后面藏掉一部分场景——我当时直觉这是关卡设计师的灵机一动。Will纠正说,顺序反了:是技术组先把这套CPU软件遮挡做扎实了,再跑去跟美术和策划推荐,“你们能不能多放几座山?” 这差不多就是今天我们要聊的整场技术基建的缩影。
如果把终末地的渲染架构画成一幅“核心图”,你会发现它根本不像标准Unity里的积木,底下垫着四层自研的承重墙: ECS数据内核、可共享可见性的剔除-排序-批处理链路、手写的原生C++运行时渲染管线,以及最底下那层焊死了Vulkan语义的设备抽象。每一层都动了引擎的根,而它们共同回答一个问题——怎么同时伺候PC上八九万面的角色、移动端四五万面的同一角色、战斗中四个同屏人、满地图的动态物件,还不让手机烫成暖手宝。
接下来,我就按这张图,从上到下扒一遍鹰角“突破底层代码”的野路子。有些地方我会用玩家听得懂的比方,但该硬的地方绝不兑水——毕竟Will已经帮我筛过一遍,保证里头的技术账本没一处是凭空捏的。
第一层:ECS内核,替GameObject背锅
玩过Unity的朋友都知道,引擎默认的GameObject+Component虽然灵活,可一碰上每帧几万个小东西同时动弹,就显出不划算。终末地光是工厂区随便转个视角,可渲染的对象就可能蹦到数万个。于是团队从引擎的内核下手,自研了一套ECS(实体-组件-系统)数据内核。这玩意不跟你讲面向对象,它把数据按类型整整齐齐排进连续内存,CPU处理起来就像流水线上的扫码枪,刷刷刷,完全不用在内存里跳来跳去。借着ECS,数万对象的高吞吐处理才有了底。更重要的是,后续所有剔除、排序、批处理,全以ECS的数据为导向,不再依赖Unity原生的场景管理方式。换句话说,这层等于是把引擎的数据心脏给换了。
第二层:共享可见性+一揽子排序键
有了ECS,接下来是决定“画什么”和“怎么画”。终末地的多个视图(比如战斗中的分屏或者不同摄像机)不再各自维护自己的可见对象列表,而是共享同一份可见性数据。每个物体会得到一个32位标记,哪几个视角看得见它,就对应位上写个1。这样做的好处是剔除逻辑只跑一次,而不是每个视角都重复劳动。
剔除分三步走:先做视锥体测试,看不看得到;再做LOD选择,用哪一档精度;最后才是遮挡剔除。前两步和遮挡剔除是同时进行的,线程之间不用互相等——Will管这叫“不留空闲气泡”。遮挡剔除本身也很有说头:团队从场景里挑出几百个主要遮挡物,不拿原模型,而是用最简单的几个四边形片(quad)代替。CPU程序在每帧把这些片变换、裁剪成屏幕空间三角形,画进一张低分辨率深度图,再拿这张图去判断哪些对象被实时挡得严严实实。被完全遮住的就不画,下一帧再重新评估。注意,这里是CPU光栅化,不是GPU,原因后头会细说。
剔除之后,剩下的对象进入排序。团队玩了个“一箭三雕”:把屏幕顺序、管线类型、网格、材质、到摄像机的距离等所有与渲染状态相关的信息,编码进一个128-bit(16字节)的排序键。一次标准排序下去,相同状态的draw就自动抱团了。相邻的draw用同一套参数,绑定操作就可以整段跳过。然后又冒出一个新问题:每个物体自己的位置、参数都不一样,这些特有数据怎么传?如果给每个物体建独立的绑定表,内存分配会爆炸。他们的解法是全场景共用一份绑定表,指向同一个大缓冲区,每次draw只偏移一下指针,指向那一段专属数据,从而实现零分配。哪怕画几万个物体,额外开销几乎是零。至此,剔除、视图共享、批处理、零分配参数绑定全部打通,形成了以数据驱动的绘制准备流水线。
第三层:原生C++渲染管线,手写Render Graph
料备好了,交给谁炒? Unity原生的渲染管线用C#做高层调度,底下C++再干活,跨语言边界的损耗不小;而且管线本身属于通用设计,无法为终末地的极端状况做精细调度。Will他们决定在Unity编辑器、资源管线和工作流之下,重写一条原生的C++运行时渲染管线。这条管线在CPU侧手写了一版Render Graph,把渲染任务抽象成一个个节点和依赖关系,用来管理资源生命周期和屏障——什么时候加载纹理、什么时候释放、什么时候该插个同步,全由代码精确安排,而不是让驱动去猜。这样做虽然写起来累,但换来了帧间内存抖动极小、跨平台一致性极佳的结果。
第四层:焊死Vulkan语义的设备抽象
下面再垫一层设备抽象,可不是简单的画个接口,而是沿用了Vulkan原生的语义——描述符、内存屏障、命令缓冲这些底层概念原样保留。这意味着,移动端和PC端被拉到同一条以数据为导向的路径上,上层的排序复用、绑定免除、屏障合并在每个平台都一路贯通,无需为不同图形API做特殊妥协。在原生支持Vulkan的设备上,这层抽象几乎零开销,只是让代码在PC、主机和手机间保持一致的心智模型。
我听到这里忍不住问:“为什么不直接用GPU做遮挡剔除?性能不比CPU强?”Will说,团队参考过英特尔公开的论文,行业也有类似实践。关键是GPU做遮挡查询,结果要回读,容易造成CPU和GPU互相等待;而CPU软件遮挡灵活得多,不用打扰GPU的流水线。在终末地的复合高压下——角色高面数、动态物件多、视距远、还要跨平台——CPU手写一套低分辨率光栅化反而能见缝插针,把GPU从琐事里解放出来,专心画大场面。当然这需要对引擎底层有充分掌控,也正是他们从ECS到设备抽象一路改下来的真正价值。
这趟聊完,我最大的感受是,鹰角这套技术牌组的逻辑很清晰:不是挑着改一两个痛点,而是从数据核心、可见性、渲染管线到底层平台抽象,纵向贯穿地重构了一条数据驱动的运行时渲染框架。所有“远景不糊”“多个同屏角色不发烫”的表象,都是这套框架运转起来的结果,而不是靠某个孤立的小聪明。
作为一个“峡谷一级保护废物”,我未必能完全读懂每行代码背后的数学,但至少明白了,为什么终末地在手机上能看到那么远的天际线,能同时带着四个队员跑工厂而不掉帧。它不是靠遮,而是先给你一个能遮的基础,再把能省的每一分性能都从引擎的缝隙里抠出来。Will最后半开玩笑地说:“我们也不是非要手写,纯粹是被逼的。”我想,这大概就是技术人最傲娇的浪漫了。
热门跟贴