Skip to content

Latest commit

 

History

History
447 lines (326 loc) · 26.6 KB

File metadata and controls

447 lines (326 loc) · 26.6 KB

十、带来真实的生活世界

正如您在上一篇第 9 章中了解到的,自己动手——本机模块和 Three.js,我们可以将本机代码和 JavaScript 代码包含到我们的世界中。除了通过使我们的世界在视觉上变得更加有趣来为我们的世界注入生命之外,我们还可以将外部世界带入我们的世界。

在本章中,您将学习如何使用 React 和 JavaScript 将 web 带入虚拟现实世界。您将学习如何在 VR 中使用现有的高性能代码。

首先,我们需要一个虚拟现实世界。这次,我们要去火星!

在本章中,您将学习以下主题:

  • 执行 JSON/WebAPI 调用
  • Fetch声明
  • 跨源资源共享CORS
  • 诊断的“网络”选项卡
  • Cylindrical Pano声明
  • 对齐文本,类似于 flexbox(React Native 的一部分)
  • 条件渲染
  • 样式表

去火星(最初的世界创造)

你可能会认为太空中没有天气,但事实上有,我们在那里有气象站。我们将去火星了解天气。这将是一个实时程序,将从火星科学实验室或其名为好奇号的探测车获取天气数据。

好奇号是一辆 SUV 大小的机器人漫游车,于 2011 年 11 月 26 日被送往火星,并于 2012 年 8 月 6 日着陆。如果你开着你的 SUV 去那里,即使你可以买汽油,你也需要 670 年才能到达那里。火星探测车的设计任务为两年,但它的任务延长了,这对我们来说是幸运的。

驾驶那辆 SUV 去火星获取天气报告会很麻烦。我甚至不知道加油站在哪里。

创造最初的世界

首先,正如我们之前所做的,转到您存储世界的目录并创建一个,如下所示:

react-vr init MarsInfo

然后,从下载资产 https://github.com/jgwinner/ReactVRBook/tree/master/Chapter10/MarsInfo

Although I uploaded all of the files to make this work, not just the static assets, you really should try to code this yourself. You don't really learn anything from downloading files and running them.

Making mistakes is what builds character. I uploaded the files and will maintain them just in case there is too much character.

现在我们有了一个初始世界,我们将开始设置 web 服务以获取数据。

Jason 和 JSON

当你听到人们谈论 JSON 时,希望你没有想到这个家伙:

我在网上找到了这张图片,上面标着 CreativeCommons;这是蒙特利尔漫画展的杰森·沃希斯(cosplay)的服装。这张照片来自加拿大拉瓦尔的皮卡维尔。

严肃地说,JSON 是通过 web 服务引入外部世界的最常见方式;然而,正如我们已经看到的包含本机代码和 JavaScript 的方法一样,您可以通过多种方式集成您的系统。

React VR 的另一个巨大优势是它基于 React,所以通常可以使用 React 做的事情,可以在 React VR 中做,但有一些重要的区别

为什么 JSON 与 React 无关

首先,您可能会想,“如何在 React VR 中处理 AJAX 请求?”

你不知道,不是真的。React VR 和 React Native 不支持任何特定的数据获取方式。事实上,就 React 而言,它甚至不知道图片中有一个服务器

React 仅使用来自两个位置的数据渲染组件:道具和状态。

这是学术上的答案。真正的答案要宽泛一点。你可以用任何你喜欢的方式获取数据。说到这里,大多数 React 程序员通常会使用以下 API 和/或框架之一:

  • :接近标准,内置反应功能,通常已经包含;参见http://bit.ly/FetchAPI 查看使用说明和示例
  • Axios:Axios 围绕承诺(异步完成 API)展开,尽管它也可以以更简单的方式用于单线程应用;参见http://bit.ly/AxiosReadme 了解更多详情
  • 超级代理:如果你不喜欢承诺,但喜欢回扣;参见http://bit.ly/SuperagentAPI 了解更多信息

在这些示例中,我们将展示 fetch,因为不需要安装不同的模块和设置回调。说到这里,您可能希望构建一个响应性稍高的应用,它使用某种类型的异步完成回调,以便您的应用可以在外部等待数据时执行某些操作。Fetch 确实通过承诺实现异步完成,因此我们将进行条件渲染,以利用这一点并维护响应迅速的 VR 应用。

