Using WebVR & Three.JS for Basic Image Viewing

Blog Posts ,Programming ,Virtual Reality ,Web Development
July 8, 2015

I tend to gravitate towards space when I’m learning new virtual reality development tool sets, and found myself playing around with Three.JS on the new Microsoft Edge browser on my shiny new Windows 10 partition (good news everyone: build 10162 has been development-stable for me!). As with most of my Three.JS projects, I actually started with Boris Smus’ WebVR boilerplate code, but I haven’t tested with a WebVR-enabled browser yet because… Edge is simply gorgeous, and the performance is really good.

picture of space drawn in threejs using Microsoft edge browser

A screenshot from an earlier iteration of the web app – image shown (c) Scott Kelly

Viewing photos online has been the crux of many big internet players, and VR demos are notoriously difficult to pull off fluidly for masses at expo events. I wanted to put something together that I could easily show someone, with or without a VR headset, and have a tiny degree of interactivity (changing between pictures) without needing to explain mechanics a million times in one go.

So how about viewing photos from outer space, while floating above Earth as the moon orbits peacefully? And to top it all off, we’ll add in music from space itself, captured by NASA.

The code behind my oh-so-creatively named “WebVR Space” project is incredibly simple. Web Audio makes playing back sounds easy, and audio can be attached to objects within a WebGL scene to create positional playback. Playing the “song” that is made by the rings of Saturn was as easy as converting the original .au file to a .wav, and loading it into an audio listener:

// Add audio
var listener = new THREE.AudioListener();
camera.add(listener);

var sound = new THREE.Audio(listener);
sound.load("saturn.wav");
sound.autoplay = true;

To make the environment feel ethereal, I added in three objects to my scene: a giant “Earth” object that rotates slowly beneath the camera to help ground viewers, a moon object that orbits the planet, and a giant sky box that uses a tiled star pattern to make the viewer feel as though she is seeing things that are out of this world.

// Create the home planet

var geo_earth = new THREE.SphereGeometry(13.5, 32, 32);
var txtr_earth = THREE.ImageUtils.loadTexture('earth.jpg');
var mat_earth = new THREE.MeshBasicMaterial({map: txtr_earth});

var planet = new THREE.Mesh(geo_earth, mat_earth);
// Position planet mesh

planet.position.z = -1;
planet.position.y = -15; // Planet should be directly below the camera
planet.add(sound);
scene.add(planet);

// Create the skybox as a background
var boxWidth = 30;
var txtr_skybox = THREE.ImageUtils.loadTexture('space.jpg');
txtr_skybox.wrapS = THREE.RepeatWrapping;
txtr_skybox.wrapT = THREE.RepeatWrapping;
//txtr_skybox.repeat.set(2, 2);

var geo_skybox = new THREE.BoxGeometry(boxWidth, boxWidth, boxWidth);
var mat_skybox = new THREE.MeshBasicMaterial({
 map: txtr_skybox,
 color: 0xffffff,
 side: THREE.BackSide});
 
var geo_sun = new THREE.SphereGeometry(2, 32, 32);
var mat_sun = new THREE.MeshBasicMaterial({});

var skybox = new THREE.Mesh(geo_skybox, mat_skybox);
scene.add(skybox);

// Create moon
var geo_moon = new THREE.SphereGeometry(1, 32, 32);
var txtr_moon = THREE.ImageUtils.loadTexture('moon.jpg');
var mat_moon = new THREE.MeshBasicMaterial({map: txtr_moon});

var moon = new THREE.Mesh(geo_moon, mat_moon);

moon.position.z = -12;
moon.position.x = 9;
moon.position.y = 2;
scene.add(moon);

I wanted to add a dynamic element to the scene, so I created a small function that would have the moon orbit the planet under the user and pass through the sky above before setting under the planet, which took a few tries to get right.

