Three.js-硬要自学系列31之专项学习动画混合

动画混合核心原理

动画混合(Animation Blending) 就像一位 DJ 同时播放多首歌,并将它们自然过渡拼接成一首新曲。它让 3D 模型(比如游戏角色)能在不同动作之间平滑切换(如从"走路"渐变到"跑步"),不会出现卡顿或突兀的跳跃。

想象你的角色有两个动作:

  1. 站立不动(动画 A)
  2. 挥手打招呼(动画 B)

直接切换会显得僵硬🤖,就像突然从静止跳到挥手。动画混合的作用是:

  • 权重(Weight)控制:给每个动作分配一个"音量旋钮"(权重值 0~1),比如:

    • 初始:动画 A 权重=1(站立),动画 B 权重=0(挥手关闭)
    • 过渡期:动画 A 权重从 1 → 0,动画 B 权重从 0 → 1
    • 最终:动画 A 权重=0,动画 B 权重=1(完全挥手)
  • 实时混合计算 :每一帧,Three.js 会根据权重比例,同时播放两个动作并叠加效果,生成中间过渡状态。

动画混合的常见用途

场景 效果说明 混合策略
角色动作切换 走路 → 跑步 → 跳跃 自然过渡 权重渐变(0→1/1→0)
复合动作 边走路边挥手 多个动作权重同时为 1
情绪叠加 受伤时跛脚行走 正常行走 + 受伤姿态混合

使用注意

  1. 权重总和 ≤ 1:所有动作的权重相加不超过 1,否则动作会"变形"
  2. 时间同步:混合的动画需时间轴对齐(如都是从左脚起步)。
  3. 性能优化:过多动画混合会增加计算量,需合理控制数量

向量值定义动画案例

效果如图

需要了解的API

动画混合器(Animation Mixer)

  • 角色动画的"总控台",负责管理所有动画片段和混合效果

动画行为(Animation Action)

  • 每个动作(如走路、跑步)都是一个独立的 AnimationAction 对象,其用于创建并控制动画行为

动画剪辑 (AnimationClip)

  • 用于存储和管理关键帧动画数据,整合多个轨道,形成完整的动画片段,其本身不执行动画

实现思路

使用VectorKeyframeTrack来定义动画运动轨迹,

js 复制代码
const track = new THREE.VectorKeyframeTrack('.position', [0,1],[
    5, 0, -5,
    -5, 0, 5
])

这里定义了两个时间节点,相对应的是两个坐标点,这意味着0时对应在第一个坐标位置,1时在第二个坐标位置

js 复制代码
const clip = new THREE.AnimationClip('move', -1, [track]);
const mixer = new THREE.AnimationMixer(mesh);
const action = mixer.clipAction(clip);
action.play();

这里定义了一个名为move的动画剪辑,其作用于mesh模型上,并将无限循环播放

js 复制代码
const update = (frame, frameMax) => {
    const a_frame = frame / frameMax; // 当前帧占总帧数的比例
    const a_framesin = ( Math.sin( Math.PI *2 * a_frame) + 1) / 2; 
    mixer.setTime( 1 * a_framesin); // 设置动画时间
}

mixer.setTime用于精确控制动画的播放时间点

JSON格式的动画效果案例

效果如图

需要了解的API

AnimationClip.parse

该api是一个静态方法,用于将JSON格式的动画数据反序列化为 AnimationClip 对象

JSON 数据结构要求
字段 类型 说明 是否必需
name string 动画名称
tracks Array<Object> 关键帧轨迹数据
duration number 动画总时长(秒)
blendMode number 混合模式(如 NormalAnimationBlendMode
uuid string 唯一标识符

在这个案例中,使用AnimationClip.parse将给定的json数据转换为AnimationClip对象

js 复制代码
const str_json = `{
    "name": "scale",
    "duration":3,
    "tracks":[
        {
        "name":".scale",
        "times":[0,1.5,3],
        "values":[ 1,1,1, 0.5,3,0.5, 1,1,1],
        "type":"vector"
        },
        {
        "name":".position",
        "times":[0, 0.5, 1.5, 2.5, 3],
        "values":[ 0,0.5,0, 2,1.5,0, -2,1.5,0, -2,1.5,2, 0,0.5,0],
        "type":"vector"
        }
    ],
    "uuid":"eff507b6-8c41-47d4-a62e-77119a5ee288",
    "blendMode":2500
}`
const clip = THREE.AnimationClip.parse( JSON.parse(str_json) )

const mixer = new THREE.AnimationMixer(mesh);
const action = mixer.clipAction(clip);
action.play();

从代码中可以看到定义了两个轨道动画状态,一个是缩放,一个是位置

加载json格式场景数据案例

如图效果

需要了解的API

ObjectLoader().parse 该方法将JSON格式的 3D 场景数据实时解析为 Three.js 可操作对象(场景、模型、动画等),实现动态场景加载与重建。

数据结构如下

js 复制代码
const str_json = `{
    "metadata": {
        "version": 4.3,
        "type": "Object",
        "generator": "Hand Coded"
    },
    "textures": [],
    "images": [],
    "geometries": [
        {
            "uuid": "bce89f32-4cab-41c8-b3f6-15e04b1dd68e",
            "type": "BufferGeometry",
            "data": {
                "attributes": {
                    "position": {
                        "itemSize": 3,
                        "type": "Float32Array",
                        "array": [0,0,0, 4,0,0, 0,0,4 ],
                        "normalized": false
                    }
                },
                "morphTargetsRelative": true,
                "morphAttributes": {
                    "position": [
                        {
                            "itemSize":3,
                            "type":"Float32Array",
                            "array":[
                                0.0, 3.0, 0.0,
                                -4.0, 3.0, 0.0,
                                0.0, 3.0,-4.0
                            ],
                            "normalized":false
                        }
                    ]
                }
            }
        }
    ],
    "materials": [
        {
            "uuid": "0246dafa-bf34-4460-a7eb-f5098b2120af",
            "type": "PointsMaterial",
            "size": 1,
            "color": 65480
        }
    ],
    "animations": [
        {
            "name": "converge",
            "duration": 1,
            "tracks": [
                {
                    "name": ".morphTargetInfluences[0]", 
                    "times": [0, 0.5, 1],
                    "values": [0, 1, 0],
                    "type": "number"
                }
            ],
            "uuid": "584702c7-efb2-4fa1-ae37-43d18b5f7fb5",
            "blendMode": 2500
        }
    ],
    "object": {
        "uuid": "ad1ecebd-b665-4e10-9ead-6d0205bec011",
        "type": "Scene",
        "matrix": [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ],
        "children": [
            {
                "uuid": "6b95325d-5bcc-4f85-a330-fbca8f271287",
                "name": "tri_one",
                "type": "Points",
                "geometry": "bce89f32-4cab-41c8-b3f6-15e04b1dd68e",
                "material": "0246dafa-bf34-4460-a7eb-f5098b2120af",
                "matrix": [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ],
                "animations": ["584702c7-efb2-4fa1-ae37-43d18b5f7fb5"]
            }
        ]
    }
}`;

这是一个场景数据结构,包含了几何体、材质、动画,模型对象等内容

js 复制代码
scene = new THREE.ObjectLoader().parse( JSON.parse(str_json) );
const points = scene.getObjectByName('tri_one');
const mixer = new THREE.AnimationMixer(points);
const action = mixer.clipAction(points.animations[0]);
action.play();

蝴蝶动画案例

先看效果

需要了解的API

BufferGeometryLoader

该api 用于加载预序列化的BufferGeometry数据,跳过完整场景解析,直接获取几何体对象。

NumberKeyframeTrack

用于在动画中精准控制单一数值属性的关键帧变化(如透明度、强度、进度条等)

js 复制代码
const loader = new THREE.BufferGeometryLoader();
loader.load(
    './json/tri12-butterfly/set1-buffergeometry/0.json', 
    (geometry) =>{
    // 创建一个网格,使用几何体和基本材质
    state.mesh = new THREE.Mesh(
        geometry,
        new THREE.MeshBasicMaterial({vertexColors: true, side: THREE.DoubleSide})
    )
    // 将网格添加到场景中
    scene.add(state.mesh)
    // 创建一个数字关键帧轨道,用于控制网格的变形
    const track = new THREE.NumberKeyframeTrack('.morphTargetInfluences[0]', 
        [ 0, 0.25, 0.5, 0.75, 1], // 时间点
        [ 0, 0.30, 0.5, 0.15, 0]  // 变形程度
    )
    // 创建一个动画剪辑,包含一个关键帧轨道
    const clip = new THREE.AnimationClip('flap', -1, [ track ] );  
    // 将动画剪辑添加到网格的动画数组中
    state.mesh.animations.push(clip)
    // 创建一个动画混合器,用于控制网格的动画
    state.mixer = new THREE.AnimationMixer(state.mesh)
    // 获取网格的第一个动画剪辑
    const action = state.mixer.clipAction( state.mesh.animations[0] );
    // 播放动画
    action.play();

    animation()
})

这里要注意使用NumberKeyframeTrack创建了一个数字关键帧轨道,来控制变形强弱

总结动画实现步骤

  1. 创建动画剪辑
  2. 创建混合器并播放
  3. 在动画循环中更新

以上便是本篇的所有内容,自己动手练一练,感受一下实际效果

相关推荐
逝缘~6 分钟前
小白学Pinia状态管理
前端·javascript·vue.js·vscode·es6·pinia
光影少年9 分钟前
vite原理
前端·javascript·vue.js
陌上烟雨寒15 分钟前
vue手写一个步骤条steps
javascript·css·vue
C MIKE17 分钟前
ztree.js前端插件样式文字大小文字背景修改
开发语言·前端·javascript
!win !36 分钟前
uni-app项目怎么实现多服务环境切换
前端·uni-app
源猿人37 分钟前
文化与代码的交汇:OpenCC 驱动的中文语系兼容性解决方案
前端·vue.js
xw540 分钟前
uni-app项目怎么实现多服务环境切换
前端·uni-app
Kjjia41 分钟前
到底是 react 性能拉胯?还是吃了机制的亏
前端·react.js
ViceBoy_42 分钟前
前端的Promise的方法all,race,any
前端·javascript
飞翔的猪猪1 小时前
GitHub Recovery Codes - 用于 GitHub Two-factor authentication (2FA) 凭据丢失时登录账号
前端·git·github