Tutorial AR

原文:Tutorial AR

在原文基础上有部分修改,如:

  • class component 用 function component 重写
  • assets 文件失效链接更新

前置任务

需要完成 快速入门 章节

纪念品

这份教程将一步一步地指导你开发出一个简单的 AR APP。

你接下来将会:

  • 了解 HelloWorldSceneAR.js 这份代码
  • 将一个带纹理的 box 放入周围世界场景
  • 在场景中添加笑脸 emoji
  • 选择一个 AR 放置平面
  • emoji 添加到平面上
  • emoji 添加阴影效果
  • 使 emoji 可拖动
  • box 增加动画

了解 HelloWorldSceneAR.js

打开你的测试 app,你应该可以在相机视图中看到以下白色的 “Hello World” 字样。

你见到的场景正是 HelloWorldSceneARApp.js 是整个 AR APP 的入口,默认导出一个返回 ViroARSceneNavigator 组件的箭头函数。我们见到的 HelloWorldSceneAR,则作为 ViroARSceneNavigator 组件的 initialScene 参数值传入,如以下代码所示:

import React from 'react';
import { StyleSheet } from 'react-native';
import { ViroARSceneNavigator } from '@viro-community/react-viro';
import { HelloWorldSceneAR } from './js/HelloWorldSceneAR';

let styles = StyleSheet.create({
  f1: { flex: 1 },
});

export default () => {
  return (
    <ViroARSceneNavigator
      autofocus={true}
      initialScene={{
        scene: HelloWorldSceneAR,
      }}
      style={styles.f1}
    />
  );
};

ViroReact 构建在 React Native 之上,并使用 RN 来轻松创建原生 AR APP。除了解 Javascript 外,你还需要了解一些基本的 React 概念,例如 JSXcomponentsstateprops

以下是 HelloWorldSceneAR 的代码

import React, { useState } from 'react';
import { StyleSheet } from 'react-native';
import {
  ViroARScene,
  ViroText,
  ViroConstants,
} from '@viro-community/react-viro';

const HelloWorldSceneAR = () => {
  const [text, setText] = useState('Initializing AR...');

  function onInitialized(state, reason) {
    console.log('guncelleme', state, reason);
    if (state === ViroConstants.TRACKING_NORMAL) {
      setText('Hello World!');
    } else if (state === ViroConstants.TRACKING_NONE) {
      // Handle loss of tracking
    }
  }

  return (
    <ViroARScene onTrackingUpdated={onInitialized}>
      <ViroText
        text={text}
        scale={[0.5, 0.5, 0.5]}
        position={[0, 0, -1]}
        style={styles.helloWorldTextStyle}
      />
    </ViroARScene>
  );
};

export default HelloWorldSceneAR;

接下来介绍下这段代码都做了些什么

导入工具 components

首先导入需用到的 components,分别是:

  • 从 react 导入 React 和 一个 react hooks useState
  • 从 react native 导入样式工具 StyleSheet
  • 还需导入这个 app 用到的, react-viro 提供的组件: ViroARSceneViroTextViroConstants
import React, { useState } from 'react';
import { StyleSheet } from 'react-native';
import {
  ViroARScene,
  ViroText,
  ViroConstants,
} from '@viro-community/react-viro';

HelloWorldSceneAR 组件

import 语句下面,我们创建了一个 react 函数组件 HelloWorldSceneAR

首先我们使用 useState 来定义一个 text state,及其更新函数 setText()。变量text 类型为字符串,初始化值为 Initializing AR...

接下来我们 return 一个 jsx 元素,它描述了 AR 场景该如何展示。

...
const HelloWorldSceneAR = () => {
  const [text, setText] = useState('Initializing AR...');

  function onInitialized(state, reason) {
    console.log('guncelleme', state, reason);
    if (state === ViroConstants.TRACKING_NORMAL) {
      setText('Hello World!');
    } else if (state === ViroConstants.TRACKING_NONE) {
      // Handle loss of tracking
    }
  }

  return (
    <ViroARScene onTrackingUpdated={onInitialized}>
      <ViroText
        text={text}
        scale={[0.5, 0.5, 0.5]}
        position={[0, 0, -1]}
        style={styles.helloWorldTextStyle}
      />
    </ViroARScene>
  );
};
...

