vibespire.ai / Puppet Theater /

how it's made

enter the theater β†—

Vibespire presents

How the Puppet Theater Is Made

There is no animation data in this project. No keyframes, no clips, no motion capture, nothing baked. Every frame you see is solved from scratch. Here is what replaces it.

The two demos below import the engine's own classes.

The shape of it

Four layers, one direction of travel

Each layer sets targets for the layer below it and then leaves it alone. Nothing reaches down two levels, and nothing reaches back up.

Layer 0

Dynamics & rig

One spring class, two-bone IK, and the parts list that says where each pivot goes. Knows nothing about puppets.

src/core Β· src/rig Β· src/render

Layer 1

Motor skills

Standing, breathing, blinking, stepping, reaching, looking. Takes orders like β€œwalk to x” and β€œlook at that.”

src/puppet

Layer 2

Affect

A psychological state that compiles, every frame, into a bundle of multipliers the layer below applies to everything it does.

src/affect

Layer 3

Score

A list of timed beats β€” intentions with timestamps. It never touches a joint angle.

src/score

The constraint that makes this work: no layer can express a pose. The only vocabulary going downward is targets and tuning.

The rig contract

A puppet is a parts list

Each part declares four things: a frame size, a pivot (the point that sits on its parent's joint), a tip (the point its own child hangs from), and whether to paint a rivet over the pivot.

That is the whole interface between the art and the engine. The renderer takes a live bone — two joint positions the simulation just produced — and rotates and scales the frame so that its pivot→tip segment lands exactly on that bone. Which means one motion library drives art of any proportions: give a character longer forearms and the same reach behaviour still works, because nothing in the motion code knows a forearm's length until it reads it off the rig.

Bone angles are always derived with atan2 from two joint positions, never stored. There is nowhere in the codebase to put a stored rotation, which is what keeps a pose from sneaking in.

A slot is one z-ordered appearance of a part, with a side flag: -1 draws the far limb tinted darker, +1 the near one. Both slots point at the same frame β€” a puppet has two arms and one arm drawing. Mouths are a set of nine named shapes, deliberately coarse.

Rivets are not decoration. A painted cap over the pivot is the cut-out animator's oldest trick for hiding the gap a rotating joint opens.

CHARACTER SHEET head Γ—3 angles torso arm pieces Γ—2 + hands leg pieces Γ—2 + feet mouths, one per sound ASSEMBLED gold dots = pins at the joints; extra paper hides inside each one
The rig names the parts and the pins. Costume and proportion vary per actor; the engine reads the same fields either way.

Layer 0 Β· Dynamics

One equation does all the moving

Every animated quantity in the theater β€” joint angles, hand targets, pupil offsets, breath, the mood values themselves β€” is a value chasing a target through the same second-order system:

y + k₁·ẏ + kβ‚‚Β·ΓΏ = x + k₃·ẋ

You never set k₁, kβ‚‚, k₃. They are computed from three parameters that mean something to a person:

f β€” natural frequency in Hz. How quickly it responds.
z β€” damping ratio. Below 1 it overshoots and rings; at 1 it settles cleanly; above 1 it drags in slowly.
r β€” initial response. At 0 it eases off the mark, at 1 it leaves immediately, above 1 it overshoots on departure, and below zero it winds up backwards first β€” anticipation, for free, from a negative number.

The overshoot-and-settle that z < 1 produces is most of what reads as β€œalive,” and you get it without authoring anything. Try the knobs: the dashed line is the target, the gold line is the system's output.

Two details that matter in practice. Integrated naively, a stiff spring blows up as soon as the frame time gets long relative to its period β€” and a stiff spring is exactly what you want for eyes. So the step checks which regime it is in and either clamps kβ‚‚ or switches to pole-zero matching, which keeps a fast system stable on a slow frame instead of throwing the puppet across the stage. And angles use a variant that unwraps the target first, so a head turning from +170Β° to βˆ’170Β° goes the short way round rather than spinning through zero.

The engine's actual SecondOrderScalar, imported by this page.

Layer 1 Β· Gait

Walking with no walk cycle

There is no step animation. A foot is planted at a fixed world position and simply stays there while the body moves over it.

Each foot has a home point β€” a hip offset hanging off the sprung body. When the distance between the planted foot and its drifting home point crosses a threshold, the foot notices it has been left behind and swings. It swings along an arc to a predicted landing spot: where the home point will be, capped so a foot can't reach implausibly far ahead. On landing it fires an impulse, and the body's springs turn that into a bob, a squash and a small lean. The bounce in the walk is a consequence of the landing, not a curve someone drew.

A coordinator stops both feet swinging at once β€” usually. Above roughly 55% of top speed it permits an overlap once the other foot is past 62% of its swing, and a flight phase appears on its own. Nobody implemented running.

The threshold is scaled by speed, which is the whole trick: walk slowly and you get short shuffling steps, speed up and the stride opens, all from one number.

Hands on

Try it

Move the pointer across the stage β€” the puppet reaches for the lantern. Then change the mood. The reaching rule is identical in all four cases; only the tuning of the springs changes.

Easy springs, a level bar, a settled gaze.

Layer 2 Β· Affect

Mood is a set of multipliers

You just felt what this layer does. It does not select a different animation, because there are none to select. It retunes the springs, and every behaviour built on them changes character at once.

The state has seven continuous values: valence (miseryβ†’joy), arousal (torporβ†’alarm), dominance (cowedβ†’commanding), and four effort qualities borrowed from Laban notation β€” sustained↔sudden, light↔strong, bound↔free, indirect↔direct. Each of the seven runs through its own slow spring β€” 0.35 Hz by default, retuned when a beat asks for a faster or slower transition β€” so a mood arrives over a second or two instead of being switched on. Transitions get shaped by the same machinery as everything else.

Every frame those seven values compile into a bundle of about twenty numbers. Four of them are the ones that matter most, and the mapping comes from the EMOTE model: effort Time drives spring frequency and initial response, Flow drives damping, Weight drives amplitude, and Space drives path curvature. The rest set gait tempo and stride, posture, gaze directness, dart rate, blink and breath rates, brow height and angle, lid opening, and the resting mouth shape.

A worked example. Fear is stored as valence βˆ’0.7, arousal 0.8, dominance βˆ’0.7, and as sudden, light, bound and indirect. High arousal and suddenness raise the frequency multiplier to about 1.5Γ— neutral. Bound flow raises damping too β€” which is the interesting part, because it means a frightened puppet is not wobbly. It is fast and clipped: it gets to a target quickly and stops dead, the way tense people move. The twitchiness comes from somewhere else entirely, a dart rate that goes from zero to 1.6 look-aways a second. Meanwhile stride shortens to roughly half, gaze directness falls from 0.7 to zero, and blink and breath rates roughly double. Give that puppet and a weary one the same instruction β€” β€œwalk to the window” β€” and you get two performances out of one code path.

f, z, r multipliershow fast, how wobbly, how eager every motion is
Amplitude & curvaturehow large the gesture, how bent its path
Posture & gaze policyspine, head bias, how directly a look is held
Autonomic ratesblink, breath, weight-shift and dart frequencies

The face is built on a bet from the design note: that brows carry around seventy per cent of the readable emotion. Hence two rotated line segments doing more work than the mouth does.

Layer 1 Β· Gestures and gaze

Two places a spring isn't enough

Springs get you most of the way. Two behaviours needed something extra, and both are worth knowing about.

A spring cannot curve. Pull a value toward a target and it travels in a straight line. Real gestures arc. So a gesture is a generator with a four-part phase envelope β€” anticipation, stroke, hold, release-with-settle β€” and the Laban Space quality bends the stroke by inserting a lateral waypoint partway along it. An indirect, wandering mood gets a curved reach; a direct one gets a straight one. Seven gestures share that one envelope, which is where the classical animation principles live: once, in the envelope, rather than in seven hand-drawn curves.

Eyes and heads move at different speeds. The pupils get a stiff, nearly ballistic spring; the head gets a much slower one. Then one detail does something disproportionate: the pupil target is recomputed against the head's current rotation every frame. Because the head is still catching up, the eyes automatically counter-rotate to stay locked on the target β€” vestibulo-ocular reflex, which nobody wrote. Idle attention wanders on Poisson clocks, micro-saccades jitter the fixation, and a large shift triggers a blink, because real eyes blink when they jump.

Breathing, blinking, weight shifts and idle sway run whether or not anything is happening. A standing puppet that is perfectly still reads as dead in about half a second.

Layer 3 Β· Score

The script says what, never how

A score is a list of beats with timestamps. Nine verbs, and not one of them mentions a body part:

{ t:  5.6, who: "josef", do: "say",     text: "Marta." }
{ t:  6.1, who: "marta", do: "react",   kind: "flinch" }
{ t:  6.3, who: "marta", do: "emote",   state: "fear" }
{ t:  7.0, who: "marta", do: "look-at", target: "josef" }
{ t:  7.6, who: "marta", do: "say",
           text: "You came *back.*" }
{ t: 15.2, who: "marta", do: "gesture", kind: "arms-fold" }

Asterisks mark stress. When a say beat fires, the line is split into words and each gets a slot: the whole line lasts 0.9 + 0.34 Γ— words seconds, divided evenly. Each word's syllable count is estimated from its vowels, and it flaps through up to three shapes, drawn from a pool of five, as its slot elapses. The jaw rides a sine over the slot, its swing scaled by 1.25 on a stressed word against 0.85 on an unstressed one.

A stressed word also triggers an emphasis accent β€” a nod and a brow flick β€” and one time in three a beat gesture, if the hands are free. That alignment between the accent and the stressed syllable is most of what makes a mouth flapping at a caption read as talking.

A look-at target can be a stage mark, the pointer, or another actor's name. Naming an actor resolves to a function that returns their head position, so the gaze layer tracks a moving target with no extra machinery.

Half of a two-hander is the puppet who isn't speaking, and none of that is in the script.

When a line begins, everyone else turns to the speaker β€” 90% of the time, so it doesn't look mechanical. On each stressed word, each listener has a 45% chance of nodding, scheduled 0.15 to 0.4 seconds late. That lag is the point: a listener who nods exactly on the beat looks like a metronome, and one who nods slightly after looks like they are following the sentence.

Every 0.9 to 1.6 seconds a listener re-decides where to look, with the probability of breaking eye contact taken from their own mood: 0.1 + (1 βˆ’ gazeDirect) Γ— 0.4. A confident listener holds your eye; a frightened one keeps glancing away mid-sentence. Nobody scripted either.

This is why the layering earns its keep. The listener behaviour is three blocks totalling about forty lines, and it works for any script, any mood and any cast, because it only ever issues orders the layer below already knows how to carry out.

The bill

What this approach costs

Solving motion instead of storing it buys generality and gives up control. Some of it is a fair trade and some of it is just the cost.

You cannot author an exact pose. Springs approach their targets asymptotically, so there is no frame on which a hand is guaranteed to be at a particular point. If a scene needed a silhouette to land precisely on a musical cue, this engine is the wrong tool.

Elbows and knees can't invert. Two-bone IK by the law of cosines has two mirror solutions, and each limb picks one at build time with a sign. It gives correct joints and no way to reverse a bend for effect.

The mouth is a heuristic, not phonetics. Syllables come from counting vowels and scaling by 0.8. There is no phoneme model, and the shape set is nine wide on purpose β€” at this scale a coarse flap that hits the right rhythm reads better than an accurate one that doesn't.

Stress is hand-marked. The asterisks in the script are written by a person. Nothing infers which word in a sentence carries the emphasis, and everything that makes speech look intentional hangs off getting them right.

The trade in one line: the engine stores no motion and cannot reproduce a specific movement twice, and in exchange every character it is given can do everything, in any mood, on the first try.

Enter the theater β†—

Marcin β€” creator of vibespire.ai

About

Hi, I'm Marcin.

I build these experiments whenever something sparks my curiosity β€” a paper, a game, the way grass bends in the wind, the smallest everyday things. Inspiration shows up, and I chase it into a little living world you can open in a browser.

vibespire is where those experiments live. Poke at them, break them, read how they're made β€” and if something sparks an idea for you too, say hello.