WebVR Teil 4: Canvas-Datenvisualisierungen

Veröffentlicht: 2022-03-11

Hurra! Wir haben uns vorgenommen, einen Proof of Concept für WebVR zu erstellen. Unsere vorherigen Blogposts haben die Simulation abgeschlossen, also ist es jetzt Zeit für ein wenig kreatives Spiel.

Dies ist eine unglaublich aufregende Zeit, um Designer und Entwickler zu sein, denn VR ist ein Paradigmenwechsel.

Im Jahr 2007 verkaufte Apple das erste iPhone und leitete damit die Revolution des Smartphone-Konsums ein. Bis 2012 waren wir gut in „mobile-first“ und „responsive“ Webdesign. 2019 veröffentlichten Facebook und Oculus das erste mobile VR-Headset. Lass uns das machen!

Das „mobile-first“-Internet war keine Modeerscheinung, und ich sage voraus, dass das „VR-first“-Internet es auch nicht sein wird. In den vorherigen drei Artikeln und Demos habe ich die technologischen Möglichkeiten in Ihrem aktuellen Browser demonstriert.

Wenn Sie das in der Mitte der Serie aufgreifen, bauen wir eine himmlische Gravitationssimulation von wirbelnden Planeten.

  • Teil 1: Einführung und Architektur
  • Teil 2: Web Worker erhalten uns zusätzliche Browser-Threads
  • Teil 3: WebAssembly und AssemblyScript für unseren O(n²)-Leistungsengpasscode

Angesichts der Arbeit, die wir geleistet haben, ist es Zeit für kreatives Spielen. In den letzten beiden Beiträgen werden wir Canvas und WebVR und die Benutzererfahrung untersuchen.

  • Teil 4: Canvas-Datenvisualisierung (dieser Beitrag)
  • Teil 5: WebVR-Datenvisualisierung

Heute werden wir unsere Simulation zum Leben erwecken. Rückblickend bemerkte ich, wie viel aufgeregter und interessierter ich am Abschluss des Projekts war, als ich mit der Arbeit an den Visualizern begann. Die Visualisierungen machten es für andere interessant.

Der Zweck dieser Simulation war es, die Technologie zu erforschen, die WebVR – Virtual Reality im Browser – und das kommende VR-First- Web ermöglichen wird. Dieselben Technologien können Browser-Edge-Computing unterstützen.

Um unseren Proof of Concept abzurunden, erstellen wir heute zunächst eine Canvas-Visualisierung.

Canvas-Visualisierung
Canvas Visualizer Demo, Beispielcode

Im letzten Beitrag werden wir uns mit dem VR-Design befassen und eine WebVR-Version erstellen, um dieses Projekt „fertig“ zu machen.

WebVR-Datenvisualisierung

Das Einfachste, was möglicherweise funktionieren könnte: console.log()

Zurück zu RR (Real Reality). Lassen Sie uns einige Visualisierungen für unsere browserbasierte „n-body“-Simulation erstellen. Ich habe Leinwand in Webvideoanwendungen in früheren Projekten verwendet, aber nie als Leinwand eines Künstlers. Mal sehen, was wir tun können.

Wenn Sie sich an unsere Projektarchitektur erinnern, haben wir die Visualisierung an nBodyVisualizer.js .

Delegieren Sie die Visualisierung an nBodyVisualizer.js

nBodySimulator.js hat eine Simulationsschleife start() , die ihre Funktion step() () aufruft, und das Ende von step() ruft 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() }

Wenn wir den grünen Knopf drücken, fügt der Hauptthread dem System 10 zufällige Körper hinzu. Wir haben den Schaltflächencode im ersten Beitrag berührt, und Sie können ihn hier im Repo sehen. Diese Körper eignen sich hervorragend zum Testen eines Machbarkeitsnachweises, aber denken Sie daran, dass wir uns in einem gefährlichen Leistungsgebiet befinden - O(n²).

Menschen sind darauf ausgelegt, sich um die Menschen und Dinge zu kümmern, die sie sehen können, daher entfernt trimDebris() Objekte, die außer Sichtweite fliegen, damit sie den Rest nicht verlangsamen. Dies ist der Unterschied zwischen wahrgenommener und tatsächlicher Leistung.

Nachdem wir nun alles außer dem letzten this.visualize() behandelt haben, werfen wir einen Blick darauf!

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

