<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. BIM Math
  3. 3D geometry and solids
All lessons in this course

Lesson 36 of 44 16 minAdvanced

Clash detection and the separating-axis theorem

Find elements that run into each other: a cheap broad phase with boxes, then an exact test for convex shapes based on the separating-axis theorem, with tolerances for touching and clearance.

Clash detection finds places where the structural, architectural and services models disagree: a duct through a beam, a pipe through a door opening, a cable tray too close to a sprinkler. A coordinated hospital can have hundreds of thousands of elements, so checking every pair (n·(n − 1)/2 pairs, about 5 × 10¹⁰ for 300 000 elements) is out of the question.

Every clash checker therefore works in two phases:

  1. Broad phase: compare cheap bounding boxes, using sorting or a spatial index, to find the few pairs that might touch.
  2. Narrow phase: run an exact test on those candidate pairs only.

Broad phase: sweep and prune

One simple broad phase sorts the AABBs by their minimum X. Walking along that list, each box only needs comparing with the boxes that start before it ends on X; as soon as a box starts beyond its end, so do all later ones.

BroadPhase.cs
var items = new (string Name, double X0, double X1, double Y0, double Y1, double Z0, double Z1)[]{    ("beam B1",   0,   6,    0,   0.3, 3.0, 3.5),    ("duct D1",   2,   2.4, -1,   2,   3.2, 3.6),    ("pipe P1",   2,   2.2, -1,   2,   3.6, 3.8),    ("column C1", 5.8, 6.2, -0.1, 0.3, 0,   3.0),    ("beam B2",   7,  13,    0,   0.3, 3.0, 3.5),    ("duct D2",  12,  12.4, -1,   2,   3.3, 3.7)}; var order = items.OrderBy(i => i.X0).ToArray();int tests = 0;for (int i = 0; i < order.Length; i++)    for (int j = i + 1; j < order.Length && order[j].X0 < order[i].X1; j++)    {        tests++;        var (a, b) = (order[i], order[j]);        if (a.Y0 < b.Y1 && b.Y0 < a.Y1 && a.Z0 < b.Z1 && b.Z0 < a.Z1)            Console.WriteLine($"candidate: {a.Name} and {b.Name}");    }Console.WriteLine($"{tests} box tests instead of {items.Length * (items.Length - 1) / 2}");
Output
candidate: beam B1 and duct D1
candidate: beam B2 and duct D2
5 box tests instead of 15

The column touches the underside of beam B1 (both at z = 3.0) and is correctly not a candidate, and the pipe sits just above the beam. For large models the same job is done with a uniform grid, an octree or a BVH; the DSA course’s spatial indexes lesson covers them.

The separating-axis theorem

For the narrow phase, many elements are well approximated by convex shapes: boxes for beams, ducts, columns and slabs. For two convex shapes there is a beautiful exact test. Two convex shapes do not overlap if and only if there is a direction in which their shadows (projections) do not overlap. Such a direction is a separating axis, and a plane at right angles to it fits between the shapes.

The catch is that there are infinitely many directions. The theorem’s second half says which ones are enough. For two polygons in 2D, the normals of their edges: two rectangles have only four. Try it in the widget: pick each axis to see both shadows on it.

Interactive

Separating-axis test

AB
axisoverlap
A₁0.01 m
A₂0.58 m
B₁gap 0.09 m
B₂1.20 m
No clash. On axis B₁ the two shadows have a gap of 0.09 m, and one gap is enough: a line at right angles to that axis fits between the boxes.

Projecting a box onto an axis does not need its corners. The shadow is centred on the projection of the box’s centre, with a half length (its radius) of hx·|U·axis| + hy·|V·axis|, using the box’s own axes U and V and half sizes:

Sat2D.cs
var duct = new Rect(new P2(3.4, 3.2), 2.2, 0.8, 20); foreach (var (x, y, deg) in new[] { (6.0, 4.0, 60.0), (7.0, 4.6, 60.0), (6.4, 5.4, 30.0) })    Console.WriteLine($"B at ({x}, {y}), {deg} deg: {Sat(duct, new Rect(new P2(x, y), 1.3, 0.6, deg))}"); static string Sat(Rect a, Rect b){    var axes = new[] { ("A1", a.U), ("A2", a.V), ("B1", b.U), ("B2", b.V) };    double least = double.MaxValue;    string leastName = "";    foreach (var (name, ax) in axes)    {        var (a0, a1) = a.Project(ax);        var (b0, b1) = b.Project(ax);        double overlap = Math.Min(a1, b1) - Math.Max(a0, b0);        if (overlap <= 0) return $"apart (gap {-overlap:0.00} on {name})";        if (overlap < least) (least, leastName) = (overlap, name);    }    return $"clash (smallest overlap {least:0.00} on {leastName})";} readonly record struct P2(double X, double Y); readonly record struct Rect(P2 C, double Hx, double Hy, double Deg){    double T => Deg * Math.PI / 180;    public P2 U => new(Math.Cos(T), Math.Sin(T));    public P2 V => new(-Math.Sin(T), Math.Cos(T));     public (double, double) Project(P2 ax)    {        double c = C.X * ax.X + C.Y * ax.Y;        double r = Hx * Math.Abs(U.X * ax.X + U.Y * ax.Y) + Hy * Math.Abs(V.X * ax.X + V.Y * ax.Y);        return (c - r, c + r);    }}
Output
B at (6, 4), 60 deg: clash (smallest overlap 0.78 on B2)
B at (7, 4.6), 60 deg: apart (gap 0.28 on A1)
B at (6.4, 5.4), 30 deg: apart (gap 0.09 on B1)

The last case shows why both boxes’ axes are needed: on A’s two axes the shadows overlap (on A1 by just 0.01 m), and only B’s own axis B1 finds the 0.09 m gap. The test stops at the first gap, so pairs that are clearly apart cost very little. When every axis overlaps, the smallest overlap is a bonus: it is the shortest distance one shape would have to move to clear the other, which a clash report can show as the depth of the clash.

Boxes in 3D: fifteen axes

In 3D, face normals are not enough. Two boxes can miss each other edge to edge, like two sticks crossing at different heights, with every face normal showing an overlap. The extra candidates are the cross products of an edge direction of one box with an edge direction of the other. For two OBBs that is 3 + 3 face axes plus 3 × 3 edge–edge axes: 15 in all.

ObbClash.cs
var beam = new Obb(new V3(0, 0, 0), Rot(0, 0), new V3(2, 0.15, 0.3));     // 4 m beam along X var cases = new (string Name, V3 Centre)[]{    ("duct through the beam", new(0, 0, 0.3)),    ("duct just past the beam edge", new(0, -1, 0.3)),    ("duct well above", new(0, 0, 1.6))};foreach (var (name, c) in cases){    var duct = new Obb(c, Rot(30, 45), new V3(0.1, 1.5, 0.1));         // a sloping duct    Console.WriteLine($"{name}: {Separating(beam, duct) ?? "clash"}");} static string? Separating(Obb a, Obb b){    var axes = new List<(string, V3)>();    for (int i = 0; i < 3; i++) axes.Add(($"A{"xyz"[i]}", a.Axes[i]));    for (int i = 0; i < 3; i++) axes.Add(($"B{"xyz"[i]}", b.Axes[i]));    for (int i = 0; i < 3; i++)        for (int j = 0; j < 3; j++)        {            var c = V3.Cross(a.Axes[i], b.Axes[j]);            if (c.Length > 1e-9) axes.Add(($"A{"xyz"[i]} x B{"xyz"[j]}", c));   // skip parallel edges        }     var d = b.Centre - a.Centre;    foreach (var (name, l) in axes)        if (Math.Abs(V3.Dot(d, l)) > a.Radius(l) + b.Radius(l)) return $"apart, separated on {name}";    return null;} // Rz(z) * Rx(x), returned as the three turned axesstatic V3[] Rot(double z, double x){    double cz = Math.Cos(z * Math.PI / 180), sz = Math.Sin(z * Math.PI / 180);    double cx = Math.Cos(x * Math.PI / 180), sx = Math.Sin(x * Math.PI / 180);    return new V3[] { new(cz, sz, 0), new(-sz * cx, cz * cx, sx), new(sz * sx, -cz * sx, cx) };} readonly record struct Obb(V3 Centre, V3[] Axes, V3 Half){    // Half the length of the box's shadow on the direction l    public double Radius(V3 l) =>        Half.X * Math.Abs(V3.Dot(Axes[0], l)) + Half.Y * Math.Abs(V3.Dot(Axes[1], l)) + Half.Z * Math.Abs(V3.Dot(Axes[2], l));} 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
duct through the beam: clash
duct just past the beam edge: apart, separated on Ax x By
duct well above: apart, separated on Az

