<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. BIM Math
  3. Matrices and transforms
All lessons in this course

Lesson 27 of 44 13 minAdvanced

Composing transforms: order and inverses

Chain transforms by multiplying their matrices, read right to left. Scale, then rotate, then move is the usual order. Undo a chain with the inverses in reverse order, and invert a rigid placement cheaply with the transpose.

Real placements are chains: scale a block, turn it, move it into position, then carry it along with its storey and its building. Multiplying the matrices gives one matrix for the whole chain. The catch is the order, because, as the matrices lesson showed, A·B is usually not B·A.

Interactive

Order matters: composing transforms

First, A:
Then, B:
A·BB·A
A[0 −1 0] [1 0 0]
B[1 0 2] [0 1 1]
M = B·A[0 −1 2] [1 0 1]
A·B[0 −1 −1] [1 0 2]
M⁻¹ = A⁻¹·B⁻¹[0 1 −1] [−1 0 2]
“Rotate 90°” then “Move (2, 1)” is M = B·A: the matrix applied first sits on the right. The orange outline is the other order, A·B, and it ends up somewhere else.

Read right to left

In M = B·A, a point is multiplied as B·(A·p): A acts first, then B. The transform written nearest the point happens first. In the widget, the blue F is B·A and the orange outline is the other order, A·B. “Rotate 90°, then move (2, 1)” and “move (2, 1), then rotate 90°” end up in different places, because in the second case the move itself gets rotated.

Order.cs
var p = new V3(1, 0, 0);M4 rotate = M4.RotateZ(90), move = M4.Move(2, 1, 0); Console.WriteLine($"Rotate, then move (move * rotate): {(move * rotate).Point(p)}");Console.WriteLine($"Move, then rotate (rotate * move): {(rotate * move).Point(p)}"); // The usual order for placing an object: scale, then rotate, then moveM4 place = M4.Move(10, 5, 0) * M4.RotateZ(30) * M4.Scale(2, 2, 2);Console.WriteLine($"Scaled, turned and placed: {place.Point(p)}"); sealed class M4                       // 4x4, column vectors: p' = M * p{    public readonly double[,] A = new double[4, 4];     public static M4 Identity()    {        var m = new M4();        for (int i = 0; i < 4; i++) m.A[i, i] = 1;        return m;    }    public static M4 Move(double x, double y, double z)    {        var m = Identity();        m.A[0, 3] = x; m.A[1, 3] = y; m.A[2, 3] = z;        return m;    }    public static M4 Scale(double x, double y, double z)    {        var m = Identity();        m.A[0, 0] = x; m.A[1, 1] = y; m.A[2, 2] = z;        return m;    }    public static M4 RotateZ(double degrees)    {        double t = degrees * Math.PI / 180, c = Math.Cos(t), s = Math.Sin(t);        var m = Identity();        m.A[0, 0] = c; m.A[0, 1] = -s;        m.A[1, 0] = s; m.A[1, 1] = c;        return m;    }    public static M4 operator *(M4 p, M4 q)    {        var m = new M4();        for (int i = 0; i < 4; i++)            for (int j = 0; j < 4; j++)                for (int k = 0; k < 4; k++) m.A[i, j] += p.A[i, k] * q.A[k, j];        return m;    }    public V3 Point(V3 p) => Apply(p, 1);        // w = 1: moved    public V3 Direction(V3 v) => Apply(v, 0);    // w = 0: only turned and scaled    V3 Apply(V3 v, double w) => new(        A[0, 0] * v.X + A[0, 1] * v.Y + A[0, 2] * v.Z + A[0, 3] * w,        A[1, 0] * v.X + A[1, 1] * v.Y + A[1, 2] * v.Z + A[1, 3] * w,        A[2, 0] * v.X + A[2, 1] * v.Y + A[2, 2] * v.Z + A[2, 3] * w);} readonly record struct V3(double X, double Y, double Z){    public static V3 operator +(V3 a, V3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);    public static V3 operator -(V3 a, V3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z);    public static V3 operator *(V3 v, double k) => new(v.X * k, v.Y * k, v.Z * k);    public static double Dot(V3 a, V3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z;    public static V3 Cross(V3 a, V3 b) =>        new(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X);    public double Length => Math.Sqrt(Dot(this, this));    public V3 Normalized() => this * (1 / Length);    static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###");    public override string ToString() => $"({F(X)}, {F(Y)}, {F(Z)})";}
Output
Rotate, then move (move * rotate): (2, 2, 0)
Move, then rotate (rotate * move): (-1, 3, 0)
Scaled, turned and placed: (11.732, 6, 0)

