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:
<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>
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 .
<!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>