虚拟现实登陆网络(第二部分)

帧循环详解

Joe Medley
Joe Medley

最近,我发表了一篇名为“Virtual reality comes to the web”的文章,介绍了 WebXR Device API 背后的基本 概念。我还提供了有关如何请求、输入和结束 XR 会话的说明。

本文介绍了帧循环,这是一个用户代理控制的无限循环,其中内容会重复绘制到屏幕上。内容以称为帧的离散块绘制。帧的连续性会产生运动的错觉。

本文不涉及的内容

在 WebXR 应用的帧循环期间,WebGL 和 WebGL2 是呈现内容的唯一方式。幸运的是,许多框架都在 WebGL 和 WebGL2 的基础上提供了一层抽象。此类框架包括 three.jsbabylonjsPlayCanvas,而 A-FrameReact 360 则是专为与 WebXR 互动而设计的。

本文将使用 Immersive Web Working Group's Immersive VR Session 示例 (演示, 源代码)。 如果您想深入了解 WebGL 或其中一个框架,可以在网上找到越来越多的资源。

参与者和游戏

在尝试了解帧循环时,我一直迷失在细节中。 其中涉及许多对象,有些对象仅通过其他对象的引用属性命名。为了帮助您理清思路,我将介绍这些对象,我称之为“参与者”。然后,我将介绍它们如何互动,我称之为“游戏”。

参与者

XRViewerPose

姿态是指某事物在 3D 空间中的位置和方向。观看者和输入设备都有姿态,但我们这里关注的是观看者的姿态。观看者和输入设备姿态都有一个 transform 属性,用于将位置描述为相对于原点的向量,并将方向描述为相对于原点的四元数。原点是根据调用 XRSession.requestReferenceSpace() 时请求的参考空间类型指定的。

参考空间需要一些时间来解释。我在增强 现实中对此进行了深入介绍。我用作本文基础的 文章使用 'local' 参考空间,这意味着原点位于 会话创建时观看者的位置,没有明确定义的地板, 其精确位置可能因平台而异。

XRView

视图对应于观看虚拟场景的摄像头。视图还有一个 transform 属性,用于将位置描述为向量,并描述其方向。这些属性以向量/四元数对和等效矩阵的形式提供,您可以根据最适合您代码的形式使用任一表示法。每个视图都对应于设备用于向观看者呈现图像的显示屏或显示屏的一部分。XRView 对象在 XRViewerPose 对象返回的数组中。数组中的视图数量各不相同。在移动设备上,AR 场景有一个视图,该视图可能会或可能不会覆盖设备屏幕。头戴式设备通常有两个视图,每只眼睛各一个。

XRWebGLLayer

层提供了位图图像的来源,并描述了这些图像在设备中的呈现方式。此描述并不能完全体现此参与者的作用。我将其视为设备和 WebGLRenderingContext 之间的中间人。MDN 的观点大致相同,指出它在两者之间“提供链接”。因此,它提供了对其他参与者的访问权限。

一般来说,WebGL 对象存储用于呈现 2D 和 3D 图形的状态信息。

WebGLFramebuffer

帧缓冲区向 WebGLRenderingContext 提供图像数据。从 XRWebGLLayer 中检索后,您将其传递给当前的 WebGLRenderingContext。除了调用 bindFramebuffer()(稍后会详细介绍)之外,您永远不会直接访问此对象。您只需将其从 XRWebGLLayer 传递给 WebGLRenderingContext。

XRViewport

视口提供了 WebGLFramebuffer 中矩形区域的坐标和尺寸。

WebGLRenderingContext

渲染上下文是画布(我们绘制的空间)的编程访问点。为此,它需要 WebGLFramebuffer 和 XRViewport。

请注意 XRWebGLLayerWebGLRenderingContext 之间的关系。一个对应于观看者的设备,另一个对应于网页。 WebGLFramebufferXRViewport 从前者传递到后者。

XRWebGLLayer 与 WebGLRenderingContext 之间的关系
和 之间的关系 XRWebGLLayerWebGLRenderingContext

游戏

现在我们知道了参与者是谁,接下来看看他们玩的游戏。这是一个每帧都重新开始的游戏。回想一下,帧是帧 循环的一部分,帧循环的速率取决于底层硬件。对于 VR 应用,每秒帧数可能介于 60 到 144 之间。Android 版 AR 以每秒 30 帧的速度运行。您的代码不应假定任何特定的帧速率。

帧循环的基本流程如下所示:

  1. 调用 XRSession.requestAnimationFrame()。作为响应,用户代理会调用您定义的 XRFrameRequestCallback
  2. 在回调函数内:
    1. 再次调用 XRSession.requestAnimationFrame()
    2. 获取观看者的姿态。
    3. WebGLFramebufferXRWebGLLayer 传递(“绑定”)到 WebGLRenderingContext
    4. 遍历每个 XRView 对象,从 XRWebGLLayer 中检索其 XRViewport,并将其传递给 WebGLRenderingContext
    5. 在帧缓冲区中绘制内容。

由于上篇文章介绍了第 1 步和第 2a 步,因此我将从第 2b 步开始。

获取观看者的姿态

这可能是不言而喻的。如需在 AR 或 VR 中绘制任何内容,我需要知道观看者在哪里以及他们正在看哪里。观看者的位置和 方向由 XRViewerPose 对象提供。我通过对当前动画帧调用 XRFrame.getViewerPose() 来获取观看者的姿态。我将设置会话时获取的参考空间传递给它。此对象返回的值始终相对于我进入当前会话时请求的参考空间。您可能还记得,在请求姿态时,我必须传递当前参考空间。

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
  if (xrViewerPose) {
    // Render based on the pose.
  }
}

有一个观看者姿态表示用户的整体位置,即观看者的头部或手机摄像头。 该姿态会告知您的应用观看者在哪里。实际图像渲染使用 XRView 对象,我稍后会介绍这些对象。

在继续之前,我会测试是否返回了观看者姿态,以防系统因隐私原因而丢失跟踪信息或阻止姿态。跟踪是指 XR 设备能够了解自身及其输入设备相对于环境的位置。跟踪可能会以多种方式丢失,具体取决于用于跟踪的方法。例如,如果使用头戴式设备或手机上的摄像头进行跟踪,则设备在光线不足或没有光线的情况下可能会失去确定自身位置的能力,或者摄像头被遮盖时也可能会失去此能力。

因隐私原因而阻止姿态的一个示例是,如果头戴式设备显示安全对话框(例如权限提示),浏览器可能会在此期间停止向应用提供姿态。但我已调用 XRSession.requestAnimationFrame(),以便在系统可以恢复的情况下,帧循环将继续。否则,用户代理将结束会话并调用 end 事件处理脚本。

简短绕行

下一步需要会话设置期间创建的对象。 回想一下,我创建了一个画布,并指示它创建一个与 XR 兼容的 Web GL 渲染上下文,我通过调用 canvas.getContext() 获取了该上下文。所有绘制都是使用 WebGL API、WebGL2 API 或基于 WebGL 的框架(例如 Three.js)完成的。除了 XRWebGLLayer 的新实例之外,此上下文还通过 updateRenderState() 传递给了会话对象。

let canvas = document.createElement('canvas');
// The rendering context must be based on WebGL or WebGL2
let webGLRenContext = canvas.getContext('webgl', { xrCompatible: true });
xrSession.updateRenderState({
    baseLayer: new XRWebGLLayer(xrSession, webGLRenContext)
  });

传递(“绑定”)WebGLFramebuffer

XRWebGLLayer 为专门用于 WebXR 并替换渲染上下文默认帧缓冲区的 WebGLRenderingContext 提供了一个帧缓冲区。在 WebGL 语言中,这称为“绑定”。

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
  if (xrViewerPose) {
    let glLayer = xrSession.renderState.baseLayer;
    webGLRenContext.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
    // Iterate over the views
  }
}

遍历每个 XRView 对象

获取姿态并绑定帧缓冲区后,就可以获取视口了。XRViewerPose 包含一个 XRView 接口数组,每个接口都表示一个显示屏或显示屏的一部分。它们包含正确呈现设备和观看者内容所需的信息,例如视野范围、眼睛偏移和其他光学属性。由于我为两只眼睛绘制内容,因此我有两个视图,我会遍历这两个视图并为每个视图绘制单独的图像。

在为基于手机的增强现实实现时,我只有一个视图,但我仍然会使用循环。虽然遍历一个视图似乎毫无意义,但这样做可以让您为各种沉浸式体验提供单一的渲染路径。这是 WebXR 与其他沉浸式系统之间的重要区别。

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
  if (xrViewerPose) {
    let glLayer = xrSession.renderState.baseLayer;
    webGLRenContext.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
    for (let xrView of xrViewerPose.views) {
      // Pass viewports to the context
    }
  }
}

将 XRViewport 对象传递给 WebGLRenderingContext

XRView 对象是指屏幕上可观察到的内容。但如需在该视图中绘制内容,我需要特定于我的设备的坐标和尺寸。与帧缓冲区一样,我从 XRWebGLLayer 请求它们,并将它们传递给 WebGLRenderingContext

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
  if (xrViewerPose) {
    let glLayer = xrSession.renderState.baseLayer;
    webGLRenContext.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
    for (let xrView of xrViewerPose.views) {
      let viewport = glLayer.getViewport(xrView);
      webGLRenContext.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
      // Draw something to the framebuffer
    }
  }
}

webGLRenContext

在编写过程中,我曾与几位同事就 webGLRenContext 对象的命名进行了讨论。示例脚本和大多数 WebXR 代码都将此变量称为 gl。在尝试理解示例时,我一直忘记 gl 指的是什么。我将其称为 webGLRenContext,以便在您学习时提醒您,这是 WebGLRenderingContext 的实例。

原因是,使用 gl 可以使方法名称看起来与 OpenGL ES 2.0 API 中的对应名称类似,后者用于在编译语言中创建 VR。如果您使用 OpenGL 编写过 VR 应用,这一点显而易见,但如果您是这项技术的全新用户,则可能会感到困惑。

在帧缓冲区中绘制内容

如果您雄心勃勃,可以直接使用 WebGL,但我并不推荐这样做。使用顶部列出的框架之一要简单得多。

总结

这并不是 WebXR 更新或文章的结束。您可以在 MDN 中找到所有 WebXR 接口和成员的参考。如需了解接口本身的即将推出的增强功能,请在 Chrome Status 中关注 各个功能。