SpaceMath库的由来和设计思路

设计思路

三维向量、旋转矩阵、四元数、欧拉角和轴角这些量的数学公式实现并不复杂,很多应用自行实现所需算法,也有不少功能全面的开源数学库可用。但"能用"和"好用"是两个差别很大的概念:自实现的模块往往不规范、功能不全面,甚至存在算法错误,也难在其他项目中复用;现成的通用库又常常过于庞大、缺乏针对性,移植成本高。基于对算法与需求的既有理解,以及一些蠢蠢欲动的新想法和对漂亮代码的偏执,特开发了此数学库,重点解决三个问题------约定容易混乱 、功能边界不完整 、数值细节不稳健。

为此,库确立了以下几条全局设计约定:

  1. 统一的浮点类型与常量体系。 精度由 CFG_FLOATWORD_BITSIZE 一处配置,ftype_t 是唯一的浮点类型名;所有常量宏按配置取值,MFLOAT_C(c) 保证字面量类型与当前精度一致,MFABS、MSQRT、MATAN2 等数学宏按精度映射到 fabsf/fabs/fabsl 等实现。切换精度只需改一行宏,所有调用点无需改动,从根本上杜绝 float 与 double 混用导致的隐式转换与精度损失。
  2. 统一的类型表示与多视图访问。 五种类型都以结构体承载语义,成员名即含义;同时以联合体保留数组视图,便于与外部数组数据(文件、总线、第三方库)零成本互操作。枚举 MINDEX_* 为数组下标提供具名索引,避免魔法数字。旋转矩阵采用列优先存储,与数学书写惯例一致,且同一列元素在内存中连续,按列访问及矩阵乘向量时 cache 命中更好。
  3. 统一的欧拉角转序配置。 默认转序由 CFG_EULER_ORDER 全局指定,可选 "xyz"、"xzy"、"yxz"、"yzx"、"zxy"、"zyx" 六种;全局使用一致转序可避免多转序混用导致的隐蔽错误,同时简化参数配置。确需在局部使用其它转序时,可用 rotation_e2m_order、rotation_m2e_order、rotation_e2q_order、rotation_q2e_order 直接指定,兼顾简洁与灵活。
  4. 值语义的接口形态。 除按数组构造的 *_v/*_vv 接口外,入参与返回值均为结构体值,没有指针参数,也无需空指针检查;调用可级联书写,例如 vec3_unit(vec3_rotate(v, axis, angle))、quat_normalize(quat_mul(q0, q1))。
  5. 统一的命名规约。 类型以单字母编码(a 轴角、e 欧拉角、m 旋转矩阵、q 四元数、v 向量),旋转接口统一为 rotation_XRY,rotation_qRv 一眼可读出"用四元数旋转向量";*_v/*_vv 表示按数组构造,*_order 表示显式指定转序,*_eps 表示自定义容差。签名自解释,不读文档也能正确调用。
  6. 实现与测试同步。 每个接口在实现的同时编写单元测试,库源文件与测试源文件一一对应,接口与用例同步演进;测试可一键自动运行并输出统计结果,使功能调整、精度切换和重构都有回归保障。

不追求过度的通用性------那会增加传参与实现的复杂度、降低易用性。库只解决空间平移与旋转线性计算这一类问题,并把这类问题的接口做到完整、明确、不易误用。

为什么用结构体而不是数组

使用结构体而非数组来表示三维向量、三维旋转矩阵、四元数、欧拉角和轴角,主要有以下考虑。

  1. 可读性更好,成员名本身就是文档。
    结构体成员名直接表达含义,q.w、q.x、axis.angle 比 q[0]、a[3] 直观得多,阅读者不必反复查约定,也不必依赖注释确认下标含义。结构体是封闭的对象化表示,数组则把内部布局直接暴露给使用者。

  2. 消除顺序歧义与对应错误。
    如果是数组表示,三维向量中v0、v1、v2 分别对应 x、y、z 轴的坐标可能不容易出错。但对于四元数,v[0]和v[3] 哪个是常量?三维旋转矩阵是行优先还是列优先? 轴角中,a[0] 和 a[3] 哪个是角度?实际情况是不同顺序约定都有库在使用,极容易搞混淆。经验表明,容易出错的地方就一定会出错。成员名把含义强制写清楚,至少在本库内部形成稳定、明确的语义,降低跨模块、跨团队协作时的沟通与出错成本。

  3. 避免越界访问。
    数组下标写错就会越界读写,甚至导致程序崩溃;结构体成员由编译器按名字解析,不存在"下标越界"问题,可显著减少这类低级但危险的错误。

  4. 避免类型错判。
    五种类型若都用数组表示,类型信息随之丢失,容易错判,并因各类型长度不同而级联导致越界访问。

  5. 可直接赋值。
    同类型结构体之间可直接写 a = b;,数组则必须借助 memcpy 或循环赋值。按值传参使函数内部获得独立副本,调用者的数据不会被意外修改,局部性、可重入性与并发安全性更好。

  6. 便于类型检查与接口约束。
    参数要求 quat_st 时,传入 vec3_st 或 mat3_st 会被编译器发现;数组因底层类型相近,容易在编译期蒙混过关,把错误推迟到运行期。

  7. 便于实现级联调用。
    接口以传值方式输入输出,可以像数学公式一样层层嵌套,一行完成多级运算。例如把两个姿态 q0、q1 复合出旋转,归一化后旋转向量 v,再单位化,最后与参考方向 vref 求夹角,直接用本库接口写成:

    c 复制代码
    ftype_t a = vec3_angle(vec3_unit(rotation_qRv(quat_normalize(quat_mul(q0, q1)), v)), vref);

    这行代码读起来与数学式子完全一致,没有中间变量,也就没有中间变量用错的可能。若接口改为数组传参的实现,同样的计算既无法嵌套,又必须为每一步准备具名临时数组,代码会繁杂很多:

    c 复制代码
    /* 以下 _arr 后缀接口为数组传参风格的假想写法,仅用于对比 */
    ftype_t qm[4], qn[4], vr[3], vn[3];   /* 中间结果必须全部具名暴露 */
    quat_mul_arr(q0a, q1a, qm);           /* 复合姿态 */
    quat_normalize_arr(qm, qn);           /* 归一化 */
    rotation_qRv_arr(qn, va, vr);         /* 旋转向量 */
    vec3_unit_arr(vr, vn);                /* 单位化 */
    ftype_t a = vec3_angle_arr(vn, vrefa); /* 求夹角 */

    五步运算变成五行代码加四个临时数组,参数顺序错一处就不易察觉;级联写法只需一行,结果的类型与层次一目了然。外部数据本来就是数组时也不受影响:本库的 vec3_v()、quat_v()、mat3_v()、mat3_vv() 只负责"从数组构造",在数据边界处转换一次即可进入级联,例如

    c 复制代码
    /* pa 为外部 ftype_t[3] 数据 */
    ftype_t a = vec3_angle(vec3_unit(rotation_qRv(quat_normalize(q), vec3_v(pa))), vref);
  8. 性能未必有损失。
    这类小对象(3~9 个标量)通常能被编译器充分优化,更容易命中 cache,也利于寄存器分配与内联,按值传递往往比想象中更高效。

相关推荐
吾日三省Java7 个月前
SpringBoot锁设计:让你的系统不再“抢”出问题!
java·spring boot·设计思路