LIGHTS! COLOR! ACTION!

Picking up where we left off in the last chapter, we’re going to add some life to our scene with animation, color, and lights.

Open up your code from the last chapter, or use this CodeSandbox which has the code from Chapter 1.1 already completed.

Code Organisation

Let’s take some time to structure our code better. So far we’ve just been adding lines one after another as we go, which was fine when our app was just a couple of lines long. But as our app grows in size, this will quickly become confusing.

We’ll wrap everything we’ve written so far in a function called init(), except for the line renderer.render( scene, camera ) which will go inside an animate() function. Once we’ve done this, our code will look like this:

// these need to be accessed inside more than one function so we'll declare them first
let container;
let camera;
let renderer;
let scene;
let mesh;

function init() {

  // Get a reference to the container element that will hold our scene
  container = document.querySelector( '#scene-container' );

  // create a Scene
  scene = new THREE.Scene();

  scene.background = new THREE.Color( 0x8FBCD4 );

  // set up the options for a perspective camera
  const fov = 35; // fov = Field Of View
  const aspect = container.clientWidth / container.clientHeight;

  const near = 0.1;
  const far = 100;

  camera = new THREE.PerspectiveCamera( fov, aspect, near, far );

  // every object is initially created at ( 0, 0, 0 )
  // we'll move the camera back a bit so that we can view the scene
  camera.position.set( 0, 0, 10 );

  // create a geometry
  const geometry = new THREE.BoxBufferGeometry( 2, 2, 2 );

  // create a default (white) Basic material
  const material = new THREE.MeshBasicMaterial();

  // create a Mesh containing the geometry and material
  mesh = new THREE.Mesh( geometry, material );

  // add the mesh to the scene object
  scene.add( mesh );

  // create a WebGLRenderer and set its width and height
  renderer = new THREE.WebGLRenderer();
  renderer.setSize( container.clientWidth, container.clientHeight );

  renderer.setPixelRatio( window.devicePixelRatio );

  // add the automatically created <canvas> element to the page
  container.appendChild( renderer.domElement );

}

function animate() {

  // render, or 'create a still image', of the scene
  renderer.render( scene, camera );

}

// call the init function to set everything up
init();

// then call the animate function to render the scene
animate();

Code organization is extremely important and will make any piece of software much easier for both you and other people to understand and maintain. We’ll be further improving our code structure as we go, but for now, it looks pretty good. Let’s move on.

Introducing the Animation Loop

You may have noticed that, in our reorganized code, the animate() function is badly named - as the comment in the code points out, renderer.render( scene, camera ) renders a still image of the scene from the point of view of the camera - and it seems a little unfair to call a single image an animation.

Let’s fix that. We want to call animate() once before each frame so that we can perform any updates to our scene and then render a new frame.

To do so, we’ll use a method that is built into every modern browser, called requestAnimationFrame(), or window.requestAnimationFrame to give it it’s full title:

Recursively Calling animate() Using requestAnimationFrame()

The updated, recursive animate function

function animate() {

  // call animate recursively
  requestAnimationFrame( animate );

  // render, or 'create a still image', of the scene
  // this will create one still image / frame each time the animate
  // function calls itself
  renderer.render( scene, camera );

}

Using requestAnimationFrame is pretty straightforward - the trick is to call it recursively. A recursive function is simply a function that calls itself repeatedly - refer back to our brief JavaScript Tutorial in the intro if this concept is unfamiliar to you.

So, we’ll call our animate function within our animate function, using requestAnimationFrame to handle the timing. Once we’ve done so, our app should be updating at a smooth 60 frames per second.

Well, except of course, that our scene doesn’t actually look any different. It may be updating at up to 60 frames per second, but nothing is moving so we can’t see that this happening.

We’ll add some movement soon, but first, let’s add some lights and a splash of color.

Color in three.js

We saw the Color object in the last chapter when we set the background to a light blue color using scene.background = new THREE.Color( 'skyblue' ).

As we mentioned then, we can use any of the CSS color names here. However, there are only 140 of them. This might seem like a lot, but it’s a long way from the 16,777,216 colors that a standard computer monitor can display. How do we go about specifying the rest?

Actually, there are a number of ways, as you’ll see if you take a look at the Color docs page. We’ll mainly be sticking with one in this book though since it’s the same method most often used in CSS: Hexadecimal Triplets, commonly known as Hex Colors.

Setting a Material’s Color

  // create a geometry
  const geometry = new THREE.BoxBufferGeometry( 2, 2, 2 );

  // create a purple Basic material
  const material = new THREE.MeshBasicMaterial( { color: 0x800080 } );

  // create a Mesh containing the geometry and material
  mesh = new THREE.Mesh( geometry, material );

