WebVR 第 3 部分:釋放 WebAssembly 和 AssemblyScript 的潛力

已發表: 2022-03-11

WebAssembly 絕對不能替代JavaScript 作為網絡和世界的通用語言。

WebAssembly(縮寫為 Wasm)是一種用於基於堆棧的虛擬機的二進制指令格式。 Wasm 被設計為可移植的目標,用於編譯 C/C++/Rust 等高級語言,支持在 Web 上部署客戶端和服務器應用程序。” –WebAssembly.org

重要的是要區分 WebAssembly 不是一種語言。 WebAssembly 就像一個 '.exe' - 甚至更好 - 一個 Java '.class' 文件。 它由 Web 開發人員從另一種語言編譯,然後下載並在您的瀏覽器上運行。

WebAssembly 為 JavaScript 提供了我們偶爾想藉用但從未真正想要擁有的所有功能。 就像租船或租馬一樣,WebAssembly 讓我們可以旅行到其他語言,而不必做出奢侈的“語言生活方式”選擇。 這讓網絡專注於重要的事情,比如提供功能和改善用戶體驗。

超過 20 種語言編譯為 WebAssembly:Rust、C/C++、C#/.Net、Java、Python、Elixir、Go,當然還有 JavaScript。

如果您還記得我們模擬的架構圖,我們將整個模擬委託給nBodySimulator ,因此它管理網絡工作者。

仿真的架構圖
圖 1:整體架構。

如果您還記得介紹文章中的內容, nBodySimulator有一個step()函數,每 33 毫秒調用一次。 step()函數做了這些事情——在上圖中編號:

  1. nBodySimulator 的calculateForces()調用this.worker.postMessage()開始計算。
  2. workerWasm.js this.onmessage()獲取消息。
  3. workerWasm.js 同步運行 nBodyForces.wasm 的nBodyForces()函數。
  4. workerWasm.js 使用this.postMessage()回复具有新力量的主線程。
  5. 主線程的this.worker.onMessage()編組返回的數據並調用。
  6. nBodySimulator 的applyForces()來更新身體的位置。
  7. 最後,可視化器重新繪製。

UI線程,Web工作線程
圖 2:模擬器的 step() 函數內部

在上一篇文章中,我們構建了包裝 WASM 計算的 Web Worker。 今天,我們正在構建標有“WASM”的小盒子並將數據移入和移出。

為簡單起見,我選擇了 AssemblyScript 作為源代碼語言來編寫我們的計算。 AssemblyScript 是 TypeScript 的一個子集——它是一種類型化的 JavaScript——所以你已經知道了。

例如,這個 AssemblyScript 函數計算兩個物體之間的重力:f64中的someVar:f64將 someVar 變量標記為編譯器的浮點數。 請記住,此代碼是在與 JavaScript 完全不同的運行時編譯和運行的。

 // AssemblyScript - a TypeScript-like language that compiles to WebAssembly // src/assembly/nBodyForces.ts /** * Given two bodies, calculate the Force of Gravity, * then return as a 3-force vector (x, y, z) * * Sometimes, the force of gravity is: * * Fg = G * mA * mB / r^2 * * Given: * - Fg = Force of gravity * - r = sqrt ( dx + dy + dz) = straight line distance between 3d objects * - G = gravitational constant * - mA, mB = mass of objects * * Today, we're using better-gravity because better-gravity can calculate * force vectors without polar math (sin, cos, tan) * * Fbg = G * mA * mB * dr / r^3 // using dr as a 3-distance vector lets * // us project Fbg as a 3-force vector * * Given: * - Fbg = Force of better gravity * - dr = (dx, dy, dz) // a 3-distance vector * - dx = bodyB.x - bodyA.x * * Force of Better-Gravity: * * - Fbg = (Fx, Fy, Fz) = the change in force applied by gravity each * body's (x,y,z) over this time period * - Fbg = G * mA * mB * dr / r^3 * - dr = (dx, dy, dz) * - Fx = Gmm * dx / r3 * - Fy = Gmm * dy / r3 * - Fz = Gmm * dz / r3 * * From the parameters, return an array [fx, fy, fz] */ function twoBodyForces(xA: f64, yA: f64, zA: f64, mA: f64, xB: f64, yB: f64, zB: f64, mB: f64): f64[] { // Values used in each x,y,z calculation const Gmm: f64 = G * mA * mB const dx: f64 = xB - xA const dy: f64 = yB - yA const dz: f64 = zB - zA const r: f64 = Math.sqrt(dx * dx + dy * dy + dz * dz) const r3: f64 = r * r * r // Return calculated force vector - initialized to zero const ret: f64[] = new Array<f64>(3) // The best not-a-number number is zero. Two bodies in the same x,y,z if (isNaN(r) || r === 0) return ret // Calculate each part of the vector ret[0] = Gmm * dx / r3 ret[1] = Gmm * dy / r3 ret[2] = Gmm * dz / r3 return ret }

