loading...
Cover image for Build Augmented Reality Applications With React-Native

Build Augmented Reality Applications With React-Native

juliendemangeon profile image Demangeon Julien Originally published at marmelab.com ・12 min read

Note: This post was originally posted on marmelab.com.

Augmented Reality is one of the most important trends currently. So, after our trial using the browser over 1 year ago, I wanted to test a framework offering the possibility to create native augmented reality experiences. Read on to see how I developed a reversi game application on mobile using React-Native.

What is Augmented Reality?

As the "Artificial Intelligence" term can be mixed up with other related concepts, Augmented Reality (AR) is quite often mistaken with Virtual Reality (VR). In fact, VR and AR are not the same at all. While VR is a projection of a virtual world to our eyes, AR is a blended projection of a virtual object in the real world.

I invite you to check a more detailed description of these concepts in our previous blog post about AR in the browser.

Augmented Reality In Javascript With Native Performance

At Marmelab, we are absolute fans of React and its ecosystem. That's why we develop a lot of open-source tools and projects for our customers using this technology.

I don't pretend to be a good Java, Kotlin, CSharp or Swift developer. But I also want to have good performance on mobile, so using a web framework like React is out of the question. So I started looking for a native framework which lets me develop iOS and Android apps with both Javascript and React.

Viro

After several minutes of research, the only obvious choice was to use ViroReact. Under the hood, this framework is based on two APIs that dominate the world of Augmented and Virtual Reality for mobile phones: ARKit for iOS and ARCore for Android.

ARKit is actually the biggest existing AR platform. It allows to develop rich immersive experiences on Apple devices having at least an A9 chip and iOS 11.

ARCore is more or less the same, except that it supports a short list of devices that are considered to be powerful enough to run the API at its best. And also iOS devices, apparently?.

The rather limited support of devices is the major weakness of these APIs for the moment. Over time, phones will become more and more powerful, which will make it possible to use them more often.

Viro, The Outsider

Viro is a free AR/VR development platform that allows building cross-platform applications using React-Native, and fully native Android applications using Java. It supports multiple platforms and APIs such as ARKit, ARCore, Cardboard, Daydream or GearVR.

Viro Supported Platforms

As previously said, Viro allows building both fully native application and React-Native ones. That's why Viro provides two distinct packages: ViroCore and ViroReact.

To use it, you're still required to sign up. The API key which is provided following registration is mandatory to be able to use the platform.

Sadly, Viro is not open-source but (only) free to use with no limits on distribution. According to the ViroMedia CEO, the API key is used for internal analytics and to guard against possible license violations.

VIRO reserves the right, at any time, to modify, suspend, or discontinue the Software, or change access requirements, with or without notice.

Regarding the license note above, it is therefore necessary to remain vigilant regarding its use since we have no guarantee on the evolution of the platform.

First Contact With ViroReact

In this section, I'll cover the major parts of the Viro Framework with a simple use case: a 3D projection of the Marmelab logo !

First, we need to create a 3D mesh to be able to include it in our project. Special thanks to @jpetitcolas who created the Marmelab logo using blender a few years ago.

Installation

Before using Viro, we need to install some npm dependencies. Viro requires react-native-cli and react-viro-cli as global packages.

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

Then, we can initialize a Viro project using the special command react-viro init, followed by the project name. A folder with the same name is then created.

react-viro init marmelab_for_real
Enter fullscreen mode Exit fullscreen mode

So, what can we see in this project? Well, the folder structure is quite similar to the usual ones we encounter with React-Native, no surprise on this point.

├── 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

Developer Experience

Once the project is initialized, we just have to launch it using the npm start command. Viro will automatically create an ngrok tunnel, which can be used by any phone connected to the internet around the globe.

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

