WebAssembly + Forge實戰 - 整合Forge AR/VR ToolKit + Unity場景至前端框架

在瀏覽器環境下,解釋運行JavaScript腳本實現高階功能早已經是屢見不鮮,然而,Web前端突飛猛進的需求已逐漸沒法徹底依賴JavaScript實現。幸運的是,打破瓶頸的新技術已逐漸成熟,它就是WebAssemblyhtml

什麼是WebAssembly

WebAssembly是一項神奇的技術,簡而言之就是一種底層的類彙編語言,其編譯後的二進制模塊wasm可在瀏覽器中運行以接近原生的性能運行CC++、C#、Java、GO、PHP、Rust等等語言的代碼!自2015年頒佈、2017年初正式發佈最小功能版本以來,WebAssembly迅速開始盛行,並已獲得主流瀏覽器的普遍支持,詳細支持狀況能夠參見下圖或MDN
圖片描述
圖片描述
(數據採於2019-01-25)前端

須要強調的是:WebAssembly並不旨在取代JavaScript或任何現有的H5/ES6技術,而是與他們共存 - 咱們耳熟能詳的WebGL、Web Audio等組件都是WebAssembly模塊在瀏覽器端的運行時,在瀏覽器端實現所需功能
圖片描述vue

優點與異同

那麼問題來了 - WebAssembly究竟和asm.jsDart等相似技術有何不一樣?咱們早已能夠經過Emscripten編譯asm.js在瀏覽器中跑c/c++了,爲何還須要WebAssembly呢?相比之下,WebAssembly主要具有如下優點:react

  • WebAssembly模塊不論在加載速度和性能上都有明顯優點 - 它以二進制碼的形式在瀏覽器中原生運行,無需像asm.js那樣將原始語言編譯成JavaScript,遠超JavaScript引擎解釋腳本的運行速度,即使數一數二的Chrome V8有JIT加持也無濟於事。
  • WebAssembly並非基於現有組建的擴展,而是一個Web開發新特性/標準,它有獨立的路線圖,不斷有新特性加入進來。
  • 不受asm.js等技術在AOT等層面的限制,特性拓展潛力極大,應用場景普遍,詳見底部延伸閱讀部分的介紹。

編譯與運行

那麼如此神奇的技術究竟如何編譯運行?當下最主流編譯器可謂就是Emscripten了,普遍應用於原始語言->LLVM中間碼->JavaScipt(asm.js)的編譯。固然在WebAssembly全面企穩的今天,直接將原始語言編譯成WebAssembly(wasm)也不在話下。
圖片描述
較新版本的Emscripten支持跳過LLVM中間碼->asm.js->wasm的過分,直接編譯wasm,以c語言爲例可經過以下命令直接編譯:c++

# `WASM=1`:僅生成wasm模塊(默認爲LLVM中間碼),`SIDE_MODULE=1`:僅編譯用戶代碼,而不包含printf、memalloc等函數
./emcc hello-world.c -O3 -s WASM=1 -s SIDE_MODULE=1 -o hello-world.html

編譯生成的結果包括:git

  • hello-world.wasm: wasm模塊二進制碼
  • hello-world.html: 展現頁面
  • hello-world.js: 讀取wasm模塊的JavaScript

其中編譯生成的hello-world.js是幫助咱們在頁面中調用加載wasm模塊的腳本,咱們也可結合Fetch API在本身的代碼進行加載:github

fetch('path/to/wasm')
    .then(response => response.arrayBuffer())  //將wasm文件響應轉爲二進制數組
    .then(bits => WebAssembly.compile(bits))  //編譯模塊
    .then(module => { return new WebAssembly.Instance(module) }); //生成模塊實例

可經過自帶的emrun工具在指定瀏覽器中運行編譯結果,或直接託管在Web服務器上:web

emrun --browser /path/to/browser/executable hello-world.html

實戰Unity+WebAssembly+Forge AR/VR

接下來咱們就進入今天的實戰:將經由Autodesk Forge Model Derivative服務輕量化的模型,經過Forge AR/VR Toolkit導入Unity場景,結合C#/JSLIB腳本與Unity插件,編譯爲WebAssembly,並集成至咱們的前端框架中!
圖片描述
(左爲Forge Viewer,右爲本例)npm

圖片描述

  • 將輕量化後的模型導入至Unity場景,如圖所示填入模型的URN和從Forge服務端獲取的Access Token

圖片描述

  • 完成場景的建模與開發,本例結合Cinemachine的Freelook Camera插件與簡單的c#腳本實現由鍵鼠操控的場景漫遊。Cinemachine是一套強大的Unity相機管理工具,可利用其路徑(Path)路點(Waypoint)等特性(並結合Timeline)輕鬆製做強大的預製路線漫遊等效果。
  • 經過較新Unity3D(2017/5.6+)直接將場景編譯爲WebAssembly,設定發佈目標平臺爲WebGL,並在發佈設定中將鏈接器目標設爲WebAssembly,開始Build編譯:

圖片描述
圖片描述

  • 編譯結果包括:json

    • html:展現頁面
    • Build目錄:<項目名>.json(包括運行所需的參數與設置)、UnityLoad.js(瀏覽器加載wasm所需的腳本)、<項目名>.*.unityweb(發佈設定中指定格式的壓縮包,包含wasm模塊與場景資源等)
    • Template:展現頁面依賴

與前端框架整合

  • 瀏覽展現頁面確認實際效果無誤後,將Build目錄導入前端項目的靜態資源路徑(如./src/assets
  • 接下來將分別介紹針對Vue、React、Angular框架與無框架的整合

React

npm install react-unity-webgl
  • 在React組件中調用
import Unity, { UnityContent } from "react-unity-webgl";
class App extends Component {
  unityContent:UnityContent = new UnityContent(
      "Build/forge_sample.json",  \\引用編譯結果,將全部編譯結果置於相同路徑下
      "Build/UnityLoader.js" \\並確保瀏覽器會話能夠http協議訪問
  );

  //....
  render() {
    //...
    <Unity unityContent={this.unityContent}  />
  }
}
  • 與Unity中的對象通信
this.unityContent.send(
      "Unity對象名稱", 
      "C#或JSLIB腳本函數名稱", 
      1 //參數值
    );
  • 在Unity腳本中與JavaScript通信,先在C#腳本中暴露事件:
[DllImport("__Internal")]
  private static extern void EventName (int arg);
  
  public void CallAnEvent (int arg) {
    EventName(arg);
  }
  • 而後,在Unity中建立JSLIB腳本(如Assets/Plugins/WebGL/forge-sample.jslib)注入事件:
mergeInto(LibraryManager.library, {
 
  EventName: function(arg) {
    ReactUnityWebGL.EventName(arg);
  }
});
  • 在前端監聽該事件
this.unityContent.on("EventName", arg => {
   //...
  });
  • 同理咱們能夠在前端監聽Unity的生命週期事件:
public class NewBehaviourScript : MonoBehaviour {
//...    
   [DllImport("__Internal")]
   private static extern void EventName ();
   void OnSceneLoaded (Scene scene, LoadSceneMode mode) {
        EventName();
    }
//...    
}

Vue

npm install vue-unity-webgl
  • 在Vue組件中調用
<template>
  <div>
    ...
    <unity src="Build/forge_sample.json"  unityLoader="Build/UnityLoader.js"></unity>
    <!-- 引用編譯結果,將全部編譯結果置於相同路徑下,並確保瀏覽器會話能夠http協議訪問 -->
    ...
  </div>
</template>
<script>
import Unity from 'vue-unity-webgl'
//...
export default {
  components: { Unity }
  //...
}
</script>

Angular與無框架

  • 對於適用於Angular的Unity組件庫,咱們只找到了ng-unity,但在實測中出現報錯,彷佛是因爲其內置的UnityLoader.js與咱們的模塊並不兼容(該庫不能引用外置Loader),所以咱們結合了無框架的引用方式來作示範
  • 在頁面中引用編譯生成的UnityLoad.js
<script language="JavaScript" src="assets/Build/UnityLoader.js"></script>
  • 組件頁面中加入容器元素
<!-- app.component.html -->
   <div id='unityContainer'></div>
  • 在組件中載入模塊
//app.component.ts
declare var UnityLoader: any;  //聲明UnityLoader爲任意類
export class AppComponent implements AfterViewInit{
  private unityInstance: any;
  //...
  ngAfterViewInit(){
    (<any>window).UnityLoader = UnityLoader; \\將UnityLoader對象暴露爲窗體具柄
    this.unityInstance = UnityLoader.instantiate('unityContainer', './assets/Build/forge_sample.json'); //引用編譯結果,將全部編譯結果置於相同路徑下,並確保瀏覽器會話能夠http協議訪問
    
  }

  sendMessage(objectName: string, functionName: any, argumentValue: any) {
    this.unityInstance.SendMessage(objectName, functionName, argumentValue); //與Unity對象通信
  }
  //...
}
  • 運行結果以下

圖片描述
(左爲Forge Viewer,右爲本例)

調試與優化

編譯後的wasm是二進制的,能夠經過編譯工具(如WABTBinaryen等)生成或轉換爲WebAssembly Text (wat) Format - 人類可讀的類彙編代碼:

(module
  (func $i (import "imports" "imported_func") (param i32))
  (func (export "exported_func")
    i32.const 42
    call $i
  )
)

在瀏覽器中也能夠查看wat,並斷點調試
圖片描述

優化考量

延伸閱讀

相關文章
相關標籤/搜索