【Siggraph 2021】The Rendering Arc

2023-12-11  本文已影响0人  离原春草

留在港口的小船最安全,亲爱的,但这不是造船的目的
—— 弗雷德里克\cdot巴克曼《焦虑的人》

这边来学习下Roblox在Siggraph2021上所做的技术分享。

1. 基本情况介绍

在深入技术细节之前,作者先对Roblox做了一个定义或者描述:

  1. roblox不是一个付费工具,不是一个具有统一大厅的虚拟世界
  2. roblox是一个社区,不对玩法、美术风格进行预设或限制,但是添加了一些基础规则,比如真实的物理模拟效果,第三人称视角等。
  3. 相比于Unity/UE,更偏向于Youtube、Instagram等内容创作与分享平台,更重视用户的创意与用户之间的链接关系
  4. 整个世界用高抽象层次的Primitives表示,游戏引擎负责完成底层的实现(模拟、渲染等)

Roblox公司的基本情况:

下面来看看相应的技术细节。

2. Roblox虚拟世界的技术细节

整个虚拟世界的组织形式类似于HTML DOM(树状、层次结构),使用Lua作为脚本语言。

虚拟世界包含多个Root Service(根节点),如上图所示:

数据交互模式采用C/S模式,用RBXL表示我们说过的描述(静态)虚拟世界的类似HTML数据,Client不需要拿到这个数据,这个数据是服务器使用的,客户端拿到的数据是从服务器流式下发下来的(同样可以实现一定的安全控制,避免客户端控制游戏逻辑导致问题),但是不需要拿到所有的数据(比如有些数据是服务器专用的,比如一些远景数据会被替换成一些轻量级的voxel、imposter数据等)。

Lua脚本是在沙盒中执行的(安全考虑):

整个Roblox可以看成是Primitives构成的世界,Primitives对于Roblox而言,意味着如下的一些元素:

  1. Parts
    通过使用Cubical Projected贴图(这个指的是立体贴图吗?)+贴花来实现(具体怎么实现,是说通过立体贴图来实现纹理效果吗?),支持在运行时的CSG操作(布尔运算),碰撞体同样也是通过CSG来实现?
  2. Mesh Parts
    带有UV的Parts,用来实现一些复杂的模型效果,碰撞体通过对多个凸多面体进行组合来得到(这个是动态拆解计算得到的吗?),支持运行时的自动LOD(不知道效果是否能令人满意)。
    这个方案看起来有点复杂,在2006年首次上线,在2008年撤回,在2016年重新上线。
    2021年增加了蒙皮模型。
  3. 其他部分
    基于体素的地形方案,特效系统
  4. 材质部分
    这里的材质不仅仅是影响视觉的效果材质,同时还有影响听觉与玩法的物理材质。

Roblox使用了这一种分布式计算方案,整个方案可以分成客户端、服务器以及云服务三部分。
服务器部分实际上是Linux版本的引擎(听起来类似于传统的游戏服务器,负责游戏玩法逻辑)
云服务则指的是一些脱离游戏玩法逻辑的服务,如面向开发者的数据分析、数据存储、资源商城服务等,其中包含影响在目前而言最为重要的一项服务,如资源服务。

说到资源,就需要介绍一下UGC的内容制作管线,首先,资源是不可更改的(Immutable),不过支持通过上传新版本的方式来实现一定的修改自由度(之所以不支持直接在原资源上进行修改,是因为原资源可能已经发布出去被其他的关卡引用了,修改的结果是不可控的,当然,可以通过对资源添加引用计数来判断是否可以修正或删除,不过这么做复杂度就会高很多)。

资源包括了贴图、模型、声音、动作、静态的CSG物体等,这里说的不可修改,其实就意味着资源可能会被审核,审核方案包含AI+人工两种(不知道具体怎么判定哪些需要人工,是不是AI进行粗审,人工对可疑内容进行复核,或者全部AI,在玩家申诉之后再走人工?),Bespoke引擎为每种资源都开发了对应的审核工具。

