WebVR الجزء 4: تصورات بيانات Canvas
نشرت: 2022-03-11يا هلا! شرعنا في إنشاء إثبات مفهوم لـ WebVR. أكملت منشوراتنا السابقة في المدونة المحاكاة ، لذا حان الوقت الآن للعب القليل من الإبداع.
هذا وقت مثير للغاية أن تكون مصممًا ومطورًا لأن الواقع الافتراضي هو نقلة نوعية.
في عام 2007 ، باعت شركة Apple أول هاتف iPhone ، مما أدى إلى ثورة استهلاك الهواتف الذكية. بحلول عام 2012 ، كنا مهتمين بتصميم الويب "الجوال أولاً" و "سريع الاستجابة". في عام 2019 ، أصدر Facebook و Oculus أول سماعة رأس للواقع الافتراضي. هيا بنا نقوم بذلك!
لم يكن الإنترنت "المحمول أولاً" بدعة ، وأتوقع أن الإنترنت "VR-first" لن يكون كذلك. في المقالات والعروض التوضيحية الثلاثة السابقة ، أوضحت الإمكانية التكنولوجية في متصفحك الحالي .
إذا كنت تلتقط هذا في منتصف السلسلة ، فنحن نبني محاكاة الجاذبية السماوية للكواكب الشائكة.
- الجزء 1: المقدمة والعمارة
- الجزء 2: يزودنا عمال الويب بسلاسل مستعرض إضافية
- الجزء 3: WebAssembly و AssemblyScript لكود اختناق الأداء O (n²)
بالوقوف على العمل الذي قمنا به ، حان الوقت لبعض اللعب الإبداعي. في آخر نشرتين ، سنستكشف اللوحة القماشية و WebVR وتجربة المستخدم.
- الجزء 4: تصور بيانات Canvas (هذا المنشور)
- الجزء 5: تصور بيانات WebVR
اليوم ، سنجعل محاكاةنا تنبض بالحياة. بالنظر إلى الوراء ، لاحظت كم كنت أكثر حماسة واهتمامًا بإكمال المشروع بمجرد أن بدأت العمل على المصورين. جعلتها التصورات مثيرة للاهتمام للآخرين.
كان الغرض من هذه المحاكاة هو استكشاف التكنولوجيا التي ستمكّن WebVR - الواقع الافتراضي في المتصفح - وشبكة VR الأولى القادمة. يمكن لهذه التقنيات نفسها تشغيل حوسبة حافة المتصفح.
بعد الانتهاء من إثبات المفهوم ، سنقوم اليوم أولاً بإنشاء تصور للقماش.
في المنشور الأخير ، سنلقي نظرة على تصميم VR ونصنع نسخة WebVR لإنجاز هذا المشروع.
أبسط شيء يمكن أن ينجح: console.log()
العودة إلى RR (الواقع). لنقم بإنشاء بعض التصورات لمحاكاة "n-body" القائمة على المتصفح. لقد استخدمت قماشًا في تطبيقات فيديو الويب في مشاريع سابقة ولكن لم أستخدمها أبدًا كقماش فنان. دعونا نرى ما يمكننا القيام به.
إذا كنت تتذكر بنية مشروعنا ، فقد فوضنا التصور إلى nBodyVisualizer.js
.
يحتوي nBodySimulator.js
على start()
تستدعي دالة step()
() ، ويستدعي الجزء السفلي من step()
this.visualize()
// src/nBodySimulator.js /** * This is the simulation loop. */ async step() { // Skip calculation if worker not ready. Runs every 33ms (30fps). Will skip. if (this.ready()) { await this.calculateForces() } else { console.log(`Skipping calculation: ${this.workerReady} ${this.workerCalculating}`) } // Remove any "debris" that has traveled out of bounds // This keeps the button from creating uninteresting work. this.trimDebris() // Now Update forces. Reuse old forces if worker is already busy calculating. this.applyForces() // Now Visualize this.visualize() }
عندما نضغط على الزر الأخضر ، يضيف الخيط الرئيسي 10 هيئات عشوائية إلى النظام. لقد لمسنا رمز الزر في المنشور الأول ، ويمكنك رؤيته في الريبو هنا. هذه الهيئات رائعة لاختبار إثبات المفهوم ، لكن تذكر أننا في منطقة أداء خطيرة - O (n²).
صُمم البشر trimDebris()
تزيل الأشياء التي تطير بعيدًا عن الأنظار حتى لا تبطئ البقية. هذا هو الفرق بين الأداء المتصور والفعلي.
الآن بعد أن غطينا كل شيء ما عدا النهائي this.visualize()
، دعنا نلقي نظرة!
// src/nBodySimulator.js /** * Loop through our visualizers and paint() */ visualize() { this.visualizations.forEach(vis => { vis.paint(this.objBodies) }) } /** * Add a visualizer to our list */ addVisualization(vis) { this.visualizations.push(vis) }
تتيح لنا هاتان الوظيفتان إضافة متخيلات متعددة. يوجد متخيلان في إصدار اللوحة القماشية:
// src/main.js window.onload = function() { // Create a Simulation const sim = new nBodySimulator() // Add some visualizers sim.addVisualization( new nBodyVisPrettyPrint(document.getElementById("visPrettyPrint")) ) sim.addVisualization( new nBodyVisCanvas(document.getElementById("visCanvas")) ) …
في إصدار لوحة الرسم ، المتخيل الأول هو جدول الأرقام البيضاء المعروضة بتنسيق HTML. المتخيل الثاني هو عنصر قماش أسود تحته.
لإنشاء هذا ، بدأت بفئة أساسية بسيطة في nBodyVisualizer.js
:
// src/nBodyVisualizer.js /** * This is a toolkit of visualizers for our simulation. */ /** * Base class that console.log()s the simulation state. */ export class nBodyVisualizer { constructor(htmlElement) { this.htmlElement = htmlElement this.resize() } resize() {} paint(bodies) { console.log(JSON.stringify(bodies, null, 2)) } }
هذه الفئة تطبع إلى وحدة التحكم (كل 33 مللي ثانية!) وتتتبع أيضًا عنصر html - الذي سنستخدمه في الفئات الفرعية لتسهيل التصريح عنها في main.js
هذا هو أبسط شيء يمكن أن يعمل.
ومع ذلك ، في حين أن تصور console
هذا بسيط بالتأكيد ، إلا أنه في الواقع لا "يعمل". لم يتم تصميم وحدة تحكم المتصفح (وتصفح البشر) لمعالجة رسائل السجل بسرعة 33 مللي ثانية. لنجد ثاني أبسط شيء يمكن أن يعمل.
تصور المحاكاة بالبيانات
كان التكرار التالي "طباعة جميلة" هو طباعة نص إلى عنصر HTML. هذا أيضًا هو النمط الذي نستخدمه لتنفيذ قماش الرسم.
لاحظ أننا htmlElement
مرجعًا لعنصر html سوف يرسم المتخيل عليه. مثل أي شيء آخر على الويب ، يتميز بتصميمه المحمول أولاً. على سطح المكتب ، يؤدي هذا إلى طباعة جدول بيانات الكائنات وإحداثياتها على يسار الصفحة. على الهاتف المحمول قد ينتج عنه فوضى بصرية لذلك نتخطاه.
/** * Pretty print simulation to an htmlElement's innerHTML */ export class nBodyVisPrettyPrint extends nBodyVisualizer { constructor(htmlElement) { super(htmlElement) this.isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); } resize() {} paint(bodies) { if (this.isMobile) return let text = '' function pretty(number) { return number.toPrecision(2).padStart(10) } bodies.forEach( body => { text += `<br>${body.name.padStart(12)} { x:${pretty(body.x)} y:${pretty(body.y)} z:${pretty(body.z)} mass:${pretty(body.mass)}) }` }) if (this.htmlElement) this.htmlElement.innerHTML = text } }
يحتوي متخيل "تدفق البيانات" هذا على وظيفتين:
- إنها طريقة "للتحقق من سلامة" مدخلات المحاكاة في المتخيل. هذه نافذة "تصحيح".
- من الرائع النظر إليه ، لذلك دعونا نحتفظ به في العرض التوضيحي لسطح المكتب!
الآن بعد أن أصبحنا واثقين تمامًا من مدخلاتنا ، فلنتحدث عن الرسومات والقماش.

