SpaceMath 旋转矩阵 API 全解:mat3_st 列优先布局与 61 个矩阵接口详解

一、引言

旋转矩阵是三维旋转四种表示(欧拉角、旋转矩阵、四元数、轴角)中唯一无歧义、可直接作用于向量的表示:9 个数唯一确定一个旋转,v' = R·v 一次矩阵乘向量即完成旋转。它也是计算机图形学、机器人运动学、惯性导航姿态解算中最底层的计算形式——欧拉角和四元数最终往往都要落成矩阵去变换点云。

SpaceMath 中与旋转矩阵相关的接口分布在两个文件中:

文件接口数职责
matrix.c39(38 个 mat3_* + 主构造 mat3())通用 3×3 矩阵线性代数:构造、判定、度量、算术、求逆、级数、正交化、插值、基本旋转矩阵构造
rotation.c22矩阵与其他旋转表示的互转、相对旋转、矩阵作为/被旋转量

本文逐一解析这 61 个接口的功能、用法和注意事项,并给出可直接编译运行的 C 语言示例。文中示例均在 CFG_FLOATWORD_BITSIZE = 64(双精度)构建下实测验证。


二、数据结构 mat3_st:列优先存储与四重视图

理解 mat3_st 的内存布局是用好一切矩阵接口的前提。

2.1 列优先(Column-Major)布局

mat3_st 是一个联合体(union),同一块 9 元素内存提供四套访问视图(见 spacemath.h L296-328):

数学布局(行×列):        内存布局(列优先,9 个连续 ftype_t):
[ m11 m12 m13 ]           [ m11  m21  m31  m12  m22  m32  m13  m23  m33 ]
[ m21 m22 m23 ]     =     [ v[0] v[1] v[2] v[3] v[4] v[5] v[6] v[7] v[8] ]
[ m31 m32 m33 ]           [ vv[0][0] vv[0][1] vv[0][2] vv[1][0] ...      ]
                          [ v3[0].x  v3[0].y  v3[0].z  v3[1].x  ...      ]

四套视图对照:

视图类型索引含义示例
具名成员m.mij第 i 行第 j 列m.m12 是第 1 行第 2 列
一维数组m.v[9]列优先展开m.v[1] 是 m21,不是 m12!
二维数组m.vv[3][3]vv[列][行]m.vv[1][0] 是 m12
列向量m.v3[3]三个 vec3_st 列m.v3[0] 是第 1 列

头文件还提供了 MINDEX_M11 ... MINDEX_M33 枚举常量,用于按列优先索引 v[9](如 MINDEX_M12 == 3)。

2.2 为什么用列优先

  1. 物理意义直白:旋转矩阵的三个列向量就是坐标系三个基向量旋转后的像。m.v3[0] 直接给出"X 轴转到了哪里",做姿态可视化、方向提取(如机体系前向矢量)时零计算量。
  2. 符合数学惯例:与 Fortran、MATLAB、OpenGL 的列主序传统一致,v' = R·v 中 v 按列向量理解。
  3. cache 友好:按列遍历时内存连续,利于 CPU 缓存预取。

2.3 数组视图的自洽性(已修复)

mat3_st 的 v[9]、vv[3][3]、v3[3] 三套视图均为列优先。对应的三个数组构造函数 mat3_v、mat3_vv、mat3_v3 现已统一按列优先读取入参,因此按视图原样重建矩阵恒成立:

mat3_v(m.v)   == m;   // ✓ 自洽
mat3_vv(m.vv) == m;   // ✓ 自洽
mat3_v3(m.v3) == m;   // ✓ 自洽

三、构造函数(7 个)

接口功能入参约定
mat3(m11,...,m33)9 分量构造按数学行序传参(m11, m12, m13, m21, …)
mat3_v(v)一维数组构造按列优先读取,v[0…2]=第1列,v[3…5]=第2列,v[6…8]=第3列
mat3_vv(vv)二维数组构造按列优先读取,vv[列][行]
mat3_v3(v3)三列向量构造v3[0..2] 依次为第 1/2/3 列
mat3_zero()全零矩阵—
mat3_one()全 1 矩阵—
mat3_identity()单位矩阵—

3.1 主构造 mat3()

9 个参数按数学书写的行序排列,与直觉一致:

// 手写绕 Z 轴转 90° 的旋转矩阵
// [ 0 -1  0 ]
// [ 1  0  0 ]
// [ 0  0  1 ]
mat3_st m = mat3(0, -1, 0,
                 1,  0, 0,
                 0,  0, 1);

3.2 数组构造的统一约定(列优先)

