四元数到矩阵 Quaternion to Matrix
将代表旋转的 单位四元数 转换为 同样代表旋转的 3×3 矩阵可能是常见的需求,3×3 旋转矩阵可以看成由三个 列向量 x,y,z 轴基向量 在 世界坐标 中的方向组成,而最直接的做法就是将这三个 标准基向量 (1,0,0),(0,1,0),(0,0,1) 分别用四元数旋转,得到的新向量作为矩阵的三列
Matrix3 ToMatrix_NonOptimal(Quaternion quat)
{
// 1) 定义三条基向量
Vector3 right = Vector3(1, 0, 0); // x 轴
Vector3 up = Vector3(0, 1, 0); // y 轴
Vector3 forward = Vector3(0, 0, 1); // z 轴
// 2) 用四元数旋转三条向量
right = Mul(right, quat);
up = Mul(up, quat);
forward = Mul(forward, quat);
// 3) 把结果装进矩阵
return Matrix3(
right.x, up.x, forward.x, // 第一行
right.y, up.y, forward.y, // 第二行
right.z, up.z, forward.z // 第三行
);
}
Mul(v, quat) 表示把向量 v 通过四元数 quat 进行旋转
也可以构造完整的 4×4 变换矩阵
Matrix4 ToMatrix(Quaternion q) {
Vector3 r = q * Vector3(1, 0, 0); // 右轴 (Right)
Vector3 u = q * Vector3(0, 1, 0); // 上轴 (Up)
Vector3 f = q * Vector3(0, 0, 1); // 前轴 (Forward)
// OpenGL 列主序 4×4 矩阵(平移列保持 [0 0 0 1])
return Matrix4(
r.x, r.y, r.z, 0,
u.x, u.y, u.z, 0,
f.x, f.y, f.z, 0,
0 , 0 , 0 , 1
);
}
从矩阵到四元数 From Matrix to Quaternion
我们也可以反向操作,从 旋转矩阵 构建对应的 四元数
Quaternion FromMatrix(Matrix4 m)
{
// 1) 从矩阵里取出“上轴”和“前轴”列向量,并标准化
Vector3 up = Normalized(Vector3(m[0], m[1], m[2]));
Vector3 forward = Normalized(Vector3(m[8], m[9], m[10]));
// 2) 通过叉乘重建“右轴”,并再一次正交化 up
Vector3 right = Cross(up, forward);
up = Cross(forward, right);
// 3) 根据 forward/up 做一次 look-at,得到旋转四元数
return lookAt(forward, up);
}
理论上一个纯旋转矩阵的列向量应互相正交且单位长度,但在数值误差或包含缩放的情况下可能不再正交,所以上图用 Cross 与 Normalized 做一次正交化,获取到想要的目标 forward 和 up 以后,就可以使用四元数 lookAt 创造一个将 局部坐标系 的 forward (0,0,1) 和 up (0,1,0) 旋转至 目标 的四元数
矩阵 vs 四元数 表示旋转
| 维度 | 旋转矩阵 (R) | 四元数 (q) |
|---|
| 存储量 | 9 个浮点数 (或 12 个若含齐次列) | 4 个浮点数 |
| 约束 | 列向量需保持正交且单位长度 (R · Rᵀ = I, det R = 1) | 需保持归一化 (‖q‖ = 1);q 与 −q 表示同一姿态 |
| 变换向量代价 | v' = R·v —— 9 乘 + 6 加 | v' = q v q⁻¹ |
| 组合(叠加)旋转 | 矩阵乘:R_new = R2 · R1 (约 27 乘) | 四元数乘:q_new = q2 * q1 (约 16 乘) |
| 差值插值 | 需先正交化或用 SVD;LERP 会失真 | 可用 SLERP / NLERP,自然保持单位长度、无万向锁 |
| 数值稳定 | 连续相乘误差会破坏正交,需要定期正交化 | 连续相乘误差只影响长度,归一化即可 |
| 包含缩放/投影 | 可以同时携带非均匀缩放、剪切 (但会破坏正交) | 只能表示纯旋转 |
| 直观性 | 易理解:列=三个轴的方向;可直接观察 | 抽象;但几何意义明确 (旋转轴+半角) |
| 奇异/万向锁 | 无;覆盖全 SO(3) 唯一 | 无;但 q 与 –q 双覆盖 |
| 硬件/API 支持 | GPU 直接接受矩阵、可与齐次变换合并 | GPU 常先转矩阵上传 |
| 常见用途 | 顶点着色、骨骼变换、齐次坐标链 | 姿态累积、插值动画、物理仿真、摄像机轨迹 |
实践做法:常见 “内部四元数、外部矩阵”——内部算法/动画用四元数,真正绘制前一次性转成矩阵上传即可
四元数转换为矩阵的更优化方法
虽然对 单位矩阵 的每个轴做旋转的方法可以很直观的将 四元数 代表的旋转转换为 旋转矩阵,实际上也很容易,但是它在计算上并不高效,它需要执行多次的四元数旋转向量的操作,我们有办法更高效的执行这个转换过程
我们知道,四元数旋转向量的过程是
v′=qvq∗
- v′ 是旋转后的向量 (纯四元数)
- v 是原向量 (纯四元数)
- q 和 q∗ 互为 共轭
设
q=(qw,qx,qy,qz),q∗=(qw∗,qx∗,qy∗,qz∗)=(qw,−qx,−qy,−qz),v=(vw,vx,vy,vz)
四元数乘法表
| 虚部 | i | j | k |
|---|
| i | −1 | k | -j |
| j | -k | −1 | i |
| k | j | -i | −1 |
我们早在第一章就介绍过,四元数乘法可以写成 矩阵形式
qv=(qw,qx,qy,qz)(vw,vx,vy,vz)=(qw+qxi+qyj+qzk)(vw+vxi+vyj+vzk)=qwvw+qwvxi+qwvyj+qwvzk+=qxvwi+qxvxi2+qxvyij+qxvzik+=qyvwj+qyvxji+qyvyj2+qyvzjk+=qzvwk+qzvxki+qzvykj+qzvzk2=qwvw+qwvxi+qwvyj+qwvzk+=qxvwi−qxvx+qxvyk−qxvzj+=qyvwj−qyvxk−qyvy+qyvzi+=qzvwk+qzvxj−qzvyi−qzvz=qwvw−qxvx−qyvy−qzvz+=(qwvx+qxvw+qyvz−qzvy)i+=(qwvy−qxvz+qyvw+qzvx)j+=(qwvz+qxvy−qyvx+qzvw)k+=qwvw−qxvx−qyvy−qzvz+=(qxvw+qwvx−qzvy+qyvz)i+=(qyvw+qzvx+qwvy−qxvz)j+=(qzvw−qyvx+qxvy+qwvz)k+=qwqxqyqz−qxqwqz−qy−qy−qzqwqx−qzqy−qxqwvwvxvyvz
同理
vq∗=(vw,vx,vy,vz)(qw∗,qx∗,qy∗,qz∗)=(vw,vx,vy,vz)(qw,−qx,−qy,−qz)=(vw+vxi+vyj+vzk)(qw−qxi−qyj−qzk)=vwqw−vwqxi−vwqyj−vwqzk+=vxqwi−vxqxi2−vxqyij−vxqzik+=vyqwj−vyqxji−vyqyj2−vyqzjk+=vzqwk−vzqxki−vzqykj−vzqzk2=vwqw−vwqxi−vwqyj−vwqzk+=vxqwi+vxqx−vxqyk+vxqzj+=vyqwj+vyqxk+vyqy−vyqzi+=vzqwk−vzqxj+vzqyi+vzqz=vwqw+vxqx+vyqy+vzqz+=(−vwqx+vxqw−vyqz+vzqy)i+=(−vwqy+vxqz+vyqw−vzqx)j+=(−vwqz−vxqy+vyqx+vzqw)k=vwqw+vxqx+vyqy+vzqz+=(−vwqx+vxqw−vyqz+vzqy)i+=(−vwqy+vxqz+vyqw−vzqx)j+=(−vwqz−vxqy+vyqx+vzqw)k=qw−qx−qy−qzqxqwqz−qyqy−qzqwqxqzqy−qxqwvwvxvyvz
所以我们得出,
qv=qwqxqyqz−qxqwqz−qy−qy−qzqwqx−qzqy−qxqwvwvxvyvz
这是将 qv 写为 矩阵形式 的相乘,可以将 q 所代表的矩阵定义为 左乘矩阵 L(q),因为 q 乘在 v 的 左边,那么可以得到
qv=L(q)vcol
- vcol 是写为 列向量 形式的 纯四元数
同样,可以将 q∗ 所代表的矩阵定义为 右乘矩阵 R(q∗),因为 q∗ 乘在 v 的 右边,我认为这是一个糟糕的名字,因为以矩阵乘法形式表示时,R(q∗) 仍然乘在 列向量 的左边
vq∗=qw−qx−qy−qzqxqwqz−qyqy−qzqwqxqzqy−qxqwvwvxvyvz=R(q∗)vcol
- vcol 是写为 列向量 形式的 纯四元数
如此一来,我们得到
qvq∗=L(q)R(q∗)vcol
只要计算出 L(q)R(q∗) 就可以将表示旋转的 四元数 转换为 旋转矩阵 形式
L(q)R(q∗)=qwqxqyqz−qxqwqz−qy−qy−qzqwqx−qzqy−qxqwqw−qx−qy−qzqxqwqz−qyqy−qzqwqxqzqy−qxqw=w2+x2+y2+z20000w2+x2−y2−z22xy+2wz2xz−2wy02xy−2wzw2−x2+y2−z22yz+2wx02xz+2wy2yz−2wxw2−x2−y2+z2
注意有些推导可能最终是下面这样
w2+x2−y2−z22xy+2wz2xz−2wy02xy−2wzw2−x2+y2−z22yz+2wx02xz+2wy2yz−2wxw2−x2−y2+z20000w2+x2+y2+z2
这和我们的推导结果在逻辑上并没有什么不同,只是我们使用的是 (w,x,y,z),而这个结果则使用 (x,y,z,w)
所以 四元数 形式表示的旋转 转换为对应的 矩阵 形式表示,也可以如下实现
Matrix4 ToMatrix(Quaternion q) {
float ww = q.w * q.w;
float xx = q.x * q.x;
float yy = q.y * q.y;
float zz = q.z * q.z;
float wx = q.w * q.x;
float wy = q.w * q.y;
float wz = q.w * q.z;
float xy = q.x * q.y;
float xz = q.x * q.z;
float yz = q.y * q.z;
return Matrix4(
ww + xx - yy - zz, 2 * xy - 2 * wz, 2 * xz + 2 * wy, 0,
2 * xy + 2 * wz, ww - xx + yy - zz, 2 * yz - 2 * wx, 0,
2 * xz - 2 * wy, 2 * yz + 2 * wx, ww - xx - yy + zz, 0,
0, 0, 0, ww + xx + yy + zz
);
// Just so there is no confusion, the above constructor takes arguments
// in row order, but stores them in column order (transposed).
// See the non-optimal example for memory layout.
}
如果要转换为 3×3 旋转矩阵 而不是 4×4 的矩阵,只需要丢弃 最后一行 和 最后一列,也就是很多 0 的那行和列
这种方法相比起 旋转基向量 来将 四元数 转换为 矩阵,计算上更为高效,在工程中几乎总是使用这种 公式展开法,因为它
- 快(没有 3 次四元数-向量乘法)
- 准(不需要显式归一化)
- 结果自然正交(如果输入四元数是单位的,而表示旋转的四元数应当是单位四元数,否则说明存在问题)