20 December 16 -

Animating scenes with WebGL + Three.js.

3D graphics, technology, building things, and the internet: these are a few of my favourite things.

Now, thanks to the emergence of WebGL — a new JavaScript API for rendering interactive 3D graphics in browsers without the need for additional plug-ins — it’s never been easier to bring them all together.

With the development of virtual and augmented reality applications, brands are increasingly moving towards tactile digital experiences: touchy-feely tech.

Or, at least, that’s the hope of the investors who’ve contributed a $1.1 billion influx of capital into the VR and AR industries this year.

From Google’s interactive tour of Abbey Road Studios to the fleet of ships used in filming Deadliest Catch, all kinds of products, services and environments can be better articulated by physically immersing audiences in a non–physical world.

As more experiential technology becomes easier to access, 2D starts to feel flat. Literally and figuratively.

Let’s be realistic though. For now, a lot of the applications involved in creating these experiences are still at the cutting edge, and a speck on the distant horizon for most businesses.

Or are they?

Enter WebGL: a practical and flexible way to create more immersive 3D content. Whether it’s Porsche showcasing the curves on a new 911, NASA highlighting what it’s like on Mars, or a celebration of J DIlla’s much-loved Donuts album, WebGL can be used in various capacities to tell all kinds of stories.

To get you acquainted with this powerful technology, I’m going to provide a quick overview of how it works, and walk through the steps involved in creating a simple 3D environment using Three.js — a JavaScript library built around the WebGL APIs.

First up, what is WebGL?

WebGL is a web technology that brings hardware-accelerated 3D graphics to the browser, without the need for installing additional plugins or downloading extra software.

As a result, it’s much more accessible for a wide range of audiences. The browser support is pretty comprehensive at present too, with Chrome, Firefox, IE, Opera and Safari all known to offer good support on both mobile and desktop browsers.

Most computers and smartphones have advanced graphics processing units (GPUs), but until relatively recently, the majority of web pages and mobile sites weren’t capable of using them. The result was slow-loading and low-quality graphics, and a general hesitance to embrace 3D content.

WebGL goes some way to solving this issue. Based on the well-known OpenGL 3D graphics standard, WebGL offers Javascript plug-in free access to a device’s graphics hardware via the HTML 5 canvas element, with the 3D implemented directly into the browser. The result is 3D, 360-degree content that’s easier to build — eliminating the effort involved in a standalone application or plugin — and easier for users to view in high quality on the internet.

And what’s Three.js?

Both OpenGL and WebGL are relatively complex.

Three.js is here to help. It’s an open-source library that simplifies the creation of WebGL tools and environments. It covers the majority of the lower level programming involved in developing GPU-accelerated 3D animations.

Enough chat. Let’s code.

The examples above represent much more complex implementations using the Three.js library. For this exercise, I’ll keep things simple and
create a low poly environment to showcase what can be achieved with just a basic understanding of the fundamentals.

I’m going to build an illustration we’ve recently used on our site.

Christmas-Closure_Header

Let’s make a start with the basics.

A Renderer, a Scene, and a Camera.

See the Pen Step One
by Matt Agar (@agar)
on CodePen.

Click and drag, have a play.

The CodePen above demonstrates a bare bones ‘helloworld’ example of the core elements we need to get started with Three.js.

Firstly we need a Scene — a group or stage containing all the objects we want to render. Scenes allow you to set up what and where is going to be rendered by Three.js. This is where you place objects, lights, and cameras.

var scene = new THREE.Scene();
Creating a scene. In a good way.

Next up we add a Camera. In this case, I’ve gone with a PerspectiveCamera, but there are other options available. The first two parameters specify the field of view and aspect ratio of the camera respectively. The last two parameters represent the cutoff distances for objects that will be rendered by this camera.

var camera = new THREE.PerspectiveCamera(
    75,                                   // Field of view
    window.innerWidth/window.innerHeight, // Aspect ratio
    0.1,                                  // Near clipping pane
    1000                                  // Far clipping pane
);

