Three.js中加载动画模型

1,266 阅读7分钟

您是否发现了一个非常逼真的3D模型,但不确定是否能将其集成到Three.js应用程序中?不要害怕,本文可以满足你的需求。

如何下载模型

首先,在加载模型之前,我们需要准备好一个项目。如果你已经准备好了,那就太好了!如果没有,也不用担心我有一个模板可用。

启动并运行您的项目:

现在,让我们选择一个模型来使用。就我个人而言,我推荐quaternius.com上的免费模型。对于这个例子,我将使用飞龙

模型有三种文件扩展名: .obj.fbx.gltf。虽然它们都可以工作,但建议使用 .gltf .glb 文件。 加载模型需要一个加载器。有多种加载器可用,您需要使用的加载器取决于模型的文件扩展名。

在我们的例子中,我们将使用GLTFLoader,因为文件扩展名是 .gltf

注意: GLTFLoader用于加载 .gltf.glb文件,因为它们基本上是相同的格式。

要使用GLTFLoader,我们必须首先将其导入到项目中。

import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader';

下一步是创建一个GLTFLoader的实例,并在其上调用load方法。load方法有两个参数:第一个是文件的路径,第二个是回调函数。

const gltfLoader = new GLTFLoader();
gltfLoader.load('./assets/Dragon_Evolved.gltf', function(gltf) {
   // What should be done once the model is loaded.
});

将其集成到场景中:

scene.add(model);

正如你所看到的,模型被引入到场景中。但由于没有光源,它是全黑的。让我们添加一些光。

// Outside the body of the load callback function.
const aLight = new THREE.AmbientLight(0xFFFFFF, 1);
scene.add(aLight);

const dLight = new THREE.DirectionalLight(0xFFFFFF, 10);
scene.add(dLight);
dLight.position.set(4, 10, 3);

完成后,您现在应该可以看到您的模型。

load()的第三个参数是一个在加载过程中调用的函数,它提供有关已加载数据量的信息。

// Ensure that you have a large file available for loading.
// The one I'm loading here is 14.1 MB
gltfLoader.load('./assets/free_datsun_280z.glb', function(glb) {
    const model = glb.scene;
    scene.add(model);
}, function (xhr) {
    console.log((xhr.loaded / xhr.total * 100) + '% loaded');
});

xhr参数表示XMLHttpRequest实例,该实例包含表示文件总大小的total和表示已加载字节数的loaded

load()方法的第四个也是最后一个参数是一个函数,在发生加载错误时调用。它接受一个参数,其中包含默认的错误消息。

gltfLoader.load('./assets/wrong_file_name.glb', function(glb) {
    const model = glb.scene;
    scene.add(model);
}, function (xhr) {
    console.log((xhr.loaded / xhr.total * 100) + '% loaded');
}, function (error) {
    console.log('Loading failed');
});

带动画的模型

为模型设置动画

你可能知道,模型通常带有一组动画。通常,下载模型的网站提供了预览这些动画的方法。但是,如果他们不这样做,有一个方法来查看这些动画。

说到这里,转到Three.js编辑器并拖放您的模型。

同样,由于没有光源,您的模型看起来全黑。要添加一个光源,在导航菜单上的添加平行光

现在,要在我们自己的场景中播放我们刚刚在编辑器中看到的动画,我们需要遵循以下步骤。

首先,我们需要创建一个全局变量。

// Outside the body of the load callback function.
let mixer;

然后,在load回调中,我们将AnimationMixer类的一个实例赋给mixer变量,并将模型作为参数传递给构造函数。

AnimationMixer是场景中的动画播放器。

// Within the body of the load callback function.
mixer = new THREE.AnimationMixer(model);

接下来,我们将AnimationClip调用静态方法findByName()选择要播放的动画。

// Within the body of the load callback function.

// Array of clips
const clips = gltf.animations;

mixer = new THREE.AnimationMixer(model);
const clip = 
THREE.AnimationClip.findByName(clips, 'Flying_Idle');
// You can select it directly if you know
// its index in the clips array.
// const clip = clips[2];

下一步是使用mixer中的clipAction()方法将选定的动画转换为可播放的动作,然后调用play()开始播放动画。

动作通常指动画的可播放实例。它表示场景中特定动画片段的控制和播放。当您使用clipAction()方法将剪辑转换为动作时,您实际上是在创建该动画的可播放实例。

// Within the body of the load callback function.
const action = mixer.clipAction(clip);
action.play();

要播放动画,我们将导航到animate()函数,并从mixer变量调用update()方法。

update()方法需要一个参数deltaTime。它使您能够根据时间的推移更新对象位置、速度和其他属性,从而确保即使帧速波动,动画也以一致的速率进行。我们从Clock实用程序获取deltaTime。

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

完成后,您的模型现在应该有动画了。

AnimationAction属性和方法

现在,让我们探索一些操作方法和属性。

我们可以使用paused属性来暂停动画。将paused设置为true将暂停动画,将其设置为false将恢复动画。

// Within the body of the load callback function.
window.addEventListener('keydown', function(e) {
    if(e.code === 'Space')
        action.paused = !action.paused;
});

我们可以将动画设置为循环一次,每次结束时从头开始重复,或者在向前和向后动画之间交替。

// plays the clip once.
action.loop = THREE.LoopOnce;

// Repeats the animation from the beginning each time it ends.
// This is the default loop mode.
action.loop = THREE.LoopRepeat;

// Repeats the animation forwards and backwards,
// from start to end and then from end to start.
action.loop = THREE.LoopPingPong;

如果要设置特定的重复次数,可以直接为repetitions属性赋值,也可以使用setLoop()方法。

