使用 WebVR API

已弃用: 不再推荐使用此功能。尽管某些浏览器可能仍然支持它,但它可能已从相关的 Web 标准中删除,可能正在被弃用,或者可能仅出于兼容性目的而保留。避免使用它,如果可能,请更新现有代码;请参阅本页面底部的兼容性表 以指导您的决策。请注意,此功能可能随时停止工作。

注意: WebVR API 已被WebXR API 替换。WebVR 从未被批准为标准,仅在极少数浏览器中默认实施和启用,并且支持少量设备。

WebVR API 是 Web 开发人员工具包中的一项绝佳补充,它允许将 WebGL 场景呈现到虚拟现实显示器(如 Oculus Rift 和 HTC Vive)中。但是,如何开始开发 Web 的 VR 应用?本文将指导您了解基础知识。

入门

要开始,您需要

  • 支持 VR 硬件。
    • 最便宜的选择是使用移动设备、支持的浏览器和设备支架(例如 Google Cardboard)。这种体验可能不如专用硬件好,但您无需购买功能强大的计算机或专用的 VR 显示器。
    • 专用硬件可能成本较高,但它确实提供了更好的体验。目前最兼容 WebVR 的硬件是 HTC VIVE 和 Oculus Rift。webvr.info 的首页提供了一些关于可用硬件以及哪些浏览器支持它们的更多有用信息。
  • 如果需要,一台功能强大的计算机可以处理使用您的专用 VR 硬件渲染/显示 VR 场景。为了让您了解需要什么,请查看您购买的 VR 的相关指南(例如 VIVE READY 计算机)。
  • 安装支持的浏览器 - 最新版本的 Firefox NightlyChrome 是您目前在桌面或移动设备上最好的选择。

组装好所有东西后,您可以通过访问我们的 简单的 A-Frame 演示 来测试您的设置是否可以使用 WebVR,并查看场景是否渲染以及您是否可以通过按下右下角的按钮进入 VR 显示模式。

A-Frame 是您想要快速创建兼容 WebVR 的 3D 场景的最佳选择,而无需了解大量新的 JavaScript 代码。但是,它不会教您原始 WebVR API 的工作原理,这就是我们接下来要介绍的内容。

介绍我们的演示

为了说明 WebVR API 的工作原理,我们将研究我们的原始 webgl 示例,它看起来有点像这样

A gray rotating 3D cube

注意: 您可以在 GitHub 上找到 我们演示的源代码,还可以在线查看

注意: 如果 WebVR 在您的浏览器中无法正常工作,您可能需要确保它正在通过您的显卡运行。例如,对于 NVIDIA 显卡,如果您已成功设置 NVIDIA 控制面板,则会出现一个上下文菜单选项 - 右键单击 Firefox,然后选择使用图形处理器运行 > 高性能 NVIDIA 处理器

我们的演示具有 WebGL 演示的圣杯 - 一个旋转的 3D 立方体。我们使用原始 WebGL API 代码实现了这一点。我们不会讲解任何基本的 JavaScript 或 WebGL,只讲解 WebVR 部分。

我们的演示还具有

  • 一个按钮可以开始(和停止)我们的场景在 VR 显示器中呈现。
  • 一个按钮可以显示(和隐藏)VR 姿态数据,即头显的位置和方向,实时更新。

当您查看 我们演示的主要 JavaScript 文件的源代码 时,您可以通过在前面的注释中搜索字符串“WebVR”轻松找到 WebVR 特定的部分。

注意: 要了解有关基本 JavaScript 和 WebGL 的更多信息,请查阅我们的 JavaScript 学习资料 和我们的 WebGL 教程

它是如何工作的?

在这一点上,让我们看看代码的 WebVR 部分是如何工作的。

一个典型的(简单的)WebVR 应用的工作原理如下

  1. Navigator.getVRDisplays() 用于获取对您的 VR 显示器的引用。
  2. VRDisplay.requestPresent() 用于开始向 VR 显示器呈现。
  3. WebVR 的专用 VRDisplay.requestAnimationFrame() 方法用于以显示器的正确刷新率运行应用的渲染循环。
  4. 在渲染循环内,您获取显示当前帧所需的数据(VRDisplay.getFrameData()),绘制两次显示的场景 - 一次用于每只眼睛的视图 - 然后通过(VRDisplay.submitFrame())将渲染的视图提交到显示器以显示给用户。

