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

本文概述

欢呼!我们着手为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版本, 以使该项目”完成”。

WebVR数据可视化

可能工作的最简单方法:console.log()

返回RR(现实)。让我们为基于浏览器的” n体”模拟创建一些可视化效果。我在过去的项目中曾在网络视频应用程序中使用过画布, 但从未将其用作艺术家的画布。让我们看看我们能做什么。

如果你还记得我们的项目架构, 我们会将可视化委托给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中轻松声明它们。

这是可能可行的最简单的方法。

但是, 尽管此控制台可视化绝对简单, 但实际上并不”起作用”。浏览器控制台(和正在浏览的人)并非旨在以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

  }

}

此”数据流”可视化器具有两个功能:

  1. 这是一种”合理检查”仿真器在可视化器中的输入的方法。这是一个”调试”窗口。
  2. 看起来很酷, 因此让我们将其保留在桌面演示中!

现在我们对输入内容非常有信心, 让我们来谈谈图形和画布。

使用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可视化设计说明

我们的调整大小针对默认的画布实现。如果我们要可视化产品或数据图, 则需要:

  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为中心的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第一世界提供动力。

微信公众号
手机浏览(小程序)
0
分享到:
没有账号? 忘记密码?