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 提供了以下關鍵功能:

WebXR api 是建立在早期 WebVR api 之上的,如今除了支持 VR 之外,還添加了對 AR 的支持,VR、AR 是從感官體驗的角度來區分的:

一個 WebXR 應用的生命週期

一個 WebXR 應用,底層都要經過以下生命週期:

WebXR 開發 VR 應用的優點

WebXR 是基於網頁的 XR 應用程序,可以用來支持一些本地 XR 應用不那麼適合的場景,比如一些短小精幹時效不長的營銷推廣頁面、在線沉浸式視頻、電商、在線小遊戲和藝術創作等。

相比於本地 XR 應用,WebXR 具備如下等優點:

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 社區之一。

特性

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 開發所需要的設置:

<!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-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>

其他的一些文本渲染方案還有:

添加光標

在 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 資源網站:

添加一個武器

引入 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() 生命週期時給對應的實體註冊了 mouseentermouseleave 事件監聽,當觸發 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 方式。

如下代碼中,給剛剛創建的球體設置了 radiuscolorpositionanimation 等組件:

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,完成剩餘的部分工作:

總結

通過本文,你應該收穫了有關 WebXR 的概念、標準以及如何通過 aframe 框架開發 WebXR 應用等知識,WebXR 無論在 Web 開發還是 VR 開發中都是目前參與人數較少的一片藍海,其前景十分的廣闊,甚至我們的教育業務如果結合 WebXR 技術,也是一個新的思路。

希望通過本文能夠引起大家對 WebXR 的興趣,總結了 WebXR 的一些學習資料和開發資源,便於感興趣的同學開發上手:

參考資料

[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