3、关键帧动画:关键帧概念、插值算法(线性插值、贝塞尔插值)、时间轴管理
大家好,我是你们的老朋友。今天我们来聊聊关键帧动画。
说实话,我刚入行那会儿,觉得动画就是“动起来就行”。直到第一次做角色走路,发现脚像踩在冰面上滑行,才意识到关键帧和插值有多重要。嗯,这节课我们就把它彻底搞明白。
3.1 关键帧概念
关键帧,说白了就是动画的“路标”。
你想想看,从A点到B点,中间怎么走?关键帧只记录A和B的位置,中间的过程交给计算机算。这就是关键帧动画的核心思想——只存关键姿势,中间靠插值。
举个例子,一个角色从站立到跳跃:
- 关键帧1:站立姿势(时间0秒)
- 关键帧2:下蹲蓄力(时间0.3秒)
- 关键帧3:腾空最高点(时间0.6秒)
- 关键帧4:落地缓冲(时间1.0秒)
我只存这4个姿势,中间每一帧的姿势都由计算机自动生成。这样既省内存,又方便调整。
核心要点:关键帧不是越多越好。关键帧越多,动画越精细,但调整起来也越痛苦。我个人的习惯是,先定大节奏,再补细节帧。
3.2 插值算法
有了关键帧,接下来就是怎么算中间帧。这里我重点讲两种:线性插值和贝塞尔插值。
3.2.1 线性插值(Lerp)
线性插值是最简单、最直观的方法。公式长这样:
// 线性插值公式
// t 在 [0, 1] 之间
value = start + (end - start) * t
举个例子,从位置(0,0)到(10,0),t=0.5时,位置就是(5,0)。
但线性插值有个大问题——运动太生硬。你想想看,一个球从静止到运动,如果全程匀速,看起来就像被鬼推着走。现实中物体都有加速和减速。
避坑指南:我曾经在做一个弹跳球动画时,全程用了线性插值。结果测试同事说“这球是机器人扔的吧?”后来我才意识到,线性插值只适合机械运动,比如UI滑动、相机平移。
3.2.2 贝塞尔插值
贝塞尔插值就高级多了。它通过控制点来调整运动曲线,实现加速、减速、弹性等效果。
最常用的是三次贝塞尔曲线,有4个控制点:
- P0:起始点
- P1、P2:控制点(决定曲线形状)
- P3:结束点
公式稍微复杂一点:
// 三次贝塞尔曲线公式
// t 在 [0, 1] 之间
B(t) = (1-t)³·P0 + 3(1-t)²·t·P1 + 3(1-t)·t²·P2 + t³·P3
实际项目中,我们很少手写这个公式。引擎一般都提供了现成的接口。比如Unity的AnimationCurve,或者自己封装一个贝塞尔工具类。
我的经验:做角色动画时,我习惯用贝塞尔曲线来控制位移和旋转。比如角色转身,用贝塞尔曲线可以让转身动作更自然——先慢后快再慢,符合人体运动规律。
这里给一个简单的贝塞尔插值实现:
// 贝塞尔插值工具类(简化版)
public class BezierInterpolator {
// 计算三次贝塞尔曲线上的点
public static float Evaluate(float t, float p0, float p1, float p2, float p3) {
float u = 1 - t;
return u*u*u * p0 + 3*u*u*t * p1 + 3*u*t*t * p2 + t*t*t * p3;
}
// 使用示例:从0到1,带缓入缓出
// 控制点设为 (0, 0), (0.2, 0), (0.8, 1), (1, 1)
float t = 0.5f;
float value = Evaluate(t, 0f, 0.2f, 0.8f, 1f);
// value ≈ 0.35,比线性插值的0.5要慢,实现了缓入效果
}
3.3 时间轴管理
有了关键帧和插值算法,接下来就是怎么组织它们。这就是时间轴管理要做的事。
一个完整的时间轴系统,至少包含这几个部分:
| 组件 | 作用 | 我的建议 |
|---|---|---|
| 时间轴(Timeline) | 管理所有轨道,控制播放、暂停、跳转 | 用双缓冲机制,避免播放时修改数据 |
| 轨道(Track) | 管理同一类型的动画,比如位移轨道、旋转轨道 | 每个轨道独立时间线,方便分层控制 |
| 关键帧(Keyframe) | 存储具体的时间点和数值 | 用红黑树或有序列表存储,方便快速查找 |
| 插值器(Interpolator) | 根据时间计算当前值 | 支持策略模式,方便切换插值算法 |
时间轴的核心逻辑其实不复杂:
// 时间轴更新逻辑(伪代码)
void UpdateTimeline(float deltaTime) {
// 1. 更新时间
currentTime += deltaTime;
// 2. 遍历所有轨道
foreach (var track in tracks) {
// 3. 找到当前时间前后的两个关键帧
var prevKey = track.GetPreviousKeyframe(currentTime);
var nextKey = track.GetNextKeyframe(currentTime);
// 4. 计算插值比例
float t = (currentTime - prevKey.time) / (nextKey.time - prevKey.time);
// 5. 根据插值算法计算当前值
float value = interpolator.Evaluate(t, prevKey.value, nextKey.value);
// 6. 应用到目标对象
track.ApplyValue(value);
}
}
关键点:时间轴管理最怕的是“时间跳变”。比如玩家暂停游戏再恢复,如果直接累加deltaTime,动画会突然跳一大段。我建议用时间缩放因子来控制播放速度,而不是直接修改deltaTime。
另外,时间轴还要处理几个边界情况:
- 循环播放:到达末尾后重置到开头
- 反向播放:时间倒流,关键帧顺序反过来
- 时间缩放:慢动作、快进效果
- 跳转:直接跳到某个时间点,需要重新计算所有轨道
小技巧:做时间轴系统时,我习惯把“时间”和“值”分开存储。这样方便做时间重映射——比如同一个动画,可以慢速播放前半段,快速播放后半段。
好了,关键帧动画的核心内容就这些。总结一下:
- 关键帧:存关键姿势,不存中间帧
- 线性插值:简单但生硬,适合UI和机械运动
- 贝塞尔插值:灵活自然,适合角色动画
- 时间轴管理:组织关键帧,控制播放节奏
下一节我们会讲动画混合和状态机,到时候你会看到这些基础概念是怎么组合起来,做出复杂的角色行为的。嗯,今天就到这里,有问题随时交流。