本节摘要:动画系统在每帧第1站按时间对属性做插值,"按时间"是它与"每帧改一点"式手写动画的本质区别。本节确立帧时间纪律,讲关键帧、插值模式、缓动函数与动画组四层结构,并处理动画与手写运动并存的轨道问题。承接第1章的帧流水线,为 4.3 的物理与第6章的游戏循环打底。
同一个"每帧前进 0.1"的物体,在 60 帧的台式机上每秒走 6 个单位,在掉到 30 帧的旧手机上每秒只走 3 个单位——机器越慢世界越慢,这是手写动画的第一宗罪。病根是把"帧"当成了时间单位,而帧长并不恒定:60 帧时每帧 16.7 毫秒,掉帧时可能 33 毫秒。正确做法是一律乘帧时间增量,把"每帧走多少"改写成"每秒走多少":
// 帧时间纪律:速度按秒定义,按帧结算 scene.onBeforeRenderObservable.add(() => { const dt = engine.getDeltaTime() / 1000; // 毫秒转秒:帧长增量 hero.position.z += 2.5 * dt; // 每秒前进 2.5 个单位,与帧率无关 });
引擎的动画系统把这个纪律内化了:动画按"动画时间"推进,帧长波动被自动折算。所以凡是要"精确时长"的运动——角色跳起 0.6 秒、门开 1.2 秒——优先交给动画系统而不是手写增量;手写增量保留给"没有固定时长"的运动(持续按住的方向键移动)。

关键帧声明"第几帧是什么值",插值曲线决定"两点之间怎么走"。引擎的动画对象把两层都管起来:
// 跳起动画:目标属性 position.y,四个关键帧 const jumpAnim = new BABYLON.Animation( "jump", "position.y", // 要插值的属性路径 60, // 帧率:每秒 60 个"动画帧" BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE // 循环模式:播完从头 ); jumpAnim.setKeys([ { frame: 0, value: 0 }, { frame: 20, value: 3 }, { frame: 40, value: 3 }, { frame: 60, value: 0 }, ]); // 插值曲线:快起慢落的经典缓动 const ease = new BABYLON.CubicEase(); ease.setEasingMode(BABYLON.EasingFunction.EASINGMODE_EASEINOUT); jumpAnim.setEasingFunction(ease); hero.animations.push(jumpAnim); scene.beginAnimation(hero, 0, 60, true); // 循环播放第 0 到 60 帧
缓动函数是"动画性格"的全部:线性是机器,先快后慢是刹车,先慢后快是蓄力。没有缓动的动画无论关键帧多精细都带着一股机器味,这是"动画看着假"的第一嫌疑。
单个动画管一个属性,动画组把多个动画编成一台戏——角色跳起时腿收、手摆、披帛飘,四个动画同起同停、统一倍速。编组之外,混合权重让两段动画叠印:走与跑各占一半,急停时权重渐移,过渡不跳变:
// 动画组:一次管理整台戏 const jumpGroup = new BABYLON.AnimationGroup("jumpG", scene); jumpGroup.addTargetedAnimation(jumpAnim, hero); jumpGroup.addTargetedAnimation(armSwingAnim, heroArm); jumpGroup.normalize(0, 60); // 归一化帧区间,倍速控制的前提 jumpGroup.play(true); // 权重混合:走路与跑步的渐变 // 两个动画组同时驱动一副骨架,各带权重,总权重不必为 1 但建议归一 walkGroup.start(true, 1.0, walkGroup.from, walkGroup.to); runGroup.start(true, 0.0, runGroup.from, runGroup.to); // 初始权重 0:跑步先隐藏 // 加速时把权重从走渐移到跑,两段动画同时活在同一副骨架上 speedRatio += 0.02; walkGroup.setWeightForAllAnimatables(1 - speedRatio); runGroup.setWeightForAllAnimatables(speedRatio);
⚠️ 常见坑:动画系统与手写运动写同一个属性必然打架——动画每帧第1站写 position,你的回调又在渲染前写一次,谁后写谁赢,表现为"抽搐"。同一属性只允许一个所有者,要切换先 stopAnimation。
背景:第三人称角色,走跑切换用两段动画硬切,观感总是"闪一下"。操作:两段动画同时驱动同一副骨架——都保持播放,初始权重 1 与 0;速度变化时按速度区间线性移动两个权重,用 0.3 秒完成过渡;原地化的动画配代码位移,位移速度按混合后的权重结算,脚步频率与前进速度才对得上。结果:加速减速连续自然,急停也不再跳变。解读:混合的本质是"让两台插值器同时活着",权重是各自的音量旋钮;过渡期两套关键帧同时在插值,成本约为单动画的 1.6 倍,速度稳定后把权重 0 的组暂停,这笔开销随时能收回。变式:上下半身分离(边跑边挥手)走骨架分部的路子,思路同源——同一骨架的不同部位允许不同的所有者,冲突只发生在同一属性上。
动画播久了位置"漂移"怎么回事? 循环动画的首末关键帧值不完全一致,浮点误差逐圈累积。解法:首末帧显式写成同一个值,位移类属性干脆别放进循环动画,交给代码按秒结算。
暂停游戏时动画怎么办? 动画时间跟着引擎时钟走,把引擎的时钟速率调为零,动画、粒子、相机惯性一起停,比逐个暂停动画组省事,也是做暂停菜单的标准姿势。
属性级的动画讲完了。下一节处理"用户的手"——点击如何穿过屏幕命中三维世界里的那个物体。