React VR aims to allow web developers to author virtual reality (VR) applications using the declarative approach of React and, in particular, React Native.
React VR is similar to React Native in that it uses View
, Image
, and Text
as core components and supports Flexbox layouts. In addition, React VR adds VR components like Pano
, Mesh
, and PointLight
into the mix.
In this guide, we'll create a simple VR app to learn how to create a scene with a panorama image, 3d objects, buttons, and a flexbox layout. Our mock app is based on two of the official samples of React VR and the Mesh and Layout samples.
The app will render 3D models of the Earth and the Moon in a cubemap of space along with some buttons to zoom in and zoom out. This is how the finished app looks like:
The models, their scale, and their rotation are not true replicas of the Earth-Moon system. This relationship is just to demonstrate how React VR works. Along the way, we'll explain some key 3D modeling concepts. Once you've achieved mastery with ReactVR, feel free to create more pieces; make it to scale, add in the Sun, and create more planets!
You can find the source code of the final app on GitHub.
At the time of writing, Virtual Reality is a rather new technology, and there are few ways to develop or test our VR apps.
WebVR and Is WebVR Ready? can help you know what browser and devices support the latest VR specs.
But don't worry, you don't need any special device, such as Oculus Rift, HTC Vive, or Samsung Gear VR to try a WebVR app right now.
Here's what you do need:
There are some builds of Chrome that support the WebVR API, and even an extension that emulates it, but you'll get the best support with Firefox Nightly and Windows.
If you happen to also have an Android device and a Gear VR headset, you can install the Carmel Developer Preview browser to explore your React VR app through your headset.
First, we need to install the React VR CLI tool. Use the NPM:
1npm install -g react-vr-cli
Using the React VR CLI, we can create a new application, let's say EarthMoonVR
:
1react-vr init EarthMoonVR
This will create an EarthMoonVR
directory with a sample application inside, also installing the required dependencies so it can take a while. Installing Yarn will speed things up.
(See this guide for more on Yarn.)
Once the install is ready, cd
into that directory:
1cd EarthMoonVR
To test the sample app, you can execute a local development server with:
1npm start
Open your browser at http://localhost:8081/vr
. It can take some time to build and initialize the app, but at the end, you should see the following:
The directory of this sample app has the following structure:
1+-__tests__
2+-node_modules
3+-static_assets
4+-vr
5\-.babelrc
6\-.flowconfig
7\-.gitignore
8\-.watchmanconfig
9\-index.vr.js
10\-package.json
11\-rn-cli-config.js
12\-yarn.lock
I want to highlight the index.vr.js
file, which contains your application code, and the static_assets
directory, which contains external resource files, like images and 3D models.
You can know more about the project structure here.
The following is the content of the index.vr.js
file:
1import React from "react";
2import { AppRegistry, asset, StyleSheet, Pano, Text, View } from "react-vr";
3
4class EarthMoonVR extends React.Component {
5 render() {
6 return (
7 <View>
8 <Pano source={asset("chess-world.jpg")} />
9 <Text
10 style={{
11 backgroundColor: "blue",
12 padding: 0.02,
13 textAlign: "center",
14 textAlignVertical: "center",
15 fontSize: 0.8,
16 layoutOrigin: [0.5, 0.5],
17 transform: [{ translate: [0, 0, -3] }]
18 }}
19 >
20 hello
21 </Text>
22 </View>
23 );
24 }
25}
26
27AppRegistry.registerComponent("EarthMoonVR", () => EarthMoonVR);
The code is pre-processed by the React Native packager, which provides compilation (ES2015, JSX), bundling, and asset loading among other things.
In the return
statement of the render
function, there's a:
View
component, which is typically used as a container for other components.Pano
component, which renders a 360 photo (chess-world.jpg
) that forms our world environment.Text
component, which renders strings in a 3D space.Notice how the Text
component is styled with a style object. Every component in React VR has a style
attribute that can be used to control its look and layout.
Aside from the addition of some special components like Pano
or VrButton
, React VR works with the same concepts of React and React Native, such as components, props, state, lifecycle methods, events, and flexbox layouts.
Finally, the root component should register itself with AppRegistry.registerComponent
, so the app can be bundled and run.
Now that we know what our code does, we can dive into panorama images.
Generally, the world inside our VR app is composed by a panorama (pano) image, which creates a sphere of 1000 meters (in React VR distance and dimensional units are in meters) and places the user at its center.
A pano image allows you to see the image from every angle including above, below, behind and next to you, that's the reason they are also called 360 images or spherical panoramas.
There are two main formats of 360 panoramas: equirectangular and cubic. React VR supports both.
An equirectangular pano consists of a single image with an aspect ratio of 2:1, meaning that the width must be twice the height.
These images are created with a special 360 camera. An excellent source of equirectangular images is Flickr, you just need to search for the equirectangular
tag. For example, by trying this search, I found this photo:
Looks weird, doesn't it?
Anyway, download the photo (at the highest resolution available) to the static_assets
directory of our project and change the render
function to show it:
1render() {
2 return (
3 <View>
4 <Pano source={asset('sample_pano.jpg')}/>
5 </View>
6 );
7 }
The source
attribute of the Pano
component takes an object with an uri
property with the location of the image. Here we are using the asset
function that will look for the sample_pano.jpg
file in the static_assets
directory and return and object with the correct path in the uri
property. In other words, the above code is equivalent to:
1render() {
2 return (
3 <View>
4 <Pano source={ {uri:'../static_assets/sample_pano.jpg'} }/>
5 </View>
6 );
7}
When we refresh the page in the browser (and assuming the local server is still running), we should see something like this:
By the way, if we want to avoid refreshing the page at every change, we can enable hot reloading by appending ?hotreload
to the URL (http://localhost:8081/vr/?hotreload).
Cubemaps are the other format of 360 panoramas. This format uses six images for the six faces of a cube that will fill the sphere around us. It's also known as a skybox.
The basic idea is to render a cube and place the viewers at the center, following them as they move.
For example, consider this image that represents the sides of a cube:
To use this cubemap in React VR, the source attribute of the Pano
component must be specified as an array of six individual images presented in the order [+x, -x, +y, -y, +z, -z]
. Something like this:
1render() {
2 return (
3 <View>
4 <Pano source={
5 {
6 uri: [
7 '../static_assets/sample_right.jpg',
8 '../static_assets/sample_left.jpg',
9 '../static_assets/sample_top.jpg',
10 '../static_assets/sample_bottom.jpg',
11 '../static_assets/sample_back.jpg',
12 '../static_assets/sample_front.jpg'
13 ]
14 }
15 } />
16 </View>
17 );
18}
In 2D layouts, the X-axis points to the right and the Y-axis points down, which means that the top left is (0, 0) and the bottom right will be the width and the height of the element at (width, height).
However, in a 3D space, React VR uses the same right-handed coordinate system that OpenGL uses, with positive X pointing to the right, positive Y pointing up, and positive Z pointing forwards towards the user. Because the default view of the user starts out at the origin, this means they'll start out looking in the negative Z direction:
You can read more about the React VR coordinate system here.
This way, our cubicmap (or skybox) will look like this:
Skyboxes are used a lot with Unity, so there are a lot of places where you can find them for download. For example, I downloaded one of the Sahara desert from this page. When I extract the images and change the code to:
1render() {
2 return (
3 <View>
4 <Pano source={
5 {
6 uri: [
7 '../static_assets/sahara_rt.jpg',
8 '../static_assets/sahara_lf.jpg',
9 '../static_assets/sahara_up.jpg',
10 '../static_assets/sahara_dn.jpg',
11 '../static_assets/sahara_bk.jpg',
12 '../static_assets/sahara_ft.jpg'
13 ]
14 }
15 } />
16 </View>
17 );
18}
This is the result:
Can you notice that the top and the bottom images don't fit quite right? We can correct them by rotating the top image 90 degrees clockwise and the bottom one 90 degrees counterclockwise:
Now let's create a space skybox for our app.
The best program to this is Spacescape, a free tool for creating space skyboxes (including stars and nebulas) that is available for Windows and Mac.
Using this configuration:
We can export the six images for the skybox:
The Export For option in the Export Skybox dialog just applies a particular naming convention, it doesn't produce different images.
If we change the code:
1<Pano
2 source={{
3 uri: [
4 "../static_assets/space_right.png",
5 "../static_assets/space_left.png",
6 "../static_assets/space_up.png",
7 "../static_assets/space_down.png",
8 "../static_assets/space_back.png",
9 "../static_assets/space_front.png"
10 ]
11 }}
12/>
This will be the result:
Now let's talk about 3D models.
React VR has a Model component that supports the Wavefront .obj file format to represent 3D models.
A mesh is a collection of vertices, edges, and faces that define the shape of a 3D object.
A .obj file is a plain text file that contains coordinates of geometric vertices, texture coordinates, vertex normals and polygonal face elements, among other things.
Typically, a .obj file references an external .mtl file where the materials (or textures) that describe the visual aspect of the polygons are stored.
There's also a lot of sites where you can download 3D models either for free or at a cost. The following are three of the best ones:
For our app, we're going to use this 3D Earth model and this 3D Moon model from TF3DM.
When we extract the files of the Earth model to the static_assets
directory of our app, we can see there's a bunch of images (the textures) along with the .obj and .mtl files. If we open the latter in a text editor, we'll see the material definitions:
1# 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware
2# File Created: 25.01.2016 02:22:51
3
4newmtl 01___Default
5 Ns 10.0000
6 Ni 1.5000
7 d 1.0000
8 Tr 0.0000
9 Tf 1.0000 1.0000 1.0000
10 illum 2
11 Ka 0.0000 0.0000 0.0000
12 Kd 0.0000 0.0000 0.0000
13 Ks 0.0000 0.0000 0.0000
14 Ke 0.0000 0.0000 0.0000
15 map_Ka C:\Documents and Settings\glenn\Desktop\erth\02\4096\4096_earth.jpg
16 map_Kd C:\Documents and Settings\glenn\Desktop\erth\02\4096\4096_earth.jpg
17 map_Ke C:\Documents and Settings\glenn\Desktop\erth\02\4096\4096_night_lights.jpg
18 map_bump C:\Documents and Settings\glenn\Desktop\erth\02\4096\4096_bump.jpg
19 bump C:\Documents and Settings\glenn\Desktop\erth\02\4096\4096_bump.jpg
20
21newmtl 02___Default
22 Ns 10.0000
23 Ni 1.5000
24 d 1.0000
25 Tr 0.0000
26 Tf 1.0000 1.0000 1.0000
27 illum 2
28 Ka 0.5882 0.5882 0.5882
29 Kd 0.5882 0.5882 0.5882
30 Ks 0.0000 0.0000 0.0000
31 Ke 0.0000 0.0000 0.0000
32 map_Ka C:\Documents and Settings\glenn\Desktop\erth\02\4096\4096_clouds.jpg
33 map_Kd C:\Documents and Settings\glenn\Desktop\erth\02\4096\4096_clouds.jpg
34 map_d C:\Documents and Settings\glenn\Desktop\erth\02\4096\4096_clouds.jpg
We need to remove the absolute paths to the images so our .obj file can find them. Since we are going to place both files in the same directory, the .mtl file should look like this:
1# 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware
2# File Created: 25.01.2016 02:22:51
3
4newmtl 01___Default
5 Ns 10.0000
6 Ni 1.5000
7 d 1.0000
8 Tr 0.0000
9 Tf 1.0000 1.0000 1.0000
10 illum 2
11 Ka 0.0000 0.0000 0.0000
12 Kd 0.0000 0.0000 0.0000
13 Ks 0.0000 0.0000 0.0000
14 Ke 0.0000 0.0000 0.0000
15 map_Ka 4096_earth.jpg
16 map_Kd 4096_earth.jpg
17 map_Ke 4096_night_lights.jpg
18 map_bump 4096_bump.jpg
19 bump 4096_bump.jpg
20
21newmtl 02___Default
22 Ns 10.0000
23 Ni 1.5000
24 d 1.0000
25 Tr 0.0000
26 Tf 1.0000 1.0000 1.0000
27 illum 2
28 Ka 0.5882 0.5882 0.5882
29 Kd 0.5882 0.5882 0.5882
30 Ks 0.0000 0.0000 0.0000
31 Ke 0.0000 0.0000 0.0000
32 map_Ka 4096_clouds.jpg
33 map_Kd 4096_clouds.jpg
34 map_d 4096_clouds.jpg
Now we can add the Model
component with the following code:
1<Model
2 source={{ obj: asset("earth.obj"), mtl: asset("earth.mtl") }}
3 lit={true}
4/>
The lit
attribute specifies that the materials used in the mesh should work with lights using Phong shading.
Also, don't forget to export the Model
component from react-vr
:
1import {
2 ...
3 Model,
4} from 'react-vr';
However, if we only add this component to our app, nothing will be shown. What we need, first, is to add a light source.
React VR has four types of light:
You can try all types of lights to see which one yields the best result for you. In this case, we are going to use an AmbientLight
with an intensity value of 2.6
:
1import React from "react";
2import {
3 AppRegistry,
4 asset,
5 StyleSheet,
6 Pano,
7 Text,
8 View,
9 Model,
10 AmbientLight
11} from "react-vr";
12
13class EarthMoonVR extends React.Component {
14 render() {
15 return (
16 <View>
17 ...
18 <AmbientLight intensity={2.6} />
19 <Model
20 source={{ obj: asset("earth.obj"), mtl: asset("earth.mtl") }}
21 lit={true}
22 />
23 </View>
24 );
25 }
26}
27
28AppRegistry.registerComponent("EarthMoonVR", () => EarthMoonVR);
Next, we need to give our model some style properties for placement, size, and rotation. By trying with different values, I came up with the following configuration:
1class EarthMoonVR extends React.Component {
2 render() {
3 return (
4 <View>
5 ...
6 <Model
7 style={{
8 transform: [
9 { translate: [-25, 0, -70] },
10 { scale: 0.05 },
11 { rotateY: -130 },
12 { rotateX: 20 },
13 { rotateZ: -10 }
14 ]
15 }}
16 source={{ obj: asset("earth.obj"), mtl: asset("earth.mtl") }}
17 lit={true}
18 />
19 </View>
20 );
21 }
22}
23
24AppRegistry.registerComponent("EarthMoonVR", () => EarthMoonVR);
Transforms are represented as an array of objects within a style object, and they are applied last to first (remember that the units are meters).
translate
positions your model in the x, y, z space, scale
gives your model a size, and rotate
turns your model around the axes based on the degree measurement provided.
This is the result:
This Earth model has more than one texture we can apply. It comes with the clouds texture by default, but we can change it in the .mtl file by replacing 4096_clouds.jpg
in the last three lines with 4096_earth.jpg
:
1# 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware
2# File Created: 25.01.2016 02:22:51
3
4newmtl 01___Default
5 ...
6
7newmtl 02___Default
8 ...
9 map_Ka 4096_earth.jpg
10 map_Kd 4096_earth.jpg
11 map_d 4096_earth.jpg
This is the result:
By the way, if your model doesn't come with a .mtl file, React VR allows you to specify a texture with:
1<Model
2 source={{ obj: asset("model.obj"), texture: asset("model.jpg") }}
3 lit={true}
4/>
We do the same with the Moon model, fixing the texture's path in the .mtl file and trying different values for the scale and placement. You don't need to add another source of light, AmbientLight
will work fine for both models.
Here's the code for the Moon model I came up with:
1render() {
2 return (
3 <View>
4
5 ...
6
7 <Model
8 style={{
9 transform: [
10 {translate: [10, 10, -100]},
11 {scale: 0.05},
12 ],
13 }}
14 source={{obj:asset('moon.obj'), mtl:asset('moon.mtl')}}
15 lit={true}
16 />
17 </View>
18 );
19 }
The result:
If you want to know a little more about 360 panoramas in the context of WebVR, check out the developer documentation at Oculus.
Now let's animate our models.
React VR has an Animated library to compose some types of animation in a simple way.
At this time, only a few components can be animated natively (View
with Animated.View
, Text
with Animated.Text
, and Image
with Animated.Image
). The documentation mentions that you can create custom ones with createAnimatedComponent
, but I couldn't find more about it.
Another option is to use requestAnimationFrame , an essential part of JavaScript-based animation APIs.
So what we can do is to have a state property to represent the rotation value on the Y-axis of both models (on the Moon model, let's make the rotation a third of the Earth's rotation to make it slower):
1class EarthMoonVR extends React.Component {
2 constructor() {
3 super();
4 this.state = {
5 rotation: 130
6 };
7 }
8
9 render() {
10 return (
11 <View>
12 ...
13 <Model
14 style={{
15 transform: [
16 { translate: [-25, 0, -70] },
17 { scale: 0.05 },
18 { rotateY: this.state.rotation },
19 { rotateX: 20 },
20 { rotateZ: -10 }
21 ]
22 }}
23 source={{ obj: asset("earth.obj"), mtl: asset("earth.mtl") }}
24 lit={true}
25 />
26 <Model
27 style={{
28 transform: [
29 { translate: [10, 10, -100] },
30 { scale: 0.05 },
31 { rotateY: this.state.rotation / 3 }
32 ]
33 }}
34 source={{ obj: asset("moon.obj"), mtl: asset("moon.mtl") }}
35 lit={true}
36 />
37 </View>
38 );
39 }
40}
Now let's code a rotate
function that will be called every frame through the requestAnimationFrame
function, updating the rotation on a time measurement basis:
1class EarthMoonVR extends React.Component {
2 constructor() {
3 super();
4 this.state = {
5 rotation: 130,
6 };
7 this.lastUpdate = Date.now();
8
9 this.rotate = this.rotate.bind(this);
10 }
11
12 rotate() {
13 const now = Date.now();
14 const delta = now - this.lastUpdate;
15 this.lastUpdate = now;
16
17 this.setState({
18 rotation: this.state.rotation + delta / 150
19 });
20 this.frameHandle = requestAnimationFrame(this.rotate);
21 }
22
23 ...
24}
The magic number 150
just controls the rotation speed (the greater this number, the slower the rotation speed). We save the handler returned by requestAnimationFrame
so we can cancel the animation when the component unmounts and start the rotation animation on componentDidMount
:
1class EarthMoonVR extends React.Component {
2 constructor() {
3 super();
4 this.state = {
5 rotation: 130,
6 };
7 this.lastUpdate = Date.now();
8
9 this.rotate = this.rotate.bind(this);
10 }
11
12 componentDidMount() {
13 this.rotate();
14 }
15
16 componentWillUnmount() {
17 if (this.frameHandle) {
18 cancelAnimationFrame(this.frameHandle);
19 this.frameHandle = null;
20 }
21 }
22
23 rotate() {
24 const now = Date.now();
25 const delta = now - this.lastUpdate;
26 this.lastUpdate = now;
27
28 this.setState({
29 rotation: this.state.rotation + delta / 150
30 });
31 this.frameHandle = requestAnimationFrame(this.rotate);
32 }
33
34 ...
35}
This is the result (you may not notice it, but the moon is rotating very slowly):
Now let's add some buttons to make this a little more interactive.
As you saw, React VR is a library that allows you to create VR experiences in a fast and easy way.
There are alternatives with a more complete feature set and bigger community, like A-Frame. However, if you just want to make VR apps around 360 panos, 3D models, and simple animations, and you already know React/React Native, then React VR is an excellent choice. If you found ReactVR to be a good springboard, feel free to experiment with more advanced VR platforms as well.
Remember that you can find the source code of the app on GitHub.
Thanks for reading!