在前面文章中,我们了解了 VR 概念以及它们如何在 WebXR 中映射。 这使你可以考虑想要为用户提供的体验。 在本文中,我们将介绍如何将 WebXR 与 Three.JS 结合使用来创建针对大型异构用户群的沉浸式体验。
警告:WebXR API 仍在完善中(第一个公共工作草案刚刚发布),因此我将尽力更新本系列以反映更改并保持这些文章常青。 WebXR 设备 API 规范将出现新功能,如果它简化了代码,我将相应地更新本文。
用 3DConvert 在线工具快速转换3D模型的格式,无需本地安装。
1、Three.js 渲染管线快速概览
我不会花太多时间讨论 Three.JS 渲染管道的工作原理,因为它在互联网上有详细记录(例如此处)。 我将在下图中列出基础知识,以便更容易理解各个部分的去向。
2、WEBXR 设备 API 入门
在我们深入了解 WebXR API 本身之前,您应该知道 WebXR 设备 API Polyfill 可通过两种主要方式帮助开发人员:
如果设备/浏览器不支持 WebXR 设备 API,它将尝试使用陀螺仪和加速计等可用传感器对其进行填充,从而允许开发人员提供基本的纸板式体验或内联渲染。
如果浏览器支持旧版 WebVR API,它将在 WebVR 之上填充 WebXR 设备 API,从而允许开发人员首先利用为支持 WebVR 所做的所有工作(从而允许其利用其下方的 VR 运行时)。
用户可以进入的主要 3D 体验类型包括:
- 基于台式计算机的键盘/鼠标,没有任何沉浸式支持。
- 利用手机传感器的内联渲染或魔术窗口。 内联渲染是一种很好的方式来“逗弄”用户你的内容,向他们展示你的体验,希望这能让他们单击按钮并在 HMD 内进入更身临其境的体验。 这是一个例子:
- 具有专用 VR 系统的沉浸式 VR、基于移动设备的 VR 或类似纸板的体验。
WebXR 设备 API 基于会话的概念。 例如,你请求会话,浏览器负责在 HMD 中启动渲染。 当你结束会话时,渲染会在 HMD 内停止,你可以像往常一样在页面上再次开始渲染。 会话有 3 种类型:沉浸式 VR、沉浸式 AR(本文未介绍)和内联。
这是一个非常简单的流程图,可以帮助你根据设备功能或 WebXR 设备 API 支持等因素决定提供哪种体验。
3、请求WebXR会话并呈现内容
本节介绍使用 WebXR 设备 API 请求会话和呈现内容所需步骤的高级流程。 我们将在某些步骤中详细介绍,主要关注渐进方面而不是渲染本身。
在本文中,我们将参考此处的一个简单演示。 它使用 WebXR 和 Three.JS,是本文中代码片段的基础。 完整来源位于此处。
4、检查 WEBXR 支持
你会发现不支持 WebXR 设备 API(即使使用 WebXR polyfill)的浏览器或设备并不罕见。 在这种情况下,你可以考虑基于键盘和鼠标的回退,而不是提供空页面,以便用户可以在体验中导航(类似于 3D 游戏)。
在 Three.JS 中,支持这一点相对简单。 你可以使用 PointerLockControls 类自动映射鼠标来移动相机(与 FPS 射击游戏的方式相同)。 使用指针锁定的好处是,当获取锁定时,它将发送鼠标移动的增量而不是它们在视口中的绝对位置。 另一个好处是,除非用户解锁指针(通常使用转义键,为您提供暂停体验的方法),否则光标无法离开浏览器窗口,并且光标将被隐藏。 这非常适合我们的需要。
this._controls = new THREE.PointerLockControls(this._camera);
this._scene.add(this._controls.getObject());
请注意,THREE.PointerLockControls 不会为你锁定指针。 通常,你希望与一个按钮进行交互来开始,该按钮会提醒用户将要发生某些事情。 这是执行此操作的一段简化代码:
document.body.addEventListener( 'click', _ => {
// Ask the browser to lock the pointer
document.body.requestPointerLock = document.body.requestPointerLock ||
document.body.mozRequestPointerLock ||
document.body.webkitRequestPointerLock;
document.body.requestPointerLock();
}, false);
THREE.PointerLockControls 将在鼠标移动时负责更新相机。
最后要处理的部分是键盘移动,这非常简单:
document.addEventListener('keydown', event => {this._onKeyDown(event)}, false);
document.addEventListener('keyup', event => {this._onKeyUp(event)}, false);
在你的处理程序中,只需更新存储在某个变量中的相机的位置即可。 在演示中,我存储了预期的运动方向,因为我通过速度来平滑运动,这样用户看起来就不会跳跃位置。
完成更新渲染函数内的位置后,请更新 THREE.PointerLockControls:
let controls_yaw = this._controls.getObject();
controls_yaw.translateX(new_position.x);
controls_yaw.translateZ(new_position.z);
然后照常渲染场景并使用 requestAnimationFrame 再次循环:
this._renderer.render(this._scene, this._camera);
return requestAnimationFrame(this._update);
5、检查支持的WebXR会话模式
如果你的浏览器支持 WebXR(无论是本机还是通过 polyfill),需要查询 XR 会话支持的模式来决定后续步骤,例如,添加一个按钮以进入沉浸式模式。 下面是一个尝试确定是否支持沉浸式 VR 模式的示例:
navigator.xr.supportsSession('immersive-vr').then(() => {
this._createPresentationButton();
}).catch((err) => {
console.log("Immersive VR is not supported: " + err);
});
如果Promise得到解析,那么你可以在页面中添加一个按钮,通知用户他们可以使用他们拥有的 HMD 进入 VR 模式。 你还可以使用内联会话进行查询,看看是否可以在页面内渲染某些内联内容(也称为魔术窗口)。
6、调整以添加对 WEBXR 设备 API 的支持
如果将 Three.JS 渲染循环设置为在 2D 屏幕上进行常规渲染,则需要对其进行调整以支持使用 WebXR 进行渲染。 首先,以下是渲染如何与 WebXR 设备 API 一起工作的基本流程,无论是沉浸式会话还是内联会话。
让我扩展一下该图中的一些方框:
请求会话
navigator.xr.requestSession({mode: 'request-mode'})
mode参数可以是 immersive-vr、 immersive-ar 或 inline。 请记住,如果在你询问支持和请求会话之间 XR 设备不再可用,请妥善处理拒绝。
请求参考空间
当 requestSession Promise成功解决后,可以使用以下方式请求参考空间:
xrSession.requestReferenceSpace({ type:'type' })
本文前面讨论了各种可能的类型和子类型。 如果要指定子类型,请使用以下代码:
xrSession.requestReferenceSpace({ type:'type', subtype:'subtype' })
我建议你请求体验所需的功能最少的参考空间,因为它允许你支持更广泛的现有 XR 设备。
7、设置 XR 层
我不会在这里深入讨论细节,因为这里的 WebXR 设备 API 解释器对此进行了详细介绍。 然而,我将介绍正在进行内联渲染的具体示例,然后用户想要“升级”他们的体验以切换到更沉浸的模式。
现在,如果你想要使用内联模式然后切换到沉浸式模式,则需要两个画布:一个用于渲染沉浸式内容,另一个用于渲染内联内容。 每个画布都有自己的 webgl 上下文。 需要两个上下文的主要原因是,当你要求 GL 上下文与 XR 兼容(使用 makeXRCompatible)时,底层实现确保在连接 HMD 的 GPU 上创建上下文。 这与具有多个 GPU 的计算机非常相关,其中 HMD 可能连接到包含附加 GPU 的外壳。
注意:WebXR 设备 API 社区内部进行了一次讨论,以避免可能需要 2 个会话,从而更轻松地从一种体验切换到另一种体验。 这是正在讨论的问题。 还有一项工作正在进行中,以避免必须请求 2 个会话(一个内联,一个沉浸式),这将简化相当多的代码。
以下是使用 Three.JS 设置 XR 层的代码:
await this._renderer.context.makeXRCompatible();
this._xrSession.baseLayer = new XRWebGLLayer(this._xrSession, this._renderer.context);
8、调整渲染以支持沉浸式 VR 渲染
每当你使用 WebXR 设备 API 进行渲染时,渲染循环都需要更新。 首先也是最重要的,你不会在窗口上请求新的动画帧,而是在会话本身上请求新的动画帧。 原因是 XRSession 将根据 HMD 建议的刷新率以正确的频率调用你的代码。
在VR中,刷新率很可能与主屏幕的刷新率不同。 例如,一些一体式 HMD 的刷新率为 72 Hz,而 Oculus* Rift 或 HTC* Vive 等高端 VR HMD 的刷新率为 90 Hz,我们预计会看到 120 Hz 的 HMD 在不久的将来刷新率。 以更高刷新率渲染 VR 体验的主要原因之一是为用户提供流畅、更舒适的体验(这也有助于对抗晕动病)。
通常你会有这样的东西:
this._xrSession.requestAnimationFrame(this._render);
渲染函数如下所示:
function _render(timestamp, xrFrame);
xrFrame参数是携带当前XR设备渲染当前帧所需信息的对象。 在继续渲染之前,需要对 Three.JS 进行一些记录,以确保渲染可以与 WebXR 配合使用:
// Disable autoupdating because these values will be coming from the
// xrFrame data directly.
this._scene.matrixAutoUpdate = false;
// Make sure not to clear the renderer automatically, because we will need
// to render it ourselves twice, once for each eye.
this._renderer.autoClear = false;
// Clear the canvas manually.
this._renderer.clear();
下一步是最终进入 WebXR 特定的渲染位。 首先,你需要绑定层,这意味着你将告诉 Three.JS 渲染器渲染到 XRSession 层:
let xrLayer = this._xrSession.baseLayer;
this._renderer.setSize(xrLayer.framebufferWidth, xrLayer.framebufferHeight, false);
this._renderer.context.bindFramebuffer(this._renderer.context.FRAMEBUFFER, xrLayer.framebuffer);
然后你需要获取设备的姿态(姿态是旋转和位置,如果有的话):
let pose = xrFrame.getViewerPose(xrReferenceSpace);
if (!pose)
return;
该姿态将为你提供渲染 XR 设备场景所需的所有信息。 然后你将需要渲染视图。 WebXR 设备 API 具有视图概念,通常映射到 VR HMD 内的屏幕数量。 通常,你将有 2 个视图(每只眼睛一个):
从那里你可以迭代视图并渲染每只眼睛:
for (let view of pose.views) {
let viewport = xrSession.baseLayer.getViewport(view);
this._renderEye(pose.viewMatrix, view.projectionMatrix, viewport);
}
渲染每只眼睛将如下所示:
_renderEye(viewMatrixArray, projectionMatrix, viewport) {
// Set the left or right eye half.
this._renderer.setViewport(viewport.x, viewport.y, viewport.width, viewport.height);
let viewMatrix = new THREE.Matrix4();
viewMatrix.fromArray(viewMatrixArray);
// Update the scene and camera matrices.
this._camera.projectionMatrix.fromArray(projectionMatrix);
this._camera.matrixWorldInverse.copy(viewMatrix);
this._scene.matrix.copy(viewMatrix);
// Tell the scene to update (otherwise it will ignore the change of matrix).
this._scene.updateMatrixWorld(true);
this._renderer.render(this._scene, this._camera);
// Ensure that left eye calcs aren't going to interfere.
this._renderer.clearDepth();
}
“视图”方法的最佳部分是,你可以继续为内联会话重用相同的渲染代码,因为唯一的区别(除了矩阵值之外)是你只有一个要渲染的视图。
原文链接:Three.js WebXR渲染入门 — BimAnt