关于 threejs-impl-animation
面向 Three.js 开发者的动画实现参考,涵盖动画片段、动作与混合器,并说明播放、混合、计时及常见实现问题。
主要功能
- 01混合器与动作生命周期指南
- 02片段播放与循环模式
- 03淡入淡出和交叉淡化模式
- 04叠加与变形目标动画
- 05基于 Clock 的帧计时
适用场景
- 01播放 GLTF 角色动画
- 02切换不同动画状态
- 03叠加呼吸或受击动作
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ impertio-studio/ three.js-claude-skill-package --skill threejs-impl-animation安装后在 Claude Code 中输入/threejs-impl-animation确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.gitkeepreferences/anti-patterns.mdexamples.mdmethods.md详细介绍
threejs-impl-animation
快速参考
架构
AnimationClip (data: array of KeyframeTrack objects)
└── AnimationAction (playback controller: play, pause, fade, crossfade)
└── AnimationMixer (master scheduler: one per animated root object)
└── Clock (provides delta time for mixer.update)
每个动画根对象始终只创建一个 AnimationMixer。
每帧始终在渲染循环中调用 mixer.update(delta)。
切勿直接实例化 AnimationAction——始终使用 mixer.clipAction(clip)。
必要导入
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
最简动画设置
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const clock = new THREE.Clock();
let mixer;
const loader = new GLTFLoader();
loader.load('character.glb', (gltf) => {
scene.add(gltf.scene);
mixer = new THREE.AnimationMixer(gltf.scene);
// Play all animations from the GLTF file
gltf.animations.forEach((clip) => {
mixer.clipAction(clip).play();
});
});
function animate() {
const delta = clock.getDelta();
if (mixer) mixer.update(delta);
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
重要警告
切勿忘记在渲染循环中调用 mixer.update(delta)——否则动画不会播放。
切勿使用 new Date() 或 performance.now() 手动计算 delta——始终使用 THREE.Clock 或 renderer.setAnimationLoop,它们能提供稳定的帧时间。
切勿在渲染循环中反复调用 mixer.clipAction(clip)——虽然它会在内部缓存,但查找会带来不必要的开销。始终将返回的 action 存储在变量中。
切勿每帧都调用 .play()——只需调用一次即可开始播放。对正在播放的 action 再次调用 .play() 不会产生效果,但这表明你对其用法理解有误。
重新启动已停止或已结束的 action 时,始终先调用 .reset() 再调用 .play(),否则 action 可能会从上次停止的位置继续播放。
使用 LoopOnce 时,始终设置 action.clampWhenFinished = true——否则 action 结束时会重置到第一帧。
AnimationMixer
用于驱动单个对象层级中所有动画 action 的主调度器。
构造函数
const mixer = new THREE.AnimationMixer(rootObject);
rootObject 是动画模型的根 Object3D(通常为 gltf.scene)。
属性
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
.time | number | 0 | 全局混合器时间,单位为秒 |
.timeScale | number | 1 | 全局速度倍率;设为 0 会暂停所有 action |
方法
| 方法 | 返回值 | 描述 |
|---|---|---|
.clipAction(clip, root?, blendMode?) | AnimationAction | 返回或创建 clip 对应的 action |
.existingAction(clip, root?) | AnimationAction | null | 返回之前创建的 action,或返回 null |
.update(delta) | this | 将混合器推进 delta 秒——必须每帧调用 |
.setTime(seconds) | this | 设置全局时间并更新所有 action |
.stopAllAction() | this | 停用所有已调度的 action |
.getRoot() | Object3D | 返回混合器的根对象 |
.uncacheAction(clip, root?) | void | 释放缓存的 action |
.uncacheClip(clip) | void | 释放 clip 数据 |
.uncacheRoot(root) | void | 释放根对象数据 |
事件
通过 mixer.addEventListener(type, callback) 监听:
| 事件 | 触发时机 |
|---|---|
'finished' | action 播放完成(仅当使用 LoopOnce 且 clampWhenFinished = true 时) |
'loop' | action 完成一轮循环 |
事件对象属性:{ action, loopDelta, type }。
混合模式
将其作为第三个参数传递给 mixer.clipAction(clip, root, blendMode):
| 常量 | 行为 |
|---|---|
THREE.NormalAnimationBlendMode | 标准混合(默认) |
THREE.AdditiveAnimationBlendMode | 叠加在基础动画之上 |
AnimationAction
控制单个动画 clip 的播放。切勿直接实例化。
属性
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
.blendMode | number | NormalAnimationBlendMode | 混合策略 |
.clampWhenFinished | boolean | false | 完成后暂停在最后一帧 |
.enabled | boolean | true | 禁用但不重置 |
.loop | number | LoopRepeat | 循环模式 |
.paused | boolean | false | 冻结播放 |
.repetitions | number | Infinity | 循环次数 |
.time | number | 0 | 本地时间(秒) |
.timeScale | number | 1 | 速度:0 表示暂停,负值表示倒放 |
.weight | number | 1 | 混合影响 [0, 1] |
.zeroSlopeAtEnd | boolean | true | 在循环结束时平滑插值 |
.zeroSlopeAtStart | boolean | true | 在循环开始时平滑插值 |
循环模式
| 常量 | 行为 |
|---|---|
THREE.LoopOnce | 播放一次后停止 |
THREE.LoopRepeat | 每次循环都从头开始 |
THREE.LoopPingPong | 在正向和反向之间交替播放 |
播放方法
| 方法 | 描述 |
|---|---|
.play() | 开始播放 |
.stop() | 停止并重置到开头 |
.reset() | 将时间、权重和速度重置为初始状态 |
.startAt(mixerTime) | 延迟到指定的混合器时间后开始 |
淡入淡出和交叉淡化方法
| 方法 | 描述 |
|---|---|
.fadeIn(duration) | 将权重从 0 淡入到 1 |
.fadeOut(duration) | 将权重从 1 淡出到 0 |
.crossFadeFrom(fadeOutAction, duration, warp) | 从另一个动作交叉淡化到此动作 |
.crossFadeTo(fadeInAction, duration, warp) | 从此动作交叉淡化到另一个动作 |
.stopFading() | 取消所有正在进行的淡化 |
速度和计时方法
| 方法 | 描述 |
|---|---|
.halt(duration) | 在指定时长内将 timeScale 减速至 0 |
.warp(startScale, endScale, duration) | 平滑过渡播放速度 |
.stopWarping() | 取消所有正在进行的速度渐变 |
.setDuration(seconds) | 调整 timeScale,使一次循环恰好持续 seconds 秒 |
.setEffectiveTimeScale(scale) | 设置有效时间缩放 |
.setEffectiveWeight(weight) | 设置有效权重 |
.setLoop(mode, repetitions) | 设置循环模式和次数 |
.syncWith(otherAction) | 与另一个动作同步时间 |
查询方法
| 方法 | 返回值 | 描述 |
|---|---|---|
.isRunning() | boolean | 仅当正在播放时返回 true |
.isScheduled() | boolean | 如果已调用 .play(),则返回 true |
.getClip() | AnimationClip | 关联的片段 |
.getMixer() | AnimationMixer | 所属的混合器 |
.getRoot() | Object3D | 根对象 |
.getEffectiveTimeScale() | number | 计算后的时间缩放 |
.getEffectiveWeight() | number | 计算后的权重 |
AnimationClip
一组可重复使用的关键帧轨道。通常从 GLTF 文件加载。
构造函数
const clip = new THREE.AnimationClip(name, duration, tracks, blendMode);
name-- 字符串标识符(GLTF 片段使用文件中的名称)duration-- 秒数;-1表示根据轨道自动计算tracks--KeyframeTrack对象数组blendMode-- 可选的混合模式常量
主要静态方法
| 方法 | 描述 |
|---|---|
AnimationClip.findByName(arrayOrObject, name) | 按名称查找片段 |
AnimationClip.CreateFromMorphTargetSequence(name, targets, fps, noLoop) | 根据变形目标创建片段 |
AnimationClip.parse(json) | 从 JSON 反序列化 |
KeyframeTrack 类型
| 轨道类型 | 值类型 | 用途 |
|---|---|---|
VectorKeyframeTrack | Vector3 | 位置、缩放 |
QuaternionKeyframeTrack | Quaternion | 旋转(使用球面线性插值) |
NumberKeyframeTrack | number | 不透明度、强度 |
BooleanKeyframeTrack | boolean | 可见性切换 |
ColorKeyframeTrack | Color | 颜色动画 |
StringKeyframeTrack | string | 离散字符串值 |
插值模式
| 常量 | 行为 |
|---|---|
THREE.InterpolateDiscrete | 阶跃函数,不进行平滑 |
THREE.InterpolateLinear | 线性插值(默认) |
THREE.InterpolateSmooth | 三次样条插值 |
PropertyBinding 路径格式
"meshName.position" // animate position
"meshName.material.opacity" // animate material property
"meshName.morphTargetInfluences[0]" // animate morph target
"boneName.quaternion" // animate bone rotation
时钟
构造函数
const clock = new THREE.Clock(autoStart); // autoStart defaults to true
方法
| 方法 | 返回值 | 描述 |
|---|---|---|
.getDelta() | number | 距上次调用 getDelta() 的秒数 |
.getElapsedTime() | number | 总经过时间(秒) |
.start() | void | 启动时钟 |
.stop() | void | 停止时钟,但不重置 |
淡入淡出模式(角色状态机)
const actions = {};
gltf.animations.forEach((clip) => {
actions[clip.name] = mixer.clipAction(clip);
});
let currentAction = actions['Idle'];
currentAction.play();
function switchAction(toName, duration = 0.5) {
const toAction = actions[toName];
toAction.reset();
toAction.setEffectiveTimeScale(1);
toAction.setEffectiveWeight(1);
toAction.crossFadeFrom(currentAction, duration, true);
toAction.play();
currentAction = toAction;
}
交叉淡化前,务必对即将进入的动作调用 .reset()。
务必保存当前动作的引用,以用于下一次过渡。
加法动画混合
const baseAction = mixer.clipAction(baseClip);
const additiveAction = mixer.clipAction(
additiveClip, undefined, THREE.AdditiveAnimationBlendMode
);
baseAction.play();
additiveAction.play();
additiveAction.setEffectiveWeight(0.5);
分层效果(如呼吸、受伤反应、瞄准偏移)请使用加法混合。
变形目标动画
// Manual control
mesh.morphTargetInfluences[0] = Math.sin(elapsed) * 0.5 + 0.5;
// Via GLTF animation clip (preferred)
const morphAction = mixer.clipAction(morphClip);
morphAction.play();
参考链接
- references/methods.md——完整的 API 签名
- references/examples.md——可运行的代码示例
- references/anti-patterns.md——不应采用的做法
官方来源
- https://threejs.org/docs/#api/en/animation/AnimationMixer
- https://threejs.org/docs/#api/en/animation/AnimationAction
- https://threejs.org/docs/#api/en/animation/AnimationClip
- https://threejs.org/docs/#api/en/animation/KeyframeTrack
- https://threejs.org/docs/#api/en/core/Clock
- https://threejs.org/examples/#webgl_animation_skinning_blending