WebVR Parte 4: Visualizzazioni dei dati su tela

Pubblicato: 2022-03-11

Evviva! Abbiamo deciso di creare una Proof of Concept per WebVR. I nostri precedenti post sul blog hanno completato la simulazione, quindi ora è il momento di fare un po' di gioco creativo.

Questo è un momento incredibilmente eccitante per essere un designer e uno sviluppatore perché la realtà virtuale è un cambio di paradigma.

Nel 2007 Apple vende il primo iPhone, dando il via alla rivoluzione del consumo degli smartphone. Entro il 2012, eravamo nel web design "mobile-first" e "reattivo". Nel 2019, Facebook e Oculus hanno rilasciato il primo visore VR mobile. Facciamolo!

L'Internet "mobile-first" non era una moda passeggera e prevedo che non lo sarà nemmeno l'Internet "VR-first". Nei tre articoli e demo precedenti, ho dimostrato le possibilità tecnologiche nel tuo browser attuale .

Se lo raccogli nel mezzo della serie, stiamo costruendo una simulazione di gravità celeste di pianeti spinosi.

  • Parte 1: Introduzione e architettura
  • Parte 2: i Web Worker ci forniscono thread del browser aggiuntivi
  • Parte 3: WebAssembly e AssemblyScript per il nostro codice collo di bottiglia delle prestazioni O(n²).

Basandosi sul lavoro che abbiamo fatto, è tempo di giocare in modo creativo. Negli ultimi due post, esploreremo canvas e WebVR e l'esperienza dell'utente.

  • Parte 4: Visualizzazione dei dati su tela (questo post)
  • Parte 5: Visualizzazione dei dati WebVR

Oggi daremo vita alla nostra simulazione. Guardando indietro, ho notato quanto fossi più eccitato e interessato a completare il progetto una volta che ho iniziato a lavorare sui visualizzatori. Le visualizzazioni lo rendevano interessante per altre persone.

Lo scopo di questa simulazione era esplorare la tecnologia che abiliterà WebVR (la realtà virtuale nel browser) e l'imminente VR-first web. Queste stesse tecnologie possono potenziare l'edge computing del browser.

A completamento della nostra Proof of Concept, oggi creeremo prima una visualizzazione su tela.

Visualizzazione della tela
Dimostrazione del visualizzatore di tela, codice di esempio

Nel post finale, esamineremo il design VR e realizzeremo una versione WebVR per portare a termine questo progetto.

Visualizzazione dei dati WebVR

La cosa più semplice che potrebbe funzionare: console.log()

Torna a RR (Real Reality). Creiamo alcune visualizzazioni per la nostra simulazione "n-body" basata su browser. Ho usato la tela in applicazioni video web in progetti passati, ma mai come tela di un artista. Vediamo cosa possiamo fare.

Se ricordi la nostra architettura di progetto, abbiamo delegato la visualizzazione a nBodyVisualizer.js .

Delega la visualizzazione a nBodyVisualizer.js

nBodySimulator.js ha un ciclo di simulazione start() che chiama la sua funzione step() e la parte inferiore di step() chiama 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() }

Quando premiamo il pulsante verde, il thread principale aggiunge 10 corpi casuali al sistema. Abbiamo toccato il codice del pulsante nel primo post e puoi vederlo nel repository qui. Quei corpi sono ottimi per testare un proof of concept, ma ricorda che siamo in un territorio pericoloso per le prestazioni - O(n²).

Gli esseri umani sono progettati per prendersi cura delle persone e delle cose che possono vedere, quindi trimDebris() rimuove gli oggetti che volano fuori vista in modo che non rallentino il resto. Questa è la differenza tra prestazioni percepite e effettive.

Ora che abbiamo coperto tutto tranne il finale this.visualize() , diamo un'occhiata!

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

Queste due funzioni ci consentono di aggiungere più visualizzatori. Ci sono due visualizzatori nella versione 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")) ) …

Nella versione canvas, il primo visualizzatore è la tabella dei numeri bianchi visualizzata come HTML. Il secondo visualizzatore è un elemento di tela nero sottostante.

Visualizzatori di tela
A sinistra, il visualizzatore HTML è la tabella dei numeri bianchi. Il visualizzatore della tela nera è al di sotto

Per creare questo, ho iniziato con una semplice classe base in 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)) } }

Questa classe stampa sulla console (ogni 33 ms!) e tiene traccia anche di un htmlElement, che useremo nelle sottoclassi per renderle facili da dichiarare in main.js .

Questa è la cosa più semplice che potrebbe funzionare.

Tuttavia, sebbene questa visualizzazione della console sia decisamente semplice, in realtà non "funziona". La console del browser (e la navigazione umana) non sono progettate per elaborare i messaggi di registro a una velocità di 33 ms. Troviamo la prossima cosa più semplice che potrebbe funzionare.

Visualizzazione delle simulazioni con i dati

La successiva iterazione di "stampa graziosa" consisteva nel stampare il testo su un elemento HTML. Questo è anche il modello che utilizziamo per l'implementazione della tela.

Nota che stiamo salvando un riferimento a un htmlElement il visualizzatore disegnerà. Come ogni altra cosa sul Web, ha un design mobile first. Sul desktop, stampa la tabella dati degli oggetti e le loro coordinate a sinistra della pagina. Sui dispositivi mobili comporterebbe un disordine visivo, quindi lo saltiamo.

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

