WebVRパート3:WebAssemblyとAssemblyScriptの可能性を解き放つ

公開: 2022-03-11

WebAssemblyは、Webと世界の共通語として、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に委任したので、Webワーカーを管理します。

シミュレーションのアーキテクチャ図
図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ワーカースレッド
図2:シミュレーターのstep()関数の内部

前回の投稿では、WASM計算をラップするWebワーカーを構築しました。 今日、私たちは「WASM」というラベルの付いた小さなボックスを構築し、データを出し入れしています。

簡単にするために、計算を記述するためのソースコード言語としてAssemblyScriptを選択しました。 AssemblyScriptはTypeScriptのサブセットであり、型付きのJavaScriptであるため、既にご存知でしょう。

たとえば、このAssemblyScript関数は、2つのボディ間の重力を計算しsomeVar:f64:f64 f64は、someVar変数をコンパイラのfloatとしてマークします。 このコードは、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関数は、2つのボディの(x、y、z、mass)を受け取り、ボディが相互に適用する(x、y、z)力ベクトルを表す3つのfloatの配列を返します。 JavaScriptはどこにあるのかわからないため、JavaScriptからこの関数を呼び出すことはできません。 JavaScriptに「エクスポート」する必要があります。 これは私たちの最初の技術的課題につながります。

WebAssemblyのインポートとエクスポート

ES6では、JavaScriptコードでのインポートとエクスポートについて検討し、RollupやWebpackなどのツールを使用して、 importrequire()を処理するためにレガシーブラウザーで実行されるコードを作成します。 これにより、トップダウンの依存関係ツリーが作成され、「ツリーシェイク」やコード分割などの優れた技術が可能になります。

WebAssemblyでは、インポートとエクスポートはES6インポートとは異なるタスクを実行します。 WebAssemblyのインポート/エクスポート:

  • WebAssemblyモジュールのランタイム環境を提供します(例: trace()およびabort()関数)。
  • ランタイム間の関数と定数をインポートおよびエクスポートします。

以下のコードでは、 env.abortenv.traceは、WebAssemblyモジュールに提供する必要のある環境の一部です。 nBodyForces.logIおよびfriends関数は、コンソールにデバッグメッセージを提供します。 WebAssemblyの唯一のタイプはi32、i64、f32、f64の数値であり、i32は抽象線形メモリを参照しているため、WebAssemblyに文字列を出し入れすることは簡単ではないことに注意してください。

注:これらのコード例は、JavaScriptコード(Webワーカー)と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

これがJavaScriptから呼び出すnBodyForces()のエクスポートです。 ファイルの先頭にタイプFloat64Arrayをエクスポートして、Webワーカーで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

AssemblyScriptnBodyForces.tsをnBodyForces.ts 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ワーカーができました。 爆破の準備をしなさい! そして、いくつかのメモリ管理!

統合を完了するには、floatの変数配列をWebAssemblyに渡し、floatの変数配列を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ワーカーは別のスレッドで実行され、本質的にはswitch()ステートメントを使用したonmessage()関数であることを忘れないでください。

loaderは、いくつかの便利なメモリ管理機能を備えたwasmモジュールを作成します。 __retain()および__release()はワーカーランタイムでガベージコレクション参照を管理します__allocArray()はパラメーター配列をwasmモジュールのメモリにコピーします__getFloat64Array()は結果配列をwasmモジュールからワーカーランタイムにコピーします

これで、 nBodyForces()の内外でfloat配列をマーシャリングし、シミュレーションを完了することができます。

 // 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ワーカーとWebAssemblyの旅を振り返ってみましょう。 新しいブラウザへようこそ-ウェブのバックエンド。 これらはGitHubのコードへのリンクです:

  1. GET Index.html
  2. main.js
  3. nBodySimulator.js-Webワーカーにメッセージを渡します
  4. workerWasm.js-WebAssembly関数を呼び出します
  5. nBodyForces.ts-力の配列を計算して返します
  6. workerWasm.js-結果をメインスレッドに返します
  7. nBodySimulator.js-力の約束を解決します
  8. nBodySimulator.js-次に、ボディに力を適用し、ビジュアライザーにペイントするように指示します

ここから、 nBodyVisualizer.jsを作成してショーを始めましょう! 次の投稿では、Canvas APIを使用してビジュアライザーを作成し、最後の投稿はWebVRとAframeで締めくくります。

関連: WebAssembly / Rustチュートリアル:ピッチパーフェクトなオーディオ処理