Depth Pass

Preparing the Depth Buffer in the Depth Pass

The stencil pass needs to know, for every pixel, how far away the visible surface is, so the first pass draws the whole scene only for its depth. The usual recipe masks color writes with colorMask(false, false, false, false); this chapter's demo writes black instead, with every light switched off, for a reason Ambient Pass explains:

Pass 1: fill the depth buffer (and paint the scene black)JavaScript
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
gl.enable(gl.DEPTH_TEST); gl.enable(gl.CULL_FACE);
drawScene(vp, black, black);                // lamp color, ambient color: all off

colorMask(r, g, b, a), depthMask(flag) and stencilMask(bits) switch writes to each buffer on and off. Every pass must use the same matrices, since later passes compare with LEQUAL against these exact depths.

Pass 1 of stencil shadows: the scene painted black while its depths fill the depth buffer (right: those depths as grey)HTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f4ee; }
  canvas { display: block; width: 100%; max-width: 600px; }
  .names { display: flex; max-width: 600px; text-align: center; font: 11px monospace; color: #333; }
  .names div { flex: 1; padding: 4px 2px; }
</style>
<canvas id="c" width="1240" height="340"></canvas>
<div class="names"><div>colour after pass 1: the scene in black</div><div>depth buffer after pass 1 (gl_FragCoord.z, 0.95 to 0.98)</div></div>
<script>
const m4 = {
  perspective(fovy, aspect, near, far) {       // far = Infinity gives the infinite projection
    const f = 1 / Math.tan(fovy * Math.PI / 360);
    if (far === Infinity) return [f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, -1, -1, 0, 0, -2 * near, 0];
    const d = near - far;
    return [f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, (far + near) / d, -1, 0, 0, 2 * far * near / d, 0];
  },
  lookAt(eye, target, up = [0, 1, 0]) {
    const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
    const unit = (v) => v.map((c) => c / Math.hypot(...v));
    const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
    const z = unit(eye.map((c, i) => c - target[i])), x = unit(cross(up, z)), y = cross(z, x);
    return [x[0], y[0], z[0], 0, x[1], y[1], z[1], 0, x[2], y[2], z[2], 0, -dot(x, eye), -dot(y, eye), -dot(z, eye), 1];
  },
  multiply(a, b) {
    const out = new Array(16).fill(0);
    for (let c = 0; c < 4; c++) for (let r = 0; r < 4; r++) for (let k = 0; k < 4; k++) out[c * 4 + r] += a[k * 4 + r] * b[c * 4 + k];
    return out;
  },
  model(x, y, z, sx, sy, sz, turn = 0) {        // T * Ry(turn) * S
    const c = Math.cos(turn), s = Math.sin(turn);
    return [c * sx, 0, -s * sx, 0, 0, sy, 0, 0, s * sz, 0, c * sz, 0, x, y, z, 1];
  },
  normal(m) {
    const [a, b, c, , d, e, f, , g, h, i] = m;
    const n = [e * i - f * h, f * g - d * i, d * h - e * g, c * h - b * i, a * i - c * g, b * g - a * h, b * f - c * e, c * d - a * f, a * e - b * d];
    const det = a * n[0] + b * n[1] + c * n[2];
    return n.map((v) => v / det);
  },
};
const CUBE = (() => {                             // 36 vertices: position, normal (counterclockwise outside)
  const faces = [[[0,0,1],[1,0,0],[0,1,0]], [[0,0,-1],[-1,0,0],[0,1,0]], [[1,0,0],[0,0,-1],[0,1,0]],
                 [[-1,0,0],[0,0,1],[0,1,0]], [[0,1,0],[1,0,0],[0,0,-1]], [[0,-1,0],[1,0,0],[0,0,1]]];
  const out = [];
  for (const [n, u, v] of faces) for (const [a, b] of [[-1,-1],[1,-1],[1,1],[-1,-1],[1,1],[-1,1]])
    out.push(...[0, 1, 2].map(i => (n[i] + a * u[i] + b * v[i]) / 2), ...n);
  return new Float32Array(out);
})();
function program(vs, fs) {
  const p = gl.createProgram();
  for (const [type, src] of [[gl.VERTEX_SHADER, vs], [gl.FRAGMENT_SHADER, fs]]) {
    const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s);
    if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
    gl.attachShader(p, s);
  }
  gl.linkProgram(p);
  return p;
}

