首页
/ three.js AnimationUtils 深度解析:动画剪辑工具类的七个静态方法与源码实现

three.js AnimationUtils 深度解析:动画剪辑工具类的七个静态方法与源码实现

2026-09-06 16:48:50作者:晏闻田Solitary

AnimationUtils 是 three.js 动画系统中一个纯静态工具类,提供数组类型转换、关键帧排序、AOS 关键帧解析、片段裁剪(subclip)和加性混合(additive blending)等辅助能力,是 AnimationClipKeyframeTrack 底层解析链路中直接依赖的一组基础函数。本文基于文档 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 实现

其中 getKeyframeOrderflattenJSON 属于“内部管线型”工具,直接服务于 AnimationClip 的构造与解析;subclipmakeClipAdditive 则面向开发者,用于制作动画资产时的裁剪与混合预处理。

类型转换:convertArray 与 isTypedArray

.convertArray( array, type )

文档定义:将一个数组转换为指定类型。

AnimationUtils.convertArray( array, type );
  • array:待转换的数组(TypedArray | Array)。
  • type:目标类型的构造器,例如 Float32ArrayArray
  • 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

}

三个关键行为值得注意:

  1. 短路返回:当 array 为空或其构造器已等于 type 时直接原样返回,不产生新对象——这意味着对已是正确类型的数组调用是零开销的;
  2. 通过 BYTES_PER_ELEMENT 判断目标是否为 TypedArray:所有标准 TypedArray 构造器(Float32ArrayUint8Array 等)都带有 BYTES_PER_ELEMENT 静态属性,普通 Array 没有,据此区分两条转换路径;
  3. 回退为普通数组:目标是 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——即 Int8ArrayFloat32Array 等真正的 TypedArray 返回 true,而 ArrayDataView 返回 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 位”。返回索引而非直接排序的原因在于:timesvalues 必须同步重排,而 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 得到顺序,再分别对 timesvalues(两者 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,其逻辑分三步:

  1. 定位首个有效关键帧:从 jsonKeys[0] 开始向后扫描,跳过不含 valuePropertyName 属性的项;若整个列表都没有数据则直接返回;
  2. 按首个有效值的形态分三条分支处理后续所有关键帧:
    • 值是数组values.push( ...value ) 展开推入全部分量;
    • 值带 toArray 方法(源码注释为 “assume THREE.Math-ish”,即 Vector3Quaternion 等数学对象):调用 value.toArray( values, values.length ) 追加分量;
    • 其他标量值:原样 push
  3. 每条分支都同步执行 times.push( key.time ),保证 timesvalues 的对应关系。

真实调用点:当 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,完整流程包含四个阶段:

  1. 克隆并改名const clip = sourceClip.clone(),随后覆盖 clip.name = name
  2. 按帧范围过滤每条 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 ] );

    }

}
  1. 时间轴归零:找出所有保留 track 中最早的 times[0] 作为 minStartTime,再对每条 track 调用 shift( -minStartTime ),使子片段从 t = 0 开始播放。这意味着截取的片段不会因为源片段中点截取而带有一个大的初始时间偏移;
  2. 重算时长clip.resetDuration() 让新片段的 duration 与裁剪后的时间轴一致。

裁剪结果中 timesvalues 还会经 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,匹配不到则跳过;boolstring 类型的非数值 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 / sortedArrayAnimationClip.CreateFromMorphTargetSequence 用于 morph target 动画的时间归序;flattenJSONAnimationClip.parse 用于 AOS 风格 keys 数据展平;convertArraysubclip 等路径的类型还原手段。这些方法通常无需开发者直接调用,但理解它们有助于排查自定义 JSON 动画解析问题;
  • 面向资产制作subclip 以“帧”为单位截取片段并自动把起点平移到 t=0,适合程序化切分长动画;makeClipAdditive 把片段转为加性格式并自动写入 AdditiveAnimationBlendMode,与 AnimationAction 的混合机制配合使用,是加性动画(如“叠加挥手到走路循环”)的标准预处理步骤;
  • 注意默认 fps 的一致性subclipmakeClipAdditivefps 默认值均为 30,而参考帧换算依赖 referenceFrame / fps——若源动画并非 30fps,务必显式传入真实帧率,否则截取区间或参考姿态会偏移。

单元测试目前仅为占位模块(见 test/unit/src/animation/AnimationUtils.tests.js),因此验证这些方法行为的可靠方式是结合上述 AnimationClip 调用点与官方示例自行构造关键帧数据核对。完整 API 签名可回查文档 docs/pages/AnimationUtils.html.md 与源码 src/animation/AnimationUtils.js

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

项目优选

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