Multiple Lights

How to 3D

Chapter 6: Lighting

Multiple Lights

Imagine a game in which the player is running down a hallway with a series of light fixtures mounted on the ceiling. Imagine a painting on the wall illuminated by several candles. Imagine the red and blue lights of an emergency vehicle. None of these lighting effects can be achieved by the fragment shaders we've written so far as they consider only a single light source. We need to extend our algorithm to support multiple lights.

When paint mixes together, it gets darker because each pigment acts as an additional filter, absorbing more light. When light mixes together, it gets brighter because each light is contributing additional energy. Accordingly, to shade a fragment with multiple light sources, we calculate its diffuse and specular terms for each individual light source and then add all the terms together.

This fragment shader computes the diffuse terms for two light sources:

uniform vec3 lightPositionsEye[2];
uniform vec3 diffuseColors[2];
uniform vec3 albedo;

in vec3 mixNormal;
in vec3 mixPositionEye;

out vec4 fragmentColor;

void main() {
  vec3 normal = normalize(mixNormal);
  vec3 diffuse = vec3(0.0);

  for (int i = 0; i < 2; ++i) {
    vec3 lightDirection = normalize(lightPositionsEye[i] - mixPositionEye);
    float litness = max(0.0, dot(normal, lightDirection));
    diffuse += litness * albedo * diffuseColors[i];
  }

  fragmentColor = vec4(diffuse, 1.0);
}
uniform vec3 lightPositionsEye[2];
uniform vec3 diffuseColors[2];
uniform vec3 albedo;

in vec3 mixNormal;
in vec3 mixPositionEye;

out vec4 fragmentColor;

void main() {
  vec3 normal = normalize(mixNormal);
  vec3 diffuse = vec3(0.0);

  for (int i = 0; i < 2; ++i) {
    vec3 lightDirection = normalize(lightPositionsEye[i] - mixPositionEye);
    float litness = max(0.0, dot(normal, lightDirection));
    diffuse += litness * albedo * diffuseColors[i];
  }

  fragmentColor = vec4(diffuse, 1.0);
}

The lights' positions and colors are sent in as elements in an array of uniforms. To upload individual elements in an array of uniforms from TypeScript, you include the index in the uniform's name:

shaderProgram.setUniform3f('diffuseColors[0]', diffuseColors[0].x, diffuseColors[0].y, diffuseColors[0].z);
shaderProgram.setUniform3f('diffuseColors[1]', diffuseColors[1].x, diffuseColors[1].y, diffuseColors[1].z);
shaderProgram.setUniform3f('lightPositionsEye[0]', lightPositionsEye[0].x, lightPositionsEye[0].y, lightPositionsEye[0].z);
shaderProgram.setUniform3f('lightPositionsEye[1]', lightPositionsEye[1].x, lightPositionsEye[1].y, lightPositionsEye[1].z);
shaderProgram.setUniform3f('diffuseColors[0]', diffuseColors[0].x, diffuseColors[0].y, diffuseColors[0].z);
shaderProgram.setUniform3f('diffuseColors[1]', diffuseColors[1].x, diffuseColors[1].y, diffuseColors[1].z);
shaderProgram.setUniform3f('lightPositionsEye[0]', lightPositionsEye[0].x, lightPositionsEye[0].y, lightPositionsEye[0].z);
shaderProgram.setUniform3f('lightPositionsEye[1]', lightPositionsEye[1].x, lightPositionsEye[1].y, lightPositionsEye[1].z);

The fragment shader above is used in this renderer to illuminate the torus in both orange and blue lights:

Theoretically you may add as many lights as you need. However, if a scene has a large number of lights, the fragment shader will run slowly. Game developers avoid these costs by considering only the few nearest lights or by precomputing as many of the lighting calculations as possible. The results are stored in or baked into images, which are pasted onto the surfaces.

← Specular TermCross Product →