SpaceMath 旋转 API 全解:从欧拉角到四元数,60 个旋转接口的功能、用法与注意事项

一、引言

三维空间旋转是图形学、机器人、航空航天姿态解算的核心问题。同一个旋转可以用欧拉角、旋转矩阵、四元数、轴角四种表示描述,它们各有优劣:

表示优点缺点
欧拉角直观、人类易读万向锁、转序敏感、插值不平滑
旋转矩阵无歧义、可直接乘向量参数冗余(9个数)、不直观
四元数无万向锁、插值平滑、紧凑不直观、有 q≡-q 双重覆盖
轴角最直观(轴+角)角度接近 0/π 时不稳定

SpaceMath 的 rotation.c 正是为了解决"四种表示之间的互联互通"而存在——它提供了 60 个旋转相关 API,覆盖:基本旋转构造、四种表示两两互转、由向量对求旋转、相对旋转计算、以及"用任意表示旋转任意对象"的统一应用接口。

本文将逐一解析这些 API 的功能、用法和注意事项,并给出可直接编译运行的 C 语言示例。


二、命名约定与整体设计

在逐个介绍 API 之前,先理解 SpaceMath 的三条核心设计约定,它们贯穿所有 60 个接口。

2.1 单字母类型编码

字母类型全称
aaxis_st轴角(Axis-angle)
eeula_st欧拉角(Euler angles)
mmat3_st旋转矩阵(Matrix)
qquat_st四元数(Quaternion)
vvec3_st三维向量(Vector)

2.2 内旋约定(Intrinsic Rotation)

欧拉角到矩阵/四元数的转换采用内旋约定:旋转顺序字符串从左到右表示绕自身当前坐标轴依次旋转。矩阵组合顺序为 R = R₃ · R₂ · R₁,即先执行最右侧的旋转。

例如转序 "zyx" 表示:先绕自身 Z 轴旋转,再绕新的 Y 轴,最后绕新的 X 轴。

2.3 值语义 + 级联调用

所有接口入参和返回值均为结构体值(无指针),可像数学公式一样级联书写:

// 用四元数旋转向量,再单位化,再与参考方向求夹角
ftype_t a = vec3_angle(vec3_unit(rotation_qRv(q, v)), vref);

三、基本旋转操作(16 个)

这组 API 用于构造"绕坐标轴的基本旋转"或求"逆旋转",是所有旋转计算的原子积木。

3.1 绕坐标轴旋转构造(12 个)

每种表示都有 rx/ry/rz 三个接口,输入角度(弧度),返回对应表示的旋转。

轴角欧拉角矩阵四元数
rotation_a_rxrotation_e_rxrotation_m_rxrotation_q_rx
rotation_a_ryrotation_e_ryrotation_m_ryrotation_q_ry
rotation_a_rzrotation_e_rzrotation_m_rzrotation_q_rz

示例:构造绕 Z 轴旋转 90° 的各种表示

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

int main(void) {
    ftype_t angle = MPI_2;  // 90°,库提供的 π/2 常量

    axis_st a = rotation_a_rz(angle);   // 轴角:轴(0,0,1),角π/2
    eula_st e = rotation_e_rz(angle);   // 欧拉角:(0, 0, π/2)
    mat3_st m = rotation_m_rz(angle);   // 旋转矩阵
    quat_st q = rotation_q_rz(angle);   // 四元数

    printf("Euler: (%.3f, %.3f, %.3f)\n", e.x, e.y, e.z);
    printf("Quat : (%.3f, %.3f, %.3f, %.3f)\n", q.x, q.y, q.z, q.w);
    return 0;
}

输出:

Euler: (0.000, 0.000, 1.571)
Quat : (0.000, 0.000, 0.707, 0.707)

3.2 逆旋转(4 个)

接口实现方式
rotation_a_rvs(a)旋转轴取反,角度不变
rotation_e_rvs(e)三个角度分量全部取反
rotation_m_rvs(m)矩阵转置(旋转矩阵的逆 = 转置)
rotation_q_rvs(q)四元数共轭

示例:验证逆旋转的性质

// 构造一个旋转 q 和它的逆 q_inv
quat_st q     = rotation_q_rz(MPI_4);      // 绕 Z 轴 45°
quat_st q_inv = rotation_q_rvs(q);         // 逆旋转

