Creating Quaternions 创建四元数
整理从角轴、from-to shortest arc、Look At 以及旋转矩阵创建四元数的方法。
Creating Quaternions 创建四元数
我们一般直接使用 角度-转轴 信息来创建四元数,我们想要表示的是绕 给定轴向 进行 逆时针 旋转 的行为
根据我们前面章节的推导,我们知道用于表示 旋转 的 四元数 是一个特殊的 单位四元数,它的 标量 分量 并不直接等于旋转的角度 ,但却与之相关,准确来说是
旋转发生在与转轴 正交 的平面内,也就是说平面的 法向量 就是转轴方向,为了保持 四元数 的长度为 ,需要对转轴 (单位向量) 进行大小为 的缩放才能保持 四元数 整体为 单位四元数,这也在我们之前的推导中有所体现
- 正的角度表示 逆时针 旋转
Quaternion AngleAxis(float degrees, Vec3 axis) {
float radians = degrees * 0.0174533f;
if (MagnitudeSq(axis) != 1) { // Do epsilon check here!
axis = Normalize(axis);
}
Quaternion result;
result.x = axis.x * sinf(radians * 0.5f);
result.y = axis.y * sinf(radians * 0.5f);
result.z = axis.z * sinf(radians * 0.5f);
result.w = cosf(radians * 0.5f);
return result;
}从一个方向转向另一个方向的最短弧 shortest arc of from-to
如果我们已知两个 归一化 的方向向量 和 ,如何如何求出 “最短”、“唯一” 的那一次旋转,把 转到 ?实际上存在无数种旋转,但是我一般来说想要的是代表 最短弧旋转 的那个 四元数
从几何上看,可以将 单位向量 看成位于 单位球面 上的 “点”,连接 , 以及 球心 形成的平面,球面与该平面的交线是一条大圆弧;在这条弧上的弧长就是 “最短弧”

