Discover three.js is now open source!
Word Count:2552, reading time: ~12minutes

Ambient Lighting: Illumination from Every Direction

At the end of the last chapter, we discovered a rather glaring problem with our lighting setup. Our scene is illuminated using a single DirectionalLight, and although this type of light fills the entire scene with light rays, all the rays shine in a single direction. Faces of the cube in the direct path of the light are brightly illuminated. However, as soon as we rotate the camera to see another direction, we find that any faces of the cube that point away from the direction of the light rays don’t receive any light at all!

 

Any faces of the cube not in the path of the light rays
don’t receive any light at all

In this chapter, we’ll investigate what’s going on here, and explore some methods for improving our lighting setup. Along the way, we’ll find the time for a brief review of some of lighting techniques commonly used when working with three.js.

Lighting in the Real World

In the real world, an infinite number of light rays reflect and bounce an infinite number of times from all the objects in a scene, gradually fading and changing color with each bounce until finally, they reach our eyes or cameras. This creates the beautiful and subtle patterns of light and shadows we see every day in the world around us.

Simulating Lighting in Real-Time

Unfortunately for us, computers have trouble simulating the infinite. A technique called ray tracing can be used to simulate a few thousand lights rays each bouncing a few times around the scene. However, it takes too much processing power to render frames in real-time using this technique, so ray-tracing and related techniques like path tracing are better suited for creating pre-rendered images or animations.

Instead, as we discussed in the Physically Based Rendering chapter, real-time graphics engines split lighting into two parts:

  1. Direct lighting: light rays that come directly from a light source and hit an object.
  2. Indirect lighting: light rays that have bounced off the walls and other objects in the room before hitting an object, changing color and losing intensity with each bounce.

There is a third category that aims to perform direct and indirect lighting at the same time, called global illumination, of which ray tracing and path tracing are two examples. Indeed, there are a huge number of techniques for simulating or approximating lighting in the field of 3D computer graphics. Some of these techniques simulate direct lighting, some simulate indirect lighting, while others simulate both. Most of these techniques are too slow to use on the web where we have to consider people accessing our app from low powered mobile devices. However, even when we limit ourselves to only the techniques suitable for real-time use and available in three.js, the number of lighting methods we can use is still quite high.

Creating high-quality lighting using three.js is a matter of choosing a combination of these techniques to create a complete lighting setup. In three.js, the light classes are divided into two categories to match the two categories of lighting:

  1. Direct lights, which simulate direct lighting.
  2. Ambient lights, which are a cheap and somewhat believable way of faking indirect lighting.

The DirectionalLight currently illuminating our scene is a form of direct lighting. In this chapter, we’ll pair this light with an ambient light. Ambient lighting is one of the simplest techniques for adding indirect lighting to your scenes, and a DirectionalLight paired with an ambient light is one of the most common lighting setups.

But first, let’s take a brief tour of some of the lighting techniques available to us when using three.js.

A Brief Overview of Lighting Techniques

Multiple Direct Lights

One solution to the problem of our poorly illuminated cube is to add more direct lights, like the DirectionalLight or SpotLight, until the objects in your scene are illuminated from all angles. However, this approach creates a new set of problems:

  1. We have to keep track of the lights to make sure all directions are illuminated.
  2. Lights are expensive, and we want to add as few lights as possible to our scenes.

Adding more and more direct lights to your scene will quickly kill your framerate, so direct lights alone are rarely the best choice.

No Lights at All!

Another lighting technique is to avoid using lights completely. Some materials, such as the MeshBasicMaterial, don’t need lights to be seen. You can get nice results using a MeshBasicMaterial and appropriate textures.

The MeshBasicMaterial in action

In the above scene, first, set the color to white (0xffffff), and then change .map to the bricks texture. Next, remove the brick texture and set the environment map (.envMap) to reflection. As you can see, the MeshBasicMaterial is not quite so basic as the name suggests. Nonetheless, this solution is more appropriate for intentionally low-fidelity scenes, or when performance is of utmost importance.

Image-Based Lighting (IBL)

Image-based lighting is the name for a family of techniques that involve pre-calculating lighting information and storing it in textures. The most important IBL technique is environment mapping (also known as reflection mapping), which you saw a moment ago when you set the MeshBasicMaterial.envMap.

Image Based Lighting (IBL): the scene background is reflected on the sphere

Environment maps are usually generated using specialized photography techniques or external 3D rendering programs. There are several formats used to store the resulting images, of which two are demonstrated in the above scene: cube maps and equirectangular maps. Click the options in the menu to see an example of each. Environment mapping is one of the most powerful lighting techniques available in three.js, and we’ll explore this in detail later.

The Fast and Easy Solution: Ambient Lighting

Ambient lighting is a method of faking indirect lighting which is both fast and easy to set up while still giving reasonable results. There are two ambient light classes available in the three.js core:

  • The AmbientLight adds a constant amount of light to every object from all directions.
  • The HemisphereLight fades between a sky color and a ground color and can be used to simulate many common lighting scenarios.

We mentioned these briefly back in the Physically Based Rendering chapter. Using either of these lights follows the same process as using the DirectionalLight. Simply create an instance of the light, then add it to your scene. The following scene demonstrates using a HemisphereLight in combination with a DirectionalLight to give the effect of a bright outdoor scene.

A simple scene lit by a directional light and a hemisphere light