三个数组构造函数现已统一按列优先读取入参,与 mat3_st 的三套视图一一对应,不再有约定不一致的问题:

函数实现行为是否自洽
mat3_v(v)v[0..2]→第1列(m11,m21,m31),v[3..5]→第2列,v[6..8]→第3列mat3_v(m.v) == m ✓
mat3_vv(vv)vv[列][行],vv[0][0]→m11, vv[1][0]→m12...mat3_vv(m.vv) == m ✓
mat3_v3(v3)v3[i] 直接赋给第 i 列mat3_v3(m.v3) == m ✓

示例:三种自洽的构造方式

#include "spacemath.h"
#include <stdio.h>

int main(void)
{
    // 同一个旋转矩阵:绕 Z 轴 90°
    mat3_st m1 = mat3(0, -1, 0,  1, 0, 0,  0, 0, 1);   // 行序参数

    ftype_t v[9] = {0, 1, 0,  -1, 0, 0,  0, 0, 1};      // 列优先:第1列(0,1,0) 第2列(-1,0,0)
    mat3_st m2 = mat3_v(v);

    ftype_t vv[3][3] = {{0, 1, 0}, {-1, 0, 0}, {0, 0, 1}}; // vv[列][行]
    mat3_st m3 = mat3_vv(vv);

    vec3_st cols[3] = {vec3(0, 1, 0), vec3(-1, 0, 0), vec3(0, 0, 1)};
    mat3_st m4 = mat3_v3(cols);

    printf("m1 == m2: %d\n", mat3_is_equal(m1, m2));   // 1
    printf("m1 == m3: %d\n", mat3_is_equal(m1, m3));   // 1
    printf("m1 == m4: %d\n", mat3_is_equal(m1, m4));   // 1

    // 列向量视图:旋转后基向量的像
    printf("X轴的像: (%.4f, %.4f, %.4f)\n", m1.v3[0].x, m1.v3[0].y, m1.v3[0].z);
    return 0;
}

输出:

m1 == m2: 1
m1 == m3: 1
m1 == m4: 1
X轴的像: (0.0000, 1.0000, 0.0000)

注意:mat3_zero() 返回的零矩阵不是合法旋转矩阵(行列式为 0、不正交),仅用作占位或累加初值。表示"无旋转"请用 mat3_identity()。


四、判定函数(8 个)

接口功能容差
mat3_is_zero(m)是否零矩阵MEPSILON
mat3_is_one(m)是否全 1 矩阵MEPSILON
mat3_is_identity(m)是否单位矩阵MEPSILON
mat3_is_equal(m0, m1)两矩阵是否相等MEPSILON
mat3_is_equal_eps(m0, m1, eps)自定义容差判等调用者指定
mat3_is_symmetric(m)是否对称矩阵(mij = mji)MEPSILON
mat3_is_orthogonal(m)是否正交矩阵(M·Mᵀ ≈ I)MEPSILON
mat3_is_diagonal(m)是否对角矩阵MEPSILON

32 位构建下 MEPSILON 为 FLT_EPSILON ≈ 1.19e-7,64 位为 DBL_EPSILON ≈ 2.22e-16。

4.1 mat3_is_orthogonal 的两个注意事项

实现为:计算 M·Mᵀ,再用 mat3_is_equal(容差 MEPSILON)与单位矩阵比较。

  1. 不查行列式:反射矩阵(如 diag(1,1,-1),det = −1)同样满足 M·Mᵀ = I,会被判为"正交"。数学上"旋转矩阵"还需满足 det = +1,严格校验旋转合法性应同时检查:
bool is_rotation = mat3_is_orthogonal(m) && (mat3_det(m) > MFLOAT_C(0.0));
  1. 容差恰在误差量级:容差取的是机器精度本身(64 位下 DBL_EPSILON ≈ 2.22e-16),恰处于单次浮点运算的误差量级。直接构造的精确矩阵能通过;但经插值、正交化等运算累积误差后,M·Mᵀ 与 I 的偏差很容易达到甚至超过该容差,导致误判为不正交(本文第十三章的综合示例实测触发了这一边界:正交化结果的 M·Mᵀ 对角元偏差恰为 −2.22e-16,is_orthogonal 返回 false)。工程上建议改用自定义容差自行判定:
mat3_st p = mat3_mul(m, mat3_transpose(m));
bool ortho = mat3_is_equal_eps(p, mat3_identity(), MFLOAT_C(1.0e-4));

4.2 示例

mat3_st rz90 = mat3_rotation_z(MPI_2);

printf("正交: %d\n", mat3_is_orthogonal(rz90));        // 1
printf("对称: %d\n", mat3_is_symmetric(rz90));         // 0
printf("对角: %d\n", mat3_is_diagonal(rz90));          // 0
printf("det = %.4f\n", mat3_det(rz90));                // 1.0000

五、度量函数(4 个)

接口功能实现
mat3_det(m)行列式Sarrus 法则(6 项乘积和)
mat3_trace(m)迹(对角和)m11 + m22 + m33
mat3_norm(m, type)矩阵范数type=0:Frobenius;type=1:1-范数;type=2:∞-范数
mat3_cond(m)条件数‖M‖_F · ‖M⁻¹‖_F

5.1 mat3_norm 的 type 约定

type范数定义
0Frobenius 范数全部元素平方和开根号
11-范数列元素绝对值和的最大值
2∞-范数行元素绝对值和的最大值
其他—返回 0

注意:type=2 是 ∞-范数,不是谱范数(最大奇异值 σmax)。需要谱范数需自行对 MᵀM 求特征值,库中未提供。

5.2 mat3_cond 的约定

  • 使用 Frobenius 范数(不是常见的 2-范数条件数)。对正交矩阵:谱条件数 = 1,F 范数条件数 = ‖R‖_F·‖Rᵀ‖_F = √3·√3 = 3,不要因数值不为 1 而误判矩阵病态。
  • 内部调用 mat3_inverse,而 mat3_inverse 对奇异矩阵静默返回单位矩阵(见第六章),因此奇异矩阵的 cond 会给出误导性的有限值 ‖M‖_F·√3,而非无穷大。判奇请直接看 mat3_det。

5.3 示例

mat3_st s = mat3_scale_matrix(2.0, 4.0, 4.0);   // 对角缩放矩阵

printf("det      = %.4f\n", mat3_det(s));        // 32.0000
printf("trace    = %.4f\n", mat3_trace(s));      // 10.0000
printf("F-norm   = %.4f\n", mat3_norm(s, 0));    // 6.0000
printf("1-norm   = %.4f\n", mat3_norm(s, 1));    // 4.0000
printf("inf-norm = %.4f\n", mat3_norm(s, 2));    // 4.0000
printf("cond     = %.4f\n", mat3_cond(s));       // 3.6742

(cond 验算:‖S‖_F = √(4+16+16) = 6,S⁻¹ = diag(0.5, 0.25, 0.25),‖S⁻¹‖_F = √0.375 ≈ 0.6124,乘积 ≈ 3.6742。)


六、单目运算:取负、转置、求逆(3 个)

接口功能备注
mat3_negate(m)全元素取负−M
mat3_transpose(m)转置旋转矩阵的转置 = 逆旋转
mat3_inverse(m)求逆伴随矩阵法;奇异时静默返回单位矩阵

6.1 mat3_inverse 的静默降级(重点)

实现用伴随矩阵法:M⁻¹ = adj(M) / det(M)。但开头有保护:

if (MFABS(det) < MEPSILON) {
    return mat3_identity();   /* 奇异矩阵:返回单位矩阵作为安全值 */
}

这意味着调用者无法从返回值区分"求逆成功得到单位阵"和"矩阵奇异求逆失败"。对来源不可信的矩阵,务必先检查行列式:

if (MFABS(mat3_det(m)) > MFLOAT_C(1.0e-6)) {
    mat3_st inv = mat3_inverse(m);
    /* 安全使用 inv */
} else {
    /* 矩阵奇异或接近奇异,走降级路径 */
}

6.2 求逆与转置的关系

对旋转矩阵(正交且 det=+1),R⁻¹ = Rᵀ,转置远比伴随矩阵法廉价且数值稳定。这正是 rotation.c 中 rotation_m_rvs(m) 直接用 mat3_transpose(m) 的原因。但对一般矩阵(含缩放、剪切)必须用 mat3_inverse。

mat3_st r = mat3_rotation_z(MPI_3);          // 旋转矩阵
mat3_st inv1 = mat3_inverse(r);              // 通用求逆
mat3_st inv2 = mat3_transpose(r);            // 旋转矩阵专用捷径
// inv1 ≈ inv2(求逆法精度略差且慢)

七、算术运算(9 个)

接口功能数学式
mat3_add(m0, m1)矩阵加法M0 + M1
mat3_add_f(m, f)全元素加标量M + f
mat3_sub(m0, m1)矩阵减法M0 − M1
mat3_sub_f(m, f)全元素减标量M − f
mat3_mul_vec(m, v)矩阵乘向量M·v(列向量)
mat3_mul(m0, m1)矩阵乘法M0·M1
mat3_mul_f(m, f)标量乘f·M
mat3_scale(m, scalar)缩放同 mat3_mul_f(包装)
mat3_scale_matrix(sx, sy, sz)构造对角缩放矩阵diag(sx, sy, sz)

7.1 mat3_mul 的复合顺序(重点)

mat3_mul(m0, m1) 计算 M0·M1。作用于向量时:

mat3_mul_vec(mat3_mul(m0, m1), v) == mat3_mul_vec(m0, mat3_mul_vec(m1, v))

即先对 v 施加 m1,再施加 m0(右乘先作用)。组合旋转时顺序不可交换:

mat3_st rz = mat3_rotation_z(MPI_2);   // 绕 Z 转 90°
mat3_st ry = mat3_rotation_y(MPI_2);   // 绕 Y 转 90°

vec3_st v = vec3(1.0, 0.0, 0.0);

// 先绕 Y 转再绕 Z 转
vec3_st r1 = mat3_mul_vec(mat3_mul(rz, ry), v);   // → (0, 0, -1)

// 先绕 Z 转再绕 Y 转
vec3_st r2 = mat3_mul_vec(mat3_mul(ry, rz), v);   // → (0, 1, 0)

两者结果完全不同,这就是"旋转不可交换"的代码体现。

7.2 加减法对旋转矩阵无意义

mat3_add/mat3_sub 是逐元素算术,两个旋转矩阵相加一般不再是旋转矩阵(行列式、正交性全部破坏)。它们服务于一般线性代数(如级数展开、mat3_exp 内部),不要用来"叠加旋转"——旋转的复合是乘法。


八、矩阵级数:exp / log / pow(3 个)

接口功能级数
mat3_exp(m)矩阵指数泰勒展开前 5 项:I + M + M²/2! + M³/3! + M⁴/4!
mat3_log(m)矩阵对数log(I+A) ≈ A − A²/2 + A³/3 − A⁴/4,A = M − I
mat3_pow(m, p)矩阵幂M^p = exp(p·log(M))

8.1 mat3_exp 系数已修复(历史缺陷)

mat3_exp 实现泰勒级数 exp(M) ≈ I + M + M²/2! + M³/3! + M⁴/4!,各项系数现已与阶乘一致:

/* I + M */
result = mat3_add(result, term);

/* + M²/2! */
term = mat3_mul_f(term, 0.5);
result = mat3_add(result, term);

/* + M³/3! = M³/6 */
term = mat3_mul(term, m);
term = mat3_mul_f(term, 1.0/6.0);    /* 3! = 6 */
result = mat3_add(result, term);

/* + M⁴/4! = M⁴/24 */
term = mat3_mul(term, m);
term = mat3_mul_f(term, 1.0/24.0);   /* 4! = 24 */
result = mat3_add(result, term);

8.2 mat3_log 的收敛域限制

mat3_log 的级数系数(1, −1/2, 1/3, −1/4)是正确的,但该级数仅在 ‖M − I‖ < 1 时收敛,且只取了 4 项。即它只适合接近单位矩阵的 M(小旋转、小形变)。对旋转 90° 的矩阵,‖M−I‖ ≈ 1.41 已超出收敛半径,结果不可信。

8.3 小结

这三个级数函数目前是"简化实现"(源码注释自述),仅适合小量级矩阵的近似计算。涉及旋转的指数/对数映射,正确路径是:

指数映射:反对称矩阵 → 轴角 → rotation_a2m
对数映射:旋转矩阵 → rotation_m2a → 轴 × 角 构造反对称矩阵

九、工具函数:正交归一化与插值(2 个)

接口功能
mat3_orthonormalization(basis)对三个列向量做 Gram-Schmidt 正交归一化
mat3_lerp(from, to, t)逐元素线性插值

9.1 mat3_orthonormalization:数值漂移的修复器

旋转矩阵经过多次乘法累积浮点误差后会"漂移"(列向量不再严格单位正交)。此函数对列做施密特正交化,把矩阵拉回正交群附近:

mat3_st fixed = mat3_orthonormalization(drifted_rotation);

注意事项:

  1. 按列处理:第一列方向保持不变,第二列减去在第一列上的投影,第三列减去在前两列上的投影。
  2. 线性相关时静默原样返回:三列若共面(行列式为 0),不做任何处理直接返回输入,调用方无感知。
  3. 不保证 det = +1:若输入是反射(det = −1),输出仍是反射。它只保证正交归一,不保证是"旋转"。
  4. 对漂移严重的矩阵,Gram-Schmidt 的几何意义是"偏向第一列",误差集中在后两列;更优方案是转四元数归一化后再转回(rotation_q2m(quat_normalize(rotation_m2q(m)))),误差在三列间均摊。

