This blog post was written under the Pusher Guest Writer program. React VR is a library that allows you to write virtual reality apps for the web using JavaScript and React on top of the WebVR API.
This blog post was written under the Pusher Guest Writer program.
React VR is a library that allows you to write virtual reality apps for the web using JavaScript and React on top of the WebVR API. This specification is now supported by the latest (or in some cases experimental) versions of browsers such as Chrome, Firefox, and Edge, and you don’t need to have a headset to access VR in a browser.
WebVR Experiments is a site that showcases some projects that show you what’s possible with WebVR. One project that caught my attention was The Musical Forest, made by the awesome people of Google Creative Lab using A-Frame, a web framework for WebVR developed by the Mozilla VR team.
In the Musical Forest, hitting a shape triggers a note, and using WebSockets, people can play music together in realtime. However, due to all the features and technologies used, the app is somewhat complicated (you can find the source code here). So, why not create a similar realtime React VR app with multi-user support using Pusher?
Here’s how the React VR/Pusher version looks:
A user can enter a channel identifier in the URL. When a 3D shape is hit, a sound will play and a Pusher event will be published so other users in the same channel can receive the event and the app can play that sound too.
We’ll have a Node.js backend to publish the events, so you should be familiar with JavaScript and React. If you’re not very familiar with some VR concepts or React VR, here’s a guide to get you started.
For reference (or if you just want to try the app), you can find the React VR project here and the Node.js backend here.
Let’s start by installing (or updating) the React VR CLI tool:
1npm install -g react-vr-cli
Next, create a new React VR project:
1react-vr init musical-exp-react-vr-pusher
Now go to the directory it created and execute the following command to start the development server:
1cd musical-exp-react-vr-pusher 2 npm start
In your browser, go to http://localhost:8081/vr/. Something like the following will show up:
If you’re using a compatible browser (like Firefox Nightly on Windows), you should also see the View in VR button to view the app with a headset:
Now let’s start coding our app.
We’re going to use an equirectangular image as the image background. The main characteristic of this type of images is that the width must be exactly twice the height, so open your favorite image editing software and create an image of size 4096×2048 with a gradient or color of your choice:
Create a new folder called images
inside the static_assets
directory in the root of your app and save your image there. Now open the file index.vr.js
and replace the content of the render
method with the following:
1render() { 2 return ( 3 <View> 4 <Pano source={asset('images/background.jpg')} /> 5 </View> 6 ); 7 }
When you reload the page (or if you enable hot reloading), you should see something like this:
Now, to simulate a tree, we’re going to use a Cylinder. In fact, we’ll need a hundred of this to create a forest around the user. In the original Musical Forest, we can find the algorithm to generate the trees around the users in the js/components/background-objects.js file. If we adapt the code into a React component for our project, we can get this:
1import React from 'react'; 2 import { 3 View, 4 Cylinder, 5 } from 'react-vr'; 6 7 export default ({trees, perimeter, colors}) => { 8 const DEG2RAD = Math.PI / 180; 9 10 return ( 11 <View> 12 {Array.apply(null, {length: trees}).map((obj, index) => { 13 const theta = DEG2RAD * (index / trees) * 360; 14 const randomSeed = Math.random(); 15 const treeDistance = randomSeed * 5 + perimeter; 16 const treeColor = Math.floor(randomSeed * 3); 17 const x = Math.cos(theta) * treeDistance; 18 const z = Math.sin(theta) * treeDistance; 19 20 return ( 21 <Cylinder 22 key={index} 23 radiusTop={0.3} 24 radiusBottom={0.3} 25 dimHeight={10} 26 segments={10} 27 style={{ 28 color: colors[treeColor], 29 opacity: randomSeed, 30 transform: [{scaleY : 2 + Math.random()}, {translate: [x, 3, z]},], 31 }} 32 /> 33 ); 34 })} 35 </View> 36 ); 37 }
This functional component takes three parameters:
trees
, which indicates the number of trees the forest will haveperimeter
, a value to control how far the trees will be rendered from the usercolors
, an array with values of colors for the trees.Using Array.apply(null, {length: trees})
, we can create an array of empty values to which we can apply the map function to render an array of cylinders with random colors, opacities and positions inside a View component.
We can save this code in a file called Forest.js
inside a components
directory and use it inside of index.vr.js
in the following way:
1... 2 import Forest from './components/Forest'; 3 4 export default class musical_exp_react_vr_pusher extends React.Component { 5 render() { 6 return ( 7 <View> 8 <Pano source={asset('images/background.jpg')} /> 9 10 <Forest trees={100} perimeter={15} colors={['#016549', '#87b926', '#b1c96b']} 11 /> 12 </View> 13 ); 14 } 15 }; 16 17 ...
In the browser, you should see something like this:
Great, our background is complete, now let’s add the 3D objects to play the sounds.
We are going to have six 3D shapes and each will play six different sounds when clicked. Also, a little animation when the cursor enters and exits the shape will come in handy.
To do that, we’ll need a VrButton, an Animated.View, and a Box, a Cylinder, and a Sphere for the shapes. However, as each shape is going to be different, let’s just encapsulate in a component what is the same. Save the following code in the file components/SoundShape.js
:
1import React from 'react'; 2 import { 3 VrButton, 4 Animated, 5 } from 'react-vr'; 6 7 export default class SoundShape extends React.Component { 8 9 constructor(props) { 10 super(props); 11 this.state = { 12 bounceValue: new Animated.Value(0), 13 }; 14 } 15 16 animateEnter() { 17 Animated.spring( 18 this.state.bounceValue, 19 { 20 toValue: 1, 21 friction: 4, 22 } 23 ).start(); 24 } 25 26 animateExit() { 27 Animated.timing( 28 this.state.bounceValue, 29 { 30 toValue: 0, 31 duration: 50, 32 } 33 ).start(); 34 } 35 36 render() { 37 return ( 38 <Animated.View 39 style={{ 40 transform: [ 41 {rotateX: this.state.bounceValue}, 42 ], 43 }} 44 > 45 <VrButton 46 onEnter={()=>this.animateEnter()} 47 onExit={()=>this.animateExit()} 48 > 49 {this.props.children} 50 </VrButton> 51 </Animated.View> 52 ); 53 } 54 };
When the cursor enters the button area, Animated.spring
will change the value of this.state.bounceValue
from 0
to 1
and show a bouncy effect. When the cursor exits the button area, Animated.timing
will change the value of this.state.bounceValue
from 1
to 0
in 50
milliseconds. For this to work, we wrap the VrButton
with an Animated.View
component that will change the rotateX
transform of the View
on each state change.
In index.vr.js
, we can add a SpotLight
(you can add any other type of light you want or change its properties) and use the SoundShape
component to add a cylinder this way:
1... 2 import { 3 AppRegistry, 4 asset, 5 Pano, 6 SpotLight, 7 View, 8 Cylinder, 9 } from 'react-vr'; 10 import Forest from './components/Forest'; 11 import SoundShape from './components/SoundShape'; 12 13 export default class musical_exp_react_vr_pusher extends React.Component { 14 render() { 15 return ( 16 <View> 17 ... 18 19 <SpotLight intensity={1} style={{transform: [{translate: [1, 4, 4]}],}} /> 20 21 <SoundShape> 22 <Cylinder 23 radiusTop={0.2} 24 radiusBottom={0.2} 25 dimHeight={0.3} 26 segments={8} 27 lit={true} 28 style={{ 29 color: '#96ff00', 30 transform: [{translate: [-1.5,-0.2,-2]}, {rotateX: 30}], 31 }} 32 /> 33 </SoundShape> 34 </View> 35 ); 36 } 37 }; 38 ...
Of course, you can change the properties of the shapes or even replace them with 3D models.
Let’s also add a pyramid (which is a cylinder with a zero op radius and four segments):
1<SoundShape> 2 <Cylinder 3 radiusTop={0} 4 radiusBottom={0.2} 5 dimHeight={0.3} 6 segments={4} 7 lit={true} 8 style={{ 9 color: '#96de4e', 10 transform: [{translate: [-1,-0.5,-2]}, {rotateX: 30}], 11 }} 12 /> 13 </SoundShape>
A cube:
1<SoundShape> 2 <Box 3 dimWidth={0.2} 4 dimDepth={0.2} 5 dimHeight={0.2} 6 lit={true} 7 style={{ 8 color: '#a0da90', 9 transform: [{translate: [-0.5,-0.5,-2]}, {rotateX: 30}], 10 }} 11 /> 12 </SoundShape>
A box:
1<SoundShape> 2 <Box 3 dimWidth={0.4} 4 dimDepth={0.2} 5 dimHeight={0.2} 6 lit={true} 7 style={{ 8 color: '#b7dd60', 9 transform: [{translate: [0,-0.5,-2]}, {rotateX: 30}], 10 }} 11 /> 12 </SoundShape>
A sphere:
1<SoundShape> 2 <Sphere 3 radius={0.15} 4 widthSegments={20} 5 heightSegments={12} 6 lit={true} 7 style={{ 8 color: '#cee030', 9 transform: [{translate: [0.5,-0.5,-2]}, {rotateX: 30}], 10 }} 11 /> 12 </SoundShape>
And a triangular prism:
1<SoundShape> 2 <Cylinder 3 radiusTop={0.2} 4 radiusBottom={0.2} 5 dimHeight={0.3} 6 segments={3} 7 lit={true} 8 style={{ 9 color: '#e6e200', 10 transform: [{translate: [1,-0.2,-2]}, {rotateX: 30}], 11 }} 12 /> 13 </SoundShape>
After you add the necessary imports, save the file and refresh your browser. You should see something like this:
Now let’s add some sounds!
For audio, React VR supports wav
, mp3
, and ogg
files, among others. You can find the complete list here.
You can go to Freesound or other similar sites to get some sound files. Download the ones you like and place them in the directory static_assets/sounds
. For this project, we’re going to use six animal sounds, something like a bird, another bird, another bird, a cat, a dog, and a cricket (as a quick note, I had to re-save this file lowering its bitrate so it can be played by React VR).
For our purposes, React VR give us three options to play a sound:
However, only the Sound component supports 3D/positional audio so the left and right balance of the sound will change as the listener moves around the scene or turns their head. So let’s add it to our SoundShape
component along with an onClick
event to the VrButton
:
1... 2 import { 3 ... 4 Sound, 5 } from 'react-vr'; 6 7 export default class SoundShape extends React.Component { 8 ... 9 render() { 10 return ( 11 <Animated.View 12 ... 13 > 14 <VrButton 15 onClick={() => this.props.onClick()} 16 ... 17 > 18 ... 19 </VrButton> 20 <Sound playerState={this.props.playerState} source={this.props.sound} /> 21 </Animated.View> 22 ); 23 } 24 }
We’re going to use a MediaPlayerState to control the playing of the sound. Both will be passed as properties of the component.
This way, let’s define an array with this information in index.vr.js
:
1... 2 import { 3 ... 4 MediaPlayerState, 5 } from 'react-vr'; 6 ... 7 8 export default class musical_exp_react_vr_pusher extends React.Component { 9 10 constructor(props) { 11 super(props); 12 13 this.config = [ 14 {sound: asset('sounds/bird.wav'), playerState: new MediaPlayerState({})}, 15 {sound: asset('sounds/bird2.wav'), playerState: new MediaPlayerState({})}, 16 {sound: asset('sounds/bird3.wav'), playerState: new MediaPlayerState({})}, 17 {sound: asset('sounds/cat.wav'), playerState: new MediaPlayerState({})}, 18 {sound: asset('sounds/cricket.wav'), playerState: new MediaPlayerState({})}, 19 {sound: asset('sounds/dog.wav'), playerState: new MediaPlayerState({})}, 20 ]; 21 } 22 23 ... 24 }
And a method to play a sound using the MediaPlayerState object when the right index is passed:
1... 2 3 export default class musical_exp_react_vr_pusher extends React.Component { 4 5 ... 6 7 onShapeClicked(index) { 8 this.config[index].playerState.play(); 9 } 10 11 ... 12 }
Now, we only need to pass all this information to our SoundShape components. So let’s group our shapes in an array and use a map function to generate the components:
1... 2 3 export default class musical_exp_react_vr_pusher extends React.Component { 4 5 ... 6 7 render() { 8 const shapes = [ 9 <Cylinder 10 ... 11 />, 12 <Cylinder 13 ... 14 />, 15 <Box 16 ... 17 />, 18 <Box 19 ... 20 />, 21 <Sphere 22 ... 23 />, 24 <Cylinder 25 ... 26 /> 27 ]; 28 29 return ( 30 <View> 31 ... 32 33 {shapes.map((shape, index) => { 34 return ( 35 <SoundShape 36 onClick={() => this.onShapeClicked(index)} 37 sound={this.config[index].sound} 38 playerState={this.config[index].playerState}> 39 {shape} 40 </SoundShape> 41 ); 42 })} 43 44 </View> 45 ); 46 } 47 48 ... 49 }
If you restart your browser and try, you should hear the sounds as you click on the shapes.
Now let’s add to our React VR app multi-user support in realtime with Pusher.
Create a free account at https://pusher.com/signup.
When you create an app, you ’ll be asked to enter some configuration options:
Enter a name, choose React as your front-end tech, and Node.js as the back-end tech. This will give you some sample code to get you started:
But don’t worry, this won’t lock you into this specific set of technologies as you can always change them. With Pusher, you can use any combination of libraries.
Next, copy your cluster ID (next to the app title, in this example mt1
), App ID, Key, and Secret information as we’ll need them next. You can also find them in the App Keys tab.
React VR acts as a Web Worker (you can know more about React VR architecture in this video) so we need to include Pusher’s worker script in index.vr.js
this way:
1... 2 importScripts('https://js.pusher.com/4.1/pusher.worker.min.js'); 3 4 export default class musical_exp_react_vr_pusher extends React.Component { 5 ... 6 }
We have two requirements that need to be taken care of. First, we need to be able to pass an identifier through the URL (like http://localhost:8081/vr/?channel=1234
) so users can choose which channel they want to be in and share it with their friends.
To address this, we need to read the URL. Luckily, React VR comes with the native module Location, which makes available to the React context the properties of the object window.location
.
Next, we need to make a call to a server that will publish the Pusher event so all the connected clients can also play the event. However, we don’t want the client that broadcasts the event to receive it too, because in that case, the sound will be played twice, and there’s no point in waiting to receive the event to play the sound when you can play it immediately when the user clicks the shape.
Each Pusher connection is assigned a unique socket ID. To exclude recipients from receiving events in Pusher, we just need to pass to the server the socket ID of the client we want to be excluded a socket_id
when this is triggering an event. (You can find more information here.)
This way, adapting a little bit a function (getParameterByName
) to read the parameters of the URL, and saving the socketId
when a successful connection is made to Pusher, we can address both requirements with this:
1... 2 import { 3 ... 4 NativeModules, 5 } from 'react-vr'; 6 ... 7 const Location = NativeModules.Location; 8 9 export default class musical_exp_react_vr_pusher extends React.Component { 10 componentWillMount() { 11 const pusher = new Pusher('<INSERT_PUSHER_APP_KEY>', { 12 cluster: '<INSERT_PUSHER_APP_CLUSTER>', 13 encrypted: true, 14 }); 15 this.socketId = null; 16 pusher.connection.bind('connected', () => { 17 this.socketId = pusher.connection.socket_id; 18 }); 19 this.channelName = 'channel-' + this.getChannelId(); 20 const channel = pusher.subscribe(this.channelName); 21 channel.bind('sound_played', (data) => { 22 this.config[data.index].playerState.play(); 23 }); 24 } 25 26 getChannelId() { 27 let channel = this.getParameterByName('channel', Location.href); 28 if(!channel) { 29 channel = 0; 30 } 31 32 return channel; 33 } 34 35 getParameterByName(name, url) { 36 const regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"); 37 const results = regex.exec(url); 38 if (!results) return null; 39 if (!results[2]) return ''; 40 return decodeURIComponent(results[2].replace(/\+/g, " ")); 41 } 42 43 ... 44 }
If there isn’t a channel parameter in the URL, by default we assign the ID 0. This ID will be added to the Pusher channel to make it unique.
Finally, we just need to call an endpoint on the server side that will publish the event, passing the socket ID of the client and the channel where we’ll be publishing events:
1... 2 export default class musical_exp_react_vr_pusher extends React.Component { 3 ... 4 onShapeClicked(index) { 5 this.config[index].playerState.play(); 6 fetch('http://<INSERT_YOUR_SERVER_URL>/pusher/trigger', { 7 method: 'POST', 8 headers: { 9 'Accept': 'application/json', 10 'Content-Type': 'application/json', 11 }, 12 body: JSON.stringify({ 13 index: index, 14 socketId: this.socketId, 15 channelName: this.channelName, 16 }) 17 }); 18 } 19 ... 20 }
And that’s all the code of the React part. Now let’s take a look at the server.
Execute the following command to generate a package.json
file:
1npm init -y
Add the following dependencies:
1npm install --save body-parser express pusher
And save the following code in a file:
1const express = require('express'); 2 const bodyParser = require('body-parser'); 3 const Pusher = require('pusher'); 4 5 const app = express(); 6 app.use(bodyParser.json()); 7 app.use(bodyParser.urlencoded({ extended: false })); 8 /* 9 The following headers are needed because the development server of React VR 10 is started on a different port than this server. 11 When the final project is published, you may not need this middleware 12 */ 13 app.use((req, res, next) => { 14 res.header("Access-Control-Allow-Origin", "*") 15 res.header("Access-Control-Allow-Headers", 16 "Origin, X-Requested-With, Content-Type, Accept") 17 next(); 18 }); 19 20 const pusher = new Pusher({ 21 appId: '<INSERT_PUSHER_APP_ID>', 22 key: '<INSERT_PUSHER_APP_KEY>', 23 secret: '<INSERT_PUSHER_APP_SECRET>', 24 cluster: '<INSERT_PUSHER_APP_CLUSTER>', 25 encrypted: true, 26 }); 27 28 app.post('/pusher/trigger', function(req, res) { 29 pusher.trigger(req.body.channelName, 30 'sound_played', 31 { index: req.body.index }, 32 req.body.socketId ); 33 res.send('ok'); 34 }); 35 36 const port = process.env.PORT || 5000; 37 app.listen(port, () => console.log(`Running on port ${port}`));
As you can see, here we set up an Express server, the Pusher object, and the route /pusher/trigger
, which just triggers an event with the index of the sound to be played and the socketID to exclude the recipient of the event.
And we’re done. Let’s test it.
Execute the Node.js backend with:
1node server.js
Update your server URL in index.vr.js
(with your IP instead of localhost
) and enter in your browser an address like http://localhost:8081/vr/?channel=1234 in two browser windows. When you click on a shape, you should hear the sound played twice (of course, it’s more fun doing this with a friend in another computer):
React VR is a great library to create virtual reality experiences in an easy way, especially if you already know React/React Native. Pair it with Pusher and you’ll have powerful tools to program the next generation of web applications.
You can build a production release of this project to deploy it in any web server by following the steps on this page.
Also, you can customize this code by changing the colors, the shapes, the sounds, or add more functionality from the original Musical Forest.
Finally, remember that you can find the code of the app in this GitHub repository.
Are you lost with VR development? Check it out Pusher’s guide on how you can become an AR/VR developer