To access the application, we just have to use the special TestBed application from Viro with the corresponding tunnel or local ip (if you're connected locally). On those aspects, Viro reminds me of Expo. Then, we're able to access the test application:

In addition to these running facilities, Viro also offers hot-reloading, live-reloading, error messages & warnings directly on the device, just like any React-Native application does.

Initializing a Scene Navigator

Depending on the type of project you want, Viro provides 3 distinct SceneNavigator components which are the following:

  • ViroVRSceneNavigator: For VR Applications
  • ViroARSceneNavigator: For AR Applications
  • Viro3DSceneNavigator: For 3D (not AR/VR) Applications

This components are used as entry points for our application. You must choose one depending on what you want to do, in our case ViroARSceneNavigator for Augmented Reality.

Each SceneNavigator requires two distinct props which are apiKey and initialScene. The first one comes from your registration on the Viro website, the second one is an object with a scene attribute with our scene component as value.

// 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

Since we want to keep our Viro apiKey private, we use the react-native-dotenv package in conjunction with a .env file at the root of our project folder.

To make it psosible, just install this package with yarn add -D react-native-dotenv and create a .env file with VIROAPIKEY=<YOUR-VIRO-API-KEY> in it.

The last step is to add the preset to babel has described below.

// .babelrc

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

Adding a Scene

Now that the bootstrap is done, it's time to develop our first scene!

Viro Scenes act as containers for all our UI Objects, Lights and 3D objects. There are 2 types of Scene components: ViroScene and ViroARScene.

Each Scene contains a hierarchical tree structure of nodes that are managed by a full-featured 3D scene graph engine. ViroScene children are positioned through ViroNode components that represent positions and transformations in 3D space.

So, almost every object under the tree has a position, rotation and scale prop that accept an array of coordinates/vector (x, y, z) as described below.

<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

Now that we know how it works, we can create our first ViroARScene (aka 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

In the previous code, we've introduced 2 new Viro Components that are Viro3DObject and ViroAmbiantLight.

The Viro3DObject allows creating 3D objects from 3D structure / textures files that can be placed on our Viro Scene. In our case, we declare a component using our previously blended Marmelab logo object.

The ViroAmbientLight introduce some lighting in our Scene. Without that light, no object is visible.

The final result is really amazing, especially since we spent very little time on it.

Level Up: Developing A Reversi In AR

After this little exploration, it's time for us to develop a more tangible application using this technology. Since I don't want to do modeling or coding business logic this time, I'll reuse an existing codebase and blended objects (disks) from a previous projects I worked on during a hackday. It's a Reversi Game using ThreeJS.

Anchor & Target

The Reversi PlayScene

According to our previous experiment, we're going to replace our PlayScene to include a new Game component that contains a Board that itself contains Disk object components.

// 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

The Game relies on a Board and a Disk component:

// 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

It's working! However, I think we all agree that it is not possible to play Reversi on a floating board... That's why we're going to define an Anchor on which we can place our Game / Board.

Placing Objects in Real-World

In Augmented Reality terminology, the concept of attaching virtual objects to a real-world point is called Anchoring. According to that word, Anchors are used to achieve this task.

Anchors are vertical or horizontal planes, or images (often markers) found in the real world by the AR system (ARCore or ARKit) on which we can rely to build a virtual world.

With Viro, Anchors are represented by an Anchor object which can be found through Targets using different detection methods, as described below.

  • ViroARPlane: This component allows to use either "manual" (though an "anchorId") or "automatic" detection of a plane in the real-world to place objects on it.
  • ViroARPlaneSelector: This component shows all the available planes discovered by the system and allows the user to select one.
  • ViroARImageMarker: This component allows to use an illustrated piece of paper as a physic anchor for our virtual objects.

In my case, I've chosen the ViroARImageMarker anchoring system because it seems more stable and performs better (at first glance).

ViroARImageMarker has a mandatory prop called target. This prop which must contain the name of a registered target which has previously been declared using ViroARTrackingTargets module.

The first thing to do is to create our target using the createTargets function. In our case, we declare an image target named marmelabAnchor (yes, I'm very corporate...) because I used the Marmelab logo as an anchor.

Then, we can use this anchor name directly as anchor prop value of our new ViroARImageMarker element around our 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

All children that are declared under the ViroARImageMarker element in the tree are placed relatively to it. In our case, the Game component is then placed over the ViroARImageMarker target.

Animating The Scene

Now the AR reversi game is working better. But it lacks a little bit of animation. So, how can we add the same disk flip effects as we made in our previous ThreeJS project?

Disk Flip

To fill this usual need, ViroReact provides a global animation registry called ViroAnimations that can be used everywhere in conjunction with any component that accepts an animation prop.

In our case, we're gonna compose transformations together to create a complete disk flipping effect. Here is the desired scenario over time:

0 - 300ms Move Up
300 - 600ms Move Down
150 - 350ms Rotate (during disk reaches the top)

First, we're gonna register an animation according to this transformation timeline.

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

As you see, we declare 3 distinct animations, and compose them using the fourth one, flipDisk. moveUp and moveDown are in the same array because they are executed one after the other. flip runs in parallel to these two transformations.

Secondly, we just need to use this registered animation in our Disk component using the animation prop, as follows:

    // ...

    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

The animation prop accepts an object of the following structure:

{
    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

In our case, we've just used name, run, and onFinish attributes to define which disk is currently flipping, and remove it from the flipping list when the animation ends.

Conclusion

Using ViroReact for building an Augmented Reality project was a great choice for many reasons. Whereas it was my first experience in this domain, I haven't faced any difficulties at any time. Quite the contrary, Viro has helped me to explore this world with confidence.

The developer experience is rich as it offers ReactJS binding, hot-reload and unambiguous documentation. Nevertheless, I don't recommend to use it for complex / performance-based applications because of the React-Native javascript thread which can lead to event congestion and lags. So, in case performance matters, I'd recommend full-native solutions instead.

By the way, Google is constantly adding augmented reality features within its applications, like on Google Map. Augmented Reality has never been so expanding. So, don't miss it.

Many other features remain to be explored, such as Skeletal animations, particles effects, physics, video and sounds. Don't be shy, share your experiences though comments ;)

You can find the final code on GitHub, in the marmelab/virothello repository.

Discussion

pic
Editor guide