该弧所在的平面垂直于一条轴,这条轴的方向就是 转轴方向,它可以借助叉乘得出,我们知道 叉乘 是
- 向量叉乘给出的结果天然正交于两个输入向量
因为我们假设 和 都是 单位向量,所以 ,那么
我们可以求得 转轴
而两向量的夹角,也就是应当旋转的 角度,则可以由点乘得出
- 图中的 是指如果使用四元数,那么应该使用 半角
如此一来我们就能得到 角度-转轴 信息来构造表达这个旋转的 四元数,但是由于 或者说 acos 在运行时计算的很慢,我们可以用一些技巧来避免计算它,我们知道
而要构造从 旋转到 的 四元数,根据我们前面章节的推导可得
此时 向量 部分的 近乎是 叉乘 ,而 标量 部分的 则近乎是 点乘 ,之所以说是 “近乎” 是因为构造这个 四元数 需要使用 半角 ,而 和 中使用的则是原始的 全角 ,我们有两种办法来等到等价的 半角 结果
-
半程向量
将 归一化 的 和 相加之后再 归一化,得到 半程向量然后只求 旋转到 的 四元数 ,这本质上就是 “半角” 了
Quaternion FromToRotation(Vector3 from, Vector3 to) { Vector3 p0 = Normalize(from); Vector3 p1 = Normalize(to); if (p0 == -p1) { Vector3 mostOrthogonal = Vector3(1, 0, 0); // 分量最小的那个轴就是最 正交 于 p0 的轴 if (abs(p0.y) < abs(p0.x)) { mostOrthogonal = Vector3(0, 1, 0); } if (abs(p0.z) < abs(p0.y) && abs(p0.z) < abs(p0.x)) { mostOrthogonal = Vector3(0, 0, 1) } Vector3 axis = Normalize(Cross(p0, mostOrthogonal)); return Quaternion(axis.x, axis.y, axis.z, 0); } Vector3 half = Normalize(p0 + p1); Vector3 axis = Cross(p0, half); Quaternion result; result.x = axis.x; result.y = axis.y; result.z = axis.z; result.w = Dot(p0, half); return result; }一个需要处理的问题是,如果两个向量方向完全相反 (即 ) 这意味着旋转角度为 ,在这种情况下,直接通过这两个向量的 叉积 无法计算旋转轴,因此必须通过一个与 正交的向量来构建旋转轴,做法是挑一个与 最不平行的坐标轴 (分量最小的那个轴),再取
axis = normalize(p0 × mostOrthogonal)生成 四元数 -
平方根整旋转
算出 全角 的 完整旋转四元数 ,也就是说直接使用 和 来计算然后,想要 “旋转减半” 就是要一个新 四元数 ,满足 ,对 单位四元数 来说,这等同于把 向量部分 乘以单位化的系数,使角度减半,再正规化
我们已经知道两条已 归一化 的向量
那么可以定义
其中 是它们的 夹角,目标是构造把 旋转到 的 最短弧四元数 而不显式求
我们知道 半角恒等式
以及 标准轴–角 到 四元数 的公式是
- 是 单位旋转轴
标量部分
向量部分
所以
虽然这已经不需要显式计算 ,只需要计算 点乘 和 叉乘,但是我们仍然有优化的空间
根据 正弦的倍角公式 ,我们知道
再加上之前已经提过的 半角恒等式
我们可以将 向量部分 改写为
相应的,我们也将 标量部分 写为 的形式
所以我们从头到尾 只需要计算一次平方根 即可
Quaternion FromToRotationFast(vec3 from, vec3 to) { vec3 p0 = normalize(from); vec3 p1 = normalize(to); const float EPS = 1e-6f; float d = dot(p0, p1); // (a) 同向 if (d > 1.0f - EPS) // θ ≈ 0 return quat(0,0,0,1); // (b) 反向 if (d < -1.0f + EPS) // θ ≈ π,需要任选正交轴 { vec3 ortho = (fabs(p0.x) < 0.6f) ? vec3(1,0,0) : (fabs(p0.y) < 0.6f) ? vec3(0,1,0) : vec3(0,0,1); vec3 axis = normalize(cross(p0, ortho)); return quat(axis.x, axis.y, axis.z, 0); // 180° } // (c) 一般情况:只用一次 sqrt vec3 c = cross(p0, p1); // 3 mul, 3 sub float s = sqrtf(2.f * (1.f + d)); // 唯一一次 sqrt float inv = 1.f / s; // 或用 rsqrt 快速倒数 return quat(c.x * inv, // = c / √(2(1+d)) c.y * inv, c.z * inv, 0.5f * s); // = √((1+d)/2) }- ALU 统计(常规路径)
1 × dot + 1 × cross1 × sqrt + 1 × rsqrt/recip:若rsqrt有硬件指令,可把sqrt+div折成rsqrt + 1乘
- 访存
只读2 × vec3,写1 × quat;无额外临时向量存储
这里,不需再做第二次
sqrt,因为s已经是那条根号;也不需再做一次除法——乘常量0.5会跟最终归一化常数折叠到同一条 FMA(乘-加)指令里,CPU/GPU 都很便宜,这就完成了 最短弧旋转四元数 的构造,且再无额外根号或除法
Look At
要把物体 局部坐标 的 前向(forward) 转到 目标方向(direction),同时保证物体的新 up 方向 与期望的 up 向量 一致,我们需要
| 步骤 | 说明 | 关键运算 |
|---|---|---|
| ① 找旋转 | 求一个四元数,把默认前向 (0, 0, 1) 转到目标方向 direction | from-to rotation |
| ② 正交化参考坐标 | 先算右向 right = cross(direction, desiredUp),再算新的 up:desiredUp = cross(right, direction),保证 up ⟂ direction | 叉积 |
| ③ 计算物体原 up | 用步骤①得到的四元数把参考 up (0, 1, 0) 旋转过去,得到 “物体眼中的 up” | 向量×四元数 |
| ④ 再建四元数 | 求一个四元数,把步骤③得到的 up 转到步骤②的 desiredUp | from-to rotation |
| ⑤ 组合旋转 | 把①④两个四元数相乘(先用①,再用④,所以乘法顺序反过来)得到最终旋转 | 四元数乘 |
四元数复合后仍然只是 一次旋转,但它等价于“先绕球面走到目标前向,再绕同一球面走到正确的 up”。因此组合后的结果角轴既唯一又没有万向锁
Quaternion LookAt(Vector3 direction, Vector3 desiredUp) {
// Normalize input data
direction = Normalize(direction);
desiredUp = Normalize(desiredUp)
// 共线判定
const float EPS = 1e-5f;
if (length(direction) < EPS) return Quaternion::Identity();
if (abs(dot(direction, desiredUp)) > 1.f - EPS) {
// 选一个与 direction 最不平行的世界轴替代 up
desiredUp = abs(direction.y) < 0.99f ? Vector3(0,1,0) : Vector3(1,0,0);
}
// Step 1, Find quaternion that rotates from forward to direction
Quaternion fromForwardToDirection = FromToRotation(Vector3(0, 0, 1), direction);
// Step 2, Make sure up is perpendicular to desired direction
Vector3 right = Cross(direction, desiredUp);
desiredUp = Cross(right, direction);
// Step 3, Find the up vector of the suaternion from Step 1
// Quaternion-vector multiplication (will be covered later)
Vector3 objectUp = Mul(Vector3(0, 1, 0), fromForwardToDirection);
// Step 4, Create quaternion from object up to desired up
Quaternion fromObjectUpToDesiredUp = FromToRotation(objectUp, desiredUp);
// Step 5, Combine rotations (in reverse! forward applied first, then up)
// Quaternion-quaternion multiplication (will be covered later)
Quaternion result = Mul(fromObjectUpToDesiredUp, fromForwardToDirection);
// Should not be needed, but normalize output data
return Q_Normalize(result);
}从四元数提取 旋转轴 与 旋转角
- 轴(axis) = 取四元数的向量部分
(x,y,z)再归一化Vector3 GetAxis(Quaternion q) { return normalize(Vector3(q.x, q.y, q.z)); } - 角(angle) = 利用标量部分 w = cos(θ/2)
float GetAngleRadians(Quaternion q) { return 2 * acos(q.w); } float GetAngleDegrees(Quaternion quat) { return 2.0 * acos(quat.w) * 57.2958;}
