四元数

Reference

游戏引擎架构—4.4四元数

Intro

$3\times 3$矩阵可以表示三维中的任意旋转

但是这并不是理想的表现形式, 因为:

  • 矩阵需要九个浮点值表示旋转, 而旋转只有三个自由度(degree of freedom, DOF)—偏航角, 俯仰角, 滚动角.
  • 矢量矩阵旋转一个点, 需要三个点积.
  • 在游戏和计算机图像学中, 经常需要计算两个已知旋转的中间状态. 比如平滑地将摄像机在几秒内从起始定向A旋转到目标定向B, 这需要计算中间许多旋转.

我们可以用四元数(quaternion)解决上述问题, 四元数看似四维向量, 但行为上有很大区别. 通常将四元数写为非斜非粗体.

四元数遵守一组规则, 这些规则称为实数域上的四维赋范可除数. 我们只需要知道单位长度的四元数($q_x^2+q_y^2+q_z^2+q_w^2=1$)能代表三维旋转.

单位四元数与三维旋转

其中$\mathbf{a}$为旋转轴方向的单位矢量, $\theta$是旋转的角度, 旋转方向使用右手定则.

其中

四元数运算

四元数乘法

给定两个四元数p和q, 分别代表旋转$P$和$Q$, 则pq代表两旋转的合成旋转(先旋转$Q$再旋转$P$). 四元数乘法有几种, 这里只讨论和三维旋转应用有关的乘法. 此乘法称为格拉斯曼积(Grassmann product).

共轭四元数

逆四元数

四元数q的逆为$q^{-1}$, 逆四元数和原四元数的乘积为标量1

而逆四元数为

当q为单位四元数时

对于两个四元数

用四元数旋转矢量

给定三维矢量$\mathbf v$, 将其写为四元数形式$v=\begin{bmatrix}\mathbf v & 0 \end{bmatrix}=\begin{bmatrix}v_x & v_y & v_z & 0\end{bmatrix}$

使用四元数q旋转矢量$\mathbf{v}$:

因为旋转用的四元数都是单位长度, 所以使用共轭也是等同的, 只需要提取$v’$的矢量部分

上述公式还可以改写简化

此公式无需将$\mathbf{v}$转化为四元数, 而且运算量也有所减少

旋转有很多应用, 比如游戏中将模型空间中的矢量转移到世界空间.

四元数的串接

四元数可以通过相乘串接旋转. 比如三个四元数$q_1,q_2,q_3$, 其对于等价矩阵为$R_1,R_2,R_3$. 我们希望求得合成旋转矩阵$R_{net}$和其旋转矢量$\mathbf{v}$:

等价的四元数和矩阵

接下来我们探讨三维旋转的$3\times 3$矩阵表示方式$R$和四元数表示方式$q$之间自由转换.

$q$ -> $R$

设$q=\begin{bmatrix}\mathbf{q}_V & q_S\end{bmatrix}=\begin{bmatrix}
q_x & q_y & q_z & q_w\end{bmatrix}=\begin{bmatrix}x&y&z&w\end{bmatrix}$

$R$ -> $q$

R[row][col], q[4]=q[qx qy qz qw]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void MatrixToQuaterion
(
const float R[3][3],
float q[4]
)
{
float trace = R[0][0] + R[1][1] + R[2][2];

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 {
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][i] - (R[j][j] + R[k][k]))
+ 1.0f
);

float t;
if (s != 0.0f) 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;
}
}

旋转性的线性插值

如intro中举例, 游戏引擎中动画, 动力学, 摄像机系统, 都需要旋转性的插值. LERP为较简单的插值算法

四元数$q_A$和$q_B$对应两个旋转$A$和$B$, $\beta$为旋转百分点, $q_{LERP}$为中间旋转

1543493357926

LERP问题在于, 没考虑四元数是四维超球上的点. LERP实际上是沿超球的弦上插值. 插值的角速度不是恒定的.

解决此问题的方法是使用球面线性插值算法(SLERP). 如此后插值的角速度恒定.

1543493553501

SLERP和LERP类似, 但使用$w_p$和$w_q$取代原加权值$(1-\beta)$和$\beta$. 其中需要用到两个四元数之间夹角的正弦$\theta$.

因为SLERP的代价较高, 对于是否在游戏引擎中使用SLERP有争论. 但其实良好优化的SLERP表现也不差. 顽皮狗团队的SLERP的良好实现, 效能接近LERP. (SLERP 20 ticks, LERP 16.25 ticks). 在具体引擎中, 需要取舍.

文章目录
  1. 1. Reference
  2. 2. Intro
  3. 3. 单位四元数与三维旋转
  4. 4. 四元数运算
    1. 4.1. 四元数乘法
    2. 4.2. 共轭四元数
    3. 4.3. 逆四元数
    4. 4.4. 用四元数旋转矢量
      1. 4.4.1. 四元数的串接
    5. 4.5. 等价的四元数和矩阵
    6. 4.6. 旋转性的线性插值
|