WebVR 第 4 部分:画布数据可视化

已发表: 2022-03-11

欢呼! 我们着手为 WebVR 创建概念证明。 我们之前的博客文章完成了模拟,所以现在是时候进行一些创造性的游戏了。

对于设计师和开发人员来说,这是一个令人难以置信的激动人心的时刻,因为 VR 是一种范式转变。

2007 年,苹果推出第一款 iPhone,开启了智能手机消费革命。 到 2012 年,我们已经进入了“移动优先”和“响应式”的网页设计。 2019 年,Facebook 和 Oculus 发布了第一款移动 VR 耳机。 我们开工吧!

“移动优先”的互联网并不是一种时尚,我预测“VR 优先”的互联网也不会成为时尚。 在之前的三篇文章和演示中,我展示了您当前浏览器中的技术可能性。

如果您在本系列的中间看到这个,我们正在构建一个旋转行星的天体引力模拟。

  • 第 1 部分:介绍和架构
  • 第 2 部分: Web Workers 为我们提供额外的浏览器线程
  • 第 3 部分:我们的 O(n²) 性能瓶颈代码的 WebAssembly 和 AssemblyScript

站在我们已经完成的工作上,是时候进行一些创造性的游戏了。 在最后两篇文章中,我们将探讨画布和 WebVR 以及用户体验。

  • 第 4 部分:画布数据可视化(这篇文章)
  • 第 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 个随机体添加到系统中。 我们在第一篇文章中触摸了按钮代码,您可以在此处的 repo 中看到它。 这些机构非常适合测试概念验证,但请记住,我们处于危险的性能领域 - 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 画布可视化模拟

“游戏引擎”是带有爆炸的“模拟引擎”。 两者都是极其复杂的工具,因为它们专注于资产管道、流式关卡加载以及各种不应该被注意到的令人难以置信的无聊东西。

网络还通过“移动优先”设计创造了自己的“永远不应该被注意到的东西”。 如果浏览器调整大小,我们的画布的 CSS 将调整 DOM 中的画布元素的大小,因此我们的可视化器必须适应或遭受用户的蔑视。

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

此要求驱动nBodyVisualizer基类和画布实现中的resize()

 /** * 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 - 绘图区域的尺寸

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

您可能会注意到底部的两个“小行星”。 这些零质量对象是一种黑客,用于将模拟的最小视口“固定”到以 0,0 为中心的 30x30 区域。

我们现在已经准备好我们的绘画功能了。 物体云可以“摆动”远离原点 (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。 这可以节省一些内存副本,从而提高性能:

  • 到 AssemblyScript 的 CanvasRenderingContext2D 原型
  • 使用 AssemblyScript 优化 CanvasRenderingContext2D 函数调用
  • OffscreenCanvas — 使用 Web Worker 加快画布操作

就像 WebAssembly 和 AssemblyScript 一样,这些项目正在处理上游兼容性中断,因为规范设想了这些惊人的新浏览器功能。

所有这些项目——以及我在这里使用的所有开源项目——都在为 VR 优先的互联网公共空间的未来奠定基础。 我们见到你,谢谢你!

在最后一篇文章中,我们将了解创建 VR 场景与创建平面网页之间的一些重要设计差异。 由于 VR 不平凡,我们将使用 WebVR 框架构建我们的旋转世界。 我选择了 Google 的 A-Frame,它也是基于画布构建的。

进入 WebVR 的起点是一段漫长的旅程。 但是这个系列不是关于 A-Frame hello world 演示的。 我兴奋地写了这个系列,向您展示将为互联网的 VR 优先世界提供动力的浏览器技术基础。