数学基础回顾:向量运算、矩阵与变换、欧拉角与四元数

各位同学,欢迎来到第二章。说实话,数学基础这部分,很多做游戏开发的朋友一开始都觉得枯燥。我当年入行时也这么想——直到第一次在项目中遇到角色旋转时出现“万向锁”,整个动画系统直接崩掉,我才老老实实回来啃数学。

这一章,我们不讲纯理论。我会结合自己在物理引擎开发中踩过的坑,带你把向量、矩阵、四元数这些“武器”真正用起来。你想想看,没有这些基础,你连一个刚体怎么飞出去都算不准。

2.1 向量运算:点积与叉积

向量是物理引擎的基石。说白了,位置、速度、力,全是向量。我个人习惯把向量看作“带箭头的线段”——方向比长度更重要。

2.1.1 点积(Dot Product)

点积的计算公式很简单:a · b = |a||b|cosθ。但它的物理意义才是关键。

  • 投影长度:点积可以算出向量a在b方向上的投影。我在做碰撞检测时,经常用这个来判断两个物体是否“面对面”。
  • 夹角判断:如果点积大于0,夹角小于90度;等于0,垂直;小于0,大于90度。嗯,这里要注意——点积为0不代表向量没有关系,只是正交而已。

实用场景:计算光照强度时,用光线方向与法线的点积,就能得到漫反射系数。我在第一个渲染器里就用了这个,效果立竿见影。

// C++ 点积实现
float DotProduct(const Vector3& a, const Vector3& b) {
    return a.x * b.x + a.y * b.y + a.z * b.z;
}

2.1.2 叉积(Cross Product)

叉积的结果是一个向量,垂直于原来的两个向量。方向由右手定则决定。

  • 法线计算:给定三角形三个顶点,用两条边的叉积就能得到法线。我在做地形碰撞时,每帧都要算几千个三角形的法线。
  • 力矩计算:力矩 = 力臂 × 力。叉积在这里直接上场。

避坑指南:我曾经在计算叉积时搞反了顺序,结果所有物体的旋转方向都反了。调试了整整一个下午才发现——记住,a × b = -(b × a),顺序很重要!

// C++ 叉积实现
Vector3 CrossProduct(const Vector3& a, const Vector3& b) {
    return Vector3(
        a.y * b.z - a.z * b.y,
        a.z * b.x - a.x * b.z,
        a.x * b.y - a.y * b.x
    );
}

2.2 矩阵与变换

矩阵是游戏引擎的“翻译官”。它把物体的位置、旋转、缩放打包成一个4x4的表格,方便我们一次性处理。

2.2.1 变换矩阵的结构

一个标准的4x4变换矩阵长这样:

RxxRxyRxzTx
RyxRyyRyzTy
RzxRzyRzzTz
0001

左上角3x3是旋转和缩放,右上角3x1是平移。最后一行永远是[0,0,0,1]——这是齐次坐标的约定。

我的习惯:在代码里,我通常用行主序存储矩阵。但OpenGL用列主序,DirectX用行主序。你最好一开始就定好,不然后面改起来很痛苦。

2.2.2 矩阵乘法与组合变换

多个变换可以合并成一个矩阵。比如先旋转再平移:M = T * R。注意顺序——矩阵乘法不满足交换律。

为什么会这样?你想想看,先旋转再平移,和先平移再旋转,结果完全不同。我在做角色控制器时,就因为这个顺序问题,角色总是跑到奇怪的位置。

// 矩阵乘法示例
Matrix4x4 Multiply(const Matrix4x4& a, const Matrix4x4& b) {
    Matrix4x4 result;
    for (int i = 0; i < 4; i++)
        for (int j = 0; j < 4; j++)
            result.m[i][j] = a.m[i][0]*b.m[0][j] + 
                             a.m[i][1]*b.m[1][j] + 
                             a.m[i][2]*b.m[2][j] + 
                             a.m[i][3]*b.m[3][j];
    return result;
}

2.3 欧拉角与四元数

说到旋转,就不得不提这两个“冤家”。欧拉角直观但有问题,四元数难懂但好用。

2.3.1 欧拉角的问题

欧拉角用三个角度(俯仰、偏航、翻滚)描述旋转。听起来简单,但有个致命缺陷——万向锁

当俯仰角达到±90度时,偏航和翻滚的旋转轴会重合,导致失去一个自由度。我曾经在飞行模拟器项目中遇到这个问题,飞机在做筋斗时突然卡住,检查了半天才发现是万向锁在作怪。

经验之谈:如果你的应用只需要小角度旋转(比如角色头部转动),欧拉角还能用。但涉及全自由度旋转,赶紧换四元数。

2.3.2 四元数入门

四元数可以看作“带角度的轴”。它用四个数表示:q = (w, x, y, z),其中w是标量部分,(x,y,z)是向量部分。

四元数的好处:

  • 无万向锁:这是最大的优势
  • 插值平滑:用球面线性插值(SLERP)可以实现平滑旋转
  • 计算高效:比矩阵少用内存,组合旋转也更快
// 四元数乘法(组合旋转)
Quaternion Multiply(const Quaternion& a, const Quaternion& b) {
    return Quaternion(
        a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z,
        a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
        a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
        a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w
    );
}

实用技巧:我建议你在引擎内部统一用四元数存储旋转,只在UI或调试时转成欧拉角。这样既避免了万向锁,又方便人工理解。

2.3.3 欧拉角与四元数的转换

有时候你不得不处理欧拉角(比如从动画文件读取数据)。这时候就需要转换函数:

// 欧拉角转四元数(ZYX顺序)
Quaternion FromEuler(float yaw, float pitch, float roll) {
    float cy = cos(yaw * 0.5f);
    float sy = sin(yaw * 0.5f);
    float cp = cos(pitch * 0.5f);
    float sp = sin(pitch * 0.5f);
    float cr = cos(roll * 0.5f);
    float sr = sin(roll * 0.5f);
    
    return Quaternion(
        cr * cp * cy + sr * sp * sy,
        sr * cp * cy - cr * sp * sy,
        cr * sp * cy + sr * cp * sy,
        cr * cp * sy - sr * sp * cy
    );
}

注意:转换时一定要明确旋转顺序。不同的引擎(Unity、Unreal、Godot)用的顺序可能不同。我曾经因为没注意这个,导致模型在两个引擎间来回导时旋转全乱了。

小结

这一章的内容,说白了就是三个工具:向量算方向,矩阵做变换,四元数管旋转。你不需要一次全记住,但一定要理解它们各自的适用场景。

下一章,我们会把这些数学工具用到刚体运动上。到时候你会发现,今天学的每一个公式,都会在物理模拟中派上用场。

嗯,今天就到这里。记得动手写写代码,光看是学不会的。