Switching between animations from different fbx files applied on glb

I have here a model.glb, and two different fbx animations without skin. I want to switch between the animations smoothly; here is a working prototype with abrupt animation switching:

function Model(props) {
  const { nodes, scene } = useGLTF(props.url);
  const wavingAction = useFBX("Waving.fbx");
  const wavingAnimation = wavingAction.animations;
  const talkingAction = useFBX("Talking.fbx");
  const talkingAnimation = talkingAction.animations;

  const mixer = useRef(new AnimationMixer());

  useFrame((state, delta) => mixer.current.update(delta));

  useEffect(() => {
    mixer.current.clipAction(talkingAnimation[0], scene).play();
  }, [])

  useEffect(() => {
    const unsubscribe = subscribe(proxy, () => {
      if (proxy.isTalking) {
        mixer.current.clipAction(talkingAnimation[0], scene).play();
        mixer.current.clipAction(wavingAnimation[0], scene).stop();
      } else {
        mixer.current.clipAction(wavingAnimation[0], scene).play();
        mixer.current.clipAction(talkingAnimation[0], scene).stop();
      }
    });
    return () => {
      unsubscribe();
    };
  }, [proxy]); // Make sure to pass the correct dependencies

  return <primitive {...props} object={scene} />;
}

So I replaced the state conditions like this:

      if (proxy.isTalking) {
        mixer.current.clipAction(talkingAnimation[0], scene).fadeIn(0.5).play();
        mixer.current.clipAction(wavingAnimation[0], scene).fadeOut(0.5).stop();
      } else {
        mixer.current.clipAction(wavingAnimation[0], scene).fadeIn(0.5).play();
        mixer.current.clipAction(talkingAnimation[0], scene).fadeOut(0.5).stop();
      }

But now it is animating once and then bouncing back to the reset (T-pose) form.

there is also the possiblity of crossFadeTo(action); but I think I need to rearange my code structure and use the mixer differently.

three fiber, drei is welcome; but I also appriciate basic three.js.

Whether I’m using r3f or vanilla, the general sequence I use is,

  • fadeOut the old action,
  • reset, fadeIn and play the new action.

E.g., going from idle to walking

actions['idle'].fadeOut(0.1)
actions['walk'].reset().fadeIn(0.1).play()

R3F example : Obstacle Course
Vanilla/TypeScript : Kick Boxing

In react you could also use this basic pattern (untested, use it as a guide)

  const [action, setAction] = useState()

  useEffect(() => {
    action && action.reset().fadeIn(0.1).play()
    return () => {
      action && action.fadeOut(0.1)
    }
  }, [action])

then some where else in your code call
setAction(some-preloaded-animationAction)

Also. if you open your FBX animations in blender, you can then export them as glB and the file sizes will be much smaller.

Don’t you need an animationmixer to handle the transition between the animations? I updated the component similar to your code; what I noticed is it is always going to the T-pose first and then from there transition to the next animation - shouldn’t it just fade into the next animation where ever the bones are at the moment when I say fadeout? - or does fadeout means: go to reset pose?

      if (proxy.isTalking) {
          // wavingClipAction.crossFadeTo(talkingClipAction, 1, true); not worked
          wavingClipAction.fadeOut(0.4)
          talkingClipAction.reset().fadeIn(0.1).play()
      } else {
        // talkingClipAction.crossFadeTo(wavingClipAction, 1, true); not worked
        talkingClipAction.fadeOut(0.4)
        wavingClipAction.reset().fadeIn(0.1).play()
      }

This here gives a better “transition” - at least it does not try to go to the T-pose:

      if (proxy.isTalking) {
          wavingClipAction.fadeOut(1)
          talkingClipAction.fadeIn(1).play()
          talkingClipAction.reset();
      } else {
        talkingClipAction.fadeOut(1)
        wavingClipAction.fadeIn(1).play()
        wavingClipAction.reset();
      }

what does reset() mean here? go back to the pose without animation applied? (T-pose in my case)

Hey, I have since tested what I suggested at the beginning, and it works.

Animation Controller : https://sbcode.net/react-three-fiber/animation-controller/
image

haha the fancy pose got me.
thx for the example! <3

hi, i’ve the same problem, with the same code as you
my code here : useAnimations (@react-fiber/drei) + NextJS14
But my prob is i’m with NextJS, then i don’t understand why it not works well

Thanks @seanwasere I was having this T-pose issue and your referance code helped me to resolve it. Here is my custom hook for reference.

import { useEffect, useMemo, useState } from "react";
import { useFBX } from "@react-three/drei";
import { AnimationMixer } from "three";
import { useFrame } from "@react-three/fiber";

const useCustomAnimation = (group, initialAnimationName = "Breathing") => {
  const angryAnimation = useFBX("animations/angry_point.fbx").animations;
  const laughingAnimation = useFBX("animations/laughing.fbx").animations;
  const breathingAnimation = useFBX("animations/breathing_Idle.fbx").animations;
  const greetingsAnimation = useFBX("animations/greetings.fbx").animations;
  const dancingAnimation = useFBX("animations/silly_dancing.fbx").animations;
  const surprisedAnimation = useFBX("animations/surprised.fbx").animations;
  const talking1Animation = useFBX("animations/talking_1.fbx").animations;
  const talking2Animation = useFBX("animations/talking_2.fbx").animations;
  const talking3Animation = useFBX("animations/talking_3.fbx").animations;
  const tauntAnimation = useFBX("animations/taunt.fbx").animations;

  const actions = useMemo(() => [], []);
  const mixer = useMemo(() => new AnimationMixer(), []);
  const [currentAnimation, setCurrentAnimation] = useState(
    actions[initialAnimationName]
  );

  useEffect(() => {
    actions["Angry"] = mixer.clipAction(angryAnimation[0], group.current);
    actions["Laughing"] = mixer.clipAction(laughingAnimation[0], group.current);
    actions["Breathing"] = mixer.clipAction(breathingAnimation[0] ,group.current);
    actions["Greetings"] = mixer.clipAction(greetingsAnimation[0], group.current);
    actions["Dancing"] = mixer.clipAction(dancingAnimation[0], group.current);
    actions["Surprised"] = mixer.clipAction(surprisedAnimation[0], group.current);
    actions["Talking_1"] = mixer.clipAction(talking1Animation[0], group.current);
    actions["Talking_2"] = mixer.clipAction(talking2Animation[0], group.current);
    actions["Talking_3"] = mixer.clipAction(talking3Animation[0], group.current);
    actions["Taunt"] = mixer.clipAction(tauntAnimation[0], group.current);
    
    const initialAction = actions[initialAnimationName] || actions["Greetings"]
    setCurrentAnimation(initialAction)
    initialAction.play()
  }, []);

  useFrame((_, delta) => {
    // Update the mixer with the time delt
    mixer.update(delta);
  });

  useEffect(() => {
    currentAnimation?.reset().fadeIn(0.5).play();
    return () => {
      currentAnimation?.fadeOut(0.5);
    };
  }, [currentAnimation]);

  const changeAnimation = (animationName) => {
    console.log("Changing animation to", animationName);
    setCurrentAnimation(actions[animationName]);
  };

  return { actions, changeAnimation };
};

export default useCustomAnimation;