Question

I understand that the dot (or inner) product of two quaternions is the angle between the rotations (including the axis-rotation). This makes the dot product equal to the angle between two points on the quaternion hypersphere.
I can not, however, find how to actually compute the dot product.

Any help would be appreciated!

current code:

public static float dot(Quaternion left, Quaternion right){
    float angle;

    //compute

    return angle;
}

Defined are Quaternion.w, Quaternion.x, Quaternion.y, and Quaternion.z.

Note: It can be assumed that the quaternions are normalised.

Was it helpful?

Solution

The dot product for quaternions is simply the standard Euclidean dot product in 4D:

dot = left.x * right.x + left.y * right.y + left.z * right.z + left.w * right.w

Then the angle your are looking for is the arccos of the dot product (note that the dot product is not the angle): acos(dot).

However, if you are looking for the relative rotation between two quaternions, say from q1 to q2, you should compute the relative quaternion q = q1^-1 * q2 and then find the rotation associated withq.

OTHER TIPS

Just NOTE: acos(dot) is very not stable from numerical point of view.

as was said previos, q = q1^-1 * q2 and than angle = 2*atan2(q.vec.length(), q.w)

Should it be 2 x acos(dot) to get the angle between quaternions.

The "right way" to compute the angle between two quaternions

There is really no such thing as the angle between two quaternions, there is only the quaternion that takes one quaternion to another via multiplication. However, you can measure the total angle of rotation of that mapping transformation, by computing the difference between the two quaternions (e.g. qDiff = q1.mul(q2.inverse()), or your library might be able to compute this directly using a call like qDiff = q1.difference(q2)), and then measuring the angle about the axis of the quaternion (your quaternion library probably has a routine for this, e.g. ang = qDiff.angle()).

Note that you will probably need to fix the value, since measuring the angle about an axis doesn't necessarily give the rotation "the short way around", e.g.:

if (ang > Math.PI) {
    ang -= 2.0 * Math.PI;
} else if (ang < -Math.PI) {
    ang += 2.0 * Math.PI;
}

Measuring the similarity of two quaternions using the dot product

Update: See this answer instead.

I assume that in the original question, the intent of treating the quaternions as 4d vectors is to enable a simple method for measuring the similarity of two quaternions, while still keeping in mind that the quaternions represent rotations. (The actual rotation mapping from one quaternion to another is itself a quaternion, not a scalar.)

Several answers suggest using the acos of the dot product. (First thing to note: the quaternions must be unit quaternions for this to work.) However, the other answers don't take into account the "double cover issue": both q and -q represent the exact same rotation.

Both acos(q1 . q2) and acos(q1 . (-q2)) should return the same value, since q2 and -q2 represent the same rotation. However (with the exception of x == 0), acos(x) and acos(-x) do not return the same value. Therefore, on average (given random quaternions), acos(q1 . q2) will not give you what you expect half of the time, meaning that it will not give you a measure of the angle between q1 and q2, assuming that you care at all that q1 and q2 represent rotations. So even if you only plan to use the dot product or acos of the dot product as a similarity metric, to test how similar q1 and q2 are in terms of the effect they have as a rotation, the answer you get will be wrong half the time.

More specifically, if you are trying to simply treat quaternions as 4d vectors, and you compute ang = acos(q1 . q2), you will sometimes get the value of ang that you expect, and the rest of the time the value you actually wanted (taking into account the double cover issue) will be PI - acos(-q1 . q2). Which of these two values you get will randomly fluctuate between these values depending on exactly how q1 and q2 were computed!.

To solve this problem, you have to normalize the quaternions so that they are in the same "hemisphere" of the double cover space. There are several ways to do this, and to be honest I'm not even sure which of these is the "right" or optimal way. They do all produce different results from other methods in some cases. Any feedback on which of the three normalization forms above is the correct or optimal one would be greatly appreciated.

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));
        }
    }
}

Also note that:

  1. acos is known to not be very numerically accurate (given some worst-case inputs, up to half of the least significant digits can be wrong);
  2. the implementation of acos is exceptionally slow in the JDK standard libraries;
  3. acos returns NaN if its parameter is even slightly outside [-1,1], which is a common occurrence for dot products of even unit quaternions -- so you need to bound the value of the dot product to that range before calling acos. See this line in the code above:
        return Math.toDegrees(Math.acos(Math.max(-1.0, Math.min(1.0, val))));

According to this cheatsheet Eq. (42), there is a more robust and accurate way of computing the angle between two vectors that replaces acos with atan2 (although note that this does not solve the double cover problem either, so you will need to use one of the above normalization forms before applying the following):

ang(q1, q2) = 2 * atan2(|q1 - q2|, |q1 + q2|)

I admit though that I don't understand this formulation, since quaternion subtraction and addition has no geometrical meaning.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top