6.2 动画系统:剪辑、混合与手写缓动 本节摘要:动效传动有两条链:官方动画系统按时间轴播放与混合剪辑,手写缓动在两个状态间插值。本节用叉车的行走与举升演示 AnimationMixer 的剪辑调度,用货架呼吸灯演示缓动手法,并给出一张分工表——选对传动链,动效代码就短一半。 两条传动链,两副脾气 第 5 章进场时转交的 clips(动画片段)是设计师在建模软件里排好的时间轴:关键帧、骨骼曲线、事件标记全都封在片段里。AnimationMixer 是它们的播放机——把模型绑给 mixer,把片段交给 mixer,每帧 mixer.update(dt) 推进,模型就按设计师的编排动起来。它的强项是"编排":多片段并行、淡入淡出过渡、按权重混合,都是一行配置。
本节摘要:动效传动有两条链:官方动画系统按时间轴播放与混合剪辑,手写缓动在两个状态间插值。本节用叉车的行走与举升演示 AnimationMixer 的剪辑调度,用货架呼吸灯演示缓动手法,并给出一张分工表——选对传动链,动效代码就短一半。
第 5 章进场时转交的 clips(动画片段)是设计师在建模软件里排好的时间轴:关键帧、骨骼曲线、事件标记全都封在片段里。AnimationMixer 是它们的播放机——把模型绑给 mixer,把片段交给 mixer,每帧 mixer.update(dt) 推进,模型就按设计师的编排动起来。它的强项是"编排":多片段并行、淡入淡出过渡、按权重混合,都是一行配置。
手写缓动的强项是"插值":从当前值到目标值走出一条优雅曲线。第 4 章的一键特写已经在用(lerp 加 easeOutCubic),本节把它推广成通用工具——任何数值属性(位置、颜色、缩放、强度)都能在任意两个状态间滑行。两条链的分工一句话:时间轴上的戏交给 mixer,状态之间的路交给缓动。用 mixer 播放一个"从 A 滑到 B"的简单位移,等于为三秒钟的活建一条生产线;用缓动硬写骨骼动画,等于徒手掰关节。

背景:叉车模型带两条剪辑(drive 行走、lift 举升),沙盘要求:点击"作业"按钮后叉车边走边举升,动作衔接不生硬;同时货架指示灯做呼吸式明暗,烘托"运转中"氛围。
操作:骨骼戏交给 mixer 与交叉过渡,呼吸灯交给缓动式正弦,两条链并行互不干扰。
const mixer = new THREE.AnimationMixer(forklift.model); const actions = { drive: mixer.clipAction(forklift.clips.find(c => c.name === 'drive')), lift: mixer.clipAction(forklift.clips.find(c => c.name === 'lift')) }; actions.drive.play(); // 先进入待机循环 // 点击"作业":交叉过渡到举升,播完回行走 function startWork() { actions.lift.reset(); actions.lift.setLoop(THREE.LoopOnce); // 举升是一次性动作 actions.lift.clampWhenFinished = true; // 播完停在最后一帧 actions.drive.crossFadeTo(actions.lift, 0.3, false); // 0.3 秒淡入淡出 actions.lift.addEventListener('finished', () => { actions.lift.crossFadeTo(actions.drive, 0.3, false); // 收工回行走 }); } // 呼吸灯:自写缓动,每帧向目标逼近的正弦调制 const lampMat = shelf.children[0].material; stage.loop((dt, elapsed) => { mixer.update(dt); // 传动链一:mixer 推进所有剪辑 const glow = 0.5 + Math.sin(elapsed * 2.2) * 0.5; // 0 到 1 往复 lampMat.emissiveIntensity = 0.2 + glow * 0.8; // 传动链二:属性插值 stage.renderer.render(stage.scene, stage.camera); });
结果:点击按钮,叉车平稳地从行走切到举升(0.3 秒的过渡带吃掉了生硬的关节跳变),举完自动切回行走;货架灯带以 2.2 弧度每秒的节拍呼吸。两条传动链在同一心跳上并行,互不抢戏。
解读:crossFadeTo 的第三个参数容易被忽略——false 表示同步过渡(两条剪辑时间轴对齐),true 表示各自按比例走。动作衔接类过渡几乎都该用 false,否则剪辑各自的起点错位,接缝处照样抽搐。另一个细节是 clampWhenFinished:一次性动作播完默认回到第 0 帧,叉车会"举到一半突然放下";钳在末帧配合 finished 事件回切,才构成完整的动作闭环。呼吸灯那段刻意不用 mixer:一个发光强度的正弦调制建剪辑纯属杀鸡用牛刀——分工表的价值就在这种"不上生产线"的判断里。
变式:多条剪辑叠加(边走边鸣笛)用权重混合:把 drive 的权重设 1、horn 的权重设 0.4,setEffectiveWeight 一行,两段动作按比例融合。自制动效想要"点到即止"的弹性手感(缩放脉冲后回弹),把 easeOutCubic 换成带过冲的 easeOutBack——缓动函数库是公共知识,套用即可,别手搓三阶贝塞尔。
混合器上手快,忘事也快,三件常忘事按报障频率排。其一,忘了每帧 mixer.update:剪辑"加载成功却一动不动",八成是循环里漏了推进——mixer 不是自走的钟,心跳不喂它就停在第一帧。其二,多个模型各建各的 mixer:每个 mixer 只管自己绑定的模型,A 模型的剪辑别想借 B 的 mixer 播放;场景里五个带动画的外协件,就老老实实五个 mixer,各自的 update 一行都不能少。其三,一次性动作播完不回收:clipAction 默认反复播放,配了 LoopOnce 又忘了 clampWhenFinished,动作播完瞬间弹回第一帧——货箱"举到一半突然归位"就是这么来的。三件忘事对应三行代码,检查成本不到一分钟,先查它们再怀疑别的。
骨骼戏与属性动效都已上线。下一节接操纵杆:射线拾取把点击翻译成命中,交互闭环的最后一块拼图。