Loading and Working with Textures, and Preparing Textures for Use in Your Scenes9.1

Adding Realism with Environment Maps9.2

MeshStandardMaterial and the Metal/Rough Workflow9.3

MeshPhongMaterial and the Specular Workflow9.4

Bump, Normal, and Displacement Maps9.5

Two Ways of Approaching Transparency9.6

Emissive, Light, and Ambient Occlusion Maps9.7

Understanding Geometry10

Basic Geometry Concepts: Vertices, Normals and UVs10.1

Creating A Custom Geometry10.2

Points, Particles Systems, and Sprites11

Particle Systems11.1

Introducing Sprites11.2

Lines, Shapes, and Text12

Throwing Shapes: Recreating the 2D Canvas API in 3D 12.1

Text in 3D: The FontLoader and TextBufferGeometry12.2

Rendering Your Scenes with WebGL13

The WebGLRenderer in Depth13.1

Rendering Offscreen to a WebGLRendererTarget13.2

Animating Your Scenes14

Unraveling the Animation System14.1

Introducing Morph Targets14.2

Bones, Skinning, and Skeletal Animation14.3

Post-Processing, Shaders, and Effects15

Adding Post-Processing To A Scene15.1

Anti-Aliasing A Post-Processed Scene15.2

A Big List of all the Post Effects (currently) Available in three.js15.3

Sound in a 3-Dimensional World16

The WebAudio API16.1

Positional Sound16.1

References and Resources

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

functionanimate(){// 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 requestAnimationFrame for 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:

Get user input

Update animations

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
functionupdate(){// 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
functionrender(){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
functionupdate(){// 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
functionrender(){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 that much 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.

Virtual Reality on the Web

Web Virtual Reality - WebVR - and the related Web Augmented Reality or WebAR (which are combined into a unified API called WebXR) - are both new technologies and are likely to change considerably as they are developed.

Support for these APIs was added to three.js around the start of 2018, and if you’re fortunate enough to own a virtual reality device you can view some three.js based VR examples
here.

By abstracting our animation loop into setAnimationLoop(), we can guarantee that we don’t need to worry about any changes to these APIs beyond keeping three.js up to date, and if we later want to update a standard scene to work in VR or AR, we’ll just need to change one or two lines.

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 click, scroll, keypress 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

functionrender(){renderer.render(scene,camera);}functiononWindowResize(){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.

Although most events can be attached to any HTML element, the resize event only works when attached to the global window object!

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!
functiononWindowResize(){// 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:

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!
functiononWindowResize(){// 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!
functiononWindowResize(){// 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.

We actually need to update the <canvas> size as well. However, as you may recall, when we initially set the renderer’s size using renderer.setSize(), it automatically takes care of this for us.

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

What About the Pixel Ratio?

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 that in Section Two.

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.