// q 之后施加 q_inv,应回到单位旋转(等价于 q * q_inv = identity)
quat_st result = rotation_qRq(q_inv, q);   // 用 q_inv 旋转 q
// 此时 result 应为单位四元数 (0,0,0,1)

注意:四元数的逆旋转是共轭而非取负。q 和 -q 表示同一个旋转,-q 不是逆旋转。


四、旋转表示互转

4.1 显式指定转序的欧拉角转换(4 个)

接口功能
rotation_e2m_order(e, order)欧拉角 → 旋转矩阵(指定转序)
rotation_m2e_order(m, order)旋转矩阵 → 欧拉角(指定转序)
rotation_e2q_order(e, order)欧拉角 → 四元数(指定转序)
rotation_q2e_order(q, order)四元数 → 欧拉角(指定转序)

支持的 6 种转序:"xyz"、"xzy"、"yxz"、"yzx"、"zxy"、"zyx"。

示例:用 zyx 转序将欧拉角转矩阵

eula_st e = eula(MPI_4, MPI_6, MPI_3);  // roll=45°, pitch=30°, yaw=60°
mat3_st m = rotation_e2m_order(e, "zyx");

// 验证:用 m 旋转向量,再转回欧拉角应一致
eula_st e_back = rotation_m2e_order(m, "zyx");
万向锁处理

rotation_m2e_order 是实现最复杂的函数。当第二个旋转角接近 ±π/2 时会发生万向锁(Gimbal Lock),此时第一和第三个旋转轴重合,自由度丢失。代码对 6 种转序分别做了万向锁检测与处理:

// 以 zyx 转序为例,当 m13 接近 ±1 时判定为万向锁
if (MFABS(m.m13 + 1.0) < epsilon) {
    e.y = -MPI_2;          // pitch = -90°
    e.x = 0.0;             // roll 置零
    e.z = MATAN2(m.m21, m.m22);  // yaw 取合并值
} else if (MFABS(m.m13 - 1.0) < epsilon) {
    e.y = MPI_2;
    e.x = 0.0;
    e.z = MATAN2(-m.m21, -m.m22);
}

注意:万向锁下欧拉角分解不唯一,库将第一个角置零、把合并量放到第三个角。如需避免万向锁问题,建议使用四元数或轴角表示。

4.2 默认转序的两两互转(12 个)

这是最常用的一组接口,使用 spacemath.h 中 CFG_EULER_ORDER(默认 "yxz")指定的转序。

        a        e        m        q
    ┌─────────────────────────────────┐
a   │   -    a2e     a2m     a2q     │
e   │  e2a     -     e2m     e2q     │
m   │  m2a    m2e      -     m2q     │
q   │  q2a    q2e     q2m      -     │
    └─────────────────────────────────┘

核心实现原理:

  • rotation_m2q:Shepperd 算法,按矩阵迹的正负和主对角线最大元分 4 种情况计算,避免数值不稳定
  • rotation_q2m:四元数归一化后代入标准公式
  • rotation_m2a:angle = acos((trace-1)/2),轴从反对称部分提取,做除零保护
  • rotation_q2a:angle = 2*acos(w),轴为 (x,y,z) 归一化,角度用 remainder 规范到 [-π, π]
  • rotation_a2m:直接用罗德里格斯公式 R = I + sinθ·K + (1-cosθ)·K²

示例:四元数与欧拉角互转

// 欧拉角 → 四元数
eula_st e = eula(0.0, 0.0, MPI_2);       // 绕 Z 轴 90°
quat_st q = rotation_e2q(e);

// 四元数 → 欧拉角
eula_st e2 = rotation_q2e(q);
// e2 应等于 (0, 0, π/2)

示例:矩阵转四元数的数值稳定性

// 单位矩阵的迹=3>0,走第一分支,直接得到单位四元数
mat3_st I = mat3_identity();
quat_st q = rotation_m2q(I);   // → (0, 0, 0, 1)

// 旋转 180° 的矩阵迹=-1,会走到其他分支
mat3_st m180 = rotation_m_rx(MPI);  // 绕 X 轴 180°
quat_st q2 = rotation_m2q(m180);    // → (1, 0, 0, 0) 附近

