WebVR Parte 4: Visualizações de dados da tela
Publicados: 2022-03-11Viva! Nós nos propusemos a criar uma Prova de Conceito para WebVR. Nossas postagens anteriores do blog completaram a simulação, então agora é hora de um pouco de jogo criativo.
Este é um momento incrivelmente empolgante para ser designer e desenvolvedor, porque VR é uma mudança de paradigma.
Em 2007, a Apple vendeu o primeiro iPhone, dando início à revolução do consumo de smartphones. Em 2012, estávamos bem avançados em web design “mobile-first” e “responsivo”. Em 2019, o Facebook e a Oculus lançaram o primeiro headset VR móvel. Vamos fazer isso!
A internet “mobile-first” não era uma moda passageira, e prevejo que a internet “VR-first” também não será. Nos três artigos e demos anteriores, demonstrei a possibilidade tecnológica em seu navegador atual .
Se você está pegando isso no meio da série, estamos construindo uma simulação de gravidade celestial de planetas giratórios.
- Parte 1: Introdução e Arquitetura
- Parte 2: Web Workers nos dão threads de navegador adicionais
- Parte 3: WebAssembly e AssemblyScript para nosso código de gargalo de desempenho O(n²)
De pé sobre o trabalho que fizemos, é hora de um jogo criativo. Nas duas últimas postagens, exploraremos a tela e o WebVR e a experiência do usuário.
- Parte 4: Visualização de dados do Canvas (este post)
- Parte 5: Visualização de Dados WebVR
Hoje, vamos dar vida à nossa simulação. Olhando para trás, percebi o quanto estava mais animado e interessado em concluir o projeto quando comecei a trabalhar nos visualizadores. As visualizações tornaram interessante para outras pessoas.
O objetivo desta simulação foi explorar a tecnologia que permitirá a WebVR - Realidade Virtual no navegador - e a próxima web VR-first . Essas mesmas tecnologias podem potencializar a computação na borda do navegador.
Completando nossa Prova de Conceito, hoje vamos primeiro criar uma visualização de tela.
No post final, veremos o design de VR e faremos uma versão WebVR para que este projeto seja “pronto”.
A coisa mais simples que poderia funcionar: console.log()
De volta ao RR (Real Realidade). Vamos criar algumas visualizações para nossa simulação “n-body” baseada em navegador. Eu usei tela em aplicativos de vídeo da web em projetos anteriores, mas nunca como tela de um artista. Vamos ver o que podemos fazer.
Se você se lembra de nossa arquitetura de projeto, delegamos a visualização a nBodyVisualizer.js
.
nBodySimulator.js
tem um loop de simulação start()
que chama sua função step()
, e a parte inferior de step()
chama 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 pressionamos o botão verde, o thread principal adiciona 10 corpos aleatórios ao sistema. Tocamos no código do botão no primeiro post e você pode vê-lo no repositório aqui. Esses corpos são ótimos para testar uma prova de conceito, mas lembre-se de que estamos em território de desempenho perigoso - O(n²).
Os humanos são projetados para se preocupar com as pessoas e coisas que podem ver, então trimDebris()
remove objetos que estão voando para fora da vista para que eles não atrapalhem o resto. Essa é a diferença entre o desempenho percebido e o real.
Agora que cobrimos tudo, menos o this.visualize()
final, vamos dar uma olhada!
// 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) }
Essas duas funções nos permitem adicionar vários visualizadores. Existem dois visualizadores na versão de tela:
// 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")) ) …
Na versão canvas, o primeiro visualizador é a tabela de números brancos exibida como HTML. O segundo visualizador é um elemento de tela preta embaixo.
Para criar isso, comecei com uma classe base simples em 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)) } }
Essa classe imprime no console (a cada 33ms!) e também rastreia um htmlElement - que usaremos em subclasses para facilitar a declaração em main.js
.
Esta é a coisa mais simples que poderia funcionar.
No entanto, embora essa visualização do console
seja definitivamente simples, na verdade não “funciona”. O console do navegador (e os humanos que navegam) não são projetados para processar mensagens de log na velocidade de 33ms. Vamos encontrar a próxima coisa mais simples que poderia funcionar.
Visualizando Simulações com Dados
A próxima iteração “pretty print” foi imprimir texto em um elemento HTML. Este também é o padrão que usamos para a implementação do canvas.
Observe que estamos salvando uma referência a um htmlElement
no qual o visualizador irá pintar. Como tudo na web, ele tem um design mobile-first. Na área de trabalho, isso imprime a tabela de dados dos objetos e suas coordenadas à esquerda da página. No celular, isso resultaria em confusão visual, então o ignoramos.
/** * 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 “fluxo de dados” tem duas funções:
- É uma forma de “verificar a sanidade” das entradas da simulação no visualizador. Esta é uma janela de “depuração”.
- É legal de se ver, então vamos mantê-lo para a demonstração da área de trabalho!
Agora que estamos bastante confiantes em nossas entradas, vamos falar sobre gráficos e telas.

Visualizando Simulações com Tela 2D
Um “Game Engine” é um “Simulation Engine” com explosões. Ambas são ferramentas incrivelmente complicadas porque se concentram em pipelines de ativos, carregamento de nível de streaming e todos os tipos de coisas incrivelmente chatas que nunca devem ser notadas.
A web também criou suas próprias “coisas que nunca devem ser notadas” com design “mobile-first”. Se o navegador for redimensionado, o CSS do nosso canvas irá redimensionar o elemento canvas no DOM, então nosso visualizador deve se adaptar ou sofrer o desprezo dos usuários.
#visCanvas { margin: 0; padding: 0; background-color: #1F1F1F; overflow: hidden; width: 100vw; height: 100vh; }
Esse requisito direciona resize()
na classe base nBodyVisualizer
e na implementação de tela.
/** * 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') }
Isso resulta em nosso visualizador com três propriedades essenciais:
-
this.vis
- pode ser usado para desenhar primitivos -
this.sizeX
-
this.sizeY
- as dimensões da área de desenho
Notas de design de visualização 2D da tela
Nosso redimensionamento funciona em relação à implementação de tela padrão. Se estivéssemos visualizando um produto ou gráfico de dados, gostaríamos de:
- Desenhe na tela (em um tamanho e proporção preferidos)
- Em seguida, faça com que o navegador redimensione esse desenho no elemento DOM durante o layout da página
Nesse caso de uso mais comum, o produto ou gráfico é o foco da experiência.
Nossa visualização é, em vez disso, uma visualização teatral da vastidão do espaço , dramatizada jogando dezenas de pequenos mundos no vazio por diversão.
Nossos corpos celestes demonstram esse espaço por modéstia - mantendo-se entre 0 e 20 pixels de largura. Esse redimensionamento dimensiona o espaço entre os pontos para criar uma sensação de amplitude “científica” e aumenta a velocidade percebida.
Para criar uma sensação de escala entre objetos com massas muito diferentes, inicializamos corpos com um drawSize
proporcional à 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) } }
Fabricação de sistemas solares sob medida
Agora, quando criarmos nosso sistema solar em main.js
, teremos todas as ferramentas necessárias para nossa visualização:
// 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()
Você pode notar os dois “asteroides” na parte inferior. Esses objetos de massa zero são um hack usado para “fixar” a menor janela de visualização da simulação em uma área de 30x30 centrada em 0,0.
Agora estamos prontos para nossa função de pintura. A nuvem de corpos pode “balançar” para longe da origem (0,0,0), então também devemos mudar além da escala.
Estamos “feitos” quando a simulação tem uma sensação natural. Não existe uma maneira “certa” de fazer isso. Para organizar as posições iniciais dos planetas, apenas brinquei com os números até que ficassem juntos por tempo suficiente para serem interessantes.
// 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 } }
O código de desenho real da tela tem apenas cinco linhas - cada uma começando com this.vis
. O resto do código é o controle da cena.
A arte nunca está acabada, deve ser abandonada
Quando os clientes parecem estar gastando dinheiro que não vai ganhar dinheiro, agora é um bom momento para trazer isso à tona. Investir em arte é uma decisão de negócios.
O cliente deste projeto (eu) decidiu passar da implementação do canvas para o WebVR. Eu queria uma demonstração de WebVR chamativa e cheia de hype. Então vamos encerrar isso e pegar um pouco disso!
Com o que aprendemos, poderíamos levar esse projeto de tela em várias direções. Se você se lembra do segundo post, estamos fazendo várias cópias dos dados do corpo na memória:
Se o desempenho for mais importante que a complexidade do design, é possível passar o buffer de memória da tela diretamente para o WebAssembly. Isso economiza algumas cópias de memória, o que aumenta o desempenho:
- Protótipo CanvasRenderingContext2D para AssemblyScript
- Otimizando chamadas de função CanvasRenderingContext2D usando AssemblyScript
- OffscreenCanvas — Acelere suas operações de tela com um Web Worker
Assim como o WebAssembly e o AssemblyScript, esses projetos estão lidando com quebras de compatibilidade de upstream conforme as especificações prevêem esses incríveis novos recursos do navegador.
Todos esses projetos - e todo o código aberto que usei aqui - estão construindo as bases para o futuro do VR-first internet commons. Nos vemos e obrigado!
Na postagem final, veremos algumas diferenças de design importantes entre a criação de uma cena de RV e uma página da web plana. E como a RV não é trivial, construiremos nosso mundo giratório com uma estrutura WebVR. Eu escolhi o A-Frame do Google, que também é construído sobre tela.
Tem sido uma longa jornada para chegar ao início do WebVR. Mas esta série não era sobre a demo do A-Frame hello world. Eu escrevi esta série empolgada para mostrar a vocês os fundamentos da tecnologia de navegadores que alimentarão os primeiros mundos de VR da Internet que estão por vir.