Three.js 如何将多个没有皮肤的 Mixamo 动画添加到 FBX 模型?

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

我正在尝试创建一个游戏,其中有一个动画角色,其中包含来自 Mixamo 的多个动画。我希望动画根据用户在游戏中的操作而变化,例如 WalkingRunningIdle。这是我加载 FBX 模型(没有动画)的方式:

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);
});

我还有以下文件,都是动画没有皮肤

Idle.fbx
Walking.fbx
Running.fbx

我的目标是尝试做出类似 this or like this 的东西。这两个链接的唯一问题是,在第一个链接中,他们使用了一个附加了多个动画的模型(我有一个带有 3 个没有皮肤的动画的普通模型),在第二个链接中,代码是使用 TypeScript(我更喜欢JavaScript)。

我是 3D 建模的新手,所以我不知道如何将所有没有皮肤的动画附加到主 fbx 模型。 如何在 Blender 中将动画组合到一个模型中,或者有没有办法在 three.js 中做到这一点?

感谢您对此提供的任何帮助,谢谢!


编辑:

根据@GuyNachshon 的说法,这是我应该如何处理的吗?

所以我首先加载没有动画的模型 (yourMesh),然后创建一个 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);
});

然后,我必须加载没有皮肤的 3 个动画文件并将它们添加到 animationsArray。 (不确定我是否正确加载了动画...):

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);
});

一切都完全加载后,我创建 actions:

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

但是,在你说到做到之后:

actions.play();

那句台词要播放什么? animationsArray会播放第一个动画吗?

您需要创建一个 AnimationMixer

假设您已经创建了一个场景,添加了一个网格等。现在您可以初始化一个动画混合器

let mixer = new THREE.AnimationMixer(yourMesh);

然后添加动画使用 clipActions,

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

现在播放

actions.play();

但要真正了解如何使用它,您应该阅读文档(附在上面 :))


编辑 - 回应您的编辑

为了控制要播放的动画,您可以做几件事,这里是 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();
} );

现在,如果您在构建代码时遇到问题,这里有一个关于如何将动画附加到 fbx 并控制它们的一般示例:

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()

放在一起:

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()