WebVR Partea 4: Vizualizări de date Canvas

Publicat: 2022-03-11

Ura! Ne-am propus să creăm o Proof of Concept pentru WebVR. Postările noastre anterioare de pe blog au completat simularea, așa că acum este timpul pentru o mică joacă creativă.

Acesta este un moment incredibil de interesant pentru a fi designer și dezvoltator, deoarece VR este o schimbare de paradigmă.

În 2007, Apple a vândut primul iPhone, dând startul revoluției consumului de smartphone-uri. Până în 2012, eram bine în design web „în primul rând pe mobil” și „responsive”. În 2019, Facebook și Oculus au lansat primul set de căști VR pentru mobil. Să o facem!

Internetul „în primul rând pe mobil“ nu a fost un moft și prevăd că nici internetul „în primul rând VR“ nu va fi. În cele trei articole și demonstrații anterioare, am demonstrat posibilitatea tehnologică în browserul dvs. actual .

Dacă înțelegi asta la mijlocul seriei, construim o simulare gravitațională cerească a planetelor spinoase.

  • Partea 1: Introducere și arhitectură
  • Partea 2: Lucrătorii web ne oferă fire de navigare suplimentare
  • Partea 3: WebAssembly și AssemblyScript pentru codul nostru de blocaj de performanță O(n²).

Având în vedere munca pe care am făcut-o, este timpul pentru o joacă creativă. În ultimele două postări, vom explora pânza și WebVR și experiența utilizatorului.

  • Partea 4: Vizualizarea datelor Canvas (acest post)
  • Partea 5: Vizualizarea datelor WebVR

Astăzi, vom aduce simularea noastră la viață. Privind în urmă, am observat cât de mult mai entuziasmat și mai interesat eram de finalizarea proiectului odată ce am început să lucrez la vizualizatoare. Vizualizările l-au făcut interesant pentru alte persoane.

Scopul acestei simulări a fost să exploreze tehnologia care va permite WebVR - Realitatea Virtuală în browser - și viitorul web VR-first . Aceleași tehnologii pot alimenta computerul la marginea browserului.

Pentru a completa Proof of Concept, astăzi vom crea mai întâi o vizualizare canvas.

Vizualizare canvas
Canvas Visualizer Demo, exemplu de cod

În postarea finală, ne vom uita la designul VR și vom face o versiune WebVR pentru ca acest proiect să se „termine”.

Vizualizarea datelor WebVR

Cel mai simplu lucru care ar putea funcționa: console.log()

Înapoi la RR (Realitate Reală). Să creăm câteva vizualizări pentru simularea noastră „n-body” bazată pe browser. Am folosit pânza în aplicații video web în proiectele anterioare, dar niciodată ca pânză de artist. Să vedem ce putem face.

Dacă vă amintiți arhitectura proiectului nostru, am delegat vizualizarea către nBodyVisualizer.js .

Delegați vizualizarea la nBodyVisualizer.js

nBodySimulator.js are o buclă de simulare start() care își apelează funcția step() , iar partea de jos a step() apelează 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() }

Când apăsăm butonul verde, firul principal adaugă 10 corpuri aleatorii sistemului. Am atins codul butonului în prima postare și îl puteți vedea în repo aici. Aceste corpuri sunt grozave pentru a testa o dovadă a conceptului, dar amintiți-vă că ne aflăm pe un teritoriu de performanță periculos - O(n²).

Oamenii sunt proiectați să le pese de oameni și de lucrurile pe care le pot vedea, așa că trimDebris() elimină obiectele care zboară din vedere, astfel încât să nu încetinească restul. Aceasta este diferența dintre performanța percepută și cea reală.

Acum că am acoperit totul, cu excepția this.visualize() , să aruncăm o privire!

 // 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) }

Aceste două funcții ne permit să adăugăm mai multe vizualizatoare. Există două vizualizatoare în versiunea canvas:

 // 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")) ) …

În versiunea canvas, primul vizualizator este tabelul cu numere albe afișate ca HTML. Al doilea vizualizator este un element de pânză neagră dedesubt.

Vizualizatoare canvas
În stânga, vizualizatorul HTML este tabelul numerelor albe. Vizualizatorul de pânză neagră este dedesubt

Pentru a crea asta, am început cu o clasă de bază simplă în 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)) } }

Această clasă se imprimă pe consolă (la fiecare 33 ms!) și urmărește, de asemenea, un htmlElement - pe care îl vom folosi în subclase pentru a le face ușor de declarat în main.js .

Acesta este cel mai simplu lucru care ar putea funcționa.

Cu toate acestea, deși această vizualizare a console este cu siguranță simplă, de fapt nu „funcționează”. Consola browserului (și oamenii de navigare) nu sunt proiectate pentru a procesa mesajele de jurnal la o viteză de 33 ms. Să găsim următorul lucru cel mai simplu care ar putea funcționa.

Vizualizarea simulărilor cu date

Următoarea iterație „pretty print” a fost tipărirea textului într-un element HTML. Acesta este și modelul pe care îl folosim pentru implementarea canvasului.