在下面的部分中,我们将详细查看我们的原始 webgl 演示,并查看上述功能的确切使用位置。

从一些变量开始

您遇到的第一个与 WebVR 相关的代码是以下代码块

js
// WebVR variables

const frameData = new VRFrameData();
let vrDisplay;
const btn = document.querySelector(".stop-start");
let normalSceneFrame;
let vrSceneFrame;

const poseStatsBtn = document.querySelector(".pose-stats");
const poseStatsSection = document.querySelector("section");
poseStatsSection.style.visibility = "hidden"; // hide it initially

const posStats = document.querySelector(".pos");
const orientStats = document.querySelector(".orient");
const linVelStats = document.querySelector(".lin-vel");
const linAccStats = document.querySelector(".lin-acc");
const angVelStats = document.querySelector(".ang-vel");
const angAccStats = document.querySelector(".ang-acc");
let poseStatsDisplayed = false;

让我们简要解释一下这些

  • frameData 包含一个使用 VRFrameData 构造函数创建的 VRFrameData() 对象。最初它是空的,但稍后将包含渲染每一帧以显示在 VR 显示器中所需的数据,并在渲染循环运行时不断更新。
  • vrDisplay 最初未初始化,但稍后将保存对我们的 VR 耳机 (VRDisplay - API 的中央控制对象) 的引用。
  • btnposeStatsBtn 保存对我们用于控制应用程序的两个按钮的引用。
  • normalSceneFramevrSceneFrame 最初未初始化,但稍后将保存对 Window.requestAnimationFrame()VRDisplay.requestAnimationFrame() 调用的引用 - 这些将启动普通渲染循环和特殊的 WebVR 渲染循环的运行;我们将在稍后解释这两者之间的区别。
  • 其他变量存储对 VR 姿势数据显示框的不同部分的引用,您可以在 UI 的右下角看到它。

获取对我们的 VR 显示器的引用

我们代码中的主要函数之一是 start() - 当主体加载完成后,我们运行此函数。

js
// start
//
// Called when the body has loaded is created to get the ball rolling.

document.body.onload = start;

首先,start() 获取一个 WebGL 上下文,用于将 3D 图形渲染到 <canvas> 元素中,该元素位于 我们的 HTML 中。然后我们检查 gl 上下文是否可用 - 如果可用,我们将运行许多函数来设置场景以进行显示。

