Three.js-硬要自学系列27 (关键帧动画、动画播放、解析外部模型动画、模拟机械拆装动画、变形动画、骨骼动画)

0 阅读4分钟

本章主要学习知识点

  • 了解什么是关键帧动画
  • 掌握动画的各种控制方法
  • 学会如何解析并播放外部模型自带的动画

关键帧动画

关键帧动画就像给 3D 模型制作“动态剧本”——通过在时间轴上设定关键动作点,让模型自动完成从起点到终点的平滑过渡。

在时间轴上选择几个关键时间点,定义模型在这些时刻的状态(如位置、颜色、旋转角度等)。例如:

  • 第 0 秒:模型在原点,颜色为红色
  • 第 3 秒:模型移动到 (10,0,0),颜色变为蓝色

Three.js会根据关键帧之间的状态自动计算中间过渡效果,我们无需手动逐帧设置

下面是一个简单的示例

先创建关键帧轨道

const times = [0,1,3,5];
const values = [0,0,0, 10,0,0, 0,0,10, 0,0,0];
const keyframeTrack = new THREE.KeyframeTrack(
    'cube.position', // 属性名称
    times, // 时间序列
    values, // 属性值序列
);
// 创建一个颜色关键帧轨道
const colorKeyframeTrack = new THREE.ColorKeyframeTrack(
    'cube.material.color', // 属性名称
    [0.5,2,4], // 时间序列
    [0.2,1,0.4, 0.3,0.2,0.5, 0.1,0.4,0.8], // 属性值序列
);

然后,我们需要用到AnimationClip创建动画剪辑, 将多个关键帧轨道打包成一个动画片段

const clip = new THREE.AnimationClip(
    'Action', // 动画名称
    5, // 动画持续时间
    [keyframeTrack,colorKeyframeTrack] // 关键帧轨道数组
);

创建动画混合器(AnimationMixer),这相当于动画播放器,控制动画的播放逻辑

const mixer = new THREE.AnimationMixer(cube);

最后播放动画

const action = mixer.clipAction(clip);
// 播放动画
action.play();

不要忘记在循环动画函数中更新动画混合器,推进动画时间轴

const frameT = clock.getDelta();
mixer.update(frameT);

231.gif

动画播放控制

Three.js 提供了stop停止,play播放,paused暂停,timeScale调速等方法来控制播放

在页面上添加一个面板,实现上述功能看看

stopBtn.addEventListener('click', () => {
    action.stop();
})
playBtn.addEventListener('click', () => {
    action.play();
})
pauseBtn.addEventListener('click', () => {
    if(action.paused){
        action.paused = false;
        pauseBtn.innerText = 'PAUSE';
    } else {
        action.paused = true;
        pauseBtn.innerText = 'CONTINUE';
    }
})
speedBtn.addEventListener('click', () => {
    action.timeScale = 2;
})

1.gif

逐帧播放

通过设置动画的time来实现逐帧播放,这很简单

const next = document.getElementById('next');
next.addEventListener('click', () => {
    action.time += 0.1;
})

效果如下

23.gif

解析外部模型动画

要解析外模模型动画,必须保证模型自带动画 , 我们导入模型,打印naimations便可查看模型是否自带动画

loader.load('model/animated_butterflies/scene.gltf', function (gltf) {
    scene.add(gltf.scene);
    console.log(gltf.naimations);
    //包含关键帧动画的模型作为参数创建一个播放器
    const mixer = new THREE.AnimationMixer(gltf.scene);
    //获取gltf.animations[0]的第一个clip动画对象
    const action = mixer.clipAction(gltf.animations[0]);
    action.play()

    const clock = new THREE.Clock();
    function loop() {
        const delta = clock.getDelta();
        mixer.update(delta);
        requestAnimationFrame(loop);
    }
    loop()

}, undefined, function (error) {
    console.error(error);
});

image.png

效果如下

3.gif

模拟机械拆装动画

加载自带动画的模型,通过gui可视化修改参数查看效果

const duration = action.getClip().duration;
const gui = new GUI();
gui.add(action, 'paused').name('暂停');
gui.add(action, 'play').name('播放');
gui.add(action, 'stop').name('停止');

gui.add(action, 'clampWhenFinished').name('结束是否停止');
gui.add(action, 'repetitions', 0, 10).name('重复次数');
gui.add(action, 'timeScale', 0, 10).name('时间缩放');
gui.add(action, 'time',0,duration).step(0.01).name('拖动');   

4.gif

变形动画