Mit diesen beiden Funktionen können wir mehrere Visualizer hinzufügen. Es gibt zwei Visualizer in der Canvas-Version:

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

In der Canvas-Version ist der erste Visualizer die Tabelle mit weißen Zahlen, die als HTML angezeigt wird. Der zweite Visualizer ist ein schwarzes Canvas-Element darunter.

Canvas-Visualisierer
Auf der linken Seite ist der HTML-Visualizer die Tabelle mit weißen Zahlen. Darunter befindet sich der schwarze Leinwand-Visualizer

Um dies zu erstellen, habe ich mit einer einfachen Basisklasse 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)) } }

Diese Klasse gibt auf der Konsole aus (alle 33 ms!) und verfolgt auch ein htmlElement – ​​das wir in Unterklassen verwenden werden, damit sie in main.js einfach deklariert werden können.

Das ist das Einfachste, was funktionieren könnte.

Obwohl diese console definitiv einfach ist, „funktioniert“ sie nicht wirklich. Die Browserkonsole (und die Benutzer beim Surfen) sind nicht darauf ausgelegt, Protokollnachrichten mit einer Geschwindigkeit von 33 ms zu verarbeiten. Lassen Sie uns die nächste einfachste Sache finden, die möglicherweise funktionieren könnte.

Visualisierung von Simulationen mit Daten

Die nächste „schön gedruckte“ Iteration bestand darin, Text in ein HTML-Element zu drucken. Dies ist auch das Muster, das wir für die Canvas-Implementierung verwenden.

Beachten Sie, dass wir einen Verweis auf ein htmlElement , auf das der Visualizer malt. Wie alles andere im Web hat es ein Mobile-First-Design. Auf dem Desktop druckt dies die Datentabelle der Objekte und ihre Koordinaten links auf der Seite. Auf dem Handy würde dies zu einer visuellen Unordnung führen, also überspringen wir es.

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

Dieser „Datenstrom“-Visualizer hat zwei Funktionen:

  1. Es ist eine Möglichkeit, die Eingaben der Simulation in den Visualizer zu überprüfen. Dies ist ein „Debug“-Fenster.
  2. Es ist cool anzusehen, also behalten wir es für die Desktop-Demo!

Jetzt, da wir uns unserer Eingaben ziemlich sicher sind, lassen Sie uns über Grafiken und Leinwand sprechen.

Visualisierung von Simulationen mit 2D Canvas

Eine „Game Engine“ ist eine „Simulation Engine“ mit Explosionen. Beide sind unglaublich komplizierte Tools, weil sie sich auf Asset-Pipelines, das Laden von Streaming-Levels und alle möglichen unglaublich langweiligen Dinge konzentrieren, die niemals bemerkt werden sollten.

Auch das Web hat mit „Mobile-First“-Design seine eigenen „Dinge, die niemals auffallen sollten“ geschaffen. Wenn der Browser die Größe ändert, ändert das CSS unseres Canvas die Größe des Canvas-Elements im DOM, sodass sich unser Visualizer anpassen muss oder die Verachtung der Benutzer erleidet.

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

Diese Anforderung steuert resize() in der nBodyVisualizer -Basisklasse und der Canvas-Implementierung.

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

Dies führt dazu, dass unser Visualizer drei wesentliche Eigenschaften hat:

  • this.vis - kann zum Zeichnen von Primitives verwendet werden
  • this.sizeX
  • this.sizeY - die Abmessungen des Zeichenbereichs

Designhinweise für Canvas-2D-Visualisierung

Unsere Größenänderung funktioniert gegen die standardmäßige Canvas-Implementierung. Wenn wir ein Produkt- oder Datendiagramm visualisieren würden, möchten wir:

  1. Zeichnen Sie auf die Leinwand (in einer bevorzugten Größe und einem bevorzugten Seitenverhältnis)
  2. Lassen Sie dann den Browser die Größe dieser Zeichnung während des Seitenlayouts in das DOM-Element ändern

In diesem häufigeren Anwendungsfall steht das Produkt oder die Grafik im Mittelpunkt des Erlebnisses.

Unsere Visualisierung ist stattdessen eine theatralische Visualisierung der Weite des Weltraums , dramatisiert, indem Dutzende winziger Welten zum Spaß in die Leere geschleudert werden.