在 return 的 jsx 语句中,顶层元素是一个 <ViroARScene /> 。每个 AR 场景的顶层元素都必须是 ViroARScene ,其它组件则只能作为 ViroARScene 的子组件。

我们使用一个 callback 类型的参数,onTrackingUpdated ,来调用 onInitialized() 函数。当跟踪状态为 TRACKING_NORMAL时,该函数会将 text 设置为 “Hello World!”。

接下来声明一个 ViroText,其文本内容根据 text 状态在 “Initializing AR...” 和 “Hello World” 之间切换。使用 style 属性指定的字体、字体大小和颜色来渲染该文本对象。对象坐标设为[0,0,-1]。在 viro 的坐标系中,观察者面向负 Z 方向,因此物体对象坐标 Z = -1 ,会将其置于观察者的前方。

声明样式

除了定义 HelloWorldSceneAR 组件,还需声明在组件中使用的样式。样式通常表示组件的布局属性。

在我们的 app 中,我们使用 StyleSheet 创建了一个 styles 变量,其内定义了一个名为 helloWorldTextStyle 的样式,用于描述 ViroText 组件的字体类型、颜色、大小和对齐方式。

let styles = StyleSheet.create({
  helloWorldTextStyle: {
    fontFamily: 'Arial',
    fontSize: 30,
    color: '#ffffff',
    textAlignVertical: 'center',
    textAlign: 'center',
  },
});

目前已经描述了我们的场景是如何工作的,现在让我们看看如何去扩展它。

下载 assets

首先下载我们将用于本教程的资源文件:

  • emoji_smile/
    • emoji_smile.vrx
    • emoji_smile_diffuse.png
    • emoji_smile_normal.png
    • emoji_smile_specular.png
  • grid_bg.jpg

请按照以下步骤操作:

  • 下载资源包:res

    注:此为 github repo 内文件,请使用任意合适方法下载。可使用插件:GitZip for github

  • 放至根目录下的 assets 文件夹中。

往场景中加入组件

以 HelloWorld 场景为例,在 "Hello World" 文本对象坐标下方加一个 3D Box。我们可用 ViroBox 组件做这件事,遵循以下步骤:

首先,从 @viro-community/react-viro 包中导入 ViroBoxViroMaterials

import {
  ...
  ViroBox,
  ViroMaterials,
} from '@viro-community/react-viro';

接下来把 Box 加入到场景中。可以参考 ViroBox API 设置 ViroBox 对象属性以自定义我们的 Box。

将下面的代码放置在 ViroText 组件代码之下:

<ViroBox
  position={[0, -0.5, -1]}
  scale={[0.3, 0.3, 0.1]}
  materials={['grid']}
/>

定制 ViroBox 属性

在上面的代码,我们把 ViroBox 的位置坐标设置为 [0, -0.5, -1],从 y 轴来看,我们把 Box 放置在了 Text 的下面。

ViroBox 物体对象的 widthheightlength 属性值默认为 1(meter) 。通过 scale 属性值,将其依序缩放 [0.3, 0.3, 0.1]

materials 属性允许我们设置一个预定义好的材质(详情请参考 ViroMaterials)作为 Box 的纹理。在这个例子中,我们在 ViroBox 上设置了一个名为 grid 的材质,我们将在下一步中定义它。

定义材质

在使用上面讲的 grid 材质之前,我们需要定义它。由于我们已经导入了 ViroMaterials,我们可以简单地在 styles 样式声明下方添加以下代码:

ViroMaterials.createMaterials({
  grid: {
    diffuseTexture: require('../assets/grid_bg.jpg'),
  },
});

由此我们定义了一个包含 diffuseTexture(漫反射纹理) 的 grid 材质。该材质指向 assets 目录中的文件 grid_bg.jpg。

这里要注意两点:

  1. require() 函数是 React 提供的一个特殊函数,它将文件路径转换为一个可用于获取资源的值。
  2. require() 的参数是一个文件路径,且为一个相对路径。

这个时候,我们的 HelloWorldSceneAR.js 代码长这样:

import React, { useState } from 'react';
import { StyleSheet } from 'react-native';
import {
  ViroARScene,
  ViroText,
  ViroConstants,
  ViroBox,
  ViroMaterials,
} from '@viro-community/react-viro';

