Discover three.js has been updated to ES6!

You are viewing the new version of the book.

Click here to view the old version.

Word Count: 5527, reading time: ~26minutes

The Animation Loop

The output from a single call of
renderer.render

Over the last couple of chapters, we’ve made amazing progress with our app. We have lights, colors, physically correct rendering, anti-aliasing, automatic-resizing, we know how to move objects around in 3D space, and our code is clean, modular, and well-structured. But our scene is missing one vital ingredient: movement!

We’re using the renderer.render method to draw the scene. This method takes a scene and a camera as input and outputs a single still image to the HTML <canvas> element. The output is the non-moving purple box you can see above.

World.js: drawing a single frame with renderer.render
    
  render() {
    // draw a single frame
    renderer.render(scene, camera);
  }

  

In this chapter, we’ll add a simple rotation animation to the cube. Here’s how we’ll do it:

  • call renderer.render(...)
  • wait until it’s time to draw the next frame
  • rotate the cube a tiny amount
  • call renderer.render(...)
  • wait until it’s time to draw the next frame
  • rotate the cube a tiny amount
  • call renderer.render(...)
  • wait until it’s time to draw the next frame
  • rotate the cube a tiny amount

… and so on in an endless loop called an animation loop. Setting up this loop is simple since three.js does all the hard work for us via the renderer.setAnimationLoop method.

We’ll also introduce the three.js Clock in this chapter, a simple stopwatch class that we can use to keep animations in sync. We’ll be dealing with time values less than one second throughout this chapter, so we’ll use milliseconds (ms), which are thousandths of a second.

Once we’ve set up the loop, our goal is to generate a steady stream of frames at a rate of sixty frames a second (60FPS), which means we need to call .render approximately once every sixteen milliseconds. In other words, we need to ensure that all of the processing we do in a frame takes less than 16ms (this is sometimes referred to as a frame budget). That means we need to update animations, perform any other tasks that need to be calculated across frames (such as physics), and render the frame, in less than sixteen milliseconds on the lowest spec hardware that we intend to support. Over the rest of this chapter, as we set up the loop and create a simple rotating animation for the cube, we’ll discuss how best to achieve this.

Similarities with the Game Loop

Most game engines use the concept of a game loop that runs once per frame and is used to update and render the game. A basic game loop might consist of these four tasks:

  1. Get user input
  2. Calculate physics
  3. Update animations
  4. Render a frame

Even though three.js is not a game engine and we are calling our loop an animation loop, our goals are pretty similar. This means, instead of starting from scratch, we can borrow some tried and trusted ideas from game engine design. The loop we create in this chapter is very simple, but if you later find yourself needing a more complex one, perhaps to update animations and physics at a different rate than you render the scene, you can refer to a book on game development for more info.

Later, we’ll make our scene interactive. Fortunately for us, handling user input in the browser is easy thanks to addEventListener, so we don’t need to handle this task in the loop. Also, we won’t be doing any physics calculations for now (although several great physics libraries work with three.js), so we can skip the physics step. Rendering is already covered by renderer.render. That leaves us with two tasks in this chapter: set up the loop itself, and then create a system for updating animations.

We’ll set up the loop first to generate a stream of frames, and then we’ll set up the animation system.

Creating an Animation Loop with three.js

The Loop.js Module

Open (or create) the systems/Loop.js module and create a new Loop class inside. This class will handle all the looping logic and the animation system. You’ll notice that we have imported Clock, which we’ll use below to keep animations in sync. Next, since we’ll use renderer.render(scene, camera) to generate frames, it’s a fair bet we’ll need the camera, scene, and renderer within the Loop class, so pass them to the constructor and save them as instance variables. Finally, create .start and .stop methods that we can later use to start/stop the loop.

Loop.js: initial setup
    

import { Clock } from 'three';

class Loop {
  constructor(camera, scene, renderer) {
    this.camera = camera;
    this.scene = scene;
    this.renderer = renderer;
  }

  start() {}

  stop() {}
}

export { Loop }


  

Over in World, add this new class to the list of imports:

World.js: import the Loop class
    import { createCamera } from './components/camera.js';
import { createCube } from './components/cube.js';
import { createLights } from './components/lights.js';
import { createScene } from './components/scene.js';

