WebVR Parte 4: Visualizaciones de datos de lienzo
Publicado: 2022-03-11¡Viva! Nos propusimos crear una prueba de concepto para WebVR. Nuestras publicaciones de blog anteriores completaron la simulación, por lo que ahora es el momento de un poco de juego creativo.
Este es un momento increíblemente emocionante para ser diseñador y desarrollador porque la realidad virtual es un cambio de paradigma.
En 2007, Apple vendió el primer iPhone, dando inicio a la revolución del consumo de teléfonos inteligentes. Para 2012, estábamos bien metidos en el diseño web "móvil primero" y "responsivo". En 2019, Facebook y Oculus lanzaron los primeros auriculares VR móviles. ¡Hagámoslo!
El Internet "móvil primero" no fue una moda pasajera, y predigo que el Internet "VR primero" tampoco lo será. En los tres artículos y demostraciones anteriores, demostré la posibilidad tecnológica en su navegador actual .
Si estás retomando esto a la mitad de la serie, estamos construyendo una simulación de gravedad celestial de planetas giratorios.
- Parte 1: Introducción y Arquitectura
- Parte 2: los Web Workers nos proporcionan subprocesos de navegación adicionales
- Parte 3: WebAssembly y AssemblyScript para nuestro código de cuello de botella de rendimiento O(n²)
Basándonos en el trabajo que hemos hecho, es hora de un poco de juego creativo. En las últimas dos publicaciones, exploraremos canvas, WebVR y la experiencia del usuario.
- Parte 4: Visualización de datos de Canvas (esta publicación)
- Parte 5: Visualización de datos WebVR
Hoy vamos a dar vida a nuestra simulación. Mirando hacia atrás, noté cuánto más emocionado e interesado estaba en completar el proyecto una vez que comencé a trabajar en los visualizadores. Las visualizaciones lo hicieron interesante para otras personas.
El propósito de esta simulación fue explorar la tecnología que habilitará WebVR (realidad virtual en el navegador) y la próxima web VR-first . Estas mismas tecnologías pueden potenciar la computación en el borde del navegador.
Completando nuestra prueba de concepto, hoy primero crearemos una visualización de lienzo.
En la publicación final, veremos el diseño de realidad virtual y haremos una versión de WebVR para "terminar" este proyecto.
Lo más simple que posiblemente podría funcionar: console.log()
Volver a RR (Realidad Real). Vamos a crear algunas visualizaciones para nuestra simulación de "n-cuerpos" basada en navegador. He usado lienzos en aplicaciones de video web en proyectos anteriores, pero nunca como lienzo de artista. Veamos qué podemos hacer.
Si recuerda la arquitectura de nuestro proyecto, delegamos la visualización a nBodyVisualizer.js
.
nBodySimulator.js
tiene un ciclo de simulación start()
que llama a su función step()
, y la parte inferior de step()
llama a 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() }
Cuando presionamos el botón verde, el hilo principal agrega 10 cuerpos aleatorios al sistema. Tocamos el código del botón en la primera publicación, y puedes verlo en el repositorio aquí. Esos cuerpos son geniales para probar una prueba de concepto, pero recuerda que estamos en un territorio de rendimiento peligroso: O(n²).
Los humanos están diseñados para preocuparse por las personas y las cosas que pueden ver, por lo que trimDebris()
elimina los objetos que vuelan fuera de la vista para que no ralenticen al resto. Esta es la diferencia entre el rendimiento percibido y el real.
Ahora que hemos cubierto todo menos el final this.visualize()
, ¡echemos un vistazo!
// 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) }
Estas dos funciones nos permiten agregar múltiples visualizadores. Hay dos visualizadores en la versión de lienzo:
// 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")) ) …
En la versión de lienzo, el primer visualizador es la tabla de números blancos que se muestra como HTML. El segundo visualizador es un elemento de lienzo negro debajo.
Para crear esto, comencé con una clase base simple en 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)) } }
Esta clase se imprime en la consola (¡cada 33 ms!) y también realiza un seguimiento de un htmlElement, que usaremos en las subclases para facilitar su declaración en main.js
Esta es la cosa más simple que podría funcionar.
Sin embargo, aunque esta visualización de la console
es definitivamente simple, en realidad no "funciona". La consola del navegador (y los humanos que navegan) no están diseñados para procesar mensajes de registro a una velocidad de 33 ms. Encontremos la siguiente cosa más simple que podría funcionar.
Visualización de simulaciones con datos
La siguiente iteración de "impresión bonita" fue imprimir texto en un elemento HTML. Este es también el patrón que usamos para la implementación del lienzo.
Observe que estamos guardando una referencia a un htmlElement
el que pintará el visualizador. Como todo lo demás en la web, tiene un diseño móvil primero. En el escritorio, esto imprime la tabla de datos de los objetos y sus coordenadas a la izquierda de la página. En el móvil daría lugar a un desorden visual, por lo que lo omitimos.
/** * 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 } }
Este visualizador de "flujo de datos" tiene dos funciones:
- Es una forma de "comprobar la cordura" de las entradas de la simulación en el visualizador. Esta es una ventana de "depuración".
- Es genial de ver, ¡así que dejémoslo para la demostración de escritorio!
Ahora que estamos bastante seguros de nuestras entradas, hablemos de gráficos y lienzos.

Visualización de simulaciones con lienzo 2D
Un "Motor de juego" es un "Motor de simulación" con explosiones. Ambas son herramientas increíblemente complicadas porque se centran en canalizaciones de activos, carga de nivel de transmisión y todo tipo de cosas increíblemente aburridas que nunca deberían notarse.
La web también ha creado sus propias "cosas que nunca deberían notarse" con un diseño "móvil primero". Si el navegador cambia de tamaño, el CSS de nuestro lienzo cambiará el tamaño del elemento de lienzo en el DOM, por lo que nuestro visualizador debe adaptarse o sufrir el desprecio de los usuarios.
#visCanvas { margin: 0; padding: 0; background-color: #1F1F1F; overflow: hidden; width: 100vw; height: 100vh; }
Este requisito impulsa resize resize()
en la clase base nBodyVisualizer
y la implementación del lienzo.
/** * 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') }
Esto da como resultado que nuestro visualizador tenga tres propiedades esenciales:
-
this.vis
- se puede utilizar para dibujar primitivas -
this.sizeX
-
this.sizeY
- las dimensiones del área de dibujo
Notas de diseño de visualización 2D de Canvas
Nuestro cambio de tamaño funciona en contra de la implementación del lienzo predeterminado. Si estuviéramos visualizando un producto o un gráfico de datos, querríamos:
- Dibuja en el lienzo (con el tamaño y la relación de aspecto que prefieras)
- Luego, haga que el navegador cambie el tamaño de ese dibujo en el elemento DOM durante el diseño de la página
En este caso de uso más común, el producto o gráfico es el foco de la experiencia.
Nuestra visualización es, en cambio, una visualización teatral de la inmensidad del espacio , dramatizada arrojando docenas de pequeños mundos al vacío por diversión.
Nuestros cuerpos celestes demuestran ese espacio a través de la modestia, manteniéndose entre 0 y 20 píxeles de ancho. Este cambio de tamaño escala el espacio entre los puntos para crear una sensación de amplitud "científica" y mejora la velocidad percibida.
Para crear una sensación de escala entre objetos con masas muy diferentes, inicializamos cuerpos con un drawSize
proporcional a la 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) } }
Fabricación de sistemas solares a medida
Ahora, cuando creemos nuestro sistema solar en main.js
, tendremos todas las herramientas que necesitamos para nuestra visualización:
// 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()
Puede notar los dos "asteroides" en la parte inferior. Estos objetos de masa cero son un truco que se utiliza para "fijar" la ventana de visualización más pequeña de la simulación en un área de 30x30 centrada en 0,0.
Ahora estamos listos para nuestra función de pintura. La nube de cuerpos puede "bambolearse" alejándose del origen (0,0,0), por lo que también debemos cambiar además de la escala.
Hemos "terminado" cuando la simulación tiene una sensación natural. No hay una forma "correcta" de hacerlo. Para organizar las posiciones iniciales de los planetas, jugué con los números hasta que se mantuvieron juntos el tiempo suficiente para ser interesantes.
// 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 } }
El código de dibujo del lienzo real tiene solo cinco líneas, cada una de las cuales comienza con this.vis
. El resto del código es el agarre de la escena.
El arte nunca se acaba, hay que abandonarlo
Cuando los clientes parecen estar gastando dinero que no los va a hacer ganar dinero, ahora es un buen momento para sacar el tema. Invertir en arte es una decisión empresarial.
El cliente de este proyecto (yo) decidió pasar de la implementación del lienzo a WebVR. Quería una demostración de WebVR llamativa y llena de publicidad. ¡Así que terminemos esto y obtengamos algo de eso!
Con lo que hemos aprendido, podríamos llevar este proyecto de lienzo en una variedad de direcciones. Si recuerdas de la segunda publicación, estamos haciendo varias copias de los datos del cuerpo en la memoria:
Si el rendimiento es más importante que la complejidad del diseño, es posible pasar el búfer de memoria del lienzo a WebAssembly directamente. Esto ahorra un par de copias de memoria, lo que se suma al rendimiento:
- CanvasRenderingContextPrototipo 2D a AssemblyScript
- Optimización de las llamadas a la función CanvasRenderingContext2D mediante AssemblyScript
- OffscreenCanvas: acelere sus operaciones de lienzo con un trabajador web
Al igual que WebAssembly y AssemblyScript, estos proyectos están manejando interrupciones de compatibilidad ascendentes a medida que las especificaciones contemplan estas nuevas y sorprendentes características del navegador.
Todos estos proyectos, y todo el código abierto que utilicé aquí, están construyendo los cimientos para el futuro de los bienes comunes de Internet VR-first. ¡Nos vemos y gracias!
En la publicación final, veremos algunas diferencias de diseño importantes entre la creación de una escena de realidad virtual y una página web plana. Y debido a que la realidad virtual no es trivial, construiremos nuestro mundo giratorio con un marco WebVR. Elegí A-Frame de Google, que también está construido sobre lienzo.
Ha sido un largo viaje para llegar al comienzo de WebVR. Pero esta serie no se trataba de la demostración hello world de A-Frame. Escribí esta serie en mi entusiasmo para mostrarles los fundamentos de la tecnología de navegador que impulsarán los primeros mundos de realidad virtual de Internet por venir.