变形动画(Morph Animation)是一种通过改变模型顶点位置实现形状过渡的技术,类似于“魔法面团”被捏成不同造型的效果。

变形原理

  • 顶点变形
    每个 3D 模型由顶点(Vertex)构成,变形动画通过预定义多组顶点坐标(称为变形目标),在不同目标之间平滑过渡。例如:

    • 人脸模型定义「笑」和「哭」两组顶点数据
    • 通过权重控制从「哭」到「笑」的渐变效果
  • 权重混合
    每个变形目标有一个权重值(0到1),控制其对最终形状的影响。例如:

    • 权重 0:完全保持原始形状
    • 权重 1:完全变为目标形状
    • 权重 0.5:原始和目标各占一半

定义原始几何体和目标几何体

const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const target1 = new THREE.BoxGeometry( 1, 2, 1 ).attributes.position;
const target2 = new THREE.BoxGeometry( 0.2, 1, 0.2 ).attributes.position;
geometry.morphAttributes.position = [target1, target2]; // 变形目标数组

创建模型并设置权重

const material = new THREE.MeshBasicMaterial( { color: 'deeppink' } );
cube = new THREE.Mesh( geometry, material );
cube.morphTargetInfluences[ 0 ] = 0.5;  // 变形目标权重
cube.morphTargetInfluences[ 1 ] = 0.5;

生成变形动画

cube.name = 'Box';
const KF1 = new THREE.KeyframeTrack('Box.morphTargetInfluences[0]', [0, 1], [0, 1]);
const KF2 = new THREE.KeyframeTrack('Box.morphTargetInfluences[1]', [0, 3], [0, 1]);
const clip = new THREE.AnimationClip('t', 3, [KF1, KF2]);

const mixer = new THREE.AnimationMixer(cube);
const action = mixer.clipAction(clip);
action.play();
action.loop = THREE.LoopOnce;
action.clampWhenFinished = true; 

45.gif

骨骼操作

在three.js 中 通过new THREE.Bone()创建骨骼对象,

const Bone1 = new THREE.Bone(); // 创建骨骼Bone1
const Bone2 = new THREE.Bone(); // 创建骨骼Bone2
const Bone3 = new THREE.Bone(); // 创建骨骼Bone3
// 设置骨骼的父子关系
Bone1.add(Bone2);
Bone2.add(Bone3);
// 设置骨骼的初始位置
Bone2.position.y = 5;
Bone3.position.y = 2;
Bone1.position.set(5,0,5);

// 骨骼关节旋转
Bone1.rotateX(Math.PI / 4);
Bone2.rotateX(Math.PI / 4);

const group = new THREE.Group();
group.add(Bone1);

接下来我们添加gui进行参数控制

const gui = new GUI();
gui.add(Bone1.rotation, 'x', 0, Math.PI / 3 ).name('关节1');
gui.add(Bone2.rotation, 'x', 0, Math.PI / 3 ).name('关节2');

234.gif

骨骼动画

先看效果

3123.gif

这是一个全骨骼控制案例,细致到手指头,下面是代码

const loader = new GLTFLoader();
loader.load('model/bone_boi/scene.gltf', gltf => {
    scene.add(gltf.scene);
    // 创建一个骨骼助手,用于显示骨骼动画
    const skeletonHelper = new THREE.SkeletonHelper(gltf.scene);
    scene.add(skeletonHelper);
    let boneArr = [];
    const gui = new GUI();
    gltf.scene.traverse((child) => {
        if(child.isMesh){
            console.log(child.skeleton);
            console.log(child.skeleton.bones);
            boneArr = child.skeleton.bones;
            for(let i = 0; i < boneArr.length; i++){
                gui.add(boneArr[i].rotation, 'x').min(-10).max(10).step(0.01).name(boneArr[i].name + 'x');
                gui.add(boneArr[i].rotation, 'y').min(-10).max(10).step(0.01).name(boneArr[i].name + 'x');
                gui.add(boneArr[i].rotation, 'z').min(-10).max(10).step(0.01).name(boneArr[i].name + 'x');
            }             
        }
    })
    // 将模型设置在原点
    const boundingBox = new THREE.Box3().setFromObject(gltf.scene);
    const center = boundingBox.getCenter(new THREE.Vector3());
    gltf.scene.position.x += (gltf.scene.position.x - center.x);
    gltf.scene.position.y += (gltf.scene.position.y - center.y);
    gltf.scene.position.z += (gltf.scene.position.z - center.z);
})

以上案例均可在案例中心查看体验

THREE 案例中心

image.png