Deferred rendering splits shading into two passes: the first stores each visible pixel's surface data in several textures at once, the second lights every pixel once from them. The twenty-first part of webgl-helper.js generalizes Framebuffers and Shadows's framebuffer to one color texture per format:
// Section 3.15: a framebuffer with multiple render targets: one texture per format in
// formats, on COLOR_ATTACHMENT0, 1, 2..., all enabled with drawBuffers(), plus a depth texture
GLH.targets = function (gl, width, height, formats) {
gl.getExtension('EXT_color_buffer_float'); // makes RGBA16F and RGBA32F renderable
const fbo = gl.createFramebuffer(), T = gl.TEXTURE_2D, W = WebGL2RenderingContext;
const texture = (format, attachment) => {
const t = gl.createTexture(), name = Object.keys(W).find((k) => W[k] === format);
const filter = /\dU?I$|DEPTH/.test(name) ? gl.NEAREST : gl.LINEAR; // integer, depth
gl.bindTexture(T, t);
gl.texStorage2D(T, 1, format, width, height);
const settings = [[gl.TEXTURE_MIN_FILTER, filter], [gl.TEXTURE_MAG_FILTER, filter],
[gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE], [gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE]];
settings.forEach(([key, value]) => gl.texParameteri(T, key, value));
gl.framebufferTexture2D(gl.FRAMEBUFFER, attachment, T, t, 0);
return t;
};
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
const attachments = formats.map((format, i) => gl.COLOR_ATTACHMENT0 + i);
const textures = formats.map((format, i) => texture(format, attachments[i]));
gl.drawBuffers(attachments); // fragment output i -> attachment i
const depth = texture(gl.DEPTH_COMPONENT24, gl.DEPTH_ATTACHMENT);
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
if (status !== gl.FRAMEBUFFER_COMPLETE) throw new Error(`Incomplete framebuffer: ${status}`);
return { fbo, textures, depth, width, height };
};