The Inner Product, March 2002
Jonathan Blow (jon@number-none.com)
Last updated 17 January 2004
Hacking Quaternions
Related articles:
IK with
Quaternion Joint Limits (April 2002)
Understanding Slerp, Then Not Using It (April 2004)
Quaternions are a nifty way to represent rotations in 3D space. You can find
many introductions to quaternions out there on the internet, so I'm going to
assume you know the basics. For a refresher, see the papers by Shoemake or
Eberly in the References.
In this article we will look closely at the tasks of quaternion interpolation
and normalization, and we'll develop some good tricks.
Interpolation
When game programmers want to interpolate between quaternions, they tend to copy
Ken Shoemake's code without really understanding it. Ken uses a function called
slerp that walks along the unit sphere in 4-dimensional space from one
quaternion to the other. Because it's navigating a sphere, it involves a fair
amount of trigonometry, and is correspondingly slow.
Lacking a strong grasp of quaternions, most game developers just accept this:
slerp is slow, and if you want something faster, maybe you should go back to
Euler angles and all their nastiness. But the situation is not so bad. There's a
cheap approximation to slerp that will work in most cases, and is so braindead
simple and fast that it's shocking. Shocking, I tell you.
What Slerp Does
Slerp is desirable because of two main properties; any approximation we
formulate would ideally have the same properties. The first, and perhaps most
important, is that slerp produces the shortest path between the two orientations
on that unit sphere in 4D; this is equivalent to finding the "minimum torque"
rotation in 3D space, which you can think of as the smoothest transition between
two orientations. The second property of slerp is that it travels this path at a
constant speed, which basically means you have full control over the nature of
the transition (if you want to add some style, like starting slowly and then
speeding up, you can just spline your time parameter before feeding it into
slerp.)
So here's our approximation: linearly interpolate the two quaternions,
componentwise. That is, if t is your time parameter from 0 to 1, then x =
x0 + t (x1 - x0), and similarly for y, z, and
w.
One might say: Ridiculous! How could that possibly be a worthwhile
interpolation, when the "right answer" is so much more complicated? Let's take a
look.
Figure 1 shows a 2-dimensional version of quaternion interpolation. Slerp walks
around the edge of the unit circle, which is what we want. Linear interpolation
results in a chord that cuts inside the circle. But here's the thing to realize:
normalizing all the points along the chord stretches them out to unit length, so
that they lie along the slerp path. To restate: if you linearly interpolate two
quaternions from t = 0 to t = 1, and then normalize the result, you get the same
minimal-torque transition that slerp would have given you.
Figure 1: A 2-dimensional picture of quaternion interpolation. The blue circle is the unit sphere; the two yellow vectors are the quaternions. The red arc represents the path traveled by slerp; the green chord shows the path taken by linear interpolation. |
The Linear Algebra way to see this is that both the great circle and the chord
lie in Span(q0,
q1),
which is a 2D subspace of the 4D embedding space. Adding the constraint that
Length(Interpolate(q0,
q1,
t)) = 1 reduces the dimensionality to one, so both paths must lie along the same
circle. And both forms of interpolation produce only a continuous path of points
between q0
and q1,
so they must be the same.
If q0
and q1
lie on opposing points of the sphere, the chord will pass through the origin and
normalization will be undefined. But that's okay -- unless you're doing
something wacky, you don't want your quaternions to be further than 90 degrees
apart in the first place. (Because every rotation has 2 quaternion
representations on the unit sphere, and you want to pick the closest ones to
interpolate between.) So the normalization will always be well-defined.
So the normalized linear interpolation and the slerp both trace out the same
path. There is a difference between them, though: they travel at differing
speeds. The linear interpolation will move quickly at the endpoints and slowly
in the middle. Figure 2 shows a graph of the worst case, 90 degrees.
The function graphed in Figure 2 is
where α is the original angle between the
two quaternions. I figured this out just by drawing a 2D graph like Figure 1,
where one of my vectors is the x axis (1, 0) and the other one is (cos α, sin
α). Then I just wrote an expression for linearly interpolating between them by
't', and then finding the resulting angle by tan-1. This rather
simplistic approach is valid because: (a) since all the action happens in Span(q0,
q1),
we can just take that 2D cross-section out of 4D space; studying it in
isolation, we see the entirety of what is happening. And (b) on the resulting 2D
unit circle, because the set of all possibilities for the two unit vectors is
redundant by rotational symmetry, we can choose one of the vectors to be
anything we like; I chose (1, 0) to simplify the math.
Casey Muratori is the first person I know of who considered linear interpolation
of quaternions as a serious option. He investigated numerically and found linear
interpolation, when properly employed, to be quite worthwhile. So hats off to
Casey, eh? Casey makes the animation toolkit Granny 2 (by RAD Game Tools), and
he has eradicated all slerps from his code. Granny 2 never slerps at runtime,
which is pretty cool considering all the stuff it does.
Figure 2: Worst case of lerp speed variation. Green: ideal result produced by slerp; Red: distorted result produced by lerp. The error between these two functions should be measured vertically, so they are more different than they may appear at first. | Figure 3: The compensating cubic spline, k = 0.45, shown in yellow atop the graph of figure 2. |
Augmenting Linear Interpolation
The linear interpolation is monotonic from q1
to q2,
so if you are doing an application where you're binary searching for a result
that satisfies some constraint, using the linear interpolation works just fine.
If your quaternions are very close together (under 30 degrees, say), as you have
when playing back a series of time-sampled animation data, linear interpolation
works fine. And if you have some number of different character poses (like an
enemy pointing his gun in several different directions), and you want to mix
them based on a blending parameter, linear interpolation probably works fine.
Linear interpolation won't work if you need precise speed control and wide
interpolation angles. But maybe we can fix that.
Perhaps we can make a spline that cancels most of the speed distortion. Looking
at Figure 2, can we concoct a function that, when multiplied against the curve,
causes it to lie much closer to the ideal line? The way I chose to visualize
this was with a cubic spline that tries to pull the distortion function onto the
diagonal. Figure 3 shows a cubic spline with the equation y = 2kt3 -
3kt2 + (1 + k)t, where the tuning parameter k = 0.45, graphed against
the plot of figure 2. [Note: We are looking for some function g to help
us correct f; strictly speaking, the most general approach is to structure the
correction as g(f(t)), not g(t)f(t). But I can get away with thinking of
this correction as a multiplication problem because: (1) In this case f(t) = t,
and (2) I'm using splines with no constant coefficients, so I can factor t out
of the spline. Essentially I used some foreknowledge about the nature of
the solution to simplify the problem.]
Because both the distortion curve and our compensating spline have an average
value of 't', and are approximately complementary, when we multiply them
together we get a function that is approximately g(t) = t2. We want
g(t) = t, so we'll divide the cubic spline by t. Fortunately, since the spline
passes through the origin, it has no 'd' coefficient; so dividing by t just
turns it into a quadratic curve:
y = 2kt2
- 3kt + 1 + k.
So now, if we're linearly interpolating two splines that are 90 degrees apart,
we find t' = 2kt2
- 3kt + 1 + k, and use t' as our interpolation parameter. We get
something very close to constant-speed interpolation (I will quantify how close
in a little bit). However, if we reduce the angle between the input quaternions,
we get something that's less accurate than the original 't'.
That's because I concocted this spline specifically for the worst-case scenario,
by defining its slope at t = 0 and t = 1. That's where the 'k' parameter comes
in: it's a slope control mechanism. To get this spline to compensate for
distortion across the full range of quaternion input angles, we want to adjust
the tuning parameter as some easily-computable function of the angle between the
two quaternions.
Well, taking the dot product of two quaternions gives us cos α, the cosine of
the angle between them. I started playing around with simple functions of cos α
until I found something reasonable. Basically, we want a function that is 1 when
cos α = 0, and that is near 0 when cos α = 1. After some experimentation I
landed on k = 0.45 (1 - s cos α)2, where s = 0.9 for now. To cursory
visual inspection, this gives pretty good results across the full range of α
from 0 to 90 degrees.
These numbers are in the right neighborhood, but because I just made them up,
they're not going to be as close as we can get. So I wrote some code to do
hillclimbing least-squares minimization. The initial distortion function has an
RMS error of about 1.6 x 10-2 when averaged over all interpolation
sizes (the worst case, graphed in figure 2, has an RMS error of 3.234 x 10-2).
The minimzier gave me the following values: k = 0.5069269, s = 0.7878088 yields
an overall error of 2.07 x 10-3, which is about 8 times lower than
we'd started with. See Listing 1.
Listing 1: A function that splines 't' to compensate for the distortion induced by lerping.
float
correction(float t, double alpha, double k, double attenuation) { |
But while I had been aligning things by eye, I noticed that if I gave k a high
value, I got results that were close to exact from t = 0 to t = 0.5, but
diverged after t = 0.5. So I wrote an interpolator that only needs to evaluate t
from 0 to 0.5. If you pass in a t higher than 0.5, it just swaps the endpoints
of interpolation. Running the optimizer on this, I got k = 0.5855064, s =
0.8228677, overall error 5.85 x 10-4 -- a reduction of more than 27
from the original. We incur another small cost to gain this accuracy, an extra
'if' statement.
You can probably do better than these numbers; my methods were ad-hoc, and there
are many possibilities I haven't explored. I should also give a few warnings,
for example, the 'if' statement mentioned above introduces a slight
discontinuity at t = 0.5; you can fix this discontinuity by shifting the
midpoint away from .5 but this wasn't important for my needs.
So we can interpolate pretty quickly now, but we end up with non-unit
quaternions. Probably we want unit quaternions, so how do we normalize without
doing a really slow inverse sqrt operation?
Normalization
To normalize any vector, quaternions included, we want to divide the vector by
its length. The squared length of some vector v is cheap to compute -- it's v·v
-- so we need to obtain 1/sqrt(v·v) and
multiply the vector by that. Division and square-rooting are pretty expensive
though.
We can compute a fast 1/sqrt(x) by using a tangent-line approximation to the
function. This is like a really simple 1-step Newton-Raphson iteration, and by
tuning it for our specific case, we can achieve high accuracy for cheap. (A
Newton-Raphson iteration is how specialized instruction sets like 3DNow and SSE
compute fast 1/sqrt).
The basic idea is that we graph the function 1/sqrt(x), locate some neighborhood
that we're interested in, and pretend that the function is linear there. A
linear function is cheap to evaluate.
So: we want to approximate f(x) = 1/sqrt(x). We are interested in vectors whose
lengths are somewhere near 1, meaning f(x) = 1, which means x = 1. So we are
going to focus on the neighborhood x = 1, as you see in Figure 4. To get the
line, we just take the derivative of f, f'(x) = -1/2
x-3/2,
and evaluate it at 1: f'(1) = -1/2.
Figure 4: In green, the function f(x) = 1/sqrt(x); in yellow, its tangent line at x = 1. |
An equation that says "locally, a function is approximately its value at some
point plus its first derivative extrapolated over distance" is: f(x + Δx)
≈ f(x) + Δx f'(x). We evaluate this at x=1
to get f(1 + Δx) ~~ f(1) + Δx f'(1) = 1 - 1/2 Δx.
Now for the last trick: we want to represent the squared length of our input
vector, which we'll call s, as a value in the neighborhood of 1, so we
can plug it into our new linear function. We say s = 1 + Δx, and thus Δx = s -
1.
That is all we need. When we plug Δx = s - 1 into our approximation, we get f(1
+ s - 1) ≈ 1 - 1/2(s - 1). Simplified, this
says: f(s) ≈ 1/2(3 - s). For as wide of a
neighborhood as 1/sqrt is well-approximated by a tangent line, this extremely
fast computation will give us the factor to normalize a vector. Figure 5 graphs
the vector lengths we get when we use this computation to normalize. As long as
we start with a vector whose length is near 1, we get results that are fairly
accurate.
Figure 5: The length of an approximately normalized vector (yellow), versus the squared length of the input, when using the naive tangent line approximation. The green line indicates the ideal result. |
For some applications, accuracy in a narrow range is all we need. If you are
reconstructing quaternions from splines, as one might do in a skeletal animation
system that stores animation data in a small memory footprint, you can ensure a
maximum length deviation during the spline-fitting process (inserting extra
keyframes to alleviate any problems). Then at runtime you just evaluate the
splines and pump the coefficients into this one-step normalizer, and you can be
assured that the results are good.
On the other hand, this isn't good enough to use blindly on the results of
quaternion linear interpolation. We can see that, during our worst-case
interpolation from (1, 0) to (0, 1), the closest we get to the origin is (1/2,
1/2), which gives us a squared vector length s = 1/2. So for good results after
lerping, we need a fast normalizer that produces good results all the way
through the interval from s = 1/2 to s = 1.
Re-Tuning the Tangent Line Approximation
When we linearly interpolate quaternions, we get a chord that cuts through the
unit sphere; that is, the resulting length is always less than 1. So we don't
need our linear approximation to be accurate above 1. We can, in effect, slide
the graph of figure 5 to the left a little bit, making our approximation more
effective for shorter vectors.
Also, if we are going to permit some small amount of error
ε in our result, it probably makes sense to
allow results in the range 1 ± ε, instead of
just 1 - ε as in figure 4. So we can scale
the approximation by some small factor. This roughly doubles the zone of good
results.
But this still doesn't cover the full range from 1/2 to 1, so what do we do if
we need to handle all that? A simple idea would be to just check the value of
's', and if it is too low, just compute the answer the slow way. For most
applications, wide-angle interpolations will be extremely rare so the speed hit
will be small. But if you need to be faster than that, there are some hackish
things we can do.
I wrote some code that repeatedly applies the fast normalization, tuned by some
optimization parameters, in order to achieve the least error across the interval
we are interested in (Listing 2). Running the numerical optimizer on this yields
x_offset = .959066, scale = 1.000311, and a root-mean-square error of 2.15 x 10-4...
which seems pretty darn good. This loop only iterates at most 3 times over the
interval we care about, so you can re-phrase the loop as a small series of
nested 'if' statements, which are mostly never descended into (Listing 3).
Listing 2: A loop
that repeatedly applies the fast normalizer until an acceptable value is
reached. (This is an expanded version to facilitate experimentation; a
production version would have a lot of constants factored out, like 1/sqrt(neighborhood).)
while (factor
* factor * s < limit2) { |
Listing 3: A possible production version of Listing 2.
inline float
isqrt_approx_in_neighborhood(float s) { |
Sample Code
This month's sample code implements fast linear interpolation and
renormalization, as well as the numerical optimization code that computes the
best parameters. See you next time.
References
"Animating Rotation with Quaternion Curves", Ken Shoemake, Computer Graphics
V.19 N.3, 1985.
"Quaternion Algebra and Calculus", David Eberly. http://www.magic-software.com/Documentation/quat.pdf