Why Use a Math Library

Why Hand-Rolled Matrix Code Does Not Scale

A dozen matrix functions are enough to learn from, but they fail a growing scene in three ways:

The listing counts the functions each library offers, with gl-matrix's UMD build loaded (Installing gl-matrix):

Functions in GLH.m4 and in five gl-matrix modulesJavaScript
const count = (o) => Object.values(o).filter((f) => typeof f === 'function').length;
const modules = ['vec3', 'vec4', 'mat3', 'mat4', 'quat'];
console.log('GLH.m4:', count(GLH.m4), '| gl-matrix:',
  modules.map((m) => `${m} ${count(glMatrix[m])}`).join(', '));
Output
GLH.m4: 11 | gl-matrix: vec3 49, vec4 41, mat3 32, mat4 52, quat 39

The counts include short aliases such as mat4.mul. Keep GLH.m4 for teaching; build scenes on the library.

Hand-rolled helpers against gl-matrix: functions offered and matrices allocated per frameHTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  .wrap { position: relative; width: 100%; max-width: 600px; }
  canvas { display: block; width: 100%; }
  .lbl { position: absolute; left: 6px; transform: translateY(-50%); font-weight: 600; }
  .val { position: absolute; transform: translateY(-50%); }
  h4 { margin: 6px 0 2px; font-size: 12px; }
</style>
<script src="https://cdn.jsdelivr.net/npm/gl-matrix@3.4.4/gl-matrix-min.js"></script>
<div class="wrap"><canvas id="view" width="600" height="300"></canvas></div>
<script>
// The chapter's hand-rolled helper set: a dozen functions, each returning a NEW array.
const m4 = {
  identity() {}, translation() {}, scaling() {}, rotation() {}, multiply() {}, chain() {},
  apply() {}, lookAt() {}, ortho() {}, perspective() {}, normal() {},
};
const count = (o) => Object.values(o).filter((f) => typeof f === 'function').length;
const rows = [
  ['hand-rolled m4', count(m4), [0.55, 0.55, 0.55]],
  ...['vec3', 'vec4', 'mat3', 'mat4', 'quat'].map((m, i) =>
    [`gl-matrix ${m}`, count(glMatrix[m]), [0.12 + i * 0.04, 0.37 + i * 0.05, 0.55]]),
  ['allocations per frame, m4', 55, [0.71, 0.27, 0.18]],        // the shelf: one array per call
  ['allocations per frame, gl-matrix', 3, [0.25, 0.49, 0.23]],  // outputs allocated once, reused
];

const gl = document.getElementById('view').getContext('webgl2');
const wrap = document.querySelector('.wrap');
const max = Math.max(...rows.map(r => r[1]));
const rowH = 300 / rows.length;
rows.forEach(([name, n], i) => {
  const top = ((i + 0.5) * rowH / 300 * 100) + '%';
  const px = 230 + (n / max) * 330;
  wrap.insertAdjacentHTML('beforeend', `<div class="lbl" style="top:${top}">${name}</div>` +
    `<div class="val" style="top:${top};left:${(px + 6) / 6}%">${n}</div>`);
});

// Bars as scissored clears, growing in over the first second.
gl.enable(gl.SCISSOR_TEST);
function frame(ms) {
  const grow = Math.min(1, ms / 1000);
  gl.scissor(0, 0, 600, 300);
  gl.clearColor(0.97, 0.96, 0.93, 1);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.scissor(0, 300 - 6 * rowH, 600, 1);                 // a divider between the two charts
  gl.clearColor(0.7, 0.68, 0.64, 1);
  gl.clear(gl.COLOR_BUFFER_BIT);
  rows.forEach(([, n, rgb], i) => {
    const y = 300 - (i + 1) * rowH + 6;                  // scissor y counts from the bottom
    gl.scissor(230, y, Math.max(2, (n / max) * 330 * grow), rowH - 12);
    gl.clearColor(...rgb, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);
  });
  if (grow < 1) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>