The middle case is the one that needs the extra axes: the duct passes beside the beam’s edge, every face axis shows an overlap, and only the cross product of the beam’s length with the duct’s length finds the gap. Comparing |d · l| with the two radii avoids normalising l, because both sides scale by the same length.

Touching, tolerances and clearance

  • Touching is not clashing. A beam sitting on a column, or a duct resting on a hanger, shares a face. Exact zero overlaps are rare in floating point, so checkers treat an overlap smaller than a tolerance (for example 1 mm) as touching.
  • Clearance (soft) clashes. Some elements must keep a distance: insulation around a pipe, access space in front of a panel. Inflate one shape by the clearance and test again, or compute the actual distance between the shapes and compare it with the requirement.
  • Duplicates. Two identical elements on top of each other overlap completely. Reporting them separately (same type, same geometry) keeps the clash list readable.
Quick check

Two rectangles in plan have a gap on one of their four edge-normal axes and overlap on the other three. What do you conclude?

Quick check

Why does the 3D box test need the nine edge × edge axes as well as the six face normals?

Practice

Write a soft-clash check for two AABBs: compute the shortest distance between them (0 if they overlap) and report a clash if it is less than the required clearance. A pipe with 50 mm insulation needs 0.05 m clear. Test the box (0, 0, 3) .. (6, 0.3, 3.5) against (2, −1, 3.53) .. (2.2, 2, 3.73), (2, −1, 3.6) .. (2.2, 2, 3.8) and (6.04, 0.32, 0) .. (6.4, 0.6, 3.5).

Show a hint

On each axis the gap is max(0, b.Min − a.Max, a.Min − b.Max). The distance is the length of the vector of the three gaps.

Show the solution
Solution.cs
var beam = (Min: new V3(0, 0, 3), Max: new V3(6, 0.3, 3.5));var others = new (V3 Min, V3 Max)[]{    (new V3(2, -1, 3.53), new V3(2.2, 2, 3.73)),    (new V3(2, -1, 3.6), new V3(2.2, 2, 3.8)),    (new V3(6.04, 0.32, 0), new V3(6.4, 0.6, 3.5))};const double Clearance = 0.05;foreach (var o in others){    var gap = new V3(Gap(beam.Min.X, beam.Max.X, o.Min.X, o.Max.X),                     Gap(beam.Min.Y, beam.Max.Y, o.Min.Y, o.Max.Y),                     Gap(beam.Min.Z, beam.Max.Z, o.Min.Z, o.Max.Z));    double dist = gap.Length;    Console.WriteLine($"{o.Min} .. {o.Max}: {dist:0.000} m apart -> {(dist < Clearance ? "soft clash" : "ok")}");} static double Gap(double a0, double a1, double b0, double b1) => Math.Max(0, Math.Max(b0 - a1, a0 - b1)); 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
(2, -1, 3.53) .. (2.2, 2, 3.73): 0.030 m apart -> soft clash
(2, -1, 3.6) .. (2.2, 2, 3.8): 0.100 m apart -> ok
(6.04, 0.32, 0) .. (6.4, 0.6, 3.5): 0.045 m apart -> soft clash

Remember

  • Clash detection runs a cheap broad phase on bounding boxes, then an exact narrow phase on the few candidates.
  • Sweep and prune sorts boxes by min X so each box is only compared with boxes that start before it ends.
  • Separating-axis theorem: two convex shapes are apart if and only if their shadows are apart on some axis.
  • Rectangles need their 4 edge normals; boxes in 3D need 6 face normals plus 9 edge × edge cross products.
  • Use a tolerance so touching elements are not clashes, and clearance distances for soft clashes.
0 of 44 lessons completed