注意:rotation_m2q 和 rotation_q2a 在角度接近 0 或 π 时会做特殊处理,避免除以零。rotation_q2a 对零角返回 axis_zero()。

4.3 由向量对求旋转(4 个)

接口功能
rotation_vv2a(to, from)求将 from 旋转到 to 的轴角
rotation_vv2e(to, from)求将 from 旋转到 to 的欧拉角
rotation_vv2m(to, from)求将 from 旋转到 to 的旋转矩阵
rotation_vv2q(to, from)求将 from 旋转到 to 的四元数

参数顺序:第一个是目标向量 to,第二个是起始向量 from。

示例:将 X 轴旋转到 Y 轴

vec3_st from = vec3(1.0, 0.0, 0.0);   // 起始方向
vec3_st to   = vec3(0.0, 1.0, 0.0);   // 目标方向

quat_st q = rotation_vv2q(to, from);  // 旋转四元数

// 验证:用 q 旋转 from,应得到 to
vec3_st result = rotation_qRv(q, from);
// result ≈ (0, 1, 0)

注意:当 from 和 to 平行(夹角 0° 或 180°)时,旋转轴不唯一。quat_from_vv 内部会处理这种退化情况。

4.4 相对旋转(4 个)

计算从 from 姿态到 to 姿态的"旋转增量"。

接口功能
rotation_aa2a(to, from)两轴角之间的相对旋转轴角
rotation_ee2e(to, from)两欧拉角之间的相对旋转欧拉角
rotation_mm2m(to, from)两矩阵之间的相对旋转矩阵
rotation_qq2q(to, from)两四元数之间的相对旋转四元数

数学原理:to = R_relative · from,因此 R_relative = to · from⁻¹。

  • 四元数:R = to * inverse(from)
  • 矩阵:R = to * transpose(from)
  • 欧拉角/轴角:先转四元数或矩阵,计算后转回

示例:计算两姿态之间的相对旋转

quat_st from = quat_identity();              // 起始姿态
quat_st to   = rotation_q_rz(MPI_2);         // 目标姿态:绕 Z 转 90°

quat_st rel = rotation_qq2q(to, from);       // 相对旋转 = 绕 Z 转 90°

// 验证:用 rel 旋转 from,应得到 to
quat_st check = rotation_qRq(rel, from);
// check ≈ to

注意:相对旋转的参数顺序是 (to, from),与直觉可能相反——先写目标,再写起始。


五、旋转应用接口 rotation_XRY(20 个)

这是 SpaceMath 最具特色的一组 API:用任意一种表示去旋转任意一种表示或向量,共 4×5 = 20 个接口。

命名规则:rotation_XRY,其中 X 是旋转量的表示,Y 是被旋转对象的表示。

参数顺序:第一个参数是旋转量,第二个是被旋转对象。

        被旋转对象 →   a       e       m       q       v
旋转量 ↓
a                    aRa     aRe     aRm     aRq     aRv
e                    eRa     eRe     eRm     eRq     eRv
m                    mRa     mRe     mRm     mRq     mRv
q                    qRa     qRe     qRm     qRq     qRv

5.1 旋转向量(最常用,4 个)

rotation_aRv、rotation_eRv、rotation_mRv、rotation_qRv

示例:用四元数旋转向量

quat_st q = rotation_q_rz(MPI_2);   // 绕 Z 轴 90°
vec3_st v = vec3(1.0, 0.0, 0.0);     // X 轴

vec3_st v2 = rotation_qRv(q, v);     // 用 q 旋转 v
// v2 ≈ (0, 1, 0),即 Y 轴

示例:用欧拉角旋转向量

eula_st e = eula(0.0, 0.0, MPI_2);   // 绕 Z 轴 90°
vec3_st v = vec3(1.0, 0.0, 0.0);

vec3_st v2 = rotation_eRv(e, v);
// v2 ≈ (0, 1, 0)

内部实现:rotation_aRv/rotation_qRv 走四元数分量法 quat_rotate_vec3_components;rotation_eRv/rotation_mRv 先转矩阵再乘向量。

5.2 旋转四元数(4 个)

