<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 29 of 44 13 minAdvanced

Mirroring and handedness

A mirror is a transform with determinant −1. It turns a right-handed frame into a left-handed one, reverses the winding of every outline and flips normals inwards. Detect it with the determinant and fix the faces, or store it as a flip.

Mirroring is common in BIM: mirrored apartments on either side of a corridor, a left-hand door copied as a right-hand one, a symmetrical facade. It looks like just another transform, but it is the one that breaks things: a mirrored door changes its hand, outlines run the other way round, and solids turn inside out unless the software notices.

Interactive

Mirroring a door

det of the 2×2 part−1
X′ × Y′−Z (left-handed)
leaf outlineclockwise
hingeopposite hand
as flip + rotationflip local X, then rotate 0°
A mirror has det = −1. X′ × Y′ now points down (⊗), every outline’s winding reverses, and a left-hand door becomes a right-hand one. Store it as a rotation of 0° plus a “flipped” flag, and reverse the face order when you build geometry, or normals will point inwards.

The mirror matrix

Mirroring a point P in a plane with unit normal n and offset d moves it twice its signed distance back through the plane: P′ = P − 2(n·P + d)·n. A direction only loses twice its component along n: v′ = v − 2(n·v)·n. As a matrix, the 3×3 part is I − 2·n·nᵀ. In 2D, the mirror in a line at angle φ has columns (cos 2φ, sin 2φ) and (sin 2φ, −cos 2φ).

MirrorPlane.cs
// Mirror a door across the grid line x = 5: the plane with unit normal (1, 0, 0) and d = -5var n = new V3(1, 0, 0);double d = -5; var hinge = new V3(3, 2, 0);var across = new V3(1, 0, 0);       // from the hinge across the openingvar swing = new V3(0, 1, 0);        // the way the leaf opens Console.WriteLine($"Hinge  {hinge} -> {MirrorPoint(hinge, n, d)}");Console.WriteLine($"Across {across} -> {MirrorDirection(across, n)}");Console.WriteLine($"Swing  {swing} -> {MirrorDirection(swing, n)}"); static V3 MirrorPoint(V3 p, V3 n, double d) => p - n * (2 * (V3.Dot(n, p) + d));   // n is a unit normalstatic V3 MirrorDirection(V3 v, V3 n) => v - n * (2 * V3.Dot(n, v)); 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  (3, 2, 0) -> (7, 2, 0)
Across (1, 0, 0) -> (-1, 0, 0)
Swing  (0, 1, 0) -> (0, 1, 0)

The hinge lands on the other side of the grid line and “across” now points the other way, while “swing” is unchanged. The door now opens from its other side: a left-hand door has become a right-hand one. No rotation can do that, which you can check in the widget by switching to “Rotate”.

Handedness and the determinant

A frame is right-handed when X × Y = Z, as in every BIM tool. After a mirror, the transformed axes satisfy X′ × Y′ = −Z′: the frame is left-handed. The test is the determinant of the 3×3 part, which equals the triple product (X′ × Y′)·Z′: positive for rotations, negative whenever an odd number of mirrors is involved. Two mirrors make a rotation again.

Handedness.cs
var instances = new (string Name, M4 M)[]{    ("D1, turned 90", M4.Move(4, 0, 0) * M4.RotateZ(90)),    ("D2, mirrored", M4.Move(10, 0, 0) * M4.Scale(-1, 1, 1)),    ("D3, mirrored and turned", M4.Move(14, 0, 0) * M4.RotateZ(90) * M4.Scale(1, -1, 1)),    ("D4, mirrored twice", M4.Scale(-1, 1, 1) * M4.Scale(1, -1, 1))}; foreach (var (name, m) in instances){    V3 x = m.Direction(new V3(1, 0, 0)), y = m.Direction(new V3(0, 1, 0)), z = m.Direction(new V3(0, 0, 1));    double triple = V3.Dot(V3.Cross(x, y), z);    Console.WriteLine($"{name,-24} (X x Y).Z = {triple,2}  {(triple < 0 ? "mirrored" : "not mirrored")}");} 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
D1, turned 90            (X x Y).Z =  1  not mirrored
D2, mirrored             (X x Y).Z = -1  mirrored
D3, mirrored and turned  (X x Y).Z = -1  mirrored
D4, mirrored twice       (X x Y).Z =  1  not mirrored

