本文概述
- WebVR和Google A-Frame
- 定义整合挑战
- 如何实现虚拟现实
- 虚拟现实设计师学习”悬崖”
- 回到我们的虚拟现实
- WebVR中的交互
- 碰撞是最”类似于VR”的互动
- 投影是在3D空间中的2D”类似于Web”的单击
- 不使用”注视”的控制器进行投影
- 我们有一个整合计划
- 通过JavaScript以编程方式管理A-Frame对象
- 定义A帧事件和交互
- WebVR:Veni, Look, Vici
我喜欢让项目”完成”。我们已经走到了旅程的尽头-WebVR中天体重力模拟的诞生。
在最后一篇文章中, 我们将把高性能的仿真代码(第1, 2, 3条)插入到基于canvas可视化工具(第4条)的WebVR可视化工具中。
- ” n体问题”简介和体系结构
- 网络工作者为我们提供了其他浏览器线程
- WebAssembly和AssemblyScript用于我们的O(n²)性能瓶颈代码
- 画布数据可视化
- WebVR数据可视化
这是一篇较长的文章, 因此我们将跳过之前介绍的一些技术细节。如果你想了解方向, 请查阅以前的文章, 或危险阅读。
我们一直在探索浏览器的范式从单线程JavaScript运行时到多线程(网络工作者)高性能运行时(WebAssembly)的转变。这些性能桌面计算功能可在Progressive Web Apps和SaaS分发模型中使用。
WebVR演示, 示例代码
VR将创建引人注目的, 无干扰的销售和营销环境, 以进行交流, 说服和衡量参与度(眼动和互动)。数据仍将是零和一, 但是预期的执行摘要和消费者体验将是WebVR-就像我们今天为平面Web构建移动仪表板体验一样。
这些技术还支持分布式浏览器边缘计算。例如, 我们可以创建一个基于Web的应用程序, 以在模拟中为数百万颗恒星运行WebAssembly计算。另一个示例是一个动画应用程序, 它可以在你编辑自己的作品时呈现其他用户的作品。
娱乐内容正在引领虚拟现实技术的发展, 就像移动设备上的娱乐一样。但是, 一旦VR正常(就像今天的移动设备优先设计), 那将是预期的体验(VR优先设计)。对于设计师和开发人员而言, 这是一个非常激动人心的时刻-VR是一种完全不同的设计范例。
如果你抓不住, 你不是VR设计师。这是一个大胆的声明, 今天是对VR设计的深入了解。你在阅读本文时就发明了这个领域。我的目的是分享我在软件和电影方面的经验, 以启动” VR优先设计”对话。我们彼此学习。
考虑到这些宏伟的预测, 我想以专业技术演示的形式完成此项目-WebVR是一个不错的选择!
WebVR和Google A-Frame
WebVR git repo是canvas版本的一个分支, 有几个原因。它使在Github页面上托管项目变得更加容易, 并且WebVR需要进行一些更改, 这些更改会使画布版本和这些文章变得混乱。
如果你还记得我们关于体系结构的第一篇文章, 我们将整个模拟委托给了nBodySimulator。
网络工作者的帖子显示, nBodySimulator具有一个step()函数, 每33ms模拟一次。 step()调用calculateForces()来运行我们的O(n²)WebAssembly模拟代码(第3条), 然后更新位置并重新绘制。在上一篇创建画布可视化的文章中, 我们从这个基类开始使用canvas元素实现了这一点:
/**
* Base class that console.log()s the simulation state.
*/
export class nBodyVisualizer {
constructor(htmlElement) {
this.htmlElement = htmlElement
this.resize()
this.scaleSize = 25 // divided into bodies drawSize. drawSize is log10(mass)
// This could be refactored to the child class.
// Art is never finished. It must be abandoned.
}
resize() {}
paint(bodies) {
console.log(JSON.stringify(bodies, null, 2))
}
}
定义整合挑战
我们有模拟。现在, 我们希望与WebVR集成-无需重新设计项目。我们对仿真所做的任何调整都会在paint(bodies)函数的主UI线程中每33ms发生一次。
这就是我们衡量”完成”的方式。我很兴奋-让我们开始工作吧!
如何实现虚拟现实
首先, 我们需要一个设计:
- VR由什么制成?
- WebVR设计如何表达?
- 我们如何与之互动?
虚拟现实可以追溯到时间的曙光。每个篝火故事都是微小的虚拟世界, 这些琐碎的细节掩盖了荒诞的夸张。
通过添加3D立体视觉效果和音频, 我们可以将篝火故事放大10倍。我的电影制作预算讲师曾经说过:”我们只为海报付费。我们不是在建立现实。”
如果你熟悉浏览器DOM, 就会知道它会创建树状的分层结构。
平面”场景图”。
网页设计中隐含的是查看者从”正面”进行查看。从侧面看, 将DOM元素显示为线, 从背面看, 我们仅看到<body>标签, 因为它遮盖了其子元素。
VR身临其境的体验的一部分是让用户控制他们的观点, 样式, 节奏和交互顺序。他们不必特别注意任何事情。如果你以编程方式移动或旋转相机, 则它们实际上会从VR疾病中呕吐出来。
请注意, VR疾病不是开玩笑。我们的眼睛和内耳都可以检测到运动。对于直立行走的动物来说非常重要。当那些运动传感器不同意时, 我们的大脑自然会认为我们的嘴巴又在胡说八道并呕吐。我们都是孩子一次。关于VR的这种生存本能已有许多文献报道。 Steam上免费提供” Epic Fun”头衔, 过山车是我发现的最好的VR病演示。
虚拟现实表示为”场景图”。场景图具有与DOM相同的树状图案, 以隐藏令人信服的3D环境的细节和复杂性。但是, 我们将查看器放置在他们想要向其拉动体验的位置, 而不是滚动和路由。
这是Google A-Frame WebVR Framework的Hello World场景图:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello, WebVR! • A-Frame</title>
<meta name="description" content="Hello, WebVR! • A-Frame">
<script src="https://aframe.io/releases/0.9.2/aframe.min.js"></script>
</head>
<body>
<a-scene background="color: #FAFAFA">
<a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9" shadow></a-box>
<a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E" shadow></a-sphere>
<a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D" shadow></a-cylinder>
<a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4" shadow></a-plane>
</a-scene>
</body>
</html>
该HTML文档在浏览器中创建一个DOM。 <a-*>标签是A-Frame框架的一部分, 而<a-scene>是场景图的根。在这里, 我们看到了场景中显示的四个3D图元。
平面网络浏览器中的A帧场景。
首先, 请注意, 我们正在通过平面网络浏览器查看场景。右下角的小面具邀请用户切换到3D立体模式。
虚拟现实中的A帧场景-每只眼睛一张图像。
从理论上讲, 你应该能够:
- 在手机上打开
- 抬起手机面对面
- 在新现实的辉煌中欢欣鼓舞!
如果没有VR耳机的精美镜头, 我永远无法做到这一点。你可以在便宜的价格(基于Google Cardboard的基本设备)上为Android手机获得VR耳机, 但是, 对于开发内容, 我建议使用独立的HMD(头盔显示器), 例如Oculus Quest。
就像潜水或跳伞一样, 虚拟现实是一项齿轮运动。
虚拟现实设计师学习”悬崖”
欢迎来到重力与光线的舒适现实。
请注意, A帧Hello World场景具有默认的照明和摄像头:
- 立方体的面是不同的颜色-立方体是自阴影的。
- 立方体在平面上投下阴影-有定向光。
- 立方体和平面之间没有缝隙-这是一个有重力的世界。
这些关键提示会向观看者说:”放轻松, 这东西在你的脸上是完全正常的。”
另请注意, 默认设置在上面的Hello World场景代码中是隐式的。 A-Frame明智地提供了明智的默认设置, 但请注意-平面设计人员必须交叉的摄像头和照明设备才能创建VR。
我们将默认照明设置视为理所当然。例如, 按钮:
注意这种隐式照明在设计和摄影中的普及程度。甚至”平面设计”按钮也无法摆脱网络的默认照明-它向右下方投射了阴影。
设计, 交流和实现照明和摄像头设置是WebVR设计师的学习重点。 “电影的语言”是文化规范的集合, 表现为不同的相机和照明设置, 可以将故事情感地传达给观众。负责在场景周围设计/移动灯光和相机的电影专业人士是握把部门。
回到我们的虚拟现实
现在, 让我们重新开始工作。我们的天体WebVR场景具有类似的模式:
<!DOCTYPE>
<html>
<head>
<script src="https://aframe.io/releases/0.9.2/aframe.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/aframe-event-set-component.min.js"></script>
<script src="main.js"></script>
</head>
<body>
<a-scene id="a-pocket-universe">
<a-sky color="#222"></a-sky>
<a-entity geometry="primitive: circle; radius: 12" position="0 0 -.5"
material="color: #333; transparent: true; opacity: 0.5">
<a-sphere color="black" radius=."02"></a-sphere>
</a-entity>
<a-entity id="a-bodies"></a-entity>
<a-entity geometry="primitive: plane; width: 2; height: auto" position="0 -10 .3" rotation="55 0 0"
material="color: blue"
text="value: Welcome Astronaut!...">
</a-entity>
<a-entity id="rig" position="0 -12 .7" rotation="55 0 0">
<a-camera>
<a-cursor color="#4CC3D9" fuse="true" timeout="1"></a-cursor>
</a-camera>
</a-entity>
</a-scene>
</body>
</html>
该HTML文档加载了A-Frame框架和一个交互插件。我们的场景始于<a-scene id =” a-pocket-universe”>。
在内部, 我们从<a-sky color =”#222″> </ a-sky>元素开始, 为场景中未定义的所有内容设置背景颜色。
接下来, 我们为观众创建一个”轨道平面”, 以便观众在我们陌生而未知的世界中飞翔。我们将其创建为圆盘和(0, 0, 0)处的黑色小球。没有这个, 转弯对我来说是”没有根据的”:
<a-entity geometry="primitive: circle; radius: 12" position="0 0 -.5"
material="color: #333; transparent: true; opacity: 0.5">
<a-sphere color="black" radius=."02"></a-sphere>
</a-entity>
接下来, 我们定义一个集合, 可以在其中添加/删除/重新定位A-Frame实体。
<a-entity id="a-bodies"></a-entity>
这是nBodyVisualizers绘画(主体)执行其工作的许可。
然后, 我们在观众和这个世界之间建立关系。作为技术演示, 这个世界的目的是让观看者探索WebVR和支持它的浏览器技术。一个简单的”宇航员”叙述创造了一种玩耍的感觉, 而这个恒星的路标则是导航的另一个参考点。
<a-entity geometry="primitive: plane; width: 2; height: auto" position="0 -10 .3" rotation="55 0 0"
material="color: blue"
text="value: Welcome Astronaut!\n ...">
</a-entity>
这样就完成了场景图。最后, 我希望用户和这个棘手的世界之间在电话演示中进行某种交互。我们如何在VR中重新创建” Throw Debris”按钮?
该按钮是所有现代设计的主要元素-VR按钮在哪里?
WebVR中的交互
虚拟现实有它自己的”之上”和”下端”。观看者的首次互动是通过其化身或照相机进行的。这是所有要缩放的控件。
如果你是在台式机上阅读此书, 则可以使用WASD进行移动, 并使用鼠标旋转相机。此探索揭示了信息, 但没有表达你的意愿。
现实具有几个非常重要的功能, 这些功能在网络上并不常见:
- 透视-当物体远离我们时, 物体会明显变小。
- 遮挡-根据位置隐藏和显示对象。
VR模拟这些功能来创建3D效果。它们还可以在VR中用于显示信息和界面-并在演示交互之前设置心情。我发现大多数人只需要花一点时间就可以享受体验, 然后继续前进。
在WebVR中, 我们在3D空间中进行交互。为此, 我们有两个基本工具:
- 碰撞-两个对象共享同一空间时触发的被动3D事件。
- 投影-激活的2D函数调用, 列出与一条线相交的所有对象。
碰撞是最”类似于VR”的互动
在VR中, “碰撞”的确切含义是:当两个对象共享同一空间时, A-Frame会创建一个事件。
为了使用户”按下”按钮, 我们必须给他们一个棋子和一些东西来按下按钮。
不幸的是, WebVR尚不能假定控制器-许多人会在台式机或电话上查看平板版本, 许多人会使用Google Cardboard或Samsung的Gear VR之类的耳机来显示立体版本。
如果用户没有控制器, 则他们无法伸出手并”触摸”事物, 因此任何碰撞都必须与他们的”个人空间”有关。
我们可以给玩家一个宇航员形状的棋子来回走动, 但是强迫用户进入旋转的行星状of骨似乎有点令人反感, 这与我们设计的宽敞性背道而驰。
投影是在3D空间中的2D”类似于Web”的单击
除了”碰撞”, 我们还可以使用”投影”。我们可以在场景中投射一条线, 然后看一下它的触感。最常见的示例是”传送射线”。
传送射线描绘了世界上的一条线, 以显示玩家可以移动的位置。这个”投影”寻找着陆的地方。它在投影的路径中返回一个或多个对象。这是一个传送射线示例:
虚幻引擎默认内容中的传送射线。
请注意, 射线实际上是作为指向下方的抛物线实现的。这意味着它自然地与”地面”相交, 就像抛出的物体一样。这自然也设置了最大的隐形传送距离。限制是VR中最重要的设计选择。幸运的是, 现实有许多自然的局限性。
投影将3D世界”拉平”为2D, 因此你可以指向东西以像鼠标一样单击它。第一人称射击游戏是在精致而令人沮丧的按钮上进行的”二维点击”精心制作的游戏-常常带有精心制作的故事, 以解释为什么那些没用的按钮在”点击”你的背上就不好了。
VR中的枪支之所以多, 是因为枪支已被完善为精确而可靠的3D鼠标-而点击就是消费者知道的, 而无需学习。
投影还可以确保与场景之间的距离安全。记住, 接近VR中的某些事物自然会遮盖所有其他尚未显露其重要性的事物。
不使用”注视”的控制器进行投影
要在不带控制器的WebVR中创建此交互原语, 我们可以将观众的”凝视”投射为视线”光标”。可以以编程方式使用此光标与具有”保险丝”的对象进行交互。这会以蓝色小圆圈的形式传达给查看者。现在我们点击!
如果你还记得篝火的故事, 那么谎言越大, 出售它所需要的细节就越少。一个明显而荒谬的”凝视”互动是凝视太阳。我们使用此”凝视”来触发向模拟添加新的”碎片”行星。从来没有观众质疑过这种选择-VR荒谬时非常吸引人。
在A-Frame中, 我们表示摄像机(玩家的隐形兵), 并且将视线”光标”表示为我们的摄像机装备。将<a-cursor>放置在<a-camera>中会导致将相机的变换也应用于光标。当玩家移动/旋转其棋子(a摄像机)时, 它也会移动/旋转其凝视(a光标)。
// src/index.html
<a-entity id="rig" position="0 -12 .7" rotation="55 0 0">
<a-camera>
<a-cursor color="#4CC3D9" fuse="true" timeout="1"></a-cursor>
</a-camera>
</a-entity>
在发出事件之前, 光标的”保险丝”要等到经过一秒钟的”凝视”。
我使用了默认照明, 因此你可能会注意到太阳的”后背”没有照明。虽然我没有走出轨道平面, 但我不认为这是太阳的工作方式。但是, 它适用于我们的现实技术演示海报。
另一种选择是将灯光放置在相机元素内部, 以便随用户移动。这将创造出更亲密的, 甚至可能更怪异的小行星矿工体验。这些是有趣的设计选择。
我们有一个整合计划
这样, 我们现在有了A框架<a-scene>与JavaScript仿真之间的集成点:
A帧<a-场景>:
-
实体的命名集合:<a-entity id =” a-bodies”> </ a-entity>
-
将发出投影事件的光标:<a-cursor color =”#4CC3D9″ fuse =” true” timeout =” 1″> </ a-cursor>
我们的JavaScript模拟:
-
nBodyVisWebVR.paint(bodies)-从模拟主体中添加/删除/重新放置VR实体
-
addBodyArgs(name, color, x, y, z, mass, vX, vY, vZ)为模拟添加新的碎片体
index.html加载main.js, 它初始化我们的模拟就像画布版本一样:
// src/main.js
import { nBodyVisualizer, nBodyVisWebVR } from ."/nBodyVisualizer"
import { Body, nBodySimulator } from ."/nBodySimulator"
window.onload = function() {
// Create a Simulation
const sim = new nBodySimulator()
// this Visualizer manages the UI
sim.addVisualization(new nBodyVisWebVR(document.getElementById("a-bodies"), sim))
// making up stable universes is hard
// name color x y z m vz vy vz
sim.addBody(new Body("star", "yellow", 0, 0, 1, 1e9))
sim.addBody(new Body("hot-jupiter", "red", -1, -1, 1, 1e4, .24, -0.05, 0))
sim.addBody(new Body("cold-jupiter", "purple", 4, 4, .5, 1e4, -.07, 0.04, 0))
// Start simulation
sim.start()
// Add another
sim.addBody(new Body("saturn", "blue", -8, -8, .1, 1e3, .07, -.035, 0))
}
你会在这里注意到, 我们将可视化工具的htmlElement设置为a-body集合以容纳主体。
通过JavaScript以编程方式管理A-Frame对象
在index.html中声明了场景之后, 我们现在就可以对可视化工具进行编码了。
首先, 我们设置nBodyVisualizer以从nBodySimulation主体列表中进行读取, 并在<a-entity id =” a-bodies”> </ a-entity>集合中创建/更新/删除A-Frame对象。
// src/nBodyVisualizer.js
/**
* This is the WebVR visualizer.
* It's responsible for painting and setting up the entire scene.
*/
export class nBodyVisWebVR extends nBodyVisualizer {
constructor(htmlElement, sim) {
// HTML Element is a-collection#a-bodies.
super(htmlElement)
// We add these to the global namespace because
// this isn't the core problem we are trying to solve.
window.sim = sim
this.nextId = 0
}
resize() {}
在构造函数中, 我们保存A-Frame集合, 为凝视事件设置全局变量以查找模拟, 并初始化一个ID计数器, 以用于在模拟和A-Frame的场景之间进行匹配。
paint(bodies) {
let i
// Create lookup table: lookup[body.aframeId] = body
const lookup = bodies.reduce( (total, body) => {
// If new body, give it an aframeId
if (!body.aframeId) body.aframeId = `a-sim-body-${body.name}-${this.nextId++}`
total[body.aframeId] = body
return total
}, {})
// Loop through existing a-sim-bodies and remove any that are not in
// the lookup - this is our dropped debris
const aSimBodies = document.querySelectorAll(."a-sim-body")
for (i = 0; i < aSimBodies.length; i++) {
if (!lookup[aSimBodies[i].id]) {
// if we don't find the scene's a-body in the lookup table of Body()s, // remove the a-body from the scene
aSimBodies[i].parentNode.removeChild(aSimBodies[i]);
}
}
// loop through sim bodies and upsert
let aBody
bodies.forEach( body => {
// Find the html element for this aframeId
aBody = document.getElementById(body.aframeId)
// If html element not found, make one.
if (!aBody) {
this.htmlElement.innerHTML += `
<a-sphere
id="${body.aframeId}"
class="a-sim-body"
dynamic-body
${ (body.name === "star") ? "debris-listener event-set__enter='_event: mouseenter; color: green' event-set__leave='_event: mouseleave; color: yellow'" : ""}
position="0 0 0"
radius="${body.drawSize/this.scaleSize}"
color="${body.color}">
</a-sphere>`
aBody = document.getElementById(body.aframeId)
}
// reposition
aBody.object3D.position.set(body.x, body.y, body.z)
})
}
首先, 我们遍历模拟主体以标记和/或创建查找表, 以将A-Frame实体与模拟主体匹配。
接下来, 我们遍历现有的A型车架车身, 并删除通过模拟修剪的任何车架, 以超出范围。这增加了体验的感知性能。
最后, 我们遍历sim主体以为缺失的主体创建一个新的<a-sphere>, 并使用aBody.object3D.position.set(body.x, body.y, body.z)重新定位其他主体
我们可以使用标准DOM函数以编程方式更改A帧场景中的元素。要向场景添加元素, 我们在容器的innerHTML后面附加一个字符串。这段代码对我来说很奇怪, 但是可以用, 但我发现没有什么比这更好的了。
你会注意到, 当我们创建要附加的字符串时, ” star”附近有一个三元运算符来设置属性。
<a-sphere
id="${body.aframeId}"
class="a-sim-body"
dynamic-body
${ (body.name === "star") ? "debris-listener event-set__enter='_event: mouseenter; color: green' event-set__leave='_event: mouseleave; color: yellow'" : ""}
position="0 0 0"
radius="${body.drawSize/this.scaleSize}"
color="${body.color}">
</a-sphere>`
如果身体是”星星”, 我们添加一些描述其事件的额外属性。这是安装在DOM中后我们的星星的外观:
<a-sphere id="a-sim-body-star-0"
class="a-sim-body"
dynamic-body=""
debris-listener=""
event-set__enter="_event: mouseenter; color: green"
event-set__leave="_event: mouseleave; color: yellow"
position="0 0 0"
radius="0.36"
color="yellow"
material=""
geometry=""></a-sphere>
碎片侦听器, 事件设置__输入和事件设置__三个属性设置了我们的交互, 并且是我们集成的最后一圈。
定义A帧事件和交互
我们在实体的属性中使用NPM包” aframe-event-set-component”, 以在观看者”注视”太阳时更改太阳的颜色。
这种”凝视”是观看者位置和旋转的投影, 并且互动会提供必要的反馈, 告知他们的凝视正在做某事。
现在, 我们的星际球有两个由插件启用的速记事件, event-set__enter和event-set__leave:
<a-sphere id="a-sim-body-star-0"
...
event-set__enter="_event: mouseenter; color: green"
event-set__leave="_event: mouseleave; color: yellow"
…
></a-sphere>
接下来, 我们用碎片侦听器装饰星状球, 并将其实现为自定义A帧组件。
<a-sphere id="a-sim-body-star-0"
...
debris-listener=""
…
></a-sphere>
A-Frame组件是在全局级别定义的:
// src/nBodyVisualizer.js
// Component to add new bodies when the user stares at the sun. See HTML
AFRAME.registerComponent('debris-listener', {
init: function () {
// Helper function
function rando(scale) { return (Math.random()-.5) * scale }
// Add 10 new bodies
this.el.addEventListener('click', function (evt) {
for (let x=0; x<10; x++) {
// name, color, x, y, z, mass, vx, vy, vz
window.sim.addBodyArgs("debris", "white", rando(10), rando(10), rando(10), 1, rando(.1), rando(.1), rando(.1))
}
})
}
})
该A帧组件的作用类似于”点击”侦听器, 可以由凝视光标触发, 以将10个新的随机物体添加到场景中。
总结一下:
- 我们使用标准HTML中的A-Frame声明WebVR场景。
- 我们可以从JavaScript中以编程方式添加/删除/更新场景中的A-Frame实体。
- 我们可以通过A-Frame插件和组件在JavaScript中使用事件处理程序创建交互。
WebVR:Veni, Look, Vici
我希望你能像我一样从这个技术演示中受益匪浅。在将这些功能(Web Worker和WebAssembly)应用于WebVR的地方, 它们也可以应用于浏览器边缘计算。
巨大的技术浪潮已经到来-虚拟现实(VR)。无论你第一次拿着智能手机时的感受如何, 第一次体验VR都会在计算的各个方面带来10倍的情感体验。距第一部iPhone才十二年。
VR已经存在了很长时间, 但是将VR带给普通用户所需的技术是通过移动革命和Facebook的Oculus Quest而不是PC革命来实现的。
互联网和开源是人类世界最伟大的奇迹之一。对于所有创建扁平化互联网的人们-我向你的勇气和冒险精神敬酒。
宣言!我们将创造世界, 因为我们拥有创造的力量。
画布演示, WebVR演示, 示例代码