An degree \(n\) Bézier curve is given by the following formula \begin{equation} C(t) = \sum_{i=0}^n a_i B_i^n(t). \end{equation} The coefficients \(a_i\) are called control points are \(n\)-dimensional vectors to produce an \(n\)-dimensional curve. The function \(B_i^n(t) = \binom{n}{i}t^i(1-t)^{n-i}\) is known as the \(i\)th Bernstein basis function of degree \(n\) (with \(\binom{n}{i} = \frac{n!}{i!(n-i)!})\). As with most polynomials, you can evaluate \(C(t)\) at a given value of \(t\) by evaluating the basis functions at \(t\) and using the above formula.
But Bézier curves have a cool property: you can compute \(C(t)\) directly from the control points without evaluating the basis functions. The algorithm to achieve this is called de Casteljau’s algorithm and can be expressed compactly in the following recursive formula: \begin{equation} a_i^r = (1-t)a^{r-1}_i + ta_{i+1}^{r-1},\quad r = 1,\dots, n, \quad i=0,\dots, n-r \end{equation} with \(a_i^0 = a_i\). Evaluating \(a_0^n\) for a given \(t\) will produce the value \(C(t)\), i.e., \(a_0^n = C(t)\). Each iteration of \(r\) performs a linear interpolation between the control points (or the intermediate control points from the previous recursive evaluation). In pseudocode form, we can write this recursively as:
def de_casteljau(r, a):
if r == 0:
return a[i]
else:
# a^{r-1} and a^r are arrays of coefficients
a^{r-1} = de_casteljau(r-1, a)
for i in range(n):
a^r[i] = (1-t) * a^{r-1}[i] + t * a^{r-1}[i+1]
return a^r
Why are we talking about flowers?
This algorithm becomes much more interesting when we ask: what happens if we vary \(t\) with each recursive step? This means that we now have a vector \(\mathbf{t} = (t_1, t_2, \dots, t_n)\) and our equation becomes: \begin{equation} a_i^r = (1-t_r)a^{r-1}_i + t_ra_{i+1}^{r-1},\quad r = 1,\dots, n, \quad i=0,\dots, n-r \end{equation} again with \(a_i^0 = a_i\). This is what we call the blossom of the control points \(a_i\) above over the values \(\mathbf{t}\), which we’ll write as \(a_i^r[\mathbf{t}]\) or \(a_i^r[t_1,\dots, t_n]\) for the intermediate blossom levels.
To evaluate the full blossom, we write \(a_0^n[\mathbf{t}]\), as with de Casteljau. The pseudocode looks very similar to de Casteljau:
def blossom(r, a):
if r == 0:
return a[i]
else:
a^{r-1} = blossom(r-1, a)
for i in range(r):
a^r[i] = (1-t) * a^{r-1}[i] + t * a^{r-1}[i+1]
return a^r
When \(t_i = t\) for each \(i\), we recover de Casteljau’s algorithm, but we’re now free to vary \(t\) throughout the algorithm.
Nice properties of blossoms
This might seem like a trivial generalization, but blossoms have some interesting uses:
- Blossoms are symmetric: \(a_0^n[\mathbf{t}] = a_0^n[\pi(\mathbf{t})]\), where \(\pi\) is a permutation (i.e., reordering) of the entries of \(\mathbf{t}\).
- Blossoms are multiaffine: \(a_0^n[bt_1 + ct_2, \dots] = ba_0^n[t_1, \dots] + ca_0^n[t_2,\dots]\)
- We can express the \(i\)th control point as \(a_i^0 = a_0^n[\mathbf{\tau}_i]\) with the vector \(\tau_i = (0, 0,\dots, 1,1)\) containing \(n-i\) zeros and \(i\) ones.
- Similarly, we can write down the Bézier form of a subcurve on the domain \([c,d]\) with in terms of blossoms. We can compute the \(i\)th control point of the subcurve by evaluating \(a_0^n[\mathbf{\eta}_i]\) with \(\eta_i= (c,c,\dots, d,d)\) being a vector of \(n-i\) copies of \(c\) and \(i\) copies of \(d\).
- We can differentiate Bézier curves trivially with blossoms, using the following expression \begin{equation} \frac{dC}{dt} = n a^n_0[t,\dots, t,1], \end{equation} evaluting the blossom with \(n-1\) copies of \(t\) followed by a single 1.
- We can elevate the degree of a Bézier curve by summing over various blossoms: \begin{equation} a_0^{n+1}[t_1, \dots, t_{n+1}] = \frac{1}{n+1}\sum_{i=0}^{n+1} a_0^n[t_1,\dots,t_{n+1}\mid t_i], \end{equation} where the notation \(t_1,\dots,t_{n+1}\mid t_i\) means that the entry \(t_i\) is omitted from the sequence. This isn’t the most efficient way to evaluate an elevated degree Bézier curve, but it does lead to a compact formula for the elevated curve’s control points \(\tilde{a}_i^0\): \begin{equation} \tilde{a}_i^0= a_0^{n+1}[0,0, \dots, 1,1] \end{equation} with \(n+1-i\) zeroes and \(i\) ones as arguments in the blossom.
Every polynomial in one variable has a unique form as a blossom, but blossoms are mostly used in the context of Bézier curves. For some intuition to see why this might be true, lets look at the quadratic case. We know that \(a_0^n[t,t] = C(t)\), and since \(C(t)\) is a polynomial, we can write it as: \begin{equation} C(t) = c_0 + c_1 t +c_2 t^2 \end{equation} for some coefficients \(c_i\). If we define \(t_1 = t_2 = t\), we might be able to convince ourselves that \begin{equation} a_0^n[t_1,t_2] = c_0^\prime + c_1^\prime t_1 + c_2^\prime t_2 + c_3^\prime t_1t_2 \end{equation} for some other coefficients \(c^\prime_i\). By equating these two equations, we can see that \(c_0 = c_0^\prime, c_1 = \frac{c_1^\prime + c_2^\prime}{2},\) and \(c_3 = c_3^\prime\). A cubic example is worked out here on page 4.
de Castejau implementation
Here’s a simple C++ implementation of de Casteljau’s algorithm, using Eigen. It’s fairly simple to implement without many surprises. The full implementation can be found in this commit of nanospline.
// _dim and _degree are dimension and degree of the Bezier curve.
using Scalar = double;
using ControlPoints = Eigen::Matrix<Scalar, _dim, _degree+1>
...
ControlPoints de_casteljau(Scalar t, int num_recursions) const {
const auto degree = Base::get_degree();
if (num_recursions < 0 || num_recursions > degree) {
throw invalid_setting_error( "Number of de Casteljau recursion
cannot exceeds degree");
}
if (num_recursions == 0) {
// get original control points at the bottom of the recursion
return Base::m_control_points;
} else {
ControlPoints ctrl_pts = de_casteljau(t, num_recursions-1);
assert(ctrl_pts.rows() >= degree+1-num_recursions);
for (int i=0; i<degree+1-num_recursions; i++) {
// ctrl_pts.row(i) gets the i-th control point.
ctrl_pts.row(i) = (1.0-t) * ctrl_pts.row(i) +
t * ctrl_pts.row(i+1);
}
return ctrl_pts;
}
}
Blossom implementation
For our blossom implementation, we can essentially just add a vector input instead of a single value of \(t\).
For a slightly cleaner implementation, we use two for loops instead of an explicit recursion.
The full implementation is available in the Bezier<...>::evaluate
function in nanospline.
using Scalar = double;
using ControlPoints = Eigen::Matrix<Scalar, _dim, _degree+1>
using BlossomVector = Eigen::Matrix<Scalar, _degree, 1>
...
void blossom(const BlossomVector& blossom_vector, int degree,
ControlPoints& control_pts) const {
for (int r = 1; r <= degree; r++) {
for (int j = degree; j >= r; j--) {
Scalar t = blossom_vector(r- 1);
control_pts.row(j) =(1. - t) * control_pts.row(j - 1) +
t * control_pts.row(j);
}
}
}
For more information
Blossoms are explained in the most detail in Gerald Farin’s Curves and Surfaces for Computer-Aided Graphics and Design. For more Bézier algorithms and useful visualizations, the Primer on Bézier curves is a great resource. These lecture notes provide a bit more detail about Bézier and B-Spline blossoms.