WebVR, часть 4: визуализация данных на холсте

Опубликовано: 2022-03-11

Ура! Мы решили создать Proof of Concept для WebVR. Наши предыдущие записи в блоге завершили симуляцию, так что теперь пришло время для небольшой творческой игры.

Это невероятно захватывающее время для дизайнера и разработчика, потому что виртуальная реальность — это смена парадигмы.

В 2007 году Apple продала первый iPhone, положив начало революции потребления смартфонов. К 2012 году мы хорошо разобрались в «мобильном» и «отзывчивом» веб-дизайне. В 2019 году Facebook и Oculus выпустили первую мобильную VR-гарнитуру. Давай сделаем это!

Интернет «сначала мобильный» не был причудой, и я предсказываю, что интернет «сначала виртуальная реальность» тоже не будет. В предыдущих трех статьях и демонстрациях я демонстрировал технологические возможности вашего текущего браузера.

Если вы читаете это в середине серии, мы создаем симулятор небесной гравитации вращающихся планет.

  • Часть 1: Введение и архитектура
  • Часть 2. Веб-воркеры предоставляют нам дополнительные потоки браузера
  • Часть 3: WebAssembly и AssemblyScript для нашего кода узкого места производительности O(n²)

Опираясь на проделанную работу, пришло время для творческой игры. В последних двух постах мы рассмотрим холст, WebVR и взаимодействие с пользователем.

  • Часть 4: Визуализация данных Canvas (этот пост)
  • Часть 5: Визуализация данных WebVR

Сегодня мы воплотим нашу симуляцию в жизнь. Оглядываясь назад, я заметил, насколько больше я был взволнован и заинтересован в завершении проекта, как только начал работать над визуализаторами. Визуализации сделали его интересным для других людей.

Цель этой симуляции состояла в том, чтобы изучить технологию, которая позволит использовать WebVR — виртуальную реальность в браузере — и грядущий VR-первый Интернет. Эти же технологии могут использоваться для вычислений на границе браузера.

Завершая наше доказательство концепции, сегодня мы сначала создадим визуализацию холста.

Визуализация холста
Демонстрация Canvas Visualizer, пример кода

В заключительном посте мы рассмотрим дизайн VR и создадим версию WebVR, чтобы этот проект был «завершен».

Визуализация данных WebVR

Самая простая вещь, которая может работать: console.log()

Назад к RR (Реальная Реальность). Давайте создадим несколько визуализаций для нашей браузерной симуляции «n-body». Я использовал холст в веб-видеоприложениях в прошлых проектах, но никогда не использовал его в качестве холста художника. Давайте посмотрим, что мы можем сделать.

Если вы помните архитектуру нашего проекта, визуализацию мы делегировали nBodyVisualizer.js .

Делегируйте визуализацию в nBodyVisualizer.js

nBodySimulator.js есть цикл моделирования start() , который вызывает функцию step() , а в нижней части step() вызывается 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() }

Когда мы нажимаем зеленую кнопку, основной поток добавляет в систему 10 случайных тел. Мы коснулись кода кнопки в первом посте, и вы можете увидеть его в репозитории здесь. Эти тела отлично подходят для проверки концепции, но помните, что мы находимся на опасной территории — O(n²).

Люди созданы, чтобы заботиться о людях и вещах, которые они могут видеть, поэтому trimDebris() удаляет объекты, которые улетают из поля зрения, чтобы они не замедляли работу остальных. Это разница между воспринимаемой и фактической производительностью.

Теперь, когда мы рассмотрели все, кроме окончательного this.visualize() , давайте взглянем!

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

Эти две функции позволяют нам добавить несколько визуализаторов. В версии для холста есть два визуализатора:

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

В версии для холста первым визуализатором является таблица белых чисел, отображаемая в виде HTML. Второй визуализатор представляет собой черный элемент холста под ним.

Визуализаторы холста
Слева визуализатор HTML — это таблица белых чисел. Визуализатор черного холста находится внизу

Чтобы создать это, я начал с простого базового класса в 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)) } }

Этот класс выводит на консоль (каждые 33 мс!), а также отслеживает htmlElement, который мы будем использовать в подклассах, чтобы упростить их объявление в main.js

Это самое простое, что может сработать.

Однако, хотя эта console визуализация определенно проста, на самом деле она не «работает». Консоль браузера (и просматривающие ее люди) не предназначены для обработки сообщений журнала со скоростью 33 мс. Давайте найдем следующую простейшую вещь , которая могла бы работать.

Визуализация моделирования с данными

Следующей итерацией «красивой печати» была печать текста в HTML-элемент. Это также шаблон, который мы используем для реализации холста.

Обратите внимание, что мы сохраняем ссылку на htmlElement , на котором визуализатор будет рисовать. Как и все остальное в сети, он ориентирован на мобильные устройства. На рабочем столе это печатает таблицу данных объектов и их координат в левой части страницы. На мобильных устройствах это приведет к визуальному беспорядку, поэтому мы его пропустим.

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

