Heading image for post: Playing with React VR

Playing with React VR

Profile picture of Gabriel Reis

The React concept of "Learn once, write anywhere" is virtually real, I mean, it is real!

After attending a local React meetup here in Jacksonville, I decided to try React VR. I've been doing React Native for almost two years now, and almost the same APIs are available in React VR, so the learning curve for me was super low.

So if you've been doing React or React Native for the past months, you'll see that React VR is super simple to get started and will let you build exciting 360 experiences.

To get started with React VR you have to install the CLI:

npm install -g react-vr-cli

After that let's create a new project:

react-vr init HashrocketVR

Now, let's go to the created directory and start the server:

cd HashrocketVR
npm start

Now open your browser:

open http://localhost:8081/vr/

You should see this page:

Open scene in new tab for full 360 experience

Awesome. Move your mouse, and you will be able to rotate the scene. You should also try and open that url from your phone. React VR can access the accelerometer and gyroscope sensors from your phone and change the scene around.

Now let's try to understand what is happening inside the index.vr.js file that was generated:

  import {
    AppRegistry,
    asset,
    Pano,
    Text,
    View,
  } from 'react-vr';

  export default class HashrocketVR extends React.Component {
    render() {
      return (
        <View>
          <Pano source={asset('chess-world.jpg')}/>
          <Text
            style={{
              backgroundColor: '#777879',
              fontSize: 0.8,
              fontWeight: '400',
              layoutOrigin: [0.5, 0.5],
              paddingLeft: 0.2,
              paddingRight: 0.2,
              textAlign: 'center',
              textAlignVertical: 'center',
              transform: [{translate: [0, 0, -3]}],
            }}>
            hello
          </Text>
        </View>
      );
    }
  };

  AppRegistry.registerComponent('HashrocketVR', () => HashrocketVR);

If you are familiar with React Native, you recognize the Text and View components.

The Pano component is responsible for displaying a panoramic image inside the scene. This image needs to be in a particular format called Equirectangular so that component can render a 360 image perfectly.

React VR uses the OpenGL coordinate system. The coordinate (x: 0, y: 0, z: 0) is the viewer's camera in the world. Axis X is horizontal, Y is vertical, and Z is depth. The unit dimension is in meters, not pixels.

To change the position of the objects within the scene, you can apply some transformations.

