WebAssembly + Forge實戰 - 整合Forge AR/VR ToolKit + Unity場景至前端框架
在瀏覽器環境下,解釋執行JavaScript指令碼實現高階功能早已是家常便飯,然而,Web前端日新月異的需求已逐漸無法完全依賴JavaScript實現。幸運的是,打破瓶頸的新技術已逐漸成熟,它就是 WebAssembly 。
什麼是WebAssembly
WebAssembly是一項神奇的技術,簡而言之就是一種底層的類組合語言,其編譯後的二進位制模組 wasm
可在瀏覽器中執行以接近原生的效能執行CC++、C#、Java、GO、PHP、 Rust 等等語言的程式碼!自2015年頒佈、2017年初正式釋出最小功能版本以來,WebAssembly迅速開始盛行,並已得到主流瀏覽器的廣泛支援,詳細支援情況可以參見下圖或 MDN :
(資料採於2019-01-25)
需要強調的是:WebAssembly並不旨在取代JavaScript或任何現有的H5/ES6技術,而是與他們共存 - 我們耳熟能詳的WebGL、Web Audio等元件都是WebAssembly模組在瀏覽器端的執行時,在瀏覽器端實現所需功能
優勢與異同
那麼問題來了 - WebAssembly究竟和 asm.js 、 Dart 等類似技術有何不同?我們早已可以通過Emscripten編譯asm.js在瀏覽器中跑c/c++了,為什麼還需要WebAssembly呢?相比之下,WebAssembly主要具備以下優勢:
- 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語言為例可通過如下命令直接編譯:
# `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
編譯生成的結果包括:
- hello-world.wasm: wasm模組二進位制碼
- hello-world.html: 展示頁面
- hello-world.js: 讀取wasm模組的JavaScript
其中編譯生成的hello-world.js是幫助我們在頁面中呼叫載入wasm模組的指令碼,我們也可結合 Fetch API 在自己的程式碼進行載入:
fetch('path/to/wasm') .then(response => response.arrayBuffer())\\將wasm檔案響應轉為二進位制陣列 .then(bits => WebAssembly.compile(bits))\\編譯模組 .then(module => { return new WebAssembly.Instance(module) }); \\生成模組例項
可通過自帶的emrun工具在指定瀏覽器中執行編譯結果,或直接託管在Web伺服器上:
emrun --browser /path/to/browser/executable hello-world.html
實戰Unity+WebAssembly
接下來我們就進入今天的實戰:將經由 Autodesk Forge Model Derivative服務輕量化 的模型,通過 Forge AR/VR Toolkit
匯入Unity場景,結合C#/JSLIB指令碼與Unity外掛,編譯為WebAssembly,並整合至我們的前端框架中!
- 參考 該教程 ,為 Unity3D 專案配置好 Forge AR/VR Toolkit
- 將輕量化後的模型匯入至Unity場景,如圖所示填入模型的URN和從Forge服務端獲取的Access Token
- 需注意將.NET執行時版本調整為4.5以便支援TLS 1.2
- 完成場景的建模與開發,本例結合 Cinemachine 的Freelook Camera外掛與簡單的c#指令碼實現由鍵鼠操控的場景漫遊。Cinemachine是一套強大的Unity相機管理工具,可利用其路徑(Path)路點(Waypoint)等特性(並結合 Timeline )輕鬆製作強大的預製路線漫遊等效果。
- 通過較新Unity3D(2017/5.6+)直接將場景編譯為WebAssembly,設定
釋出目標
平臺為WebGL,並在釋出設定
中將聯結器目標
設為WebAssembly
,開始Build
編譯:
-
編譯結果包括:
- html:展示頁面
- Build目錄:
<專案名>.json
(包括執行所需的引數與設定)、UnityLoad.js
(瀏覽器載入wasm所需的指令碼)、<專案名>.*.unityweb
(釋出設定
中指定格式的壓縮包,包含wasm模組與場景資源等) - Template:展示頁面依賴
與前端框架整合
- 瀏覽展示頁面確認實際效果無誤後,將
Build
目錄匯入前端專案的靜態資源路徑(如./src/assets
) - 接下來將分別介紹針對Vue、React、Angular框架與無框架的整合
React
- 推薦使用 react-unity-webgl 載入Unity WebAssembly:
npm install vue-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通訊:
[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
- 推薦使用 vue-unity-webgl 元件載入Unity WebAssembly:
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物件通訊 } \\... }
- 執行結果如下
除錯與優化
編譯後的wasm是二進位制的,可以通過編譯工具(如 WABT 、 Binaryen 等)生成或轉換為 WebAssembly Text (wat) Format
- 人類可讀的類彙編程式碼:
(module (func $i (import "imports" "imported_func") (param i32)) (func (export "exported_func") i32.const 42 call $i ) )
在瀏覽器中也可以檢視 wat
,並斷點除錯
優化考量
- 將wasm等依賴在伺服器端壓縮,加速網路傳輸,以Node後臺為例: https://blog.csdn.net/github_...
- 編譯時的優化(如使用WebGL2.0,.NET4.5等),參考: https://docs.unity3d.com/Manu...
- 使用WebAssembly JIT: https://webassembly.org/docs/...
延伸閱讀
- Forge AR/VR介紹: https://segmentfault.com/a/11...
- Unity Cinemachine外掛學習筆記: https://blog.csdn.net/l773575...
- Emscripten-WebAssembly專欄: https://segmentfault.com/blog...
- WebAssembly入門: https://www.jianshu.com/p/bff...
- WebAssembly入門到入門: https://blog.csdn.net/m549393...
- WebAssembly應用案例: https://blog.csdn.net/frf0lw4...