rotation_aRq、rotation_eRq、rotation_mRq、rotation_qRq

示例:连续施加两个旋转

quat_st q1 = rotation_q_rz(MPI_2);   // 先绕 Z 转 90°
quat_st q2 = rotation_q_ry(MPI_2);   // 再绕 Y 转 90°

// 组合:先 q1 后 q2(注意四元数乘法顺序)
quat_st combined = rotation_qRq(q2, q1);
// 含义:用 q2 去旋转 q1,等价于 q2 * q1

注意:四元数旋转的组合顺序。rotation_qRq(rq, q) 内部是 quat_mul(rq, q),即 rq 左乘 q,表示"先施加 q,再施加 rq"。

5.3 旋转矩阵(4 个)

rotation_aRm、rotation_eRm、rotation_mRm、rotation_qRm

示例:用旋转矩阵旋转旋转矩阵

mat3_st rz = rotation_m_rz(MPI_2);    // 旋转量:绕 Z 90°
mat3_st m  = mat3_identity();          // 被旋转对象:单位矩阵

mat3_st result = rotation_mRm(rz, m);
// result ≈ rz(单位矩阵被旋转后等于旋转量本身)

5.4 旋转欧拉角(4 个)

rotation_aRe、rotation_eRe、rotation_mRe、rotation_qRe

示例:用四元数旋转欧拉角

quat_st q = rotation_q_rz(MPI_2);     // 旋转量
eula_st e = eula_zero();               // 被旋转对象:零欧拉角

eula_st result = rotation_qRe(q, e);
// result ≈ (0, 0, π/2)

5.5 旋转轴角(4 个)

rotation_aRa、rotation_eRa、rotation_mRa、rotation_qRa

示例:用轴角旋转轴角

axis_st ra = rotation_a_rz(MPI_2);    // 旋转量
axis_st a  = axis_zero();              // 被旋转对象:零轴角

axis_st result = rotation_aRa(ra, a);
// result ≈ ra

内部实现:轴角和欧拉角的旋转组合统一通过四元数中转(先转四元数、相乘、再转回),避免矩阵/欧拉角的数值问题,且自动归一化。


六、注意事项汇总

6.1 欧拉角转序

  • 默认转序由 CFG_EULER_ORDER 宏控制(当前为 "yxz"),修改它会影响所有不带 _order 后缀的欧拉角转换接口。
  • 航天领域常用 "zyx"(yaw-pitch-roll),如需切换可改宏或使用 *_order 接口。
  • 六种有效转序:xyz、xzy、yxz、yzx、zxy、zyx。转序字符串必须由小写 xyz 组成且三轴不重复。

6.2 万向锁

  • 欧拉角在第二个角接近 ±90° 时发生万向锁,rotation_m2e_order 已做处理但分解不唯一。
  • 建议:需要频繁插值或避免奇异的场合,用四元数;只在人机交互或最终输出时用欧拉角。

6.3 四元数双重覆盖

  • q 和 -q 表示同一个旋转。rotation_m2q 返回的四元数符号由算法分支决定,可能与预期相反。
  • 比较两个四元数是否表示同一旋转,应使用 quat_is_equal(库内部会处理符号),而非逐分量 ==。

6.4 归一化

  • rotation_q2m、rotation_q2a 内部会先归一化四元数,输入未归一化的四元数也能正确工作。
  • rotation_XRq 系列(旋转四元数的结果)会自动归一化输出。
  • 但作为旋转量传入的四元数,建议事先归一化以获得最佳精度。

6.5 角度单位

  • 所有角度参数和返回值均为弧度。角度转弧度用 MDEG2RAD,弧度转角度用 MRAD2DEG,或用 eula_deg2rad/eula_rad2deg。

6.6 零旋转处理

  • rotation_a2q、rotation_a2m 对零轴角返回单位四元数/单位矩阵。
  • rotation_q2a、rotation_m2a 对接近零的旋转角返回 axis_zero(),避免除零。

6.7 旋转组合顺序

  • 矩阵:rotation_mRm(rm, m) = rm * m,表示先施加 m 再施加 rm。
  • 四元数:rotation_qRq(rq, q) = rq * q,表示先施加 q 再施加 rq。
  • 统一规律:第一个参数后施加,第二个参数先施加。