资源的传输逻辑为CDN+基于资源地址的存储方案,CDN的问题在于高延迟,高带宽消耗,不知道这里基于Amazon S3的资源地址存储方案具体指的是什么?

资源的生命周期包含如下的几个阶段:

  1. 上传,在这个过程中会进行资源的验证(进行一些数据规格的确认?)与归一化(指的是资源的LOD自动生成,或者其他参数的转换?),这个过程是在网页端完成的(听起来,数据上传是在服务器进行处理的)
  2. 审核
    审核通过之后就意味着资源可以被外部使用了,在这个过程中也会做一些初始化的处理,比如生成缩略图等
  3. 客户端请求某个ID的资源
    这时候会根据客户端所在的平台,选择最合适的一个可用版本进行下发。在必要的时候会进行延迟优化,这些优化包括如下的一些操作:

场景里的所有数据都是可以编辑与动态替换的:

  1. 资源上99.9%的属性都是可以修改的,且已经暴露给lua脚本,可以在运行时进行动态修正。
    Roblox的Studio工具实际上可以看成是运行时版本上添加了一套UI界面,底层数据模型与技术方案是完全互通的。
  2. 任何资源默认都是带有物理效果的:服务端上会对所有物件进行物理计算,客户端则会对附近的物体进行物理计算
  3. 示例说明:

渲染方案需要因此而考虑这个动态世界的设定所造成的影响:

在设计层面,会在每个方面都考虑如何将技术细节隐藏起来,这样做有两个好处:

  1. 避免增加玩家的心智负担
  2. 技术升级对玩家来说是无感知的,操作习惯可以不需要改变就能实现效果与性能的提升

Roblox在技术上:
经历了15年的迭代升级,渲染API经历了从GLES2/DX9到如今的DX11/Vulkan/Metal等的跃迁。
在用户体验上,则秉持着一些高层次、通俗易理解的通用概念:

遵循上面的一些设计原则,也为Roblox带来了一些副作用,总的来说就是添加了一些约束,不过在其带来的好处面前,这些不足可以忽略:

下面来看下Roblox的引擎设计,这里放了一张在Roblox中制作输出的Cyberpunk 2077风格的世界效果图,大概是想说明Roblox的引擎能够实现3A品质的效果?

3. Roblox的引擎设计 - 反3A

总的来说,引擎设计由于如下的一些约束而变得困难:

为了说明工作的复杂度,这里举了profiling功能为例,总结起来就是在大数据分析上存在技术挑战,在适配或调试工作上现有工具不够给力,还需要面对复杂的用户设备与设置与行为。

具体应对措施,大致的做法是从优化的思路转变为适配的思路,即针对不同设备进行分级处理。

  1. 合理的需求降级
  1. 最简单的方式:关闭一些重要程度相对较低的功能,或者采用fallback方案来取代高精方案
  1. 尽可能的进行分帧处理

添加了一个FrameRateManager,根据帧率动态调整渲染参数:

这种方法的问题在于参数很难调整(在于设备众多、游戏种类众多?还是在于性能与效果的兼顾?),这里的做法是将某个游戏的参数配置直接缓存在本地(客户端,解决了什么问题呢?)

Roblox现在输出的品质在跟3A产品有很大差距,但是从长远看来,基于场景可以实时修改,添加更多细节,且底层技术可以不受玩家感知的进化,最终Roblox是能够在品质上不断提升,逼近甚至超越3A的

这里对前面的工作做了个总结,下面来看下引擎的一些实现细节。

4. 引擎实现细节

先来看下渲染架构,这里采用的是经典的Main+Render双线程模型

  1. 主线程负责完成数据的准备,场景更新
  2. 渲染线程负责将场景渲染出来

这里还采用了经典的线程池方案:

这里想要表达的是什么呢?线程池方案的优点在于实现简单,且目前远没有达到其所能达到的优化的极致,且怀疑即使这个方案做到头,其优化的程度或者系统复杂度可能都不如直接使用Graph System(那为什么不用?)

逐步逐步看下从DataModel(主线程)到渲染(线程)是怎么交互的。

DataModel到Rendering,是通过混合式的事件驱动+主动Push方案+被动Pull来实现的

对于Part或MeshPart而言,DM中的所有对象都会继承自一种实例(instance)类型,这个类型的作用是为对象提供反射能力与事件响应能力。
GfxBinding是一个渲染接口,其作用为监听属性变化,并且完成child的添加或者移除逻辑(这里child指的应该是其管理的对象的一个描述)。
每个渲染子系统都是GfxBinding的一个实现,用来实现对其管理的所有DM对象的操作,通常来说这些操作不过是将一些渲染结构标记为无效,或者将标记无效这个事件存入队列等到合适时机取出执行。

对于2D/3D GUI对象来说,使用的是GfxGui接口,这个接口的作用是记录GUI虚拟机的指令。

先来看下,主线程这边是如何完成数据准备的。

  1. 对DM添加了只读锁
    一些工作可以做成并行的(目前还没来得及),此外对于只读锁处理的工作而言,要尽可能的轻量
  2. 对于无效队列的处理
    比如对于Parts/MeshParts,会获取其相关的一些改变(比如Transform数据),只是不了解这里的pull指的是什么?
  3. 处理一些全局状态(如光照设置等)
    这里采用的是直接拷贝覆盖的方法,没有做增量处理或者事件化处理

对于场景的渲染,采用的是基于cluster的加速策略。

  1. 避免每个物体触发一个drawcall的情况发生,希望drawcall跟物件数的比为100:1
  2. 会考虑将场景划分为多个cluster,这个划分是基于object type与API能力(渲染API吗?)来实现的?
  3. 这里的渲染会基于一个假设来进行:虽然场景中存在大量的动态物件,但是每一帧只有少量物件在发生变化

Roblox的Clustering系统主要有两个:

FastClusters是一项在所有设备上都支持的特性,其基本原理是在运行时创建一个新的IB/VB,并将一大堆的几何数据塞入进去(动态合批)。
这里的一个要点在于如何考虑,哪些需要合并在一起,哪些不能:

在合批方案中,有一些特殊的处理,比如角色就不参与到合批中,因为基本上每个角色都是与众不同的,合批是负优化的概率较高。
不过这里也做了一些优化,将角色身上的多个贴图合并成一个atlas,通过一个drawcall完成绘制。
接下来要做或者正在做的优化有:

InstanceCluster对硬件有一些要求:支持硬件实例化。

在实际情况中,合批会需要根据具体情况来判断采用的策略(这两种做法背后的逻辑是?):

Instance的好处是,内存消耗较少,可以绘制更广的视野。
这种方法在Mesh LOD上的处理是针对整个cluster进行LOD切分处理。

渲染线程主要通过两个结构完成抽象:

  1. GfxRender:负责绘制
  1. GfxCore:负责与硬件的API对接

GfxCore支持了众多的API,同时还实现了Roblox的Shader Compiler:

下面看看光照与Shading部分的实现。

Shading的原则是:

  1. 尽可能的解耦
  1. 不针对特定设备实现功能

这里介绍Lighting的几个显著特点:

  1. 支持Voxel Lighting
  1. Shadows
  1. Forward+管线

分析式光照计算则准备分享如下两点:

  1. 所有的解析式光源都使用了PBR
  1. 按照品质从低到高来看,依次有如下的顺序:

最后对前面的内容做一个总结。

参考

[1]. The Rendering Architecture of Roblox - Slides
[2]. The Rendering Architecture of Roblox - Video
[3]. Angelo Pesce - Rendering the Metaverse across Space and Time

上一篇 下一篇

猜你喜欢

热点阅读