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

Lesson 17 of 44 15 minIntermediate

Bézier curves and splines

Bézier curves bend smoothly towards control points without passing through them. Evaluate them with de Casteljau’s algorithm, find their tangents and lengths, and chain them into smooth splines.

Freeform roofs, curved façades, handrails and the spline tools in Revit, Rhino and AutoCAD all use curves defined by control points. The simplest and most widespread is the Bézier curve: the same curve behind fonts and the pen tool in every drawing program.

A cubic Bézier has four control points, P0 to P3. It starts at P0 heading towards P1, and ends at P3 arriving from the direction of P2. It does not pass through P1 and P2: they pull the curve towards them, like magnets.

Interactive

Cubic Bézier curve

Drag the control points and move t. The thin lines are de Casteljau’s interpolation, stage by stage.

P0P1P2P3
weights of P0…P30.275, 0.444, 0.239, 0.043
B(t)(−1.77, 1.82)
tangent B′(t)⟨11.46, 6.67⟩
length, 4 segments14.7196
length, 1024 segments14.9656
chord, polygon10.05, 19.60
Lerp the 4 control points to 3 (purple), those to 2 (orange), and those to 1: the point B(0.35). The weights always add up to 1, so the curve stays inside the control polygon.

De Casteljau: interpolate, and repeat

To find the point at t, interpolate between each pair of neighbouring control points with A + t(B − A), the formula from the first lesson of this module. That gives three points. Do the same to them to get two, and once more to get one: that last point is on the curve. The widget’s purple and orange lines are those stages.

DeCasteljau.cs
var pts = new V2[] { new(0, 0), new(1, 3), new(4, 3), new(5, 0) }; foreach (double t in new[] { 0, 0.25, 0.5, 0.75, 1 })    Console.WriteLine($"t = {t,-4}  {Bezier(pts, t)}"); static V2 Bezier(V2[] control, double t){    var p = (V2[])control.Clone();    for (int n = p.Length - 1; n > 0; n--)          // each pass leaves one point fewer        for (int i = 0; i < n; i++)            p[i] = p[i] + (p[i + 1] - p[i]) * t;    return p[0];} 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 static double Dot(V2 a, V2 b) => a.X * b.X + a.Y * b.Y;    public static double Cross(V2 a, V2 b) => a.X * b.Y - a.Y * b.X;    public double Length => Math.Sqrt(X * X + Y * Y);    public override string ToString() => $"({X:0.###}, {Y:0.###})";}
Output
t = 0     (0, 0)
t = 0.25  (1.063, 1.688)
t = 0.5   (2.5, 2.25)
t = 0.75  (3.938, 1.688)
t = 1     (5, 0)

The same function works for any number of control points: three gives a quadratic Bézier, four a cubic. It is also numerically very stable, because it only ever takes weighted averages of points.

The formula behind it

Multiplying out the interpolations gives the Bernstein form: B(t) = (1 − t)³·P0 + 3(1 − t)²t·P1 + 3(1 − t)t²·P2 + t³·P3. The four weights always add up to 1, so B(t) is a weighted average of points, as in the points-vs-vectors lesson. As a result, the curve always stays inside the polygon of its control points (the convex hull property). That gives a cheap bounding box for clash pre-checks.

Tangent and length

The derivative is another Bézier, built from the differences: B′(t) = 3[(1 − t)²(P1 − P0) + 2(1 − t)t(P2 − P1) + t²(P3 − P2)]. At t = 0 it is 3(P1 − P0), which is why the curve leaves P0 heading straight at P1. It also equals 3 × the last segment of the de Casteljau construction: the widget’s green arrow.

There is no closed formula for a Bézier curve’s length. Sample it at n + 1 points, add up the n chord lengths, and increase n until the answer stops changing. Chords always cut corners, so the estimate only ever grows towards the true length:

BezierLength.cs
var pts = new V2[] { new(0, 0), new(1, 3), new(4, 3), new(5, 0) }; for (int n = 4; n <= 256; n *= 4){    double length = 0;    V2 prev = pts[0];    for (int i = 1; i <= n; i++)    {        V2 p = Bezier(pts, (double)i / n);        length += (p - prev).Length;        prev = p;    }    Console.WriteLine($"{n,3} segments: {length:F4}");} static V2 Bezier(V2[] control, double t){    var p = (V2[])control.Clone();    for (int n = p.Length - 1; n > 0; n--)        for (int i = 0; i < n; i++)            p[i] = p[i] + (p[i + 1] - p[i]) * t;    return p[0];} 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 static double Dot(V2 a, V2 b) => a.X * b.X + a.Y * b.Y;    public static double Cross(V2 a, V2 b) => a.X * b.Y - a.Y * b.X;    public double Length => Math.Sqrt(X * X + Y * Y);    public override string ToString() => $"({X:0.###}, {Y:0.###})";}
Output
  4 segments: 7.0755
 16 segments: 7.1835
 64 segments: 7.1902
