When a racing game swings its camera around a hairpin bend, when a platformer's fireball traces an arc, or when a UI panel eases into place with a bounce, Bézier curves are almost certainly doing the work. Named after French engineer Pierre Bézier who popularized them at Renault in the 1960s (though mathematician Paul de Casteljau independently discovered them earlier at Citroën), Bézier curves are among the most useful tools in any game developer's toolkit.
Unlike rigid geometric primitives, Bézier curves offer intuitive, artist-friendly control over smooth paths. Place a handful of control points and the math does the rest, producing curves that feel natural without forcing you to solve systems of equations. Understanding them deeply unlocks an entire category of game mechanics: smooth camera rails, procedural animation, spline-based road networks, projectile trajectories, easing functions, and more.
The Foundation: Linear Interpolation
Every Bézier curve, regardless of degree, reduces to repeated applications of one simple operation: linear interpolation, or LERP. Given two points $\mathbf{P}_0$ and $\mathbf{P}_1$ and a scalar parameter $t \in [0, 1]$, the interpolated point is:
$$\mathbf{B}(t) = (1 - t)\,\mathbf{P}_0 + t\,\mathbf{P}_1$$
At $t = 0$ you get $\mathbf{P}_0$. At $t = 1$ you get $\mathbf{P}_1$. At $t = 0.5$ you are exactly halfway between them. This is itself a first-degree (linear) Bézier curve. Everything that follows is just this same operation applied recursively to its own intermediate results.
function lerp(a, b, t) {
return {
x: a.x + (b.x - a.x) * t,
y: a.y + (b.y - a.y) * t
};
}Quadratic Bézier Curves
Add a third control point and apply LERP twice, and you get a quadratic (second-degree) Bézier curve. Given $\mathbf{P}_0$, $\mathbf{P}_1$, and $\mathbf{P}_2$, first interpolate between adjacent control points to get two intermediate points:
$$\mathbf{Q}_0(t) = (1-t)\,\mathbf{P}_0 + t\,\mathbf{P}_1 \qquad \mathbf{Q}_1(t) = (1-t)\,\mathbf{P}_1 + t\,\mathbf{P}_2$$
Then interpolate between those two results:
$$\mathbf{B}(t) = (1-t)\,\mathbf{Q}_0 + t\,\mathbf{Q}_1$$
Expanding and collecting terms yields the closed-form quadratic Bézier:
$$\mathbf{B}(t) = (1-t)^2\,\mathbf{P}_0 + 2(1-t)t\,\mathbf{P}_1 + t^2\,\mathbf{P}_2$$
The curve starts at $\mathbf{P}_0$, ends at $\mathbf{P}_2$, and is pulled toward the off-curve handle $\mathbf{P}_1$ without ever touching it. The curve leaves $\mathbf{P}_0$ tangent to the line $\mathbf{P}_0 \to \mathbf{P}_1$ and arrives at $\mathbf{P}_2$ tangent to $\mathbf{P}_1 \to \mathbf{P}_2$. This automatic tangent-continuity is what gives Bézier curves their characteristic smoothness.
Cubic Bézier Curves
The cubic (third-degree) Bézier curve with four control points is the workhorse of game development. SVG paths, CSS easing functions, TrueType font outlines, Unity's Animation Curves, and most professional spline editors all use cubic Bézier segments. With $\mathbf{P}_0$, $\mathbf{P}_1$, $\mathbf{P}_2$, $\mathbf{P}_3$:
$$\mathbf{B}(t) = (1-t)^3\,\mathbf{P}_0 + 3(1-t)^2 t\,\mathbf{P}_1 + 3(1-t)t^2\,\mathbf{P}_2 + t^3\,\mathbf{P}_3$$
The four coefficients are the cubic Bernstein basis polynomials. They always sum to exactly 1 for any value of $t$, which enforces the convex hull property: no matter how you position the handles, the curve never strays outside the convex hull of its four control points. This makes Bézier curves safe and predictable for artists.
function cubicBezier(p0, p1, p2, p3, t) {
const mt = 1 - t;
const mt2 = mt * mt, mt3 = mt2 * mt;
const t2 = t * t, t3 = t2 * t;
return {
x: mt3*p0.x + 3*mt2*t*p1.x + 3*mt*t2*p2.x + t3*p3.x,
y: mt3*p0.y + 3*mt2*t*p1.y + 3*mt*t2*p2.y + t3*p3.y
};
}De Casteljau's Algorithm
The recursive construction behind Bézier curves is called De Casteljau's algorithm. Rather than evaluating the polynomial directly, it applies LERP repeatedly to adjacent pairs of points until one point remains. The interactive demo above visualizes this live: the orange lines are level-1 interpolations, the red lines are level-2, and the white dot is the final curve point.
For a cubic Bézier at parameter $t$:
- Level 1 — LERP adjacent control points: $\mathbf{Q}_0 = \text{lerp}(\mathbf{P}_0, \mathbf{P}_1, t)$, $\;\mathbf{Q}_1 = \text{lerp}(\mathbf{P}_1, \mathbf{P}_2, t)$, $\;\mathbf{Q}_2 = \text{lerp}(\mathbf{P}_2, \mathbf{P}_3, t)$
- Level 2 — LERP the level-1 results: $\mathbf{R}_0 = \text{lerp}(\mathbf{Q}_0, \mathbf{Q}_1, t)$, $\;\mathbf{R}_1 = \text{lerp}(\mathbf{Q}_1, \mathbf{Q}_2, t)$
- Level 3 — Final LERP gives the curve point: $\mathbf{B}(t) = \text{lerp}(\mathbf{R}_0, \mathbf{R}_1, t)$
// General De Casteljau — works for any degree
function decasteljau(points, t) {
let P = points.slice();
while (P.length > 1) {
const next = [];
for (let i = 0; i < P.length - 1; i++) {
next.push(lerp(P[i], P[i + 1], t));
}
P = next;
}
return P[0];
}De Casteljau's algorithm has a useful side property: at split parameter $t$, the intermediate points $[\mathbf{P}_0, \mathbf{Q}_0, \mathbf{R}_0, \mathbf{B}(t)]$ and $[\mathbf{B}(t), \mathbf{R}_1, \mathbf{Q}_2, \mathbf{P}_3]$ are the exact control points for the two sub-curves on either side of the split. This makes it ideal for curve subdivision, splitting a Bézier cleanly into two identical-looking halves at any $t$, a technique used in font rasterization and adaptive tessellation.
The Bernstein Polynomial Form
Expanding the De Casteljau recursion symbolically for a degree-$n$ curve produces the Bernstein polynomial form:
$$\mathbf{B}(t) = \sum_{i=0}^{n} \binom{n}{i}(1-t)^{n-i}\,t^i\,\mathbf{P}_i$$
where $\binom{n}{i} = \frac{n!}{i!(n-i)!}$ is the binomial coefficient. The basis functions $B_{i,n}(t) = \binom{n}{i}(1-t)^{n-i}t^i$ are always non-negative on $[0,1]$ and sum to 1. These properties guarantee the convex hull bound and make Bézier curves numerically stable. The endpoint-interpolation property follows directly: $\mathbf{B}(0) = \mathbf{P}_0$ and $\mathbf{B}(1) = \mathbf{P}_n$ always hold, because at the endpoints all basis functions except one collapse to zero.
Tangents and the Derivative
Beyond position, you often need the direction of travel along the curve, for aligning a spaceship to its flight path, orienting a camera look-at, or computing normals along a road. The tangent vector is the derivative $\mathbf{B}'(t)$:
$$\mathbf{B}'(t) = 3(1-t)^2(\mathbf{P}_1 - \mathbf{P}_0) + 6(1-t)t(\mathbf{P}_2 - \mathbf{P}_1) + 3t^2(\mathbf{P}_3 - \mathbf{P}_2)$$
This is itself a quadratic Bézier on the differences of the original control points. Its magnitude is not constant; it depends on the spacing of the handles, which connects directly to the arc-length problem discussed next.
function cubicBezierTangent(p0, p1, p2, p3, t) {
const mt = 1 - t;
return {
x: 3*(mt*mt*(p1.x-p0.x) + 2*mt*t*(p2.x-p1.x) + t*t*(p3.x-p2.x)),
y: 3*(mt*mt*(p1.y-p0.y) + 2*mt*t*(p2.y-p1.y) + t*t*(p3.y-p2.y))
};
}Arc-Length Parameterization
The most important gotcha with Bézier curves in games: $t$ does not correspond to equal distances along the curve. Incrementing $t$ by 0.01 near a gentle section advances the point much further than the same step near a tight bend. Move an object by advancing $t$ at a constant rate and it will visibly speed up and slow down.
For constant-speed motion you need arc-length parameterization: a mapping from normalized arc-length $u \in [0,1]$ to the corresponding $t$ value. The standard approach is a precomputed lookup table:
function buildArcTable(curveFn, samples = 200) {
const table = [{ t: 0, d: 0 }];
let total = 0, prev = curveFn(0);
for (let i = 1; i <= samples; i++) {
const t = i / samples;
const pt = curveFn(t);
total += Math.hypot(pt.x - prev.x, pt.y - prev.y);
table.push({ t, d: total });
prev = pt;
}
return table;
}
function tFromArcLength(table, u) {
const target = u * table[table.length - 1].d;
for (let i = 1; i < table.length; i++) {
if (table[i].d >= target) {
const alpha = (target - table[i-1].d) / (table[i].d - table[i-1].d);
return table[i-1].t + alpha * (table[i].t - table[i-1].t);
}
}
return 1;
}A 100–200-sample table gives sub-pixel accuracy for typical game paths and runs in microseconds. For high-precision applications like font rendering, you can refine the initial estimate with one or two Newton-Raphson iterations.
Bézier Curves in Real Games
Bézier curves appear throughout shipped titles in many forms:
- Camera rails — Cinematic systems in games like God of War and The Last of Us define camera paths as chains of Bézier segments. Directors position handles to frame shots, and the engine follows the path at arc-length-parameterized speed.
- Projectile arcs — Rather than fully simulating physics for lobbed grenades or mortar shells, many games bake the trajectory as a cubic Bézier evaluated cheaply each frame, giving designers direct artistic control over the arc shape.
- UI easing — CSS's
cubic-bezier()— used in Unity's iTween, Godot's Tween, and countless web games — is literally a cubic Bézier sampled for its Y-value at a given X-value, producing ease-in, ease-out, and overshoot motion profiles. - Road and rail networks — Open-world games like GTA V and Forza Horizon represent road center-lines as Catmull-Rom splines (a close cubic cousin). Artists place waypoints and the engine auto-generates smooth road meshes and AI navigation data.
- Font rendering — TrueType glyphs use quadratic Béziers; OpenType/PostScript use cubics. Any game rendering custom logos or UI text in-engine is drawing Bézier curves per frame.
- Particle systems — Color-over-lifetime gradients, size curves, and velocity profiles in Unity's Shuriken and Unreal's Niagara are all Bézier splines artists edit in curve editors.
Chaining Segments into Splines
A single cubic Bézier gives one smooth segment. For complex paths you chain segments into a spline. The challenge is ensuring smoothness at joins. Three continuity levels are defined:
- C0 (positional) — The endpoint of segment $k$ equals the start of segment $k+1$. No gap, but there may be a visible corner.
- C1 (tangent) — Tangent vectors match in direction and magnitude at the join. No corner. The outgoing handle of segment $k$ must be the exact mirror of the incoming handle of segment $k+1$ through the shared endpoint.
- C2 (curvature) — Both first and second derivatives match. No sudden change in curvature. Natural cubic splines achieve C2 everywhere but require solving a global system; Bézier splines need extra constraints.
For game use, C1 is almost always sufficient. Unity's AnimationCurve, Unreal's SplineComponent, and Godot's Path3D all enforce C1 by default, mirroring the opposite handle automatically when you drag one.
Practical Implementation
Here is a production-ready cubic Bézier class combining everything above:
class CubicBezier {
constructor(p0, p1, p2, p3) {
this.P = [p0, p1, p2, p3];
this._table = null;
}
evaluate(t) {
const [p0, p1, p2, p3] = this.P;
const mt = 1 - t, mt2 = mt*mt, mt3 = mt2*mt, t2 = t*t, t3 = t2*t;
return {
x: mt3*p0.x + 3*mt2*t*p1.x + 3*mt*t2*p2.x + t3*p3.x,
y: mt3*p0.y + 3*mt2*t*p1.y + 3*mt*t2*p2.y + t3*p3.y
};
}
tangent(t) {
const [p0, p1, p2, p3] = this.P;
const mt = 1 - t;
return {
x: 3*(mt*mt*(p1.x-p0.x) + 2*mt*t*(p2.x-p1.x) + t*t*(p3.x-p2.x)),
y: 3*(mt*mt*(p1.y-p0.y) + 2*mt*t*(p2.y-p1.y) + t*t*(p3.y-p2.y))
};
}
_buildTable(n = 200) {
this._table = [{ t: 0, d: 0 }];
let total = 0, prev = this.evaluate(0);
for (let i = 1; i <= n; i++) {
const t = i / n, pt = this.evaluate(t);
total += Math.hypot(pt.x - prev.x, pt.y - prev.y);
this._table.push({ t, d: total });
prev = pt;
}
}
evaluateArc(u) {
if (!this._table) this._buildTable();
const tbl = this._table;
const target = u * tbl[tbl.length - 1].d;
for (let i = 1; i < tbl.length; i++) {
if (tbl[i].d >= target) {
const alpha = (target - tbl[i-1].d) / (tbl[i].d - tbl[i-1].d);
return this.evaluate(tbl[i-1].t + alpha * (tbl[i].t - tbl[i-1].t));
}
}
return this.evaluate(1);
}
}With evaluate(t) for parameter-based positions, tangent(t) for orientation, and evaluateArc(u) for constant-speed motion, this class covers most game use cases, from projectile arcs to camera rails to procedural geometry generation.
Comments