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