Setting up the depth texture and the framebuffer objects are one-time steps that should happen in your initialization routine. Drawing into the texture, on the other hand, needs to happen whenever the light source changes.
The shader you use while drawing into the texture could be the same one that you 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 you are effectively rendering the scene twice to perform shadow mapping, you 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);
}
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 of like the render
functions that you've used in past renderers. You bind a shader program and draw your VAOs. However, it has some differences. You 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 you 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);
}
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 you are ready to draw the shadowed scene to the default framebuffer.