您可能已经编写了很多这样的代码。如前所述,React VR 是用于 VR 对象的渲染系统,因此可以使用各种外部 JavaScript 系统

寻找 API——从火星远道而来

现在,我们将从火星获取所有的天气数据。不,我不是开玩笑。参见http://bit.ly/MarsWeatherAPI ,如果您感兴趣,它描述了 API 并提供了一些科学背景。此 API 设置为使用 JSON 或 JSONP 格式的 XML 数据,并以 JSON 或 JSONP 格式返回。以下是结果数据,您也可以参考:http://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中找到 MarsInfoComponent {声明,添加以下内容:
export default class MarsInfo extends Component {
    componentDidMount() {
        fetch(`http://marsweather.ingenology.com/v1/latest/`,
            {
                method: 'GET'
            })
            .then(console.log(result))
    }

    render() {
  1. 粘贴并运行它。
  2. 在浏览器中打开控制台(Ctrl+Shift+K在 Firefox 中每晚)。虽然我们刚才展示的代码非常合理,并且在浏览器中运行良好,但当我们运行此代码时,我们将得到一个错误:

有什么问题?是科尔斯。这是一种使来自不同服务器的跨源或 web 内容安全的机制。本质上,这是一种 web 服务器说“我可以嵌入另一个网页”的方式。例如,您的银行不希望将您的银行详细信息嵌入其他网站的网页中;你的支票账户很容易被泄露,你会认为你是在登录你真正的银行,你会被泄露。

Note that I could have used an API that did not give these errors, but you may well run into the same problem with your own content, so we will discuss how to spot a CORS problem and how to fix it.

  1. 为了找出为什么会出现这个错误,我们需要查看协议头;单击工具->Web 开发人员->网络打开网络选项卡:

此窗口对于了解本机 JSON 请求问题和网站集成非常有用。

  1. 打开控制台后,您将看到不同的 HTTP 操作;单击未完成的项目:

然后我们将查看返回的数据。

  1. 看下面屏幕截图的右侧;在这里,您可以单击响应和标题来检查数据。我们可以看到网站确实返回了数据;但是,我们的浏览器(Firefox)通过生成 CORS 错误阻止了它的显示:

代码是正确的,但网站不包含重要的 CORS 标题,因此根据 CORS 安全规则,网站会阻止它。有关 CORS 的更多信息,请访问:http://bit.ly/HTTPCORS

如果出现此错误,可能有一种解决方案,可以向请求中添加头。要添加头,需要修改fetch请求;fetch请求还允许'cors'模式。然而,无论出于什么原因,对于这个特定的网站,'cors'选项似乎对我不起作用;对于其他网站,它可能会更好。其语法如下所示:

fetch(`http://marsweather.ingenology.com/v1/latest/`,
    {
        method: 'GET',
        mode: 'cors',
    })

为了更好地控制我们所请求的内容,创建一个 header 对象并将其传递到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.ingnology.com也无法使用,因此,很可能他们的服务器尚未升级到现代网络安全标准。

这是可能发生的!在我们的例子中,确实没有通用的解决方案。我确实找到了一个 Firefox 插件,它可以让你绕过 CORS 限制(记住,问题不在于服务器,而是浏览器在看到服务器已经发送的有效负载时关闭了你的代码),但这需要人们下载插件并摆弄它们。

我们需要找到一个更好的 API。美国宇航局有一个很棒的网络 API 目录,我们将使用他们的火星探测车摄像头 API。你可以免费获取数十万张照片中的任何一张。一旦我们使用了一个不同的 web API,我们就会得到我们一直在寻找的正确的 CORS 头,而且一切都很好。一旦我们询问具有现代安全标准的服务器,我们会注意到它自动包含 Firefox 需要的访问控制允许源(此处为通配符),如下图所示,取自网络选项卡:

所以,我们不看火星上的天气,而是看真实的照片。

来自美国宇航局的更好的 API

要查看一些优秀的 web API,您可以访问:http://bit.ly/NasaWebAPI并查看您可以使用的 API 列表,或者更好的是,使用您已经编写的一些 web API。React VR 通过 React 和 React Native 的强大功能使这些功能的集成变得非常容易。我们将使用火星照片 API。要启用它,您可能需要一个开发人员密钥。当您发出请求时,您可以将 API 密钥添加到 URL 或使用DEMO_KEY。这将成为 API 调用的一部分,例如,https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos?sol=1000 &api_ 键=演示键。请注意,URL 末尾没有句点。

如果您在开发代码时开始出现错误,那么您可能使用了太多的DEMO_KEY;获得自己的开发者 API 非常快速和容易;相关说明可在我提到的网站上找到:http://bit.ly/NasaWebAPI

为了从美国宇航局获取数据,我们所要做的就是稍微更改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对象初始化为空数组(集合)。这样我们就可以在获取数据之前和之后使用类似的代码。
  • 但是,您仍然应该检查故障。
  • 我们初始化为2currentPhoto值是一种“欺骗”。这是欺骗的原因是,当我写这本书时,如果你让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()更新和更改的数据:

在这个特殊的案例中(第 11197 天),有大量的图像。JSON 很好地处理了这一点,在我们环顾虚拟现实世界的同时,对数据进行切片(哎哟,糟糕的双关语)和切分。

另一件需要注意的事情是,render()循环只调用了两次。如果你习惯了游戏开发模式,这可能看起来很奇怪,因为正如我们所讨论的,要建立一种沉浸感,我们需要每秒超过 60 帧。如果只渲染两次,我们怎么能做到这一点

React VR 不会生成实际的图像,three.js 会。当 React-VR“渲染”时,它只需要使用 React-VR 语法,应用任何道具或状态更改,并调用render()来处理那些曾经更改过的对象

为了显示检索到的数据,我们将构建一个新对象。

  1. 创建一个名为CameraData的新文件,并将其作为一个单独的组件。我们还将更改index.vr.js中的render()方法

每个人都需要一个样式表

发型不仅仅适合你的头发;在这种情况下,使用样式表将有助于使代码更简单、更清晰、更易于维护。样式重用非常容易。风格不是一种独立的语言;它们是 JavaScript,与 React 中的其他内容一样。React VR 中的所有核心对象都接受一个名为styles的道具。我们将在文件中定义此样式并重用它。

创建以下样式定义,以便我们可以将其用于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],
    },
});

