关于帧循环
最近,我发表了将虚拟现实引入网络一文,其中介绍了 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
属性,用于描述它作为矢量的位置及其方向。这些表示形式既以向量/四元数对的形式提供,又作为等效矩阵提供,您可以根据哪种表示法使用最适合您的代码。每个 View 都对应于设备用来向观看者呈现图像的一个屏幕或屏幕的一部分。XRView
对象通过 XRViewerPose
对象的数组返回。数组中的浏览次数各不相同。在移动设备上,AR 场景有一个视图,该视图不一定会覆盖设备屏幕。头戴式耳机通常有两个视图,每只眼睛一个。
XRWebGLLayer
图层提供位图图像的来源,以及这些图像如何在设备中渲染的说明。此说明并不能准确反映该玩家的行为。我认为它就像是设备和 WebGLRenderingContext
之间的中间人。MDN 采用大致相同的观点,指出它在两者之间“提供关联”。因此,它可以提供对其他玩家的访问权限。
一般来说,WebGL 对象会存储用于渲染 2D 和 3D 图形的状态信息。
WebGLFramebuffer
帧缓冲区会向 WebGLRenderingContext
提供图像数据。从 XRWebGLLayer
检索该 ID 后,您只需将其传递给当前的 WebGLRenderingContext
即可。除了调用 bindFramebuffer()
(稍后会详细介绍)之外,您永远不会直接访问此对象。您只需将其从 XRWebGLLayer
传递给 WebGLRenderingContext 即可。
XRViewport
视口可提供 WebGLFramebuffer
中矩形区域的坐标和尺寸。
WebGLRenderingContext
渲染上下文是画布(我们在上面绘制的空间)的程序化访问点。为此,它同时需要 WebGLFramebuffer
和 XRViewport。
请注意 XRWebGLLayer
和 WebGLRenderingContext
之间的关系。一个对应于观看者的设备,另一个对应于网页。WebGLFramebuffer
和 XRViewport
会从前者传递到后者。
游戏
了解了玩家是谁,接下来我们看看他们玩的游戏。这是一款从每一帧画面重新开始的游戏回想一下,帧是帧循环的一部分,帧循环的发生频率取决于底层硬件。对于 VR 应用,每秒帧数可以是 60 到 144 之间。AR for Android 的运行速度为每秒 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)
});
传递(“bind”) 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 状态页面中的个别功能。
照片由 JESHOOTS.COM 提供,由 Unsplash 提供