js
function start() {
  canvas = document.getElementById("glcanvas");

  initWebGL(canvas);      // Initialize the GL context

  // WebGL setup code here

接下来,我们通过将画布设置为填充整个浏览器视口并首次运行渲染循环 (drawScene()) 来开始将场景实际渲染到画布上的过程。这是非 WebVR - 常规 - 渲染循环。

js
// draw the scene normally, without WebVR - for those who don't have it and want to see the scene in their browser

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
drawScene();

现在进入我们的第一个 WebVR 特定代码。首先,我们检查 Navigator.getVRDisplays 是否存在 - 这是进入 API 的入口点,因此是 WebVR 的良好基本功能检测。您将在代码块的末尾(在 else 子句内)看到,如果它不存在,我们将记录一条消息以指示浏览器不支持 WebVR 1.1。

js
  // WebVR: Check to see if WebVR is supported
  if (navigator.getVRDisplays) {
    console.log('WebVR 1.1 supported');

在我们的 if () { } 代码块中,我们运行 Navigator.getVRDisplays() 函数。它返回一个 Promise,该 Promise 将以包含连接到计算机的所有 VR 显示设备的数组的形式完成。如果没有连接任何设备,则数组将为空。

js
    // Then get the displays attached to the computer
    navigator.getVRDisplays().then((displays) => {

在 Promise 的 then() 代码块中,我们检查数组长度是否大于 0;如果是,我们将 vrDisplay 变量的值设置为数组中索引为 0 的项。vrDisplay 现在包含一个表示我们已连接显示器的 VRDisplay 对象!

js
      // If a display is available, use it to present the scene
      if (displays.length > 0) {
        vrDisplay = displays[0];
        console.log('Display found');

注意:您的计算机不太可能连接多个 VR 显示器,而这只是一个简单的演示,因此目前可以这样使用。

启动和停止 VR 演示

现在我们有了 VRDisplay 对象,我们可以用它做很多事情。接下来我们要做的事情是连接功能以开始和停止将 WebGL 内容呈现到显示器。

继续使用前面的代码块,我们现在向我们的开始/停止按钮 (btn) 添加一个事件监听器 - 当单击此按钮时,我们想要检查我们是否已经向显示器呈现内容(我们以一种相当愚蠢的方式执行此操作,通过检查按钮的 textContent 内容)。

如果显示器尚未呈现内容,我们将使用 VRDisplay.requestPresent() 方法请求浏览器开始向显示器呈现内容。它将一个表示您希望在显示器中呈现的图层的 VRLayerInit 对象数组作为参数。

由于您最多可以显示的图层数目前为 1,并且唯一必需的对象成员是 VRLayerInit.source 属性(它是您希望在该图层中呈现的 <canvas> 的引用;其他参数被赋予合理的默认值 - 请参阅 leftBoundsrightBounds),因此参数为 [{ source: canvas }]。

requestPresent() 返回一个 Promise,当演示成功开始时,该 Promise 将完成。

js
        // Starting the presentation when the button is clicked: It can only be called in response to a user gesture
        btn.addEventListener('click', () => {
          if (btn.textContent === 'Start VR display') {
            vrDisplay.requestPresent([{ source: canvas }]).then(() => {
              console.log('Presenting to WebVR display');

我们的演示请求成功后,我们现在想要开始设置以渲染内容以呈现到 VRDisplay。首先,我们将画布的大小设置为与 VR 显示区域相同。我们通过使用 VRDisplay.getEyeParameters() 获取两只眼睛的 VREyeParameters 来做到这一点。

然后,我们进行一些简单的数学计算,根据眼睛的 VREyeParameters.renderWidthVREyeParameters.renderHeight 计算 VRDisplay 渲染区域的总宽度。

js
// Set the canvas size to the size of the vrDisplay viewport

const leftEye = vrDisplay.getEyeParameters("left");
const rightEye = vrDisplay.getEyeParameters("right");

canvas.width = Math.max(leftEye.renderWidth, rightEye.renderWidth) * 2;
canvas.height = Math.max(leftEye.renderHeight, rightEye.renderHeight);

接下来,我们 取消先前由 drawScene() 函数中的 Window.requestAnimationFrame() 调用启动的动画循环,并改为调用 drawVRScene()。此函数渲染与之前相同的场景,但会使用一些特殊的 WebVR 魔法。此处的循环由 WebVR 的特殊 VRDisplay.requestAnimationFrame 方法维护。

js
// stop the normal presentation, and start the vr presentation
window.cancelAnimationFrame(normalSceneFrame);
drawVRScene();

最后,我们更新按钮文本,以便下次按下它时,它将停止向 VR 显示器呈现内容。

js
              btn.textContent = 'Exit VR display';
            });

为了在随后按下按钮时停止 VR 演示,我们调用 VRDisplay.exitPresent()。我们还反转按钮的文本内容,并交换 requestAnimationFrame 调用。您可以在这里看到我们正在使用 VRDisplay.cancelAnimationFrame 停止 VR 渲染循环,并通过调用 drawScene() 再次启动普通渲染循环。

js
          } else {
            vrDisplay.exitPresent();
            console.log('Stopped presenting to WebVR display');

            btn.textContent = 'Start VR display';

            // Stop the VR presentation, and start the normal presentation
            vrDisplay.cancelAnimationFrame(vrSceneFrame);
            drawScene();
          }
        });
      }
    });
  } else {
    console.log('WebVR API not supported by this browser.');
  }
}

演示开始后,您将能够看到浏览器中显示的立体视图。

Stereoscopic view of 3D cube

您将在下面学习如何实际生成立体视图。

