有关帧循环的所有信息
我最近发表了一篇名为虚拟现实走向 Web 的文章,介绍了 WebXR Device API 背后的基本概念。我还提供了有关请求、进入和结束 XR 会话的说明。
本文介绍了帧循环,它是一种由用户代理控制的无限循环,其中会将内容重复绘制到屏幕上。内容以离散块(称为帧)绘制。连续的帧会产生移动的错觉。
本文不涵盖的内容
WebGL 和 WebGL2 是 WebXR 应用中在帧循环期间渲染内容的唯一方式。幸运的是,许多框架在 WebGL 和 WebGL2 之上提供了一层抽象层。此类框架包括 three.js、babylonjs 和 PlayCanvas,而 A-Frame 和 React 360 则专为与 WebXR 交互而设计。
本文既不是 WebGL 教程,也不是框架教程。该博文使用沉浸式 Web 工作组的沉浸式 VR 会话示例(演示、源代码)介绍了帧循环的基础知识。如果您想深入了解 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。
请注意 XRWebGLLayer
和 WebGLRenderingContext
之间的关系。一个对应于观看者的设备,另一个对应于网页。WebGLFramebuffer
和 XRViewport
会从前者传递给后者。
游戏
现在我们已经知道玩家是谁了,接下来我们来看看他们玩的游戏。这是一个每帧重头开始的游戏。回想一下,帧是帧循环的一部分,其发生率取决于底层硬件。对于 VR 应用,每秒帧数可以介于 60 到 144 之间。Android 版 AR 的运行速度为每秒 30 帧。您的代码不应假设任何特定的帧速率。
帧循环的基本流程如下所示:
- 调用
XRSession.requestAnimationFrame()
。作为响应,用户代理会调用由您定义的XRFrameRequestCallback
。 - 在回调函数内:
- 再次调用
XRSession.requestAnimationFrame()
。 - 获取观看者的姿势。
- 将
WebGLFramebuffer
从XRWebGLLayer
传递(“绑定”)给WebGLRenderingContext
。 - 迭代每个
XRView
对象,从XRWebGLLayer
检索其XRViewport
并将其传递给WebGLRenderingContext
。 - 向帧缓冲区绘制内容。
- 再次调用
由于上文已介绍了第 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)完成。此上下文通过 updateRenderState()
以及 XRWebGLLayer
的新实例传递给会话对象。
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
为 WebGLRenderingContext
提供了一个帧缓冲区,该帧缓冲区专门用于与 WebXR 搭配使用,并替换渲染上下文的默认帧缓冲区。在 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 状态中关注各项功能。
照片由 JESHOOTS.COM 提供,由 Un 创立