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 教程:音高完美的音频处理