Default Framebuffer Pass

When the scene is drawn to the default framebuffer to produce a raster of colored pixels, you project the depth texture onto the scene using projective texturing. With the bat signal, the fragments took on the color of the projected image. With the depth map, the fragments compare themselves to the closest depth of the projected image to determine if they are in shadow.

You'll need a textureFromWorld matrix that transforms each vertex from world space coordinates all the way to texture coordinates, and you can use this code to build it:

const lightCamera = Camera.lookAt(lightPosition, lightTarget, new Vector3(0, 1, 0));
const lightFromWorld = lightCamera.matrix;
const clipFromLight = Matrix4.fovPerspective(45, 1, 0.1, 1000);
const matrices = [
  Matrix4.translate(0.5, 0.5, 0.5),
  Matrix4.scale(0.5, 0.5, 0.5),
  clipFromLight,
  lightFromWorld,
];
const textureFromWorld = matrices.reduce((accum, transform) => accum.multiplyMatrix(transform));

Like the depth texture, this matrix only needs to built when the light properties change. Each object in the scene will likely have its own worldFromModel matrix that situates it within the world, so that transform has been omitted.

In your render function, you will need to set the usual uniforms and also the ones for the projective texture lookup:

function render() {
  // ...

  shaderProgram.setUniformMatrix4("clipFromEye", clipFromEye);
  shaderProgram.setUniformMatrix4("eyeFromWorld", camera.matrix);
  shaderProgram.setUniformMatrix4("textureFromWorld", textFromWorld);
  shaderProgram.setUniform1i("depthTexture", depthTextureUnit);

  // for each object
  //   set object's worldFromModel uniform
  //   draw object

  // ...
}

The vertex shader computes the texture coordinates and passes them along to the fragment shader:

// ...
in vec3 position;
uniform mat4 worldFromModel;
uniform mat4 textureFromWorld;
out vec4 mixTexPosition;

void main() {
  // ...
  mixTexPosition =
    textureFromWorld * worldFromModel * vec4(position, 1.0); 
}

The fragment shader performs the perspective divide and the texture lookup in the standard way of projective texturing. But instead of pulling out a color that is shining on the fragment, it pulls out the depth of the surface that is closest to the light source on the fragment's line of sight:

uniform sampler2D depthTexture;
in vec4 mixTexPosition;
out vec4 fragmentColor;

void main() {
  vec4 texPosition = mixTexPosition / mixTexPosition.w;
  float fragmentDepth = texPosition.z; 
  float closestDepth = texture(depthTexture, texPosition.xy).r;
  // ...
}

The two depths are compared. If the closest depth is smaller than the fragment's depth, then the fragment has an occluder and is in shadow. A fragment in shadow needs to have its light diminished in some way. For example, you might halve the diffuse term:

uniform sampler2D depthTexture;
in vec4 mixTexPosition;
out vec4 fragmentColor;

void main() {
  // ...
  float shadowFactor = closestDepth < fragment ? 0.5 : 1.0;
  vec3 rgb = diffuse * shadowFactor;
  fragmentColor = vec4(rgb, 1.0);
}

When you put all this together in a renderer with a light source that moves back and forth, you end up with something that looks like this:

Ugh. That's awful. However, do you see the shadows? Try moving around the scene with WASD and the mouse. Raise and lower the camera and .

Shadow mapping is especially vulnerable to distracting artifacts. Thankfully, there are a number of tricks that you can use to eliminate or minimize them.