发布于 2026-01-06 3 阅读
0

使用 React Native 构建增强现实应用程序

使用 React Native 构建增强现实应用程序

注:本文最初发布于marmelab.com

增强现实是目前最重要的趋势之一。因此,继一年多前我们尝试使用浏览器进行增强现实应用开发之后,我想测试一个能够创建原生增强现实体验的框架。请继续阅读,了解我是如何使用 React-Native 在移动设备上开发一款黑白棋游戏应用的。

什么是增强现实?

由于“人工智能”一词容易与其他相关概念混淆,增强现实(AR)经常被误认为是虚拟现实(VR)。事实上,VRAR完全不同。VR是将虚拟世界投射到我们眼前,而AR则是将虚拟物体与现实世界融合投射。

我邀请您查看我们之前关于浏览器中的 AR 的博客文章,其中有对这些概念的更详细描述。

使用 JavaScript 实现原生性能的增强现实

在 Marmelab,我们是 React 及其生态系统的忠实拥趸。因此,我们利用这项技术为客户开发了大量开源工具项目。

我并不自诩为优秀的 Java、Kotlin、C# 或 Swift 开发者。但我也希望移动端应用拥有良好的性能,所以使用 React 之类的 Web 框架就不在我的考虑范围之内了。因此,我开始寻找一个原生框架,它能让我同时使用 JavaScript 和 React 开发 iOS 和 Android 应用。

病毒

经过几分钟的研究,唯一显而易见的选择就是使用ViroReact。该框架底层基于两个在移动增强现实和虚拟现实领域占据主导地位的 API:iOS 的 ARKitAndroid 的 ARCore

ARKit实际上是目前最大的 AR 平台。它允许在至少配备 A9 芯片和 iOS 11 的苹果设备上开发丰富的沉浸式体验。

ARCore与此大同小异,只是它只支持少数几款性能足够强大的设备,才能发挥 API 的最佳性能。而且,它似乎也支持 iOS 设备?

目前这些API的主要缺点在于其设备支持较为有限。随着时间的推移,手机性能会越来越强大,届时它们的使用频率也会更高。

维罗,局外人

Viro 是一个免费的 AR/VR 开发平台,它允许使用 React-Native 构建跨平台应用程序,以及使用 Java 构建完全原生的 Android 应用程序。它支持多种平台和 API,例如 ARKit、ARCore、Cardboard、Daydream 或 GearVR。

Viro支持的平台

如前所述,Viro 既支持构建原生应用,也支持构建 React Native 应用。因此,Viro 提供了两个不同的软件包:ViroCoreViroReact

要使用该平台,您仍然需要注册。注册后获得的API密钥是使用该平台所必需的。

遗憾的是,Viro 并非开源软件,但可以免费使用,且分发无限制。据 ViroMedia 首席执行官称,API 密钥用于内部分析,并用于防范可能的许可违规行为。

VIRO 保留随时修改、暂停或终止软件,或更改访问要求的权利,无论是否事先通知。

关于上述许可说明,因此有必要对其使用保持警惕,因为我们无法保证该平台的未来发展

初次接触 ViroReact

在本节中,我将通过一个简单的用例来介绍 Viro 框架的主要部分:Marmelab 徽标的 3D 投影!

首先,我们需要创建一个3D网格模型,以便将其添加到我们的项目中。特别感谢@jpetitcolas,他几年前使用Blender创建了Marmelab的标志。

安装

在使用 Viro 之前,我们需要安装一些 npm 依赖项。Viro 需要react-native-clireact-viro-cli作为全局包。

npm install -g react-native-cli
npm install -g react-viro-cli
Enter fullscreen mode Exit fullscreen mode

然后,我们可以使用特殊命令初始化一个 Viro 项目react-viro init,后跟项目名称。之后会创建一个同名文件夹。

react-viro init marmelab_for_real
Enter fullscreen mode Exit fullscreen mode

那么,这个项目有什么特点呢?嗯,文件夹结构与我们通常在 React-Native 中遇到的结构非常相似,这一点并不令人意外。