import { createRenderer } from './systems/renderer.js';
import { Resizer } from './systems/Resizer.js';
import { Loop } from './systems/Loop.js';

  

Create the loop as a module scoped variable like the camera, renderer, and scene, since we don’t want it to be accessible from outside the World class:

World.js: create a loop instance
    

let camera;
let renderer;
let scene;
let loop;

class World {
  constructor(container) {
    camera = createCamera();
    renderer = createRenderer();
    scene = createScene();
    loop = new Loop(camera, scene, renderer);
    container.append(renderer.domElement);

    ...
  }


  

Finally, add .start and .stop methods to World, which simply call their counterparts in Loop. This is how we’ll provide access to the loop from within main.js:

World.js: create the .start and .stop methods
      render() {
    // draw a single frame
    renderer.render(scene, camera);
  }

  start() {
    loop.start();
  }

  stop() {
    loop.stop();
  }

  

Then, over in main.js, switch out world.render:

main.js: render a single still frame
    function main() {
  // Get a reference to the container element
  const container = document.querySelector('#scene-container');

  // create a new world
  const world = new World(container);

  // draw the scene
  world.render();
}

  

… for world.start:

main.js: start the animation loop
    function main() {
  // Get a reference to the container element
  const container = document.querySelector('#scene-container');

  // create a new world
  const world = new World(container);

  // start the animation loop
  world.start();
}

  

The scene will go black when you do this, but don’t worry. It’ll spring back to life again in a few moments once we have finished creating the loop.

Creating the Loop with .setAnimationLoop

Now, everything is set up and we can create the loop. As we mentioned above, we don’t need to worry about the technicalities of creating an animation loop since three.js provides a method that does everything for us: WebGLRenderer.setAnimationLoop.

Creating a loop using .setAnimationLoop
    
import { WebGLRenderer } from 'three';

const renderer = new WebGLRenderer();

// start the loop
renderer.setAnimationLoop(() => {
  renderer.render(scene, camera);
});

  

This will call renderer.render over and over to generate a stream of frames. We can cancel a running loop by passing null as the callback:

Stop a running loop
    
// stop the loop
renderer.setAnimationLoop(null);

  

Internally, the loop is created using .requestAnimationFrame. This built-in browser method intelligently schedules frames in sync with the refresh rate of your monitor and will smoothly reduce the frame rate if your hardware can’t keep up. Since .setAnimationLoop was added fairly recently, older three.js examples and tutorials often use .requestAnimationFrame directly to set up the loop, and it’s fairly simple to do it that way. However, with .setAnimationLoop there’s a little extra magic to ensure the loop will work in virtual reality and augmented reality environments.

The Loop.start and Loop.stop Methods

Now, we can create the loop. We’ll do it in Loop.start using .setAnimationLoop:

Loop.js: create the .start method
      start() {
    this.renderer.setAnimationLoop(() => {
      // render a frame
      this.renderer.render(this.scene, this.camera);
    });
  }


  

Next, create the counterpart .stop method, passing in null as the callback to stop the loop:

Loop.js: create the .stop method
      stop() {
    this.renderer.setAnimationLoop(null);
  }

  

As soon as you make these changes, your app will start to pump out frames at a rate of around sixty per second (or possibly higher, depending on the refresh rate of your monitor). However, you won’t see any difference. Nothing is moving yet, so we are simply drawing the same frame over and over. Our loop now looks like this:

  • call renderer.render(...)
  • wait until it’s time to draw the next frame
  • call renderer.render(...)
  • wait until it’s time to draw the next frame
  • call renderer.render(...)
  • wait until it’s time to draw the next frame

If you compare that to the loop we described at the start of the chapter, you’ll see we are missing a vital step:

  • call renderer.render(...)
  • wait until it’s time to draw the next frame
  • rotate the cube a tiny amount
  • call renderer.render(...)
  • wait until it’s time to draw the next frame
  • rotate the cube a tiny amount
  • call renderer.render(...)
  • wait until it’s time to draw the next frame
  • rotate the cube a tiny amount

We need some way to adjust the cube’s rotation right before we render each frame, and we need to do so in a way that works for any kind of animated object, not just a rotating cube. More generally, our loop should look like this:

