第一章:动画基础与数学原理

大家好,我是你们这趟旅程的向导。做游戏动画这些年,我最大的感触就是——数学这东西,你越躲它,它越找你麻烦。今天咱们就把向量、矩阵、四元数这些老朋友请出来,好好聊聊它们在游戏动画里到底怎么用。

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