WebVR Część 4: Wizualizacje danych na płótnie
Opublikowany: 2022-03-11Hurra! Postanowiliśmy stworzyć Proof of Concept dla WebVR. Nasze poprzednie wpisy na blogu zakończyły symulację, więc teraz nadszedł czas na odrobinę kreatywnej zabawy.
To niesamowicie ekscytujący czas dla projektanta i programisty, ponieważ VR to zmiana paradygmatu.
W 2007 roku Apple sprzedał pierwszego iPhone'a, rozpoczynając rewolucję w konsumpcji smartfonów. Do 2012 roku byliśmy już na etapie projektowania stron internetowych „z myślą o urządzeniach mobilnych” i „responsywnych”. W 2019 roku Facebook i Oculus wypuściły pierwszy mobilny zestaw słuchawkowy VR. Zróbmy to!
Internet „mobile-first” nie był modą i przewiduję, że Internet „VR-first” też nie będzie. W poprzednich trzech artykułach i demach zademonstrowałem możliwości technologiczne w Twojej obecnej przeglądarce.
Jeśli wybierasz to w środku serii, budujemy symulację grawitacji niebieskiej wirujących planet.
- Część 1: Intro i architektura
- Część 2: Web Workers dostarczają nam dodatkowe wątki przeglądarki
- Część 3: WebAssembly i AssemblyScript dla naszego kodu wąskiego gardła wydajności O(n²)
Stojąc na pracy, którą wykonaliśmy, czas na kreatywną zabawę. W ostatnich dwóch postach omówimy kanwę i WebVR oraz wrażenia użytkownika.
- Część 4: Wizualizacja danych na płótnie (ten post)
- Część 5: Wizualizacja danych WebVR
Dzisiaj ożywimy naszą symulację. Patrząc wstecz zauważyłem, o ile bardziej podekscytowany i zainteresowany byłem ukończeniem projektu, gdy zacząłem pracować nad wizualizatorami. Wizualizacje sprawiły, że było to interesujące dla innych osób.
Celem tej symulacji było zbadanie technologii, która umożliwi WebVR – wirtualną rzeczywistość w przeglądarce – oraz nadchodzącą sieć VR-first . Te same technologie mogą zasilać komputery w przeglądarce.
Uzupełniając nasz Proof of Concept, dzisiaj najpierw stworzymy wizualizację płótna.
W ostatnim poście przyjrzymy się projektowaniu VR i stworzymy wersję WebVR, aby ten projekt został „ukończony”.
Najprostsza rzecz, która może działać: console.log()
Powrót do RR (Real Reality). Stwórzmy wizualizacje dla naszej opartej na przeglądarce symulacji „n-body”. Używałem płótna w internetowych aplikacjach wideo w poprzednich projektach, ale nigdy jako płótna artysty. Zobaczmy, co możemy zrobić.
Jeśli pamiętasz naszą architekturę projektu, wizualizację oddelegowaliśmy do nBodyVisualizer.js
.
nBodySimulator.js
ma pętlę symulacji start()
, która wywołuje funkcję step()
, a dół step()
wywołuje 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() }
Po naciśnięciu zielonego przycisku główny wątek dodaje do systemu 10 losowych ciał. Dotknęliśmy kodu przycisku w pierwszym poście i możesz go zobaczyć w repozytorium tutaj. Te ciała świetnie nadają się do testowania weryfikacji koncepcji, ale pamiętaj, że znajdujemy się na niebezpiecznym terytorium - O(n²).
Ludzie są stworzeni do dbania o ludzi i rzeczy, które widzą, więc trimDebris()
usuwa obiekty, które wylatują poza zasięg wzroku, aby nie spowalniały reszty. To jest różnica między postrzeganą a rzeczywistą wydajnością.
Teraz, gdy omówiliśmy już wszystko oprócz końcowego this.visualize()
, przyjrzyjmy się!
// 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) }
Te dwie funkcje pozwalają nam dodać wiele wizualizatorów. W wersji na płótnie dostępne są dwa wizualizery:
// 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")) ) …
W wersji na płótnie pierwszym wizualizatorem jest tabela białych liczb wyświetlanych jako HTML. Drugi wizualizator to czarny element płótna pod spodem.
Aby to stworzyć, zacząłem od prostej klasy bazowej w 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)) } }
Ta klasa wyświetla się w konsoli (co 33 ms!), a także śledzi element htmlElement — którego użyjemy w podklasach, aby ułatwić ich zadeklarowanie w main.js
.
To najprostsza rzecz, jaka mogłaby zadziałać.
Jednak chociaż ta wizualizacja console
jest zdecydowanie prosta, w rzeczywistości nie „działa”. Konsola przeglądarki (i przeglądanie ludzi) nie jest zaprojektowana do przetwarzania komunikatów dziennika z prędkością 33 ms. Znajdźmy kolejną najprostszą rzecz , która mogłaby zadziałać.
Wizualizacja symulacji z danymi
Kolejna iteracja „ładnego wydruku” polegała na wydrukowaniu tekstu do elementu HTML. Jest to również wzorzec, którego używamy do implementacji kanwy.
Zauważ, że zapisujemy odwołanie do elementu htmlElement
, na którym będzie malował wizualizator. Jak wszystko inne w sieci, ma design zorientowany na urządzenia mobilne. Na komputerze wyświetla tabelę danych obiektów i ich współrzędne po lewej stronie. Na urządzeniach mobilnych spowodowałoby to wizualny bałagan, więc go pomijamy.
/** * 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 } }
Ten wizualizator „strumienia danych” ma dwie funkcje:
- Jest to sposób na „sprawdzenie poprawności” danych wejściowych symulacji do wizualizatora. To jest okno „debugowania”.
- Fajnie się na to patrzy, więc zostawmy to dla wersji demonstracyjnej na komputery stacjonarne!
Teraz, gdy jesteśmy dość pewni naszych danych wejściowych, porozmawiajmy o grafice i płótnie.

Wizualizacja symulacji za pomocą płótna 2D
„Silnik gry” to „silnik symulacji” z eksplozjami. Oba są niesamowicie skomplikowanymi narzędziami, ponieważ skupiają się na potokach zasobów, ładowaniu poziomów strumieniowania i wszelkiego rodzaju niewiarygodnie nudnych rzeczach, których nigdy nie należy zauważać.
Sieć stworzyła również własne „rzeczy, których nigdy nie należy zauważać” dzięki projektowi „mobile-first”. Jeśli przeglądarka zmieni rozmiar, CSS naszego kanwy zmieni rozmiar elementu canvas w DOM, więc nasz wizualizator musi się dostosować lub cierpieć z pogardy użytkowników.
#visCanvas { margin: 0; padding: 0; background-color: #1F1F1F; overflow: hidden; width: 100vw; height: 100vh; }
To wymaganie steruje resize()
w klasie bazowej nBodyVisualizer
i implementacji kanwy.
/** * 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') }
Dzięki temu nasz wizualizator ma trzy podstawowe właściwości:
-
this.vis
- może służyć do rysowania prymitywów -
this.sizeX
-
this.sizeY
- wymiary obszaru rysowania
Płótno Wizualizacja 2D Uwagi projektowe
Nasza zmiana rozmiaru działa wbrew domyślnej implementacji kanwy. Gdybyśmy wizualizowali wykres produktu lub danych, chcielibyśmy:
- Rysuj na płótnie (w preferowanym rozmiarze i proporcjach)
- Następnie poproś przeglądarkę o zmianę rozmiaru tego rysunku do elementu DOM podczas układu strony
W tym bardziej powszechnym przypadku użycia produkt lub wykres jest głównym elementem doświadczenia.
Nasza wizualizacja jest zamiast tego teatralną wizualizacją bezmiaru przestrzeni , udramatyzowaną przez rzucanie dziesiątek malutkich światów w pustkę dla zabawy.
Nasze ciała niebieskie demonstrują tę przestrzeń poprzez skromność - zachowując szerokość od 0 do 20 pikseli. Ta zmiana rozmiaru skaluje przestrzeń między kropkami, aby stworzyć wrażenie „naukowej” przestrzeni i zwiększyć postrzeganą prędkość.
Aby stworzyć wrażenie skali między obiektami o bardzo różnych masach, inicjujemy ciała z drawSize
proporcjonalnym do masy:
// 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) } }
Rękodzieło na zamówienie systemów słonecznych
Teraz, gdy stworzymy nasz układ słoneczny w main.js
, będziemy mieli wszystkie narzędzia, których potrzebujemy do naszej wizualizacji:
// 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()
Możesz zauważyć dwie „asteroidy” na dole. Te obiekty o zerowej masie to hack używany do „przypięcia” najmniejszego rzutni symulacji do obszaru 30x30 wyśrodkowanego na 0,0.
Jesteśmy teraz gotowi do naszej funkcji malowania. Chmura ciał może „chwiać się” od początku (0,0,0), więc oprócz skali musimy także przesuwać się.
„Skończyliśmy”, gdy symulacja ma naturalny charakter. Nie ma na to „właściwego” sposobu. Aby uporządkować początkowe pozycje planet, po prostu bawiłem się liczbami, dopóki nie utrzymały się wystarczająco długo, aby były interesujące.
// 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 } }
Rzeczywisty kod rysowania na płótnie to tylko pięć wierszy - każda zaczyna się od this.vis
. Reszta kodu to chwyt sceny.
Sztuka nigdy nie jest skończona, trzeba ją porzucić
Kiedy wydaje się, że klienci wydają pieniądze, które nie przyniosą im pieniędzy, teraz jest dobry moment, aby o tym wspomnieć. Inwestowanie w sztukę to decyzja biznesowa.
Klient tego projektu (ja) zdecydował się przejść od implementacji canvas do WebVR. Chciałem mieć efektowną, wypełnioną szumem demo WebVR. Więc zakończmy to i zdobądźmy trochę tego!
Dzięki temu, czego się nauczyliśmy, mogliśmy poprowadzić ten projekt na płótnie w różnych kierunkach. Jeśli pamiętasz z drugiego postu, robimy kilka kopii danych ciała w pamięci:
Jeśli wydajność jest ważniejsza niż złożoność projektu, można bezpośrednio przekazać bufor pamięci kanwy do WebAssembly. Oszczędza to kilka kopii pamięci, co zwiększa wydajność:
- CanvasRenderingContext2D prototyp do AssemblyScript
- Optymalizacja wywołań funkcji CanvasRenderingContext2D za pomocą AssemblyScript
- OffscreenCanvas — Przyspiesz swoje operacje na płótnie z pracownikiem sieciowym
Podobnie jak WebAssembly i AssemblyScript, projekty te radzą sobie z przerwami w kompatybilności, ponieważ specyfikacje przewidują te niesamowite nowe funkcje przeglądarki.
Wszystkie te projekty – i całe oprogramowanie open-source, którego tu użyłem – budują fundamenty pod przyszłość Internetu w VR-first. Widzimy się i dziękujemy!
W ostatnim poście przyjrzymy się kilku ważnym różnicom projektowym między tworzeniem sceny VR a płaskiej strony internetowej. A ponieważ VR nie jest trywialne, zbudujemy nasz spinny świat za pomocą frameworka WebVR. Wybrałem ramkę A firmy Google, która również jest zbudowana na płótnie.
Droga do początków WebVR była długa. Ale ta seria nie dotyczyła demo hello world A-Frame. Napisałem tę serię w moim podekscytowaniu, aby pokazać podstawy technologii przeglądarek, które będą napędzać nadchodzące światy VR w Internecie.