3D中的方位和角位移(5)

四元数的对数、指数和标量乘运算

首先,让我们重写四元数的定义,引入一个新的变量α,等于半角θ/2:

α = θ/2

|| n || = 1

q = [cosα   n sinα] = [cosα   xsinα   ysinα   zsinα]

q 的对数定义为公式10.15:

log q = log([cosα   n sinα]) ≡ [0  α n ]

公式10.15   四元数的对数

≡表示"恒等于",注意log q 的结果,它一般不是单位四元数。

指数以严格相反的方式定义,首先,设四元数 p 的形式为[0  α n ], n 为单位向量:

p = [0  α n ] = [0  (αx   αy   αz)]

|| n || = 1

接着,指数定义为公式10.16:

exp p = exp([0 α n ]) = [cosα  n sinα]

公式10.16  四元数的指数

根据定义,exp p 总是返回单位四元数。

四元数的对数和指数类似于它们的标量形式,回忆一下,对于标量α,有下列关系成立:

image

同样,四元数指数运算为四元数对数运算的逆运算:

exp(log q ) = q

最后,四元数能与一个标量相乘。其计算方法非常直接:每个分量都乘以这个标量,给定标量k和四元数 q ,有公式 10.17:

k q = k[w  v ] = [kw   k v ] = k[w  (x  y  z)] = [kw  kx  ky  kz]

公式10.17   四元数和标量相乘

一般不会得到单位四元数,这也是为什么在表达角位移的场合中标量乘不是那么有用的原因。

 

四元数求幂

四元数能作为底数,记作 qt (不要和指数运算混淆,指数运算只接受一个四元数作为参数,而四元数求幂有两个参数 ---- 四元数和指数)。四元数求幂的意义类似于实数求幂。回忆一下,a0 = 1, a1 = a,a为非零标量。当t从0变到1时,at从1到a。四元数求幂有类似的结论:当t从0变到1, qt从[1, 0 ]到 q 。

这对四元数求幂非常有用,因为它可以从角位移中抽取"一部分"。例如,四元数 q 代表一个角位移,现在想要得到代表1/3这个角位移的四元数,可以这样计算: q1/3。

指数超出[0, 1]范围外的几何行为和预期的一样(但有一个重要的注意事项)。例如, q2代表的角位移是 q 的两倍。假设 q 代表绕x轴顺时针旋转30度,那么 q2代表绕x轴顺时针旋转60度, q-1/3代表绕x轴逆时针旋转10度。

上面提到的注意事项是,四元数表达角位移时使用最短圆弧,不能"绕圈"。继续上面的例子, q4不是预期的绕x轴顺时针旋转240度,而是逆时针80度。显然,向一个方向旋转240度等价于向相反的方向旋转80度,都能得到正确的"最终结果"。但是,在此基础上的进一步运算,产生的就可能不是预期的结果了。例如,(q4)1/2不是 q 2,尽管我们感觉应该是这样。一般来说,凡是涉及到指数运算的代数公式,如(as)t = a(st),对四元数都不适用。

现在,我们已经理解四元数求幂可以为我们做什么了。让我们看看它的数学定义,四元数求幂定义在前一节讨论的"有用"运算上,定义如公式10.18:

3D中的方位和角位移(5)_第1张图片

注意,对于标量求幂,也有类似结论:

image

不难理解为什么当t从0变到1时 q'从单位四元数变到 q 。注意到对数运算只是提取了轴 n 和角度θ;接着,和指数t进行标量乘时,结果是θ乘以t;最后,指数运算"撤销"了对数运算,从tθ和 n 重新计算w和 v 。上面给出的定义就是标准数学定义,在理论上非常完美,但直接转换到代码却是很复杂的。程序清单10.1所示的代码展示了怎样计算 q'的值。
Listing 10.1: Code to raise a quaternion to a power

// Quaternion (input and output)
float w,x,y,z;

// Input exponent
float exponent;

// Check for the case of an identity quaternion.
// This will protect against divide by zero
if (fabs(w) < .9999f)
{
  // Extract the half angle alpha (alpha = theta/2)
  float alpha = acos(w);

  // Compute new alpha value
  float newAlpha = alpha * exponent;

  // Compute new w value
  w = cos(newAlpha);

  // Compute new xyz values
  float mult = sin(newAlpha) / sin(alpha);

  x *= mult;
  y *= mult;
  z *= mult;
}


关于这些代码,需要注意的地方有:

(1)有必要做单位四元数的检查。因为w=+(-)1会导致mult的计算中出现除零现象。单位四元数的任意次方还是单位四元数。因此,如果检测到输入是单位四元数,忽略指数直接返回原四元数即可。

(2)计算alpha时,使用了acos函数,它的返回值是正的角度。这并不会违背一般性,任何四元数都能解释成有正方向的旋转角度,因为绕某轴的负旋转等价于绕指向相反方向的轴的正旋转。

 

