WebVR الجزء 3: فتح إمكانات WebAssembly و AssemblyScript

نشرت: 2022-03-11

WebAssembly بالتأكيد ليس بديلاً عن JavaScript باعتبارها لغة مشتركة للويب والعالم.

WebAssembly (اختصارًا Wasm) هو تنسيق تعليمات ثنائي لجهاز ظاهري قائم على المكدس. تم تصميم Wasm كهدف محمول لتجميع اللغات عالية المستوى مثل C / C ++ / Rust ، مما يتيح النشر على الويب لتطبيقات العميل والخادم. " –WebAssembly.org

من المهم التمييز بين أن WebAssembly ليس لغة. يشبه WebAssembly ملف ".exe" - أو أفضل منه - ملف Java ".class". يتم تجميعها بواسطة مطور الويب من لغة أخرى ، ثم يتم تنزيلها وتشغيلها على متصفحك.

يمنح WebAssembly جافا سكريبت بجميع الميزات التي أردنا استعارةها من حين لآخر ولكننا لم نرغب أبدًا في امتلاكها. يتيح لنا WebAssembly ، تمامًا مثل استئجار قارب أو حصان ، السفر إلى لغات أخرى دون الاضطرار إلى اتخاذ خيارات "أسلوب حياة اللغة" الباهظة. وقد سمح ذلك للويب بالتركيز على أشياء مهمة مثل تقديم الميزات وتحسين تجربة المستخدم.

يتم تجميع أكثر من 20 لغة إلى WebAssembly: Rust و C / C ++ و C # /. Net و Java و Python و Elixir و Go وبالطبع JavaScript.

إذا كنت تتذكر الرسم التخطيطي لهندسة المحاكاة ، فقد فوضنا المحاكاة بالكامل إلى nBodySimulator ، لذلك فهو يدير عامل الويب.

مخطط هندسة المحاكاة
الشكل 1: العمارة الشاملة.

إذا كنت تتذكر من منشور المقدمة ، nBodySimulator له وظيفة step() تسمى كل 33 مللي ثانية. تقوم وظيفة step() بهذه الأشياء - مرقمة في الرسم التخطيطي أعلاه:

  1. يستدعي nBodySimulator's calculateForces() this.worker.postMessage() لبدء الحساب.
  2. workerWasm.js this.onmessage() يحصل على الرسالة.
  3. يعمل workerWasm.js بشكل متزامن على تشغيل وظيفة nBodyForces() .
  4. يرد workerWasm.js باستخدام this.postMessage() على الموضوع الرئيسي بالقوى الجديدة.
  5. ينظم هذا الموضوع الرئيسي this.worker.onMessage() البيانات والمكالمات التي تم إرجاعها.
  6. nBodySimulator's applyForces() مواقع الجثث.
  7. أخيرًا ، يعيد المتخيل الطلاء.

مؤشر ترابط واجهة المستخدم ، مؤشر ترابط عامل الويب
الشكل 2: داخل وظيفة step () الخاصة بالمحاكي

في المنشور السابق ، قمنا ببناء عامل الويب الذي يغلف حسابات 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 في عامل الويب الخاص بنا للحصول على البيانات (انظر أدناه):

 // 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 ثنائي وعامل ويب لتشغيله. احصل على استعداد للانطلاق! وبعض إدارة الذاكرة!

لإكمال التكامل ، يتعين علينا تمرير مصفوفة متغيرة من العوامات إلى 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 لبساطته ومرونته ولم أنظر إلى الوراء أبدًا.

تذكر أن عامل الويب الخاص بنا يعمل في سلسلة رسائل منفصلة وهي في الأساس وظيفة onmessage() مع عبارة switch() .

ينشئ loader وحدة wasm الخاصة بنا مع بعض وظائف إدارة الذاكرة المفيدة الإضافية. __retain() و __release() إدارة مراجع جمع البيانات المهملة في وقت تشغيل العامل __allocArray() ينسخ مصفوفة المعلمات الخاصة بنا في ذاكرة وحدة wasm __getFloat64Array() ينسخ مصفوفة النتيجة من الوحدة النمطية wasm إلى وقت تشغيل العامل

يمكننا الآن تنظيم مصفوفات الطفو داخل وخارج 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. مرحبًا بك في المتصفح الخلفي الجديد للويب. هذه روابط للكود على جيثب:

  1. احصل على Index.html
  2. main.js
  3. nBodySimulator.js - يمرر رسالة إلى عامل الويب الخاص بها
  4. workerWasm.js - لاستدعاء وظيفة WebAssembly
  5. nBodyForces.ts - حساب وإرجاع مصفوفة من القوى
  6. العامل Wasm.js - يمرر النتائج مرة أخرى إلى الموضوع الرئيسي
  7. nBodySimulator.js - يحسم الوعد بالقوى
  8. nBodySimulator.js - ثم يطبق القوى على الأجسام ويطلب من المتخيل أن يرسم

من هنا ، لنبدأ العرض بإنشاء nBodyVisualizer.js ! المنشور التالي ينشئ متخيلًا باستخدام Canvas API ، وينتهي المنشور الأخير مع WebVR و Aframe.

الموضوعات ذات الصلة: WebAssembly / برنامج Rust التعليمي: معالجة الصوت بدرجة مثالية