├── android
├── bin
├── ios
├── js
├── node_modules
├── App.js
├── app.json
├── index.android.js
├── index.ios.js
├── index.js
├── metro.config.js
├── package.json
├── rn-cli.config.js
├── setup-ide.sh
└── yarn.lock
Enter fullscreen mode Exit fullscreen mode

开发者体验

项目初始化完成后,只需使用npm start命令启动它即可。Viro 将自动创建一个ngrok 隧道,全球任何连接到互联网的手机都可以使用该隧道。

julien@julien-laptop /tmp/foo $ npm start

> foo@0.0.1 prestart /tmp/foo
> ./node_modules/react-viro/bin/run_ngrok.sh

 ----------------------------------------------------------
|                                                          |
| NGrok Packager Server endpoint: http://32a5a3d7.ngrok.io |
|                                                          |
 ----------------------------------------------------------

> foo@0.0.1 start /tmp/foo
> node node_modules/react-native/local-cli/cli.js start

┌──────────────────────────────────────────────────────────────────────────────┐
│                                                                              │
│  Running Metro Bundler on port 8081.                                         │
│                                                                              │
│  Keep Metro running while developing on any JS projects. Feel free to        │
│  close this tab and run your own Metro instance if you prefer.               │
│                                                                              │
│  https://github.com/facebook/react-native                                    │
│                                                                              │
└──────────────────────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

要访问该应用程序,我们只需使用 Viro 提供的专用TestBed 应用程序,并配合相应的隧道或本地 IP 地址(如果您是本地连接)。在这方面,Viro 让我想起了Expo。然后,我们就可以访问测试应用程序了:

除了这些运行功能外,Viro 还像任何 React-Native 应用程序一样,提供热重载、实时重载、错误消息和警告直接显示在设备上的功能。

初始化场景导航器

根据您所需的项目类型,Viro 提供SceneNavigator以下 3 个不同的组件:

  • ViroVRSceneNavigator:适用于 VR 应用
  • ViroARSceneNavigator:适用于 AR 应用
  • Viro3DSceneNavigator:适用于 3D(而非 AR/VR)应用

这些组件用作我们应用程序的入口点。您必须根据您的需求选择一个,在本例中,我们的目标ViroARSceneNavigator是实现增强现实功能。

每个组件都SceneNavigator需要两个不同的属性,分别是 `<property>`apiKey和 `<property> initialScene`。第一个属性来自您在 Viro 网站上的注册信息,第二个属性是一个对象,其 `<property>`scene属性的值是我们场景组件的值。

// App.js

import React from 'react';
import { View, StyleSheet } from 'react-native';
import { ViroARSceneNavigator } from 'react-viro';
import { VIROAPIKEY } from 'react-native-dotenv';

import PlayScene from './src/PlayScene';

const styles = StyleSheet.create({
    root: {
        flex: 1,
        backgroundColor: '#fff',
    },
});

const App = () => (
    <View style={styles.root}>
        <ViroARSceneNavigator
            apiKey={VIROAPIKEY}
            initialScene={{ scene: PlayScene }}
        />
    </View>
);

export default App;
Enter fullscreen mode Exit fullscreen mode

由于我们希望 Viro 保持apiKey私密性,因此我们将该软件包与项目文件夹根目录下的react-native-dotenv一个文件结合使用。.env

要实现这一点,只需安装此软件包并在其中yarn add -D react-native-dotenv创建一个.env文件即可。VIROAPIKEY=<YOUR-VIRO-API-KEY>

最后一步是将预设添加到 Babel 中,具体步骤如下所述。

// .babelrc

{
  "presets": [
    "module:metro-react-native-babel-preset",
+   "module:react-native-dotenv"
  ]
}
Enter fullscreen mode Exit fullscreen mode

添加场景

现在引导程序已经完成,是时候开发我们的第一个场景了!

Viro 场景充当所有 UI 对象、灯光和 3D 对象的容器。场景组件有两种类型:ViroSceneViroARScene

每个Scene组件都包含一个由功能齐全的 3D 场景图引擎管理的节点层级树状结构。子组件通过表示3D 空间中位置变换的ViroScene组件进行定位。ViroNode

因此,树下的几乎每个对象都有一个position,rotationscaleprop,它们接受一个坐标/向量 (x, y, z) 数组,如下所述。

