游戏数学之linear algebra(二)
本文转自我的公众号—游戏开发手账
转载请标明出处
来看看游戏世界中的物体是怎么旋转的。
——我说的
上一篇提到了使用矩阵变换来帮助旋转,并且4 x 4的仿射矩阵还能完成包括旋转、平移、缩放和切边及其组合在内的仿射变换。
功能确实是能完成,但是总觉得有些冗余,因为旋转明明只有三个自由度(偏航角Yaw、俯仰角Pitch、滚动角Roll),却用了9个浮点值去表示旋转部分,并且用矢量矩阵乘法来进行矢量的旋转需要进行三次点积计算。
[
r
x
r
y
r
z
]
[
1
0
0
0
cos
Φ
sin
Φ
0
−
sin
Φ
cos
Φ
]
\begin{bmatrix} r_{x} & r_{y} & r_{z} \end{bmatrix} \begin{bmatrix} 1 & 0 & 0 \\ 0 & \cos Φ & \sin Φ \\ 0 & -\sin Φ & \cos Φ \\ \end{bmatrix}
[rxryrz]⎣⎡1000cosΦ−sinΦ0sinΦcosΦ⎦⎤
有没有什么更好的方法来减少浮点值的存储并且加速🚀旋转计算呢?
四元数,它似乎听到了你的抱怨。
四元数
先一句话概括四元数:四元数利用旋转轴方向和旋转角度来表达三维旋转,四个元素中,三个矢量一个标量。
具体来说,单位四元数的构成为三维矢量加上第四维的标量,矢量部分是旋转的单位轴乘以旋转半角的正弦值,标量部分是旋转半角余弦值,公式如下👇。
q
=
[
q
⃗
v
q
s
]
=
[
a
⃗
sin
θ
/
2
cos
θ
/
2
]
=
[
q
x
q
y
q
z
q
w
]
q = \begin{bmatrix}\vec q_{v}& q_{s}\end{bmatrix} = \begin{bmatrix} \vec a \sin θ/2 & \cos θ/2\end{bmatrix} = \begin{bmatrix}q_{x}& q_{y}& q_{z} & q_{w}\end{bmatrix}
q=[qvqs]=[asinθ/2cosθ/2]=[qxqyqzqw]
a
⃗
\vec a
a就是旋转轴方向的单位矢量,θ为旋转角度(所以根据公式可以看到使用的是旋转半角θ/2)。
两个旋转的合成旋转用四元数的乘积来表示,对于旋转m和旋转n,mn表示旋转n之后再旋转m,它们的乘法称为格拉斯曼积(Grassmann product),计算公式如下👇。
m
n
=
(
m
s
n
⃗
v
+
n
s
m
⃗
v
+
m
⃗
v
⋅
n
⃗
v
)
(
m
s
n
s
−
m
⃗
v
⋅
n
⃗
v
)
mn = (m_{s}\vec n_{v} + n_{s}\vec m_{v}+ \vec m_{v} \cdot \vec n_{v}) (m_{s}n_{s} - \vec m_{v}\cdot \vec n_{v})
mn=(msnv+nsmv+mv⋅nv)(msns−mv⋅nv)
四元数是单位长度的,这给计算四元数的逆带来了相对更快的速度,因为四元数的共轭为
q
∗
=
[
−
q
⃗
v
q
s
]
q^{*} = \begin{bmatrix} -\vec q_{v}& q_{s} \end{bmatrix}
q∗=[−qvqs],并且四元数的逆的计算为
q
−
1
=
q
∗
/
∣
q
∣
2
q^{-1} = q^{*} / |q|^{2}
q−1=q∗/∣q∣2,四元素的单位长度特性使得逆的计算无需除以模平方。
好了,到了具体的问题:怎样利用上面提到的四元数去旋转一个矢量?
其实就两步:
- 把你要旋转的矢量写成对应的四元数形式
- 左乘 q q q,右乘 q − 1 q^{-1} q−1
把矢量写成四元数形式只需要在他后面的标量位置添0即可,也就是这样:
v
=
[
v
⃗
0
0
]
=
[
v
x
v
y
v
z
0
]
v= \begin{bmatrix}\vec v_{0}& 0\end{bmatrix} = \begin{bmatrix}v_{x}& v_{y}& v_{z}& 0\end{bmatrix}
v=[v00]=[vxvyvz0]
左乘
q
q
q,右乘
q
−
1
q^{-1}
q−1也就是这样:
v
′
=
q
v
q
−
1
=
q
v
q
∗
v' = qvq^{-1} = qvq^{*}
v′=qvq−1=qvq∗(上面已经提到了,因为四元数是单位长度的,所以
q
∗
=
q
−
1
q^{*} = q^{-1}
q∗=q−1)。
再具体一点的例子,上一篇提到了模型空间向世界空间的转换,也涉及到了旋转,那么求一下飞机飞行方向的单位矢量?
飞机飞行的方向,自然就是它的向前方向,在飞机的模型空间中,它的向前方向是
[
0
0
1
]
\begin{bmatrix}0& 0& 1\end{bmatrix}
[001],把模型空间的这个方向(先转换成对应的四元数
[
0
0
1
0
]
\begin{bmatrix}0& 0& 1& 0\end{bmatrix}
[0010])转换至世界空间就需要用上面的方法左乘代表飞机定向的四元数
q
q
q,右乘其逆
q
−
1
q^{-1}
q−1,则从模型空间的方向
F
⃗
M
\vec F_{M}
FM向世界空间方向
F
⃗
W
\vec F_{W}
FW的转变为:
F
⃗
W
=
q
F
⃗
M
q
−
1
\vec F_{W} = q\vec F_{M}q^{-1}
FW=qFMq−1
四元数和旋转矩阵
既然四元数扮演着和旋转矩阵相同的功能,那很容易联想到它们二者之间具有等价转换的关系,由于篇幅关系,这里仅给出从四元数转换至等价的旋转矩阵的结果。
设四元数q = [x y z w](按照上文习惯,标量放在第四位,有些学术文章中是放在首位的),对应的旋转矩阵R如👇。
[
1
−
2
y
2
−
2
z
2
2
x
y
+
2
w
z
2
x
z
−
2
w
y
2
x
y
−
2
w
z
1
−
2
x
2
−
2
z
2
2
y
z
+
2
w
x
2
x
z
+
2
w
y
2
y
z
−
2
w
x
1
−
2
x
2
−
2
y
2
]
\begin{bmatrix} 1-2y^{2}-2z^{2} & 2xy+2wz & 2xz-2wy \\ 2xy-2wz & 1-2x^{2}-2z^{2} & 2yz+2wx\\ 2xz+2wy& 2yz-2wx& 1-2x^{2}-2y^{2}\end{bmatrix}
⎣⎡1−2y2−2z22xy−2wz2xz+2wy2xy+2wz1−2x2−2z22yz−2wx2xz−2wy2yz+2wx1−2x2−2y2⎦⎤
旋转插值
按照惯例,先放结论:四元数相比旋转矩阵,能更轻易地用LERP或者SLERP运算进行旋转插值。
游戏场景中,旋转的插值几乎无处不在,最快速简单的方法就是套用四维矢量的线性插值(LERP)到四元数,据此,在A旋转至B点的过程中的第β个百分点的位置的四元数为:
q
L
E
R
P
=
n
o
r
m
a
l
i
z
e
(
(
1
−
β
)
q
A
+
β
q
B
∣
(
1
−
β
)
q
A
+
β
q
B
∣
)
q_{LERP} = normalize(\frac{(1-\beta)q_{A}+\beta q_{B}}{\lvert (1-\beta)q_{A}+\beta q_{B}\rvert})
qLERP=normalize(∣(1−β)qA+βqB∣(1−β)qA+βqB)
这里在进行LERP后要注意进行归一化,因为LERP运算对四元数不保持矢量长度。
不过LERP虽然简单,但却有一定的缺陷,因为四元数实际上是四维超球上的点,LERP做的插值实际上是在超球的弦上进行,而不是在超球的面的大圆上进行,所以这种插值并不均匀,实际场景中的表现为:旋转在始末两端比较缓慢,而在动画的中间相对更快。
解决此问题的方法是使用球面线性插值(SLERP),SLERP在LERP基础上添加正余弦使得插值在四维超球面的大圆上,公式如👇(对照上面LERP公式一同食用效果更佳):
q
S
L
E
R
P
=
sin
(
1
−
β
)
θ
sin
θ
+
sin
β
θ
sin
θ
q
B
q_{SLERP} = \frac{\sin (1-\beta)\theta}{\sin \theta}+\frac{\sin\beta\theta}{\sin\theta}q_{B}
qSLERP=sinθsin(1−β)θ+sinθsinβθqB
这里两四元数之间的夹角的余弦值等于它们的四维点积:
cos
θ
=
p
⋅
q
=
p
x
q
x
+
p
y
q
y
+
p
z
q
z
+
p
w
q
w
\cos θ = p \cdot q = p_{x}q_{x} + p_{y}q_{y} + p_{z}q_{z} + p_{w}q_{w}
cosθ=p⋅q=pxqx+pyqy+pzqz+pwqw
求出
c
o
s
θ
cosθ
cosθ后便可以用反三角函数求出θ。
最后简单总结一下
- 用四元数代替旋转矩阵进行旋转可以减少存储上的冗余,加快计算
- 四元数相比于旋转矩阵,能更容易完成游戏中的旋转插值操作(使用LERP / SLERP)
- SLERP能够解决LERP的非匀速旋转的问题(但实际中其实更多还是采用LERP,因为这种非匀速还可接受,而且SLERP计算开销大)
- 如果拿轴角这种表示方式(这一部分较为简单,文中没有提及)和四元数进行比较,轴角表示会更加直观,同样没有存储上的冗余,但是不能像四元数一样进行简单插值,并且轴角形式的旋转不能直接施加于点和矢量,而四元数可通过 q v q − 1 qvq^{-1} qvq−1方式完成