Texture Pass

How to 3D

Chapter 13: Shadows

Texture Pass

Setting up the depth texture and the framebuffer objects are one-time steps that should happen in our initialization routine. Drawing into the texture, on the other hand, needs to happen whenever the light source changes.

The shader we use while drawing into the texture could be the same one that we use when drawing into the default framebuffer. However, only the depths are recorded, which means the lighting calculations and texture lookups are wasted computation. Since we are effectively rendering the scene twice to perform shadow mapping, we should try to make the extra pass as speedy as possible.

This routine builds a very slim shader program that transforms the vertex and does no unnecessary work:

function initializeDepthProgram() {
  const vertexSource = `
uniform mat4 clipFromWorld;
uniform mat4 worldFromModel;
in vec3 position;

void main() {
  gl_Position = clipFromWorld * worldFromModel * vec4(position, 1.0);
}
  `;

  const fragmentSource = `
out vec4 fragmentColor;

void main() {
  fragmentColor = vec4(1.0);
}
    `;

  depthProgram = new ShaderProgram(vertexSource, fragmentSource);
}
function initializeDepthProgram() {
  const vertexSource = `
uniform mat4 clipFromWorld;
uniform mat4 worldFromModel;
in vec3 position;

void main() {
  gl_Position = clipFromWorld * worldFromModel * vec4(position, 1.0);
}
  `;

  const fragmentSource = `
out vec4 fragmentColor;

void main() {
  fragmentColor = vec4(1.0);
}
    `;

  depthProgram = new ShaderProgram(vertexSource, fragmentSource);
}

The fragment shader must still emit a color, even if that color is thrown away because the FBO has no color attachment.

The routine that draws to the texture is a lot like the render functions that we've used in past renderers. We bind a shader program and draw our VAOs. However, it has some differences. We must also bind the FBO, clear only the depth buffer and not the color buffer, shape the viewport to fit the texture, and render from the point of view of the light source, just as we saw with projective texturing.

These differences are captured in this skeleton of the renderDepths function:

function renderDepths(width, height, fbo) {
  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);

  gl.viewport(0, 0, width, height);
  gl.clear(gl.DEPTH_BUFFER_BIT);

  const clipFromWorld = clipFromLight.multiplyMatrix(lightFromWorld);

  depthProgram.bind();
  // for each object
  //   clipFromModel = clipFromWorld * object's worldFromModel
  //   set clipFromModel uniform
  //   draw object
  depthProgram.unbind();

  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}
function renderDepths(width, height, fbo) {
  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);

  gl.viewport(0, 0, width, height);
  gl.clear(gl.DEPTH_BUFFER_BIT);

  const clipFromWorld = clipFromLight.multiplyMatrix(lightFromWorld);

  depthProgram.bind();
  // for each object
  //   clipFromModel = clipFromWorld * object's worldFromModel
  //   set clipFromModel uniform
  //   draw object
  depthProgram.unbind();

  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}

This function needs to be called whenever the light's position or direction changes. The texture dimensions and FBO are passed as parameters. After the drawing finishes, the texture contains the new depths, and we are ready to draw the shadowed scene to the default framebuffer.

← Framebuffer ObjectsDefault Framebuffer Pass →