A webVR with threeJS Tutorial

A webVR with threeJS Tutorial

posted in: Tutorials, webVR | 0

Two weeks ago at Art&&Code I ran an extremely introductory workshop on getting started creating webVR content with threeJS. Unlike the workshop that I ran at the Illustrating Mathematics conference earlier this year, this was more of a “workshop talk”, which meant that rather than introducing concepts and then having the time to go around and assist people as I have done in the past, this workshop was more of a non-interactive tutorial. To aid this rather different format, I made some rather detailed slides for people to use as reference later.

As an additional resource for people who attended or were not able to attend* my workshop, here is a walk-through of what we covered, so you can follow along at home. This tutorial assumes very little to no coding and development experience. Given how highly requested introductory webVR content has been from various conferences this year, I’m thinking that this information might be more broadly desirable.

You can find the slide deck, a demo, and code resources here. I highly recommend downloading them before you continue as I will be referencing the slide deck in this tutorial.

 

Getting Started (slides 15-18)

Before you can start doing any coding, you need some basic setup. A lot of this setup is just to save you the time and energy of coding from scratch resources that other people have already devoted a lot of time to creating and maintaining.

Slide 16 covers a few useful downloads. For this tutorial, the most important one is the webVR boilerplate. I am recommending Boris Smus’ boilerplate because it is kept updated. Assuming you have some kind of text/code editor and a browser already, you can definitely get away without the other resources until such a time as you want to test your code out with a real or emulated tethered VR headset like the HTC Vive. If you don’t have a good text/code editor, I highly recommend also downloading Sublime Text, which does nice Javascript syntax highlighting.

Slide 18 has links to resources for people who want to explore beyond the basics covered in this tutorial.

 

Making our VR World (slides 19-28)

 

> Setup (slide 20)

Slide 19 covers some useful setup steps, most of which I advise people to do because they may make things easier later should you decide to extend this project. I’m going to mostly not worry about these for this tutorial, but you may want to go back and do them later.

First, let’s make sure that the boilerplate that you downloaded is working. Open the index.html file in your browser. This boilerplate comes with some demo code and you should see something, but you are likely to see nothing. This is because of cross-origin issues. The way around this as you develop is to run a little server off of your computer and load your page from localhost. If you have python installed (default on Macs and Linux, here is a tutorial for Windows), then you should be able to run a simple server by running the command:

python -m SimpleHTTPServer 8000

from your command line. Navigating to localhost:8000 in your web browser will then bring up whatever is in the directory that you are running your server from. You should then be able to navigate into the directory for your boilerplate and see a scene that looks like this:

 

default-scene

This is a nice intro scene, but not what we’re going for, so lets open that HTML file in our text/code editors and remove some stuff so that we can create our own scene.

First, remove lines 208-223, 200-202,145, and 120-143 (listed in reverse order to ensure consistency of line numbering as you delete lines) to eliminate the green “skybox”. Now, when you load the scene you should get just the rotating cube. If you are seeing something different, this may be because you deleted lines incorrectly, or it could be because the default HTML file provided with the boilerplate has changed slightly.

Next, remove these two lines from the animate function to stop the cube from spinning (we’ll add something similar back later, just so that you can see how it’s done).

 // Apply rotation to cube mesh
 cube.rotation.y += delta * 0.0006;

Finally, remove these lines to remove the cube entirely.

// Create 3D objects.
var geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
var material = new THREE.MeshNormalMaterial();
var cube = new THREE.Mesh(geometry, material);

// Position cube mesh to be right in front of you.
cube.position.set(0, controls.userHeight, -1);

// Add cube mesh to your three.js scene
scene.add(cube);

You should now see a black screen when you load your page.

 

> Coding (slides 21-27)

>> Code from slide 21

The cube that we just removed is actually a great example of adding an object to a scene. We’re going to do something really similar now, but what we’re going to add is just one tetrahedron. Go ahead and start adding these lines to the same part of the code that you removed your cube from earlier.

Objects in three.js are triangle meshes with a “geometry” and a “material”. The “geometry” defines the shape. For a tetrahedron we can use the built-in THREE.TetrahedronGeometry, like this:

var tetGeometry = new THREE.TetrahedronGeometry(1);

The material defines what our shape is “made of”. To start off, let’s use the same MeshNormalMaterial as the cube above because it doesn’t require any lighting to be visible in our scene.

var tetMaterial = new THREE.MeshNormalMaterial();

Finally, we can put these together into our tetrahedron object:

var tet = new THREE.Mesh(tetGeometry, tetMaterial);

 