let styles = StyleSheet.create({
  f1: { flex: 1 },
  helloWorldTextStyle: {
    fontFamily: 'Arial',
    fontSize: 30,
    color: '#ffffff',
    textAlignVertical: 'center',
    textAlign: 'center',
  },
});

ViroMaterials.createMaterials({
  grid: {
    diffuseTexture: require('../assets/grid_bg.jpg'),
  },
});

const HelloWorldSceneAR = () => {
  const [text, setText] = useState('Initializing AR...');

  function onInitialized(state, reason) {
    console.log('guncelleme', state, reason);
    if (state === ViroConstants.TRACKING_NORMAL) {
      setText('Hello World!');
    } else if (state === ViroConstants.TRACKING_NONE) {
      // Handle loss of tracking
    }
  }

  return (
    <ViroARScene onTrackingUpdated={onInitialized}>
      <ViroText
        text={text}
        position={[0, 0, -1]}
        scale={[0.5, 0.5, 0.5]}
        style={styles.helloWorldTextStyle}
      />
      <ViroBox
        position={[0, -0.5, -1]}
        scale={[0.3, 0.3, 0.1]}
        materials={['grid']}
      />
    </ViroARScene>
  );
};

export { HelloWorldSceneAR };

保存 HelloWorldSceneAR.js 文件,重载 app。你现在应该在 Hello World 文本下看到了一个粉红色和灰色的立方体。

对于 ios 设备调试,若要重新加载文件,只需摇动设备,就会出现一个调试菜单,如下所示。点击“重新加载”,将出现一个选择 AR 或 VR 的屏幕。点击 AR,更改将刷新。 对于 android 设备,会自动热更新重加载。

加一个 3D 物体到场景中

现在让我们向场景中添加一个 3D 对象。 assets 文件夹中有一个“emoji_smile”文件夹,这些文件将被用来向场景中添加 3D emoji。

导入组件

我们首先需要导入能用到的组件,如:Viro3DObjectViroAmbientLightViroSpotLight

import {
    ...
  Viro3DObject,
  ViroAmbientLight,
  ViroSpotLight,
} from '@viro-community/react-viro';

接下来,我们需要把 Viro3DObject 和光源加入到场景中。复制下面的代码,将其加入到 <ViroARScene /> 中,注意放置位置为 ViroBox 组件下面。

<Viro3DObject
  source={require('../assets/emoji_smile/emoji_smile.vrx')}
  resources={[
    require('../assets/emoji_smile/emoji_smile_diffuse.png'),
    require('../assets/emoji_smile/emoji_smile_normal.png'),
    require('../assets/emoji_smile/emoji_smile_specular.png'),
  ]}
  position={[-0.5, 0.5, -1]}
  scale={[0.2, 0.2, 0.2]}
  type='VRX'
/>

保存文件,重载 app 页面。你将会看到下面的场景。

如果一开始看不到所有物体组件的话,移动一下手机视角,物体可能在你的左侧。

使用 ViroARPlane

在一个 AR app 中,设备(例如手机)的摄像头通常被用来呈现一个真实物理世界的实时屏幕视图。三维的虚拟物体被叠加在这个视图中,产生出一种它们真实存在的假象。

一种在真实世界放置物体的方法是使用组件 ViroARPlaneViroARPlaneSelector。当 AR 系统检测到一个平面时,ViroReact 会尝试将它绑定到已经声明的 ViroARPlane 组件上,并持续将虚拟平面固定在检测到的真实世界平面上。至于另一种,ViroARPlaneSelector 组件可以让开发人员实现——允许其用户选择希望所使用的平面。

为了了解它是如何工作的,让我们在场景中添加一个 ViroARPlaneSelector

首先,如下所示,导入组件 ViroARPlaneSelector

import {
  ...
  ViroARPlaneSelector,
} from '@viro-community/react-viro';

然后在 <ViroARScene /> 的 children 中复制粘贴以下代码,加入一个 ViroARPlaneSelector 组件。

<ViroARPlaneSelector />

保存文件,重载测试 app。除了之前的场景,当你在房间内移动时,视图上还会出现一些检测到的虚拟平面。在我们的现实世界中,桌子和地板平面都被检测到,如下所示:

