WebXR 應用開發之 aframe 框架入門
本文主要共包含三大部分:第一部分爲 WebXR,包括 WebXR 的概念、標準、優點以及主流的開發方式;由 WebXR 開發方式中【使用封裝好的第三方庫開發】又引出了第二部分—— aframe 框架,其簡介、特性及其中應用的 ECS 架構;第三部分爲通過一個小遊戲 demo,快速掌握 aframe 開發基礎。
可以先對小遊戲進行一個體驗:
遊戲體驗地址:https://webxr-game.zlxiang.com
源碼地址:https://github.com/zh-lx/webxr-game
WebXR
什麼是 WebXR?
WebXR 是一組支持將渲染 3D 場景用來呈現虛擬世界(虛擬現實,也稱作 VR)或將圖形圖像添加到現實世界(增強現實,也稱作 AR)的標準。這個標準實際上是一組 WebXR Device API[1],它們實現了 WebXR 功能集的核心,管理輸出設備的選擇,以適當的幀速率將 3D 場景呈現給所選設備,並管理使用輸入控制器創建的運動矢量。
WebXR 兼容性設備包括沉浸式 3D 運動和定位跟蹤耳機,通過框架覆蓋在真實世界場景之上的眼鏡,以及手持移動電話,它們通過用攝像機捕捉世界來增強現實,並通過計算機生成的圖像增強場景。
爲了完成這些事情,WebXR Device API 提供了以下關鍵功能:
-
查找兼容的 VR 或 AR 輸出設備
-
以適當的幀率將 3D 場景渲染到設備
-
(可選)將輸出鏡像到 2D 顯示器
-
創建代表輸入控件運動的向量
WebXR api 是建立在早期 WebVR api 之上的,如今除了支持 VR 之外,還添加了對 AR 的支持,VR、AR 是從感官體驗的角度來區分的:
-
VR 是用戶藉助外設輸入輸出(頭戴、手柄、體感、運動感知等軟硬件系統)來和純虛擬場景的交互體驗
-
AR 也是用戶藉助外設來體驗額外的虛擬內容,區別是虛擬內容是疊加在真實世界上,其方式可以是通過投射或者視頻疊加
一個 WebXR 應用的生命週期
一個 WebXR 應用,底層都要經過以下生命週期:
WebXR 開發 VR 應用的優點
WebXR 是基於網頁的 XR 應用程序,可以用來支持一些本地 XR 應用不那麼適合的場景,比如一些短小精幹時效不長的營銷推廣頁面、在線沉浸式視頻、電商、在線小遊戲和藝術創作等。
相比於本地 XR 應用,WebXR 具備如下等優點:
-
Web 的即時性:我們只需通過鏈接分享一段內容然後單擊,就能立即使用該內容。從用戶的角度來看,這是一個優勢——無需安裝 App 即可使用內容。從開發者的角度來看,我們可以完全掌控自己的工作,無需徵得許可,也無需通過管理或審批流程即可發佈應用。
-
Web 標準的穩定性:因爲 Web 標準的存在,WebXR api 在現階段已經發布之後,瀏覽器幾乎就不會刪除這些 api 了,因此我們通過 WebXR 建立的應用,可以保持長時間的穩定性,而不像原生應用那般需要不斷隨系統升級而進行適配。
-
Web 開發擁有大量的從業者:目前 XR 在 Web 開發中尚未流行起來,而 Web 開發者的羣體基數十分龐大,一旦 WebXR 開發在 Web 開發從業者中流行,那麼勢必會得到一個快速發展,促進該項技術的繁榮。
WebXR 應用的開發方式
主流的 WebXR 應用開發方式有三種:
使用封裝好的第三方庫
對於沒有 WebGL 基礎的用戶,學習和開發成本相對都比較高,因此市面上有一些在 WebGL 基礎上封裝的庫,幫助我們快速上手開發 WebXR,例如 aframe[2]、babylon[3]、three.js[4]
WebGL + WebXR api
使用 WebGL 加 WebXR api 開發的方式,相對來說是比較貼近於底層的,對於底層,特別是渲染模塊我們可以做一些優化操作從而提升 XR 的性能和體驗。
傳統 3d 引擎 + emscripten
傳統的 3D 應用開發我們一般都會採用一些比較知名的 3D 引擎例如 unity、unreal 等,藉助 emscripten[5],我們可以將 C 和 C++ 代碼編譯爲 WebAssembly,從而實現 web 端的 XR。
aframe 框架
簡介
aframe 是一個用來構建虛擬現實(VR)應用的網頁開發框架,它基於 HTML 之上,使其上手十分簡單。但是 aframe 不僅僅是一個 3D 場景渲染引擎或者一個標記語言,其核心思想是基於 Three.js 來提供一個聲明式、可擴展以及組件化的編程結構。
它由 WebVR 的發起人 Mozilla VR[6] 團隊所構建,是當下用來開發 WebVR 內容主流技術方案,現在由 aframe 在 Supermedium 中的聯合創建者維護。作爲一個獨立的開源項目,aframe 已經成長爲最大的 VR 社區之一。
特性
-
簡單的 VR 製作:只需要引入
<script>
標籤 和<a-scene>
,aframe 將自動生成 3D 渲染的樣板代碼、VR 相關設置和缺省的交互控制。不需要安裝任何東西也無需編譯構建。 -
聲明式 HTML:aframe 通過 html 標籤的方式,將大量的 3D 邏輯封裝在內,容易閱讀,理解和複製粘貼。
-
ECS 架構:aframe 基於強大的 three.js[7] 框架, 同時提供聲明式、組件化、可複用的實體組件結構。HTML 只是冰山的一角,開發者可以自由的使用 JavaScript、DOM API,Three.js,WebVR,和 WebGL。
-
高性能:aframe 從底層對 WebVR 做了優化,儘管其使用 DOM,但其元素並不接觸瀏覽器的佈局引擎。3D 對象的更新全部在低開銷內存中通過單個
requestAnimationFrame
來調用,甚至能夠像本地應用一樣來運行 (90+ FPS)。 -
跨平臺:A-Frame 能構建能兼容主流頭顯設備的 VR 應用程序,如 HTC Vive, Rift, Daydream, GearVR,Pico, Oculus 乃至在普通電腦和手機上運行。
-
工具無關性:由於是構建在 HTML 之上,所以 A-Frame 和大多數開發庫、框架和工具如 react[8], vue[9],angular[10] 等都能夠兼容。
-
可視化的檢測工具:aframe 提供一個便捷的內置 3D 可視化檢測工具。打開任意的 A-Frame 場景,Mac 敲擊
<control> + <option> + <i>
或者 windows 敲擊<ctrl> + <alt> + i
組合鍵,將切換到 3D 元素檢測模式。 -
-
豐富的組件:aframe 內置了很豐富的組件,核心組件如幾何模型 (geometries),材料 (materials),光線 (lights),動畫 (animations),模式 (models),光線投射 (raycasters),陰影 (shadows),定位音頻 (positional audio),文本 (text),和 Vive / Touch / Daydream / GearVR / Cardboard 等控制。以及更多社區貢獻的組件如:粒子系統 (aframe-particle-system-component[11])、物理系統 (aframe-physics-system[12]),多人模式 (networked-aframe[13])、海洋 (oceans[14])、山脈 (mountain[15])、語音識別 (aframe-speech-command-component[16])、運動捕捉 (aframe-motion-capture[17])、瞬移 (aframe-teleport-controls[18])、人手 (aframe-super-hands-component[19])、以及增強現實 (augmented-reality[20]) 等等。
ECS 架構
ECS 全稱 Entity-component-system(實體 - 組件 - 系統) ,是一種主要在遊戲開發領域使用的架構模式。ECS 架構遵循組合模式要好於繼承和層次結構的設計原則,具有很大的靈活性。
組成
實體
實體是指存在於遊戲中的一個物體,實際上它是一系列組件的集合。
aframe 中使用 <a-entity>
元素來表示一個實體,如同在 ECS 架構中定義的那樣,實體是一個佔位符對象,我們通過插入組件來提供其外觀、行爲和功能。其中,位置(position), 旋轉(rotation)和尺寸(scale)是實體的固有組件。
在代碼中,一個實體你可以看做是一個 html 標籤:
<!-- 一個空實體,它沒有外表、行爲或功能 -->
<a-entity />
<!-- 我們可以給實體加上幾何模型(geometry)和 材料(material)組件 ,使它具有形狀和外觀-->
<a-entity geometry= primitive: box material= color: red />
組件
組件是一個可重用和模塊化的數據塊,我們將其插入到一個實體中,以添加外觀、行爲或功能。aframe 中,組件修改場景中的三維對象實體,我們將組件組合在一起構建複雜對象(實際上其封裝了 three.js 和 js 代碼邏輯)。
aframe 內置了大量的組件供我們使用:https://aframe.io/docs/1.3.0/components/animation.html
在代碼中,一個組件可以看作是 html 標籤的一個屬性:
<!-- 如下給實體添加了 position 組件,用以改變實體在三維座標中的位置 -->
<a-entity position= 1 2 3 ></a-entity>
可以通過 AFRAME.registerComponent
api 來註冊一個組件:
<script>
AFRAME.registerComponent('very-high', {
init: function () {
this.el.setAttribute('position', '0 9999 0')
}
});
</script>
<a-entity very-high></a-entity>
系統
一個系統爲組件類提供全局範圍,服務和管理 , 它爲組件類提供公共 API (方法和屬性) 。一個系統可以通過場景元素來訪問,並能幫助組件和全局場景交互。
系統的註冊方式和組件類似,通過 AFRAME.registerSystem
進行註冊。如下代碼,註冊了一個 car
系統,它爲 car
組件提供服務,car
組件可以通過 this.system
來訪問它的同名系統,根據 car
組件的不同 type
,系統爲組件對應的實體設置了不同的 speed
。
AFRAME.registerSystem('car', {
getSpeed: function (type) {
if (type === 'tractor') {
return 40;
} else if (type === 'sports car') {
return 300;
} else {
return 100;
}
}
})
AFRAME.registerComponent('car', {
schema: {
type: { default: 'tractor' },
},
init: function () {
this.el.setAttribute('speed', this.system.getSpeed(this.data.type))
},
})
優勢
在 3D 和 VR 遊戲開發領域,ECS 架構經久考驗,著名的 unity 遊戲引擎就是採用 ECS 架構,那麼相比於 OOP(面向對象),ECS 有什麼優勢呢?
與面向對象相比,ECS 架構最大的區別就是面向對象是通過繼承的方式來構建複雜的類,而 ECS 通過組合的方式來構建複雜的實體。在 OOP 模式下,當一個新的類型需要多個老類型的不同功能的時候,不能很好的繼承出來;而 ECS 把大量的模塊進行集成並解耦,用最小的耦合來集成大量分散的系統,更爲靈活。
舉個例子:
現在我們有一個遊戲,裏面有玩家、敵人、建築、樹等物體,遊戲開發了一段時間後,我們需要增加一類會攻擊的建築。
如果通過面向對象的方式,在一系列冗長的繼承鏈之後,會攻擊的建築無法再直接繼承 Building 和 Enemy 類了(Enemy 類繼承了 Dynamic 類):
而在 ECS 架構下,由於每個組件都是最小單元且相互解耦的,所以只需要組合 Position、Rotation、Scale、Recover、Attack 組件就可以構建出新的 AttackBuilding 實體。
VR 開發中的重要概念
VR 開發是基於 3D 的,幾乎在所有的 3D 開發中,都有以下兩個較爲重要的概念:相機 (camera) 和 三維座標系,理解這兩個概念,是進行 3D 開發的基礎。
相機 (camera)
相機定義了用戶從哪個角度查看場景,你可以將相機理解爲觀察者的眼睛,只有相機看到的畫面,纔會呈現在屏幕畫布上。相機通常與允許輸入設備移動和旋轉相機的控件組件配對。
相機通常分爲正交相機(OrthographicCamera)和透視相機(PerspectiveCamera),3D 場景中一般使用透視相機,而正交相機一般用於 2D 渲染。
正交相機
正交相機(OrthographicCamera)所看到的物體都是三維的,但是人的眼睛只能看到正面,不能看到被遮擋的背面,你看到的是一個 2D 的投影圖。空間幾何體轉化爲一個二維圖的過程就是投影,不同的投影方式意味着投影尺寸不同的算法。
透視相機
透視相機(PerspectiveCamera)的結果除了與幾何體的角度有關,還和距離相關,人的眼睛觀察世界就是透視投影,比如你觀察一條鐵路距離越遠你會感到兩條軌道之間的寬度越小。
aframe 是基於 three.js 的,無論正投影還是透視投影,three.js 都對相關的投影算法進行了封裝,大家只需要根據不同的應用場景自行選擇不同的投影方式。使用 OrthographicCamera 相機對象的時候,three.js 會按照正投影算法自動計算幾何體的投影結果;使用 PerspectiveCamera 相機對象的時候,three.js 會按照透視投影算法自動計算幾何體的投影結果。
三維座標系
aframe 中的 3d 座標系使用右手座標系統,默認的攝像機的朝向,就是下圖視角:
距離單位
aframe 的距離單位是米(meters),因爲 WebXR API 以米爲單位返回姿勢數據。
旋轉單位
aframe 的旋轉單位是角度(degrees),它會在 three.js 內部轉換爲弧度。要確定旋轉的正方向,需要使用右手法則:把大拇指指向正軸的方向,我們手指繞的方向就是旋轉的正方向。
從一個小遊戲上手 aframe 開發
瞭解了 WebXR 及 aframe 的一些基本概念後,我們可以嘗試動手製作一個小遊戲了。通過這個小遊戲的製作過程,你將學習到 a-frame 開發的一些常用 api,並具備上手開發的能力。
構建 aframe 開發環境
首先要引入 aframe 框架,aframe 支持多種引入方式 (https://github.com/aframevr/aframe#usage),這裏我選擇了通過 script 標籤的方式直接在 html 中引入:
<!DOCTYPE html>
<html lang= en >
<head>
<meta charset= UTF-8 />
<meta http-equiv= X-UA-Compatible content= IE=edge />
<meta name= viewport content= width=device-width, initial-scale=1.0 />
<title>WebXR Game</title>
<script src= https://aframe.io/releases/1.3.0/aframe.min.js ></script>
</head>
<body>
</body>
</html>
需要注意的是,aframe 中的資源 (assets)、紋理貼圖 (textures) 以及模型 (models) 通常需要遠程加載,如果是直接通過本地絕對路徑打開 html 頁面會因爲跨域而無法正常訪問資源,所以需要通過 host 或者 localhost 訪問 html 文件進行開發:
這裏我通過 webpack 起了一個 devServer 去訪問本地的 HTML 文件。
添加原語 / 實體
什麼是原語
實體我們前面說過了,aframe 中通過 <a-entity>
創建一個實體,表示 VR 世界中的一個物體。原語 (primitives)[21] 同樣是 <a-xxx>
形式的一個 html 標籤,其內部實際上是 實體 - 組件 的封裝。aframe 內置了大量的原語供我們使用: https://aframe.io/docs/1.3.0/primitives/a-box.html
添加場景
前面引入了 aframe 框架,接下來我們在 body 中添加一個 <a-scene>
原語, <a-scene>
是場景容器,用來包含所有實體,我們所有的實體和原語都需要添加在 <a-scene>
裏面。<a-scene>
幫我們處理了所有 XR 開發所需要的設置:
-
設置畫布(canvas),渲染器(renderer)以及渲染循環
-
缺省相機和光照
-
設置 webvr-polyfill, VREffect
-
添加進入 VR 的界面,來啓動 WebXR API
<!DOCTYPE html>
<html lang= en >
<head>
<meta charset= UTF-8 />
<meta http-equiv= X-UA-Compatible content= IE=edge />
<meta name= viewport content= width=device-width, initial-scale=1.0 />
<title>WebXR Game</title>
<script src= https://aframe.io/releases/1.3.0/aframe.min.js ></script>
</head>
<body>
<a-scene></a-scene>
</body>
</html>
添加了 <a-scene>
之後,我們在頁面的右下角能看到一個 VR 的圖標,表示添加成功了。點擊圖標我們可以進入到 VR 的頁面,來啓動 WebXR API:
引入社區資源
對於我們 web 開發者來說,VR 最難的可能是建立合適的 3D 模型,好在社區有許多已經封裝好的資源供我們使用,A-Frame Registry[22] 收集並組織這些資源以便開發者發現和複用,在這裏面我們可以找到許多供我們開箱即用的資源。
這裏我引用了 aframe-environment-component[23],它可以幫助我們快速創建一個美觀的場景,引入腳本,然後在 <a-scene
上面添加一個 environment:
<!DOCTYPE html>
<html lang= en >
<head>
<meta charset= UTF-8 />
<meta http-equiv= X-UA-Compatible content= IE=edge />
<meta name= viewport content= width=device-width, initial-scale=1.0 />
<title>WebXR Game</title>
<script src= https://aframe.io/releases/1.3.0/aframe.min.js ></script>
<script src= https://unpkg.com/aframe-environment-component@1.3.1/dist/aframe-environment-component.min.js ></script>
</head>
<body>
<a-scene environment= preset: forest; ></a-scene>
</body>
</html>
一個森林的場景就出現在了我們的屏幕中:
添加一面牆
接下來我們要在場景中添加一面牆,用於展示我們遊戲的開始、結束、得分以及生命值等信息。使用 <a-box>
原語,在場景中建立一個立方體作爲牆:
<a-scene environment= preset: forest; >
<a-box></a-box>
</a-scene>
前面我們說了原語是對 實體 - 組件 的封裝,上面的代碼等價於:
<a-scene environment= preset: forest; >
<a-entity geometry= primitive: box; ></a-entity>
</a-scene>
添加 3D 座標變換
爲我們的牆添加 scale= 30 20 4
,將其設置爲一堵 x 軸方向長 30 米,z 軸方向寬 4 米,y 軸方向高 20 米的牆;並添加 postion= 0 0 -20
,將其位置設置在 z 軸方向 -20 米的位置:
<a-scene environment= preset: forest; >
<a-box scale= 30 20 4 position= 0 0 -20 ></a-box>
</a-scene>
應用圖片紋理
給 <a-box>
添加一個 src
屬性,指定一個圖片地址, aframe 會將圖片作爲貼圖渲染在物體的表面。如下代碼,我們給牆壁表面貼上了石頭紋理:
<a-scene environment= preset: forest; >
<a-box
scale= 30 20 4
position= 0 0 -20
src= https://image-1300099782.cos.ap-beijing.myqcloud.com/wall.jpeg
>
<a-box position= 0 0 0 ></a-box>
</a-box>
</a-scene>
使用資源管理系統
上面給牆添加圖片紋理的方式有一個缺點:牆體的資源加載不會等待圖片加載完再開始,這可能導致場景中先渲染出沒有圖片紋理的牆,等牆體的圖片加載完成後牆面纔會渲染圖片紋理。
出於性能考慮我們推薦使用資源管理系統(<a-assets>
)。資源管理系統使瀏覽器緩存資源更容易(例如圖像,視頻,模型),並且 A-Frame 框架將確保所有的資源都在渲染之前被獲取到。
如果我們在資源管理系統裏面定義一個 <img>
,three.js 就無需在底層再創建一個 <img>
。在 aframe 中自行創建 <img>
也給了我們更多的控制,讓我們在多個實體上重用紋理。同時必要時 aframe 還能自動設置 crossOrigin
以及其他一些屬性。
要將資源管理系統用於圖像紋理:
-
添加
<a-assets>
到場景中。 -
將紋理定義爲
<a-assets>
下面的<img>
。 -
給
<img>
一個 HTML ID (e.g.id= boxTexture
)。 -
以 DOM 選擇器格式使用 ID 來引用資源(
src= #boxTexture
)
使用資源管理系統來實現上面給牆添加圖片紋理效果的代碼如下:
<a-scene environment= preset: forest; >
<a-assets timeout= 30000 >
<img
id= wallImg
src= https://image-1300099782.cos.ap-beijing.myqcloud.com/wall.jpeg
/>
</a-assets>
<a-box scale= 30 20 4 position= 0 0 -20 src= #wallImg ></a-box>
</a-scene>
添加文字
WebGL 有多種方法來處理文字的渲染,各有優缺點,aframe 採用 SDF 文本 [24] 實現方案,使用了three-bmfont-text
,簡單且性能好。通過添加 <a-text>
原語,實現文本的渲染。
<a-scene environment= preset: forest; >
<!-- ... -->
<a-text
id= start-text
value= Start
color= #BBB
position= -3 6 -18
scale= 10 10 10
font= mozillavr
></a-text>
</a-scene>
其他的一些文本渲染方案還有:
-
text-geometry[25]:三維文本,渲染代價要高些
-
html-shader[26]:把 HTML 渲染爲一個紋理,好處是容易設置樣式,缺點是性能糟糕。
添加光標
在 VR 世界中,我們可以通過 VR 設備的控制器進行交互,考慮到目前許多開發人員沒有合適的帶控制器的 VR 硬件,我們可以使用內置的 cursor 組件來進行交互。
光標原語 <a-cursor>
既可以用於基於注視的交互,也可以用於基於控制器的交互。默認的外觀是一個環形幾何圖形,它通常作爲相機的子對象放置。
如下我們通過 <a-camera>
添加一個自己的相機去代替 <a-scene>
設置的缺省相機,並將 <a-cursor>
作爲相機的子對象掛載,這樣無論我們的相機如何旋轉和移動,我們始終能看到光標。
這裏說明一下:aframe 中當一個實體作爲另一個實體的子對象掛載後,子對象實體的 3d 座標屬性都是相對於其父對象實體的座標位置,而不是整個 3d 世界的座標位置。
<a-scene environment= preset: forest; >
<a-camera>
<a-cursor color= #FAFAFA ></a-cursor>
</a-camera>
</a-scene>
使用 gltf 模型
gltf[27] 是 Khronos 的一個開放項目,它爲 3D 資產提供了一種通用的、可擴展的格式,這種格式既高效又與現代 web 技術高度可交互。gltf-model
組件使用 glTF ( .gltf
或 .glb
) 文件來加載模型數據,我們此應用中大量的 3d 模型都將使用 gltf 模型。
開放資源
下面是幾個開放的 gltf 資源網站:
-
Sketchfab[28]:提供所有可下載模型的自動轉換,包括 PBR 模型以及 gltf 格式
-
Poimandres Market[29]:提供 gltf 格式的 3D 資源下載
-
Poly Haven[30]:提供 CC0 HDRIs、PBR 紋理和 gltf 模型
添加一個武器
引入 gltf 模型來加載一個武器,在資源管理系統中,通過 <a-asset-item>
原語引入一個 gltf 資源,然後在相機下掛載一個實體子對象,將實體的 src
設置爲 <a-asset-item>
原語的 id:
<a-scene environment= preset: forest; >
<a-assets timeout= 30000 >
<img
id= wallImg
src= https://image-1300099782.cos.ap-beijing.myqcloud.com/wall.jpeg
/>
<a-asset-item
id= weapon
src= https://vazxmixjsiawhamofees.supabase.co/storage/v1/object/public/models/blaster/model.gltf
></a-asset-item>
</a-assets>
<a-camera>
<a-cursor color= #FAFAFA ></a-cursor>
<a-gltf-model
id= _weapon
src= #weapon
position= 0.5 -0.5 -0.8
scale= 1 1 1
rotation= 0 180 0
></a-gltf-model>
</a-camera>
</a-scene>
在頁面上就出現了我們的武器:
使用組件
前面我們提到過組件的註冊方式,下面我們要實現當光標聚焦了 start 文字時,文字變大並且變色;光標失焦是還原的效果。
組件有很多生命週期,在本例中我們使用了 init()
方法,它方法在組件生命週期開始時被調用一次,通常被用於設置初始狀態和變量 、 綁定方法 以及 附加事件偵聽器 。
註冊組件
下面的代碼中我們註冊了一個 start-focus
組件,它在 init()
生命週期時給對應的實體註冊了 mouseenter
和 mouseleave
事件監聽,當觸發 mouseenter
事件時,start 字體會變大並且變成橙色;當觸發 mouseleave
事件時,start 字體會復原:
AFRAME.registerComponent('start-focus', {
init: function () {
this.el.addEventListener('mouseenter', function () {
if (window.startLeaveTimer) {
clearTimeout(window.startLeaveTimer);
window.startLeaveTimer = null;
}
window.CursorFocusEntity = 'start';
this.setAttribute('scale', '12 12 12');
this.setAttribute('color', 'orange');
});
this.el.addEventListener('mouseleave', function () {
window.startLeaveTimer = setTimeout(() => {
window.CursorFocusEntity = null;
this.setAttribute('scale', '10 10 10');
this.setAttribute('color', '#bbb');
}, 500);
});
},
});
掛載組件
我們把剛剛註冊的 start-focus
組件掛載到 start 文本原語上:
<a-scene environment= preset: forest; >
<!-- ... -->
<a-text
id= start-text
value= Start
color= #BBB
position= -3 6 -18
scale= 10 10 10
font= mozillavr
start-focus
></a-text>
</a-scene>
現在就實現了我們想要的效果:
監聽光標點擊事件
按照剛剛的方法,故技重施,如法炮製,給光標添加一個點擊監聽事件:
<script>
AFRAME.registerComponent('cursor-listener', {
init: function () {
// 點擊進行攻擊
this.el.addEventListener('click', function (evt) {
console.log('光標點擊了')
});
}
});
</script>
<a-camera>
<a-cursor color= #FAFAFA cursor-listener></a-cursor>
</a-camera>
javaScript 進行交互
因爲 aframe 本質上就是 HTML,所以我們可以像普通 Web 開發一樣使用 JavaScript 和 DOM[31] API 來控制其中的場景和實體。
下面我們要實現一個點擊光標時,武器向光標位置發射子彈的效果,使用 JavaScript 的 DOM API 來做一些實體與場景的交互。
獲取光標點信息
當光標點擊事件觸發時,回調函數有一個默認參數 evt,裏面包含了光標的相關信息,我們可以打印一下:
AFRAME.registerComponent('cursor-listener', {
init: function () {
// 點擊進行攻擊
this.el.addEventListener('click', function (evt) {
console.log(evt)
});
}
});
通過打印的結果得知,evt.detail.intersection.point
包含了當前光標所指向的三維座標位置:
現在我們在光標點擊事件的回調函數中,執行一個 createAttack
方法,其接收 evt.detail.intersection.point
作爲參數:
AFRAME.registerComponent('cursor-listener', {
schema: {},
init: function () {
// 點擊進行攻擊
this.el.addEventListener('click', function (evt) {
createAttack(evt.detail.intersection.point);
});
},
});
創建實體
我們要發射子彈,首先要創建子彈實體,通過 document.createElement
api 來創建子彈實體,如下代碼創建了一個球體:
function createAttack(point) {
const attackEntity = document.createElement('a-sphere');
}
給實體設置組件
通過 javascript 給實體設置組件,與 js 給 dom 設置屬性一樣,也是通過 Node.setAttribute
方式。
如下代碼中,給剛剛創建的球體設置了 radius
、color
、position
和 animation
等組件:
function createAttack(point) {
const { newX, newY, newZ } = getPosition(point);
const attackEntity = document.createElement('a-sphere');
attackEntity.setAttribute('radius', '0.2');
attackEntity.setAttribute('color', 'red');
attackEntity.setAttribute('position', `${newX} ${newY} ${newZ}`);
attackEntity.setAttribute(
'animation',
`property: position; dur: 300; to: ${point.x} ${point.y} ${point.z};`
);
}
其中 position 的位置,是球體生成的位置,我們需要讓子彈從武器的槍口位置處發出,最終通過動畫,發射到光標所在的位置。
計算子彈的初始位置
上面的代碼中,getPosition
是計算子彈初始位置的函數,此部分設計複雜且枯燥的數學計算,不感興趣的可以跳過。
前面我們提到過,aframe 中實體的 position 是相對於父對象實體的,我們的子彈最終是要掛載到 <a-scene>
下面,由於當相機轉動時,武器在三維世界座標系中的位置會發生變化,所以我們需要計算出武器槍口所在的初始位置,即子彈的初始位置。
我們先看三維座標系中的其中一個平面,以 x 軸和 z 軸所在的平面爲例:
首先無論相機如何轉動,槍口位置和相機指向的位置與相機所在點的連線在 xz 平面所成的夾角是始終不變的。
光標起始點和點擊時光標所在點的座標我們都是已知的,所以可以求出點擊時和初始狀態在 xz 平面所旋轉的弧度 θ,然後根據下面的數學知識,我們能計算出點擊時槍口所在的位置。
座標系中求兩條直線之間的夾角:
直線 l1 的斜率 k1 : k1 = (y1 - y) ``/ (x1 - x)
直線 l2 的斜率 k2:k2 = (y2 - y) ``/ (x2 - x)
夾角 θ 的正切值:tanθ = (k2 - k1) ``/ (1 + k1 * k2)
夾角 θ:θ = Math.atan(tanθ)
座標系求一個點以另一個點爲圓心旋轉 θ 後的座標:
x2 = (x1 - x) * cosθ - (y1 - y) * sinθ + x
y2 = (y1 - y) * cosθ + (x1 - x) * sinθ + y
通過上面的數學公式,代入 getPosition
函數中,就可以求出我們子彈的初始位置。
獲取實體
我們最終要把創建出的子彈掛載到場景中,所以需要先獲取到場景,通過 document.querySelector
api 去獲取:
const scene = document.querySelector('a-scene');
掛載實體
同樣通過 appendChild
dom api,我們可以將剛剛創建的子彈實體給掛載到場景中:
function createAttack(point) {
const { newX, newY, newZ } = getPosition(point);
const attackEntity = document.createElement('a-sphere');
attackEntity.setAttribute('radius', '0.2');
attackEntity.setAttribute('color', 'red');
attackEntity.setAttribute('position', `${newX} ${newY} ${newZ}`);
attackEntity.setAttribute(
'animation',
`property: position; dur: 300; to: ${point.x} ${point.y} ${point.z};`
);
scene.appendChild(attackEntity);
}
銷燬實體
當子彈發射到光標所在位置之後,我們不能讓其一直停留在場景中,需要將其銷燬,即通過 removeChild
將其從場景中移除:
function createAttack(point) {
const { newX, newY, newZ } = getPosition(point);
const attackEntity = document.createElement('a-sphere');
attackEntity.setAttribute('radius', '0.2');
attackEntity.setAttribute('color', 'red');
attackEntity.setAttribute('position', `${newX} ${newY} ${newZ}`);
attackEntity.setAttribute(
'animation',
`property: position; dur: 300; to: ${point.x} ${point.y} ${point.z};`
);
scene.appendChild(attackEntity);
const timer = setTimeout(() => {
scene.removeChild(attackEntity);
clearTimeout(timer);
}, 300);
}
至此我們的子彈發射效果就完成了:
添加音頻資源
音頻對於在虛擬現實中提供沉浸感是很重要的,方法是添加一個<audio>
元素到我們的 HTML(最好是<a-assets>
)中來播放一個音頻文件,並在相機下面掛載一個實體,通過 sound
組件來掛載對應的音頻:
<a-scene environment= preset: forest; >
<a-assets timeout= 30000 >
<audio
id= shooting-sound
src= https://audio-1300099782.cos.ap-beijing.myqcloud.com/shooting.mp3
preload= auto
></audio>
</a-assets>
<a-camera>
<a-cursor color= #FAFAFA cursor-listener></a-cursor>
<a-gltf-model
id= _weapon
src= #weapon
position= 0.5 -0.5 -0.8
scale= 1 1 1
rotation= 0 180 0
></a-gltf-model>
<a-entity
sound= src: #shooting-sound
id= shooting_sound_player
position= 0.5 -0.5 -0.8
poolSize= 10
></a-entity>
</a-camera>
</a-scene>
通過 entity.components.sound.playSound()
方法,我們可以播放實體上掛載的音頻,所以我們在 createAttack
方法執行時通過如下代碼播放射擊的音頻:
const shootingSoundPlayer = document.querySelector('#shooting_sound_player');
function createAttack(point) {
shootingSoundPlayer.components.sound.playSound();
const { newX, newY, newZ } = getPosition(point);
const attackEntity = document.createElement('a-sphere');
attackEntity.setAttribute('radius', '0.2');
attackEntity.setAttribute('color', 'red');
attackEntity.setAttribute('position', `${newX} ${newY} ${newZ}`);
attackEntity.setAttribute(
'animation',
`property: position; dur: 300; to: ${point.x} ${point.y} ${point.z};`
);
scene.appendChild(attackEntity);
}
其餘工作
到這裏,我們本遊戲 demo 所涉及的所有 aframe 的知識點都已經講完了,你可以根據上面的知識點,結合 javascript,完成剩餘的部分工作:
-
定時生成怪物
-
怪物間隔一定時間對我們發射攻擊,造成傷害
-
攻擊怪物,造成傷害,消滅怪物時獲得分數
-
更新分數和我們剩餘的 HP
-
遊戲開始、結束和重新開始
總結
通過本文,你應該收穫了有關 WebXR 的概念、標準以及如何通過 aframe 框架開發 WebXR 應用等知識,WebXR 無論在 Web 開發還是 VR 開發中都是目前參與人數較少的一片藍海,其前景十分的廣闊,甚至我們的教育業務如果結合 WebXR 技術,也是一個新的思路。
希望通過本文能夠引起大家對 WebXR 的興趣,總結了 WebXR 的一些學習資料和開發資源,便於感興趣的同學開發上手:
-
WebXR 標準:https://immersive-web.github.io/webxr/#xrpose-interface
-
aframe
-
Sketchfab[32]
-
Poimandres Market[33]
-
Poly Haven[34]
-
文檔:https://aframe.io/docs/1.3.0/introduction/
-
資源:https://aframe.io/aframe-registry/
-
gltf 資源
-
WebGL:https://github.com/KhronosGroup/WebGL
-
Three.js:https://github.com/mrdoob/three.js
參考資料
[1]
WebXR Device API: https://immersive-web.github.io/webxr/#terminology
[2]
aframe: https://github.com/aframevr/aframe
[3]
babylon: https://github.com/BabylonJS/Babylon.js
[4]
three.js: https://github.com/mrdoob/three.js
[5]
emscripten: https://github.com/emscripten-core/emscripten
[6]
Mozilla VR: https://mozvr.com/
[7]
three.js: https://github.com/mrdoob/three.js
[8]
react: https://github.com/aframevr/aframe-react/
[9]
vue: https://vuejs.org/
[10]
angular: https://angularjs.org/
[11]
aframe-particle-system-component: https://github.com/IdeaSpaceVR/aframe-particle-system-component
[12]
aframe-physics-system: https://github.com/donmccurdy/aframe-physics-system
[13]
networked-aframe: https://github.com/haydenjameslee/networked-aframe
[14]
oceans: https://github.com/donmccurdy/aframe-extras/tree/master/src/primitives
[15]
mountain: https://github.com/ngokevin/kframe/tree/master/components/mountain/
[16]
aframe-speech-command-component: https://github.com/lmalave/aframe-speech-command-component
[17]
aframe-motion-capture: https://github.com/dmarcos/aframe-motion-capture
[18]
aframe-teleport-controls: https://github.com/fernandojsg/aframe-teleport-controls
[19]
aframe-super-hands-component: https://github.com/wmurphyrd/aframe-super-hands-component
[20]
augmented-reality: https://github.com/jeromeetienne/AR.js#augmented-reality-for-the-web-in-less-than-10-lines-of-html
[21]
原語 (primitives): https://aframe.io/docs/1.3.0/introduction/html-and-primitives.html#primitives
[22]
A-Frame Registry: https://aframe.io/aframe-registry
[23]
aframe-environment-component: https://github.com/supermedium/aframe-environment-component
[24]
SDF 文本: https://steamcdn-a.akamaihd.net/apps/valve/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf
[25]
text-geometry: https://github.com/supermedium/superframe/tree/master/components/text-geometry/
[26]
html-shader: https://github.com/mayognaise/aframe-html-shader
[27]
gltf: https://github.com/KhronosGroup/glTF
[28]
Sketchfab: https://sketchfab.com/features/gltf
[29]
Poimandres Market: https://market.pmnd.rs/
[30]
Poly Haven: https://polyhaven.com/
[31]
DOM: https://developer.mozilla.org/docs/Web/API/Document_Object_Model/Introduction
[32]
Sketchfab: https://sketchfab.com/features/gltf
[33]
Poimandres Market: https://market.pmnd.rs/
[34]
Poly Haven: https://polyhaven.com/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/A75jdXCZ45WTMOGo1nNllA