Lighting the Covers

Lighting BookNest's Covers on a Shelf

The shelf gets a complete per-fragment setup: a cool ambient fill, dim daylight as a directional light, and two point lights, a warm reading lamp at the left and a screen's blue glow at the right. The left pane is the normal view of Normal Debugging:

BookNest's shelf: normals (left) and lit by daylight, a lamp and a screen (right)HTML
<canvas id="c" width="1240" height="340" style="width:620px;height:170px"></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.lit(gl, `#version 300 es
  precision highp float;
  struct Light { vec3 position, color; };
  in vec3 vPosition, vNormal; out vec4 outColor;
  uniform vec3 uColor, uAmbient, uToSun, uSunColor; uniform Light uLights[2];
  uniform bool uShowNormals;
  void main() {
    vec3 normal = normalize(vNormal);
    if (uShowNormals) { outColor = vec4(normal * 0.5 + 0.5, 1.0); return; }   // Section 3.9.10
    vec3 light = uAmbient + uSunColor * max(dot(normal, uToSun), 0.0);
    for (int i = 0; i < 2; i++) {                       // point lights, range 4
      vec3 toLight = uLights[i].position - vPosition;
      float d = length(toLight), fade = clamp(1.0 - pow(d / 4.0, 4.0), 0.0, 1.0) / (d * d);
      light += uLights[i].color * max(dot(normal, toLight / d), 0.0) * fade;
    }
    outColor = vec4(uColor * light, 1.0);
  }`);
const shelf = GLH.shelf(gl, attributes), { vec3 } = glMatrix;
uniforms.uAmbient([0.18, 0.2, 0.25]);
uniforms.uToSun(vec3.normalize(vec3.create(), [-0.5, 0.8, 0.6]));
uniforms.uSunColor([0.45, 0.43, 0.4]);                               // dim daylight
[[[-1.1, 0.7, 0.9], [1.5, 1.15, 0.7]], [[1.4, 0.2, 1.0], [0.3, 0.45, 1.0]]]   // lamp, screen
  .forEach(([position, color], i) => {
    uniforms[`uLights[${i}].position`](position);
    uniforms[`uLights[${i}].color`](color);
  });
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
GLH.panes(gl, 2, (pane, aspect) => {
  uniforms.uShowNormals(pane === 0 ? 1 : 0);
  uniforms.uViewProjection(shelf.camera(aspect));
  shelf.draw(uniforms);
});
window.__done = true;
</script>
Browser output of Listing 3.77
Browser output of 77

The lamp warms the left-hand covers and the plank beneath them and the screen tints the right-hand books; since light multiplies each cover's color, the yellow book barely reacts to the blue light while the teal one glows. Specular Lighting and Fog adds specular highlights and fog.

BookNest's shelf: normals (left) and lit by daylight, a warm lamp and a screen's blue glow (right)HTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f4ee; }
  canvas { display: block; width: 100%; max-width: 600px; }
</style>
<canvas id="c" width="1240" height="340"></canvas>
<script>
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;
  },
  model(x, y, z, sx, sy, sz, turn = 0) {
    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);
  },
};
function cube() {
  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; 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; }`], [gl.FRAGMENT_SHADER, `#version 300 es
  precision highp float;
  struct Light { vec3 position, color; };
  in vec3 vPosition, vNormal; out vec4 outColor;
  uniform vec3 uColor, uAmbient, uToSun, uSunColor; uniform Light uLights[2];
  uniform bool uShowNormals;
  void main() {
    vec3 normal = normalize(vNormal);
    if (uShowNormals) { outColor = vec4(normal * 0.5 + 0.5, 1.0); return; }   // the debug view
    vec3 light = uAmbient + uSunColor * max(dot(normal, uToSun), 0.0);
    for (int i = 0; i < 2; i++) {                       // point lights, range 4
      vec3 toLight = uLights[i].position - vPosition;
      float d = length(toLight), fade = clamp(1.0 - pow(d / 4.0, 4.0), 0.0, 1.0) / (d * d);
      light += uLights[i].color * max(dot(normal, toLight / d), 0.0) * fade;
    }
    outColor = vec4(uColor * light, 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);

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 items = covers.map((hex, i) => ({ color: rgb(hex),
  model: m4.model(-1.3 + i * 0.52, 0.5, 0, 0.42, 0.98, 0.12, (30 - i * 12) * Math.PI / 180) }));
items.push({ color: rgb('#8a6240'), model: m4.model(0, -0.03, 0, 3.4, 0.06, 0.7) });

const sun = [-0.5, 0.8, 0.6], len = Math.hypot(...sun);
gl.uniform3f(u('uAmbient'), 0.18, 0.2, 0.25);
gl.uniform3fv(u('uToSun'), sun.map(c => c / len));
gl.uniform3f(u('uSunColor'), 0.45, 0.43, 0.4);                       // dim daylight
[[[-1.1, 0.7, 0.9], [1.5, 1.15, 0.7]], [[1.4, 0.2, 1.0], [0.3, 0.45, 1.0]]]   // lamp, screen
  .forEach(([position, color], i) => {
    gl.uniform3fv(u(`uLights[${i}].position`), position);
    gl.uniform3fv(u(`uLights[${i}].color`), color);
  });
gl.enable(gl.DEPTH_TEST); gl.enable(gl.CULL_FACE); gl.enable(gl.SCISSOR_TEST);
gl.uniformMatrix4fv(u('uViewProjection'), false, m4.multiply(m4.perspective(40, 620 / 340, 0.1, 20), m4.lookAt([1.1, 1.2, 3.2], [0.1, 0.35, 0])));
for (let pane = 0; pane < 2; pane++) {
  gl.viewport(pane * 620, 0, 620, 340); gl.scissor(pane * 620 + 3, 0, 614, 340);
  gl.clearColor(0.93, 0.91, 0.87, 1);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.uniform1i(u('uShowNormals'), pane === 0 ? 1 : 0);
  for (const { color, model } of items) {
    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);
  }
}
</script>