WebGL and Three.js: creating a real scene
Written by Greg Stier
What is WebGL?
WebGL is a Javascript API used to render 3D graphics to the screen in a browser. The WebGL API can be complicated and messy, but there are libraries that simplify this. One such library is Three.js. Three.js is a lightweight 3D library that hides a lot of the WebGL complexities and makes it very simple to do 3D programming on the web. Three.js can be downloaded from github or the three.js website, where you will also find links to documentation and examples.
Introduction
In my past tutorials I’ve talked about the basics of a 3D scene, explained how texture mapping works, covered different types of lighting, and finally, explained how to create reflections. In all of those examples I created my scenes using simple cubes, planes and spheres. While those things work great for creating tutorials and simple prototyping, they only get you so far. In this tutorial I’m going to create a ‘real’ scene. The major points I’m going to cover are creating a skybox, loading 3D models that were created and textured by a third party, and using a bump map to give a primitive plane more detail.
Getting started
The source code for this tutorial and my others can be found here. If you want to implement this tutorial, you will need to get the three-scene-tut.html, all associated javascript, images, and models. For this tutorial, I’m going to talk about the code located here. If you’ve read my past tutorials, then you will notice that most of the code here is not new. You’ll see the init function at the beginning. In this function we create the scene object, add a perspective camera, create a directional light, and finally create a WebGL renderer. You will also notice that in the init function we make a call to two other functions that are new, the loadSkyBox function and the addSceneElements function. It’s these two functions that I’m going to focus on. Let’s begin with the skybox.
Skybox
What is a skybox? A skybox is a simple technique for adding detail to the background of our scene without the need for creating complex geometry. A skybox is six separate images of the sky or background mapped to the inside of a box that is surrounding us, hence the name skybox. Let’s take a look at the loadSkyBox function and its helper function createMaterial:
function loadSkyBox() {// Load the skybox images and create list of materials
var materials = [
createMaterial( 'images/skyX55+x.png' ), // right
createMaterial( 'images/skyX55-x.png' ), // left
createMaterial( 'images/skyX55+y.png' ), // top
createMaterial( 'images/skyX55-y.png' ), // bottom
createMaterial( 'images/skyX55+z.png' ), // back
createMaterial( 'images/skyX55-z.png' ) // front
];// Create a large cube
var mesh = new THREE.Mesh( new THREE.BoxGeometry( 100, 100, 100, 1, 1, 1 ), new THREE.MeshFaceMaterial( materials ) );// Set the x scale to be -1, this will turn the cube inside out
mesh.scale.set(-1,1,1);
scene.add( mesh );
}function createMaterial( path ) {
var texture = THREE.ImageUtils.loadTexture(path);
var material = new THREE.MeshBasicMaterial( { map: texture, overdraw: 0.5 } );return material;
}
In the first line of code we create an array of materials. The materials are created by the createMaterial function, where you can see we first load the image at the given path, then create the material. After creating the six materials needed for our skybox, we then create a box with the dimensions of 100 wide, 100 tall, and 100 deep. We create the box using a MeshFaceMaterial. This will map each material to a face of the box. Next, you will notice that we set the scale of the box to -1 along the x axis. Why do this? Well, the standard box geometry that we are using faces outwards, but we want our images to be mapped to the inside of the box. By setting the scale to -1 along one of the axis, it doesn’t matter which one, we then invert the box so that it now faces inward.
The next thing I’m going to go over is the addSceneElements function. In this function we do two things: we load an externally created model along with all its textures, and we create the ground.
Loading Models
To load an external model we need to use one of the many loaders that have been written for Three.js. The loader you will use will depend on what file format your model is saved as. A list of all the loaders can be found in the Three.js github repo.
The model format we are using for this example is OBJ. The OBJ format uses a separate file to store the material information for the model, an MTL file. Let’s take a look at the code:
var loader = new THREE.OBJMTLLoader();
loader.load("models/TreeCar.obj", "models/TreeCar.mtl", function (loadedObject) {
loadedObject.name = 'Car';
loadedObject.position.set(0,1.6,0);scene.add( loadedObject );
}, onProgress, onError);
Since we have both an OBJ file and an MTL file we use the OBJMTLoader. In the first line we create an instance of the loader. In the second line we call the load function. The load function takes up to five parameters. The first is the path to your model and the second is the path to the material file. The third function is a callback function for when the model is loaded. The last two parameters are also callback functions. One is to track progress and the other is used to capture any errors that may occur while fetching the model. As you can see we’ve defined our load callback function inline. In it we simply give our loaded object a name so that we can easily reference later if needed. Next we set its position, and finally add it to the scene. Pretty simple and straightforward. One thing to note, the MTL file format specifies the location of the images used for textures. You may need to modify this path to load properly. The location of the textures is defined as relative to the MTL file. You can use any text editor to edit the MTL file.
In the second part of the addSceneElements function we create a ground to place our car on.
Bump map
To create the ground, we’re simply going to create a plane, and texture it. But to give it a bit more of a convincing look we are going to use another texture called a bump map to make it appear as though it has more detail than it actually does.
A bump map is a special kind of texture. Instead of giving our model color, a bump map affects how the light is calculated at each pixel when rendering the model. Using bump maps is a very inexpensive way to add detail to a model without the need to create detailed geometry. Let’s take a look at the code:
First, we load the texture:
var groundTexture = THREE.ImageUtils.loadTexture('images/wet-sand.jpg');
Next we tell Three.js that we are going to tile this texture across our plane by setting its wrap settings to RepeatWrapping:
groundTexture.wrapS = THREE.RepeatWrapping;
groundTexture.wrapT = THREE.RepeatWrapping;
Next we load the bump map and set its wrapping to also be RepeatWrapping:
var groundBump = THREE.ImageUtils.loadTexture('images/wet-sand-normal.jpg');
groundBump.wrapS = THREE.RepeatWrapping;
groundBump.wrapT = THREE.RepeatWrapping;
Next we create a material using the two textures we just loaded.
var groundMat = new THREE.MeshPhongMaterial( { map: groundTexture, bumpMap: groundBump, color: 0x957D69 } );
Now we are going to set our ground texture to repeat, this prevents it from looking too stretched out.
groundMat.map.repeat.set(5,5);
Finally we create the plane that is our ground using the material we just created, rotate the plane and add it to our scene.
var groundMesh = new THREE.Mesh( new THREE.PlaneGeometry(100, 100, 2, 2), groundMat);
groundMesh.rotation.set(-90 * (3.14/180), 0, 0, 'XYZ');scene.add(groundMesh);
Conclusion
As you can see, the code for creating our ‘real’ scene isn’t any more complex than creating scenes with plain white backgrounds that use only primitive objects, but it is much more interesting. Using the methods above let you use other more powerful and specialized tools like Blender, or Clara.io for creating our models. And even when we do use primitive objects, we can add detail to them by using textures and bump maps. If you would like to tweak the scene even more, try one or more of the following:
- Change the repeat settings for the ground texture
- Add a repeat for the bump map
- Change the color of the ground material
- Change the color and position of the directional light
- Try using different types of lights
Image courtesy of Unsplash.