翻译 | 使用A-Frame打造WebVR版《我的世界》

我是 Kevin Ngo,一名就职于 Mozilla VR 团队的 web 虚拟现实开发者,也是 A-Frame 的核心开发人员。今天,我们来看看如何使用 A-Frame 构建一个够在 HTC Vive、Oculus Rift、Samsung GearVR、Google Cardboard、桌面设备以及移动设备上运行的、支持空间追踪(room-scale)技术的 WebVR 版《我的世界》示例。该示例基于 A-Frame,且仅使用 11 个 HTML 元素!

A-Frame

几年前,Mozilla 发明并开发了 WebVR —— 一套在浏览器中创造身临其境 VR 体验的 JavaScript API —— 并将其发布在一个实验版本的 Firefox 浏览器中。此后,WebVR 得到了 Google、Microsoft、Samsung 以及 Oculus 等其他公司的广泛支持。而现在,WebVR 更是在短短几个月内就被内嵌在发行版的 Firefox 浏览器中,并被设置为默认开启!

为什么会诞生 WebVR?Web 为 VR 带来了开放性;在 Web 上,内容并不由管理员所控制,用户也不被关在高高的围墙花园(walled garden)中。Web 也为 VR 带来了连通性;在 Web 上,我们能够在世界中穿梭 —— 就像我们点击超链接在页面见穿梭一样。随着 WebGL 的成熟以及诸如 Web Assembly 和 Service Workers 规范的提出,WebVR 已经准备好了。

Mozilla VR 团队创造了 A-Frame 框架来为 WebVR 生态系统抛砖引玉,该框架给予开发者构建 3D 和 VR 世界的能力。

A-Frame 官方网站首页

A-Frame 是一个构建虚拟现实体验设的 web 框架,它基于 HTML 和实体组件范式(the Entity-Component pattern)。HTML 是所有计算机语言中最易理解的语言,这使得任何人都能快速上手 A-Frame。下面是一个使用 HTML 搭建的完整的 3D 和 VR 场景,它能够在诸如桌面设备和移动设备等任何 VR 平台运行:

<script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>

<a-scene>
  <a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
  <a-box position="-1 0.5 -3" rotation="0 45 0" width="1" height="1" depth="1" color="#4CC3D9"></a-box>
  <a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
  <a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane>
  <a-sky color="#ECECEC"></a-sky>
</a-scene>

在 CodePen 中打开