256 segments: 7.1906

Splines: chaining curves

One cubic can only make a simple bend, so longer shapes chain several into a spline. Sharing the end point makes the chain continuous, but there is a kink unless the tangents line up: the control points on either side of a joint must be in line with it. If they are also the same distance from it, the curve is smooth in speed as well as direction. Many spline tools never show you Bézier control points at all:

SplinePasses through its points?Typical use
Bézier chainonly at the jointspen tools, fonts, freeform edges
Catmull-Romyes, every onea smooth path through surveyed points, camera paths
B-spline and NURBSno (usually only the ends)Rhino, Revit’s spline, IFC curves; NURBS can also draw exact circles
Hermiteyes, with given tangentsroad and rail transitions, animation

A Catmull-Rom span between the points P(i) and P(i+1) is a cubic Bézier with the control points P(i) + (P(i+1) − P(i−1)) ÷ 6 and P(i+1) − (P(i+2) − P(i)) ÷ 6. With that, the code above draws a smooth handrail through surveyed points. For the first and last spans, repeat the end points.

CatmullRom.cs
var p = new V2[] { new(0, 0), new(2, 2), new(5, 2), new(7, 0) };   // surveyed points // The span from p[1] to p[2], as a cubic BezierV2 c1 = p[1] + (p[2] - p[0]) * (1.0 / 6);V2 c2 = p[2] - (p[3] - p[1]) * (1.0 / 6);var span = new[] { p[1], c1, c2, p[2] }; Console.WriteLine($"Control points {c1} and {c2}");Console.WriteLine($"Halfway: {Bezier(span, 0.5)}"); static V2 Bezier(V2[] control, double t){    var q = (V2[])control.Clone();    for (int n = q.Length - 1; n > 0; n--)        for (int i = 0; i < n; i++)            q[i] = q[i] + (q[i + 1] - q[i]) * t;    return q[0];} 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 static double Dot(V2 a, V2 b) => a.X * b.X + a.Y * b.Y;    public static double Cross(V2 a, V2 b) => a.X * b.Y - a.Y * b.X;    public double Length => Math.Sqrt(X * X + Y * Y);    public override string ToString() => $"({X:0.###}, {Y:0.###})";}
Output
Control points (2.833, 2.333) and (4.167, 2.333)
Halfway: (3.5, 2.25)
Quick check

Which control points does a cubic Bézier curve always pass through?

Quick check

Two cubic Bézier curves share the end point J. When is the joint free of a kink?

Practice

Write Tangent(control, t) for a cubic Bézier using the derivative formula. For the control points (0, 0), (0, 2), (3, 3) and (4, 3), print the curve point and the unit tangent at t = 0, 0.5 and 1.

Show a hint

With s = 1 − t, B′(t) = 3[s²(P1 − P0) + 2st(P2 − P1) + t²(P3 − P2)]. Divide by its length to get the unit tangent. At the ends it should point along the first and last legs of the control polygon.

Show the solution
Solution.cs
var pts = new V2[] { new(0, 0), new(0, 2), new(3, 3), new(4, 3) }; foreach (double t in new[] { 0, 0.5, 1 }){    V2 d = Tangent(pts, t);    Console.WriteLine($"t = {t}: point {Bezier(pts, t)}, tangent {d * (1 / d.Length)}");} static V2 Tangent(V2[] p, double t){    double s = 1 - t;    return ((p[1] - p[0]) * (s * s) + (p[2] - p[1]) * (2 * s * t) + (p[3] - p[2]) * (t * t)) * 3;} static V2 Bezier(V2[] control, double t){    var p = (V2[])control.Clone();    for (int n = p.Length - 1; n > 0; n--)        for (int i = 0; i < n; i++)            p[i] = p[i] + (p[i + 1] - p[i]) * t;    return p[0];} 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 static double Dot(V2 a, V2 b) => a.X * b.X + a.Y * b.Y;    public static double Cross(V2 a, V2 b) => a.X * b.Y - a.Y * b.X;    public double Length => Math.Sqrt(X * X + Y * Y);    public override string ToString() => $"({X:0.###}, {Y:0.###})";}
Output
t = 0: point (0, 0), tangent (0, 1)
t = 0.5: point (1.625, 2.25), tangent (0.868, 0.496)
t = 1: point (4, 3), tangent (1, 0)

Remember

  • A cubic Bézier starts at P0 towards P1 and ends at P3 from P2; it passes through neither P1 nor P2.
  • De Casteljau’s algorithm evaluates it by repeated interpolation: 4 points, then 3, 2 and 1.
  • The Bernstein weights add up to 1, so the curve stays inside its control polygon.
  • The tangent is B′(t); the length has no formula, so sum chords and increase the count until it settles.
  • Splines chain curves; line up the control points at each joint to avoid kinks. Catmull-Rom passes through every point.
0 of 44 lessons completed