// Option 1
action.loop = THREE.LoopPingPong;
action.repetitions = 3;

// Option 2
action.setLoop(THREE.LoopPingPong, 3);

如果你想在一段时间内逐渐引入动画,而不是立即引入。你可以使用fadeIn()方法。

const action = mixer.clipAction(clip);
action.play();
// The animation will start and gradually increase
// in speed, reaching the normal speed after 5 seconds.
action.fadeIn(5);

我们还有fadeOut(),它的作用与fadeIn()相反。这意味着动画将逐渐降低速度,直到完全停止。

const action = mixer.clipAction(clip);
action.play();
// The animation will start and gradually decrease 
// in speed, stopping completely after 5 seconds.
action.fadeOut(5);

此外,我们还有crossFadeTo()crossFadeFrom()方法,它们可以在不同的动画之间实现无缝过渡。

const clip2 = 
THREE.AnimationClip.findByName(clips, 'Fast_Flying');
const action2 = mixer.clipAction(clip2);

window.addEventListener('keydown', function(e) {
    if(e.code === 'Space')
        action.paused = !action.paused;
    if(e.code === 'KeyF') {
        action2.play();
        action.crossFadeTo(action2, 1);
    }
});

现在,按下F键将动画从Idle动作转换为Fast_Flying动作。

window.addEventListener('keydown', function(e) {
    if(e.code === 'Space')
        action.paused = !action.paused;
    if(e.code === 'KeyF') {
        action2.play();
        action.crossFadeTo(action2, 1);
    }
    if(e.code === 'KeyS') {
        action.play();
        action.crossFadeFrom(action2, 1);
    }
});

混合器事件和动画链接

有两个事件表示在mixer下订阅的动作的完成。

要确定一个动作是否完成,我们需要使用“finished”事件。

重要的是要注意,您必须为动作设置有限的重复次数。

const clip3 = 
THREE.AnimationClip.findByName(clips, 'Yes');
const action3 = mixer.clipAction(clip3);
action3.play();
action3.loop = THREE.LoopOnce;

const clip4 = 
THREE.AnimationClip.findByName(clips, 'No');
const action4 = mixer.clipAction(clip4);
action4.play();
action4.repetitions = 3;

mixer.addEventListener('finished', function(e) {
    console.log(`Action ${e.action._clip.name} is finished`);
});

回调函数中的代码将执行两次:一次是在action3完成时,另一次是在action4的所有重复完成时。

要确定循环的一次迭代是否完成,我们需要使用“loop”事件。

const clip4 = 
THREE.AnimationClip.findByName(clips, 'No');
const action4 = mixer.clipAction(clip4);
action4.play();
action4.repetitions = 3;

mixer.addEventListener('loop', function(e) {
    console.log(`One iteration of ${e.action._clip.name} is finished`);
});

现在,有了这些知识,我们可以很容易地创建一个由几个动作组成的简单循环。

gltfLoader.load('./assets/Dragon_Evolved.gltf', function(gltf) {
    const model = gltf.scene;
    scene.add(model);
    const clips = gltf.animations;

    mixer = new THREE.AnimationMixer(model);

    const clip3 = 
    THREE.AnimationClip.findByName(clips, 'Yes');
    const action3 = mixer.clipAction(clip3);
    action3.play();
    action3.loop = THREE.LoopOnce;

    const clip4 = 
    THREE.AnimationClip.findByName(clips, 'No');
    const action4 = mixer.clipAction(clip4);
    action4.loop = THREE.LoopOnce;

    mixer.addEventListener('finished', function(e) {
       if(e.action._clip.name === 'Yes') {
        action4.reset();
        action4.play();
       } else 
       if(e.action._clip.name === 'No') {
        action3.reset();
        action3.play();
       }
    });
});

真实模型

你遇到了一个令人着迷的超现实模型,比如这辆车,并决定在你的应用程序中使用它。然而,在设置了必要的代码后,你遇到的不是预期的有光泽和反光的汽车,而是这个结果。

模型显示在具有AmbientLightDirectionalLight实例的场景中,但它看起来不像在Sketchfab查看器中那样逼真。这背后的原因是汽车模型中使用的材料需要特殊类型的照明。

为了实现逼真的外观,照明也需要逼真。这可以通过从 .hdr图像加载灯光并将其设置为环境光源来实现。

如果你不熟悉环境和.hdr图像,这篇文章绝对是必读!

为了在Three.js中加载.hdr镜像,我们使用了一个特殊的加载器,称为RGBELoader

import {RGBELoader} from 'three/examples/jsm/loaders/RGBELoader';

然后,创建它的一个实例并调用load(),传递两个参数:.hdr图像的路径和一个回调函数。

const rgbeLoader = new RGBELoader();

rgbeLoader.load('./assets/hdrImage.hdr', function(texture) {
    
});

在回调函数中,指定纹理的贴图模式,并将其设置为场景的环境贴图。

texture.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = texture;

最后,您可以选择将THREE.ACESFilmicToneMapping设置为渲染器的色调映射算法,以获得更好的效果。

const gltfLoader = new GLTFLoader();
const rgbeLoader = new RGBELoader();

renderer.toneMapping = THREE.ACESFilmicToneMapping;

rgbeLoader.load('./assets/hdrImage.hdr', function(texture) {
    texture.mapping = THREE.EquirectangularReflectionMapping;
    scene.environment = texture;

    gltfLoader.load('./assets/car.glb', function(glb) {
        const model = glb.scene;
        scene.add(model);
    });
});

结果如下:

原文:waelyasmina.net/articles/al…