WebVR第5部分(设计和实现)

本文概述

  • WebVR和Google A-Frame
  • 定义整合挑战
  • 如何实现虚拟现实
  • 虚拟现实设计师学习” 悬崖”
  • 回到我们的虚拟现实
  • WebVR中的交互
  • 碰撞是最” 类似于VR” 的互动
  • 投影是在3D空间中的2D” 类似于Web” 的单击
  • 不使用” 注视” 的控制器进行投影
  • 我们有一个整合计划
  • 通过JavaScript以编程方式管理A-Frame对象
  • 定义A帧事件和交互
  • WebVR:Veni, Look, Vici
我喜欢让项目” 完成” 。我们已经走到了旅程的尽头-WebVR中天体重力模拟的诞生。
在最后一篇文章中, 我们将把高性能的仿真代码(第1, 2, 3条)插入到基于canvas可视化工具(第4条)的WebVR可视化工具中。
  1. ” n体问题” 简介和体系结构
  2. 网络工作者为我们提供了其他浏览器线程
  3. WebAssembly和AssemblyScript用于我们的O(n2)性能瓶颈代码
  4. 画布数据可视化
  5. WebVR数据可视化
这是一篇较长的文章, 因此我们将跳过之前介绍的一些技术细节。如果你想了解方向, 请查阅以前的文章, 或危险阅读。
我们一直在探索浏览器的范式从单线程JavaScript运行时到多线程(网络工作者)高性能运行时(WebAssembly)的转变。这些性能桌面计算功能可在Progressive Web Apps和SaaS分发模型中使用。
WebVR第5部分(设计和实现)

文章图片
WebVR演示, 示例代码
VR将创建引人注目的, 无干扰的销售和营销环境, 以进行交流, 说服和衡量参与度(眼动和互动)。数据仍将是零和一, 但是预期的执行摘要和消费者体验将是WebVR-就像我们今天为平面Web构建移动仪表板体验一样。
这些技术还支持分布式浏览器边缘计算。例如, 我们可以创建一个基于Web的应用程序, 以在模拟中为数百万颗恒星运行WebAssembly计算。另一个示例是一个动画应用程序, 它可以在你编辑自己的作品时呈现其他用户的作品。
娱乐内容正在引领虚拟现实技术的发展, 就像移动设备上的娱乐一样。但是, 一旦VR正常(就像今天的移动设备优先设计), 那将是预期的体验(VR优先设计)。对于设计师和开发人员而言, 这是一个非常激动人心的时刻-VR是一种完全不同的设计范例。
如果你抓不住, 你不是VR设计师。这是一个大胆的声明, 今天是对VR设计的深入了解。你在阅读本文时就发明了这个领域。我的目的是分享我在软件和电影方面的经验, 以启动” VR优先设计” 对话。我们彼此学习。
考虑到这些宏伟的预测, 我想以专业技术演示的形式完成此项目-WebVR是一个不错的选择!
WebVR和Google A-Frame WebVR git repo是canvas版本的一个分支, 有几个原因。它使在Github页面上托管项目变得更加容易, 并且WebVR需要进行一些更改, 这些更改会使画布版本和这些文章变得混乱。
如果你还记得我们关于体系结构的第一篇文章, 我们将整个模拟委托给了nBodySimulator。
WebVR第5部分(设计和实现)