const gl = document.getElementById('c').getContext('webgl2', { stencil: true });   // ask for stencil bits
const lit = program(`#version 300 es
layout(location = 0) in vec3 aPosition; layout(location = 1) in vec3 aNormal;
uniform mat4 uViewProjection, uModel; uniform mat3 uNormalMatrix; out vec3 vPosition, vNormal;
void main() { vec4 w = uModel * vec4(aPosition, 1.0); vPosition = w.xyz; vNormal = uNormalMatrix * aNormal; gl_Position = uViewProjection * w; }`,
`#version 300 es
precision highp float;
in vec3 vPosition, vNormal; uniform vec3 uColor, uLamp, uLampColor, uAmbient, uEye; out vec4 c;
void main() {
  vec3 n = normalize(vNormal), toLamp = uLamp - vPosition; float d = length(toLamp); vec3 l = toLamp / d;
  float lambert = max(dot(n, l), 0.0), fade = 1.0 / (d * d);
  float spec = lambert > 0.0 ? pow(max(dot(n, normalize(l + normalize(uEye - vPosition))), 0.0), 40.0) : 0.0;
  c = vec4(uColor * (uAmbient + uLampColor * lambert * fade) + uLampColor * spec * fade * 0.2, 1.0);
}`);

const cubeVAO = gl.createVertexArray();
gl.bindVertexArray(cubeVAO);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, CUBE, gl.STATIC_DRAW);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 24, 0); gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 12); gl.enableVertexAttribArray(1);

const covers = ['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f'];
const rgb = (hex) => [1, 3, 5].map((k) => parseInt(hex.slice(k, k + 2), 16) / 255);
const books = covers.map((hex, i) => ({ color: rgb(hex), model: m4.model(-0.81 + i * 0.32, 0, 0, 0.26, 0.9, 0.2, (20 - i * 8) * Math.PI / 180) }));
const scene = [...books,
  { color: rgb('#8a6240'), model: m4.model(0, -0.495, 0, 2.3, 0.08, 0.7) },    // plank
  { color: rgb('#cdbfa8'), model: m4.model(0, 0.13, -0.45, 3.5, 1.3, 0.04) }];  // wall
const lamp = [-0.2, 1.3, 1.1], eye = [0.9, 0.9, 2.9], black = [0, 0, 0];
const lampColor = [3, 2.8, 2.55], ambient = [0.28, 0.29, 0.33];


function drawScene(vp, lampLight, ambientLight) {
  gl.useProgram(lit);
  const u = (n) => gl.getUniformLocation(lit, n);
  gl.uniformMatrix4fv(u('uViewProjection'), false, vp);
  gl.uniform3fv(u('uLamp'), lamp); gl.uniform3fv(u('uEye'), eye);
  gl.uniform3fv(u('uLampColor'), lampLight); gl.uniform3fv(u('uAmbient'), ambientLight);
  gl.bindVertexArray(cubeVAO);
  for (const { color, model } of scene) {
    gl.uniformMatrix4fv(u('uModel'), false, model);
    gl.uniformMatrix3fv(u('uNormalMatrix'), false, m4.normal(model));
    gl.uniform3fv(u('uColor'), color);
    gl.drawArrays(gl.TRIANGLES, 0, 36);
  }
}

// For the right pane only: a program that writes each fragment's stored depth as a grey level.
const depthView = program(`#version 300 es
layout(location = 0) in vec3 aPosition; uniform mat4 uViewProjection, uModel;
void main() { gl_Position = uViewProjection * uModel * vec4(aPosition, 1.0); }`, `#version 300 es
precision highp float; out vec4 c;
void main() { c = vec4(vec3((gl_FragCoord.z - 0.95) / 0.03), 1.0); }   // the depth test's value, stretched`);

gl.clearColor(0.93, 0.91, 0.87, 1);
gl.enable(gl.DEPTH_TEST); gl.enable(gl.CULL_FACE); gl.enable(gl.SCISSOR_TEST);
const vp = m4.multiply(m4.perspective(40, 620 / 340, 0.1, Infinity), m4.lookAt(eye, [0.1, -0.2, 0]));

// Pass 1: fill the depth buffer (and paint the scene black).
gl.viewport(0, 0, 620, 340); gl.scissor(3, 0, 614, 340);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
drawScene(vp, black, black);                // lamp colour, ambient colour: all off

// The same geometry with the same matrices (later passes compare against these exact depths).
gl.viewport(620, 0, 620, 340); gl.scissor(623, 0, 614, 340);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.useProgram(depthView);
gl.uniformMatrix4fv(gl.getUniformLocation(depthView, 'uViewProjection'), false, vp);
gl.bindVertexArray(cubeVAO);
for (const { model } of scene) {
  gl.uniformMatrix4fv(gl.getUniformLocation(depthView, 'uModel'), false, model);
  gl.drawArrays(gl.TRIANGLES, 0, 36);
}
</script>