How can I min-max quaternions?

What I'm doing

In my engine, I'm trying to implement a camera that will follow a target object, such as a player.

I wanted to avoid just simply using the inverse of the target's transformation, because the camera may end up "jerky", so instead I interpolate between the current and target orientation over time, this way the camera's rotation eases in on the desired rotation:

quat targetQuat   = target->getOrientation();
quat currentQuat  = getOrientation();

quat slerpQuat    = glm::slerp(currentQuat, targetQuat, 0.05f);

setOrientation(slerpQuat);

Where my problem begins:

Since I'm just interpolating between quaternions, it is possible that the target may spin faster than the camera can catch up, and may wind up falling behind.

I want to somehow impose a quaternion rotation cap, or somehow determine which of the two is larger so I can min/max 2 quaternions. For example:

quat targetQuat   = target->getOrientation();
quat currentQuat  = getOrientation();

quat slerpQuat    = glm::slerp(currentQuat, targetQuat, 0.05f);
quat maximumQuat  = targetQuat*quat(0.707, 0, -0.3535, 0);

quat desiredQuat  = min(slerpQuat, maximumQuat);

setOrientation(desiredQuat);

How can I determine which of two quaternions rotates/deviates the furthest? Are there any better alternatives methods? The general idea I had was just to min/max the quat such that it always falls within the bounds I need.

I know that I can calculate a delta quaternion, or rather multiply q0 by inverse q1, but even so I don't know what I would do with it or how it could inform me which of the two is the largest one.

• Did you experiment with the slerp only implementation? There is no such thing as a min/max quat, but you can measure, as stated, the distance (or angle) between the current and target quats for a scalar idea of how different they are. If your target object constantly spins, it is not really possible to ease in and settle. If the object does not spin constantly, then you can achieve the result you desire. May 25 '16 at 6:46

I bit on the mathematical side, but here's a Q & A on MSE on computing quanternion distance. Using that you could do something like:

quat targetQuat   = target->getOrientation();
quat currentQuat  = getOrientation();

quat lerpQuat     = glm::lerp(currentQuat, targetQuat, 0.05f);
quat maximumQuat  = targetQuat*quat(0.707, 0, -0.3535, 0);

float d1 = quant_dist(currentQuat, lerpQuat);
float d2 = quant_dist(currentQuat, maximumQuat);

if(d1 < d2){
desiredQuat = lerpQuat;
}
else{
desiredQuat = maximumQuat;
}

setOrientation(desiredQuat);

Alright so I figured out an even more ideal solution.

The way I was trying to solve the problem was going to lead to more issues than I needed to deal with, so instead I slept on the problem and thought up a different approach.

Instead of trying to min-max my orientation, I now calculate how far from the target orientation the camera is, If it lays above a certain threshold, I lerp the camera's quat against the targets quat, by an amount that would return it to that threshold.

1. Calculate the angle between the camera and the target (between 0-1, where 1 = 180°)
2. If the angle is greater than a specified amount, go to step 3
3. Divide the desired angle by the current angle
4. We now know how far we have to lerp from the camera to the target in order to return to the maximum desired value.

The advantage of this is that the camera is given a specified "cone" of freedom to smoothly move around in, which the camera will drag along the edge of instead of falling behind (imagine a funnel with a stick in it).

To constrain my camera's orientation by an angle of 45° from the target, I do the following:

float getAngleBetween(quat q1, quat q2)
{
float theta = acosf(q1.w*q2.w + q1.x*q2.x + q1.y*q2.y + q1.z*q2.z);
if (theta > M_PI_2)
theta   = M_PI - theta;
return theta;
}

void DCamera::update()
{
quat targetQuat      = glm::normalize(target->getOrientation());
quat currentQuat     = glm::normalize(getOrientation());

// Angle between the camera and the target
float angle          = getAngleBetween(targetQuat, currentQuat);
const float maxAngle = 0.25f;

// If the angle is greater than 45 degrees
// (0-1 range == 0-180, thus 0.25 = 45)
if (angle > maxAngle) {
// Inteprolate a quaternion between the camera and the target
// The mixing amount is the desired angle divided by the total angle in this range
setOrientation(glm::slerp(targetQuat, currentQuat, maxAngle / angle));
}
}

So if my camera is 135 degrees away from the target, and the maximum allowed angle is 45 degrees in any direction, then we rotate the camera straight towards the target by 90 degrees.

• There is one mathematical inaccuracy your algorithm: the angle between quaternions and the angle between your camera forward vector and the target forward vector is not necessarily the same. This is because, for all we care, both camera and target forward vectors can be aligned, but the target can be rolled about its forward vector at more than 45 degrees. In this case, the camera and target are obviously inside the 45 degree cone you describe, but the angle between the quaternions is larger than this. Is this what you want? May 26 '16 at 9:15

If I understand your question, you want to limit the rotational velocity of a turn?

Get the delta.

From your rotation delta, you can find out how many radians of turn it represents from the S element (or sometimes called W element). The S element is cos(rotationAmt / 2), so can be extracted as rotationAmt = acos(deltaQuaternion.s) * 2.

From there you could clamp the radians-per-tick movement amount. Like:

rotationAmt = acos(deltaQuaternion.s) * 2.0;
{
deltaQuaternion.s = rotationAmt;

// renormalize it
deltaQuaternion = normalizeQuat(deltaQuaternion);
}

(Ok, I have not run this code. Also it might need to handle negative rotationAmt, if your quaternions are flipped or anything. But that's the gist of it.)

The idea of measuring the distance between quaternions is indeed a useful similarity measure.

In essence, what you can measure the dot product between two quaternions qA and qB just as you compute the dot product of two vectors (see this). Moreover, since you have unit quaternions (they do represent rotations!), the dot product should be between [-1,1]. The more similar your quats are (i.e. closer), the larger the dot product value (i.e. closer to 1).

Refer to the image below. The A arc on the unit (hyper)sphere is the path taken by the slerp. The length of A is actually the distance between your quats and is equal to the angle between them (computable, but not directly required, from the dot product). The slerp is also a pretty good equivalent to normal lerp, but on curved spaces (the unit hypersphere). So if you have two quantities, qA and qB and you want to produce a value qT such that qT starts from qA and moves towards qB, the slerp is going to do that. All you need to make sure is that the slerp interpolation parameter (t between [0,1]) traverses the [0,1] interval. If your qB quaternion does not remain constant over time, you must restart the slerp from by setting qA = qT (the current orientation becomes the start of a new slerp), while qB is your new target orientation. If you want to have your camera trace the target orientation in a fixed amount of time, you must restart the tracing be setting qA to your current camera orientation and qB to the new target orientation (assuming the target has rotated at all).

Summarizing this all in an algorithm:

OnTargetRotationSet(qNew) // called when you (re)set the target orientation
{
qTarget = qNew;
if (dot(qCam, qTarget) < 1 - epsilon)
{
camAlignInProgress = true;
qStart = qCam;
qEnd = qTarget;
slerpParam = 0;
slerpLength = ComputeAngleBetween(qCam, qTarget) / 3.141592;
slerpSpeed = slerpLength / totalTimeToPerformAlignmentInSeconds;
}
}

OnGameStep() // called/triggered somewhere in your game loop before rendering anything
{
if (rotationInProgress == false)
return; // no need to align the camera with target
if (slerpParam >= 1.0)
{
qCam = qTarget; // snap to target
rotationInProgress = false;
return; // end the alignment procedure
}

qCam = QuatSlerp(qStart, qEnd, slerpParam);