If you omit the width style, objects will transform and move completely differently. I'm not sure yet if this is a bug or is a different type of layout style, but note that if your transform statements are not moving a text or view object, it might be because you don't have a width: prop for your text style.

构建图像和状态用户界面

接下来,我们需要以两种不同的方式渲染摄影机数据。第一,当我们没有CameraData时,换句话说,当应用启动时,或者如果我们没有互联网连接;第二,当我们获取数据并需要显示它时。我们还希望保持这些例程相当模块化,以便在启动状态更改时,我们可以轻松地重新绘制需要它的对象。

请注意,很多操作都是由 React VR 自动完成的。如果对象的道具或状态不变,则不会要求其渲染自身。在本例中,我们的主线程已经具有修改更改的 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()。我们可以假设,如果没有照片,我们正在加载它。前面代码的好处是,我们在使用道具之前检查道具并确保它们有效。

Many computer courses and self-help books strip out error handling to "concentrate on the important things".  Error handling is the most important thing in your app.In this case, it's particularly important as while we are retrieving data, we don't have the photographs loaded yet, so we have nothing to display. We'll get an error if we don't handle this. What I have stripped out are console.log statements; if you download the source code for the book, you will find more extensive comments and trace statements.

现在,让我们转到实际渲染。这看起来似乎很简单,主要是因为序列化、获取和选择性渲染的所有工作已经完成。这就是编程应该力求干净、健壮、易于理解和维护的地方。

Some of these code samples are getting long, so I'm putting closing braces and tags at the end of the object they are closing. I would recommend that you buy a large desktop screen and code in a more expansive way; when you spend an hour tracking down a missing or out of place />, you will appreciate a large format display device. It just helps productivity.

  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>
);
}
}