// Reposition the camera
camera.position.set(5,5,0);

// Point the camera at a given coordinate
camera.lookAt(new THREE.Vector3(0,0,0));
Adding the camera, FOV, aspect ratio and cutoff distances.

The last critical piece of the puzzle is the renderer itself, which handles the rendering of a Scene from a given camera angle. Three.js offers a couple of alternative renderers, however I’m going to stick with the standard WebGL renderer for this exercise.

var renderer = new THREE.WebGLRenderer({ antialias: true });

// Size should be the same as the window
renderer.setSize( window.innerWidth, window.innerHeight );

// Set a near white clear color (default is black)
renderer.setClearColor( 0xeeeeee );

// Append to the document
document.body.appendChild( renderer.domElement );

// Render the scene/camera combination
renderer.render(scene, camera);
Adding the renderer.

This example also includes some basic Geometry — in this case a flat plane — so we can see something rendered out with depth. Without it, we’d only see an empty screen. I’ll go into more detail around Geometry, Materials, and Meshes shortly.

// A mesh is created from the geometry and material, then added to the scene
var plane = new THREE.Mesh(
	new THREE.PlaneGeometry( 5, 5, 5, 5 ),
	new THREE.MeshBasicMaterial( { color: 0x222222, wireframe: true } )
);
plane.rotateX(Math.PI/2);
scene.add( plane );
Adding the flat plane.

A side note on controlling the camera.

You may have noticed in this example I’m using an external module. This is one of the many re-usable components that can be found in the main Github repo for Three.js.

In this case it’s the OrbitControls, which allow us to capture mouse events on the canvas element to re-position our camera around the scene.

var controls = new THREE.OrbitControls( camera, renderer.domElement );
controls.addEventListener( 'change', function() { renderer.render(scene, camera); } );
Implementing OrbitControls.

To check out OrbitControls in action, click and drag or scroll the mouse wheel in the CodePen example above. In this case, as we have no animation loop (I’ll get to animation loops later, once I’m in the process of decorating my tree), we also need to re-render the scene when the controls are updated.

Let’s get ready to render.

Ok, so the previous example may have been a little dull, but you can’t build a nice house, or 3D Christmas tree for that matter, without a solid base.

Time to start adding objects to our scene, and there’s three things we need to explore — Geometries, Materials, and Meshes.

See the Pen Step Two
by Matt Agar (@agar)
on CodePen.

Jingle bells. Well, they will be eventually.
Click and drag to explore the full Scene.

Adding some low poly geometry with flat shading.

The first thing we need is some Geometry. Geometry is any object that contains all the points (vertices) and fill (faces) of the cube.

Three.js simplifies this with a range of built in basic Geometry objects we can use to construct our scene. There are also many different file loaders for various 3D formats. Alternatively, you can always create your own Geometry by specifying the vertices and faces yourself.

For now, we’ll start with a basic octahedron.

var geometry = new THREE.OctahedronGeometry(10,1);
Adding Geometry.

Materials describe the appearance of objects. They are defined in a (mostly) renderer-independent way, so you don’t have to rewrite Materials if you decide to use a different renderer.

There are multiple types of Materials available, and all Materials take an object of properties which will be applied to them.

The example below produces a flat shaded Material that shows off our low poly objects, rather than trying to smooth them out.

var material = new THREE.MeshStandardMaterial( {
    color: 0xff0051,
    shading: THREE.FlatShading, // default is THREE.SmoothShading
    metalness: 0,
    roughness: 1
} );
Determining the texture of our objects with Materials.

The third thing we need is a Mesh. A Mesh is an object that takes a Geometry and applies a Material to it, which we then can insert into our Scene and move freely around.

Here’s how to combine Geometry and Material into a Mesh and add to the Scene. Note that we can freely re-position or rotate the Mesh in the scene once it’s been added.

var shapeOne = new THREE.Mesh(geometry, material);
shapeOne.position.y += 10;
scene.add(shapeOne);
Combining Geometry and Material into a Mesh and adding to the Scene.

Adding lights.