七、综合示例:姿态解算中的旋转链式计算

下面是一个贴近实际的示例:已知传感器测得的角速度,用四元数递推姿态,然后转换为欧拉角输出,并计算相对起始姿态的偏航角。

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

int main(void) {
    // 1. 初始姿态:单位四元数
    quat_st q = quat_identity();

    // 2. 模拟角速度(弧度/秒)和时间步长
    vec3_st omega = vec3(MDEG2RAD(10.0), MDEG2RAD(20.0), MDEG2RAD(30.0));
    ftype_t dt = MFLOAT_C(0.01);  // 10ms

    // 3. 用角速度积分递推四元数(库内置接口)
    q = quat_integrate_w(q, omega, dt);

    // 4. 四元数 → 欧拉角(默认 yxz 转序),用于显示
    eula_st e = rotation_q2e(q);
    printf("Attitude (deg): roll=%.2f pitch=%.2f yaw=%.2f\n",
           e.x * MRAD2DEG, e.y * MRAD2DEG, e.z * MRAD2DEG);

    // 5. 计算相对起始姿态的旋转(偏航增量)
    quat_st rel = rotation_qq2q(q, quat_identity());
    eula_st rel_e = rotation_q2e(rel);
    printf("Yaw delta (deg): %.2f\n", rel_e.z * MRAD2DEG);

    // 6. 将当前姿态的 X 轴方向旋转到世界坐标系
    vec3_st body_x = vec3(1.0, 0.0, 0.0);
    vec3_st world_x = rotation_qRv(q, body_x);
    printf("Body X in world: (%.3f, %.3f, %.3f)\n",
           world_x.x, world_x.y, world_x.z);

    return 0;
}

编译运行(64 位双精度):

gcc -O2 -I./src demo.c src/*.c -lm -o demo && ./demo

八、API 速查表

基本旋转操作(16)

类别接口
逆旋转rotation_a_rvs rotation_e_rvs rotation_m_rvs rotation_q_rvs
绕X轴rotation_a_rx rotation_e_rx rotation_m_rx rotation_q_rx
绕Y轴rotation_a_ry rotation_e_ry rotation_m_ry rotation_q_ry
绕Z轴rotation_a_rz rotation_e_rz rotation_m_rz rotation_q_rz

表示互转(24)

类别接口
指定转序rotation_e2m_order rotation_m2e_order rotation_e2q_order rotation_q2e_order
两两互转rotation_a2e rotation_a2m rotation_a2q rotation_e2a rotation_e2m rotation_e2q rotation_m2a rotation_m2e rotation_m2q rotation_q2a rotation_q2e rotation_q2m
向量对求旋转rotation_vv2a rotation_vv2e rotation_vv2m rotation_vv2q
相对旋转rotation_aa2a rotation_ee2e rotation_mm2m rotation_qq2q

旋转应用 rotation_XRY(20)

旋转量 \ 对象轴角 a欧拉 e矩阵 m四元数 q向量 v
轴角 aaRaaReaRmaRqaRv
欧拉 eeRaeReeRmeRqeRv
矩阵 mmRamRemRmmRqmRv
四元数 qqRaqReqRmqRqqRv

九、总结

SpaceMath rotation.c 的 60 个旋转 API 形成了一个完整、自洽的旋转计算体系:

  1. 全方向互转:四种表示两两可达,无需绕道"中间表示"。
  2. 统一命名:rotation_XRY 一眼可读,参数顺序一致(旋转量在前,对象在后)。
  3. 数值稳健:矩阵转四元数用 Shepperd 算法分 4 分支;轴角提取做除零保护;欧拉角转换处理 6 种转序的万向锁。
  4. 内旋约定:转序字符串从左到右为内旋顺序,矩阵组合 R = R₃·R₂·R₁。
  5. 值语义级联:无指针参数,可像数学公式一样嵌套调用。

掌握这些接口后,姿态解算、坐标变换、旋转插值等常见任务都能用一两行级联代码完成,无需再纠结四元数乘法顺序或欧拉转序约定。


参考链接:

  • SpaceMath 源码仓库:https://github.com/scilogyhunter/SpaceMath
Logo

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

更多推荐