這個 AssemblyScript 函數獲取兩個物體的 (x, y, z, mass) 並返回一個包含三個浮點數的數組,描述物體相互應用的 (x, y, z) 力向量。 我們不能從 JavaScript 調用這個函數,因為 JavaScript 不知道在哪裡可以找到它。 我們必須將其“導出”到 JavaScript。 這給我們帶來了第一個技術挑戰。

WebAssembly 導入和導出

在 ES6 中,我們考慮 JavaScript 代碼中的導入和導出,並使用 Rollup 或 Webpack 等工具來創建在舊版瀏覽器中運行的代碼來處理importrequire() 。 這創建了一個自上而下的依賴樹,並啟用了“tree-shaking”和代碼分割等很酷的技術。

在 WebAssembly 中,導入和導出完成的任務與 ES6 導入不同。 WebAssembly 導入/導出:

  • 為 WebAssembly 模塊提供運行時環境(例如, trace()abort()函數)。
  • 在運行時之間導入和導出函數和常量。

在下面的代碼中, env.abortenv.trace是我們必須提供給 WebAssembly 模塊的環境的一部分。 nBodyForces.logI和朋友函數向控制台提供調試消息。 請注意,將字符串傳入/傳出 WebAssembly 並非易事,因為 WebAssembly 的唯一類型是 i32、i64、f32、f64 數字,其中 i32 引用抽象線性內存。

注意:這些代碼示例在 JavaScript 代碼(Web Worker)和 AssemblyScript(WASM 代碼)之間來回切換。

 // Web Worker JavaScript in workerWasm.js /** * When we instantiate the Wasm module, give it a context to work in: * nBodyForces: {} is a table of functions we can import into AssemblyScript. See top of nBodyForces.ts * env: {} describes the environment sent to the Wasm module as it's instantiated */ const importObj = { nBodyForces: { logI(data) { console.log("Log() - " + data); }, logF(data) { console.log("Log() - " + data); }, }, env: { abort(msg, file, line, column) { // wasm.__getString() is added by assemblyscript's loader: // https://github.com/AssemblyScript/assemblyscript/tree/master/lib/loader console.error("abort: (" + wasm.__getString(msg) + ") at " + wasm.__getString(file) + ":" + line + ":" + column); }, trace(msg, n) { console.log("trace: " + wasm.__getString(msg) + (n ? " " : "") + Array.prototype.slice.call(arguments, 2, 2 + n).join(", ")); } } }

在我們的 AssemblyScript 代碼中,我們可以像這樣完成這些函數的導入:

 // nBodyForces.ts declare function logI(data: i32): void declare function logF(data: f64): void

注意Abort 和trace 是自動導入的

從 AssemblyScript 中,我們可以導出我們的界面。 以下是一些導出的常量:

 // src/assembly/nBodyForces.ts // Gravitational constant. Any G could be used in a game. // This value is best for a scientific simulation. export const G: f64 = 6.674e-11; // for sizing and indexing arrays export const bodySize: i32 = 4 export const forceSize: i32 = 3