<ViroNode
    position={[2.0, 5.0, -2.0]}
    rotation={[0, 45, 45]}
    scale={[2.0, 2.0, 2.0]}
 />
Enter fullscreen mode Exit fullscreen mode

既然我们知道了它的工作原理,我们就可以创建我们的第一个ViroARScene(即PlayScene)。

// src/PlayScene.js

import React from 'react';

import {
    ViroARScene,
    Viro3DObject,
    ViroAmbientLight
} from 'react-viro';

const MarmelabLogo = () => (
    <Viro3DObject
        source={require('../assets/marmelab.obj')}
        resources={[require('../assets/marmelab.mtl')]}
        highAccuracyEvents={true}
        position={[0, 0, -1]} // we place the object in front of us (z = -1)
        scale={[0.5, 0.5, 0.5]} // we reduce the size of our Marmelab logo object
        type="OBJ"
    />
);

const PlayScene = () => (
    <ViroARScene displayPointCloud>
        <ViroAmbientLight color="#fff" />
        <MarmelabLogo />
    </ViroARScene>
);

export default PlayScene;
Enter fullscreen mode Exit fullscreen mode

在之前的代码中,我们引入了两个新的 Viro 组件,分别是Viro3DObjectViroAmbiantLight

Viro3DObject允许从 3D 结构/纹理文件创建 3D 对象,并将其放置在我们的 Viro 上Scene。在本例中,我们使用之前混合的 Marmelab 徽标对象声明了一个组件。

ViroAmbientLight我们需要引入一些照明Scene如果没有这些照明,任何物体都看不见。

最终结果真的令人惊艳,尤其是考虑到我们只花了很少的时间。

升级:在 AR 中开发黑白棋

经过一番探索,现在是时候利用这项技术开发一个更实际的应用了。由于这次我不想进行建模或编写业务逻辑,我将复用之前在黑客马拉松活动中使用的代码库和混合对象(圆盘)。这是一个使用 ThreeJS 开发的黑白棋游戏

锚点与目标

黑白棋游戏场景

根据我们之前的实验,我们将替换我们的组件,使其PlayScene包含一个新Game组件,Board该组件本身包含一个Disk对象组件。

// src/PlayScene.js

import React from 'react';

import {
    ViroARScene,
    ViroAmbientLight,
} from 'react-viro';

import Game from './components/Game';
import { create as createGame } from './reversi/game/Game';
import { create as createPlayer } from './reversi/player/Player';
import { TYPE_BLACK, TYPE_WHITE } from './reversi/cell/Cell';

const defaultGame = createGame([
    createPlayer('John', TYPE_BLACK),
    createPlayer('Charly', TYPE_WHITE),
]);

const PlayScene = () => {
    const [game] = useState(defaultGame);

    return (
        <ViroARScene displayPointCloud>
            <ViroAmbientLight color="#fff" />
                <Game game={game} />
        </ViroARScene>
    );
};

export default PlayScene;
Enter fullscreen mode Exit fullscreen mode
// src/components/Game.js

import React, { Component } from 'react';

import Board from './Board';
import { getCurrentPlayer } from '../reversi/game/Game';

class Game extends Component {
    // ...

    render() {
        const { game } = this.state;

        return (
            <Board
                board={game.board}
                currentCellType={getCurrentPlayer(game).cellType}
                onCellChange={this.handleCellChange}
            />
        );
    }
}

export default Game;
Enter fullscreen mode Exit fullscreen mode

游戏需要用到游戏板和圆盘两个组件:

// src/components/Board.js

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { ViroNode } from 'react-viro';

import Disk from './Disk';
import { TYPE_WHITE, TYPE_EMPTY } from '../reversi/cell/Cell';

class Board extends Component {
    // ...

    renderCellDisk = cell => (
        <Disk
            key={`${cell.x}${cell.y}`}
            position={[0.03 * cell.x, 0, -0.3 - 0.03 * cell.y]}
            rotation={[cell.type === TYPE_WHITE ? 180 : 0, 0, 0]}
            opacity={cell.type === TYPE_EMPTY ? 0.15 : 1}
            onClick={this.handleClick(cell)}
        />
    );