Observați că salvăm o referință la un htmlElement pe care va picta vizualizatorul. La fel ca orice altceva de pe web, are un design mai întâi mobil. Pe desktop, aceasta tipărește tabelul de date al obiectelor și coordonatele acestora în partea stângă a paginii. Pe mobil ar avea ca rezultat dezordine vizuală, așa că o sărim peste.

 /** * 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 } }

Acest vizualizator „flux de date” are două funcții:

  1. Este o modalitate de a „verifica corect” intrările simulării în vizualizator. Aceasta este o fereastră de „depanare”.
  2. Este grozav de privit, așa că să-l păstrăm pentru demonstrația desktop!

Acum că suntem destul de încrezători în intrările noastre, să vorbim despre grafică și pânză.

Vizualizarea simulărilor cu pânză 2D

Un „motor de joc” este un „motor de simulare” cu explozii. Ambele sunt instrumente incredibil de complicate, deoarece se concentrează pe conductele de active, pe încărcarea nivelului de streaming și pe tot felul de lucruri incredibil de plictisitoare care nu ar trebui niciodată observate.

Web-ul și-a creat, de asemenea, propriile „lucruri care nu ar trebui niciodată observate” cu un design „în primul rând pe mobil”. Dacă browserul se redimensionează, CSS-ul pânzei noastre va redimensiona elementul pânzei din DOM, astfel încât vizualizatorul nostru trebuie să se adapteze sau să sufere disprețul utilizatorilor.

 #visCanvas { margin: 0; padding: 0; background-color: #1F1F1F; overflow: hidden; width: 100vw; height: 100vh; }

Această cerință determină resize() în clasa de bază nBodyVisualizer și implementarea 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') }

Astfel, vizualizatorul nostru are trei proprietăți esențiale:

  • this.vis - poate fi folosit pentru a desena primitive
  • this.sizeX
  • this.sizeY - dimensiunile zonei de desen

Note de proiectare pentru vizualizare 2D Canvas

Redimensionarea noastră funcționează împotriva implementării implicite a pânzei. Dacă am vizualiza un produs sau un grafic de date, am dori să:

  1. Desenați pe pânză (la o dimensiune și raport de aspect preferat)
  2. Apoi cereți browserului să redimensioneze acel desen în elementul DOM în timpul aspectului paginii

În acest caz de utilizare mai frecvent, produsul sau graficul este punctul central al experienței.

Vizualizarea noastră este, în schimb, o vizualizare teatrală a vastității spațiului , dramatizată prin aruncarea în gol a zeci de lumi minuscule pentru distracție.

Corpurile noastre cerești demonstrează acest spațiu prin modestie - păstrându-se între 0 și 20 de pixeli lățime. Această redimensionare scalează spațiul dintre puncte pentru a crea un sentiment de spațiu „științific” și îmbunătățește viteza percepută.

Pentru a crea un sentiment de scară între obiecte cu mase foarte diferite, inițializam corpurile cu un drawSize proporțional cu masa:

 // 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) } }

Realizarea manuală a sistemelor solare la comandă

Acum, când ne creăm sistemul solar în main.js , vom avea toate instrumentele de care avem nevoie pentru vizualizarea noastră:

 // 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()

Este posibil să observați cei doi „asteroizi” din partea de jos. Aceste obiecte cu masă zero sunt un hack folosit pentru a „fix” cea mai mică fereastră de vizualizare a simulării într-o zonă de 30x30 centrată pe 0,0.

Acum suntem pregătiți pentru funcția noastră de vopsire. Norul de corpuri se poate „clatina” departe de origine (0,0,0), așa că trebuie să ne schimbăm și pe lângă scară.

Am „terminat” atunci când simularea are o senzație naturală. Nu există o modalitate „corectă” de a face acest lucru. Pentru a aranja pozițiile inițiale ale planetelor, m-am jucat doar cu numerele până când s-au ținut împreună suficient de mult pentru a fi interesant.

 // 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 } }

Codul real de desen al pânzei este de doar cinci linii - fiecare începând cu this.vis . Restul codului este strânsoarea scenei.

Arta nu este niciodată terminată, trebuie abandonată

Când clienții par să cheltuiască bani care nu îi vor aduce bani, acum este un moment bun pentru a-i aduce în discuție. Investiția în artă este o decizie de afaceri.

Clientul acestui proiect (eu) a decis să treacă de la implementarea canvas la WebVR. Îmi doream un demo WebVR plin de hype. Așa că hai să încheiem asta și să obținem o parte din asta!

Cu ceea ce am învățat, am putea duce acest proiect de pânză într-o varietate de direcții. Dacă vă amintiți din a doua postare, facem mai multe copii ale datelor corpului în memorie:

Copii ale datelor corpului în memorie

Dacă performanța este mai importantă decât complexitatea designului, este posibil să transmiteți tamponul de memorie al pânzei direct către WebAssembly. Acest lucru salvează câteva copii de memorie, ceea ce crește performanța:

  • Prototipul CanvasRenderingContext2D la AssemblyScript
  • Optimizarea apelurilor de funcții CanvasRenderingContext2D utilizând AssemblyScript
  • OffscreenCanvas — Accelerați-vă operațiunile Canvas cu un lucrător web

La fel ca WebAssembly și AssemblyScript, aceste proiecte se ocupă de întreruperi de compatibilitate în amonte, deoarece specificațiile prevăd aceste noi caracteristici uimitoare ale browserului.

Toate aceste proiecte - și toate sursele deschise pe care le-am folosit aici - construiesc baze pentru viitorul comunității internetului VR-first. Ne vedem si multumim!

În postarea finală, vom analiza câteva diferențe importante de design între crearea unei scene VR și o pagină web plată. Și pentru că VR nu este banal, ne vom construi lumea spinnoasă cu un cadru WebVR. Am ales A-Frame de la Google, care este, de asemenea, construit pe pânză.

A fost o călătorie lungă până la începutul WebVR. Dar această serie nu a fost despre demonstrația A-Frame Hello World. Am scris această serie în entuziasmul meu de a vă arăta bazele tehnologiei browserului care vor alimenta primele lumi VR ale internetului care vor veni.