這是我們將從 JavaScript 調用的nBodyForces()的導出。 我們在文件頂部導出類型Float64Array ,以便我們可以在 Web Worker 中使用 AssemblyScript 的 JavaScript 加載器來獲取數據(見下文):

 // src/assembly/nBodyForces.ts export const FLOAT64ARRAY_ID = idof<Float64Array>(); ... /** * Given N bodies with mass, in a 3d space, calculate the forces of gravity to be applied to each body. * * This function is exported to JavaScript, so only takes/returns numbers and arrays. * For N bodies, pass and array of 4N values (x,y,z,mass) and expect a 3N array of forces (x,y,z) * Those forces can be applied to the bodies mass to update its position in the simulation. * Calculate the 3-vector each unique pair of bodies applies to each other. * * 0 1 2 3 4 5 * 0 xxxxx * 1 xxxx * 2 xxx * 3 xx * 4 x * 5 * * Sum those forces together into an array of 3-vector x,y,z forces * * Return 0 on success */ export function nBodyForces(arrBodies: Float64Array): Float64Array { // Check inputs const numBodies: i32 = arrBodies.length / bodySize if (arrBodies.length % bodySize !== 0) trace("INVALID nBodyForces parameter. Chaos ensues...") // Create result array. This should be garbage collected later. let arrForces: Float64Array = new Float64Array(numBodies * forceSize) // For all bodies: for (let i: i32 = 0; i < numBodies; i++) { // Given body i: pair with every body[j] where j > i for (let j: i32 = i + 1; j < numBodies; j++) { // Calculate the force the bodies apply to one another const bI: i32 = i * bodySize const bJ: i32 = j * bodySize const f: f64[] = twoBodyForces( arrBodies[bI], arrBodies[bI + 1], arrBodies[bI + 2], arrBodies[bI + 3], // x,y,z,m arrBodies[bJ], arrBodies[bJ + 1], arrBodies[bJ + 2], arrBodies[bJ + 3], // x,y,z,m ) // Add this pair's force on one another to their total forces applied x,y,z const fI: i32 = i * forceSize const fJ: i32 = j * forceSize // body0 arrForces[fI] = arrForces[fI] + f[0] arrForces[fI + 1] = arrForces[fI + 1] + f[1] arrForces[fI + 2] = arrForces[fI + 2] + f[2] // body1 arrForces[fJ] = arrForces[fJ] - f[0] // apply forces in opposite direction arrForces[fJ + 1] = arrForces[fJ + 1] - f[1] arrForces[fJ + 2] = arrForces[fJ + 2] - f[2] } } // For each body, return the sum of forces all other bodies applied to it. // If you would like to debug wasm, you can use trace or the log functions // described in workerWasm when we initialized // Eg trace("nBodyForces returns (b0x, b0y, b0z, b1z): ", 4, arrForces[0], arrForces[1], arrForces[2], arrForces[3]) // x,y,z return arrForces // success }

WebAssembly 工件:.wasm 和 .wat

當我們的 AssemblyScript nBodyForces.ts被編譯成 WebAssembly nBodyForces.wasm二進製文件時,還有一個選項可以創建一個“文本”版本來描述二進製文件中的指令。

WebAssembly 工件
圖 3:請記住,AssemblyScript 是一種語言。 WebAssembly 是一個編譯器和運行時。

nBodyForces.wat文件中,我們可以看到這些導入和導出:

 ;; This is a comment in nBodyForces.wat (module ;; compiler defined types (type $FUNCSIG$iii (func (param i32 i32) (result i32))) … ;; Expected imports from JavaScript (import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32))) (import "env" "trace" (func $~lib/builtins/trace (param i32 i32 f64 f64 f64 f64 f64))) ;; Memory section defining data constants like strings (memory $0 1) (data (i32.const 8) "\1e\00\00\00\01\00\00\00\01\00\00\00\1e\00\00\00~\00l\00i\00b\00/\00r\00t\00/\00t\00l\00s\00f\00.\00t\00s\00") ... ;; Our global constants (not yet exported) (global $nBodyForces/FLOAT64ARRAY_ID i32 (i32.const 3)) (global $nBodyForces/G f64 (f64.const 6.674e-11)) (global $nBodyForces/bodySize i32 (i32.const 4)) (global $nBodyForces/forceSize i32 (i32.const 3)) ... ;; Memory management functions we'll use in a minute (export "memory" (memory $0)) (export "__alloc" (func $~lib/rt/tlsf/__alloc)) (export "__retain" (func $~lib/rt/pure/__retain)) (export "__release" (func $~lib/rt/pure/__release)) (export "__collect" (func $~lib/rt/pure/__collect)) (export "__rtti_base" (global $~lib/rt/__rtti_base)) ;; Finally our exported constants and function (export "FLOAT64ARRAY_ID" (global $nBodyForces/FLOAT64ARRAY_ID)) (export "G" (global $nBodyForces/G)) (export "bodySize" (global $nBodyForces/bodySize)) (export "forceSize" (global $nBodyForces/forceSize)) (export "nBodyForces" (func $nBodyForces/nBodyForces)) ;; Implementation details ...

