The Depth Buffer and depthFunc

WebGL paints surfaces in submission order. The depth buffer stores, per pixel, the depth of the nearest fragment so far (0 at the near plane, 1 at the far one); with the depth test enabled, a fragment survives only if it passes depthFunc()'s comparison, LESS by default. The buffer exists by default (24 bits here), but the test starts disabled, and the buffer must be cleared each frame. The right pane previews Face Culling:

No depth test (left), the depth test (middle), and front faces culled (right)HTML
<canvas id="c" width="1240" height="300" style="width:620px;height:150px"></canvas>
<script src="https://cdn.jsdelivr.net/npm/gl-matrix@3.4.4/gl-matrix-min.js"></script>
<script src="webgl-helper.js"></script>
<script>
const gl = GLH.context('c'), { attributes, uniforms } = GLH.tint(gl);
const shelf = GLH.shelf(gl, attributes);
GLH.panes(gl, 3, (pane, aspect) => {                    // panes() clears color and depth
  if (pane === 1) gl.enable(gl.DEPTH_TEST);             // hidden surfaces removed
  if (pane === 2) { gl.enable(gl.CULL_FACE); gl.cullFace(gl.FRONT); }   // Section 3.8.4
  uniforms.uViewProjection(shelf.camera(aspect, [1.1, 1.2, 3.8]));
  shelf.draw(uniforms);                                 // books first, then the plank
});
window.__done = true;
</script>
Browser output of Listing 3.66
Browser output of 66

With no depth test (left), each book's dark back face, drawn after its front, covers it, and the plank's underside covers the books; the depth test (middle) fixes both. depthFunc() also takes LEQUAL (for a second pass over the same geometry), GREATER, GEQUAL, EQUAL, NOTEQUAL, ALWAYS and NEVER. depthMask(false) tests without writing, clearDepth() sets the cleared value (1), and depthRange() squeezes the 0-to-1 mapping.

The shelf with no depth test (left), with the depth test (middle), and with front faces culled (right)HTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  canvas { display: block; width: 100%; max-width: 600px; }
  .names { display: flex; max-width: 600px; text-align: center; font-family: monospace; }
  .names div { flex: 1; padding: 4px 2px; }
</style>
<canvas id="c" width="1200" height="320"></canvas>
<div class="names"><div>no DEPTH_TEST</div><div>enable(DEPTH_TEST), LESS</div><div>CULL_FACE + cullFace(FRONT)</div></div>
<script>
// --- small 4x4 column-major matrix helpers ---
const m4 = {
  perspective(fovy, aspect, near, far) {
    const f = 1 / Math.tan(fovy * Math.PI / 360), 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;
  },
  box: (x, y, z, sx, sy, sz) => [sx, 0, 0, 0, 0, sy, 0, 0, 0, 0, sz, 0, x, y, z, 1],   // T * S
};
function cube() {        // unit cube, counterclockwise seen from outside: position + normal
  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);
}

const gl = document.getElementById('c').getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
layout(location = 0) in vec3 aPosition; layout(location = 1) in vec3 aNormal;
uniform mat4 uViewProjection, uModel; out float vLight;
void main() {
  vLight = 0.3 + 0.7 * max(dot(normalize(mat3(uModel) * aNormal), normalize(vec3(0.4, 0.8, 0.6))), 0.0);
  gl_Position = uViewProjection * uModel * vec4(aPosition, 1.0);
}`], [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; uniform vec3 uColor; in float vLight; out vec4 c;
void main() { c = vec4(uColor * vLight, 1.0); }`]]) {
  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(program, s);
}
gl.linkProgram(program); gl.useProgram(program);
const u = (n) => gl.getUniformLocation(program, n);
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);

// The shelf: six books, then the plank (drawn last).
const colors = [[0.12, 0.37, 0.55], [0.36, 0.25, 0.6], [0.88, 0.6, 0.06], [0.25, 0.49, 0.23], [0.71, 0.27, 0.18], [0.16, 0.62, 0.56]];
const items = colors.map((color, i) => ({ color, model: m4.box(-1.25 + i * 0.5, 0.5 + (i % 3) * 0.05, 0, 0.4, 1 + (i % 3) * 0.1, 0.6) }));
items.push({ color: [0.54, 0.38, 0.25], model: m4.box(0, -0.05, 0, 3.4, 0.1, 0.9) });

gl.enable(gl.SCISSOR_TEST);
const viewProjection = m4.multiply(m4.perspective(40, 400 / 320, 0.1, 20), m4.lookAt([1.1, 1.6, 3.8], [0, 0.4, 0]));
gl.uniformMatrix4fv(u('uViewProjection'), false, viewProjection);
for (let pane = 0; pane < 3; pane++) {
  gl.viewport(pane * 400, 0, 400, 320); gl.scissor(pane * 400 + 3, 0, 394, 320);
  gl.clearColor(0.93, 0.91, 0.87, 1);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);   // the depth buffer must be cleared too
  gl.disable(gl.DEPTH_TEST); gl.disable(gl.CULL_FACE);
  if (pane >= 1) { gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LESS); }   // hidden surfaces removed
  if (pane === 2) { gl.enable(gl.CULL_FACE); gl.cullFace(gl.FRONT); }   // only the inner walls remain
  for (const { color, model } of items) {
    gl.uniformMatrix4fv(u('uModel'), false, model);
    gl.uniform3fv(u('uColor'), color);
    gl.drawArrays(gl.TRIANGLES, 0, 36);
  }
}
</script>