three.js tutorials

Drawing the coordinate axes

NOTE: This was written in 2013. It might not work with the latest version of Three.js.


Having the coordinate axes shown on screen is very useful in a number of scenarios, the most prominent of all being as a debug aid while developing, to ensure you're putting things where expected.

This is going to be a very simple tutorial, but will demonstrate the use of several interesting three.js techniques anyway: object composition, dashed lines, some of the built-in geometries, and the TrackballControls. If you want to follow along, you can also get the source code for this tutorial before continuing.

Got it? Let's go then!

First we'll set up a renderer and a simple scene:


renderer = new THREE.WebGLRenderer({ antialias: true });
document.body.appendChild( renderer.domElement );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setClearColorHex( 0xeeeeee, 1.0 );

scene = new THREE.Scene();

Now we'll place some objects in the scene. Three.js comes with a bunch of predefined geometries that we can use in our projects, so let's use some of them instead of using just cubes! We'll use a different geometry per quadrant, but we'll share the same magenta wireframe material amongst every mesh. It is named, rather imaginatively, meshMaterial:


meshMaterial = new THREE.MeshBasicMaterial({ color: 0xFF00FF, wireframe: true });

var cube = new THREE.Mesh( new THREE.CubeGeometry( 5, 5, 5 ), meshMaterial );
cube.position.set( 25, 25, 25 );
scene.add( cube );

var sphere = new THREE.Mesh( new THREE.SphereGeometry( 5 ), meshMaterial );
sphere.position.set( -25, 25, 25 );
scene.add( sphere );

var icosahedron = new THREE.Mesh( new THREE.IcosahedronGeometry( 5 ), meshMaterial );
icosahedron.position.set( 25, 25, -25 );
scene.add( icosahedron );

var torus = new THREE.Mesh( new THREE.TorusGeometry( 5, 3 ), meshMaterial );
torus.position.set( -25, 25, -25 );
scene.add( torus );

var cylinder = new THREE.Mesh( new THREE.CylinderGeometry( 5, 5, 5 ), meshMaterial );
cylinder.position.set( 25, -25, 25 );
scene.add( cylinder );

var circle = new THREE.Mesh( new THREE.CircleGeometry( 5 ), meshMaterial );
circle.position.set( -25, -25, 25 );
scene.add( circle );

var octahedron = new THREE.Mesh( new THREE.OctahedronGeometry( 5 ), meshMaterial );
octahedron.position.set( 25, -25, -25 );
scene.add( octahedron );

var torusKnot = new THREE.Mesh( new THREE.TorusKnotGeometry( 5, 1 ), meshMaterial );
torusKnot.position.set( -25, -25, -25 );
scene.add( torusKnot );

If you're curious about the meaning of the parameters to each geometry, looking at the source might be your best bet, as the Three.js documentation was not completed for all the geometries when the tutorial was written.

Now we need to create the axes object. If you look at the code, we're getting the axes just by doing this:


axes = buildAxes( 1000 );

Let's look into the buildAxes function to see how the axes object is built:


function buildAxes( length ) {
    var axes = new THREE.Object3D();

    axes.add( buildAxis( new THREE.Vector3( 0, 0, 0 ), new THREE.Vector3( length, 0, 0 ), 0xFF0000, false ) ); // +X
    axes.add( buildAxis( new THREE.Vector3( 0, 0, 0 ), new THREE.Vector3( -length, 0, 0 ), 0xFF0000, true) ); // -X
    axes.add( buildAxis( new THREE.Vector3( 0, 0, 0 ), new THREE.Vector3( 0, length, 0 ), 0x00FF00, false ) ); // +Y
    axes.add( buildAxis( new THREE.Vector3( 0, 0, 0 ), new THREE.Vector3( 0, -length, 0 ), 0x00FF00, true ) ); // -Y
    axes.add( buildAxis( new THREE.Vector3( 0, 0, 0 ), new THREE.Vector3( 0, 0, length ), 0x0000FF, false ) ); // +Z
    axes.add( buildAxis( new THREE.Vector3( 0, 0, 0 ), new THREE.Vector3( 0, 0, -length ), 0x0000FF, true ) ); // -Z

    return axes;

}

What we are using in the function is called object composition, i.e., an object (the parent) will contain more objects (the children). In this case, the axes object is just an instance of Object3D, to which the rest of subaxes are added. And then we return the axes object from the function, and add it to the scene.

Object3D instances are invisible when rendered, but what you see instead is whatever children they have. If you think about it, it works in exactly the same way that when you add objects to a scene: the scene is invisible per se; what we see is the objects in the scene instead.

At the end we always end up with a hierarchy of objects, with more or less nested levels, that represent what is in the scene. This is called a scene graph.

Working in this way allows you to easily add and remove objects from the scene by adding or removing just one object, not the separate children one by one.

For example, if we wanted to remove the axes after they've been added to the scene, we'd just do this:


scene.remove( axes );

whereas if we had added each axis separately to the scene, the task would be way more tedious:


scene.remove( axis1 );
scene.remove( axis2 );
scene.remove( axis3 );
// ... you get the idea!

