Process
MotionAtelier, Behind the Panel
How a curve panel for Houdini turns a few parameters into a handful of Bézier keys, and writes them back without breaking your scene.
MotionAtelier is a curve panel for Houdini 20.0 to 22.0. You select some keys, it turns them into a curve you can shape (or you start from a Bounce, an Elastic, a Wave or some Noise), you stack effects on top, and it writes the result back onto as many channels as you like, as a few native Bézier keys, in one undo step. This post goes through it in the order the data flows. Every part comes with the bit of the interface that drives it, and those bits are the real panel rebuilt for the page, running the same maths as the tool. So go ahead and poke at them.
Or skip ahead and poke at the whole thing first. This is the complete panel, with a few simulated channels of a bouncing ball under it standing in for Houdini (select two keys of the ball’s height, hit Get Keys, shape the curve, Commit Keys).
The interactive panel needs JavaScript.
One Curve From 0 to 1
Everything starts by forgetting the scene. Get Keys takes your selection and squashes it into a curve that runs from (0, 0) to (1, 1): time gets divided by the span of the selection, value by its range, and every slope gets scaled by the ratio of the two. In numbers, for a selection from time t0 to t1 with values between vmin and vmax:
x = (t − t0) / (t1 − t0)
y = (v − vmin) / (vmax − vmin)
slope = slope · (t1 − t0) / (vmax − vmin)A bounce on a 12-frame drop and a bounce on a 400-frame camera move end up as the same object. That’s what lets one curve go onto any number of channels at once.
The curve spans every key between the first and last selected one, not just the two ends. Dropping the keys in between would quietly change the shape you meant to edit, and a tool that changes stuff you didn’t touch is a tool nobody trusts.
There’s exactly one combination it refuses: a curve that starts and ends at the same value, committed onto keys that change value. You can’t stretch a flat line over a change, so the panel tells you instead of guessing. Overshoot, on the other hand, is free. Values above 1 or below 0 are fine everywhere and simply read as “past the last key”.
Knots and Handles
A knot is a value at a time, with two handles. Each handle is a slope and a weight, and the weight is how far the handle reaches into its segment (as a fraction of it). Between two knots you get a cubic Bézier, built straight from those numbers:
P0 = (a.x, a.y)
P1 = (a.x + wa·d, a.y + sa·wa·d) (sa, wa) = a's outgoing handle
P2 = (b.x − wb·d, b.y − sb·wb·d) (sb, wb) = b's incoming handle
P3 = (b.x, b.y) d = b.x − a.xAnimation curves have one rule drawing curves don’t: time can’t run backwards. With weights kept between 0 and 1 it can’t, and the proof is short enough to live in a code comment. The derivative of the time polynomial (divided by 3d) is wa(1−u)² + 2u(1−u)(1−wa−wb) + wb·u². It’s bilinear in the two weights, so its minimum over the allowed weights sits at a corner, where it comes out as 2u(1−u), (1−u)², u² or (1−2u)². None of those can go negative. Pretty neat.
Asking a Bézier “what’s the value at time x” means inverting that time polynomial first. The tool uses Newton’s method kept inside a bracket: a Newton step only counts if it stays in the bracket and shrinks fast enough, otherwise the bracket gets halved. Every root is checked before it’s used, and if the check fails the solver throws an error instead of handing back something that’s almost right.
Most of the knot operations are what you’d expect: smooth, break, flatten, insert, delete. Two have a rule worth knowing. Inserting a knot splits the segment exactly, so the curve doesn’t move by a pixel. And flipping in value mirrors the curve around the straight line between its ends, not around the horizontal. Since everything gets normalised before it’s committed, a horizontal mirror would write the exact same keys, which makes it a button that does nothing.
The interactive panel needs JavaScript.
Speed, Not Value
Animators judge motion by speed way more than by position, so the panel has a second view of the same curve: its velocity, sampled 48 times per segment. In that view the knots turn into different controls. A circle is the speed at a knot (drag it up or down to change the slope, sideways to retime the knot). A square is an influence, how far a handle reaches into its segment (drag it sideways to change the weight).
Nothing new gets stored. The velocity view edits the exact same slopes and weights as the value view, it just shows them in the units an animator actually thinks in.
You can try it on the Ease panel above: hit Velocity in its top right corner.
Shapes From Physics
Hand-drawn curves are only one way in. Four shapes are generated from a few parameters, and each one is built to come out as exact keys, not a dense sampled curve you’d have to clean up afterwards.
Bounce is a ball dropped under gravity that loses some speed at every contact. The units are picked so the arithmetic mostly disappears: the first fall lasts 1 and drops 1, so gravity is 2 and the height is 1 − t². Rebound k leaves the ground at 2eᵏ (e is the elasticity, the coefficient of restitution), peaks at e²ᵏ and lands 2eᵏ later. The curve shows progress towards the target, 1 − height, so it hits 1 at every contact.
Let’s just look at the code that plans the flights (from the tool, docstring trimmed):
def flight_plan(bounces: int, restitution: float) -> tuple[list[float], list[float], int]:
contacts = [1.0]
speeds: list[float] = []
kept = 0
for k in range(1, bounces + 1):
ek = restitution**k
if ek * ek < MIN_APEX_HEIGHT:
break
speeds.append(2.0 * ek)
contacts.append(contacts[-1] + 2.0 * ek)
kept += 1
return contacts, speeds, keptBreaking it down:
- the first contact is always at time 1 (the first fall)
- each rebound launches at
2eᵏand lasts that long, so the next contact is the last one plus2eᵏ - a rebound whose apex (
e²ᵏ) is belowMIN_APEX_HEIGHTends the loop, and everything after it is dropped
Here’s the nice part. Every half-flight is a parabola, and a cubic Bézier with handles at a third of the segment is a parabola. Exactly, not approximately. So the whole bounce comes down to a knot where the ball lets go, one per contact and one per apex. That’s it. Contacts are broken knots (the velocity flips there), apexes are smooth and flat.
The one shortcut is on purpose. A rebound lower than 0.1% of the drop gets left out: on a 100-unit move that’s 0.1 unit, below anything you’d see, and it keeps the shortest half-flight at least 3% of the first fall so knots never crowd each other. The panel tells you how many it dropped.
The interactive panel needs JavaScript.
Try 12 bounces with an elasticity of 0.5, then push the elasticity up and watch the rebounds come back as soon as they’re tall enough to see. The Velocity view shows the same motion as a sawtooth: the speed changes at a constant rate during each flight (gravity is constant) and jumps at every contact.
Elastic is a damped spring settling on its target, the classic step response (ζ is the damping):
y(t) = 1 − e^(−ζωt)·(cos ω_d t + ζ/√(1−ζ²)·sin ω_d t), ω_d = ω√(1−ζ²)Its extremes fall at regular times and every half swing is a scaled copy of the first, which is what makes it cheap: one pair of handle weights, fitted once with least squares, works for every knot. I say least squares because the minimax search kept jumping between solutions. Damping stops at 0.7. Past that the overshoot is under 4.6%, the best flat cubic misses the real curve by more than 1.2%, and it’d need handles almost as long as their segments, which Houdini can’t store.
The interactive panel needs JavaScript.
Wave adds a decaying sine to the straight line between the ends:
y(x) = x + A·env(x)·sin θ(x), θ = 2π·c·x + φ
env(x) = 4x(1 − x)·e^(−λx) (zero at both ends; λ = decay)Its knots go where the oscillation turns (found by bracketing and bisection), and segments get split until the curve is within 0.05% of the motion everywhere, with a cap that only kicks in at extreme settings.
The interactive panel needs JavaScript.
Noise offsets a lattice of points from that same straight line using a hash of the seed and the point’s index, so the same seed always gives the same shape and nothing depends on a global random state. The hash is small enough to show in full:
MASK32 = 0xFFFFFFFF
GOLDEN_GAMMA_32 = 0x9E3779B9 # ⌊2^32 / φ⌋, Knuth's multiplicative hashing constant
def lowbias32(x: int) -> int:
x &= MASK32
x ^= x >> 16
x = (x * 0x7FEB352D) & MASK32
x ^= x >> 15
x = (x * 0x846CA68B) & MASK32
x ^= x >> 16
return x
def lattice_hash(seed: int, index: int) -> int:
"""32-bit hash of a (seed, lattice index) pair."""
return lowbias32((lowbias32(seed) + (index * GOLDEN_GAMMA_32)) & MASK32)
def lattice_unit(seed: int, index: int) -> float:
"""Hash mapped linearly onto ``[-1, 1]`` (both ends attainable)."""
return lattice_hash(seed, index) / MASK32 * 2.0 - 1.0Breaking it down: lowbias32 scrambles 32 bits with two multiply-and-shift rounds, the index gets spread by the golden-ratio constant before the second round, and lattice_unit maps the result onto −1…1, the offset each lattice point gets (times the amplitude).
The interactive panel needs JavaScript.
The Motion Stack
On top of a shape sits the Motion Stack: effects as layers, each with its own mix, applied top to bottom. Select a layer and you see the result up to that layer. Commit Keys always uses all of them.
The effects aren’t all the same kind of maths, so the stack treats them differently:
- Contrast and Arc Bias are analytic: they change the curve’s knots and slopes exactly
- Noise, Gaussian Smooth, Sharpen and Delay work on samples, and get rebuilt with monotone cubic interpolation (PCHIP)
- Spring, Lag and Jiggle are followers: they simulate something chasing the curve, on a fixed grid of 2048 steps, so the result never depends on how big the window is
Arc Bias took three tries. The first version remapped timing inside each rise and fall. The second bent the values through the same map, and it rounded every sharp extreme off, so a bounce’s impacts came out soft, which is exactly what a bounce shouldn’t do. The “fix” in the third version: the slope at the near end of each arc never drops below a quarter of its value, so impacts stay sharp and the arc still bows.
The interactive panel needs JavaScript.
That panel opens with an Arc Bias on the bounce. Drag its bias around and watch the impacts: they stay sharp.
Back to a Few Keys
Once the stack has run, the curve is dense (a follower alone gives you hundreds of knots). Houdini should get a handful. The fitter finds the fewest Bézier spans that stay within a tolerance of the dense curve, 0.5% of the value range by default, or it refuses. It never writes one key per sample.
Here’s the order it works in:
- evaluate the curve on a fine table of probes
- mark what has to survive as a key: the ends, every kink, every extreme taller than twice the tolerance
- between those landmarks, try spans whose end values and slopes come straight from the source, so only the two handle weights are free, and search those
- pick the fewest spans that pass, with dynamic programming
- check every probe against the exact source
If any check fails, the commit is refused and the panel tells you why. The worst case I’ve measured so far is a jiggle on a bounce: 513 samples fitted in 0.78 s.
The interactive panel needs JavaScript.
This one opens with a Jiggle on the bounce. The squares and the thin white line are what the fitter would commit (that overlay isn’t in the tool, it’s here to show what happens behind it), and the status line counts the keys.
Writing Into Houdini
Everything above happens inside the panel and doesn’t touch your scene. Commit Keys is the one moment that does, and it’s built to be one undo step that either fully works or leaves the scene exactly as it was.
The plan gets worked out first, without touching anything: which keys stay, which get deleted, which get written. Boundary keys keep their times and values, and keys outside the selection are never written. Then the panel checks that every channel still matches what it read, opens one undo group, applies the plan and reads every key back. If anything’s off, it undoes the group and checks again that every channel matches its snapshot.
Most of the work here was figuring out what Houdini actually does, by measuring it. A few things I found:
- A key’s “acceleration” is the length of its handle, in seconds and value units together. The time a handle spans is its acceleration divided by
√(1 + slope²). - Setting an acceleration through the Python API quietly switches the key into ratio mode, unless you turn ratio mode off again right after.
- An acceleration of exactly zero next to a non-zero one gets read as “unset”, and the two sides get tied. So the panel writes 1e-15 instead, a handle way too short to see that Houdini still treats as a real value.
- Deleting a key rescales the handles of its neighbours. The panel writes every key twice: the second pass doesn’t change any structure and just puts the neighbours back.
Here’s how a key gets written, with two of those findings baked in (trimmed to the parts that matter):
def _storable_accel(accel: float, other: float) -> float:
"""Houdini treats an acceleration of 0 as unset and ties it to the other side's,
so a 0 next to a non-zero side is written as :data:`ZERO_ACCEL` (same evaluation)."""
return ZERO_ACCEL if accel == 0.0 and other != 0.0 else accel
def build_keyframe(write: KeyWrite):
"""A fully explicit ``hou.Keyframe`` for a :class:`KeyWrite` (auto off, absolute accel)."""
k = hou.Keyframe()
k.setTime(write.time)
k.setValue(write.value)
# …
k.setSlopeAuto(False)
k.setInSlopeAuto(False)
k.setSlope(write.out_half.slope)
k.setInSlope(write.in_half.slope)
out_accel, in_accel = write.out_half.accel, write.in_half.accel
k.setAccel(_storable_accel(out_accel, in_accel))
k.setInAccel(_storable_accel(in_accel, out_accel))
k.interpretAccelAsRatio(False) # after setAccel, which would enable ratio mode
return kBreaking it down: auto tangents go off, so Houdini can’t recompute anything behind the panel’s back; a zero acceleration next to a real one becomes ZERO_ACCEL (the 1e-15 stand-in); and ratio mode gets switched off after setAccel, because setAccel is what switches it on.
The Whole Panel
Everything above in one place: every shape, the Ease editor, the Motion Stack, the recipes and the key tools. Under it, a few channels of a bouncing ball stand in for Houdini, so Get Keys, Commit Keys and the key tools have something to chew on. Select two keys of the ball’s height, hit Get Keys, shape the curve, and commit it back.
The interactive panel needs JavaScript.
What It Doesn’t Do
Houdini 20.0 is supported by construction but hasn’t been tested on a real install yet; 20.5, 21.0 and 22.0 have. The Channel Editor’s graph selection can’t be driven from a script, so some checks still need a person sitting in front of Houdini. Elastic stops at a damping of 0.7. And when the fitter can’t find fewer keys within tolerance, the commit gets refused instead of baked.
Sources and Links
- Fritsch and Carlson, Monotone Piecewise Cubic Interpolation (1980): the monotone cubic (PCHIP) the Motion Stack rebuilds sampled effects with, and the Hermite form behind Bounce’s exact parabolas.
- Press, Teukolsky, Vetterling and Flannery, Numerical Recipes:
rtsafe, the safeguarded Newton and bisection the curve solver follows. - Chris Wellons, Prospecting for Hash Functions (2018):
lowbias32, the hash behind Noise. - Donald Knuth, The Art of Computer Programming, volume 3: multiplicative (Fibonacci) hashing, the golden-ratio constant that spreads Noise’s lattice indices.