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.

1 Like

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;
1 Like