个性化阅读
专注于IT技术分析

WebVR第3部分:释放WebAssembly和AssemblyScript的潜力

本文概述

WebAssembly绝对不是JavaScript的替代品, 因为它是网络和世界的通用语言。

WebAssembly(缩写为Wasm)是基于堆栈的虚拟机的二进制指令格式。 Wasm被设计为可移植目标, 用于编译C / C ++ / Rust等高级语言, 从而可以在Web上为客户端和服务器应用程序进行部署。” –WebAssembly.org

请务必区分WebAssembly不是一种语言。 WebAssembly就像一个” .exe”, 甚至更好-Java的” .class”文件。它是由Web开发人员从另一种语言编译而成的, 然后下载并在你的浏览器上运行。

WebAssembly为JavaScript提供了我们偶尔想借用但从未真正拥有的所有功能。就像租船或骑马一样, WebAssembly使我们可以使用其他语言, 而不必做出奢侈的”语言生活方式”选择。这使Web专注于重要的事情, 例如交付功能和改善用户体验。

超过20种语言可编译为WebAssembly:Rust, C / C ++, C#/。Net, Java, Python, Elixir, Go, 当然还有JavaScript。

如果你还记得我们模拟的架构图, 我们会将整个模拟委托给nBodySimulator, 以便它管理网络工作者。

模拟的架构图

图1:总体架构。

如果你在介绍性帖子中还记得, nBodySimulator具有每33ms调用一次的step()函数。 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 Worker线程

图2:模拟器的step()函数内部

在上一篇文章中, 我们构建了包装WASM计算的Web Worker。今天, 我们正在构建一个标有” WASM”的小盒子, 并将数据移入和移出。

为简单起见, 我选择AssemblyScript作为源代码语言来编写我们的计算。 AssemblyScript是TypeScript(它是一种类型化的JavaScript)的子集, 因此你已经知道了。

例如, 此AssemblyScript函数计算两个物体之间的重力:someVar:f64中的: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。这使我们面临第一个技术挑战。

Web装配体的进出口

在ES6中, 我们考虑使用JavaScript代码导入和导出, 并使用Rollup或Webpack之类的工具来创建在旧版浏览器中运行的代码, 以处理import和require()。这将创建一个自上而下的依赖关系树, 并启用”摇树”和代码拆分等酷技术。

在WebAssembly中, 导入和导出完成与ES6导入不同的任务。 WebAssembly导入/导出:

  • 为WebAssembly模块提供运行时环境(例如trace()和abort()函数)。
  • 在运行时之间导入和导出函数和常量。

在下面的代码中, env.abort和env.trace是我们必须提供给WebAssembly模块的环境的一部分。 nBodyForces.logI和friends函数将调试消息提供给控制台。请注意, 将字符串传入/传出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

注意:中止和跟踪将自动导入。

从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

这是nBodyForces()的导出, 我们将从JavaScript中调用它。我们在文件顶部导出了Float64Array类型, 因此我们可以在网络工作者中使用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   x x x x x
 * 1     x x x x
 * 2       x x x
 * 3         x x
 * 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
  // E.g. trace("nBodyForces returns (b0x, b0y, b0z, b1z): ", 4, arrForces[0], arrForces[1], arrForces[2], arrForces[3]) // x, y, z
  return arrForces  // success
}

Web组装工件:.wasm和.wat

当将AssemblyScript nBodyForces.ts编译为WebAssembly nBodyForces.wasm二进制文件时, 可以选择创建一个”文本”版本, 以描述二进制文件中的指令。

Web组装工件

图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二进制文件和一个网络工作者来运行它。准备好升空!还有一些内存管理!

要完成集成, 我们必须将浮点数的可变数组传递给WebAssembly, 然后将浮点数的可变数组返回给JavaScript。

对于朴素的JavaScript资产阶级, 我开始大胆地将这些花哨的可变大小的数组传入和传出跨平台高性能运行时。到目前为止, 向WebAssembly传递数据/从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是因为它的简单性和灵活性, 并且从不回头。

请记住, 我们的网络工作者在单独的线程中运行, 本质上是带有switch()语句的onmessage()函数。

loader使用一些额外的便捷内存管理功能来创建我们的wasm模块。 __retain()和__release()在工作程序运行时中管理垃圾回收引用__allocArray()将参数数组复制到wasm模块的内存中__getFloat64Array()将结果数组从wasm模块复制到工作程序运行时

现在, 我们可以将float数组编组到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
      })
  }
}

总结一下, 让我们回顾一下我们的网络工作者和WebAssembly之旅。欢迎使用新的网络浏览器后端。这些是GitHub上代码的链接:

  1. GET Index.html
  2. main.js
  3. nBodySimulator.js-将消息传递给其网络工作者
  4. workerWasm.js-调用WebAssembly函数
  5. nBodyForces.ts-计算并返回一个力数组
  6. workerWasm.js-将结果传递回主线程
  7. nBodySimulator.js-解决对部队的承诺
  8. nBodySimulator.js-然后将力施加到主体并告诉可视化工具进行绘制

从这里开始, 通过创建nBodyVisualizer.js开始表演!我们的下一篇文章使用Canvas API创建一个可视化工具, 最后一篇文章使用WebVR和Aframe进行总结。

赞(0)
未经允许不得转载:srcmini » WebVR第3部分:释放WebAssembly和AssemblyScript的潜力

评论 抢沙发

评论前必须登录!