如果您已经输入了到目前为止的所有代码,当 world 加载时,您将看到一个绿色对话框,通知您它正在接收数据。几秒钟后,这将被照片 2 和来自火星的数据的详细元信息所取代,如下所示:

If you want to open two virtual words at the same time, for example, to check on some imports without incurring the round trip web hits that we're in the middle of programming, you can do it by going to your second world that's set up, instead of npm start, using the react-native start --port 9091 command.

我在前面简要地提到了这一点,但重要的是要注意多线程 React 是如何实现的;当元素的道具或状态发生变化时,元素的渲染也会发生变化,而不会被告知。它是多线程的,不需要更改代码。这允许您在世界填充其数据时移动相机并查看。

这使得虚拟世界看起来更“真实”;它对输入的响应就像它是现实一样。是我们创造了一个虚拟现实。

如何(不)让人生病

您可能已经注意到,我们将此应用的用户界面放在图标和屏幕上—有点远;到目前为止,我们已经把所有东西都放在了至少五米外。为什么呢?

这是因为调和冲突。

当你的眼睛“指向”某物时,就像我们在第一章中讨论的,什么是虚拟现实,真的?,如果那个东西真的离你的脸很近,你的眼睛会试图聚焦它。然而,您的 HMD 是一个固定焦距的设备,无论距离有多远,它总是以焦距显示对象。在现实世界中,比如说,距离 3 到 4 英尺远的物体需要你的眼睛聚焦,而不是距离 10 英尺远的物体

因此,你的眼睛会集中在一个你应该更加关注的图像上,但你所看到的已经是焦点(一切都是这样),所以在现实世界中你所期望看到的和在 HMD 中看到的是不同的。

这不会导致任何实际的视觉问题,一切都是清晰的和集中的。

你会得到的是眼睛疲劳和模糊的不适感,使用 HMD 的时间越长,这种不适感越严重。

避免这种情况的方法是尝试始终将 UI 元素放置在至少与本例中相同的位置。例如,不要把浮动屏幕放在一副眼镜的地方。如果你这样做了,人们会看着他们,他们的眼睛会期望聚焦在六英寸以外的东西上,但是从聚焦的角度来看,这个东西比手臂长得多。这将使您的用户感到紧张。

这就是为什么大多数虚拟现实让你看着远处的大屏幕进行选择。您想要创建的 UI 元素可能最接近手腕,即使这样也有点风险。

我确实觉得使用虚拟现实的人越多,他们的眼睛和注意力就会受到越多的训练,然而,我不知道有任何医学研究显示这种效果。我提到它只是因为我有一只近视眼和一只远视眼;当我戴上眼镜时,我的焦距改变了。有趣的是,如果我戴上眼镜而不戴任何眼镜,我的聚焦仍然会改变。我觉得人类的大脑具有无限的适应性,我们可以克服这种适应性冲突。

但是,用户的里程数可能会有所不同,因此不要将物品放在离他们脸不到一米的地方,让他们感到疲劳。

总结

在这一章中,你学到了很多东西。通过构建使用 JSON API 的 web 服务调用,我们实现了我们的世界,并使它们真正具有交互性。我们已经看到了一些获取数据的方法,并或多或少地使用了内置的fetch语句。这些 API 调用现在是异步的,所以我们可以环顾世界,欣赏火星,而我们所要求的相机数据正在加载。

我们已经了解了如何通过处理跨站点脚本问题来构建安全世界。我们已经创建了对齐文本并构建了条件渲染。我们还讨论了错误处理。

完成所有这些都需要一些时间,我们曾经有过几次在开发过程中花费数小时尝试排列对象。我被关闭了几次,因为我在一个小时内超过了DEMO_KEY检索次数。这就是为什么我建议您获得自己的 API 密钥,然后您可以请求更多的图像

这一章已经相当长了,世界在检索真实世界的数据时,还不是完全互动的。在下一章中,您将学习如何使您的世界与我们的输入交互。这就是我在前面的视图中构建+和-箭头的原因。查看下一章,了解如何通过我们的火星数据将它们连接到一个页面。我将展示一个不同的世界,但展示如何使按钮互动。这将是你的练习,提出并进行简单的道具更改,使加号和减号按钮成为现实。