CMPUT-411 · Lesson 10 of 12 · 40 min

Curves, surfaces and animation basics

Triangle meshes are the currency of rendering, but they are not how designers think about smooth shapes or how animators think about motion. The body of a car, the outline of a letter in a font and the path of a camera through a scene are described by smooth parametric curves and surfaces controlled by a handful of points. Motion is described by keyframes interpolated over time, by skeletons that deform meshes, and by physical simulation. This lesson introduces Bézier curves and the de Casteljau algorithm, splines and continuity, the surfaces built from them, and the foundations of computer animation: keyframing and easing, rotations in time, skeletal skinning, inverse kinematics and particle simulation.

Parametric curves

A parametric curve is a function \(p(t)\) from a parameter t, usually in \([0, 1]\), to points in the plane or in space. Unlike an implicit curve \(f(x, y) = 0\), it is easy to draw: evaluate it at a sequence of t values and connect the points. Its derivative \(p'(t)\) is the tangent vector, whose direction gives the curve's direction and whose length gives the speed at which the parameter traces it. Polynomial curves are favoured because they are cheap to evaluate and differentiate, and cubics, of degree three, are the lowest degree that can have an inflection point and match a position and tangent at both ends.

Bézier curves and de Casteljau's algorithm

Bézier curves were developed independently in the early 1960s by Paul de Casteljau at Citroën and Pierre Bézier at Renault, both for designing car bodies. A Bézier curve of degree n is defined by n + 1 control points \(P_0, \dots, P_n\). De Casteljau's algorithm evaluates it by repeated linear interpolation: interpolate each consecutive pair of control points at parameter t, giving n points; interpolate those, giving n − 1; and continue until one point remains, which lies on the curve. The same curve can be written with the Bernstein polynomials \(B_{i,n}(t) = \binom{n}{i}(1-t)^{n-i}t^i\) as weights on the control points.

\[p(t) = \sum_{i=0}^{n} \binom{n}{i} (1-t)^{n-i}\, t^{i}\, P_i\]
A cubic Bezier curve arching from P0 at lower left to P3 at lower right, with control points P1 and P2 above, connected by a thin grey control polygon. At t equals 0.5, dashed lines show the first round of interpolated points on each polygon edge, a second round between those, and a final point on the curve, marked with a dot.
De Casteljau's construction of a cubic Bézier curve at t = 0.5: three rounds of linear interpolation reach a point on the curve

Bézier curves have properties that make them easy to design with. They pass through the first and last control points and are tangent there to the first and last legs of the control polygon; the intermediate control points act as handles that pull the curve without lying on it. The curve lies within the convex hull of its control points, which makes bounding boxes and intersection tests cheap. It is affine invariant: transforming the control points transforms the curve, so there is no need to transform sampled points. And de Casteljau's algorithm subdivides the curve: the intermediate points computed at t = 0.5 are exactly the control points of the two halves, so a renderer can split a curve recursively until each piece is flat enough to draw as a line.

Evaluating and subdividing a cubic Bézier curve with de Casteljau's algorithm
# De Casteljau's algorithm for a cubic Bezier curve, compared with the
# Bernstein-polynomial form, plus subdivision of the curve at t = 0.5.
import numpy as np
P = np.array([[0.0, 0.0], [1.0, 2.0], [3.0, 2.0], [4.0, 0.0]])
def casteljau(P, t):
    pts = [P]
    while len(pts[-1]) > 1:
        Q = pts[-1]
        pts.append((1 - t) * Q[:-1] + t * Q[1:])   # repeated linear interpolation
    return pts
def bernstein(P, t):
    n = len(P) - 1
    from math import comb
    return sum(comb(n, i) * (1 - t) ** (n - i) * t ** i * P[i] for i in range(n + 1))
for t in (0.0, 0.25, 0.5, 0.75, 1.0):
    a = casteljau(P, t)[-1][0]; b = bernstein(P, t)
    print(f"t={t:4.2f}  de Casteljau {a.round(4)}  Bernstein {b.round(4)}")
levels = casteljau(P, 0.5)
left = np.array([lv[0] for lv in levels]); right = np.array([lv[-1] for lv in levels][::-1])
print("left half control points: ", left.round(3).tolist())
print("right half control points:", right.round(3).tolist())
d0 = 3 * (P[1] - P[0]); d1 = 3 * (P[3] - P[2])
print("tangent at start", d0, " tangent at end", d1)
Output
t=0.00  de Casteljau [0. 0.]  Bernstein [0. 0.]
t=0.25  de Casteljau [0.9062 1.125 ]  Bernstein [0.9062 1.125 ]
t=0.50  de Casteljau [2.  1.5]  Bernstein [2.  1.5]
t=0.75  de Casteljau [3.0938 1.125 ]  Bernstein [3.0938 1.125 ]
t=1.00  de Casteljau [4. 0.]  Bernstein [4. 0.]
left half control points:  [[0.0, 0.0], [0.5, 1.0], [1.25, 1.5], [2.0, 1.5]]
right half control points: [[2.0, 1.5], [2.75, 1.5], [3.5, 1.0], [4.0, 0.0]]
tangent at start [3. 6.]  tangent at end [ 3. -6.]

De Casteljau's algorithm and the Bernstein form agree to every printed digit. The left half's control points run from \(P_0\) to the midpoint (2, 1.5), and the right half's from there to \(P_3\); both halves share the midpoint and the tangent there, so the subdivided curve is identical to the original. The end tangents are three times the first and last legs of the control polygon, so moving \(P_1\) changes the starting direction and speed. Bézier curves are everywhere: quadratic Béziers define TrueType font outlines, cubic Béziers define PostScript, PDF, OpenType CFF fonts and SVG paths, and the pen tool in every vector drawing program edits cubic Bézier handles.

Splines and continuity

One Bézier curve of high degree is hard to control: moving any control point changes the whole curve, and high-degree polynomials oscillate. Instead, long curves are built from piecewise cubic segments joined end to end, called splines (after the flexible strips draftsmen bent around weights). How smoothly the pieces join is measured by continuity. \(C^0\) continuity means the segments meet; \(C^1\) means their first derivatives match at the join, so a moving point has no jump in velocity; \(C^2\) means second derivatives match, so there is no jump in acceleration or curvature, which matters for car bodies, where reflections of lights reveal curvature breaks. Geometric continuity \(G^1\) relaxes \(C^1\): the tangent directions must match but their lengths may differ, which looks smooth but changes the parameter speed.

Continuity at a joint between Bézier segments, and the C² continuity of a uniform cubic B-spline
# Joining two cubic Bezier segments: checking C0, C1 and G1 continuity at the
# shared point, then evaluating a uniform cubic B-spline, which is C2 by construction.
import numpy as np
A = np.array([[0, 0], [1, 2], [2, 2], [3, 1]], float)      # first segment
B_c1 = np.array([[3, 1], [4, 0], [5, 1], [6, 2]], float)   # second segment, mirrored handle
B_g1 = np.array([[3, 1], [5, -1], [6, 1], [7, 2]], float)  # same direction, longer handle
def check(A, B):
    c0 = np.allclose(A[3], B[0])
    ta, tb = 3 * (A[3] - A[2]), 3 * (B[1] - B[0])
    c1 = c0 and np.allclose(ta, tb)
    cosang = ta @ tb / (np.linalg.norm(ta) * np.linalg.norm(tb))
    g1 = c0 and abs(cosang - 1) < 1e-9
    return c0, c1, g1, ta, tb
for name, B in (("mirrored handle", B_c1), ("longer handle", B_g1)):
    c0, c1, g1, ta, tb = check(A, B)
    print(f"{name:16} C0={c0} C1={c1} G1={g1}  end tangent {ta} start tangent {tb}")
# uniform cubic B-spline segment basis (Catmull and Clark's surfaces generalize this)
M = np.array([[-1, 3, -3, 1], [3, -6, 3, 0], [-3, 0, 3, 0], [1, 4, 1, 0]]) / 6
ctrl = np.array([[0, 0], [1, 2], [2, 2], [3, 1], [4, 0], [5, 1]], float)
def seg(i, t):
    T = np.array([t ** 3, t ** 2, t, 1]); return T @ M @ ctrl[i:i + 4]
def d2(i, t):
    T = np.array([6 * t, 2, 0, 0]); return T @ M @ ctrl[i:i + 4]
print("B-spline joint between segments 0 and 1:")
print("  position", seg(0, 1.0).round(4), seg(1, 0.0).round(4))
print("  second derivative", d2(0, 1.0).round(4), d2(1, 0.0).round(4))
print("  passes through control point ctrl[2]?", np.allclose(seg(0, 1.0), ctrl[2]))
Output
mirrored handle  C0=True C1=True G1=True  end tangent [ 3. -3.] start tangent [ 3. -3.]
longer handle    C0=True C1=False G1=True  end tangent [ 3. -3.] start tangent [ 6. -6.]
B-spline joint between segments 0 and 1:
  position [2.     1.8333] [2.     1.8333]
  second derivative [ 0. -1.] [ 0. -1.]
  passes through control point ctrl[2]? False

With the second segment's first handle mirrored across the joint, the tangents match exactly, so the join is \(C^1\). With a longer handle in the same direction, the join is only \(G^1\): visually smooth, but an object animated along the curve would jump in speed. The B-spline joint matches both position and second derivative, so it is \(C^2\) automatically, but it does not pass through its control points; ctrl[2] is not on the curve. That is the general trade-off between interpolating splines, which pass through their control points (Catmull–Rom splines, used for camera paths), and approximating splines, which are smoother and give more local control (B-splines).

B-splines (basis splines), developed by Isaac Schoenberg in 1946 and made practical by Carl de Boor and Maurice Cox's recursive evaluation in 1972, define a whole curve with one set of control points and a knot vector; each control point influences only a few neighbouring segments, so editing is local. Non-uniform rational B-splines (NURBS) add a weight to each control point and allow non-uniform knot spacing. The rational form represents conic sections exactly, including circles, which polynomials cannot. NURBS became the standard of computer-aided design through the IGES and STEP exchange formats and are the core representation in CAD systems such as CATIA, SolidWorks, Rhino and Autodesk Alias.

Surfaces

Curves extend to surfaces by adding a second parameter. A tensor-product Bézier patch is defined by a grid of control points, 4 × 4 for a bicubic patch, and evaluated by applying de Casteljau in u to each row and then in v to the results. The Utah teapot, modelled by Martin Newell in 1975, consists of 32 bicubic Bézier patches and became graphics' most famous test object. NURBS surfaces are the tensor-product extension of NURBS curves. Patches are awkward for objects of arbitrary topology, because rectangular patches must be stitched with continuity constraints along shared edges and at points where three or five patches meet.

Subdivision surfaces, covered in lesson 7, solved that problem for animation: Catmull–Clark subdivision generalizes the uniform bicubic B-spline surface to meshes of any topology, and Pixar adopted it from Geri's Game (1997) onward. Pixar's OpenSubdiv library (open-sourced in 2012) evaluates these surfaces on GPUs. Implicit surfaces, defined by \(f(x, y, z) = 0\), are a third family: metaballs and signed distance fields blend smoothly and support easy booleans, are ray-marched in demoscene and shader art, and are converted to meshes with marching cubes (Lorensen and Cline, 1987) for medical and scientific visualization. Rendering any of these surfaces on a GPU normally means tessellating them into triangles, either ahead of time or with hardware tessellation stages.

Keyframe animation and interpolation

Most animation in films and games is keyframed: an animator sets values (positions, rotations, facial controls, light intensities) at chosen times, the keys, and the computer fills in the frames between, the in-betweens, a term from hand-drawn animation, where senior animators drew key poses and assistants drew the rest. Interpolation determines the character of motion. Linear interpolation moves at constant speed and stops abruptly at each key, which looks mechanical. Ease-in and ease-out curves accelerate from and decelerate into keys; the smoothstep polynomial \(3u^2 - 2u^3\) is the simplest. Spline interpolation through all keys gives continuous velocity: Catmull–Rom splines (1974) pass through every key with tangents set from the neighbouring keys, and Kochanek–Bartels splines (1984) add tension, continuity and bias controls. Animation software exposes these as editable curves in a graph editor.

Linear, eased and Catmull–Rom interpolation of four keyframes
# Keyframe interpolation of a camera height: linear, smoothstep easing and a
# Catmull-Rom spline through four keys. Printed at several times.
import numpy as np
keys_t = [0.0, 1.0, 2.0, 3.0]
keys_y = [0.0, 4.0, 3.0, 6.0]
def linear(t):
    return float(np.interp(t, keys_t, keys_y))
def smooth(t):                                     # ease in and out within each interval
    i = min(int(t), 2); u = t - i; s = u * u * (3 - 2 * u)
    return keys_y[i] + (keys_y[i + 1] - keys_y[i]) * s
def catmull_rom(t):
    i = min(int(t), 2); u = t - i
    p = [keys_y[max(i - 1, 0)], keys_y[i], keys_y[i + 1], keys_y[min(i + 2, 3)]]
    return 0.5 * ((2 * p[1]) + (-p[0] + p[2]) * u + (2 * p[0] - 5 * p[1] + 4 * p[2] - p[3]) * u * u
                  + (-p[0] + 3 * p[1] - 3 * p[2] + p[3]) * u ** 3)
print("  t    linear  smoothstep  Catmull-Rom")
for t in np.arange(0, 3.01, 0.25):
    print(f"{t:4.2f}  {linear(t):7.3f}  {smooth(t):10.3f}  {catmull_rom(t):11.3f}")
h = 1e-4
for name, f in (("linear", linear), ("smoothstep", smooth), ("Catmull-Rom", catmull_rom)):
    vin = (f(1.0) - f(1.0 - h)) / h; vout = (f(1.0 + h) - f(1.0)) / h
    print(f"{name:12} velocity just before key 1: {vin:6.2f}  just after: {vout:6.2f}")
Output
  t    linear  smoothstep  Catmull-Rom
0.00    0.000       0.000        0.000
0.25    1.000       0.625        0.836
0.50    2.000       2.000        2.062
0.75    3.000       3.375        3.258
1.00    4.000       4.000        4.000
1.25    3.750       3.844        4.008
1.50    3.500       3.500        3.562
1.75    3.250       3.156        3.086
2.00    3.000       3.000        3.000
2.25    3.750       3.469        3.539
2.50    4.500       4.500        4.438
2.75    5.250       5.531        5.367
3.00    6.000       6.000        6.000
linear       velocity just before key 1:   4.00  just after:  -1.00
smoothstep   velocity just before key 1:   0.00  just after:  -0.00
Catmull-Rom  velocity just before key 1:   1.50  just after:   1.50

At the key at t = 1, linear interpolation jumps from a velocity of 4 to −1 instantly, a visible jerk. Smoothstep brings the velocity to zero on both sides, so the camera stops and starts at every key, which is right for a pose held deliberately but wrong for continuous motion. Catmull–Rom keeps the velocity continuous through the key, at a value set by the neighbouring keys, so the motion flows through it, at the cost of overshooting between keys when values change direction. Rotations need their own interpolation: lesson 4's slerp, or its spline extension squad, interpolates quaternions without the gimbal problems of interpolating Euler angles.

Skeletal animation and skinning

Characters are animated with a skeleton, a hierarchy of joints (bones), each with a transformation relative to its parent, exactly the scene-graph structure of lesson 3. Animating a few dozen joints is far easier than animating thousands of vertices. The mesh, called the skin, is attached to the skeleton by rigging: each vertex is assigned weights for the bones that influence it, typically up to four, summing to 1. Linear blend skinning (LBS), also called smooth skinning, computes each deformed vertex as the weighted sum of the vertex transformed by each influencing bone, \(v' = \sum_i w_i M_i B_i^{-1} v\), where \(B_i^{-1}\) is the inverse of the bone's rest pose and \(M_i\) its current transformation. It runs easily in a vertex shader and is used almost universally in games.

Linear blend skinning collapses a twisting forearm: the candy-wrapper artefact
# Linear blend skinning of a cylinder-like arm around an elbow, showing the
# "candy wrapper" collapse when the forearm twists 180 degrees.
import numpy as np
def rot_x(a):
    c, s = np.cos(a), np.sin(a)
    return np.array([[1, 0, 0], [0, c, -s], [0, s, c]])
# a ring of 8 vertices around the arm axis (x) at the elbow, radius 1
ring = np.array([[0.0, np.cos(k * np.pi / 4), np.sin(k * np.pi / 4)] for k in range(8)])
def skin(twist, w):
    upper = np.eye(3)                 # upper arm bone: no rotation
    fore = rot_x(twist)               # forearm bone twisted about the arm axis
    return np.array([w * (fore @ v) + (1 - w) * (upper @ v) for v in ring])
for deg in (0, 90, 150, 180):
    r = skin(np.radians(deg), 0.5)
    radius = np.linalg.norm(r[:, 1:], axis=1).mean()
    print(f"twist {deg:3d} deg, weights 0.5/0.5: mean ring radius {radius:.3f}")
print("dual-quaternion skinning would keep the radius at 1.000 by blending rotations, not matrices")
Output
twist   0 deg, weights 0.5/0.5: mean ring radius 1.000
twist  90 deg, weights 0.5/0.5: mean ring radius 0.707
twist 150 deg, weights 0.5/0.5: mean ring radius 0.259
twist 180 deg, weights 0.5/0.5: mean ring radius 0.000
dual-quaternion skinning would keep the radius at 1.000 by blending rotations, not matrices

Blending matrices linearly does not blend rotations properly: averaging a rotation of 0 and 180 degrees about the arm axis produces a matrix that squashes the ring to a point instead of rotating it 90 degrees. This is the candy-wrapper artefact, visible at wrists and shoulders, along with volume loss at bent elbows. Dual-quaternion skinning (Kavan and colleagues, 2007) blends rigid transformations represented as dual quaternions and preserves volume under twist, at a small extra cost, and is available in major game engines and digital content tools. Film rigs add corrective blend shapes, muscle simulation and machine-learned deformers on top of skinning. Blend shapes (morph targets), which store complete alternative positions for vertices and mix them with weights, drive facial animation.

Inverse kinematics

Forward kinematics computes where an end effector, such as a hand or foot, ends up from the joint angles. Animators often want the reverse: place the hand on a door handle or plant the foot on uneven ground and let the computer find joint angles. That is inverse kinematics (IK), shared with robotics (see the robotics courses for the full treatment of kinematic chains and Jacobians). For a two-bone chain such as an arm or leg, there is a closed-form solution from the law of cosines, with two mirror solutions (elbow up or down) chosen by a pole vector. Longer chains use iterative methods: Jacobian transpose or damped least-squares steps, cyclic coordinate descent, or FABRIK (2011), which repeatedly moves joints along lines toward the target and back.

Analytic two-bone inverse kinematics with both elbow solutions, checked by forward kinematics
# Analytic inverse kinematics for a planar two-link arm (upper arm 3, forearm 2),
# then verification with forward kinematics. Unreachable targets are clamped.
import math
L1, L2 = 3.0, 2.0
def fk(a, b):
    x = L1 * math.cos(a) + L2 * math.cos(a + b)
    y = L1 * math.sin(a) + L2 * math.sin(a + b)
    return x, y
def ik(x, y, elbow=+1):
    d = math.hypot(x, y)
    d = min(max(d, abs(L1 - L2)), L1 + L2)        # clamp to the reachable annulus
    cb = (d * d - L1 * L1 - L2 * L2) / (2 * L1 * L2)
    b = elbow * math.acos(max(-1.0, min(1.0, cb)))
    a = math.atan2(y, x) - math.atan2(L2 * math.sin(b), L1 + L2 * math.cos(b))
    return a, b
for target in ((4.0, 1.0), (1.5, 2.5), (6.0, 0.0)):
    for elbow in (+1, -1):
        a, b = ik(*target, elbow)
        x, y = fk(a, b)
        print(f"target {target}  elbow {'down' if elbow > 0 else 'up  '}: shoulder {math.degrees(a):7.2f} deg,"
              f" elbow {math.degrees(b):7.2f} deg -> reaches ({x:.3f}, {y:.3f})")
Output
target (4.0, 1.0)  elbow down: shoulder  -13.18 deg, elbow   70.53 deg -> reaches (4.000, 1.000)
target (4.0, 1.0)  elbow up  : shoulder   41.25 deg, elbow  -70.53 deg -> reaches (4.000, 1.000)
target (1.5, 2.5)  elbow down: shoulder   19.55 deg, elbow  112.02 deg -> reaches (1.500, 2.500)
target (1.5, 2.5)  elbow up  : shoulder   98.53 deg, elbow -112.02 deg -> reaches (1.500, 2.500)
target (6.0, 0.0)  elbow down: shoulder    0.00 deg, elbow    0.00 deg -> reaches (5.000, 0.000)
target (6.0, 0.0)  elbow up  : shoulder    0.00 deg, elbow   -0.00 deg -> reaches (5.000, 0.000)

For both reachable targets, both elbow configurations reach the target exactly, which forward kinematics confirms. The target at (6, 0) lies beyond the arm's reach of 5, so the solver clamps the distance and stretches the arm straight toward it, the usual behaviour in game engines, which also blend IK with keyframed animation so that feet adapt to stairs without losing the character of the walk cycle.

Physically based animation

Simulation animates what is too complex to keyframe: cloth, hair, smoke, water, crowds, debris and ragdolls. The simplest system is a set of particles, introduced by William Reeves in 1983 for the Genesis effect in Star Trek II: each particle has position, velocity and forces, and a numerical integrator advances them in time steps. Explicit (forward) Euler updates position with the old velocity, which is simple but adds energy each step and becomes unstable for stiff systems such as springs. Semi-implicit (symplectic) Euler updates velocity first and uses the new velocity for position; for the same cost it conserves energy far better on average. Velocity Verlet and position-based dynamics (Müller and colleagues, 2007), which projects positions directly to satisfy constraints, are the workhorses of game cloth and soft bodies. Implicit integration solves a system of equations per step for stiff cloth in film.

Energy behaviour of explicit Euler, semi-implicit Euler and Verlet on a mass-spring oscillator
# A mass on a spring (x'' = -x, period 2*pi) integrated for 20 s with a 0.1 s
# step by explicit Euler, semi-implicit (symplectic) Euler and velocity Verlet.
# The exact energy 0.5*(x^2 + v^2) should stay at 0.5.
import math
dt, steps = 0.1, 200
def run(method):
    x, v = 1.0, 0.0
    worst = 0.0
    for _ in range(steps):
        if method == "explicit":
            x, v = x + v * dt, v - x * dt            # both updates use old values
        elif method == "semi-implicit":
            v = v - x * dt; x = x + v * dt           # velocity first, then position
        else:                                         # velocity Verlet
            a = -x
            x = x + v * dt + 0.5 * a * dt * dt
            v = v + 0.5 * (a - x) * dt               # average of old and new acceleration
        e = 0.5 * (x * x + v * v)
        worst = max(worst, abs(e - 0.5) / 0.5)
    return x, v, worst
print(f"exact after {steps * dt:.0f} s: x = {math.cos(steps * dt):+.3f}")
for m in ("explicit", "semi-implicit", "Verlet"):
    x, v, worst = run(m)
    e = 0.5 * (x * x + v * v)
    print(f"{m:14} x = {x:+.3f}  energy now {e:.3f} ({100 * (e - 0.5) / 0.5:+7.1f}%)  worst error {100 * worst:6.1f}%")
Output
exact after 20 s: x = +0.408
explicit       x = +1.265  energy now 3.658 ( +631.6%)  worst error  631.6%
semi-implicit  x = +0.355  energy now 0.484 (   -3.3%)  worst error    5.3%
Verlet         x = +0.400  energy now 0.499 (   -0.2%)  worst error    0.2%

Explicit Euler's energy grows by a factor of \(1 + \Delta t^2\) every step, so after 200 steps the spring has over seven times the energy it started with, and with a larger step or a stiffer spring it would explode. Semi-implicit Euler's energy oscillates within a few per cent of the true value but never drifts away, because the method is symplectic: it exactly conserves a slightly perturbed energy. Velocity Verlet does even better at the same cost, and its position stays close to the exact cosine. This is why games use semi-implicit Euler or Verlet rather than explicit Euler. Rigid-body engines such as Box2D, Bullet, PhysX and Havok, and the Jolt engine used in Horizon Forbidden West (2022) and open-sourced, combine these integrators with broad-phase collision detection (bounding-volume hierarchies again) and constraint solvers. Fluids use grids, particles (smoothed-particle hydrodynamics) or hybrids such as FLIP and the material point method, which Disney used for snow in Frozen (2013).

Motion capture and learned animation

Motion capture records the movement of real performers, with optical markers tracked by infrared cameras, inertial suits or, increasingly, markerless video, and retargets it onto characters with different proportions. Performance capture of faces and bodies drove films such as Avatar (2009) and many current games. Motion matching, used in For Honor (2017) and The Last of Us Part II (2020), searches a large database of captured motion each frame for the clip that best matches the desired trajectory, producing responsive, natural movement. Neural networks now generate character motion, deform faces and solve IK, and text-to-motion models produce animation from descriptions, though production pipelines still rely heavily on animators' judgment.

Worked examples

Worked problem

Quadratic Bézier by hand

Evaluate the quadratic Bézier curve with control points (0, 0), (2, 4) and (4, 0) at t = 0.25 using de Casteljau.

Show solution
  1. First round: \(0.75(0,0) + 0.25(2,4) = (0.5, 1)\) and \(0.75(2,4) + 0.25(4,0) = (2.5, 3)\).
  2. Second round: \(0.75(0.5,1) + 0.25(2.5,3) = (1.0, 1.5)\).

Answer: The curve passes through (1.0, 1.5) at t = 0.25.

Worked problem

Continuity of a join

A cubic segment ends with control points P2 = (4, 2) and P3 = (5, 2). The next segment starts at Q0 = (5, 2). Where must Q1 be for C1 continuity, and what is allowed for G1?

Show solution
  1. C1 requires \(Q_1 - Q_0 = P_3 - P_2 = (1, 0)\), so \(Q_1 = (6, 2)\).
  2. G1 requires only the same direction: \(Q_1 = (5 + k, 2)\) for any k > 0.

Answer: Q1 = (6, 2) for C1; any point on the ray to the right of (5, 2) for G1.

Worked problem

Two-bone IK

A leg has thigh length 0.45 m and shin length 0.40 m. What knee angle (measured as the bend from straight) places the foot 0.70 m from the hip?

Show solution
  1. Law of cosines: \(\cos b = (0.70^2 - 0.45^2 - 0.40^2)/(2 \times 0.45 \times 0.40) = (0.49 - 0.2025 - 0.16)/0.36 = 0.354\).
  2. \(b = \arccos 0.354 = 69.3°\).

Answer: The knee bends about 69 degrees from straight.

Common mistakes

  • MistakeUse one high-degree Bézier curve for a long, complex path.

    InsteadJoin cubic segments into a spline with the continuity the application needs.

  • MistakeAssume G1 continuity is enough for animation paths.

    InsteadG1 looks smooth but changes speed; use C1 (or arc-length parameterization) for motion.

  • MistakeInterpolate rotations by interpolating Euler angles or matrices.

    InsteadInterpolate quaternions with slerp or squad.

  • MistakeBlame the rig for collapsing wrists without checking the skinning method.

    InsteadLinear blend skinning causes candy-wrapper artefacts; use dual quaternions or correctives.

  • MistakeIntegrate stiff springs with explicit Euler and a large time step.

    InsteadUse semi-implicit, Verlet, position-based or implicit methods and smaller steps.

Summary and key terms

  • Bézier curves are defined by control points, evaluated by repeated linear interpolation (de Casteljau) and subdivided for free.
  • They interpolate their end points, are tangent to the control polygon there, and lie in its convex hull.
  • Splines join cubic segments; C1 matches velocity, C2 matches curvature, and G1 matches only tangent direction.
  • B-splines and NURBS give local control and C2 smoothness; NURBS represent conics exactly and dominate CAD.
  • Keyframe animation interpolates keys with linear, eased or spline curves; rotations need quaternion interpolation.
  • Linear blend skinning deforms meshes by weighted bone transforms but collapses under twist.
  • Inverse kinematics solves joint angles for an end-effector target, analytically for two bones and iteratively for chains.
  • Simulation needs stable integrators: semi-implicit Euler and Verlet conserve energy far better than explicit Euler.
Bézier curve
A polynomial curve defined by control points with Bernstein-polynomial weights.
De Casteljau's algorithm
Evaluating a Bézier curve by repeated linear interpolation of control points.
Convex hull property
A Bézier curve lies within the convex hull of its control points.
C1 continuity
Matching first derivatives where curve segments join.
G1 continuity
Matching tangent directions, but not necessarily magnitudes, at a join.
B-spline
A piecewise polynomial curve with local control defined by control points and knots.
NURBS
Non-uniform rational B-splines, which can represent conic sections exactly.
Keyframe
A value set by an animator at a specific time, between which values are interpolated.
Linear blend skinning
Deforming vertices by a weighted sum of bone transformations.
Inverse kinematics
Computing joint angles that place an end effector at a target.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. How many control points define a cubic Bézier curve?

    Show answer

    Four.

  2. Does a cubic Bézier curve pass through its middle control points?

    Show answer

    Generally not; it passes only through the first and last.

  3. What is the tangent of a cubic Bézier at t = 0?

    Show answer

    \(3(P_1 - P_0)\).

  4. What property makes Bézier bounding boxes cheap?

    Show answer

    The convex hull property.

  5. Which continuity matters for reflections on a car body?

    Show answer

    C2 (or G2), continuity of curvature.

  6. Why can NURBS represent a circle exactly?

    Show answer

    Their rational form (weighted control points) represents conic sections.

  7. What does smoothstep do to the velocity at each key?

    Show answer

    Brings it to zero.

  8. What artefact does linear blend skinning show under twist?

    Show answer

    The candy-wrapper collapse.

  9. How many solutions does two-bone IK usually have for a reachable target?

    Show answer

    Two (elbow up or elbow down).

  10. Which integrator updates velocity before position?

    Show answer

    Semi-implicit (symplectic) Euler.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz