3D游戏中的数学 - 四元数
四元数(quaternion)在3D游戏中主要用于表示旋转。
四元数 \(q = [q_x, q_y, q_z, w]\) 看起来像是一个四维向量(但并非向量)。
单位长度(unit-length)四元数(\(|q| = 1\))用于表示3D旋转,本文只考虑单位四元数。
单位四元数可以看作一个三维向量 \(\boldsymbol{q}_V\) 外加一个标量 \(q_S\)。
以单位向量 \(\boldsymbol{a}\) 为旋转轴,\(\theta\) 为旋转角度,单位四元组 \(\boldsymbol{q}\) 表示为:
$$ \begin{align} q & = [\boldsymbol{q}_V \quad q_S] \\ & = [\boldsymbol{a}\sin\frac{\theta}{2} \quad \cos\frac{\theta}{2}] \end{align} $$四元数运算
Grassman 乘法
实际上四元数有多种乘法,这里只讨论 Grassman 乘法。
多个四元数相乘表示组合。
$$ p q = [ (p_S\boldsymbol{q}_V + q_S\boldsymbol{p}_V + \boldsymbol{p}_V\times\boldsymbol{q}_V) \quad (p_S q_S - \boldsymbol{p}_V\bullet\boldsymbol{q}_V) ] $$逆四元数
四元数 \(q\) 的逆记为 \(q^{-1}\)。可以将逆四元数理解为反向操作。
$$q q^{-1} = [0 \quad 0 \quad 0 \quad 1]$$
共轭四元数(conjugate)记为 \(q^{\ast}\)。
$$q^\ast = [-\boldsymbol{q}_V \quad q_S]$$
$$q^{-1} = \frac{q^{\ast}}{|q|^2}$$
由于我们只考虑单位四元数(\(|q| = 1\)),所以:
$$q^{-1} = q ^\ast = [-\boldsymbol{q}_V \quad q_S] \qquad (当\ |q| = 1)$$
这意味着针对单位四元数可以快速求逆。
$$(p q)^\ast = q^\ast p^\ast$$
$$(p q)^{-1} = q^{-1} p^{-1}$$
四元数应用于向量旋转
首先将三维向量 \(\boldsymbol{v}\) 转化为四元数形式 \(v\):
$$v = [\boldsymbol{v} \quad 0]$$
通过单位四元数 \(q\) 对向量 \(\boldsymbol{v}\) 旋转得到 \(v'\):
$$v' = rotate(q, \boldsymbol{v}) = q v q^{-1} = q v q^\ast$$
最后从结果四元数 \(v\) 中提取结果向量 \(\boldsymbol{v}\)。
多个四元数组合:
$$q_{net} = q_3 q_2 q_1$$
$$ \begin{align} v' & = q_3 q_2 q_1\ v\ q_1^{-1} q_2^{-1} q_3^{-1} \\ & = q_{net}\ v\ q_{net}^{-1} \end{align} $$四元数与变换矩阵
四元数可以与旋转变换矩阵(3x3形式)相互转化。
这里的变换矩阵为仅包含3x3旋转部分的矩阵。
设四元数 \(q = [x\ y\ z\ w]\) ,其对应的旋转矩阵为 \(\boldsymbol{R}\)
$$ \boldsymbol{R} = \begin{bmatrix} 1-2y^2-2z^2 & 2xy+2zw & 2xz-2yw \\ 2xy-2zw & 1-2x^2-2z^2 & 2yz+2xw \\ 2xz+2yw & 2yz-2xw & 1-2x^2-2y^2 \end{bmatrix} $$同样的,给定旋转矩阵 \(\boldsymbol{R}\),可以找到对应的四元数 \(q\)。代码引用自 Gamasutra [Nick Bobic]。
void matrixToQuaternion(const float R[3][3], float q[/*4*/]) {
float trace = R[0][0] + R[1][1] + R[2][2];
// check the diagonal
if (trace > 0.0f) {
float s = sqrt(trace + 1.0f);
q[3] = s * 0.5f;
float t = 0.5f / s;
q[0] = (R[2][1] - R[1][2]) * t;
q[1] = (R[0][2] - R[2][0]) * t;
q[2] = (R[1][0] - R[0][1]) * t;
} else {
// diagonal is negative
int i = 0;
if (R[1][1] > R[0][0]) i = 1;
if (R[2][2] > R[i][i]) i = 2;
static const int NEXT[3] = {1, 2, 0};
int j = NEXT[i];
int k = NEXT[j];
float s = sqrt((R[i][j] - (R[j][j] + R[k][k])) + 1.0f);
q[i] = s * 0.5f;
float t;
if (s!=0.0) t = 0.5f / s;
else t = s;
q[3] = (R[k][j] - R[j][k]) * t;
q[j] = (R[j][i] + R[i][j]) * t;
q[k] = (R[k][i] + R[i][k]) * t;
}
}
线性插值
常用于动画系统、动作系统、Camera系统。
在 \(q_A\) \(q_B\) 之间进行插值,\(\beta\) 为过程百分比:
$$ \begin{align} q_{LERP} & = LERP(q_A, q_B, \beta) \\ & = \frac{(1-\beta)q_A+\beta q_B}{|(1-\beta)q_A+\beta q_B|} \end{align} $$四元数可以看作是四维超球体上的点,LERP 实际上是沿着超球体的弦进行插值,而非超球体的表面。这导致使用 LERP 时四元数的变化速度(角速度)不固定,而是两端快中间慢。
相比 LERP,SLERP 是沿着超球体的表面进行插值。
$$SLERP(p, q, \beta) = w_p p + w_q q$$
$$ w_p = \frac{\sin(1-\beta)\theta}{\sin \theta} \qquad w_q = \frac{\sin \beta\theta}{\sin \theta} $$ $$ \begin{align} \cos\theta & = p\bullet q = p_x q_x + p_y q_y + p_z q_z + p_w q_w \\ \theta &= \cos^{-1}(p \bullet q) \end{align} $$一般来说,SLERP 相对开销较大,但存在可降低开销的优化算法。实际应用中,可以通过对实际代码进行性能分析来确定使用 SLERP 或 LERP。