文章图片
网络工作者的帖子显示, nBodySimulator具有一个step()函数, 每33ms模拟一次。 step()调用calculateForces()来运行我们的O(n2)WebAssembly模拟代码(第3条), 然后更新位置并重新绘制。在上一篇创建画布可视化的文章中, 我们从这个基类开始使用canvas元素实现了这一点:
/** * Base class that console.log()s the simulation state. */export class nBodyVisualizer {constructor(htmlElement) {this.htmlElement = htmlElementthis.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, 就会知道它会创建树状的分层结构。
WebVR第5部分(设计和实现)

文章图片
平面” 场景图” 。
网页设计中隐含的是查看者从” 正面” 进行查看。从侧面看, 将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="http://img.readke.com/220518/14551KK4-3.jpg"> < /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图元。
WebVR第5部分(设计和实现)

文章图片
平面网络浏览器中的A帧场景。
首先, 请注意, 我们正在通过平面网络浏览器查看场景。右下角的小面具邀请用户切换到3D立体模式。
WebVR第5部分(设计和实现)

文章图片
虚拟现实中的A帧场景-每只眼睛一张图像。
从理论上讲, 你应该能够:
  1. 在手机上打开
  2. 抬起手机面对面
  3. 在新现实的辉煌中欢欣鼓舞!
如果没有VR耳机的精美镜头, 我永远无法做到这一点。你可以在便宜的价格(基于Google Cardboard的基本设备)上为Android手机获得VR耳机, 但是, 对于开发内容, 我建议使用独立的HMD(头盔显示器), 例如Oculus Quest。
就像潜水或跳伞一样, 虚拟现实是一项齿轮运动。
虚拟现实设计师学习” 悬崖”
WebVR第5部分(设计和实现)

文章图片
欢迎来到重力与光线的舒适现实。
请注意, A帧Hello World场景具有默认的照明和摄像头:
  • 立方体的面是不同的颜色-立方体是自阴影的。
  • 立方体在平面上投下阴影-有定向光。
  • 立方体和平面之间没有缝隙-这是一个有重力的世界。
这些关键提示会向观看者说:” 放轻松, 这东西在你的脸上是完全正常的。”
另请注意, 默认设置在上面的Hello World场景代码中是隐式的。 A-Frame明智地提供了明智的默认设置, 但请注意-平面设计人员必须交叉的摄像头和照明设备才能创建VR。
我们将默认照明设置视为理所当然。例如, 按钮:
WebVR第5部分(设计和实现)

文章图片
注意这种隐式照明在设计和摄影中的普及程度。甚至” 平面设计” 按钮也无法摆脱网络的默认照明-它向右下方投射了阴影。
设计, 交流和实现照明和摄像头设置是WebVR设计师的学习重点。 “ 电影的语言” 是文化规范的集合, 表现为不同的相机和照明设置, 可以将故事情感地传达给观众。负责在场景周围设计/移动灯光和相机的电影专业人士是握把部门。
回到我们的虚拟现实 现在, 让我们重新开始工作。我们的天体WebVR场景具有类似的模式:
< !DOCTYPE> < html> < head> < script src="http://img.readke.com/220518/14551KK4-3.jpg"> < /script> < script src="https://unpkg.com/[email  protected]/dist/aframe-event-set-component.min.js"> < /script> < script src="http://www.srcmini.com/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” 的单击 除了” 碰撞” , 我们还可以使用” 投影” 。我们可以在场景中投射一条线, 然后看一下它的触感。最常见的示例是” 传送射线” 。
传送射线描绘了世界上的一条线, 以显示玩家可以移动的位置。这个” 投影” 寻找着陆的地方。它在投影的路径中返回一个或多个对象。这是一个传送射线示例:
WebVR第5部分(设计和实现)

文章图片
虚幻引擎默认内容中的传送射线。
请注意, 射线实际上是作为指向下方的抛物线实现的。这意味着它自然地与” 地面” 相交, 就像抛出的物体一样。这自然也设置了最大的隐形传送距离。限制是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.jsimport { nBodyVisualizer, nBodyVisWebVR } from ."/nBodyVisualizer"import { Body, nBodySimulator } from ."/nBodySimulator"window.onload = function() {// Create a Simulationconst sim = new nBodySimulator()// this Visualizer manages the UIsim.addVisualization(new nBodyVisWebVR(document.getElementById("a-bodies"), sim)) // making up stable universes is hard//namecolorxyzmvzvyvzsim.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 simulationsim.start()// Add anothersim.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 = simthis.nextId = 0}resize() {}

在构造函数中, 我们保存A-Frame集合, 为凝视事件设置全局变量以查找模拟, 并初始化一个ID计数器, 以用于在模拟和A-Frame的场景之间进行匹配。
paint(bodies) {let i// Create lookup table: lookup[body.aframeId] = bodyconst lookup = bodies.reduce( (total, body) => {// If new body, give it an aframeIdif (!body.aframeId) body.aframeId = `a-sim-body-${body.name}-${this.nextId++}`total[body.aframeId] = bodyreturn total}, {})// Loop through existing a-sim-bodies and remove any that are not in// the lookup - this is our dropped debrisconst 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 sceneaSimBodies[i].parentNode.removeChild(aSimBodies[i]); } }// loop through sim bodies and upsertlet aBodybodies.forEach( body => {// Find the html element for this aframeIdaBody = 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)}// repositionaBody.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 HTMLAFRAME.registerComponent('debris-listener', {init: function () {// Helper functionfunction rando(scale) { return (Math.random()-.5) * scale }// Add 10 new bodiesthis.el.addEventListener('click', function (evt) {for (let x=0; x< 10; x++) {// name, color, x, y, z, mass, vx, vy, vzwindow.sim.addBodyArgs("debris", "white", rando(10), rando(10), rando(10), 1, rando(.1), rando(.1), rando(.1))}})}})

该A帧组件的作用类似于” 点击” 侦听器, 可以由凝视光标触发, 以将10个新的随机物体添加到场景中。
总结一下:
  1. 我们使用标准HTML中的A-Frame声明WebVR场景。
  2. 我们可以从JavaScript中以编程方式添加/删除/更新场景中的A-Frame实体。
  3. 我们可以通过A-Frame插件和组件在JavaScript中使用事件处理程序创建交互。
WebVR:Veni, Look, Vici 我希望你能像我一样从这个技术演示中受益匪浅。在将这些功能(Web Worker和WebAssembly)应用于WebVR的地方, 它们也可以应用于浏览器边缘计算。
巨大的技术浪潮已经到来-虚拟现实(VR)。无论你第一次拿着智能手机时的感受如何, 第一次体验VR都会在计算的各个方面带来10倍的情感体验。距第一部iPhone才十二年。
VR已经存在了很长时间, 但是将VR带给普通用户所需的技术是通过移动革命和Facebook的Oculus Quest而不是PC革命来实现的。
互联网和开源是人类世界最伟大的奇迹之一。对于所有创建扁平化互联网的人们-我向你的勇气和冒险精神敬酒。
宣言!我们将创造世界, 因为我们拥有创造的力量。
WebVR第5部分(设计和实现)

文章图片
【WebVR第5部分(设计和实现)】画布演示, WebVR演示, 示例代码

    推荐阅读