ReactVR-入门手册-全-

ReactVR 入门手册(全)

原文:zh.annas-archive.org/md5/BB76013B3798515A13405091AD7CB582

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

从计算机的角度来看,虚拟现实自上世纪 60 年代以来就存在。它在 90 年代末再次大规模出现,然后在一段时间内基本上崩溃了——尽管它从来没有真正消失。现在它又回来了,而且这一次它是来留下的。

导致这种变化的是手机——在手机上使用的大型、高分辨率显示技术已经帮助创建了 HMDs(头戴式显示器,或 VR 护目镜)。电路和计算机的速度也比以前快得多;1998 年曾经花费 25 万美元的计算机图形现在不到两千美元,而且速度更快。

构建 VR 世界一直是困难的。然而,你必须是一名 C++程序员,并且对高速编程、实时图形、几何学和其他复杂主题有很多了解。在过去几年中,游戏开发引擎已经简化了这一点,但只是到了一定程度。

使用 React VR,这更加简单。你现在可以使用 React 语法编写 VR 世界,这是一种简单的声明性类 HTML 语言。如果你想创建一个盒子,你只需要声明一个具有正确宽度、高度等的盒子,而不是编写过程性代码。语法可能很简单,但这些世界可以是事件驱动的,动画的,并且对用户输入和从网络获取信息也是响应的。

这将使你能够用简单的 JavaScript 和类似 HTML 的代码构建复杂的虚拟世界。这使用了一种名为 WebVR 的新基于浏览器的编程范式;普通 PC 和移动设备上的浏览器现在可以在 VR 中查看世界。

你也可以做到这一点,这本书将向你展示如何做到这一点。

你需要为这本书做什么

你需要一台 Windows PC,几乎任何类型的都可以;为了获得最大的乐趣,你需要一个 VR 设备。可以是 HTC Vive、Oculus Rift、三星 Gear VR、Google Daydream 或其他 VR 护目镜(包括 Google Cardboard)和一部手机。

即使你没有复杂的头戴式显示器(HMD)或 VR 头盔,你也可以开发这些 WebVR 世界;你可以在普通计算机屏幕上以平面模式查看它们。你可以花不到 20 美元甚至在许多地方免费获得一个简单的 VR 手机支架/头盔(Google Cardboard 或类似产品),所以不要让硬件成为了解下一个伟大事物的障碍。

这本书是为谁写的

这本书适用于任何想要在网络上学习虚拟现实并通过 React VR 创建引人入胜的 3D 网站的人。如果你已经了解一些 JavaScript,那么你会更快地掌握这些知识,如果你已经了解 React 或 React Native,那么学习速度会更快。即使你不了解,这本书也会逐步向你展示该怎么做。如果你已经知道如何进行多边形建模,这会有所帮助,但这本书也会向人们展示如何使用免费开源的 Blender 进行一些基本建模,以及在哪里获取免费下载。你不需要 VR 设备就能享受这本书——你可以在普通 PC 上进行示例,并且甚至可以将其发布到互联网上。

约定

在这本书中,你会发现一些区分不同信息类型的文本样式。以下是一些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名都会以以下方式显示:“组件是真实的东西,不仅仅是标签或占位符,因为它们通过render()函数内置了展示自己的方式。”

代码块设置如下:

<Box
 dimWidth={4}
 dimDepth={1}
 dimHeight={9}
 lit
/>

当我们希望引起你对代码块的特定部分的注意时,相关行或项目会以粗体显示:

f:
mkdir f:\reactVR
cd \reactVR

任何命令行输入或输出都以以下方式书写:

npm install mersenne-twister --save

新术语重要单词以粗体显示。例如,屏幕上看到的单词,比如菜单或对话框中的单词,会以这样的方式出现在文本中:“一旦你分配了多边形,点击 View->Front,然后点击 Mesh->UV Unwrap->Cylinder Projection。”

警告或重要提示会以这样的框出现。提示和技巧会以这样的方式出现。

第一章:虚拟现实,到底是什么?

你正在阅读这本书来学习制作虚拟现实VR),但虚拟现实到底是什么?

这似乎是一个足够简单的问题,但答案却五花八门。大多数人认为 VR 意味着几乎真实或者另一个现实。

这才不是虚拟现实的含义。

我认为这是因为虚拟这个词可以有几种不同的含义。对于计算机科学家来说,虚拟这个词意味着模拟其虚拟化的东西。换句话说,虚拟硬盘假装是一个硬盘。

虚拟对象表现得像是真实的,但实际上并不是——通常情况下,它比物理对象更灵活,更容易控制、修改和支持。在许多方面,它比物理对象更好。例如,虚拟磁盘就像计算机磁盘一样。它可以存储数据。然而,这些数据可能存储在物理旋转磁盘、固态硬盘,甚至内存中。虚拟磁盘可以调整大小,而物理磁盘只能复制到更大(或更小)的磁盘上。虚拟磁盘更加灵活。

有些人认为虚拟意味着几乎。如果特斯拉开过,他们可能会说,“那几乎是无噪音的!” 人们知道它并不真的无噪音,但它比大排量 V8 引擎开过要安静得多。或者,那个人几乎是个圣人,关于他们喜欢的人。在这种情况下,它的意思是几乎或者几乎可以说是。

虚拟也可以意味着有美德的人。行为端正的人是有美德的,尽管这不是这个词的正常用法(应该是 virtuous)。这就是这个词的起源;在拉丁语中,virtualis 意味着力量或美德。然而,在我们的情况下,我们指的是一些看起来真实,但实际上并不是。

我认为这是关于虚拟现实的误解。人们认为它几乎是真实的。许多人认为 VR 还没有达到那个水平,因为它看起来几乎不像真实世界。在 VR 头盔中看到的景象要像真实世界还需要相当长的时间;其他感官,尤其是触觉和味觉可能需要相当长的时间才能被模拟出来。

然而,这不是重点;虚拟现实的重点不是它几乎真实。重点是,当你在其中时,它看起来真实,即使它看起来与现实毫不相似。

我会再次说这个,因为这是一个重要的区别。虚拟现实,或者说增强现实,不需要几乎真实,但当你在其中时,它会看起来真实(即使它看起来并不真实)。

在本章结束时,你会学到:

  • 虚拟现实是什么以及它是如何工作的

  • 一些虚拟现实的历史-它并不新,这项技术已经有 50 多年的历史了!

  • 用户代理-通过控制器与世界互动

  • 渲染硬件

  • 如何观看虚拟现实

  • 头戴式耳机的类型

虚拟现实是什么以及它是如何工作的

我们有许多感官。为了让我们感觉到另一种现实是真实的,我们需要利用这些感官来愚弄大脑。大多数虚拟现实系统利用了两种:视觉和听觉;触觉也被使用,但不是以完全伸手触摸某人的意义(尽管人们正在研究它!)。

Tor Nørretranders 收集了有关感官及其相对带宽的数据,以计算机术语来说,这有点像比较苹果和机油,尽管有用,但可以看出它如何应用于虚拟现实。

因此,我们可以看到,如果我们让你看到几乎真实的东西,我们可能能够说服大脑它是真实的。然而,简单地将一个视觉屏幕放在我们面前并不是完整的答案。

给予某人深度感知是大部分答案。

这是一个相当复杂的话题,但显示物体深度的主要方法是立体深度感知。还记得那些 ViewMaster 玩具吗?这是一个例子:

你放入一张左眼和右眼图像的光盘。左右眼的图像几乎看起来相同,但它们代表了右眼和左眼在那个位置看到的东西;由于视差,每个图像略有不同。从光盘上,我们可以看到左眼和右眼的图像。前面的 View-Master 中的透镜将你的眼睛聚焦在图像上。

你的大脑会看着这两幅图像,将它们融合成看起来真实的东西。这使用了一种称为立体深度感知的深度感知技术。

是的,View-Master 是早期的虚拟现实观看设备!

现在,这里真正发生了什么?立体声是如何工作的?

当你看东西时,透视和你的眼睛之间的距离会使你在看近处的东西和看远处的东西时以不同的方式聚焦你的眼睛。在这个图示中,黄色线显示我们对近处物体的视线,橙色线显示我们对远处物体的视线。请注意,黄色线之间的角度大于橙色线的狭窄角度。

一个友好的机器人借给我们她眼睛的下半部分来制作这张图片(这就是为什么它显示电路板)。你的真实眼睛构造有些类似;出于说明的目的,我省略了光线以及它们落在眼睛背面的位置。

你的大脑会根据黄线和橙线之间的角度差异自动判断你的眼睛是指向近处还是远处的物体。

这只是我们的大脑用来区分深度的一种方法。另一种对虚拟现实同样至关重要的方法是使用视差

视差指的是,不仅左右两个安卓眼睛指向不同(就像你的眼睛,当它们连接在你的头上时一样),而且每只眼睛看到同一物体的略微不同的视图。即使你只有一只眼睛,当你把头向左右移动时,这也会起作用,这是单视觉者感知深度的方式之一。

这是你的左眼看到的场景:

这是右眼看到同一物体的方式:

视差指的是,当用另一只眼睛观看时,更远的物体将比附近的物体更少向右/左移动,或者(一个延伸)当你把头从左到右移动时。我们的大脑(以及动物的大脑)会本能地将这些视为更近/更远。

红色的立方体要么在蓝色的立方体旁边,要么在绿色的立方体旁边,这取决于哪只眼睛看到了这张图片。你的大脑会结合这一点,再加上立方体在你左右移动眼睛时的移动方式,来给你一种深度感。

如果你是那个不感知 3D 电影的人群,不要绝望。它们严格依赖立体深度感知,不考虑视差效应;它们是预先录制的。

使用真正的VR(计算机生成或基于光场的 360 度视频),如果你移动头部,你会看到视差效应,VR 看起来就像拥有立体深度感知的人一样真实。

我有单视觉,因为我有一个近视眼和一个远视眼,而且 VR 对我来说效果很好。你的体验可能会有所不同,但如果你不喜欢 3D 电影,可以试试 VR(再说一遍,我真的很喜欢 3D 电影)。

视差深度感知会在你只有一只眼睛时起作用,当你把头从右到左移动时。

你的大脑还有一种额外的方法来确定物体的深度 - 焦点。 (实际上,除了列出的这些方法之外,还有许多其他方法,比如远处物体的蓝色移位,如山脉,以及其他效果)。在现实世界中聚焦于一个物体会使该物体和大致相同距离的其他物体看起来清晰,而更远和更近的物体会显得模糊。有点像这样:

当前的头戴式显示器无法准确显示焦点效果。你看到的是一个小屏幕,通常焦点固定在你前面约 5 英尺处。所有物体,无论远近,看起来都是一样清晰的,因为它们实际上只是显示在屏幕上。这可能会导致轻微的虚拟现实不适,称为住宿-聚合冲突。基本上,如果你聚焦于远处的立方体(鲑鱼色的那个),你的眼睛仍然会聚焦于鲑鱼立方体实际所在的位置,而你的眼球会以立体视觉的方式瞄准它应该在的位置。这种效果在非常近的物体上最为明显。

住宿-聚合冲突在靠近物体时最严重 - 因此尽量不要让任何东西,比如 GUI,位于用户位置太近的地方。这样可以减少晕动病。

这意味着你可能需要将 GUI 元素浮动到房间外,而不是让它们非常靠近。这可能会导致 UI 元素重叠。

虚拟现实设计是具有挑战性的。我期待你的设计!

在虚拟现实中使用立体声和视差

早在 1968 年,伊万·E·萨瑟兰就首次观察到具有立体深度知觉的物体,当用户的头部移动时似乎位于空间中(运动视差),看起来是真实的。

他和鲍勃·斯普劳尔开发的系统,通常被称为“达摩克利斯之剑”,只在空中显示了几条发光的线,然而:

“即使是这种相对粗糙的系统,三维幻觉也是真实的。” -伊万·E·萨瑟兰,AFIPS'68(秋季,第一部分)1968 年 12 月 9-11 日秋季联合计算机会议的论文集,第一部分:bit.ly/2urAV5e

在这种情况下,“真实”意味着尽管缺乏逼真的渲染 - 只是一个发光的立方体 - 人们仍然将其视为真实。这是由于立体渲染和视差效应。人们可以转动头部并稍微左右移动。

他们发明了第一款 VR 头显,或者头戴式显示器HMD)。

被广泛认为创造虚拟现实术语的人,贾伦·兰尼尔(Jaron Lanier)说:

“这是一种非常有趣的现实。它绝对和物理世界一样共享。有些人说,嗯,物理世界并不那么真实。这是一个共识世界。但问题是,无论物理世界有多真实 - 我们永远无法真正知道 - 虚拟世界也完全一样真实,并且达到了相同的地位。但与此同时,它还具有无限的可能性,这是物理世界所没有的:在物理世界中,你不能突然把这座建筑变成郁金香;这是不可能的。但在虚拟世界中你可以……[虚拟现实]给了我们这种感觉,让我们能够成为没有限制的自己;让我们的想象力变得客观并与其他人共享。” - 贾伦·兰尼尔(Jaron Lanier),SIGGRAPH Panel 1989,虚拟环境与互动性:未来之窗。http://bit.ly/2uIl0ib

一位名叫梅尔·斯莱特的研究人员对这个概念进行了进一步的研究,提出了进一步的术语“存在感”和“可信度”。有些人称这一切为沉浸。屏幕上的 3D 图像不如当你戴上 HMD 时,你的视野只能看到构建的 3D 世界那样引人入胜。即使渲染不像真实世界那样,你也会因音频和视觉线索而产生存在感。可信度意味着你所看到的有规则,并且即使不完全像真实世界那样,也能正常运作。

只能看到 HMD 中的东西,加上视差和立体视图,以及任何音频(如果做得好的话,声音非常重要),都会让你沉浸在 VR 世界中。有了所有这些东西,即使图形不真实,你也会感到沉浸其中,它变得真实起来。有关更多学术细节,请参阅bit.ly/2vGFso0,尽管我将在本节中对此进行更多解释。

这真的很有效。

虚拟现实不必看起来像现实,但它会看起来像是真实的。例如,看看游戏Quell4D

图形像积木一样,图像看起来一点也不像现实。然而,当古老的三根象鼻巨树巫师向你走来时,你会感到恐惧。它们看起来很真实。对你来说,当你玩游戏时,它们实际上是致命的真实,这意味着如果你不认真对待它们,你的(游戏中的)人物将会死亡。

虚拟现实中的火焰模拟会让大约 10%的人因恐慌而离开房间,即使火焰看起来一点也不像真正的火焰。

虚拟现实已经出现了。我们不必等到图形变得更好。很多人都这样说虚拟现实,但这是因为他们还没有尝试过,并且正在对它必须是什么做出假设。

跳进去,水很好!

因此,虚拟现实是一种看起来真实的东西,而不是一定看起来真实的东西(但如果看起来真实会更有帮助!)。

你不必等到更好的图形出现。

虚拟现实为什么会起作用,即使它看起来并不 100%真实?

我们的眼睛可能是向我们展示世界构成的最重要的感官。如果我们用这些图像替代图像并让某人沉浸其中,它们会开始变得真实。当你第一次进入虚拟现实时,你的最初反应是,“那看起来并不真实”,但是通过一个良好的虚拟现实设置,你会达到一个程度,你会认为“哇,那是真实的”,即使你知道你看到的基本上是一个电脑游戏。

快速的帧速率(显示速度)和足够的分辨率会欺骗你的大脑,让你认为你所看到的视觉上真的存在。这是一个强大的效果,大多数人在沉浸在这样的图像中时会有,但并非所有人都能看到 3D 电影(即使视力正常的人也不是每个人都能看到)。

事实上,现实感是如此强烈,以至于人们可能因观看 VR 而感到恶心。这是因为你的眼睛可能会说那是真的,但你的其他感官,比如你的内耳,会说我们并没有跳 10 英尺高。如果你的眼睛认为你在空中弹跳,而你的腿部肌肉(本体感觉)说你在地面上,你的皮肤说你感觉不到风,你的内耳说你在向前飞行时没有倾斜,你的大脑会在非常深层次上感到困惑。

当你的感官强烈不一致时,你的身体会有一种防御机制。它会认为你中毒了;因此,你的身体会感到恶心,甚至可能会生病。你的身体担心你的眼睛看到的与身体其他部分感觉到的不一样,所以它可能会试图排除你胃里的一切,以防你吃的东西中毒了。

是的,这并不好玩;不同的人会有不同的反应。

然而,并非所有的 VR 都会产生这种效果!一般来说,制作不良的 VR 会让你有这种感觉。关于这种效果已经有学术论文写成。本书将把这些讨论总结成几条简单的规则,让你的 VR 对人们来说更加舒适。

虚拟现实的另一个重要方面是,它是你可以与之互动的东西(现实本身)。这带来了机械困难;并不是每个人都拥有 3D 控制器。我们在用户代理-通过控制器与世界互动部分进行了讨论。真正的虚拟现实可以进行互动,即使只是简单的凝视检测-看着某物(凝视)然后事情发生-移动发生,你被传送,动画播放。

其他类型的 VR;AR,XR,SR/FR

还有另一种图像,有时被称为 VR,那就是360 视频。有特殊的视频播放器可以记录所有方向。复杂的软件会将不同的摄像头输入拼接在一起,制作成一个视频流,播放软件会将其投影到你周围。当你转头时,你似乎改变了在拍摄的世界内的视角。就好像你在现实世界中,随心所欲地四处张望。

360 视频看起来可能比大多数计算机生成的 VR 更好,但对我来说,它并不是现实,因为在最好的情况下,你只是一个无形的幽灵。当然,世界看起来很棒,但你无法伸手触摸事物,因为它已经被拍摄了。360 视频和类似的系统超出了本书的范围。话虽如此,我认为 360 视频肯定是一种有效的艺术形式,值得追求-只是本书没有涉及。

请理解 - 我并不是贬低 360 度视频,只是因为它不是真正的虚拟现实。(像最后一个词加上引号那样发音)。360 度视频可以是非常温暖、激烈、情感丰富的戏剧。你确实会感受到一丝存在感,视觉效果令人惊叹。随着更多人熟悉它并解决细节问题,我们应该看到更多令人惊叹的艺术作品。

我提出一个新术语用于 360 度视频;拍摄现实FR)或环绕现实SR)。(尽管这些实际上没有人用胶片,但“拍摄”一词仍然意味着通过镜头记录某些东西,但也许 SR 更好。你来选择!)

还有其他类型的虚拟现实。有很多人使用短语XR,意思是(任何)现实;主要指增强现实和虚拟现实。什么是增强现实?

头戴式显示器(HMD)由一些小型显示器和复杂的光学器件组成,当你戴上头盔时,可以看到立体的 3D 图像。大多数虚拟现实头盔在你使用时会故意屏蔽其他世界,以进一步让你沉浸在虚拟现实中。虽然这是虚拟现实的一个重要组成部分,但还有一种称为增强现实(AR)的虚拟现实类型,其中虚拟现实物品通过戴一种透明的 HMD 投影到现实世界中。有许多制造商,尽管微软的 Hololens 可能是最知名的。还有游戏Pokemon Go,这是一种增强现实。人们举起手机,显示在现实之上叠加的图像。这不是头戴式显示器,但仍然是增强现实。现实世界已经被 Pokemon 世界增强。

虚拟现实系统也可以是世界之窗系统,尽管今天通常不被称为虚拟现实。换句话说,一个真实的、持久的 3D 世界,你坐在键盘前通过屏幕观看。在上一波虚拟现实浪潮中,这被称为虚拟现实,尽管今天它已经很普遍,人们不再称之为虚拟现实。你可能听说过魔兽世界

这是一种虚拟现实;虽然它通常不是 3D 的,但它是一个持久存在的世界在另一个现实中。它也是一个完整的 3D 世界,你可以通过屏幕看到;屏幕将你带到一个虚拟现实中,所以它类似于世界之窗系统(尽管没有头部跟踪)。

观看电影可以被视为一种有效的虚拟现实形式;你被带到另一个世界,短暂的时间里,感觉自己沉浸在故事中。电视是一种虚拟现实。

事实上,对 VR 术语的第一次使用是指戏剧。虽然今天许多人会说这不是虚拟现实,但他们大部分时间都在观看其他现实,而不关注坐在他们旁边的人。这难道不是虚拟现实吗?你对《与星共舞》很着迷,但你认识他们吗?他们在虚拟现实中是真实存在的。

然而,这并不是大多数人所认为的。本书将使用现代(2014 年以后)对虚拟现实的解释,即通过 VR 头盔或某种 HMD 查看的东西。如今,VR 这个词通常意味着头盔或 HMD,并且经常与某种形式的手柄配对。现在,对消费者来说,有很多好的、有效的 HMD 可供选择。现在是对 VR 感兴趣的好时机。

WebVR 的好处在于,我们可以在没有头戴式显示器的情况下,通过浏览器仍然可以看到这些虚拟现实世界;这对于测试和没有硬件的人来说非常方便。

WebVR 非常包容。

虚拟现实的历史

大多数人也认为 VR 是相当新的,但实际上它已经存在很长时间了,我指的是传统类型的带头盔的 VR。第一个 HMD 是由伊凡·萨瑟兰和鲍勃·斯普劳尔在 1968 年创建的。由于当时的技术,它又大又重,因此悬挂在研究室的天花板上。它只显示线框图像。由于它的大小,它被称为达摩克利斯之剑。它展示了一个简单的线框世界。当时的计算机速度不够快,无法显示比一些发光线更复杂的东西。

在 90 年代末,个人电脑开始足够快,可以显示 3D 世界,于是出现了新一波的 VR。我参与了这些努力;我当时正在为 CompuServe 开发一个 3D 环境,那时是一个热门的地方。

你可以去商场参与,通过昂贵的 HMD,在线上与最多四个人共享虚拟世界。这被称为基于位置的娱乐,因为这些系统又大又昂贵。如今,你也可以去 VR 游乐场,尝试硬件,但如今 VR 的令人兴奋之处在于,许多这些系统对家庭爱好者来说非常实惠。

用户代理-通过控制器与世界互动

HMD 并不是一切,尽管它确实是最重要的部分。能够看到虚拟现实世界很棒,但在某个时候,你希望能够与之互动。如果世界是静态的,你会感觉自己像一个游荡的幽灵。只有当你能够与世界互动时,才算是虚拟现实。

最终,类似全身触觉反馈和身体跟踪的东西,再加上复杂的软件,将使我们能够伸手触摸虚拟世界。这是未来值得期待的事情。

目前,我们通常通过各种手持控制器与世界互动。不同的控制器具有截然不同的功能和要求。高端(但仍然面向消费者)VR 设置,如 Rift 和 Vive,控制器的工作方式与移动 VR 控制器大不相同。我们将先讨论高端系统,然后再讨论移动 VR 控制器。

PC、Mac 和 Linux 的高端控制器

对于 PC VR,比如 HTC Vive 或 Oculus Rift,控制器给予了与虚拟现实世界互动的非常重要的能力。这些控制器在 3D 空间中被跟踪,以便软件知道它们的位置。开发人员可以编写代码,使其看起来像手、枪等。这使你能够伸手触摸周围的世界——这对于使你与之互动的虚拟现实非常重要。

为了实现这一点,Oculus 和 Vive 控制器都需要外部跟踪硬件。对于 Vive 来说,这些是放置在 VR 区域角落的光线或 VR 基站。(这里有一个图表,可以在bit.ly/VIVEManual找到)。这些小巧的不显眼的立方体发出红外跟踪信号,控制器和头戴设备接收并用于准确定位它们在真实的 3D 世界中的位置。对于 Rift,还有两到三个传感器来跟踪设备,以给出它们在真实世界中的位置:

基站和跟踪硬件对于 HMD 本身也非常重要。

你真实世界位置的跟踪(你的真实头部/手)是使移动、转动头部、移动手/控制器看起来真实的原因,因为头戴设备和控制器在真实的 3D 空间中被如此精确地跟踪,一旦软件向用户显示了 VR 世界,任何头部运动都会看起来真实。

实际上,这意味着 PC 控制器看起来就在你看到它们的地方。我在科技演示中第一次体验 HTC Vive 时感到惊讶——我戴上 HTC Vive 头盔,在虚拟世界中看到控制器在我面前。我以为我会摸索一番,直到控制器在我想象的位置。我伸出手,我的手指确切地感觉到控制器在我眼睛看到的地方——通过 HMD。

我着迷了!虚拟世界真的是虚拟现实!我看到的幻影控制器真实,尽管我知道我看到的是我脸前的一个小屏幕。

它们是如何工作的?

HTC Vive

HTC Vive 使用两个小方块,称为基站灯塔,位于它们覆盖的区域的对立面。它们发出红外线束,覆盖 120 度;这意味着如果它们在一个角落,它们可以离角落几英寸远,仍然覆盖墙壁(否则,你就得在墙上挖个洞,把灯塔放在正确的位置!)

通常,你会在房间的对立面安装两个基站,相距约 16 英尺或 5 米,并且在头顶高度上方,大约 2 米或 6 英尺 6 英寸。如果你个子更高,也可以安装得更高!

基站也可以用合适的适配器或自定义支架安装在麦克风架上。并不是每个人都有一个大客厅,所以这些安排可能有助于安装。

Vive 也可以以坐姿配置使用,尽管真正的重点是所谓的Room Scale

Room Scale VR 意味着你可以在虚拟现实世界中四处走动,就像在现实世界中一样。不需要传送或其他技巧。当然,区域需要清空家具,这是虚拟现实的一个问题;并不是每个人都有一个可以清空的大房间。

如果你离得太近,Vive 会通过显示边界或保护来保护你的安全。

如果是墙或其他区域,使你的房间边界略小于实际房间。如果是沙发或椅子,你可以走到椅子的尽头。

我们这样做是为了避免你撞到墙。如果你站在墙边,但在虚拟世界中看不见墙,挥动手臂,你的手不会穿过真实的墙!

靠沙发边缘是不错的,因为你的小腿会碰到沙发,而不是你的手碰到墙。实际上这并不是什么大问题,因为你会在靠近之前看到防护装置。注意 Vive/Steam VR 教程!

HTC Vive 通过一些惯性测量单元IMUs)工作,这些单元可以检测 HMD 以及控制器的位置。这些 IMU 会漂移,所以基站有一个红外线束在房间里扫过。当控制器、跟踪器或 HMD 检测到这些光束时,它们会重新校准自己。这种重新校准是完全不可察觉的。这种系统的优势在于,即使一个控制器从一个基站或光房的视线中消失,VR 系统仍然知道该物品的位置和指向。

总体效果是精准和存在感,尽管主要效果是稳定性。如果你交叉双手,一个控制器短暂地从基站的视线中消失,控制器不会失去锁定。

尽量不要把你的 VR 空间放在有很多窗户或镜子的地方。

红外线束可以反射,导致不稳定。

Oculus Rift

Rift最初只是一个头盔,没有控制器。它最初的基站是两个摄像头,你可以把它们放在桌子的左右两侧;它们指向 HMD,并用于将其定位在世界中。

不久之后,Rift 增加了第三个摄像头的功能;有了三个摄像头,你可以进行房间规模的 VR。它们的位置略有不同于 Vive;查看 Rift 文档以获得最佳位置。

小心处理电缆。当我写这本书时,Rift 的电缆直接插在 PC 的后面。如果你绊倒电缆,可能会猛烈地把它们从 PC 上拔出来,导致损坏。

Vive 有一个分机箱,所以如果你绊倒电缆,希望你能把它从盒子里拔出来。

不要绊倒电缆。

这本书的目的不是分析 Vive 或 Rift 比另一个好还是差;它们都大致以相同的方式工作,基站/摄像头帮助控制器和 HMD 跟踪它们的位置和旋转。以下是一个典型的设置:

在其中,Vive 基站安装在墙上;我们有一台台式电脑和一个 VR 用户观看一个 3D 模型,就像它是真实的一样。VR 用户手持两个 Vive 控制器;虚拟图像手持一个类似 Xbox 的游戏控制器。

这张图片还展示了 Oculus Rift 3 摄像头跟踪系统。它们是坐在屏幕左右两侧的浅灰色物品,以及沙发后面的橱柜上(就在我们面前)。

没错,这个人是使用者。她不需要 HMD;系统直接将视频传输到她的眼睛。虚拟对象是一个看起来坐在桌子旁的人。

前面的图是沙发前面的第三人可能看到的场景。

灯塔看到的实际上有点不同,但很有趣。它们实际上有一对红外线条,横扫视野,控制器看到这些线条在跟踪。当他们这样做时,控制器(和 HMD)将重新同步它们的惯性跟踪定位。这意味着即使一个控制器不在基站的视野范围内,它仍然保持跟踪,尽管你不希望控制器长时间隐藏。惯性跟踪系统会漂移。漂移的视觉问题是你的手臂似乎慢慢远离你的身体 - 这显然是非常令人不安的。Vive 灯塔和 Rift 摄像头可以防止漂移发生。Vive 灯塔投射的视角约为 120 度。如果灯塔有这个视野的摄像头,右后方的灯塔将看到以下内容:

你可以通过这个灯塔看到两个控制器和 HMD。然而,有一个问题。注意红色圆圈 - 左侧的大镜子实际上是一个巨大的电视,但它很闪亮。因此,灯塔红外线会反射在上面,控制器会感应到两束光:一束直接,一束反射。

这可能会导致 HMD 和你的视角跳动,或者你的控制器莫名其妙地移动。

在你的 VR 房间里避免闪亮的物体、镜子和窗户。

你可能需要拉上窗帘,甚至在电视、玻璃橱柜等上面铺上床单。

艺术需要牺牲!

从另一个灯塔,一个控制器被阻挡,但仍然通过其内部惯性跟踪和另一个灯塔 100%跟踪。

移动 VR

对于移动 VR,还有 Google Daydream 和三星 Gear VR 控制器。由于使用了更简单的硬件,使价格更合理,这些控制器并非完全 3D 跟踪。

由于移动 VR 缺少房间跟踪外部传感器,Vive 和 Oculus 都有,VR 控制器的跟踪就不那么精确。实际上它们看起来会一样真实,但会周期性地漂移。就好像你的手在没有你控制的情况下慢慢向右移动。因此,移动 VR 有一个重置控制器的按钮,可以将控制器移动到预定义的位置,比如靠近你的臀部。你的手可能是伸直的,但如果你按下 Home 按钮,VR 显示将会显示你的手现在在你的臀部。

这可能需要一些时间来适应。这种设置有一些优势;它更便宜,需要更少的外部硬件,而且世界上有更多这样的系统。然而,PC 硬件确实提供了更好的 VR 体验。

移动控制器的另一个问题是只有三个自由度DOF)。这意味着它们可以跟踪倾斜、偏航和翻滚,但不能跟踪位置;如果你把控制器平放在左边,在游戏中你的控制器根本没有移动。这就是为什么你不能用移动控制器抓东西。Vive 和 Rift 都有 6 个 DOF 控制器,所以你可以移动它们并抓取东西。

渲染硬件

为了避免 VR 晕动病,你需要一个快速的帧速率。什么是帧速率?这是你的电脑生成屏幕上图像的速度。当然,很多事情取决于场景的复杂性;显示一个立方体和一个盒子比显示洛杉矶市所有建筑物的速度要快得多。

当然,你在设计 VR 世界时可以控制这一点。

每个图像都必须实时生成。大多数 VR 头显都尝试达到 90 赫兹。赫兹指的是频率-每秒循环次数,或者在这种情况下,每秒帧数。

VR 的难点在于没有什么可以减慢这个帧速率。如果有什么东西需要加载,或者需要获取一个网页,如果你稍微减慢帧速率,人们会感到头晕。

加快帧速率有两种方法。一种是减少场景复杂度,另一种是使用快速的电脑。

电影《大白鲨》中的经典台词是当他们发现鲨鱼比他们预期的要大得多,并且撕毁了他们的船时。罗伊·施耐德说:“你需要一艘更大的船。”

观看 VR,你需要一台更大的电脑。

幸运的是,电脑变得越来越快。在这里我们构建的世界中,一个相当快的智能手机应该没问题。

场景复杂性有点两难;您希望拥有丰富、详细的虚拟世界,但您也希望该虚拟世界能够快速渲染。快速渲染意味着每秒 90 帧(更新),如前所述。您还需要了解硬件支持的目标受众。他们是否都使用配备数千美元视频卡的高端 PC?(有点过度;我在这里表达一个观点。)还是他们使用去年的手机型号,配备 10 美元的硬纸板盒和一些透镜?如果您了解潜在的目标受众,您可以开发一个能够在其系统上良好运行的 VR 应用程序。

美国海军陆战队有一句话:“训练就像你将要战斗一样。”在二战期间,他们练习了在南加利福尼亚海岸进行两栖登陆的作战行动。当他们在太平洋战争期间不得不执行这项任务时,他们没有计划过珊瑚礁。因此,他们制定了一个原则,即应该在与他们预期要在其中作战的相同或相似的环境中训练人员。

虽然良好的 VR 体验(希望)并非生死攸关,但这仍然是宝贵的建议。如果您认为大多数客户或您的 VR 应用程序的消费者将使用去年的手机,那么请使用去年的手机进行测试。如果您认为他们将使用高端 PC,请使用高端 PC 进行测试。

不要假设,如果您的 VR 应用程序运行缓慢,客户的计算机会好得多,一切都会没问题。获取与他们使用的类似的设备,然后您将在客户之前遭受恶心和眩晕,然后重新编码或简化场景以使其足够快速。

需要多少硬件?为此,您应该咨询您计划针对的头戴式显示器的最低规格。由于这可能会发生变化,我不会在本书中总结它,但不同 VR 制造商提供的指南是很好的建议。

您可能需要更大的 PC(或手机);这是成为早期采用者所付出的代价!

如何查看 VR?

要查看 VR,您需要某种类型的头戴式显示器或 HMD。在过去,VR 还以 2D 屏幕上的 3D 图像为特征。实际上,当时的 VR 意味着使用任何设备查看的任何 3D 程序 - 基本上就像正常情况下坐在 PC 前一样,但这并不是真正沉浸式的体验。今天,VR 意味着使用 HMD/头戴式显示器;因此,要查看 VR,您需要一个头戴式显示器。

具有讽刺意味的是,React VR 在浏览器中作为一个 3D 世界也可以正常运行,并且可以用来制作具有视差效果的网页,尽管这有点过度。

VR 可能是危险的

你可能会认为,这很安全。然而,一个 VR 头戴设备附带了 33 页的警告。请阅读它们

大多数警告都是常识,例如,如果你在靠近物体或人的地方,不要挥舞你的手。戴上这种类似眼罩的东西,你可能会真的打到自己的手。从哲学上讲,我不相信有人会过分干涉,但你确实可能会在 VR 中受伤。想象一下,如果有人给了你一个眼罩,告诉你戴上它,然后在你家里四处走动。你可能会感到有点不舒服。

这大致是我们在这本书中要做的事情,只是增加了一个你将在一种惊奇和兴奋的状态中四处游荡的细节。有很多 YouTube 视频显示人们撞墙,撞到墙上,打翻灯具等等。他们看起来很傻,但当你戴上头戴式显示器时,你完全沉浸在虚拟世界中,不会想要控制自己的力量。因此,请确保你清空房间,并警告朋友不要进来。

这也包括你的毛茸茸的朋友。很难让宠物远离你的 VR 区域,但这是个好主意,因为它们不会明白 HMD 让你分心,而且你看不见它们。如果可以找到方法,最好防止它们在你脚下乱跑,否则你可能会无意中踩到它们。

VR 是安全的;请负责任地使用。

VR 头戴设备选项

在 WebVR 中,有一些选择。我将保持这些最简单、最常用的头戴设备。你当然可以使用开源虚拟现实OSVR),这实际上是一个硬件平台,但你需要弄清楚要使用什么浏览器等等。一些术语,比如凝视,稍后将在 UI 部分介绍。目前,凝视移动意味着你需要盯着某物才能移动到那里或选择一个对象,通常是这样的。

以下是各种主流的 WebVR 选项(您可以在webvr.info/上阅读相关信息):

类型 控制 移动 成本
Gear VR(移动) 1 个手持设备,头戴式显示器 凝视/触摸板 中等
Daydream VR(移动) 1 个手持设备 凝视/触摸板 中等
Cardboard/其他头戴设备 无(可以点击) 凝视选择
HTC Vive 跟踪,2 个控制器 四处走动
Oculus Rift 2 摄像头 键盘/游戏手柄 凝视选择
Oculus Rift 3 摄像头 跟踪,2 个控制器 可四处走动

头盔类型

广义上来说,它们可以是连接 PC 的头盔或移动耳机。一些独立式头盔,如 Hololens 或 Vive 独立式 VR 头盔,包括一个完全工作的 PC,因此它们实际上更像是移动耳机,但不需要 PC。

移动耳机

移动耳机实际上只是利用你的手机来显示数据并将你置身于 VR 世界。因此,性能完全取决于你的手机能做什么。

这是一个时候,更大真的更好。

不过有一个限制;有一些头盔使用平板电脑,但它们非常重,并且实际上没有比较小的移动设备更多的优势。

当你使用移动耳机时,你会遇到电池寿命、重量和控制问题。市场上有各种各样的 VR 控制器,还有捆绑选项,如三星 Gear VR 和 Google Daydream,其中包括一个控制器和一个手机壳。

这些捆绑包的好处是,手机通常经过认证可以正常工作,软件也很容易使用。你可以自己组装 VR 头盔/控制器组合。

移动耳机也可以简单到只是一个带有一些镜片的盒子,尽管在光学尺寸和细节方面实际上有很多数学。最常提到的是 Google Cardboard;谷歌不直接销售它们,但公司可以实现 Cardboard 观看器。还有非官方的 Cardboard 以及一些价格合理的更好的支架,可以放入手机。

一般来说,它们大多没有传感器。一些有一个小杠杆会触摸屏幕,除了移动之外还允许一些控制。

你也可以购买单独的蓝牙控制器,尽管它们很可能没有三维定位。我们在第十一章中介绍了不同类型的控制器,走进未知领域

一些 VR 头盔适合戴眼镜,而一些则不适合——很大程度上取决于你的脸部大小、你使用的眼镜大小以及你的视力问题。我一个近视眼和一个远视眼,都不需要戴眼镜(任何一只眼睛都不需要!),但你的情况可能不同。我强烈建议在购买前试戴一下头盔,或者从有良好退货政策的商家购买。

在移动头戴式耳机的高端产品中,有三星 Gear VR 和 Google Daydream。它们提供了一个精心制作的头戴式耳机,你可以把手机放进去,还有一个独立的控制器。

控制器是最重要的部分(尽管头戴式耳机也很值得,因为它们比最好的 Cardboard 观看器舒适得多)。Daydream 和 Gear VR 捆绑包中的控制器都是蓝牙无线的,并且可以进行一定程度的跟踪。

它们内置了传感器来检测运动,但在空间中并不是精确定位的。因此,它们上面有中心按钮。这是因为这些单位中的三维定位传感器类型会随着时间漂移。在虚拟现实世界中,你的手/控制器/枪(或者控制器的任何视觉表示)似乎会从你身边漂移,甚至移动到你的身后!这可能会让人感到非常不安。如果/当这种情况发生时,只需使用适当的按钮重新校准你的控制器。

高端 PC 设置有不同类型的跟踪,通常不需要重新校准。但是,请注意它们需要初始校准/设置,并且也可能出现跟踪问题。

关于 GearVR 的一些注意事项。我在使用 GearVR 时犯了一些小错误。有一个额外的弹性带,我以为是用来松弛的;其实不是。他们告诉你要戴上这些带子,但忽略了这个额外的带子是用来固定控制器的。在戴上头戴式耳机的带子之前,先翻到控制器部分。

实际上,控制器应该是你首先要摆弄的部分。你需要配对它并进行一些下载,而这在戴着头戴式耳机时是做不了的,所以先做这部分。

PC、Mac 和 Linux 头戴式耳机

大多数人认为选择 PC 头戴式耳机将在 HTC Vive 和 Oculus Rift 之间进行,但实际上有数十甚至数百种 PC 类型的头戴式耳机。

它们的性能都取决于你的 PC 的性能。朋友们,这就是 Mac 有点劣势的地方;你需要一张快速的显卡,而 Mac 通常对于图形和一些游戏来说速度足够快,但不适合虚拟现实。然而,苹果已经推出了准备好虚拟现实的 PC。当你决定用哪个平台进行 React VR 时,请考虑这一点。

就目前而言,Mac 对 Oculus Rift 或 HTC Vive 的支持充其量是试验性的,因此这些步骤和示例将假定您正在使用 PC。Linux 对几款头戴设备承诺提供支持,但就目前而言,这是试验性的。如果您使用 Linux,您需要查阅文档和/或尽量按照 Windows 示例进行操作。

大多数 React VR 演示的几何图形比许多虚拟现实世界简单,因此它们可以在相对较小的硬件上运行。请查看您头戴设备的制造商的最低要求;不要认为您可以用低于最低要求的硬件。否则您会感到不适或体验不舒适。

在整个市场中,我们将在本书中介绍的头戴设备实际上只有两款:HTC Vive 和 Oculus Rift。如果您有其他头戴设备,示例应该可以正常运行,但您可能需要稍微调整一下。

一般来说,PC、Mac 和 Linux 头戴设备将与 Firefox 或实验性浏览器Servo.org兼容。实验性版本的 Chrome(Chromium)也可能可以用于查看 WebVR。请在webvr.info上查看完整的最新列表。

总结

在本章中,我们已经涵盖了虚拟现实,为什么它有效,以及它真正是什么(故意的双关语)。我们还介绍了如何通过简要概述虚拟现实硬件和软件进入虚拟现实世界。

请记住,即使您没有昂贵的 HTC Vive 或 Oculus Rift,您仍然可以在台式电脑上查看 WebVR。

接下来,我们将介绍如何在非常高的层面上编程虚拟现实。有许多不同的构建虚拟现实软件应用程序的方法,我们将介绍这些不同的方法,以及它们的优缺点。您将阅读有关不同软件包的概述,以及 WebVR 的优势。由于本书是关于 WebVR 的,我们将介绍安装 React VR、Node.js 和其他工具,开始创造您自己的现实 - 真的!

第二章:Flatland and Beyond: VR 编程

在上一章中,您了解了 VR 是什么以及它可以成为什么样子。程序员和开发人员(就像你)是如何创建这些虚拟世界的?我们发现这是一件难事。我们必须保持快速的帧率和正确的立体渲染。我们如何快速而轻松地做到这一点?继续阅读,找出答案。

在本章中,我们将涵盖以下主题:

  • HTML 和常见的编程方法,如 Node.js,JavaScript 和游戏引擎

  • React 库

  • 图形库,用于显示 2D 和 3D 图像

  • 如何安装所有这些软件,以便我们可以开始编程

HTML 和超越 2D 互联网的方法

在 Web 发展的同时,早期的 HTML 语言发生了巨大变化。一个良好的网页体验通常不仅仅涉及 HTML。增加更多互动性的一种方式是通过 JavaScript。HTML、XML 和 JavaScript 的组合是构建 Web 的重要部分,包括 Google 文档或在线 Microsoft Word 等应用程序(也是免费的)。

然而,这些都是平面的。要进入第三维度通常需要高速软件,通常是用 C++编写的。随着计算机变得越来越快,图形处理单元(GPU)已经接管了实际 3D 生成的大部分工作,用于描述 3D 游戏的语言也在不断发展。

目前有许多编程 VR 的方法。在 Rift 和 Vive 显示的分辨率下生成每秒 90 帧是具有挑战性的,因此大多数 VR 编程是用高速语言进行的,这些语言直接面向硬件或低级,如 C 和 C++。然而,游戏引擎,如 Unity、Unreal 或 Cryengine,可以为你做很多工作。

起初,你可能会想“为什么我要使用游戏引擎?我不是在写游戏”。更普遍地说,这些引擎是为游戏而构建的,但不一定只能构建游戏。现代游戏引擎处理渲染(我们需要的)、物理学(我们需要这个来构建逼真的世界)、地形(用于户外场景)、照明(用于复杂的渲染)、人工智能(用于填充我们的世界)、网络(用于构建多用户环境)和其他代码。这些都不一定是游戏特定的,尽管各种游戏引擎都有更适合游戏而不是企业数据可视化的命名约定。例如,在 Unity 中,一个基本的 3D 对象被称为GameObject。所以即使你不是在写游戏,你也会有GameObject

目前,VR 软件的主要竞争者是:

许多这些游戏引擎也适用于移动平台。使用游戏引擎的优势在于你可以“一次编写,到处运行”,这意味着大多数游戏引擎都支持移动设备和 PC。基本上,你构建一个 PC 应用程序,然后更改构建设置并构建移动应用程序。现在你有了每个平台的两个或更多不同的应用程序。

使用游戏引擎可能存在相当陡峭的学习曲线,尽管这仍然比编写自己的渲染代码要容易。你确实需要构建一个完整的应用程序,这可能令人望而生畏。

与当前的网络编程技术相比,人们只想描述他们想要看到的东西,而不是编写服务器端代码将网页发送到手机,也不是编写自定义应用程序来下载信息并显示它。

那么,为什么 VR 需要你这样做呢?

使用 React VR,你不需要这样做。

你可以用 JavaScript 构建你的世界,而不是学习游戏编程引擎。你可以使用声明性组件构建 VR 世界和 UI,而不是构建渲染代码。实际上,你可以在更高的层次上描述你的 VR 世界中有什么,而不是一次一个像素地构建这个世界。这听起来不是更有趣吗?

Node.js 和 JavaScript 的背景

大声说出 Node.js。祝你健康!

Node.js 是一个开源系统,用于在服务器端使用 JavaScript。当然,这是 Web 浏览器执行代码的主要方式。它是在 Web 早期的时候发明的,有几个原因。

React 和 React VR 大量使用 JavaScript。要将 React 网页呈现到浏览器,需要服务器端 JavaScript,这意味着 Web 服务器不仅仅是将文件发送到浏览器,而是在服务器端执行代码。Node.js 允许您使用与浏览器相同的语言编写服务器端代码。对于全栈开发人员来说,这是理想的,因为您可以沉浸在一种语言中。

使服务器 React

React VR 基于 React,这是一个允许通过声明而不是编程构建 Web 页面和交互式用户界面的框架。您为应用程序中的每个状态构建视图,然后 React 将使用正确的组件来显示该应用程序。

声明式视图使您的代码更容易、更健壮,更容易修改和调试。

组件使用了封装的面向对象概念,这意味着它们是自给自足的,并管理自己的状态。然后,您可以使用这些组件来创建复杂的用户界面。

React 允许开发人员创建随时间变化的应用程序,而无需不断刷新浏览器页面。它使用了模型-视图-控制器设计模式/模板,并且可以与其他 JavaScript 库(如 Angular.JS)结合使用。

React 首次在 2011 年与 Facebook 的新闻订阅中使用。它在 2015 年 3 月开源。

您可以在facebook.github.io/react/找到有关 React 的更多详细信息。

图形库-OpenGL 和 WebGL

本节涵盖了一般的 3D 编程,但需要讨论一些不同的事情。

OpenGL是一种显示图形的标准。不涉及 PC 与工作站政治(现在已经是古老的历史),它是一个工作站供应商(SGI)开创的标准,用于标准化计算机图形和程序显示图形的能力。

还有其他 API,比如由微软支持的 DirectX,许多 PC 游戏开发人员、CAD 软件和其他 PC 计算机图形都在使用。

OpenGL 严格来说不是开源的;然而,该软件可以在不支付版税的情况下使用,并且有文档和免费提供(公平地说,DirectX 也是如此)。

基本上,OpenGL 是软件显示图形的一种方式。在这种情况下,软件通常指的是 C++(或其他可以调用本地库和操作系统实用程序的语言)。

Vulkan 基本上是 OpenGL 的预期继任者。它比 OpenGL 更低级,并且提供了更多的并行任务处理能力,并直接利用大多数智能手机和 PC 中的 GPU 的能力。由于它是一种低级格式,你会听到更多关于 Vulkan 在计算机图形方面的讨论,而在 Web 图形方面的讨论较少。与 OpenGL 一样,它通常由编译的本地模式软件(C++等)使用。

WebGL是一个 JavaScript API,用于在 Web 浏览器中渲染 3D 图形,无需插件。由于 OpenGL 的创建者 SGI 已经不再经营业务,现在由 Khronos 集团支持、定义和推广 OpenGL 和 WebGL,这是一个非盈利的、由成员资助的联盟。WebGL 可以通过 JavaScript 或其他浏览器支持的语言使用。

three.js是一系列使 WebGL 编程更容易的 JavaScript 文件。然而,它是一个相当大的下载。

React VR 是基于 three.JS 和 React 构建的。

A-Frame是另一个 WebGL 前端;它与 React VR 有类似的概念,意味着它是声明性的、高级的,并且基于 three.js 构建。你不必创建点并将它们连接起来以制作一个立方体;你只需声明一个立方体并给它一个位置、颜色等。虽然本书主要涵盖了 React VR,但它们之间有一些区别。

  • React VR 应用程序是用JSX编写的。这是一种允许类似 HTML 的标记混合到 JavaScript 代码中的语法。React VR 基于 React 和 React Native。如果你已经了解 React,你可以很快学会 React VR,底层概念是相同的,所以你会感觉很自然。

  • A-Frame 应用程序使用 HTML,带有自定义 HTML 标记。它是一个强大的框架,为 three.js 提供了一种声明性、可组合、可重用的实体-组件结构。A-Frame 可以从 HTML 中使用,尽管开发人员仍然可以访问 JavaScript、DOM API、three.js、WebVR 和 WebGL。

  • 它们都允许自定义 JavaScript 代码并直接与 three.js 和 WebGL 进行交互。

但为什么要做出决定呢?你不必这样做。你可以同时使用两者。让我们安装 React VR。

安装 Node.js 和 React VR。

目前大多数桌面 VR 硬件都使用 Windows;因此,以下说明,实际上本书的大部分内容将是 Windows 安装和 GearVR 观看的混合。在撰写本书时,Linux 可以被黑客入侵以适配 HTC Vive 和 Oculus Rift,但这是一条艰难的道路,超出了本书的范围。苹果电脑刚刚具备了添加外部显卡以进行 VR 的功能,因为它们中的大多数根本没有足够的视频处理能力来渲染 Vive 和 Rift 头戴设备使用的分辨率。

然而,React 并不特定于 PC。您可以使用 Linux 或 Mac 构建本书中的所有示例,并跟随使用 Google Daydream、非官方 Cardboard 或三星 Gear VR 来查看所有示例。在这种情况下,一些示例可能使用略有不同的语法。我写这本书是为了让大多数使用 Vive 和 Rift 的用户能够跟上,对于其他用户的平台限制,我在此提前致歉。

为什么我们不能和睦相处呢?

在可能的情况下,我会包含其他平台的链接和信息。

安装 Node.js

我们假设您知道自己所在的平台,并且有一台能够安装 Node.js 和 React VR 的计算机(台式 PC)。

首先,我们需要安装 Node.js。如果您已经安装了,那很好,只需确保(在撰写本书时)您至少拥有 4.0 版本。本书是使用 LTS 版本:v6.11.0 和 v8.5.0 构建的,这应该是稳定的,并且在您阅读本书时已经发布。(包括npm3.10.10)

  1. 您应该能够从以下网址获取 Node.js:nodejs.org/en/download/。下载 64 位的.msi预构建安装程序文件。下载完成后,双击该文件或根据您的浏览器运行它。

我们生活在一个保姆国家,所以它会警告你正在下载本地代码。再说,人们经常被利用钓鱼攻击(病毒邮件让你自己感染)而受害。这个应该是安全的。

只有在您下载的程序没有经过数字签名时才会出现这些警告。数字代码签名证书并不难获得;坚持要求公司和非营利组织对其代码进行签名。

这将使互联网更安全。

对于您发布的任何代码都要这样做。

  1. 点击“下一步”。

  2. 我知道,但你真的应该阅读条款和条件——同意并点击“下一步”。

  3. 默认位置没问题。谢天谢地,我们不必担心 Linux 无法处理文件名中的空格(开个玩笑,针对所有 Linux 用户)。

  4. 大多数安装选项都没问题。它们不需要太多空间,所以安装一切都没问题。

  5. 点击“安装”。

  6. 您可能(应该)会收到一条警告,提示正在安装一款软件;告诉 Windows 没问题。

  7. 您已经完成了!现在安装其余的 React VR。点击“完成”。

Mac 下的 Node.js:

Node.js 组织建议使用 Homebrew:brew.sh/

但是,您也可以通过 Node.js 下载页面安装 Node.js:nodejs.org/en/download/

安装应该很简单。

Linux 下的 Node.js:

虽然源代码在这里:nodejs.org/en/download/, 您可以从软件包管理器更轻松地下载 Node.js;说明在这里:nodejs.org/en/download/package-manager/.

安装应该很简单;这是 Linux,我相信你可以处理任何问题。

Node.js 安装后 — 安装 React VR

Node.js 软件包括一个名为npm的软件包管理器。软件包管理器安装软件以及该软件的依赖项。您将使用它来安装 React VR。它使安装变得非常简单和及时更新。无论您使用哪种平台,您都需要打开命令提示符才能使用本书中的大多数示例。命令提示符是一个被错误地称为 DOS 的窗口。在 Windows 中,这被称为 Node.js命令行界面CLI)工具,尽管实际标题是 Node.js 命令提示符。Node.js 安装程序在安装时设置了这一点。您应该使用安装时的 CLI,因为它设置了某些环境变量等。话虽如此,我使用了一种叫做Take Command ConsoleTCC)的替代命令行工具,在 Node.js 进行了安装并注册了路径变量(安装的一部分)后,我可以从我的 TCC shell 运行npm和其他命令。

安装步骤如下:

  1. 打开您首选的 CLI(开始 | Node.js 命令提示符

  2. 输入以下命令:

npm install -g react-vr-cli

您可以从任何位置(文件夹)执行此操作,软件包管理器(npm命令)将安装以下内容:

如果你第二次运行这个程序,好处是它会确认已经存在的内容(尽管像很多开源程序一样,它有点简洁)。

npm还有很多其他非常有用的选项。例如,你可以使用npm ls,它会(就像 linux 中的ls一样)给你列出所有已安装的对象。你可以通过运行命令npm help npm来获得详尽的文档,它会打开一个网页。

  1. 然后,我们想使用相同的 CLI 来安装WelcomeToVR示例。首先,进入一个你想要安装示例和代码的地方(文件夹/目录)。我安装了第二个大硬盘作为 F:(你的里程、平台和磁盘配置会有所不同)。所以,在我开始在桌面或我的文档上安装所有东西之前,我切换到了我的数据驱动器:
f:
mkdir f:      2;reactVR
cd \reactVR
  1. 然后,我继续使用 ReactCLI 安装WelcomeToVR演示:
f:\reactVR>react-vr init WelcomeToVR

进程将开始:

这将需要一些时间。在过程结束时,它会完成并告诉你接下来该做什么:

  1. 然后,进入工具刚刚创建的WelcomeToVR项目目录,并初始化/启动本地开发服务器:
cd WelcomeToVR 
npm start

这个过程会花一些时间。在运行时,这个命令行界面窗口将忙于运行程序。它不是一个服务。如果你关闭窗口,它会停止。所以,不要关闭窗口。

这个窗口还会在你访问各种网页时显示有用的状态信息:

  1. 然后,从你的桌面打开浏览器到http://localhost:8081/vr/index.html,就像 CLI 提示的那样。你完成了!

有一个比在 CLI 窗口中打开网页浏览器并输入 URL 更容易的方法。你应该打开快速编辑模式。以下是显示这一过程的截图:

  1. 点击 CLI 窗口角落的小 C:\窗口。这被称为系统菜单:

  2. 完成后,点击属性。在属性中,打开快速编辑模式:

  1. 点击确定。现在,快速编辑模式已打开,你可以在窗口中高亮文本并按Enter进行选择。然后,你可以直接将 URL 粘贴到你的 WebVR 浏览器中。简单!

  2. 如果像我一样,您喜欢使用与 COMMAND.COM 不同的 CLI(我使用 4nt 或 TCC),假设默认安装如我们讨论的那样,您只需将以下内容添加到您的path中,假设您将 Node.js 安装在默认位置:C:\Users\<user>\AppData\Roaming\npm;C:\Program Files\nodejs

安装 WebVR 浏览器

现在您已经安装了服务器端软件,您需要安装一个能够显示 WebGL、OpenGL 和 WebVR 的 Web 浏览器。这些内容会不断变化,所以我强烈建议去 WebVR 并检查他们的兼容性列表。

Firefox,或者实验性的 Firefox Nightly 可能是最容易使用的浏览器。更多信息,请参考bit.ly/WebVRInfo.

好消息是,从 Firefox 55 版本开始,常规的 WebVR 支持已经内置到 Firefox 中,所以只需确保您的 Firefox 是最新的,您就可以查看 WebVR。要查看您刚生成的新的 VR 网站,您需要执行以下步骤:

  1. 确保您的浏览器可以运行 JavaScript。这是默认设置,除非您以安全意识的方式锁定了浏览器(这是一件好事)。WebVR 广泛使用 JavaScript。您还可以将本地主机添加到白名单中。

  2. 一旦您打开了支持 WebVR 的浏览器(在 PC 上,基本上是 Chromium、Firefox Nightly 或 IE),您将看到 hello。但是,您还没有进入 VR!您需要点击在 VR 中查看。您的 VR 应用程序应该启动。然后您可以戴上头盔,您将看到一个简单的 hello。没有世界?您已经进入了一个 VR 世界!

  1. 您将看到一个链接,上面写着在 VR 中查看。在您点击此链接之前(在您的常规桌面上),您的 Vive/Oculus 将无法工作。

  2. 一旦您点击在 VR 中查看,并戴上头盔,您将在您的 VR 世界中看到 hello。

恭喜!您已经建立了您的第一个 VR 世界。我敢打赌这比学习 Unity 要快得多。

您也可以使用移动 VR 进行查看,但您需要找出开发机器的 IP 地址,并从移动设备访问该网站,类似于:http://192.168.1.100/vr

在 URL http://localhost:8081/vr/index.html中,您将localhost替换为服务器的 IP 地址(您的桌面开发 PC)。

请注意,这几乎肯定不是正确的 IP 地址,您需要从开发机器/服务器获取 IP 地址,并将其输入到移动 VR 头戴式显示器中。对于 Windows,可以转到网络属性,或者从 CLI 中输入ipconfig。如果您的台式 PC 是 192.168.0.100,那么从移动头戴式显示器中,您可以通过虚拟键盘输入http://192.168.0.100:8081/vr/index.html

清洁您的移动屏幕

如果不清洁屏幕,任何规格、指纹或污垢都会清晰地显示出来。这些规格会分散注意力,因为它们似乎悬浮在一切之前。

恭喜!您已经运行并查看了您的第一个 React VR 世界!

现在,如果您没有头戴式显示器,或者厌倦了频繁戴上和摘下头戴式显示器,并不得不走出您的房间范围以返回到您的 PC,有一个快速的预览世界的方法。在 Firefox Nightly 中,您只需点击小雷达显示器,屏幕上显示的内容将向您展示 VR 头戴式显示器中一个眼睛所看到的内容。这对像您这样的开发者来说非常有用!

总结

在本章中,我们介绍了如何实际编程 VR 世界的基础知识,以及使用哪些软件。我们还介绍了安装 React VR 系统,以便开发我们自己的 VR 世界!在下一章中,我们将介绍 3D 基础知识和构建 VR 世界所需的数学知识。

不用担心数学;不会有弹出式测验。

现在要真正创建一个有趣的世界。但首先,我们需要了解世界由什么组成。下一节将描述 React VR 术语,以描述您的虚拟世界。

第三章:3D 或其他维度中的现实

我们决定进入一个几乎真实的世界。要理解如何绘制这个世界,我们需要准确理解如何描述它。

这一章描述了我们如何在数学意义上做到这一点。别担心,这不是回到高中数学!(好吧,也许是几何。好吧,也许有一点点。好吧,也许有很多。我会尽量让它不那么痛苦。)

描述世界的方式有很多种;不管我们如何描述,它仍然是同一个世界。正如莎士比亚在《罗密欧与朱丽叶》中所说的:

“名字有什么含义?不管我们如何称呼玫瑰,它依然芬芳。”

在我们的情况下,有趣的是,情况并非如此:一个描述错误的盒子看起来会完全不同。你需要学会这种语言。不仅如此,你还需要了解 React VR 如何描述这个世界,因为不同的 3D 图形程序都使用不同的数字(缩放)、方向(向量)和旋转。

对于虚拟世界,不同类型的软件和硬件都需要不同的描述方式。例如,坐标可以是左手系或右手系。如果搞混了,物体会朝着与你预期的方向不同的方向移动!

特别是,在 3D 中,上下有不同的含义;更具体地说,不同的 3D 程序之间通常上方方向是不标准的。在 React VR 中,Y 轴向上。为什么 Y 轴向上?继续阅读了解:

  • 坐标:这些是空间中的固定点

  • 点:这些是多边形的构建块

  • 向量:这些是方向

  • 变换:这些是将事物移动到你想要它们的地方

  • 渲染:这将把点和变换的讨论转化为真实的东西

超越平面 - 3D 概念

为了在 3D 中表示事物,我们必须将所见的东西转化为计算机可以用来生成图像的东西。这些方法将涉及到具有 3D 几何、图片和代码的文件。首先,让我们讨论如何在 3D 中定位事物。

为了在 3D 中表示物体,我们需要它们的位置。像 Excel 这样的电子表格使用 A-Z(横向)和 1-66(实际上是 A-XFD 和 1-1048576)。计算机图形使用数字表示所有三个轴。然而,编码这些坐标的方式有很多种。

这适用于比例(什么是一英寸?一英里?)以及它们移动的方向(是 Y 还是 Z 向上?)。为了弄清楚这一点,我们需要谈论坐标系。

坐标

我们都习惯于使用图纸、网格、发光的电子表格,带有XY网格,或者在你使用的任何电子表格程序中的数字和字母,比如 A1 和 B1。进入第三维可能会让人感到困惑,尽管那是我们生活的空间。这就是为什么我将这一部分称为超越平面

我们在二维或一般数学中认为理所当然的数学运算在三维中却是不同的。例如,如果你将XY相乘,你得到的答案与将YX相乘的答案相同。然而在三维中,旋转并不是这样的。要看到这一点,试着拿起这本书的两份副本。(我买了两本,你呢?妈妈,你在读吗?)

好了,认真地,请拿起任意两本书,纸质书。如果你有两个 Kindle,你也可以用它们。

  1. 对于第一本书:

  2. 物理上将其(合上)向左旋转四分之一圈(朝向你的右手)。

  3. 然后,将背边向你翻转(翻过来)。

  4. 你现在看到的是背面,侧面。

  5. 对于第二本书,以相反的顺序翻转:

  6. 将背边向你翻转(翻过来)。

  7. 然后将其顺时针向左旋转四分之一圈(朝向你的右手)。

这两本书面对着不同的方向,尽管你两次以相同的方式旋转它们,只是顺序略有不同。

三维数学可能会让人感到困惑。通常,如果你将AB相乘,你得到的结果与将B乘以A的结果相同。

当涉及到平移、旋转和缩放时,这个概念非常重要。你编码的顺序决定了你的物体最终在世界中的位置和它们的外观。

我们将使用三个数字来指定三维空间中的每个位置,具体来说是XYZ

这被称为笛卡尔坐标系。还有其他类型的坐标系,但几乎每个计算机系统都使用笛卡尔坐标系来表示空间位置。旋转和向量有时会使用其他坐标系。这是一个欧几里得空间

为了让三维更加令人困惑,有些人使用XYZ成为新的维度,而其他人则说Y是向上的。为什么(它)是向上的?当处理屏幕时,你习惯于使用XY。一张纸类似,尽管纸通常是水平的,而屏幕是垂直的。

这导致了一个有趣的三维转换问题。在三维中,我们使用XYZ。如果你习惯使用XY,那么Z就必须成为新的第三维,它将是向上的。然而,如果你习惯将XY看作是一张图纸,那么Y已经是向上的,所以Z最终会是内部和外部。每个三维系统似乎都有些不同。

基于 WebGL 的 React VR 使用熟悉的XY作为左/右和上/下;所以Z必须是内部/外部。然而,一个不同之处是在 React VR 中,Y是向上的;在标准的 HTML 中,Y是向下的。换句话说,HTML 和 React 使用的坐标是(零乘零)作为左上角。Y是向上的吗?大多数三维程序使用YZ作为向上,也就是说在我们的情况下,正的Y是向上的。

WebGL 和 HTML 与 React 不同,可能需要一些时间来适应。要将一个物体放在你面前,以便你能看到它,你需要给它一个负的 Z。

在三维中,坐标可以是左手的或右手的。正如我们在XYZ中看到的,有时箭头的方向并不是你所期望的。为什么 React VR(实际上是 OpenGL)没有决定让 Z 进入屏幕呢?那么坐标将是左手的。相反,大多数图形系统使用右手坐标系。

我哥哥是左撇子。

左撇子也没什么不好。

(实际上,他是右撇子,但为什么要用事实来破坏一个好故事呢?)

右手和左手有什么意思?这是一个助记符,有助于箭头的方向和旋转。如果你拿起任何一只手,伸开前三个手指,它们就会拼出XYZ的方向。一个图表会有所帮助;你的前三个手指(拇指、食指、中指)指向正的XYZ

在这个图表中,有几件事情需要注意。相机代表我们在左边,透明地看着屏幕。因为Y是向上的(为什么?),X是向右的,OpenGL 使用的坐标系统与 HTML 或一张图纸并不相似,但它更或多或少是网络上的标准。

选择这种方式更容易映射到 3D 模型、计算机辅助设计(CAD)和建模程序(如 Blender、Maya、3DSMax)的构造方式。这与 React 的工作方式相反——Y向下为正。这是一个右手坐标系;如果你用左手尝试,XYZ轴的顺序会不同。

旋转呢?

在 React VR 和 OpenGL 中,围绕任何轴的旋转也是右手坐标系。这意味着围绕任何轴的正旋转将沿着拇指指向的方向进行,手指弯曲。例如:

你是否在看着你的右手并弯曲手指?没关系,这有助于可视化。是的,那些箭头显示了各自轴上的正方向。

老实说,Y朝上和Z朝上似乎在 3D CAD 世界中很常见混淆。你的 CAD 系统可能工作方式不同。没关系,我们可以翻转和反转它——只要知道在导入模型时,你可能会发现它们侧倒,甚至是里外颠倒。

在 Blender 中,Z朝上,XY在平面上;然而,在导出时,Y可以替代朝上。为什么?因为这是正当合法的。

这些数字是无单位的;一个边长为 1 的立方体可以被视为 1 英里或 1 英尺。然而,在 WebVR 和 React VR 中,单位通常被认为是米。

Blender 可以使用无单位、公制或英制,因此在导入物体时需要调整比例。

程序 Poser 使用奇怪的单位——你需要调整从中导入的任何东西的比例。

用于导入模型的 OBJ 文件没有单位信息;它们是无尺寸的:1 就是 1,不是 1 米。

点是指空间中的 3D 位置,通常通过XYZ位置来确定。在 React VR 中很少直接描述点,除非你正在进行本地渲染,但空间中的位置经常被描述为点。例如,一个变换节点可能会说:

  transform: [{
         translate: [0, 400, 700]
         }]

应用变换的对象的中心将位于位置X=0,Y=400,Z=700 处。

向量

向量指的是方向。在航空中,飞行员谈论向量。从电影《飞机》中的场景中,克拉伦斯·奥弗、罗杰·默多克、维克多·巴斯塔和控制塔讨论航向:(bit.ly/WhatsOurVector)

罗杰·默多克:2-0-9 号航班,你已获准起飞。

奥弗船长:罗杰!

罗杰·默多克:啊?

塔台声音:L.A.离港频率,123 点 9。

奥维尔船长:罗杰!

罗杰·默多克:啊?

维克多·巴斯塔:请求矢量,结束。

奥维尔船长:什么?

塔台声音:飞行 2-0-9 清除矢量 324。

罗杰·默多克:我们有许可,克拉伦斯。

奥维尔船长:罗杰,罗杰。我们的矢量是多少,维克多?

塔台声音:塔台的无线电许可,结束!

奥维尔船长:那是克拉伦斯·奥维尔。结束。

从我们作为 VR 人的角度来看,他们真的是指航向。在 3D 空间中,你也可以朝上或朝下。这三个方向对我们来说都非常重要。

翻译,严格来说,使用向量;如果你给一个对象的变换属性为[0, 2, 0],你是在告诉对象在+Y方向上移动2个单位,而不一定是绝对位置 0,2,0。然而,需要注意的是,如果对象的原点是在 0,0,0,那么它是一样的。在翻译时,考虑你的 3D 对象的原点以及对象是否采用绝对或相对定位是很重要的。

变换

这不是一本关于奇异的可折叠机器人的书,所以我们谈论的是变换,而不是变形金刚。

变换是放置、定位、移动和缩放对象的方式,基本上是任何转换对象、点等的XYZ坐标。

在 React VR 中,变换通常是样式的一部分。例如:

style={{
    transform: [
      {rotateZ : this.state.rotation},
      {translate: [0, 2, 0]},
      {scale : 0.01 },
    ],
  }

变换顺序非常重要。正如我们之前讨论的,在 3D 中,变换不是传递的 - 如果你先平移,然后旋转,你最终会停在一个不同的位置,而不是如果你先旋转,然后平移。记得书上的例子吗?

在 React VR 中,变换是大多数具有物理存在的对象的标准属性节点。(见附录和第四章《React VR 库》。)

变换确实有三个主要参数(和一些已弃用的属性);一个变换,或矩阵参数。

是的,我说的是矩阵。

矩阵一直是一个数学概念已经有一段时间了。它也是一部很棒的电影。由于版权限制,我不能在这里包含矩阵的图片,但上面是我在矩阵中查看我们的 VR 控制器场景的表示。无论如何,我不是指电影。我们将使用矩阵来创建我们自己的 3D 场景。

矩阵是描述平移(向量)、旋转、缩放和扭曲的数学方式。我的一些朋友在周末会有些扭曲,但扭曲是一个数学术语,意思是移动对象的顶部比底部更多。你可以把它想象成倾斜。

要完全理解矩阵,让我们谈谈非基努·里维斯的做法。

每当有一个物理对象,比如一个盒子、一个模型、一个灯光或一个 VR 按钮,你都有各种样式属性,其中之一就是变换。变换节点可以使用矩阵,或者有时候更容易一些,直接使用平移属性。例如,如果你在 React VR 中定义一个Cylinder,你可以这样变换它:

<Cylinder
       radiusTop={0} 
       radiusBottom={2.20} 
       dimHeight={2.8} 
       segments={10} 
       style={{
               transform: [
               {rotateX: -45}, 
               {translate: [0,1, -4]}
               {scale: .4}
               ]
            }}
          />

变换顺序很重要。这是一个三个圆柱体的例子,除了颜色和变换之外都是相同的:

  <Cylinder
    radiusTop={2} 
    radiusBottom={2.20} 
    dimHeight={5} 
    segments={10} 
    lit = {true }
    style={{
      color: 'red',
      transform: [
        {translate: [1,.5, -6]},
        {rotateZ: -90}, 
        {scale: .2}
      ]
    }}
  />
  <Cylinder
    radiusTop={2} 
    radiusBottom={2.20} 
    dimHeight={5} 
    segments={10} 
    lit = {true }
    style={{
      color: 'blue',
      transform: [
        {rotateZ: -90}, 
        {translate: [1,.5, -6]},
        {scale: .2}
      ]
    }}
  />
  <Cylinder
    radiusTop={2} 
    radiusBottom={2.20} 
    dimHeight={5} 
    segments={10} 
    lit = {true }
    style={{
      color: 'green',
      transform: [
        {scale: .2},
        {rotateZ: -90}, 
        {translate: [1,.5, -6]}
      ]
    }}
  />

这是生成的 React VR 世界:

矩阵怎么样?

矩阵是一个四列四行的数字系列(数组)。

您也可以在变换节点中使用矩阵。矩阵数学的完整讨论超出了本书的范围。网上有很多参考资料。基本概念相当简单,但旋转可能有点困难(尽管是确定性的)手工编程。

平移被存储为:

[1, 0, 0, 0,
 0, 1, 0, 0,
 0, 0, 1, 0,
 Tx,Ty,Tz,1].

缩放由以下表示:

[Sx,0, 0, 0, 
 0, Sy,0, 0, 
 0, 0, Sz,0, 
 0, 0, 0, 1].

旋转可以用 R 值表示:

[R00,R01,R02,0, 
 R10,R11,R12,0, 
 R20,R21,R22,0, 
 0, 0, 0, 1].

通过矩阵数学进行的旋转非常精确,但也非常复杂。正如我们之前所看到的,旋转的顺序会改变结果对象的位置。矩阵本身不会有这些问题,因为顺序已经嵌入到矩阵中。计算旋转可能会很混乱。

一般来说,当手动移动对象时,您会希望使用变换样式而不是矩阵数学。

当复制对象的位置和方向或以编程方式移动对象时,您会希望使用矩阵。

通过矩阵按比例缩小所有轴十分之一并按[3, 2, 1]进行平移的矩阵可以应用如下:

style={{
    transform: [
      {matrix : [0.1,0,0,0, 0,0.1,0,0, 0,0,0.1,0, 3,2,1,1]},
    ],
  }}

您不能同时使用变换矩阵和变换样式(单独的平移、旋转、缩放)。实际上没有必要,因为您可以通过指定单独的变换来完成矩阵可以完成的一切。如果您使用矩阵,您就是相当专业!无论如何,您创建的任何变换都将在幕后转换为矩阵。

欢迎来到矩阵-现在你可以创建它。

渲染

Bing 将渲染定义为:

好吧,那很有趣,但显然不是我们的意思。渲染是指将我们一直在讨论的所有数学描述转化为可视化的东西。

React VR 使用的渲染引擎是 three.js(bit.ly/2wHI8S9),通常使用 WebGL 进行渲染(bit.ly/2wKoKCe)。WebGL 是一个强大的 JavaScript API,用于生成高性能图形。它将使用系统中的任何高性能图形硬件(GPU),并且在大多数浏览器中是本地的,无需插件即可进行 3D 图形处理。

然而,网络通常是一个狂野的地方。你会认为浏览器制造商会从 20 年前的兼容性问题中吸取教训,但遗憾的是情况并非如此。一些浏览器可能会出现问题或兼容性问题,尤其是在移动设备上。

我们难道不能和平相处吗?

这将影响 React VR 的运行情况。如果你想知道你的浏览器和硬件是否支持 WebGL,请访问bit.ly/WebGLTestPage的 WebGL 测试页面。该页面将显示一个旋转的立方体;如果你使用的浏览器不是百分之百兼容的,它可能还会显示一些警告。WebGL 维基百科页面(bit.ly/2wKoKCe)对各种浏览器的功能有很好的描述,但事情会发生变化。

你需要进行测试。

测试它的外观

在第二次世界大战中,美国海军陆战队在太平洋战争前对水陆两栖登陆进行了大量测试。他们已经掌握了战术和战略。

他们第一次登陆时,撞上了珊瑚礁。圣迭戈/彭德尔顿营地没有珊瑚礁。因此,尽管这是一次成功的登陆,但比他们预想的更加灾难性。因此,美国海军陆战队有一句话:

"训练就像你将要战斗一样。"

这显然是一个长期存在的说法。罗马军团过去常说:

训练应该是一场无血的战斗,这样在战斗中,就会像血腥的训练一样。-罗马军团训练法则

如果你习惯使用 Firefox,将你的 React VR 解决方案发布到世界上,你的 Petunia 阿姨使用 Orchestra 浏览器查看你的世界,可能不起作用,Petunia 阿姨仍然会认为你是她那个无目标的兄弟的懒散后代。请注意,据我所知,Orchestra 不是一个真正的浏览器,尽管我使用 Opera。

如果这是一个内部公司专用的应用程序,并且你有一个标准(并且可以强制执行!)只使用一个浏览器,那么你可以使用该浏览器进行测试和开发。

然而,如果你希望许多人使用你的 React VR 应用程序,你确实应该使用各种浏览器进行测试。如果你有一台 Mac、一台 PC 和一台 Linux 机器,那就更好了。这些可以是虚拟机(特别是 Windows,带有适当的许可证,和 Linux),否则你需要 Beta 测试人员。

你需要像你期望人们使用它一样测试它,否则你会认为它运行良好,但人们对你的虚拟世界不满意。你需要尽可能多地在各种浏览器和硬件平台上进行测试。当然,这实际上并不现实,但却是必要的。这就是 Beta 测试人员可以派上用场的地方。

渲染的工作原理

我们正在谈论渲染。这是将数学模型与表达的属性转化为屏幕上可见的东西的过程。

为了在 VR 中渲染你的模型,React VR 使用了基于 OpenGL 的 WebGL。WebGL 是 JavaScript 的实现,如果浏览器中有一个相当功能的实现,它通常应该在大多数平台上看起来一样。如果你描述一个竖直的红色箭头,那么在所有浏览器中它都应该看起来像一个指向上方的箭头。关于测试的建议通常是确保它能够正常工作,而不是确保一个向上的箭头不会突然指向右边。

在游戏行业,人们通常需要在 DirectX 和 OpenGL 之间做出决定。WebVR 既不使用 DirectX 也不使用 OpenGL,尽管 WebGL 是基于 OpenGL 设计的。与 OpenGL 不同,它还包括 HTML 元素,如 HTML5 画布和 DOM(文档对象模型)。如果你在使用 WebVR,你就是在使用 WebGL。

如果你正在测试的浏览器没有正确显示你的场景,但其他浏览器可以,请务必提交错误报告。这些应用程序中的许多都是像互联网上的大多数酷东西一样处于前沿。

你希望这些错误得到修复-所以告诉人们。程序员无法修复他们不知道的问题。你将为更加理智、更加清洁、更加有效的网络做出贡献。

成为解决方案的一部分!

WebGL 通常会使用高性能的 GPU,通过 OpenGL 或 DirectX。你不需要担心,也无法控制 React VR 使用的内容(除非使用原生应用),尽管这是一个优势。Web 浏览器通常会做正确的事情,并利用手机(手机,平板电脑),笔记本电脑或台式机上可用的任何硬件。

如果你想更精确地控制渲染呢?这是可能的,使用 React VR Native,我们将在以后的章节中更详细地介绍。

总结

在本章中,我们学习了描述我们的 VR 世界的重要方法。如果你想这样想,我们正在学习矩阵的语言,甚至涉及到矩阵。我们学习了关于 3D 坐标,点,向量,右手和左手世界以及变换。

我们学习了所有这些概念是如何结合在一起并呈现的,以及不同的网页如何利用这些信息创建一个视觉空间。我们还学会了如何进行测试!

为了进行测试,我们需要在浏览器中有一些东西。为了做到这一点,我们不仅需要知道如何用数字和旋转来描述世界,还需要知道这些数字如何在构建块中使用。下一节将介绍 React VR 用来描述世界的关键词,组件和对象。

第四章:React VR 库

这一章是关于 React VR 库的布局;其中的对象和组件。本章中的许多概念将在后面的章节中引用,所以如果你正在阅读电子版本的这本书,它将被大量超链接以供你享受和愉悦。

React VR 有六个基本元素,并且使用一种新的、但又熟悉的范式 JSX(JavaScript eXtension)进行编码。如果你已经了解 React,那么你对 React VR 也应该很熟悉,尽管有一些重要的区别。我们将涵盖以下内容:

  • JSX,React VR 的语言和语法:

  • React VR 和 React 之间的区别

  • 组件和 VR 组件:

  • 属性

  • 状态

  • 事件

  • 布局

  • 样式

  • 所有组件和关键字的细节:

  • 对象-可见和不可见

  • 灯光

  • 多媒体-声音和视频

  • 摄像机和视图

我没有涵盖所有可用的 API,因为那基本上是一个冗长的、像字典一样的背诵,最好是使用网站上的文档来探索 API。在后面的章节中,我们将使用关键的 API 来为我们的世界增添生机并在其中导航。要获取完整的、最新的 API 列表,请查看文档(facebook.github.io/react-vr/docs/getting-started.html)。

JSX-React VR 的语法

React VR 看起来很像 HTML;这使得阅读、编辑和部署变得容易。在幕后,React 和 React VR 使用的 UI 语法粘合剂将被编译成 JSX 或 JavaScript eXtension。JSX 是 React 的语法扩展,允许在 HTML 和 JavaScript 的混合中编码。你也可以直接编写 JSX。

React JSX 的一个例子如下:

const  element  =  <h1>My title!</h1>; 

这不是一个字符串,因为它不在引号内,也不是 JavaScript。它比直接在 JavaScript 中编码更易读、更易用。JSX 使编程更快速、更具声明性。

它很有用,但所有这些易读和易编程的特性也伴随着一些陷阱。其中之一是分号会自动输入。就像 HTML 一样,你可以包含额外的行,但你的代码可能会得到你没有打算的额外的分号。

在你的代码周围加上括号以避免这个问题——我也强烈建议你学习一下 JavaScript 的语法。这本书中的一些内容花费了我比应该更长的时间,因为我是 C++程序员,不是本地的 JavaScript 程序员。

在编译 React VR 之后,JSX 会自动转换为 JavaScript。这意味着你可以在任何需要使用 JavaScript 的地方包含 JSX。

React VR 和 React 之间的区别

在 React 中,你的大部分思考都围绕着自 JavaScript 诞生以来一直吸引和激怒我们的文档对象模型DOM)。在 React VR 中,你需要忘记 DOM;在这方面,React VR 更类似于 React Native。即便如此,也有一些概念你需要忘记。

忘记像素作为一个维度;在 VR 中这个概念是没有意义的。

你可以将一张巨大的照片铺满世界,看起来很模糊,而一张小照片放在一个小物体的一侧,看起来非常清晰。你可以离物体更近或更远(假设你已经编程了移动),这将极大地改变某物的“像素”宽度。相反,一切都以现实世界的米为单位(如果你来自一个坚持使用过时的“英尺”单位的国家,你可以假装单位是码。对于 VR 工作来说足够接近)。

在 React VR 中可能会显得有点奇怪的另一个概念是渲染的速度。在 React 中,你的页面被加载,然后显示,然后页面的元素可以被交互(点击),但整个页面很少重新渲染,除非有人点击刷新。当对象的属性发生变化时,render方法会被调用。这并不意味着你必须有一个定时器来“刺激”你的页面在 VR 中的渲染。

使用 React VR,整个页面在不到 16 毫秒的时间内被渲染,以实现每秒 60 帧的速度,这现在被认为是 VR 的基本要求。整个页面不会被重新解析。这在某种程度上与常规 HTML 相反。特别是在活跃的网页中,单独的 VR 组件将以每秒 60 帧的速度被渲染(显示),当它们的属性发生变化时,它们将被渲染(到 three.js 代码)以更新该表示。

渲染对象并不等同于页面渲染。这可能会有点令人困惑。即使各种对象的render()方法尚未运行,页面在开始加载时就会被渲染,以将它们转换为 three.js 代码。

最终结果是,没有任何额外的编程,当属性在您的世界中更新时,对象将根据这些属性更改适当地显示。这是 React 工作的基石,它在 React VR 中同样适用。这增加了每秒多个帧率的渲染。

既然我们已经讨论了 React VR 不是什么,让我们来看看 React VR 是什么。

核心组件

React VR 具有可重用的 UI 元素,您可以在各种地方使用。这些被称为组件。有两个内置组件:

  • 文本

  • 图像

您还可以通过扩展React.Component来构建自己的组件。

组件是真实的东西,不仅仅是标签或占位符,因为它们已经以通过render()函数在世界中呈现自己的方式构建。这不仅仅是一个函数;像 React VR 的一切一样,render()通常有一组子组件,用于呈现或描述其内容。组件的一个示例可能如下:

<Greeting/>

这将是一个文本组件,一个内置类型。

VR 组件

VR 对象,您通常会认为是组件,稍后会涉及。React VR 文档在核心组件部分没有提到它们,这有点令人困惑。您可能会想“只有文本和图像?物体呢?” VR 组件是我对以下事物的术语:

  • VR 物理组件:这些是您可以在世界中“看到”的对象:

  • 3D 基元,包括盒子,圆柱体,平面,球体和导入的对象(可以非常详细)

  • UI 元素,如面板和按钮

  • 灯光:这些照亮前面的对象,可以是几种类型。请注意,在 React VR 中,当前照明不会为实时速度投射阴影。

  • 多媒体:这包括视频和声音。通过这种方式,您可以为 360 度视频创建移动背景,也可以在您创建的世界中有“电视”。

  • 摄像头和场景:摄像头控制渲染,场景包含您放置其中的所有对象。

我们将在下一级-细节部分中逐个介绍这些关键词。

属性

如果组件没有属性,那将会很无聊。我们的问候语的一个属性示例可能如下:

<Greeting name='Hello React VR Dude!'/>

name和其他值如此被称为props。属性是名称,有一个值,我设置为一个幽默的字符串。它们可以通过编程访问,例如,{this.props.name}

许多 3D 对象也有属性;这些属性因对象而异。

状态

也许我们处于一种困惑的状态,但 React VR 状态非常重要,因为它影响所有组件的显示,因此也影响这些组件的各种属性。如果组件的属性(外部)或状态(内部)发生变化,组件将重新渲染自身。

渲染并不一定指的是“为眼球创建图像”,尽管它可以。在这种情况下,渲染可以指的是通过 React VR/JSX 编译过程构建代码。

React VR 是封装的,根据对象导向的哲学/编程范式,因此可修改的状态位于组件内的this.state对象中。它应该只通过一个“set”函数进行修改,具体如下:this.setState({myStateVariableBeers: 99 })

请注意,虽然乍看起来这似乎是在扭曲 HTML/JSX 格式,但这正是使 React VR 如此强大和易于使用的原因。

事件

事件不仅仅是在你的社区里去玩的有趣的事情,它们也是让你的 VR 世界真正活跃起来的方式。事件是当用户通过用户界面UI)执行某些操作时生成的。当你将光标移入和移出视图区域时,View组件会发送onEnteronExit事件。

敏锐的读者可能会感到困惑——我们正在谈论 VR,我刚提到了区域。为什么一个 2D 的概念被讨论为 3D 语言的一个基本组成部分?

事件和布局(下面会讨论)遵循 2D 范例,并且是易于桥接的一个例子,使你能够轻松地过渡到你习惯使用的东西(HTML、CSS 和 JavaScript 以及 VR 世界)。然而,也存在差异,其中一个声明“像素”在任何属性和关键字中根本不被考虑可能看起来基本上很奇怪。这是因为在真正的 3D 世界中,使用像素作为测量单位的概念基本上是没有用的。在你面前一米处的物体将具有比你身后十米处的物体更宽的屏幕表示。因此,尺寸是以世界空间的单位给出的;一个单位是一米(略大于一码)。

React VR 的重点是快速和声明性地构建出色的 3D 世界。这是一种声明性的编程方法。如果你想要构建更复杂的世界,React VR 的强大之处在于你可以使用 React Native 和其他 Node.js 编程方法来扩展 React VR。

布局和样式

WebVR 和 React VR 的一些方面仍然遵循浏览器范式。光标被视为 2D 交互,UI 元素通常以 2D 弹性框和布局规则的形式描述,以在 2D 中布置这些组件。这并不意味着我们没有开发 VR 环境;尽管大多数 UI 是以 2D 格式存在,但这些完全存在于 VR 环境中。

布局和样式自然地转移到 3D。您不必为每个项目(内联)描述您的 3D 对象,而是可以设置类似于样式表或 CSS 的东西。它实际上并不类似于样式表,它就是一个样式表,因此您所有的技能都将转移到这里。

样式表可能会很混乱,所以 React VR 使得布局 UI 元素变得更加容易。它使用 Flexbox,通过 YogaLayout(在github.com/facebook/yoga)。React VR 的重点是快速创建现实。React 关注用户界面,因此 React VR 中的 UI 元素非常强大是很自然的。

下一个级别 - 细节

尽管 React VR 库很简单,但要真正了解它的全部内容,您需要学习很多语法。您可以略读,但是知道一点而不熟悉所有内容是有危险的。

“一知半解是危险的事情;

深饮,或者不尝试皮耶里亚之泉:

浅浅的饮料会使大脑陶醉,

大口地喝则会使我们清醒。-亚历山大·蒲柏的《批评论文》。

您可能会想“好吧,但是所有的 VR 东西在哪里?你知道,桌子,椅子,灯,人... *等等。”这确实需要深入研究-有相当多的组件。

最好的参考是在线文档,尽管有时可能有点稀疏。请记住在线文档是实时的,这意味着您可以提交问题,甚至修改它,如果您发现有错别字或需要澄清。

我强烈建议您将下一节视为参考部分。当然,你可能需要帮助入睡,那么请继续阅读!在说完这些之后,这一部分非常重要,因为您需要使用许多或所有这些组件来实际构建您的 VR 世界。我将尝试使这一部分有趣。我写书是件好事,而不是试图在舞台上谋生。

物品(可见或不可见的对象)

世界上大部分有趣的事物都是可见的物体或可以与之交互的物体。大致上,按照复杂性的顺序,它们如下:

  • 盒子

  • 圆柱体

  • 平面

  • 球体

  • 圆柱面板

  • 模型

  • 全景

  • VideoControl

  • VrButton

基元

盒子,圆柱体,平面和球体都是 3D 基元。它们具有littexturewireframe属性。发光的物体将受到场景中的光线影响。如果指定了纹理(通常是图像文件),您的浏览器将查找(获取或渲染)此图像,并将其用于包裹 3D 基元。UV 映射我们将在第六章中讨论,与 Poly 和 Gon 家族合作和第七章中讨论,与(虚拟)茶壶坐下,但大多数 3D 基元的映射方式与您期望的方式相同。

请注意,纹理可以是一个string(指向图像文件),一个asset()调用,或一个require()

盒子

Box是一个基本的立方体。如果未指定尺寸,它的尺寸将默认为一(单位)。

<Box
 dimWidth={4}
 dimDepth={1}
 dimHeight={9}
 lit
/>

这将是《2001 太空漫游》中的石碑;尺寸是前三个质数的平方。有关更多信息,请参见facebook.github.io/react-vr/docs/box.html

圆柱体

Cylinder是一个基本的封闭圆柱体。也可以通过将顶部半径设为零(或底部为封闭漏斗)来制作圆锥体。

Cylinder使用半径,而不是直径。不要把你的圆柱体做得比需要的大两倍!

// Round cylinder 
//Doric order column
<Cylinder 
   radiusTop={.825} 
   radiusBottom={1} 
   dimHeight={8} 
   segments={20} /> 

// Great Pyramid
<Cylinder 
   radiusTop={0} 
   radiusBottom={2.20} 
   dimHeight={2.8} 
   segments={4} 
/>

注意创造性地使用边数来使圆锥体成为金字塔。有关更多信息,请参见facebook.github.io/react-vr/docs/cylinder.html

与所有 3D 基元一样,Cylinder具有littexturewireframe属性。

平面

这不是一架空中客车,而是一个平坦的表面。虽然它被称为平面,但更像是一个平坦的,正方形的二维板。它不是一个立方体板,那将是一个Box

//concrete slab using industry norms for size
<Plane
   dimWidth={2.4} dimHeight={2.4}
  />

关于平面的一件棘手的事情是它们只能从其主要一侧可见。它们是快速,轻量级的对象,但只能在其上有一个纹理贴图,因此如果使用大平面可能会显得重复。如果你把平面旋转错了,可能什么也看不到;你可能在看背面。在变换时要小心,或者使用Box而不是Plane

有关更多信息,请参阅facebook.github.io/react-vr/docs/plane.html

与所有 3D 基元一样,“圆柱体”具有littexturewireframe属性。

球体

跟随弹跳的球,尽管动画稍后会涉及。与“圆柱体”一样,“球体”有一个属性可以改变其分辨率:

<Sphere
  radius={0.5}
  widthSegments={20}
  heightSegments={12}
/>

与我们做金字塔的方式类似,为宽度和高度输入非常低的段数可以使“球体”看起来像不同类型的固体。有关更多信息,请参阅facebook.github.io/react-vr/docs/sphere.html

与所有 3D 基元一样,“球体”具有littexturewireframe属性。

模型

“模型”组件允许我们做非常有趣的事情。到目前为止,VR 对象一直相当简单,但模型允许您导入任意复杂度的 CAD 模型。

小心使用“模型”:

您可以轻松导入比您的平台处理能力更复杂的对象。请记住,您仍然保持所需的平滑帧速率,以使虚拟现实看起来真实。

在第六章中,使用 Poly 和 Gon Family,我们将探讨有效使用“模型”的细节。显示“模型”的基本方法如下:

带有材料文件的“模型”:

 <Model
 source={{
 obj: asset('sculpture.obj'),
 mtl: asset('sculpture.mtl'),
 }}
/>

没有材料文件的“模型”:

<Model
 source={{
 obj: asset('standalone.obj'),
 }}
/>

在撰写本书时,“模型”导入 Wavefront OBJ 文件格式,以及 GL 传输格式(glTF)。 OBJ 是最常见的 3D 模型格式。人们可能会想知道为什么 React 不导入 X3D,这是 WebVR 的首选格式。这是我当初对 VRML 和 X3D 投入如此多精力的事情之一,让我感到沮丧。

无论如何,OBJ 文件通常由两个文件组成;filename.obj包含对象的几何形状,而伴随的.MTL文件(材料)包含颜色、材料和对外部纹理(图像文件)的引用。请注意,这意味着如果 OBJ 文件在材料文件中加载了许多纹理,您可能需要远不止这两个文件。

我们将在第六章中更深入地介绍这一点,使用 Poly 和 Gon Family

请注意,Modellittexturewireframe 属性。纹理属性应用于整个模型,可能有多个 UV 映射。通常最好通过 .MTL 文件分配纹理,这可能会自动从建模程序中完成。

不要指望纹理关键字适用于您导入的 Model。最好在您使用的 CAD 程序中为模型贴图和映射,而不是在 React VR 中尝试覆盖它。

其次,您可能需要手动编辑 .MTL 文件;我的经验是,大多数导出程序无法处理基于节点的着色器的所有复杂性,即使是实时引擎也会大量使用;因此,您的 .MTL 文件几乎肯定不会包含所有不同的烘焙贴图。

CylindricalPanel

CylindricalPanel 是一个过渡对象。它旨在拥有子对象,并提供在当前视点上的无形圆柱体上绘制这些对象的能力。它的主要目的是允许将熟悉的 2D 元素放置在 3D 世界中。要能够做到这一点需要一些过时的元素。

当您使用 HTML 时,要精确布局 HTML 元素,您可能需要考虑和编写像素级的代码;例如,某个元素可能有 200 像素宽。这使您能够精确布局图形。

在 3D 中,这些都不适用。月亮宽是 1 个像素、2 个像素还是 10 个像素?世界没有每英寸点数。因此,大多数 VR 基元将它们的实际大小,嗯,它们的虚拟大小,以米为单位。然后,您的 VR 显示方法将显示正确数量的像素。如果您把头移动到那个立方体旁边,它可能是 2000 像素;如果您在走廊的尽头看到它,它可能只有 10 像素宽。因此,通常不使用像素来确定 React VR 中的大小。

然而,CylindricalPanel 对象确实需要一个像素数量的属性。这不是为了对象本身(嗯,有点是),而是为了一个屏幕外缓冲区来保存任何子对象的可见渲染。像网页中的许多东西一样,它有合理的默认值。默认值相当大,但这是为了使它在近距离看起来不那么粗糙。

我强烈建议不要使用 CylindricalPanel,而是将您的 UI 重新编码为实际的 3D 对象。这种方式可能会降低分辨率和系统资源使用(主要是 RAM)。

例如:

<CylindricalPanel
 layer={{
 width: bufferWidthPx,
 height: bufferHeightPx,
 density: numberOfPxForACompleteTurn,
 radius: distanceFromTheViewer
 }}>
 ... Child components ...
</CylindricalPanel>

Child components行非常重要--这里是你放置实际的 2D 对象,这些对象将显示在CylindricalPanel上。这不是字面上的代码。

VideoControl

VideoControl是一个具有正常VideoPlayer功能的物理对象,换句话说,开始、暂停等等。由于它旨在用于播放视频,这里的示例(直接来自文档)将显示它嵌入到一个动画对象中:

class VideoPlayer extends React.Component {
constructor(props) {
   super(props);
   this.state = {
   // init with muted, autoPlay
   playerState: new MediaPlayerState({autoPlay: true, muted: true}), 
 };
}
render() {
   return (
   <View>
      <Video
      style={{height: 2.25, width: 4}}
      source={{uri: 'assets/1.webm'}}
      playerState={this.state.playerState} />
   <VideoControl
      style={{height: 0.2, width: 4}}
      playerState={this.state.playerState} />
   </View>
   );
}
}

不要局限于其预期用途。你也可以进行实验--也许它是一个不错的火车控制器!

VrButton

VrButton实际上并不是一个真正的按钮(好吧,它都是虚拟的,对吧?),这意味着它没有任何几何形状,但是这是一个你可能会发现非常有用的对象,可以包含在世界中。

VrButton主要用于凝视检测。我们将在第十一章 走在野性的一面中讨论这个以及其他 VR 移动(运动)技术。现在,让我们只讨论一下VrButton是什么:

<VrButton
 style={{width: 0.7}}
 onClickSound={{ ogg:  asset('click.ogg'), mp3:  asset('click.mp3'),  }}
 onClick={()=>this._onViewClicked()}>
 <Image style={{width:1, height:1}}
 source={{uri:'../../Assets/Images/gaze_cursor_cross_hi.png'}}
 inset={[0.2,0.2,0.2,0.2]}
 insetSize={[0.05,0.45,0.55,0.15]} >
 </Image>
</VrButton>

这个VrButton包裹了一个图像并播放声音。我们将在第八章 给你的世界注入生命中进一步讨论声音,但在这里简要介绍,文件格式允许浏览器决定在你选择的浏览器中播放哪种声音。

灯光

如果没有灯光,世界将会是一个非常黑暗且充满吸血鬼的地方。让我们赶走这些不死族。有四种主要的灯光:

  • AmbientLight

  • DirectionalLight

  • PointLight

  • SpotLight

常见灯光属性

所有灯具都有两个共同的属性:

  • intensity:这是场景中灯光的亮度。默认值是{1.0},但你可以调高。在实践中,更高的设置会使物体(例如球体)曲边上的阴影更加清晰和更亮(变淡),但实际上不能比白色更白(RGB 255, 255, 255)。

  • color:颜色并未列在灯光属性下,但它是所有灯具都具有的样式属性。这是一个 RGB 属性。你甚至可以拥有有颜色的环境光,这可以用于赭色调等,以及模拟来自明亮环境的背景照明。例如,在森林中,也许是浅绿色的环境光。默认值是白色。

其他灯具具有特定于它们所代表的照明类型的属性。

AmbientLight

AmbientLight是使你的场景可见的最简单的方法。实际上它并不是一个真正的光,但它确实照亮了场景中的一切。

现实世界中的照明非常复杂。光子四处反射,反射物体,渗透进物体,甚至使一些物体发光(荧光和发光)。一个有用的技巧是让物体即使没有灯也能发光,或者在房间中添加一些光线以帮助模拟背景光的散射,而不需要计算这个过程的开销。

这被称为环境光。许多 CAD 系统在材料中都有环境作为一个值。 AmbientLight让你点亮整个房间。对于喜欢迪斯科、节日的人来说,它甚至可以让你把颜色从白色变成任何你想要的颜色。现在,你可以制作一个看起来像 W 酒店连锁酒店走廊的场景。

奇怪的是,React VR 下载中没有一个示例显示如何使用AmbientLight;尽管这并不难,但它很重要。

这是一个环境为.2的球体的屏幕截图:

代码如下:

<AmbientLight
intensity={.2}
/>

注意几件事——我们在最后一张照片中还有一个定向光,所以你可以看到不同之处。球体是来自定向光的白色,然而底部是黑暗的,但不是漆黑。 AmbientLight可以在实时中伪造一点全局照明或辐射。GI 是光线从其他物体反射并在现实的、非虚拟的世界中创造“填充光”的数量。Three.js 还有一个THREE.HemisphereLight用于此,你可以通过本机视图或本机桥将其添加到 React VR 中。

DirectionalLight

AmbientLightDirectionalLight,我们正在从抽象到稍微不那么抽象。 DirectionalLight实际上是用来代替太阳的。太阳的光线总是相互平行的;同样,DirectionalLight不像更接近的光源那样扩散。

这是一个DirectionalLight和没有AmbientLight的情况:

代码如下:

<DirectionalLight
  intensity={.9}
  style={{ transform: [{ rotateZ: 35 }] }}
/>

在图片中,我们把DirectionalLight稍微旋转到一侧;球体看起来很有趣,但与场景的其余部分相比不太对。这是因为全景背景的照明与场景的照明有很大不同。你会想要尝试用适当的变换语句来匹配两者与你的<DirectionalLight>

PointLight

Pointlight就像一个老式的灯泡;光从点上向各个方向扩散。关于点光和聚光灯的一个有趣的事情是,再次简化了我们的 VR 看起来真实的方法。为了避免渲染速度非常慢,大气层并没有严格建模。这意味着通常由于大气层而逐渐消失的光会在数英里之外发光(大气效应可能在我所在的地方比在你所在的地方更严重,除非你住在月球上。如果是的话,给我一张票,我会亲自去给你大声朗读这本书)。

为了避免建模大气效应,如消光(褪色)、雾、云等,PointLightSpotLight都采用了衰减和距离属性。

distance是光线照射的距离。如果它不为零,光的强度将在那个距离为零。

decay是光线消失的频率。这是一个通用的(无量纲)数字;2是物理上真实的光线衰减。0.1会使光线消失得更快,对艺术效果很有用。

例如:

<PointLight 
  intensity={1}
  style={{ transform: [ { translate: [0, 0, -5] }]}}
  distance={10}
  decay={2}
/>

为了更好地可视化前面的内容,我构建了一个演示场景三次;第一次距离为 10,第二次距离为 4,第三次距离为 4,衰减为0.1而不是 2。你可以看到第三个场景看起来非常不自然。请注意,所有三个场景的强度都是完全一样的。

如果你的点光看起来昏暗,检查一下距离参数。我建议将衰减保持在 2。

SpotLight

SpotLight就像那些在糟糕的黑色电影中对着坏家伙脸上发光的灯罩一样,或者像手电筒。与PointLight一样,它也有衰减和距离属性(如前所述)。

distancedecay属性与PointLight相同。SpotLight还有penumbraangle属性;这两个属性决定了光的扩散范围。angle是最大的外部angle,而penumbra是一个从一到 100 的数字,定义了SpotLight的柔和程度。

<SpotLight
    intensity={1}
    style={{ transform: [{ translate: [0, 2, -5] }] }}
    distance={25}
    decay={.1}
    penumbra={1}
    angle={40}
  />

目前,SpotLight 的位置定义了光线的“发光”位置。光的目标,换句话说,它指向的东西,目前在 React VR 中没有暴露出来。在撰写本书时,这个问题还没有解决。

使用 View 来包装 SpotLight 似乎也不会改变目标。

我建议不要使用 SpotLight,除非你可以安排你的场景让感兴趣的物体位于[0,0,0]。

多媒体-声音和视频

如果你听不到任何声音,世界将是一个无聊的地方。视频通常是动态网页的一部分,尽管在 VR 中,我们有一些挑战——视频本身可能不那么吸引人,除非是 360 度视频,有些人称之为 VR(它不能给你更多的超脱感,所以从我的观点来看,它并不是真正的 VR,因为你不能完全沉浸其中,但其他人可能认为它是 VR。在 VR/AR/XR 的这一点上,我们真的需要和平相处!)。

在 VR 世界中,视频可以是提供氛围的重要组成部分。如果你走进一个房间,视频正在播放,它会更像大多数家庭。

声音

在 VR 中,Sound比起最初听起来要复杂得多(双关语)。Sound节点允许将音频源放置到您的 VR 世界中。Sound会让你的世界变得生动起来。

从 React VR 手册中,考虑一个waterfall的例子:

<Image style={{height:  2.0, width:  2.0}} source={uri:  'images/waterfall.jpg'}> <Sound source={uri:  'sounds/waterfall.wav'}  /> </Image>

这个例子展示了在 React VR 中声明并添加东西是多么容易。waterfall声音简单地附加到waterfall图像的位置。如果你在 3D 世界中四处走动,你会听到瀑布就好像它就在图像所在的位置;这一切都是通过简单地将Sound组件添加为叶节点(在这种情况下是图像的子节点)来完成的。Sound节点本身不应该有任何子组件。

如果Sound节点没有附加到具有位置的对象上,它将默认为绝对位置,例如position: absolute

Sound节点有许多属性。它们如下:

  • autoPlay:布尔值

当组件加载时音频自动开始播放。默认值为true

  • loop:布尔值

当音频播放完成时自动重复。默认值为false

  • muted:布尔值 当音频被静音时。默认值为false

  • onDurationChange:(回调函数)

当声音持续时间改变时,调用此函数,带有声音持续时间的参数。

  • onEnded:(回调函数)

当音频播放完成时,将调用onEnded函数。

  • onPlayStatusChange:(回调函数)当播放状态改变时调用此函数。event.nativeEvent.playStatus:这是声音的播放状态;字符串之一为'closed''loading''error''ended''paused''playing''ready'

  • onTimeUpdate:(回调函数)当声音的currentTime改变时调用此函数。

  • event.nativeEvent.currentTime:音频文件的 currentTime

  • playControlplaypausestop

这个变量控制播放状态。如果未设置,autoPlay 的值将决定组件加载时是否播放音频。

  • playerState:(对象)

playerState 是一个控制视频播放的 MediaPlayerState,具有其内部状态。设置 playerState 时,autoPlay 的值、静音音量和 playControl 属性将被忽略,因为它们将由 playerState 而不是自己设置。参见 MediaPlayerState

  • source:(对象)

以 {uri: http} 形式的对象源音频。

  • volume:0-1.0(实际上没有限制)

音频音量的值。最小值为零,将声音静音,建议的最大值为 1.0,这也是默认值。允许大于 1 的值;这可能会导致剪切/失真,取决于音频硬件。

例如:要将音量降低 50%,设置 volume={0.5}。由于不同平台可能具有不同的音频功能(叹息),源可以是几种不同的文件格式,浏览器将选择它可以读取的适当格式。

单声道文件似乎效果最佳;并非所有浏览器都支持立体声音频文件。这是因为浏览器会将声音转换为立体声音,并尝试复制 3D 音频(这可以通过头部相关传输函数仅用两个扬声器完成)。

使用单声道文件以获得最佳兼容性。

Video

由于 Video 只是一个二维(2D)对象,它需要宽度和高度。这不是像你可能习惯的以像素为单位,而是以世界单位,出于前面讨论的原因。如果人们将视角移近或远离你的 2D 视频,它将从每英寸点数的角度改变分辨率。您可能需要尝试不同的大小和视频压缩/存储方式,以找到质量、下载速度和分辨率(颗粒度)的理想平衡。

Video 与前面在本章中描述的 VideoControl 结合使用效果最佳。

这个例子展示了一个 Video 以及一个 VideoController

<Video 
   style={{height:  3, width:  4}} source={{uri:  'assets/Video1.webm'}} playerState={this.state.playerState}  /> <VideoControl 
   style={{height:  0.2, width:  4}} playerState= {this.state.playerState}  /> 

请注意,VideoControl 不是 Video 的子级,它是一个独立的对象,有自己的位置。在这个例子中,可能播放一个 4:3 的视频,通过 this.state.playerState 协调它们的停止/开始/暂停活动。实际上,你可以将 VideoControlplayerState 视为输出,将 VideoplayerState 视为输入。

相机和观看

有一个名为LiveEnvCamera的相机对象,尽管它不是你通常期望的那样。

在大多数 CAD 系统中,相机设置基本统计数据,如焦距、焦点、相机朝向的方向等。

在 React VR 中,我们在index.vr.js的顶层有一个<View>;这是构建 VR 视图的方式。

视图的可见性参数实际上由您的物理观看设备控制。如果您的 HMD 水平视野为 110 度,您将看到 110 度的水平视野。

这是传统 3D 艺术家必须适应的事情——VR 是不同的。

同样,镜头眩光和其他效果,尽管在电影中看起来很棒,在 VR 中看起来很糟糕。你的眼睛没有镜头眩光。

不要尝试使用 React Native 添加它们。

您可能会想,我们如何移动相机?答案是您转换<View>。如果您想向前移动五米,您将视图向后移动五米,视点将移入场景。请注意,这对于SpotLight不起作用。

LiveEnvCamera

该对象显示面向环境的相机。这可能是您的观看设备的一部分,也可能不是。例如,GearVR 可能有一个面向环境的相机;Vive 有,Google Cardboard 可能没有。

默认情况下,相机是position: absolute <LiveEnvCamera />。相机图像显示在距离观看者 1000 米的几何体上。

LiveEnvCamera 可能是为增强现实AR)应用而设计的;在 React VR 中使用它充其量是实验性的。

视图

View对象既是世界中的初始场景或相机,也有助于聚合世界中的对象。在这种方式上,它与传统 CAD 程序中的组节点非常相似。作为分组节点,它对于有效的 React VR 软件非常重要,而不仅仅是主渲染循环。

我们提到视图是相机的原因是它具有布局属性和变换。如果在您的主render()循环中,您转换了<View>,实际上是在移动相机,您当前的视点所看的位置。

如果您将<View>用作分组节点,则变换将应用于其所有子节点。您可以通过适当地公开其属性和变换以此方式构建关节模型,尽管更可能是通过 glTF 文件来实现这一点。

总结

在这一章中,我们介绍了 React VR 库的基础知识,其中包括它的组件、API 和编码技术,它们将用于构建您的应用程序。足够的背景知识!现在我们已经做到了,接下来让我们在下一章中创建一个真正的 VR 应用程序。

第五章:您的第一个 VR 应用程序

好吧,四章的背景。希望您刚才浏览了最后一章,尽管我希望您会经常查看它,或者在线文档,因为您构建每个世界时都会用到。既然您已经有了这个背景,我们已经准备好构建第一个 React VR 应用程序了。我们将首先深入研究 React VR 组件、props 和状态。

您将学习以下主题:

  • 初始世界创建/建立 React VR 框架

  • 设置一个良好的背景图像

  • 如何修复背景图像,使其真正等距圆柱形

  • 添加 VR 组件

  • 创建新的 VR 关键词(类构造)

超越“你好,世界”-我们的第一个 VR 世界

这实际上不是我们的第一个 VR 应用程序,尽管我们并没有真正制作第一个应用程序,npm 安装程序做了。在第二章,“平面世界与超越-VR 编程”中,在“安装 Node.JS 之后-安装 React VR”部分,我们安装了一个简单的Hello World示例。

我们将首先创建一个新的应用程序(目录)。但首先,让我们谈谈我们正在创建的东西,为此,您将获得特别奖品!

VR 世界设计-或者,恭喜,你是新的天文博物馆馆长!

任何项目都应该开始,即使不正式地,也应该有一个设计。在这种情况下,您收到了一封电子邮件,告诉您“恭喜,您已被选为新的欧洲航天局户外博物馆馆长!”关于您的奖品,您不必担心,这次访问期间不会损坏任何天文学家的夜视。作为博物馆馆长,您可以选择与我不同的艺术品;实际上,您也可以创建一个全新的位置。

如果我们在这方面做得很好,那么我们将进入外太空,并且能够成为轨道上的第一个艺术博物馆。因为我们是第一个,所以我们将是轨道上最好的博物馆。

创建基本的 React VR 组件

React VR 有许多基本组件和代码需要安装。将近 19,987 个文件和 8,111 个目录。您不必手动安装所有这些(浏览器也不会下载所有这些,其中许多是可能打包的框架)。那么,我们如何安装所有这些?

安装所有内容只需要一步。我们将打开一个 Node.js 命令提示符,导航到您想要放置应用程序的任何目录,并创建一个新的 React VR 模板。在进入正确的目录后,键入以下代码:

react-vr init SpaceGallery

这将安装一个名为SpaceGallery的新应用程序。它将开始安装东西:

注意这一行:

这些命令通常需要很长时间才能运行,但它们显示的一些信息很重要。

在这种情况下,当我写书时,一些工具升级了,系统告诉了我。我通常建议一旦收到这些通知就立即升级;如果不这样做,您提交的任何错误都不会得到您想要的关注,并且经常会添加新功能和错误修复。

有时,您会收到关于您无法真正控制的东西的警告,比如关于connect@2.30.2的提示。

React VR init 命令将处理安装所需的所有内容,包括依赖项(有很多),然后将其全部安装。完成后,它将列出所有内容,然后退出。不用担心,不是所有这些代码都会传送到客户端。实际下载相当小,与 Unity 或 Unreal 等 VR 游戏引擎相比。

React VR init 命令甚至会告诉您如何启动您的世界。如果我们启动它,我们将看到基本上与我们在第二章中看到的hello world应用程序相同,平面世界与超越-VR 编程

首先,确保您已经停止了已经在运行的 hello world 应用程序--使用Ctrl+C,然后使用Y来停止批处理文件。然后,使用cd命令(更改目录)进入您创建的新SpaceGallery目录。

如果您无论尝试什么都似乎无法改变您的应用程序,很可能您在其他目录中留下了一个 npm 包在运行。终止所有 npm 会话并重新启动它。

继续启动它:

如果您收到错误消息yarn is not recognized as an internal or external command, operable program or batch file您可以忽略此错误;Yarn 类似于 npm。高级用户可以使用任何一个;由于 React VR 示例中使用了 npm,我将在本书中使用它。

欢迎来到开源世界,有时会有太多的选择。DuckDuckGo 是您的朋友。

让我们改变背景来让我们进入状态。

创建更大的世界-背景图像

这是一个章节 - 之前的章节涵盖了 VR 的一般背景。然而,现在我们实际上要改变背景(图像)。

你在 hello world 应用程序中看到的国际象棋世界实际上是在Pano语句(在index.vr.js文件中)中使用的全景图像。当我第一次安装 React VR 时,我以为这是默认世界中包含的一些几何图形。这是我们在SpaceGallery应用程序的视图(2D 浏览器视图):

除了 hello 文本框之外,所有内容都来自<Pano>对象:

 <Pano source={asset('chess-world.jpg')}/>

这是一个特别构造的球形全景等距投影。它被扭曲以在 360 度左/右和 180 度上/下(就像纬度和经度覆盖+/- 90 和+/- 180 一样,一个 360x180 的球形图像覆盖整个球体)。

这是chess-world.jpg,这是每次创建 VR 世界时包含的背景文件:

需要注意的几点:

  • 这是一个非常大的文件。它是 4096x2048 像素。即使如此,当你在 3D 中四处张望时,它偶尔会显得有点粗糙。这是因为当你在一个 15 英寸的笔记本电脑上看一个物品时,比如说大约 35 厘米宽,离你的眼睛大约半米远,你看到的是一个 1920 像素的图像,看起来很清晰。当你把这个图像 360 度展开时,相当于超过 17,000 像素(2pi.5m * 1920 / .35m)。

  • 大文件会导致下载速度变慢,即使在今天的世界中也是如此。

  • 现在想象一下,如果背景是一个视频。VR 具有非常高的带宽要求。这是入场的价格。

  • 文件看起来是扭曲的,但当它在浏览器和你喜欢的 HMD 中显示时,它会看起来是直的。

  • 由于直线,这张特定的图像非常适合测试;如果你直视下方或直视上方,一切都会匹配。

你可以在网上找到 360 度全景照片,但要确保它们是球形 360x180 全景照片。如果它们是由相机拍摄的,通常在图像的顶部和底部会出现奇怪的东西;许多人认为你不会直视下方或直视上方。

地图投影也是等距投影图像,所以你可能熟悉它们以及在极点处拉伸的事物。如果你把一个地图投影作为你的背景,它会看起来像你在一个地球仪里面。

可能有点奇怪。

在你的世界中直接在视点下方放置一些几何图形是个好主意,以覆盖你的球形全景图中的任何不连续或畸变。

这也有助于避免漂浮的感觉;因为Pano是二维的,而且无限远,立体深度感知无法显示Pano有多远。如果你在 VR 世界中改变你的视角,物体看起来会在地板上奇怪地移动。通过在视角下方的基本几何图形或模型语句来避免这种情况,可以使你的世界看起来更加真实。

由于我们正在谈论太空中的一个画廊,在我们进入轨道之前,让我们调查一些新的全景照片,并用它们来准备我们的背景。随意使用你喜欢的搜索引擎在网上搜索更多的全景照片。以下是我遵循的步骤:

  1. 让我们去欧洲空间局ESO)并从bit.ly/PanoESO复制一个奇妙的太空全景图像。如果你想尝试不同的分辨率,他们在下载页面上有一系列分辨率可供选择。这张图片很棒:

  1. 将其下载到我们创建新应用程序的static_assets文件夹下,然后打开index.vr.js。在那个文件中,对Pano语句进行更改:<Pano source={asset('uhd_vlt_circular_cc_eq.jpg')}/>

  2. 现在刷新你的浏览器,我们已经看到太空了:

  1. 你会注意到建筑物看起来有点奇怪。如果我们继续往下看,背景图片看起来更奇怪:

  1. 这是因为,如果你仔细看原始背景图片,它实际上不是 360x180 度,而更像是 360x90 度。这在使用手机拍摄全景图像时非常常见;很少有人捕捉顶部和底部使其成为真正的 360x180 度。真正的全景照片在底部和顶部看起来一样扭曲。例如,看看我们之前看到的平面国际象棋世界图像。

  2. 这是可以修复的。修复方法就是在原始的 360x90 全景照片底部添加一条黑色条纹(或背景颜色),就像这样:使用这种技术,你可以更或多或少地修复任何全景图像,只要它至少是 360x90;即使没有标记,这些也是相当常见的。现在,如果我们把这张全景图像放在我们的Pano语句中,或者 VR 应用看起来会好得多:为了做到这一点,我使用了一个名为Irfanview的免费图像查看/编辑程序,尽管你也可以使用 Photoshop 或任何其他图像编辑软件。我强烈推荐 Inkscape 或 Gimp;它们功能齐全并且是免费/开源的。

  3. 当我们在 VR 中查看这个并检查控制台时,我们可以看到一个错误:

THREE.WebGLRenderer: image is not power of two (2000x2000). Resized to 2048x2048

让我们通过调整大小来修复这个问题。Irfanview 有一个很好且相当快速的调整大小功能,可以保留大部分细节。只需调整大小(Ctrl+R),使其成为 2048x2048 或 2048x1024。由于我们在底部添加了黑色条纹,我们可以调整图像的大小而不会拉伸它。如果必要的话,你应该裁剪或延伸图像的底部;这些 360 度的投影很难分辨,但如果你改变图像的宽高比,当你查看它时,世界看起来会被挤压。

如果你得到了一个空白或不正确的背景,请检查图像格式和大小。

混乱世界-添加我们的第一个 VR 组件

好的,现在让我们开始添加我们的物体。

在本章开头的描述中,我们提到了移动到轨道。你可能会想知道为什么我们没有从太空背景开始。我们会的,在我们在地面上创建一个世界之后。我发现太空图像作为背景可能会让人迷失方向,没有地板,我们需要在漂浮和上下变得毫无意义之前讨论等距投影图像。所以,现在,在我们最终到达太空之前,你必须在地球上做出出色的画廊。

这带来了一个重要的观点。<Pano>语句通常被描述为一个背景。更好的方式是把<Pano>语句和背景图像看作是整个世界或者你无法触摸的世界的一部分。无论你的背景图像是什么,没有任何 VR 对象,它都会把你放在那里<Pano>实际上不仅仅是一个背景,它实际上是整个世界,除了你放置的对象之外。

这就是为什么你选择的背景对于存在感很重要。如果你选择了奇怪或者令人迷惑的背景,人们会感到迷失方向。这可能是你想要的,也可能破坏了沉浸感。此外,他们永远无法触摸到“全景”中的物体,所以如果有靠近的物体,可能会让人感到迷失方向。

因此,让我们为室外画廊的地板添加一个“平面”,这样我们就不会漂浮在我们的背景上。在index.vr.js中代码生成器生成的第一个<View>语句之后,添加以下“平面”和“盒子”元素:

export default class SpaceGallery extends React.Component {
    render() {
        return (
            <View>
//the above code is generated automatically, add your code below
//or after the <Pano> statement
<Plane 
    dimWidth={5}
    dimHeight={5}
    texture={asset('DeckPlate.jpg')}
    lit
    style={{
       transform: [
         {translate: [0,-1.8, -5]},
         {rotateX: -90}
         ] }}
/>
<Box 
          dimWidth={5}
          dimDepth={5}
          dimHeight={.1}
          texture={asset('DeckPlate.jpg')}
          lit={true}
          style={{
          transform: [{
            translate: [5.2,-1.8,.1],
          }]
          }}
        />

在保存并查看世界之前,我们需要DeckPlate.jpg文件。你需要从bit.ly/VR_Chap5下载这个文件,放在static_assets文件夹里,并将其复制到SpaceGallery文件夹内的static_assets文件夹中。我使用了一个叫做 Substance Designer 的程序,由 Allegorithmic 公司开发,位于bit.ly/AllegSub上的他们的材料分享网站。我在这里找到了这个材料bit.ly/MatSciFi01。由于你可能没有 substance player,我为你导出了这张图片,名为DeckPlate.jpg。以后,我们将构建更复杂的模型,并使用其他纹理来构建实际的材料。如果你使用 Photoshop,你也可以使用 Quixel.se 来获得良好的效果。请参阅bit.ly/QuixelSuite

我们这样做之后会发生什么?我们点击刷新,然后得到一个空白屏幕。打开你的网页开发者控制台。如果你使用的是 Firefox,点击工具|Web 开发者|切换工具(或者在 PC 上按 Ctrl+Shift+I)。

控制台将打开,我们会看到很多错误,然后是:

Expected a component class, got [object Object]

这是我们忘记添加到import指令的线索。当我们生成对象时,它会放置我们需要的所有 React VR JSX 导入,以便进行 hello world。每当我们添加新的对象或 API 时,我们需要确保 React VR 知道它。插入以下粗体行:

import {
 AppRegistry,
 asset,
 Box,
 Pano,
 Plane,
 Text,
 View,
} from 'react-vr';

在一个大型项目中,你可能偶尔会忘记这一点。如果你的场景没有改变,可以在浏览器中检查控制台。我还建议通过按字母顺序或者你使用它们的顺序添加你的import声明来进行一些代码组织。按字母顺序更容易快速扫描。你可以直接导入所有内容,但这可能会增加一些你不需要的开销。

点亮世界

一旦我们添加了导入语句,我们会注意到世界有些黑暗;可能很难看到我们添加的盒子和平面。为了照亮事物,我们将添加AmbientLightDirectionalLight(太阳或月光)到图像中。在这个应用程序中,我们有一个明显的夜间图像背景,但我们的物体将被照亮。添加AmbientLightDirectionalLight是伪造自然世界的一种简单方法。在现实世界中,物体会从附近物体反射的光线中获得光照。AmbientLight可以模拟这种柔和的发光(在其他渲染系统中,这经常被称为全局照明)。方向光模拟了室内顶部照明(许多灯光,比如在教室中)或太阳或月亮的光线。

将您的照明语句放在应用程序的render()语句顶部,这样您就可以轻松找到它们。我会把它们放在顶级<View>之后的第一条语句。

如果灯光是物体的一部分,比如一盏台灯,保持灯光靠近物体(或作为物体的子级)。

这将允许您快速修改场景照明。

我们的AmbientLight语句很简单;DirectionalLight需要更多的思考,但也很简单。按照这种方式编写它们:

        <AmbientLight
        intensity = {.3}
        />

        <DirectionalLight
        intensity = {.7}
        style={{
          transform:[{
            rotateZ: 45
          }]
        }}
        /> 

现在你应该能看到平台了,尽管它们看起来很小。

不要忘记导入语句!

为什么我让你粘贴PlaneBox

它们都成为了我们世界的地板,那么为什么我们两者都做了呢?如果你在 VR 视图中向右看,你会发现Box的边缘看起来有点奇怪。纹理映射均匀应用于所有六个面,当拉伸到整体尺寸不接近的盒子时可能看起来很奇怪。这是Box的一个缺点。Plane没有这个问题,但如果你把Plane倾斜错了方向,你可能看不到它;Plane是单面的,所以如果倾斜离开当前摄像头,它将是看不见的。Plane也是无限薄的。我包括了两者,这样你就可以看到它们的样子。

在变换部分,我提到了变换的顺序是多么重要。这对于Plane对象尤其重要;如果你先旋转,然后平移,图像可能会完全错位,在Plane的情况下,会变得看不见。

要构建整个画廊,我们需要为平台包括几个正方形。在它们之间留下一点间隙。现在,我们可以通过复制和粘贴每个 BoxPlane(使用你喜欢的任何一个!)并更新 translate 语句来实现这一点:

    {translate: [0,-1.8,-5.1]},
...
    {translate: [0,-1.8,.1]},
...
    {translate: [5.1,-1.8,.1]}

然而,有一个更好的方法;React VR 不仅仅是一个硬编码的几何文件,而是一个真正的面向对象的 JSX 文件,我们可以利用它。

让我们定义一个新对象,它是我们的甲板板块之一。我们将除了位置之外的所有内容都放入该对象的组件中。首先(我在这个问题上纠结了一段时间),将代码的第一行从:

import React from 'react';

到:

import React, {Component } from 'react';

请注意,这是一个不同的 import 语句,不同于我们在第二行使用的(很容易忽视)。

如果你看不到你创建的对象,或者得到一个你知道是有效的关键字但却没有被识别的错误,请不要忘记文件顶部的 import 指令!

得到类 - 将对象合并为新关键字

一旦我们导入了 React 组件,我们就可以将一个对象定义为 class。对于这个版本,我们将使用单独的数字作为 props(就像参数),然后根据需要实例化它们。我们可以将这段代码粘贴到任何地方;现在,你可以将它放在 index.vr.js 文件中的 export default class SpaceGallery extends React.Component 行之前。你的新 class 是:

class Platform extends Component {
  render() {
    return ( 
    <Box 
      dimWidth={5}
      dimDepth={5}
      dimHeight={.1} 
      texture={asset('DeckPlate.jpg')}
      style={{
        transform: [
              {
              translate: [ this.props.MyX, -1.8, this.props.MyZ]
              } 
              ]
          }}
    />

    );
  }
}

要实例化此对象的副本,在你的 <View> 中使用此代码(在你的 SpaceGallery 类中):

 <View>
...
 <Platform MyX='0' MyZ='-5.1'/>
 <Platform MyX='0' MyZ='0'/>
 <Platform MyX='0' MyZ='5.1'/>
 <Platform MyX='5.1' MyZ='-5.1'/>
 <Platform MyX='5.1' MyZ='0'/>
 <Platform MyX='5.1' MyZ='5.1'/>
 <Platform MyX='-5.1' MyZ='-5.1'/>
 <Platform MyX='-5.1' MyZ='0'/>

这是制作预制件或类的最直接方式,这样你就不必剪切和粘贴无休止的 Box 组件声明来制作你的地板。

如果你熟悉 React 中状态的概念,这个时候不要使用状态。你应该为随时间变化的值使用状态;我们将在第七章 给你的世界注入生命 中更多地讨论这一点,特别是第十一章 走进未知领域。由于这是应用的静态版本,你不需要它。我们将使用 props 在正确的位置构建平台。

你可能会认为,你可以用一个向量来代替两个命名参数,但是 JavaScript 没有向量的概念。但是,你可以使用 { } 运算符,粘贴适当的代码。创建我们的板块地板段的另一种方法如下:

class VecPlat extends Component {
  render() {
    return ( 
    <Box 
      dimWidth={5}
      dimDepth={5}
      dimHeight={.1} 
      texture={asset('DeckPlate.jpg')}
      style={{
        transform: [
              {
              translate: this.props.MyPos
              } 
              ]
          }}
    />

    );
  }
}

注意translate语句中没有方括号围绕this.props.MyPos,就像使用文字时那样。然后像这样实例化它:

        <VecPlat MyPos={[-5.1, -1.8, -5.1]}/>

请注意translate语句中额外的{}括号和缺少的括号,如前所述。在这种情况下,你需要它来创建向量(数组)。

把所有东西放在一起

现在我们有一个基本的平台来放我们的艺术品,你已经学会了如何创建我们可以实例化的对象,我们已经在场景中放置了基本的照明--现在让我们添加一些物体。

你从权威人士那里得知,你做得很棒,现在可以把太空画廊移到太空中。从 GitHub 的链接中,你可以下载一些新文件,包括BabbageStation_v6_r5.jpg。一旦我们用这个文件改变Pano语句,我们就会直接进入轨道--没有在加速椅子上摇晃的情况。非常安静。

让我们回到建造画廊。首先,让我们巩固并使用Platform;一旦你下载了新的static_assets,你的新index.vr.js文件应该如下:

import React, {Component } from 'react';

import {
  AppRegistry,
  asset,
  AmbientLight,
  Box,
  DirectionalLight,
  Div,
  Pano,
  Plane,
  Text,
  Vector,
  View,
  } from 'react-vr';

  class Platform extends Component {
       render() {
          return ( 
             <Box 
              dimWidth={5}
              dimDepth={5}
              dimHeight={.1} 
              texture={asset('DeckPlate.jpg')}
              style={{
                transform: [{ translate: [ this.props.MyX, -1.8, this.props.MyZ] } ]
                }}
             />
          );
      }
   }

export default class SpaceGallery extends React.Component {
    render() {
        return (
          <View>
            <Pano source={asset('BabbageStation_v6_r5.jpg')}/>
            <AmbientLight

    intensity = {.3}

    />
    <DirectionalLight
    intensity = {.7}
    style={{
        transform:[{
            rotateZ: 45
        }]
    }}
         /> 
         <Platform MyX='0' MyZ='-5.1'/>
         <Platform MyX='0' MyZ='0'/>
         <Platform MyX='0' MyZ='5.1'/>
         <Platform MyX='5.1' MyZ='-5.1'/>
         <Platform MyX='5.1' MyZ='0'/>
         <Platform MyX='5.1' MyZ='5.1'/>
         <Platform MyX='-5.1' MyZ='-5.1'/>
         <Platform MyX='-5.1' MyZ='0'/>
         <Platform MyX='-5.1' MyZ='5.1'/>

         <Text
         style={{
             backgroundColor: '#777879',
             fontSize: 0.8,
             fontWeight: '400',
             layoutOrigin: [0.5, 0.5],
             paddingLeft: 0.2,
             paddingRight: 0.2,
             textAlign: 'center',
             textAlignVertical: 'center',
             transform: [{ 
                 translate: [0, 0, -4]}]
         }}>
    Hello
  </Text>
</View>
);
    }
};

AppRegistry.registerComponent('SpaceGallery', () => SpaceGallery);

请注意,如果你使用PlatformVecPlat作为你的对象,你的代码会更短。

请注意,我使用了MyX='-5.1'而不是MyX={5.1}。这样做是可以的,但实际上是不正确的。{}用于将 JS 插入代码中。基本上,如果你想让MyX成为一个数字,就在初始化列表中使用{}。JavaScript 会进行转换,但有时如果你没有将数字作为数字传递,可能会导致奇怪的行为。

你可能已经注意到,React VR 包含的基本原语有点稀疏。没有建设性的实体几何,通常称为布尔运算,或者本地 three.js,你可以创建的东西是有限的。

幸运的是,你可以从其他 CAD 程序中导入文件。我们将在第六章《与 Poly 和 Gon 家族合作》中更多地介绍这一点,但现在,你可以导入一些我在书中的文件中包含的模型,网址是bit.ly/VR_Chap5

添加基座

在我们把艺术品放到世界上之前,我们需要建造一个基座。为了更容易对齐,我们可以像之前一样创建一个对象。让我们制作一个方形基座,并在顶部和底部放一个盖子。

如果你为了胜利而结束了一天的工作,那就重新启动 React VR 服务器吧;为了方便起见,我们可以复制 URL,这样我们就可以将其粘贴到我们的网络浏览器中:

如果您选择了前面的文本粘贴到浏览器中,您将等待很长时间。请注意地址栏中显示的“选择 npm”。

当您使用控制台启动应用程序时,如果“选择”任何文本,并保持其选定状态,可能会阻止 Web 浏览器提供内容。

因此,我们将不再使用简单的立方体作为地板,而是创建一个带有顶部和底部的方形“基座”:

class Pedestal extends Component {
    render() {
        return (
          <Box 
          dimWidth={.4}
          dimDepth={.4}
          dimHeight={.5}
          lit
          texture={asset('travertine_striata_vein_cut_honed_filled_Base_Color.jpg')}
          style={{
            transform: [ { translate: [ this.props.MyX, -1.4, this.props.MyZ] } ]
            }}
        />
          <Box 
          dimWidth={.5}
          dimDepth={.5}
          dimHeight={.1}
          lit
          texture={asset('travertine_striata_vein_cut_honed_filled_Base_Color.jpg')}
          style={{
            transform: [ { translate: [ this.props.MyX, -1.1, this.props.MyZ] } ]
            }}
        />
          <Box 
          dimWidth={.5}
          dimDepth={.5}
          dimHeight={.1}
          lit
          texture={asset('travertine_striata_vein_cut_honed_filled_Base_Color.jpg')}
          style={{
            transform: [ { translate: [ this.props.MyX, -1.7, this.props.MyZ] } ]
            }}
          />
    )
     }
     }

现在,当您尝试这样做时,您会收到一个错误:

Adjacent JSX elements must be wrapped in an enclosing tag (31:10)

请记住,这是 React VR;在常规的 React 中,您会将多个标签包含在<div>语句中。这在这里行不通,因为我们不处理 HTML;最接近的是 React-Native。因此,对于 VR,我们希望将多个元素/对象包装在<View>语句中。因此,正确的代码是这样的:

class Pedestal extends Component {
    render() {
        return (
          <View>
              <box etc='...'/>
              <snipped for='brevity'/>
          </View>
     }
     }

如果您在 Web 控制台中收到错误Expected a component class, got [object Object],可能是您意外地输入了 view 而不是View

现在我们已经设置了“基座”对象,将其粘贴到您的index.vr.js中,放在所有平台的下面:

         <Platform MyX={ 0.0} MyZ={-5.1}/>
         <Platform MyX={ 0.0} MyZ={ 0.0}/>
         <Platform MyX={ 0.0} MyZ={ 5.1}/>
         <Platform MyX={ 5.1} MyZ={-5.1}/>
         <Platform MyX={ 5.1} MyZ={ 0.0}/>
         <Platform MyX={ 5.1} MyZ={ 5.1}/>
         <Platform MyX={-5.1} MyZ={-5.1}/>
         <Platform MyX={-5.1} MyZ={ 0.0}/>
         <Platform MyX={-5.1} MyZ={ 5.1}/>

         <Pedestal MyX={ 0.0} MyZ={-5.1}/>
         <Pedestal MyX={ 0.0} MyZ={ 0.0}/>
         <Pedestal MyX={ 0.0} MyZ={ 5.1}/>
         <Pedestal MyX={ 5.1} MyZ={-5.1}/>
         <Pedestal MyX={ 5.1} MyZ={ 0.0}/>
         <Pedestal MyX={ 5.1} MyZ={ 5.1}/>
         <Pedestal MyX={-5.1} MyZ={-5.1}/>
         <Pedestal MyX={-5.1} MyZ={ 0.0}/>
         <Pedestal MyX={-5.1} MyZ={ 5.1}/>

现在,我们有一系列漂亮的“基座”。我们所做的另一件事是,通过仔细的缩放,每个物体在二维空间中都位于相同的位置[5.1 ... 0 ... -5.1]。这将使导入各种艺术品更容易。

正如我们在创建其他类或组件时所看到的,React VR 中的项目组合有点困难。它并不打算成为一个完整的 3D 建模工具;它是一个 VR 演示系统。因此,任何真正复杂的对象都应该在 CAD 系统中创建。在那里,您将拥有某种类型的视觉建模,比起尝试估计堆叠时<Box>的偏移量要容易得多。

我们将使用 Model 语句。在第四章“React VR 库”中,我们详细介绍了 Model 关键字。现在是使用它的时候了!在主代码中的<View>语句内部添加以下行,放在它下面的“基座”上。不要忘记更改import行!

<Model
  source={{
      obj: asset('teapot2.obj'),
      mtl: asset('teapot2.mtl'),
      }}
      lit
      style={{
        transform: [{ translate: [ -5.1, -1, -5.1 ] }]
        }}
  />

我从互联网来源和 Blender 中进行了一些 UV 编辑,创建了一个版本的犹他茶壶,这是'teapot2.obj'对象。在 Blender 中创建对象可能是一整本书的主题,也可能是,所以现在,您可以从书中的文件中下载茶壶。它们位于bit.ly/VR_Chap5static_assets文件夹中。

这个有点不同,因为它有一个橡胶把手,蓝色珐琅和一个铜龙头。目前,材料文件(.mtl 文件)只有简单的颜色,但在下一章中,我们将学习如何使用纹理贴图使它们丰富多彩。

如果你得到Model is not defined,这意味着你忘记在文件顶部的import行中添加Model

继续保存,你会看到我们添加了一个茶壶!然而,有一些问题 - 甲板看起来有点无聊,边缘有点奇怪,我们想看到茶壶看起来有点不同。我们可以通过在下一章中创建自己的模型来实现这一点。

摘要

恭喜!你的画廊现在完成了,只有一个简单的物体。你已经学会了如何修改我们的世界,改变背景使它看起来像我们想要的任何地方,还学会了如何创建对象组并实例化它们。接下来,你将学会如何用更多的模型来填充它;阅读下一章以了解详情!

第六章:与 Poly 和 Gon 家族合作

当刚开始接触计算机图形的人看到一些最初的 VR 图形时,他们的第一反应是“哦,不是多边形!”我的一个朋友在看到她的第一个大型多人在线角色扮演游戏时,感到恼火时说了这句话。它并不像《Money for Nothing》那样低多边形,但它非常接近。《Money for Nothing》是第一个使用计算机图形并且看起来像这样的音乐视频之一:

多边形是呈现实时图形的最佳方式。在本节中,我们将不得不制作其中的一些!您可能已经熟悉计算机辅助(设计/草图/绘图)(CAD)软件或计算机建模软件;或者您可能是一个完全的新手。有很多不同的 CAD 系统,我们将使用 Blender,一个免费的可用/开源 CAD 系统,来说明带入虚拟现实中一些重要的方式。

在本章中,您将学到:

  • 如何执行基本多边形建模

  • 如何从 Blender 中以 OBJ 形式导出模型

  • 如何应用基本 UV 纹理映射

  • 如何导出纹理贴图

  • 如何创建 MTL 文件以正确显示实时 OBJ 纹理和材质

多边形及我们为什么喜欢它们

我认为对“哦,不是多边形”感到困惑的原因是,多边形,除非它们被提升到艺术形式,如前面的音乐视频中那样,否则可能是一种创建东西的非常粗糙的方式。例如,这看起来并不像一个苹果:

许多 CAD 系统确实有其他表示形式,如均匀有理 B 样条(NURBS),这是一种曲线,或者基本上没有多边形但是它们所代表的原始图形。例如,一个球可能是任意光滑的,没有面或平坦区域。

如果一切都是立方体和球体,世界将会很无聊。除非是 Minecraft,那将会很酷。除了 Minecraft,许多 CAD 系统通过构造实体几何(CSG)来构建更有趣的对象,通过在其他原始图形上钻孔和添加基本原始图形来制作更复杂的对象。

为什么 VR 不使用一些这些技术?

一般来说,它们很慢。需要有东西将精确、准确的数学模型转换成视频硬件可以显示的东西。一些视频卡和高级 API 可以用其他东西构建对象,计算平滑曲线等等,但迄今为止,在 VR 和游戏行业中最常见的工作流程仍围绕着多边形和纹理。

因此,我们可以将多边形视为一种给定的形式。现代视频卡和高端手机在渲染对象时具有相当多的能力,尽管为了保持 VR 的帧率,我们确实需要注意多边形的数量。

好消息是,你可以用相当低的多边形数量制作出非常好看的 VR 对象。例如,看看我们的苹果。刚刚显示的低分辨率版本只有 44 个面(多边形)和 24 个顶点(点)。如果我们将多边形数量增加到 492 个,它看起来会好得多:

在这些例子中,我使用的是 Blender 版本 v2.79。你可以使用任何能读写 OBJ 文件的 CAD 程序,几乎所有的 CAD 程序都可以。我使用 Blender 是因为它是免费的,所以任何读者都可以跟着学习,而不用担心购买昂贵的 CAD 程序。

Blender 非常功能齐全,当然可以用于生产工作,尽管描述每种可能的 CAD 系统并推荐其中一种超出了本书的范围(而且我从不喜欢公开讨论宗教!)。不过从前面的模型中,你可以看到 Blender 的局限性;这个模型有相当奇怪的纹理映射,而且分辨率降低太多会在纹理贴图上产生一些奇怪的条纹。

当然,我们可以像在 2030 年的 PC 上运行一样向系统投放多边形,几乎比我们现在拥有的快 512 倍,如果摩尔定律成立的话。我们的苹果会看起来像这样:

这与中等分辨率的苹果并没有太大的不同,尽管那些奇怪的纹理线已经消失了。看起来相当不错(这不是一个完整的细节渲染)。为了使分辨率更低的多边形,我在 Blender 中进行了快速的减面。减面是 Blender 的一种方式,可以将具有大量多边形的模型减少到更少的多边形,这是一种非常方便的方式,可以将非常复杂的模型制作成虚拟现实准备。手动进行减面,并对模型应用新的纹理,可能会消除接缝。

对于高级建模者,你可以使用你的模型的低多边形版本,结合高多边形版本,制作一个法线贴图,这与凹凸贴图不同,可以让模型看起来比实际多边形更多。

你可能需要尝试一些法线贴图;这真的取决于浏览器和模型。

现在,你可能会想你更愿意使用拥有 25,206 个面的苹果。这可能有效,但这是一个相当大的模型。很多人会问“我可以使用多少多边形?”虽然这是一个很难回答的问题。这就好像问你妈妈她能把多少杂货装进车里一样?很大程度上取决于装的是什么杂货。如果她要带回一包 24 卷的舒适卫生纸,我可以告诉你,根据我的个人经验,一两卷才能装进一辆两座位的跑车里。(放心,我不是在炫耀,我的跑车已经 12 年了。)

将你的多边形预算想象成与你可能拥有的其他物体相比更好。那个高分辨率的苹果?以同样的速度(非常粗略地说),你可以拥有超过 48 个中等分辨率的苹果。

如果你要为你的太空画廊顾客提供茶点,你更愿意提供 1 个还是 48 个?

保持你的物体尽可能低分辨率,并且仍然保持你需要的视觉外观。你可能需要访问低多边形物体或一个可以减少多边形的好 CAD 系统。

说了这些之后,我从之前的模型中得到了一些相当合理的帧速率。我的目标不是给你一个绝对的数字,而是要展示顶点预算有多么重要。

什么是多边形?讨论顶点、多边形和边

如果你使用建模程序,你将不必处理这些对象的定义的复杂性。然而,偶尔你可能需要深入了解细节,因此有必要了解一些背景知识。如果你是计算机图形方面的老手,你可能已经了解很多。我确实提供了一些建议,关于如何最好地将它们引入 React VR,所以最好进行复习。

多边形是由顶点(点)、边和面组成的n边对象。面可以朝内或朝外,也可以是双面的。对于大多数实时 VR,我们使用单面多边形;当我们首次将平面放置在世界中时,我们注意到这一点,根据方向的不同,你可能看不到它。

为了真正展示这一切是如何运作的,我将展示 OBJ 文件的内部格式。通常情况下,你不会手动编辑这些文件——我们已经超越了由几千个多边形构建的 VR 时代(我的第一个 VR 世界有一个代表下载的火车,它有六个多边形,每个点都是精心手工制作的),因此手动编辑并不是必要的,但你可能需要编辑 OBJ 文件以包含正确的路径或进行模型师无法原生完成的更改——所以让我们深入了解吧!

多边形是通过在 3D 空间中创建点并用面连接它们来构建的。你可以认为顶点是通过线连接的(大多数建模工具都是这样工作的),但在 React VR 所基于的原生 WebGL 中,它实际上只是面。这些点并不是真正存在的,而是更多地“锚定”了多边形的角落。

例如,这是在 Blender 中建模的一个简单三角形:

在这种情况下,我用三个顶点和一个面(在这种情况下只是一个纯色,绿色;如果你正在阅读一本实体书或电子墨水阅读器(Kindle),当然会是灰色的一种)构建了一个三角形。边缘以黄色或浅色显示,是为了模型师的方便,不会被显式渲染。

这是我们画廊内三角形的样子:

如果你仔细看 Blender 的照片,你会注意到物体并不在世界中心。当导出时,它将以你在 Blender 中应用的平移导出。这就是为什么三角形在基座上略微偏离中心。好消息是我们在外太空中,漂浮在轨道上,因此不必担心重力。(React VR 没有物理引擎,尽管添加一个是很简单的。)

你可能注意到的第二件事是,在 Blender 中三角形周围的黄色线条(在打印中是浅灰色线条)在 VR 世界中并不持续存在。这是因为文件被导出为一个面,连接了三个顶点。

顶点的复数是顶点,不是 vertexes。如果有人问你关于 vertexes,你可以笑话他们,几乎和有人把 Bézier 曲线发音为“bez ee er”一样多。

好吧,公平地说,我曾经那样做过,现在我总是说 Beh zee a。

好了,开玩笑的时间到此为止,现在让我们让它看起来比一个平面绿色三角形更有趣。这是通过通常称为纹理映射的东西来完成的。

老实说,“纹理”和“材质”这个词经常被互换使用,尽管最近它们已经在一定程度上稳定下来,材质指的是物体外观的任何东西,除了它的形状;材质可以是它有多光滑,有多透明等等。纹理通常只是物体的颜色 - 瓷砖是红色的,皮肤可能有雀斑 - 因此通常被称为纹理贴图,用 JPG、TGA 或其他图像格式表示。

没有真正的跨软件文件格式用于材料或着色器(通常是代表材料的计算机代码)。当渲染时,有一些标准的着色器语言,尽管这些语言并不总是在 CAD 程序中使用。

你需要了解你的 CAD 程序使用的是什么,并熟练掌握它如何处理材料(和纹理贴图)。这远远超出了本书的范围。

OBJ 文件格式(通常是 React VR 使用的)允许使用多种不同的纹理贴图来正确构建材料。它还可以通过文件中编码的参数指示材料本身。首先,让我们看看三角形由什么组成。我们通过Model关键字导入 OBJ 文件:

<Model
    source={{
        obj: asset('OneTri.obj'),
        mtl: asset('OneTri.mtl'),
        }}
    style={{
            transform: [
                { translate: [ -0, -1, -5\. ] },
                { scale: .1 },
            ]
        }}
/>

首先,让我们打开MTL(材质)文件(因为.obj 文件使用.mtl 文件)。OBJ 文件格式是由 Wavefront 开发的:

# Blender MTL File: 'OneTri.blend'
# Material Count: 1

newmtl BaseMat
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.040445 0.300599 0.066583
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

其中很多是例行公事,但重要的是以下参数:

  • Ka:环境颜色,以 RGB 格式

  • Kd:漫反射颜色,以 RGB 格式

  • Ks:镜面反射颜色,以 RGB 格式

  • Ns:镜面反射指数,从 0 到 1,000

  • d:透明度(d 代表dissolved)。请注意,WebGL 通常无法显示折射材料,或显示真实的体积材料和光线追踪,所以d只是光线被阻挡的百分比。1(默认值)是完全不透明的。请注意,.obj 规范中的d适用于 illum 模式 2。

透明材料,在撰写本书时,不受 React VR 支持。然而,目前正在开发中,所以也许很快它们会被支持。

  • Tr:透明度的替代表示;0 是完全不透明。

  • illum <#>(从 0 到 10 的数字)。并非所有照明模型都受 WebGL 支持。当前的列表是:

  1. 颜色开启,环境关闭。

  2. 颜色开启,环境开启。

  3. 高亮(和颜色)<=这是正常设置。

  4. 还有其他照明模式,但目前没有被 WebGL 使用。当然,这可能会改变。

  • Ni是光学密度。这对 CAD 系统很重要,但在 VR 中支持它的机会相当低,除非有很多技巧。计算机和显卡的速度一直在不断提高,所以也许光学密度和实时光线追踪最终会得到支持,这要感谢摩尔定律(统计上,计算能力大约每两年翻一番)。

非常重要:确保在所有模型声明中包含“lit”关键字,否则加载程序将假定你只有一个发光的对象,并将忽略材料文件中的大部分参数!

你已经被警告了。它看起来会很奇怪,你会完全困惑。别问我为什么我知道!

OBJ 文件本身包含了几何图形的描述。这些通常不是你可以手动编辑的东西,但是看到整体结构是很有用的。对于之前显示的简单对象,它是相当容易管理的:

# Blender v2.79 (sub 0) OBJ File: 'OneTri.blend'
# www.blender.org
mtllib OneTri.mtl
o Triangle
v -7.615456 0.218278 -1.874056
v -4.384528 15.177612 -6.276536
v 4.801097 2.745610 3.762014
vn -0.445200 0.339900 0.828400
usemtl BaseMat
s off
f 3//1 2//1 1//1

首先,您会看到一个注释(用#标记),告诉您是什么软件制作的,以及原始文件的名称。这可能会有所不同。mtllib 是对特定材质文件的调用,我们已经看过了。o 行(如果有组,还有g 行)定义了对象和组的名称;尽管 React VR 目前并不真正使用这些,但在大多数建模软件中,这将列在对象的层次结构中。vvn 关键字是有趣的地方,尽管这些仍然不是可见的东西。v 关键字在 x、y、z 空间中创建一个顶点。稍后将连接构建的顶点成多边形。vn 建立了这些对象的法线,vt 将创建相同点的纹理坐标。稍后再详细讨论纹理坐标。

usemtl BaseMat 建立了在接下来的面中将使用的材质,该材质在您的.mtl 文件中指定。

s off 意味着关闭了平滑。平滑和顶点法线可以使物体看起来光滑,即使它们由很少的多边形制成。例如,看看这两个茶壶;第一个没有平滑。

看起来很像计算机图形,对吧?现在,看看在整个文件中指定了“s 1”参数,并且法线包含在文件中的相同茶壶。这是相当正常的(双关语),我的意思是大多数 CAD 软件会为您计算法线。您可以使法线光滑、锐利,并在需要时添加边缘。这可以在不增加多边形的情况下增加细节,并且渲染速度快。

光滑的茶壶看起来更真实,对吧?好吧,我们还没看到最好的!让我们讨论纹理。

我过去不喜欢寿司是因为口感。我们不是在谈论那种口感。

纹理映射很像是用一张圣诞包装纸包裹一个奇形怪状的物体。就像在圣诞节收到那个奇怪的礼物,不太知道该怎么做一样,有时包装并没有明确的正确方式。盒子很容易,但大多数有趣的物体并不总是一个盒子。我在网上找到了这张带有标题“我希望是 X-Box.的照片。

“包裹”是通过 CAD 系统中的 U、V 坐标完成的。让我们来看一个带有正确 UV 坐标的三角形。然后我们去拿我们的包装纸,也就是说,我们拿一张图像文件作为纹理,就像这样:

然后我们在 CAD 程序中将其包装起来,指定它作为纹理贴图。然后我们将三角形导出,并放入我们的世界中。

您可能期望在纹理贴图上看到“左侧和底部”。在我们的建模软件(仍然是 Blender)中仔细观察后,我们发现默认的 UV 映射(使用 Blender 的标准工具)尝试尽可能多地使用纹理贴图,但从艺术角度来看,可能并不是我们想要的。

这并不是要表明 Blender 是“你做错了”,而是要说明在导出之前您必须检查纹理映射。此外,如果您尝试导入没有 U、V 坐标的对象,请再次检查它们!

如果您正在手动编辑.mtl 文件,并且您的纹理没有显示出来,请仔细检查.obj 文件,并确保您有vt行;如果没有,纹理将不会显示出来。这意味着纹理映射的 U、V 坐标没有设置。

纹理映射并不是一件简单的事情;关于它有很多艺术性的东西,甚至有整本书专门讲述纹理和光照。话虽如此,如果您从互联网上下载了一些东西并希望让它看起来更好一些,您可以通过 Blender 和任何 OBJ 文件来取得相当大的进展。我们将向您展示如何修复它。最终目标是获得一个更可用和高效的 UV 贴图。并非所有的 OBJ 文件导出器都会导出正确的纹理贴图,而且您在网上找到的.obj 文件可能有 UV 设置,也可能没有。

您可以使用 Blender 来修复模型的展开。虽然这不是一个 Blender 教程,但我会在这里向您展示足够的内容,让您可以通过一本关于 Blender 的书(Packt 有几本很好的 Blender 书)来快速入门。您也可以使用您喜欢的 CAD 建模程序,比如 Max、Maya、Lightwave、Houdini 等等。(如果我错过了您喜欢的软件,请原谅我!)。

这很重要,所以我会在信息框中再次提到它。如果您已经使用了不同的多边形建模器或 CAD 页面,您不必学习 Blender;您的程序肯定会正常工作。您可以略过这一部分。

如果你不想学习 Blender,你可以从 Github 链接下载我们构建的所有文件。如果你要通过示例进行工作,你将需要一些图像文件。本章的文件位于:bit.ly/VR_Chap7

获取 3D 模型的途径

这就引出了一个简短的分歧。首先,你从哪里得到这些模型?

获取 3D 模型的最佳方式是自己制作。如果你这样做,你可能不会读到这里,因为你已经知道多边形是什么,以及如何给它们贴图。然而,更有可能的是你会去付费或免费的模型网站下载你觉得吸引人的东西,用于你想要创建的世界。这只是为了节省时间。以下是我多年来发现有用的一些网站的简要介绍。其中一些网站可能有非常昂贵的模型,因为它们经常迎合高端图形公司(电视、建筑、电影、设计师),以及高质量但昂贵的游戏艺术。游戏艺术是你要寻找的,以做好 VR;一些网站现在有“低多边形”或 VR/AR 类别。其中一些,特别是 ShareCG 和 Renderosity,在某些地方往往非常业余。网站本身很棒,但上传的文件经常没有编辑控制;因此,你可能会找到侵犯版权的东西(星球大战和星际迷航模型),这是因为律师的明显原因,你在其他网站上找不到这些东西。另一方面,你可能会在这些网站上找到别人正在赚钱的你自己的内容,因此想找到你自己的律师。

说到律师,你需要检查任何你下载的文件的许可证。例如,你可能有权使用这些模型进行渲染,但不能进行分发。这可能允许你在游戏中使用这些模型,或者可能需要额外(更昂贵)的许可证。

一些网站(绝非独家)可以下载模型,包括:

为什么你会在这些网站上找到这么好的模型?为什么一些模型看起来如此奇怪,艺术性如此之高?许多艺术家有一些不需要排他性的合同,或者人们正在制作一个游戏,但最终没有发布。他们可以上传这些未使用或较少使用的模型,让其他人使用,并甚至从销售中获利。

你可以花上几天的时间在所有这些网站上搜索适合你网站的完美内容。

你已经被警告了!

还有许多旨在用于 3D 打印的 3D 模型网站。这些模型可能非常密集(高多边形),但可能有一些你可以使用的内容。

我喜欢使用一个叫做“Poser”的程序来进行人体建模,尽管许多 CGI 艺术家更喜欢自己制作。DAZ3D 也出售人体模型,其中许多可以与 Poser 一起使用。这两个网站都是廉价、合理质量渲染的良好资源网站(取决于你设置场景的技能)。Poser 程序有许多专门用于对象、场景、模型和纹理的网站可供使用。由于高多边形数量和非常密集的纹理,Poser 人体模型在 VR 中显示效果不佳,但这些网站可能仍然有物体和附加工具,通常价格非常合理。

一些拥有良好 Poser 模型的网站,以及许多其他免费物体的网站是:

本书中的几幅图像是用 Poser 和 DAZ Studio 完成的。

总结

在这一章中,你学会了使用 Blender 进行多边形建模的基础知识。你已经了解了多边形预算的重要性,如何导出这些模型,以及关于 OBJ/MTL 文件格式的细节。你还学会了我们可以在哪里获取我们世界的 3D 模型。

这些物体看起来可能很普通;然而,在下一节中,你将学会如何在茶壶周围包裹一张纸。这不仅是一种给人们礼物的技能,它对于使我们的虚拟世界看起来真实将是至关重要的。

第七章:坐在(虚拟)茶壶旁

在上一章中,我们了解了很多关于多边形以及如何在实时图形中使用它们的知识。我们将继续使用多边形,并学习更多关于给它们贴图的知识。

在本章中,我们将学习以下内容:

  • 如何使用 Blender 的基础知识

  • 如何应用基本的 UV 纹理映射

  • 如何导出纹理映射

  • 如何创建 MTL 文件以正确显示实时 OBJ 纹理和材质

  • 为我们的茶壶画廊整合一切

Blender 只是许多多边形建模器之一,您可以使用它来制作用于 WebVR 的虚拟对象。如果您已经熟悉多边形建模的概念,并且创建和编辑 UV 映射,那么您实际上不需要本章的大部分内容。一旦我们完成 UV 映射,我们就将模型导入到世界中。我还将本章的静态文件放在了bit.ly/VR_Chap7,这样您就可以下载它们,而不是自己构建它们。

UV 建模可能会很乏味。如果您只是下载文件,我不会介意的。但请浏览以下内容,因为我们构建这些模型时,我们将把它们放在虚拟世界中。

在 Blender 中的茶壶

要学习如何 UV 映射,让我们在 Blender 中放一个茶壶。今天,这将运行得相当顺利,但通常茶壶不会适合在 Blender 中。

您可以在blender.org下载 Blender。在那里,我强烈推荐网站上的教程bit.ly/BlendToots。Packt 还有很多关于 Blender 的好书。您可以在bit.ly/BlenderBooks找到这些书。如果您还没有通过这些教程,对基本的光标移动和选择可能会感到有些困惑或沮丧;看到光标移动的动画比写作更有帮助。特别是,请观看入门下的光标选择教程:bit.ly/BlendStart

为了开始贴图,我们将使用 Martin Newell 的著名的“犹他州茶壶”。这是计算机图形学中更著名的“测试模型”之一。这是原始的犹他州茶壶,目前在加利福尼亚州山景城的计算机历史博物馆展出(由 Marshall Astor 提供):

计算机图形学版本被压扁在演示中,这种压扁是固定的。您可以在bit.ly/DrBlinn了解更多信息。

这是 Blender 中的茶壶。您可以通过在首选项中打开额外形状来到这里:

  1. 点击菜单文件,然后用户首选项(文件->用户首选项),然后点击额外对象:

  1. 不要忘记然后点击屏幕底部的按钮“保存用户设置”,否则下次进入时对象将不在那里。保存后,关闭 Blender 用户首选项窗口。

  2. 然后,在 3D 窗口底部的菜单上,点击“添加->网格->额外->茶壶+”:

  1. 一旦你这样做了,仅供教学目的,选择左下角窗格上的分辨率为 3,如图所示。

增加茶壶的分辨率是相当不错的;如果我早点注意到这一点,写这一章节时就可以节省我一个小时在互联网上搜索了。我们将其更改为 3,以使多边形更大,这样在进行本教程时更容易点击。

  1. 然后,您要在 3D 窗口中点击茶壶(左键)以选择它;然后茶壶将有一个橙色的轮廓。然后通过点击对象菜单旁边的“对象模式”一词,返回到编辑模式,然后选择“编辑模式”:

一旦你进入编辑模式,我们需要在选择茶壶的多边形时能够看到 UV 贴图。最初,可能不会有 UV 贴图;继续跟着我们,我们会创建一个。

  1. 将鼠标放在时间轴窗口上方的细线上,在屏幕底部的窗口(以下截图中用红色圈出的区域)上拖动窗口向上。这将为窗口留出足够的空间。

  1. 我们不做动画,所以我们不需要那个窗口,我们会把它改成 UV 显示。要做到这一点,点击时间轴显示的小时钟图标(哇,还记得模拟时钟吗?),选择 UV/Image Editor:

这只是改变窗口布局的一种方式。在 Blender 中令人困惑的一点是,你可能会因为不小心点击了一些东西而真正搞乱你的用户界面,但其中一个很棒的地方是你可以通过鼠标点击轻松地创建窗口、子窗口、拉出、架子等等。我刚刚向你展示的方法是教学中最直接的方式,但对于真正的工作,你应该按照自己的意愿自定义窗口。

一旦你改变了这个视图,请注意你可以像其他 Blender 窗口一样放大、平移和移动窗口。关于如何放大、平移等等,你应该观看位于bit.ly/BlendStart的教程视频文件。

  1. 所以,我们可以看到我们的模型使用我们的纹理是什么样子的;点击“打开”并找到一个你想要映射到你的茶壶(或模型)上的纹理文件。我正在使用ButcherTile_Lettered.jpg

  2. 完成后,进行第一次 UV 展开!在上窗口的菜单中,点击 Mesh->UV Unwrap->Unwrap,就像这样:

在底部窗口,它会显示出纹理的展开情况。

看起来很糟糕。你的结果可能会因不同的模型而有所不同。

为什么这个 UV 贴图看起来很糟糕?从实时图形的角度来看,它并不糟糕;它将所有多边形都打包到一个纹理贴图上,这将有助于视频卡的内存:

对于一些物体来说,这可能没问题。如果你看右上角和右下角,我们可以看到壶嘴和手柄,它们看起来有点奇怪。渲染出来可能会有点滑稽;让我们看看它的效果。为了做到这一点,我们必须分配一些纹理,然后导出茶壶。(我们稍后会介绍导出;现在,我们只需要看到我们在 Blender 中还有额外的工作要做。)

请注意,你可以通过在 Blender 内部渲染来快速查看,但这可能会让你失望,因为 Blender 几乎肯定会以完全不同的方式渲染你的模型。总体的颜色和纹理将是相同的,但 React VR 和 WebGL 能够实现的更微妙(也更重要)的纹理细节将会丢失(或者更好的是,使用离线、非实时渲染器);相反,如果你真的在 Blender 中工作或者想要更好的效果,渲染可以产生惊人的作品。

例如,在 Blender 中,使用循环渲染器,渲染我们的茶壶花了 11.03 秒。

在 React VR 中,为了保持至少 60 帧每秒,这必须在不到 0.016 秒内完成。而 Blender 花了 600 多倍的时间来生成相同的图像;难道它不应该看起来更好吗?茶壶看起来并不差,但 UV 映射只是很奇怪。

我们可以看到方块在茶壶上有点奇怪地拉长了。(如果你停下来想想我们在做什么,我们只是在茶壶上放了一个瓷砖图案;这就是计算机图形的奇迹。我正在使用棋盘格图案,所以我们可以看到壶上的拉伸。以后,我会用 Substance Designer 制作一个更好的纹理。)

你可以在 Blender 中进行实验,点击多边形(在编辑模式中),看看该多边形在 UV 映射中的位置。为了辩护 Blender,这个映射并不是很糟糕,只是不是我们想要的。有时(几乎总是),需要一个人来真正创作艺术。

修复茶壶的 UV 映射

为了更容易地给壶上纹理,首先让我们为壶嘴、手柄和盖子创建单独的材料。这将使我们的纹理地图更大,拉伸得更少。你也可以通过将纹理打包在一个更大的位图中来做到这一点,老实说,有时这对于 VR 来说更好一些;总体方法是相同的,只是更多地打包在一个较小的区域内。

让我们为壶、手柄、壶嘴和盖子创建四种材料(你应该仍然处于编辑模式)。

  1. 点击那个看起来有点像闪亮的地球的小图标。然后,点击“+”键四次,如图所示,然后点击“新建”:

  1. 一旦你点击了“+”键四次,你将有四个我们正在创建的材料的插槽。然后你点击“新建”来实际添加一个材料。这似乎有点笨拙,但这就是 Blender 的工作方式:

  1. 点击“新建”时,你会得到一个 Material.001:

  1. 你可以点击红圈中的区域并更改名称。这样,创建四种材料,如下所示:

  2. 创建一个壶材料(将是陶瓷涂层金属)。

  3. 创建一个盖子材料(和壶一样的纹理)。

  4. 创建一个壶嘴材料(让我们把它做成铜制的)。

  5. 创建一个手柄材料(让我们把它做成磨损的橡胶)。

我们并不真的需要创建这些材质;你可以在几个 UV 上叠加相同的纹理贴图,但我想对茶壶进行一次新的尝试(正如我们所看到的,它是一个实心的陶瓷制品),看到不同的材质是有益的。

现在这些额外的材质已经创建,你可以移动 UV 以更好地映射对象。UV 映射是一个庞大的主题,需要一定的技术和艺术技能才能做好,或者 PC 可以自动完成。这超出了本书的范围,但我会向你展示一个快速而粗糙的方法来对一些常见的物体进行 UV 映射。你在网上找到的许多文件可能没有应用良好的 UV 映射,所以你可能会发现自己处于这样一种情况,你认为自己不需要学习建模,但会用它来纠正 UV 映射(这在多边形建模时是一个相当高端的活动!)。

一旦你创建了这四种材质,你可以将每个部分独立地映射到自己的 UV 映射上;当我们在 VR 世界中展示时,我们将为每个部分使用不同的纹理贴图。如果你想制作一个单独的陶瓷壶,你可以使用相同的纹理贴图,但我们破旧的金属壶可能看起来更好。

这是艺术;美在于观者的眼中。

一旦你像上面那样确定了四种材质,选择每个主要区域的多边形,然后点击“分配”使它们成为这种材质的一部分:

  1. 按下键盘上的“A”键(或选择->(取消)选择所有| A)取消选择所有的多边形。然后我们将选择每个区域的多边形,盖子、把手、壶嘴和壶(主体)。

  2. 切换到“多边形选择”。Blender 有不同的选择模式-点、线、多边形。对于这个,你需要切换到选择多边形,点击这个图标:

  1. 点击主壶多边形,使用Shift + 点击选择多个多边形。Blender 拥有丰富的选择工具,如框选等,可以参考教程:bit.ly/BlendStart

  2. 一旦你选择了主体的多边形,点击“分配”按钮将该多边形分配给一个材质,比如“壶”材质。

  1. 一旦你分配了多边形,点击“视图->前视”,然后点击“网格->UV 展开->圆柱投影”。然后在我们之前设置的图像编辑器中会有一个 UV 映射,尽管它会从你分配的图像上拉伸出来。

  2. 要解决这个问题,在屏幕下半部分的菜单中,选择 UVs->Pack Islands:

这是基本的纹理映射。你可以对此进行很多调整(这可能会让人沮丧)。Blender 有许多有用的自动 UV 分配工具;在 3D(建模)窗口中,正如我们之前看到的那样,Mesh->UV Unwrap->(选项)提供了许多解包的方法。我发现从视图投影以及圆柱投影,都可以从严格的上/下/左/右视图中很好地展开 UV。在说了这些之后,一些艺术性就会发挥作用。壶嘴、壶盖和手柄比壶身小,所以如果你希望你的纹理与主要的壶和纹理更或多或少地对齐,你可能需要浪费一些 UV 空间并将这些部分缩小。

或者你可以从 GitHub 文件中下载teapot2.objteapot2_Mats.mtl,并节省一些理智:bit.ly/VR_Chap7

这四个 UV 映射不错(但是请随意学习,研究,做得更好!我不是艺术家!)。主体的 UV 映射,壶的材质在这里显示:

盖子材质的 UV 映射:

手柄材质的 UV 映射(故意缩小,以使方块与主壶更或多或少对齐):

壶嘴材质的 UV 映射(故意缩小,以使方块与主壶更或多或少对齐):

使用这些 UV 分配,我们的茶壶显示两次,在每次之间略微旋转,看起来好多了:

你可以对 UV 进行很多调整。在前面的截图中,如果我们要在壶上映射大部分是瓷砖方块的纹理,我们可以看到,尽管手柄和壶嘴与主体相匹配得很好,但是盖子,虽然看起来没有我们第一张图片那样拉伸,但仍然比其他方块小一点。解决这个问题的方法是进入 3D 面板,仅选择盖子多边形(首先按下"a"直到没有选择任何内容),转到属性选项卡中的材质,点击盖子材质,“选择”以选择所有多边形,然后转到 UV 窗口,将 UV 映射的多边形缩小一点。

然而,在我们的情况下,无论如何,我们都希望为这些物品制作完全不同的材料,所以在这一点上过于担心 UV 可能是错误的。

你的效果可能会有所不同。

导入材料

同时,我们可以利用 React VR 在材料方面提供的所有功能。不幸的是,MTL 文件并不总是具有可能的值。如果您使用的是现代材料,具有基本颜色、凹凸贴图或法线贴图、高度、镜面(光泽)或金属(类似于光泽)贴图,我发现您可能需要手动编辑 MTL 文件。

你可能会认为有这么多的计算机图形程序,我们不会到这一步。不幸的是,不同的渲染系统,特别是基于节点的系统,对于 OBJ 导出器来说太复杂,无法真正理解;因此,通常随 OBJ 文件一起使用的大多数 MTL 文件(材料)只有基本颜色作为纹理贴图。

如果您使用 Quixel 或 Substance Designer 等程序,大多数基于物理的渲染PBR)材料由以下大部分纹理贴图(图像)组成,这也受到 OBJ 文件格式的支持:

  • 基本颜色:这通常是材料的外观,几乎总是与大多数 CAD 系统一起导出到 OBJ(MTL)文件中作为map_Ka

  • 漫反射贴图:通常与基本颜色相同,它是物体的“漫反射”颜色。您可以将其实现为map_Ka

  • 凹凸贴图:凹凸贴图是“高度”信息,但不会物理变形多边形。它们看起来像是被雕刻的,但如果你仔细看,多边形实际上并没有位移。这可能会在 VR 中引起问题。你的一只眼睛会说这是凹陷的,但你的立体深度感知会说不是。然而,在适当的情况下,凹凸可以让事物看起来非常好。在 MTL 文件中写为bump

  • 高度贴图:与凹凸贴图非常相似,高度贴图通常会在物体表面上物理位移多边形。然而,在大多数网络渲染中,它只会位移建模的多边形,因此比离线渲染器要不太有用。(游戏引擎可以进行微位移。)

  • 法线贴图:法线贴图是一种 RGB 表示,比高度或凹凸贴图更复杂,后者是灰度。法线贴图是 RGB 贴图,可以使多边形向位移,而不仅仅是上下。现代游戏引擎会从高分辨率(数十万到数百万)模型计算法线贴图到低分辨率模型。它使得简单多边形的物体看起来像是由数百万多边形构建而成。它可能会或可能不会在物体上产生物理变形(取决于着色器)。它不受 OBJ/MTL 文件格式直接支持,但受到 WebGL 和 three.js 的支持,尽管实现留给读者自行完成。

  • 高光贴图:这控制着物体的光泽度。通常是灰色贴图(没有颜色信息)。更具体地说,高光贴图控制着纹理的某个区域是否有光泽。这是 map_Ns。Map_Ks 也是高光贴图,但控制着高光的颜色。例如,可以用于汽车上的“幽灵漆”。

  • 光泽度:与高光不完全相同,但经常被混淆。光泽度是指高光的亮度;它可以是宽泛但有光泽,如暗橡胶,也可以是紧致而有光泽,如糖苹果或铬。基本上是应用于高光贴图的。通常与 PBR 一起使用,不受 OBJ/MTL 文件格式支持。

  • 粗糙度:与高光和光泽度贴图非常相似,通常是替代或与前者一起使用。通常与 PBR 一起使用,不受 OBJ/MTL 文件格式支持。

  • 反射率:一般来说,OBJ 文件格式用于离线渲染,进行射线追踪反射,近似模拟真实世界的工作方式。出于性能原因,WebGL 并不对所有内容进行射线追踪,但可以使用反射贴图模拟反射。在 OBJ 文件中,反射的程度是静态的;你无法直接制作斑驳的反射。这个贴图在 OBJ 文件中被编码为refl,但在 OBJ/MTL 文件格式中,React VR 不模拟它。

  • 透明度:映射为dmap_d。(d 在原始 MTL 文件中代表“密度”)。这不是折射透明度;光线要么穿过要么不穿过。对于玻璃瓶之类的物体很有用,但 React VR 不使用。

  • 贴花:这会在物体顶部应用模板,并且非常有用,可以避免重复的纹理外观,并在顶部添加文字。在 MTL 中,文件被编码为decal。这可能非常有用,并且在 React VR 中支持贴花。但是,我发现大多数建模者不会导出它,因此您可能需要手动编辑材质文件以包含贴花。这并不太糟糕,因为通常您的世界中的不同模型将具有不同的贴花(例如标志、污渍等)。

修复甲板板

现在我们已经学会了如何进行 UV 映射,让我们修复那些用来表示甲板板的立方体。在对基本的 React VR 对象进行纹理处理时,我们发现,立方体在所有六个面上都表示相同的纹理。因此,当我们制作一个薄的立方体,就像我们为基座的顶部和底部或甲板板所做的那样时,纹理贴图在侧面看起来“挤压”。红色箭头显示了挤压的纹理;这是因为我们有一个高度只有.1,宽度为 5 的盒子,而纹理是正方形的(双重红色箭头),所以看起来被挤压了。

我们可以在 Blender 中用一个立方体来修复这个问题。我们还将添加我们下载的额外纹理贴图。

我有 Substance Designer,这是一个很棒的纹理工具;还有许多其他工具,比如 Quixel。它将根据您的设置输出不同的纹理贴图。您还可以使用各种软件包来烘焙纹理。WebGL 将允许您使用着色器,但这有些复杂。它通过 React Native 支持,但目前有点困难,因此让我们讨论不同材质值的个别纹理贴图的情况。通常在.obj 文件中,这将会分解为这样的情况(.obj 没有现代 GPU 着色器的概念):

  1. 在 Blender 中创建一个立方体,并调整其大小(在编辑模式中),使其比宽或高短得多。这将成为我们的甲板板。在我们的 VR 世界中,我们将其设置为 5x5x.1,因此让 Blender 立方体也设置为 5x5x.1。

  2. 然后,我们粗略地对其进行纹理贴图,如下所示:

  1. 将其导出为 OBJ 并选择以下参数;重要的参数是-Z 向前,Y 向上(Y 向上!)和 Strip Path(否则,它将包括您的物理磁盘位置,显然无法从 Web 服务器中调用):

一旦完成这些,我们将以困难但直接的方式来做,即修改甲板板的 MTL 文件,直接包含我们想要的纹理:

# Blender MTL File: 'DeckPlate_v1.blend'
# Material Count: 1 newmtl Deck_Plate

Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2
map_Kd 1_New_Graph_Base_Color.jpg
bump -bm 0.01 1_New_Graph_Height.jpg # disp will be mostly ignored, unless you have a high-polygon cube
# disp -mm .1 5 1_New_Graph_Height.png
map_Ks 1_New_Graph_Metallic.jpg

位移纹理有点无用;当前的渲染引擎会应用位移贴图,但不会自动细分任何多边形以实现微位移。因此,你必须生成具有尽可能多多边形的几何体来进行位移。

如果你生成了那么多多边形,更好的方法是在建模程序中直接烘烤位移,并导出已经位移的多边形。这样无论如何都是相同数量的多边形,而且你有更多的控制。你也可以选择性地减少多边形数量,并仍然保留你的表面细节。

烘烤位移会显着增加场景中的顶点和多边形数量,所以这是一个权衡。在离线渲染器(非虚拟现实渲染)中使用位移贴图通常是为了减少多边形数量,但并不总是适用于虚拟现实。可能虚拟现实着色器会进行微位移和自适应细分,因为技术不断前进。

如果你得到一个刺眼的白色纹理,或者某些东西看起来不像你期望的那样,双重检查 node.js 控制台,并寻找 404,就像这样:

Transforming modules 100.0% (557/557), done.

::1 - - [20/Sep/2017:21:57:12 +0000] "GET /static_assets/1_New_Graph_Metallic_Color.jpg HTTP/1.1" **404** 57 "http://localhost:8081/vr

/?hotreload" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:57.0) Gecko/20100101 Firefox/57.0"

这意味着你拼错了纹理名称。

然后,我们将使用面向对象的设计编码来修改我们创建的类,这将更新所有的甲板板!将平台调用更改为新的甲板板 OBJ 文件,而不是一个盒子。

完成的虚拟现实世界

你的完整代码应该是这样的:

import React, {Component } from 'react';

import {
  AppRegistry,
  asset,
  AmbientLight,
  Box,
  DirectionalLight,
  Div,
  Model,
  Pano,
  Plane,
  Text,
  Vector,
  View,
  } from 'react-vr';

class Pedestal extends Component {
    render() {
        return (
          <View>
          <Box 
          dimWidth={.4}
          dimDepth={.4}
          dimHeight={.5}
          lit
          texture={asset('travertine_striata_vein_cut_honed_filled_Base_Color.jpg')}
          style={{
            transform: [ { translate: [ this.props.MyX, -1.4, this.props.MyZ] } ]
            }}
        />
          <Box 
          dimWidth={.5}
          dimDepth={.5}
          dimHeight={.1}
          lit
          texture={asset('travertine_striata_vein_cut_honed_filled_Base_Color.jpg')}
          style={{
            transform: [ { translate: [ this.props.MyX, -1.1, this.props.MyZ] } ]
            }}
        />
          <Box 
          dimWidth={.5}
          dimDepth={.5}
          dimHeight={.1}
          lit
          texture={asset('travertine_striata_vein_cut_honed_filled_Base_Color.jpg')}
          style={{
            transform: [ { translate: [ this.props.MyX, -1.7, this.props.MyZ] } ]
            }}
          />
     </View>
    )
     }
     }

         class Platform extends Component {
             render() {
                 return ( 
                    <Model
                    source={{
                        obj: asset('DeckPlate_v1.obj'),
                        mtl: asset('DeckPlate_v1_AllMats.mtl'),
                        }}
                        lit
                        style={{
                            transform: [ {
                            translate: [ this.props.MyX, -1.8, this.props.MyZ]
                        }] }}
                    /> 

        );
          }
         }

export default class SpaceGallery extends React.Component {
    render() {
        return (
          <View>
            <Pano source={asset('BabbageStation_v6_r5.jpg')}/>
            <AmbientLight

    intensity = {.3}

    />
    <DirectionalLight
    intensity = {.7}
    style={{
        transform:[{
            rotateZ: -45
        }]
    }}
         /> 
         <Platform MyX={ 0.0} MyZ={-5.1}/>
         <Platform MyX={ 0.0} MyZ={ 0.0}/>
         <Platform MyX={ 0.0} MyZ={ 5.1}/>
         <Platform MyX={ 5.1} MyZ={-5.1}/>
         <Platform MyX={ 5.1} MyZ={ 0.0}/>
         <Platform MyX={ 5.1} MyZ={ 5.1}/>
         <Platform MyX={-5.1} MyZ={-5.1}/>
         <Platform MyX={-5.1} MyZ={ 0.0}/>
         <Platform MyX={-5.1} MyZ={ 5.1}/>

         <Pedestal MyX={ 0.0} MyZ={-5.1}/>
         <Pedestal MyX={ 0.0} MyZ={ 0.0}/>
         <Pedestal MyX={ 0.0} MyZ={ 5.1}/>
         <Pedestal MyX={ 5.1} MyZ={-5.1}/>
         <Pedestal MyX={ 5.1} MyZ={ 0.0}/>
         <Pedestal MyX={ 5.1} MyZ={ 5.1}/>
         <Pedestal MyX={-5.1} MyZ={-5.1}/>
         <Pedestal MyX={-5.1} MyZ={ 0.0}/>
         <Pedestal MyX={-5.1} MyZ={ 5.1}/>

         <Model
            source={{
                obj: asset('teapot2.obj'),
                mtl: asset('teapot2.mtl'),
                }}
                lit
                style={{
                    transform: [{ translate: [ -5.1, -1, -5.1 ] }]
                    }}
            />
            <Model
            source={{
                obj: asset('Teapot2_NotSmooth.obj'),
                mtl: asset('teapot2.mtl'),
                }}
                lit
                style={{
                    transform: [{ translate: [ -5.1, -1, 0 ] },
                    { rotateY: -30 },
                    { scale: 0.5} ]

                    }}
            />

            <Model
            source={{
                obj: asset('Chap6_Teapot_V2.obj'),
                mtl: asset('Chap6_Teapot_V2.mtl'),
                }}
                lit
                style={{
                    transform: [{ translate: [ -5.1, -1, 5.2 ] },
                    { rotateY: -30 },
                    { scale: 0.5} ]
                }}
            />

            <Model
            source={{
                obj: asset('Chap6_Teapot_V5_SpoutDone.obj'),
                mtl: asset('Chap6_Teapot_V5_SpoutDone.mtl'),
                }}
                lit
                style={{
                    transform: [{ translate: [ 5.1, -1, 0 ] },
                    { rotateY: -30 },
                    { rotateX: 45 },
                    { scale: 0.5} ]

                    }}
            />

            <Model
            source={{
                obj: asset('Chap6_Teapot_V5_SpoutDone.obj'),
                mtl: asset('Chap6_Teapot_V5_SpoutDone.mtl'),
                }}
                lit
                style={{
                    transform: [{ translate: [ 5.1, -1, 5.1 ] },
                    { rotateY: 46 },
                    { scale: 0.5} ]

                    }}
            />
        <Text
            style={{
                backgroundColor: '#777879',
                fontSize: 0.1,
                fontWeight: '400',
                layoutOrigin: [0.0, 0.5],
                paddingLeft: 0.2,
                paddingRight: 0.2,
                textAlign: 'center',
                textAlignVertical: 'center',
                transform: [ 
                    {translate: [-5.2, -1.4, -4.6] }]
                    }}>
            Utah teapot
        </Text>
        <Text
            style={{
                backgroundColor: '#777879',
                fontSize: 0.1,
                fontWeight: '400',
                layoutOrigin: [0.0, 0.5],
                paddingLeft: 0.2,
                paddingRight: 0.2,
                textAlign: 'center',
                textAlignVertical: 'center',
                transform: [ 
                    {translate: [0, -1.3, -4.6] }]
                    }}>
            One Tri
        </Text>

        &amp;amp;lt;Model
        lit
        source={{
            obj: asset('OneTriSkinnyWUVTexture_1.obj'),
            mtl: asset('OneTriSkinnyWUVTexture_1.mtl'),
            }}
            style={{
                transform: [
                    { translate: [ -0, -.8, -5.2 ] },
                    { rotateY: 10 },
                    { scale: .2 },
]
                }}
        />

         <Text
         style={{
             backgroundColor: '#777879',
             fontSize: 0.2,
             fontWeight: '400',
             layoutOrigin: [0.0, 0.5],
             paddingLeft: 0.2,
             paddingRight: 0.2,
             textAlign: 'center',
             textAlignVertical: 'center',
             transform: [ 
                {translate: [0, 1, -6] }]
         }}>
    Space Gallery
  </Text>
</View>
);
    }
};

AppRegistry.registerComponent('SpaceGallery', () => SpaceGallery);

这是一个很多要输入的内容,也是很多 UV 建模。你可以在这里下载所有这些文件:bit.ly/VR_Chap7

在上述代码中,我使用了这个:

<Platform MyX='0' MyZ='-5.1'/>

这样做是可以的,但更正确的做法是这样的:

<Platform MyX={0} MyZ={-5.1}/>

如果你懂 JSX 和 React,这将是一个明显的错误,但不是每个人都会注意到它(老实说,作为 C++程序员,我一开始也没有注意到)。花括号{}内的任何内容都是代码,而任何带引号的都是文本。文档中说:

Props - 组件可以接受参数,例如 <Greeting name='Rexxar'/>*中的名称。这些参数称为属性或 props,并通过 this.props 变量访问。例如,从这个例子中,名称可以作为{this.props.name}访问。您可以在组件、props 和状态下阅读更多关于这种交互的信息。

关于参数的提及仅适用于文本属性。对于数字属性,使用引号语法如'0.5*'*似乎可以工作,但会产生奇怪的后果。我们将在第十一章中看到更多内容,走进野生,但基本上,对于数字变量,您应该使用{0.5}(大括号)。

总结

在本章中,我们学习了如何使用 Blender 进行多边形建模,以及如何覆盖纹理分配并将纹理包裹在模型周围。我们学会了制作可以使您的世界看起来更真实的纹理。

然而,世界仍然是静态的。在下一章中,您将学习如何使事物移动,真正让您的世界生动起来。

第八章:给你的世界注入生命

在上一章中,我们通过材料使物体看起来更真实。我们知道这对于 VR 来说并不是完全必要的,正如我们在第一章中讨论的那样,虚拟现实到底是什么,但这确实有所帮助。现在,我们将学习如何通过使它们移动来使事物看起来真实。这样做有两个好处:移动的东西看起来更有生命力,而且还有助于视差深度感知。

React VR 具有许多 API,这将使包含流畅和自然的动画变得非常容易。在大多数传统 CGI 中,使动画流畅并不容易;您必须慢慢开始运动,加速到速度,然后轻轻地减速,否则运动看起来是假的。

我们将在本章中涵盖以下主题:

  • 用于动画化对象的Animated API

  • 一次性动画

  • 连续动画

  • 生命周期事件,如componentDidMount()

  • 如何将声音注入到世界中

运动和声音在使世界看起来活跃方面起到了很大作用。让我们来做吧!

动画 API

React 和 React VR 使这变得容易,因为动画 API 具有许多动画类型,使这变得简单易懂,无需进行数学计算或使用关键帧,就像传统动画一样。您可以逐渐增加事物,弹跳和停顿。这些属性是 spring,decay 和 timing;有关这些的更多详细信息,请参阅在线文档bit.ly/ReactAnims

动画是可以的,但我们需要知道我们要去哪里。为此,动画 API 具有两种值类型:标量(单个值)和矢量的 ValueXY。您可能会想知道为什么在这种情况下,矢量只是XY - ValueXY 是用于 UI 元素的,它们的性质是平的。如果您需要动画化 X,Y 和 Z 位置,您将使用三个标量。

首先,我们将创建一个旋转的动画茶壶。这将特别有助于了解我们的纹理映射是如何工作的。如果您一直在跟着代码,您的SpaceGallery应用程序应该已经具备我们开始编写本章所需的大部分内容。如果没有,您可以下载源文件开始:bit.ly/VR_Chap7。如果您真的不想输入所有这些,我把最终文件放在了:bit.ly/VR_Chap8

假设你已经下载或完成了上一章,从第七章中拿出index.vr.js与(虚拟)茶壶一起坐下,在文件的顶部但在import语句下面输入以下新类TurningPot()(请注意,我们仍然在SpaceGallery应用程序中)。

 class TurningPot extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        yRotation: new Animated.Value(0),
      };
    }

这设置了我们的动画值/变量—yRotation。我们已经将它创建为一个标量,这是可以的,因为我们将把它映射到rotateY

不要忘记import动画关键字。

接下来,我们将使用一个叫做componentDidMount的生命周期重写。生命周期重写是在加载和创建(渲染)VR 世界期间特定时间调用的事件;在这种情况下,componentDidMount函数在挂载后被调用(根据事件名称中“Did”片段的含义)。挂载意味着对象已加载、可用,并在 three.js 内创建;换句话说,它在世界中。componentWillMount函数在该组件即将被挂载但尚不存在时被调用;我们不使用这个函数,因为我们希望对象在实际可见对象时移动,尽管它对加载对象、初始化状态等非常有用。

请注意,我们还没有完成声明,所以最终的闭合{括号还没有出现:

   componentDidMount() {
        Animated.timing( 
          this.state.yRotation, // Animate variable `yRotation`
          {
            duration: 10000,    // Time
            toValue: 360,       // Spin around a full circle
          }
        ).start();              // Start the animation
      } 

componentDidMount()是一个重要的对象生命周期 API 调用,用于做像我们正在做的事情;开始动画。

这个事件很可能会在浏览器加载完所有内容之前发生,所以你可能会错过实际的开始。如果这是一个问题,你可以重载一些其他方法来确保它在正确的时间触发,或者引入一个小的延迟。

飞行的茶壶

现在是重要的事情,渲染本身。使用Animated.View关键字编写以下方法:

    render() {
      return (
        <Animated.View // Base: Image, Text, View
          style={{
            flex: 1,
            width: 1,
            height: 1,
            transform: [ 
              {rotateY: this.state.yRotation}, // Map yRotation to rotateY
            ]
          }}
          >
          <Model
          source={{
              obj: asset('teapot2.obj'),
              mtl: asset('teapot2_Mats.mtl'),
              }}
              lit
              style={{
                  transform: [{ translate: [0, -0.7, -5.1 ] }]
                  }}
          />
      </Animated.View>
      );
    }

  }

现在保存这个文件。如果你在 URL localhost:8081/vr/?hotreload 中使用了?hotreload,并且输入了一切正确,你会看到茶壶在你面前自动旋转。否则,点击浏览器中的“刷新”按钮。

等等,什么?刚刚发生了什么?为什么壶在飞!

茶壶围绕我们,即<view>的中心旋转,而不是围绕它自己的轴旋转。为什么会这样?记住翻译顺序很重要。在这种情况下,我们有一个单独的平移和旋转:

 render() {
      return (
        <Animated.View 
...
          {rotateY: this.state.yRotation}, // Map yRotation to rotateY
...
          <Model
...
                  transform: [{ translate: [0, -0.7, -5.1 ] }]
...
      </Animated.View>
      );

这里发生的是视图在旋转,然后模型在变换。我们希望以相反的顺序进行。一个解决方案是将模型保持在原地,并将render()循环更改为以下内容(注意粗体部分):

    render() {
      return (
        <Animated.View // Base: Image, Text, View
          style={{
            transform: [ 
 {translate: [0, -0.7, -5.1 ] },
 {rotateY: this.state.yRotation}, // Map `yRotation' to rotateY 
            ]
          }}
          >
          <Model
          source={{
              obj: asset('teapot2.obj'),
              mtl: asset('teapot2_Mats.mtl'),
              }}
              lit
              // we comment this out because we translate the view above
 // style={{
              // transform: [{ translate: [0, -0.7, -5.1 ] }]
              // }}
          />
      </Animated.View>
      );
    }

一旦旋转,永远

当我们保存这个文件并在 VR 浏览器中再次查看它时,我们会看到壶转动一次。请注意,我们可能看不到启动,并且当壶完成转动时,它会优雅地完成,而不是计算机动画的“猛然停止”:

这太棒了,但是壶转动然后停止了。我们可能希望它继续转动。所以让我们这样做!

修改组件创建以执行以下操作(是的,我们有点摆脱了所有酷炫的 Animate 关键字):

  class TurningPot extends React.Component {
    constructor(props) {
      super(props);
      this.state = {yRotation: 0};
      this.lastUpdate = Date.now();
      this.rotate = this.rotate.bind(this); 
    }

好的,在这部分,注意几件事。我们使用的变量称为yRotation;我们还使用了单词rotate,这实际上是一个新函数:

    rotate() { //custom function, called when it is time to rotate
        const now = Date.now();
        const delta = now - this.lastUpdate;
        this.lastUpdate = now;
        console.log("Spinning the pot");

        //note: the 20 is the rotation speed; bad form to
        //hard code it- this is for instructional purposes only
        this.setState({yRotation: this.state.yRotation + delta / 20} );
        //requestAnimationFrame calls the routine specified, not a variable
        this.frameHandle = requestAnimationFrame(this.rotate);
      } 

我们还需要改变对象的加载/卸载例程,既开始旋转,也结束定时器回调:

   componentDidMount() { //do the first rotation
        this.rotate();
    } 
    componentWillUnmount() { //Important clean up functions
        if (this.frameHandle) {
          cancelAnimationFrame(this.frameHandle);
          this.frameHandle = null;
        }
      } 

<View>本身不会改变;它只是像驱动函数一样旋转对象;这一次,我们使用一个名为render()的自定义函数来驱动它。

检查经过的时间非常重要,因为不同的平台会有不同的帧率,取决于硬件、GPU 和许多其他因素。为了确保所有类型的计算机和移动设备看到壶以相同的速度旋转,我们使用now变量并计算nowthis.lastUpdate之间的差值,得到一个增量时间。我们使用增量来确定实际的旋转速度。

最终代码

现在我们已经解决了所有这些问题,我们有一个良好渲染的旋转茶壶。在编码过程中,我们还修复了一个糟糕的编程错误;壶的速度被硬编码为 20 左右。从编程的最大化来看,最好是将其作为const,“永远不要将常量嵌入程序主体中”:

import React, {Component } from 'react';

import {
  Animated,
  AppRegistry,
  asset,
  AmbientLight,
  Box,
  DirectionalLight,
  Div,
  Model,
  Pano,
  Plane,
  Text,
  Vector,
  View,
  } from 'react-vr';

  class TurningPot extends React.Component {
    constructor(props) {
      super(props);
      this.state = {yRotation: 0};
      this.lastUpdate = Date.now();
      this.rotate = this.rotate.bind(this); 
    }
    rotate() { //custom function, called when it is time to rotate
        const now = Date.now();
        const delta = now - this.lastUpdate;
        const potSpeed = 20;
        this.lastUpdate = now;
        this.setState({yRotation: this.state.yRotation + delta / potSpeed} );
        //requestAnimationFrame calls the routine specified, not a variable
        this.frameHandle = requestAnimationFrame(this.rotate);
      } 
    componentDidMount() { //do the first rotation
        this.rotate();
    } 
    componentWillUnmount() { //Important clean up functions
        if (this.frameHandle) {
          cancelAnimationFrame(this.frameHandle);
          this.frameHandle = null;
        }
      } 
    render() {
      return (
        <Animated.View // Base: Image, Text, View
          style={{
            transform: [ // `transform` is an ordered array
              {translate: [0, -0.5, -5.1 ] },
              {rotateY: this.state.yRotation}, // Map `yRotation' to rotateY 
            ]
          }}
          >
          <Model
          source={{
              obj: asset('teapot2.obj'),
              mtl: asset('teapot2_Mats.mtl'),
              }}
              lit
              //style={{
              // transform: [{ translate: [0, -0.7, -5.1 ] }]
              // }}
          />
      </Animated.View>
      );
    }

  }

class Pedestal extends Component {
    render() {
        return (
          <View>
          <Box 
          dimWidth={.4}
          dimDepth={.4}
          dimHeight={.5}
          lit
          texture={asset('travertine_striata_vein_cut_honed_filled_Base_Color.jpg')}
          style={{
            transform: [ { translate: [ this.props.MyX, -1.4, this.props.MyZ] } ]
            }}
        />
          <Box 
          dimWidth={.5}
          dimDepth={.5}
          dimHeight={.1}
          lit
          texture={asset('travertine_striata_vein_cut_honed_filled_Base_Color.jpg')}
          style={{
            transform: [ { translate: [ this.props.MyX, -1.1, this.props.MyZ] } ]
            }}
        />
          <Box 
          dimWidth={.5}
          dimDepth={.5}
          dimHeight={.1}
          lit
          texture={asset('travertine_striata_vein_cut_honed_filled_Base_Color.jpg')}
          style={{
            transform: [ { translate: [ this.props.MyX, -1.7, this.props.MyZ] } ]
            }}
          />
     </View>
    )
     }
     }

         class Platform extends Component {
             render() {
                 return ( 
                    <Model
                    source={{
                        obj: asset('DeckPlate_v1.obj'),
                        mtl: asset('DeckPlate_v1_AllMats.mtl'),
                        }}
                        lit
                        style={{
                            transform: [ {
                            translate: [ this.props.MyX, -1.8, this.props.MyZ]
                        }] }}
                    /> 

    );
          }
         }

export default class SpaceGallery extends React.Component {
    render() {
        return (
          <View>
            <Pano source={asset('BabbageStation_v6_r5.jpg')}/>
            <AmbientLight

    intensity = {.3}

    />
    <DirectionalLight
    intensity = {.7}
    style={{
        transform:[{
            rotateZ: -45
        }]
    }}
         /> 
         <Platform MyX='0' MyZ='-5.1'/>
         <Platform MyX='0' MyZ='0'/>
         <Platform MyX='0' MyZ='5.1'/>
         <Platform MyX='5.1' MyZ='-5.1'/>
         <Platform MyX='5.1' MyZ='0'/>
         <Platform MyX='5.1' MyZ='5.1'/>
         <Platform MyX='-5.1' MyZ='-5.1'/>
         <Platform MyX='-5.1' MyZ='0'/>
         <Platform MyX='-5.1' MyZ='5.1'/>

         <Pedestal MyX='0' MyZ='-5.1'/>
         <Pedestal MyX='0' MyZ='5.1'/>
         <Pedestal MyX='5.1' MyZ='-5.1'/>

         <Pedestal MyX='5.1' MyZ='5.1'/>
         <Pedestal MyX='-5.1' MyZ='-5.1'/>
         <Pedestal MyX='-5.1' MyZ='0'/>
         <Pedestal MyX='-5.1' MyZ='5.1'/>

         <Model
            source={{
                obj: asset('teapot2.obj'),
                mtl: asset('teapot2_Mats.mtl'),
                }}
                lit
                style={{
                    transform: [{ translate: [ -5.1, -1, -5.1 ] }]
                    }}
            />

        <Text
            style={{
                backgroundColor: '#777879',
                fontSize: 0.1,
                fontWeight: '400',
                layoutOrigin: [0.0, 0.5],
                paddingLeft: 0.2,
                paddingRight: 0.2,
                textAlign: 'center',
                textAlignVertical: 'center',
                transform: [ 
                    {translate: [-5.2, -1.4, -4.6] }]
                    }}>
            Utah Teapot
        </Text>
        <Text
            style={{
                backgroundColor: '#777879',
                fontSize: 0.1,
                fontWeight: '400',
                layoutOrigin: [0.0, 0.5],
                paddingLeft: 0.2,
                paddingRight: 0.2,
                textAlign: 'center',
                textAlignVertical: 'center',
                transform: [ 
                    {translate: [0, -1.3, -4.6] }]
                    }}>
            Spinning Pot
        </Text> 

         <Text
         style={{
             backgroundColor: '#777879',
             fontSize: 0.2,
             fontWeight: '400',
             layoutOrigin: [0.0, 0.5],
             paddingLeft: 0.2,
             paddingRight: 0.2,
             textAlign: 'center',
             textAlignVertical: 'center',
             transform: [ 
                {translate: [0, 1, -6] }]
         }}>
    Space Gallery
  </Text>
  <TurningPot/>

</View>
);
    }
};

AppRegistry.registerComponent('SpaceGallery', () => SpaceGallery);

声音

VR 中的声音实际上非常复杂。我们的耳朵听到的声音与别人的耳朵听到的声音不同。许多 VR 系统都采用简单的“如果在右边,对我的右耳来说更响”的立体声定位,但这并不是实际声音工作的方式。对于 VR 和它们所需的高帧率,就像我们的光照效果跳过完整的光线追踪一样,这种声音定位是可以的。

更复杂的 VR 系统将使用一种叫做头部相关传递函数(HRTF)的东西。HRTF 是指当你转动头部时声音如何变化。换句话说,声音如何根据你的头部“传递”?每个人都有自己的 HRTF;它考虑了他们的耳朵形状、头部的骨密度以及鼻子和口腔的大小和形状。我们的耳朵,再加上我们的成长方式,在这个过程中我们训练我们的大脑,让我们能够用 HRTF 做出惊人的事情。例如,人类可以通过只从两个点听到声音来在三维空间中定位某物。这就像只用一只眼睛就能看立体影像一样!HRTF 给了我们视觉所不能给的;它给了我们对周围发生的事情的空间意识,即使我们看不见。

使用 HRTF 进行虚拟现实需要每个在虚拟世界中听到声音的人都将他们的 HRTF 加载到 VR 世界的声音系统中。此外,这个 HRTF 必须在无反射室(墙壁上覆盖有泡沫衬里以消除回声的房间)中进行测量。这显然并不常见。

因此,大多数 VR 声音只是左右平移。

这是 VR 可以取得重大突破的领域。声音非常重要,让我们能够在三维空间中感知事物;这是沉浸的重要方面。许多人认为立体声平移就是 3D;这只是声音在一个耳朵比另一个耳朵更响。在音频系统中,这是平衡旋钮。在耳机中,听起来会很奇怪,但实际上并没有定位声音。在现实世界中,你的右耳会在左耳之前(或之后)听到声音,当你转动头部时,你的耳朵的曲线会改变这种延迟,你的大脑会说“啊,声音就在那里”。

没有 HRTF 测量,立体声平移是唯一能做的事情,但 HRTF 要好得多。好消息是,现在音频硬件和计算能力非常强大,有了 HRTF 或合理的软件来模拟平均 HRTF,更复杂的声音处理是可能的。期待未来在这个领域的进展。

React VR 的强大再次拯救了我们。我们不必担心这一切;我们只需要把声音放在我们的世界里。

说真的,不要因为所有这些谈话而感到沮丧,只要意识到声音很难(和图形渲染一样重要),但在这一点上,你真正需要做的就是获得一个好的单声道(不是立体声)声音,并在场景文件中描述它。

这就是 React VR 的全部意义。描述你想要的东西;你不需要告诉人们如何做。不过,你需要知道幕后发生了什么。

在我们的世界中放置声音

现在,让我们真的发出一些声音。Freesound.com是一个获取免费游戏声音的好地方。那里的大部分声音都需要归属。给那些帮助建立你的世界的人以信用是正确的做法。去这个网站下载几个你喜欢的声音文件。我在freesound.com找到的一些是这些:

我以.mp3文件格式下载了这些;这应该是相当跨平台的。把它们复制到static_assets目录中一个名为sounds的新文件夹中。我只在实际世界中使用了其中一个,但你可以尝试其他的。有时你不知道它是否有效,直到你在世界中听到它。

声音是一个必须附加到视图、图像或文本的节点——React VR 的唯一组件。你可能想把它附加到一个盒子、模型或其他东西上;只需用<View>包裹对象,并把sound组件放在其中,如下所示:

 <View>
    <Model
       source={{
        obj: asset('teapot2.obj'),
        mtl: asset('teapot2_Mats.mtl'),
        }}
        lit
        style={{
            transform: [{ translate: [ -5.1, -1, -5.1 ] }]
            }}
    >
    </Model>
 <Sound 
        loop
        source={{wav: asset('sounds/211491__abrez__boiling-water.mp3') }}
        />
    </View>

有一件有趣的事情是,声音并不是来自我们的茶壶所在的地方(当你第一次看到这个世界时,它在左上角)。为什么呢?看看前面的代码;我们只是简单地在Model周围包裹了View标签;所以它的变换与声音不同。

有些声音比其他的效果更好;你需要进行实验或录制自己的声音。修复变换留给读者作为练习。(实际上,这很容易,但确保你不要把变换粘贴为子 XML 元素。)正确的代码是这样的:

<View
    style={{
 transform: [{ translate: [-5.1, -1, -5.1] }]
 }}
>
    <Model
        source={{
            obj: asset('teapot2.obj'),
            mtl: asset('teapot2_Mats.mtl'),
        }}
        lit
    >
    </Model>
    <Sound
        loop
        source={{ wav: asset('sounds/211491__abrez__boiling-water.mp3') }} />
</View>

总结

我们学会了如何通过程序性地改变对象的位置和使用更高级的方法来构建动画,比如使用定时器和动画 API。我们明显看到了如果使用错误的<View>来进行动画会发生什么,并开发了一种让对象永远动画的方法。Energizer 兔会感到自豪。我们还添加了声音,这对虚拟世界来说是非常重要的事情。

定时器可以做很多事情;我强烈建议你研究在线文档并进行实验!

到目前为止,我们一直在 React VR 范围内。有时,有些事情是 React 不允许我们做的。在下一章中,我们将转向原生(即原生 React)!

有人能把那个沸腾的锅炉关掉吗?

第九章:自己动手-本机模块和 Three.js

React VR 使得在不需要了解 three.js 的情况下进行 VR 变得容易。three.js 是帮助实现 WebGL 的包装类,WebGL 本身是一种本机 OpenGL 渲染库的形式。

React VR 相当包容,但像所有 API 一样,它无法做到一切。幸运的是,React VR 预料到了这一点;如果 React VR 不支持某个功能并且您需要它,您可以自己构建该功能。

在本章中,您将涵盖以下主题:

  • 从 React VR 代码内部使用 three.js

  • 基本的 three.js 程序代码

  • 设置 three.js 以与我们的 React VR 组件进行交互

  • 使用 three.js 在视觉上执行低级别的操作

本机模块和视图

也许您确实了解 three.js 并且需要使用它。React Native 模块是您的代码可以直接包含原始的 three.js 编程。如果您需要以编程方式创建本机的 three.js 对象,修改材质属性,或者使用 React VR 没有直接暴露的其他 three.js 代码,这将非常有用。

您可能有一些执行业务逻辑的 JavaScript 代码,并且不想或无法将其重写为 React VR 组件。您可能需要从 React VR 访问 three.js 或 WebVR 组件。您可能需要构建一个具有多个线程的高性能数据库查询,以便主渲染循环不会变慢。所有这些都是可能的,React Native 可以实现。

这是一个相当高级的主题,通常不需要编写引人入胜、有效的 WebVR 演示;但是,了解 React VR 和 React 是如此可扩展,这仍然是令人难以置信的。

制作一个 three.js 立方体演示

首先,让我们看一个简单的盒子演示。让我们从一个新生成的站点开始。转到您的 node.js 命令行界面,并关闭任何正在运行的npm start窗口,并通过发出以下命令重新创建一个新的、新鲜的站点:

f:\ReactVR>React-vr init GoingNative

第一个任务是转到vr文件夹并编辑client.js。到目前为止,我们还没有必须编辑此文件;它包含样板 React VR 代码。今天,我们将编辑它,因为我们不只是在做样板。以下代码中的粗体行是我们将添加到client.js中的行:

// Auto-generated content.
// This file contains the boilerplate to set up your React app.
// If you want to modify your application, start in "index.vr.js"

// Auto-generated content.
import {VRInstance} from 'react-vr-web';
import {Module} from 'react-vr-web';
import * as THREE from 'three';

function init(bundle, parent, options) {
const scene = new THREE.Scene();
const cubeModule = new CubeModule();
const vr = new VRInstance(bundle, 'GoingNative', parent, {
 // Add custom options here
 cursorVisibility: 'visible',
 nativeModules: [ cubeModule ],
 scene: scene,
 ...options,
 });

 const cube = new THREE.Mesh(
 new THREE.BoxGeometry(1, 1, 1),
 new THREE.MeshBasicMaterial()
 );
 cube.position.z = -4;
 scene.add(cube);
 cubeModule.init(cube);

 vr.render = function(timestamp) {
 // Any custom behavior you want to perform on each frame goes here
//animate the cube
 const seconds = timestamp / 1000;
 cube.position.x = 0 + (1 * (Math.cos(seconds)));
 cube.position.y = 0.2 + (1 * Math.abs(Math.sin(seconds)));
 };
 // Begin the animation loop
 vr.start();
 return vr;
};

window.ReactVR = {init};

我们还需要创建 CubeModule 对象。如果它变得复杂,您可以将其放在一个单独的文件中。现在,我们可以将其添加到 client.js 的底部:

export default class CubeModule extends Module {
  constructor() {
    super('CubeModule');
  }
  init(cube) {
    this.cube = cube;
  }
  changeCubeColor(color) {
    this.cube.material.color = new THREE.Color(color);
  }
}

不需要做其他更改。现在你会看到一个弹跳的纯白色立方体。我们没有改变 index.vr.js,所以它仍然显示着 hello 文本。这表明 React VR 和原生代码,在这种情况下是 three.js,同时运行。

好的,我们放了一个弹跳的立方体。这段代码的好处是它展示了一些高度的集成;然而,这是以一种非常干净的方式完成的。例如,这一行代码——const scene = new THREE.Scene()——给你一个可访问的 three.js 场景,所以我们可以用 three.js 做任何我们想做的事情,然而,所有的 React VR 关键词都能正常工作,因为它将使用现有的场景。你不需要从一边导入/导出场景到另一边并维护句柄/指针。这一切都是干净的、声明式的,就像 React VR 应该是的那样。我们在正常的 React VR 语法之外创建了常规场景和对象。

在我们之前的动画中,我们改变了index.vr.js。在这种情况下,对于 three.js 对象,我们直接在client.js的这部分进行更改;就在代码生成器建议的地方:

vr.render = function(timestamp) {

// 在这里执行每帧的自定义行为

使原生代码与 React VR 交互

如果我们继续让这个对象与世界其他部分进行交互,你就能真正看到 React VR 的强大之处。为了做到这一点,我们需要改变index.vr.js。我们还将第一次使用VrButton

注意 VrButton 中的拼写。我在这个问题上纠结了一段时间。我自然地会输入"VR"而不是"Vr",但它确实遵循了 React VR 的大小写规范。

线索是,在控制台中你会看到 VRButton is not defined,这通常意味着你在import语句中忘记了它。在这种特殊情况下,你会看到 React 的一个奇怪之处;你可以输入 import { YoMomma } from 'react-vr'; 而不会出错;试试看。React VR 显然太害怕回答 YoMomma 了。

当我们点击按钮时,沉浸感的一个重要部分是它们发出的点击声音。任何将手机调成静音且没有震动的人都知道我的意思;你按一下手机,什么声音都没有,以为它坏了。所以,让我们去FreeSound.org下载一些点击声音。

我找到了 IanStarGem 制作的 Switch Flip #1,并且它是根据知识共享许可证授权的。所以,让我们把它放在 static_assets 文件夹中:

  1. 首先,我们需要包括我们的NativeModule的声明;通常,你会在import指令之后的顶部这样做,如下所示:
// Native Module defined in vr/client.js const  cubeModule  =  NativeModules.CubeModule;

请注意,你可以将你的对象称为CubeModule,但你可能会在实现与定义之间感到困惑。这样打字会更容易。JavaScript 可能会很宽容。这可能是好事,也可能不是。

  1. 无论如何,在index.vr.js中,我们需要设置我们的新初始状态,否则会出现黑屏和错误:
class GoingNative extends React.Component {
 constructor(props) {
 super(props);
 this.state = { btnColor: 'white', cubeColor: 'yellow' };
 cubeModule.changeCubeColor(this.state.cubeColor);
 }
  1. 在同一个文件中,在render()语句的下面,将<View>的定义更改为以下内容(注意我们仍然在视图中,并且尚未关闭它):
      <View
        style={{
          transform:[{translate: [0, 0, -3]}],
          layoutOrigin: [0.5, 0, 0],
          alignItems: 'center',
        }}>

我们在这里稍微作弊,也就是说,将视图向后移动,这样物体就在我们面前。

由于 React VR 不是 CAD 系统,你无法进行可视化编辑,因此在编写代码时必须考虑物品的定位。

对于一些复杂的情况,布局图纸也可能有所帮助。

  1. <Pano>语句之后,并在</View>结束标记之前,插入以下内容(更改模板生成的 Text 语句):
  <VrButton
    style={{
      backgroundColor: this.state.btnColor,
      borderRadius: 0.05,
      margin: 0.05,
    }}
    onEnter={() => { this.setState({ btnColor: this.state.cubeColor }) }}
    onExit={() => { this.setState({ btnColor: 'white' }) }}
    onClick={() => {
      let hexColor = Math.floor(Math.random() * 0xffffff).toString(16);
      // Ensure we always have 6 digits by padding with leading zeros.
      hexColor = '#' + (('000000' + hexColor).slice(-6));
      this.setState({ cubeColor: hexColor, btnColor: hexColor });
      // Asynchronous call to custom native module; sends the new color.
      cubeModule.changeCubeColor(hexColor);
    }}
    onClickSound={asset('freesound__278205__ianstargem__switch-flip-1.wav')}
  >
    <Text style={{
      fontSize: 0.15,
      paddingTop: 0.025,
      paddingBottom: 0.025,
      paddingLeft: 0.05,
      paddingRight: 0.05,
      textAlign: 'center',
      textAlignVertical: 'center',
    }}>
      button
    </Text>
  </VrButton>

当你刷新浏览器时,立方体仍然会四处弹跳,但你可以点击按钮看到立方体变色。当你将鼠标或控制器的光标悬停在按钮上(显示为<Text>组件),你会看到按钮变成立方体的当前颜色。

一个很好的做法是在静态变量中预先生成立方体的新颜色(这样它不会像 let 一样消失),然后使鼠标悬停的颜色变成那种颜色。

白色背景上的默认颜色也应该修复。

继续尝试吧;这是一个有趣的练习。

当我们播放声音时,在浏览器的控制台中会出现以下错误:

VrSoundEffects: must load sound before playing ../static_assets/freesound__278205__ianstargem__switch-flip-1.wav

你可能还会看到以下错误:

Failed to fetch audio: ../static_assets/freesound__278205__ianstargem__switch-flip-1.wav
The buffer passed to decodeAudioData contains invalid content which cannot be decoded successfully.
  1. 解决这个问题的方法是确保你的浏览器有正确的音频格式。正确的格式有:

  2. 音频文件需要是单声道;这样它们才能被转换成 3D 空间。

  3. 音频文件需要是 48 千赫或更低。这似乎在 Firefox 55 和 59 之间有所改变,但尽可能通用是最安全的。

  4. 如果你的文件格式错误,或者你听不到声音,有两种可能的解决方法:

  5. 你可以使用 Audacity 或其他音频编辑工具来修复这些问题。

  6. 你可以让我来修复它!我已经在书中的文件中下载并转换了文件。但是,如果你不尝试修复,你就学不到。你可以只下载 48 千赫单声道文件,避免转换,但实际上这些相当罕见。使用 Audacity 转换声音很容易和免费,你只需要学一点这个程序就可以了。在 VR 按钮内,我们需要做的就是加载修改后的单声道声音文件:

onClickSound={asset('freesound__278205__ianstargem__switch-flip-48kmono.wav')}

我在早期的部分提到过这一点,但值得重申的是,如果您遇到无法解释的错误,并且大声说“我知道文件在那里并且可以播放!”,请尝试检查声音文件的格式。

总结到目前为止的代码

我们添加了很多代码;让我们总结一下我们的进展。React VR 有时可能会令人困惑,因为它是 JavaScript 和 XML“ish”代码(JSX)的混合,所以这里是完整的index.vr.js

import React from 'react';
import {
  AppRegistry,
  Animated,
  asset,
  Easing,
  NativeModules,
  Pano,
  Sound,
  Text,
  View,
  VrButton
} from 'react-vr';

const cubeModule = NativeModules.CubeModule;

class GoingNative extends React.Component {
  constructor(props) {
    super(props);
    this.state = { btnColor: 'white', cubeColor: 'yellow' };
    cubeModule.changeCubeColor(this.state.cubeColor);
  }
  render() {
    return (
      <View
        style={{
          transform: [{ translate: [0, 0, -3] }],
          layoutOrigin: [0.5, 0, 0],
          alignItems: 'center',
        }}>
        <Pano source={asset('chess-world.jpg')} />
        <VrButton
          style={{
            backgroundColor: this.state.btnColor,
            borderRadius: 0.05,
            margin: 0.05,
          }}
          onEnter={() => { this.setState({ btnColor: this.state.cubeColor }) }}
          onExit={() => { this.setState({ btnColor: 'white' }) }}
          onClick={() => {
            let hexColor = Math.floor(Math.random() * 0xffffff).toString(16);
            // Ensure we always have 6 digits by padding with leading zeros.
            hexColor = '#' + (('000000' + hexColor).slice(-6));
            this.setState({ cubeColor: hexColor, btnColor: hexColor });
            // Asynchronous call to custom native module; sends the new color.
            cubeModule.changeCubeColor(hexColor);
          }}
          onClickSound={asset('freesound__278205__ianstargem__switch-flip-48kmono.wav')}
        >
          <Text style={{
            fontSize: 0.15,
            paddingTop: 0.025,
            paddingBottom: 0.025,
            paddingLeft: 0.05,
            paddingRight: 0.05,
            textAlign: 'center',
            textAlignVertical: 'center',
          }}>
            button
    </Text>
        </VrButton>
      </View>
    );
  }
};

AppRegistry.registerComponent('GoingNative', () => GoingNative);

vr文件夹(文件夹名称为小写)中的client.js文件中将包含以下内容:

import {VRInstance} from 'react-vr-web';
import {Module} from 'react-vr-web';
import * as THREE from 'three';

function init(bundle, parent, options) {
const scene = new THREE.Scene();
const cubeModule = new CubeModule();
const vr = new VRInstance(bundle, 'GoingNative', parent, {
    cursorVisibility: 'visible',
    nativeModules: [ cubeModule ],
    scene: scene,
    ...options,
  });

  const cube = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshBasicMaterial()
  );
  cube.position.z = -4;
  scene.add(cube);

  cubeModule.init(cube);

  vr.render = function(timestamp) {
    const seconds = timestamp / 1000;
    cube.position.x = 0 + (1 * (Math.cos(seconds)));
    cube.position.y = 0.2 + (1 * Math.abs(Math.sin(seconds)));
  };
  vr.start();
  return vr;
};

window.ReactVR = {init};

export default class CubeModule extends Module {
  constructor() {
    super('CubeModule');
  }
  init(cube) {
    this.cube = cube;
  }
  changeCubeColor(color) {
    this.cube.material.color = new THREE.Color(color);
  }
}

更多视觉效果

我们做了一些很棒的交互,这是很棒的,尽管直接使用 three.js 的另一个重要原因是在渲染方面做一些 React VR 无法做到的事情。实际上,React VR 可以通过本地方法做一些令人惊叹的事情,所以让我们确切地做到这一点。

首先,让我们将我们的立方体从四处弹跳改为旋转。当我们添加一些视觉效果时,它会看起来更令人印象深刻。

让我们也添加一些球体。我们希望有一些东西可以反射。我选择反射作为一个令人印象深刻的事情,目前在 WebVR 中你实际上不能做到,尽管我们可以通过环境映射做一些非常接近的事情。关于环境映射是什么的讨论比较长,你可以去这里了解:bit.ly/ReflectMap

将以下代码添加到您现有的index.vr.js中,在</VrButton>下方:

     <Sphere
      radius={0.5}
      widthSegments={20}
      heightSegments={12}
      style={{
        color: 'blue',
        transform: [{ translate: [-1, 0, -3] }],
      }}
      lit />
    <Sphere
      radius={1.5}
      widthSegments={20}
      heightSegments={12}
      style={{
        color: 'crimson',
        transform: [{ translate: [1, -2, -3] }],
      }}
      lit />

我们还将在顶层<View>内的index.vr.js中添加环境光和定向光:

  <AmbientLight  intensity={.3} />
  <DirectionalLight
    intensity={.7}
    style={{ transform: [{
        rotateZ: 45
      }]
    }}
  />

继续加载,并确保您看到一个漂亮的蓝色球和一个大红色球。请注意,我编码比平常稍微密集一些,这样这本书就不会消耗更多的树木或光子。我们大部分的更改将在client.js中进行。首先,在init下初始化我们需要的所有变量:

 var materialTorus;
 var materialCube;
 var torusCamera;
 var cubeCamera;
 var renderFrame;
 var torus;
 var texture;
 var cube;

然后,我们将为场景设置自定义背景。有趣的是,在我们有<Pano>语句时,这并不会显示出来,但这是件好事,因为我们现在正在用three.js编码;它不理解 VR,所以背景不太对。这会在图像上显示出来,但最好由读者自行修复。要为three.js设置自定义背景,继续按照以下方式添加代码:

  var textureLoader = new THREE.TextureLoader();
  textureLoader.load('../static_assets/chess-world.jpg', function (texture) {
    texture.mapping = THREE.UVMapping;
    scene.background = texture;
  });

然后,我们将创建一个圆环和之前创建的立方体(记住,这一切仍然在init语句中):

  torusCamera = new THREE.CubeCamera(.1, 100, 256);
  torusCamera.renderTarget.texture.minFilter = THREE.LinearMipMapLinearFilter;
  scene.add(torusCamera);

  cubeCamera = new THREE.CubeCamera(.1, 100, 256);
  cubeCamera.renderTarget.texture.minFilter = THREE.LinearMipMapLinearFilter;
  scene.add(cubeCamera);

我们在这里做的是创建了一些额外的摄像头。我们将把这些摄像头移动到圆环和我们的弹跳立方体所在的位置,然后将这些摄像头渲染到一个屏幕外的缓冲区(看不见)。现在我们已经创建了这些摄像头,我们可以创建我们的立方体和圆环 three.js 对象;请注意,这对我们之前的立方体有一点改变:

  materialTorus = new THREE.MeshBasicMaterial({ envMap: torusCamera.renderTarget.texture });
  materialCube = new THREE.MeshBasicMaterial({ envMap: cubeCamera.renderTarget.texture });

  torus = new THREE.Mesh(new THREE.TorusKnotBufferGeometry(2, .6, 100, 25), materialTorus);
  torus.position.z = -10; torus.position.x = 1;
  scene.add(torus);

  cube = new THREE.Mesh( new THREE.BoxGeometry(1, 1, 1), materialCube);
  cube.position.z = -4;
  scene.add(cube);

  renderFrame = 0;
  cubeModule.init(cube);

请注意,cubeModule.init(cube);语句应该已经存在。现在,我们只需要真正地将假锡箔包裹在我们的物体周围;我们将在vr.render函数中完成这个操作。以下是整个函数:

vr.render = function (timestamp) {
    // Any custom behavior you want to perform on each frame goes here
    const seconds = timestamp / 2000;
    cube.position.x = 0 + (1 * (Math.cos(seconds)));
    cube.position.y = 0.2 + (1 * Math.abs(Math.sin(seconds)));
    cube.position.y = 0.2 + (1 * Math.sin(seconds));

    var time = Date.now();
    torus.rotation.x += 0.01;
    torus.rotation.y += 0.02;

    //we need to turn off the reflected objects, 
    //or the camera will be inside.
    torus.visible = false;
    torusCamera.position.copy(torus.position);
    torusCamera.update(vr.player.renderer, scene)
    materialTorus.envMap = torusCamera.renderTarget.texture;
    torus.visible = true;

    cube.visible = false;
    cubeCamera.position.copy(cube.position);
    cubeCamera.update(vr.player.renderer, scene);
    materialCube.envMap = cubeCamera.renderTarget.texture;
    cube.visible = true;

    renderFrame++;

  };
  // Begin the animation loop
  vr.start();
  return vr;
};

我稍微改变了盒子,去掉了正弦波周围的Math.abs(..)函数,这样它就会在一个完整的圆圈中旋转;这样我们就可以看到反射贴图的优点和缺点。

希望我们已经把所有内容都粘贴进去了。你可以面带微笑地观看显示。漂亮的铬结对象!当你盯着它看时,你会注意到有些地方不太对劲。你可以看到在方框中伪造的反射和真实的反射之间的区别。它看起来有点“不对劲”,但铬结看起来不错。

看看以下图像中红色高亮和绿色的区别:

创建良好的 VR 主要是关于合理的妥协。在反射的情况下,它们看起来可能很棒,就像前面的图像所示的那样,但它们也可能看起来有点不舒服。盒子或平面镜子就是一个不好的例子。曲面物体看起来更自然,正如你所看到的。

游戏和实时编程与仔细的设计一样重要,也是对真实世界的忠实再现。记住,我们不是在创造真实的东西;我们所要做的就是创造一个看起来真实的东西。

在 three.js 中有一个真正的反射器叫做THREE.Reflector,如果你想建造一个平面镜子。在 three.js 的示例中有很好的文档记录。

借助这些技术和 React Native 桥接,您可以在不深入常规 three.js 编程的情况下,在 React VR 中做一些令人惊叹的事情。

下一步

现在您已经看到了材料的基本 three.js 语法,您可以查看各种 three.js 示例,并复制其中的一些代码。不要只看屏幕上的示例。您还会想在 VR 中尝试它们。一些游戏技巧,比如镜头反射或屏幕空间反射,在 VR 中看起来并不好。一如既往,测试,测试和测试。

我还略微改变了按钮的颜色,当我们切换到 VR 模式时,我们没有光标,所以按钮按下并不总是有效。在下一章中,我将向您展示如何解决这个问题,或者您可以自行调查。

我还在源文件中加载了一个类似金属的反射纹理,名为static_assets/metal_reflect.jpg。您不必进行相机渲染来获得看起来闪亮的东西,特别是如果它是一种暗淡的反射,并且可能不希望额外增加帧速率(所有这些相机渲染都需要时间)。如果是这种情况,您可以做一个简单的环境贴图,跳过相机加载和渲染。

扩展 React VR — 本机视图

您还可以通过一种称为本机视图的东西来扩展 React VR 本身。视图这个词可能让您想到相机渲染,尽管在这种情况下,意思有点不同。把它们看作是本机 three.js 的新 React VR 对象更为合适。它们非常有用。您可以使用我们刚刚介绍的 three.js 代码来混合原始的 three.js 编程,但是以这种方式使用声明式编程的能力有限。有没有更适合 React VR 的方法?您可以通过本机视图来实现这一点。

扩展语言

当您实现本机视图时,您可以控制属性和代码与其余运行时代码的交互方式。这些注入通常是视觉的,尽管您也可以注入声音。

您还可以实现新的本机对象。编程方式与我们迄今为止所做的类似;您实现基本属性,将新关键字暴露给运行时,然后将它们编码,就好像它们是 React VR 语言的一部分。还有其他关键字和函数,让您能够根据属性和类型描述您的新 React VR 视图。

要创建本机视图,可以查看文档:bit.ly/RCTNativeView.  

你现在已经到了可以用 React VR 做一些令人惊叹的事情的地步了,我完全相信你可以分解我的例子,扩展它们,并且玩得开心。

总结

在本章中,我们讨论了如何在 React VR 中使用 three.js 的全部功能。在学习这一点的同时,我们演示了如何放置本地代码和 React VR 本地桥接。我们直接通过 JavaScript 构建了three.js网格,并添加了使世界更加生动的声音。我们还使用了 React Native Views 和本地桥接来进行自定义渲染,包括反射贴图 - 我们为 VR 添加了 Chrome(而不是用 Chrome 查看 VR)。我们还展示了如何通过vr.player.renderer访问 React VR 相机来进行更多的 three.js 处理。

有了完整的 three.js,我们真的可以用 React VR 做任何我们想做的事情。然而,我们应该在需要的地方使用 React VR,在需要更多细节的地方使用 three.js,否则 React VR 将成为螺栓上的糖霜。它可能会生锈并容易脱落。

第十章:引入真实世界

正如您在上一章第九章中学到的,自己动手-本地模块和 Three.js,我们可以将本地代码和 JavaScript 代码包含到我们的世界中。除了通过使其在视觉上更有趣来为我们的世界注入生命外,我们还可以将外部世界引入其中。

在本章中,您将学习如何使用 React 和 JavaScript 将网络带入 VR 世界。您将学习如何在 VR 中使用现有的高性能代码。

首先,我们需要一个 VR 世界来开始。这一次,我们要去火星了!

在本章中,您将学习以下主题:

  • 执行 JSON/Web API 调用

  • Fetch语句

  • 跨域资源共享(CORS)

  • 诊断的网络选项卡

  • Cylindrical Pano语句

  • 类似于 flexbox 的文本对齐(React Native 的一部分)

  • 条件渲染

  • 样式表

前往火星(初始世界创建)

您可能会认为太空中没有天气,但实际上是有的,我们在那里有天气站。我们将前往火星获取我们的天气。这将是一个实时程序,将从火星科学实验室或其名为好奇号的探测车获取天气数据。

好奇号是一辆体积为 SUV 大小的火星探测车,于 2011 年 11 月 26 日发射到火星,于 2012 年 8 月 6 日着陆。如果您开着 SUV 去那里,即使您能买到汽油,也需要大约 670 年才能到达那里。火星探测车最初设计为两年的任务,但其任务被延长了,这对我们来说是幸运的。

开着 SUV 去火星获取天气报告将是一件麻烦事。我甚至不知道加油站在哪里。

创建初始世界

首先,就像以前做过的那样,转到存储世界的目录并创建一个,如下所示:

react-vr init MarsInfo

然后,从github.com/jgwinner/ReactVRBook/tree/master/Chapter10/MarsInfo下载资产。

尽管我上传了所有文件来使其工作,而不仅仅是静态资产,但您真的应该尝试自己编写代码。从下载文件并运行它们中,您并不会真正学到任何东西。

犯错误是塑造性格的过程。我上传了文件并将继续维护它们,以防有太多的性格。

现在我们有了一个初始世界,我们将开始设置 Web 服务以获取数据。

Jason 和 JSON

当您听到人们谈论 JSON 时,希望您不会想到这个家伙:

我在网上找到了这张图片,标记为创意共享;这是来自加拿大拉瓦尔的 Pikawil 拍摄的蒙特利尔 Comic-Con 上的 Jason Voorhees 服装(角色扮演)。

认真地说,JSON 是通过 Web 服务引入外部世界的最常见方式;然而,正如我们已经看到包括原生代码和 JavaScript 的方式,您可以以各种方式集成您的系统。

React VR 的另一个巨大优势是它基于 React,因此您可以在 React VR 中常见的事情,也可以在 React VR 中做,只是有一些重要的区别。

为什么 JSON 与 React 无关

起初,您可能会想,"在 React VR 中如何进行 AJAX 请求?"

实际上并不是。React VR 和 React Native 对获取数据的方式没有任何忠诚度。事实上,就 React 而言,它甚至不知道图片中有服务器

React 只是使用来自两个地方的数据(props 和 state)简单地渲染组件。

这是学术答案。真实答案要广泛一些。您可以以任何您喜欢的方式获取数据。在说完这些之后,通常大多数 React 程序员将使用这些 API 和/或框架之一:

  • Fetch:几乎是一个标准,它内置在 React 中,因为它通常已经包含;有关用法说明和示例,请参阅bit.ly/FetchAPI

  • Axios:Axios 围绕着承诺(异步完成 API)展开,尽管它也可以在单线程应用程序中以更简单的方式使用;有关更多详细信息,请参阅bit.ly/AxiosReadme

  • Superagent:如果您不喜欢承诺,但喜欢回调;有关更多信息,请参阅bit.ly/SuperagentAPI

在这些示例中,我们将展示 fetch,因为没有必要安装不同的模块和设置回调。在说完这些之后,您可能希望构建一个稍微更具响应性的应用程序,该应用程序使用某种类型的回调或异步完成,以便在等待外部数据时执行某些操作。Fetch 确实通过承诺进行异步完成,因此我们将进行条件渲染以利用这一点,并保持响应性 VR 应用程序。

你可能已经写了很多这样的代码。React VR,正如前面讨论的那样,是一个用于 VR 对象的渲染系统,因此你可以使用各种外部 JavaScript 系统。

找到 API——从火星一直到地球

现在,我们将从火星获取天气数据。不,我并不是在开玩笑。参考bit.ly/MarsWeatherAPI,如果你感兴趣,这里描述了 API 并提供了一些科学背景。这个 API 被设置为从 XML 数据中获取并以 JSON 或 JSONP 格式返回。以下是结果数据,你也可以参考:marsweather.ingenology.com/v1/latest/

{
  "report": {
    "terrestrial_date": "2019-04-21",
    "sol": 2250,
    "ls": 66.0,
    "min_temp": -80.0,
    "min_temp_fahrenheit": -112.0,
    "max_temp": -27.0,
    "max_temp_fahrenheit": -16.6,
    "pressure": 878.0,
    "pressure_string": "Higher",
    "abs_humidity": null,
    "wind_speed": null,
    "wind_direction": "--",
    "atmo_opacity": "Sunny",
    "season": "Month 4",
    "sunrise": "2019-04-21T11:02:00Z",
    "sunset": "2019-04-21T22:47:00Z"
  }
}

我们可以相当容易地将这转换为我们的 JSON 对象。首先,让我们测试连接性,并对实际返回的 JSON 文本进行合理检查。我们在浏览器中测试了前面的 JSON 数据,但我们需要测试代码以确保它能正常工作。要做到这一点,请按照以下步骤:

  1. index.vr.js中找到 MarsInfo Component {的声明,添加以下内容:
export default class MarsInfo extends Component {
    componentDidMount() {
        fetch(`http://marsweather.ingenology.com/v1/latest/`,
            {
                method: 'GET'
            })
            .then(console.log(result))
    }

    render() {
  1. 粘贴这个并运行它。

  2. 在浏览器中打开控制台(在 Firefox Nightly 中按Ctrl+Shift+K)。虽然我们刚刚展示的代码非常合理,在浏览器中运行良好,但当我们运行时,会出现错误:

问题是什么?是 CORS。这是一种机制,用于使跨源或不来自同一服务器的 Web 内容安全可靠。基本上,这是 Web 服务器表明“我可以嵌入到另一个网页中”的一种方式。例如,你的银行不希望你的银行详细信息被嵌入到其他网站的网页中;你的支票账户可能会很容易地受到威胁,你会认为自己正在登录真正的银行——而实际上并非如此。

请注意,我本可以使用一个不会出现这些错误的 API,但你可能会遇到自己内容的相同问题,所以我们将讨论如何发现 CORS 问题以及如何解决它。

  1. 要找出我们为什么会出现这个错误,我们需要查看协议头;点击工具->Web 开发者->网络,打开网络选项卡:

这个窗口对于解决原生 JSON 请求问题和网站集成非常有价值。

  1. 一旦打开控制台,你会看到不同的 HTTP 操作;点击那个没有完成的操作:

然后我们将查看返回的数据。

  1. 查看以下截图的右侧;在这里,您可以单击响应和头部来检查数据。我们可以看到网站确实返回了数据;但是,我们的浏览器(Firefox)通过生成 CORS 错误来阻止显示:

代码是正确的,但网站没有包括重要的 CORS 头,因此根据 CORS 安全规则,网站将其阻止。您可以在以下网址了解有关 CORS 的更多信息:bit.ly/HTTPCORS

如果出现此错误,可能可以通过向请求添加头部来解决。要添加头部,您需要修改fetch请求;fetch请求还允许使用'cors'模式。然而,出于某种原因,对于这个特定的网站,'cors'选项似乎对我不起作用;对于其他网站,可能效果更好。其语法如下:

fetch(`http://marsweather.ingenology.com/v1/latest/`,
    {
        method: 'GET',
        mode: 'cors',
    })

为了更好地控制我们的请求,创建一个头部对象并将其传递给fetch命令。这也可以用于所谓的预检查,即简单地进行两个请求:一个是为了找出 CORS 是否受支持,第二个请求将包括来自第一个请求的值。

  1. 要构建请求或预检查请求,请设置如下头部:
var myHeaders = new Headers();
myHeaders.append('Access-Control-Request-Method', 'GET');
myHeaders.append('Access-Control-Request-Headers', 'Origin, Content-Type, Accept');

fetch(`http://marsweather.ingenology.com/v1/latest/`,
    {
        headers: myHeaders,
        method: 'GET',
        mode: 'cors',
    })

头部值'Access-Control-Request-Headers'可以设置为服务器将返回的自定义头部选项(如果支持 CORS),以验证客户端代码是否是有效的 CORS 请求。截至 2016 年,规范已经修改以包括通配符,但并非所有服务器都会更新。如果出现 CORS 错误,您可能需要进行实验并使用网络选项卡来查看发生了什么。

在这种情况下,我们需要使用“预检查”的选项,但即使在修改了 React VR 网络代码之后,这在marsweather.ingenology.com上也没有起作用,因此他们的服务器很可能还没有升级到现代网络安全标准。

这种情况可能会发生!在我们的情况下,确实没有通用的解决方法。我找到了一个 Firefox 插件,可以让您绕过 CORS 限制(请记住,问题不是来自服务器,而是浏览器在看到服务器已经发送的有效负载时关闭您的代码),但这需要人们下载插件并进行调试。

我们需要找到一个更好的 API。NASA 拥有一个出色的 Web API 目录,我们将使用他们的火星探测器相机 API。你可以免费获取数十万张照片中的任何一张。一旦我们使用不同的 Web API,我们将得到我们一直在寻找的正确的 CORS 标头,一切都运行得很好。一旦我们向具有现代安全标准的服务器发出请求,我们会注意到它自动包含了 Firefox 需要的access-control-allow-origin(在这里是通配符),如下图所示,取自网络选项卡:

因此,我们将看实际图片,而不是火星上的天气。

来自 NASA 的更好的 API

要查看一些很棒的 Web API,你可以访问:bit.ly/NasaWebAPI并查看你可以使用的 API 列表,或者更好的是,使用你已经编写的一些 Web API。React VR 使得通过 React 和 React Native 的强大功能集成这些 API 变得非常容易。我们将使用火星照片 API。要启用它,你可能需要一个开发者密钥。当你发出请求时,你可以将你的 API 密钥添加到 URL 中,或者使用DEMO_KEY。这将成为 API 调用的一部分,例如,api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos?sol=1000&api_key=DEMO_KEY。请注意,URL 末尾没有句号。

如果在开发代码时出现错误,你可能使用了DEMO_KEY太多次;获取你自己的开发者 API 非常快速和简单;有关说明可以在我提到的网站上找到:bit.ly/NasaWebAPI

要从 NASA 获取数据,我们只需稍微更改fetch命令,如下所示;事实证明,我们不需要自定义标头:

  1. index.vr.js更改为以下内容,直到render()语句:
export default class MarsInfo extends Component {
    constructor() {
        super();
        this.state = {
            currentPhoto: 2,
            photoCollection: { photos: []}
        };
    };
    componentDidMount() {
        fetch('https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos?sol=1197&api_key=DEMO_KEY',
            { method: 'GET' })
            .then(response => response.json())
            .then(console.log("Got a response"))
            .then(json => this.setState({ photoCollection:json }))

    };

这就是我们从 NASA 获取火星数据并将其放入集合中所需做的一切。太棒了!以下是我们所做的一些注意事项:

  • photoCollection对象被初始化为空数组(集合)。这样我们在获取数据之前和之后可以使用类似的代码。

  • 但是,你仍然应该检查是否有失败。

  • 我们将currentPhoto值初始化为2,有点像是在“作弊”。这样做的原因是,当我写这本书的时候,如果你让currentPhoto默认为第一张图片,你在火星的第一个视图会很无聊。前几张图片都是测试图片,相当普通,所以我让你把currentPhoto改成2,这样我们就能看到一些有趣的东西。如果你有一个返回特定数据的 API,你也可以做同样的事情。

  • 这段代码只是获取数据;它不会渲染它。为此,我们将开发一个单独的对象来保持我们的代码模块化。

  1. 出于调试目的,我们还将在render()线程中添加一行,以查看我们确切拥有的数据。插入以下console.log语句:
  render() {
      console.log("Render() main thread, photo collection:", this.state.photoCollection);
      return (

这对于解决渲染代码和理解当前状态以及其变化非常有用。运行这段代码,我们可以在控制台中看到返回的对象。首先,我们从render()线程中得到一行,显示一个空的photo collection

注意photo collection是空的;这是有道理的,因为我们是这样初始化的。几秒钟后——在这段时间内你可以查看虚拟世界——你会看到另一个render()更新和更改的数据:

在这种特殊情况下(第 1,1197 天),有很多图片。JSON 处理这些数据非常出色,同时我们在 VR 世界中四处张望。

另一个需要注意的事情是render()循环只被调用了两次。如果你习惯于游戏开发范式,这可能看起来很奇怪,因为正如我们讨论过的,为了建立沉浸感,我们需要超过 60 帧每秒。如果我们只渲染了两次,我们怎么能做到呢?

React VR 并不实际生成图像,而是由 three.js 完成。当 React VR“渲染”时,它只是采用 React VR 语法,并应用任何 props 或状态更改,并为那些已经改变的对象调用render()

为了显示我们检索到的数据,我们将构建一个新对象。

  1. 创建一个名为CameraData的新文件,并将其作为一个单独的组件。我们还将改变index.vr.js中的render()方法。

每个人都需要一个样式表

样式不仅仅适用于你的头发;在这种情况下,使用样式表将有助于使我们的代码更简单、更清洁、更易于维护。样式重用非常容易。样式不是一种单独的语言;它们像 React 中的其他所有内容一样都是 JavaScript。React VR 中的所有核心对象都接受一个名为styles的 prop。我们将在我们的文件中定义这个样式并重用它。

创建以下样式定义,以便我们可以在CameraData.js组件中使用它们(请注意,您可以将其放在文件的任何位置):

const styles = StyleSheet.create({
    manifestCard: {
        flex: 1,
        flexDirection: 'column',
        width: 2,
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: 'green',
        opacity: 0.8,
        borderRadius: 0.1,
        borderColor: '#000',
        borderWidth: 0.02,
        padding: 0.1,
        layoutOrigin: [-1, 0.3],
        transform: [
            {
                rotateY: -30,
                translate: [1, 0, -2]
            }
        ]
    },

    manifestText: {
        textAlign: 'center',
        fontSize: 0.1
    },
    frontCard: {
        flex: 1,
        flexDirection: 'column',
        width: 2,
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: 'green',
        borderRadius: 0.1,
        borderColor: '#000',
        borderWidth: 0.02,
        padding: 0.05,
        transform: [{ translate: [-1, 1, -3] }],
    },
    panoImage: {
        width: 500,
        height: 500,
        layoutOrigin: [-.5, 0],
    },
    baseView: {
        layoutOrigin: [0, 0],
    },
});

如果省略width样式,对象将以完全不同的方式进行变换和移动。我还不确定这是否是一个错误,还是一种不同类型的布局样式,但请注意,如果您的transform语句没有移动文本或视图对象,可能是因为您的文本样式没有width:属性。

构建图像和状态 UI

接下来,我们需要以两种不同的方式渲染相机数据。第一种是当我们还没有CameraData时,换句话说,就是在应用程序启动时,或者如果我们没有互联网连接;第二种是当我们获取数据并需要显示它时。我们还希望保持这些例程相当模块化,以便在启动状态变化时可以轻松地重新绘制需要的对象。

请注意,React VR 自动完成了很多工作。如果一个对象的 props 或状态没有改变,它就不会被告知重新渲染自己。在这种情况下,我们的主线程已经具有了修改更改的 JSON 处理,因此主循环中不需要创建任何内容来重新渲染相机数据。

  1. 添加以下代码:
export default class CameraData extends Component {
    render() {
        if (!this.props) {
            return this.renderLoadingView();
        }
        var photos = this.props.photoCollection.photos;
        if (!photos) {
            return this.renderLoadingView();
        }
        var photo = photos[this.props.currentPhoto];
        if (!photo) {
            return this.renderLoadingView();
        }
        return this.renderPhoto(photo);
    };

请注意,我们还没有完成组件,所以不要输入最终的};。让我们讨论一下我们添加了什么。先前的主render()循环实质上是检查哪些值是有效的,并调用两个例程中的一个来实际进行渲染,要么是renderPhoto(photo),要么是renderLoadingView()。我们可以假设如果我们没有照片,我们正在加载它。前面的代码的好处是在使用之前检查我们的 props 并确保它们是有效的。

许多计算机课程和自助书籍剥离了错误处理以“专注于重要的事情”。

错误处理是你的应用程序中重要的事情。在这种情况下,它特别重要,因为当我们检索数据时,我们还没有加载照片,所以我们没有东西可以显示。如果我们不处理这个问题,我们会得到一个错误。我剥离的是console.log语句;如果你下载本书的源代码,你会发现更多的详细注释和跟踪语句。

现在,让我们继续进行实际的渲染。这看起来欺骗性地简单,主要是因为所有序列化、获取和有选择地渲染的辛苦工作已经完成。这就是编程应该努力做到的—清晰、健壮、易于理解和维护。

一些代码示例变得很长,所以我把闭合括号和标签放在它们要关闭的对象的末尾。我建议你买一个大的台式屏幕,以更宽广的方式编码;当你花一个小时追踪丢失或放错的/>时,你会感激大尺寸的显示设备。这只会提高生产力。

  1. 添加以下代码:
renderLoadingView() {
    console.log('CameraData props during renderLoadingView', this.props);
    return (
        <View style={styles.frontCard} >
            <Text style={styles.manifestText}>Loading</Text>
            <Text style={styles.manifestText}>image data</Text>
            <Text style={styles.manifestText}>from NASA</Text>
            <Text style={styles.manifestText}>...</Text>
        </View>
    );
};
renderPhoto(photo) {
return (
   <View style={styles.baseView}>
      <CylindricalPanel
         layer={{
            width: 1000,
            height: 1000,
            density: 4680,
            radius: 20 }}>
         <Image
            source={{ uri: photo.img_src }}
            style={styles.panoImage}>
         </Image>
      </CylindricalPanel>
      <Model
         source={{
            obj: asset('ArrowDown.obj'),
            mtl: asset('ArrowDown.mtl'), }}
         lit
         style={{
            transform: [{ translate: [-2.5, -1, -5.1] }] }} />
      <Model
         source={{
            obj: asset('ArrowUp.obj'),
            mtl: asset('ArrowUp.mtl'), }}
         lit
         style={{
            transform: [{ translate: [1.3, -1, -5.1] }] }} />
      <View style={styles.manifestCard}>
         <Text style={styles.manifestText}>
            {photo.camera.full_name}</Text>
         <Text style={styles.manifestText}>
            {photo.rover.name} Rover #{photo.rover.id}</Text>
         <Text style={styles.manifestText}>
            Landed on: {photo.rover.landing_date}</Text>
         <Text style={styles.manifestText}>
            Launched on: {photo.rover.launch_date}</Text>
         <Text style={styles.manifestText}>
            Total Photos: {photo.rover.total_photos}</Text>
         <Text style={styles.manifestText}>
            Most recent: {photo.rover.max_date} Latest earth date</Text>
         <Text style={styles.manifestText}>
            Viewing: {photo.rover.max_sol} Mars Sol</Text>
         <Text style={styles.manifestText}>
            Taken: {photo.earth_date} Earth (GMT)</Text>
      </View>
   </View>
);
}
}

如果你迄今为止已经输入了所有的代码,当世界加载时,你会看到一个绿色的对话框,告诉你它正在接收数据。几秒钟后,它将被照片 2 和来自火星的数据的详细元信息所取代。

如果你想同时打开两个虚拟世界,例如,为了检查一些导入而不产生我们正在编程中的往返网络请求,你可以通过转到设置好的第二个世界,而不是npm start,使用react-native start --port 9091命令来实现。

我之前简要提到过这一点,但重要的是要注意 React 是多线程的;当它们的 props 或状态改变时,元素会改变它们的渲染,而无需告诉它们。这是多线程的,而不需要改变代码。这使你能够在世界填充数据时移动摄像机并查看。

这使虚拟世界看起来更加“真实”;它对输入做出响应,就像它是现实一样。它就是—我们创造了虚拟现实。

如何(不)让人生病

你可能已经注意到,我们把用户界面——图标和屏幕——放得有点远;到目前为止,我们把所有东西都放在至少五米外。为什么呢?

这是因为容纳-聚焦冲突。

当你的眼睛“注视”着某样东西,就像我们在第一章“虚拟现实到底是什么?”中讨论的那样,如果那个东西离你的脸很近,你的眼睛会试图对其进行聚焦。然而,你的头戴式显示器是一个固定焦距的设备,无论物体离你有多近或多远,它总是显示清晰的图像。在现实世界中,比如说,距离小于 3 到 4 英尺的物体会需要你的眼睛进行更多的聚焦,而距离 10 英尺的物体则需要较少的聚焦。

因此,你的眼睛会聚焦在一个你本应该需要更多聚焦的图像上,但你所看到的已经是清晰的(因为一切都是清晰的),所以你期望在现实世界中看到的和在头戴式显示器中看到的有所不同。

这不会导致任何实际的视觉问题——一切都是清晰的和聚焦的。

你可能会感到眼睛疲劳和一种模糊的不适感,这种感觉会随着使用头戴式显示器的时间变得更糟。

避免这种情况的方法是尽量将 UI 元素放得比我们在这个例子中展示的更远。比如不要将浮动屏幕放在眼镜的位置。如果你这样做,人们会看着它们,他们的眼睛会期望对着距离大约六英寸的东西进行聚焦,但从聚焦的角度来看,这个物体的距离已经超过了手臂的长度。这会让你的用户感到疲劳。

这就是为什么大多数虚拟现实让你看着远处的大屏幕进行选择。你可能希望将 UI 元素放在手腕上,甚至那样也有点冒险。

我觉得人们使用虚拟现实的次数越多,他们的眼睛和聚焦就会得到重新训练,然而,我不知道有没有任何医学研究显示这种效果。我之所以提到这一点,是因为我的一只眼睛近视,另一只眼睛远视;当我戴上眼镜时,我的聚焦会发生变化。有趣的是,如果我戴上“没有镜片”的眼镜,我的聚焦仍然会发生变化。我觉得人类大脑是无限适应的,我们可以克服调节-调节冲突。

然而,用户的体验可能会有所不同,所以不要让他们因为把东西放得太近(距离小于一米)而感到疲劳。

总结

在本章中,你学到了很多东西。我们通过构建消耗 JSON API 的网络服务调用,使我们的世界真正实现了互动。我们看到了一些获取数据的方法,并使用了更多或更少内置的fetch语句。这些 API 调用现在是异步的,所以我们可以环顾四周,欣赏火星,而我们请求的相机数据正在加载。

我们已经看到了如何通过处理跨站脚本问题来构建安全的世界。我们创建了合理的文本并进行了条件渲染。我们还讨论了错误处理。

做所有这些需要一些时间,我们在开发过程中有几次花了几个小时来排列对象。有几次我被关闭,因为我在一个小时内超过了DEMO_KEY检索次数。这就是为什么我建议你获取自己的 API 密钥,然后你就可以请求更多的图片。

这一章相当长,虽然检索了真实世界的数据,但世界还不是完全互动的。在下一章中,你将学习如何使你的世界与我们的输入互动。这就是为什么我在前面的视图中加入了+和-箭头。查看下一章,找出如何将它们连接到页面通过我们的火星数据。我会展示一个不同的世界,但展示如何使按钮互动。你可以通过做简单的属性更改来使加号和减号按钮变得真实。

第十一章:走在野生的一边

到目前为止,在前面的章节中,我们已经建立了一些真实但小型的世界。

然而,有一些东西一直缺失。一开始,我谈到了 VR 作为一种可以互动的东西——一种现实,即使它看起来并不真实。到目前为止,我们所做的大部分是看和观察事物,但我们无法四处移动。

在本章中,我们将做到这一点。

你将学习以下主题:

  • 使用 NPM 添加组件

  • 凝视按钮

  • 使用凝视按钮触发事件

  • 添加 JavaScript 文件

  • 将 JavaScript 文件转换为动态构建几何图形

  • 在我们创建的世界中移动观点

  • 移动使事物看起来更真实

  • 更多关于 VR 控制器的信息

发疯了——VR 运动方式

我小时候晕车。VR 也会让你晕车——之前在介绍 VR 时已经讨论了这些原因,但这是一个非常重要的话题,所以值得重复。

如果你移动一个观点,独立于用户的行为(用户代理),大脑会知道它没有移动。然而,大脑也会看到世界通过你(VR)的眼睛移动。然后,大脑依赖于一个非常古老、重要的生存特征——你会认为自己中毒了。

当你中毒时,你的身体非常擅长呕吐。用不太临床的术语来说,你会呕吐。你的身体认为有什么东西试图杀死你,所以它只是想尽快摆脱胃里的任何东西,作为一种恐慌反应。

那么,在 VR 中如何移动?如何在不让人发疯的情况下启用 VR 运动方式?

VR 运动方式的类型

讨论 VR 运动方式,至少要稍微讨论一下 VR 控制器。你手中拿着的东西,脚下的东西,支撑你的东西,或者让你在周围滚动的东西显然会产生巨大的影响。

我们正在讨论 WebVR,虽然对人们来说非常容易上手,但这可能意味着你的用户可能没有各种类型的 VR 装备。如果你确实有装备,你可能会发现对于你的应用程序,更简单的运动方式更好,而且编码速度肯定更快。

在讨论设备时,人们讨论自由度(DOF)。这实际上与严格考虑自由度有关,但主要是关于被跟踪的内容。

如果您有手持设备,您可能只有3DOF;这意味着电子设备可以跟踪您是否围绕其中心旋转。6DOF控制器是这样跟踪的,但它也可以检测自己是否在移动,换句话说,是在进行平移。通常,每个都有 3 个度。

6DOF 控制器更加逼真;您可以伸手触摸物体。然而,它们需要某种形式的跟踪,对于目前的行业状态来说,通常意味着外部跟踪器,比如 Vive 灯塔或 Oculus 摄像头。

还有一种称为内部跟踪的跟踪方式,这意味着头戴设备本身可以看到控制器并确定它们的位置。它们确实使用摄像头,只是不是散布在房间周围的外部摄像头。

很难将运动类型归类为无控制器的运动方式;它也可能与控制器(传送)一起很好地工作。

我不会真的包括四处移动头部(或四处移动鼠标),尽管那也是移动;没有这个,VR 系统实际上不是真正的 VR(按我的定义)。然而,确实有一些 VR 头戴设备不包括这个功能,或者做得不好。这是高端手机(三星 Gear VR 和 Google Daydream)和 PC 头戴设备 Vive 和 Rift 的真正突破。

考虑以下类型的 VR 运动:

  • 凝视检测:您看着某物,它会激活一个效果,一个眨眼,或者让您移动

  • 车辆/驾驶舱运动:您的视野显示墙壁或驾驶舱的细节

  • 可以通过凝视检测移动

  • 带有控制器(游戏手柄等)

  • 定时/人工(按下按钮或在一段时间后移动玩家)

  • 只有轻微的生病几率

  • 房间规模

  • 四处走动(直到边界)

  • 生病的几率非常低

  • 需要硬件

  • 传送或眨眼

  • 通常使用凝视或 3DOF 或 6DOF 控制器

  • 传送也可以分成小步骤进行——消除运动(视觉加速);这会让您感觉自己在移动,但不会让您感到恶心

  • 跑步机

  • 一种您站在上面并移动脚,它会检测您的移动方式

  • 还有滑翔伞模拟器和飞行模拟器,您可以躺下或坐下,通过移动身体重量来飞行

  • 所有这些都很大且昂贵,通常只限于 VR 游乐场

  • 跟踪的 6DOF 控制器运动范式

  • Vive/Rift 通常使用传送,而 6DOF 控制器使其变得容易

  • 有许多其他使用 6DOF 控制器的移动方式;一个好的列表可以在bit.ly/VRLoco找到

  • 人工运动/轨道:

  • 一旦你使用 UI 指示要做什么,VR 系统就会沿着一条路径将你移动。

  • 凝视/头部控制的转向属于这一类。

  • 很容易让人感到恶心。

  • 如果你的头转动,它可能会很烦人;只需改变你的移动方式;即使你不会感到恶心,你也会感觉自己被带走了。不过,通过谨慎的实施,它也可以起作用。

围绕移动的方式当然受到你拥有多少硬件的限制。另一个限制是你想要多大的受众群体。如果你设计你的 VR 应用程序为房间规模(自然四处走动),你就排除了每个手机用户。然而,同样地,如果你决定使用凝视瞬移系统,那些拥有房间规模 VR 的人会感到沮丧,因为他们不能四处走动。

WebVR 目前更多地针对移动 VR,房间规模是一个很大的编程挑战。这是可能的,但在 React-VR 和 WebVR 中并没有内置。从硬件可用性的角度来看:

  • 无需设备(Google Cardboard):

  • 自然运动(平移/倾斜)- 仅限少量

  • 凝视检测

  • 通过定时器或凝视检测的人工运动('轨道'运动,就像你在轨道上)

  • 带控制器的 VR 头盔(Gear VR,Daydream 等):

  • 现在我们有更好的方法,但仍然可以做所有以前的方法:

  • 自然运动(平移/倾斜)- 仅限少量

  • 凝视检测

  • 通过定时器或凝视检测的人工运动('轨道'运动,就像你在轨道上)

  • 驾驶舱运动

  • 通过控制器进行瞬间移动

  • 操纵杆/控制器

  • PC VR–Vive/Rift:

  • 现在我们有更好的方法,但仍然可以做所有以前的方法:

  • 自然运动(平移/倾斜)- 仅限少量

  • 凝视检测

  • 通过定时器或凝视检测的人工运动('轨道'运动,就像你在轨道上)

  • 驾驶舱运动

  • 通过控制器进行瞬间移动

  • 操纵杆/控制器(在被跟踪的 6DOF 控制器上)

  • 被跟踪的 6DOF 控制器运动范式

  • 房间规模行走

  • 高端设备:

  • 全景虚拟跑步机或其他跑步机

避免幽灵效应

还有另一个原因,为什么我们希望人们能够在没有某种用户代理的情况下四处移动;没有移动,它真的不是虚拟现实。实际上,我们都在四处移动;猫在潜行时会侧着头。如果你感到好奇,你会歪着头。在 360 度视频中,一个挑战是你只能四处看看;你不能移动。歪着头真的没什么用。

360 度视频会发生什么,尽管它可能非常详细,但你会感觉自己像一个游荡的幽灵。你不能往下看到自己(尽管你可能会看到摄像机支架),你不能四处移动,也不能伸手触摸东西,也不能改变你的视角。如果你歪着头,或者左右移动,就没有视差效果。

我真的很喜欢 360 度视频,但我也觉得它并不真正是虚拟现实,因为最终你会感到游离,本质上是一个被束缚的幽灵。当然,视频可能会移动,但你无法改变它的移动方式;你只是随波逐流。

我对 WebVR 非常印象深刻的一个微妙之处是,如果你歪着头,VR 视图会稍微移动,就好像你在侧头。这是一个微妙的效果;它不是室内尺度的 VR,你不能四处走动,但它是一种 VR。你不会感觉自己像一个游荡的幽灵。

让人们探索他们的环境是很重要的;没有这一点,你真的会感觉自己像一个幽灵。在我们的例子中,我们将使用传送移动的隐喻,让人们探索一个迷宫。

没有与世界互动和移动的能力,你会感觉自己像一个游荡的幽灵。虽然我们几乎用了整本书的篇幅才达到这一点,但与环境和世界互动的能力是虚拟现实中最重要的事情之一。

在这一章中,你将能够使用任何 WebVR 客户端来做到这一点。如果我们知道每个人都有 HTC Vive 或室内尺度的 Oculus Rift,我们可以向你展示在迷宫中四处走动的代码,尽管这会带来一些有趣的用户界面问题——如果有人走过篱笆会怎么样?在我们获得全身触觉套装之前,你可以穿过虚拟墙。有一些使用用户界面来抵消这一点的方法,比如将屏幕短暂地变黑,然后将用户传送回起点,只是允许他们作弊(不好),或者其他有趣的方法来解决这个问题。

现在,我们将简单地允许用户移动到迷宫中的下一个单元格/开放位置,并且仅限于该位置。我们将使用凝视选择,这意味着当您盯着一个 UI 元素时,我们会知道您已经点击它。这将适用于市场上所有的 VR 设备,这真的是开始的最佳地点。更复杂的 UI 元素需要检查用户拥有的 VR 控制器和跟踪类型,并根据需要启用适当的移动。这超出了本书的范围。

在讨论如何在我们的世界中移动之前,我们需要有一些有趣的东西可以四处走动。例如,也许我们在森林中漫步,发现迷宫挡住了我们的去路,或者是清晨,我们想去一个小湖看清晨的雾。

让我们来建造那个迷宫。

建造迷宫

我们可以建造迷宫的几种方式。最直接的方法是启动我们的 3D 建模软件(比如 Blender)并用多边形创建一个迷宫。这样做效果很好,也可以非常详细。

然而,这也会很无聊。为什么?第一次通过迷宫会很激动,但几次尝试之后,你会知道通往目的地的路。当我们构建 VR 体验时,通常希望人们经常访问并每次都有愉快的时光。

建模的迷宫会很无聊。生命太短暂,没有时间做无聊的事情。

因此,我们希望随机生成一个“迷宫”。这样,您可以每次都改变“迷宫”,使其保持新鲜和不同。为了做到这一点,我们需要通过随机数来确保“迷宫”不会围绕我们移动,所以我们实际上希望用伪随机数来实现。要开始做到这一点,我们需要创建一个基本的应用程序。请转到您的 VR 目录并创建一个名为“WalkInAMaze”的应用程序:

react-vr init WalkInAMaze

几乎随机-伪随机数生成器

为了有机会重播价值或能够比较不同人之间的分数,我们真的需要一个伪随机数生成器。基本的 JavaScript Math.random()不是伪随机生成器;它每次都会给你一个完全随机的数字。我们需要一个带有种子值的伪随机数生成器。如果你给随机数生成器相同的种子,它将生成相同的随机数序列。(它们并不是完全随机的,但非常接近。)随机数生成器是一个复杂的话题;例如,它们被用于密码学,如果你的随机数生成器不是完全随机的,有人可能会破解你的代码。

我们不太担心这一点,我们只是想要可重复性。尽管这方面的用户界面可能超出了本书的范围,但以一种点击刷新不会生成完全不同的Maze的方式创建Maze真的是一件好事,会避免用户的沮丧。这也将允许两个用户比较分数;我们可以为Maze持续一个板号,并显示这个。这可能超出了我们书的范围;然而,拥有可预测的Maze在开发过程中将会极大地帮助。如果没有这一点,你可能会在工作中迷失方向。(好吧,可能不会,但这样测试会更容易。)

包含来自其他项目的库代码

到目前为止,我已经向你展示了如何在 React VR(或 React)中创建组件。有趣的是,JavaScript 在include方面有一个历史问题。在 C++、Java 或 C#中,你可以在另一个文件中include一个文件或在项目中引用一个文件。在那之后,那些其他文件中的所有内容,比如函数、类和全局属性(变量),都可以从发出include语句的文件中使用。

在浏览器中,“包含”JavaScript 的概念有点不同。在 Node.js 中,我们使用package.json来指示我们需要哪些包。要将这些包引入我们的代码中,我们将在.js 文件中使用以下语法:

var MersenneTwister = require('mersenne-twister');

然后,我们将创建一个新的随机数生成器并传递一个种子,而不是使用Math.random()

  var rng = new MersenneTwister(this.props.Seed);

从这一点开始,你只需要调用rng.random()而不是Math.random()

目前,我们只需使用 npm install <package>require 语句来正确格式化包。在下一章中,我们将讨论升级并修改 package.json,以确保代码正确地发布和更新。执行 npm 命令可以为您完成其中的大部分工作:

npm install mersenne-twister --save

记住,--save 命令用于更新项目中的清单。在此期间,我们还可以安装另一个以后会用到的包:

npm install react-vr-gaze-button --save

现在我们有一个很好的随机数生成器,让我们用它来复杂化我们的世界。

迷宫渲染()

我们如何构建一个 Maze?我想开发一些动态生成 Maze 的代码;任何人都可以在一个包中对其进行建模,但 VR 世界应该是活生生的。拥有能够动态构建 Maze 的代码(在一定程度上)将允许您重复玩您的世界。

有许多用于打印迷宫的 JavaScript 包。我选择了一个似乎无处不在的、公共领域的 GitHub 上的包,并对其进行了 HTML 修改。这个应用程序由两部分组成:Maze.htmlmakeMaze.JS。它们都不是 React,而是 JavaScript。它运行得相当不错,尽管数字并不真正代表宽度。

首先,我确保只有一个 x 在垂直和水平方向上显示。这样打印效果可能不好(行通常比列),但我们正在构建一个虚拟的 Maze,而不是纸质的 Maze

我们使用 Maze.htmllocalhost:8081/vr/maze.html)和 JavaScript 文件 makeMaze.js 生成的 Maze 现在看起来是这样的:

x1xxxxxxx
x   x   x
xxx x x x
x x   x x
x xxxxx x
x x   x x
x x x x x
x   x   2
xxxxxxxxx

这有点难以阅读,但你可以数一下方块和 x 的数量。别担心,它会看起来更加花哨。现在我们已经让 HTML 版本的 Maze 工作了,我们将开始建造树篱。

这段代码比我预期的要长一些,所以我把它分成了几部分,并将 Maze 对象加载到 GitHub 上,而不是在这里粘贴整个代码,因为它太长了。您可以在以下链接找到源代码:bit.ly/VR_Chap11

添加地板和类型检查

正如我们之前讨论过的,360 全景背景的一个奇怪之处是,你似乎可以“漂浮”在地面上。除了修复原始图像之外,另一个解决方法就是简单地添加一个地板。这就是我们在太空画廊中所做的,看起来相当不错,因为我们假设我们在太空中漂浮。

对于这个版本,让我们import一个地面方块。我们可以使用一个大方块来包含整个Maze;然后如果Maze的大小发生变化,我们就必须调整它的大小。我决定使用一个较小的立方体,并对其进行修改,使其“位于”Maze的每个单元格下方。这将使我们在将来有一些余地,可以旋转方块以制作磨损的路径、水陷阱或其他东西。

为了制作地板,我们将使用一个简单的立方体对象,我稍微修改了它,并进行了 UV 映射。我用 Blender 做的这个。我们还import了一个Hedge模型和一个Gem,它将代表我们可以传送到的地方。在Maze.js内部,我们添加了以下代码:

import Hedge from './Hedge.js';
import Floor from './Hedge.js';
import Gem from './Gem.js';

然后,在Maze.js内部,我们可以用以下代码实例化我们的地板:

<Floor X={-2} Y={-4}/>

注意,当我们进行导入时,我们不使用'vr/components/Hedge.js';我们在 Maze.js 内部。然而,在 index.vr.js 中包含 Maze 时,我们确实需要:

import Maze from './vr/components/Maze.js';

然而,情况稍微复杂一些。在我们的代码中,当属性发生变化时,迷宫会构建数据结构;在移动时,如果迷宫需要重新渲染,它会简单地遍历数据结构并构建一个包含所有地板、传送目标和树篱的集合(mazeHedges)。鉴于此,要创建地板,在Maze.js中的代码实际上是:

        mazeHedges.push(<Floor {...cellLoc} />);

在这里,我遇到了两个大问题,我会告诉你发生了什么,这样你就可以避免这些问题。最初,我一直在试图弄清楚为什么我的地板看起来像树篱。这个问题很容易——我们从Hedge.js文件中导入了Floor。地板看起来像树篱(你在我的前面的代码中注意到了吗?如果是的话,我是故意这样做的,作为一个学习经验。诚实地说)。

这是一个简单的修复。确保你的代码中有import Floor from './floor.js';注意Floor没有经过类型检查。(毕竟,这是 JavaScript。)我觉得这很奇怪,因为hedge.js文件导出了一个Hedge对象,而不是一个Floor对象,但请注意,你可以在import它们时重命名对象。

我遇到的第二个问题更像是一个简单的失误,如果你没有真正思考 React,很容易发生。你可能也会遇到这个问题。JavaScript 是一种可爱的语言,但有时我会想念一种强类型的语言。这是我做的:

<Maze SizeX='4' SizeZ='4' CellSpacing='2.1' Seed='7' />

maze.js文件中,我有这样的代码:

for (var j = 0; j < this.props.SizeX + 2; j++) {

经过一些调试,我发现j的值从0变成了42。为什么会变成42而不是6呢?原因很简单。我们需要充分理解 JavaScript 才能编写复杂的应用程序。错误在于将 SizeX 初始化为'4';这使它成为一个字符串变量。当从0(一个整数)计算j时,React/JavaScript 会取2,将其加到一个字符串'4'上,得到字符串42,然后将其转换为整数并赋给j

当这样做时,非常奇怪的事情发生了。

当我们构建 Space Gallery 时,我们可以轻松地使用'5.1'的值作为输入到框中:

<Pedestal MyX='0.0' MyZ='-5.1'/>

然后,在类中使用下面的转换语句:

 transform: [ { translate: [ this.props.MyX, -1.7, this.props.MyZ] } ]

React/JavaScript 会将字符串值放入This.Props.MyX,然后意识到它需要一个整数,然后悄悄地进行转换。然而,当你得到更复杂的对象,比如我们的Maze生成时,你就逃不过这一点。

记住,你的代码并不是“真正”的 JavaScript。它是经过处理的。在本质上,这种处理是相当简单的,但其影响可能是致命的。

注意你所编写的代码。在 JavaScript 这样的弱类型语言中,再加上 React,你所犯的任何错误都会悄悄地转换成你意想不到的结果。

你是程序员。要正确编程。

所以,回到MazeHedgeFloor基本上是初始Gem代码的副本。让我们来看看我们的起始Gem,尽管请注意它后来变得更加复杂(以及在你的源文件中):

import React, { Component } from 'react';
import {
    asset,
    Box,
    Model,
    Text,
    View
} from 'react-vr';

export default class Gem extends Component {
    constructor() {
        super();
        this.state = {
            Height: -3 };
    }
    render() {
        return (
            <Model
                source={{
                    gltf2: asset('TeleportGem.gltf'),
                }}
                style={{
                    transform: [{ translate: [this.props.X, this.state.Height, this.props.Z] }]
                }}
            />
        );
    }
}

HedgeFloor本质上是相同的东西。(我们本可以让一个 prop 成为加载的文件,但我们希望Gem有不同的行为,所以我们将大幅编辑这个文件。)

要运行这个示例,首先,我们应该像之前一样创建一个名为WalkInAMaze的目录。一旦你这样做了,从本章的 Git 源下载文件(bit.ly/VR_Chap11)。一旦你创建了应用程序,复制了文件并启动了它(进入WalkInAMaze目录并输入npm start),你应该看到类似这样的东西一旦你四处看看——除了有一个 bug。这就是迷宫应该看起来的样子(如果你在Hedge.js中使用文件'MazeHedges2DoubleSided.gltf',在<Model>语句中):

那么,我们是如何在游戏中得到那些看起来整洁的树篱的呢?(好吧,它们的多边形确实很低,但仍然可以。)Web 标准改进的速度之一是它们的新功能。现在,React VR 不仅支持.obj 文件格式,还可以加载 glTF 文件。

使用 glTF 文件格式进行建模

glTF 文件是一种新的文件格式,与 WebGL 非常自然地配合。有许多不同的 CAD 软件的导出器。我喜欢 glTF 文件的原因是,获得正确的导出相当简单。Lightwave OBJ 文件是行业标准,但在 React 的情况下,并非所有选项都被导入。一个主要的问题是透明度。OBJ 文件格式允许这样做,但在撰写本书时,这并不是一个选项。许多其他现代硬件可以处理的图形着色器无法用 OBJ 文件格式描述。

这就是为什么 glTF 文件是 WebVR 的下一个最佳选择。这是一种现代和不断发展的格式,正在努力增强功能,并在 WebGL 可以显示的内容和 glTF 可以导出的内容之间取得相当好的匹配。

然而,这是一章关于与世界互动的内容,所以我会简要提及如何导出 glTF 文件并提供对象,特别是Hedge,作为 glTF 模型。

从建模方面来看,glTF 的好处是,如果您使用它们的材质规范,例如 Blender,那么您就不必担心导出不够准确。今天的基于物理的渲染PBR)倾向于使用金属/粗糙模型,这些比尝试将 PBR 材质转换为 OBJ 文件的镜面光照模型更容易导入。这是我用作凝视点的看起来金属质的Gem

使用 glTF 金属粗糙模型,我们可以分配纹理贴图,例如 Substance Designer 等程序计算并轻松导入。结果看起来金属的地方看起来金属,油漆仍然保持的地方看起来暗淡。

我在这里没有使用环境遮挡,因为这是一个非常凸起的模型;表面凹陷更多的东西会与环境遮挡搭配得很棒。例如,对于建筑模型和家具,也会看起来很棒。

要转换您的模型,可以在bit.ly/glTFExporting找到用户文档。您需要下载并安装 Blender glTF 导出器。或者,您可以直接下载我已经转换过的文件。如果您要进行导出,简而言之,您需要执行以下步骤:

  1. bit.ly/gLTFFiles下载文件。您将需要gltf2_Principled.blend文件,假设您使用的是 Blender 的较新版本。

  2. 在 Blender 中,打开您的文件,然后链接到新的材质。转到文件->链接,然后选择gltf2_Principled.blend文件。一旦您这样做了,进入“NodeTree”,然后选择 glTF 金属粗糙度(用于金属)或其他材质的 glTF 高光光泽。

  3. 选择要导出的对象;确保选择 Cycles 渲染器。

  1. 在窗口中打开节点编辑器(就像您在之前的章节中处理图像时所做的那样)。向下滚动到节点编辑器窗口的底部,并确保“使用节点”框被选中。

  1. 通过节点菜单添加节点,添加->组->glTF 高光光泽或金属粗糙度。

  2. 添加节点后,转到添加->纹理->图像纹理。添加与图像地图数量相同的图像纹理,然后将它们连接起来。您应该得到类似于这个图表的东西。

  1. 要导出模型,我建议您禁用相机导出并合并缓冲区,除非您认为将要导出共享几何图形或材质的多个模型。我使用的导出选项如下:

现在,要包含导出的 glTF 对象,使用<Model>组件,就像使用 OBJ 文件一样,只是没有 MTL 文件。所有材质都在.glTF 文件中描述。要包含导出的 glTF 对象,只需将文件名作为<Model中的 gltf2 属性:

 <Model
 source={{ gltf2: asset('TeleportGem2.gltf'),}}
...

要了解更多关于这些选项和流程的信息,您可以访问 glTF 导出网站:bit.ly/WebGLTF。该网站还包括主要 CAD 软件的教程以及非常重要的 glTF 着色器(例如,我之前展示的 Blender 模型)。

我已经加载了几个.OBJ 文件和.glTF 文件,您可以在bit.ly/VR_Chap11上尝试不同的低多边形和透明度的组合。当在 React VR 版本 2.0.0 中添加了 glTF 支持时,我感到非常兴奋,因为透明度贴图对于许多 VR 模型非常重要,特别是植被;就像我们的树篱一样。然而,事实证明在 WebGL 或 three.js 中存在一个 bug,无法正确渲染透明度。因此,我在 GitHub 网站上的文件中选择了低多边形版本;上面的图片是使用Hedges.js文件中的MazeHedges2DoubleSided.gltf文件(在 vr/components 中)。

如果您遇到 404 错误,请检查 glTF 文件中的路径。这取决于您使用的导出器——如果您使用的是 Blender,Khronos 组的 gltf2 导出器会正确计算路径,但 Kupoman 的导出器有选项,您可能会导出错误的路径。

动画 — VR 按钮

好了!我们想要做一些动画。为了做到这一点,我们将使用 VRButton。当发生以下情况之一时,它会激活:

  • XBox 游戏手柄上的 A 按钮

  • 键盘上的空格键

  • 用鼠标左键单击

  • 屏幕上的触摸

不幸的是,我们的“最低公共分母”是 Google Cardboard,可能有,也可能没有按钮。您不想不得不把手指伸进去尝试触摸屏幕。(说了这些之后,更新的 VR 头盔有一个小杠杆可以戳屏幕,即使是在实际的硬纸板版本中)。我们将使用凝视按钮。当鼠标指针或屏幕中心(由一个小点标记)悬停在您的对象上时,事件将被调用,我们的代码将处理这个问题。

凝视按钮也被打包成了npm生态系统中的一个漂亮的<GazeButton>对象。请参考网页:bit.ly/GazeButton。要使用它,我们需要了解它的功能,以及如何让视图知道一个Gem已经被“触摸”(或者被观察了两秒)。我们在本章的前面已经安装了它;如果你到目前为止还没有安装,我们可以通过使用 Node.js 命令提示符并输入以下命令来安装它:

npm install react-vr-gaze-button

我们可以使用 VR 按钮,但那样我们就必须处理进入对象、离开对象、倒计时等等。GazeButton会为我们处理所有这些。请注意,它对子元素的期望方式与我们到目前为止所习惯的方式有些不同。

现在,您的Gem.js代码(注意大写)应该如下所示:

import GazeButton from 'react-vr-gaze-button'
export default class Gem extends Component {

  constructor() {
    super();
    this.state = {
      Height: -3,
      buttonIsClicked: false
    };
  }
  onGemClicked() {
    this.setState({ buttonIsClicked: true });
    console.log("Clicked on gem " + this.props.X + " x " + this.props.Z);
  }
  render() {
    const { buttonIsClicked } = this.state
    return (
      <GazeButton onClick={() => this.onGemClicked()}
        duration={2000}>
        {time => (

          <Model
            source={{
              gltf2: asset('TeleportGem.gltf'),
            }}
            style={{
              transform: [{ translate: [0, -1, 0] }]
            }}
            style={{
              transform: [{ translate: 
                [this.props.X, this.state.Height, this.props.Z] }]
            }}
          />
        )}
      </GazeButton>
    );
  }
}

现在,当我们在桌面上尝试这样做时,似乎可以工作,但在手机上(我尝试了三星 GearVR),没有光标,也没有可以点击的东西。我们需要实现一个射线投射器(即使没有控制)。

正如我们在本章开头简要讨论的那样,有许多不同类型的 VR 控制系统,默认情况下是“没有”VR 输入设备,包括屏幕中心光标。

适当的控制系统的实施在我们手中。

当您使用桌面浏览器进行初始开发时,您会得到一个鼠标光标(包括在跟踪组件上时的光标),这可能意味着内置了注视光标;实际上并没有。只需意识到这是有合理理由的。

射线投射器

射线投射器向世界发射一条射线并计算它触及了什么。通常您会看到这些作为 VR 控制器发出的发光线。没有控制器时,射线投射器将从屏幕中心发射一条射线;这正是我们需要实现我们的注视按钮的地方。

在这种情况下,就像我们对按钮所做的那样,已经有一个simple-raycaster。如果您还没有安装它,您需要通过以下命令从npm安装它:

npm install --save simple-raycaster

在尝试使用软件包时,您可能希望跳过--save; 如果您这样做,请记得手动更新您的package.json文件,或者通过适当的工具进行更新。

实现simple-raycaster非常容易。在client.js中,在现有的import行(VRInstance)下面,添加以下import语句:

import * as SimpleRaycaster from "simple-raycaster";

在“//在此处添加自定义选项”处,插入以下行:

    raycasters: [
         SimpleRaycaster // Add SimpleRaycaster to the options
    ],
    cursorVisibility: "auto", // Add cursorVisibility

在您的 PC 上,此时情况会有点奇怪——屏幕中心会激活(并丢弃)宝石,即使您没有点击。这正是整个重点。

如果我们有更多的页面,当你的目光进入宝石时,我们会让宝石旋转。但现在,我们将把这个练习留给读者。

您将希望在onClick处理程序中开始动画。

到目前为止,我们已经展示了当注视宝石时如何获得事件。这很好,我们可以使用事件来触发移动,但我们如何移动呢?

有一件有点奇怪的事情是,React VR 没有像许多图形系统那样移动摄像头的方法。要移动当前的视角,你需要在index.vr.js的开头将<View>向相反方向进行平移;这会使世界中的一切朝相反方向移动,看起来就像你在向前移动。要移动视角,我们需要将点击事件从Gem传递给其父级的父级(顶级 View)。

Props,state 和 events

React,以及 React VR,在其核心,以可预测、确定的方式处理 props、事件和状态,这就是使 React 应用程序保持一致、清晰且易于维护的原因。

当对象声明时创建 props,并且在对象的生命周期内不应更改。如果对象需要更改,例如我们的传送门宝石,那么应将这些值分配给state。这强制实现了自顶向下的单向数据流。如果组件在不同区域需要相同的状态,那么该state应该被提升到最高级的父级。

这会引发有趣的问题,如果你想让一个子组件告诉父组件有关事件的信息,或者根据较低级别的事件改变其状态。

处理这个问题有几种方法;在 React 世界中,这可能是一个复杂的主题。React VR 在处理状态、props 和事件方面与 React Native 或 React 没有区别。一个很好的起点是 React 文档中的State and Lifecycle

基本上,在 React 应用程序中,应该有一个用于变化的单一真相来源。如果父级不关心,例如Gem是更高还是更低(被踩或未被踩),那么你不需要让父级跟踪其子级的高度。将state保持在尽可能低的级别是正确的决定。高度可以从“我们是否踩了Gem”中计算出来,因此不应该是传递下来的 prop。(尽管在很多书籍文件中,出于简洁起见,我们已经硬编码了值,但你可能会考虑起始高度作为一个 prop;良好的编程规范说不要硬编码值。)

在我们的迷宫世界中,我们遇到了一个困境。我们通过改变世界树顶部的<View>节点来移动视角。然而,当我们点击每个<Gem>时,我们希望视图发生变化。

我们可以用上下文来处理这个问题;许多库,比如 Redux 或 MobX,在内部使用上下文。还有一些使用上下文和其他功能的事件库。然而,上下文是一个有点高级的概念,对于我们正在做的事情来说有点过度。

在这种特殊情况下,我们将简单地将一个“回调”函数传递到子树中。我们这样做的原因如下:

  • 此时,从层次结构的角度来看,我们的应用程序相当小,只有三个级别。

  • “迷宫”本身可能需要知道用户何时到达终点(例如,显示烟花或更新高分)。“宝石”不知道这一点。如果“宝石”直接向视图发送通知,那么“迷宫”将永远不知道。

  • 我们可以引入额外的库,但这是一个简单的项目,在开源世界中太多外部依赖可能会导致问题。通常这不是一个大问题,如果出了问题,那就是开源的,去修复它。

如果在寻找外部包时出现问题,你需要卸载有问题的包,然后通过运行以下命令重新启动你的 Node.js 服务器:

npm start -- --reset-cache

npm cache clean --force不会执行此缓存重置。如果你忘记了,你得到的错误消息应该指出这一点。

使更新向上流动

尽管更新会传播下来,但我们需要传递信息。我们怎么做?很简单,用一个功能性的“回调”。

index.vr.js中,创建一些例行程序,并将这些例行程序与WalkInAMaze组件进行重要的绑定。在这一点上,我只展示了更改的行:

constructor(props) {
  super(props);
  this.state = {
    // ... existing member state initialization
  }
  this.handleClickGem = this.handleClickGem.bind(this); 
};

onClickFloor(X, Z) {
  this.setState({ newX: X, newZ: Z });
}

handleClickGem(X, Z) {
  this.setState({ newX: X, newZ: Z });
};

在我们的Gem.js中,我们已经有一个onClick方法。我们只需要添加一些新的props

    onGemClicked() {
        this.setState({ buttonIsClicked: true });
        //send it to the parent
        this.props.onClickGem(this.props.X, this.props.Z);
    }

现在,这个this.props.onClickGem是什么?这是一个从父级传递的函数 prop。在我们创建“宝石”时,我们只需插入以下 prop(加粗的插入行,注意源代码不能加粗):

... 
mazeHedges.push(<Gem {...cellLoc}
 onClickGem={this.handleClickGem}
  />);

好的,我们从哪里得到this.handleClickGem?在这个(简单)情况下,“迷宫”不会对事件做任何处理,只是将其传递。在Maze.js中,我们将添加一个处理程序:

constructor(props) {
  super(props);
  // existing code here doesn't change
  // at the bottom:
  this.handleClickGem = this.handleClickGem.bind(this);
}

handleClickGem(X, Z) {
  this.props.onClickGem(X, Z);
}

现在,我们注意到这里还有另一个 prop。这当然是由迷宫的父级传递给我们的;所以,在index.vr.js中,添加(加粗)这一行:

<Maze sizeX={this.state.sizeX} sizeZ={this.state.sizeZ} 
  cellSpacing={this.state.cellSpacing} seed={this.state.seed}
  onClickGem={this.handleClickGem} />

基本上就是这样。当Gem的 VR 注视按钮检测到点击时会发生什么?它调用了一个函数作为 prop。这会导致迷宫的handleClickGem被调用;它反过来调用了index.vr.js中的handleClickGem()。这个例程(双关语)然后设置内部状态。这个状态导致视图重新渲染:

handleClickGem(X, Z) {
        this.setState({ startX: -X, startZ: -Z });
    };

就是这样。请注意,你不仅仅通过this.startX = -X来设置state,你需要像前面的代码中所示调用this.setState()。然后这个例程将处理render()更新的传递。

这些都是大文件,我们刚刚做了很多改变。我在上面标出了重要的行,但我强烈建议你从bit.ly/VR_Chap11下载源文件并看看我们做了什么。在其中,我建立了一个 4x4 的迷宫,应该在大多数 PC 和移动设备上有合理的帧率。你可以尝试一些其他版本的各种对象(看起来像树篱或低多边形树篱)。

接下来怎么办?

这是一个相当基础的游戏,但你可以做很多事情。我们之前讨论过的一些东西,很容易包括在内,比如:

  • 我们的传送有点突然。我们应该有声音,或者甚至通过改变HandleClickGem()例程进行两次更新,以添加一个简短的动画或两步传送。请注意,通常不建议平滑地动画化视图本身;这会让人们感到不适,因为他们的眼睛说他们在移动,但他们的身体说没有。

  • 点击的宝石数量可以成为得分。这使我们有优势可以慢慢前进,一步一步地点击所有的宝石。

  • 你可以计时到达出口所需的时间,较短的时间可以增加你的得分。这使得快速前进并跳过传送宝石有优势。这两个目标是互斥的,通过平衡可以使游戏变得有趣。

  • 你可以在迷宫前面加入按钮来增加/减少大小或生成不同的随机数(并显示它们)。

  • 得分和随机数可以加载到高分 API 中。

  • 事件传递库,比如eventing-bus,使传递props变得更容易。我的目标是向你展示 React VR 的做法。

总结

在本章中,我们学习了构建完整的网络应用程序和游戏的最后一部分;再加上我们之前学到的知识,我们的旅程几乎已经完成;这实际上只是将现实带到网络的第一步。我们讨论了如何在 VR 世界中移动,包括基本的传送机制。我们讨论了凝视按钮和使用射线投射来实现它们。我们讨论了propsstate和事件的重要机制。为了实现这些流程,我们复习了重要的 React 哲学,即将state向上传递并处理下游事件。我们还讨论了使用伪随机数生成器来确保我们的propsstate不会发生混乱性变化。总的来说,我们现在知道如何创建、在其中移动,并使世界对我们做出反应。

在下一章中,我们将讨论接下来该怎么做,如何升级 React VR,以及如何在互联网上发布你的虚拟世界。

第十二章:发布您的应用程序,以及接下来要做什么

在家中开发和体验虚拟世界是很有趣的。但最终,您希望世界能看到您的世界。为了做到这一点,我们需要打包和发布我们的应用程序。在开发过程中,可能会出现对 React 的升级;在发布之前,您需要决定是否需要“冻结代码”并使用稳定版本进行发布,或者升级到新版本。这是一个设计决策。

在某个时候,您将需要升级,并且您将需要发布。本章将解释如何同时做到这两点,以及如何组织您的代码并检查您的干衣机和代码是否有 lint。我们将在这里涵盖以下主题:

  • 升级类型:Rip and Replace 或“Facelift”升级,或“现场升级”

  • 如何确保您的组件的正确版本存在

  • 开发与非开发版本、组件和库

  • 分发许可证

  • 链接和嵌入 VR 内容

  • 发布到常见的 Web 主机和内容交付网络。

  • 未来 5 年 VR 的发展方向

升级 React VR

其中一个很棒的事情,尽管可能会令人沮丧,是 Web 项目经常会更新。在编写本书时,React VR 已经更新。有几种不同的升级方式:

  • 您可以安装/创建一个同名的新应用程序

  • 然后您将转到您的旧应用程序并复制所有内容

  • 这是一种facelift升级或Rip and Replace

  • 进行更新。主要是对package.json进行更新,然后删除node_modules并重新构建。这是一种现场升级

您可以选择使用哪种方法,但主要区别在于现场升级有点更容易——无需修改和复制源代码——但可能有效也可能无效。Facelift 升级还取决于您是否使用了正确的react-vr-cli。每当您从命令提示符运行 React VR 时,都会有一个通知告诉您它是否过时:

当您从命令提示符运行 React VR 时,关于升级的错误或警告可能会很快地飞过。运行需要一段时间,所以您可能会离开一会儿去喝杯咖啡。

认真关注红线。

要进行就地升级,通常会从 Git 那里收到更新通知,如果您已经订阅了该项目。如果没有,您应该转到:bit.ly/ReactVR,创建一个帐户(如果您还没有),然后单击眼睛图标加入观察列表。然后,每次有升级时,您都会收到一封电子邮件。我们将介绍最简单的升级方式——就地升级,首先。

就地升级

您如何知道您安装了哪个版本的 React?从 Node.js 提示符中,键入以下内容:

npm list react-vr

还要检查react-vr-web的版本:

npm list react-vr-web

检查react-vr-cli(命令行界面,实际上只用于创建hello world应用程序)的版本。

npm list react-vr-cli 

检查ovrui(open VR 的用户界面)的版本:

npm list ovrui

您可以根据文档中的版本进行检查。如果您已经在 GitHub 上订阅了 React VR(您应该这样做!),那么您将收到一封电子邮件告诉您有升级。请注意,CLI 还会告诉您是否过时,尽管这仅适用于创建新应用程序(文件夹/网站)时。

发布说明在:bit.ly/VRReleases。在那里,您将找到升级的说明。升级说明通常要求您执行以下操作:

  1. 删除您的node_modules目录。

  2. 打开您的package.json文件。

  3. react-vrreact-vr-webovrui更新为"新版本号",例如 2.0.0。

  4. react更新为"a.b.c"。

  5. react-native更新为"~d.e.f"。

  6. three更新为"^g.h.k"。

  7. 运行npm install 或 yarn。

请注意和^符号;版本表示大致等于版本,^版本表示与版本兼容。这是一个帮助,因为您可能有其他包可能需要其他版本的react-nativethree。要获取{a...k}的值,请参考发布说明。

我还发现您可能需要在package.jsondevDependencies部分中包含这些模块:

"react-devtools": "².5.2",
"react-test-renderer": "16.0.0",

您可能会看到此错误:

module.js:529
 throw err;
 ^
Error: Cannot find module './node_modules/react-native/packager/blacklist'

如果您这样做,请在项目的根目录中进行以下更改

rncli.config.js文件。

var blacklist = require('./node_modules/react-native/packager/blacklist');行替换为var blacklist = require('./node_modules/metro-bundler/src/blacklist');

第三方依赖

如果你一直在尝试实验并使用npm install <something>添加模块,你可能会发现,在升级后,事情不起作用。package.json文件也需要知道你在实验过程中安装的所有额外包。这是项目方式(npm 方式)确保 Node.js 知道我们需要特定的软件。如果你遇到这个问题,你需要使用--save参数重复install,或者编辑你的package.json文件中的dependencies部分。例如(我加粗的那行),在上一章中我们尝试随机数时,我们可以手动添加这行:

{
  "name": "WalkInAMaze",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node -e \"console.log('open browser at http://localhost:8081/vr/\\n\\n');\" && node node_modules/react-native/local-cli/cli.js start",
    "bundle": "node node_modules/react-vr/scripts/bundle.js",
    "open": "node -e \"require('xopen')('http://localhost:8081/vr/')\"",
    "devtools": "react-devtools",
    "test": "jest"
  },
  "dependencies": {
    "ovrui": "~2.0.0",
    "react": "16.0.0",
    "react-native": "~0.48.0",
    "three": "⁰.87.0",
    "react-vr": "~2.0.0",
    "react-vr-web": "~2.0.0",
    "mersenne-twister": "¹.1.0"
  },
  "devDependencies": {
    "babel-jest": "¹⁹.0.0",
    "babel-preset-react-native": "¹.9.1",
    "jest": "¹⁹.0.2",
    "react-devtools": "².5.2",
    "react-test-renderer": "16.0.0",
    "xopen": "1.0.0"
  },
  "jest": {
    "preset": "react-vr"
  }
}

再次强调,这是手动的方法;更好的方法是使用npm install <package> -save

-s限定符将你安装的新包保存在package.json中。手动编辑可以确保你得到正确的版本,如果出现版本不匹配的情况。

如果你尝试安装和移除足够多的包,最终会搞乱你的模块。即使删除了node_modules后仍然出现错误,可以使用以下命令:

npm cache clean --force

npm start -- --reset-cache

单独的缓存清理不起作用;你需要reset-cache,否则问题包仍然会被保存,即使它们实际上不存在!

真的坏掉的升级 - 拆除和替换

然而,即使经过所有这些工作,你的升级仍然不起作用,也不是没有希望。我们可以进行拆除和替换升级。请注意,这有点像“最后的手段”,但它确实效果相当不错。按照以下步骤操作:

  1. 确保你的react-vr-cli包是全局最新的:
[F:\ReactVR]npm install react-vr-cli -g
C:\Users\John\AppData\Roaming\npm\react-vr -> C:\Users\John\AppData\Roaming\npm\node_modules\react-vr-cli\index.js
+ react-vr-cli@0.3.6
updated 8 packages in 2.83s

这很重要,因为当 React 有新版本时,你可能没有最新的react-vr-cli。它会在你使用它时告诉你有新版本发布了,但那行经常会滚动过去;如果你感到无聊并且没有注意到,你可能会花费很长时间尝试安装更新的版本,但却毫无结果。

npm 会生成大量的文字,但重要的是要阅读它说了什么,尤其是红色格式的行。

  1. 确保所有 CLI(DOS)窗口、编辑会话、Node.js 运行的 CLI 等都已关闭。(但是你不需要重新启动;只需关闭使用旧目录的所有内容)。

  2. 代码重命名为MyAppName140(在旧的react-vr目录末尾添加版本号)。

  3. 使用react-vr init MyAppName创建应用程序,换句话说,就是原始应用程序名称。

  4. 下一步最容易使用差异程序(参考bit.ly/WinDiff)。我使用 Beyond Compare,但也有其他选择。选择一个并安装它,如果需要的话。

  5. 比较两个目录,.\MyAppName(新)和.\MyAppName140,看看哪些文件已更改。

  6. 从旧应用中移动任何新文件,包括资产(您可能可以复制整个 static_assets 文件夹)。

  7. 合并任何已更改的文件,除了package.json。通常,您需要合并这些文件:

  • index.vr.js

  • client.js(如果您已更改)

  1. 对于package.json,查看已添加了哪些行,并通过 npm install <missed package> --save在新应用程序中安装这些包,或者启动应用程序并查看缺少了什么。

  2. 删除hello world应用程序种子的任何文件,例如chess-world.jpg(除非您当然正在使用该背景)。

  3. 通常情况下,您不会更改rn-cli.config.js文件(除非您修改了种子版本)。

大多数代码将直接移动过来。确保如果更改了目录名称,则更改应用程序名称,但根据前述的指示,您不必这样做。

如果 React VR 有大规模的更改,前述的升级步骤可能会稍微容易一些;这将需要一些对源文件的挑选。源代码非常直接,所以在实践中应该很容易。

我发现如果自动升级没有起作用,这些技术将效果最佳。

进行升级的最佳时间

如前所述,进行重大升级的时间可能不是在发布应用程序之前,除非有一些新功能是必需的。您希望充分测试您的应用程序,以确保没有任何错误。

我在这里包括升级步骤,但不是因为您应该在发布之前立即执行。

准备好发布您的代码

老实说,您永远不应该推迟整理衣服,直到,哦,等等,我们在谈论代码。您不应该推迟整理代码,直到您想要发布的那天晚上。即使您认为是临时的代码,也可能最终投入生产。从一开始就学习良好的编码习惯和风格。

良好的代码组织

从一开始就良好的代码对许多原因非常重要:

  • 如果您的代码缩进混乱,那么阅读起来会更加困难。许多代码编辑器,如 Visual Studio Code,Atom 和 Webstorm,都会为您格式化代码,但不要依赖这些工具。

  • 糟糕的命名约定可能会隐藏问题。

  • 变量的不正确用法可能会隐藏问题,比如使用this.State而不是this.state

  • 编码的大部分时间,高达 80%,都是在维护。如果你看不懂代码,就无法维护它。当你作为一名初学者程序员时,你经常会认为自己永远能读懂自己的代码,但当几年后你拿起一段代码并说“谁写的这破玩意?”然后意识到是你自己时,你就会停止使用像 a、b、c、d 这样的变量名。

  • 大多数软件在某个时候都会被其他人维护、阅读、复制或使用,而不是作者本人。

  • 大多数程序员认为代码标准是给“别人”准备的,但却抱怨自己必须写好代码。那么是谁来做呢?

  • 大多数程序员会立刻要求代码文档,并在找不到时翻白眼。我通常会要求看他们上一个项目的文档。我雇佣的每个程序员通常都会露出茫然的表情。这就是为什么我通常要求代码中有良好的注释。

  • 一个好的注释不是这样的:

//count from 99 to 1
for (i=99; i>0; i--)
    ... 
    • 一个好的注释是这样的:
//we are counting bottles of beer
for (i=99; i>0; i--)
    ... 

清理绒毛滤网(检查代码标准)

当你洗衣服时,绒毛会堆积,最终会堵塞你的洗衣机或烘干机,或者引发火灾。在 PC 世界中,旧代码、打字不好的名称等也会堆积起来。

重构是清理代码的一种方法。我强烈建议你使用某种版本控制,比如 Git 或 bitbucket 来检查你的代码;在重构时,完全有可能彻底搞砸你的代码,如果你不使用版本控制,可能会丢失很多工作。

在发布之前,对你的工作进行代码审查的一个很好的方法是使用linter。Linter 会检查你的代码并指出问题(垃圾)、不正确的语法,可能与你意图不同的东西,并且通常会尽力整理你的房间,就像你妈妈一样。虽然你可能不喜欢你妈妈这样做,但这些工具是非常宝贵的。毕竟,计算机非常挑剔,为什么不让它们互相对抗呢?

让软件检查你的 JavaScript 代码的最常见方法之一是一个名为ESLint的程序。你可以在这里了解更多信息:bit.ly/JSLinter。要安装 ESLint,你可以像大多数软件包一样通过 npm 来安装——npm install eslint --save-dev

--save-dev选项在你开发项目时会对你的项目有要求。一旦你发布了你的应用程序,你就不需要将 ESLint 信息与你的项目一起打包了!

还有一些其他事情你需要做才能让 ESLint 正常工作;阅读配置页面并完成教程。很大程度上取决于你使用的 IDE。例如,你可以在 Visual Studio 中使用 ESLint。

安装了 ESLint 之后,你需要配置一个本地配置文件。使用以下命令完成:

eslint --init

--init命令将显示一个提示,询问你如何配置它将遵循的规则。它会问一系列问题,并询问要使用什么样的风格。AirBNB 是相当常见的,尽管你可以使用其他的;没有错误的选择。如果你在公司工作,他们可能已经有标准,所以请向管理层确认。其中一个提示将询问你是否需要 React。

React VR 编码风格

编码风格几乎可以说是宗教,但在 JavaScript 和 React 世界中,一些标准是非常普遍的。AirBNB 在这里有一个很好的、相当受推崇的样式指南:bit.ly/JStyle

对于 React VR,一些要考虑的样式选项如下:

  • 变量名的第一个字母使用小写。换句话说,使用 this.props.currentX,而不是 this.props.CurrentX,并且不要使用下划线(这被称为驼峰命名法)。

  • 只有在命名构造函数或类时才使用帕斯卡命名法

  • 由于你在文件中使用帕斯卡命名法,所以文件名要与类名匹配,因此

导入MyClass from './MyClass'

  • 小心 0 与{0}。一般来说,学习 JavaScript 和 React。

  • 始终使用constlet声明变量,以避免污染全局命名空间。

  • 避免使用++和--。这对我来说很难,因为我是一个 C++程序员。希望在你读到这篇文章时,我已经在源代码示例中修复了它。如果没有,那就照我说的做,而不是照我做的做!

  • 学习=之间的区别,并正确使用它们,这对于 C++和 C#程序员来说是新的。

总的来说,我强烈建议你仔细研究这些编码风格,并在编写代码时使用一个代码检查工具:

第三方依赖

为了使您发布的网站/应用程序真正可靠地工作,我们还需要更新package.json;这是一种“项目”方式,确保 Node.js 知道我们需要特定的软件。我们将编辑"dependencies"部分以添加最后一行(我加粗的部分,加粗在文本编辑器中不会显示出来,显然!):

{
  "name": "WalkInAMaze",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node -e \"console.log('open browser at http://localhost:8081/vr/\\n\\n');\" && node node_modules/react-native/local-cli/cli.js start",
    "bundle": "node node_modules/react-vr/scripts/bundle.js",
    "open": "node -e \"require('xopen')('http://localhost:8081/vr/')\"",
    "devtools": "react-devtools",
    "test": "jest"
  },
  "dependencies": {
    "ovrui": "~2.0.0",
    "react": "16.0.0",
    "react-native": "~0.48.0",
    "three": "⁰.87.0",
    "react-vr": "~2.0.0",
    "react-vr-web": "~2.0.0",
    "mersenne-twister": "¹.1.0"
  },
  "devDependencies": {
    "babel-jest": "¹⁹.0.0",
    "babel-preset-react-native": "¹.9.1",
    "jest": "¹⁹.0.2",
    "react-devtools": "².5.2",
    "react-test-renderer": "16.0.0",
    "xopen": "1.0.0"
  },
  "jest": {
    "preset": "react-vr"
  }
}

这是手动的方法;更好的方法是使用npm install <package> -s

-s 限定符将安装的新软件包保存在package.json中。手动编辑可以确保您拥有正确的版本,如果出现版本不匹配的情况。

如果您尝试安装和删除足够多的软件包,最终会搞乱您的模块。如果删除node_modules后仍然出现错误,请发出以下命令:

npm start -- --reset-cache

npm cache clean --force

仅进行缓存清理是不够的;您需要重置缓存,否则问题软件包仍将被保存,即使它们在物理上不存在!

在网络上发布捆绑

假设您已经正确设置了项目依赖项,以便通过 ISP 或服务提供商从 web 服务器运行项目,您需要“捆绑”它。React VR 有一个脚本,可以将所有内容打包成几个文件。

当然,您的台式机也算是“web 服务器”,尽管我不建议您将开发机暴露在网络上。让其他人体验您的新虚拟现实的更好方法是将其捆绑并放在商业网络服务上。

为在网站上发布 React VR 进行打包

使用 React VR 提供的脚本,基本流程很容易:

  1. 转到通常运行npm start的 VR 目录,并运行npm run bundle命令:

  1. 然后,您将以与通常上传文件相同的方式转到您的网站,并创建一个名为vr的目录。

  2. 在您的项目目录中,在我们的案例中是f:\ReactVR\WalkInAMaze,在.\VR\Build中找到以下文件:

client.bundle.js

index.bundle.js

  1. 将它们复制到您的网站上。

  2. 创建一个名为static_assets的目录。

  3. 将您的所有文件(您的应用程序使用的文件)从AppName\static_assets复制到新的static_assets文件夹。

  4. 确保您为所有内容设置了 MIME 映射;特别是,.obj、.mtl 和.gltf 文件可能需要新的映射。请查阅您的 web 服务器文档:

  • 对于 gltf 文件,使用model/gltf-binary

  • 任何被 gltf 使用的.bin 文件应为application/octet-stream

  • 对于.obj 文件,我使用了application/octet-stream

  • 官方列表位于bit.ly/MimeTypes

  • 非常一般地说,application/octet-stream将以服务器上的文件的“原样”发送,因此这在某种程度上是一种通用的“捕获所有”

  1. index.html从应用程序的根目录复制到您发布应用程序的网站目录中;在我们的情况下,它将是vr目录,因此该文件与两个.js 文件并列。

  2. 修改index.html的以下行(请注意更改为./index.vr):

<html>
  <head>
    <title>WalkInAMaze</title>
    <style>body { margin: 0; }</style>
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
  </head>
  <body>
    <!-- When you're ready to deploy your app, update this line to point to your compiled client.bundle.js -->
 <script src="./client.bundle?platform=vr"></script>
    <script>
      // Initialize the React VR application
      ReactVR.init(
        // When you're ready to deploy your app, update this line to point to
        // your compiled index.bundle.js
 './index.vr.bundle?platform=vr&dev=false',
        // Attach it to the body tag
        document.body
      );
    </script>
  </body>
</html>

请注意,对于生产发布,这意味着如果您指向静态 Web 服务器上的预构建捆绑包而不是 React Native 捆绑器,则开发和平台标志实际上不会起作用,因此dev=truedev=false或甚至dev=foobar之间没有区别。

获取发布和归属

如果您从网络上的任何地方使用了任何资产,请确保您拥有适当的发布。例如,许多 Daz3D 或 Poser 模型不包括发布几何信息的权利;将这些信息包含在您的网站上作为 OBJ 或 glTF 文件可能会违反协议。某人可以轻松地下载模型,或者几乎所有的几何信息,然后将其用于其他用途。

我不是律师;您应该与您获取模型的地方核实您是否有权限,并在必要时正确归属。

归属许可在 VR 世界中有点困难,除非您将归属信息嵌入到某个图形中;正如我们所见,添加文本有时可能会分散注意力,并且您总会遇到比例问题。如果您在页面中嵌入了一个带有<iframe>的 VR 世界,您可以在 HTML 端始终给予适当的归属。但是,这并不是真正的 VR。

检查图像大小并使用内容交付站点

您使用的一些图像,特别是<pano>语句中的图像,可能相当大。您可能需要优化这些图像以获得适当的网页速度和响应性。

这是一个相当一般的话题,但可以帮助的一件事是内容交付网络CDN),特别是如果您的世界将是高流量的。

将 CDN 添加到您的 Web 服务器很容易。您可以从一个单独的位置托管您的资产文件,并在ReactVR.init()调用时将根目录作为 assetRoot 传递。例如,如果您的文件托管在https://cdn.example.com/vr_assets/,您可以更改index.html中的方法调用以包括以下第三个参数:

ReactVR.init(
   './index.bundle.js?platform=vr&dev=false',
   document.body,
   { assetRoot: 'https://cdn.example.com/vr_assets/' }
);

优化您的模型

在第十章中,引入真实世界,我们建立了一个迷宫,迷宫的每个方块都重复使用了相同的灌木模型。如果您观察了 Web 控制台,您可能会注意到这个模型一遍又一遍地被加载。这并不一定是最有效的方法。考虑其他技术,比如将模型作为属性传递给各种子组件。

多边形减少是另一种在优化 Web 和 VR 模型中非常有价值的技术。使用 glTF 文件格式,您可以使用“法线贴图”,仍然使低多边形模型看起来像高分辨率模型。这些技术在游戏开发领域得到了很好的记录。这些技术确实非常有效。

您还应该优化模型以不显示看不见的几何图形。例如,如果您显示一个有黑色窗户的汽车模型,就没有必要加载发动机细节和内部细节(除非窗户是透明的)。这听起来很明显,尽管我发现我用来说明照明示例的灯几乎比需要的多了两倍的多边形数量;玻璃灯罩内外都有多边形在模型内部。

既然我们已经发布了,接下来呢?

一旦我们建立了我们的现实,我们该怎么办?我们该去哪里?

如果您没有某种 VR 体验的想法,您就不会读这本书。我鼓励您去玩耍和尝试。您甚至可以尝试一些您听说过的不好的东西(比如移动视角)。

当 VR 再次爆发时,我曾经感到有些不满;我在之前的 VR 时代所做的大部分事情似乎已经被遗忘了;人们认为“VR 用户界面是新的!”。而与此同时,VR 学术文献实际上已经有几十年的历史,讨论了感知效果和 VR 用户界面,仅仅是 VR 的一个领域。

然而,这并不是一件坏事。如果涌入 VR 的人们能够提出一些新鲜的想法,也许那将是每个人都在寻找的“杀手级应用程序”。

也许你可以成为这样的人!我希望你能。

React VR 是一个轻量级的、支持 VR 的渲染系统。仍然有很多东西可以添加。

物理学-使世界相互作用

现实世界中有物体移动并相互作用。编写这种交互可能会变得乏味;这就是一个好的物理包可以胜任的地方。

简而言之,如果您的物体具有真实世界的物理特性,它们看起来会更加真实

我看到的许多物理演示都是弹跳球类型的演示,显示物体飞来飞去并撞击物体。我认为对物理的更微妙的方法,尽管准确(基于物理引擎),将在 VR 世界中给人一种逼真感,这是不应该被忽视的。

有几个 JavaScript 物理引擎:Cannon.JSOimo.js

Cannon.JS 是一个刚体物理引擎,包括简单的碰撞检测、各种物体形状、接触、摩擦和约束。源代码和文档在:bit.ly/CannonJS

碰撞检测算法本身就足以使用这些包之一,即使您不预见编写保龄球游戏或向砖墙射击球体。例如,碰撞检测可用于确定虚拟化身是否可以在 VR 世界中导航到特定位置。

涵盖 React VR 和 Cannon.js 的博客文章在:bit.ly/ReactPhysics

Oimo.js 是一个类似的刚体物理引擎;可以在:bit.ly/OmioPhysics找到。

请注意,Oimo.js 的示例显示本机的 three.js 单位,比 React VR(通常为 10 到 1,000)小。在 React VR 中,单位更多或少为 1=1 米,因此 Oimo.js 将集成得相当顺畅。

游戏引擎-让您与其他人互动

有一些网站可以打包 TCP/IP 代码以启用多人游戏。只要它们使用 JavaScript,React VR 就可以很好地集成到其中。其中一个引擎是Lance.gg,位于:bit.ly/Lance_gg

这是一个基于节点的游戏服务器。它是为了让 JavaScript 开发人员能够构建实时在线多人游戏而创建的,而不必担心实现网络同步代码。它致力于为开发人员和玩家提供流畅的体验,而不受延迟的影响。它具有以下功能:

  • Lance 负责网络代码,因此我们可以专注于 VR 部分

  • 可以支持任何类型的游戏或流派

  • 优化网络

  • 通过 Websockets 的 TCP

  • 通信被打包并序列化为二进制

  • 网络尖峰的自动处理和步骤校正

  • 用于处理延迟的智能同步策略

  • 外推(客户端预测)与步骤重演

  • 用于最佳对象运动的插值

  • 用于调试和跟踪的工具

货币化 VR

有几种方法可以从 VR 中赚钱。这是一种非常新的艺术形式,许多应用程序仍在被发现。像任何新领域一样,那里有很多实验或资助的亏损项目。主要的硬件制造商甚至通过资助开发人员来建立软件生态系统。

目前是“硬件先行还是杀手级应用程序先行”的情况。头戴式显示器的销售额强劲,尽管移动头戴式显示器绝对领先,三星 GearVR、Google Daydream 和 Cardboard 头戴式显示器的销量几乎是 Rift 和 Vive 等高端型号的近十倍。PSVR 是一个不错的系统,但对于不属于游戏工作室的开发人员来说,可能很难获得访问权限。

但是,不要忘记,您甚至不需要 VR 头盔就可以欣赏 React VR。您所编写的大部分代码将在浏览器中查看,而无需进入完整的 VR 模式,许多观众可能也会选择这样做。即使没有 VR 设备,将 VR 窗口嵌入网站仍将为人们带来引人入胜的体验。

在利用 VR 进行货币化方面,React VR 可能具有优势,至少在广告方面是这样。

赚取 VR 的方式如下:

  • 销售应用程序:要在网站上实现这一点,您可能需要实施某种付费观看系统。大多数人不喜欢网站周围的付费墙,因此这可能最适合完整的 VR 游戏,例如使用 Unity 和 Unreal 构建的游戏。但是,让我们不要忽视这一点。

  • 被付费构建 VR 体验:大多数搜索 VR 广告的网络搜索结果将显示出这些类型的应用程序。它们几乎总是免费下载,并且通常非常引人入胜。然而,在我看来,这不是 VR 生产者的增长模式;您可以获得相当不错的报酬来开发 VR 应用程序,但一旦完成,您的收入流也就结束了。我认为,发布与电影相关内容的 VR 网站的工作室版本基本上是相同的概念,我认为 React VR 非常适合这个领域。

  • 在你的世界中嵌入 VR 广告:React VR 的一个优势是它可以访问 React 本身的所有布局可能性,因此对于 React VR 来说,这可能比任何其他 VR 系统更容易和更直接。一个挑战是,沉浸在 VR 世界中的人不喜欢分心,因此弹出广告可能会产生相反的效果。不过,在你的世界中进行产品放置或者广告牌可能会非常有效,特别是如果你正在构建某种虚拟城市。

  • 从 VR 世界内部链接到产品或网站:这对于常规网站来说效果很好,尽管对于 VR 来说,挑战在于你可能需要摘下 VR 头盔才能真正与你在 VR 世界中刚刚点击的内容进行交互。一些广告公司已经讨论过在 VR 广告世界中构建链接,你可以从 VR 世界中跳转到这些链接,尽管到目前为止,大多数都是针对传统游戏引擎的。不过,我们可以期待未来在这一领域的大量发展。

  • 出售元数据:在这种模式下,应用本身将是免费的,但 VR 开发者将启用凝视跟踪。VR 世界内的热点区域可以以与更平面的 HTML 模型相同的方式出售给广告商,就像点击或印象一样。这是另一个新兴标准领域。

  • 从演示版本到完整版本:你可以使用从完整游戏引擎中获取的模型和资产构建一个 React VR 世界,然后将其放在网页上作为联系或免费赠品,以吸引人们对你的完整 VR 应用感兴趣。拥有行业标准格式,如 OBJ 和 glTF,将有助于实现这一点,尽管很多逻辑将不得不从头开始开发。一个可能更好的方法是在付费墙后面拥有一个免费的网页 URL 和一个付费的 URL。

  • 应用内购买:这将是游戏中的事物触发游戏外购买的一种方式。例如,视频播放器可以请求支付特定的视频,然后 React VR 代码将播放该视频。由于 React VR 具有直接集成 JavaScript 的简单方式,应用内购买将会相当简单。

VR 在未来五年将会走向何方

如果我确切地知道 VR 在未来五年会走向何方,那么五年后我会变得富有,如果你投资我的预测,你也会。让我知道结果如何。

我希望你通过开发令人惊叹的世界来投资 VR,即使不是通过 React VR。我真的相信 VR,并希望这一次它能取得成功。

这就提出了我所说的“这个时候”的意思。我至少经历过一次 VR 的浪潮;那时,每个人都认为 VR 是未来的浪潮。关于 VR 的所有伟大事物,我以前都听过。我大约在 1995 年到 2000 年左右开始接触 VR。VR 遭遇了严重的崩溃。当时有人在制造头戴式显示器、数据手套和整个虚拟世界。

大多数人说当时的图形太粗糙了。这有一些道理,但是那些还没有尝试过 VR 的人,仍然说“我会等到图形更真实”而没有意识到,正如我们已经讨论和看到的,VR 并不需要出色的图形才能显得“真实”。

我们所谓的 VR 也有一些不同。那时,似乎很久以前,在 PC 上的任何 3D 程序都被宽泛地称为 VR。我甚至在 VRML 语言被发明时(现在是 X3D),预测我们需要把所有这些搞对,否则人们会走到来自世界各地的陌生人面前,在美丽的 3D 环境中(虚拟地)杀死他们。

在某种程度上,我是对的;例如,在魔兽世界中,这正是发生的事情。现代 VR 之所以拥有一切,都是因为电脑游戏。所有那些击杀游戏已经创造了对廉价、高质量视频卡的需求。你可能认为高端视频卡并不便宜,但与最初的 VR 硬件相比,它们非常便宜。

屏幕上的 3D 电脑游戏不是我们今天所说的 VR,这是真的。无论电脑游戏看起来多么出色,都不如基于 HMD 的 VR 体验那样沉浸。

我担心 VR 可能会再次崩溃;然而,我不认为这次会发生。这次的不同之处在于,我们都在口袋里拥有高性能的 VR 设备——我们的手机。现代手机和 Silicon Graphics 的 10 万美元的 Reality Engine 一样强大。我们可以把手机放入一个简单、便宜的 VR 头盔中看 VR。它可能不像带有跟踪控制器的设备那样互动,但它可以工作。蓝牙控制器不会消失,手机也不会消失。

因此,VR 是来留下的,但它将走向何方呢?

不要等待明年的技术

很久以前,当我买我的第一个音响时,我 20 岁,从大学暑假工作中赚了一大笔钱,我真的很想要一个音响。我花了几个星期研究规格,听力测试,比较功能,总的来说,我对选项有点犹豫不决。

我带着一大堆钱到立体声商店,想买点东西。

我去了一家商店,问销售员他有什么立体声系统。

“哦,我有明年的型号,”他说。

什么?我想。是不是真的有什么很棒的东西要出来

“哦,那你在等什么?”我问。“有什么要出来的吗?”

“哦,明年我也会等待明年,”他说。“你看,明年总是明年,所以我从来没有过时的立体声,”他说。他根本就没有买过立体声。

我理解他的观点。你可能会想,等它变得更好

我想说的是,在过去的 30 年里,我听过的音乐比那个销售员听过的要多得多。

你不应该等待购买下一件伟大的 VR 设备,现在就开始。如果有新的东西出来,你会比等待更长时间更享受 VR,并且你会更了解自己喜欢什么。现在没有任何错误的决定。

更好的 HMD

我们今天拥有的 HMD,比如 HTC Vive、Oculus Rift、PS VR,以及像三星 Gear VR、Google Daydream 和 Google Cardboard 这样的基于移动设备的 VR 设备,都相当不错,但它们可以做得更好。

人眼实际上无法与像素显示进行比较,尽管我们确切知道我们看到的细节比任何现有的 HMD 都要多。我们还有更宽的视野。

当前的显示屏大致相似;1,080 x 1,200 像素,视野约为 110 度。这给我们大约每度 10 到 15 像素。人眼在十英寸处可以看到从 500 到约 1,000 像素每英寸;这给我们大约 90 到 177 像素每度。这将意味着一个近 20,000 或 40,000 像素的显示屏。我们谈论的不是百万像素,而是十亿像素。

我们能在 5 年内达到那个水平吗?这是一个非常高的分辨率,但我认为我们的分辨率至少会比现在好 4 到 16 倍。

HMD 可能会更舒适。由于光学问题,我不确定它们会变得更小,尽管在 10 到 15 年内,我预计它们的大小将与隐形眼镜或眼镜相同。这只是一个非常粗略的猜测。

更好和更逼真的图形

显卡的速度和价格都在不断提高。在五年内,我们将需要每一点处理能力来生成与现在相同视觉复杂度的显示屏,不是更好看的,因为像素增加了。换句话说,随着显示屏像素的增加,使得显示更好更宽,驱动这些显示所需的处理能力也会增加,因此视觉保真度的增加并不像你期望的那样多。这个隧道的尽头有光吗?还是隧道尽头有一列朝着另一个方向行驶的火车?有好消息,那就是凹凸渲染。为此,我们需要眼球追踪,我们稍后会讨论。

无论是眼球追踪和凹凸渲染,我们会看到更加逼真的渲染和更加逼真的人物形象。会有显著的改善吗?会看起来真实吗?如果你还记得我们书的开头,图形不需要看起来像现实生活才能显得真实,所以虽然我期待更好的图形,但我不认为 VR 依赖于它们。

更容易的内容创作和更高端的内容

到目前为止,我们只讨论了 VR 中的技术变化。我认为还会发生很多事情,这些事情将对 VR 产生更大的影响。无论图像是如何生成的,声音、外观甚至感觉,我们都需要所谓的“杀手级应用”。现在,大部分赚钱的 VR 都是游戏。一旦更大的游戏开发工作室看到 VR 的未来,他们将开发更多 AAA 游戏(AAA 游戏意味着预算庞大,通常是数十亿或数亿美元,团队庞大,包括艺术家、开发人员、设计师、作者和项目经理)。在这本书出版后不久,将有三款 AAA 游戏推出:《毁灭战士 VR》、《辐射 4 VR》和《上古卷轴 VR》。一旦我们有了数十甚至数百小时的大型游戏,如果它引人入胜(我相信它会非常引人入胜),我们将看到 VR 腾飞。

在接下来的五年里,我认为这肯定会进一步发展。我期待看到大型持久世界,大型多人在线角色扮演游戏(MMORPGS)成为 VR 的主打。你不再是在《魔兽世界》中玩游戏,而是《魔兽世界》中。哇!(双关语)

在内容方面,我们还将看到更多高端的创作软件也在 VR 中。目前,诸如 Max、Maya 和 Blender 等更复杂的创作工具制作了我们在 VR 中看到的最好的模型,但它们本身是传统的应用程序。今天,我们看到一些应用程序,比如Tilt-brush,让我们在 VR 中设计物体。我预计更高端的 CAD 也将拥有普遍的 VR 模式。

HMD 在一段时间后可能会变得炎热、沉重和令人疲劳(主要是由于之前讨论的聚散调节冲突)。我并不认为所有的工作都会在 VR 中完成,但它将在初始阶段以及检查模型时有所帮助。

哪种更快:在屏幕上看两个物体并用鼠标拖动它们,还是直接用手抓住它们并移动?想象一下用魔法泥塑塑造物体,而且还有一个“撤销”命令。你无法在真实的黏土上做到这一点,但在 VR 中你将能够做到。

眼动追踪

我之前提到的眼动追踪是什么?它是 HMD 内部的传感器,用来追踪你的眼球注视的位置。这种扫描有一些优势,主要是社交和渲染方面。

在我看来,社交优势是巨大的。当你看着别人的头像时,这是另一个人在游戏中的代表,他们可能看起来非常僵硬和没有表情。人类从眼睛中感知到大量的情感;通过眼动追踪,你的头像可以展现出一些这些表情。一个简单的测试显示,通过简单地放置卡通眼球——只是一个白色球中的黑点——并让这些眼睛随着用户的注视而移动,可以显著改善。即使是卡通形象,头像看起来更真实。

眼动追踪的渲染改进,如之前简要提到的,是一种称为凹凸渲染的渲染类型。通过眼动追踪,我们可以只显示你正在注视的部分高清细节,而不是填充每个像素都是高细节对象。眼睛的其余部分通常没有那么多细节。我们眼睛中的视杆和锥体在中心密集分布,在眼睛的外缘密度要低得多。凹凸渲染利用这一点。

这真的很有效,可以显著加快图形处理速度;你不需要计算你不显示的部分。这是在 2016 年的年度图形专业兴趣小组SIGGRAPH)会议上首次展示的。

音频改进

大多数人在谈到 VR 音频时,担心头戴设备的音效效果如何,或者当你坐在沙发上而不摘下头戴设备时,是否会屏蔽掉猫尖叫的声音。(希望不会!)然而,VR 音频还有很多其他方面;我们的耳朵可以用两个传感器惊人地确定声音来自何处(通常,需要三个传感器才能检测到任何 3D 声源的距离和方向)。我们的耳朵是如何做到的呢?这是通过一种叫做 HRTF 的东西完成的。目前,我们确实有技术可以实时计算 HRTF。然而,确定要计算哪个HRTF 并不那么容易。这并不奇怪;人们是独一无二的。

每个人的 HRTF 都是不同的;如果我们播放使用我的 HRTF 生成的声音,我闭上眼睛会听到声音好像直接来自设计者打算的位置。如果你用同样的头戴设备听到同样的声音,你可能会觉得它听起来是假的,或者来自不同的方向。这只是我们处理声音的现实。

目前有一些解决方案——要么在专门设计用于计算这一点的房间里测量你的 HRTF,要么可能使用头戴设备中的额外扬声器。时间会告诉我们这些技术中哪一种会是最好的;这仍然是一个开放的领域。

除了用 HRTF 计算来定位(确定)声音,我们需要更多更好的软件方式来定位那个声音。

我认为在五年内,我们用于虚拟世界的物理系统可能能够产生适当的音效,而不仅仅是播放预先录制的声音。例如,如果你撞到了一堵混凝土墙,也许软件可以生成沉闷的撞击声,位于正确的位置并发出正确的声音,考虑到墙有多厚以及其他参数。目前,我们只是在正确的位置播放预先录制的声音(这仍然很惊人,但可以有所改进)。这是我想要看到的东西。

控制 VR

当今的控制器通常由两个手持设备组成,带有多个按钮。大多数都有一种简单的机械方式通过软件来振动。创新的可能性非常巨大。VR 行业已经在几十年里致力于研发复杂的设备,可以实现更好的触觉反馈、更小、更精确的控制器和数据手套。当你可以伸手去抓东西时,为什么还要使用控制器呢?

  • 触觉是涉及触摸的任何互动。这不仅意味着触摸或握住鼠标;它可以是任何感觉到当你移动控制器时的点击或震动(目前的技术水平),但它也可以是一种在你移动时产生阻力的设备。这些已经被演示多年,也有商用现成产品可以做到让你感受到表面。这些将变得更常见,成为更大规模生产的消费级设备。

  • 数据手套是您戴在手上的设备,可以跟踪 VR 中每个手指关节和动作。它们已经问世几十年了。甚至有一些系统(Leap Motion 设备)可以让你用真实的手伸出来,并在虚拟世界中进行互动。我相信这是未来几年潜力巨大的领域。

  • 全身控制器将允许您的整个虚拟身体在 VR 世界中被视觉呈现、准确地跟踪。这是另一个潜力巨大的领域。甚至有一些原型套装可以提供全身触觉。想象一下穿上一套衣服,能够感受和触摸世界,而不仅仅是在图像中挥舞魔杖。

社会和法律问题及解决方案

如果你可以把一个虚拟雕像放在公共空间中,那算不算破坏公物?在 AR 或 VR 中在商业建筑的一侧绘画呢?起初,你可能会认为这是完全可以的;我自己可能会想看到一个科幻模型在公园中央,人们可以与之互动。如果那个模型是一个赞成奴隶制的南方战争英雄呢?如果不满的邻居在虚拟空间中用诽谤性的指控标记你的房子呢?

在社交 VR 中,我们可以以一种在现实生活中不可能的方式骚扰人。如果有人进入您的空间,您可以把他们推开,但在 VR 中,不良行为可能会非常容易。VR 环境将需要考虑这些问题。

如果我们有防止虚拟亵渎的保护措施,我们如何决定什么是对的,什么是错的,尤其是在公共空间?VR 可能类似于现实世界——诽谤就是诽谤,但 VR 也提供了一些独特的可能性。如果你是一个南方绅士,你可能想看到叛军雕像,你可以。你旁边的年轻人可能会看到乔治·华盛顿·卡弗。我们都可以和睦相处;或者我们可以吗?如果你旁边的人想展示化学尾迹沉积呢?

我确实认为持久的虚拟世界将蓬勃发展,并且我们将针对先前的问题提出创新和有趣的解决方案。只要记住,许多不喜欢技术或普遍害怕技术的人并不理解它。如果我们可以创造任何我们想要的世界,我们只需要想要创造有价值的世界。

请务必!

总结

在本章中,我们学习了如何将我们的现实释放到互联网上。具体来说,我们讨论了如何进行版本升级,以及如果需要的话,如何进行rip and replace升级。我们讨论了何时进行升级。我们真的应该更早讨论编码标准,因为现在开始永远不会太早,但首先我们必须掌握一些 React VR 语法,所以在我们将代码发布到世界之前我们先进行了讨论。我们进一步讨论了如何使用 ESLint 和其他代码检查工具来帮助您编写良好的代码。一旦您有了良好的干净代码,我们还讨论了如何将您的开发 React VR 内容打包到网络上,以及如何通过优化和内容交付网络CDN)使其快速。

我们讨论了如何通过物理学进一步使您的网站真实(通过物理学),玩游戏以及如何实现盈利。

您现在已经知道了在网络上使用 React VR 所需学习的一切。我期待着看到您创造出什么!

posted @   绝不原创的飞龙  阅读(53)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示