为什么 WebVR 有自己的 requestAnimationFrame()?

这是一个好问题。原因是,为了在 VR 显示器中实现流畅渲染,您需要以显示器的原生刷新率渲染内容,而不是计算机的刷新率。VR 显示器的刷新率高于 PC 的刷新率,通常高达 90fps。该速率将与计算机的核心刷新率不同。

请注意,当 VR 显示器未呈现内容时,VRDisplay.requestAnimationFrame 的运行方式与 Window.requestAnimationFrame 完全相同,因此,如果您愿意,您可以只使用一个渲染循环,而不是我们应用程序中使用的两个循环。我们使用了两个循环,因为我们希望根据 VR 显示器是否正在呈现内容执行略微不同的操作,并为了易于理解而将它们分开。

渲染和显示

至此,我们已经了解了访问 VR 硬件、请求将场景呈现到硬件以及开始运行渲染循环所需的所有代码。现在让我们看一下渲染循环的代码,并解释其 WebVR 特定部分的工作原理。

首先,我们开始定义渲染循环函数 - drawVRScene()。我们在此处首先执行的操作是调用 VRDisplay.requestAnimationFrame() 以在该循环被调用一次后继续运行(这在我们之前的代码中开始向 VR 显示器呈现内容时发生)。此调用被设置为全局 vrSceneFrame 变量的值,因此我们可以在退出 VR 呈现后使用对 VRDisplay.cancelAnimationFrame() 的调用来取消循环。

