<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 26 of 44 14 minAdvanced

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.

A matrix always maps the origin to the origin: M·(0, 0, 0) = (0, 0, 0). So no 3×3 matrix can move (translate) anything, and placing a family instance at (5, 3, 0) needs something more. The standard trick is to give every point a fourth coordinate, w, and use 4×4 matrices. These are homogeneous coordinates.

Interactive

Homogeneous coordinates: points and directions

The wall is scaled, then rotated, then moved. P is its end point; the arrow is its face normal.

P′origin′
0.866−0.52
0.50.8661
001
P (3, 0, w = 1)→ (4.598, 2.5)
n (0, 1, w = 0)→ (−0.5, 0.866)
|n′|1.000
The last column holds the move. A point has w = 1, so it is moved; the normal has w = 0, so it is only turned and scaled.

The 4×4 transform

A transform that rotates or scales with a 3×3 matrix R and then moves by t = (tx, ty, tz) is written as one 4×4 matrix. R fills the top-left block, t is the last column, and the bottom row is 0, 0, 0, 1:

xyzw
x′R₁₁R₁₂R₁₃tx
y′R₂₁R₂₂R₂₃ty
z′R₃₁R₃₂R₃₃tz
w′0001

Multiply it by (x, y, z, w) and the move is added w times: x′ = R₁₁x + R₁₂y + R₁₃z + tx·w. The columns still mean something: the first three are where the X, Y and Z axes go, and the last is where the origin goes. That is exactly what a BIM placement stores: Revit’s 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.

Libraries usually give two methods instead of making you write w. In the code below they are Point and Direction; in Revit they are OfPoint and OfVector.

Homogeneous.cs
// 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)})";}
Output
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

Treating a normal as a direction is right for rotations and moves. With a non-uniform scale, though, it fails: stretch a 45° wall face to twice its length in X, and its old normal, stretched the same way, is no longer at right angles to the face. Normals must be transformed by the inverse transpose of the 3×3 part. For a scale that simply means dividing by the scale factors instead of multiplying.

NormalScale.cs
// 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)})";}
Output
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

For rotations and uniform scales the inverse transpose points the same way as the matrix itself, which is why the shortcut usually works. Placements in BIM are almost always rotation plus move, but scaled blocks, stretched imported meshes and family types with non-uniform scale do occur, and that is where shading and clash normals go wrong.

Quick check

A transform moves by (10, 0, 0) and turns 90° about Z. What happens to the direction (1, 0, 0)?

Quick check

Which of these should be transformed with w = 0?

Practice

A door family is placed at (8, 2, 0) and turned 180°. In the family, the hinge is at (0, 0, 0), the handle at (0.9, 0.05, 1) and the leaf opens towards (0, 1, 0). Print the hinge, the handle and the opening direction in world coordinates.

Show a hint

Build the placement as Move · RotateZ, then use Point for the hinge and the handle and Direction for the opening direction.

Show the solution
Solution.cs
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)})";}
Output
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).
0 of 44 lessons completed