启动和关闭 WebXR 会话
假设你已经熟悉 3D 图形(尤其是 WebGL),那么迈出混合现实的下一步(即在现实世界中或取代现实世界呈现人造场景或物体)并不太复杂。在开始渲染增强或虚拟现实场景之前,你需要创建并设置 WebXR 会话,并且也应该知道如何正确关闭它。本文将教你如何完成这些操作。
访问 WebXR API
你的应用访问 WebXR API 是从 XRSystem 对象开始的。此对象代表通过用户设备上的硬件和驱动程序可用的整个 WebXR 设备套件。你的文档可以通过 Navigator 属性 xr 使用全局 XRSystem 对象,如果根据可用硬件和文档环境有合适的 XR 硬件可用,该属性将返回 XRSystem 对象。
因此,获取 XRSystem 对象的最简单代码是
const xr = navigator.xr;
如果 WebXR 不可用,xr 的值将为 null 或 undefined。
WebXR 可用性
作为一种新的且仍在开发中的 API,WebXR 的支持仅限于特定的设备和浏览器;即使在这些设备和浏览器上,它也可能默认未启用。但是,即使你没有兼容的系统,也可能有可用的选项允许你试验 WebXR。
WebXR Polyfill
WebXR 规范的设计团队发布了一个 WebXR polyfill,你可以用它来在不支持 WebXR API 的浏览器上模拟 WebXR。如果浏览器支持旧的 WebVR API,则使用它。否则,polyfill 会回退到使用 Google Cardboard VR API 的实现。
polyfill 与规范同步维护,并与规范保持同步。此外,它还会更新以保持与浏览器的兼容性,因为它们对 WebXR 以及与 WebXR 和 polyfill 实现相关的其他技术的支持会随着时间的推移而改变。
请务必仔细阅读自述文件;polyfill 有几个版本,具体取决于你的目标浏览器包含新 JavaScript 功能的程度。
模拟器使用
虽然与使用实际头戴设备相比有些笨拙,但这使得在桌面计算机上试验和开发 WebXR 代码成为可能,而 WebXR 通常在桌面计算机上不可用。它还允许你在将代码部署到真实设备之前执行一些基本测试。但请注意,模拟器尚未完全模拟所有 WebXR API,因此你可能会遇到意想不到的问题。同样,在开始之前请仔细阅读自述文件,并确保你了解其局限性。
重要提示:在发布或交付产品之前,你始终应该在实际的 AR 和/或 VR 硬件上测试你的代码!模拟、仿真或 polyfill 环境不能充分替代在物理设备上的实际测试。
获取扩展
请从下方下载你的受支持浏览器的 WebXR API 模拟器
此 扩展的源代码 也可在 GitHub 上获取。
模拟器问题和注意事项
虽然这不是一篇关于该扩展的完整文章,但有一些具体事项值得一提。
该扩展的 0.4.0 版本于 2020 年 3 月 26 日发布。它通过 WebXR AR Module 引入了对增强现实 (AR) 的支持,该模块正接近稳定状态。AR 的文档很快将在 MDN 上发布。
其他改进包括更新模拟器以将 XR 接口重命名为 XRSystem,引入对挤压(握持)输入源的支持,并添加对 XRInputSource 属性 profiles 的支持。
上下文要求
WebXR 兼容环境始于安全加载的文档。你的文档需要从本地驱动器加载(例如使用 https:///… 等 URL),或者在加载页面时使用 HTTPS。JavaScript 代码也必须安全加载。
如果文档未安全加载,你将无法走得很远。navigator.xr 属性甚至在文档未安全加载时也不存在。如果不存在兼容的 XR 硬件,也可能是这种情况。无论哪种情况,你都需要为缺少 xr 属性做好准备,并优雅地处理错误或提供某种形式的备用方案。
回退到 WebXR polyfill
一种备用选项是 WebXR polyfill,由负责 WebXR 标准化过程的 沉浸式 Web 工作组 提供。polyfill 为没有原生 WebXR 支持的浏览器带来了 WebXR 支持,并解决了支持 WebXR 的浏览器之间实现不一致的问题,因此即使原生 WebXR 可用,它有时也可能有用。
在这里,我们定义了一个 getXR() 函数,它在可选安装 polyfill 后返回 XRSystem 对象,假设 polyfill 已通过之前的 <script> 标签包含或加载。
let webxrPolyfill = null;
function getXR(usePolyfill) {
let tempXR;
switch (usePolyfill) {
case "if-needed":
tempXR = navigator.xr;
if (!tempXR) {
webxrPolyfill = new WebXRPolyfill();
tempXR = webxrPolyfill;
}
break;
case "yes":
webxrPolyfill = new WebXRPolyfill();
tempXR = webxrPolyfill;
break;
case "no":
default:
tempXR = navigator.xr;
break;
}
return tempXR;
}
const nativeXr = getXR("no"); // Get the native XRSystem object
const polyfilledXr = getXR("yes"); // Always returns an XRSystem from the polyfill
const xr = getXR("if-needed"); // Use the polyfill only if navigator.xr missing
然后可以根据 MDN 上提供的文档使用返回的 XRSystem 对象。全局变量 webxrPolyfill 仅用于保留对 polyfill 的引用,以确保它在不再需要之前保持可用。将其设置为 null 表示当不再有依赖它的对象使用它时,可以对其进行垃圾回收。
当然,你可以根据需要简化它;由于你的应用程序可能不会在是否使用 polyfill 之间频繁切换,你可以将其简化为只针对你需要的特定情况。
权限和安全
WebXR 围绕着许多安全措施。其中最重要的一点是,使用 immersive-vr 模式(它完全取代了用户对世界的看法)要求 xr-spatial-tracking 权限策略 到位。除此之外,文档需要是安全的并且当前处于焦点状态。最后,你必须从用户事件处理程序(例如 click 事件的处理程序)中调用 requestSession()。
有关保护 WebXR 活动和使用的更多具体信息,请参阅文章 WebXR 的权限和安全。
确认所需的会话类型可用
在尝试创建新的 WebXR 会话之前,通常明智的做法是首先检查用户的硬件和软件是否支持你希望使用的演示模式。例如,这也可以用于确定是使用沉浸式还是内联演示。
要查明是否支持给定模式,请调用 XRSystem 方法 isSessionSupported()。它返回一个 promise,如果给定类型的会话可用,则解析为 true,否则解析为 false。
const immersiveOK = await navigator.xr.isSessionSupported("immersive-vr");
if (immersiveOK) {
// Create and use an immersive VR session
} else {
// Create an inline session instead, or tell the user about the
// incompatibility if inline is required
}
创建并启动会话
WebXR 会话由一个 XRSession 对象表示。要获取 XRSession,你可以调用你的 XRSystem 的 requestSession() 方法,该方法返回一个 promise,如果能够成功建立会话,则解析为 XRSession。从根本上说,它看起来像这样
xr.requestSession("immersive-vr").then((session) => {
xrSession = session;
/* continue to set up the session */
});
请注意此代码片段中传递给 requestSession() 的参数:immersive-vr。此字符串指定了你想要建立的 WebXR 会话类型——在本例中,是完全沉浸式的虚拟现实体验。有三个选项
immersive-vr-
使用头戴设备或类似设备进行完全沉浸式虚拟现实会话,该设备将用户周围的世界完全替换为你呈现的图像。
immersive-ar-
一种增强现实会话,其中使用头戴设备或类似设备将图像添加到现实世界中。此选项尚未广泛支持,因为 AR 规范仍在变化。
inline-
在文档窗口的上下文中显示 XR 图像。
如果由于某种原因无法创建会话(例如功能策略不允许使用或用户拒绝授予使用头戴设备的权限),则 promise 会被拒绝。因此,一个更完整的启动并返回 WebXR 会话的函数可能如下所示
async function createImmersiveSession(xr) {
session = await xr.requestSession("immersive-vr");
return session;
}
此函数返回新的 XRSession,或者在创建会话时发生错误时抛出异常。
自定义会话
除了显示模式,requestSession() 方法还可以接受一个可选的带有初始化参数的对象,用于自定义会话。目前,会话唯一可配置的方面是应该使用哪个参考空间来表示世界坐标系。你可以指定所需或可选的参考空间,以获取与你所需或偏好使用的参考空间兼容的会话。
例如,如果你需要一个 unbounded 参考空间,你可以将其指定为必需功能,以确保你获得的会话可以使用无界空间
async function createImmersiveSession(xr) {
session = await xr.requestSession("immersive-vr", {
requiredFeatures: ["unbounded"],
});
return session;
}
另一方面,如果你需要一个内联会话并且更喜欢 local 参考空间,你可以这样做
async function createInlineSession(xr) {
session = await xr.requestSession("inline", {
optionalFeatures: ["local"],
});
return session;
}
此 createInlineSession() 函数将尝试创建一个与 local 参考空间兼容的内联会话。当你准备创建参考空间时,可以尝试局部空间,如果失败,则回退到 viewer 参考空间,所有设备都要求支持该空间。
准备新会话以供使用
一旦 requestSession() 方法返回的 promise 成功解析,你就知道你手上有一个可用的 WebXR 会话。然后你可以继续准备会话以供使用并开始你的动画。
为了完成会话配置,你需要(或可能需要)执行的关键事项包括
- 添加你需要监视的事件处理程序。这最可能至少包括
end,以便你可以检测会话何时结束。 - 如果你使用 XR 输入控制器,请监视
inputsourceschange事件以检测 XR 输入控制器的添加或移除,以及各种 选择和挤压动作事件。 - 你可能希望监视
XRSystem事件devicechange,以便在可用沉浸式设备集发生变化时收到通知。 - 通过在目标上下文上调用
HTMLCanvasElement方法getContext()来获取你打算渲染帧的 canvas 的 WebGL 上下文。 - 设置你的 WebGL 数据和模型,并准备渲染场景。
- 通过创建
XRWebGLLayer并设置会话renderState属性baseLayer的值,将 WebGL 上下文设置为 XR 系统的源。 - 根据需要计算对象的初始位置和比例。
- 开始帧渲染周期。
以基本形式,完成此最终设置的代码可能如下所示
async function runSession(session) {
session.addEventListener("end", onSessionEnd);
const canvas = document.querySelector("canvas");
const gl = canvas.getContext("webgl", { xrCompatible: true });
// Set up WebGL data and such
const worldData = loadGLPrograms(session, "world-data.xml");
if (!worldData) {
return null;
}
// Finish configuring WebGL
worldData.session.updateRenderState({
baseLayer: new XRWebGLLayer(worldData.session, gl),
});
// Start rendering the scene
referenceSpace = await worldData.session.requestReferenceSpace("unbounded");
worldData.referenceSpace = referenceSpace.getOffsetReferenceSpace(
new XRRigidTransform(
worldData.playerSpawnPosition,
worldData.playerSpawnOrientation,
),
);
worldData.animationFrameRequestID =
worldData.session.requestAnimationFrame(onDrawFrame);
return worldData;
}
为了此示例的目的,创建了一个名为 worldData 的对象来封装关于世界和渲染环境的数据。这包括 XRSession 本身、用于在 WebGL 中渲染场景的所有数据、世界参考空间以及由 requestAnimationFrame() 返回的 ID。
首先,设置 end 事件的处理程序。然后获取渲染 canvas 并检索对其 WebGL 上下文的引用,在调用 getContext() 时指定 xrCompatible 选项。
接下来,在执行 WebGL 渲染器所需的任何数据和设置之后,然后将 WebGL 配置为使用 WebGL 上下文的帧缓冲区作为其自身的帧缓冲区。这是通过使用 XRSession 方法 updateRenderState() 将渲染状态的 baseLayer 设置为新创建的封装 WebGL 上下文的 XRWebGLLayer 来完成的。
准备渲染场景
此时,XRSession 本身已完全配置,因此我们可以开始渲染。首先,我们需要一个参考空间,在该空间中声明世界的坐标。我们可以通过调用 XRSession 的 requestReferenceSpace() 方法来获取会话的初始参考空间。在调用 requestReferenceSpace() 时,我们指定我们想要的参考空间的类型名称;在本例中是 unbounded。你可以根据需要轻松指定 local 或 viewer。
注意:要了解如何根据你的需求选择正确的参考空间,请参阅 选择参考空间类型。
requestReferenceSpace() 返回的参考空间将原点 (0, 0, 0) 放置在空间的中心。这很好——如果你的玩家的视角从世界的正中心开始。但很可能并非如此。如果是这样,你可以在初始参考空间上调用 getOffsetReferenceSpace() 来创建一个新的参考空间,该空间抵消了坐标系,以便 (0, 0, 0) 位于观看者的位置,方向也相应地移动以面向所需方向。getOffsetReferenceSpace() 的输入值是一个 XRRigidTransform,它封装了玩家在默认世界坐标中指定的位置和方向。
手头有新的参考空间并存储在 worldData 对象中以供安全保存后,我们调用会话的 requestAnimationFrame() 方法来安排一个回调,该回调将在 WebXR 会话的下一帧动画渲染时执行。返回的值是一个 ID,我们可以稍后使用它来取消请求(如果需要),所以我们也将其保存到 worldData 中。
最后,worldData 对象返回给调用者,允许主代码稍后引用它需要的数据。此时,设置过程完成,我们已进入应用程序的渲染阶段。要了解有关渲染的更多信息,请参阅文章 渲染和 WebXR 帧动画回调。
关于操作细节
显然,这只是一个例子。你不需要一个 worldData 对象来存储所有内容;你可以以任何你想要的方式维护你需要的信息。你可能需要不同的信息,或者有不同的特定要求,导致你以不同的方式或以不同的顺序做事。
同样,你用于加载模型和其他信息以及设置 WebGL 数据(纹理、顶点缓冲区、着色器等)的具体方法将根据你的需求、你正在使用的任何框架等而有很大差异。
重要的会话维护事件
在 WebXR 会话期间,你可能会收到多个事件,这些事件指示会话状态的变化,或者让你了解为了使会话正常运行你需要做的事情。
检测会话可见性状态的变化
当 XRSession 的可见性状态发生变化时——例如会话被隐藏或显示,或者用户已将焦点切换到另一个上下文——会话会收到一个 visibilitychange 事件。
session.onvisibilitychange = (event) => {
switch (event.session.visibilityState) {
case "hidden":
myFrameRate = 10;
break;
case "blurred-visible":
myFrameRate = 30;
break;
case "visible":
default:
myFrameRate = 60;
break;
}
};
此示例根据可见性状态的变化来更改变量 myFrameRate。渲染器大概使用此值来计算动画循环进行时渲染新帧的频率,从而使场景越“模糊”,渲染频率越低。
检测参考空间重置
在追踪用户在世界中的位置时,有时可能会出现 原生原点 的不连续或跳跃。最常见的情况是用户请求重新校准其 XR 设备,或者从 XR 硬件接收的追踪数据流中出现中断或故障。这些情况会导致原生原点突然跳跃,跳跃距离和方向角是使原生原点与用户位置和朝向方向重新对齐所需的。
当发生这种情况时,一个 reset 事件会发送到会话的 XRReferenceSpace。事件的 transform 属性是一个 XRRigidTransform,详细说明了重新对齐原生原点所需的变换。
注意:reset 事件是在 XRReferenceSpace 而不是 XRSession 上触发的!
reset 事件的另一个常见原因是当边界参考空间(bounded-floor)的几何形状(由 XRBoundedReferenceSpace 的属性 boundsGeometry 指定)发生变化时。
有关参考空间重置的更多常见原因以及更多详细信息和示例代码,请参阅 reset 事件的文档。
检测可用 WebXR 输入控件集何时更改
WebXR 维护一个特定于 WebXR 系统的输入控件列表。这些设备包括手持控制器、运动感应摄像头、运动感应手套和其他反馈设备。当用户连接或断开 WebXR 控制器设备时,inputsourceschange 事件会分派给 XRSession。这是一个机会,可以通知用户设备的可用性,开始监视它以获取输入,提供配置选项,或者你可能需要对其执行的任何操作。
结束 WebXR 会话
当用户的 VR 或 AR 会话接近尾声时,会话结束。XRSession 的关闭可能由于会话本身决定是时候关闭(例如用户关闭其 XR 设备),或者用户点击按钮结束会话,或者根据你的应用程序的需要发生其他情况。
这里我们讨论如何请求关闭 WebXR 会话以及如何检测会话何时结束,无论是通过你的请求还是其他方式。
关闭会话
为了在你完成 WebXR 会话后 cleanly 关闭它,你应该调用会话的 end() 方法。这会返回一个 promise,你可以用它来知道关闭何时完成。
async function shutdownXR(session) {
if (session) {
await session.end();
/* At this point, WebXR is fully shut down */
}
}
当 shutdownXR() 返回给其调用者时,WebXR 会话已完全安全关闭。
如果会话结束时必须完成工作,例如释放资源等,你应该在你的 end 事件处理程序中完成该工作,而不是在主代码体中。这样,无论关闭是自动触发还是手动触发,你都将处理清理。
检测会话何时结束
如前所述,你可以通过监视 XRSession 接收到的 end 事件来检测 WebXR 会话何时结束——无论是你调用了其 end() 方法,用户关闭了他们的头戴设备,还是 XR 系统中发生了某种无法解决的错误。
session.onend = (event) => {
/* the session has shut down */
freeResources();
};
在这里,当会话结束并收到 end 事件时,会调用 freeResources() 函数来释放之前为处理 XR 展示而分配和/或加载的资源。通过在 end 事件处理程序中调用 freeResources(),我们可以在用户点击触发关闭的按钮(例如通过调用上面显示的 shutdownXR() 函数)时以及会话自动结束(无论是由于错误还是其他原因)时调用它。