Skip to content

Latest commit

 

History

History
598 lines (406 loc) · 39.1 KB

File metadata and controls

598 lines (406 loc) · 39.1 KB

十一、在野外散散步

到目前为止,我们已经在前面的章节中构建了一些真实但很小的世界。

然而,我们缺少了一些东西。一开始,我谈到虚拟现实是一种你可以与之互动的东西——一种现实,即使它看起来不是真实的。到目前为止,我们所做的大部分工作都是看东西,但我们不能四处走动。

在本章中,我们将确切地做到这一点。

您将学习以下主题:

  • 使用 NPM 添加组件
  • 凝视按钮
  • 使用凝视按钮触发事件
  • 添加 JavaScript 文件
  • 将 JavaScript 文件转换为动态构建几何体
  • 在我们创造的世界中改变观点
  • 搬家使事情看起来更真实
  • 关于 VR 控制器的更多信息

行驶位置–虚拟现实运动

我小时候经常晕车。虚拟现实也可以做到这一点——前面介绍虚拟现实时讨论了原因,但这是一个非常重要的话题,因此值得重复。

如果你独立于用户的行为(用户代理)而移动一个观点,大脑就会知道它没有移动。然而,大脑也可以通过你的(虚拟现实)眼睛看到世界在移动。大脑依赖于一个非常古老、重要的生存特征,你会认为自己中毒了。

当你中毒时,你的身体很擅长呕吐。用较少的临床术语来说,你会呕吐。你的身体感觉到有东西想杀死你,所以它只是把你胃里的东西作为一种恐慌反应处理掉。

那么,你如何在虚拟现实中移动?如何在不造成人员失事的情况下启用虚拟现实移动?

虚拟现实运动的类型

如果不稍微讨论一下虚拟现实控制器,虚拟现实运动的讨论就不完整。你手上、脚下、支撑着你或让你在里面打滚的东西显然会产生巨大的不同。

我们正在讨论 WebVR,虽然人们很容易进入,但这可能意味着你的用户可能手头没有所有类型的 VR 设备。如果你手头有装备,你可能仍然会发现,对于你的应用来说,简单的运动类型更好,而且编码速度肯定更快。

在讨论设备时,人们讨论的是自由度自由度。这实际上不是严格考虑自由度,而是关于跟踪的内容。

如果您有手持设备,您可能只有3DOF;这意味着电子设备可以跟踪你是否围绕它的中心旋转。一个6DOF控制器就是这样被跟踪的,但它也能检测到它是否在移动,换句话说就是在平移。通常,每个都有 3 度。

6 自由度控制器更逼真;你可以伸手去摸东西。然而,它们需要某种形式的跟踪,就目前的行业状况而言,通常是指外部跟踪装置,如 Vive 灯塔或 Oculus 摄像机。

还有第三种跟踪方式,称为由内而外跟踪,这意味着耳机本身可以看到控制器并找出它们的位置。他们使用摄像机,只是不使用分散在房间里的外部摄像机

很难将运动类型归类为没有控制器的运动类型;它还可以与控制器(远程传送)配合使用

我不会真的包括移动你的头(或鼠标),虽然这是运动;没有这一点,虚拟现实系统就不是真正的虚拟现实(在我的定义中)。然而,也有一些 VR 头戴式耳机不具备这一功能或做得不好。这是高端手机(三星 Gear VR 和谷歌 Daydream)以及 PC 耳机 Vive 和 Rift 的真正突破。