If you look at the style for the Text component, it is setting thetransform property to {translate: [0, 0, -3]. It's positioning that component to be 3 meters away from the viewer.

Great. Let's do some modifications here now. How about we display a 3D Hashrocket logo instead of the Hello text.

React VR comes with a Model component where you can use any 3D model in the obj format.
(We don't have our logo in that format, but we have in SVG. I used this site svg2stl.com to generate a .stl file, import it on Blender and then export it to the obj format.)

You can put the .obj file inside the static_assets folder and render the Model component like this:

import {
  ...
  Model,
} from 'react-vr';

render() {
  return (
    <View>
      <Pano source={asset('chess-world.jpg')}/>
      <Model
        source={{
          obj: asset('hashrocket.obj')
        }}
        style={{ 
          color: '#af1e23',
          transform: [
            {translate: [0, -3, -12]}
          ]
        }}
      />
    </View>
  );
}

Notice that we had to apply a transformation on the Model component in order to position it in front of the viewer. We also specified the color to paint the 3D model correctly.

This is how it should look like:

Open scene in new tab for full 360 experience

How about we add some animation to make the logo rotate in the Y axis. React VR uses the same Animated API used in React Native. First thing we have to do is to create an animated component based on Model:

import {
  ...
  Animated
} from 'react-vr';

const AnimatedModel = Animated.createAnimatedComponent(Model);

We have to do that because the animated API only ships with animated versions of primitive components like Text, View and Image.

Now we just replace the Model component with the AnimatedModel:

render() {
  return (
      ...
      <AnimatedModel
        source={{
          obj: asset('hashrocket.obj')
        }}
        style={{
          color: '#af1e23',
          transform: [
            {translate: [0, -3, -12]},
          ]
        }}
      />
      ...
  );
}

Now we need to use the Animated API to perform the animation. First thing we do is to keep an instance of Animated.Value in our state so it can be used in the AnimatedModel style to set the rotateY transformation:

export default class HashrocketVR extends React.Component {
  state = {
    rotation: new Animated.Value(0)
  }

  render() {
    return (
      ...
        <AnimatedModel
          style={{
            transform: [
              {translate: [0, -3, -12]},
              {rotateY: this.state.rotation}
            ]
          }}
      ...
    );
  }
};

Now we create a function that will use the timing animation to do a 360 rotation in 10 seconds:

export default class HashrocketVR extends React.Component {
  ...

  rotate = () => {
    this.state.rotation.setValue(0);
    Animated.timing(
      this.state.rotation,
      {
        toValue: 360,
        duration: 10000,
      }
    ).start(this.rotate);
  }

  ...
};

Notice that we are passing the same function as the first argument of the start function. This will make it recursive, and the animation will never stop.

Now we can call that function on componentDidMount so it will be triggered right when the component is rendered on the screen:

export default class HashrocketVR extends React.Component {
  ... 
  componentDidMount() {
    this.rotate();
  }
  ...
}

This is how the whole component should look like:

export default class HashrocketVR extends React.Component {
  state = {
    rotation: new Animated.Value(0)
  }

  componentDidMount() {
    this.rotate();
  }

  rotate = () => {
    this.state.rotation.setValue(0);
    Animated.timing(
      this.state.rotation,
      {
        toValue: 360,
        duration: 10000,
      }
    ).start(this.rotate);
  }

  render() {
    return (
      <View>
        <Pano source={asset('chess-world.jpg')}/>
        <AnimatedModel
          source={{
            obj: asset('hashrocket.obj')
          }}
          style={{
            color: '#af1e23',
            transform: [
              {translate: [0, -3, -12]},
              {rotateY: this.state.rotation}
            ]
          }}
        />
      </View>
    );
  }
};

And this is the actual component in React VR:

Open scene in new tab for full 360 experience

If you pay attention to the animation right before the logo is finishing a 360 rotation you can notice that it reduces its speed. By default, the timing animation uses EaseIn/Out. You can disable that by setting a lineareasing property on the animation. You will also have to import Easing from react-native:

import {
  Easing
} from 'react-native';

rotate = () => {
  this.state.rotation.setValue(0);
  Animated.timing(
    this.state.rotation,
    {
      toValue: 360,
      duration: 10000,
      easing: Easing.linear,
    }
  ).start(this.rotate);
}

Great. Now that we have our logo rotating you can see that something is missing. In the real world, light plays an important role, and it changes the appearance of any objects. In a 3D virtualized world is the same thing. To have a more realistic UI, we need to add lights to our scene. React VR provides 4 different components for lights: AmbientLight, DirectionalLight, PointLight and SpotLight.

Let's change our component and add AmbientLight and PointLight:

 import {
   ...
    AmbientLight,
    PointLight,
  } from 'react-vr';

    render() {
      return (
        <View>
          <Pano source={asset('chess-world.jpg')}/>
          <AmbientLight intensity={0.5} />
          <PointLight 
            style={{
              color:'white', transform:[{translate:[0, 0, 0]}]
            }} 
          />
          <AnimatedModel
            lit
            source={{
              obj: asset('hashrocket.obj'),
            }}
            style={{
              color: '#af1e23',
              transform: [
                {translate: [0, -3, -12]},
                {rotateY: this.state.rotation}
              ]
            }}
          />
        </View>
      );
    }

Notice that we also had to set the prop lit on the AnimatedModel. That will enable that component to receive lights.

This is how the final scene looks like with the lights:

Open scene in new tab for full 360 experience

Wow!!! The lights changed the logo, and now you can clearly see the edges of it. That is awesome!!!

Done. Our 3D scene is complete. But before we finish you should know that React VR can bundle all your js files so you can upload to a static page or embed as an iframe (same as I did here with the examples). Just run this in the command line:

npm run bundle

The files will be generated inside vr/build.

That is it for this blog post. The source code for the whole example app is available here. Hope you got inspired to try React VR and create some awesome 360 experiences.

React VR also comes with other interesting components that I haven't played with like VrButton, Sound, VideoPano. Make sure to read the docs.

React is a great library. When you learn React you can write apps for various platforms like Web, iOS, Android, Apple TV, Windows and now VR. The future for React is brilliant, can't wait to see what platform is next.

More posts about react react-native