Three.js how to add multiple Mixamo animations without skin to an FBX model?

2.9k views Asked by At

I'm trying to create a game where there is an animated character that has multiple animations from Mixamo. I want the animation to change based on what the user is doing on the game, like Walking, Running, or Idle. Here is how I'm loading the FBX model (without animations):

loader.load('Assets/Animations/Main.fbx', function(object){
    object.traverse(function (child){
        if (child.isMesh) {
           child.castShadow = true;
           child.receiveShadow = true;
           child.frustumCulled = false;
        }
   });

   object.rotation.x = Math.PI / 2
   object.position.x = 11;
   scene.add(object);
});

I also have the following files, which are animations without skin.

Idle.fbx
Walking.fbx
Running.fbx

My goal is to try to make something like this or like this. The only problem with these 2 links is that in the first one, they are using a model with multiple animations attached to it (I have a plain model with 3 animations without skin), and in the second one, the code is written using TypeScript (I prefer JavaScript).

I am a newbie to 3D modelling, so I don't know how to attach all the animations without skin to the main fbx model. How can I combine the animations to one model in Blender, or is there a way to do it in three.js?

I appreciate any help with this, thanks!


EDIT:

According to @GuyNachshon, is this how I should handle this?

So first I load the model without animations (yourMesh), and also create an AnimationMixer:

var mixer;

loader.load('Assets/Animations/Main.fbx', function(object){
    object.traverse(function (child){
        if (child.isMesh) {
           child.castShadow = true;
           child.receiveShadow = true;
           child.frustumCulled = false;
        }
   });

   mixer = new THREE.AnimationMixer(object);
   object.rotation.x = Math.PI / 2
   object.position.x = 11;
   scene.add(object);
});

Then, I have to load the 3 animations files without skin and add them to animationsArray. (Not sure if I'm loading the animations correctly...):

loader.load('Assets/Animations/Idle.fbx', function(object){
    object.traverse(function (child){
        if (child.isMesh) {
           child.castShadow = true;
           child.receiveShadow = true;
           child.frustumCulled = false;
        }
   });

   object.rotation.x = Math.PI / 2
   object.position.x = 11;

   animationsArray.push(object);
   scene.add(object);
});

loader.load('Assets/Animations/Walking.fbx', function(object){
    object.traverse(function (child){
        if (child.isMesh) {
           child.castShadow = true;
           child.receiveShadow = true;
           child.frustumCulled = false;
        }
   });

   object.rotation.x = Math.PI / 2
   object.position.x = 11;

   animationsArray.push(object);
   scene.add(object);
});

loader.load('Assets/Animations/Running.fbx', function(object){
    object.traverse(function (child){
        if (child.isMesh) {
           child.castShadow = true;
           child.receiveShadow = true;
           child.frustumCulled = false;
        }
   });

   object.rotation.x = Math.PI / 2
   object.position.x = 11;

   animationsArray.push(object);
   scene.add(object);
});

After everything has loaded completely, I create the actions:

let actions = mixer.clipAction(animationsArray).play();

But, after you say to do:

actions.play();

What is that line going to play? Is it going to play the first animation in animationsArray?

1

There are 1 answers

13
Guy Nachshon On BEST ANSWER

you need to create an AnimationMixer.

so let's say you have created a scene, added a mesh etc. now you can init an animation mixer

let mixer = new THREE.AnimationMixer(yourMesh);

then to add animations use clipActions,

let actions = mixer.clipAction(animationsArray).play();

now play

actions.play();

but to really know how to use it you should read the docs (attached above :) )


Edit - responding to your edit

In order to control what animation will play you can do several things, here is an example from the docs:

const mixer = new THREE.AnimationMixer( mesh );
const clips = mesh.animations;

// Update the mixer on each frame
function update () {
    mixer.update( deltaSeconds );
}

// Play a specific animation
const clip = THREE.AnimationClip.findByName( clips, 'dance' );
const action = mixer.clipAction( clip );
action.play();

// Play all animations
clips.forEach( function ( clip ) {
    mixer.clipAction( clip ).play();
} );

now, if you are having trouble with structuring your code, here is a general example regarding how to attach animations to fbx and control them:

let mixer = THREE.AnimationMixer
let modelReady = false
const animationActions = THREE.AnimationAction
let activeAction = THREE.AnimationAction
let lastAction = THREE.AnimationAction
const fbxLoader = new FBXLoader()

after we initiated everything we need, let's load everything:

fbxLoader.load(
    (object) => {
        'path/to/your/model.fbx',
        object.scale.set(0.01, 0.01, 0.01)
        mixer = new THREE.AnimationMixer(object)

        const animationAction = mixer.clipAction(animations[0])
        animationActions.push(animationAction)
        animationsFolder.add(animations, 'default')
        activeAction = animationActions[0]  // sets current animation

        scene.add(object)  // adds animated object to your scene

        //add an animation from another file
        fbxLoader.load(
            'path/to/animation.fbx',
            (object) => {
                console.log('loaded animation')

                const animationAction = mixer.clipAction(.animations[0])
                animationActions.push(animationAction)
                animationsFolder.add(animations, 'animationName')

                //add an animation from another file
                fbxLoader.load(
                    'path/to/other/animation.fbx',
                    (object) => {
                        console.log('loaded second animation')
                        const animationAction = mixer.clipAction(animations[0])
                        animationActions.push(animationAction)
                        animationsFolder.add(animations, 'animationName')

                        //add an animation from another file
                        fbxLoader.load(
                            'path/to/animation.fbx',
                            (object) => {
                                console.log('loaded third animation');
                                const animationAction = mixer.clipAction(animations[0])
                                animationActions.push(animationAction)
                                animationsFolder.add(animations, 'animationName')

                                modelReady = true
                            },
                            (xhr) => {
                                console.log(
                                    (xhr.loaded / xhr.total) * 100 + '% loaded'
                                )
                            },
                            (error) => {
                                console.log(error)
                            }
                        )
                    },
                    (xhr) => {
                        console.log((xhr.loaded / xhr.total) * 100 + '% loaded')
                    },
                    (error) => {
                        console.log(error)
                    }
                )
            },
            (xhr) => {
                console.log((xhr.loaded / xhr.total) * 100 + '% loaded')
            },
            (error) => {
                console.log(error)
            }
        )
    },
    (xhr) => {
        console.log((xhr.loaded / xhr.total) * 100 + '% loaded')
    },
    (error) => {
        console.log(error)
    }
)

now we should set what our animations and actions are:

const animations = {
   function default() {
        setAction(animationActions[0])
    },
    function firstAnimation() {
        setAction(animationActions[1])
    },
   function sceondAnimation() {
        setAction(animationActions[2])
    },
   function thirdAnimation() {
        setAction(animationActions[3])
    }
}

const setAction = {
    if (toAction != activeAction) {
        lastAction = activeAction
        activeAction = toAction
        //lastAction.stop()
        lastAction.fadeOut(1)
        activeAction.reset()
        activeAction.fadeIn(1)
        activeAction.play()
    }
}

lets animate!

const clock = new THREE.Clock()

function animate() {
    requestAnimationFrame(animate)

    controls.update()

    if (modelReady) {mixer.update(clock.getDelta())}

    render()

}

function render() {
    renderer.render(scene, camera)
}

animate()

putting it all together:

import * as THREE from 'three'

import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'


let mixer = THREE.AnimationMixer
    let modelReady = false
    const animationActions = THREE.AnimationAction
    let activeAction = THREE.AnimationAction
    let lastAction = THREE.AnimationAction
    const fbxLoader = new FBXLoader()

fbxLoader.load(
        (object) => {
            'path/to/your/model.fbx',
            object.scale.set(0.01, 0.01, 0.01)
            mixer = new THREE.AnimationMixer(object)
    
            const animationAction = mixer.clipAction(animations[0])
            animationActions.push(animationAction)
            animationsFolder.add(animations, 'default')
            activeAction = animationActions[0]  // sets current animation
    
            scene.add(object)  // adds animated object to your scene
    
            //add an animation from another file
            fbxLoader.load(
                'path/to/animation.fbx',
                (object) => {
                    console.log('loaded animation')
    
                    const animationAction = mixer.clipAction(.animations[0])
                    animationActions.push(animationAction)
                    animationsFolder.add(animations, 'animationName')
    
                    //add an animation from another file
                    fbxLoader.load(
                        'path/to/other/animation.fbx',
                        (object) => {
                            console.log('loaded second animation')
                            const animationAction = mixer.clipAction(animations[0])
                            animationActions.push(animationAction)
                            animationsFolder.add(animations, 'animationName')
    
                            //add an animation from another file
                            fbxLoader.load(
                                'path/to/animation.fbx',
                                (object) => {
                                    console.log('loaded third animation');
                                    const animationAction = mixer.clipAction(animations[0])
                                    animationActions.push(animationAction)
                                    animationsFolder.add(animations, 'animationName')
    
                                    modelReady = true
                                },
                                (xhr) => {
                                    console.log(
                                        (xhr.loaded / xhr.total) * 100 + '% loaded'
                                    )
                                },
                                (error) => {
                                    console.log(error)
                                }
                            )
                        },
                        (xhr) => {
                            console.log((xhr.loaded / xhr.total) * 100 + '% loaded')
                        },
                        (error) => {
                            console.log(error)
                        }
                    )
                },
                (xhr) => {
                    console.log((xhr.loaded / xhr.total) * 100 + '% loaded')
                },
                (error) => {
                    console.log(error)
                }
            )
        },
        (xhr) => {
            console.log((xhr.loaded / xhr.total) * 100 + '% loaded')
        },
        (error) => {
            console.log(error)
        }
    )

const animations = {
       function default() {
            setAction(animationActions[0])
        },
        function firstAnimation() {
            setAction(animationActions[1])
        },
       function sceondAnimation() {
            setAction(animationActions[2])
        },
       function thirdAnimation() {
            setAction(animationActions[3])
        }
    }
    
    const setAction = {
        if (toAction != activeAction) {
            lastAction = activeAction
            activeAction = toAction
            //lastAction.stop()
            lastAction.fadeOut(1)
            activeAction.reset()
            activeAction.fadeIn(1)
            activeAction.play()
        }
    }

const clock = new THREE.Clock()
    
    function animate() {
        requestAnimationFrame(animate)
    
        controls.update()
    
        if (modelReady) {mixer.update(clock.getDelta())}
    
        render()
    
    }
    
    function render() {
        renderer.render(scene, camera)
    }
    
    animate()