9.2 mat3_lerp:逐元素插值不保证正交

result.mij = from.mij + (to.mij - from.mij) * t;

对 9 个元素各自线性插值。两个旋转矩阵的 lerp 结果几乎必然不是旋转矩阵(列向量长度被压缩、不再正交):

mat3_st a = mat3_identity();
mat3_st b = mat3_rotation_z(MPI_2);
mat3_st mid = mat3_lerp(a, b, MFLOAT_C(0.5));

printf("det = %.4f\n", mat3_det(mid));               // 0.5000(不是 1)
printf("正交: %d\n", mat3_is_orthogonal(mid));        // 0(不正交)

姿态插值的正确做法是用四元数球面插值 quat_slerp(等角速度、全程保持旋转性)。mat3_lerp 适合非旋转用途(如材质、形变参数混合)。若条件所限必须用矩阵插值,可在 lerp 后接 mat3_orthonormalization 近似修复(对小夹角效果尚可,且会偏向 from 的第一列方向)。


十、基本旋转矩阵构造(3 个)

接口功能非零元素
mat3_rotation_x(f)绕 X 轴旋转 f 弧度m22=c, m23=−s, m32=s, m33=c
mat3_rotation_y(f)绕 Y 轴旋转 f 弧度m11=c, m13=s, m31=−s, m33=c
mat3_rotation_z(f)绕 Z 轴旋转 f 弧度m11=c, m12=−s, m21=s, m22=c

采用主动旋转、右手系、列向量约定 v' = R·v:拇指指向轴正方向,四指方向为正角度旋转方向。

以绕 Z 转 90° 为例:

mat3_st rz = mat3_rotation_z(MPI_2);
vec3_st v = mat3_mul_vec(rz, vec3(1.0, 0.0, 0.0));   // X 轴 → Y 轴
// v ≈ (0, 1, 0)

注意:rotation.c 中的 rotation_m_rx/ry/rz 就是这三个函数的薄包装,二者完全等价。mat3_rotation_y 的符号布局(m13=s, m31=−s)是右手系标准形式,与某些采用行向量约定的教材符号相反,混用资料时注意。


十一、rotation.c 中的矩阵相关接口(22 个)

矩阵与欧拉角/四元数/轴角的互转全部集中在 rotation.c,详细原理见《SpaceMath 旋转 API 技术博客》,这里按功能分组列出与矩阵直接相关的 22 个。

11.1 基本操作(4 个)

接口功能实现
rotation_m_rvs(m)逆旋转mat3_transpose(m)(旋转矩阵的逆 = 转置)
rotation_m_rx(angle)绕 X 基本旋转mat3_rotation_x(angle)
rotation_m_ry(angle)绕 Y 基本旋转mat3_rotation_y(angle)
rotation_m_rz(angle)绕 Z 基本旋转mat3_rotation_z(angle)

11.2 其他表示 → 矩阵(5 个)

接口功能实现要点
rotation_a2m(a)轴角 → 矩阵罗德里格斯公式 R = I + sinθ·K + (1−cosθ)·K² 解析直算
rotation_e2m(e)欧拉角 → 矩阵(默认转序 CFG_EULER_ORDER = “yxz”)e2m_order 包装
rotation_e2m_order(e, order)欧拉角 → 矩阵(指定 6 种转序之一)按转序连乘三个基本旋转
rotation_q2m(q)四元数 → 矩阵先归一化,再代标准公式
rotation_vv2m(to, from)向量对 → 矩阵q2m(vv2q(to, from)),求将 from 转到 to 的旋转

11.3 矩阵 → 其他表示(4 个)

接口功能实现要点
rotation_m2a(m)矩阵 → 轴角走四元数路径 q2a(m2q(m)),角度规范到 [0, π]
rotation_m2e(m)矩阵 → 欧拉角(默认转序)m2e_order 包装
rotation_m2e_order(m, order)矩阵 → 欧拉角(指定转序)6 分支解析提取,含万向锁处理
rotation_m2q(m)矩阵 → 四元数Shepperd 方法:按迹符号与最大对角元分 4 支,数值稳定