四元数插值 ---- "slerp"

当今3D数学中四元数存在的理由是由于一种称作slerp的运算,它是球面线性插值的缩写(Spherical Linear Interpolation)。slerp运算非常有用,因为它可以在两个四元数间平滑插值。slerp运算避免了欧拉角插值的所有问题。

slerp是一种三元运算,这意味着它有三个操作数。前两个操作数是两个四元数,将在它们中间插值。设这两个"开始"和"结束"四元数分别为 q 0和 q 1。插值参数设为变量t,t在0到1之间变化,slerp函数:slerp( q 0, q 1, t),将返回 q 0到 q 1之间的插值方位。

能否利用现有的数学工具推导出slerp公式呢?如果是在两个标量a0和a1间插值,我们会使用下面的标准线性插值公式:

Δa = a1 - a0

lerp(a0, a1, t) = a0 + tΔa

标准线性插值公式从a0开始,并加上a0和a1差的t倍,有三个基本步骤:

(1)计算两个值的差。

(2)取得差的一部分。

(3)在初始值上加上差的一部分。

可以使用同样的步骤在四元数间插值:

(1)计算两个值的差, q 0到 q 1的角位移由Δ q = q 0-1 q 1给出。

(2)计算差的一部分,四元数求幂可以做到,差的一部分由(Δ q )t给出。

(3)在开始值上加上差的一部分,方法是用四元数乘法来组合角位移: q 0(Δ q )t

这样,得到slerp的公式如公式10.19所示:

image

这是理论上的slerp计算过程,实践中,将使用一种更加有效的方法。

我们在4D空间中解释四元数,因为所有我们感兴趣的四元数都是单位四元数,所以它们都"存在"于一个 4D"球面"上。

slerp的基本思想是沿着4D球面上连接两个四元数的弧插值(这就是球面线性插值这个名称的由来)。

可以把这种思想表现在平面上,设两个2D向量 v 0和 v 1,都是单位向量。我们要计算 v 1,它是沿 v 0到 v 1弧的平滑插值。设w是 v 0到 v 1弧所截的角,那么 v t就是绕 v 1沿弧旋转tw的结果,如图10.10所示:

3D中的方位和角位移(5)_第2张图片

将 v t表达成 v 0和 v 1的线性组合,从另一方面说,存在两个非零常数k0和k1,使得:

v t = k0 v 0 + k1 v 1

可以用基本几何学求出k0和k1,图10.11展示了计算的方法:

3D中的方位和角位移(5)_第3张图片

对以k1 v 1为斜边的直角三角形应用三角公式得:

3D中的方位和角位移(5)_第4张图片

这里有两点需要考虑。第一,四元数 q 和- q 代表相同的方位,但它们作为slerp的参数时可能导致不一样的结果,这是因为4D球面不是欧式空间的直接扩展。而这种现象在2D和3D中不会发生。解决方法是选择 q 0和 q 1的符号使得点乘 q 0 . q 1的结果是非负。第二个要考虑的是如果 q 0和 q 1非常接近,sinθ会非常小,这时除法可能会出现问题。为了避免这样的问题,当sinθ非常小时使用简单的线性插值。程序清单10.2把所有的建议都应用到了计算四元数的slerp中:
Listing 10.2: How slerp is computed in practice

// The two input quaternions
float w0,x0,y0,z0;
float w1,x1,y1,z1;

// The interpolation parameter
float t;

// The output quaternion will be computed here
float w,x,y,z;

// Compute the "cosine of the angle" between the
// quaternions, using the dot product
float cosOmega = w0*w1 + x0*x1 + y0*y1 + z0*z1;

// If negative dot, negate one of the input
// quaternions to take the shorter 4D "arc"
if (cosOmega < 0.0f) {
  w1 = –w1;
  x1 = –x1;
  y1 = –y1;
  z1 = –z1;
  cosOmega = –cosOmega;
}

// Check if they are very close together to protect
// against divide-by-zero
float k0, k1;

if (cosOmega > 0.9999f) {
  // Very close - just use linear interpolation
  k0 = 1.0f–t;
  k1 = t;
} else {
  // Compute the sin of the angle using the
  // trig identity sin^2(omega) + cos^2(omega) = 1
  float sinOmega = sqrt(1.0f – cosOmega*cosOmega);

  // Compute the angle from its sin and cosine
  float omega = atan2(sinOmega, cosOmega);

  // Compute inverse of denominator, so we only have
  // to divide once
  float oneOverSinOmega = 1.0f / sinOmega;

  // Compute interpolation parameters

  k0 = sin((1.0f – t) * omega) * oneOverSinOmega;
  k1 = sin(t * omega) * oneOverSinOmega;
}

// Interpolate
w = w0*k0 + w1*k1;
x = x0*k0 + x1*k1;
y = y0*k0 + y1*k1;
z = z0*k0 + z1*k1;

你可能感兴趣的:(3D中的方位和角位移(5))