WebVR Partie 4 : Visualisations de données Canvas

Publié: 2022-03-11

Hourra! Nous avons entrepris de créer une preuve de concept pour WebVR. Nos articles de blog précédents ont terminé la simulation, alors maintenant il est temps pour un petit jeu créatif.

C'est une période incroyablement excitante pour être concepteur et développeur, car la réalité virtuelle est un changement de paradigme.

En 2007, Apple a vendu le premier iPhone, donnant le coup d'envoi de la révolution de la consommation des smartphones. En 2012, nous étions bien avancés dans la conception de sites Web "mobile-first" et "responsive". En 2019, Facebook et Oculus ont lancé le premier casque VR mobile. Faisons cela!

L'Internet "mobile-first" n'était pas une mode, et je prédis que l'Internet "VR-first" ne le sera pas non plus. Dans les trois articles et démos précédents, j'ai démontré la possibilité technologique dans votre navigateur actuel .

Si vous repérez cela au milieu de la série, nous construisons une simulation de gravité céleste de planètes spinny.

  • Partie 1 : Introduction et architecture
  • Partie 2 : Les Web Workers nous fournissent des threads de navigateur supplémentaires
  • Partie 3 : WebAssembly et AssemblyScript pour notre code de goulot d'étranglement de performance O(n²)

Sur la base du travail que nous avons accompli, il est temps de jouer de manière créative. Dans les deux derniers articles, nous explorerons canvas et WebVR et l'expérience utilisateur.

  • Partie 4 : Visualisation des données Canvas (cet article)
  • Partie 5 : Visualisation des données WebVR

Aujourd'hui, nous allons donner vie à notre simulation. Avec le recul, j'ai remarqué à quel point j'étais plus excité et intéressé à terminer le projet une fois que j'ai commencé à travailler sur les visualiseurs. Les visualisations l'ont rendu intéressant pour les autres.

Le but de cette simulation était d'explorer la technologie qui permettra WebVR - Réalité Virtuelle dans le navigateur - et le prochain Web VR-first . Ces mêmes technologies peuvent alimenter l'informatique de pointe du navigateur.

Aujourd'hui, pour compléter notre preuve de concept, nous allons d'abord créer une visualisation de canevas.

Visualisation du canevas
Démo du visualiseur Canvas, exemple de code

Dans le dernier article, nous examinerons la conception VR et créerons une version WebVR pour que ce projet soit "terminé".

Visualisation de données WebVR

La chose la plus simple qui pourrait éventuellement fonctionner : console.log()

Retour à RR (réalité réelle). Créons quelques visualisations pour notre simulation "n-corps" basée sur un navigateur. J'ai utilisé la toile dans des applications vidéo Web dans des projets antérieurs, mais jamais en tant que toile d'artiste. Voyons ce que nous pouvons faire.

Si vous vous souvenez de notre architecture de projet, nous avons délégué la visualisation à nBodyVisualizer.js .

Déléguer la visualisation à nBodyVisualizer.js

nBodySimulator.js a une boucle de simulation start() qui appelle sa fonction step() , et le bas de step() appelle 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() }

Lorsque nous appuyons sur le bouton vert, le thread principal ajoute 10 corps aléatoires au système. Nous avons touché le code du bouton dans le premier message, et vous pouvez le voir dans le repo ici. Ces corps sont parfaits pour tester une preuve de concept, mais rappelez-vous que nous sommes dans un territoire de performance dangereux - O(n²).

Les humains sont conçus pour se soucier des personnes et des choses qu'ils peuvent voir, donc trimDebris() supprime les objets qui volent hors de vue afin qu'ils ne ralentissent pas le reste. C'est la différence entre la performance perçue et la performance réelle.

Maintenant que nous avons tout couvert sauf le this.visualize() final, jetons un coup d'œil !

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

Ces deux fonctions nous permettent d'ajouter plusieurs visualiseurs. Il existe deux visualiseurs dans la version canevas :

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

Dans la version canvas, le premier visualiseur est le tableau de nombres blancs affiché en HTML. Le deuxième visualiseur est un élément de toile noire en dessous.

Visualiseurs de canevas
Sur la gauche, le visualiseur HTML est le tableau des nombres blancs. Le visualiseur de toile noire est en dessous

Pour créer cela, j'ai commencé avec une simple classe de base dans 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)) } }

Cette classe imprime sur la console (toutes les 33 ms !) et suit également un htmlElement - que nous utiliserons dans les sous-classes pour les rendre faciles à déclarer dans main.js .

C'est la chose la plus simple qui puisse fonctionner.

Cependant, bien que cette visualisation de la console soit vraiment simple, elle ne "fonctionne" pas réellement. La console du navigateur (et les humains qui naviguent) ne sont pas conçus pour traiter les messages du journal à une vitesse de 33 ms. Trouvons la prochaine chose la plus simple qui pourrait éventuellement fonctionner.

Visualiser des simulations avec des données

La prochaine itération "jolie impression" consistait à imprimer du texte sur un élément HTML. C'est également le modèle que nous utilisons pour l'implémentation du canevas.

Notez que nous enregistrons une référence à un htmlElement sur lequel le visualiseur va peindre. Comme tout le reste sur le Web, il a une conception axée sur le mobile. Sur le bureau, cela imprime la table de données des objets et leurs coordonnées sur la gauche de la page. Sur mobile, cela entraînerait un encombrement visuel, nous l'ignorons donc.

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