    render() {
        const { board } = this.props;

        return (
            <ViroNode position={[0.0, 0.0, 0.5]}>
                {board.cells
                    .reduce(
                        (agg, row, y) => [...agg, ...row.map((type, x) => createCell(x, y, type))],
                        [],
                    )
                    .map(this.renderCellDisk)}
            </ViroNode>
        );
    }
}

Board.propTypes = {
    onCellChange: PropTypes.func.isRequired,
    currentCellType: PropTypes.number.isRequired,
    board: PropTypes.shape({
        cells: PropTypes.array,
        width: PropTypes.number,
        height: PropTypes.number,
    }),
};

export default Board;
Enter fullscreen mode Exit fullscreen mode
// src/Disk.js

import React from 'react';
import { Viro3DObject } from 'react-viro';

const Disk = props => (
    <Viro3DObject
        source={require('../assets/disk.obj')}
        resources={[require('../assets/disk.mtl')]}
        highAccuracyEvents={true}
        position={[0, 0, -1]}
        scale={[0.0007, 0.0007, 0.0007]}
        type="OBJ"
        {...props}
    />
);

export default Disk;
Enter fullscreen mode Exit fullscreen mode

它奏效了!但是,我想我们都同意,在浮动棋盘上玩黑白棋是不可能的……这就是为什么我们要定义一个锚点,我们可以在上面放置我们的Game/ Board

在现实世界中放置物体

在增强现实术语中,将虚拟物体附着到现实世界中的某个点上的概念称为锚定。顾名思义,锚点就是用来实现这一目标的。

锚点是AR 系统(ARCore 或 ARKit)在现实世界中找到的垂直或水平平面,或图像(通常是标记),我们可以依靠它们来构建虚拟世界。

Viro 中,锚点由一个对象表示,可以通过目标Anchor使用不同的检测方法找到该对象,如下所述。

  • ViroARPlane该组件允许使用“手动”(通过“anchorId”)或“自动”检测现实世界中的平面,并将对象放置在该平面上。
  • ViroARPlaneSelector该组件显示系统发现的所有可用飞机,并允许用户选择一个。
  • ViroARImageMarker该组件允许使用一张带插图的纸作为虚拟对象的物理锚点。

就我而言,我选择了ViroARImageMarker锚定系统,因为它看起来更稳定,性能也更好(乍一看)。

ViroARImageMarker有一个名为 `target` 的必填属性target。该属性必须包含先前使用 `module` 声明的已注册目标的名称ViroARTrackingTargets

首先,我们需要使用该createTargets函数创建目标。在本例中,我们声明了一个名为“ marmelabAnchor(是的,我的名字很正式……)”的图像目标,因为我使用了 Marmelab 的徽标作为锚点。

ViroARImageMarker然后,我们可以直接使用这个锚点名称作为新元素的锚点属性值Game

// src/PlayScene.js

import React from 'react';

import {
    ViroARScene,
    ViroAmbientLight,
+   ViroARTrackingTargets,
+   ViroARImageMarker,
} from 'react-viro';

import Game from './components/Game';
import { create as createGame } from './reversi/game/Game';
import { create as createPlayer } from './reversi/player/Player';
import { TYPE_BLACK, TYPE_WHITE } from './reversi/cell/Cell';

const defaultGame = createGame([
    createPlayer('John', TYPE_BLACK),
    createPlayer('Charly', TYPE_WHITE),
]);

const PlayScene = () => {
    const [game] = useState(defaultGame);

    return (
        <ViroARScene displayPointCloud>
            <ViroAmbientLight color="#fff" />
+           <ViroARImageMarker target={'marmelabAnchor'}>
                <Game game={game} />
+           </ViroARImageMarker>
        </ViroARScene>
    );
};

+ ViroARTrackingTargets.createTargets({
+     marmelabAnchor: {
+         type: 'Image',
+         source: require('./assets/target.jpg'), // source of the target image
+         orientation: 'Up', // desired orientation of the image
+         physicalWidth: 0.1, // with of the target in meters (10 centimeters in our case)
+     },
+ });

export default PlayScene;
Enter fullscreen mode Exit fullscreen mode