Unsere Himmelskörper demonstrieren diesen Raum durch Bescheidenheit - indem sie sich zwischen 0 und 20 Pixel breit halten. Diese Größenänderung skaliert den Abstand zwischen den Punkten, um ein Gefühl von „wissenschaftlicher“ Räumlichkeit zu erzeugen und die wahrgenommene Geschwindigkeit zu verbessern.

Um ein Gefühl der Skalierung zwischen Objekten mit sehr unterschiedlichen Massen zu erzeugen, initialisieren wir Körper mit einer zur Masse proportionalen 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) } }

In Handarbeit gefertigte maßgeschneiderte Solarsysteme

Wenn wir jetzt unser Sonnensystem in main.js , haben wir alle Werkzeuge, die wir für unsere Visualisierung benötigen:

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

Möglicherweise bemerken Sie die beiden „Asteroiden“ unten. Diese Zero-Masse-Objekte sind ein Hack, der verwendet wird, um das kleinste Ansichtsfenster der Simulation an einen 30x30-Bereich zu „heften“, der auf 0,0 zentriert ist.

Wir sind jetzt bereit für unsere Malfunktion. Die Körperwolke kann vom Ursprung (0,0,0) weg „wackeln“, also müssen wir zusätzlich zur Skalierung auch verschieben.

Wir sind „fertig“, wenn sich die Simulation natürlich anfühlt. Es gibt keinen „richtigen“ Weg, dies zu tun. Um die anfänglichen Planetenpositionen zu arrangieren, habe ich einfach mit den Zahlen herumgespielt, bis sie lange genug zusammengehalten haben, um interessant zu sein.

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

Der eigentliche Zeichencode für die Zeichenfläche besteht aus nur fünf Zeilen, die jeweils mit this.vis beginnen. Der Rest des Codes ist der Griff der Szene.

Kunst ist nie fertig, sie muss aufgegeben werden

Wenn Kunden scheinbar Geld ausgeben, das ihnen kein Geld bringt, ist jetzt ein guter Zeitpunkt, dies zur Sprache zu bringen. In Kunst zu investieren ist eine unternehmerische Entscheidung.

Der Kunde für dieses Projekt (ich) entschied sich, von der Canvas-Implementierung zu WebVR überzugehen. Ich wollte eine auffällige, mit Hype gefüllte WebVR-Demo. Also lasst uns das einpacken und etwas davon bekommen!

Mit dem, was wir gelernt haben, könnten wir dieses Leinwandprojekt in verschiedene Richtungen lenken. Wenn Sie sich an den zweiten Post erinnern, machen wir mehrere Kopien der Körperdaten im Speicher:

Kopien der Körperdaten im Speicher

Wenn die Leistung wichtiger ist als die Designkomplexität, ist es möglich, den Arbeitsspeicherpuffer der Leinwand direkt an die WebAssembly zu übergeben. Dies spart ein paar Speicherkopien, was sich positiv auf die Leistung auswirkt:

  • CanvasRenderingContext2D-Prototyp für AssemblyScript
  • Optimieren von CanvasRenderingContext2D-Funktionsaufrufen mit AssemblyScript
  • OffscreenCanvas – Beschleunigen Sie Ihre Canvas-Operationen mit einem Web Worker

Genau wie WebAssembly und AssemblyScript handhaben diese Projekte Upstream-Kompatibilitätsunterbrechungen, da die Spezifikationen diese erstaunlichen neuen Browserfunktionen vorsehen.

Alle diese Projekte – und alle Open-Source-Projekte, die ich hier verwendet habe – bilden die Grundlage für die Zukunft der VR-First-Internet-Commons. Wir sehen uns und danken Ihnen!

Im letzten Beitrag werden wir uns einige wichtige Designunterschiede zwischen der Erstellung einer VR-Szene und einer flachen Webseite ansehen. Und weil VR nicht trivial ist, bauen wir unsere spinnige Welt mit einem WebVR-Framework auf. Ich habe mich für Googles A-Frame entschieden, das ebenfalls auf Leinwand basiert.

Es war ein langer Weg, um zu den Anfängen von WebVR zu gelangen. Aber in dieser Serie ging es nicht um die A-Frame-Hello-World-Demo. Ich habe diese Serie in meiner Aufregung geschrieben, Ihnen die Grundlagen der Browsertechnologie zu zeigen, die die zukünftigen VR-Welten des Internets antreiben werden.