Model Matrix

The Model Matrix's Translation, Rotation and Scale

The model matrix moves an object from its own coordinates into the world. A translation puts x, y and z in the last column (elements 12 to 14), which works because positions carry a fourth coordinate, w = 1 (Clip Space); a scaling puts its factors on the diagonal; a rotation about a unit axis uses Rodrigues' formula. The seventh part of webgl-helper.js builds, multiplies and applies them (apply() rounds for printing):

webgl-helper.js, seventh part: translation, rotation, scaling and multiplicationJavaScript
// Section 3.6: 4x4 column-major matrices as Float32Arrays (Section 3.7 swaps in gl-matrix)
GLH.m4 = {
  identity: () => GLH.m4.scaling(1, 1, 1),
  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) {                  // counterclockwise about the unit axis 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]);
  },
  multiply(a, b) {                              // a * b: b's transformation happens first
    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) => GLH.m4.multiply(a, b)),   // A * B * C...
  apply: (m, [x, y, z, w = 1]) => [0, 1, 2, 3].map((r) =>     // m * (x, y, z, w)
    +(m[r] * x + m[4 + r] * y + m[8 + r] * z + m[12 + r] * w).toFixed(3)),
};

A positive angle turns counterclockwise as seen from the axis's positive end: 90 degrees about z takes x to y.

A cover moved by a translation, a rotation, a scaling and all three combined as T * R * SHTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  canvas { display: block; width: 100%; max-width: 600px; }
  .names { display: flex; max-width: 600px; text-align: center; font-family: monospace; }
  .names div { flex: 1; padding: 4px 2px; }
</style>
<canvas id="view" width="600" height="200"></canvas>
<div class="names"><div>translation(x, y, 0)</div><div>rotation(deg, 0, 0, 1)</div><div>scaling(sx, sy, 1)</div><div>T * R * S</div></div>
<script>
// 4x4 column-major matrices as Float32Arrays
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) {                  // counterclockwise about the unit axis 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]);
  },
  multiply(a, b) {                              // a * b: b's transformation happens first
    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 gl = document.getElementById('view').getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
uniform mat4 uMatrix; out vec2 vUV;
void main() {
  vUV = vec2[6](vec2(0), vec2(1, 0), vec2(1), vec2(0), vec2(1), vec2(0, 1))[gl_VertexID];
  gl_Position = uMatrix * vec4((vUV - 0.5) * vec2(0.5, 0.75), 0.0, 1.0);   // a cover around the origin
}`], [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; uniform vec3 uColor; in vec2 vUV; out vec4 c;
void main() { c = vec4(uColor * (vUV.x < 0.12 ? 0.6 : 1.0) * mix(0.8, 1.1, vUV.y), 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);
gl.bindVertexArray(gl.createVertexArray());
const uMatrix = gl.getUniformLocation(program, 'uMatrix');
const uColor = gl.getUniformLocation(program, 'uColor');
const aspect = m4.scaling(200 / 150, 1, 1);     // each panel is 150x200 pixels

gl.enable(gl.SCISSOR_TEST);
function frame(ms) {
  const t = ms * 0.001;
  const T = m4.translation(0.35 * Math.sin(t), 0.25 * Math.cos(t * 1.3), 0);
  const R = m4.rotation(40 * t, 0, 0, 1);
  const S = m4.scaling(1 + 0.35 * Math.sin(t * 1.7), 1 + 0.35 * Math.cos(t * 1.1), 1);
  const panels = [[T, [0.12, 0.37, 0.55]], [R, [0.36, 0.25, 0.6]], [S, [0.88, 0.6, 0.06]],
                  [m4.chain(T, R, S), [0.71, 0.27, 0.18]]];
  panels.forEach(([model, color], i) => {
    gl.viewport(i * 150, 0, 150, 200);
    gl.scissor(i * 150 + 2, 0, 146, 200);
    gl.clearColor(0.93, 0.91, 0.87, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.uniformMatrix4fv(uMatrix, false, m4.multiply(aspect, m4.scaling(0.12, 0.12, 1)));   // origin marker
    gl.uniform3f(uColor, 0.6, 0.6, 0.6);
    gl.drawArrays(gl.TRIANGLES, 0, 6);
    gl.uniformMatrix4fv(uMatrix, false, m4.multiply(aspect, model));
    gl.uniform3fv(uColor, color);
    gl.drawArrays(gl.TRIANGLES, 0, 6);
  });
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>