ThreeJs Improving Our Animation Loop and Adding Automatic Resizing

IMPROVING OUR ANIMATION LOOP AND ADDING AUTOMATIC RESIZING

Welcome back! Here’s where we finished up at the end of the last chapter:
It’s a very respectable result for such a small amount of code.
However, there is a problem with our app that will quickly make it look at lot less professional to our users - that is, our scene doesn’t resize when the browser window changes size, such as when the user resizes the browser on their laptop, or when they change from landscape to portrait mode on their phone or tablet.
We’ll fix that in just a moment. First, let’s take a look at a couple of things we can do to improve our code and make sure it’s future proof as our app grows in complexity.

Improving Our Animation Loop


Our current animation loop 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 );

}
Take a look at the animate() function. Ignoring requestAnimationFramefor now, we can see that it’s currently doing two things - first, it’s updating the rotation of the mesh, and then it’s rendering the scene.

Introducing the Game Loop

Most game engines use the concept of a game loop, which is called once per frame and is used to update the game and then render the scene. A minimal game loop might look something like this:
  1. Get user input
  2. Update animations
  3. Render the frame
Looks familiar? Even though three.js is not a game engine and we are calling our loop an animation loop rather than a game loop, most of the same logic applies here, so we’ll take some ideas for this part of our app from game engine theory.

Split the Animation Loop into update() and render()

We’re not currently getting any user input so we’ll ignore step 1 for now and come back to it in Chapter 1.5: Camera Controls. That leaves the last two steps, “Update animations”, and “Render the frame”. So let’s split our app into two functions called update() and render(), adding these functions to the end of our app.js file, just before the call to init().

The update() Function

// perform any updates to the scene, called once per frame
// avoid heavy computation here
function update() {

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

}

// render, or 'draw a still image', of the scene
function render() {

  renderer.render( scene, camera );

}

// call the init function to set everything up
init();
Anything that involves updating the scene should go in here. The only thing that we’re currently updating each frame is the rotation of the mesh, so move those three lines into this function.
In a more complex app, this function could be doing a lot more. For example, if we were creating a driving game it would be calculating the direction, position, and velocity of each car from frame to frame. Physics is usually calculated separately to this function though, often on a separate thread.

The render() Function

// perform any updates to the scene, called once per frame
// avoid heavy computation here
function update() {

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

}

// render, or 'draw a still image', of the scene
function render() {

  renderer.render( scene, camera );

}

// call the init function to set everything up
init();
We’re currently rendering our frame using a single line, so it may seem like overkill to put it inside its own function and generally, this function won’t get thatmuch more complicated.
However, you might want to do things like post-processing, or drawing to a separate frame buffer, or scissor tests or… OK, sorry, we won’t introduce too much advanced terminology just yet. We’ll explore these in much more detail in Section 8: The WebGLRenderer.
For now, we’ll just keep the call to renderer.render() separate to make sure our app is fully future-proof.

Introducing the setAnimationLoop Method

Virtual Reality devices handle requestAnimationFrame() differently than normal web pages. This means that our current animate function will not work as a WebVR app.
To make dealing with this easier, a new method called setAnimationLoop was recently added to the WebGLRenderer. This handles setting up of the animation loop for us and makes sure that it works no matter what kind of device we are viewing our app on.
As an added bonus using this method actually makes our code a little cleaner, since calling requestAnimationFrame is handled automatically for us.






Using setAnimationLoop()


Setting up the animation loop using the setAnimationLoop method
  container.appendChild( renderer.domElement );

  // start the animation loop
  renderer.setAnimationLoop( () => {

    update();
    render();

  } );

}
Switching to the setAnimationLoop method allows us to abstract away requestAnimationFrame() completely. Delete your current animate()function and replace with the setAnimationLoop code.
If we want to stop the animation loop at any time, we can pass in null to the method like this:

renderer.setAnimationLoop( null );
For example, at a later date we might want to add play and stop functions like this:

function play() {

  renderer.setAnimationLoop( () => {

    update();
    render();

  } );

}

function stop() {

  renderer.setAnimationLoop( null );

}

Seamlessly Handling Browser Window Size Changes

The user may resize their browser at any time. For example, they may rotate their phone from portrait to landscape, or they may change the size of the browser window on their laptop.
We want to handle this gracefully, in a manner that is essentially invisible to the user and involves a minimum of effort on our part.
Fortunately, this is easy to do, using a built-in browser method called addEventListener.
You can listen for all kinds of events using addEventListener, such as clickscrollkeypress and many more, on any HTML element. In [Ch 3.4: Adding Interactivity to Our Scene with Event Listeners], we’ll see how we can listen for keypress events to add keyboard controls to our scene

Adding a resize Event Listener


Add the following at the end of your code, just before the call to the init function, to create a listener for the resize event
function render() {

  renderer.render( scene, camera );

}

function onWindowResize() {

  console.log( 'You resized the browser window!' );

}

