Three.js 四元数 slerp 在两极附近给出了不好的结果

Three.js quaternion slerp gives bad result near poles

视频示例:https://drive.google.com/file/d/18Ep4i1JMs7QvW9m-3U4oyQ4sM0CfIFzP/view

你在这里可以看到,我有一条光线在鼠标下方击中地球的世界位置。然后我 lookAt()THREE.Group 到那个位置以获得正确旋转的四元数。我鼠标下一直有红点,证明这个四元数没问题。接下来,从代表黄色大圆顶中心的四元数开始,我使用 rotateTowards(它在内部使用 slerp,我尝试直接使用 slerp 方法,但这给了我相同的结果)到鼠标位置的四元数(红色dot) 并将此四元数设置为一直跟随鼠标的蓝点的旋转。当我的鼠标离得更远时,理论上这应该总是 "stick" 到那个圆顶。当我在靠近南半球的地方做这些时,你可以看到它确实是 "sticking"。但在北极附近,它变得一团糟。它会像它应该的那样计算更短的距离,甚至不在正确的大圆上。

相关代码:

// using hammerjs pan events I send an event to the blue sphere with the position on the sphere whats under the mouse, event.point is correct, the red sphere always under the mouse proves this.

this.helperGroup.lookAt(event.point); // To get the requested rotation
const p = this.helperGroup.quaternion.clone(); // helpergroup is just an empty group in (0, 0, 0) to get quaternions with lookAt more easily
// p is now a rotation towards the point under the mouse

const requestedDistance = dome.quaternion.angleTo(p); // dome is the center of the yellow dome in the video, allowedDistance is the arc-length of said dome in radians.
// The reason I rotate the parent of blueSphere because its parent is another group in (0, 0, 0) and the (relative) position of the blue sphere is (0, 0, 1), the planets radius is 1 too.
if (allowedDistance >= requestedDistance) {
    blueSphere.parent.lookAt(event.point);
} else {
    blueSphere.parent.quaternion.copy(
        dome.quaternion.clone().rotateTowards(p, allowedAngle)
    );
}

//  this snippet is heavily modified for the sake of an example.

更新和不同的方法:

我最初使用这个 lookAt() 和基于旋转的布局来尽可能避免数学计算。但它反击了。所以现在我只需使用笛卡尔坐标、法向量和简单的基于轴的旋转就可以正确地完成它。 (原来使用数学实际上比避免它更简单)

const requestedDistance = blueSphere.angleTo(event.point);
let norm = dome.position.clone().cross(event.point).normalize();
if (allowedDistance >= requestedDistance) {
    blueSphere.position.set(event.point); // Not using a group as parent anymore
} else {
    blueSphere.position.set(dome.position.clone()
                            .applyAxisAngle(norm, allowedAngle);
}

极点附近的奇点是四元数 slerp 函数的一部分;除非使用不同的方法,否则无法避免。 Jonathan Blow 的文章“Understanding Slerp, Then Not Using It”讨论了 slerp 函数及其问题,并建议替代 slerp(规范化 lerp 或 nlerp)是大多数时候首选的四元数插值器。

请注意,即使那篇文章中的 slerp C++ 代码也承认 slerp 函数中存在的奇点。