- 原文地址:Minecraft in WebVR with HTML Using A-Frame
- 原文作者:Kevin Ngo
- 譯者:Felix
- 校對:阿希
我是 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>
就是這樣!只用使用一行 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>
預加載資源
通過 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>
添加背景
讓我們使用 <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/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>
添加體素
在我們的 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>
組件可以插入到任何實體中,但並不需要像在傳統繼承模式中那樣創建或擴展類。如果我們想在類似 <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>
隨后我們使用 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 也提供了諸如 mouseenter
及 mouseleave
這樣的事件。事件包含了用戶交互的詳細信息。
這賦予了我們點擊的能力,但我們還得寫一些響應點擊事件處理生成磚塊的邏輯。可以使用事件監聽器及 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>
試試看
我們的 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>
如果你有 VR 頭盔(例如 HTC Vive、Oculus Rift + Touch),那么可以找一個支持 WebVR 的瀏覽器並打開示例。
如果你想使用桌面或移動設備觀看 VR 是什么樣的,可以查看錄制好的 VR 動作捕捉和手勢演示。
iKcamp原創新書《移動Web前端高效開發實戰》已在亞馬遜、京東、當當開售。