本文概述
欢呼!我们着手为WebVR创建概念证明。我们之前的博客文章完成了模拟, 因此现在该进行一些创造性的练习了。
这是成为设计师和开发人员的激动人心的时刻, 因为VR是一种范式转变。
2007年, 苹果销售了第一部iPhone, 掀开了智能手机消费革命。到2012年, 我们已经进入”移动优先”和”响应式”网页设计。在2019年, Facebook和Oculus发布了第一款移动VR头显。我们开工吧!
“移动优先”互联网不是一种时尚, 我预测” VR优先”互联网也不会。在前三篇文章和演示中, 我展示了当前浏览器中的技术可能性。
如果你是在本系列的中间进行讲解的话, 那么我们正在建立一个尖刺行星的天体重力模拟。
- 第1部分:简介和体系结构
- 第2部分:Web Workers为我们提供了其他浏览器线程
- 第3部分:用于我们的O(n²)性能瓶颈代码的WebAssembly和AssemblyScript
站在我们已经完成的工作上, 是时候进行一些创意工作了。在最后两篇文章中, 我们将探讨canvas和WebVR以及用户体验。
- 第4部分:Canvas数据可视化(本文)
- 第5部分:WebVR数据可视化
今天, 我们将使仿真变得栩栩如生。回顾过去, 我发现一旦开始使用可视化工具, 我对完成该项目会感到更加兴奋和感兴趣。可视化使其他人感到有趣。
该模拟的目的是探索将使WebVR(浏览器中的虚拟现实)和即将面世的VR优先的Web成为可能的技术。这些相同的技术可以支持浏览器边缘计算。
为了完善概念验证, 今天我们首先创建一个画布可视化。
Canvas Visualizer演示, 示例代码
在最后一篇文章中, 我们将研究VR设计并制作WebVR版本, 以使该项目”完成”。
可能工作的最简单方法:console.log()
返回RR(现实)。让我们为基于浏览器的” n体”模拟创建一些可视化效果。我在过去的项目中曾在网络视频应用程序中使用过画布, 但从未将其用作艺术家的画布。让我们看看我们能做什么。
如果你还记得我们的项目架构, 我们会将可视化委托给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中轻松声明它们。
这是可能可行的最简单的方法。
但是, 尽管此控制台可视化绝对简单, 但实际上并不”起作用”。浏览器控制台(和正在浏览的人)并非旨在以33ms的速度处理日志消息。让我们找到可能可行的下一个最简单的方法。
用数据可视化仿真
下一个”漂亮打印”迭代是将文本打印到HTML元素。这也是我们用于canvas实现的模式。
请注意, 我们正在保存对可视化工具将在其上绘制的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
}
}
此”数据流”可视化器具有两个功能:
- 这是一种”合理检查”仿真器在可视化器中的输入的方法。这是一个”调试”窗口。
- 看起来很酷, 因此让我们将其保留在桌面演示中!
现在我们对输入内容非常有信心, 让我们来谈谈图形和画布。
使用2D Canvas可视化仿真
“游戏引擎”是带有爆炸的”模拟引擎”。两者都是难以置信的复杂工具, 因为它们专注于资产管道, 流级别加载以及永远不应该被注意到的各种令人厌烦的东西。
网络还通过”移动优先”设计创建了自己的”不应该引起注意的事物”。如果浏览器调整大小, 我们画布的CSS将调整DOM中画布元素的大小, 因此我们的可视化工具必须适应或遭受用户的鄙视。
#visCanvas {
margin: 0;
padding: 0;
background-color: #1F1F1F;
overflow: hidden;
width: 100vw;
height: 100vh;
}
此要求在nBodyVisualizer基类和canvas实现中驱动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可视化设计说明
我们的调整大小针对默认的画布实现。如果我们要可视化产品或数据图, 则需要:
- 在画布上绘制(以首选的尺寸和纵横比)
- 然后让浏览器在页面布局期间将该图形调整为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为中心的30×30区域的hack。
现在我们准备好绘画功能。物体的云可以从原点(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
- 使用AssemblyScript优化CanvasRenderingContext2D函数调用
- OffscreenCanvas —通过Web Worker加速你的Canvas操作
就像WebAssembly和AssemblyScript一样, 这些项目正在处理上游兼容性中断, 因为规范预见了这些令人惊叹的新浏览器功能。
所有这些项目以及我在此使用的所有开源都为VR优先的Internet共享的未来奠定了基础。我们见到你, 谢谢!
在最后一篇文章中, 我们将介绍在创建VR场景与平面网页之间的一些重要设计差异。而且由于VR并非无关紧要, 因此我们将使用WebVR框架来构建我们的世界。我选择了Google的A-Frame, 它也是基于画布构建的。
开始WebVR的过程很漫长。但是本系列不是关于A-Frame的世界演示。我激动地写了这个系列文章, 向你展示了浏览器技术基础, 这些基础技术将为互联网的VR第一世界提供动力。