window.addEventListener( 'resize', onWindowResize );

// call the init function to set everything up
init();
Here, we want to listen for a resize event, which will fire any time the browser’s window changes size.
You can add event listeners to any HTML element, but in this case, we want to listen for an event on the whole window so we’ll use window.addEventListener. This will call the onWindowResize function every time the window resizes.





Logging resize event to console

We need to be careful here though since whenever you resize the browser window, the function might get called many times - potentially hundreds of times when you thought you had resized the window just once. So don’t do any heavy calculation in here.
For now, we’ve just put console.log( ... ) inside the function. This is a useful way of making sure that something is working correctly. Open up the browser console now, then resize the window and you should see something like the image above.
Once we’ve confirmed that the event listener is firing as we expect, we can go ahead and add the desired functionality to it.

The onWindowResize Function


Our final resize handling code will look like this
// a function that will be called every time the window gets resized.
// It can get called a lot, so don't put any heavy computation in here!
function onWindowResize() {

  // set the aspect ratio to match the new browser window aspect ratio
  camera.aspect = container.clientWidth / container.clientHeight;


  // update the camera's frustum
  camera.updateProjectionMatrix();

  // update the size of the renderer AND the canvas
  renderer.setSize( container.clientWidth, container.clientHeight );

}

window.addEventListener( 'resize', onWindowResize );

// call the init function to set everything up
init();
Now that we’ve added the event listener and confirmed that the event is firing correctly, what should we put inside the onWindowResize function?
It’s fairly easy to figure this out actually - go over the code in the init function and make a note of everywhere that we used container.clientWidth or container.clientHeight.
Since the dimensions of the container will probably have changed after the resize, these are the things that we need to update.
Currently, there are only two places where we used the container’s size:
  1. when we set the aspect ratio of the camera
  2. when we set the renderer’s size

  const aspect = container.clientWidth / container.clientHeight;


  renderer.setSize( container.clientWidth, container.clientHeight );
We need to figure out a way of updating these to use the new width and height.

Update the Camera’s Aspect Ratio

Change the aspect ratio, then update the frustum
// a function that will be called every time the window gets resized.
// It can get called a lot, so don't put any heavy computation in here!
function onWindowResize() {

  // set the aspect ratio to match the new browser window aspect ratio
  camera.aspect = container.clientWidth / container.clientHeight;


  // update the camera's frustum
  camera.updateProjectionMatrix();

  // update the size of the renderer AND the canvas
  renderer.setSize( container.clientWidth, container.clientHeight );

}

window.addEventListener( 'resize', onWindowResize );

// call the init function to set everything up
init();
The camera’s aspect ratio is stored in camera.aspect, so we can just change that to the new value. However, we need to do one more thing to make the new aspect ratio take effect, and that is to update the camera’s frustum, we can do by calling the camera.updateProjectionMatrix method.
You will need to do this any time you make any changes to parameters that change the shape of the camera’s frustum, such as changing the Field of View, stored in camera.fov, updating the aspect ratio as we are doing here, or updating the clipping planes stored in camera.near and camera.far.






Update the Renderer’s Size


Call the renderer.setSize method with the new sizes
// a function that will be called every time the window gets resized.
// It can get called a lot, so don't put any heavy computation in here!
function onWindowResize() {

  // set the aspect ratio to match the new browser window aspect ratio
  camera.aspect = container.clientWidth / container.clientHeight;


  // update the camera's frustum
  camera.updateProjectionMatrix();

  // update the size of the renderer AND the canvas
  renderer.setSize( container.clientWidth, container.clientHeight );

}

window.addEventListener( 'resize', onWindowResize );

// call the init function to set everything up
init();
To update the renderer’s size (and automatically update the canvas element’s size), we can just call renderer.setSize() again with the new values.






Now try resizing the window and again and watch as your scene resizes to match. Nice!

What About the Pixel Ratio?

What about the pixel ratio, which we set back in Ch 1.1: Hello Cube?

renderer.setPixelRatio( window.devicePixelRatio );
Can this change when the browser’s window resizes?
Well, no. It’s fixed for a particular screen. However, there are some rare situations when a user may have a multiple monitor set up with screens that have different pixel ratios. We can safely ignore that for now, but we’ll come back to it in Ch 2.2: A Bullet-Proof Reusable App.

Final Result

Great work! Our app now looks much more professional, and the code is fully future-proofed and ready to be expanded on in the next few chapters. Here’s our app running with the resize code and improved animation loop.
And here is the previous chapter’s code, without the resize function. Try resizing your browser now and see the difference. It will be easier to see if you start with a narrow window and increase the size.
Next up we’ll look at an important technique called texture mapping, which we can use to create photorealistic materials for our objects.

Comments

Popular posts from this blog

How to download a file using command prompt (cmd) Windows?

The future of Artificial Intelligence: 6 ways it will impact everyday life

How to Include ThreeJs in Your Projects