Этот визуализатор «потока данных» имеет две функции:

  1. Это способ «проверить правильность» входных данных моделирования в визуализаторе. Это окно «отладки».
  2. На него приятно смотреть, так что давайте оставим его для демо-версии!

Теперь, когда мы достаточно уверены в наших входных данных, давайте поговорим о графике и холсте.

Визуализация симуляций с помощью 2D Canvas

«Игровой движок» — это «движок моделирования» со взрывами. Оба являются невероятно сложными инструментами, потому что они сосредоточены на конвейерах ресурсов, загрузке уровней потоковой передачи и всевозможных невероятно скучных вещах, которые никогда не следует замечать.

Сеть также создала свои собственные «вещи, которые никогда не следует замечать» с дизайном «сначала мобильные». Если браузер изменит размер, CSS нашего холста изменит размер элемента холста в DOM, поэтому наш визуализатор должен адаптироваться или страдать от неуважения пользователей.

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

Это требование приводит к resize() в базовом классе nBodyVisualizer и реализации холста.

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

В результате наш визуализатор обладает тремя важными свойствами:

  • this.vis — можно использовать для рисования примитивов
  • this.sizeX
  • this.sizeY - размеры области рисования

Примечания к дизайну 2D-визуализации на холсте

Наше изменение размера работает против реализации холста по умолчанию. Если бы мы визуализировали график продукта или данных, мы бы хотели:

  1. Рисовать на холсте (с предпочтительным размером и соотношением сторон)
  2. Затем попросите браузер изменить размер этого рисунка в элемент DOM во время макета страницы.

В этом более распространенном варианте использования продукт или график находятся в центре внимания.

Вместо этого наша визуализация представляет собой театральную визуализацию необъятности космоса , драматизированную броском десятков крошечных миров в пустоту для развлечения.

Наши небесные тела скромно демонстрируют это пространство, сохраняя ширину от 0 до 20 пикселей. Это изменение размера масштабирует пространство между точками, чтобы создать ощущение «научного» пространства и увеличить воспринимаемую скорость.

Чтобы создать ощущение масштаба между объектами с сильно различающимися массами, мы инициализируем тела с размером 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) } }

Солнечные системы ручной работы на заказ

Теперь, когда мы создадим нашу солнечную систему в main.js , у нас будут все инструменты, необходимые для визуализации:

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

Вы можете заметить два «астероида» внизу. Эти объекты с нулевой массой — это хак, используемый для «прикрепления» наименьшего окна просмотра симуляции к области 30x30 с центром в 0,0.

Теперь мы готовы к нашей функции рисования. Облако тел может «раскачиваться» от начала координат (0,0,0), поэтому мы должны еще и смещаться в дополнение к масштабу.

Мы «готовы», когда симуляция кажется естественной. Не существует «правильного» способа сделать это. Чтобы упорядочить начальные положения планет, я просто возился с числами, пока они не держались вместе достаточно долго, чтобы быть интересными.

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

Фактический код рисования холста состоит всего из пяти строк, каждая из которых начинается с this.vis . Остальной код — это захват сцены.

Искусство никогда не заканчивается, оно должно быть брошено

Когда кажется, что клиенты тратят деньги, которые не принесут им денег, сейчас самое подходящее время, чтобы поднять эту тему. Инвестиции в искусство — это деловое решение.

Клиент этого проекта (я) решил перейти от реализации холста к WebVR. Я хотел яркую демонстрацию WebVR, наполненную шумихой. Итак, давайте завершим это и возьмем часть этого!

Благодаря тому, что мы узнали, мы могли бы использовать этот проект холста в самых разных направлениях. Если вы помните из второго поста, мы делаем несколько копий данных тела в память:

Копии данных тела в памяти

Если производительность важнее сложности дизайна, можно напрямую передать буфер памяти холста в WebAssembly. Это экономит пару копий памяти, что увеличивает производительность:

  • Прототип CanvasRenderingContext2D для AssemblyScript
  • Оптимизация вызовов функций CanvasRenderingContext2D с использованием AssemblyScript
  • OffscreenCanvas — ускорьте работу с Canvas с помощью Web Worker

Так же, как WebAssembly и AssemblyScript, эти проекты обрабатывают перерывы в совместимости вышестоящего уровня, поскольку спецификации предусматривают эти удивительные новые функции браузера.

Все эти проекты — и все ресурсы с открытым исходным кодом, которые я здесь использовал, — закладывают основу для будущего интернет-общества, ориентированного на виртуальную реальность. Мы видим вас и спасибо!

В последнем посте мы рассмотрим некоторые важные различия в дизайне между созданием сцены виртуальной реальности и плоской веб-страницы. А поскольку виртуальная реальность нетривиальна, мы построим наш вращающийся мир с помощью фреймворка WebVR. Я выбрал A-Frame от Google, который также построен на холсте.

Это был долгий путь, чтобы добраться до начала WebVR. Но эта серия была не о демо A-Frame hello world. Я написал эту серию в своем волнении, чтобы показать вам основы технологии браузера, которые будут способствовать появлению первых миров Интернета в виртуальной реальности.