What breaks, and how to fix it

  • Winding reverses. A counterclockwise outline becomes clockwise, so signed areas turn negative and the outer/hole convention is swapped.
  • Normals flip inwards. A face normal computed as (B − A) × (C − A) from the mirrored corners points into the solid, so lighting, clash tests and volume signs go wrong.
  • Hands and text swap. Doors, stairs and other “handed” families change hand, and any text or symbol modelled in 3D reads backwards.

The fix for geometry is mechanical: when det < 0, reverse the order of every face’s vertices after transforming them. Mesh exporters, IFC viewers and game engines all do this.

FixWinding.cs
// The top face of a slab, counterclockwise seen from above, so its normal points upvar face = new V3[] { new(0, 0, 0.2), new(4, 0, 0.2), new(4, 3, 0.2), new(0, 3, 0.2) };M4 mirror = M4.Scale(-1, 1, 1); var moved = face.Select(mirror.Point).ToArray();Console.WriteLine($"Before:   normal {Normal(face)}");Console.WriteLine($"Mirrored: normal {Normal(moved)}  <- points into the slab"); if (Det3(mirror) < 0) Array.Reverse(moved);Console.WriteLine($"Reversed: normal {Normal(moved)}"); static V3 Normal(V3[] p) => V3.Cross(p[1] - p[0], p[2] - p[0]).Normalized(); static double Det3(M4 m) =>      m.A[0, 0] * (m.A[1, 1] * m.A[2, 2] - m.A[1, 2] * m.A[2, 1])    - m.A[0, 1] * (m.A[1, 0] * m.A[2, 2] - m.A[1, 2] * m.A[2, 0])    + m.A[0, 2] * (m.A[1, 0] * m.A[2, 1] - m.A[1, 1] * m.A[2, 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
Before:   normal (0, 0, 1)
Mirrored: normal (0, 0, -1)  <- points into the slab
Reversed: normal (0, 0, 1)

Storing a mirror as a flip

Every mirror can be written as a flip of one local axis followed by a rotation; the widget shows the angle. BIM tools use this to keep placements as tidy rotations. Revit, for example, reports Mirrored, HandFlipped and FacingFlipped on a FamilyInstance, so you can ask whether a door has been mirrored rather than digging the sign out of its matrix. When reading placements from any source, still check the determinant: it is the one test that never lies.

Quick check

What is the determinant of any mirror?

Quick check

An instance is mirrored in X and then mirrored in Y. What is it now?

Practice

A room has corners (1, 1, 0), (5, 1, 0), (5, 4, 0) and (1, 4, 0), counterclockwise. Mirror it in the plane x = 0 with M4.Scale(−1, 1, 1). Print its signed area (shoelace on X and Y) before and after mirroring, then reverse the mirrored corners and print it again.

Show a hint

The shoelace sum is Σ (x_i·y_(i+1) − x_(i+1)·y_i) ÷ 2. Use Array.Reverse on the mirrored array.

Show the solution
Solution.cs
var room = new V3[] { new(1, 1, 0), new(5, 1, 0), new(5, 4, 0), new(1, 4, 0) };M4 mirror = M4.Scale(-1, 1, 1); var moved = room.Select(mirror.Point).ToArray();Console.WriteLine($"Before: {Area(room)}");Console.WriteLine($"Mirrored: {Area(moved)}");Array.Reverse(moved);Console.WriteLine($"Reversed: {Area(moved)}"); static double Area(V3[] p){    double s = 0;    for (int i = 0; i < p.Length; i++)    {        V3 a = p[i], b = p[(i + 1) % p.Length];        s += a.X * b.Y - b.X * a.Y;    }    return s / 2;} 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
Before: 12
Mirrored: -12
Reversed: 12

Remember

  • Mirror a point in a plane with P′ = P − 2(n·P + d)·n; the matrix is I − 2·n·nᵀ.
  • A mirror has det = −1 and turns a right-handed frame left-handed; two mirrors make a rotation.
  • Check the determinant (or the triple product of the axes) to detect mirrored instances.
  • After a mirror, reverse each face’s vertex order so normals point outwards again.
  • Tools often store a mirror as a flip plus a rotation, but the determinant is the reliable test.
0 of 44 lessons completed