  • call renderer.render(...)
  • wait until it’s time to draw the next frame
  • move animations forward one frame
  • call renderer.render(...)
  • wait until it’s time to draw the next frame
  • move animations forward one frame
  • call renderer.render(...)
  • wait until it’s time to draw the next frame
  • move animations forward one frame

Remove the onResize Hook

First, let’s tidy up. Now that the loop is running, whenever we resize the window a new frame will be produced on the next iteration of the loop. This is fast enough that you won’t notice any delay so we don’t need to manually redraw the scene on resizing anymore. Remove the resizer.onResize hook from World:

World.js: remove the highlighted lines
    

constructor(container) {
  camera = createCamera();
  scene = createScene();
  renderer = createRenderer();
  container.append(renderer.domElement);

  const cube = createCube();
  const light = createLights();

  updatables.push(cube);

  scene.add(cube, light);

  const resizer = new Resizer(container, camera, renderer);
  resizer.onResize = () => {
    this.render();
  };
}


  

Now, try resizing the scene and notice that it works smoothly. This shows us that the loop is running correctly.

The Animation System

Consider a simple game where you explore a map and pick apples. Here are some animated objects you might add to this game:

  • The heroine, who has various animations like walk/run/jump/climb/pick.
  • Trees with apples. The apples grow over time, and the leaves blow in the wind.
  • Some scary bees that will try to chase you from the garden.
  • An interesting environment with objects like water, wind, leaves, and rocks.
  • Power-ups in the form of rotating cubes that hover above the ground.

… and so on. Each time the loop runs, we want to update all of these animations by moving them forward one frame. Just before we render each frame, we’ll make the heroine step forward a tiny bit, we’ll make each bee move towards her, we’ll make the leaves move, the apples grow, and the powerups rotate, each by a tiny, tiny amount that is almost too small for the eye to see but over time creates a smooth animation.

The Loop.tick Method

To handle all of this, we need a function that updates all the animations, and this function should run once at the start of each frame. However, the word update is already used a lot throughout three.js, so we’ll choose the word tick instead. Before we draw each frame, we’ll make each animation tick forward one frame. Add the Loop.tick method at the end of the Loop class, and then call it within the animation loop:

Loop.js: create the .tick method
    

start() {
  this.renderer.setAnimationLoop(() => {
    // tell every animated object to tick forward one frame
    this.tick();

    // render a frame
    this.renderer.render(this.scene, this.camera);
  });
}

stop() {
  this.renderer.setAnimationLoop(null);
}

tick() {
  // Code to update animations will go here
}


  

Centralized or Decentralized?

When it comes to implementing this new .tick method, we have to make some design choices. One obvious solution is to create a complicated, centralized update function that controls all of the animated objects in our scene. It might look something like this:

A centralized animation system
    

tick() {
  if(controls.state.run) {
    character.runAnimation.nextFrame();
  }

  beeA.moveTowards(character.position);
  beeB.moveTowards(character.position);
  beeC.moveTowards(character.position);

  powerupA.rotation.z += 0.01;
  powerupB.rotation.z += 0.01;
  powerupC.rotation.z += 0.01;

  leafA.rotation.y += 0.01;

  // ... and so on
}


  

Well, you get the picture. This might be ok if we have just a couple of animated objects in our scene, but it’s not going to scale well. With fifty or a hundred animated objects, it’s going to be downright ugly. It also breaks all kinds of software design principles, since now the Loop class has to have a deep understanding of how each animated object works.

Here’s a better idea: we’ll define the logic for updating each object on the object itself. Each object will expose that logic using a generic .tick method of its own. Now, the Loop.tick method will be simple. Each frame, we’ll loop over a list of animated objects and tell each of them to .tick forward by one frame. It will look something like this:

A decentralized animation system
    


// somewhere in the Loop class:
this.updatables = [character, beeA, beeB, beeC, powerupA, powerupB, powerupC, leafA, ... ]
...

tick() {
  for(const object of this.updatables) {
    object.tick();
  }
}


  

This is much better. Now, all the Loop class knows is that ‘animated objects have a .tick method’. These methods can be as complex or simple as needed for each object. For example, here’s what a simple rotating powerup might look like:

Creating a rotating powerup with a .tick method
    

function createPowerup() {
  const geometry = new BoxBufferGeometry(2, 2, 2);
  const material = new MeshStandardMaterial({ color: 'purple' });
  const powerup = new Mesh(geometry, material);

  // this method will be called once per frame
  powerup.tick = () => {
    // increase the powerup's rotation each frame
    powerup.rotation.z += 0.05;

  };

  return powerup;
}


  

If you compare this to components/cube.js, you’ll see this is quite similar. We just need to add a cube.tick method.

This approach fits better with the modular philosophy we’re using to design our application. Instead of having one part of the app grow more and more complicated, we’ll break the complexity into small pieces, with each piece of logic defined at the place where it’s used. This way, we can design each object as a self-contained entity. Every object, from the humble spinning cube to the apple picking heroine, will encapsulate its behavior. This is a powerful concept which we’ll build on throughout the book.

Loop.updatables

For this to work, we need a list of animated objects within the loop class. We’ll use a simple array for this purpose, and we’ll call this list updatables. Go ahead and create it now.

Loop.js: create a list to hold animated objects
      constructor(camera, scene, renderer) {
    this.camera = camera;
    this.scene = scene;
    this.renderer = renderer;
    this.updatables = [];
  }

  

Next, within Loop.tick, loop over this list and call .tick on any object within it.

Loop.js: loop over animated objects and call their .tick method
    

tick() {
  for (const object of this.updatables) {
    object.tick();
  }
}


  

Take careful note of the fact that Loop.tick will run every frame, which means it will run sixty times per second. It’s important to keep the amount of work done here to a minimum, which means that each animated object’s .tick method method must be as simple as possible.

The cube.tick Method

Before we can add cube to the updatables list, it needs a .tick method, so go ahead and create one. This .tick method is where we’ll define the logic for rotating the cube.

Each type of animated object will have a different .tick method. In our apple picking game, the heroine’s tick method will check whether she is walking, running, jumping, or standing still, and then play a frame from one of those animations, while the apple tree’s tick method will check the ripeness of the apples and rustle the leaves, and each of the evil bee’s tick methods will check the position of the heroine then move the bee towards her a tiny bit. If she is close enough, the bee will attempt to sting her.

Here, we’ll simply update the cube’s rotation on the $X$, $Y$, and $Z$ axes by a tiny amount each frame. This will give it a random-looking tumble.

cube.js: create the .tick method
    

function createCube() {
  const geometry = new BoxBufferGeometry(2, 2, 2);
  const material = new MeshStandardMaterial({ color: 'purple' });
  const cube = new Mesh(geometry, material);

  cube.rotation.set(-0.5, -0.1, 0.8);

  // this method will be called once per frame
  cube.tick = () => {
    // increase the cube's rotation each frame
    cube.rotation.z += 0.01;
    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;
  };

  return cube;
}


  

Note: adding a property to an existing class at run-time like this is known as monkey-patching (here, we’re adding .tick to an instance of Mesh). It’s common practice, and in our simple app won’t cause any problems. However, we shouldn’t get into the habit of doing this carelessly since in certain situations it can cause performance issues. We’ll only allow ourselves to do this here as the alternatives are more complex.

0.01 is a value that gives a fairly slow rotation speed, and we discovered it by trial and error. Rotations in three.js are measured in radians so internally this value is being interpreted as 0.01 radians, which is roughly half a degree. So, we’re rotating the cube by about half a degree on each axis every frame. At sixty frames a second, this means our cube will rotate $60 \times 0.5 = 30 ^{\circ}$ each second, or one full rotation around each of the $X$, $Y$ and $Z$ axes approximately every twelve seconds.

Add the cube to Loop.updatables

Next, over in World, add the cube to the the Loop.updatables list.

World.js: add the cube to Loop.updatables
      constructor(container) {
    camera = createCamera();
    renderer = createRenderer();
    scene = createScene();
    loop = new Loop(camera, scene, renderer);
    container.append(renderer.domElement);

    const cube = createCube();
    const light = createLights();

    loop.updatables.push(cube);

    scene.add(cube, light);

    const resizer = new Resizer(container, camera, renderer);
  }

  

Right away, the cube should start rotating.

Timing in the Animation System

Look at this sentence again: at sixty frames a second, this means our cube will rotate $60 \times 0.5 = 30 ^{\circ}$ each second, or one full rotation around each of the $X$, $Y$ and $Z$ axes approximately every twelve seconds. But, what if our app is not running at sixty frames a second? If it’s running at slower than 60FPS the animation will run slowly, while if it runs faster, the animation will run faster. In other words, the speed of our animation depends on the device it’s being viewed on. Not good. To understand how to fix this, we need to take a deeper look at what we mean by the word frame.

Fixed and Dynamic Frames

There’s an important distinction between the kind of frames we are talking about in this chapter and the kind of frames that make up television shows or movies. Frame rates in film are fixed. Movies are usually shot at 24 frames per second (FPS), while the standard for television shows is 30FPS, although some newer shows may be filmed at 60FPS. Whatever frame rate is chosen, that rate won’t change for the entire duration of the movie or show.

However, our animation loop doesn’t generate frames at a fixed rate. The loop will attempt to render frames at the hardware-defined refresh rate of your screen (behind the scenes the browser is using .requestAnimationFrame to do this). At the time of writing, most screens have a 60Hz refresh rate, but this value can be as high as 240Hz on new screens, while in VR it will be at least 90Hz. This means, on a 60Hz screen, the target frame rate is 60FPS, on a 90Hz screen, the target frame rate is 90FPS, and so on.

However, we might not succeed in generating frames that quickly. If the device your app is running on is not be powerful enough to reach the target frame rate, the animation loop will run more slowly. Even on fast hardware, your app will have to share computing resources with other applications, and there may not always be enough to go around. In each of these cases, the animation loop will generate frames at a lower rate, and this rate may fluctuate from one moment to the next depending on many factors. This is called a variable frame rate.

That means, as we have currently set up the animation of our cube, it will rotate more slowly on an old, slow device, while on fancy new 240Hz gaming monitor it will go into hyper-speed. $240 = 4\times60$, meaning the cube will rotate at four times the desired speed!

To prevent this, we need to decouple animation speed from frame rate. Here’s how we’ll do it: when we tell an object to .tick forward a frame, we’ll scale the size of the movement by how long the previous frame took. This way, as the frame rate varies, we’ll constantly adjust the size of each .tick so that the animation remains smooth. Our adjustments will always be one frame behind, but the frames are generated so quickly this won’t be visible to the user. This way, animations will run at the same speed on all devices.

Measuring Time Across Frames

This is where the Clock class comes in. We’ll use Clock.getDelta to measure how long the previous frame took.

The Clock.getDelta method
    
import { Clock } from 'three';

const clock = new Clock();

const delta = clock.getDelta();

  

.getDelta tells us how much time has passed since the last time we called .getDelta. If we call it once, and only once, at the start of each frame, it will tell us how long the previous frame took. Note: if you call it .getDelta more than once per frame, subsequent calls will measure close to zero. Only call .getDelta once at the very start of a frame!

Create a clock

Over in Loop, create a module scoped clock instance at the top of the file.

Loop.js: create the clock
    import { Clock } from 'three';

const clock = new Clock();

class Loop {
  ...
  

Call .getDelta at the Start of Each Frame

Next, we’ll call .getDelta at the start of Loop.tick, saving the result in a variable called delta which we’ll then pass into the .tick method of each animated object.

Loop.js: pass time deltas to animated objects
      tick() {
    // only call the getDelta function once per frame!
    const delta = clock.getDelta();

    for (const object of this.updatables) {
      object.tick(delta);
    }
  }

  

Scale the Cube’s Rotation by delta

Scaling movements by delta is easy. We simply decide how much we want to move an object in one second, and then multiply that value by delta within the objects .tick method. In cube.tick, we found a value that resulted in the cube rotating approximately thirty degrees a second at 60FPS.

cube.js: the unscaled tick method
    

cube.tick = () => {
  // increase the cube's rotation each frame
  cube.rotation.z += 0.01;
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;
};


  

Now, we’ll fix that so the cube rotates thirty degrees per second at any FPS. First, we need to convert thirty degrees to radians, and for that, we’ll use MathUtils.degToRad method (refer back to the transformations chapter if you need a reminder of how that works):

Converting degrees to radians
    
import { MathUtils } from 'three';

const radiansPerSecond = MathUtils.degToRad(30);

  

Next, we’ll scale radiansPerSecond by delta each frame.

cube.js: the updated tick method, now scaling by delta
    

cube.tick = (delta) => {
  // increase the cube's rotation each frame
  cube.rotation.z += radiansPerSecond * delta;
  cube.rotation.x += radiansPerSecond * delta;
  cube.rotation.y += radiansPerSecond * delta;
};


  

Putting all that together, here’s our final cube.js module:

cube.js: final code
    import {
  BoxBufferGeometry,
  MathUtils,
  Mesh,
  MeshStandardMaterial,
} from 'three';

function createCube() {
  const geometry = new BoxBufferGeometry(2, 2, 2);
  const material = new MeshStandardMaterial({ color: 'purple' });
  const cube = new Mesh(geometry, material);

  cube.rotation.set(-0.5, -0.1, 0.8);

  const radiansPerSecond = MathUtils.degToRad(30);

  // this method will be called once per frame
  cube.tick = (delta) => {
    // increase the cube's rotation each frame
    cube.rotation.z += radiansPerSecond * delta;
    cube.rotation.x += radiansPerSecond * delta;
    cube.rotation.y += radiansPerSecond * delta;
  };

  return cube;
}

  

Now, once again the cube will be rotating thirty degrees per second around each axis, but with an important difference: the animation will now play at the same speed no matter where we run it, whether on a VR rig running at 90FPS, or a ten-year-old smartphone that can barely crank out 10FPS, or some future system from the year 3000 that runs at a billion FPS. The frame rate may change, but the animation speed will not.

With this change, we have successfully decoupled animation speed from frame rate.

To Loop or Not to Loop

Now that we’ve started the loop, .render is being called over and over, creating a steady stream of frames, and before we render each frame, we’re rotating the cube by a tiny amount. As long as the frames are being generated with sufficient speed (around 12FPS or above), and the difference between successive frames is small enough, we’ll perceive this as an animation.

The animation loop will be the driving force of many apps. This loop, when combined with the idea of encapsulating the animation logic in each object’s .tick method, is a powerful tool that we’ll continue to explore and build on throughout the book. Later, we’ll use the loop to drive behavior that is much more complex and interesting than our simple rotating cube, either created in our code or loaded from an external application.

An animation created in code
An animation created in an external application

Animations like these are beautiful. However, they come at a cost, which will probably be obvious to you right now if you are viewing this on a low-powered device. As you chase the goal of sixty frames per second, you must work hard to keep the loop running fast. This is one place in your app where constant vigilance, profiling, and optimization is a necessity.

Not all scenes have animation though. Some scenes update only occasionally, for example, only during user interaction. A common example of this is a product display app. Such apps are used to display a 3D product such as a shoe or milk bottle that the user can rotate or zoom to get a better look. In this type of scene, whenever the user is not interacting, the scene will remain unchanged between frames. Here’s another example of a scene without an animation loop.

No animation loop doesn’t mean no movement!

Running the loop for an app like this would be a waste. This will be especially noticeable on mobile devices, where the constant GPU and CPU churn of the loop will drain the battery. As a result, you should only use the loop when you need to.

World.render and World.start give us two ways of producing frames. For apps with constant animation, we’ll use .start to run a loop, and for apps that update occasionally, we’ll call .render whenever a new frame is needed. We’ll refer to the second technique as rendering on demand.

main.js: two ways of producing frames
    
const world = new World();

// produce a single frame (render on demand)
world.render();

// start the loop (produce a stream of frames)
world.start();

  

Rendering on demand may reduce battery use, but on the other hand, using the loop is simpler. Instead of thinking about where and when you need to draw frames, you simply churn out a constant, steady supply, and for this reason, most of the examples in this book will use the loop. However, this is not an endorsement of the loop over rendering on demand. It’s up to you to decide which method is appropriate for your app.

Next up, we’ll see how to make our materials more interesting using textures.

Challenges

Import Style
Selected Texture