我是 Kevin Ngo,Mozilla VR 团队的虚拟现实 Web 开发人员,也是 A-Frame 的核心开发者。今天,我们将介绍如何构建一个适用于 HTC Vive、Oculus Rift、Samsung GearVR、Google Cardboard、桌面和移动设备的房间级 WebVR Minecraft 演示。该演示将使用 A-Frame 构建,仅需 11 个 HTML 元素!

A-Frame
几年前,Mozilla 发明并率先推出了 WebVR,这是一个用于在 Firefox 的实验版本中浏览器中创建沉浸式 VR 体验的 JavaScript API。从那时起,WebVR 已经获得了 Google、Microsoft、Samsung 和 Oculus 等其他公司的广泛支持。而且它现在将在几个月内默认启用并发布在 Firefox 中!
为什么选择 WebVR?Web 为 VR 带来了开放性;在 Web 上,内容不受守门人的控制,用户也不会被限制在封闭花园中。Web 还为 VR 带来了连接性;在 Web 上,我们可以像今天一样使用链接从一个世界跳转到另一个世界,从一个页面跳转到另一个页面。随着 WebGL 的成熟以及 WebAssembly 和 Service Workers 等新规范的出现,Web 已为 VR 做好准备。
Mozilla VR 团队 创建了 A-Frame 来启动 WebVR 生态系统,使 Web 开发人员能够构建 3D 和 VR 世界。

A-Frame 是一个用于构建虚拟现实体验的 Web 框架。A-Frame 基于 HTML 和 实体-组件模式。HTML 是所有计算中最易理解的语言,使任何人都可以 入门。下面是仅用 HTML 编写的完整 3D 和 VR 场景,可在多个 VR 平台以及桌面和移动设备上运行
查看 CodePen 上的 mozvr (@mozvr) 编写的 Hello World — A-Frame。
就是这样!仅指定一行 HTML (<a-scene>
) 将处理所有 3D 和 VR 样板:画布、场景、渲染器、渲染循环、相机和灯光。然后,我们通过场景中的子元素添加对象。无需构建步骤,只需一个便于复制粘贴的 HTML 文件。

并且我们可以像使用 标准 JavaScript 和 DOM API (例如,querySelector
、getAttribute
、addEventListener
、setAttribute
) 一样动态查询和操作 A-Frame 的 HTML。
// Query scene graph using `querySelector`.
var sceneEl = document.querySelector('a-scene');
var boxEl = sceneEl.querySelector('a-box');
// Get data about entity with `getAttribute`.
console.log(box.getAttribute('position'));
// >> {x: -1, y: 0.5, z: -3}
// Add event listener with `addEventListener`.
box.addEventListener('click', function () {
// Modify entity with `setAttribute`.
box.setAttribute('color', 'red');
});

而且由于它只是 HTML 和 JavaScript,A-Frame 可以与许多现有框架和库一起使用

