本文概述
- 可能工作的最简单方法:console.log()
- 用数据可视化仿真
- 使用2D Canvas可视化仿真
- 手工定制太阳能系统
- 艺术从未完成, 必须被遗弃
这是成为设计师和开发人员的激动人心的时刻, 因为VR是一种范式转变。
2007年, 苹果销售了第一部iPhone, 掀开了智能手机消费革命。到2012年, 我们已经进入” 移动优先” 和” 响应式” 网页设计。在2019年, Facebook和Oculus发布了第一款移动VR头显。我们开工吧!
“ 移动优先” 互联网不是一种时尚, 我预测” VR优先” 互联网也不会。在前三篇文章和演示中, 我展示了当前浏览器中的技术可能性。
如果你是在本系列的中间进行讲解的话, 那么我们正在建立一个尖刺行星的天体重力模拟。
- 第1部分:简介和体系结构
- 第2部分:Web Workers为我们提供了其他浏览器线程
- 第3部分:用于我们的O(n2)性能瓶颈代码的WebAssembly和AssemblyScript
- 第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(n2)。
人们的设计目的是关心他们所看到的人和物, 因此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 Simulationconst sim = new nBodySimulator()// Add some visualizerssim.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 visualizationresize() { 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-绘图区域的尺寸
我们的调整大小针对默认的画布实现。如果我们要可视化产品或数据图, 则需要:
- 在画布上绘制(以首选的尺寸和纵横比)
- 然后让浏览器在页面布局期间将该图形调整为DOM元素的大小
相反, 我们的可视化是对广阔空间的戏剧性可视化, 通过将数十个微小的世界扑向虚空以进行娱乐来进行戏剧化。
我们的天体通过谦虚展示了空间-将自身保持在0到20像素之间。调整大小可缩放点之间的空间, 以创建” 科学的” 宽敞感并增强感知的速度。
为了在质量大不相同的对象之间创建比例感, 我们使用与质量成比例的drawSize初始化物体:
// nBodySimulation.jsexport 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 //namecolorx y z mvz vyvzsim.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 simulationsim.start()
你可能会注意到底部的两个” 小行星” 。这些零质量的对象是用于将模拟的最小视口” 固定” 在以0, 0为中心的30× 30区域的hack。
现在我们准备好绘画功能。物体的云可以从原点(0, 0, 0)” 摆动” 开, 因此除了比例之外, 我们还必须移动。
当模拟具有自然感觉时, 我们就” 完成” 了。没有做到这一点的” 正确” 方法。为了排列行星的初始位置, 我只是弄弄数字, 直到它们保持足够长的时间以至于变得有趣。
// Paint on the canvaspaint(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) =>
{// Centerconst drawX = (body.x - shiftX) * scaleXconst drawY = (body.y - shiftY) * scaleY// Draw on canvasthis.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 zbounds(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.xif (ret.xMax <
body.x) ret.xMax = body.xif (ret.yMin >
body.y) ret.yMin = body.yif (ret.yMax <
body.y) ret.yMax = body.yif (ret.zMin >
body.z) ret.zMin = body.zif (ret.zMax <
body.z) ret.zMax = body.z
})
return ret
}}
实际的画布绘制代码只有五行-每行以this.vis开头。其余的代码是场景的抓地力。
艺术从未完成, 必须被遗弃 如果客户似乎在花钱, 而这并不能赚钱, 那么现在正是提倡这笔钱的好时机。投资艺术品是一项商业决策。
这个项目(我)的客户决定从画布实现过渡到WebVR。我想要一个华丽的, 充满炒作的WebVR演示。因此, 让我们总结一下并得到其中的一部分!
根据我们所学的知识, 我们可以在各个方向进行这个画布项目。如果你还记得第二篇文章, 我们将在内存中复制身体数据的多个副本:
文章图片
如果性能比设计复杂度更重要, 则可以将画布的内存缓冲区直接传递给WebAssembly。这样可以节省几个内存副本, 从而增加了性能:
- CanvasRenderingContext2D原型到AssemblyScript
- 使用AssemblyScript优化CanvasRenderingContext2D函数调用
- OffscreenCanvas —通过Web Worker加速你的Canvas操作
所有这些项目以及我在此使用的所有开源都为VR优先的Internet共享的未来奠定了基础。我们见到你, 谢谢!
在最后一篇文章中, 我们将介绍在创建VR场景与平面网页之间的一些重要设计差异。而且由于VR并非无关紧要, 因此我们将使用WebVR框架来构建我们的世界。我选择了Google的A-Frame, 它也是基于画布构建的。
【WebVR第4部分(画布数据可视化)】开始WebVR的过程很漫长。但是本系列不是关于A-Frame的世界演示。我激动地写了这个系列文章, 向你展示了浏览器技术基础, 这些基础技术将为互联网的VR第一世界提供动力。
推荐阅读
- WebVR第5部分(设计和实现)
- 线框映射(如何避免示波器蠕变)
- 所有的好处,没有麻烦(Angular 9教程)
- 不听客户的意见-为什么用户研究如此重要
- 保持控制(Webpack和React指南(2))
- 电视UI设计(使用空白)
- 复杂的原型-为什么要使用Axure
- 新现实(VR,AR,MR和设计的未来)
- 灰色问题–设计过程中的思维导图是什么()