如果尝试点击“选择”一个平面,这些平面将全部消失,因为 ViroARPlaneSelector 中没有添加任何内容,在下一节中,我们将展示如何向其添加物体组件。

添加一个 3D 物体到平面

先前,当我们将表情符号添加到场景中时,使用的是一个固定位置 {[-.5, -.5, -1]},如下所示:

<Viro3DObject
  source={require('../assets/emoji_smile/emoji_smile.vrx')}
  resources={[
    require('../assets/emoji_smile/emoji_smile_diffuse.png'),
    require('../assets/emoji_smile/emoji_smile_normal.png'),
    require('../assets/emoji_smile/emoji_smile_specular.png'),
  ]}
  position={[-0.5, 0.5, -1]}
  scale={[0.2, 0.2, 0.2]}
  type='VRX'
/>

对于 AR,我们经常希望将对象放置在与现实世界相关的位置。使用我们之前确定的平面,将表情符号放在一个平面上。首先,从你的 js 文件中删除你刚刚添加的代码。然后将 HelloWorldSceneAR.js 文件中的 Viro3DObject 代码替换为以下代码:

<ViroARPlaneSelector>
  <Viro3DObject
    source={require('../assets/emoji_smile/emoji_smile.vrx')}
    resources={[
      require('../assets/emoji_smile/emoji_smile_diffuse.png'),
      require('../assets/emoji_smile/emoji_smile_normal.png'),
      require('../assets/emoji_smile/emoji_smile_specular.png'),
    ]}
    position={[0, 0.5, 0]}
    scale={[0.2, 0.2, 0.2]}
    type='VRX'
  />
</ViroARPlaneSelector>

请注意,我们还将表情符号的位置更改为 [0, 0.5, 0]。这是因为表情符号的中心在表情符号本身内,所以为了让它正好“坐在”平面上,我们需要将它稍微移动到平面所在的位置上方。

保存文件,重载测试 app。

现在我们已经将 3D 对象组件放置在 ViroARPlaneSelector 组件中,当点击一个平面时,表情符号将放置在所选平面上,其它平面将消失。

交互和动画

AR 的一大优点,是用户可以在真实世界中移动,从不同角度查看虚拟对象,并与之交互。让我们为表情符号添加交互,并为盒子添加一些动作。

首先,我们需要使表情符号可拖拽,以便通过拖拽手势而移动它。首先我们需要导入另一个组件 ViroNode

import {
  ...
  ViroNode,
} from '@viro-community/react-viro';

在上一步中,正如下所示,我们将表情符号放置在 ViroARPlaneSelector 组件中:

<ViroARPlaneSelector>
  <Viro3DObject
    source={require('../assets/emoji_smile/emoji_smile.vrx')}
    resources={[
      require('../assets/emoji_smile/emoji_smile_diffuse.png'),
      require('../assets/emoji_smile/emoji_smile_normal.png'),
      require('../assets/emoji_smile/emoji_smile_specular.png'),
    ]}
    position={[0, 0.5, 0]}
    scale={[0.2, 0.2, 0.2]}
    type='VRX'
  />
</ViroARPlaneSelector>

为了让我们的表情符号沿着现实世界的表面拖动,我们需要用 ViroNode 替换 ViroARPlaneSelector,将 dragType 设置为 FixedToWorld,并添加一个空的匿名函数,让平台知道我们想要拖动这个对象。

用下面的代码块替换上面的代码块:

<ViroNode position={[0, -1, 0]} dragType='FixedToWorld' onDrag={() => {}}>
  <Viro3DObject
    source={require('../assets/emoji_smile/emoji_smile.vrx')}
    resources={[
      require('../assets/emoji_smile/emoji_smile_diffuse.png'),
      require('../assets/emoji_smile/emoji_smile_normal.png'),
      require('../assets/emoji_smile/emoji_smile_specular.png'),
    ]}
    position={[0, 0.5, 0]}
    scale={[0.2, 0.2, 0.2]}
    type='VRX'
  />
</ViroNode>

保存文件,重载测试 app。

表情符号现在应该出现在你的面前和左侧。且现在应该能够在场景中触摸和拖动表情符号,注意它如何沿着现实世界的表面移动。

动画

最后,让我们为 box 添加一些运动。首先,导入 ViroAnimations 组件。