考虑以下类型的 VR 运动:

  • 凝视检测****当你看到某个东西时,它会激活一个效果、眨眼或让你移动 *** 车辆/驾驶舱移动:您的视图显示驾驶舱的墙壁或细节
    • 可以通过凝视检测移动
    • 带控制器(操纵手柄等)
    • 定时/人工(按下按钮或在一段时间后移动播放器)
    • 只有一点点生病的机会* 房间规模
    • 四处走动(上界)
    • 生病的几率很低
    • 需要硬件* 隐形传送或闪烁
    • 通常使用凝视或 3 自由度或 6 自由度控制器
    • 隐形传态也可以通过小步移除运动(矢量化)来完成;这让人感觉你在移动,但不会让你生病* 跑步机
    • 你站在上面移动双脚的设备,它可以检测你的移动方式
    • 有悬挂式滑翔机模拟器和飞行模拟器,在那里你可以躺下或坐下,通过移动身体重量来飞行
    • 所有这些都是大型和昂贵的,通常仅限于虚拟现实商场* 跟踪的 6 自由度控制器运动范例
    • Vive/Rift 通常使用远程传送,6 自由度控制器使其变得简单
    • 使用 6 自由度控制器还有许多其他移动方式;在可以找到一个好的列表 http://bit.ly/VRLoco* 人工移动/轨道
    • 一旦您使用 UI 指示要做什么,VR 系统就会沿着一条路径移动您。
    • 凝视/头部控制转向属于这一类。
    • 很容易让人生病。
    • 这可能会让人恼火——如果你的头转向了,就改变你的移动方式;即使你没有生病,你也会觉得自己被冲昏头脑了。尽管如此,只要仔细实施,它仍然可以工作**

当然,前面的移动方式受硬件数量的限制。另一个制约因素是你希望你的观众有多大。如果你将你的 VR 应用设计成房间比例**(自然走动),那么你就排除了所有手机用户。然而,出于同样的原因,如果你决定使用凝视传送系统,那些拥有房间级虚拟现实的人会因为不能四处走动而感到沮丧。

WebVR 目前更多地针对移动 VR,房间规模是一个巨大的编程挑战。这是可能的,但不是内置的“开箱即用”的反应虚拟现实,并为此 WebVR。从硬件可用性的角度来看:

  • 无设备(谷歌纸板):
    • 自然移动(平移/倾斜)-仅限少量
    • 凝视检测
    • 通过计时器或凝视检测进行人工运动(“轨道”运动,就像你在轨道上一样)
  • 带控制器的 VR 耳机(Gear VR、Daydream 等):
    • 现在我们有了更好的方法,但仍然可以使用前面所有的方法:

      • 自然移动(平移/倾斜)-仅少量
      • 凝视检测
      • 通过计时器或凝视检测进行人工运动(“轨道”运动,就像你在轨道上一样)
    • 驾驶舱运动

    • 通过控制器传送

    • 操纵杆/控制器

  • PC VR–生存/裂谷
    • 现在我们有了更好的方法,但仍然可以使用前面所有的方法:

      • 自然移动(平移/倾斜)-仅限少量
      • 凝视检测
      • 通过计时器或凝视检测进行人工运动(“轨道”运动,就像你在轨道上一样)
      • 驾驶舱运动
      • 通过控制器传送
      • 操纵杆/控制器(在履带式 6 自由度控制器上)
    • 跟踪 6 自由度控制器运动范例

    • 室内步行

  • 高端设备
    • Omni 虚拟跑步机或其他跑步机

避免幽灵效应

我们希望人们能够在没有某种用户代理的情况下四处走动还有另一个原因;没有运动,它就不是虚拟现实。事实上,我们都在四处走动;猫在跟踪时把头侧向一边。如果你感兴趣或好奇,你就歪着头。有了 360 视频,挑战之一就是你只能环顾四周;你不能动。歪着头真的没用。

360 视频所发生的一切,尽可能详细,就是你感觉自己像一个无实体的幽灵。你不能低头看你自己(你可能会看到相机装备),你不能四处走动,你不能伸手去摸东西,你不能移动你的视角。如果你倾斜你的头,或者左右移动,就没有视差效应。

我真的很喜欢 360 视频,但我也觉得它不是真正的虚拟现实,因为最终你会觉得脱离了实体,本质上是一个被束缚的幽灵。当然,视频可能会移动,但你无法改变它的移动方式;你只是来搭便车的。

WebVR 给我留下深刻印象的一件微妙的事情是,如果你倾斜你的头,VR 视图会发生轻微的移动,就好像你将你的头移到一边一样。这是一种微妙的效果;它不是房间大小的虚拟现实,你可以随意走动,但它是一种虚拟现实。你不会觉得自己像个无实体的鬼魂。

