我知道两个四元数的点积(或内积)是旋转之间的角度(包括轴-旋转)。这使得点积等于四元数超球面上两点之间的夹角。
然而,我找不到如何实际计算点积。
任何帮助都将不胜感激!
当前代码:
public static float dot(Quaternion left, Quaternion right){
float angle;
//compute
return angle;
}
定义了四元数的 w、x、y 和 z。
注:可以假设四元数已被归一化。
我知道两个四元数的点积(或内积)是旋转之间的角度(包括轴-旋转)。这使得点积等于四元数超球面上两点之间的夹角。
然而,我找不到如何实际计算点积。
任何帮助都将不胜感激!
当前代码:
public static float dot(Quaternion left, Quaternion right){
float angle;
//compute
return angle;
}
定义了四元数的 w、x、y 和 z。
注:可以假设四元数已被归一化。
四元数的点积就是在四维空间中的标准欧几里得点积:
dot = left.x * right.x + left.y * right.y + left.z * right.z + left.w * right.w
那么您要找的角度是点积的反余弦函数(请注意,点积不是角度):acos(dot)
。
但是,如果您正在寻找两个四元数之间的相对旋转,比如从 q1
到 q2
,您应该计算相对四元数 q = q1^-1 * q2
,然后找到与 q
相关联的旋转。
实际上不存在两个四元数之间的角度,只有通过乘法将一个四元数转换为另一个四元数的四元数。但是,可以通过计算两个四元数之间的差异(例如 qDiff = q1.mul(q2.inverse())
或者使用类库中的qDiff = q1.difference(q2)
直接计算)来测量该映射变换的旋转总角度,然后测量围绕四元数轴线的角度(您的四元数库可能有相应的例程,例如ang = qDiff.angle()
)。
请注意,由于围绕轴线测量角度并不能保证旋转是“最短的路径”,因此您可能需要进行修正,例如:
if (ang > Math.PI) {
ang -= 2.0 * Math.PI;
} else if (ang < -Math.PI) {
ang += 2.0 * Math.PI;
}
更新:请参阅这个答案。
我认为在原始问题中,将四元数视为4D向量的想法是为了通过一种简单的方法来测量两个四元数之间的相似度,同时仍要记住四元数表示旋转。(从一个四元数到另一个四元数的实际旋转映射本身就是一个四元数,而不是标量。)
几个回答建议使用点积的acos
。(需要注意的第一件事情是:四元数必须是单位四元数才能使用这种方法。)但是其他答案没有考虑到“双重覆盖问题”: q
和-q
都代表完全相同的旋转。
因为q2
和-q2
表示相同的旋转,所以acos(q1 . q2)
和acos(q1 . (-q2))
应该返回相同的值。然而(除了x==0
的情况),acos(x)
和acos(-x)
并不返回相同的值。因此,平均而言(给定随机四元数),acos(q1 . q2)
将不会一半的时间给出您期望的答案,这意味着它将无法作为测量q1
和q2
之间夹角的度量标准,除非您关心q1
和q2
表示旋转的情况。因此,即使您只计划使用点积或acos
点积作为相似性度量标准,以测试q1
和q2
在旋转效果方面有多相似,您得到的答案也会有一半的时间是错误的。
更具体地说,如果您试图将四元数简单地视为4D向量,并计算ang = acos(q1 . q2)
,则有时您将获得您期望的ang
值,其余的时间您实际想要的值(考虑双重覆盖问题)将是PI - acos(-q1 . q2)
。 这两个值中的哪一个您得到的将根据计算q1
和q2
的方式在这些值之间随机波动!
import java.util.Random;
import org.joml.Quaterniond;
import org.joml.Vector3d;
public class TestQuatNorm {
private static Random random = new Random(1);
private static Quaterniond randomQuaternion() {
return new Quaterniond(
random.nextDouble() * 2 - 1, random.nextDouble() * 2 - 1,
random.nextDouble() * 2 - 1, random.nextDouble() * 2 - 1)
.normalize();
}
public static double normalizedDot0(Quaterniond q1, Quaterniond q2) {
return Math.abs(q1.dot(q2));
}
public static double normalizedDot1(Quaterniond q1, Quaterniond q2) {
return
(q1.w >= 0.0 ? q1 : new Quaterniond(-q1.x, -q1.y, -q1.z, -q1.w))
.dot(
q2.w >= 0.0 ? q2 : new Quaterniond(-q2.x, -q2.y, -q2.z, -q2.w));
}
public static double normalizedDot2(Quaterniond q1, Quaterniond q2) {
Vector3d v1 = new Vector3d(q1.x, q1.y, q1.z);
Vector3d v2 = new Vector3d(q2.x, q2.y, q2.z);
double dot = v1.dot(v2);
Quaterniond q2n = dot >= 0.0 ? q2
: new Quaterniond(-q2.x, -q2.y, -q2.z, -q2.w);
return q1.dot(q2n);
}
public static double acos(double val) {
return Math.toDegrees(Math.acos(Math.max(-1.0, Math.min(1.0, val))));
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
var q1 = randomQuaternion();
var q2 = randomQuaternion();
double dot = q1.dot(q2);
double dot0 = normalizedDot0(q1, q2);
double dot1 = normalizedDot1(q1, q2);
double dot2 = normalizedDot2(q1, q2);
System.out.println(acos(dot) + "\t" + acos(dot0) + "\t" + acos(dot1)
+ "\t" + acos(dot2));
}
}
}
还要注意以下几点:
acos
在数值计算上不够精确(在某些最坏情况下输入数据时,最低有效位可以错误一半);acos
的实现非常缓慢;acos
如果其参数稍微超出 [-1,1] 范围的话就会返回 NaN
,对于偶数单位四元数的点积而言,这是一个常见的情况 -- 所以在调用 acos
之前需要将点积的值限制在该范围内。请参阅上面代码中的这一行: return Math.toDegrees(Math.acos(Math.max(-1.0, Math.min(1.0, val))));
atan2
代替acos
(请注意,这仍然无法解决双重覆盖问题,所以在应用以下公式之前,您需要使用上述任一归一化形式)。ang(q1, q2) = 2 * atan2(|q1 - q2|, |q1 + q2|)
我承认,我不理解这个表述,因为四元数的减法和加法没有几何意义。
请注意:从数值角度来看,acos(dot)非常不稳定。
正如之前所说的,先令q = q1^-1 * q2,然后角度等于2乘以atan2(q.vec.length(), q.w)。
atan2
公式进行比较 - 我不理解它们之间的区别。 - Luke Hutchison如果要计算四元数之间的角度,应该使用 2 x acos(dot) 的方法。
[-1...1]
之间。我也认为这是错误的。被接受的答案应该是正确的。 - KeyC0deacos(dot)
就足够了(几乎),不需要乘以2。然而,我认为你所说的是双覆盖问题:q
和-q
表示相同的旋转。请参阅我的答案以获取详细信息(以及我为什么说“(几乎)”)。 - Luke Hutchison
acos
需要乘以2,并且由于四元数“双覆盖”问题,在调用acos
之前必须取绝对值点积。 - Luke Hutchisonq
和-q
代表完全相同的旋转。您必须首先进行归一化,以使两个四元数位于双覆盖空间的同一半球中,然后您可以将它们视为4D向量,此时点积实际上是有意义的。我在下面扩展了我的答案,以描述针对双覆盖问题的归一化。 - Luke Hutchison