تصور المحاكاة باستخدام قماش ثنائي الأبعاد
"محرك اللعبة" هو "محرك محاكاة" به انفجارات. كلاهما أدوات معقدة بشكل لا يصدق لأنهما يركزان على خطوط أنابيب الأصول ، وتحميل مستوى التدفق ، وجميع أنواع الأشياء المملة بشكل لا يصدق التي لا ينبغي ملاحظتها أبدًا.
أنشأ الويب أيضًا "أشياء لا يجب ملاحظتها أبدًا" باستخدام تصميم "الجوال أولاً". إذا تم تغيير حجم المتصفح ، فسيقوم CSS الخاص بنا بتغيير حجم عنصر Canvas في DOM ، لذلك يجب أن يتكيف المتخيل لدينا أو يعاني من ازدراء المستخدمين.
#visCanvas { margin: 0; padding: 0; background-color: #1F1F1F; overflow: hidden; width: 100vw; height: 100vh; }
يؤدي هذا المطلب إلى resize()
في الفئة الأساسية nBodyVisualizer
وتنفيذ canvas.
/** * Draw simulation state to canvas */ export class nBodyVisCanvas extends nBodyVisualizer { constructor(htmlElement) { super(htmlElement) // Listen for resize to scale our simulation window.onresize = this.resize.bind(this) } // If the window is resized, we need to resize our visualization resize() { if (!this.htmlElement) return this.sizeX = this.htmlElement.offsetWidth this.sizeY = this.htmlElement.offsetHeight this.htmlElement.width = this.sizeX this.htmlElement.height = this.sizeY this.vis = this.htmlElement.getContext('2d') }
ينتج عن هذا المصور لدينا ثلاث خصائص أساسية:
- يمكن استخدام
this.vis
- لرسم الأوليات -
this.sizeX
-
this.sizeY
- أبعاد منطقة الرسم
ملاحظات تصميم التصور 2D قماش
يعمل تغيير الحجم لدينا مقابل تنفيذ Canvas الافتراضي. إذا كنا نتخيل رسمًا بيانيًا للمنتج أو البيانات ، فسنريد:
- ارسم على اللوحة القماشية (بحجم مفضل ونسبة عرض إلى ارتفاع)
- ثم اطلب من المتصفح تغيير حجم الرسم في عنصر DOM أثناء تخطيط الصفحة
في حالة الاستخدام الأكثر شيوعًا ، يكون المنتج أو الرسم البياني هو محور التجربة.
إن تصورنا هو بدلاً من ذلك تصور مسرحي لاتساع الفضاء ، يتم تمثيله بشكل درامي من خلال قذف عشرات العوالم الصغيرة في الفراغ من أجل المتعة.
تُظهر أجرامنا السماوية تلك المساحة من خلال التواضع - مع الحفاظ على عرضها بين 0 و 20 بكسل. يؤدي تغيير الحجم هذا إلى قياس المسافة بين النقاط لخلق إحساس بالاتساع "العلمي" وتعزيز السرعة المدركة.
لخلق إحساس بالمقياس بين الكائنات ذات الكتل المختلفة إلى حد كبير ، نقوم بتهيئة الأجسام باستخدام drawSize
يتناسب مع الكتلة:
// nBodySimulation.js export class Body { constructor(name, color, x, y, z, mass, vX, vY, vZ) { ... this.drawSize = Math.min( Math.max( Math.log10(mass), 1), 10) } }
صناعة أنظمة الطاقة الشمسية حسب الطلب
الآن ، عندما ننشئ نظامنا الشمسي في main.js
، سيكون لدينا جميع الأدوات التي نحتاجها لتصورنا:
// Set Z coords to 1 for best visualization in overhead 2D canvas // Making up stable universes is hard // name color x y z m vz vy vz sim.addBody(new Body("star", "yellow", 0, 0, 0, 1e9)) sim.addBody(new Body("hot jupiter", "red", -1, -1, 0, 1e4, .24, -0.05, 0)) sim.addBody(new Body("cold jupiter", "purple", 4, 4, -.1, 1e4, -.07, 0.04, 0)) // A couple far-out asteroids to pin the canvas visualization in place. sim.addBody(new Body("asteroid", "black", -15, -15, 0, 0)) sim.addBody(new Body("asteroid", "black", 15, 15, 0, 0)) // Start simulation sim.start()
قد تلاحظ اثنين من "الكويكبات" في الأسفل. هذه الكائنات ذات الكتلة الصفرية عبارة عن اختراق يستخدم "لتثبيت" أصغر منفذ عرض للمحاكاة في منطقة 30 × 30 تتمحور حول 0،0.
نحن الآن جاهزون لوظيفة الطلاء لدينا. يمكن أن "تتمايل" سحابة الأجسام بعيدًا عن الأصل (0،0،0) ، لذلك يجب علينا أيضًا التحول بالإضافة إلى الحجم.
نحن "انتهينا" عندما يكون للمحاكاة إحساس طبيعي بها. لا توجد طريقة "صحيحة" للقيام بذلك. لترتيب المواضع الأولية للكوكب ، عابثت بالأرقام حتى تماسكت معًا لفترة كافية لتكون مثيرة للاهتمام.
// Paint on the canvas paint(bodies) { if (!this.htmlElement) return // We need to convert our 3d float universe to a 2d pixel visualization // calculate shift and scale const bounds = this.bounds(bodies) const shiftX = bounds.xMin const shiftY = bounds.yMin const twoPie = 2 * Math.PI let scaleX = this.sizeX / (bounds.xMax - bounds.xMin) let scaleY = this.sizeY / (bounds.yMax - bounds.yMin) if (isNaN(scaleX) || !isFinite(scaleX) || scaleX < 15) scaleX = 15 if (isNaN(scaleY) || !isFinite(scaleY) || scaleY < 15) scaleY = 15 // Begin Draw this.vis.clearRect(0, 0, this.vis.canvas.width, this.vis.canvas.height) bodies.forEach((body, index) => { // Center const drawX = (body.x - shiftX) * scaleX const drawY = (body.y - shiftY) * scaleY // Draw on canvas this.vis.beginPath(); this.vis.arc(drawX, drawY, body.drawSize, 0, twoPie, false); this.vis.fillStyle = body.color || "#aaa" this.vis.fill(); }); } // Because we draw the 3D space in 2D from the top, we ignore z bounds(bodies) { const ret = { xMin: 0, xMax: 0, yMin: 0, yMax: 0, zMin: 0, zMax: 0 } bodies.forEach(body => { if (ret.xMin > body.x) ret.xMin = body.x if (ret.xMax < body.x) ret.xMax = body.x if (ret.yMin > body.y) ret.yMin = body.y if (ret.yMax < body.y) ret.yMax = body.y if (ret.zMin > body.z) ret.zMin = body.z if (ret.zMax < body.z) ret.zMax = body.z }) return ret } }
يتكون رمز الرسم على القماش الفعلي من خمسة أسطر فقط - يبدأ كل منها بـ this.vis
. ما تبقى من الكود هو قبضة المشهد.
الفن لا ينتهي ابدا ، يجب التخلي عنه
عندما يبدو أن العملاء ينفقون أموالًا لن تكسبهم ، فإن الوقت الحالي هو الوقت المناسب لجلبها. الاستثمار في الفن هو قرار تجاري.
قرر عميل هذا المشروع (أنا) الانتقال من تطبيق Canvas إلى WebVR. أردت عرض WebVR توضيحيًا مبهرجًا ومليئًا بالضجيج. لذلك دعونا نختتم هذا ونحصل على بعض من ذلك!
مع ما تعلمناه ، يمكننا أن نأخذ مشروع الرسم هذا في مجموعة متنوعة من الاتجاهات. إذا كنت تتذكر من المنشور الثاني ، فإننا نقوم بعمل عدة نسخ من بيانات الجسد في الذاكرة:
إذا كان الأداء أكثر أهمية من تعقيد التصميم ، فمن الممكن تمرير مخزن ذاكرة اللوحة القماشية إلى WebAssembly مباشرة. يؤدي ذلك إلى حفظ نسختين من الذاكرة ، مما يؤدي إلى زيادة الأداء:
- النموذج الأولي CanvasRenderingContext2D إلى AssemblyScript
- تحسين استدعاءات وظيفة CanvasRenderingContext2D باستخدام AssemblyScript
- OffscreenCanvas - تسريع عمليات Canvas الخاصة بك مع عامل الويب
تمامًا مثل WebAssembly و AssemblyScript ، تتعامل هذه المشاريع مع فواصل التوافق الأولية حيث تتصور المواصفات ميزات المتصفح الجديدة المذهلة هذه.
كل هذه المشاريع - وكل المصادر المفتوحة التي استخدمتها هنا - تبني أسسًا لمستقبل مشاع الإنترنت الواقع الافتراضي. نحن نراكم وشكرا لكم!
في المنشور الأخير ، سنلقي نظرة على بعض الاختلافات المهمة في التصميم بين إنشاء مشهد VR مقابل صفحة ويب مسطحة. ونظرًا لأن الواقع الافتراضي ليس تافهًا ، فسنبني عالمنا الشائك باستخدام إطار عمل WebVR. اخترت إطار A من Google ، والذي تم إنشاؤه أيضًا على قماش.
لقد كانت رحلة طويلة للوصول إلى بداية WebVR. لكن هذه السلسلة لم تكن حول العرض التوضيحي للعالم A-Frame hello. لقد كتبت هذه السلسلة من خلال سعادتي لأظهر لك أسس تكنولوجيا المتصفح التي ستعمل على تشغيل أول عوالم VR على الإنترنت في المستقبل.