三维旋转
“定向”含义
"定向"基本上是告诉我们对象所面向的方向。但是"定向"与"方向"并不完全相同。
1.方向(Direction):可以用两个数字(也就是球面坐标的角度)
2.定向(Orientation):至少需要 个数字(即欧拉角)
3.角位移(Angular Displacement):旋转量称为角位移。在数学上,定向等同于角位移。
定向和角位移之间的这种区别类似于点和矢量之间的区别,这两个术语在数学上是等价的 ,但在概念上是不相同的。在这两种情况下,第一个术语主要用于描述单个状态 第二个术语主要用于描述两个状态之间的差异。
矩阵形式
描述三维中坐标空间的定向的一种方法是告诉该坐标空间 (+x 轴、 +y 轴和+z 轴〉的基矢量指向哪个方向。根据定义,无论坐标空间的定向是什么,它们都是[1,0,0] [0,1,0] [0,0,1] 我须使用其他一些坐标空间来描述基础矢量。只有通过这样做,才可以建立两个坐标空间的相对定向。
当这些基矢量用于形成矩阵的行或列时,就是用矩阵形式 (Malrix Forrn) 表示了定向。我们可以通过给出一个旋转矩阵来表示两个坐标空间的相对定向,旋转矩阵可用于将矢量从一个坐标空间变换到另一个坐标空间。
方向余弦矩阵
坐标空间的基矢量是相互正交的单位矢量p q r ,而具有相同原点的第二个坐标空间的基矢量是不同的(但也是正交的)基矢量 p’ q’ 和r’。将行矢量从第一个空间旋转到第二个空间的旋转矩阵可以用每对基矢量之间的角度的余弦构造。然而,两个单位矢量的点积正好等于它们之间角度的余弦,所以矩阵乘积为:
如果把[1,0,0] [0,1,0] [0,0,1]代入之后方向余弦矩阵就是这样:
这表示旋转矩阵的列由输入空间的基矢量形成,使用输出空间的坐标表示。 一般而言,转换矩阵不是这样的,它仅适用于正交矩阵,如旋转矩阵。
矩阵形式的优点
1.旋转矢量立即可用。矩阵形式最重要的特性是可以使用矩阵在对象空间和直立空间之间旋转矢量。没有其他的定向表示方法允许这种方式,要旋转矢量,通常必须将定向转换为矩阵形式。
2.图形 API 使用的格式。部分由于前一项中的原因,图形 API 使用矩阵来表示定向。当与 API 通信时,将不得不把转换表示为矩阵。
3.多个角位移的连接。矩阵的第 个优点是可以"折 "嵌套的坐标空间。例如,如果知道对象A相对于对象B的方向,并且知道对象B相对于对象C方向,那么通过使用矩阵,可以确定对象A相对于对象C的方向。
4.矩阵求逆。当以矩阵形式表示角位移时,可以通过使用矩阵求逆来计算"反向"的角位移。由于旋转矩阵是正交的,因此这种计算只需要转置一下矩阵即可。
矩阵形式的缺点
矩阵使用了9个数字来存储定向,并且可以仅使用3个数字来设置定向的参数。因此,矩阵中的这些"额外"的数字可能会导致一些问题。
1.矩阵占用更多内存。如果需要存储许多定向(例如,动画序列中的关键帧) , 那么9个数字而不是3个数字的额外空间可以真正加起来。
2.矩阵可能格式不正确。如前所述,矩阵使用9个数字,但只有3个数字是必需的。换句话说,矩阵包含六度冗余。必须满足6个约束条件才能使矩阵"有效"以表示定向。行必须是单位矢量,且它们必须相互垂直
以下几种做法会得到糟糕的矩阵:
1.我们可能从外部来源获取了错误的数据。例如,如果使用物理数据采集系统例如运动捕捉系统,则可能由于捕获过程而出现错误。
2.由于浮点舍入错误,可能会真正创建错误的数据。例如,假设对定向应用了大量的增量更改,这可能通常发生在游戏或模拟中,允许人们以交互方式控制对象的方向。大量的矩阵乘法受到有限的浮点精度的影响,可能导致格式错误的矩阵.这种现象称为矩阵蠕变。可以通过对矩阵进行正交化来对抗矩阵蠕变。
3.我们可能有一个包含缩放比例 、倾斜、反射或投影的矩阵。受此类运算影响的对象的"定向"是什么?对此确实没有明确的定义 。任何非正交矩阵都不是明确定义的旋转矩阵。反射矩阵(正交)也不是旋转矩阵。
欧拉角
欧拉角背后的基本思想是将角位移定义为围绕 3 个相互垂直的轴的 3 个旋转的序列。这听起来很复杂,但实际上它非常直观(事实上,人类易于使用是它的主要优势之一)
我们使用左手系统,其中+x 右,+y 向上, +z 向前。给定航向角、俯仰角和滚转角,具体内容如下:
1.航向角(用h表示):测量围绕Y轴的旋转量
2.俯仰角(用p表示):测量围绕X轴的旋转量(对象空间的X轴)
3.滚转角(用b表示):应用航向角和俯仰角之后,滚转测量围绕Z轴的旋转量
欧拉角的优点
欧拉角仅使用3个数字来设置定向的参数,并且这3个数字都是角度,欧拉角的这两个特征与其他形式表示定向的方法相比具有一定的优势。
1.欧拉角使用尽可能最小的表示。欧拉角仅使用3个数字来描述定向。没有任何其他系统可以使用少于3个数字来设置三维定向的参数.如果内存是需要优先考虑的事项,那么欧拉角是表示定向的最经济的方式。
2.存储的数字更容易压缩。使用普通的固定精度系统将欧拉角度打包成较小的位数相对容易。由于欧拉角是角度,因量化引起的数据丢失会均匀分布。矩阵和四元数要求使用非常小的数字,因为它们存储的值是角度的正弦和余弦。但是,这两个值之间的绝对数值差异即使很小, 其感知差异也很大,而使用欧拉角度则不会出现这种情况。一般来说,矩阵和四元数不容易打包到定点系统中
欧拉角的缺点
1.给定定向的表示不是唯一的
一种很普通的别名类型 添加 360° 的倍数并不会改变它所表示的定向,即使数字不同。但是由于3个角度彼此不完全独立,因此出现了另一种更麻烦的别名。例如,俯仰角向下旋转 135°。与先通过航向角旋转 180°,再旋转俯仰 45°,最后滚转角旋转 180°。的结果是一样的。
为了保证任何给定方向的唯一欧拉角表示,我们限制了角度的范围。一种常见的技术是将航向角和滚转角限制为(-180°,+180°]。并将俯仰角限制为[-90°,+90°]。
欧拉角所苦恼的最著名的别名问题可由以下示例进行说明:如果航向角向右旋45°,然后俯仰角向下旋转 90° ,这与俯仰角向下旋转 90° ,然后滚转角旋转 45° 的结果相同。实际上,一旦选择了+90°或-90°。 作为俯仰角的范围,我们就被限制为围绕垂直轴旋转。这种现象称为万向节死锁(Gimbal Lock)为了从欧拉角三元组的规范集中消除这种别名现象,在规范集中,如果俯仰角为+90°或-90°,则滚转角为零。
欧拉角集的规则:
-180° < h ≤ 180°
-90° ≤ p ≤ 90°
-180° < b ≤ 180°
p = ±90° ==> b = 0
2.两个定向之间的插值是有问题的。
假设希望在两个定向R0和 R 1之间进行插值。换句话说,对于给定的参数t,有0 ≤ t ≤ 1,我们希望计算出中间定向 R(t) ,当变为t从0变为1时,从R0到 R 1平滑地插值。
Δθ = θ1 - θ0
θt = θ0 + tθt
这会导致很大的问题。首先 如果不使用规范的欧拉角 那么可能会有很大的角度值。例如,R0的航向角h0为720°且h1为45°。现在,h1 = 2*360°,这跟0°形同,所以基本上h0与h1相聚45°,然而,原生插值将在错误的方向上旋转近两倍。当然,这个问题的解决方案是使用规范的欧拉角。
但是,即使都使用规范角度也不能完全解决问题。由于旋转角度的循环特性,可能发生但是,即使都使用规范角度也不能完全解决问题。由于旋转角度的循环特性,可能发生第二类插值问题。假设h0 = -170° h1 = +170°。注意 它们都是航向的规范值,二者都在(-180°,+180°]的区间内。两个航向实际上相距20°,但是,原生插值仍然无法工作,因为它们将顺时针旋转340°而不是较短的20°。
第二类问题的解决方案是将插值方程中使用的角度之间的差异限制在(-180°,+180°]区间内以找到最短的弧。可以引入以下表达式
w
r
a
p
P
i
(
x
)
=
x
−
360
f
l
o
o
r
[
(
x
+
180
°
)
/
360
°
]
wrapPi(x) = x-360floor[(x+180°)/360°]
wrapPi(x)=x−360floor[(x+180°)/360°]
C语言中的实现
float wrapPi(float theta)
{
if(fabs(theta) <= PI)
{
// 一圈是360°
const float TWOPI = 2.0f*PI;
// 超出的圈数
float revolutions = floor((theta + PI) * (1 / TWOPI));
// 再减
theta -= revolutions * TWOPI;
}
}
我们回到欧拉角。正如预期的那样,使用 wrapPi 函数可以在两个角度之间插值时以轻松获取最短弧度,具体如下:
Δθ = wrapPi(θ1 - θ0)
θt = θ0 + tθt
但是,即使有了这两个实用小工具,欧拉角的插值仍然受到万向节死锁的影响,这在很多情况下会导致不稳定、不自然的情况。例如 物体突然晃动,似乎挂在某处。基本问题就是在插值期间角速度不恒定。
对于欧拉角插值的前两个问题,规范欧拉角和wrapPi函数提供了相对简单的解决方法。然而,万向节死锁并不是一个小麻烦,这是一个根本性的问题,使用3个数字来描述三维定向是一个固有的问题。我们可以改变我们的问题,但是无法消除它们。任何使3个数字对三维空间定向进行参数化的系统都保证在参数化空间中具有奇点,因此都会遇到诸如万向节锁定之类的问题。
轴-角与指数映射
欧拉旋转定理,基本意思是,任何三维角位移都可以通过围绕 个精心选择的轴进行一次旋转来完成。 更确切地说, 给定任意两个定向R0和R1,存在一个轴n使得我们只要通过围绕轴n进行一次旋转就可以从R0到R1。使用欧拉角我们需要3次旋转来描述任何定向,因为我们被限制为围绕基本轴旋转 但是, 我们可以自由选择旋转轴时,可以找到一个只需要一次旋转的旋转轴,而且这个旋转轴是唯一确定的。
假设选择了旋转角θ 。和穿过原点的旋转轴并且平行于单位矢n 。以两个值θ和n 为原理,我们描述了轴-角(Axis-Angle)形式的角位移。由于n具有单位长度,我们可以将其乘以θ。而不会丢失信息,从而产生单个矢量 e = θn。这种描述旋转的方案是由指数映射(Exponential Map)的相当令人生畏和模糊的名称进行的。 旋转角度可以从e的长度推导出来。也就是说,θ = ||e||,并且可以通过归一化e来获得轴。指数映射比轴-角更紧凑,而且它还避免了某些奇点并具有更好的插值和微分特性。
指数映射比轴-角更常用。首先,它的插值特性比欧拉角更好。虽然它确实有奇点 ,但它们并不像欧拉角那么烦琐。指数映射的最重要和最频繁的用途是存储角位移,而不是角速度。这是因为指数映射能很好地区分(这与其更好的插值属性有些相关)并且可以轻松表示多个旋转。
与欧拉角一样 -角和指数映射形式均表现出别名现象和奇点。在单位的定向上存在明显的奇点,或者"没有角位移"的量。在这种情况下,θ = 0,并且我们对轴的选择是无关的一一可以使用任何轴。对于这种情况指映射能很好地处理,因为乘以会θ 导致e消失,无论选择的是哪一个旋转轴 。通过将θ与n变为负,可以产生轴-角空间中另一种普通的别名形式。。但是,指数映射也避开了这个问题,因为将θ与n变负会使得 e = θn 无变化。
其他别名则没有这么容易被打发。与欧拉角一样,向θ添加360°的倍数会产生角位移, 从而产生相同的结果定向,这种形式的别名会影响轴-角和指数映射。然而,这并不总是一个缺点一一为了描述角速度,这种表示诸如此类"额外’旋转的能力是一个重要且有用的特性。例如,能够以每秒 720°的速率区分围绕x轴的旋转与以每秒1080° 的速率围绕同一轴的旋转是非常重要的(即使如果应用于整数秒,这些位移会导致相同的结果定向)四元数格式则无法捕获此区别。
现在考虑连接多个旋转。假设有两个指数映射格式的旋转e1和e2,按不同的顺序执行时结果是不同的。但是,当我们将旋转角度的大小降低时,顺序的重 性也会降低,并且在极端情况下,对于"无穷小"的旋转,顺序则完全无关紧要。换句话说对于无穷小旋转,可以按矢量方式添加指数映射。
当指数映射用于定义旋转 (角位移或定向)时不会按矢量方式添加 ,但是当它们描述旋转率时,它们可以正确地按矢量方式添加。这就是指数映射非常适合描述角速度的原因。
四元数
四元数通过使用4个数字来表示定向以避免这些问题,这也是"四元数"名称的由来。
四元数表示法
四元数包含标量分量和三维矢量分量。通常将标量分量称为ω。我们可以将矢量分量称为单一的实体v或单个分量x,y和z
表示方法:
[ω,v] [ω,(x,y,z)]
四元数形式与的轴-角和指数映射形式密切相关。四元数也包含轴和角度,但是θ和n并不是简单地直接存储在四元数的4个数中,因为它们是轴角。四元数表示法:
[ω,v] = [cos(θ/2), sin(θ/2)n]
[ω,(x,y,z)] = [cos(θ/2), (sin(θ/2)nx,sin(θ/2)ny,sin(θ/2)nz)]
ω与θ有关,但不是一回事。同样v与n有关,但不相同
四元数变负
-q = -[ω, v] = [-ω, -v]
四元数q与**-q**表示相同的角位移,三维中的任何角度位移在四元数格式中具有恰好不同的两个表示,并且它们是彼此的负数。
要想清楚为什么会这样并不难。如果将360°添加到θ,那么它将不会改变q表示的角位移,但它会使q的所有分量均变负。
单位四元数
在几何学上,有两个单位四元数代表"没有角度位移“。它们是:
[1, 0] [-1, 0]
当θ是360°的偶数倍时,则 cos(θ/2) = 1,当θ是360°的奇数倍,则 cos(θ/2) = - 1。在这两种情况下, sin(θ/2) = 0,因此n的值是无关紧要的。这在直观上也不难理解:如果旋转角θ是围绕任何轴旋转的完整圈数,则在方向上不会有任何实际的改变。
在代数上,实际上只有一个单位四元数:[1, 0]。当将任何四元数乘以身份四元数时,结果为q。所以, [1, 0]是真正的单位四元数。当将四元数q乘以另一个"几何身份"四元数 [-1, 0] ,得到的是 -q。 虽然在几何上,这将导致相同的四元数,因为q与-q表示的是相同的角位移,但是在数学上,并不相等,因此[-1, 0]不是一个真实的单位四元数。
四元数的大小
||q|| = ||[ω, v]|| = sqrt(ω2 + ||v||2)
让我们来看一看对于旋转四元数来说 这在几何上意味着什么,具体如下:
||q|| = ||[ω, v]|| = sqrt(ω2 + ||v||2)
= sqrt(cos2(θ/2) + sin2(θ/2)(nx2 + ny2 + nz2))
= 1
四元数的共轭和逆
四元数的共轭,表示为q*,是通过将四元数的矢量部分变负得到的,其公式如下:
q* = [ω, v]* = [ω, -v]
术语"共轭"是从四元数作为一个复数的解释继承而来的。
四元数的逆表示为q-1 ,定义为四元数除以其大小的共辄,其公式如下:
q-1 = q* / ||q||
- q q-1 = [1, 0]
- q与**q***表示相反的角位移
四元数乘法
四元数可以相乘,其结果类似于矢量的叉积,因为它产生的是另一个四元数(而不是标量),并且它不是可交换的。
q1 q2 = [ω1ω2 - v1·v2, ω1v2 + ω2v1 + v1 x v2]
1.四元数乘法可结合但不可交换
(a b)c = a(b c)
a b ≠ b a
2.四元数乘积的大小等于它们的大小的乘积
||q1 q2|| = ||q1|| ||q2||
3.四元数倒数的乘积
(**q1 ** q2…qn-1 qn)-1 = qn-1qn-1-1…q2-1q1-1
四元数的”差“
使用四元数乘法和倒数,我们可以计算两个四元数之间的差值"差"意味着一个方向到另一个方向的角位移。给定方向a和b计算从a到b的角位移d
d a = b
d = b a-1
注:这里的顺序不能颠倒
四元数点积
q1·q2 = [ω1, v1]·[ω2, v2]
= ω1ω2 + v1· v2
= ω1ω2 + x1x2 + y1y2 + z1z2
像矢量点积一样,其结果是标量。对于单位四元数a和b,-1 ≤ a·b ≤ 1
四元数点积具有类似于矢量点积的解释。四元数点积v1· v2的绝对值越大,由v1和 v2的角位移就越“相似”。虽然矢量点积给出了矢量之间角度的余弦,但四元数点积给出了将一个四元数旋转到另一个四元数所需角度的一半的余弦。出于测量相似性的目的,通常只对v1· v2的绝对值感兴趣。由于a·b = -(a·-b)所以b和-b为相同的角位移。
点积是计算Slerp的第一步。
四元数的对数、指数和标量乘法
引入半角(θ/2)的变量α来从新定义四元数,如下:
α = θ/2 q = [cosα nsinα]
四元数的对数
logq = log[cosα nsinα] ≡ [0 αn]
指数函数以完全相反的方式定义。首先,将四元数q定义为[0 αn]形式,其中n为单位矢量
q = [0 αn] ||n|| = 1
指数函数的定义为:
exp q = exp [0 αn] ≡ [cosα nsinα]
注意,根据定义。exp p始终返回的是单位四元数,
四元数对数和指数与它们的标量类似物有关。对于任何标量a,有:
elna = a
同样,四元数 exp 函数定义为四元数对数函数的逆
exp(log q) = q
四元数可以乘以标量,结果以明显的方式计算每个分量乘以标量。给定标量k和四元数q,有
kq = k[ω v] = [kω kv]
这通常不会产生单位四元数,这就是为什么乘以标量在表示角位移的情况下不是一个非常有用的运算。
四元数指数
四元数指数表示为qt,不要与指数函数exp p搞混。指数函数只接受一个参数:四元数。四元数取幂则有两个参数四元数和标量指数 t。
四元数指数的含义与实数相似。对于任何标量a(除了零),a0 = 1且a1 = a。当指数t从0变化到1时,at从1变到a。类似的,当指数t从0变化到1时,qt 从[1 0]变化到q。
四元数取寡很有用,因为它允许提取角位移的"分数”。例如,要计算表示四元数q表示的角位移的三分之一的四元数,可以计算q1/3。
同理,q2表示q的角位移的两倍。例如,q表示围绕x轴顺时针旋转30°,则**q2**表示围绕x轴顺时针旋转60°,**q-1/3**是绕x轴逆时针旋转10°
注:四元数用最短弧表示位移,所以无法表示多圈旋转。看上面的例子**q8**并不表示绕x轴顺时针旋转240°而是绕x轴逆时针旋转120°一般来说,(as)t = ast不适用于四元数。四元数只捕获最终结果。需要旋转总量而不仅仅是旋转结果的时候(比如角速度)可以使用指数映射(或者轴—角格式)来代替。
数学上的定义:
qt = exp(t logq)
这跟标量是类似的:
at = e(tlna)
四元数指数计算
// 四元数
float w, x, y, z;
// 指数
float exponent;
// 检查四元数
// 防止除以零
if(fabs(w) < 0.9999f)
{
// 提取半角(alpha = thea/2)
float alpha = acos(w);
// 计算新alpha
float newAlpha = alpha * exponent;
// 计算新w
w = cos(newAlpha);
// 计算新x,y,z
float mult = sin(newAlpha) / sin(alpha);
x *= mult;
y *= mult;
z *= mult;
}
四元数插值
如果在两个标量a0与a1之间插值,我们可以用标准线性插值公式:
Δa = a1 - a0
lerp(a0, a1, t) = a0 + tΔa
所以结合前几小节的结论,理论上四元数Slerp公式为:
slerp(q0, q1, t) = (q1 q0-1)tq0
实际上四元数Slerp公式为:
slerp(q0, q1, t) = sin[(1 - t)ω]q0/sinω + sin(tω) q1/sinω
可能出现的问题一,q和**-q表示相同的方向,但在Slerp作用下会表示不同结果。这个问题不会发生在二维或三维中,但四维超球面的表面与欧几里得空间的拓扑结构不同。解决方法是选择q0与q1的符号使它们的点积是非负的,这样的结果就是从q0到q1**的最短弧了。
可能遇到的问题二,当ω非常小时,sinω就非常小,会导致除法问题。这种情况用简单的线性插值。
计算四元数Slerp:
// 输入两个四元数
float w0, x0, y0, z0;
float w1, x1, y1, z1;
// 插值参数
float t;
// 所求得的四元数
float w, x, y, z;
// 四元数之间的角度余弦值
float cosOmega = w0*w1 + x0*x1 + y0*y1 + z0*z1;
// 如果点积为负则将其中一个四元数变为负
if(cosOmega < 0.0f)
{
w0 = -w0;
x0 = -x0;
y0 = -y0;
z0 = -z0;
cosOmege = -cosOmege;
}
// 如果它们靠的非常近
float k0, k1;
if(cosOmege > 0.99f)
{
k0 = 1.0f - t;
k1 = t;
}
else
{
float sinOmega = sqrt(1.0f - cosOmega * cosOmega);
float omega = atan2(sinOmega, cosOmega);
float oneOverSinOmega = 1.0f/sinOmega
k0 = sin((1.0f - t)*omega) * oneOverSinOmega;
k1 = sin(t*omega) * oneOverSinOmega;
}
// 插值
w = k0*w0 + k1*w1;
x = k0*x0 + k1*x1;
y = k0*y0 + k1*y1;
z = k0*z0 + k1*z1;
四元数的优缺点
优点:
- 平滑插值,Slerp插值方法提供了定向之间的平滑插值。没有其他表示方法可以提供这种插值。
- 角位移的快速连接和逆。可以通过使用四元数叉积将一系列角位移连接成单个角位移。四元数共轭提供了一种非常有效地计算相反角位移的方法。这可以通过转置旋转矩阵来完成,但是对于欧拉角来说并不容易。
- 四元数可以快速地与矩阵形式相互转换,并且其转换速度比欧拉角更快一些。
- 只有4个数字。由于四元数包含4个标量值,因此它比使用9个数字的矩阵更经济(但比欧拉角大)
缺点:
- 略大于欧拉角。四元数使用的是4个数字,而欧拉角则仅使用3个数字,虽然这多出的一个数字看起来不多,但是当需要大量的角位移(如存储动画数据)时,就会产生额外的 33%的差异。
- 可能无效。这可能因为错误的输入数据或累积的浮点舍入错误而发生。可以通过规范化囚元数来确保它具有单位大小来解决这个问题。