Homogeneous coordinates and 4×4 transforms
A 3×3 matrix can rotate and scale but cannot move. Add a fourth coordinate w and a 4×4 matrix does all three; w = 1 marks a point and w = 0 a direction, which is why normals and offsets must be transformed differently from positions.
The 4×4 transform
| x | y | z | w | |
|---|---|---|---|---|
Transform has BasisX, BasisY, BasisZ and Origin, and an IFC IfcAxis2Placement3D stores a location plus two axis directions.
Points have w = 1, directions have w = 0
A point (a corner, an insertion point, a hinge) has w = 1, so it is moved. A direction (a face normal, an axis, an offset, a velocity) has w = 0, so it is only turned and scaled. Moving a building does not change which way its walls face. The difference of two points has w = 1 − 1 = 0: it is a direction, and the move cancels out. A point plus a direction has w = 1: still a point. Adding two points (w = 2) has no meaning on its own, which is a useful sanity check.
Point and Direction; in Revit they are OfPoint and OfVector.
// A column family placed at (5, 3, 0) and turned 90 degM4 place = M4.Move(5, 3, 0) * M4.RotateZ(90); var corner = new V3(0.2, 0.1, 0); // a corner of the column, in its own coordinatesvar faceNormal = new V3(1, 0, 0); // the way its +X face points Console.WriteLine($"Corner (w = 1): {place.Point(corner)}");Console.WriteLine($"Normal (w = 0): {place.Direction(faceNormal)}");Console.WriteLine($"Normal used as point: {place.Point(faceNormal)} <- wrong"); // The step between two points is a direction: the move cancels outvar a = new V3(0, 0, 0);var b = new V3(0.4, 0, 0);Console.WriteLine($"Width: {place.Point(b) - place.Point(a)} = {place.Direction(b - a)}"); 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)})";}Corner (w = 1): (4.9, 3.2, 0) Normal (w = 0): (0, 1, 0) Normal used as point: (5, 4, 0) <- wrong Width: (0, 0.4, 0) = (0, 0.4, 0)
Normals under non-uniform scale
// A wall face at 45 deg in plan, stretched to twice its length in XM4 stretch = M4.Scale(2, 1, 1);var along = new V3(1, -1, 0); // runs along the facevar normal = new V3(1, 1, 0).Normalized(); // at right angles to it V3 newAlong = stretch.Direction(along).Normalized();V3 naive = stretch.Direction(normal).Normalized();V3 right = new V3(normal.X / 2, normal.Y / 1, normal.Z / 1).Normalized(); // inverse transpose: divide Console.WriteLine($"Face now runs along {newAlong}");Console.WriteLine($"Naive normal {naive}: {Angle(naive, newAlong):F2} deg to the face");Console.WriteLine($"Inverse-transpose normal {right}: {Angle(right, newAlong):F2} deg to the face"); static double Angle(V3 a, V3 b) => Math.Acos(V3.Dot(a, b)) * 180 / Math.PI; 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)})";}Face now runs along (0.894, -0.447, 0) Naive normal (0.894, 0.447, 0): 53.13 deg to the face Inverse-transpose normal (0.447, 0.894, 0): 90.00 deg to the face
Show a hint
Point for the hinge and the handle and Direction for the opening direction.
Show the solution
M4 place = M4.Move(8, 2, 0) * M4.RotateZ(180); Console.WriteLine($"Hinge: {place.Point(new V3(0, 0, 0))}");Console.WriteLine($"Handle: {place.Point(new V3(0.9, 0.05, 1))}");Console.WriteLine($"Opens towards {place.Direction(new V3(0, 1, 0))}"); 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)})";}Hinge: (8, 2, 0) Handle: (7.1, 1.95, 1) Opens towards (0, -1, 0)
Remember
No 3×3 matrix can move the origin; a 4×4 matrix with the move in its last column can. The columns of a placement are the instance’s X, Y and Z axes and its origin. Points have w = 1 and are moved; directions, normals and offsets have w = 0 and are not. Some libraries use row vectors (p·M) with the move in the bottom row: the transpose of the column form. Under non-uniform scale, transform normals with the inverse transpose (for a scale, divide instead of multiply).