From ceebd8eaed08aecab531099fc38494ea3eb40d3c Mon Sep 17 00:00:00 2001 From: Omar Baradei Date: Mon, 27 Jul 2026 15:52:21 -0700 Subject: [PATCH] Play animation clips in the 3D viewer 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 --- src/areas/generate/components/Viewer3D.tsx | 46 ++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/src/areas/generate/components/Viewer3D.tsx b/src/areas/generate/components/Viewer3D.tsx index 8cff822f..9318c212 100644 --- a/src/areas/generate/components/Viewer3D.tsx +++ b/src/areas/generate/components/Viewer3D.tsx @@ -2,7 +2,7 @@ import { Component, Suspense, useCallback, useEffect, useMemo, useRef, useState import type { ReactNode, ErrorInfo, MutableRefObject } from 'react' import { Canvas, useFrame, useLoader, useThree } from '@react-three/fiber' import type { ThreeEvent } from '@react-three/fiber' -import { Environment, GizmoHelper, Lightformer, OrbitControls, useGizmoContext, useGLTF } from '@react-three/drei' +import { Environment, GizmoHelper, Html, Lightformer, OrbitControls, useGizmoContext, useGLTF } from '@react-three/drei' import { EffectComposer, Outline, Select, Selection } from '@react-three/postprocessing' import * as THREE from 'three' import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js' @@ -151,8 +151,48 @@ function MeshModel({ url, jobId, viewMode, selected, onStats, onSelect, onObject } function GltfMeshModel(props: MeshModelProps): JSX.Element { - const { scene } = useGLTF(props.url) - return + const gltf = useGLTF(props.url) + const { scene, animations } = gltf + const mixerRef = useRef(null) + const [clipIndex, setClipIndex] = useState(0) + + // Rigged models arrive with one or more clips; without a mixer they render + // frozen in bind pose, which reads as "the rig failed". + useEffect(() => { + if (!animations?.length) { + mixerRef.current = null + return + } + const mixer = new THREE.AnimationMixer(scene) + mixer.clipAction(animations[Math.min(clipIndex, animations.length - 1)]).play() + mixerRef.current = mixer + return () => { + mixer.stopAllAction() + mixer.uncacheRoot(scene) + mixerRef.current = null + } + }, [scene, animations, clipIndex]) + + useFrame((_state, delta) => mixerRef.current?.update(delta)) + + return ( + <> + + {animations && animations.length > 1 && ( + + + + )} + + ) } function ObjMeshModel(props: MeshModelProps): JSX.Element {