# VR

在 three.js 中制作一个 VR 应用相当简单。你基本上只需要告诉 three.js 你想使用 WebXR。关于 WebXR，有几点应该很容易理解。摄像机的朝向是由 VR 系统提供的，因为用户会转动头部来选择观看的方向。同样，视野范围（field of view）和长宽比也是由 VR 系统提供的，因为每个系统的视野和显示比例都不同。

我们来看一个来自制作响应式网页的示例，并让它支持 VR。

在开始之前，你需要一台支持 VR 的设备，比如 Android 智能手机、Google Daydream、Oculus Go、Oculus Rift、Vive、Samsung Gear VR，或者一部安装了WebXR 浏览器的 iPhone。

接下来，如果你在本地运行，你需要像设置教程中提到的那样运行一个简单的 Web 服务器。

如果你用于查看 VR 的设备不是运行服务的同一台电脑，那么你需要通过 https 来访问网页，否则浏览器将不允许使用 WebXR API。设置教程中提到的名为 Servez 的服务器支持启用 https。勾选该选项并启动服务器。

请注意 URL，你需要使用你电脑的本地 IP 地址。它通常会以 `192`、`172` 或 `10` 开头。在 VR 设备的浏览器中输入完整地址，包括 `https://` 部分。注意：你的电脑和 VR 设备必须在同一个本地网络或 WiFi 上，并且最好是在家庭网络中。注意：许多咖啡馆的网络配置不允许设备间直接通信。

你可能会看到如下图所示的错误提示。点击“高级”，然后点击继续。

现在你可以运行示例代码了。

如果你打算真正进行 WebXR 开发，你还应该了解一下 远程调试，这样你就可以查看控制台警告、错误，当然也可以调试你的代码。

如果你只是想看看下面的代码是否可运行，你可以直接在本网站运行它。

我们首先需要在引入 three.js 之后引入对 VR 的支持：

    import * as THREE from 'three';
    +import {VRButton} from 'three/addons/webxr/VRButton.js';  // 引入 VR 按钮模块

