three.js AnimationUtils 深度解析:动画剪辑工具类的七个静态方法与源码实现
AnimationUtils 是 three.js 动画系统中一个纯静态工具类,提供数组类型转换、关键帧排序、AOS 关键帧解析、片段裁剪(subclip)和加性混合(additive blending)等辅助能力,是 AnimationClip、KeyframeTrack 底层解析链路中直接依赖的一组基础函数。本文基于文档 AnimationUtils 与源码 src/animation/AnimationUtils.js 逐方法解析其参数、默认值与实现细节,并结合其在 AnimationClip 内部的实际调用点说明各工具在完整动画管线中的作用位置。
总览:一个纯静态工具类
从源码结构看,AnimationUtils 本身不包含任何状态:类体只是把七个顶层函数封装为 static 方法,类定义位于 AnimationUtils 类体,模块同时导出顶层函数与类本身:
// src/animation/AnimationUtils.js 末尾导出
export {
convertArray,
isTypedArray,
getKeyframeOrder,
sortedArray,
flattenJSON,
subclip,
makeClipAdditive,
AnimationUtils
};
文档中列出的七个静态方法与源码一一对应,按功能可分为四组:
| 方法 | 功能分组 | 源码位置 |
|---|---|---|
convertArray |
类型转换 | convertArray 实现 |
isTypedArray |
类型检查 | isTypedArray 实现 |
getKeyframeOrder |
关键帧排序 | getKeyframeOrder 实现 |
sortedArray |
关键帧排序 | sortedArray 实现 |
flattenJSON |
AOS 关键帧解析 | flattenJSON 实现 |
subclip |
片段裁剪 | subclip 实现 |
makeClipAdditive |
加性混合转换 | makeClipAdditive 实现 |
其中 getKeyframeOrder 与 flattenJSON 属于“内部管线型”工具,直接服务于 AnimationClip 的构造与解析;subclip 与 makeClipAdditive 则面向开发者,用于制作动画资产时的裁剪与混合预处理。
类型转换:convertArray 与 isTypedArray
.convertArray( array, type )
文档定义:将一个数组转换为指定类型。
AnimationUtils.convertArray( array, type );
- array:待转换的数组(
TypedArray | Array)。 - type:目标类型的构造器,例如
Float32Array、Array。 - Returns:转换后的数组。
实现逻辑见 convertArray:
function convertArray( array, type ) {
if ( ! array || array.constructor === type ) return array;
if ( typeof type.BYTES_PER_ELEMENT === 'number' ) {
return new type( array ); // create typed array
}
return Array.prototype.slice.call( array ); // create Array
}
三个关键行为值得注意:
- 短路返回:当
array为空或其构造器已等于type时直接原样返回,不产生新对象——这意味着对已是正确类型的数组调用是零开销的; - 通过
BYTES_PER_ELEMENT判断目标是否为 TypedArray:所有标准 TypedArray 构造器(Float32Array、Uint8Array等)都带有BYTES_PER_ELEMENT静态属性,普通Array没有,据此区分两条转换路径; - 回退为普通数组:目标是
Array时用Array.prototype.slice.call拷贝,保证返回的是独立副本。
.isTypedArray( object )
文档定义:判断给定对象是否为 TypedArray,返回 boolean。
该方法只是对 src/utils.js 中同名工具函数 的静态转发:
function isTypedArray( array ) {
return ArrayBuffer.isView( array ) && ! ( array instanceof DataView );
}
判断依据是 ArrayBuffer.isView 且排除 DataView——即 Int8Array、Float32Array 等真正的 TypedArray 返回 true,而 Array、DataView 返回 false。在动画场景中,它常用于在“普通数组”和“TypedArray”两条处理分支之间做选择。
关键帧排序:getKeyframeOrder 与 sortedArray
KeyframeTrack 要求 times 数组单调递增,但某些构造场景(如 morph target 序列)生成的时间序列天然是乱序的。这两个方法构成“先算顺序、再重排”的两段式排序工具。
.getKeyframeOrder( times )
文档定义:返回一个可用于对 times 和 values 排序的索引数组。
const order = AnimationUtils.getKeyframeOrder( times );
实现见 getKeyframeOrder:
function getKeyframeOrder( times ) {
function compareTime( i, j ) {
return times[ i ] - times[ j ];
}
const n = times.length;
const result = new Array( n );
for ( let i = 0; i !== n; ++ i ) result[ i ] = i;
result.sort( compareTime );
return result;
}
它并不返回排序后的时间值,而是返回索引置换数组:result[i] 表示“第 result[i] 个原始关键帧排在第 i 位”。返回索引而非直接排序的原因在于:times 与 values 必须同步重排,而 values 每个关键帧可能包含多个分量(position 3 个、quaternion 4 个……),排序时需要按固定步长整体搬移。
.sortedArray( values, stride, order )
文档定义:按 getKeyframeOrder() 预先计算的顺序对数组重排。
- values:待重排的值数组;
- stride:步长,即每个关键帧对应的分量个数(1 维传 1,Vector3 传 3,Quaternion 传 4);
- order:上一步计算出的排序顺序;
- Returns:重排后的新数组。
实现见 sortedArray,核心是按 stride 整体搬移每个关键帧的分量:
function sortedArray( values, stride, order ) {
const nValues = values.length;
const result = new values.constructor( nValues );
for ( let i = 0, dstOffset = 0; dstOffset !== nValues; ++ i ) {
const srcOffset = order[ i ] * stride;
for ( let j = 0; j !== stride; ++ j ) {
result[ dstOffset ++ ] = values[ srcOffset + j ];
}
}
return result;
}
注意 new values.constructor( nValues ) 这一行:结果沿用原数组的构造器,输入是 Float32Array 时输出也是 Float32Array,保持了后续插值器对数值类型的期望。
真实调用点:这两个方法在 AnimationClip.CreateFromMorphTargetSequence 中配合使用——morph 序列生成的时间 times 是循环取模产生的乱序值,需要先经 getKeyframeOrder 得到顺序,再分别对 times 和 values(两者 stride 均为 1)执行 sortedArray,最后才构造 NumberKeyframeTrack:
// src/animation/AnimationClip.js CreateFromMorphTargetSequence 内
const order = AnimationUtils.getKeyframeOrder( times );
times = AnimationUtils.sortedArray( times, 1, order );
values = AnimationUtils.sortedArray( values, 1, order );
这说明该工具对并非只供外部开发者使用,而是库自身 morph target 动画构造流程的一部分。
AOS 关键帧解析:flattenJSON
文档定义:用于解析 AOS 关键帧格式(“Used for parsing AOS keyframe formats”)。
AnimationUtils.flattenJSON( jsonKeys, times, values, valuePropertyName );
- jsonKeys:JSON 关键帧对象数组,每项形如
{ time: 0.5, value: [x, y, z] }; - times:出参,方法向其中填充关键帧时间;
- values:出参,方法向其中填充关键帧数值;
- valuePropertyName:取值属性名(例如
value),返回值为空。
这是典型的“填充式”(in/out 参数)API:调用方传入两个空数组,方法负责 push 数据。实现见 flattenJSON,其逻辑分三步:
- 定位首个有效关键帧:从
jsonKeys[0]开始向后扫描,跳过不含valuePropertyName属性的项;若整个列表都没有数据则直接返回; - 按首个有效值的形态分三条分支处理后续所有关键帧:
- 值是数组:
values.push( ...value )展开推入全部分量; - 值带
toArray方法(源码注释为 “assume THREE.Math-ish”,即Vector3、Quaternion等数学对象):调用value.toArray( values, values.length )追加分量; - 其他标量值:原样
push;
- 值是数组:
- 每条分支都同步执行
times.push( key.time ),保证times与values的对应关系。
真实调用点:当 JSON 动画数据以 { keys: [ { time, value }, ... ] } 形式提供 track 数据、而没有直接给出 times/values 时,AnimationClip.parse() 内部的 parseKeyframeTrack 会调用它完成展平,见 parseKeyframeTrack:
// src/animation/AnimationClip.js
if ( json.times === undefined ) {
const times = [], values = [];
AnimationUtils.flattenJSON( json.keys, times, values, 'value' );
json.times = times;
json.values = values;
}
因此 flattenJSON 是 three.js 解析非标准/外部 AOS 风格 JSON 动画数据的关键入口:只要数据带 keys 数组,库就能自动归一化为 KeyframeTrack 需要的 times + values 结构。
片段裁剪:subclip
文档定义:从源片段中截取两个帧号之间的区段,生成一个新的 AnimationClip。
const clip = AnimationUtils.subclip( sourceClip, name, startFrame, endFrame, fps = 30 );
- sourceClip:源动画片段;
- name:新片段名称;
- startFrame / endFrame:起止帧号(注意单位是“帧”,不是秒);
- fps:帧率,默认
30; - Returns:新的子片段。
实现见 subclip,完整流程包含四个阶段:
- 克隆并改名:
const clip = sourceClip.clone(),随后覆盖clip.name = name; - 按帧范围过滤每条 track 的关键帧:将每个关键帧时间换算为帧号
frame = track.times[ j ] * fps,只保留startFrame <= frame < endFrame(左闭右开)的项,并按track.getValueSize()逐分量拷贝values。过滤后times为空的 track 直接丢弃:
// src/animation/AnimationUtils.js
for ( let j = 0; j < track.times.length; ++ j ) {
const frame = track.times[ j ] * fps;
if ( frame < startFrame || frame >= endFrame ) continue;
times.push( track.times[ j ] );
for ( let k = 0; k < valueSize; ++ k ) {
values.push( track.values[ j * valueSize + k ] );
}
}
- 时间轴归零:找出所有保留 track 中最早的
times[0]作为minStartTime,再对每条 track 调用shift( -minStartTime ),使子片段从t = 0开始播放。这意味着截取的片段不会因为源片段中点截取而带有一个大的初始时间偏移; - 重算时长:
clip.resetDuration()让新片段的duration与裁剪后的时间轴一致。
裁剪结果中 times 和 values 还会经 convertArray 还原为原 track 的类型(如 Float32Array),保持与原始 track 的数值类型兼容。
适用场景:从长动画(如走路循环、待机动画)中切出指定帧区段做独立片段,常用于程序化制作动画资产,避免回到 DCC 软件重新切分。
加性混合转换:makeClipAdditive
文档定义:把给定动画片段的关键帧转换为加性(additive)格式,使其可与 AdditiveAnimationBlendMode 配合叠加播放。
AnimationUtils.makeClipAdditive( targetClip, referenceFrame = 0, referenceClip = targetClip, fps = 30 );
- targetClip:要转换为加性形式的片段;
- referenceFrame:参考帧,默认
0; - referenceClip:参考片段,默认
targetClip自身; - fps:帧率,默认
30; - Returns:已被更新为加性格式的片段。
加性动画的核心思想:把“绝对姿态”转换为“相对于某一参考姿态的偏移量”,叠加时只施加偏移而不覆盖基础姿态。实现见 makeClipAdditive,要点如下:
1. 帧率保护:fps <= 0 时回退为 30,与 subclip 的默认值保持一致。
2. 逐 track 匹配:以 referenceClip 的 track 为基准,在 targetClip 中按“名字相同且 ValueTypeName 相同”匹配对应 track,匹配不到则跳过;bool 与 string 类型的非数值 track 直接跳过,不做加性处理。
3. 三次样条偏移:对 GLTF Cubic Spline 插值的 track,values 中每个关键帧实际包含“切线-值-切线”三段,因此需要 referenceOffset = referenceValueSize / 3 跳过首段切线分量,只取/改中间的“值”分量。这是该方法能兼容 GLTF 标准样条动画的关键细节。
4. 参考值获取的三种情形(referenceTime = referenceFrame / fps):
- 参考帧早于首关键帧:取第一个关键帧的值;
- 参考帧晚于末关键帧:取最后一个关键帧的值;
- 位于区间内:通过
referenceTrack.createInterpolant()在referenceTime处插值求出精确参考值。
5. 类型特异的“减去参考值”:
- quaternion:不能用减法,先对参考四元数做归一化 + 共轭(
new Quaternion().fromArray( referenceValue ).normalize().conjugate()),再对目标 track 每个关键帧用Quaternion.multiplyQuaternionsFlat左乘该共轭四元数——数学上等价于“从每个姿态中抵消参考姿态的旋转”; - 其他数值类型:直接逐分量执行
targetTrack.values[ valueStart + k ] -= referenceValue[ k ]。
6. 设置混合模式:处理完成后置 targetClip.blendMode = AdditiveAnimationBlendMode,该常量定义于 src/constants.js(值为 2501)。此后把该 clip 的 AnimationAction 以叠加方式与其他动作混合时,AnimationMixer 即按加性语义合成。
示例参考:官方示例 webgl_animation_skinning_additive_blending.html 演示了骨骼动画的加性混合,其中在加载完动画数据后对片段调用:
// examples/webgl_animation_skinning_additive_blending.html
THREE.AnimationUtils.makeClipAdditive( clip );
使用建议与小结
从源码结构与调用链可以归纳出各方法的定位:
- 库内部依赖:
getKeyframeOrder/sortedArray被AnimationClip.CreateFromMorphTargetSequence用于 morph target 动画的时间归序;flattenJSON被AnimationClip.parse用于 AOS 风格keys数据展平;convertArray是subclip等路径的类型还原手段。这些方法通常无需开发者直接调用,但理解它们有助于排查自定义 JSON 动画解析问题; - 面向资产制作:
subclip以“帧”为单位截取片段并自动把起点平移到t=0,适合程序化切分长动画;makeClipAdditive把片段转为加性格式并自动写入AdditiveAnimationBlendMode,与AnimationAction的混合机制配合使用,是加性动画(如“叠加挥手到走路循环”)的标准预处理步骤; - 注意默认 fps 的一致性:
subclip与makeClipAdditive的fps默认值均为30,而参考帧换算依赖referenceFrame / fps——若源动画并非 30fps,务必显式传入真实帧率,否则截取区间或参考姿态会偏移。
单元测试目前仅为占位模块(见 test/unit/src/animation/AnimationUtils.tests.js),因此验证这些方法行为的可靠方式是结合上述 AnimationClip 调用点与官方示例自行构造关键帧数据核对。完整 API 签名可回查文档 docs/pages/AnimationUtils.html.md 与源码 src/animation/AnimationUtils.js。
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
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
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