Ce visualiseur "flux de données" a deux fonctions :

  1. C'est un moyen de "vérifier l'intégrité" des entrées de la simulation dans le visualiseur. Il s'agit d'une fenêtre de "débogage".
  2. C'est cool à regarder, alors gardons-le pour la démo de bureau !

Maintenant que nous sommes assez confiants dans nos entrées, parlons des graphiques et de la toile.

Visualisation de simulations avec 2D Canvas

Un "Game Engine" est un "Simulation Engine" avec des explosions. Les deux sont des outils incroyablement compliqués car ils se concentrent sur les pipelines d'actifs, le chargement au niveau du streaming et toutes sortes de choses incroyablement ennuyeuses qui ne devraient jamais être remarquées.

Le Web a également créé ses propres "choses qui ne devraient jamais être remarquées" avec une conception "mobile-first". Si le navigateur se redimensionne, le CSS de notre canevas redimensionnera l'élément canvas dans le DOM, notre visualiseur doit donc s'adapter ou subir le mépris des utilisateurs.

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

Cette exigence entraîne resize() dans la classe de base nBodyVisualizer et l'implémentation de canevas.

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

Il en résulte que notre visualiseur possède trois propriétés essentielles :

  • this.vis - peut être utilisé pour dessiner des primitives
  • this.sizeX
  • this.sizeY - les dimensions de la zone de dessin

Notes de conception de la visualisation 2D Canvas

Notre redimensionnement fonctionne par rapport à l'implémentation de canevas par défaut. Si nous visualisions un produit ou un graphique de données, nous voudrions :

  1. Dessiner sur le canevas (à une taille et un rapport d'aspect préférés)
  2. Demandez ensuite au navigateur de redimensionner ce dessin dans l'élément DOM lors de la mise en page

Dans ce cas d'utilisation plus courant, le produit ou le graphique est au centre de l'expérience.

Notre visualisation est plutôt une visualisation théâtrale de l' immensité de l'espace , dramatisée en jetant des dizaines de mondes minuscules dans le vide pour le plaisir.

Nos corps célestes démontrent cet espace par modestie - en se maintenant entre 0 et 20 pixels de large. Ce redimensionnement redimensionne l' espace entre les points pour créer une sensation d'espace « scientifique » et améliore la vitesse perçue.

Pour créer une impression d'échelle entre des objets de masses très différentes, nous initialisons les corps avec un drawSize proportionnel à la masse :

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

Fabrication artisanale de systèmes solaires sur mesure

Maintenant, lorsque nous créons notre système solaire dans main.js , nous aurons tous les outils dont nous avons besoin pour notre visualisation :

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

Vous remarquerez peut-être les deux "astéroïdes" en bas. Ces objets de masse nulle sont un hack utilisé pour "épingler" la plus petite fenêtre de la simulation à une zone 30x30 centrée sur 0,0.

Nous sommes maintenant prêts pour notre fonction de peinture. Le nuage de corps peut "vaciller" loin de l'origine (0,0,0), nous devons donc également nous déplacer en plus de l'échelle.

Nous avons « terminé » lorsque la simulation a une sensation naturelle. Il n'y a pas de "bonne" façon de le faire. Pour organiser les positions initiales des planètes, j'ai juste joué avec les chiffres jusqu'à ce qu'ils tiennent ensemble assez longtemps pour être intéressants.

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

Le code de dessin réel du canevas n'est que de cinq lignes - chacune commençant par this.vis . Le reste du code est la prise en main de la scène.

L'art n'est jamais fini, il doit être abandonné

Lorsque les clients semblent dépenser de l'argent qui ne leur rapportera pas d'argent, c'est le bon moment pour en parler. Investir dans l'art est une décision commerciale.

Le client de ce projet (moi) a décidé de passer de l'implémentation de canevas à WebVR. Je voulais une démo WebVR flashy remplie de battage médiatique. Alors, résumons cela et récupérons-en un peu !

Avec ce que nous avons appris, nous pourrions prendre ce projet de toile dans une variété de directions. Si vous vous souvenez du deuxième post, nous faisons plusieurs copies des données corporelles en mémoire :

Copies des données corporelles en mémoire

Si les performances sont plus importantes que la complexité de la conception, il est possible de transmettre directement la mémoire tampon du canevas au WebAssembly. Cela permet d'économiser quelques copies de mémoire, ce qui améliore les performances :

  • Prototype CanvasRenderingContext2D vers AssemblyScript
  • Optimisation des appels de fonction CanvasRenderingContext2D à l'aide d'AssemblyScript
  • OffscreenCanvas — Accélérez vos opérations Canvas avec un Web Worker

Tout comme WebAssembly et AssemblyScript, ces projets gèrent les ruptures de compatibilité en amont car les spécifications envisagent ces nouvelles fonctionnalités de navigateur étonnantes.

Tous ces projets - et tous les open-source que j'ai utilisés ici - jettent les bases de l'avenir des premiers communs Internet VR. Nous vous voyons et merci!

Dans le dernier article, nous examinerons certaines différences de conception importantes entre la création d'une scène VR et une page Web plate. Et parce que la réalité virtuelle n'est pas triviale, nous allons construire notre monde tournoyant avec un framework WebVR. J'ai choisi A-Frame de Google, qui est également construit sur toile.

Le voyage a été long pour arriver au début du WebVR. Mais cette série ne concernait pas la démo A-Frame hello world. J'ai écrit cette série dans mon enthousiasme pour vous montrer les fondements de la technologie des navigateurs qui propulseront les premiers mondes VR d'Internet à venir.