允许人们探索他们的环境是很重要的;没有这些,你真的觉得自己像个鬼。在我们的例子中,我们将使用一个心灵传输运动隐喻,让人们探索迷宫。

如果没有与世界互动和四处走动的能力,你会觉得自己像一个虚无的幽灵。虽然我们花了几乎整本书的时间才达到这一点,但与环境和世界交互的能力是虚拟现实中最重要的事情之一

在本章中,您可以使用任何 WebVR 客户端实现这一点。如果我们知道每个人都有一个 HTC Vive 或一个房间大小的 Oculus Rift,我们可以向您展示在迷宫中行走的代码,尽管这会带来一些有趣的 UI 问题-如果有人穿过树篱怎么办?在我们得到完整的从头到脚的触觉套装之前,你可以穿过一堵虚拟墙。有一些方法可以使用用户界面来抵消这一点,比如短暂地将屏幕淡入黑色,然后将用户传送回起点,只允许他们作弊(不好),或者其他有趣的方法。

现在,我们只允许用户移动到迷宫中的下一个单元/开放点,并且只移动到该位置。我们将使用凝视选择,这意味着当你凝视一个 UI 元素时,我们会知道你已经点击了。这将适用于市场上所有的虚拟现实设备,这确实是最好的起点。更复杂的 UI 元素需要检查,以查看用户拥有何种 VR 控制器和跟踪功能,并根据需要启用适当的移动。这超出了本书的范围。

在我们讨论如何在我们的世界中移动之前,我们需要一些有趣的东西来移动。例如,也许我们在森林里散步时发现路被迷宫挡住了,或者是清晨,我们想去一个小湖,看看清晨的雾。

让我们建造那个迷宫吧。

建造迷宫

有几种方法可以建造一个迷宫。最直接的方法是启动我们的 3D 建模软件包(比如说,搅拌器),然后用多边形创建一个迷宫。这将很好,可以非常详细。

不过,这也会很无聊。为什么?我们第一次穿过迷宫将是令人兴奋的,但经过几次尝试,你就会知道通过的方法。当我们构建虚拟现实体验时,您通常希望人们经常访问,并且每次都玩得很开心。

一个模型化的迷宫会很无聊。生命太短暂,不能做无聊的事情。

所以,我们想随机生成一个Maze。这样,你每次都可以改变Maze,让它变得新鲜和不同。这样做的方法是通过随机数来确保Maze不会在我们周围移动,所以我们想要用伪随机数来实现。要开始这样做,我们需要创建一个基本的应用。请转到您的 VR 目录并创建一个名为“WalkInAMaze”的应用:

react-vr init WalkInAMaze

几乎随机–伪随机数生成器

为了有机会重放数值或能够比较人与人之间的分数,我们确实需要一个伪随机数生成器。基本 JavaScriptMath.random()不是伪随机生成器;它每次都会给你一个完全随机的数字。我们需要一个接受种子值的伪随机数生成器。如果给随机数生成器提供相同的种子,它将生成相同的随机数序列。(它们不是完全随机的,但非常接近。)随机数生成器是一个复杂的主题;例如,它们用于密码学,如果您的随机数生成器不是完全随机的,可能有人会破坏您的代码。

我们并不担心这一点,我们只是想要重复性。虽然这方面的用户界面可能有点超出了本书的范围,但以点击刷新不会产生完全不同的Maze的方式创建Maze确实是一件好事,可以避免用户的挫折感。这也将允许两个用户比较分数;我们可以为Maze保留一个板号,并显示这个。这可能超出了本书的范围;然而,在开发过程中,有一个可预测的Maze将非常有帮助。如果不是因为这个,你可能会在你的世界里迷失方向。(好吧,也许不是,但它使测试更容易。)

包括来自其他项目的库代码