然后我们需要启用 three.js 的 WebXR 支持，并将 VR 按钮添加到页面中：

    function main() {
      const canvas = document.querySelector('#c');
      const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
    +  renderer.xr.enabled = true;  // 启用 WebXR 支持
    +  document.body.appendChild(VRButton.createButton(renderer));  // 将 VR 按钮添加到页面

我们需要让 three.js 来运行渲染循环。在此之前我们一直使用 `requestAnimationFrame` 循环，但为了支持 VR，我们需要让 three.js 自己控制渲染循环。我们可以调用 `WebGLRenderer.setAnimationLoop` 并传入一个回调函数来实现：

    function render(time) {
      time *= 0.001;

      if (resizeRendererToDisplaySize(renderer)) {
        const canvas = renderer.domElement;
        camera.aspect = canvas.clientWidth / canvas.clientHeight;
        camera.updateProjectionMatrix();
      }

      cubes.forEach((cube, ndx) => {
        const speed = 1 + ndx * .1;
        const rot = time * speed;
        cube.rotation.x = rot;
        cube.rotation.y = rot;
      });

      renderer.render(scene, camera);

    -  requestAnimationFrame(render);  // 原来的 requestAnimationFrame 被移除
    }

    -requestAnimationFrame(render);  // 原调用被注释
    +renderer.setAnimationLoop(render);  // 改为使用 WebXR 的渲染循环方式

还有一个细节：我们最好设置一个摄像机的高度，使其符合站立用户的平均视角高度。

    const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
    +camera.position.set(0, 1.6, 0);  // 设置摄像机高度为 1.6 米，符合站立用户的平均视角

并将立方体上移，使其位于摄像机前方：

    const cube = new THREE.Mesh(geometry, material);
    scene.add(cube);

    cube.position.x = x;
    +cube.position.y = 1.6;  // 与摄像机高度一致
    +cube.position.z = -2;  // 放置在摄像机前方 2 米处

我们将 z 设置为 `-2`，因为摄像机现在位于 `z = 0`，默认朝向 -z 轴方向。

这引出了一个非常重要的点：VR 中的单位是以米为单位。换句话说，一个单位 = 一米。这意味着摄像机在距离地面 1.6 米的位置，立方体的中心位于摄像机前方 2 米处。每个立方体的大小是 1x1x1 米。这一点非常关键，因为 VR 需要将虚拟世界中的尺寸与用户在现实世界中的动作相匹配。

现在，我们应该可以在摄像机前方看到三个旋转的立方体，并且有一个进入 VR 的按钮。

点击此处在新窗口中打开

我发现 VR 效果更好一些时，是摄像机周围有一些参考物，比如一个房间。因此我们来添加一个简单的网格立方体贴图，就像我们在背景文章中讲到的那样。我们会使用相同的网格纹理贴图在立方体的每一面上，这样可以创建一个“网格房间”。

    const scene = new THREE.Scene();
    +{
    +  const loader = new THREE.CubeTextureLoader();  // 创建立方体贴图加载器
    +  const texture = loader.load([
    +    'resources/images/grid-1024.png',  // 六个面的纹理都用同一张图
    +    'resources/images/grid-1024.png',
    +    'resources/images/grid-1024.png',
    +    'resources/images/grid-1024.png',
    +    'resources/images/grid-1024.png',
    +    'resources/images/grid-1024.png',
    +  ]);
    +  scene.background = texture;  // 设置场景背景为加载的立方体贴图
    +}

这样看起来会更好一些。

点击此处在新窗口中打开

注意：要实际看到 VR 效果，你需要一台兼容 WebXR 的设备。我相信大多数 Android 手机在使用 Chrome 或 Firefox 时都支持 WebXR。至于 iOS，你也许可以使用这个 WebXR 应用，但总体上 WebXR 在 iOS 上的支持在 2019 年 5 月时仍处于不支持状态。

在 Android 或 iPhone 上使用 WebXR，你需要一个手机专用的 VR 头显。你可以以很便宜的价格购买，比如用纸板做的只需约 5 美元，高端一些的可能需要 100 美元左右。不幸的是，我也不清楚该推荐哪款产品。我这些年买过 6 个设备，质量参差不齐，最贵的也没超过 25 美元。

以下是一些可能遇到的问题：

1. 是否适配你的手机尺寸

   手机尺寸各异，因此 VR 头显需要与之匹配。很多头显声称支持多种尺寸。从我的经验来看，适配尺寸越多，实际效果越差，因为它们不得不在多个尺寸之间做出妥协。不幸的是，支持多尺寸的头显是最常见的类型。

2. 是否能够调节焦距以适配你的脸型

   有些设备的可调节性更强。通常最多提供两种调节方式：镜片与眼睛之间的距离，以及两只眼睛之间的镜片间距。

3. 镜片是否太反光

   许多头显的镜片连接区域是一段塑料通道。如果这些塑料材质是光滑或反光的，那么它会像镜子一样反射屏幕内容，造成强烈干扰。

   几乎没有评论会提及这个问题。

4. 佩戴是否舒适

   大多数设备像眼镜一样压在鼻梁上。几分钟后可能就会感觉不适。有些设备配有环绕头部的固定带，有些还有一条从上方穿过头顶的第三条带子。这些可能或可能不会起到将设备固定在合适位置的作用。

   事实是，对大多数（甚至所有）设备来说，眼睛必须正对镜片中心。如果镜片略微偏高或偏低，图像就会变模糊。这可能非常令人沮丧，因为一开始图像是清晰的，但使用 45 到 60 秒后设备稍微移位 1 毫米，你会突然发现自己在努力看一个模糊的图像。

5. 是否支持眼镜

   如果你戴眼镜，你需要查看评论确认该设备是否支持眼镜佩戴。

很遗憾，我没法给出推荐。Google 提供了一些便宜的纸板 VR 眼镜建议，有些仅需 5 美元左右，不妨从那里开始尝试。如果你喜欢这个体验，再考虑升级。5 美元也就一杯咖啡的钱，试一试也无妨！

VR 设备大致可以分为 3 种类型：

1. 三自由度（3DoF），无输入设备

   这通常指的是手机类设备，尽管有时也可以购买第三方输入设备。所谓三自由度是指你可以上下转头（1）、左右转头（2）、以及左右倾斜头部（3）。

2. 三自由度（3DoF）+ 一个三自由度输入设备

   这类设备包括 Google Daydream 和 Oculus GO。

   它们同样支持三自由度，并配有一个小型控制器，在 VR 中像激光指针一样使用。激光指针本身也只有三自由度，系统只能识别它的指向方向，不能识别它的位置。

3. 六自由度（6DoF）+ 六自由度输入设备

   这些是真正的 VR 设备（哈哈）。六自由度意味着设备不仅知道你头部的朝向，还知道你头部的实际位置。这意味着你左右移动、前后移动、或坐下/站起，设备都能感知并在 VR 中进行同步。

   体验非常真实，令人惊艳。在一个好的演示中你可能会被震撼到，我至今仍然会被打动。

   此外，这类设备通常配有两个控制器，分别对应左右手。系统可以准确识别你双手的位置和朝向，因此你可以在 VR 中通过触摸、推动、扭动等手势操作物体。

   支持六自由度的设备包括 Vive、Vive Pro、Oculus Rift、Quest 以及我相信所有 Windows MR 设备。

讲了这么多，我也不能完全确认哪些设备确实能与 WebXR 配合使用。但我 99% 确信，大多数 Android 手机在使用 Chrome 时是可以的。你可能需要在 `about:flags` 中启用 WebXR 支持。我也知道 Google Daydream 是可用的，同样需要在 `about:flags` 中启用支持。Oculus Rift、Vive、Vive Pro 可以通过 Chrome 或 Firefox 使用。我对 Oculus Go 和 Oculus Quest 不太确定，因为它们使用的是定制操作系统，但根据网络信息，它们似乎也是可以的。

好了，介绍完 VR 设备和 WebXR，我们继续讲其他内容。

- 同时支持 VR 和 非 VR 模式

  据我所知（截至 r112 版本），three.js 并没有提供一个简单的方法来同时支持 VR 和非 VR 模式。理想情况下，如果不处于 VR 模式，我们希望可以使用任何方式控制摄像机，例如使用 `OrbitControls`，并且在切换进出 VR 模式时可以接收到事件，以便启用或禁用控制器。

如果 future 的 three.js 添加了支持，我会尝试更新本文。在此之前，你可能需要制作两个版本的页面，或者在 URL 中传入一个标记参数，例如：

    https://mysite.com/mycooldemo?allowvr=true

然后我们可以加一些链接来切换模式：

    <body>
      <canvas id="c"></canvas>
    +  <div class="mode">
    +    <a href="?allowvr=true" id="vr">启用 VR 模式</a>
    +    <a href="?" id="nonvr">使用非 VR 模式</a>
    +  </div>
    </body>

并加上一些 CSS 来定位这些链接：

    body {
        margin: 0;
    }
    #c {
        width: 100%;
        height: 100%;
        display: block;
    }
    +.mode {
    +  position: absolute;
    +  right: 1em;  /* 右上角显示 */
    +  top: 1em;
    +}