// Request animation frame loop function
function animate() {
 // Apply rotation to planet
 planet.rotation.y += 0.0001;
 
 // Radius of moon orbit is 9 
 // Y value stays constant
 // X, Z orbits circle: x^2 + z^2 = 81
 // Use Date.GetSeconds() * 6 for degree value
 // X, Z = 7cos(s), 7sin(s)
 function orbit()
 {
 moon.position.z = 9 * Math.cos(degree);
 moon.position.x = 9 * Math.sin(degree);
 moon.position.y = -9 * Math.cos(degree);
 degree+= 0.001;
 }
 
 orbit();
 // Update VR headset position and apply to camera.
 controls.update();

 // Render the scene through the manager.
 manager.render(scene, camera);

 requestAnimationFrame(animate);
}

Lastly, I needed an object to house the images that I wanted to share with the user – in this early demo, I’m just using pictures that are saved to the directory of the application, but I’m hoping to expand to allow for custom images to be used instead, or linking into the NASA Image of the Day to give some additional variety to the site. I did this by creating two nested THREE.BoxGeometry objects, one that was slightly larger than the other to serve as a frame, and one that would have the image displayed on it.

Note: there is probably a cleaner way to do this with a padding on a single box geometry, but I haven’t played around with it yet – this was my first go at a solution that looked the way I wanted it to.

// Create Image Viewer Mesh
var geo_img = new THREE.BoxGeometry(4, 3, 1);
var geo_bg = new THREE.BoxGeometry(4.2, 3.2, 1);

var txtr_img = THREE.ImageUtils.loadTexture("testimg.jpg"); //We will load in images from online source in future
txtr_img.minFilter = THREE.NearestFilter;

var mat_img = new THREE.MeshBasicMaterial({map: txtr_img});
var mat_bg = new THREE.MeshBasicMaterial();

var img = new THREE.Mesh(geo_img, mat_img);
var img_bg = new THREE.Mesh(geo_bg, mat_bg);

img.position.z = -4.5;
img_bg.position.z = -4.6;
img.position.y = 1;
img_bg.position.y = 1.05;

scene.add(img);
scene.add(img_bg);

At this point, we’re looking pretty good!

space2

 

The only thing at this point left to do (other than all of the VR testing, but that’s another post!) was to create a mechanism to switch which image was being shown in the viewer. In a more polished version of this app, I’d probably create a nice interface to move between them, but for now, I accomplished this with a simple key press check: the numbers on the keyboard map to a different image.

function onKey(event) {
 var key = event.keyCode;
 
 switch(key)
 {
 case 90:
 controls.resetSensor(); // z
 break;
 case 49:
 // Change image to 1 on pressing 1
 img.material.map = THREE.ImageUtils.loadTexture("testimg.jpg"); //We will load in images from online source
 img.material.needsUpdate = true;
 break;
 case 50: 
 // Change image to 2 on pressing 2 
 img.material.map = THREE.ImageUtils.loadTexture("img1.jpg"); //We will load in images from online source
 img.material.needsUpdate = true;
 break;
 case 51: 
 // Change image to 3 on pressing 3
 img.material.map = THREE.ImageUtils.loadTexture("img2.jpg"); //We will load in images from online source
 img.material.needsUpdate = true;
 break;
 case 52: 
 // Change image to 4 on pressing 4
 img.material.map = THREE.ImageUtils.loadTexture("img3.jpg"); //We will load in images from online source
 img.material.needsUpdate = true;
 break;
 case 53: 
 // Change image to 5 on pressing 5
 img.material.map = THREE.ImageUtils.loadTexture("img4.jpg"); //We will load in images from online source
 img.material.needsUpdate = true;
 break;
 default: 
 break;
 }

And with that – the basics are in! I still need to get it hosted online and I have a few other features to poke around with, but I wanted to share the basics of my latest project, because space is AWESOME.

As always, you can find the full code available on GitHub here!

Happy coding & zero-G floating!

Related Posts

Leave a Reply