Meta已经正式开源了通过WebXR开发的沉浸式VR花园建筑体验Project Flowerbed,从而帮助开发者进一步探索相关的架构,asset管道和游戏机制。
Project Flowerbed是在MetaQuestBrowser中运行的冥想式VR园艺体验。在体验中,你将能探索一个宁静的岛屿,并种植和养护花卉与树木等等。你同时可以拍摄花园的照片,并与朋友分享。
Meta构建Projetc Flowerbed的目的有两个:通过创建在Meta Quest Browser中运行的高质量沉浸式体验来展示Meta Quest平台的强大WebXR功能。 为开发人员提供代码库,帮助他们学习如何使用WebXR构建自己的沉浸式体验
同样是出于上述两个目的,Meta正式开源了整个Project Flowerbed代码库。项目官网请点击这个页面,而相关代码可以访问GitHub。
在下面的博文中,团队详细介绍了他们是如何整合出这样一种沉浸式WebXR体验:
我们如何构建Projetc Flowerbed
1. 基本架构
Projetc Flowerbed是基于Three.js,我们使用ECSY作为游戏逻辑层。
我们选择Three.js,既是因为它支持WebXR,又是因为它拥有一个充满活力的社区;因为Project Flowerbed是WebXR和Meta Quest Browser的案例研究应用,所以我们必须以Three.js这样由社区驱动的引擎作为基础,从而真正了解当今开发者是如何构建体验。
Three.js的WebXR功能设置和使用非常简单;在第三方扩展(例如three-mesh-ui和three-mesh-bvh)的帮助下,我们能够快速构建游戏机制的初始原型,并在最终确定机制和asset时进行迭代。然而,Three.js本身几乎完全专注于渲染,并设计为与其他代码或框架组合以提供逻辑和游戏功能。
所以,ECSY。我们决定使用Entity-Component System(ECS)作为我们的逻辑,因为它允许我们分离不同的机制,允许开发者独立地处理功能,并且可以令它们作为整体一起工作。我们选择了ECSY而不是它的竞争对手,因为它是当时比较成熟的JavaScript ECS库之一,并且具有特定的功能: 临时状态可以存储在系统中,从而减少一次性组件的component。 将数据序列化为JSON,然后将其反序列化为entity和component非常简单,这是我们保存花园的基础。
遗憾的是,ECSY已被创建者弃用,目前不再受支持或维护。社区存在众多可供选择的ECS库,我们鼓励你找到最适合自己需要的工具。
Flowerbed中几乎所有的逻辑都通过我们的一个ECSY系统运行,而你可以在src/js/systems目录中找到。同样,component可以在src/js/Components目录中找到。
2. asset管道
在深入Flowerbed的内里之前,我们必须探讨如何处理我们的3D asset:从创建到加载并在Project Flowerbed中使用它们:
2. 1 在Blender中创作关卡和游戏对象
我们所有的3D asset都是用Blender创建,包括主环境。
Blender有一个强大的脚本引擎,这意味着我们可以在Blender将游戏数据添加到模型之中,并将其用作我们的关卡编辑器。我们在Blender中为单个网格指定了Custom Properties,这允许我们将JSON数据直接附加到glTF模型中的节点,然后在引擎中读取properties以附加游戏行为。所以,环境声音将在何处播放、链接对象的放置位置、以及哪些对象具有碰撞器等都可以在Blender中定义。
这同时是我们创建主环境的方式。环境中有众多在其他文件中定义的对象和需要碰撞器的对象,而我们不想手动创建它们。所以我们创建了一个脚本来生成一堆游戏对象并创建链接。
环境,以及我们用来在环境中生成碰撞器和其他功能的脚本可以在content/models/environment中找到。
2.2 模型创建和处理
我们使用glTF文件格式处理3D模型。glTF是一种开放格式,在Three.js中具有强大的支持,可以从Blender(和其他3D建模应用程序)轻松导出。网络有众多glTF预览器,而我们使用它们来验证模型是否正确导出,并调试材质、rig等任何问题。
纹被压缩成KTX2基纹理。KTX2纹理是GPU压缩纹理,这意味着与加载到GPU内存之前需要先解压缩的PNG或JPG相比,纹理可以在GPU保持压缩。换句话说,KTX2纹理在内存中要小得多,渲染速度更快,文件大小通常更小。
我们创建了一个基于命令行的管道来压缩所有glTF并创建KTX2纹理。所述管道由gltf transform驱动。这个开源库提供了处理gltf数据的各种实用程序,可允许我们修剪多余的数据、运行压缩并创建KTX2纹理。管道可以在源代码中的asset_pipeline目录中找到,并在content/models文件夹中的每个模型运行,以生成在游戏中导入的结果。
2.3 将模型加载到Flowerbed中
对于Flowerbed,所有加载都由AssetLoadingSystem处理(这个系统与AssetDatabaseComponent交互)。其中,单个asset类型通过特定的“数据库”加载,而数据库将asset存储在内存中,并在游戏中进行所需的任何更改(例如生成对撞机和材质)。
Mesh数据库处理所有.gltf文件并执行以下操作: 通过THREE的GLTFLoader加载文件 遍历文件的每个节点,将所有碰撞器与“可见”网格分离,并将碰撞器和可见网格存储在不同的dictionary中 将阴影贴图和所需的任何材质应用于可见网格
一旦加载了所有网格,Flowerbed中的任何系统都可以获取(克隆)模型并将其添加到场景中,而AssetPlacementSystem处理大多数用例。
注意:我们这里只讨论3D模型管道,因为它是最复杂的管道,但项目中有工具和脚本来处理所有其他asset类型,如音频、图像和字体。管道都可以在scripts文件夹中找到。
3. 碰撞
Three.js没有内置的碰撞或物理,所以我们的主要选择是使用单独的物理引擎或编写自己的碰撞库。
对于Flowerbed,我们决定进行我们自己的碰撞,以进一步了解相关过程,并避免物理引擎中更常见的问题。Flowerbed在运动物体之间不会有很多碰撞;大多数碰撞是玩家对静态对象的碰撞,或者是光线对UI的碰撞。
我们使用KDTree来处理碰撞的broad-phase(我们过滤掉距离太远而不可能参与碰撞的任何对象),并使用Three-mesh-bvh来处理narrow-phase(我们检查各个对象以查看它们是否碰撞)。Three-mesh-bvh具有高效的光线投射和形状投射,允许我们检查任意网格和球体之间的交叉点。这意味着我们可以直接在Blender中创建碰撞网格并将它们导入到体验中,而无需使用它们创建特定形状。
玩家物理由PlayerPhysicsSystem处理。它每帧几次地检查玩家是否与障碍物相交,如果是,则将玩家推出障碍物。
4. 音频
Flowerbed项目使用woler.js来管理音频。我们选择使用woler.js而不是THREE的PositionalAudio节点,是因为我们想实现更多的控制:wolerjs允许我们设置声音的淡入淡出,允许我们独立于Object3D的位置设置位置,并自行管理音频池。
音频加载到AudioDatabase实例中,然后通过将OneShotAudioComponent或LoopingAudioComponement附加到entity或创建临时entity。一次性音频将在播放后立即销毁,循环播放音频则直到component(或其附加到的entity)销毁。
5. 设计体验
5.1 视觉
我们希望Flowerbed成为一个丰富的、沉浸式的视觉体验。由于Flowerbed旨在成为WebXR功能的一个案例研究,所以我们使用了先进渲染技术:具有法线贴图和不同金属/粗糙度的PBR材质。高质量的几何图形和实时照明可以实现,我们需要展示如何实现这一点。
Flowerbed同时涉及设计和种植花园,因此我们希望动态阴影随着植物的生长而变化,随着周遭动物和水而变化。
5.2 植物生长系统
植物是Flowerbed的核心特征。我们知道我们需要支持大量的植物,但我们同时希望它能令用户产生一种置身于园艺世界一样的感觉。植物有自己的身份,并且因植物而异。我们同时希望植物在种子发芽生长时具有愉快的动画。
为了支持大量的植物,我们知道需要使用实例化网格进行植物渲染。开箱即用,Three.js不支持实例化网格的任何类型动画,这使得设计一种可以在植物生长时为其设置动画的方法十分困难。
我们的技术设计师用骨骼动画制作了一种植物原型:当我们观察它的行为时,我们意识到它主要是在不同的骨骼缩放。这是我们植物生长和动画系统的种子。我们可以制作一个只支持骨骼缩放的简化骨骼网格系统,并获得出色的结果。
为了有效地实例化网格,我们需要限制每个绘制调用所需的额外数据量。因为我们只支持每个骨骼的统一比例,所以每个骨骼的状态都可以用单个标量值表示。GPU自然使用四种通道类型,并且我们已经在每个实例的根变换中编码了一个比例,因此通过将四个额外的标量值打包到一个组件中,我们可以为每个网格实例设置五个单独的骨骼比例。
相关值完全由javascript驱动。为了在植物生长时为其设置动画,我们使用了一组PD controllers(一种control loop机制,PID controllers的简化形式)来为各个比例值设置动画,并手动调整每个值的目标。
这个动画系统同时可以允许我们以不同的方式永久缩放植物的部分。通过向每个骨骼比例添加一点随机偏移,我们可以在植物中获得一点几何变化。
每种植物有一组纹理变化。现在,它们并没有以巧妙的方式实现,每个纹理变化都只是一个单独的绘制调用。如果我们想进一步优化这一点,我们可以将纹理切换到一个数组,或者添加某种色相转换来实现这一点。
植物几何结构均为标准glTF模型。Foliage对于移动GPU而言具有难度,alpha几何体层可以在很短的时间内耗尽着色器预算。我们最初试图通过设置低目标几何体层数来避免这种情况,尝试从任何角度设计具有四层或更少几何体的模型。我们同时使用了masked alpha而不是blended alpha,以避免“聚合几何体”样式。
遗憾的是,这在实践中并没有真正奏效,对于特定模型而言依然太慢。我们后来创建的网格是完全不透明的。例如,玫瑰丛的多边形比其他许多花都高,但渲染速度更快,因为它是完全不透明。如果我们将所有植物设计为完全不透明的网格,Flowerbed的整体性能会更好,因为附加多边形的成本通常比blended overdraw更容易处理。
5.3 动物
除了植物,我们同时为Flowerbed增添其他生命。我们选择了几种不同的动物:天空中的鸟类和蝴蝶,池塘和河流中的鱼和鸭子,地面上的松鼠和兔子。
最初,所有动物都是用骨骼动画制作和动画化。遗憾的是,我们很快发现,这项技术并没有扩展到我们希望在场景中填充的动物数量。经过一番调查后,我们决定将部分动物移到morph target,因为这需要的CPU资源要比骨骼动画少得多。
海鸥、鱼、鸭子和蝴蝶都移动到morph target,每个都使用一个简单的动画方案,在一个基本姿势和两个单独的目标姿势之间移动。
兔子和松鼠需要保持骨骼网的状态,因此它们的数量比其他动物少得多。
因为morph target应用在GPU,CPU只需要更新几个标量值来告诉GPU每个目标有多少与基本姿势混合。每个morph target实例只需要更新它们的根变换和几个附加标量值,而不是每帧更新每个实例几十个矩阵。
除了降低了CPU成本外,morph target的移动有另一个优势:使用实例化在一次绘制调用中渲染一批动物变得更加容易。通过扩展网格实例化代码以渲染变形目标,我们可以更新作为实例缓冲区一部分的目标权重,并在单个绘制调用中渲染每种类型的动物。
5.4 相机系统
在Flowerbed中,我们的目标是创建一种身临其境的交互式WebXR体验,其中包括用于拍摄虚拟花园照片的相机功能。为了实现这一点,我们实现了三个关键功能:相机保持机制、相机屏幕和拍照机制。
相机握持机制允许用户用虚拟的手握持相机,就像握持真实的相机一样,而扳机键定位为虚拟快门释放按钮。这为用户提供了更身临其境的直观体验。我们使用WebXR API检测用户的控制器并追踪其在虚拟环境中的位置和方向,同时创建了一个相对于控制器定位和定向的虚拟相机对象。
相机屏幕提供用户将要拍摄的图片的预览,并帮助用户有效地框定图片。我们使用具有动态更新纹理的平面几何体作为屏幕网格。相机屏幕定位和定向为与虚拟相机匹配,场景的快照渲染为目标纹理。
当然,额外渲染整个场景的成本可能非常高昂。为了降低这一成本,我们以比主场景更低的分辨率(150 x 100像素)和固定的帧速率(每秒30帧)渲染相机实时视图。我们同时注意为场景重用静态阴影贴图,因此不需要进行额外的阴影更新。
相机的初始实现使用了多个画布,浏览器的.toDataUrl函数将画布数据复制到纹理中。遗憾的是,这存在多个缺点:每个画布都需要单独的情景(因此需要单独的渲染器,这需要单独编译我们使用的所有着色器),并且在画布之间移动纹理需要在GPU使用之前将纹理解析到CPU。我们最终从基于画布的纹理切换到预览相机的RenderTarget,允许同一渲染器渲染两个视图,并显著降低显示预览纹理的成本。我们依然为高分辨率照片使用单独的画布,但因为我们直接从数据data URL保存它,所以我们不必在等待复制时阻止VR渲染器。
最后,拍照机制是体验的一个关键组成,为用户提供了一种有趣的互动方式来捕捉虚拟花园的记忆。宝丽来风格的相机和打印照片为用户提供了直观的体验。相机打印照片的动画和声音效果增加了用户兴奋和期待,使拍摄照片的过程成为令人难忘的体验。打印的照片同样可交互,允许用户抓取照片并仔细查看,从而进一步增强了沉浸式体验。
总体而言,Flowerbed中的相机功能实现涉及WebXR、Web开发技术和创意设计的结合,从而为用户创造独特的沉浸式体验。
5.5 手部动画
我们的目标是通过在虚拟环境中实现双手来增强体验的沉浸感和互动性。为了基于有限的控制器输入创建自然和令人信服的手部动画,我们采用了定义手部状态并在它们之间进行插值的独特方法。
我们将手指分为三个不同的类别,并分别为每组定义状态,从而创建了一种分而治之的策略,简化了创作过程,降低了手部状态的复杂性。这种方法允许我们基于控制器输入在手部状态之间进行插值,从而创建更直观、更令人信服的手部动画。
为了为不同的交互模式创建overriding手势,我们仔细考虑了用户体验,并设计了特定于所持对象(如相机)的动画。这种增加的细节级别有助于为用户创造更具沉浸感和说服力的体验。
总体而言,Flowerbed基于控制器的手部实现涉及一种独特的方法来定义手部状态,并基于控制器输入在它们之间进行插值。它同时涉及一种创造性的方法:通过为不同的交互模式创建overriding手势来增强体验的沉浸感和交互。
5.6 NUX&设置
我们需要一个基于2D面板的UI来显示文本(以及视频和图像),以构建新的用户体验(NUX)和设置菜单。在非VR Three.js体验中,这一般是使用DOM来实现,通过将HTML元素放在Three.jss画布并在那里呈现UI。
遗憾的是,这在WebXR中不太可能,所以我们必须找到一种显示UI的替代方法。我们研究了将UI渲染为HTML元素,然后将其作为纹理复制到WebXR中的平面的可能性;我们同时考虑使用外部画布或图像作为平面的纹理。但由于VR中像素采样的工作方式,这两种方法都存在面板模糊的问题。
我们没有尝试将UI呈现为2D HTML或图像,而是选择将UI的所有部分都呈现为3D对象。为此,我们使用了three-mesh-ui来处理面板和文本渲染,它有一个强大的API来控制布局、将图像和视频插入UI等。
我们同时编写了一个导入器,从而允许我们用JSON定义UI面板,然后令它基于JSON配置自动创建three-mesh-ui对象。这使得我们能够在Project Flowerbed中创建和修改面板,并且预览它们,无需完全重建。这同时允许我们将所有UI定义保留在一个位置。JSON导入器可以在src/js/components/UIPanelComponent中找到,UI面板定义则可以在content/UI中找到。
6. 性能和优化
性能是XR体验的最大挑战之一。为了获得舒适的用户体验,XR应用程序需要以高帧率渲染世界的两个高分辨率视图。
6.1 目标帧速率
Meta Quest Browser支持updateTargetFramerate方法,因此开发人员可以更改其目标帧速率。帧速率一致性对用户舒适度的重要性与平均帧速率一样重要。许多应用程序无法以90 FPS或更高FPS持续运行,因此目标帧速率API允许开发人员将目标帧速调整到72 FPS,以确保一致性。Flowerbed的目标帧速率是72 FPS,这意味着我们只有不到14毫秒的时间来构建和渲染每一帧。
6.2 一般优化
在high level,为了在移动GPU WebXR应用程序实现出色的性能,你需要最大限度地减少每帧使用的WebGL调用次数,仔细管理着色器的复杂性,并最大限度地降低overdraw,避免任何需要大纹理解决的操作,如后处理效果。
要最小化WebGL调用,首先要做的是优化场景中的内容。确保尽可能合并网格,确保没有binding未使用的纹理或重复设置对场景质量没有明显影响的uniforms。确保尽可能使用API功能,如VAO、UBO、instanced drawing和multidraw。
默认情况下,Three.js为所有WebXR体验启用了fovation。这显著减少了需要渲染的像素数,并为任何着色器受限的应用程序提供了即时的性能提升。
6.3 multidraw
在Meta Quest硬件,OCULUS_multiview扩展提供给Web开发人员使用。它几乎可以将渲染场景所需的调用数减半,因此它可以是任何WebXR应用程序所能进行的最具影响力的优化之一。更多相关信息请访问这个页面。
Flowerbed使用了我们自己实现了multiview的Three.js分支。我们希望将其集成到主Three.js中,以便更广泛地使用,但同时我们鼓励开发人员查看我们的OCULUS_multiview pull request,将其融入到自己的体验之中。
6.4 矩阵更新
Flowerbed场景中包含大量的对象,而其中很多都是静止的。整个岛屿和花园以及任何目前没有浇水或生长的植物都静止不动。默认情况下,Three.js会在每一帧自动更新每个对象的所有矩阵,这是最安全的方法,因为它确保不会用过时的数据绘制任何元素。
因为我们的场景中有很多静态对象,所以这种自动更新会浪费大量时间。为了解决这个问题,我们关闭了Three的DefaultMatrixAutoUpdate,因此默认情况下不会更新任何内容。我们为我们知道需要更新每一帧的内容(例如用户相机)手动启用自动更新,然后手动更新偶尔更新的内容的矩阵,例如植物。我们在object3DUtils.js中添加了updateMatrixRecursively helper函数,而你可以搜索所述函数以查找手动更新矩阵及其子对象的位置。
6.5 实例化网格
我们同时使用实例化网格来渲染我们的众多场景,使用实例化在单个绘制调用中进行渲染。Three.js的实例化网格非常简单,它们不支持截头体剔除或LOD,很难与自定义材质一起使用。
我们使用了troika的three-instanced-uniforms-mesh ,以便于构建实例自定义材质。所述软件包帮助我们定义可自动使用实例化网格缓冲区的uniforms,使得我们能够轻松构建植物缩放系统和实例化morph target动画。
为了将截头体剔除添加到实例化网格中,我们将Three.js实例化网格扩展为所谓的ManagedInstancedMesh。这个类管理不同类型的实例列表,并可以更新和重新排序实例缓冲区。它为每个实例更新添加了一点间接性(使用它的代码不能再直接寻址实例矩阵),但它可以截头体剔除网格,从而提高添加和删除新实例的效率。
在ManagedInstanceMesh之上,我们为实例化网格实现了LOD。LODInstancedMesh类的工作方式与标准的Three.js LOD非常相似,但它为每个LOD创建一个新的ManagedInstancedMesh,并启用和禁用LOD所基于的每个LOD之间的实例。对于我们的用例来说,这并不是非常高性能。如果这样做,我们计划将量化添加到LOD更新中,并且只在玩家移动一定距离时进行更新,但这并不是必需项。
6.6 着色器性能
管理着色器性能的最佳方法是积极切割对场景视觉质量没有显著贡献的功能。这意味着避免任何浪费的工作。例如,即便场景中没有probe,Three.js的标准像素着色器正在计算light probe贡献。
我们同时根据每种asset类型的植物关闭了我们不需要的功能,地面设置为最大粗糙度和零mental,水下地面使用简化的照明而不是标准着色器,我们的天空使用基本材质。这种类型的优化是特定于应用程序,每个开发人员都应该查看所使用着色器的视觉贡献,并做出必要的判断。
6.7 性能工具
我们使用了各种工具来衡量性能并发现瓶颈。
我们几乎在开发Flowerbed的整个过程中都激活了OVR Metrics tool。叠加层提供了一个即时且信息丰富的HUD,显示了帧速率、GPU和CPU使用情况以及其他关键度量。这是一种在场景中移动并检查性能热点位置的简单方法。
一旦我们发现了一个热点或想要深入了解整体性能,我们就需要知道我们是在检查CPU还是GPU性能。
对于CPU性能, remote Chrome dev tools为我们提供了所需的所有信息,你可以看到应用程序在Javascript执行中花费的时间。
在GPU端,RenderDoc Meta Fork是了解GPU使用情况的最强大工具。RenderDoc可以记录WebGL命令的整个帧,并向你展示场景的渲染方式以及成本最高昂的绘制调用。
我们同时使用Spector.js快速评估WebGL的使用情况,通过将Spector.jss与 immersive web emulator相结合,你可以在桌面浏览器捕捉场景,并快速了解每个帧中的WebGL调用和使用情况。
7. 总结
Meta正式开源了整个Project Flowerbed代码库。项目官网请点击这个页面,而相关代码可以访问GitHub。