你可以在代码中这样读取参数：

    function main() {
      const canvas = document.querySelector('#c');
      const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
    -  renderer.xr.enabled = true;
    -  document.body.appendChild(VRButton.createButton(renderer));

      const fov = 75;
      const aspect = 2;  // canvas 默认宽高比
      const near = 0.1;
      const far = 5;
      const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
      camera.position.set(0, 1.6, 0);

    +  const params = (new URL(document.location)).searchParams;
    +  const allowvr = params.get('allowvr') === 'true';  // 从 URL 中读取 allowvr 参数
    +  if (allowvr) {
    +    renderer.xr.enabled = true;
    +    document.body.appendChild(VRButton.createButton(renderer));
    +    document.querySelector('#vr').style.display = 'none';  // 隐藏“启用 VR”按钮
    +  } else {
    +    // 非 VR 模式，添加控制器
    +    const controls = new OrbitControls(camera, canvas);
    +    controls.target.set(0, 1.6, -2);
    +    controls.update();
    +    document.querySelector('#nonvr').style.display = 'none';  // 隐藏“非 VR 模式”按钮
    +  }

这到底好不好我也说不准。我感觉 VR 模式和非 VR 模式之间所需的实现差异通常非常大， 所以除了最简单的应用场景外，或许制作两个单独的页面会更合适？你需要自己决定。

注意：由于种种原因，这段代码在本网站的在线编辑器中是无法运行的， 所以如果你想试试看，可以点击这里。 页面会以非 VR 模式启动，你可以用鼠标或手指来移动摄像机。 点击“允许 VR”按钮后，页面会切换为支持 VR 模式， 如果你使用的是 VR 设备，就可以点击“进入 VR”按钮。

- 决定支持哪种等级的 VR 设备

  上文我们介绍了三种类型的 VR 设备。

  - 3DOF 无输入设备
  - 3DOF + 3DOF 输入设备
  - 6DOF + 6DOF 输入设备

  你需要决定你愿意投入多少精力来支持每种类型的设备。

  例如，对于最简单的无输入设备，你能做的通常就是在用户视野中放置一些按钮或物体， 当用户将视图中心的某个指示器对准这些物体大约 0.5 秒时，就触发点击。 常见的用户体验方式是在目标物体上显示一个小型的计时圈， 表示“如果你继续把视线保持在这里一会儿，这个按钮将被选中”。

  由于没有其他输入方式，这已经是你能做的最好的交互方式了。

  下一级别是用户拥有一个 3DOF 的输入设备。通常它可以用来指向目标， 并且用户至少有两个按钮可以使用。Daydream 控制器还有一个触控板， 可以提供常规的触摸输入。

  无论如何，如果用户使用这类设备，让他们使用控制器指向目标， 会比强迫他们通过头部移动去“看”目标舒适得多。

  一个类似等级的设备可能是 3DOF 或 6DOF 的头显配合游戏手柄使用。 你需要自己决定该如何支持这种情况。常见的方式是用户仍然需要转头瞄准目标， 而手柄只是用来触发按钮。

  最后一个层级是使用 6DOF 头显配合两个 6DOF 控制器的用户。 对于这类用户来说，如果你的应用只有 3DOF 的交互， 往往会让他们感到沮丧。同样，他们通常期望能够在 VR 中用手操作物体， 你需要决定是否要支持这种高度自由的交互方式。

如你所见，入门 VR 开发相对简单，但如果你真的想做出一个可发布的 VR 应用， 那就需要大量的决策和设计。

这篇文章只是使用 three.js 进行 VR 开发的简要介绍。 我们将在 后续文章 中介绍各种输入方式。