animated-states
animated-states
Skinned rigs — the three.js animation system authored as keyframes.
Read the rig's notes
s1 · RobotExpressive walks the WHOLE state machine inside ONE scene:
Idle → Walking → Running → Dance → Sitting → Standing → Idle, with
one-shot emotes fired over the top and expressions running against
the body the entire time.
s2 · Soldier ×3 — the multiple-skinned-meshes demo: three clones of one
loaded GLB, each on its own skeleton and its own state, then all
three re-bound to a SHARED skeleton mid-scene, locking into step.
s3 · REFERENCE — every `state` value form exercised on a real rig.
Both source demos render on light grey (0xe0e0e0 / 0xa0a0a0), so the
mandatory haze rides as PIGMENT lobes (normal blend), never additive.
THE `state` CHANNEL — the only thing that drives a GLB.
Row: TIME [EASING] VALUE [BLEND] [hold]
VALUE: Angry — every morph target AND any clip so named
morphs.Angry — every morph target so named
animations.Angry — only a clip so named
Head_2.Angry — only that mesh's target
Walking:0.5 — explicit weight (bare name = 1, :0 = off)
2 / morphs.1 / animations.2 / Head_2.1 — the same four, by index
A row STARTS a value and it runs until displaced. Clips displace clips
(crossfading over BLEND); morph values do NOT displace clips, they
override them — every RobotExpressive clip also writes the Head mesh's
weights, so an expression has to layer over the body rather than replace
it, exactly as the source demo's sliders write morphTargetInfluences
after the mixer runs. `hold` clamps the last frame instead of looping.
Everything evaluates from absolute scene-local time, so export can seek
to any frame and get the same result it would by playing there.
Ground truth from the GLBs (dumped, not guessed):
RobotExpressive clips — Dance Death Idle Jump No Punch Running Sitting
Standing ThumbsUp Walking WalkJump Wave Yes
RobotExpressive morphs — Angry Surprised Sad, and THERE IS NO SMILE; the
demo reads these from extras.targetNames rather than naming them, which
is why they have to be read off the file. The glTF calls the mesh
"Head" and splits it into three primitives, which three then uniquifies
to Head_2 / Head_3 / Head_4 — so the mesh-scoped form's names come from
the LOADER, not the file. The loader logs the whole inventory (clips
with durations, every morph mesh and its targets) on first load.
Soldier clips — Idle Run TPose Walk (the demo's animations[0]/[1]/[3];
index 2 is the T-pose it skips). No morph targets.
Preview: /packages/runtime/index.html?project=promos/new/skinned-rigs/skinned-rigs.promo- Scenes
- 3
- Kind
- reference rig
//
Skinned rigs — the three.js animation system authored as keyframes.
s1 · RobotExpressive walks the WHOLE state machine inside ONE scene:
Idle → Walking → Running → Dance → Sitting → Standing → Idle, with
one-shot emotes fired over the top and expressions running against
the body the entire time.
s2 · Soldier ×3 — the multiple-skinned-meshes demo: three clones of one
loaded GLB, each on its own skeleton and its own state, then all
three re-bound to a SHARED skeleton mid-scene, locking into step.
s3 · REFERENCE — every `state` value form exercised on a real rig.
Both source demos render on light grey (0xe0e0e0 / 0xa0a0a0), so the
mandatory haze rides as PIGMENT lobes (normal blend), never additive.
THE `state` CHANNEL — the only thing that drives a GLB.
Row: TIME [EASING] VALUE [BLEND] [hold]
VALUE: Angry — every morph target AND any clip so named
morphs.Angry — every morph target so named
animations.Angry — only a clip so named
Head_2.Angry — only that mesh's target
Walking:0.5 — explicit weight (bare name = 1, :0 = off)
2 / morphs.1 / animations.2 / Head_2.1 — the same four, by index
A row STARTS a value and it runs until displaced. Clips displace clips
(crossfading over BLEND); morph values do NOT displace clips, they
override them — every RobotExpressive clip also writes the Head mesh's
weights, so an expression has to layer over the body rather than replace
it, exactly as the source demo's sliders write morphTargetInfluences
after the mixer runs. `hold` clamps the last frame instead of looping.
Everything evaluates from absolute scene-local time, so export can seek
to any frame and get the same result it would by playing there.
Ground truth from the GLBs (dumped, not guessed):
RobotExpressive clips — Dance Death Idle Jump No Punch Running Sitting
Standing ThumbsUp Walking WalkJump Wave Yes
RobotExpressive morphs — Angry Surprised Sad, and THERE IS NO SMILE; the
demo reads these from extras.targetNames rather than naming them, which
is why they have to be read off the file. The glTF calls the mesh
"Head" and splits it into three primitives, which three then uniquifies
to Head_2 / Head_3 / Head_4 — so the mesh-scoped form's names come from
the LOADER, not the file. The loader logs the whole inventory (clips
with durations, every morph mesh and its targets) on first load.
Soldier clips — Idle Run TPose Walk (the demo's animations[0]/[1]/[3];
index 2 is the T-pose it skips). No morph targets.
Preview: /packages/runtime/index.html?project=promos/new/skinned-rigs/skinned-rigs.promo
promo#skinned-rigs
// =======================================================================
// SCENE 01 · THE STATE MACHINE — seven states, one take, no cuts
// =======================================================================
scene#s1-states(duration=26 transition=whip-pan transition-duration=0.7 transition-accent=#2fa1d6)
// Distances are chosen against the rig's real height (2.0 m — see the
// asset's `scale` below): at fov 45 the frame is 0.828·d tall, so these
// runs hold the robot between ~34% (wide) and ~56% (the emote push-in).
camera(type=perspective fov=45 near=8.5 far=200000 position="-2900 1700 6100" look_at="0 1000 0")
position
0 -2465 1445 5185
3.5 easeInOutSine -1870 1275 4760
7.5 easeInOutSine 765 1105 4845
11 easeInOutCubic 2465 1445 3570
15 easeInOutSine 1785 1785 3315
18.5 easeInOutSine 935 1445 3740
21.5 easeInOutCubic 190 1425 3800
26 easeInOutSine -1275 1615 4760
look_at
0 0 1000 0
7.5 easeInOutSine 0 950 0
15 easeInOutSine 0 600 0
18.5 easeInOutSine 0 900 0