Scale, rotate, move

The standard placement is M = T·R·S: scale first, then rotate, then move (translate). Scaling and rotating happen while the object is still at its own origin, so they act about the insertion point, and the move comes last so it is not itself scaled or turned. Swap any two and the result changes: T·S would scale the move, and R·T would swing the object round the world origin.

Some pairs do commute: two rotations about the same axis, two moves, a uniform scale with a rotation, or two scales. In the widget, try a stretch with a mirror in the Y axis. They are the exceptions; assume order matters unless you know otherwise.

Inverses: undo in reverse order

The inverse M⁻¹ undoes M: M⁻¹·M = I, the identity. For a chain, (B·A)⁻¹ = A⁻¹·B⁻¹: undo the last step first, just as you take off your shoes before your socks. Each simple transform has an easy inverse: move by −t, rotate by −θ, scale by 1/s, and a mirror is its own inverse.

Most BIM placements are rigid: rotation plus move, no scale. For those you never need a general matrix inverse. With M = [R | t], the inverse is [Rᵀ | −Rᵀ·t]: transpose the rotation, and rotate the negated move by it. It is exact, cheap and never divides by a near-zero determinant.

RigidInverse.cs
M4 place = M4.Move(10, 5, 0) * M4.RotateZ(30);M4 inverse = RigidInverse(place); var local = new V3(1.5, 0.5, 0);V3 world = place.Point(local);Console.WriteLine($"Local {local} -> world {world} -> back {inverse.Point(world)}");Console.WriteLine($"inverse * place is the identity: {IsIdentity(inverse * place)}"); static M4 RigidInverse(M4 m)            // rotation + move only: the inverse is [R^T | -R^T t]{    var r = M4.Identity();    for (int i = 0; i < 3; i++)        for (int j = 0; j < 3; j++) r.A[i, j] = m.A[j, i];          // transpose the rotation    for (int i = 0; i < 3; i++)        r.A[i, 3] = -(r.A[i, 0] * m.A[0, 3] + r.A[i, 1] * m.A[1, 3] + r.A[i, 2] * m.A[2, 3]);    return r;} static bool IsIdentity(M4 m, double eps = 1e-12){    for (int i = 0; i < 4; i++)        for (int j = 0; j < 4; j++)            if (Math.Abs(m.A[i, j] - (i == j ? 1 : 0)) > eps) return false;    return true;} sealed class M4                       // 4x4, column vectors: p' = M * p{    public readonly double[,] A = new double[4, 4];     public static M4 Identity()    {        var m = new M4();        for (int i = 0; i < 4; i++) m.A[i, i] = 1;        return m;    }    public static M4 Move(double x, double y, double z)    {        var m = Identity();        m.A[0, 3] = x; m.A[1, 3] = y; m.A[2, 3] = z;        return m;    }    public static M4 Scale(double x, double y, double z)    {        var m = Identity();        m.A[0, 0] = x; m.A[1, 1] = y; m.A[2, 2] = z;        return m;    }    public static M4 RotateZ(double degrees)    {        double t = degrees * Math.PI / 180, c = Math.Cos(t), s = Math.Sin(t);        var m = Identity();        m.A[0, 0] = c; m.A[0, 1] = -s;        m.A[1, 0] = s; m.A[1, 1] = c;        return m;    }    public static M4 operator *(M4 p, M4 q)    {        var m = new M4();        for (int i = 0; i < 4; i++)            for (int j = 0; j < 4; j++)                for (int k = 0; k < 4; k++) m.A[i, j] += p.A[i, k] * q.A[k, j];        return m;    }    public V3 Point(V3 p) => Apply(p, 1);        // w = 1: moved    public V3 Direction(V3 v) => Apply(v, 0);    // w = 0: only turned and scaled    V3 Apply(V3 v, double w) => new(        A[0, 0] * v.X + A[0, 1] * v.Y + A[0, 2] * v.Z + A[0, 3] * w,        A[1, 0] * v.X + A[1, 1] * v.Y + A[1, 2] * v.Z + A[1, 3] * w,        A[2, 0] * v.X + A[2, 1] * v.Y + A[2, 2] * v.Z + A[2, 3] * w);} readonly record struct V3(double X, double Y, double Z){    public static V3 operator +(V3 a, V3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);    public static V3 operator -(V3 a, V3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z);    public static V3 operator *(V3 v, double k) => new(v.X * k, v.Y * k, v.Z * k);    public static double Dot(V3 a, V3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z;    public static V3 Cross(V3 a, V3 b) =>        new(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X);    public double Length => Math.Sqrt(Dot(this, this));    public V3 Normalized() => this * (1 / Length);    static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###");    public override string ToString() => $"({F(X)}, {F(Y)}, {F(Z)})";}
Output
Local (1.5, 0.5, 0) -> world (11.049, 6.183, 0) -> back (1.5, 0.5, 0)
inverse * place is the identity: True
Quick check

