如何在两个四元数之间进行 Lerp?

How to Lerp between two quaternions?

我有两个四元数:

SCNVector4(x: -0.554488897, y: -0.602368534, z: 0.57419008, w: 2.0878818) 

SCNVector4(x: 0.55016619, y: 0.604441643, z: -0.576166153, w: 4.18851328)

如果我们创建两个对象,方向将非常相似

但是如果我们尝试从第一个到第二个进行 Lerp,那么位置会发生非常奇怪的变化(并查看预期但不正确的值)

[Lerp 进度演示][1]

我用谷歌搜索并找到了许多函数来做 lerp 例如简单一个:

extension SCNVector4 {

    func lerp(to: SCNVector4, v: Float) -> SCNVector4 {

        let aX = x + (to.x - x) * v
        let aY = y + (to.y - y) * v
        let aZ = z + (to.z - z) * v
        let aW = w + (to.w - w) * v
        
        return SCNVector4Make(aX, aY, aZ, aW)
        
    }
}

但是如何避免这种奇怪的翻转呢?

PS: 我尝试了 GLKit 的不同功能,但结果是一样的 [1]: https://i.stack.imgur.com/8jEvm.png

- 按照建议尝试翻转符号,但问题是我得到的点积大于 0

    extension SCNVector4 {
    
    func glk() -> GLKQuaternion {
        return GLKQuaternion(q: (x, y, z, w))
    }
    
    func lerp(to: SCNVector4, v: Float) -> SCNVector4 {
        
        let a = GLKQuaternionNormalize(glk())
        let b = GLKQuaternionNormalize(to.glk())
        
        let dot =
            a.x * b.x +
            a.y * b.y +
            a.z * b.z +
            a.w * b.w
        
        var target = b
        if dot < 0 {
            target = GLKQuaternionInvert(b)
        }
        
        let norm = GLKQuaternionNormalize(GLKQuaternionSlerp(a, target, v))
        
        return norm.scn()
        
    }
    
}

extension GLKQuaternion {
    
    func scn() -> SCNVector4 {
        return SCNVector4Make(x, y, z, w)
    }
    
}

最新的 SDK 具有 <simd/quaternion.h>,它公开了 simd_quatf 类型来表示四元数。还公开了处理四元数的不同实用程序,其中 simd_slerp 对四元数插值执行 "the right thing"。

在 iOS 11 和 macOS 10.13 中,SceneKit 公开了新的 API 来直接处理 SIMD 类型。例如除了 SCNNode.orientation you now also have access to SCNNode.simdOrientation.

编辑

大多数 SIMD API 都是内联的,因此可以在 SDK 版本之前的 OS 版本上使用。如果你真的想坚持使用 GLKit,他们的球形插值版本是 GLKQuaternionSlerp.

  1. 如果四元数点积为负,则进行符号翻转。
  2. 归一化生成的四元数。

如果你问我,你列出的 quat 值似乎是错误的。 'w' 2 或 4 的值加起来不等于标准化的 quat,所以我对 lerping 它们给你奇数值并不感到惊讶。当使用 quats 进行旋转时,它们应该是单位长度(这两个 quats 不是单位长度)。

至于 lerping,您基本上想要使用归一化 lerp (nlerp) 或球形 lerp (slerp)。当您从一个 quat 旋转到另一个时,NLerp 会导致轻微的加速/减速。 Slerp 为您提供恒定的 angular 速度(尽管它使用正弦,因此计算速度较慢)。

float dot(quat a, quat b)
{
  return a.x*b.x + a.y*b.y + a.z*b.z + a.w*b.w;
}

quat negate(quat a)
{
  return quat(-a.x, -a.y, -a.z, -a.w);
}

quat normalise(quat a)
{
  float l = 1.0f / std::sqrt(dot(a, a));
  return quat(l*a.x, l*a.y, l*a.z, l*a.w);
}

quat lerp(quat a, quat b, float t) 
{
  // negate second quat if dot product is negative
  const float l2 = dot(a, b);
  if(l2 < 0.0f) 
  {
    b = negate(b);
  }
  quat c;
  // c = a + t(b - a)  -->   c = a - t(a - b)
  // the latter is slightly better on x64
  c.x = a.x - t*(a.x - b.x);
  c.y = a.y - t*(a.y - b.y);
  c.z = a.z - t*(a.z - b.z);
  c.w = a.w - t*(a.w - b.w);
  return c;
}

// this is the method you want
quat nlerp(quat a, quat b, float t) 
{
  return normalise(lerp(a, b, t));
}

/编辑

您确定它们是 quat 值吗?如果您问我,这些值看起来很像轴角值。通过此转换函数尝试 运行 这些值,看看是否有帮助:

quat fromAxisAngle(quat q)
{
  float ha = q.w * 0.5f;
  float sha = std::sin(ha);
  float cha = std::cos(ha);
  return quat(sha * q.x, sha * q.y, sha * q.z, cha);
}

我从你的原始值得到这两个结果 quats:

(-0.479296037597, -0.520682836178, 0.496325592199, 0.50281768624)

(0.47649598094, 0.523503659143, -0.499014409188, -0.499880083257)