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,并断点调试
图片描述

优化考量

延伸阅读

相关文章
相关标签/搜索