In M = T·R·S, which transform is applied to a point first?

Quick check

A placement is P = T·R. What is P⁻¹?

Practice

Build “rotate 90° about the pivot (4, 2, 0)” as one matrix, T·R·T⁻¹, where T moves by the pivot. Apply it to (4, 2, 0), (6, 2, 0) and (4, 5, 0).

Show a hint

T⁻¹ moves the pivot to the origin (it acts first), R turns, and T moves it back. The pivot itself must not move.

Show the solution
Solution.cs
var pivot = new V3(4, 2, 0);M4 about = M4.Move(pivot.X, pivot.Y, pivot.Z) * M4.RotateZ(90) * M4.Move(-pivot.X, -pivot.Y, -pivot.Z); foreach (var p in new V3[] { new(4, 2, 0), new(6, 2, 0), new(4, 5, 0) })    Console.WriteLine($"{p} -> {about.Point(p)}"); sealed class M4                       // 4x4, column vectors: p' = M * p{    public readonly double[,] A = new double[4, 4];     public static M4 Identity()    {        var m = new M4();        for (int i = 0; i < 4; i++) m.A[i, i] = 1;        return m;    }    public static M4 Move(double x, double y, double z)    {        var m = Identity();        m.A[0, 3] = x; m.A[1, 3] = y; m.A[2, 3] = z;        return m;    }    public static M4 Scale(double x, double y, double z)    {        var m = Identity();        m.A[0, 0] = x; m.A[1, 1] = y; m.A[2, 2] = z;        return m;    }    public static M4 RotateZ(double degrees)    {        double t = degrees * Math.PI / 180, c = Math.Cos(t), s = Math.Sin(t);        var m = Identity();        m.A[0, 0] = c; m.A[0, 1] = -s;        m.A[1, 0] = s; m.A[1, 1] = c;        return m;    }    public static M4 operator *(M4 p, M4 q)    {        var m = new M4();        for (int i = 0; i < 4; i++)            for (int j = 0; j < 4; j++)                for (int k = 0; k < 4; k++) m.A[i, j] += p.A[i, k] * q.A[k, j];        return m;    }    public V3 Point(V3 p) => Apply(p, 1);        // w = 1: moved    public V3 Direction(V3 v) => Apply(v, 0);    // w = 0: only turned and scaled    V3 Apply(V3 v, double w) => new(        A[0, 0] * v.X + A[0, 1] * v.Y + A[0, 2] * v.Z + A[0, 3] * w,        A[1, 0] * v.X + A[1, 1] * v.Y + A[1, 2] * v.Z + A[1, 3] * w,        A[2, 0] * v.X + A[2, 1] * v.Y + A[2, 2] * v.Z + A[2, 3] * w);} readonly record struct V3(double X, double Y, double Z){    public static V3 operator +(V3 a, V3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);    public static V3 operator -(V3 a, V3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z);    public static V3 operator *(V3 v, double k) => new(v.X * k, v.Y * k, v.Z * k);    public static double Dot(V3 a, V3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z;    public static V3 Cross(V3 a, V3 b) =>        new(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X);    public double Length => Math.Sqrt(Dot(this, this));    public V3 Normalized() => this * (1 / Length);    static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###");    public override string ToString() => $"({F(X)}, {F(Y)}, {F(Z)})";}
Output
(4, 2, 0) -> (4, 2, 0)
(6, 2, 0) -> (4, 4, 0)
(4, 5, 0) -> (1, 2, 0)

Remember

  • In M = B·A, A acts first: read a chain of transforms from right to left.
  • Place objects with M = T·R·S: scale, then rotate, then move.
  • Only a few pairs commute; assume order matters.
  • (B·A)⁻¹ = A⁻¹·B⁻¹: undo the last step first.
  • A rigid transform [R | t] has the cheap, exact inverse [Rᵀ | −Rᵀ·t].
0 of 44 lessons completed