pmndrs / ecctrl

🕹️ A floating rigibody character controller
MIT License
460 stars 49 forks source link

Use Animations from an external FBX file? #40

Open chillbert opened 3 months ago

chillbert commented 3 months ago

My setup is usually so that I have a character.glb file and then lots of different animations from one or multiple fbx files, which I apply on the character like this:

(this setup makes totally sense when you have multiple characters in a multiplayer game with small size and have only once to load all the animations for all players (since they are all the same right?):

 const {scene}= useGLTF("character.glb");
  const walkingFbx = useFBX("walking.fbx");
  const { actions } = useAnimations(walkingFbx.animations, characterRef);

  useEffect(() => {
    actions?.[Object.keys(actions)[0]]?.play();
  }, [actions]);
//..
 <primitive ref={characterRef} object={scene} />

how would I achieve this in this case:

  /**
   * Character url preset
   */
  const characterURL = './Demon.glb'

  /**
   * Character animation set preset
   */
  const animationSet = {
    idle: 'CharacterArmature|Idle',
    walk: 'CharacterArmature|Walk',
    run: 'CharacterArmature|Run',
    jump: 'CharacterArmature|Jump',
    jumpIdle: 'CharacterArmature|Jump_Idle',
    jumpLand: 'CharacterArmature|Jump_Land',
    fall: 'CharacterArmature|Duck', // This is for falling from high sky
    action1: 'CharacterArmature|Wave',
    action2: 'CharacterArmature|Death',
    action3: 'CharacterArmature|HitReact',
    action4: 'CharacterArmature|Punch'
  }
//...

 <EcctrlAnimation characterURL={characterURL} animationSet={animationSet}>
                  <CharacterModel />
 </EcctrlAnimation>

Do I need to manipulte the EcctrlAnimation.tsx by myself?


export function EcctrlAnimation(props: EcctrlAnimationProps) {
  // Change the character src to yours
  const group = useRef();
   const animations= useFBX("allmyAnimationsIn1FBX.fbx"); //here?
  const { actions } = useAnimations(animations, group);
ErdongChen-Andrew commented 3 months ago

Unfortunately, at the moment, you will need to combine the animations to your character.glb to make it work. I tried to create a featrue using external fbx animations before, but the transition in between was really bad. I will get back to this feature maybe latter on 😅

metaverse1112 commented 1 month ago

So, how to increase the animation amount? e.g. action1, action2 ... action8

prnthh commented 3 weeks ago

I was able to add support for loading external fbx animations in my project.

Here is the updated EcctrlAnimation.tsx file:

import { useEffect, useRef, Suspense, useState, useMemo } from "react";
import * as THREE from "three";
import { useGame, type AnimationSet } from "./stores/useGame";
import React from "react";
import { useFrame, useLoader } from "@react-three/fiber";
import { FBXLoader } from "three-stdlib";

export const ANIMATIONS = {
  idle: '/resources/animation/Happy.fbx',
  walk: '/resources/animation/Happy Walk.fbx',
  run: '/resources/animation/Fast Run.fbx',
  dancing: '/resources/animation/Silly Dancing.fbx',
  sad: '/resources/animation/Sad Idle.fbx',
  excited: '/resources/animation/Excited.fbx',
  point: '/resources/animation/Angry Point.fbx',
  clap: '/resources/animation/Clapping.fbx',
  rally: '/resources/animation/Rallying.fbx',
  thankful: '/resources/animation/Thankful.fbx',
  jump: '/resources/animation/Jump.fbx',
  tpose: '/resources/animation/T-Pose.fbx',
  sleep: '/resources/animation/Sleep.fbx',
  eating: '/resources/animation/eating.fbx',
};

export function EcctrlAnimation(props: EcctrlAnimationProps) {
  const [mixer, setMixer] = useState<THREE.AnimationMixer | null>(null);
  // Change the character src to yours
  const group = useRef();
  // const { animations } = useGLTF(props.characterURL);
  const animations = useLoader(FBXLoader, Object.values(ANIMATIONS)).map(f => f.animations[0]);

  const actions = useMemo(() => mixer ? Object.keys(ANIMATIONS).reduce<{ [key: string]: THREE.AnimationAction }>((acc, key, index) => {
    acc[key] = mixer.clipAction(animations[index], props.character);
    return acc;
}, {}) : {}, [mixer, animations]);

useFrame((_, delta) => {
  mixer?.update(delta);
  // TWEEN.update();
});

useEffect(() => {
    if (!props.character) return;
    const newMixer = new THREE.AnimationMixer(props.character);
    setMixer(newMixer);

    return () => {
        newMixer.stopAllAction();
        newMixer.uncacheRoot(newMixer.getRoot());
    };
}, [props.character]);

  /**
   * Character animations setup
   */
  const curAnimation = useGame((state) => state.curAnimation);
  const resetAnimation = useGame((state) => state.reset);
  const initializeAnimationSet = useGame(
    (state) => state.initializeAnimationSet
  );

  useEffect(() => {
    // Initialize animation set
    initializeAnimationSet(props.animationSet);
  }, [actions]);

  useEffect(() => {
    // Play animation
    const action =
      actions[curAnimation ? curAnimation : props.animationSet.jumpIdle];
      if(!action) return;

    // For jump and jump land animation, only play once and clamp when finish
    if (
      curAnimation === props.animationSet.jump ||
      curAnimation === props.animationSet.jumpLand ||
      curAnimation === props.animationSet.action1 ||
      curAnimation === props.animationSet.action2 ||
      curAnimation === props.animationSet.action3 ||
      curAnimation === props.animationSet.action4
    ) {
      action
        .reset()
        .fadeIn(0.2)
        .setLoop(THREE.LoopOnce, undefined as number)
        .play();
      action.clampWhenFinished = true;
    } else {
      action?.reset().fadeIn(0.2).play();
    }

    // When any action is clamp and finished reset animation
    mixer?.addEventListener("finished", () => resetAnimation());

    return () => {
      // Fade out previous action
      action.fadeOut(0.2);

      // Clean up mixer listener, and empty the _listeners array
      // (action as any)._mixer.removeEventListener("finished", () =>
      //   resetAnimation()
      // );
      // (action as any)._mixer._listeners = [];
    };
  }, [curAnimation]);

  return (
    <Suspense fallback={null}>
      <group ref={group} dispose={null} userData={{ camExcludeCollision: true }}>
        {/* Replace character model here */}
        {props.children}
      </group>
    </Suspense>
  );
}

export type EcctrlAnimationProps = {
  character: any;
  animationSet: AnimationSet;
  children: React.ReactNode;
};