Skip to content
Docs
Open Promoeyes
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