为什么 m2a 不直接用 angle = acos((trace−1)/2)? 32 位构建下旋转角接近 π 时 (trace−1)/2 在浮点数上坍缩为 −1.0f,角度信息丢失;且轴提取需除以 2·sin(angle),θ→π 时分母是垃圾值,轴方向完全错误。四元数路径(Shepperd + 2·atan2(|v|, w))在 θ→0 和 θ→π 两端都数值稳定。这是本库实测随机测试暴露并修复过的真实缺陷。

11.4 相对旋转(1 个)

接口功能实现
rotation_mm2m(to, from)求 from 姿态到 to 姿态的相对旋转mat3_mul(to, mat3_transpose(from)),即 R_rel = R_to · R_fromᵀ

参数顺序是 (to, from)——目标在前,起始在后。验证:rotation_mRm(rotation_mm2m(to, from), from) == to。

11.5 矩阵作为旋转量(5 个)

rotation_mRY(rm, Y):用旋转矩阵 rm 去旋转对象 Y。

接口功能实现
rotation_mRa(rm, a)矩阵旋转轴角转四元数路径复合
rotation_mRe(rm, e)矩阵旋转欧拉角转矩阵复合后转回
rotation_mRm(rm, m)矩阵旋转矩阵mat3_mul(rm, m)——rm 左乘,先施加 m 再施加 rm
rotation_mRq(rm, q)矩阵旋转四元数转四元数路径复合
rotation_mRv(rm, v)矩阵旋转向量mat3_mul_vec(rm, v),最直接的用法

11.6 矩阵被其他表示旋转(3 个)

接口功能含义
rotation_aRm(ra, m)用轴角 ra 旋转矩阵 ma2m(ra) · m
rotation_eRm(re, m)用欧拉角 re 旋转矩阵 me2m(re) · m
rotation_qRm(rq, m)用四元数 rq 旋转矩阵 mq2m(rq) · m

三者都是"旋转量左乘被旋转矩阵"。


十二、注意事项汇总

  1. 列优先布局:m.v[1] 是 m21 不是 m12;m.vv[i][j] 第 i 列第 j 行;m.v3[i] 是第 i 列。写跨库数据交换代码(如对接行优先的 OpenGL ES 手写数组)时务必先确认顺序。
  2. mat3_v() 已改为列优先读入参,与 v[9] 视图自洽(mat3_v(m.v) == m 恒成立)。三个数组构造函数 mat3_v/mat3_vv/mat3_v3 现已统一按列优先。
  3. mat3_inverse 奇异时静默返回单位矩阵,无错误信号。不可信输入先查 mat3_det。
  4. mat3_cond 用 Frobenius 范数:正交矩阵 cond = 3 而非 1;奇异矩阵 cond 给出误导性有限值。
  5. mat3_norm(m, 2) 是 ∞-范数不是谱范数;谱范数未提供。非法 type 返回 0。
  6. mat3_is_orthogonal 不查 det = +1,反射矩阵也会判 true;且容差 MEPSILON 恰为机器精度量级,经插值、正交化等运算的矩阵可能恰好超界误判(综合示例实测:偏差 −2.22e-16 被判否),工程判定建议用 mat3_is_equal_eps(..., 1e-4) 自定义容差。
  7. mat3_exp 系数已修复(历史缺陷):M³ 项现用 1/6、M⁴ 项现用 1/24,与阶乘一致。剩余误差来自仅取 5 项的截断;对旋转指数映射仍建议用 rotation_a2m(罗德里格斯精确解析式)。
  8. mat3_log 仅对 ‖M−I‖ < 1 收敛(且只取 4 项),只适合近单位矩阵。
  9. mat3_lerp 逐元素插值,结果一般不再是旋转矩阵;姿态插值应使用 quat_slerp。
  10. mat3_orthonormalization 对列向量做 Gram-Schmidt:三列线性相关时静默原样返回;不保证 det = +1;误差偏向保持第一列。
  11. 矩阵复合顺序:mat3_mul(m0, m1)·v 是先对 v 施加 m1 再施加 m0(右乘先作用),旋转不可交换。
  12. 零矩阵不是旋转矩阵:mat3_zero() 仅作占位/累加初值,"无旋转"用 mat3_identity()。
  13. rotation_mm2m(to, from) 参数顺序是目标在前;所有 rotation_XRY 接口都是旋转量在前、被旋转对象在后。
  14. rotation_m2a 已改走四元数路径,避免了 acos((trace−1)/2) 在 θ→π 的浮点坍缩(32 位构建下实测触发);升级旧代码时无需修改调用,角度范围仍是 [0, π]。

十三、综合示例

下面程序完整演示:构造、视图访问、度量、求逆、旋转向量、相对旋转、转轴角、正交化修复插值漂移。可用 gcc -I./src demo.c src/*.c -o demo 直接编译运行。

#include "spacemath.h"
#include <stdio.h>

static void print_mat3(const char *name, mat3_st m)
{
    printf("%s =\n", name);
    printf("  [ %7.4f  %7.4f  %7.4f ]\n", (double)m.m11, (double)m.m12, (double)m.m13);
    printf("  [ %7.4f  %7.4f  %7.4f ]\n", (double)m.m21, (double)m.m22, (double)m.m23);
    printf("  [ %7.4f  %7.4f  %7.4f ]\n", (double)m.m31, (double)m.m32, (double)m.m33);
}

int main(void)
{
    /* ========== 1. 构造:函数生成 vs 手写分量 ========== */
    mat3_st rz90 = mat3_rotation_z(MPI_2);
    mat3_st manual = mat3(0, -1, 0,  1, 0, 0,  0, 0, 1);   /* 行序参数 */

    print_mat3("rz90", rz90);
    /* 双精度下 cos(pi/2) = 6.1e-17,与手写的 0 相差 < MEPSILON(2.22e-16) */
    printf("rz90 与手写矩阵相等: %d\n\n", mat3_is_equal(rz90, manual));

    /* ========== 2. 列向量视图:基向量旋转后的像 ========== */
    printf("X轴的像: (%.4f, %.4f, %.4f)\n",
           (double)manual.v3[0].x, (double)manual.v3[0].y, (double)manual.v3[0].z);
    printf("Y轴的像: (%.4f, %.4f, %.4f)\n\n",
           (double)manual.v3[1].x, (double)manual.v3[1].y, (double)manual.v3[1].z);

    /* ========== 3. 判定与度量 ========== */
    printf("正交: %d, det = %.4f, trace = %.4f, F范数 = %.4f\n\n",
           mat3_is_orthogonal(manual),
           (double)mat3_det(manual),
           (double)mat3_trace(manual),
           (double)mat3_norm(manual, 0));

    /* ========== 4. 矩阵旋转向量 ========== */
    vec3_st v = rotation_mRv(manual, vec3(1.0, 0.0, 0.0));
    printf("Rz90 * (1,0,0) = (%.4f, %.4f, %.4f)\n\n",
           (double)v.x, (double)v.y, (double)v.z);

    /* ========== 5. 相对旋转:Rz45 -> Rz90 的增量 ========== */
    mat3_st rz45 = mat3_rotation_z(MPI_4);
    mat3_st rel  = rotation_mm2m(manual, rz45);     /* = Rz90 * Rz45^T = Rz45 */
    print_mat3("rel (应为Rz45)", rel);

    /* ========== 6. 矩阵转轴角 ========== */
    axis_st a = rotation_m2a(manual);
    printf("m2a: 轴 = (%.4f, %.4f, %.4f), 角 = %.4f rad\n\n",
           (double)a.v3.x, (double)a.v3.y, (double)a.v3.z, (double)a.angle);

    /* ========== 7. 求逆验证:M * M^-1 = I ========== */
    mat3_st prod = mat3_mul(manual, mat3_inverse(manual));
    printf("M * M^-1 是单位阵: %d\n\n", mat3_is_identity(prod));

    /* ========== 8. lerp 漂移与正交化修复 ========== */
    mat3_st mid = mat3_lerp(mat3_identity(), manual, MFLOAT_C(0.5));
    printf("lerp中点: det = %.4f, 正交 = %d  (漂移,不是旋转矩阵)\n",
           (double)mat3_det(mid), mat3_is_orthogonal(mid));

    mat3_st fixed = mat3_orthonormalization(mid);
    print_mat3("正交化修复后", fixed);
    printf("修复后 det = %.4f\n", (double)mat3_det(fixed));
    printf("is_orthogonal(严容差): %d\n", mat3_is_orthogonal(fixed));
    printf("is_equal_eps(自定义1e-4): %d\n",
           mat3_is_equal_eps(mat3_mul(fixed, mat3_transpose(fixed)),
                             mat3_identity(), MFLOAT_C(1.0e-4)));

    return 0;
}

预期输出(64 位构建,%.4f 精度,关键值均经手工验算与实机运行核对):

rz90 =
  [  0.0000  -1.0000   0.0000 ]
  [  1.0000   0.0000   0.0000 ]
  [  0.0000   0.0000   1.0000 ]
rz90 与手写矩阵相等: 1

X轴的像: (0.0000, 1.0000, 0.0000)
Y轴的像: (-1.0000, 0.0000, 0.0000)

正交: 1, det = 1.0000, trace = 1.0000, F范数 = 1.7321

