Three.js AnimationClip 深度解析:动画剪辑的构造、序列化与 Morph Target 序列生成
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#L1210 与 src/constants.js#L1219,分别为 NormalAnimationBlendMode = 2500 和 AdditiveAnimationBlendMode = 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。仓库中确实如此:
- GLTFLoader:
new AnimationClip( animationName, undefined, tracks ); - FBXLoader:
new AnimationClip( rawClip.name, - 1, tracks ); - BVHLoader:
new AnimationClip( 'animation', - 1, tracks ); - MDDLoader:显式传入
times的最后一帧作为时长。
轨道侧的约束值得留意:KeyframeTrack 构造函数 会在 name 为 undefined 或 times 为空时直接抛出 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.toJSON(src/animation/KeyframeTrack.js#L68-L104)会输出 name、times、values,仅在插值方式不同于默认(InterpolateLinear)时追加 interpolation 字段,并强制写入 type(各轨道类的 ValueTypeName,如 "vector3"、"quaternion")。
.parse( json ) : AnimationClip(静态)
静态 parse 是 toJSON 的逆过程,有三个值得注意的实现细节:
-
帧号到秒的换算:
frameTime = 1.0 / ( json.fps || 1.0 ),随后对每条解析出的轨道调用.scale( frameTime )——即 JSON 中若带有fps字段,关键帧时间会被缩放为秒; -
uuid 与 userData 还原:
clip.uuid = json.uuid(保持序列化前后的身份一致),userData从字符串反序列化回对象; -
轨道类型路由:模块底部的 getTrackTypeForValueTypeName 把 JSON 的
type名称映射到具体轨道类:JSON type轨道类 scalar/double/float/number/integerNumberKeyframeTrackvector/vector2/vector3/vector4VectorKeyframeTrackcolorColorKeyframeTrackquaternionQuaternionKeyframeTrackbool/booleanBooleanKeyframeTrackstringStringKeyframeTrack未知
type会抛出THREE.KeyframeTrack: Unsupported typeName: ...。parseKeyframeTrack 还兼容旧格式:若 JSON 轨道缺少
times而带有keys数组,会用AnimationUtils.flattenJSON( json.keys, times, values, 'value' )展开成times/values。若轨道类定义了静态parse方法则优先走之(派生类扩展点)。各轨道类位于 src/animation/tracks/ 目录(
NumberKeyframeTrack.js、VectorKeyframeTrack.js、QuaternionKeyframeTrack.js等)。
按名称检索:.findByName( objectOrClipArray, name )
源码实现 很简单但有两个分支语义:
- 第一个参数是数组时,直接在该数组中线性查找
name === name的剪辑; - 第一个参数不是数组时(通常是 mesh),取
o.geometry && o.geometry.animations || o.animations作为搜索空间——即优先查找geometry.animations,找不到再回退到对象的animations属性。
未命中返回 null。仓库内的实际用法可参考 webgl_loader_fbx.html(THREE.AnimationClip.findByName( clips, name ))以及 MorphAnimMesh(AnimationClip.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 按时间重排;若 noLoop 为 false 且排序后首帧时间为 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:
- 用正则
/^([\w-]*?)([\d]+)$/匹配形如Walk_001、Run_002、flamingo_flyA_003的命名,把“前缀”作为分组键;源码注释说明该模式专门针对flamingo_flyA_003、flamingo_run1_003、crdeath0059这类刁钻命名做过验证; - 每一组交给上面的
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 / LoopPingPong、timeScale、weight 等)见 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' );
参考
- 文档:AnimationClip、KeyframeTrack、AnimationAction、AnimationMixer、AnimationUtils、MD2Loader
- 源码:src/animation/AnimationClip.js、src/animation/KeyframeTrack.js、src/animation/AnimationAction.js、src/constants.js、src/animation/tracks/
- 加载器用法:GLTFLoader、FBXLoader、BVHLoader、MDDLoader、MD2Loader、ColladaComposer、USDComposer
- 测试:test/unit/src/animation/AnimationClip.tests.js(覆盖实例化与
name属性断言)
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00