就是这样!只用使用一行 HTML(,包括:canvas、场景、渲染器、渲染循环、摄像机以及 raycaster。然后,我们可以通过使用添加子元素的方式来为场景添加对象。无需构建,就只是一个简单的、可随意拷贝粘贴的 HTML 文件。

我们还可以动态查询和操作 A-Frame 的 HTML,就像使用标准 JavaScript 和 DOM APIs (例如 querySelector、getAttribute、addEventListener、setAttribute)那样。

// 使用 `querySelector` 查询场景图像。
var sceneEl = document.querySelector('a-scene');
var boxEl = sceneEl.querySelector('a-box');

// 使用 `getAttribute` 获得实体的数据。
console.log(box.getAttribute('position'));
// >> {x: -1, y: 0.5, z: -3}

// 使用 `addEventListener` 监听事件。
box.addEventListener('click', function () {
  // 使用 `setAttribute` 修改属性。
  box.setAttribute('color', 'red');
});

而且,因为这些只是 HTML 和 JavaScript,因此 A-Frame 和许多现存的框架和库兼容良好:

兼容 d3、Vue、React、Redux、jQuery、Angular

尽管 A-Frame 的 HTML 看起来比较简单,但是 A-Frame 的 API 却远远比简单的 3D 声明强大。A-Frame 是一个实体组件系统(ECS)框架,ECS 在游戏开发中是一种流行的模式,值得注意的是 ECS 也被 Unity 引擎所使用。其概念包括:

  • 在场景中,所有的对象都是实体(entities),空对象本身什么也不能做,类似空 <div>。A-Frame 使用 HTML 元素在 DOM 中表示实体。
  • 接下来,我们在实体中插入组件(components) 来提供外观、行为和功能。在 A-Frame 中,组件被注册在 JavaScript 中,并且可以被用来做任何事情。它们可使用完整的 three.js 和 DOM APIs。组件注册后,可以附加在 HTML 实体上。

ECS 的优势在于它的可组合性;我们可以混合和搭配这些可复用的组件来构建出更复杂的 3D 对象。A-Frame 更上一层楼,将这些组件声明化,并使其作为 DOM 的一部分,就像我们待会在《我的世界》示例中看到那样。

示例骨架

现在来关注我们的示例。我们将搭建一个基本的 VR 立体像素制作器(voxel builder),它主要用于支持位置追踪(positional tracking)和追踪控制器(tracked controllers)的空间追踪 VR 设备(例如 HTC Vive 及 Oculus Rift + Touch)。

我们会从 HTML 骨架开始。如果你想要快速浏览(完整的 11 行 HTML),点击这里到 GitHub 查看源代码

<script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>

<body>
  <a-scene>
  </a-scene>
</body>

添加地面

<a-plane><a-circle> 都是常被用作添加地面的图元,不过我们会使用 <a-cylinder> 来更好地配合控制器完成灯光计算工作。圆柱(cylinder)的半径为 30 米,待会我们要添加的天空将会和这个半径值匹配起来。注意 A-Frame 中的单位是米,以匹配 WebVR API 返回的现实世界中的单位。

地面的纹理部署在 https://cdn.aframe.io/a-painter/images/floor.jpg。我们将纹理添加进项目中,并使用该纹理制作一个扁的圆柱实体。

<script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>

<a-scene>
  <a-cylinder id="ground" src="https://cdn.aframe.io/a-painter/images/floor.jpg" radius="32" height="0.1"></a-cylinder>
</a-scene>

在 CodePen 中打开

预加载资源

通过 src 属性指定的 URL 资源将在运行时加载。

由于网络请求会对渲染的性能产生负面影响,所以我们可以预加载纹理以保证资源被下载完成前不进行渲染工作,预加载可以通过资源管理系统(asset management system)来完成。

我们将 <a-assets> 置入 <a-scene> 中,将资源(例如图片、视频、模型及声音等)置入 <a-assets> 中,并通过选择器(例如 #myTexture)将资源指向我们的实体。

让我们将地面纹理移动到 <a-assets> 中,使用 <img> 元素来预加载它:

<script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>

<a-scene>
  <a-assets>
    <img id="groundTexture" src="https://cdn.aframe.io/a-painter/images/floor.jpg">
  </a-assets>

  <a-cylinder id="ground" src="#groundTexture" radius="32" height="0.1"></a-cylinder>
</a-scene>

在 CodePen 中打开

添加背景

让我们使用 <a-sky> 元素<a-scene> 添加一个 360° 的背景。<a-sky> 是一个在内部粘贴材质的巨大 3D 球体。就像普通图片一样,<a-sky> 可以通过 src 属性接受图片地址。最终我们将可以使用一行 HTML 代码实现身临其境的 360° 图片。稍后你也可以在 Flickr 球面投影图片池(需FQ)中选择一些 360° 图片来做练习。

我们可以添加普通的颜色背景(例如 <a-sky color="#333"></a-sky>)或渐变,不过这次让我们来添加一张背景纹理图片。该图片被部署在 https://cdn.aframe.io/a-painter/images/sky.jpg。我们所使用的图片是一张适用于半球体的图片,所以首先我们需要将刚刚的球体使用 theta-length="90" 水平截成半球体,另外我们将球的半径设置为 30 米以匹配地面。

<script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>

<a-scene>
  <a-assets>
    <img id="groundTexture" src="https://cdn.aframe.io/a-painter/images/floor.jpg">
    <img id="skyTexture" src="https://cdn.aframe.io/a-painter/images/sky.jpg">
  </a-assets>
  
  <a-cylinder id="ground" src="#groundTexture" radius="30" height="0.1"></a-cylinder>

  <a-sky id="background" src="#skyTexture" theta-length="90" radius="30"></a-sky>
</a-scene>

在 CodePen 中打开

添加体素

在我们的 VR 应用中,体素(voxels)的写法类似 <a-box>,但会添加一些自定义的 A-Frame 组件。不过让我们先大致了解实体-组件范式,来看看像 <a-box> 这样的图元是怎样合成的。

在这个部分,我们将会对若干 A-Frame 组件的实现做一些深入探讨。在实践中,我们经常会通过已由 A-Frame 社区开发人员编写好的 HTML 来使用组件,而不是从头构建它们。

实体-组件范式

在 A-Frame 场景中的每一个对象都是 <a-entity>,其本身什么也不能做,就像一个空 <div> 一样。我们将组件(不要和 Web Components 或 React Components 混淆)插入实体来给予其外观、行为和逻辑。

对于一个盒子来说,我们会为其配置及添加 A-Frame 的基础几何组件材质组件。组件使用 HTML 属性来表示,组件属性默认使用类似 CSS 样式的表示方法来表示。下面是一个 <a-box> 的基础组件拆解写法,可以看到 <a-box> 事实上包裹了若干组件:

<a-box color="red" depth="0.5" height="0.5" shader="flat" width="0.5"></a-box>

<a-entity geometry="primitive: box; depth: 0.5; height: 0.5; width 0.5"
          material="color: red; shader: standard"></a-entity>

使用组件的好处是它们的具有可组合性。我们可以通过混合和搭配一堆已有的组件来构造出各种各样的对象。

在 3D 开发中,我们可能构建出的对象类型在数量和复杂性上是无限的,因此我们需要一个简便的、全新的、非传统继承式的对象定义方法。与 2D web 相比,我们不再拘泥于使用一小撮固定的 HTML 元素并将它们嵌套在很深的层次结构中。

随机颜色组件

A-Frame 中的组件由 JavaScript 定义,它们可使用完整的 three.js 和 DOM APIs,它们可以做任何事。所有的对象都由一捆组件来定义。

现在将刚刚所描述的模式付诸实践,通过书写一个 A-Frame 组件,为我们的盒子设置随机颜色。组件通过 AFRAME.registerComponent 注册,我们可以定义 schema(组件的数据)以及生命周期方法(组件的逻辑)。对于随机颜色组件,我们并不需要设置 schema,因为它不能被配置。但我们会定义一个 init 处理函数,该函数会在组件首次附加到它的实体时被调用。

AFRAME.registerComponent('random-color', {
  init: function () {
    // ...
  }
});

对于随机颜色组件,我们的意图是为其附加的实体设置随机颜色。在组件的方法中,可以使用 this.el 访问实体的引用。

为了使用 JavaScript 来改变颜色,我们使用 .setAttribute() 来设置材质组件的颜色属性。A-Frame 只引入了少数 API,大多数 API 和原生 web 开发 API 保持一致。点此详细了解如何在 A-Frame 中使用 JavaScript 和 DOM API

我们还需要将 material 组件添加到预先初始化组件列表中,以保证材质不会被 material 组件覆盖掉。

AFRAME.registerComponent('random-color', {
  dependencies: ['material'],

  init: function () {
    // 将材质组件的颜色属性设置为随机颜色
    this.el.setAttribute('material', 'color', getRandomColor());
  }
});

function getRandomColor() {
  const letters = '0123456789ABCDEF';
  var color = '#';
  for (var i = 0; i < 6; i++ ) {
    color += letters[Math.floor(Math.random() * 16)];
  }
  return color;
}

在组件被注册后,我们可以直接使用 HTML 来链接该组件。A-Frame 框架中的所有代码都是对 HTML 的扩展,而且这些扩展可以用于其他对象和其他场景。很棒的是,开发者可以写一个向对象添加物理元素的组件,使用这个组件的人甚至不会察觉到 JavaScript 在他的场景中加入了这个物理元素!

注意力回到刚刚的盒子实体,将 random-color 作为 HTML 属性插入到 random-color 组件中。我们将组件保存为一个 JS 文件,然后在场景代码之前引用它:

<script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>
<script src="https://rawgit.com/ngokevin/kframe/csstricks/scenes/aincraft/components/random-color.js"></script>

<a-scene>
  <a-assets>
    <img id="groundTexture" src="https://cdn.aframe.io/a-painter/images/floor.jpg">
    <img id="skyTexture" src="https://cdn.aframe.io/a-painter/images/sky.jpg">
  </a-assets>
  
  <a-sky id="background" src="#skyTexture" theta-length="90" radius="30"></a-sky>

  <a-cylinder id="ground" src="#groundTexture" radius="30" height="0.1"></a-cylinder>
  
  <!-- 随机颜色的盒子 -->
  <a-entity geometry="primitive: box; depth: 0.5; height: 0.5; width 0.5"
            material="shader: standard"
            position="0 0.5 -2"
            random-color></a-entity>
</a-scene>

在 CodePen 中打开

组件可以插入到任何实体中,但并不需要像在传统继承模式中那样创建或扩展类。如果我们想在类似 <a-shpere><a-obj-model> 中附加组件,直接加就是了!

<!-- 在其他实体上重用并附加随机颜色组件 -->
<a-sphere random-color></a-sphere>
<a-obj-model src="model.obj" random-color></a-obj-model>

如果我们想要将这个组件分享给他人使用,也没问题。我们可以在 A-Frame 仓库中获取 A-Frame 生态系统中许多便利的组件,这类似 Unity 的 Asset Store。如果我们使用组件开发应用程序,那么就应当保证我们的代码在内部是模块化和可重用的!

对齐组件

我们将使用 snap 组件来将盒子对齐到网格以避免它们重叠。我们不会深入到该组件的实现原理,不过你可以看看 snap 组件的源代码(20 行 JavaScript 代码)。

将 snap 组件附加到盒子实体上,让盒子每半米对齐,同时使用 offset 来使盒子居中:

<a-entity
   geometry="primitive: box; height: 0.5; width: 0.5; depth: 0.5"
   material="shader: standard"
   random-color
   snap="offset: 0.25 0.25 0.25; snap: 0.5 0.5 0.5"></a-entity>

现在,我们有了一个由一捆组件构成的盒子实体,该实体可以用来描述我们场景中的所有体素(砖块)。

Mixins

我们可以创建 mixin 来定义可复用的组件集合。

与使用 <a-entity> 为场景添加一个对象不同,我们使用 <a-mixin> 来创建可复用的体素,使用它们就像使用预设实体一样。

<script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>
<script src="https://rawgit.com/ngokevin/kframe/csstricks/scenes/aincraft/components/random-color.js"></script>
<script src="https://rawgit.com/ngokevin/kframe/csstricks/scenes/aincraft/components/snap.js"></script>

<a-scene>
  <a-assets>
    <img id="groundTexture" src="https://cdn.aframe.io/a-painter/images/floor.jpg">
    <img id="skyTexture" src="https://cdn.aframe.io/a-painter/images/sky.jpg">
    <a-mixin id="voxel"
       geometry="primitive: box; height: 0.5; width: 0.5; depth: 0.5"
       material="shader: standard"
       random-color
       snap="offset: 0.25 0.25 0.25; snap: 0.5 0.5 0.5"></a-mixin>
  </a-assets>

  <a-sky id="background" src="#skyTexture" theta-length="90" radius="30"></a-sky>

  <a-cylinder id="ground" src="#groundTexture" radius="30" height="0.1"></a-cylinder>
  
  <a-entity mixin="voxel" position="-1 0 -2"></a-entity>
  <a-entity mixin="voxel" position="0 0 -2"></a-entity>
  <a-entity mixin="voxel" position="0 1 -2">
    <a-animation attribute="rotation" to="0 360 0" repeat="indefinite"></a-animation>
  </a-entity>
  <a-entity mixin="voxel" position="1 0 -2"></a-entity>
</a-scene>

在 CodePen 中打开

随后我们使用 mixin 添加了若干体素:

<a-entity mixin="voxel" position="-1 0 -2"></a-entity>
<a-entity mixin="voxel" position="0 0 -2"></a-entity>
<a-entity mixin="voxel" position="0 1 -2">
  <a-animation attribute="rotation" to="0 360 0" repeat="indefinite"></a-animation>
</a-entity>
<a-entity mixin="voxel" position="1 0 -2"></a-entity>

接下来,我们将通过使用追踪控制器根据用户交互来动态创建体素。让我们开始向程序中添加一双手吧。

添加手部控制器

添加 HTC Vive 或 Oculus Touch 追踪控制器非常简单:

<!-- Vive -->
<a-entity vive-controls="hand: left"></a-entity>
<a-entity vive-controls="hand: right"></a-entity>

<!-- Rift -->
<a-entity oculus-touch-controls="hand: left"></a-entity>
<a-entity oculus-touch-controls="hand: right"></a-entity>

我们将使用抽象的 hand-controls 组件来同时兼容 Vive 和 Rift 的控制,它提供基本的手模型。左手负责移动位置,右手负责放置砖块。

<a-entity id="teleHand" hand-controls="left"></a-entity>
<a-entity id="blockHand" hand-controls="right"></a-entity>

为左手添加瞬移功能

我们将为左手增加瞬移的能力,当按住左手控制器按钮时,从控制器显示一条弧线,松开手时,瞬移到弧线末端的位置。在此之前,我们已经自己写了一个实现随机颜色的 A-Frame 组件。

但也可以使用社区中已有的开源组件,然后直接通过 HTML 使用它们!

对于瞬移来说,有一个来自于 @fernandojsg 的瞬移控制组件。遵循 README,我们使用 <script> 标签引入 teleport-controls 组件,并将其附加到控制器实体上。

<script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-teleport-controls@0.2.x/dist/aframe-teleport-controls.min.js"></script>

<!-- ... -->

<a-entity id="teleHand" hand-controls="left" teleport-controls></a-entity>
<a-entity id="blockHand" hand-controls="right"></a-entity>

随后我们来配置 teleport-controls 组件,将瞬移的 type 设置为弧线。默认来说,teleport-controls 的瞬移只会发生在地面上,但我们也可以指定 collisionEntities 通过选择器来允许瞬移到砖块地面上。这些属性是 teleport-controls 组件创建的 API 的一部分。

<a-entity id="teleHand" hand-controls="left" teleport-controls="type: parabolic; collisionEntities: [mixin='voxel'], #ground"></a-entity>

就是这样!只要一个 script 标签和一个 HTML 属性,我们就能瞬移了。A-Frame 仓库中可以找到更多很酷的组件。

为右手添加体素生成器功能

在 2D 应用程序中,对象内置了处理点击的能力,而在 WebVR 中对象并没有这样的能力,需要我们自己来提供。幸运的是,A-Frame 拥有许多处理交互的组件。VR 中用于类似光标点击的场景方法是使用 raycaster,它射出一道激光并返回激光命中的物体。然后我们通过监听交互事件及查看 raycaster 来获得命中点信息。

A-Frame 提供基于注视点的光标(注:就像 FPS 游戏的准心那样),可以利用此光标点击正在注视的物体,但也有可用的控制器光标组件来根据 VR 追踪控制器的位置发射激光,就像刚刚使用 teleport-controls 组件那样,我们通过 script 标签将 controller-cursor 组件引入,然后附加到实体上。这次轮到右手了:

<script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-teleport-controls@0.2.x/dist/aframe-teleport-controls.min.js"></script>
<script src="https://unpkg.com/aframe-controller-cursor-component@0.2.x/dist/aframe-controller-cursor-component.min.js"></script>

<!-- ... -->

<a-entity id="teleHand" hand-controls="left" teleport-controls="type: parabolic; collisionEntities: [mixin='voxel'], #ground"></a-entity>
<a-entity id="blockHand" hand-controls="right" controller-cursor></a-entity>

现在当我们按下追踪控制器上的按钮时,controller-cursor 组件将同时触发控制器和交互实体的 click 事件。A-Frame 也提供了诸如 mouseentermouseleave 这样的事件。事件包含了用户交互的详细信息。

这赋予了我们点击的能力,但我们还得写一些响应点击事件处理生成砖块的逻辑。可以使用事件监听器及 document.createElement 来完成:

document.querySelector('#blockHand').addEventListener(`click`, function (evt) {
  // 创建一个砖块实体
  var newVoxelEl = document.createElement('a-entity');

  // 使用 mixin 来将其变为体素
  newVoxelEl.setAttribute('mixin', 'voxel');

  // 使用命中点的数据来设置砖块位置。
  // 上文所述的 `snap` 组件是 mixin 的一部分,它将会把砖块对齐到最近的半米
  newVoxelEl.setAttribute('position', evt.detail.intersection.point);

  // 使用 `appendChild` 添加到场景中
  this.appendChild(newVoxelEl);
});

为了概括性地处理在命中点创建实体这样的需求,我们创建了 intersection-spawn 组件,该组件接受任何事件和属性列表的配置。我们不会详细讨论其实现,但你可以在 GitHub 上查看这个简单的 intersection-spawn 组件的源码。我们将 intersection-spawn 的能力附加到右手上:

<a-entity id="blockHand" hand-controls="right" controller-cursor intersection-spawn="event: click; mixin: voxel"></a-entity>

现在当我们点击时,就可以生成体素了!

添加移动设备和桌面设备支持

我们通过组合组件了解到了如何构建一个自定义类型的对象(例如,一个具有点击功能和点击时生成砖块的手部控制器)。组件的好处之一是它们可以在不同的上下文中被重用。我们将 intersection-spawn 组件和基于注视点的 cursor 组件结合起来,便可以在一点都不改变组件的情况下,实现在移动设备和桌面设备中生成砖块的功能了。

<a-entity id="blockHand" hand-controls="right" controller-cursor intersection-spawn="event: click; mixin: voxel"></a-entity>

<a-camera>
  <a-cursor intersection-spawn="event: click; mixin: voxel"></a-cursor>
</a-camera>

试试看

在 GitHub 上查看源码

我们的 VR 体素构建器最终使用 11 个 HTML 元素实现。我们可以在桌面或移动设备上预览它。在桌面设备上,我们可以通过拖动和点击来生成砖块;在移动设备上,我们可以平移设备和点击屏幕来生成砖块。

<script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-teleport-controls@0.2.x/dist/aframe-teleport-controls.min.js"></script>
<script src="https://unpkg.com/aframe-controller-cursor-component@0.2.x/dist/aframe-controller-cursor-component.min.js"></script>
<script src="https://rawgit.com/ngokevin/kframe/csstricks/scenes/aincraft/components/random-color.js"></script>
<script src="https://rawgit.com/ngokevin/kframe/csstricks/scenes/aincraft/components/snap.js"></script>
<script src="https://rawgit.com/ngokevin/kframe/csstricks/scenes/aincraft/components/intersection-spawn.js"></script>

<body>
  <a-scene>
    <a-assets>
      <img id="groundTexture" src="https://cdn.aframe.io/a-painter/images/floor.jpg">
      <img id="skyTexture" src="https://cdn.aframe.io/a-painter/images/sky.jpg">
      <a-mixin id="voxel"
         geometry="primitive: box; height: 0.5; width: 0.5; depth: 0.5"
         material="shader: standard"
         random-color
         snap="offset: 0.25 0.25 0.25; snap: 0.5 0.5 0.5"
      ></a-mixin>
    </a-assets>

    <a-cylinder id="ground" src="#groundTexture" radius="30" height="0.1"></a-cylinder>

    <a-sky id="background" src="#skyTexture" theta-length="90" radius="30"></a-sky>

    <!-- Hands. -->
    <a-entity id="teleHand" hand-controls="left" teleport-controls="type: parabolic; collisionEntities: [mixin='voxel'], #ground"></a-entity>
    <a-entity id="blockHand" hand-controls="right" controller-cursor intersection-spawn="event: click; mixin: voxel"></a-entity>

    <!-- Camera. -->
    <a-camera>
      <a-cursor intersection-spawn="event: click; mixin: voxel"></a-cursor>
    </a-camera>
  </a-scene>
</body>

在 CodePen 中打开

如果你有 VR 头盔(例如 HTC Vive、Oculus Rift + Touch),那么可以找一个支持 WebVR 的浏览器并打开示例。

如果你想使用桌面或移动设备观看 VR 是什么样的,可以查看录制好的 VR 动作捕捉和手势演示

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏HT

拓扑图弹力布局呈现Flickr图片搜索结果

十年前有值得分享的图片我都存在Flickr上,可惜yahoo收购了Flickr之后堕落​好多年,最近yahoo在梅姐带领下Flickr团队终于恢复了生机,个人免...

1859
来自专栏WeTest质量开放平台团队的专栏

Android P专区免费开放 -- 同样的Android,不同的体验

原文链接:http://wetest.qq.com/lab/view/376.html

4743
来自专栏比原链

剥开比原看代码17:比原是如何显示交易的详细信息的?

Gitee地址:https://gitee.com/BytomBlockchain/bytom

441
来自专栏魏琼东

.NET快速开发实践之应用IExtenderProvider实现对象与UI控件的绑定

         现在企业级的信息系统应用开发之中,数据层采用了ORM,解放了之前开发最大工作量的核对SQL语句,我到现在还曾经记得在2002年实医院管理信息系...

1726
来自专栏企鹅号快讯

前端工程师面试题汇总

作者:@markyun markyun.github.io/2015/Front-end-Developer-Questions/ HTML Doctype作用...

2248
来自专栏知晓程序

一个天气小程序和他们跳过的 47 个坑,你该如何避免?

1202
来自专栏java学习

Intellij IDEA神器居然还有这些小技巧

https://blog.csdn.net/linsongbin1/article/details/80211919

942
来自专栏Jaycekon

Phantomjs+Nodejs+Mysql数据抓取(1.数据抓取)

概要: 这篇博文主要讲一下如何使用Phantomjs进行数据抓取,这里面抓的网站是太平洋电脑网估价的内容。主要是对电脑笔记本以及他们的属性进行抓取,然后在使...

3715
来自专栏娱乐心理测试

小程序完整demo

1695
来自专栏V站

WordPress丨AutoTagSlug插件优化标签Url格式

大部分WordPress用户都是懒懒散散,SEO不做好,导致收录频率极低,整天抱怨凉凉。今天无聊时发现Tag可以优化,才发现原来西顾博客也有瑕疵。在此介绍一款插...

1888

扫码关注云+社区