js
function drawVRScene() {
  // WebVR: Request the next frame of the animation
  vrSceneFrame = vrDisplay.requestAnimationFrame(drawVRScene);

接下来,我们调用 VRDisplay.getFrameData(),并将我们要用来包含帧数据的变量的名称传递给它。我们之前初始化了它 - frameData。调用完成后,此变量将包含渲染下一帧到 VR 设备所需的数据,这些数据被打包成一个 VRFrameData 对象。它包含诸如用于正确渲染左右眼视图的场景的投影和视图矩阵以及当前的 VRPose 对象等内容,该对象包含有关 VR 显示器(如方向、位置等)的数据。

必须在每一帧上调用它,以便渲染的视图始终是最新的。

js
// Populate frameData with the data of the next frame to display
vrDisplay.getFrameData(frameData);

现在,我们从 VRFrameData.pose 属性中检索当前的 VRPose,存储稍后使用的位置和方向,并将当前姿势发送到姿势统计框以进行显示,如果 poseStatsDisplayed 变量设置为 true。

js
// You can get the position, orientation, etc. of the display from the current frame's pose

const curFramePose = frameData.pose;
const curPos = curFramePose.position;
const curOrient = curFramePose.orientation;
if (poseStatsDisplayed) {
  displayPoseStats(curFramePose);
}

在开始在画布上绘制之前,我们先清除画布,以便清楚地看到下一帧,并且我们也不会看到之前渲染的帧。

js
// Clear the canvas before we start drawing on it.

gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

现在我们渲染左右眼的视图。首先,我们需要创建用于渲染的投影和视图位置。它们是 WebGLUniformLocation 对象,使用 WebGLRenderingContext.getUniformLocation() 方法创建,将着色器程序的标识符和标识名称作为参数传递给它。

js
// WebVR: Create the required projection and view matrix locations needed
// for passing into the uniformMatrix4fv methods below

const projectionMatrixLocation = gl.getUniformLocation(
  shaderProgram,
  "projMatrix",
);
const viewMatrixLocation = gl.getUniformLocation(shaderProgram, "viewMatrix");

下一个渲染步骤涉及

  • 使用 WebGLRenderingContext.viewport 指定左眼的视口大小 - 从逻辑上讲,这是画布宽度的一半和画布的完整高度。
  • 指定用于渲染左眼的视图和投影矩阵值 - 这是使用 WebGLRenderingContext.uniformMatrix4fv 方法完成的,该方法将我们上面检索到的位置值和从 VRFrameData 对象获得的左侧矩阵作为参数传递给它。
  • 运行 drawGeometry() 函数,该函数渲染实际的场景 - 由于我们在前两个步骤中指定的内容,我们将仅为左眼渲染它。
js
// WebVR: Render the left eye's view to the left half of the canvas
gl.viewport(0, 0, canvas.width * 0.5, canvas.height);
gl.uniformMatrix4fv(
  projectionMatrixLocation,
  false,
  frameData.leftProjectionMatrix,
);
gl.uniformMatrix4fv(viewMatrixLocation, false, frameData.leftViewMatrix);
drawGeometry();

现在我们执行完全相同的事情,但针对右眼。

js
// WebVR: Render the right eye's view to the right half of the canvas
gl.viewport(canvas.width * 0.5, 0, canvas.width * 0.5, canvas.height);
gl.uniformMatrix4fv(
  projectionMatrixLocation,
  false,
  frameData.rightProjectionMatrix,
);
gl.uniformMatrix4fv(viewMatrixLocation, false, frameData.rightViewMatrix);
drawGeometry();

接下来,我们定义 drawGeometry() 函数。其中大部分只是绘制 3D 立方体所需的一般 WebGL 代码。您将在 mvTranslate()mvRotate() 函数调用中看到一些 WebVR 特定部分 - 它们将矩阵传递到 WebGL 程序中,以定义当前帧的立方体的平移和旋转。

您将看到我们正在通过从 VRPose 对象获得的 VR 显示器的位置 (curPos) 和方向 (curOrient) 来修改这些值。结果是,例如,当您将头部向左移动或旋转时,x 位置值 (curPos[0]) 和 y 旋转值 ([curOrient[1]) 将添加到 x 平移值中,这意味着立方体将向右移动,正如您在查看某物然后左右移动/转动头部时所期望的那样。

这是一种使用 VR 姿势数据快速简便的方法,但它说明了基本原理。

js
function drawGeometry() {
  // Establish the perspective with which we want to view the
  // scene. Our field of view is 45 degrees, with a width/height
  // ratio of 640:480, and we only want to see objects between 0.1 units
  // and 100 units away from the camera.
  perspectiveMatrix = makePerspective(45, 640.0 / 480.0, 0.1, 100.0);

  // Set the drawing position to the "identity" point, which is
  // the center of the scene.
  loadIdentity();

  // Now move the drawing position a bit to where we want to start
  // drawing the cube.
  mvTranslate([
    0.0 - curPos[0] * 25 + curOrient[1] * 25,
    5.0 - curPos[1] * 25 - curOrient[0] * 25,
    -15.0 - curPos[2] * 25,
  ]);

  // Save the current matrix, then rotate before we draw.
  mvPushMatrix();
  mvRotate(cubeRotation, [0.25, 0, 0.25 - curOrient[2] * 0.5]);

  // Draw the cube by binding the array buffer to the cube's vertices
  // array, setting attributes, and pushing it to GL.
  gl.bindBuffer(gl.ARRAY_BUFFER, cubeVerticesBuffer);
  gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);

  // Set the texture coordinates attribute for the vertices.
  gl.bindBuffer(gl.ARRAY_BUFFER, cubeVerticesTextureCoordBuffer);
  gl.vertexAttribPointer(textureCoordAttribute, 2, gl.FLOAT, false, 0, 0);

  // Specify the texture to map onto the faces.
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, cubeTexture);
  gl.uniform1i(gl.getUniformLocation(shaderProgram, "uSampler"), 0);

  // Draw the cube.
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVerticesIndexBuffer);
  setMatrixUniforms();
  gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);

  // Restore the original matrix
  mvPopMatrix();
}

代码的下一部分与 WebVR 无关 - 它只是在每一帧上更新立方体的旋转。

js
// Update the rotation for the next draw, if it's time to do so.
let currentTime = new Date().getTime();
if (lastCubeUpdateTime) {
  const delta = currentTime - lastCubeUpdateTime;

  cubeRotation += (30 * delta) / 1000.0;
}
lastCubeUpdateTime = currentTime;

渲染循环的最后一部分涉及我们调用 VRDisplay.submitFrame() - 现在所有工作都已完成,我们已在 <canvas> 上渲染了显示,此方法然后将帧提交到 VR 显示器,以便也在那里显示。