虽然 A-Frame 的 HTML 看起来很简单,但 A-Frame 的 API 比仅仅使 3D 声明性要强大得多。A-Frame 是一个 实体-组件-系统 (ECS) 框架。值得注意的是,ECS 是游戏开发中流行的一种模式,Unity 使用了这种模式。该概念遵循
- 场景中的所有对象都是实体,一个自身不执行任何操作的空对象,类似于一个空的
<div>
。在 A-Frame 中,实体在 DOM 中表示为一个元素。 - 然后,我们将组件插入这些实体以提供外观、行为和功能。在 A-Frame 中,组件在 JavaScript 中注册,并且可以使其执行任何操作。它们可以完全访问 three.js 和 DOM API。组件可以在注册后在 HTML 中附加到实体。
ECS 的优势在于它可以组合;我们可以混合和匹配这些可重用的组件来构建更复杂的 3D 对象。A-Frame 更进一步,使其成为声明性的,并且成为 DOM 的一部分,正如我们将在 Minecraft 示例中看到的那样。
示例骨架
现在来演示。我们将构建一个基本的 VR 体素构建器。体素构建器主要用于具有位置跟踪和跟踪控制器的房间级 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>
来更好地与控制器将使用的射线投射器配合使用。圆柱体的半径将为 30 米,以匹配我们稍后将添加的天空半径。请注意,A-Frame 单位为米,以匹配从 WebVR API 返回的真实世界单位。
我们将使用的地面纹理托管在 https://cdn.aframe.io/a-painter/images/floor.jpg
上。我们将纹理添加到我们的资产中,并创建一个指向该纹理的薄圆柱体实体
查看 CodePen 上的 mozvr (@mozvr) 编写的 Minecraft VR 演示 (第 1 部分)。
预加载资产
通过 src
属性指定 URL 将在运行时加载纹理。
由于网络请求会对渲染性能产生负面影响,我们可以预加载纹理,以便场景在资产被获取之前不会开始渲染。我们可以使用 资产管理系统 来实现这一点。
我们将 <a-assets>
放入我们的 <a-scene>
中,将资产 (例如,图像、视频、模型、声音) 放入 <a-assets>
中,并通过选择器 (例如,#myTexture
) 从我们的实体指向它们。
让我们将地面纹理移动到 <a-assets>
中,以便使用 <img>
元素预加载它
查看 CodePen 上的 mozvr (@mozvr) 编写的 Minecraft VR 演示 (第 2 部分:预加载纹理)。
添加背景
让我们使用 <a-sky>
元素 为我们的 <a-scene>
添加 360° 背景。<a-sky>
是一个大的 3D 球体,内侧映射了一个材质。就像普通的图像一样,<a-sky>
可以使用 src
获取图像路径。这最终使我们能够用一行 HTML 实现沉浸式 360° 图像。作为以后的练习,尝试使用一些来自 Flickr 的
等矩形池 的 360° 图像。
我们可以添加一个纯色背景 (例如,<a-sky color="#333"></a-sky>
) 或 渐变,但让我们使用图像添加一个纹理背景。我们使用的图像托管在 https://cdn.aframe.io/a-painter/images/sky.jpg
上。我们使用的图像纹理覆盖了半球,因此我们将使用 theta-length="90"
将球体切成两半,并将球体的半径设置为 30 米,以匹配地面
查看 CodePen 上的 mozvr (@mozvr) 编写的 Minecraft VR 演示 (第 3 部分:添加背景)。
添加体素
我们 VR 应用程序中的体素就像 <a-box>
一样,但附加了一些自定义 A-Frame 组件。但首先让我们看一下实体-组件模式。让我们看看易于使用的基元 (例如,<a-box>
) 在幕后是如何组合的。
本节将深入介绍几个 A-Frame 组件的实现。但是,在实践中,我们通常会通过 A-Frame 社区开发人员已经编写的 HTML 使用组件,而不是从头开始构建它们。
实体-组件模式
A-Frame 场景中的每个对象都是 <a-entity>
,它本身什么也不做,就像一个空的 <div>
。我们将组件(不要与 Web 或 React 组件混淆)插入到该实体中,以提供外观、行为和逻辑。
对于一个盒子,我们附加并配置 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 网络进行对比,在 2D 网络中,我们使用一小部分固定的 HTML 元素进行开发,并将它们放到层次结构中。
随机颜色组件
A-Frame 中的组件在 JavaScript 中定义,它们可以完全访问 three.js 和 DOM API;它们可以做任何事情。我们将所有对象定义为组件的捆绑。
我们将通过编写一个 A-Frame 组件来将随机颜色设置为我们的盒子,来将该模式付诸行动。组件使用 AFRAME.registerComponent
注册。我们可以定义一个模式(组件的数据)和生命周期处理程序方法(组件的逻辑)。对于随机颜色组件,我们不会设置模式,因为它不可配置。但我们将定义 init
处理程序,该处理程序在附加组件时只调用一次
AFRAME.registerComponent('random-color', {
init: function () {
// ...
}
});
对于随机颜色组件,我们希望在附加该组件的实体上设置随机颜色。组件通过处理程序方法从 this.el
中引用实体。
为了使用 JavaScript 更改颜色,我们使用 .setAttribute()
更改材质组件的颜色属性。A-Frame 对一些 DOM API 进行了一些修改,但这些 API 大致上反映了
普通 Web 开发。 了解有关使用 JavaScript 和 DOM API 与
A-Frame 的更多信息。
我们还将 material
组件添加到应该在此组件之前初始化的组件列表中,这样我们的材质就不会被覆盖。
AFRAME.registerComponent('random-color', {
dependencies: ['material'],
init: function () {
// Set material component's color property to a random color.
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 文件,并在场景之前将其包含进来
查看代码笔 我的世界 VR 演示(第 4 部分:随机颜色组件) 由 mozvr (@mozvr) 在 代码笔 上创建。
组件可以插入到任何实体上,而无需像传统继承那样创建或扩展类。如果我们想将其附加到例如 <a-sphere>
或 <a-obj-model>
,我们也可以做到!
<!-- Reusing and attaching the random color component to other entities. -->
<a-sphere random-color></a-sphere>
<a-obj-model src="model.obj" random-color></a-obj-model>
如果我们想将此组件分享给其他人使用,我们也可以做到。我们从 A-Frame 注册表 中精选了许多来自生态系统的有用组件,类似于 Unity Asset Store。如果我们使用组件开发应用程序,所有代码都天生具有模块化和可重用性!
捕捉组件
我们将有一个 snap
组件,用于将我们的盒子捕捉到网格上,这样它们就不会重叠。我们不会深入讨论这个组件的实现细节,但你可以查看 捕捉组件的源代码(20 行 JavaScript 代码)。
我们将捕捉组件附加到我们的盒子,使其捕捉到每半米,并且还使用偏移量来居中盒子
<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>
现在我们有一个盒子实体,它表示为一组组件的捆绑,可以用来描述场景中的所有体素。
混合
我们可以创建一个 混合 来定义可重用的组件捆绑。
我们将使用 <a-mixin>
来描述场景,而不是 <a-entity>
,它将对象添加到场景中,它可以重复使用来创建体素,就像预制件一样
查看代码笔 我的世界 VR 演示(第 5 部分:混合) 由 mozvr (@mozvr) 在 代码笔 上创建。
我们已经使用该混合添加了体素
<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>
<!-- Or 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 制作的 传送控制器组件。按照自述文件,我们通过 <script>
标签添加组件,并在实体的控制器上设置 teleport-controls
组件
<script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>
<script src="https://unpkg.com/[email protected]/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
组件以使用弧线类型的传送。默认情况下,teleport-controls
只能在地面上传送,但我们可以使用 collisionEntities
指定在地面上和方块上传送,使用选择器。这些属性是 teleport-controls
组件创建时提供的 API 的一部分
<a-entity id="teleHand" hand-controls="left" teleport-controls="type: parabolic; collisionEntities: [mixin='voxel'], #ground"></a-entity>
就是这样!一个脚本标签和一个 HTML 属性,我们就可以传送了。要了解更多酷炫的组件,请查看 A-Frame 注册表。
向右手添加体素生成器
在 WebVR 中,点击对象的功能不像 2D 应用程序那样内置。我们必须自己提供这个功能。幸运的是,A-Frame 有许多组件来处理交互。在 VR 中,一种常见的鼠标点击式方法是使用射线投射器,它发射出一条激光,并返回与之相交的对象。然后,我们通过监听交互事件并检查射线投射器是否有相交来实现鼠标状态。
A-Frame 提供了一个基于注视的鼠标,用于通过注视对象进行点击,但也提供了一个可用的 控制器鼠标组件,它将点击激光附加到 VR 跟踪控制器。与 teleport-controls
组件一样,我们包含脚本标签并附加 controller-cursor
组件。这次附加到右手
<script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/aframe-teleport-controls.min.js"></script>
<script src="https://unpkg.com/[email protected]/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
事件。还提供了 mouseenter
、mouseleave
等事件。该事件包含有关相交的详细信息。
这为我们提供了点击功能,但我们必须连接一些代码来处理这些点击,以生成方块。我们可以使用事件监听器和 document.createElement
document.querySelector('#blockHand').addEventListener(`click`, function (evt) {
// Create a blank entity.
var newVoxelEl = document.createElement('a-entity');
// Use the mixin to make it a voxel.
newVoxelEl.setAttribute('mixin', 'voxel');
// Set the position using intersection point. The `snap` component above which
// is part of the mixin will snap it to the closest half meter.
newVoxelEl.setAttribute('position', evt.detail.intersection.point);
// Add to the scene with `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>
试试吧!
我们的 VR 体素构建器最终只有 **11 个 HTML 元素**。我们可以在桌面和移动端预览它。在桌面端,我们可以拖放并点击生成黑色的方块。在移动端,我们可以围绕设备进行平移,并点击生成方块。
查看 CodePen 上的 Minecraft VR 演示(最终版),由 mozvr (@mozvr) 创建。
如果你有一个 VR 头戴设备(例如,HTC Vive、Oculus Rift + Touch),请使用 支持 WebVR 的浏览器 并 **转到演示**。VR 通过插入 HTC Vive 或 Oculus Rift 并 使用支持 WebVR 的浏览器。
如果你想从桌面或移动设备上查看 VR 的外观,请 查看带有预先录制 VR 运动捕捉和手势的演示。
展望未来
- 125+ 个 GitHub 贡献者
- 240 个开放的/1250 个已关闭的 GitHub 问题
- 3000 名 Slack 成员
- 数百个 VR 项目在 每周的 A-Frame 博客 上亮相
A-Frame 是 WebVR 框架,它旨在革新 Web 上的 VR 外观,同时让任何人都能轻松参与 3D 和 VR 的开发。HTML 保持了极简,同时由底层的实体-组件模式支持。这些实验将成为开放元宇宙的基础,开放元宇宙是共享的、持久的、互联的虚拟空间,每个人都将生活其中。
VR 仍处于早期阶段,尤其是 WebVR,但浏览器很快将默认支持 WebVR,面向数亿用户。Web 开发人员应参与进来,让每个人都能使用 VR。

太棒了!感谢发布这篇文章。
太酷了。我想我可能可以用它来做婚礼网站。我还没有想到任何可以轻松地向他们推销这个想法的客户。
我喜欢基于 HTML 的概念,它比复杂的纯 JavaScript 交互更易于使用。
我的婚礼实际上有一个 A-Frame VR 网站,通过 Google Cardboard 展示了我和妻子的照片,包围着用户。
我知道很多人和机构通过进行面向未来的 WebVR 实验来吸引客户和 Web 开发工作。国际特赦组织英国分部、NPR 和《华盛顿邮报》等组织都将 A-Frame 用于一些特色文章。
Keven,你是如何在移动设备上处理性能的?你使用了哪款手机?我尝试了一个使用 a-frame 和仅在“a-sky”元素上放置一张 360 度照片的快速测试。在我的 iPhone 6 和 HTC MOne 8 上,性能非常糟糕。我不想让婚礼上的宾客因晕动症而呕吐 :)
我们更关注桌面(即“真实”)VR。通过它,你可以实现非常低的延迟和 90+ 帧率。
但是移动端…不同。Cardboard 非常适合对话,可以快速展示,但它并不是你真正会进入的东西。没有人会感到不适,因为你只是将其举到脸上 10 秒钟,然后就会放下,因为填充/Cardboard VR 并不足以吸引注意力。
因为它使用设备运动传感器进行填充,所以性能会很糟糕,因为至少 Android 会限制这些数据,而 iOS Safari 还是 iOS Safari。
我特地询问你在婚礼上做了什么…你说你使用了 Google Cardboard。我使用 Google Cardboard 完成了一些商业项目,它们吸引了人们几分钟的注意力。我必须使用 Unity 制作原生应用程序才能获得可用的性能。我希望移动端的 Web VR 能够很快达到这个水平。
Android 上的最新版 Chrome 不需要填充程序,但我没有注意到与之前版本相比有任何性能改进。