Let’s use the hex code 0x800080 (purple) to set our material’s color. This time we don’t need to use the Color constructor, we can just pass in color: 0x800080 as a parameter to the Material constructor and it will call the Color constructor automatically for us.

The color instance that gets created is stored in material.color, which we can use if we want to update the material’s color at any time.

The important thing to remember here is that material.color is an instance of Color - we’ll need to use the Color.set( newColor ) method if we want to update it. For example, to change the material’s color to red we can do this: material.color.set( '0xff0000' ).

Switch to a Higher Quality Material

Change your old, boring Basic material

const material = new THREE.MeshBasicMaterial( { color: 0x800080 } );

…into a fancy new physically-correct Standard material

  // create a purple Standard material
  const material = new THREE.MeshStandardMaterial( { color: 0x800080 } );

We’ll switch our MeshBasicMaterial for a higher quality (but lower performance) MeshStandardMaterial.

The MeshBasicMaterial material doesn’t react to lights at all, whereas the MeshStandardMaterial material is a physically correct material, meaning that it reacts to light in the same way an object in the real world does, or at least with a fairly good approximation.

The Basic and Standard materials lie at the two extremes of the performance/quality spectrum in three.js, and there are plenty of other material types available in between, as we’ll see in Section 4: Materials And Textures.

Let’s switch over our material …And our scene has gone completely black. Great.

Add Light

Add a directional light

  scene.add( mesh );

  // Create a directional light
  const light = new THREE.DirectionalLight( 0xffffff, 5.0 );

  // move the light back and up a bit
  light.position.set( 10, 10, 10 );

  // remember to add the light to the scene
  scene.add( light );

  // create a WebGLRenderer and set its width and height

So we need a light then? No problem. As you might expect by now, there are lots of options, as we’ll see in Section 5: Lights And Shadows.

For now, we’ll choose a DirectionalLight. Directional lights mimic light from a distant source such as the sun, and shine from a position, towards a target. By default, this target is at $(0, 0, 0)$.

We’ve moved the light back and up a bit by setting its position to $( 10, 10, 10 )$, so the light is now shining from $( 10, 10, 10 )$ towards $(0, 0, 0 )$.

Once you’ve added the light, your mesh should be visible again. Not only that, but it’s now a nice purple color, set against an equally nice light blue background. Things are looking sweet, although we’re facing our cube head on so it still looks like a square.

Purple square, blue background

Add Movement

Add some action to the scene by rotating the box at the start of each frame, in the animate function

function animate() {

  // call animate recursively
  requestAnimationFrame( animate );

  // increase the mesh's rotation each frame
  mesh.rotation.z += 0.01;
  mesh.rotation.x += 0.01;
  mesh.rotation.y += 0.01;

  // render, or 'create a still image', of the scene
  // this will create one still image / frame each time the animate
  // function calls itself
  renderer.render( scene, camera );

}

As noted above, our scene should now be animating nicely at somewhere close to 60 frames per second, but we have no way of telling this since nothing is moving. We want to keep things simple for now, so we’ll just add a small amount of rotation to the cube each frame to give it a random looking tumble.

We’ll add $0.01$ to the $X$, $Y$, and $Z$ components of the cube’s rotation each frame.

The above changes will give our cube a nice random looking tumble:

Anti-Aliasing

Add anti-aliasing to the WebGLRenderer

  scene.add( light );

  // create a WebGLRenderer and set its width and height
  renderer = new THREE.WebGLRenderer( { antialias: true } );
  renderer.setSize( container.clientWidth, container.clientHeight );
Anti-alias on and off

We’ll finish up by making one final change, which will improve the look of our scene a lot. We’ll pass in a new parameter to the WebGLRenderer, setting the antialias property to true. Without going into any great detail here, this makes jagged lines look much smoother.

Just as with the material constructor, we need to pass a spec object with named parameters into the WebGLRenderer constructor. However, unlike the material.color, we can’t change this setting after the renderer has been created. If we want to change it we’ll need to create an entirely new renderer.

When we turn this setting on, anti-aliasing is performed using the built-in WebGL method, which is currently multisample anti-aliasing. Depending on your browser and graphics card, there is a chance that this will be unavailable or disabled, although on modern hardware that is unlikely.

The Effects of Lighting

With the above changes, our app finally looks 3D! Well done. Try changing back and forth between MeshStandardMaterial (left) and MeshBasicMaterial (right) to see how the lights make an object look 3D:

Final Result

We’ve already covered quite a bit of theory and hopefully, you are beginning to get a feel for how a basic three.js app is put together. In the next chapter, we’ll further improve the structure of our code and take a look at how to make our app resize automatically when the browser window size changes, for example when a user changes their phone from portrait to landscape or drags the window on their computer.