所有在树状结构中children该元素下声明的内容都相对于该元素进行定位。在本例中,组件会被放置在目标元素之上。ViroARImageMarkerGameViroARImageMarker

场景动画

现在AR黑白棋游戏运行得更好了,但是动画效果还有些不足。那么,我们该如何添加像之前ThreeJS项目中那样的棋盘翻转效果呢?

磁盘翻转

为了满足这种常见的需求,ViroReact 提供了一个名为ViroAnimations的全局动画注册表,它可以与任何接受 prop 的组件一起使用animation

在本例中,我们将组合多种变换,以创建完整的圆盘翻转效果。以下是预期随时间推移的场景:

0-300毫秒 向上移动
300-600毫秒 向下
150-350毫秒 旋转(当圆盘到达顶部时)

首先,我们将根据这个变换时间线注册一个动画。

import { ViroAnimations } from 'react-viro';

// ...

ViroAnimations.registerAnimations({
    moveUp: {
        properties: { positionY: '+=0.03' },
        duration: 300,
        easing: 'EaseInEaseOut',
    },
    moveDown: {
        properties: { positionY: '-=0.03' },
        duration: 300,
        easing: 'EaseInEaseOut',
    },
    flip: {
        properties: { rotateX: '+=180' },
        duration: 300,
        easing: 'EaseInEaseOut',
        delay: 150
    },
    flipDisk: [['moveUp', 'moveDown'], ['flip']],
});
Enter fullscreen mode Exit fullscreen mode

如您所见,我们声明了 3 个不同的动画,并使用第四个动画将它们组合起来flipDiskmoveUp它们moveDown位于同一个数组中,因为它们是依次执行的。flip与这两个变换并行运行。

其次,我们只需要在Disk组件中使用animationprop 来调用这个已注册的动画,如下所示:

    // ...

    renderCellDisk = cell => {
        const { flipping } = this.state;

        return (
            <Disk
                key={`${cell.x}${cell.y}`}
                position={[0.03 * cell.x, 0, -0.3 - 0.03 * cell.y]}
                rotation={[cell.type === TYPE_WHITE ? 180 : 0, 0, 0]}
                opacity={cell.type === TYPE_EMPTY ? 0.15 : 1}
                onClick={this.handleClick(cell)}
                animation={{
                    name: 'flipDisk',
                    run: !!flipping.find(hasSamePosition(cell)),
                    onFinish: this.handleEndFlip(cell),
                }}
            />
        );
    };

    // ...
Enter fullscreen mode Exit fullscreen mode

animation属性接受一个具有以下结构的对象:

{
    name: string            // name of the animation
    delay: number           // number of ms before animation starts
    loop: bool              // animation can loop?
    onFinish: func          // end callback of the animation
    onStart: func           // start callback of the animation
    run: bool               // animation is active or not?
    interruptible: bool     // can we change animation when running?
}
Enter fullscreen mode Exit fullscreen mode

在我们的例子中,我们只是使用了namerunonFinish属性来定义当前正在翻转的磁盘,并在动画结束时将其从翻转列表中移除。

结论

使用 ViroReact 构建增强现实项目是一个绝佳的选择,原因有很多。虽然这是我第一次涉足这个领域,但我从未遇到任何困难。相反,Viro 帮助我充满信心地探索这个领域。

React Native 的开发者体验非常丰富,它提供了 ReactJS 绑定、热重载和清晰易懂的文档。然而,我不建议将其用于复杂或对性能要求较高的应用程序,因为 React Native 的 JavaScript 线程可能会导致事件拥塞和延迟。因此,如果性能至关重要,我建议使用纯原生解决方案。

顺便一提,谷歌一直在其应用程序中添加增强现实功能,例如谷歌地图增强现实技术从未如此蓬勃发展。所以,千万不要错过。

还有许多其他功能有待探索,例如骨骼动画粒子特效物理效果视频声音。别害羞,欢迎在评论区分享你的体验;)

您可以在 GitHub 的marmelab/virothello 仓库中找到最终代码。

文章来源:https://dev.to/juliendemageon/build-augmented-reality-applications-with-react-native-3g73