一、如何构建基本场景?
让我们从构建一个基本的 A-Frame场景开始。为此,我们需要对 HTML 有基本的了解。我们将学习如何:
-
使用原语添加 3D 实体(即对象)
-
通过位置、旋转、缩放来变换 3D 空间中的实体
-
添加环境
-
添加纹理
-
使用动画和事件添加基本交互性
-
添加文字
1.从 HTML 开始
我们从一个最小的 HTML 结构开始:
<html>
<head>
<script src="https://aframe.io/releases/1.6.0/aframe.min.js"></script>
</head>
<body>
<a-scene>
</a-scene>
</body>
</html>
我们将 A-Frame 作为脚本标记包含在 <head>
中,指向 CDN 上托管的 A-Frame 构建。它必须包含在 <a-scene>
之前,因为 A-Frame 注册了必须在附加 <a-scene>
之前定义的自定义 HTML 元素,否则 <a-scene>
将不执行任何操作。
接下来,我们将 <a-scene>
包含在 <body>
中。 <a-scene>
将包含场景中的每个实体。 <a-scene>
处理 3D 所需的所有设置:设置 WebGL、画布、相机、灯光、渲染器、渲染循环以及 HTC Vive、Oculus 等平台上开箱即用的 WebVR 支持Rift、Samsung GearVR 和智能手机 (Google Cardboard)。 <a-scene>
本身就减轻了我们很多负担!
2.添加实体
在我们的 <a-scene>
中,我们使用 A-Frame 的标准基元之一 <a-box>
附加 3D 实体。我们可以像使用普通 HTML 元素一样使用 <a-box>
,定义标签并使用 HTML 属性来自定义它。 A-Frame 附带的其他一些基元示例包括 <a-cylinder>
、 <a-plane>
或 <a-sphere>
。
这里我们定义了颜色 <a-box>
,更多属性请参见 <a-box>
的文档(例如 width
、 height
、 depth
)。
<a-scene>
<a-box color="red"></a-box>
</a-scene>
附带说明一下,原语是 A-Frame 易于使用的 HTML 元素,它包装了底层实体组件组件。它们很方便,但 <a-box>
下面是带有几何和材质组件的 <a-entity>
:
<a-entity id="box" geometry="primitive: box" material="color: red"></a-entity>
但是,由于默认相机和盒子位于 0 0 0
原点的默认位置,因此除非移动盒子,否则我们将无法看到盒子。我们可以通过使用位置组件在 3D 空间中变换盒子来做到这一点。
3.以3D方式变换实体
我们首先回顾一下 3D 空间。 A 型框架使用右手坐标系。使用默认相机方向:正 X 轴向右延伸,正 Y 轴向上延伸,正 Z 轴从屏幕向我们延伸:
A-Frame 的距离单位为米,因为 WebVR API 返回的姿势数据以米为单位。在设计 VR 场景时,考虑我们创建的实体的现实世界规模非常重要。带有 height="10"
的盒子在我们的计算机屏幕上可能看起来很正常,但在 VR 中该盒子会显得很大。
A-Frame 中 A-Frame 的旋转单位为度,但在传递给 Three.js 时会在内部转换为弧度。要确定旋转的正方向,请使用右手定则。将我们的拇指向下指向正轴的方向,并将手指围绕正旋转方向卷曲的方向。
要平移、旋转和缩放盒子,我们可以更改位置、旋转和缩放组件。让我们首先应用旋转和缩放组件:
<a-scene>
<a-box position="0 2 0" rotation="0 45 45" scale="2 4 2">
<a-sphere position="1 0 3"></a-sphere>
</a-box>
</a-scene>
这将使我们的盒子旋转一定角度并使其大小加倍。
(1)父子变换
A-Frame HTML 表示 3D 场景图。在场景图中,实体可以有一个父级和多个子级。子实体从其父实体继承变换(即位置、旋转和缩放)。
例如,我们可以将一个球体作为一个盒子的子元素:
<a-scene>
<a-box position="0 2 0" rotation="0 45 45" scale="2 4 2">
<a-sphere position="1 0 3"></a-sphere>
</a-box>
</a-scene>
如果我们计算球体的世界位置,它将是 1 2 3
,通过将球体的父位置与其自身位置组合来实现。类似地,对于旋转和缩放,球体将继承盒子的旋转和缩放。球体也会像其父盒子一样旋转和拉伸。如果盒子要改变其位置、旋转或比例,它将立即应用于并影响球体。
如果我们将一个圆柱体作为子项添加到球体中,则圆柱体的变换将受到球体和盒子变换的影响。在 Three.js 的底层,这是通过将变换矩阵相乘来完成的。幸运的是,我们不必考虑这个!
(2)将我们的Box放在相机前面
现在让我们回到从一开始就让我们的盒子对相机可见。我们可以使用位置组件将盒子在负 Z 轴上向后移动 5 米。我们还必须在正 Y 轴上将其向上移动 2 米,这样盒子就不会与地面相交,因为我们缩放了盒子并且缩放是从中心发生的:
<a-scene>
<a-box color="red" position="0 2 -5" rotation="0 45 45" scale="2 2 2"></a-box>
</a-scene>
现在我们看到了我们的盒子!
(3)默认控件
对于平板显示器(即笔记本电脑、台式机),默认控制方案让我们通过单击拖动鼠标来环顾四周,并使用 WASD
或箭头键四处移动。在移动设备上,我们可以平移手机来旋转相机。尽管 A-Frame 是为 WebVR 量身定制的,但这种默认控制方案允许人们无需耳机即可查看场景。
连接 VR 头显(例如 Oculus Rift、HTC Vive),点击护目镜图标进入 VR 后,我们可以体验沉浸式 VR 场景。如果房间规模可用,我们可以在场景中走动!
4.添加环境
A-Frame 允许开发人员创建和共享可重用组件,以供其他人轻松使用。 @feiss 的环境组件通过一行 HTML 程序为我们生成了各种完整的环境。环境组件是一种直观地引导我们的 VR 应用程序的好方法,它提供了十多个具有大量参数的环境:
首先,在 A-Frame 之后使用脚本标记包含环境组件:
<head>
<script src="https://aframe.io/releases/1.6.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-environment-component/dist/aframe-environment-component.min.js"></script>
</head>
然后在场景中添加一个附加了环境组件的实体。我们可以指定一个预设(例如 forest
)以及许多其他参数(例如 200 棵树):
<a-scene>
<a-box color="red" position="0 2 -5" rotation="0 45 45" scale="2 2 2"></a-box>
<!-- Out of the box environment! -->
<a-entity environment="preset: forest; dressingAmount: 500"></a-entity>
</a-scene>
5.应用图像纹理
确保您使用本地服务器提供 HTML,以便正确加载纹理。
我们可以使用 src
属性将图像纹理应用到带有图像、视频或 <canvas>
的框,就像使用普通的 <img>
元素一样。我们还应该删除我们设置的 color="red"
,以便颜色不会与纹理混合。默认材质颜色是 white
,因此删除颜色属性就足够了。
<a-scene>
<a-box src="https://i.imgur.com/mYmmbrp.jpg" position="0 2 -5" rotation="0 45 45"
scale="2 2 2"></a-box>
<a-sky color="#222"></a-sky>
</a-scene>
现在我们将看到我们的盒子带有从网上提取的图像纹理:
优化:使用资产管理系统
但是,我们建议使用资产管理系统来提高性能。资产管理系统使浏览器更容易缓存资产(例如图像、视频、模型),并且 A-Frame 将确保在渲染之前获取所有资产。
如果我们在资产管理系统中定义一个 <img>
,那么后面的 Three.js 就不必在内部创建一个 <img>
。自己创建 <img>
还为我们提供了更多控制权,并让我们可以在多个实体之间重用纹理。 A-Frame 也足够智能,可以在必要时设置 crossOrigin
和其他此类属性。
要将资产管理系统用于图像纹理:
-
将
<a-assets>
添加到场景中。 -
将纹理定义为
<a-assets>
下的<img>
。 -
为
<img>
提供 HTML ID(例如id="boxTexture"
)。 -
使用 DOM 选择器格式 (
src="#boxTexture"
) 中的 ID 引用资源。
<a-scene>
<a-assets>
<img id="boxTexture" src="https://i.imgur.com/mYmmbrp.jpg">
</a-assets>
<a-box src="#boxTexture" position="0 2 -5" rotation="0 45 45" scale="2 2 2"></a-box>
<a-sky color="#222"></a-sky>
</a-scene>
6.创建自定义环境(可选)
之前我们让环境组件生成环境。不过,了解一些有关为学习目的创建基本环境的知识还是有好处的。
(1)为场景添加背景
我们可以使用 <a-sky>
添加围绕场景的背景。 <a-sky>
是应用于大球体内部的材质,可以是平面颜色、360°图像或360°视频。例如,深灰色背景将是:
<a-scene>
<a-box color="red" position="0 2 -5" rotation="0 45 45" scale="2 2 2"></a-box>
<a-sky color="#222"></a-sky>
</a-scene>
或者我们可以通过使用 src
而不是 color
来使用图像纹理来获取 360° 背景图像:
<a-scene>
<a-assets>
<img id="boxTexture" src="https://i.imgur.com/mYmmbrp.jpg">
<img id="skyTexture"
src="https://cdn.aframe.io/360-image-gallery-boilerplate/img/sechelt.jpg">
</a-assets>
<a-box src="#boxTexture" position="0 2 -5" rotation="0 45 45" scale="2 2 2"></a-box>
<a-sky src="#skyTexture"></a-sky>
</a-scene>
(2)添加地面
要添加地面,我们可以使用 <a-plane>
。默认情况下,平面的方向平行于 XY 轴。为了使其与地面平行,我们需要将其定向为沿 XZ 轴。我们可以通过将平面在 X 轴上旋转负 90° 来实现。
<a-plane rotation="-90 0 0"></a-plane>
我们希望地面很大,所以我们可以增加 width
和 height
。让我们将其设置为每个方向 30 米:
<a-plane rotation="-90 0 0" width="30" height="30"></a-plane>
然后我们可以将图像纹理应用到地面:
<a-assets>
<!-- ... -->
<img id="groundTexture" src="https://cdn.aframe.io/a-painter/images/floor.jpg">
<!-- ... -->
</a-assets>
<!-- ... -->
<a-plane src="#groundTexture" rotation="-90 0 0" width="30" height="30"></a-plane>
<!-- ... -->
为了平铺我们的纹理,我们可以使用 repeat
属性。 repeat
取两个数字,X方向重复多少次,Y方向重复多少次(通常称为纹理的U和V)。
<a-plane src="#groundTexture" rotation="-90 0 0" width="30" height="30"
repeat="10 10"></a-plane>
(3)调整灯光
我们可以使用 <a-light>s
更改场景的照明方式。默认情况下,如果我们不指定任何灯光,A-Frame 会添加环境光和定向光。如果 A-Frame 没有为我们添加灯光,场景将会是黑色的。然而,一旦我们添加了自己的灯光,默认的灯光设置就会被删除并替换为我们的设置。
我们将添加一个带有轻微蓝绿色色调的环境光,与天空相匹配。环境光应用于场景中的所有实体(假设它们至少应用了默认材质)。
我们还将添加一个点光源。点光源就像灯泡;我们可以将它们放置在场景周围,点光源在实体上的效果取决于它到实体的距离:
<a-scene>
<a-assets>
<img id="boxTexture" src="https://i.imgur.com/mYmmbrp.jpg">
<img id="skyTexture"
src="https://cdn.aframe.io/360-image-gallery-boilerplate/img/sechelt.jpg">
<img id="groundTexture" src="https://cdn.aframe.io/a-painter/images/floor.jpg">
</a-assets>
<a-box src="#boxTexture" position="0 2 -5" rotation="0 45 45" scale="2 2 2"></a-box>
<a-sky src="#skyTexture"></a-sky>
<a-light type="ambient" color="#445451"></a-light>
<a-light type="point" intensity="2" position="2 4 4"></a-light>
</a-scene>
7.添加动画
我们可以使用 A-Frame 的内置动画系统向盒子添加动画。动画随着时间的推移对值进行插值或补间。我们可以在实体上设置动画组件。让我们让盒子上下悬停,为场景添加一些动作。
<a-scene>
<a-assets>
<img id="boxTexture" src="https://i.imgur.com/mYmmbrp.jpg">
</a-assets>
<a-box src="#boxTexture" position="0 2 -5" rotation="0 45 45" scale="2 2 2"
animation="property: object3D.position.y; to: 2.2; dir: alternate; dur: 2000; loop: true"></a-box>
</a-scene>
我们告诉动画组件:
-
对实体的 object3D 位置的 Y 轴进行动画处理。
-
将动画设置为高 20 厘米的
2.2
。 -
在动画的每个重复循环中交替动画的 dir(方向),以便它来回移动。
-
每个周期持续 2000 毫秒 dur(持续时间)。
-
永远循环或重复动画。
8.添加互动
让我们添加与盒子的交互:当我们查看盒子时,我们将增加盒子的大小,当我们“单击”盒子时,我们将使其旋转。
鉴于许多开发人员目前没有带有控制器的适当 VR 硬件,我们将在本节中重点介绍如何通过内置光标组件使用基本的移动和桌面输入。默认情况下,光标组件提供了通过在移动设备上凝视或注视实体来“单击”实体的功能,或者在桌面上查看实体并单击鼠标的功能。但要知道,光标组件只是添加交互的一种方式,如果我们能够访问实际的控制器,事情就会开放。
为了将可见光标固定在相机上,我们将光标放置为相机的子项,如上面的父项和子项变换中所述。
由于我们没有具体定义相机,A-Frame 为我们提供了一个默认相机。但由于我们需要添加光标作为相机的子项,因此我们现在需要定义包含 <a-cursor>
的 <a-camera>
:
<a-scene>
<a-assets>
<img id="boxTexture" src="https://i.imgur.com/mYmmbrp.jpg">
</a-assets>
<a-box src="#boxTexture" position="0 2 -5" rotation="0 45 45" scale="2 2 2"
animation="property: object3D.position.y; to: 2.2; dir: alternate; dur: 2000; loop: true"></a-box>
<a-camera>
<a-cursor></a-cursor>
</a-camera>
</a-scene>
如果我们检查 <a-cursor>
包装的光标组件的文档,我们会发现它会发出悬停事件,例如 mouseenter
、 mouseleave
以及 click
(1)事件监听器组件(中级)
手动处理光标事件的一种方法是使用 JavaScript 添加事件侦听器,就像我们使用普通 DOM 元素一样。如果您不熟悉 JavaScript,您可以跳到下面的“对事件进行动画处理”。
在 JavaScript 中,我们使用 querySelector
抓取框,使用 addEventListener
,然后使用 setAttribute
使框在悬停时扩大其规模。请注意,A 框架向 setAttribute
添加了与多属性组件配合使用的功能。我们可以传递一个完整的 {x, y, z}
对象作为第二个参数。
<script>
var boxEl = document.querySelector('a-box');
boxEl.addEventListener('mouseenter', function () {
boxEl.setAttribute('scale', {x: 2, y: 2, z: 2});
});
</script>
但更稳健的方法是将这种逻辑封装到 A-Frame组件中。这样,我们不必等待场景加载,也不必运行查询选择器,因为组件为我们提供了上下文,并且组件可以重用和配置,而不是在页面上运行不受控制的脚本。更好的是跳过调用 .setAttribute
并直接在 this.el.object3D.scale
上设置值以提高性能。
<script>
AFRAME.registerComponent('scale-on-mouseenter', {
schema: {
to: {default: '2.5 2.5 2.5', type: 'vec3'}
},
init: function () {
var data = this.data;
var el = this.el;
this.el.addEventListener('mouseenter', function () {
el.object3D.scale.copy(data.to);
});
}
});
</script>
我们可以直接从 HTML 将此组件附加到我们的盒子:
<script>
AFRAME.registerComponent('scale-on-mouseenter', {
// ...
});
</script>
<a-scene>
<!-- ... -->
<a-box src="#boxTexture" position="0 2 -5" rotation="0 45 45" scale="2 2 2"
animation="property: object3D.position.y; to: 2.2; dir: alternate; dur: 2000; loop: true"
scale-on-mouseenter></a-box>
<!-- ... -->
</a-scene>
(2)事件动画
动画组件具有在实体发出事件时启动动画的功能。这可以通过 startEvents
属性来完成,该属性采用逗号分隔的事件名称列表。
我们可以为光标组件的 mouseenter
和 mouseleave
事件添加两个动画来更改框的比例,以及一个用于在 click
上围绕 Y 轴旋转框的动画。请注意,一个实体可以通过在属性名称后缀 __<ID>
来拥有多个动画:
<a-box
src="#boxTexture"
position="0 2 -5"
rotation="0 45 45"
scale="2 2 2"
animation__position="property: object3D.position.y; to: 2.2; dir: alternate; dur: 2000; loop: true"
animation__mouseenter="property: scale; to: 2.3 2.3 2.3; dur: 300; startEvents: mouseenter"
animation__mouseleave="property: scale; to: 2 2 2; dur: 300; startEvents: mouseleave"></a-box>
9.添加音频
音频对于在 VR 中提供沉浸感和临场感非常重要。即使在背景中添加简单的白噪声也会有很大的帮助。我们建议为每个场景提供一些音频。一种方法是将 <audio>
元素添加到 HTML(最好在 <a-assets>
下)来播放音频文件:
<a-scene>
<a-assets>
<audio src="https://cdn.aframe.io/basic-guide/audio/backgroundnoise.wav" autoplay
preload></audio>
</a-assets>
<!-- ... -->
</a-scene>
或者我们可以使用 <a-sound>
添加位置音频。这使得当我们接近它时声音变得更大,当我们远离它时声音变得更小。我们可以使用 position
将声音放置在场景中。
<a-scene>
<!-- ... -->
<a-sound src="https://cdn.aframe.io/basic-guide/audio/backgroundnoise.wav" autoplay="true"
position="-3 1 -4"></a-sound>
<!-- ... -->
</a-scene>
10.添加文本
A-Frame 带有文本组件。有多种呈现文本的方法,每种方法都有自己的优点和缺点。 A-Frame 附带使用 three-bmfont-text
的 SDF 文本实现,相对清晰且高性能:
<a-entity
text="value: Hello, A-Frame!; color: #BBB"
position="-0.9 0.2 -3"
scale="1.5 1.5 1.5"></a-entity>
二、如何构建360°画廊?
让我们构建一个基于凝视的交互式 360° 图像库。用户可以单击三个面板。单击后,背景将淡出并交换 360° 图像。
本指南将练习与实体组件相关的三个概念:
-
使用 A-Frame附带的标准组件。
-
使用生态系统中的社区组件。
-
编写自定义组件来完成我们想要的任何事情。
并不是说 360° 图像完全是 A-Frame 的焦点用例,但它是一个简单的示例,作为网络上的早期用例有很多需求。
1.基本框架
这是我们场景的起点。
<a-scene>
<a-assets>
<audio id="click-sound" src="https://cdn.aframe.io/360-image-gallery-boilerplate/audio/click.ogg"></audio>
<!-- Images. -->
<img id="city" src="https://cdn.aframe.io/360-image-gallery-boilerplate/img/city.jpg">
<img id="city-thumb" src="https://cdn.aframe.io/360-image-gallery-boilerplate/img/thumb-city.jpg">
<img id="cubes" src="https://cdn.aframe.io/360-image-gallery-boilerplate/img/cubes.jpg">
<img id="cubes-thumb" src="https://cdn.aframe.io/360-image-gallery-boilerplate/img/thumb-cubes.jpg">
<img id="sechelt" src="https://cdn.aframe.io/360-image-gallery-boilerplate/img/sechelt.jpg">
<img id="sechelt-thumb" src="https://cdn.aframe.io/360-image-gallery-boilerplate/img/thumb-sechelt.jpg">
</a-assets>
<!-- 360-degree image. -->
<a-sky id="image-360" radius="10" src="#city"></a-sky>
<!-- Link template we will build. -->
<a-entity class="link"></a-entity>
<!-- Camera + Cursor. -->
<a-camera>
<a-cursor
id="cursor"
animation__click="property: scale; from: 0.1 0.1 0.1; to: 1 1 1; easing: easeInCubic; dur: 150; startEvents: click"
animation__clickreset="property: scale; to: 0.1 0.1 0.1; dur: 1; startEvents: animationcomplete__click"
animation__fusing="property: scale; from: 1 1 1; to: 0.1 0.1 0.1; easing: easeInCubic; dur: 150; startEvents: fusing"></a-cursor>
</a-camera>
</a-scene>
我们已经预定义了:
-
要在
<a-assets>
内的资产管理系统中预加载的多个图像。请注意,并非所有资产都必须预定义或预加载。 -
我们的 360° 图像占位符带有
<a-sky>
。 -
使用事件驱动动画提供视觉反馈的光标,固定在相机上。
2.使用标准组件
标准组件是 A-Frame 附带的组件,就像任何标准库一样。我们将介绍如何将这些组件附加到实体并通过 HTML 配置它们。
我们想要构建一个纹理平面作为链接,单击该链接将更改 360° 图像。我们从一个空实体开始。如果没有任何组件,任何空实体都不执行任何操作,也不渲染任何内容:
<a-entity class="link"></a-entity>
为了给出我们的实体形状,我们可以附加配置为平面形状的几何组件。我们使用类似于内联 CSS 样式的语法指定组件数据:
<a-entity
class="link"
geometry="primitive: plane; height: 1; width: 1"></a-entity>
然后为了给我们的实体外观,我们可以附加材质组件。我们将 shader
设置为 flat
,这样图像就不会受到光照的负面影响。我们将 src
设置为 #cubes-thumb
,这是资产管理系统中预加载的图像之一的选择器。或者,我们可以传递图像的 URL:
<a-entity class="link"
geometry="primitive: plane; height: 1; width: 1"
material="shader: flat; src: #cubes-thumb"></a-entity>
我们可以通过插入更多组件来继续向我们的实体添加功能。让我们再附加一个标准组件,即声音组件。我们希望当我们点击(通过凝视)链接时,它会播放点击声音。语法与以前相同,但我们现在使用声音组件的属性。我们将 on
设置为 click
,以便在单击时播放声音。我们将 src
设置为 #click-sound
,这是 <audio>
元素的选择器。
<a-entity class="link"
geometry="primitive: plane; height: 1; width: 1"
material="shader: flat; src: #cubes-thumb"
sound="on: click; src: #click-sound"></a-entity>
现在我们有了一个带纹理的平面,单击时会发出咔哒声。
3.使用社区组件
A-Frame 配备了一小部分核心标准组件,但其神奇之处在于 A-Frame 生态系统中大量的开源社区组件。我们可以从 npm 等地方找到社区组件。我们可以将它们放入场景中并直接在 HTML 中使用它们。组件能够做任何事情,并将数百行代码抽象为一个可以通过 HTML 属性插入的组件,就像物理一样!
我们将使用四个社区组件:
-
event-set 事件集
-
layout 布局
-
proxy-event 代理事件
-
template 模板
社区组件通常可以通过 GitHub 获取并在 npm 上发布。包含组件的一种简单方法是使用 unpkg.com CDN,它允许我们将 npm 上托管的组件作为脚本标签包含在内,甚至支持指定模糊版本。我们通常只需要知道组件的 npm 包名称和路径:
<html>
<head>
<title>360° Image Browser</title>
<script src="https://aframe.io/releases/1.6.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-template-component@3.x.x/dist/aframe-template-component.min.js"></script>
<script src="https://unpkg.com/aframe-layout-component@4.x.x/dist/aframe-layout-component.min.js"></script>
<script src="https://unpkg.com/aframe-event-set-component@5.x.x/dist/aframe-event-set-component.min.js"></script>
<script src="https://unpkg.com/aframe-proxy-event-component@2.1.0/dist/aframe-proxy-event-component.min.jss"></script>
</head>
<body>
<a-scene>
<!-- ... -->
</a-scene>
</body>
</html>
(1)使用template组件创建链接的
目前,我们有一个链接。我们想要创建其中三个,每个 360° 图像对应一个。我们希望能够为所有这些重用 HTML 定义。
一种解决方案是模板组件,它在运行时将模板引擎集成到 A-Frame 中。这使我们可以执行诸如封装实体组、传递数据以生成实体或迭代等操作。由于我们想要将一个链接变成三个链接,而不需要复制粘贴 HTML,因此我们可以使用模板组件。
理想情况下,我们会在构建时执行此操作(例如,使用 Super Nunjucks Webpack Loader),而不是在运行时浪费时间执行此操作。但为了本教程演示组件的简单性,我们将使用模板组件。在实践中,我们希望使用模块捆绑器(例如 Webpack)来完成此操作。
如果我们阅读模板组件的文档,我们会发现定义模板的一种方法是通过 <head>
中的脚本标签。让我们将链接作为模板并使用 id
为其命名:
<head>
<!-- ... -->
<script id="link" type="text/html">
<a-entity class="link"
geometry="primitive: plane; height: 1; width: 1"
material="shader: flat; src: #cubes-thumb"
sound="on: click; src: #click-sound"></a-entity>
</script>
</head>
然后我们可以使用模板创建多个平面,无需太多工作:
<a-entity template="src: #link"></a-entity>
<a-entity template="src: #link"></a-entity>
<a-entity template="src: #link"></a-entity>
但随后它们都会显示相同的图像纹理并且看起来相同。这里我们需要一个具有变量替换功能的模板引擎。模板组件带有简单的 ES6 字符串插值(即 ${var}
格式)。
为了允许自定义模板的每个实例,我们在模板中定义一个 ${thumb}
变量,我们可以使用数据属性传递该变量:
<a-assets>
<!-- ... -->
<script id="link" type="text/html">
<a-entity class="link"
geometry="primitive: plane; height: 1; width: 1"
material="shader: flat; src: ${thumb}"
sound="on: click; src: #click-sound"></a-entity>
</script>
</a-assets>
<!-- ... -->
<!-- Pass image sources to the template. -->
<a-entity template="src: #link" data-thumb="#city-thumb"></a-entity>
<a-entity template="src: #link" data-thumb="#cubes-thumb"></a-entity>
<a-entity template="src: #link" data-thumb="#sechelt-thumb"></a-entity>
模板组件使我们不必重复大量 HTML,从而保持场景的可读性。
(2)使用layout组件布置链接
由于实体的默认位置是 0 0 0
,因此实体将重叠。虽然我们可以手动定位每个链接,但我们可以使用布局组件来为我们完成此操作。布局组件会自动将其子组件定位到指定的布局。
我们在链接周围创建一个包装器实体,并使用 line
布局附加布局组件:
<a-entity id="links" layout="type: line; margin: 1.5" position="-1.5 -1 -4">
<a-entity template="src: #link" data-thumb="#city-thumb"></a-entity>
<a-entity template="src: #link" data-thumb="#cubes-thumb"></a-entity>
<a-entity template="src: #link" data-thumb="#sechelt-thumb"></a-entity>
</a-entity>
现在,我们的链接不再重叠,而无需我们计算和摆弄位置。布局组件支持其他布局,包括网格、圆形和十二面体。布局组件相当简单,但我们可以想象在未来,它们可以变得越来越强大,同时保留相同的使用简单性。
(3)使用event-set组件为悬停时提供视觉反馈
最后,我们将为链接添加一些视觉反馈。我们希望它们在悬停或单击时放大和缩小。这涉及编写一个事件侦听器来在缩放组件上执行 setAttribute
操作以响应光标事件。这是一种相当常见的模式,因此有一个事件集组件可以执行 setAttribute
来响应事件。
让我们在链接上附加事件侦听器,以便在人们注视它们时放大它们,在单击它们时缩小它们,并在不再注视它们时缩小它们。我们可以通过 _event
属性或通过 __<id>
指定事件名称,如下所示。其余属性定义 setAttribute
调用。请注意,事件集组件可以有多个实例:
<script id="link" type="text/html">
<a-entity class="link"
geometry="primitive: plane; height: 1; width: 1"
material="shader: flat; src: ${thumb}"
sound="on: click; src: #click-sound"
event-set__mouseenter="scale: 1.2 1.2 1"
event-set__mouseleave="scale: 1 1 1"
event-set__click="_target: #image-360; _delay: 300; material.src: ${src}"></a-entity>
</script>
请记住向链接实体添加 data-src
属性以在单击时加载完整图像:
<a-entity template="src: #link" data-src="#city" data-thumb="#city-thumb"></a-entity>
<a-entity template="src: #link" data-src="#cubes" data-thumb="#cubes-thumb"></a-entity>
<a-entity template="src: #link" data-src="#sechelt" data-thumb="#sechelt-thumb"></a-entity>
接下来,我们要实际设置新的背景图像。我们将添加一个漂亮的淡入黑色效果。
最后一个 event-set__click
更复杂,因为它在另一个实体(我们的背景标记为 ID #image-360
)上设置一个属性,延迟 300,用 material.src
4.使用proxy-event组件更改背景
接下来,我们想要连接链接上的点击以实际更改背景。我们可以使用 proxy-set
将事件从一个实体传递到另一个实体。这是一种告诉背景单击其中一个链接以开始动画的便捷方法:
<a-entity
class="link"
<!-- ... -->
proxy-event="event: click; to: #image-360; as: fade"></a-entity>
proxy-event="event: click; to: #image-360; as: fade"></a-entity>
单击链接时,它也会在我们的背景上发出该事件(ID 为 #image-360
),将该事件从 click
重命名为 fade
。现在让我们处理这个事件来开始动画:
<!-- 360-degree image. -->
<a-sky
id="image-360" radius="10" src="#city"
animation__fade="property: components.material.material.color; type: color; from: #FFF; to: #000; dur: 300; startEvents: fade"
animation__fadeback="property: components.material.material.color; type: color; from: #000; to: #FFF; dur: 300; startEvents: animationcomplete__fade"></a-sky>
我们设置了两种动画,一种将颜色设置为淡入黑色,另一种将颜色设置为淡入正常。 animation__fade
设置为黑色,监听我们之前“代理”的 fade
事件。
animation__fadeback
很有趣,因为我们在 animation__fade
完成后通过侦听动画完成时动画组件发出的 animationcomplete__fade
事件来启动它。我们有效地链接了这些动画!
通过使用组件,我们能够在几十行 HTML 中完成很多工作,在大多数头显和浏览器上运行 VR。尽管生态系统可以提供很多功能来满足常见需求,但重要的 VR 应用程序将要求我们编写特定于应用程序的组件。这将在“编写组件”中介绍,并希望在以后的指南中介绍。
三、如何构建Minecraft?
让我们构建一个基本的 Minecraft(体素生成器)演示,其目标是使用控制器(例如 Quest、Vive、Rift)的房间规模 VR。该示例至少可以在移动设备和桌面设备上使用。
1.基本框架
我们将从这个 HTML 框架开始:
<script src="https://aframe.io/releases/1.6.0/aframe.min.js"></script>
<body>
<a-scene>
</a-scene>
</body>
2.添加地面
<a-plane>
和<a-circle>
是通常用于添加地面的基本基元。我们将使用<a-cylinder>
来更好地与控制器将使用的光线投射器配合使用。圆柱体的半径为 30 米,以匹配我们稍后添加的天空半径。请注意,A-Frame 单位以米为单位,以匹配从 WebXR API 返回的实际单位。
我们将使用的地面纹理托管在https://cdn.aframe.io/a-painter/images/floor.jpg"
。我们将纹理添加到我们的资源中,并创建一个指向该纹理的薄圆柱体实体:
<script src="https://aframe.io/releases/1.6.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>
优化:预加载资源
通过src
属性指定 URL 将在运行时加载纹理。由于网络请求会对渲染性能产生负面影响,因此我们可以预加载纹理,以便场景在获取其资源之前不会开始渲染。我们可以使用资产管理系统来做到这一点。
我们将<a-assets>
放入<a-scene>
中,将资产(例如图像、视频、模型、声音)放入<a-assets>
中,并通过实体从我们的实体指向它们选择器(例如#myTexture
)。
让我们将地面纹理移动到<a-assets>
以使用<img>
元素进行预加载:
<script src="https://aframe.io/releases/1.6.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>
3.添加背景
让我们使用<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 米的半径以匹配地面:
<script src="https://aframe.io/releases/1.6.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>
4.添加体素
我们的 VR 应用程序中的体素将类似于<a-box>
,但附加了一些自定义 A 框架组件。但首先让我们回顾一下实体组件模式。让我们看看<a-box>
等易于使用的原语是如何在底层组成的。
本节稍后将更深入地探讨几个 A 型框架组件的实现。但在实践中,我们经常会通过 A-Frame 社区开发人员已经编写的 HTML 使用组件,而不是从头开始构建它们。
(1)实体组件模式
A-Frame场景中的每个对象都是<a-entity>
,它本身不执行任何操作,就像空的<div>
一样。我们将组件(不要与 Web 或 React 组件混淆)插入到该实体中以提供外观、行为和逻辑。
对于盒子,我们附加并配置 A 型框架的基本几何形状和材料组件。默认情况下,组件表示为 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 进行对比,在 2D Web 中,我们使用一小部分固定 HTML 元素进行开发,并将它们放入层次结构中。
(2)随机颜色组件
A-Frame 中的组件是用 JavaScript 定义的,它们可以完全访问 Three.js 和 DOM API;他们可以做任何事。我们将所有对象定义为一组组件。
我们将通过编写 A 框架组件来在盒子上设置随机颜色,从而将模式付诸实践。组件通过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 文件并将其包含在场景之前:
<script src="https://aframe.io/releases/1.6.0/aframe.min.js"></script>
<script src="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>
<!-- Box with random color. -->
<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-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>
组件可以插入任何实体,而无需像传统继承中那样创建或扩展类。如果我们想附加它来表示<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>
如果我们想分享这个组件供其他人使用,我们也可以。有一个社区维护的组件目录,其中列出了生态系统中的许多方便的组件,类似于 Unity Asset Store。如果我们使用组件开发应用程序,那么我们所有的代码本质上都是模块化且可重用的!
(3)捕捉组件
我们将有一个snap
组件将我们的框对齐到网格,这样它们就不会重叠。我们不会详细介绍该组件的实现方式,但您可以查看 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>
现在我们有了一个表示为一组组件的盒子实体,可用于描述场景中的所有体素。
(4)Mixins
我们可以创建一个 mixin 来定义可重用的组件包。我们将使用<a-mixin>
来描述它,而不是向场景中添加对象的<a-entity>
,它可以重复用于创建像预制件一样的体素:
<script src="https://aframe.io/releases/1.6.0/aframe.min.js"></script>
<script src="components/random-color.js"></script>
<script src="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-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-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"
animation="property: rotation; to: 0 360 0; loop: true"></a-entity>
<a-entity mixin="voxel" position="1 0 -2"></a-entity>
</a-scene>
我们使用该 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"
animation="property: rotation; to: 0 360 0; loop: true"></a-entity>
<a-entity mixin="voxel" position="1 0 -2"></a-entity>
接下来,我们将使用跟踪控制器通过交互动态创建体素。让我们开始将我们的双手添加到应用程序中。
5.添加手动控制器
添加 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="hand: left"></a-entity>
<a-entity id="blockHand" hand-controls="hand: right"></a-entity>
(1)左手添加传送到
我们将在左手插入传送功能,这样我们就可以推动拇指杆以显示从控制器中出来的弧线,然后松开拇指杆以传送到弧线的末端。之前,我们编写了自己的 A-Frame 组件。但我们也可以使用社区已经制作的开源组件,并直接从 HTML 中使用它们!
为了实现这一点,我们首先定义一个包装控制器和相机的player
实体:
<script src="https://aframe.io/releases/1.6.0/aframe.min.js"></script>
<!-- ... -->
<a-entity id="player">
<a-entity id="teleHand" hand-controls="hand: left"></a-entity>
<a-entity id="blockHand" hand-controls="hand: right"></a-entity>
<a-camera></a-camera>
</a-entity>
对于隐形传送,有一个名为眨眼控制的组件。按照自述文件,我们通过<script>
标签添加组件,然后在实体的控制器上设置blink-controls
组件:
<script src="https://aframe.io/releases/1.6.0/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/aframe-blink-controls/dist/aframe-blink-controls.min.js"></script>
<!-- ... -->
<a-entity id="player">
<a-entity id="teleHand" hand-controls="hand: left"></a-entity>
<a-entity id="blockHand" hand-controls="hand: right"></a-entity>
<a-camera></a-camera>
</a-entity>
默认情况下,blink-controls
只会在地面上传送,但我们可以使用collisionEntities
指定使用选择器在方块和地面上传送。此属性是创建blink-controls
组件所用的 API 的一部分:
<a-entity id="teleHand" hand-controls="hand: left" blink-controls="collisionEntities: [mixin='voxel'], #ground"></a-entity>
就是这样!一个脚本标签和一个 HTML 属性,我们就可以传送。如需更多酷炫组件,请查看 A 型框架组件目录。
(2)将体素生成器添加到右手
在 WebXR 中,单击对象的功能不是像 2D 应用程序那样内置的。我们必须自己提供。幸运的是,A-Frame 有许多组件来处理交互。 VR 中类似光标点击的常见方法是使用光线投射器,这是一种可以发射并返回与之相交的物体的激光。然后,我们通过监听交互事件并检查光线投射器的交叉点来实现光标状态。
A-Frame 为控制器激光交互提供laser-controls
组件,将点击激光连接到 VR 跟踪控制器。与blink-controls
组件一样,我们包含脚本标记并附加laser-controls
组件。这次是右手:
<script src="https://aframe.io/releases/1.6.0/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/aframe-blink-controls/dist/aframe-blink-controls.min.js"></script>
<!-- ... -->
<a-entity id="teleHand" hand-controls="hand: left" blink-controls="collisionEntities: [mixin='voxel'], #ground"></a-entity>
<a-entity id="blockHand" hand-controls="hand: right" laser-controls></a-entity>
现在,当我们拉动跟踪控制器上的触发按钮时,laser-controls
将在控制器及其当时相交的实体上发出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');
// Get normal of the face of intersection and scale it down a bit
var normal = evt.detail.intersection.face.normal;
normal.multiplyScalar(0.25);
// Get the position of the intersection and add our scaled normal
var position = evt.detail.intersection.point;
position.add(normal);
// 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', position);
// Add to the scene with `appendChild`.
this.appendChild(newVoxelEl);
});
为了概括从交叉事件创建实体,我们创建了一个intersection-spawn
组件,可以使用任何事件和属性列表进行配置。我们不会详细介绍实现的细节,但您可以在 GitHub 上查看简单的intersection-spawn
组件源代码。我们将intersection-spawn
功能附加到右手,并且为光线投射器提供半米的缓冲区也是一个好主意
防止体素在控制器上生成:
<a-entity id="blockHand" hand-controls="hand: right" laser-controls raycaster="near: 0.5" intersection-spawn="event: click; mixin: voxel"></a-entity>
现在,当我们单击时,我们会生成体素!
6.添加对移动设备和桌面设备的支持
我们看到如何通过将组件混合在一起来构建自定义类型的对象(即,带有手部模型的跟踪手部控制器,该手部模型具有单击功能并在单击时生成块)。组件的美妙之处在于它们可以在其他上下文中重用。我们甚至可以将intersection-spawn
组件与基于凝视的cursor
组件附加在一起,这样我们就可以在移动设备和桌面上生成块,而无需更改组件的任何内容!
<a-entity id="blockHand" hand-controls="hand: right" laser-controls raycaster="near: 0.5" intersection-spawn="event: click; mixin: voxel"></a-entity>
<a-camera>
<a-cursor intersection-spawn="event: click; mixin: voxel"></a-cursor>
</a-camera>