As you can see, the result is not realistic. Ambient lighting paired with direct lighting is more geared towards performance than quality. However, you could hugely increase the quality of this scene without changing the lighting setup, by using a different model and background or improving the model’s material.

Working with Ambient Lights

Like the direct lights, ambient lights inherit from the base Light class, so they have .color and .intensity properties. Light, in turn, inherits from Object3D, so all lights also have .position, .rotation and .scale properties. However, rotating or scaling lights has no effect. Changing the position of the AmbientLight has no effect either.

Ambient lights affect all objects in the scene. As a result, there’s no need to add more than one ambient light to your scene. Unlike the direct lights (except for RectAreaLight), ambient lights cannot cast shadows.

As usual, to use either of these light classes, you must first import them. Import both classes within the lights module now. We’ll spend the rest of this chapter experimenting with them.

lights.js: import both ambient light classes
    import {
  AmbientLight,
  DirectionalLight,
  HemisphereLight,
} from 'three';

  

The AmbientLight

The AmbientLight is the cheapest way of faking indirect lighting in three.js. This type of light adds a constant amount of light from every direction to every object in the scene. It doesn’t matter where you place this light, and it doesn’t matter where other objects are placed relative to the light. This is not at all similar to how light in the real world works. Nonetheless, in combination with one or more direct lights, the AmbientLight gives OK results.

Add an AmbientLight to the Scene

As with the DirectionalLight, pass the .color and .intensity parameters to the constructor:

lights.js: create an AmbientLight
    
function createLights() {
const ambientLight = new AmbientLight('white', 2);

const mainLight = new DirectionalLight('white', 5);
mainLight.position.set(10, 10, 10);

return { ambientLight, mainLight };
}

  

Over in World, the createLights function now returns two lights. Add both of them to the scene:

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

    const controls = createControls(camera, renderer.domElement);

    const cube = createCube();
    const { ambientLight, mainLight } = createLights();

    loop.updatables.push(controls);
    scene.add(ambientLight, mainLight, cube);

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

  

We’ll usually set the intensity of the AmbientLight to a lower value than the direct light it has been paired with. Here, white light with a low intensity results in a dim gray ambient illumination. Combined with the single bright DirectionalLight, this dim ambient light solves our lighting issues and the rear faces of the cube become illuminated:

However, the lighting on the rear faces of the cube looks rather dull. To make a setup based around AmbientLight and DirectionalLight look good, we would need to add multiple directional lights with varying direction and intensity. That runs into many of the same problems we described above for a setup using multiple direct lights. As we’ll see in a moment, the HemisphereLight gives better results here, for almost no additional performance cost.

That doesn’t mean the AmbientLight is useless. The HemisphereLight doesn’t suit every scene, for example, in which case you can fall back to an AmbientLight. Also, this light is the cheapest way to increase the overall brightness or add a slight color tint to a scene. You’ll sometimes find it useful for modulating other kinds of lighting such as environment maps or for adjusting shadow darkness.

The AmbientLight Doesn’t Show Depth

As we mentioned in the Physically Based Rendering chapter, our eyes use differences in shading across the surface of an object to determine depth. However, the light from an ambient light shines equally in all directions, so the shading is uniform and gives us no information about depth. Consequently, any object illuminated using only an AmbientLight will not appear to be 3D.

This is similar to how the MeshBasicMaterial works, to the point of being indistinguishable. One of these cubes has a MeshBasicMaterial and one has a MeshStandardMaterial illuminated only by an AmbientLight. See if you can tell them apart:

The HemisphereLight

Light from a HemisphereLight fades between a sky color at the top of the scene and a ground color at the bottom of the scene. Like the AmbientLight, this light makes no attempt at physical accuracy. Rather, the HemisphereLight was created after observing that in many of the situations where you find humans, the brightest light comes from the top of the scene, while light coming from the ground is usually less bright.

For example, in a typical outdoor scene, objects are brightly lit from above by the sun and sky and then receive secondary light from sunlight reflecting off the ground. Likewise, in an indoor environment, the brightest lights are usually on the ceiling and these reflect off the floor for dim secondary illumination.

We can adjust the fading between the sky and ground by changing the light’s .position. As with all light types, .rotation and .scale have no effect. The HemisphereLight constructor takes the same .color and .intensity parameters as all the other lights, but has an additional .groundColor parameter. Generally, we will use a bright sky .color, and a much darker .groundColor:

lights.js: create a HemisphereLight
      const ambientLight = new HemisphereLight(
    'white', // bright sky color
    'darkslategrey', // dim ground color
    5, // intensity
  );

  

We can get decent results using a single HemisphereLight with no direct lights at all:

However, since the HemisphereLight light does not shine from any particular direction, there are no shiny highlights (AKA specular highlights) in this scene. This is why we usually pair this type of light with at least one direct light. For outdoor scenes, try pairing the HemisphereLight with a single bright DirectionalLight representing the sun. For indoor scenes, you might use a PointLight to represent a lightbulb, or a RectAreaLight to simulate light coming through a bright window or from a strip light.

Ambient lights, especially the HemisphereLight, give great results for low performance cost, making them suitable for use on low-power devices. However, scenes in the real world have shadows, reflections, and shiny highlights, none of which can be added using ambient lighting alone. This means ambient lighting is best used in a supporting role alongside other techniques such as direct lighting or IBL.

Throughout the book, we’ll explore many lighting solutions. Many of these give better results than ambient lights, but virtually none have a better performance/quality tradeoff.

Challenges

Import Style
Selected Texture