Rz90 * (1,0,0) = (0.0000, 1.0000, 0.0000)

rel (应为Rz45) =
  [  0.7071  -0.7071   0.0000 ]
  [  0.7071   0.7071   0.0000 ]
  [  0.0000   0.0000   1.0000 ]

m2a: 轴 = (0.0000, 0.0000, 1.0000), 角 = 1.5708 rad

M * M^-1 是单位阵: 1

lerp中点: det = 0.5000, 正交 = 0  (漂移,不是旋转矩阵)
正交化修复后 =
  [  0.7071  -0.7071   0.0000 ]
  [  0.7071   0.7071   0.0000 ]
  [  0.0000   0.0000   1.0000 ]
修复后 det = 1.0000
is_orthogonal(严容差): 0
is_equal_eps(自定义1e-4): 1

第 1 节中 rz90 与手写精确矩阵判等通过,是因为双精度 cos(π/2) ≈ 6.1e-17 的残差小于 MEPSILON。第 8 节出现耐人寻味的对照:lerp 中点经正交化后 det 精确回到 1、与 Rz(45°) 在 1e-4 容差下完全一致,但 is_orthogonal 用 DBL_EPSILON 严容差判定却返回 0——其 M·Mᵀ 对角元偏差恰为 −2.22e-16,落在"严格小于"的边界之外。这正是注意事项第 6 条的实证:mat3_is_orthogonal 的机器级容差对经过运算的矩阵偏严,工程判定应使用 mat3_is_equal_eps 自定义容差。另外,lerp 中点修复后恰为 Rz(45°) 是 from/to 关于 Z 轴对称的特例,一般情形修复结果只是"接近"理想中值旋转;生产代码的姿态插值仍应使用 quat_slerp。


十四、API 速查表

matrix.c(39 个)

分类接口
构造(7)mat3 mat3_v mat3_vv mat3_v3 mat3_zero mat3_one mat3_identity
判定(8)mat3_is_zero mat3_is_one mat3_is_identity mat3_is_equal mat3_is_equal_eps mat3_is_symmetric mat3_is_orthogonal mat3_is_diagonal
度量(4)mat3_det mat3_trace mat3_norm mat3_cond
单目(3)mat3_negate mat3_transpose mat3_inverse
算术(9)mat3_add mat3_add_f mat3_sub mat3_sub_f mat3_mul_vec mat3_mul mat3_mul_f mat3_scale mat3_scale_matrix
级数(3)mat3_exp mat3_log mat3_pow
工具(2)mat3_orthonormalization mat3_lerp
旋转构造(3)mat3_rotation_x mat3_rotation_y mat3_rotation_z

rotation.c 矩阵相关(22 个)

分类接口
基本(4)rotation_m_rvs rotation_m_rx rotation_m_ry rotation_m_rz
转入(5)rotation_a2m rotation_e2m rotation_e2m_order rotation_q2m rotation_vv2m
转出(4)rotation_m2a rotation_m2e rotation_m2e_order rotation_m2q
相对(1)rotation_mm2m
作旋转量(5)rotation_mRa rotation_mRe rotation_mRm rotation_mRq rotation_mRv
被旋转(3)rotation_aRm rotation_eRm rotation_qRm

十五、总结

matrix.c 与 rotation.c 共同构成 SpaceMath 的矩阵能力:matrix.c 提供通用 3×3 线性代数底座(构造、判定、度量、算术、求逆、级数、正交化、插值、基本旋转矩阵),rotation.c 提供矩阵与欧拉角/四元数/轴角的互转及旋转语义封装。

使用中牢记五条主线:

  1. 布局意识:列优先四视图,三个数组构造函数 mat3_v/mat3_vv/mat3_v3 已统一按列优先、与各自视图自洽(mat3_v(m.v) == m)。
  2. 旋转合法性:正交 + det=+1 才是旋转矩阵;is_orthogonal 只查前者。
  3. 静默降级:inverse 奇异返回单位阵、orthonormalization 线性相关原样返回、norm 非法 type 返回 0——边界情况都不报错,调用方需自行前置校验。
  4. 级数函数是简化实现:mat3_exp 系数已修复(M³/6、M⁴/24),但仅取前 5 项截断误差较大;mat3_log 收敛域有限。旋转的指数/对数映射应走轴角路径(rotation_a2m/rotation_m2a)。
  5. 插值不归矩阵管:mat3_lerp 只做逐元素混合,姿态插值交给 quat_slerp。

掌握这些,矩阵这一"旋转计算的最终落点"就能用得既正确又高效。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