启动和关闭 WebXR 会话
假设您已经熟悉 3D 图形(特别是 WebGL),那么迈向混合现实的下一步——除了或代替现实世界呈现人造场景或物体——并不复杂。在开始渲染增强或虚拟现实场景之前,您需要创建和设置 WebXR 会话,并且您也应该了解如何正确关闭它。您将在本文中学习如何执行这些操作。
访问 WebXR API
WebXR 可用性
作为一项新的仍在开发中的 API,WebXR 支持仅限于特定的设备和浏览器;即使在这些设备和浏览器上,它也可能默认未启用。但是,您可能可以使用一些选项来试验 WebXR,即使您没有兼容的系统。
WebXR polyfill
设计 WebXR 规范的团队发布了一个WebXR polyfill,您可以使用它来模拟不支持 WebXR API 的浏览器的 WebXR。如果浏览器支持旧的WebVR API,则使用该 API。否则,polyfill 将回退到使用 Google 的 Cardboard VR API 的实现。
polyfill 与规范一起维护,并保持与规范的最新状态。此外,它还会更新以维护与浏览器的兼容性,因为它们对 WebXR 和与其相关的其他技术的支持以及 polyfill 的实现会随着时间的推移而发生变化。
请务必仔细阅读自述文件;polyfill 有多个版本,具体取决于目标浏览器包含的 JavaScript 新功能的兼容性程度。
模拟器用法
虽然与使用实际头显相比有点笨拙,但这使得可以在桌面电脑上试验和开发 WebXR 代码成为可能,在桌面电脑上 WebXR 通常不可用。它还允许您在将代码带到真实设备之前执行一些基本测试。但是,请注意,模拟器尚未完全模拟所有 WebXR API,因此您可能会遇到意想不到的问题。同样,在开始之前,请仔细阅读自述文件并确保您了解这些限制。
重要提示:在发布或交付产品之前,您始终应该在实际的 AR 和/或 VR 硬件上测试您的代码!模拟、模拟或 polyfill 环境不是实际在物理设备上进行测试的充分替代。
获取扩展
在下面下载您支持的浏览器的 WebXR API 模拟器
扩展程序的源代码也已在 GitHub 上提供。
模拟器问题和说明
虽然这不是撰写关于扩展程序的完整文章的地方,但有一些值得一提的具体事项。
扩展程序的 0.4.0 版本于 2020 年 3 月 26 日发布。它通过WebXR AR 模块引入了对增强现实 (AR) 的支持,该模块即将进入稳定状态。AR 的文档很快就会在 MDN 上发布。
其他改进包括更新模拟器以将XR
接口重命名为XRSystem
,引入对挤压(握持)输入源的支持,以及添加对XRInputSource
属性profiles
的支持。
上下文要求
WebXR 兼容环境始于安全加载的文档。您的文档需要从本地驱动器加载(例如,使用https://127.0.0.1/…
之类的 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()
,为要渲染帧的画布获取 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, "worlddata.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
事件的处理程序。然后获取渲染画布并检索其 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会话后干净地关闭它,您应该调用会话的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()
函数)时调用它,以及在会话自动结束时调用它,无论是因为错误还是其他原因。