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

Local vs world: families, instances and nested placement

A family is drawn once in its own local coordinates; each instance stores a placement that maps local to world. Nested families multiply placements down the chain, and the inverse takes world points back into an object’s own coordinates.

A door family, a desk block or a light fixture type is modelled once, around its own origin, in local coordinates: the door’s hinge at (0, 0, 0), its width along local X. Every instance in the project stores only a placement, a transform from that local space to the world (project) space. A hundred doors share one geometry and keep a hundred small matrices.

Interactive

Local and world coordinates

Drag the desk’s insertion point D and the world point W, and turn the desk and the chair.

QDW
Q, local → world
in the chair(0.40, 0.40)
in the desk(1.60, −0.30)
in the world(0.54, 0.54)
W, world → local
in the world(−0.25, 1.25)
in the desk(1.27, 0.71)
in the chair(0.07, 1.41)
World = DeskInWorld · ChairInDesk · local. The chair is stored once, relative to the desk, so turning the desk carries it along. Going back needs the inverses: W is at (1.27, 0.71) in desk coordinates, so it is on the desk top (the desk runs 0 to 2.4 by 0 to 1.2).

Local to world

world = Placement · local. The placement’s columns are the instance’s own X, Y and Z axes, seen in the world, plus its origin. Revit exposes this as FamilyInstance.GetTransform(), with BasisX, BasisY, BasisZ and Origin. IFC stores each element’s ObjectPlacement as an IfcLocalPlacement, a position and axes relative to another placement.

Nested placement

Placements nest. A chair inside a desk family is placed relative to the desk, the desk relative to its storey, the storey relative to the building and the building relative to the site. To get world coordinates, multiply down the chain, outermost on the left:

world = Building · Storey · Desk · Chair · local

This is why IFC placements form a chain through PlacementRelTo, and why moving a storey up by 100 mm moves everything on it without touching a single element. In the widget, turn the desk and the chair turns with it, while its position inside the desk stays the same.

NestedPlacement.cs
M4 storey = M4.Move(0, 0, 3.6);                       // level 2, in the buildingM4 desk = M4.Move(10, 4, 0) * M4.RotateZ(30);         // the desk instance, on the storeyM4 chair = M4.Move(1.2, -0.7, 0);                     // the chair, nested in the desk family var seatCorner = new V3(0.4, 0.4, 0.45);              // in the chair's own coordinatesM4 chairToWorld = storey * desk * chair; Console.WriteLine($"In the desk:   {chair.Point(seatCorner)}");Console.WriteLine($"On the storey: {(desk * chair).Point(seatCorner)}");Console.WriteLine($"In the world:  {chairToWorld.Point(seatCorner)}");Console.WriteLine($"Chair faces    {chairToWorld.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
In the desk:   (1.6, -0.3, 0.45)
On the storey: (11.536, 4.54, 0.45)
In the world:  (11.536, 4.54, 4.05)
Chair faces    (-0.5, 0.866, 0)

World to local

Going the other way answers many everyday questions: is this clicked point on the desk? Which side of the wall is that pipe? Is this point inside the room’s bounding box? Bring the world point into the object’s own coordinates with the inverse placement, and the test becomes simple there, because in local coordinates the desk is just 0 ≤ x ≤ 2.4 and 0 ≤ y ≤ 1.2. For a nested object, apply the inverses in reverse order: Chair⁻¹ · Desk⁻¹ · world.

WorldToLocal.cs
M4 desk = M4.Move(10, 4, 3.6) * M4.RotateZ(30);M4 toDesk = RigidInverse(desk); foreach (var w in new V3[] { new(11, 5, 4.35), new(9, 6, 4.35) }){    V3 p = toDesk.Point(w);    bool over = p.X >= 0 && p.X <= 2.4 && p.Y >= 0 && p.Y <= 1.2;    Console.WriteLine($"{w} is {p} in desk coordinates: {(over ? "over the desk" : "not over the desk")}");} static M4 RigidInverse(M4 m){    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];    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;} 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
(11, 5, 4.35) is (1.366, 0.366, 0.75) in desk coordinates: over the desk
(9, 6, 4.35) is (0.134, 2.232, 0.75) in desk coordinates: not over the desk
Quick check

A chair is nested in a desk, which is placed on a storey. Which product takes the chair’s local points to the world?

Quick check

Why are families modelled in local coordinates?

Practice

Three desks have their chair nested at (1.2, −0.7, 0) in desk coordinates. The desks are at (0, 0, 0), (5, 0, 0) and (10, 0, 0), turned 0°, 90° and 180°. Print where the chair’s seat corner, (0.4, 0.4, 0) in chair coordinates, ends up for each desk.

Show a hint

For each desk, multiply Move · RotateZ · chairInDesk, and apply it to the corner with Point.

Show the solution
Solution.cs
M4 chairInDesk = M4.Move(1.2, -0.7, 0);var corner = new V3(0.4, 0.4, 0);var desks = new (V3 At, double Turn)[] { (new(0, 0, 0), 0), (new(5, 0, 0), 90), (new(10, 0, 0), 180) }; foreach (var (at, turn) in desks){    M4 chairToWorld = M4.Move(at.X, at.Y, at.Z) * M4.RotateZ(turn) * chairInDesk;    Console.WriteLine($"Desk at {at}, turned {turn} deg: corner at {chairToWorld.Point(corner)}");} 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
Desk at (0, 0, 0), turned 0 deg: corner at (1.6, -0.3, 0)
Desk at (5, 0, 0), turned 90 deg: corner at (5.3, 1.6, 0)
Desk at (10, 0, 0), turned 180 deg: corner at (8.4, 0.3, 0)

Remember

  • Families are modelled once in local coordinates; each instance stores a placement.
  • world = Placement · local; the placement’s columns are the instance’s axes and origin.
  • Nested placements multiply down the chain, outermost on the left.
  • The inverse placement takes world points into an object’s own coordinates, where tests are simple.
  • Keeping geometry local keeps its coordinates small and precise.
0 of 44 lessons completed