Once we have objects in our Scene, we need to light them. To do this we’ll add two different varieties: ‘ambient’ and ‘point’ lights.

This AmbientLight’s colour gets applied to all objects in the Scene globally.

var ambientLight = new THREE.AmbientLight( 0xffffff, 0.2 );
scene.add( ambientLight );
Adding ambient lights to the Scene.

Point lights create a light at a specific position in the scene. The light shines in all directions, in roughly the same way as a light bulb.

var pointLight = new THREE.PointLight( 0xffffff, 1 );
pointLight.position.set( 25, 50, 25 );
scene.add( pointLight );
Adding point lights to the Scene.

There are other types of light available if these don’t meet your needs, including ‘directional’ and ‘spot’ lights. Check out the Three.js light manual for more information on alternative options.

Casting and receiving shadows.

Shadows are disabled by default, but really help in creating a feeling of depth — so we’ll need to enable them on the renderer.

renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
Enabling shadows on the renderer.

The next step is to specify which lights should cast a shadow, and the size of the shadow map that is going to be rendered:

pointLight.castShadow = true;
pointLight.shadow.mapSize.width = 1024;
pointLight.shadow.mapSize.height = 1024;
Let there be light. And as a result, shadow.

And finally we specify which Meshes should receive shadows. Note that any Mesh can both cast and receive shadows within the Scene.

shapeOne.castShadow = true;
shapeOne.receiveShadow = true;
Specifying Meshes with shadows.

In this Scene, we’re using a unique ShadowMaterial. This allows for a Mesh that shows only the shadow, not the object itself.

var shadowMaterial = new THREE.ShadowMaterial( { color: 0xeeeeee } );
shadowMaterial.opacity = 0.5;
Implementing ShadowMaterial.

Building complexity with simple elements.

What we’ve done so far is fine for a couple of simple objects, but things get a lot easier if we can make re-usable elements.

See the Pen Step Three
by Matt Agar (@agar)
on CodePen.

Slowly but surely, the baubles become more… bauble-y.
Click and drag in the codepen to take a closer look.

By combining and layering multiple low poly objects, we can begin to create more complex shapes.

Here’s what’s involved in extending the Three.Group object to create complex shapes in the constructor.

var Decoration = function() {

    // Run the Group constructor with the given arguments
    THREE.Group.apply(this, arguments);

    // A random color assignment
    var colors = ['#ff0051', '#f56762','#a53c6c','#f19fa0','#72bdbf','#47689b'];

    // The main bauble is an Octahedron
    var bauble = new THREE.Mesh(
        addNoise(new THREE.OctahedronGeometry(12,1), 2),
        new THREE.MeshStandardMaterial( {
            color: colors[Math.floor(Math.random()*colors.length)],
            shading: THREE.FlatShading ,
            metalness: 0,
            roughness: 1
    } )
    );
    bauble.castShadow = true;
    bauble.receiveShadow = true;
    bauble.rotateZ(Math.random()*Math.PI*2);
    bauble.rotateY(Math.random()*Math.PI*2);
    this.add(bauble);

    // A cylinder to represent the top attachment
    var shapeOne = new THREE.Mesh(
        addNoise(new THREE.CylinderGeometry(4, 6, 10, 6, 1), 0.5),
        new THREE.MeshStandardMaterial( {
            color: 0xf8db08,
            shading: THREE.FlatShading ,
            metalness: 0,
            roughness: 1
        } )
    );
    shapeOne.position.y += 8;
    shapeOne.castShadow = true;
    shapeOne.receiveShadow = true;
    this.add(shapeOne);
};
Decoration.prototype = Object.create(THREE.Group.prototype);
Decoration.prototype.constructor = Decoration;
Creating complex shapes in the constructor.

We can now re-use the ‘bauble’ multiple times to add multiple instances of the object to our Scene, making the tree much more festive with much less work than it would take to individually create each element.

var decoration = new Decoration();
decoration.position.y += 10;
scene.add(decoration);
Decking the halls.