import {
  ...
  ViroAnimations,
} from '@viro-community/react-viro'

然后将 ViroBox 组件替换为以下内容:

<ViroBox
  position={[0, -0.5, -1]}
  scale={[0.3, 0.3, 0.1]}
  materials={['grid']}
  animation={{ name: 'rotate', run: true, loop: true }}
/>

如你所见,我们添加了一个值为 {name: "rotate", run: true, loop: true} 的新属性动画。rotate 是我们将在下一步中注册的动画,就像我们在上面为 ViroMaterials 所做的那样。

找到我们注册 ViroMaterials 的位置,将以下代码复制粘贴到其下方:

ViroAnimations.registerAnimations({
  rotate: {
    properties: {
      rotateY: '+=90',
    },
    duration: 250, //0.25 seconds
  },
});

保存文件,重载测试 app。现在应该看到“Hello World”,一个旋转框,和一个可拖拽的表情符号。本教程末尾发布了完整的最终代码示例。

继续修改场景

现在你应该对 ViroReact 的工作原理有了大概的了解。查看我们的代码示例以获取其它示例,或继续向 HelloWorldScene 添加你自己的功能。例如:

  • 向场景中的其它对象添加动画。查看我们的Animation Guide以获取有关如何完成此操作的信息。
  • 尝试为场景添加阴影和照明。查看Lighting and Materials了解详细信息。

Final Code

import React, { useState } from 'react';
import { StyleSheet } from 'react-native';
import {
  ViroARScene,
  ViroText,
  ViroConstants,
  ViroBox,
  ViroMaterials,
  ViroAmbientLight,
  ViroSpotLight,
  Viro3DObject,
  ViroNode,
  ViroAnimations,
} from '@viro-community/react-viro';

/**
 * declare styles
 */
let styles = StyleSheet.create({
  helloWorldTextStyle: {
    fontFamily: 'Arial',
    fontSize: 30,
    color: '#ffffff',
    textAlignVertical: 'center',
    textAlign: 'center',
  },
});

/**
 * declare materials
 */
ViroMaterials.createMaterials({
  grid: {
    diffuseTexture: require('../assets/grid_bg.jpg'),
  },
});

/**
 * declare animations
 */
ViroAnimations.registerAnimations({
  rotate: {
    properties: {
      rotateY: '+=90',
    },
    duration: 250, //0.25 seconds
  },
});

/**
 * HelloWorldSceneAR Function Component
 * @returns {JSX.Element} component content
 */
const HelloWorldSceneAR = () => {
  const [text, setText] = useState('Initializing AR...');

  function onInitialized(state, reason) {
    console.log('guncelleme', state, reason);
    if (state === ViroConstants.TRACKING_NORMAL) {
      setText('Hello World!');
    } else if (state === ViroConstants.TRACKING_NONE) {
      // Handle loss of tracking
    }
  }

  return (
    <ViroARScene onTrackingUpdated={onInitialized}>
      <ViroText
        text={text}
        position={[0, 0, -1]}
        scale={[0.5, 0.5, 0.5]}
        style={styles.helloWorldTextStyle}
      />
      <ViroBox
        position={[0, -0.5, -1]}
        scale={[0.3, 0.3, 0.1]}
        materials={['grid']}
        animation={{ name: 'rotate', run: true, loop: true }}
      />
      <ViroAmbientLight color={'#aaaaaa'} />
      <ViroSpotLight
        innerAngle={5}
        outerAngle={90}
        direction={[0, -1, -0.2]}
        position={[0, 3, 1]}
        color='#ffffff'
        castsShadow={true}
      />
      <ViroNode position={[0, -1, 0]} dragType='FixedToWorld' onDrag={() => {}}>
        <Viro3DObject
          source={require('../assets/emoji_smile/emoji_smile.vrx')}
          resources={[
            require('../assets/emoji_smile/emoji_smile_diffuse.png'),
            require('../assets/emoji_smile/emoji_smile_normal.png'),
            require('../assets/emoji_smile/emoji_smile_specular.png'),
          ]}
          position={[0, 0.5, 0]}
          scale={[0.2, 0.2, 0.2]}
          type='VRX'
        />
      </ViroNode>
    </ViroARScene>
  );
};

export { HelloWorldSceneAR };