Skip to content

Play animation clips in the 3D viewer - #242

Open
OmarB97 wants to merge 1 commit into
lightningpixel:devfrom
OmarB97:feat/animated-model-playback
Open

Play animation clips in the 3D viewer#242
OmarB97 wants to merge 1 commit into
lightningpixel:devfrom
OmarB97:feat/animated-model-playback

Conversation

@OmarB97

@OmarB97 OmarB97 commented Jul 27, 2026

Copy link
Copy Markdown

What

The viewer creates an AnimationMixer for models that carry animation clips, ticks it from useFrame, and disposes it with the model. When a file has more than one clip, a small picker lets you switch between them.

Why

useGLTF already parses gltf.animations, but nothing ever plays them — a rigged model renders frozen in its bind pose. From the outside that looks like the rig failed, which is a confusing first experience of any rigging extension (UniRig, SkinTokens, or a hand-made rig). Three lines of state and a mixer make the model move.

Tested on Linux with GLB files carrying one clip and three clips (skinned mesh, 8-bone chain): single clips autoplay, multi-clip files default to the first and switch cleanly via the picker, and unloading a model stops and uncaches the mixer.

Notes

  • No new dependencies — AnimationMixer comes from the three already imported, and Html from the @react-three/drei already in use.
  • Models without clips take an early return, so the static path is unchanged.

🤖 Generated with Claude Code

Rigged models (from the rigging extensions, or any GLB with clips) render
frozen in bind pose today: the viewer loads the glTF but never creates an
AnimationMixer, so the clips it just parsed never run. That reads as a
failed rig rather than a missing feature.

Create a mixer for models that carry clips, tick it from useFrame, and
dispose it with the model. When a file carries more than one clip, offer a
small picker so each motion can be reviewed without leaving the app.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lightningpixel

Copy link
Copy Markdown
Owner

Thanks for tackling this — animated GLBs rendering frozen in bind pose is a real papercut, and the overall shape of the change (mixer per scene, useFrame tick, early return when there are no clips) is the right one.

Before anything else though: this branch doesn't build. Line 189 has a JSX expression with no right-hand operand:

<option key={clip.name || i} value={i}>{clip.name || }</option>

Reproduced on a fresh checkout of the PR head:

X [ERROR] Unexpected "}"

    src/areas/generate/components/Viewer3D.tsx:189:67:
      189 │ ...  <option key={clip.name || i} value={i}>{clip.name || }</option>
          ╵                                                           ^

Because it's a parse error rather than a runtime one, esbuild bails during transform, so npm run dev and npm run package both fail and the whole app fails to load — not just the viewer. Looks like a fallback (something like `Clip ${i + 1}`) got lost in an edit.

Worth double-checking npm run dev before the next push — the rest of the review is hard to act on while the branch can't run.

Three other things I noticed while reading, so they can go in the same round:

  • clipIndex never resets between models. <MeshModel> is rendered without a key and modelUrl flips straight from one done-job URL to the next, so React keeps GltfMeshModel mounted and the state survives. Go to a 3-clip model, pick clip 3, then switch to a 2-clip model: the mixer plays animations[Math.min(2, 1)] — the second clip, not the first as the PR describes — and <select value={2}> has no matching <option>, so the picker renders blank. A key={modelUrl} on MeshModel, or a reset effect on scene, fixes both.

  • key={clip.name || i} collides on duplicate names. Plenty of exporters emit several clips called Take 001 or mixamo.com. key={i} is safe here since the list is positional and never reordered.

  • The picker is anchored inside the model. <Html position={[0, 0, 0]}> with no center or screen-space anchoring projects the <select> at the model's origin, so it sits on top of the mesh and slides off-screen as you orbit. Also, wrapperClass="clip-picker" has no matching CSS anywhere in the repo, so that class is currently dead. This probably wants to live in the DOM overlay next to ViewerToolbar rather than in the 3D scene.

One thing that is correct and worth noting: the mixer teardown is fine — stopAllAction() goes through _deactivateActionbinding.restoreOriginalState(), so the scene returns to bind pose before the next mixer binds, and switching clips won't leave a stuck pose.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants