首页
/ Three.js AnimationClip 深度解析:动画剪辑的构造、序列化与 Morph Target 序列生成

Three.js AnimationClip 深度解析:动画剪辑的构造、序列化与 Morph Target 序列生成

2026-09-06 11:44:32作者:姚月梅Lane

AnimationClip 是 three.js 动画系统的核心数据结构:它以“关键帧轨道(KeyframeTrack)数组 + 时长 + 混合模式”的形式封装一段可复用的动画,被 AnimationMixer / AnimationAction 驱动播放,并被 GLTF、FBX、BVH、Collada、MD2 等各类加载器统一生产。本文基于官方 API 文档 AnimationClip 文档src/animation/AnimationClip.js 源码,完整梳理其构造函数、属性、实例方法与静态方法的语义、默认值与底层实现,帮助你在手动构造动画、序列化/反序列化、从 morph target 帧序列批量生成剪辑等场景中做到心中有数。

什么是 AnimationClip

官方文档对它的定义是:一组可复用的关键帧轨道,代表一段动画(A reusable set of keyframe tracks which represent an animation)。

src/animation/AnimationClip.js#L15-L85 的类定义可以确认它的完整状态由 6 个属性构成:

属性 类型 默认值 说明
.name string '' 剪辑名称,供 findByName 检索
.tracks Array<KeyframeTrack> [] 关键帧轨道数组
.duration number -1 时长(秒);负值表示由轨道自动推算
.blendMode NormalAnimationBlendMode | AdditiveAnimationBlendMode NormalAnimationBlendMode(常量值 2500) 多个动画同时播放时的混合方式
.uuid string(只读) 自动生成 剪辑的唯一标识
.userData Object {} 自定义数据容器,不应存放函数引用(clone() 走 JSON 深拷贝,函数会被丢弃)

blendMode 的两个常量定义在 src/constants.js#L1210src/constants.js#L1219,分别为 NormalAnimationBlendMode = 2500AdditiveAnimationBlendMode = 2501

构造函数:参数语义与 duration 自动推算

文档中的构造签名:

new AnimationClip(
  name,        // string, 默认 ''
  duration,    // number, 默认 -1;传入负值时从关键帧计算
  tracks,      // Array.<KeyframeTrack>
  blendMode    // 默认 NormalAnimationBlendMode
)

对应源码 src/animation/AnimationClip.js#L31-L85,有一个文档没有显式强调但非常实用的细节:构造函数末尾会检测 duration < 0,若成立则自动调用 resetDuration()(源码注释原话:// this means it should figure out its duration by scanning the tracks)。因此手动创建轨道时通常直接传 -1,让剪辑时长跟随“最长轨道的最后一个关键帧时间”:

import {
  AnimationClip,
  VectorKeyframeTrack,
  QuaternionKeyframeTrack,
  ColorKeyframeTrack
} from 'three';

// 各轨道时长不一致时,clip.duration 取最长者(此处为 4 秒)
const clip = new AnimationClip(
  'spin-and-fade',
  -1, // 由关键帧自动推算时长
  [
    new VectorKeyframeTrack(
      'position',
      [0, 2, 4],
      [0, 0, 0,  0, 2, 0,  0, 0, 0]
    ),
    new ColorKeyframeTrack(
      'material.color',
      [0, 2],
      [1, 0, 0,  0, 1, 0]
    )
  ]
);

console.log( clip.duration ); // 4

clip.userData.source = 'hand-crafted';

文档同时提示:在多数情况下不必直接调用构造函数——加载器在导入带动画的 3D 资产时会自动创建 AnimationClip。仓库中确实如此:

  • GLTFLoadernew AnimationClip( animationName, undefined, tracks )
  • FBXLoadernew AnimationClip( rawClip.name, - 1, tracks )
  • BVHLoadernew AnimationClip( 'animation', - 1, tracks )
  • MDDLoader:显式传入 times 的最后一帧作为时长。

轨道侧的约束值得留意:KeyframeTrack 构造函数 会在 nameundefinedtimes 为空时直接抛出 Error,且 times / values 会被 AnimationUtils.convertArray 转成 Float32Array(字符串/布尔轨道除外)。也就是说,一个合法 AnimationClip 的每条轨道至少要有 1 个关键帧。

实例方法:时长、修剪、优化、校验与复制

文档列出的 6 个实例方法在源码中全部是“原地修改 + 返回 this”的可链式风格。逐一看其实现:

.resetDuration() : AnimationClip

duration 重置为最长关键帧轨道的时长。实现见 src/animation/AnimationClip.js#L302-L319:遍历所有轨道,取 Math.max( duration, track.times[ track.times.length - 1 ] )——即每条轨道最后一帧的时间戳的最大值。

实战含义:如果你通过 track.shift() / track.trim() 等修改了轨道时间轴,应重新调用 resetDuration() 保持剪辑时长与轨道一致。

.trim() : AnimationClip

把每条轨道裁剪到 clip.duration 之内,实现见 src/animation/AnimationClip.js#L326-L336,即对每条轨道调用 track.trim( 0, this.duration )。底层的 KeyframeTrack.trim 只丢弃区间外的关键帧,不会把剩余关键帧平移到起点(源码注释解释:对插值型关键帧而言,平移会改变其取值),并且保证轨道至少保留一个关键帧。

.optimize() : AnimationClip

“通过移除相邻且等值的冗余关键帧来优化每条轨道(这在 morph target 序列中很常见)”,见 src/animation/AnimationClip.js#L364-L374。真正的压缩逻辑在 KeyframeTrack.optimize:源码注释给出了直观示例 (0,0,0,0,1,1,1,0,0,0,0,0,0,0) --> (0,0,1,1,0,0)。两个实现细节:

  • 平滑插值(InterpolateSmooth)轨道不做任何删除,因为中间关键帧会影响曲线形状(见 KeyframeTrack.js#L495-L521);
  • 压缩前会先 slice() 复制 times / values,因为这两个 TypedArray 可能被其他轨道共享,原地覆写是不安全的(KeyframeTrack.js#L473-L475)。

.validate() : boolean

对每条轨道执行“最小校验”(src/animation/AnimationClip.js#L344-L356),全部通过才返回 true。轨道级校验规则定义在 KeyframeTrack.validate,包括:值步长 valueSize 必须为整数、轨道不能为空、时间不能是 NaN、关键帧时间必须单调不减(“Out of order keys”)、数值型 values 不允许 NaN。在调试自产动画时,这是一个廉价而有效的自检入口。

.clone() : AnimationClip

返回一个复制了本实例数值的新剪辑,实现见 src/animation/AnimationClip.js#L381-L397

  • 每条轨道调用 track.clone() 深拷贝 times / values,并直接复制插值工厂方法 createInterpolant(因为该参数在构造后并未被单独保存,见 KeyframeTrack.clone);
  • userData 通过 JSON.parse( JSON.stringify( ... ) ) 深拷贝——这正是文档警告“userData 不应存放函数引用”的原因;
  • 克隆得到的是全新对象,拥有新的 uuid(新对象在构造时重新 generateUUID()),与 toJSON/parse 保留 uuid 的行为形成对比。

.toJSON() : Object

序列化为 JSON,委托给静态方法 toJSON(见下文)。

序列化:.toJSON / .parse 静态方法对

.toJSON( clip ) : Object(静态)与 .toJSON()(实例)

静态 toJSON 输出的结构为:

{
  name:      clip.name,
  duration:  clip.duration,
  tracks:    [ KeyframeTrack.toJSON( track ), ... ],
  uuid:      clip.uuid,
  blendMode: clip.blendMode,
  userData:  JSON.stringify( clip.userData )   // 注意:字符串
}

逐条轨道的 KeyframeTrack.toJSONsrc/animation/KeyframeTrack.js#L68-L104)会输出 nametimesvalues,仅在插值方式不同于默认(InterpolateLinear)时追加 interpolation 字段,并强制写入 type(各轨道类的 ValueTypeName,如 "vector3""quaternion")。

.parse( json ) : AnimationClip(静态)

静态 parsetoJSON 的逆过程,有三个值得注意的实现细节:

  1. 帧号到秒的换算frameTime = 1.0 / ( json.fps || 1.0 ),随后对每条解析出的轨道调用 .scale( frameTime )——即 JSON 中若带有 fps 字段,关键帧时间会被缩放为秒;

  2. uuid 与 userData 还原clip.uuid = json.uuid(保持序列化前后的身份一致),userData 从字符串反序列化回对象;

  3. 轨道类型路由:模块底部的 getTrackTypeForValueTypeName 把 JSON 的 type 名称映射到具体轨道类:

    JSON type 轨道类
    scalar / double / float / number / integer NumberKeyframeTrack
    vector / vector2 / vector3 / vector4 VectorKeyframeTrack
    color ColorKeyframeTrack
    quaternion QuaternionKeyframeTrack
    bool / boolean BooleanKeyframeTrack
    string StringKeyframeTrack

    未知 type 会抛出 THREE.KeyframeTrack: Unsupported typeName: ...

    parseKeyframeTrack 还兼容旧格式:若 JSON 轨道缺少 times 而带有 keys 数组,会用 AnimationUtils.flattenJSON( json.keys, times, values, 'value' ) 展开成 times / values。若轨道类定义了静态 parse 方法则优先走之(派生类扩展点)。

    各轨道类位于 src/animation/tracks/ 目录(NumberKeyframeTrack.jsVectorKeyframeTrack.jsQuaternionKeyframeTrack.js 等)。

按名称检索:.findByName( objectOrClipArray, name )

源码实现 很简单但有两个分支语义:

  • 第一个参数是数组时,直接在该数组中线性查找 name === name 的剪辑;
  • 第一个参数不是数组时(通常是 mesh),取 o.geometry && o.geometry.animations || o.animations 作为搜索空间——即优先查找 geometry.animations,找不到再回退到对象的 animations 属性。

未命中返回 null。仓库内的实际用法可参考 webgl_loader_fbx.htmlTHREE.AnimationClip.findByName( clips, name ))以及 MorphAnimMeshAnimationClip.findByName( this, label ))。

从 Morph Target 序列生成剪辑

这是文档中篇幅最大的静态方法族,也是处理 MD2 一类“每帧一个 morph target”资产的标准手段。

.CreateFromMorphTargetSequence( name, morphTargetSequence, fps, noLoop ) : AnimationClip

为每个 morph target 生成一条 NumberKeyframeTrack,轨道名形如 .morphTargetInfluences[<name>],让某个 target 在“自己的那一帧”权重为 1、相邻帧为 0。实现见 src/animation/AnimationClip.js#L162-L202,核心算法:

// 每个 target i 的三关键帧时间(帧号,随后按 1/fps 缩放为秒)
times.push(
  ( i + numMorphTargets - 1 ) % numMorphTargets, // 前一帧
  i,                                              // 本帧
  ( i + 1 ) % numMorphTargets );                 // 后一帧
values.push( 0, 1, 0 );

随后用 AnimationUtils.getKeyframeOrder + AnimationUtils.sortedArray 按时间重排;若 noLoopfalse 且排序后首帧时间为 0,则把首帧复制追加到 numMorphTargets 时刻(源码注释:// if there is a key at the first frame, duplicate it as the last frame as well for perfect loop),从而保证循环播放时首尾连续。最后所有轨道 .scale( 1.0 / fps ) 并把 duration-1 交给剪辑自动推算。

文档特别提示:fps 是必传参数,但播放速度之后仍可通过 AnimationAction.setDuration 覆盖——AnimationAction 的时长/速度缩放会独立于轨道原始时间轴生效。

.CreateClipsFromMorphTargetSequences( morphTargets, fps, noLoop ) : Array.

把整组 morph targets 按“动画组前缀”自动拆分成多个剪辑。实现见 src/animation/AnimationClip.js#L252-L295

  1. 用正则 /^([\w-]*?)([\d]+)$/ 匹配形如 Walk_001Run_002flamingo_flyA_003 的命名,把“前缀”作为分组键;源码注释说明该模式专门针对 flamingo_flyA_003flamingo_run1_003crdeath0059 这类刁钻命名做过验证;
  2. 每一组交给上面的 CreateFromMorphTargetSequence 生成一个剪辑,返回剪辑数组。

仓库内的标准用例就在 MD2Loader

// MD2Loader 解析完几何体后:
geometry.animations = AnimationClip.CreateClipsFromMorphTargetSequences( frames, 10, false );

即 MD2 资产的帧序列按 10 FPS、可循环的方式批量转换为剪辑并挂到 geometry.animations 上——这与文档中“参见 MD2Loader#parse 作为用法示例”的指引完全对应。

blendMode 在播放管线中的落地

blendMode 并非仅存在剪辑上的静态标记,它直接决定 AnimationAction 每帧如何把轨道结果写回对象属性。AnimationAction._update 中的分支:

  • AdditiveAnimationBlendMode:对每条属性走 propertyMixers[ j ].accumulateAdditive( weight ),把动作的差值叠加到当前属性状态上;
  • NormalAnimationBlendMode(默认分支):走 propertyMixers[ j ].accumulate( accuIndex, weight ),按权重在各动作之间做常规混合。

从源码结构看,加性模式的典型生产路径是 AnimationUtils.makeClipAdditive 一类工具:它对基准剪辑逐帧求差,得到“纯增量”剪辑并把 targetClip.blendMode = AdditiveAnimationBlendMode。因此实际使用中,把某剪辑标记为加性后,还需要为其创建 AnimationAction 交给 AnimationMixer 驱动,混合行为才会真正生效。更多参数(循环模式 LoopRepeat / LoopOnce / LoopPingPongtimeScaleweight 等)见 AnimationAction 文档AnimationMixer 文档

一个可直接复用的完整示例

综合构造函数、静态方法与校验流程:

import {
  AnimationClip,
  AnimationMixer,
  QuaternionKeyframeTrack,
  VectorKeyframeTrack
} from 'three';

// 1. 手动构造剪辑(duration = -1,自动取最长轨道时长 4s)
const clip = new AnimationClip(
  'walk',
  -1,
  [
    new VectorKeyframeTrack(
      'position',
      [0, 1, 2, 3, 4],
      [0, 0, 0,  1, 0, 0,  2, 0, 0,  3, 0, 0,  0, 0, 0]
    ),
    new QuaternionKeyframeTrack(
      'quaternion',
      [0, 2, 4],
      [0, 0, 0, 1,  0, 1, 0, 0,  0, 0, 0, 1]
    )
  ]
);
clip.userData.author = 'example';

// 2. 数据自检与冗余关键帧压缩
console.log( clip.validate() ); // true
clip.optimize();

// 3. 序列化往返:toJSON -> parse 会保留 uuid 与 userData
const roundTrip = AnimationClip.parse( AnimationClip.toJSON( clip ) );

// 4. 播放
const mixer = new AnimationMixer( mesh );
mixer.clipAction( clip ).play();

// 5. 按名称查找(例如从 GLTF 加载结果里)
const walkClip = AnimationClip.findByName( gltf, 'walk' )
  ?? AnimationClip.findByName( gltf.scene.geometry, 'walk' );

参考

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395