The swap is one-for-one: each GLH.m4 builder becomes a mat4.from...() call or an appended translate, rotateY or scale, degrees become radians, and every matrix is written into an array allocated once. In Virtual Shelf's shelf only the matrix code changes, after a <script> tag for gl-matrix 5,695 :
const { mat4, glMatrix: { toRadian } } = glMatrix;
const camera = mat4.perspective(mat4.create(), toRadian(40), 620 / 220, 0.1, 20);
mat4.multiply(camera, camera, mat4.lookAt(mat4.create(), [1.1, 0.9, 2.9], [0.1, -0.2, 0],
[0, 1, 0]));
const mvp = mat4.create(); // reused for every draw
gl.bindVertexArray(plank);
mat4.translate(mvp, camera, [0, -0.495, 0]);
uniforms.uMVP(mat4.rotateX(mvp, mvp, toRadian(-90)));
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
gl.bindVertexArray(books);
GLH.covers.forEach((hex, i) => {
const x = -0.81 + i * 0.32; // model = T * Ry * S * T(-x), appended in order
mat4.translate(mvp, camera, [1.6 * x, 0, 0]);
mat4.rotateY(mvp, mvp, toRadian(30 - i * 12));
mat4.scale(mvp, mvp, [1.6, 0.6, 1]);
uniforms.uMVP(mat4.translate(mvp, mvp, [-x, 0, 0]));
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, i * 12);
});Rendered, it matches Virtual Shelf's capture: comparing the screenshots found one pixel of 570,400 off by one level of blue, from rounding in rotateY(), and it allocates 3 matrices instead of 55. Later listings load gl-matrix this way; GLH.m4 stays in webgl-helper.js for the earlier ones.
<!doctype html>
<style>
body { margin: 0; background: #f7f4ee; }
canvas { display: block; width: 100%; max-width: 600px; }
</style>
<script src="https://cdn.jsdelivr.net/npm/gl-matrix@3.4.4/gl-matrix-min.js"></script>
<canvas id="c" width="1240" height="440"></canvas>
<script>
const { mat4, glMatrix: { toRadian } } = glMatrix;
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');
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));
const uMVP = gl.getUniformLocation(program, 'uMVP');
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 wall = [], indices = [];
covers.forEach((hex, i) => {
for (const [u, v] of [[0, 0], [1, 0], [1, 1], [0, 1]]) wall.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(wall, 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));
// Allocated once: the camera, its two parts and the mvp reused for every draw.
const projection = mat4.perspective(mat4.create(), toRadian(40), 620 / 220, 0.1, 20);
const view = mat4.create(), camera = mat4.create(), mvp = mat4.create();
function frame(ms) {
const sway = Math.sin(ms * 0.0006) * 0.8; // the camera drifts left and right
mat4.lookAt(view, [1.1 + sway, 0.9, 2.9], [0.1, -0.2, 0], [0, 1, 0]);
mat4.multiply(camera, projection, view);
gl.clearColor(0.93, 0.91, 0.87, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.bindVertexArray(plank);
mat4.translate(mvp, camera, [0, -0.495, 0]);
gl.uniformMatrix4fv(uMVP, false, mat4.rotateX(mvp, mvp, toRadian(-90)));
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
gl.bindVertexArray(books);
covers.forEach((hex, i) => {
const x = -0.81 + i * 0.32; // model = T * Ry * S * T(-x), appended in order
mat4.translate(mvp, camera, [1.6 * x, 0, 0]);
mat4.rotateY(mvp, mvp, toRadian(30 - i * 12));
mat4.scale(mvp, mvp, [1.6, 0.6, 1]);
gl.uniformMatrix4fv(uMVP, false, mat4.translate(mvp, mvp, [-x, 0, 0]));
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, i * 12);
});
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>