B-rep and NURBS: how BIM kernels store solids
What is inside a geometry kernel: boundary representation with faces, loops, edges and vertices, Euler–Poincaré checks, B-spline and NURBS curves, exact circles, and tessellation to a tolerance.
B-rep: topology plus geometry
| Topology | Carries the geometry | Example for a curved wall |
|---|---|---|
// A box: 8 vertices, 6 faces with one loop each (vertex numbers counter-clockwise from outside)var box = new BRep(8, new[]{ new[] { new[] { 0, 3, 2, 1 } }, new[] { new[] { 4, 5, 6, 7 } }, new[] { new[] { 0, 1, 5, 4 } }, new[] { new[] { 1, 2, 6, 5 } }, new[] { new[] { 2, 3, 7, 6 } }, new[] { new[] { 3, 0, 4, 7 } },});box.Report("box"); // A square tube (a box with a square hole through it): the top and bottom faces have two loops each.// Outer corners 0-3 (bottom) and 4-7 (top), hole corners 8-11 (bottom) and 12-15 (top).var tube = new BRep(16, new[]{ new[] { new[] { 0, 3, 2, 1 }, new[] { 8, 9, 10, 11 } }, // bottom face with the hole new[] { new[] { 4, 5, 6, 7 }, new[] { 12, 15, 14, 13 } }, // top face with the hole new[] { new[] { 0, 1, 5, 4 } }, new[] { new[] { 1, 2, 6, 5 } }, new[] { new[] { 2, 3, 7, 6 } }, new[] { new[] { 3, 0, 4, 7 } }, new[] { new[] { 8, 12, 13, 9 } }, new[] { new[] { 9, 13, 14, 10 } }, // the four faces inside the hole new[] { new[] { 10, 14, 15, 11 } }, new[] { new[] { 11, 15, 12, 8 } },});tube.Report("tube"); sealed class BRep{ readonly int vertices; readonly int[][][] faces; // face -> loops -> vertex numbers public BRep(int vertices, int[][][] faces) { this.vertices = vertices; this.faces = faces; } public void Report(string name) { var used = new Dictionary<(int, int), int>(); // directed edge -> how often a loop walks it int loops = 0; foreach (var face in faces) foreach (var loop in face) { loops++; for (int i = 0; i < loop.Length; i++) { var e = (loop[i], loop[(i + 1) % loop.Length]); used[e] = used.GetValueOrDefault(e) + 1; } } int edges = used.Count / 2; bool paired = used.All(kv => kv.Value == 1 && used.GetValueOrDefault((kv.Key.Item2, kv.Key.Item1)) == 1); int v = vertices, f = faces.Length, lhs = v - edges + f - (loops - f); int genus = 1 - lhs / 2; // one shell: lhs = 2(1 - G) Console.WriteLine($"{name}: V {v}, E {edges}, F {f}, L {loops}; V - E + F - (L - F) = {lhs}; edges paired: {paired}; genus {genus}"); }}box: V 8, E 12, F 6, L 6; V - E + F - (L - F) = 2; edges paired: True; genus 0 tube: V 16, E 24, F 10, L 12; V - E + F - (L - F) = 0; edges paired: True; genus 1
B-splines: smooth curves from control points
Local control: each basis function is non-zero only over p + 1 knot spans, so moving one control point changes only a nearby piece of the curve. Partition of unity: the basis functions always add up to 1, so the curve stays inside the convex hull of its control points and moves with them under any affine transform.
var ctrl = new V2[] { new(0.4, 0.6), new(1.4, 4.2), new(3, 1), new(4.6, 4.6), new(5.8, 1.4) };int p = 3;double[] knots = { 0, 0, 0, 0, 0.5, 1, 1, 1, 1 }; // clamped: p + 1 zeros and ones foreach (double t in new[] { 0.0, 0.25, 0.5, 1.0 }){ var n = Basis(t, p, knots, ctrl.Length); var point = new V2(0, 0); for (int i = 0; i < ctrl.Length; i++) point += ctrl[i] * n[i]; Console.WriteLine($"t = {t,4}: N = [{string.Join(", ", n.Select(v => v.ToString("0.000")))}], sum {n.Sum():0.###}, point {point}");} // Cox-de Boor: start with step functions (degree 0) and blend neighbours up to degree pstatic double[] Basis(double t, int p, double[] k, int count){ var n = new double[k.Length - 1]; for (int i = 0; i < n.Length; i++) n[i] = (t >= k[i] && t < k[i + 1]) || (t == k[^1] && k[i] < t && k[i + 1] == t) ? 1 : 0; for (int d = 1; d <= p; d++) for (int i = 0; i < k.Length - d - 1; i++) { double a = k[i + d] - k[i], b = k[i + d + 1] - k[i + 1]; n[i] = (a > 0 ? (t - k[i]) / a * n[i] : 0) + (b > 0 ? (k[i + d + 1] - t) / b * n[i + 1] : 0); } return n[..count];} readonly record struct V2(double X, double Y){ public static V2 operator +(V2 a, V2 b) => new(a.X + b.X, a.Y + b.Y); public static V2 operator -(V2 a, V2 b) => new(a.X - b.X, a.Y - b.Y); public static V2 operator *(V2 v, double k) => new(v.X * k, v.Y * k); public double Length => Math.Sqrt(X * X + Y * Y); static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###"); public override string ToString() => $"({F(X)}, {F(Y)})";}t = 0: N = [1.000, 0.000, 0.000, 0.000, 0.000], sum 1, point (0.4, 0.6) t = 0.25: N = [0.125, 0.594, 0.250, 0.031, 0.000], sum 1, point (1.775, 2.963) t = 0.5: N = [0.000, 0.250, 0.500, 0.250, 0.000], sum 1, point (3, 2.7) t = 1: N = [0.000, 0.000, 0.000, 0.000, 1.000], sum 1, point (5.8, 1.4)
The R in NURBS: weights and exact circles
const double R = 4.4;var ctrl = new V2[] { new(R, 0), new(R, R), new(0, R) }; foreach (double w in new[] { 1.0, Math.Sqrt(0.5) }){ double worst = 0; for (int s = 0; s <= 100; s++) { double t = s / 100.0; // degree 2 on knots 0,0,0,1,1,1: the basis is (1-t)^2, 2t(1-t), t^2 double b0 = (1 - t) * (1 - t), b1 = 2 * t * (1 - t) * w, b2 = t * t; var pt = (ctrl[0] * b0 + ctrl[1] * b1 + ctrl[2] * b2) * (1 / (b0 + b1 + b2)); worst = Math.Max(worst, Math.Abs(pt.Length - R)); } Console.WriteLine($"middle weight {w:0.0000}: largest distance from the circle {worst * 1000:0.000000} mm");} readonly record struct V2(double X, double Y){ public static V2 operator +(V2 a, V2 b) => new(a.X + b.X, a.Y + b.Y); public static V2 operator -(V2 a, V2 b) => new(a.X - b.X, a.Y - b.Y); public static V2 operator *(V2 v, double k) => new(v.X * k, v.Y * k); public double Length => Math.Sqrt(X * X + Y * Y); static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###"); public override string ToString() => $"({F(X)}, {F(Y)})";}middle weight 1.0000: largest distance from the circle 266.904756 mm middle weight 0.7071: largest distance from the circle 0.000000 mm
Tessellation: exact shapes to triangles
var shapes = new (string Name, double Radius, double AngleDeg)[]{ ("50 mm pipe", 0.025, 360), ("600 mm column", 0.3, 360), ("curved wall r = 12 m, 90 deg", 12, 90)};foreach (double tol in new[] { 0.001, 0.0001 }) foreach (var (name, r, angle) in shapes) { double maxStep = 2 * Math.Acos(1 - Math.Min(tol / r, 1)); // the chord angle that keeps the sagitta <= tol int n = Math.Max(3, (int)Math.Ceiling(angle * Math.PI / 180 / maxStep)); Console.WriteLine($"tolerance {tol * 1000,3} mm: {name,-28} {n,4} segments"); }tolerance 1 mm: 50 mm pipe 12 segments tolerance 1 mm: 600 mm column 39 segments tolerance 1 mm: curved wall r = 12 m, 90 deg 61 segments tolerance 0.1 mm: 50 mm pipe 36 segments tolerance 0.1 mm: 600 mm column 122 segments tolerance 0.1 mm: curved wall r = 12 m, 90 deg 193 segments
Show a hint
Show the solution
double r = 0.3, tol = 0.001;int n = (int)Math.Ceiling(2 * Math.PI / (2 * Math.Acos(1 - tol / r)));double gap = r * (1 - Math.Cos(Math.PI / n));double polygon = 0.5 * n * r * r * Math.Sin(2 * Math.PI / n);double exact = Math.PI * r * r; Console.WriteLine($"{n} segments, largest gap {gap * 1000:0.000} mm");Console.WriteLine($"polygon area {polygon:0.000000} m2, exact {exact:0.000000} m2");Console.WriteLine($"area error {(exact - polygon) / exact * 100:0.000}%");39 segments, largest gap 0.973 mm polygon area 0.281522 m2, exact 0.282743 m2 area error 0.432%
Remember
Kernels store solids as B-reps: shells of faces bounded by loops of edges and vertices, each carrying exact geometry. Euler–Poincaré, V − E + F − (L − F) = 2(S − G), and edge pairing check that a B-rep is valid. B-spline basis functions (Cox–de Boor) give local control and always add up to 1; clamped knots make the curve start and end at the end points. Weights make NURBS rational, so they can represent circles and conics exactly. Tessellation turns exact faces into triangles to a chord tolerance; tighter tolerances cost many more triangles.