Virtual Shelf

Placing BookNest's Covers on a Virtual Shelf

The cover wall becomes a 3D scene. Each book's model matrix moves its center line to the origin, scales the wall's proportions into a book's, turns it about y and puts it on the shelf; the plank is a quad laid flat about x; one view-projection matrix frames everything, drawn back to front, one cover per drawElements() offset:

BookNest's six covers standing on a shelf in perspectiveHTML
<canvas id="c" width="1240" height="440" style="width:620px;height:220px"></canvas>
<script src="webgl-helper.js"></script>
<script>
const gl = GLH.context('c'), m4 = GLH.m4;
const { attributes, uniforms } = GLH.load(gl, `#version 300 es
  in vec2 aPosition; in vec2 aUV; in float aBook; uniform mat4 uMVP; uniform vec3 uColors[7];
  out vec3 vColor;
  void main() { vColor = uColors[int(aBook)] * mix(1.15, 0.7, aUV.y);
                gl_Position = uMVP * vec4(aPosition, 0.0, 1.0); }`, `#version 300 es
  precision mediump float; in vec3 vColor; out vec4 c; void main() { c = vec4(vColor, 1); }`);
uniforms.uColors([...GLH.covers, '#8a6240'].flatMap(GLH.rgb));   // book 6 is the shelf wood
const layout = [['aPosition', 2], ['aUV', 2], ['aBook', 1]], wall = GLH.coverWall();
const books = GLH.vao(gl, attributes, wall.vertices, layout, wall.indices);
const plank = GLH.vao(gl, attributes, new Float32Array([-1.75, -0.5, 0, 0, 6,
  1.75, -0.5, 1, 0, 6, 1.75, 0.5, 1, 1, 6, -1.75, 0.5, 0, 1, 6]), layout, wall.indices);
const camera = m4.chain(m4.perspective(40, 620 / 220, 0.1, 20),
  m4.lookAt([1.1, 0.9, 2.9], [0.1, -0.2, 0]));
gl.bindVertexArray(plank);
uniforms.uMVP(m4.chain(camera, m4.translation(0, -0.495, 0), m4.rotation(-90, 1, 0, 0)));
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
gl.bindVertexArray(books);
GLH.covers.forEach((hex, i) => {
  const x = -0.81 + i * 0.32;               // center line; model = T * Ry * S * T(-x)
  uniforms.uMVP(m4.chain(camera, m4.translation(1.6 * x, 0, 0),
    m4.rotation(30 - i * 12, 0, 1, 0), m4.scaling(1.6, 0.6, 1), m4.translation(-x, 0, 0)));
  gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, i * 12);   // 6 indices x 2 bytes
});
window.__done = true;
</script>
Browser output of Listing 3.56
Browser output of 56

The flat vertices of Buffers and VAOs now recede in perspective, fanned from 30 to -30 degrees. gl-matrix rebuilds these matrices with gl-matrix 5,695 .

BookNest's six covers standing on a shelf in perspective, one model matrix eachHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f4ee; }
  canvas { display: block; width: 100%; max-width: 600px; }
</style>
<canvas id="c" width="1240" height="440"></canvas>
<script>
const m4 = {
  translation: (x, y, z) => new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1]),
  scaling: (x, y, z) => new Float32Array([x, 0, 0, 0, 0, y, 0, 0, 0, 0, z, 0, 0, 0, 0, 1]),
  rotation(degrees, x, y, z) {
    const r = degrees * Math.PI / 180, c = Math.cos(r), s = Math.sin(r), t = 1 - c;
    return new Float32Array([t * x * x + c, t * x * y + s * z, t * x * z - s * y, 0,
      t * x * y - s * z, t * y * y + c, t * y * z + s * x, 0,
      t * x * z + s * y, t * y * z - s * x, t * z * z + c, 0, 0, 0, 0, 1]);
  },
  perspective(fovy, aspect, near, far) {
    const f = 1 / Math.tan(fovy * Math.PI / 360), d = near - far;
    return new Float32Array([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 new Float32Array([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 Float32Array(16);
    for (let col = 0; col < 4; col++)
      for (let row = 0; row < 4; row++)
        for (let k = 0; k < 4; k++) out[col * 4 + row] += a[k * 4 + row] * b[col * 4 + k];
    return out;
  },
  chain: (...matrices) => matrices.reduce((a, b) => m4.multiply(a, b)),
};
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 gl = document.getElementById('c').getContext('webgl2');
gl.clearColor(0.93, 0.91, 0.87, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
  layout(location = 0) in vec2 aPosition; layout(location = 1) in vec2 aUV; layout(location = 2) in float aBook;
  uniform mat4 uMVP; uniform vec3 uColors[7];
  out vec3 vColor;
  void main() { vColor = uColors[int(aBook)] * mix(1.15, 0.7, aUV.y);
                gl_Position = uMVP * vec4(aPosition, 0.0, 1.0); }`], [gl.FRAGMENT_SHADER, `#version 300 es
  precision mediump float; in vec3 vColor; out vec4 c; void main() { c = vec4(vColor, 1); }`]]) {
  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);
gl.uniform3fv(gl.getUniformLocation(program, 'uColors'), [...covers, '#8a6240'].flatMap(rgb));   // 6 = shelf wood
const uMVP = gl.getUniformLocation(program, 'uMVP');

// The cover wall (4 vertices + 6 indices per cover) and a plank, each in its own VAO.
function vao(vertices, indices) {
  const v = gl.createVertexArray();
  gl.bindVertexArray(v);
  gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
  [[0, 2, 0], [1, 2, 8], [2, 1, 16]].forEach(([loc, size, offset]) => {
    gl.vertexAttribPointer(loc, size, gl.FLOAT, false, 20, offset);
    gl.enableVertexAttribArray(loc);
  });
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
  gl.bindVertexArray(null);
  return v;
}
const wallVertices = [], indices = [];
covers.forEach((hex, i) => {
  for (const [u, v] of [[0, 0], [1, 0], [1, 1], [0, 1]]) wallVertices.push(-0.94 + i * 0.32 + u * 0.26, -0.82 + v * 1.64, u, 1 - v, i);
  indices.push(i * 4, i * 4 + 1, i * 4 + 2, i * 4, i * 4 + 2, i * 4 + 3);
});
const books = vao(wallVertices, indices);
const plank = vao([-1.75, -0.5, 0, 0, 6, 1.75, -0.5, 1, 0, 6, 1.75, 0.5, 1, 1, 6, -1.75, 0.5, 0, 1, 6], indices.slice(0, 6));

const camera = m4.chain(m4.perspective(40, 620 / 220, 0.1, 20), m4.lookAt([1.1, 0.9, 2.9], [0.1, -0.2, 0]));
gl.bindVertexArray(plank);
gl.uniformMatrix4fv(uMVP, false, m4.chain(camera, m4.translation(0, -0.495, 0), m4.rotation(-90, 1, 0, 0)));
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
gl.bindVertexArray(books);
covers.forEach((hex, i) => {
  const x = -0.81 + i * 0.32;               // centre line; model = T * Ry * S * T(-x)
  gl.uniformMatrix4fv(uMVP, false, m4.chain(camera, m4.translation(1.6 * x, 0, 0),
    m4.rotation(30 - i * 12, 0, 1, 0), m4.scaling(1.6, 0.6, 1), m4.translation(-x, 0, 0)));
  gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, i * 12);   // 6 indices x 2 bytes
});
</script>