到目前为止,我已经向您展示了如何在 React VR(或 React)中创建组件。有趣的是,JavaScript 与include之间存在一个历史问题。使用 C++、java 或 C 语言,可以在另一个文件中引用 T1 个文件,或者在项目中引用文件。这样做之后,其他文件中的所有内容,例如函数、类和全局属性(变量),都可以从发出 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的代码;任何人都可以在软件包中对其进行建模,但虚拟现实世界应该是活生生的。拥有可以动态构建任意大小(一定程度上)的代码将允许重复播放您的世界。

有很多 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

这有点难读,但是你可以计算正方形和xs。别担心,它看起来会更漂亮。现在我们已经有了Maze的 HTML 版本,我们将开始构建树篱

这是一段比我预期的稍大的代码,因此我将它分成几部分,并将Maze对象加载到 GitHub 上,而不是将整个代码粘贴到这里,因为它很长。您可以在以下位置找到源的链接:http://bit.ly/VR_Chap11

添加楼层和类型检查

正如我们之前所说的,360 全景背景看起来很奇怪的一点是,你似乎可以“漂浮”在地面上。除了修复原始图像之外,还有一种修复方法,就是简单地添加一个地板。这就是我们用太空画廊所做的,它看起来相当不错,因为我们假设我们在太空中漂浮。

对于这个版本,让我们import一个地面广场。我们可以使用一个大正方形,将整个Maze包围起来;如果Maze的大小改变,我们就必须调整它的大小。我决定使用一个更小的立方体,并对其进行修改,使其位于Maze的每个单元的“下方”。这将使我们在未来有一定的回旋余地来旋转广场,寻找磨损的道路、积水或其他任何东西。

为了制作地板,我们将使用一个简单的立方体对象,我稍微修改了一下,并对其进行了 UV 贴图。我用搅拌机做这个。我们还有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 中包含迷宫,我们确实需要:

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对象时重命名它们

我遇到的第二个问题更像是一个简单的傻事,如果你不是真的在反应中思考,这很容易发生。你可能会碰到这个。JavaScript 是一种可爱的语言,但有时我会错过强类型语言。以下是我所做的:

<Maze SizeX='4' SizeZ='4' CellSpacing='2.1' Seed='7' />

maze.js文件中,我有如下代码:

for (var j = 0; j < this.props.SizeX + 2; j++) {

经过一些调试,我发现j的值是从042。为什么它得到的是42而不是6?原因很简单。我们需要完全理解 JavaScript 来编写复杂的应用。错误在于将 SizeX 初始化为'4';这使它成为一个字符串变量。当从0(整数)计算j时,React/JavaScript 取2,将其添加到'4'字符串中,得到42字符串,然后将其转换为整数,并将其分配给j

当这一切完成后,非常奇怪的事情发生了。

当我们建造太空馆时,我们可以很容易地使用'5.1'值作为输入框:

<Pedestal MyX='0.0' MyZ='-5.1'/>

然后,稍后在类内使用下面的 transform 语句:

 transform: [ { translate: [ this.props.MyX, -1.7, this.props.MyZ] } ]

React/JavaScript 将字符串值放入This.Props.MyX,然后意识到它需要一个整数,然后悄悄地进行转换。然而,当你得到更复杂的对象时,比如我们的Maze一代,你将无法逃脱。

请记住,您的代码不是“真正的”JavaScript。已经处理过了。从本质上讲,这一过程相当简单,但其影响可能是致命的。

Pay attention to what you code. With a loosely typed language such as JavaScript, with React on top, any mistakes you make will be quietly converted to something you didn't intend.

You are the programmer. Program correctly. 

那么,回到MazeHedgeFloor是初始Gem代码的直接副本。让我们看一下我们的起始项 T4,虽然注意到它后来变得更复杂了(并且在源文件中):

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本质上是一回事。(我们本可以在加载文件时使用道具,但我们希望Gem具有不同的行为,因此我们将广泛编辑此文件。)

要运行此示例,首先,我们应该像您之前一样创建一个名为WalkInAMaze的目录。完成此操作后,从 Git 源代码下载本章这一部分的文件(http://bit.ly/VR_Chap11 。一旦你创建了应用,复制了文件,并启动了它,(转到WalkInAMaze目录并键入npm start,一旦你环顾四周,你就会看到类似的东西——除了有一个 bug。这就是迷宫的外观(如果您在Hedge.js中的<Model>语句中使用文件“MazeHedges2DoubleSided.gltf”):

现在,我们是如何在游戏中获得那些看起来整洁的树篱的?(好的,它们的价格很低,但它仍在推动。)网络标准改进速度的一个好处是它们的新功能。React VR 现在可以加载 glTF 文件,而不仅仅是.obj 文件格式

对模型使用 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,轻松计算和导入。由此产生的数字看起来金属的地方,他们应该是金属和沉闷的油漆仍然举行。

我没有在这里使用环境光遮挡,因为这是一个非常凸的模型;使用环境光遮挡时,具有更多表面凹陷的物体看起来会非常棒。与建筑模型(例如家具)搭配也会很好看

要转换您的模型,请在中提供用户文档 http://bit.ly/glTFExporting 。您需要下载并安装 Blender glTF exporter。或者,你可以下载我已经转换的文件。简而言之,如果执行导出,请执行以下步骤:

  1. 下载文件 http://bit.ly/gLTFFiles 。假设您使用的是较新版本的 Blender,您将需要gltf2_Principled.blend文件。

  2. 在 Blender 中,打开文件,然后链接到新材质。进入文件->链接,然后选择gltf2_Principled.blend文件。完成此操作后,钻入“NodeTree”并选择 glTF 金属粗糙度(对于金属)或 glTF 镜面光泽度(对于其他材质)

  3. 选择要导出的对象;确保选择“循环”渲染器。

  1. 在窗口中打开节点编辑器(就像前面章节中对图像所做的那样)。向下滚动到节点编辑器窗口的底部,并确保选中“使用节点”框。

  1. 通过节点菜单添加节点,添加->组->glTF 镜面光泽度或金属粗糙度。

  2. 添加节点后,转到添加->纹理->图像纹理。添加尽可能多的图像纹理,然后将它们连接起来。您应该得到类似于此图的结果。

  1. 若要导出模型,我建议禁用摄影机导出并合并缓冲区,除非您认为将导出多个共享几何体或材质的模型。我使用的导出选项如下:

现在,要包含导出的 glTF 对象,请像处理 OBJ 文件一样使用<Model>组件,除非没有 MTL 文件。这些材料都在.glTF 文件中描述。要包含导出的 glTF 对象,只需将文件名作为 gltf2 道具放入<Model

 <Model
 source={{ gltf2: asset('TeleportGem2.gltf'),}}
...

要了解有关这些选项和流程的更多信息,您可以访问 glTF 导出网站:http://bit.ly/WebGLTF 。该网站还包括主要 CAD 软件包和所有重要 glTF 着色器的教程(例如,我前面展示的 Blender 模型)。

我在加载了几个.OBJ 文件和.glTF 文件 http://bit.ly/VR_Chap11 所以你可以尝试不同的低多边形和透明度组合。在 React VR 2.0.0 版中添加 glTF 支持时,我非常激动,因为透明度贴图对于许多 VR 模型非常重要,尤其是植被;就像我们的树篱。然而,事实证明 WebGL 或 three.js 中存在一个错误,无法正确渲染透明度。因此,我在 GitHub 站点上的文件中使用了低多边形版本;上面的图片与Hedges.js文件中的MazeHedges2DoubleSided.gltf文件(在 vr/components 中)一起

If you get 404 errors, check the paths in the glTF file. It depends on which exporter you use—if you are working with Blender, the gltf2 exporter from the Khronos group calculates the path correctly, but the one from Kupoman has options, and you could export the wrong paths.

动画-虚拟现实按钮

好啊我们想给一些东西做动画。为此,我们将使用 VRB 按钮。当下列情况之一发生时,它将激活:

  • XBox 游戏板上的按钮 A
  • 键盘上的空格键
  • 用鼠标左键单击
  • 触摸屏幕

不幸的是,我们的“最低公分母”是一个谷歌硬纸板,它可能有,也可能没有按钮。你不想把手指伸进里面去触摸屏幕。(话说回来,新的 VR 头戴式耳机有一个小杠杆臂,即使在实际的硬纸板版本中也能戳破屏幕)。我们将使用一个凝视按钮。当鼠标指针或屏幕中心(用小点标记)掠过对象时,将调用事件,我们的代码将处理此问题。

凝视按钮也被打包到npm生态系统中一个漂亮的<GazeButton>对象中。请参考以下网页:http://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 进行了尝试),没有光标,也没有任何东西可以点击。我们需要实现一个光线投射器(即使没有控制)

There are so many different kinds of VR control systems, as we discussed briefly at the start of the chapter, where the default is "no" VR input device, including a center of the screen cursor. 

The implementation of a proper control system is in our hands.

When you are using a desktop browser to do your initial development, you get a mouse cursor (including a hand cursor when over a tracked component), which can imply a gaze cursor is built in; they aren't. Just be aware there is a valid reason for this.

雷卡斯特

一个光线投射器将一条光线射向世界,并计算它所接触到的东西。您通常将这些视为来自 VR 控制器的发光线。如果没有控制器,光线投射器将从屏幕中心投射光线;这正是我们实现凝视按钮所需要的。

在本例中,正如我们使用按钮所做的那样,已经有了一个simple-raycaster。如果尚未安装,则需要通过以下命令从npm安装:

npm install --save simple-raycaster

You might want to skip the --save while experimenting with packages; if you do, remember to update your package.json file manually or via the appropriate tools.

simple-raycaster的实现非常简单。在client.js中,在现有import行(VRInstance)下方添加以下import语句:

import * as SimpleRaycaster from "simple-raycaster";

在标有// Add custom options here的地方,插入以下行:

    raycasters: [
         SimpleRaycaster // Add SimpleRaycaster to the options
    ],
    cursorVisibility: "auto", // Add cursorVisibility

在你的电脑上,事情变得有点奇怪,此时屏幕的中心将激活(并删除)宝石,即使你不点击。这就是重点。

如果我们有更多的页面,当你的目光进入时,我们会让宝石旋转。现在,我们将把这个练习交给读者。

You'll want to start an animation during the onClick handler.

到目前为止,我们已经展示了如何在凝视宝石时获取事件。这很好,我们可以使用事件来触发移动,但是我们如何移动呢

有一点奇怪的是,React VR 无法像许多图形系统那样移动相机。要移动当前视点,请将index.vr.js开头的<View>平移到相反方向;这将以另一种方式移动世界上的一切,这使它看起来像是在向前移动。要移动一个视点,我们需要将点击事件从Gem传递到其父级的父级(顶级视图)

道具、状态和事件

React,因此 React VR 的核心是以可预测、确定的方式处理道具、事件和状态,这就是 React 应用一致、干净、易于维护的原因

道具是在声明对象时创建的,在对象的生命周期内不应更改。如果一个对象需要更改,例如,我们的传送宝石,那么您应该将这些值分配给state。这将强制执行自上而下的单向数据流。如果一个组件在不同的区域需要相同的状态,那么state应该提升到最高的父级

如果希望子组件让父组件知道某个事件或基于较低级别的事件更改其状态,则这会引起一些有趣的问题。

有几种处理方法;这在世界上可能是一个复杂的课题。React VR 与 React Native 或 React 在处理状态、道具和事件方面没有区别。一个很好的起点是关于状态和生命周期的 React 文档

本质上,在 React 应用中,对于发生变化的事情,应该有一个单一的真相来源。如果父母不在乎,例如,如果Gem较高或较低(踩着或不踩着),那么你不需要让父母记录孩子的身高。将state保持在尽可能低的水平是正确的决定。高度可以从“我们踩到了Gem了吗?”计算出来,因此不应该是向下传递的道具。(也许你会考虑一个 To.T3 的起始高度,但是,作为一个道具;好的编程不需要硬编码的值,即使在很多的书中,我们还是要简洁)。

在我们迷宫般的世界里,我们陷入了困境。我们通过更改世界树顶部的<View>节点来移动视点。然而,当我们点击每个<Gem>时,我们希望视图发生变化。

我们可以根据上下文来处理这个问题;许多库(如 Redux 或 MobX)在封面下使用上下文。有使用上下文和其他功能的事件库。然而,上下文是一个先进的概念,对于我们正在做的事情来说有点过火。

在这种特殊情况下,我们只需将一个callback函数传递给子树。我们这样做的原因如下:

  • 从层次结构的角度来看,我们的应用相当小,只有三个级别。
  • Maze本身可能需要知道用户何时结束(例如,燃放烟花或更新高分)。Gem不知道这一点。如果Gem直接向视图发送通知,Maze永远不会知道。
  • 我们可以引入额外的库,但这是一个简单的项目,在开源世界中太多的外部依赖可能会破坏一切。这通常不是什么大问题,不过如果出现问题,这是开源的;去修理它。

If, while looking for external packages, you do break things, you need to uninstall the offending package, then restart your Node.js server by running the following command:

npm start -- --reset-cache

npm cache clean --force does not do this cache reset. The error message you get should point this out if you forget.

使更新向上游流动

虽然更新会减少,但我们需要向上传递信息。我们怎么做?简单,功能性callback

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,它是一个函数。在我们创建Gem的地方,我们只需插入以下道具(插入的行以粗体显示,而不是源代码可以粗体显示):

... 
mazeHedges.push(<Gem {...cellLoc}
 onClickGem={this.handleClickGem}
  />);

好的,我们从哪里得到this.handleClickGem?在这个(简单的)例子中,Maze不会对事件做任何事情,只是传递它。在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);
}

