第一章:动画基础与数学原理
大家好,我是你们这趟旅程的向导。做游戏动画这些年,我最大的感触就是——数学这东西,你越躲它,它越找你麻烦。今天咱们就把向量、矩阵、四元数这些老朋友请出来,好好聊聊它们在游戏动画里到底怎么用。
1.1 向量:动画世界的坐标基石
说白了,向量就是一个有方向、有大小的量。在游戏里,角色的位置、速度、朝向,全是向量。
向量的基本运算
- 加法:角色移动时,当前位置 + 位移向量 = 新位置
- 减法:两个角色之间,A位置 - B位置 = 从B指向A的向量
- 点积:判断两个方向是否一致。比如角色面朝方向与目标方向的夹角
- 叉积:求垂直于两个向量的新向量。常用于计算法线
避坑指南:我曾经在项目里遇到一个bug,角色转身时总是突然跳转180度。查了半天,发现是点积结果没做归一化处理。记住,点积前一定要确保两个向量都是单位向量!
// 向量归一化示例
Vector3 normalized = vector.normalized; // 确保长度为1
float dot = Vector3.Dot(a.normalized, b.normalized);
1.2 矩阵:空间变换的瑞士军刀
矩阵这东西,刚学时觉得抽象,用多了你会发现它就是个「变换打包器」。一个4x4矩阵,能把旋转、缩放、平移全塞进去。
矩阵在动画中的典型应用
- 模型矩阵:把角色从模型空间变换到世界空间
- 视图矩阵:把世界空间变换到摄像机空间
- 投影矩阵:把3D空间投影到2D屏幕
- 骨骼矩阵:每个骨骼的局部变换,最终合成蒙皮矩阵
个人经验:我习惯在调试时把矩阵打印出来,看看第4列的前三个数——那就是平移量。如果角色莫名其妙飞到天上去,多半是矩阵乘法顺序搞反了。
// 矩阵乘法顺序很重要!
Matrix4x4 localToWorld = parentMatrix * localMatrix; // 先局部,再父级
// 如果写成 localMatrix * parentMatrix,结果完全不一样
1.3 四元数:旋转的终极解决方案
为什么不用欧拉角?因为万向锁啊!你想想看,当俯仰角达到90度时,偏航和翻滚就分不清了。四元数完美解决了这个问题。
四元数的核心概念
- 一个四元数 = (x, y, z, w),其中w是实部,xyz是虚部
- 它表示绕某个轴旋转某个角度
- 两个四元数相乘 = 旋转叠加
- 四元数求逆 = 反向旋转
注意:四元数虽然好,但别直接用它的xyz分量做插值!那会得到完全错误的结果。必须用Slerp。
// 四元数基本操作
Quaternion rotation = Quaternion.AngleAxis(45f, Vector3.up); // 绕Y轴旋转45度
Quaternion combined = rotA * rotB; // 先rotB,再rotA
Vector3 rotatedDir = rotation * Vector3.forward; // 旋转一个方向向量
1.4 插值算法:让动画丝滑起来
动画的本质是什么?说白了就是在关键帧之间「猜」出中间状态。Lerp和Slerp就是干这个的。
1.4.1 Lerp(线性插值)
Lerp是最简单的插值方式,适合位置、颜色、浮点数这些线性量。
// Lerp的底层实现
float Lerp(float a, float b, float t) {
return a + (b - a) * t; // t在0到1之间
}
// 向量Lerp
Vector3 Lerp(Vector3 a, Vector3 b, float t) {
return new Vector3(
a.x + (b.x - a.x) * t,
a.y + (b.y - a.y) * t,
a.z + (b.z - a.z) * t
);
}
避坑指南:我曾经用Lerp插值旋转,结果角色在旋转180度时出现了奇怪的扭曲。因为Lerp处理的是线性空间,而旋转是球面空间。这时候就该Slerp上场了。
1.4.2 Slerp(球面线性插值)
Slerp专门用于四元数插值,它沿着球面走最短路径,保证旋转的平滑性。
// Slerp的底层实现(简化版)
Quaternion Slerp(Quaternion a, Quaternion b, float t) {
// 1. 计算两个四元数的夹角
float dot = a.x*b.x + a.y*b.y + a.z*b.z + a.w*b.w;
// 2. 如果夹角大于90度,取反以保证走最短路径
if (dot < 0) {
b = -b;
dot = -dot;
}
// 3. 计算插值系数
float theta = acos(dot);
float sinTheta = sin(theta);
float scaleA = sin((1-t) * theta) / sinTheta;
float scaleB = sin(t * theta) / sinTheta;
// 4. 加权求和
return a * scaleA + b * scaleB;
}
个人经验:实际项目中,我建议直接用引擎提供的Slerp函数。自己实现的话,要注意处理dot接近1时的数值稳定性——这时候sinTheta接近0,除法会出问题。一般会加个阈值判断,如果dot > 0.9995,直接用Lerp近似。
1.5 实战:角色转身动画
咱们把上面这些知识串起来,看看一个完整的角色转身动画怎么做。
// 角色平滑转身示例
public void SmoothRotate(Transform character, Vector3 targetDirection, float speed) {
// 1. 计算目标旋转
Quaternion targetRotation = Quaternion.LookRotation(targetDirection);
// 2. 用Slerp平滑插值
character.rotation = Quaternion.Slerp(
character.rotation,
targetRotation,
Time.deltaTime * speed
);
// 3. 可选:限制旋转速度
float angle = Quaternion.Angle(character.rotation, targetRotation);
if (angle < 0.1f) {
character.rotation = targetRotation; // 直接锁定
}
}
关键点:这里用Slerp而不是Lerp,因为旋转是球面空间。用Lerp的话,角色转身时会「飘」起来,看着特别假。
1.6 性能优化建议
| 操作 | 性能开销 | 优化建议 |
|---|---|---|
| 向量归一化 | 中等 | 尽量复用已归一化的向量 |
| 矩阵乘法 | 高 | 能合并的矩阵提前合并 |
| 四元数乘法 | 低 | 比矩阵乘法快,放心用 |
| Slerp | 中等 | 每帧调用次数控制在100次以内 |
性能警告:我曾经在一个项目里,每帧对1000个角色做Slerp,结果帧率直接掉到20。后来改成每4帧更新一次,加上LOD(距离远的角色用Lerp近似),问题就解决了。
1.7 本章小结
嗯,到这里咱们把动画数学的底子打好了。向量是坐标,矩阵是变换,四元数是旋转,Lerp和Slerp是插值。这些工具组合起来,就能做出流畅的角色动画。
下一章咱们聊聊骨骼动画和蒙皮,到时候这些数学知识会派上大用场。记住,数学不是用来背的,是用来解决问题的。遇到动画bug时,先想想是不是向量没归一化,或者插值用错了方法。
公众号:蓝海资料掘金营,微信deep3321