Another tip is to add an element of random imperfection to the creation of the objects.

Shifting vertices within the Geometry of the object adds an element of organic randomness to low poly shapes. Without these imperfections, things can feel a little synthetic. Here, I’ve used a helper function to randomly add noise to vertices in a Geometry:

function addNoise(geometry, noiseX, noiseY, noiseZ) {
    var noiseX = noiseX || 2;
    var noiseY = noiseY || noiseX;
    var noiseZ = noiseZ || noiseY;
    for(var i = 0; i < geometry.vertices.length; i++){
        var v = geometry.vertices[i];
        v.x += -noiseX / 2 + Math.random() * noiseX;
        v.y += -noiseY / 2 + Math.random() * noiseY;
        v.z += -noiseZ / 2 + Math.random() * noiseZ;
    }
    return geometry;
}
Adding ‘noise’ can make objects feel more organic.

Making movements.

So far we’ve only made a single render call to our WebGLRenderer. To add some movement to our Scene, we’ll need to make some updates.

See the Pen Step Four
by Matt Agar (@agar)
on CodePen.

Behold the baubles’ slow, hypnotic twirl.

The render loop

To time our updates at a speed the browser can handle, we’re making use of the browser requestAnimationFrame API to call to a new render function.

requestAnimationFrame(render);
function render() {
    // Update camera position based on the controls
    controls.update();

    // Re-render the scene
    renderer.render(scene, camera);

    // Loop
    requestAnimationFrame(render);
}
Creating a render loop with the requestAnimationFrame.

Updating elements over time.

Now, I’ll make a few changes to the complex objects, initialising a random rotation speed for the decorations with each instance creation.

this.rotationSpeed = Math.random() * 0.02 + 0.005;
this.rotationPosition = Math.random();
Getting in a spin.

We also set up a new function that can be called to increment rotation around the Y-axis based on current values. Note the rotation speed will be based on the frame rate achieved by the browser in this case, but that’s fine for this simple example. For processes like this, maths is your friend.

Decoration.prototype.updatePosition = function() {
    this.rotationPosition += this.rotationSpeed;
    this.rotation.y = (Math.sin(this.rotationPosition));
};
Observe: a decoration rotation situation.

With a new update function defined, we can update the render loop to recalculate the position of each element created each time it runs.

function render() {
    // Update camera position based on the controls
    controls.update();

    // Loop through items in the scene and update their position
    for(var d = 0; d < decorations.length; d++) {
        decorations[d].updatePosition();
    }

    // Re-render the scene
    renderer.render(scene, camera);

    // Loop
    requestAnimationFrame(render);
}
Recalculating element positions.

Putting it all together.

See the Pen Step Five
by Matt Agar (@agar)
on CodePen.

The 3D tree: fully formed and decorated to perfection.

Here it is, the finished product. Using nothing more than the basics, we’ve built an interactive 3D tree and brought a flat, two dimensional scene to life.

But this is just the beginning of the WebGL journey. While the technology’s incredibly powerful as is, there are a multitude of additional resources, helpers, and tutorials to guide you on your way. Some highlights include:

So what are you waiting for? Experiment with WebGL and Three.js, and start creating your own 3D animations. Be sure to let me know below if you make something interesting. I’d love to check it out!

  • Neil

    Hello Matt,

    Great article. I have a question.

    You have used fairly simple vector graphics in your demo but I notice that the examples at the start of the article (eg. Porsche) use much more complex imagery.

    What is the difference? I notice the Porsche page needs to load. Is this because the images are larger?

    Thanks,

    Neil

    • Matt Agar

      Hey Neil. There’s a number of different ways you can create objects within a scene. My examples build up the scene in code from simple primitives. The Porsche example on the other hand loads in around 35MB of external model data that will have been created using 3D modelling software. In this case, they are loaded in the Three.js JSON Model format (https://github.com/mrdoob/three.js/wiki/JSON-Model-format-3), however there’s a number of different loaders you can use to get geometry into a scene (check out the examples at https://threejs.org/examples/?q=loader)