Objects in three.js default their positions to being at 0,0,0. Inconveniently, that’s also where you (the camera) are by default. This means that unless you have done something to make the object visible from the inside (not true by default), you won’t be able to see the object if you don’t change it’s position. Let’s go ahead and position our tetrahedron somewhere else:

tet.position.set(2, controls.userHeight, -1);

And add it to our scene:

scene.add(tet);

Now, if you load the page you should see a tetrahedron object off to the right (arrow keys or mouse to rotate).

tet

 

>> Code from slide 22

We’d like to add some more complicated objects to our scene with different fancier kinds of materials. For that we need lighting. There are three major kinds of lighting built in to three.js – ambient, point, and directional, which mean roughly what they sound like. Here are examples of all three. I won’t walk through this in as much detail, other than to point out that 0x404040 and 0xffffff reference hex colors (grey and white). Go ahead and these to your scene above your tetrahedron.

var light = new THREE.AmbientLight( 0x404040 );
scene.add( light );
var light2 = new THREE.PointLight( 0xffffff, 1, 100 );
light2.intensity = 1;
scene.add( light2 );
var light3 = new THREE.DirectionalLight( 0xffffff );
light3.position.set( 0, 1, 1 ).normalize();
scene.add(light3);

The lighting won’t really look like anything for now, as we don’t have any objects in our scene that care about light shining on them. But, after we add our next object, you can play around with how changing the lighting colors, position, and quantity affects it.

 

>> Code from slide 23

This slide gets complicated and I include it because I want to illustrate a few things for beginners to programming. On this slide, we are going to expand upon our single tetrahedron and create four more using an array and a for-loop. We’re going to rotate each tetrahedron with respect to each other (so they aren’t all in exactly the same place) and then add them to a container object. Finally, we’ll position this container object (and all the tetrahedra in it) and place it in the scene. This code is intended to replace your original tetrahedron code.

 

Our tetrahedron geometry remains the same:

var tetGeometry = new THREE.TetrahedronGeometry(1);

But now we’re going to want a whole array of 5 tetrahedra, instead of just a single tetrahedron. We’ll define the variable as an array of length five now and then fill in it’s contents in our for-loop later. We’ll store our tetrahedra in an array to make them easy to reference individually later.

var tet = new Array(5);

We also need to create a container object to add tetrahedra to as we create them.

var fit = new THREE.Object3D(); 

 

This next set of lines define how we’re going to turn each tetrahedron relative to each other. This is math, and I won’t be going into it.

var t = ((1 + Math.sqrt(5))/2);
var fturn = 6.283/5;
var axis = new THREE.Vector3( t, 1, 0 );
axis.normalize();

 

We create our tetrahedra and add them to the fit object inside of this for-loop. This for loop defines a variable i and sets it equal to 0. As long as i is less than 5, we run through this loop, and at the end of each iteration we add 1 to (this is what i++ means). The code inside the curly brackets {} is inside the for-loop and is run for each iteration.

for(var i = 0; i < 5; i++) {
 tet[i] = new THREE.Mesh(tetGeometry, new THREE.MeshLambertMaterial());
 tet[i].rotateOnAxis(axis, i*fturn);
 fit.add(tet[i]);
}

Inside the loop we first create a new tetrahedron the same way that we did earlier, except now we’re going to use a “MeshLambertMaterial” instead of the “NormalMaterial”. This will give us some nice shadowing with our lighting. We’ll set the i-th element of our array to be this new tetrahedron object. The square brackets after our array name let us reference a specific array element. For example, tet[0] references the first array element. Note that arrays in Javascript are 0-indexed, which means that the n-th array element has index n-1.

We will then rotate our tetrahedron an amount defined by a combination of (which tetrahedron it is) and fturn (defined earlier) along the axis defined earlier.

The final thing that we do inside of our array is add our new tetrahedron object to our “master” fit object.

 

Finally, we position our fit object and add it to our scene the same way we did to our solo tetrahedron earlier.

fit.position.set(-2, controls.userHeight+1, -2);
scene.add(fit);

Now you should see five intersecting tetrahedra with nice shadowing off to the left.

fit

 

>> Code from slide 24

A quick way to add a lot of content and character to your VR experience is to use images. We’re going to add a spherical-ish panorama to our scene hybridized from some generic sky and NASA rover captured Mars images. You can easily create an equirectangular spherical panorama with your phone or spherical camera if you want.

Images in three.js are a bit complicated because they are loaded asynchronously. That means that you need a loader to load the image and that nothing else will happen until your image loads, at which point a function will be called that does something with your image. So you can generally expect images to show up after everything else in your scene.

Even though we don’t have our panorama yet, we’re going to create a placeholder variable for it named pano so that it can be accessed globally.

var pano;