我們現在有了nBodyForces.wasm二進製文件和一個運行它的 web worker。 準備好發射! 還有一些內存管理!

為了完成集成,我們必須將一個可變的浮點數組傳遞給 WebAssembly,並將一個可變的浮點數組返回給 JavaScript。

憑藉天真的 JavaScript 資產階級,我開始肆意將這些花哨的可變大小數組傳入和傳出跨平台的高性能運行時。 到目前為止,向/從 WebAssembly 傳遞數據是這個項目中最意想不到的困難。

但是,非常感謝 AssemblyScript 團隊所做的繁重工作,我們可以使用他們的“加載器”來提供幫助:

 // workerWasm.js - our web worker /** * AssemblyScript loader adds helpers for moving data to/from AssemblyScript. * Highly recommended */ const loader = require("assemblyscript/lib/loader")

require()意味著我們需要使用像 Rollup 或 Webpack 這樣的模塊打包器。 對於這個項目,我選擇 Rollup 是因為它的簡單性和靈活性,並且從未回頭。

請記住,我們的 web worker 在單獨的線程中運行,本質上是一個帶有switch()語句的onmessage()函數。

loader使用一些額外方便的內存管理功能創建我們的 wasm 模塊。 __retain()__release()管理 worker 運行時中的垃圾回收引用__allocArray()將我們的參數數組複製到 wasm 模塊的內存中__getFloat64Array()將結果數組從 wasm 模塊複製到 worker 運行時

我們現在可以將浮點數組編組進出nBodyForces()並完成我們的模擬:

 // workerWasm.js /** * Web workers listen for messages from the main thread. */ this.onmessage = function (evt) { // message from UI thread var msg = evt.data switch (msg.purpose) { // Message: Load new wasm module case 'wasmModule': // Instantiate the compiled module we were passed. wasm = loader.instantiate(msg.wasmModule, importObj) // Throws // Tell nBodySimulation.js we are ready this.postMessage({ purpose: 'wasmReady' }) return // Message: Given array of floats describing a system of bodies (x,y,x,mass), // calculate the Grav forces to be applied to each body case 'nBodyForces': if (!wasm) throw new Error('wasm not initialized') // Copy msg.arrBodies array into the wasm instance, increase GC count const dataRef = wasm.__retain(wasm.__allocArray(wasm.FLOAT64ARRAY_ID, msg.arrBodies)); // Do the calculations in this thread synchronously const resultRef = wasm.nBodyForces(dataRef); // Copy result array from the wasm instance to our javascript runtime const arrForces = wasm.__getFloat64Array(resultRef); // Decrease the GC count on dataRef from __retain() here, // and GC count from new Float64Array in wasm module wasm.__release(dataRef); wasm.__release(resultRef); // Message results back to main thread. // see nBodySimulation.js this.worker.onmessage return this.postMessage({ purpose: 'nBodyForces', arrForces }) } }

有了我們學到的所有知識,讓我們回顧一下我們的 Web Worker 和 WebAssembly 之旅。 歡迎使用新的網絡瀏覽器後端。 這些是 GitHub 上代碼的鏈接:

  1. 獲取索引.html
  2. main.js
  3. nBodySimulator.js - 將消息傳遞給它的 web worker
  4. workerWasm.js - 調用 WebAssembly 函數
  5. nBodyForces.ts - 計算並返回一組力
  6. workerWasm.js - 將結果傳回主線程
  7. nBodySimulator.js - 解決對部隊的承諾
  8. nBodySimulator.js - 然後將力施加到身體上並告訴可視化器進行繪畫

從這裡開始,讓我們通過創建nBodyVisualizer.js來開始展示吧! 我們的下一篇文章使用 Canvas API 創建了一個可視化器,最後一篇文章使用了 WebVR 和 Aframe。

相關: WebAssembly/Rust 教程:音高完美的音頻處理