空间和参考空间:WebXR 中的空间跟踪
用于实现增强现实和虚拟现实的 WebXR API 专门设计用于提供将人插入虚拟环境的能力。为此,软件不仅需要能够跟踪虚拟世界中物体的位置、方向和运动,还需要能够跟踪用户的位置、方向和运动。但 WebXR 不仅如此,它还增加了跟踪输入设备的位置、方向和运动的能力,这些设备生成的数据用于确定观看者身体各个部分的姿态和运动(通过适当的设备)。
用户头戴设备的位置和运动代表了他们在虚拟环境中的头部姿态和方向。手部控制器以同样的方式代表他们的手。其他硬件元素也可以类似地用于代表身体的其他部分,在模拟用户在环境中的动作时提供额外的数据。
在本指南中,我们将探讨 WebXR 如何使用空间,更具体地说,是参考空间,来跟踪虚拟世界中物体和用户身体的位置、方向和运动。
注意:本文假定您熟悉WebXR 中的几何和参考空间中介绍的概念:即 3D 坐标系统的基础知识,以及 WebXR 空间、参考空间,以及如何使用参考空间为场景中的单个对象或可移动组件创建局部坐标系统。
使用参考空间表示位置
如使用参考空间定义空间关系中所述,参考空间建立一个局部坐标系,该坐标系从另一个本身由某个其他空间定义的坐标系偏移。因此,参考空间可用于定义一个点的位置和方向,并以此推及该点作为原点的整个对象。虽然这对于场景中的每个对象来说有点大材小用,但对于少数特定对象以这种方式拥有自己的坐标系来说非常有用。
- 世界空间;这个空间的原点是整个 3D 画布底层的 WebGL 坐标系统的原点。
- 玩家、头像或相机;这个空间的原点用作渲染要显示给用户的场景的相机位置。
- 手和/或手部控制器;每个都代表用户的一只手,可以是手本身或控制器(或两者)。原点通常是用户握拳手的中心。
- 目标射线;每个控制器或其他手持设备都可能有一个与之关联的瞄准射线,它由一个空间表示,该空间的原点位于控制器上发射射线的位置,并定向使得 -Z 沿其指向的目标方向延伸。
因为这些都以参考空间为基础进行定义,所以 WebXR Device API 可以轻松地在坐标系之间进行转换,执行影响这些空间及其相应对象的操作,等等。
描述相对于空间的位置
在两种情况下,您可能需要描述相对于空间的位置和/或方向。第一种情况如上文所述:将参考空间应用于偏移(反之亦然,因为结果相同)以确定表示该空间坐标系中结果位置的变换矩阵。
姿态
一旦为场景中的各种关键对象建立了参考空间,有时您需要描述相对于特定参考空间原点的另一个位置。这通过姿态来完成。简而言之,姿态描述了相对于创建它的参考空间原点的位置和方向。
在 WebXR 中,姿态由一个 XRPose 对象表示,其 transform 属性是一个 XRRigidTransform,定义了应用到原始空间中的任何坐标、向量或矩阵时,将其转换为目标空间的变换矩阵。因此,姿态不仅可以用于转换和确定位置,还可以用于旋转信息。
创建 XRPose 的唯一方法是使用动画帧上的 getPose() 方法,该方法通过 XRFrame 对象提供。这意味着最常见的情况是,您将在帧渲染代码中使用姿态,该代码作为 XRFrame 方法 requestAnimationFrame() 的回调执行。
getPose() 计算 XRReferenceSpace 相对于指定 XRSpace 原点的位置,然后创建表示结果位置和方向的姿态。
例如,如果您希望使用控制器的 gripSpace 绘制手部控制器的表示,您可以像这样获取所需的姿态:
let controlPose = frame.getPose(inputSource.gripSpace, worldRefSpace);
这会将输入手柄空间的位置和方向转换为使用世界坐标系,然后生成相应的 XRPose,并将其存储在 controlPose 中。然后,您可以将 controlPose 的 transform 应用于表示控制器的对象模型中的顶点,以计算在渲染控制器表示到帧缓冲区时要使用的 WebGL 坐标。
查看器姿态
一种特殊的姿态类型,查看器姿态,表示场景观看者的视角。查看器姿态由 WebXR 的 XRViewerPose 接口表示。渲染帧时,您将使用查看器姿态来确定观看者的位置和朝向方向,以便放置虚拟相机并渲染场景。
获取将位置信息从一个空间适应到另一个空间的姿态的唯一方法是通过您在调用 XRSession 方法 requestAnimationFrame() 时指定的帧渲染回调函数接收到的 XRFrame 对象。
例如,给定一个参考空间为 worldRefSpace 的 XRSession,以下代码行将请求调度第一个动画帧:
animationFrameRequestID = xrSession.requestAnimationFrame(myDrawFrame);
然后,myDrawFrame() 函数——在需要绘制帧时执行的回调——可能类似于这样:
function myDrawFrame(currentFrameTime, frame) {
let session = frame.session;
let viewerPose = frame.getViewerPose(viewerRefSpace);
animationFrameRequestID = session.requestAnimationFrame(myDrawFrame);
if (viewerPose) {
// render the frame
}
}
frame 参数是表示 WebXR 提供的动画帧信息的 XRFrame。调用时,此函数首先从帧对象获取 XRSession,然后使用帧的 getViewerPose() 方法计算给定 viewerRefSpace 的 XRViewerPose,该参数描述了查看器的当前朝向方向和位置。
返回的查看器姿态 viewerPose 反过来可用于计算适当渲染场景中对象的位置和方向,具体取决于用户的视角。
偏移或移动参考空间
虽然您无法更改参考空间,因为 XRReferenceSpace 和 XRBoundedReferenceSpace 都是只读的,但您可以通过对它们应用偏移变换来轻松创建新的参考空间。这通过调用参考空间的 getOffsetReferenceSpace() 方法来完成。
在参考空间内偏移位置
使用 getOffsetReferenceSpace() 最简单的情况是在同一空间的上下文中转换点或矩阵。例如,要创建一个新的参考空间,将参考空间 aRefSpace 在每个方向上移动半米,您可以这样做:
let halfMeterTransform = new XRRigidTransform({
x: 0.5,
y: 0.5,
z: 0.5,
w: 1.0,
});
aRefSpace = aRefSpace.getOffsetReferenceSpace(halfMeterTransform);
这会将现有参考空间 aRefSpace 替换为一个其坐标和方向已应用变换 halfMeterTransform 的参考空间。由于变换中不包含方向数据,因此 aRefSpace 的方向不受影响。
在 WebXR 会话类型之间转换
您可能需要将位置信息从一个参考空间转换到另一个参考空间的另一个常见原因是,当需要将会话类型从 inline 更改为 immersive-vr 或反向更改时。这通常发生在您的用户界面提供了一种在网页上下文中预览场景的方式,并带有按钮或其他控件来切换到沉浸模式。
由于大多数用户希望在执行此转换时保持相同的查看器位置和朝向方向,因此您可以使用 XRFrame 方法 getViewerPose() 来获取当前的 XRViewerPose,切换会话,然后使用保存的查看器姿态来恢复查看器的位置和朝向。
let viewerPose = frame.getViewerPose(worldReferenceSpace);
let newSession = navigator.xr.requestSession("immersive-vr", {
requiredFeatures: "unbounded",
});
worldReferenceSpace = await newSession.requestReferenceSpace("unbounded");
viewerPose = worldReferenceSpace.getOffsetReferenceSpace(viewerPose.transform);
在这里,获取查看器姿态,其变换相对于当前会话的全局参考空间 worldReferenceSpace 定义。然后建立一个新的会话并创建一个参考空间作为新的世界参考空间。
最后,通过调用新参考系统的 getOffsetReferenceSpace() 方法,将保存的 viewerPose 转换为新世界空间的坐标系。有了这个,我们可以像往常一样恢复场景渲染,查看器的视角不受影响。
在有界空间和无界空间之间转换
有时当您的主要体验使用无界空间时,您可能需要暂时将用户的体验过渡到有界空间。例如,通过切换到代表单个房间的有界空间,可能更容易实现在房屋单个房间中与对象的交互。这可能使得更容易实现定制墙壁、在地板上放置家具等功能。
在这种情况下,当您需要开始使用与您一直使用的不同的参考空间进行跟踪时,您可以结合使用 getViewerPose() 和一些矩阵计算来将场景中的所有内容都基于新的参考帧原点。
由于 getViewerPose() 仅在 XRFrame 中可用,您需要在渲染回调中开始此过程,可能使用 worker 进行计算以减少掉帧。
let previousViewerPose = null;
function myDrawFrame(currentFrameTime, frame) {
let session = frame.session;
let viewerPose = frame.getViewerPose(viewerRefSpace);
animationFrameRequestID = session.requestAnimationFrame(myDrawFrame);
if (viewerPose) {
previousViewerPose ??= viewerPose;
let offsetMatrix = mat4.create();
mat4.sub(
offsetMatrix,
previousViewerPose.transform.matrix,
viewerPose.transform.matrix,
);
previousViewerPose = viewerPose;
}
}
跟踪丢失后的连续性和恢复
有时,当用户正在积极地将他们的 XR 硬件与您的应用程序一起使用时,包含用户位置和方向更新的数据流可能会丢失一段时间。您的应用程序不仅需要确定在此期间向用户显示什么,还需要在跟踪恢复时干净地恢复。
一旦 XR 硬件开始提供跟踪信息,它将继续这样做直到 XR 会话关闭。此数据在每帧中通过调用 XRFrame 方法 getViewerPose() 来获取查看器的位置和朝向方向(定义用户应该看到的内容),以及 getPose() 来获取任何其他姿态,例如手部控制器和 XR 系统的任何其他部分的位置。
检测和跟踪丢失后的功能
如果跟踪失败,例如由于头戴设备和用户设备之间的连接暂时丢失,XR 层将继续返回姿态,但这些姿态的 emulatedPosition 属性将为 true,表明姿态的计算是基于对用户当前位置的猜测。
一些 XR 硬件使用算法根据当前正在进行的运动计算用户的估计位置,而另一些硬件则报告根本没有运动,但 emulatedPosition 设置为 true。在这两种情况下,您可能希望根据您的具体需求调整渲染以弥补损失。
当跟踪恢复时
当用户位置跳跃,同时 emulatedPosition 的值从 true 变为 false 时,您可以检测到跟踪在丢失后已恢复。如何处理取决于您的应用程序。如果您的应用程序提供了一种让用户在不实际移动的情况下在虚拟世界中移动的方式(即所谓的瞬移机制),您可以接受新位置并继续,允许从您先前假设的位置跳跃立即通过新位置进行校正。
另一方面,如果您的应用程序涉及用户在真实空间中物理移动以在您的虚拟世界中移动,则采用新的跟踪位置并跳跃到那里可能会让用户感到不安,如果可能应避免这种情况。相反,使用当前位置和新跟踪位置之间的差异来计算新的瞬移偏移;也就是说,应用于所有内容的变换,以将 WebXR 的位置和方向数据适应您的需求。
您可以通过创建一个新的参考空间来做到这一点,该空间将查看器位置自上一帧以来跳跃的距离纳入其有效原点,使用 XRReferenceSpace 方法 getOffsetReferenceSpace()。
重置事件
当参考空间的本地或有效原点发生不连续或中断时,用户代理将向 XRReferenceSpace 发送一个 reset 事件。此事件表明原点位置相对于用户环境发生了重大变化。
reset 可能发生是因为 XR 硬件连接丢失了一段时间,导致用户的移动一段时间未被正确跟踪。在跟踪恢复后,reset 意味着跟踪已恢复,新的位置信息代表了 XR 硬件提供的实际位置信息,而不是缓存或“最佳猜测”数据。
发送 reset 的另一个原因是用户已退出参考空间的边界并进入另一个参考空间,或者用户已通过编程从一个参考空间过渡到另一个参考空间。每当用户空间边界几何形状发生变化时,都会发送 reset。
reset 仅用于重大的跳跃或过渡;小问题将自动吸收。该事件总是发送到每个受影响的参考空间,包括使用 getOffsetReferenceSpace() 创建的那些,因此如果您监听 reset 事件,您需要确保保留对您仍在使用的任何空间的强引用。