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.
Read right to left
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)})";}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
Inverses: undo in reverse order
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)})";}Local (1.5, 0.5, 0) -> world (11.049, 6.183, 0) -> back (1.5, 0.5, 0) inverse * place is the identity: True
Show a hint
Show the solution
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)})";}(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].