Perlin Noise for Motion

Watch a resting character in a good game or film and they are never truly still: the chest lifts and falls, the weight drifts subtly from one foot to the other, a stray lock of hair sways, the eyes make tiny wandering saccades. None of that is hand-keyed frame by frame — it would take forever and still look mechanical. It is procedural motion, and the engine that quietly drives almost all of it is coherent noise: Ken Perlin's 1983 invention (he won an Academy Award for it) that turns a clock into a natural, living wobble.

This page is about one deceptively simple move: sample a smooth noise function along time and feed the result into a parameter — a camera offset, a flicker, a sway angle. The trick is entirely in which random function you sample. Reach for the obvious one and you get garbage; reach for Perlin (or its faster cousin, simplex) and you get life.

Why random() is useless for motion

The naive idea is to jitter a value with a fresh random number every frame: x(t) = \text{amplitude} \cdot \texttt{random}(). It looks fine frozen on a single frame, but in motion it is a disaster. Plain random() is uncorrelated: the value at frame 100 knows nothing about frame 99, so every frame teleports to a brand-new spot. The eye reads that as static — buzzing, epileptic noise, not motion. Real things have momentum; where they are now is close to where they were a moment ago.

Those three properties are exactly what hand-keyed "organic wobble" tries to fake. Perlin noise gives them for free, as a pure function you can evaluate anywhere.

How Perlin noise is built

The construction is neat. Lay down a regular grid of integer points. At each grid point, deterministically hash its coordinates into a fixed pseudo-random gradient vector (in 1D just a slope, positive or negative). To evaluate the noise at some point p between grid points:

Because the value is pinned to 0 at each grid point and eased between them, the result glides smoothly, never repeats obviously, and stays inside a bounded range (roughly [-1, 1]). Simplex noise is Perlin's later reworking that uses a triangular/tetrahedral grid instead of a square one — cheaper in high dimensions and free of the faint axis-aligned artefacts of the classic version — but the idea and how you use it are identical. (For the maths of the interpolation kernels, see value and gradient noise.)

Sampling noise along time

Here is the whole idea in one line. Instead of a fresh random number per frame, evaluate 1D noise at the current time and use that as your parameter:

x(t) = A \cdot \operatorname{noise}(f \cdot t)

The amplitude A sets how far the value swings; the frequency f sets how fast it wanders (it stretches or squeezes the time axis before sampling). Because \operatorname{noise} is coherent, consecutive frames read consecutive, nearby points on the curve — the motion is smooth by construction. Swap in different amplitudes, frequencies and channels and one function paints a whole catalogue of "alive":

EffectWhat noise drivesRough settings
Idle swayroot-bone rotation / lean anglelow f, small A
Breathingchest scale / spine pitchvery low f
Candle flickerlight intensity + colourhigher f
Handheld camera shakecamera position + rotation offsetmid f, small A
Lazy flag wavevertex phase along the clothlow f
Wandering eyesgaze target x, ylow f, occasional

Every one of these used to be tedious hand-keyed wobble. As procedural motion, it is a single call per frame, and it never loops or repeats.

Worked example: procedural camera shake

Suppose we want a handheld feel: a small, restless offset added to the camera each frame. We drive it with \text{offset}(t) = A \cdot \operatorname{noise}(f \cdot t). Play with the two knobs below and watch the curve.

Two things to notice. Raising the frequency f makes the wobble jittery — more wiggles packed into the same span, a nervous, caffeinated camera. Raising the amplitude A makes it wilder — the same wandering, but with bigger swings. Yet through all of it the curve stays smooth: no vertical jumps, no teleports. Plot plain random() beside it and you would see a solid band of vertical static instead — that is the whole difference between noise and randomness. Because the underlying noise here is fully deterministic (a fixed seed), replaying the shot gives byte-for-byte the same shake: reproducible for review, unlike a per-frame RNG.

function cameraOffset(t: number, A: number, f: number): number { // noise1D is coherent: nearby t → nearby output, so no frame-to-frame jitter. return A * noise1D(f * t); } // Independent channels: SAME t, but DIFFERENT offsets, or the shake is stuck on a diagonal. const shakeX = cameraOffset(t, 0.4, 2.0); const shakeY = cameraOffset(t + 1000, 0.4, 2.0); // note the +1000

Layering detail: fBm and octaves

A single noise call gives one scale of motion — either a slow drift or a fast buzz, not both. Real motion has structure at many scales at once: a big lazy sway with tiny quick tremors riding on top. You get that by summing several noise samples at doubling frequencies and halving amplitudes — octaves — a sum called fractional Brownian motion (fBm):

\operatorname{fBm}(t) = \sum_{i=0}^{n-1} \frac{1}{2^{\,i}}\,\operatorname{noise}\!\left(2^{\,i} f\, t\right)

The first octave gives the broad, slow shape; each further octave adds finer, faster detail at progressively smaller amplitude. Two extra parameters control the mix: lacunarity (how fast frequency grows per octave, usually 2) and gain or persistence (how fast amplitude shrinks, usually \tfrac12). Coarse + fine layered together is what makes handheld camera shake read as a real operator's hands rather than a metronome.

Multi-dimensional wander: the meandering firefly

To make something drift around a 2D area — a firefly, a bird, a hovering drone — you need two smoothly-varying numbers, one for x and one for y. The instinct is to sample the same noise curve for both. Don't — that is the classic trap below. Instead read the noise at two different offsets (or with two different seeds), so the channels vary independently:

// A firefly meandering over a 2D field, driven entirely by coherent noise. function firefly(t: number): { x: number; y: number } { const speed = 0.6; return { x: 50 + 40 * noise1D(speed * t), // channel 1 y: 50 + 40 * noise1D(speed * t + 1000), // channel 2, offset far away }; }

Because the two samples come from unrelated stretches of the noise, x and y rise and fall on their own schedules, and the firefly traces a lazy, looping, never-repeating path. (In 2D/3D noise you would instead pass a whole point, \operatorname{noise}(x, y), and sample it at a moving location — same principle, different signature.)

Ken Perlin invented his noise function while working on Tron (1982), frustrated that computer-generated surfaces looked too clean and machined — that unmistakable "made by a computer" plasticness. Coherent noise let artists smear controllable, natural-looking irregularity over everything: marble veins, rolling terrain, cloud cover, wobbling flames, rusted metal. It became so fundamental to computer graphics that the Academy of Motion Picture Arts and Sciences gave him a Technical Achievement Award in 1997. The same function that roughens a marble column is the one nudging a resting character's breath — texture in space, motion in time, one idea.

The single most common procedural-motion bug: driving both x and y (or every wobble on a rig) from the same \operatorname{noise}(t). If x = \operatorname{noise}(t) and y = \operatorname{noise}(t), then x = y at every instant — so your "wandering" firefly only ever slides back and forth along the diagonal y = x, never filling the plane. It looks weirdly constrained and robotic, the opposite of what you wanted.

The fix is to give each channel its own slice of noise: sample at a different offset (\operatorname{noise}(t) and \operatorname{noise}(t + 1000)), or use a different seed per channel, or step into a genuinely different region of a 2D noise field. The offset must be large enough that the two samples aren't still correlated — a nudge of 0.001 won't do; jump by hundreds or thousands.

Sampling noise along time is one tool in the procedural-motion kit. It pairs naturally with spring-driven secondary motion for follow-through, with procedural walk cycles for foot placement, and with particle systems where thousands of agents each need their own uncorrelated wander.