空间和参考空间:WebXR 中的空间跟踪
用于实现增强现实和虚拟现实的 WebXR API 是专门为了提供将人类插入虚拟环境的能力而设计的。为了实现这一点,软件不仅需要能够跟踪虚拟世界中物体的方位、方向和移动,还需要跟踪用户的方位、方向和移动。但 WebXR 不止于此,它还增加了跟踪输入设备的位置、方向和运动的能力,这些设备生成用于确定观察者身体各个部分的位置和移动的数据(使用适当的设备)。
用户头显的位置和移动代表着他们的头部在虚拟环境中的位置和方向。手柄以同样的方式代表着他们的手。其他硬件元素可以以类似的方式用来代表身体的其他部位,提供额外的用于在环境中模拟用户行动的数据。
在本指南中,我们将探讨 WebXR 如何使用 **空间**,更具体地说,使用 **参考空间** 来跟踪物体和用户身体在虚拟世界中的位置、方向和移动。
**注意:** 本文假定您熟悉 WebXR 中的几何和参考空间 中介绍的概念:即 3D 坐标系的原理,以及 WebXR 空间、参考空间,以及如何使用参考空间为场景中的单个物体或可移动组件创建局部坐标系。
使用参考空间表示位置
正如在 使用参考空间定义空间关系 中所述,参考空间建立了一个局部坐标系,该坐标系相对于另一个坐标系偏移,而另一个坐标系本身是由某个其他空间定义的。因此,可以使用参考空间来定义一个点的方位和方向,并通过扩展来定义该点为原点的整个物体。虽然这对于场景中的每一个物体来说都过于繁琐,但对于一些特定物体来说,以这种方式拥有自己的坐标系非常有用。
- **世界空间**;该空间的原点是整个 3D 画布的 WebGL 坐标系 的原点。
- 玩家、化身或相机;该空间的原点用作渲染将显示给用户的场景的相机位置。
- 手或手柄;它们分别代表着用户的两只手,可以是手本身,也可以是手柄(或两者兼而有之)。原点通常是用户握紧的拳头中心的。
- **目标光线**;每个手柄或其他手持设备可能与之关联一个目标光线,该光线由一个空间表示,该空间的原点位于发射光线的控制器上的点,并且方向使 -Z 沿其所指的目标方向延伸。
由于它们中的每一个都是使用参考空间作为基础来定义的,因此 WebXR 设备 API 可以很容易地用于在坐标系之间进行转换,执行影响这些空间及其对应物体的操作,等等。
描述相对于空间的位置
您可能需要描述相对于空间的方位和/或方向的两种情况。第一种情况是在 上面描述的:将参考空间应用于偏移量(或反之,因为结果相同)以确定表示空间坐标系中结果位置的变换矩阵。
姿态
一旦您为场景中的各种关键物体建立了参考空间,您就会在需要描述另一个相对于特定参考空间的原点的方位时,使用姿态。姿态描述了相对于其创建的参考空间的原点的方位和方向。
在 WebXR 中,姿态由一个 XRPose
对象表示,其 transform
属性是 XRRigidTransform
,它定义了一个变换矩阵,该矩阵应用于原始空间中的任何坐标、向量或矩阵,将其转换为目标空间。因此,姿态不仅可以用于转换和确定方位,还可以用于旋转信息。
只有一种方法可以创建 XRPose
,那就是使用动画帧上提供的 XRFrame
对象上的 getPose()
方法。这意味着您最常在帧渲染代码中使用姿态,该代码作为 XRFrame
方法 requestAnimationFrame()
的回调执行。
getPose()
计算 XRReferenceSpace
相对于指定 XRSpace
的原点的位置,然后创建一个表示结果位置和方向的姿态。
例如,如果您希望使用控制器的 gripSpace
绘制手柄的表示,您可以按照以下步骤获取所需的姿态
let controlPose = frame.getPose(inputSource.gripSpace, worldRefSpace);
这将输入的握持空间的位置和方向转换为使用世界的坐标系,然后生成相应的 XRPose
,并将其存储在 controlPose
中。然后,您可以将 controlPose
的 transform
应用于代表控制器的对象模型中的顶点,以计算在将控制器的表示渲染到帧缓冲区时使用的 WebGL 坐标。
查看器姿态
作为一种特殊的姿态类型,**查看器姿态** 代表着场景查看者的视角。查看器姿态由 WebXR 的 XRViewerPose
接口表示。在渲染帧时,您将使用查看器姿态来确定查看者的位置和面向方向,以便 放置虚拟相机 并 渲染场景。
获得将位置信息从一个空间适应到另一个空间的姿态的唯一方法是通过 XRFrame
对象,该对象由您在调用 XRSession
方法 requestAnimationFrame()
时指定的帧渲染回调函数接收。
例如,给定一个参考空间为 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
参数是 XRFrame
,它代表着 WebXR 提供的动画帧信息。当被调用时,该函数首先从帧对象获取 XRSession
,然后使用帧的 getViewerPose()
方法计算 XRViewerPose
,用于查看者,给定 viewerRefSpace
,它描述了查看者当前的面向方向和位置。
返回的查看器姿态 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);
这将使用其坐标和方向应用了变换 halfMeterTransform
的参考空间替换现有的参考空间 aRefSpace
。由于变换中不包含任何方向数据,因此 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
中可用,因此您需要在渲染回调中开始此过程,可能使用工作程序来执行计算以减少帧丢失。
let previousViewerPose = null;
function myDrawFrame(currentFrameTime, frame) {
let session = frame.session;
let viewerPose = frame.getViewerPose(viewerRefSpace);
animationFrameRequestID = session.requestAnimationFrame(myDrawFrame);
if (viewerPose) {
if (!previousViewerPose) {
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
事件,则需要确保您保留对仍在使用的任何空间的强引用。