pmndrs / ecctrl

🕹️ A floating rigibody character controller
MIT License
565 stars 65 forks source link

Use Animations from an external FBX file? #40

Open chillbert opened 8 months ago

chillbert commented 8 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 8 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 😅

madspace1112 commented 6 months ago

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

prnthh commented 5 months 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;
};
nickaccent commented 1 month ago

feel free to remove this if you dislike me pointing out my own creation but it does not intefere or overwrite this in anyway.I just created and launched a threejs based site to do this, it is all browser with no backend. You can drag models (fbx, obj, glb or gltf) in, then drag all your animations in and click export and it will export a new glb with all the fbx anmations in it. you can use fbx as your starter model as well as fbx for your animations from mixamo etc.

As always if its animations based on bones etc you would probably want to drag the fbx to mixamo first then download the animations you want. then you can use my site to get them all into the main character (or multiple characters if they all have the same bone layout).

You can also use it for a batch fbx/obj to glb converter :)

https://meshamorphosis.co.uk