Geometry Library in C# — NURBS Curves
Saturday, April 4, 2026In the previous articles, we defined the curve interfaces and implemented simple curves (lines, segments). But in CAD, most curves are neither lines nor circular arcs — they're free-form curves. The standard for representing them is the NURBS curve.
What is a NURBS curve?
NURBS stands for Non-Uniform Rational B-Spline. Behind this acronym lie four concepts:
- B-Spline: the curve is defined by control points that "attract" the curve without it necessarily passing through them (unlike interpolation).
- Rational: each control point carries a weight that controls its influence. This allows exact representation of conics (circles, ellipses, hyperbolas).
- Non-Uniform: the knots (the parameter values \(t\) where the curve transitions between polynomial segments) aren't necessarily evenly spaced.
The ingredients
A NURBS curve is defined by:
| Element | Description |
|---|---|
| Control points | The positions that "sculpt" the curve. \(n + 1\) points for a curve of order \(p + 1\). |
| Weights | One scalar per control point. All equal to 1 → non-rational B-spline. |
| Knot vector | An increasing sequence of real values that defines the parametric domain. |
| Degree \(p\) | The polynomial degree of each curve segment (typically 3 for a cubic). |
The number of knots is always equal to the number of control points plus the degree plus 1: \(m = n + p + 1\).
Evaluation: the De Boor algorithm
To evaluate \(C(t)\) — i.e. find the point on the curve at a given parameter \(t\) — we use the De Boor algorithm. It's the B-spline equivalent of the De Casteljau algorithm for Bézier curves: a recursive subdivision scheme that is numerically stable.
Without going into the full algorithm details, here's the principle: we identify the knot interval \([t_i, t_{i+1})\) containing \(t\), then perform \(p\) levels of affine combinations on the neighboring control points. The result converges to the point on the curve.
NurbsCurve2d: the implementation
A NURBS curve fits naturally into IBoundedCurve2d:
public sealed class NurbsCurve2d : IBoundedCurve2d
{
readonly Point2d[] _controlPoints;
readonly double[] _weights;
readonly double[] _knots;
public int Degree { get; }
public double StartParameter => _knots[Degree];
public double EndParameter => _knots[_knots.Length - Degree - 1];
public Point2d StartPoint => GetPointAtParameter(StartParameter);
public Point2d EndPoint => GetPointAtParameter(EndParameter);
public double Length { get; }
public bool IsClosed => StartPoint.IsAlmostEqualTo(EndPoint);
public NurbsCurve2d(Point2d[] controlPoints, double[] weights, double[] knots, int degree)
{
ArgumentNullException.ThrowIfNull(controlPoints);
ArgumentNullException.ThrowIfNull(weights);
ArgumentNullException.ThrowIfNull(knots);
if (controlPoints.Length < 2)
throw new ArgumentException("The curve must have at least 2 control points.", nameof(controlPoints));
if (weights.Length != controlPoints.Length)
throw new ArgumentException("The number of weights must match the number of control points.", nameof(weights));
if (knots.Length != controlPoints.Length + degree + 1)
throw new ArgumentException("The number of knots must equal control points + degree + 1.", nameof(knots));
if (degree < 1)
throw new ArgumentException("The degree must be at least 1.", nameof(degree));
_controlPoints = controlPoints;
_weights = weights;
_knots = knots;
Degree = degree;
Length = ComputeLength();
}
public Point2d GetPointAtParameter(double parameter)
{
// De Boor algorithm to evaluate the curve at parameter t
// ...
}
// GetParameterAtPoint, GetTangentAtParameter, GetClosestPointTo,
// GetDistanceTo, GetSubCurve, CreateReversed...
}
A few observations:
- The parametric domain is defined by the internal knots: it goes from
_knots[Degree]to_knots[n - Degree], where \(n\) is the last index of the knot vector. - The start and end points are evaluated (not stored directly), because they depend on the knot vector and weights. In practice, with a "clamped" knot vector (the first and last values are repeated \(p + 1\) times), the curve passes exactly through the first and last control points.
- Length has no simple analytical formula for a NURBS. It's computed by numerical integration (Gaussian quadrature or adaptive subdivision).
Trimming
The GetSubCurve operation is fundamental for NURBS in CAD. This is trimming: keeping a piece of the curve between two parameters.
The standard algorithm is knot insertion: we insert knots at the trim parameters (using the Boehm algorithm or Oslo insertion), then extract the corresponding control points. The result is a new NURBS curve that represents exactly the desired portion, with no approximation.
public IBoundedCurve2d GetSubCurve(double startParameter, double endParameter)
{
// 1. Insert knots at startParameter and endParameter
// 2. Extract the corresponding control points, weights, and knots
// 3. Return a new NurbsCurve2d
}
Why are NURBS so important in CAD?
NURBS have a unique property: they can represent exactly all conic curves (circles, ellipses, parabolas, hyperbolas) thanks to weights. A circle, for example, is a rational NURBS of degree 2. This means a single representation — NURBS — unifies all the curves we've seen so far.
In practice, most CAD geometric kernels (ACIS, Parasolid, OpenCascade) use NURBS as the internal representation for all curves. Segments, arcs, and circles are special cases that benefit from optimized algorithms, but can always be converted to NURBS when a unified representation is needed.
What's next
NURBS can represent any individual curve. But how do we join multiple curves end to end to form a contour? That's the role of composite curves, which we'll cover in the next article.