Monday, January 19, 2009

Calculating Slerp

SLERP is a nice way to manage rotation using quaternions. The reasons to use quaternions instead of Euler angles is three-fold. First, this avoids the awkward flipping this is usually accompanied with matrix rotation due to the effect of what's called "gimbel-lock'. Second, smooth rotation is very difficult near values for nπ (0, π/2, π, 3π/2, ...). Third, setting up a rotation matrix can be a lot of CPU effort and a SLERP is far less computation.

This is ideal for rotation of an elbow joint or a knee joint and provides smooth rotation for all forms of animation. Plus, quaternions can be quantized to be smaller (quite a bit smaller) to allow data compression.

This is a relatively simple thing to calculate. Most of us are familiar with LERP and this has its origins in simple algebra (and complex algebra too):

V
= A (t) + B (1-t), 0 ≤ t ≤ 1
* multiplication is implied

As t changes, we either end up with more of A or more of B, but never more than 100% of either and such that you end up with a nice linear movement between our A value and our B value.

SLERP is very similar, but it takes values in a circle meaning that sine becomes our friend. The formula looks more like this:

V = A (sine (ϴ * (1-t))/sine (ϴ)) + B (sine (ϴ * t)/sine (ϴ)), 0 ≤ t ≤ 1

Keep in mind that most computers use float values and so one fix we have here accounts for some error that small float values introduce. So the steps to SLERP are:
1) Calculate the angle from A to B (dot product)
2) Account of the fact that when taking a dot product of quaternions, we can end up with a negative angle.
3) For very small angles, we need to use LERP because of some strange effects caused by small float values.
4) Find the angle, calculate the sine of that angle, calculate different t values for a percentage of the rotation
5) Using a percentage of the rotation, create a new quaternion

the code looks like this:

Quaternion Slerp (Quaternion A, Quaternion B, float Percentage)
{
assert (Percentage>=0 && Percentage <=1.0);

float
Mult1, Mult2;

float CosTheta = A.Dot (B);// dot product
if (CosTheta <>
{
A = -A;// inverse quat
CosTheta = -CosTheta;
}
if ((CosTheta + 1.0f) > 0.05F) // If the quaternions are close in angle, use LERP to correct for strange behavior of float values close to 0
{
if ( (1.0f - CosTheta ) < 0.05F )
{
Mult1 = 1.0f - Percentage;
Mult2 = Percentage;

Quaternion quat (Mult1 * A.x + Mult2 * B.x,
Mult1 * A.y + Mult2 * B.y,
Mult1 * A.z + Mult2 * B.z,
Mult1 * A.w + Mult2 * B.w);
return quat.Normalize ();
}
else
{
// do SLERP
float Theta = acos (CosTheta);// angle
float SinTheta = sin (Theta);
Mult1 = sin (Theta * (1.0f-Percentage)) / SinTheta;
Mult2 = sin (Theta * Percentage) / SinTheta;
}

}
else
{
// this will fall through below
B.Set (-A.y, A.x, -A.w, A.z);
Mult1 = sin (M_PI * (0.5f - Percentage));
Mult2 = sin (M_PI * Percentage);
}

// return the final LERP/SLERP
return Quaternion (Mult1 * A.x + Mult2 * B.x,
Mult1 * A.y + Mult2 * B.y,
Mult1 * A.z + Mult2 * B.z,
Mult1 * A.w + Mult2 * B.w);
}

The result is an angle pointing somewhere between A and B.

I'll throw out this nugget: if you store the acceleration (2nd derivitive) only, then you can compress SLERP down to just a few bits per frame.

No comments: