WebVR ตอนที่ 3: การปลดล็อกศักยภาพของ WebAssembly และ AssemblyScript
เผยแพร่แล้ว: 2022-03-11WebAssembly ไม่ได้ แทนที่ JavaScript อย่างแน่นอนในฐานะภาษากลางของเว็บและโลก
WebAssembly (ตัวย่อ Wasm) เป็นรูปแบบคำสั่งไบนารีสำหรับเครื่องเสมือนแบบสแต็ก Wasm ได้รับการออกแบบให้เป็นเป้าหมายแบบพกพาสำหรับการรวบรวมภาษาระดับสูง เช่น C/C++/Rust ทำให้สามารถปรับใช้บนเว็บสำหรับแอปพลิเคชันไคลเอนต์และเซิร์ฟเวอร์” –WebAssembly.org
สิ่งสำคัญคือต้องแยกแยะว่า WebAssembly ไม่ใช่ภาษา WebAssembly เหมือนกับ '.exe' - หรือดีกว่านั้น - ไฟล์ Java '.class' มันถูกรวบรวมโดยนักพัฒนาเว็บจากภาษาอื่น จากนั้นดาวน์โหลดและเรียกใช้บนเบราว์เซอร์ของคุณ
WebAssembly ให้คุณสมบัติทั้งหมดที่เราต้องการยืม JavaScript เป็นครั้งคราว แต่ไม่เคยต้องการเป็นเจ้าของ เลย เหมือนกับการเช่าเรือหรือม้า WebAssembly ช่วยให้เราเดินทางไปยังภาษาอื่นๆ ได้โดยไม่ต้องเลือก "รูปแบบการใช้ชีวิตของภาษา" ที่ฟุ่มเฟือย ซึ่งช่วยให้เว็บมุ่งเน้นไปที่สิ่งที่สำคัญ เช่น การนำเสนอคุณลักษณะและการปรับปรุงประสบการณ์ของผู้ใช้
มากกว่า 20 ภาษาที่คอมไพล์ไปยัง WebAssembly: Rust, C/C++, C#/.Net, Java, Python, Elixir, Go และแน่นอน JavaScript
หากคุณจำไดอะแกรมสถาปัตยกรรมของการจำลองได้ เราได้มอบหมายการจำลองทั้งหมดให้กับ nBodySimulator
ดังนั้นมันจึงจัดการ Web Worker
หากคุณจำได้จากโพสต์แนะนำ nBodySimulator
มีฟังก์ชัน step()
ที่เรียกทุกๆ 33ms ฟังก์ชัน step()
ทำหน้าที่เหล่านี้ - หมายเลขในแผนภาพด้านบน:
-
calculateForces()
ของ nBodySimulator เรียกthis.worker.postMessage()
เพื่อเริ่มการคำนวณ - workerWasm.js
this.onmessage()
ได้รับข้อความ - workerWasm.js เรียกใช้ฟังก์ชัน
nBodyForces()
ของ nBodyForces.wasm พร้อมกัน - workerWasm.js ตอบกลับโดยใช้
this.postMessage()
ไปยังเธรดหลักด้วยกองกำลังใหม่ - เธรดหลัก
this.worker.onMessage()
จัดการข้อมูลและการโทรที่ส่งคืน - nBodySimulator's
applyForces()
เพื่ออัปเดตตำแหน่งของร่างกาย - ในที่สุด Visualizer จะทาสีใหม่
ในโพสต์ที่แล้ว เราได้สร้าง Web Worker ที่รวมการคำนวณ WASM ของเราไว้ วันนี้ เรากำลังสร้างกล่องเล็กๆ ที่ชื่อ “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, มวล) สำหรับเนื้อหาสองส่วนและส่งคืนอาร์เรย์ของสามทุ่นที่อธิบาย (x, y, z) แรงเวกเตอร์ที่เนื้อหาใช้ซึ่งกันและกัน เราไม่สามารถเรียกฟังก์ชันนี้จาก JavaScript ได้เนื่องจาก JavaScript ไม่รู้ว่าจะหาได้จากที่ไหน เราต้อง "ส่งออก" เป็น JavaScript สิ่งนี้นำเราไปสู่ความท้าทายทางเทคนิคครั้งแรกของเรา
WebAssembly นำเข้าและส่งออก
ใน ES6 เรานึกถึงการนำเข้าและส่งออกในโค้ด JavaScript และใช้เครื่องมืออย่างเช่น Rollup หรือ Webpack เพื่อสร้างโค้ดที่ทำงานในเบราว์เซอร์รุ่นเก่าเพื่อจัดการ import
และ require()
สิ่งนี้จะสร้างแผนผังการพึ่งพาจากบนลงล่างและเปิดใช้งานเทคโนโลยีเจ๋งๆ เช่น "การเขย่าต้นไม้" และการแยกโค้ด
ใน WebAssembly การนำเข้าและส่งออกจะบรรลุภารกิจที่แตกต่างจากการนำเข้า ES6 การนำเข้า/ส่งออก WebAssembly:
- จัดเตรียมสภาพแวดล้อมรันไทม์สำหรับโมดูล WebAssembly (เช่น ฟังก์ชัน
trace()
และabort()
) - นำเข้าและส่งออกฟังก์ชันและค่าคงที่ระหว่างรันไทม์
ในโค้ดด้านล่าง env.abort
และ env.trace
เป็นส่วนหนึ่งของสภาพแวดล้อมที่เราต้องจัดเตรียมให้กับโมดูล WebAssembly nBodyForces.logI
และผองเพื่อน จัดเตรียมข้อความการดีบักไปยังคอนโซล โปรดทราบว่าการส่งสตริงเข้า/ออกจาก WebAssembly นั้นไม่สำคัญ เนื่องจากประเภทเดียวของ WebAssembly คือหมายเลข i32, i64, f32, f64 โดยที่ i32 อ้างอิงถึงหน่วยความจำเชิงเส้นแบบนามธรรม
หมายเหตุ: ตัวอย่างโค้ดเหล่านี้จะสลับไปมาระหว่างโค้ด 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
และนี่คือการส่งออกของ nBodyForces()
ซึ่งเราจะเรียกจาก JavaScript เราส่งออกประเภท Float64Array
ที่ด้านบนสุดของไฟล์ เพื่อให้เราสามารถใช้ตัวโหลด JavaScript ของ AssemblyScript ใน Web Worker ของเราเพื่อรับข้อมูล (ดูด้านล่าง):
// 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 Artifacts: .wasm และ .wat
เมื่อ AssemblyScript nBodyForces.ts
ของเราถูกคอมไพล์เป็นไบนารี nBodyForces.wasm
จะมีตัวเลือกในการสร้างเวอร์ชัน "ข้อความ" ที่อธิบายคำแนะนำในไบนารี

ภายในไฟล์ 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
และพนักงานเว็บเพื่อใช้งาน เตรียมตัวให้พร้อม! และการจัดการหน่วยความจำบางอย่าง!
เพื่อให้การผสานรวมเสร็จสมบูรณ์ เราต้องส่งอาร์เรย์ตัวแปรของ floats ไปยัง WebAssembly และส่งคืนอาร์เรย์ตัวแปรของ floats ไปยัง 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 เนื่องจากมีความเรียบง่ายและยืดหยุ่น และไม่เคยมองย้อนกลับไป
โปรดจำไว้ว่าผู้ปฏิบัติงานเว็บของเราทำงานในเธรดแยกต่างหากและโดยพื้นฐานแล้วเป็น onmessage()
ที่มีคำสั่ง switch()
loader
สร้างโมดูล wasm ของเราด้วยฟังก์ชันการจัดการหน่วยความจำที่มีประโยชน์เป็นพิเศษ __retain()
และ __release()
จัดการการอ้างอิงการรวบรวมขยะในรันไทม์ของผู้ปฏิบัติงาน __allocArray()
คัดลอกอาร์เรย์พารามิเตอร์ของเราลงในหน่วยความจำของโมดูล __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 }) } }
จากทั้งหมดที่เราได้เรียนรู้มา มาทบทวน Web Worker และเส้นทาง WebAssembly กัน ยินดีต้อนรับสู่แบ็กเอนด์เบราว์เซอร์ใหม่ของเว็บ นี่คือลิงค์ไปยังรหัสบน GitHub:
- รับ Index.html
- main.js
- nBodySimulator.js - ส่งข้อความไปยังผู้ดูแลเว็บ
- workerWasm.js - เรียกฟังก์ชัน WebAssembly
- nBodyForces.ts - คำนวณและส่งกลับอาร์เรย์ของกองกำลัง
- workerWasm.js - ส่งผลลัพธ์กลับไปที่เธรดหลัก
- nBodySimulator.js - แก้ไขคำมั่นสัญญาสำหรับกองกำลัง
- nBodySimulator.js - จากนั้นใช้แรงกับร่างกายและบอกให้นักสร้างภาพระบายสี
จากนี้ไป มาเริ่มการแสดงโดยสร้าง nBodyVisualizer.js
! โพสต์ถัดไปของเราสร้างวิชวลไลเซอร์โดยใช้ Canvas API และโพสต์สุดท้ายปิดท้ายด้วย WebVR และ Aframe