设计思路
三维向量、旋转矩阵、四元数、欧拉角和轴角这些量的数学公式实现并不复杂,很多应用自行实现所需算法,也有不少功能全面的开源数学库可用。但"能用"和"好用"是两个差别很大的概念:自实现的模块往往不规范、功能不全面,甚至存在算法错误,也难在其他项目中复用;现成的通用库又常常过于庞大、缺乏针对性,移植成本高。基于对算法与需求的既有理解,以及一些蠢蠢欲动的新想法和对漂亮代码的偏执,特开发了此数学库,重点解决三个问题------约定容易混乱 、功能边界不完整 、数值细节不稳健。
为此,库确立了以下几条全局设计约定:
- 统一的浮点类型与常量体系。 精度由
CFG_FLOATWORD_BITSIZE一处配置,ftype_t是唯一的浮点类型名;所有常量宏按配置取值,MFLOAT_C(c)保证字面量类型与当前精度一致,MFABS、MSQRT、MATAN2等数学宏按精度映射到fabsf/fabs/fabsl等实现。切换精度只需改一行宏,所有调用点无需改动,从根本上杜绝 float 与 double 混用导致的隐式转换与精度损失。 - 统一的类型表示与多视图访问。 五种类型都以结构体承载语义,成员名即含义;同时以联合体保留数组视图,便于与外部数组数据(文件、总线、第三方库)零成本互操作。枚举
MINDEX_*为数组下标提供具名索引,避免魔法数字。旋转矩阵采用列优先存储,与数学书写惯例一致,且同一列元素在内存中连续,按列访问及矩阵乘向量时 cache 命中更好。 - 统一的欧拉角转序配置。 默认转序由
CFG_EULER_ORDER全局指定,可选"xyz"、"xzy"、"yxz"、"yzx"、"zxy"、"zyx"六种;全局使用一致转序可避免多转序混用导致的隐蔽错误,同时简化参数配置。确需在局部使用其它转序时,可用rotation_e2m_order、rotation_m2e_order、rotation_e2q_order、rotation_q2e_order直接指定,兼顾简洁与灵活。 - 值语义的接口形态。 除按数组构造的
*_v/*_vv接口外,入参与返回值均为结构体值,没有指针参数,也无需空指针检查;调用可级联书写,例如vec3_unit(vec3_rotate(v, axis, angle))、quat_normalize(quat_mul(q0, q1))。 - 统一的命名规约。 类型以单字母编码(
a轴角、e欧拉角、m旋转矩阵、q四元数、v向量),旋转接口统一为rotation_XRY,rotation_qRv一眼可读出"用四元数旋转向量";*_v/*_vv表示按数组构造,*_order表示显式指定转序,*_eps表示自定义容差。签名自解释,不读文档也能正确调用。 - 实现与测试同步。 每个接口在实现的同时编写单元测试,库源文件与测试源文件一一对应,接口与用例同步演进;测试可一键自动运行并输出统计结果,使功能调整、精度切换和重构都有回归保障。
不追求过度的通用性------那会增加传参与实现的复杂度、降低易用性。库只解决空间平移与旋转线性计算这一类问题,并把这类问题的接口做到完整、明确、不易误用。
为什么用结构体而不是数组
使用结构体而非数组来表示三维向量、三维旋转矩阵、四元数、欧拉角和轴角,主要有以下考虑。
-
可读性更好,成员名本身就是文档。
结构体成员名直接表达含义,q.w、q.x、axis.angle比q[0]、a[3]直观得多,阅读者不必反复查约定,也不必依赖注释确认下标含义。结构体是封闭的对象化表示,数组则把内部布局直接暴露给使用者。 -
消除顺序歧义与对应错误。
如果是数组表示,三维向量中v0、v1、v2 分别对应x、y、z轴的坐标可能不容易出错。但对于四元数,v[0]和v[3]哪个是常量?三维旋转矩阵是行优先还是列优先? 轴角中,a[0]和a[3]哪个是角度?实际情况是不同顺序约定都有库在使用,极容易搞混淆。经验表明,容易出错的地方就一定会出错。成员名把含义强制写清楚,至少在本库内部形成稳定、明确的语义,降低跨模块、跨团队协作时的沟通与出错成本。 -
避免越界访问。
数组下标写错就会越界读写,甚至导致程序崩溃;结构体成员由编译器按名字解析,不存在"下标越界"问题,可显著减少这类低级但危险的错误。 -
避免类型错判。
五种类型若都用数组表示,类型信息随之丢失,容易错判,并因各类型长度不同而级联导致越界访问。 -
可直接赋值。
同类型结构体之间可直接写a = b;,数组则必须借助memcpy或循环赋值。按值传参使函数内部获得独立副本,调用者的数据不会被意外修改,局部性、可重入性与并发安全性更好。 -
便于类型检查与接口约束。
参数要求quat_st时,传入vec3_st或mat3_st会被编译器发现;数组因底层类型相近,容易在编译期蒙混过关,把错误推迟到运行期。 -
便于实现级联调用。
接口以传值方式输入输出,可以像数学公式一样层层嵌套,一行完成多级运算。例如把两个姿态q0、q1复合出旋转,归一化后旋转向量v,再单位化,最后与参考方向vref求夹角,直接用本库接口写成:cftype_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); -
性能未必有损失。
这类小对象(3~9 个标量)通常能被编译器充分优化,更容易命中 cache,也利于寄存器分配与内联,按值传递往往比想象中更高效。