js
  // WebVR: Indicate that we are ready to present the rendered frame to the VR display
  vrDisplay.submitFrame();
}

显示姿势(位置、方向等)数据

在本节中,我们将讨论displayPoseStats()函数,该函数在每一帧上显示我们更新的姿态数据。该函数相当简单。

首先,我们将从VRPose对象中可获得的六个不同的属性值存储到各自的变量中——每个变量都是一个Float32Array

js
function displayPoseStats(pose) {
  const pos = pose.position;
  const orient = pose.orientation;
  const linVel = pose.linearVelocity;
  const linAcc = pose.linearAcceleration;
  const angVel = pose.angularVelocity;
  const angAcc = pose.angularAcceleration;

然后,我们将数据写入信息框,并在每一帧更新它。我们使用toFixed()将每个值限制为三位小数,因为否则这些值很难阅读。

您应该注意,我们使用了条件表达式来检测线性加速度和角加速度数组是否已成功返回,然后再显示数据。大多数VR硬件目前尚未报告这些值,因此如果我们不这样做,代码将抛出错误(如果这些数组未成功报告,则它们将返回null)。

js
  posStats.textContent = `Position: ` +
    `x ${pos[0].toFixed(3)}, ` +
    `y ${pos[1].toFixed(3)}, ` +
    `z ${pos[2].toFixed(3)}`;
  orientStats.textContent = `Orientation: ` +
    `x ${orient[0].toFixed(3)}, ` +
    `y ${orient[1].toFixed(3)}, ` +
    `z ${orient[2].toFixed(3)}`;
  linVelStats.textContent = `Linear velocity: ` +
    `x ${linVel[0].toFixed(3)}, ` +
    `y ${linVel[1].toFixed(3)}, ` +
    `z ${linVel[2].toFixed(3)}`;
  angVelStats.textContent = `Angular velocity: ` +
    `x ${angVel[0].toFixed(3)}, ` +
    `y ${angVel[1].toFixed(3)}, ` +
    `z ${angVel[2].toFixed(3)}`;

  if (linAcc) {
    linAccStats.textContent = `Linear acceleration: ` +
      `x ${linAcc[0].toFixed(3)}, ` +
      `y ${linAcc[1].toFixed(3)}, ` +
      `z ${linAcc[2].toFixed(3)}`;
  } else {
    linAccStats.textContent = 'Linear acceleration not reported';
  }

  if (angAcc) {
    angAccStats.textContent = `Angular acceleration: ` +
    `x ${angAcc[0].toFixed(3)}, ` +
    `y ${angAcc[1].toFixed(3)}, ` +
    `z ${angAcc[2].toFixed(3)}`;
  } else {
    angAccStats.textContent = 'Angular acceleration not reported';
  }
}

WebVR 事件

WebVR规范包含许多触发的事件,允许我们的应用程序代码对VR显示状态的变化做出反应(请参阅窗口事件)。例如

  • vrdisplaypresentchange——当VR显示的呈现状态发生变化时触发——即从呈现到不呈现,反之亦然。
  • vrdisplayconnect——当兼容的VR显示器连接到计算机时触发。
  • vrdisplaydisconnect——当兼容的VR显示器从计算机断开连接时触发。

为了演示它们的工作原理,我们的简单演示包含以下示例

js
window.addEventListener("vrdisplaypresentchange", (e) => {
  console.log(
    `Display ${e.display.displayId} presentation has changed. Reason given: ${e.reason}.`,
  );
});

如您所见,VRDisplayEvent对象提供了两个有用的属性——VRDisplayEvent.display,它包含对响应事件触发的VRDisplay的引用,以及VRDisplayEvent.reason,它包含一个关于触发事件的人类可读原因。

这是一个非常有用的事件;您可以使用它来处理显示器意外断开连接的情况,防止抛出错误并确保用户了解情况。在Google的Webvr.info演示演示中,该事件用于运行onVRPresentChange()函数,该函数根据需要更新UI控件并调整画布大小。

总结

本文为您提供了创建简单WebVR 1.1应用程序的基础知识,以帮助您入门。