Now we’ll create a loader that can be used for image “textures”

var loader = new THREE.TextureLoader();

And use it to load an image named ‘Greeley_pan_small_stars.jpg’ (you can find this image in the same directory as the slides). onTextureLoaded is the name of a function that the loader will call whenever our image finishes loading.

loader.load('Greeley_pan_small_stars.jpg', onTextureLoaded);

 

This is what the onTextureLoaded function looks like. Note that the loader returns a variable texture that contains the texture it loaded.

function onTextureLoaded(texture) {
  var geometry = new THREE.SphereGeometry(1000, 32, 32);
  var material = new THREE.MeshBasicMaterial({
    map: texture,
    side: THREE.BackSide
  });

  pano = new THREE.Mesh(geometry, material);
  pano.position.y = 190;
  scene.add(pano);
}

Inside our loader, we do the same familiar steps to create an object. We make a geometry (this time a sphere), define a material, put them together into an object, position the object, and add that object to the scene. The most different part here is what we do when we create our material. We use a “MeshBasicMaterial” for our object now and we set some properties of it inside of the curly brackets. “map: texture” maps the surface of our material to the image texture that we loaded. “side: THREE.BackSide” makes the texture show up on the *inside* of our object rather than the outside. That means that we can position the object such that we are inside of it and still see it.

 

>> Code from slide 25

Remember the animate function that we removed cube animation from earlier? In slide 25, we add back some animation, this time to our fit. This code goes right where the old animation code was earlier (you can see exactly where on the slide). We can get the current rotation of our fit with fit.rotation. This rotation is split up into three parts, x, y, and z. We can access these parts individually with, for example, fit.rotation.x. We want to increment the rotation of our fit a little bit every time we enter the animate function (which this code is set up to do repeatedly). += is a shorthand way to set a variable equal to itself plus some value. delta is a variable that has been set up to have the amount of time since we last entered the animate function. We increment our rotation by a tiny fraction of delta to ensure a fairly constant rotation speed.

 fit.rotation.y += delta * 0.0003;
 fit.rotation.x += delta * 0.00005;

 

>> Code from slides 26 and 27

At this point we’ve made a nice scene with some animation, but we don’t have any real way to interact with it at all. Javascript event listeners are one way to interact. event listeners wait for an “event” to happen and then trigger a function when it happens. For example, we can listen for a “keydown” on the entire browser window like this:

window.addEventListener("keydown", onkey, true);

Now, whenever a key is pressed, a function called onKey is called. The onKey function looks like this:

function onkey(event) {
   event.preventDefault();
      if (event.keyCode == 32) { // space bar 
         isRotation = !isRotation;
      }
}

The very first thing we do is call “event.preventDefault()”. This prevents the default behavior associated with a key press. Then we check what key was pressed – if it’s space bar (keyCode 32), we want to do an action. Here we set the boolean value of the variable isRotation to the opposite of what is was before (true to false and vice versa). A more complicated action might involve calling a function that then performs that executes some code when the event is triggered.

 

You can do the same thing with other types of events like clicks. Taps on mobile correspond to clicks and trigger the same event listener, so this one is pretty useful for, say, Google Cardboard development. The basic pattern is the same, but now we’re going to listen for a “click” on just the body of our html document (document.body).

document.body.addEventListener( 'click', onClick);
function onClick(event) {
  isRotation = !isRotation;
}

 

Of course, listening for events by themselves doesn’t do much for us unless our interaction has a result. Let’s start and stop the rotation of our fit, in response to the value of the isRotation variable. While we’re at it, don’t forgot to define and instantiate our global boolean variable isRotation outside of all these functions.

var isRotation = true;

We’ll use an if-statement around our earlier rotation code to turn rotation on and off like this:

if (isRotation) {
     // Apply rotation 
     fit.rotation.y += delta * 0.0003;
     fit.rotation.x += delta * 0.00005;
  }

 

That’s it for the main content of this tutorial! You should end up with something that looks like this, where the giant “star” of five intersecting tetrahedra (fit) stops and starts rotating depending when you click or hit spacebar. You can see my version of it running here.

webvr-workshop

Keep playing around with making different objects and interactions to customize your webVR application.

 

Gotchas (slides 29-31)

These slides cover common bugs that you are likely to encounter as you develop a webVR application, as well as various debugging tools that you might use to discover them. I’m not going to go over them here, but they are definitely worth familiarizing yourself with if you want to continue with webVR development.

 

Other Slides

My slide deck also gives some examples of other webVR projects and other (potentially simpler) ways of developing webVR applications. Take some time to explore the space and decide for yourself how and what you want to create. I’m looking forward to seeing it!

-Andrea