Transform Order

Composing Transformations in the Right Order

Matrix products apply right to left: in T * R * S the vertex is scaled, then rotated, then translated. Since rotation and scaling pivot about the origin, order matters, and the usual model matrix is T * R * S, which spins and sizes the object in place before moving it. The listing draws a cover both ways:

translation * rotation (left) against rotation * translation (right)HTML
<canvas id="c" width="960" height="260" style="width:480px;height:130px"></canvas>
<script src="webgl-helper.js"></script>
<script>
const gl = GLH.context('c'), m4 = GLH.m4;
const { uniforms } = GLH.load(gl, `#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, 0.0, 1.0);        // a unit quad around the origin
  }`, `#version 300 es
  precision mediump float; uniform vec3 uColor; in vec2 vUV; out vec4 c;
  void main() { c = vec4(uColor * (vUV.x < 0.1 ? 0.6 : 1.0), 1.0); }`);
const aspect = m4.scaling(130 / 480, 1, 1);     // stands in for Section 3.6.5's projection
const size = m4.scaling(0.55, 0.8, 1), turn = m4.rotation(25, 0, 0, 1);
const draw = (color, ...matrices) => {
  uniforms.uMatrix(m4.chain(aspect, ...matrices, size));
  uniforms.uColor(color);
  gl.drawArrays(gl.TRIANGLES, 0, 6);
};
draw(GLH.rgb(GLH.covers[0]), m4.translation(-1.1, 0, 0), turn);  // T * R: turns in place
draw(GLH.rgb(GLH.covers[4]), turn, m4.translation(1.1, 0, 0));   // R * T: orbits the origin
window.__done = true;
</script>
Browser output of Listing 3.46
Browser output of 46

On the left the cover turns about its own center; on the right the rotation, applied after the translation, swings it 25 degrees around the canvas center. To rotate about a pivot p, use T(p) * R * T(-p).

translation * rotation (left) against rotation * translation (right)HTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  canvas { display: block; width: 100%; max-width: 480px; }
  .names { display: flex; max-width: 480px; text-align: center; font-family: monospace; }
  .names div { flex: 1; padding: 4px 2px; }
</style>
<canvas id="c" width="960" height="260"></canvas>
<div class="names"><div>T * R: turns in place</div><div>R * T: orbits the origin</div></div>
<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]);
  },
  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)),   // A * B * C...
};

const gl = document.getElementById('c').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, 0.0, 1.0);        // a unit quad 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.1 ? 0.6 : 1.0), 1.0); }`]]) {
  const s = gl.createShader(type);
  gl.shaderSource(s, src); gl.compileShader(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(130 / 480, 1, 1);     // stands in for a projection matrix
const size = m4.scaling(0.55, 0.8, 1);
function draw(color, ...matrices) {
  gl.uniformMatrix4fv(uMatrix, false, m4.chain(aspect, ...matrices, size));
  gl.uniform3fv(uColor, color);
  gl.drawArrays(gl.TRIANGLES, 0, 6);
}
function frame(ms) {
  const turn = m4.rotation(25 + 20 * Math.sin(ms * 0.0012), 0, 0, 1);
  gl.clearColor(0.93, 0.91, 0.87, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT);
  draw([0.8, 0.78, 0.72], m4.scaling(0.15, 0.15, 1));                 // the origin (canvas centre)
  draw([0.12, 0.37, 0.55], m4.translation(-1.1, 0, 0), turn);         // T * R: turns in place
  draw([0.71, 0.27, 0.18], turn, m4.translation(1.1, 0, 0));          // R * T: swings around the origin
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>