WebVR 3부: WebAssembly 및 AssemblyScript의 잠재력 실현

게시 됨: 2022-03-11

WebAssembly는 웹과 세계의 공용어로서 JavaScript를 확실히 대체 하지 않습니다 .

WebAssembly(약칭 Wasm)는 스택 기반 가상 머신을 위한 이진 명령 형식입니다. Wasm은 C/C++/Rust와 같은 고급 언어 컴파일을 위한 이식 가능한 대상으로 설계되어 클라이언트 및 서버 응용 프로그램을 위해 웹에 배포할 수 있습니다.” – WebAssembly.org

WebAssembly가 언어가 아니라는 점을 구별하는 것이 중요합니다. WebAssembly는 '.exe' 또는 더 나은 Java '.class' 파일과 비슷합니다. 웹 개발자가 다른 언어로 컴파일한 다음 다운로드하여 브라우저에서 실행합니다.

WebAssembly는 우리가 가끔 빌리고 싶었지만 실제로 소유하고 싶지 않았던 모든 기능을 JavaScript에 제공하고 있습니다. 보트나 말을 빌리는 것과 마찬가지로 WebAssembly를 사용하면 사치스러운 "언어 생활 방식"을 선택하지 않고도 다른 언어로 여행할 수 있습니다. 이를 통해 웹은 기능 제공 및 사용자 경험 개선과 같은 중요한 사항에 집중할 수 있습니다.

Rust, C/C++, C#/.Net, Java, Python, Elixir, Go 및 JavaScript와 같은 20개 이상의 언어가 WebAssembly로 컴파일됩니다.

시뮬레이션의 아키텍처 다이어그램을 기억한다면 전체 시뮬레이션을 nBodySimulator 에 위임하여 웹 작업자를 관리합니다.

시뮬레이션의 아키텍처 다이어그램
그림 1: 전체 아키텍처.

소개 포스트에서 기억한다면 nBodySimulator 에는 33ms마다 호출되는 step() 함수가 있습니다. step() 함수는 위의 다이어그램에서 번호가 매겨진 다음 작업을 수행합니다.

  1. nBodySimulator의 computeForces 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 스레드, 웹 작업자 스레드
그림 2: 시뮬레이터의 step() 함수 내부

이전 게시물에서 우리는 WASM 계산을 래핑하는 웹 작업자를 구축했습니다. 오늘날 우리는 "WASM"이라는 레이블이 붙은 작은 상자를 만들고 데이터를 안팎으로 이동합니다.

단순화를 위해 저는 AssemblyScript를 소스 코드 언어로 선택하여 계산을 작성했습니다. AssemblyScript는 유형이 지정된 JavaScript인 TypeScript의 하위 집합이므로 이미 알고 있습니다.

예를 들어, 이 AssemblyScript 함수는 두 바디 사이의 중력을 계산합니다. someVar:f64의 :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, 질량)을 취하고 몸체가 서로 적용되는 (x, y, z) 힘 벡터를 설명하는 세 개의 부동 소수점 배열을 반환합니다. JavaScript는 이 함수를 어디에서 찾을 수 있는지 모르기 때문에 JavaScript에서 이 함수를 호출할 수 없습니다. JavaScript로 "내보내기"해야 합니다. 이것은 우리에게 첫 번째 기술적 과제를 안겨줍니다.

WebAssembly 가져오기 및 내보내기

ES6에서는 JavaScript 코드의 가져오기 및 내보내기에 대해 생각하고 Rollup 또는 Webpack과 같은 도구를 사용하여 importrequire() 를 처리하기 위해 레거시 브라우저에서 실행되는 코드를 만듭니다. 이것은 하향식 종속성 트리를 생성하고 "트리 쉐이킹" 및 코드 분할과 같은 멋진 기술을 가능하게 합니다.

WebAssembly에서 가져오기 및 내보내기는 ES6 가져오기와 다른 작업을 수행합니다. WebAssembly 가져오기/내보내기:

  • WebAssembly 모듈을 위한 런타임 환경을 제공합니다(예: trace()abort() 함수).
  • 런타임 간에 함수와 상수를 가져오고 내보냅니다.

아래 코드에서 env.abortenv.trace 는 WebAssembly 모듈에 제공해야 하는 환경의 일부입니다. nBodyForces.logI 및 friend 함수는 콘솔에 디버깅 메시지를 제공합니다. WebAssembly의 유일한 유형은 추상 선형 메모리에 대한 i32 참조가 있는 i32, i64, f32, f64 숫자이므로 WebAssembly 안팎으로 문자열을 전달하는 것은 중요하지 않습니다.

참고: 이 코드 예제는 JavaScript 코드(웹 작업자)와 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() 내보내기입니다. 웹 작업자에서 AssemblyScript의 JavaScript 로더를 사용하여 데이터를 가져올 수 있도록 파일 맨 위에 Float64Array 유형을 내보냅니다(아래 참조).

 // 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 바이너리와 이를 실행할 웹 작업자가 있습니다. 폭발을 준비하세요! 그리고 약간의 메모리 관리!

통합을 완료하려면 가변 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을 선택했고 결코 뒤돌아보지 않았습니다.

웹 작업자는 별도의 스레드에서 실행되고 본질적으로 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 }) } }

우리가 배운 모든 것을 가지고 웹 작업자와 WebAssembly 여정을 검토해 보겠습니다. 웹의 새로운 브라우저 백엔드에 오신 것을 환영합니다. 다음은 GitHub의 코드에 대한 링크입니다.

  1. GET Index.html
  2. 메인.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으로 마무리합니다.

관련: WebAssembly/Rust 튜토리얼: 완벽한 피치 오디오 처리