现在,我们注意到另一个道具。当然,这是迷宫的父母传给我们的;因此,在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 });
    };

就这些。请注意,您不仅仅将state设置为this.startX = -X,还需要调用this.setState(),如前面的代码所示。然后,此例程将处理render()更新的波动。

这些文件很大,我们做了很多修改。我在上面提到了重要的几行,但我强烈建议您从下载源文件 http://bit.ly/VR_Chap11 看看我们做了什么。在其中,我构建了一个 4x4 迷宫,在大多数 PC 和移动设备上应该具有合理的帧速率。您可以尝试各种对象的其他版本(看起来像模糊限制语或低多边形模糊限制语的模糊限制语)

从这里到哪里去?

这是一个非常基本的游戏,但是你可以用它做很多事情。我们之前讨论过的一些内容很容易包括以下内容:

  • 我们的传送有点突然。我们应该有一个声音,甚至做两次更新(通过改变HandleClickGem()例程)来添加一个简短的动画或两步传送。请注意,平滑地设置视图本身的动画通常不是一个好主意;这让人感到恶心,因为他们的眼睛说他们在动,但他们的身体说不动。
  • 点击的宝石数量可能成为一个分数。这给了我们一个优势,让我们放慢脚步,一步一步地点击所有的宝石。
  • 你可以计算到达出口所需的时间,数字越小你的分数越高。这给了你更快的速度和跳过传送宝石的优势。这里的两个目标是排他性的,通过平衡,可以让它变得有趣。
  • 您可以在迷宫的前面包括按钮,以增加/减少大小或生成不同的随机数(并显示它们)
  • 分数和随机数可以加载到高分 API 中。
  • 事件传递库,例如eventing-bus,使props的传递变得更加容易。我的目标是向你展示做这件事的方法。

总结

在本章中,我们学习了在网络上构建完整应用和游戏的最后部分;再加上我们之前学到的,我们的旅程几乎完成了;这实际上只是实现网络的第一步。我们讨论的主题包括如何在虚拟现实世界中移动,包括基本的远程传送机制。我们讨论了凝视按钮和使用光线投射来实现它们。我们讨论了propsstate和事件的重要机制。为了实现这些流程,我们回顾了重要的 React 原理,即向上推state并向下处理事件。我们还讨论了使用伪随机数生成器来确保我们的propsstate不会无序变化。总之,我们现在知道如何创造,如何在其中移动,如何让世界对我们作出反应。

在下一章中,我们将讨论从何处开始,如何升级 React VR,以及如何在 internet 上发布您的虚拟世界。**