Questo visualizzatore di "flusso di dati" ha due funzioni:

  1. È un modo per "verificare l'integrità" degli input della simulazione nel visualizzatore. Questa è una finestra di "debug".
  2. È bello da vedere, quindi teniamolo per la demo desktop!

Ora che siamo abbastanza fiduciosi nei nostri input, parliamo di grafica e tela.

Visualizzazione di simulazioni con tela 2D

Un "Motore di gioco" è un "Motore di simulazione" con esplosioni. Entrambi sono strumenti incredibilmente complicati perché si concentrano su pipeline di risorse, caricamento del livello di streaming e tutti i tipi di cose incredibilmente noiose che non dovrebbero mai essere notate.

Il web ha anche creato le sue "cose ​​​​che non dovrebbero mai essere notate" con un design "mobile-first". Se il browser si ridimensiona, il CSS della nostra tela ridimensionerà l'elemento canvas nel DOM, quindi il nostro visualizzatore deve adattarsi o subire il disprezzo degli utenti.

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

Questo requisito guida resize() nella classe base nBodyVisualizer e nell'implementazione 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') }

Ciò fa sì che il nostro visualizzatore abbia tre proprietà essenziali:

  • this.vis - può essere usato per disegnare primitive
  • this.sizeX
  • this.sizeY - le dimensioni dell'area di disegno

Note di progettazione per la visualizzazione 2D della tela

Il nostro ridimensionamento funziona contro l'implementazione predefinita della tela. Se stessimo visualizzando un prodotto o un grafico di dati, vorremmo:

  1. Disegna sulla tela (con dimensioni e proporzioni preferite)
  2. Quindi chiedi al browser di ridimensionare il disegno nell'elemento DOM durante il layout della pagina

In questo caso d'uso più comune, il prodotto o il grafico è al centro dell'esperienza.

La nostra visualizzazione è invece una visualizzazione teatrale della vastità dello spazio , drammatizzata lanciando decine di minuscoli mondi nel vuoto per divertimento.

I nostri corpi celesti dimostrano quello spazio attraverso la modestia, mantenendosi tra 0 e 20 pixel di larghezza. Questo ridimensionamento ridimensiona lo spazio tra i punti per creare un senso di spaziosità "scientifica" e migliora la velocità percepita.

Per creare un senso di scala tra oggetti con masse molto diverse, inizializziamo i corpi con un drawSize proporzionale alla massa:

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

Realizzazione di sistemi solari su misura

Ora, quando creiamo il nostro sistema solare in main.js , avremo tutti gli strumenti necessari per la nostra visualizzazione:

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

Potresti notare i due "asteroidi" in basso. Questi oggetti di massa zero sono un trucco utilizzato per "fissare" la vista più piccola della simulazione su un'area 30x30 centrata su 0,0.

Ora siamo pronti per la nostra funzione di pittura. La nuvola di corpi può "oscillare" lontano dall'origine (0,0,0), quindi dobbiamo anche spostarci oltre alla scala.

Abbiamo "finito" quando la simulazione ha un aspetto naturale. Non esiste un modo "giusto" per farlo. Per organizzare le posizioni iniziali del pianeta, ho solo giocherellato con i numeri fino a quando non si è tenuto insieme abbastanza a lungo da essere interessante.

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

Il codice del disegno della tela effettivo è di sole cinque righe, ciascuna che inizia con this.vis . Il resto del codice è la presa della scena.

L'arte non è mai finita, deve essere abbandonata

Quando i clienti sembrano spendere soldi che non li faranno guadagnare, adesso è un buon momento per tirarlo fuori. Investire nell'arte è una decisione aziendale.

Il cliente per questo progetto (io) ha deciso di passare dall'implementazione della tela a WebVR. Volevo una demo WebVR appariscente e piena di clamore. Quindi concludiamo questo e prendiamo un po' di quello!

Con quello che abbiamo imparato, potremmo portare questo progetto su tela in una varietà di direzioni. Se ricordi dal secondo post, stiamo facendo diverse copie dei dati del corpo in memoria:

Copie dei dati corporei in memoria

Se le prestazioni sono più importanti della complessità del design, è possibile passare il buffer di memoria dell'area di disegno direttamente al WebAssembly. Ciò consente di risparmiare un paio di copie di memoria, il che aumenta le prestazioni:

  • CanvasRenderingContext2D prototipo in AssemblyScript
  • Ottimizzazione delle chiamate di funzione CanvasRenderingContext2D mediante AssemblyScript
  • OffscreenCanvas: velocizza le operazioni su tela con un web worker

Proprio come WebAssembly e AssemblyScript, questi progetti stanno gestendo interruzioni di compatibilità a monte poiché le specifiche prevedono queste straordinarie nuove funzionalità del browser.

Tutti questi progetti - e tutto l'open source che ho usato qui - stanno costruendo le basi per il futuro dei beni comuni di Internet VR-first. Ci vediamo e grazie!

Nel post finale, esamineremo alcune importanti differenze di design tra la creazione di una scena VR e una pagina Web piatta. E poiché la realtà virtuale non è banale, costruiremo il nostro mondo spinny con un framework WebVR. Ho scelto A-Frame di Google, anch'esso costruito su tela.

È stato un lungo viaggio per arrivare all'inizio di WebVR. Ma questa serie non riguardava la demo hello world di A-Frame. Ho scritto questa serie nella mia eccitazione per mostrarti le basi della tecnologia dei browser che alimenteranno i primi mondi VR di Internet a venire.