Besides, you can also rotate the parent object, and the objects inside it will rotate along, for example. Or translate it, or scale it... And you can nest more objects inside other objects, so this technique is really powerful.

Going back to the axes, I hope you noticed the use of the buildAxis function. Since the code for building each subaxis is pretty much the same, I abstracted the common code out into a function, so we just send it the start and end points for each subaxis, the colour and whether it is (or not) a dashed line.


function buildAxis( src, dst, colorHex, dashed ) {
    var geom = new THREE.Geometry(),
        mat; 

    if(dashed) {
        mat = new THREE.LineDashedMaterial({ linewidth: 3, color: colorHex, dashSize: 3, gapSize: 3 });
    } else {
        mat = new THREE.LineBasicMaterial({ linewidth: 3, color: colorHex });
    }

    geom.vertices.push( src.clone() );
    geom.vertices.push( dst.clone() );
    geom.computeLineDistances(); // This one is SUPER important, otherwise dashed lines will appear as simple plain lines

    var axis = new THREE.Line( geom, mat, THREE.LinePieces );

    return axis;

}

Probably the most interesting part is the use of LineDashedMaterial when the line has to be --guess what?-- dashed. But using LineDashedMaterial alone won't make any difference, and your lines will be plain straight lines, unless you pay close attention and do what this line says:


geom.computeLineDistances(); // This one is SUPER important, otherwise dashed lines will appear as simple plain lines

Subaxes?

That said, what is a "subaxis", you'll ask? Well, since we have three axes (X, Y, Z), we could just draw three lines in different colours and be done with it. But how do we know which of the two halves of an axis is the positive one once we start rotating the view and moving around?

That's where subaxes come in, as we'll split each axis in two parts: the positive and the negative, and we'll draw the negative half with a dashed line, so that it looks as if it's behind.

Using this function we could add as many axes as we wanted; we could even add random non-orthogonal axes such as diagonal axes, but we'll content ourselves with the traditional X, Y, and Z axes.

This is how we build the positive X axis:


buildAxis(
    new THREE.Vector3( 0, 0, 0 ),
    new THREE.Vector3( length, 0, 0 ),
    0xFF0000,
    false
)

See how we go from the center (0, 0, 0) to x=length (length, 0, 0)?

With this you can parametrise how long you want your axes to be.

And here we build the negative X axis:


buildAxis(
    new THREE.Vector3( 0, 0, 0 ),
    new THREE.Vector3( -length, 0, 0 ), // notice the minus sign?
    0xFF0000,
    true // ... and true because we want this axis to be dashed
)

The rest of axes follows the same scheme, so there's no need for repeating it.

Camera and controls

To see what is in a scene we need a camera, so we'll add a PerspectiveCamera and position it a little away from the objects we've already placed on the scene:


camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 10000 );
camera.position.set( 30, 50, 120 );
camera.lookAt( new THREE.Vector3( 0, 0, 0 ) );

But a static camera doesn't really justify the use of axes, right? We'll add some controls so we can move around and get lost in the scene--that is, until we look at the axes and say Oh, but of course! I'm the positive quadrant!. Or something like that ;-)

For simplicity, we'll use one of the control classes that come with Three.js, TrackballControls. This allows us to get easy mouse input and camera updating functionality in virtually no time!

The setup is pretty straightforward:


controls = new THREE.TrackballControls( camera );
controls.rotateSpeed = 1.0;
controls.zoomSpeed = 0.2;
controls.panSpeed = 0.8;

controls.noZoom = false;
controls.noPan = false;

controls.staticMoving = true;
controls.dynamicDampingFactor = 0.3;

Notice how the controls are associated to the camera we created before. This ensures that the mouse movements will induce changes in the camera.

Which exact values to use for the parameters is a matter of tweaking and fine tuning and might depend on each project, but you can start using these as a starting point and adjust as you see fit.

There's only a few missing steps. First, we need to start rendering:


animate();

Simple, right?

The animate() function is simple too, but really functional:


function animate() {
    requestAnimationFrame( animate );
    controls.update();
    renderer.render( scene, camera );
}

First we ask JavaScript to call this function sometime in the future by using requestAnimationFrame, hopefully after around 1/60 seconds (~16 ms).

Then we ask the controls to update(). What this does is actually updating the camera position with whatever changes we might have introducing by dragging with the mouse.

And finally, we ask the renderer to render the scene using the camera.

Yay! We have a 3D scene with axes!

Recommended reading and examples

You can also look at another example that uses dashed lines in the three.js examples folder: webgl_lines_dashed.html

And the trackball controls example.

You should also totally read about requestAnimationFrame. Saves battery and will make you a better person. Guaranteed.

A final note about dashed lines

Support for dashed lines is a relatively recent addition to three.js. Altered Qualia added the LineDashedMaterial class and support in the WebGLRenderer back in November 2012, so it's early days for this material and thus the syntax for configuring it might change in the future, and support for other renderers might be non-existant.

For example, I found that it wasn't working with the CanvasRenderer, so I added the missing code. This was quite easy, as the LineDashedMaterial behaviour is fairly similar to the LineBasicMaterial behaviour. So if you want to use dashed lines in other renderers, and it doesn't work, what about trying to implementing it yourself? That's the beauty of open source!