This is a post about rotation operations, that is operations that modify an orientation (or attitude). There are a few ways we can represent rotations: unit quaternions, rotation matrices and axis-angle representations are some of the most useful. Rotation matrices and unit quaternions are also commonly used to represent the orientations themselves, and axis-angle is most common when talking about changes in orientation (it’s basically an “angular velocity” representation).

There are two things we might want to do with a given rotation operator: rotate a single vector, or rotate an entire orientation. When we’re in quaternion representation, rotating an orientation looks like simple (quaternion) multiplication:

Similarly, for rotation matrices we can use matrix multiplication:

When rotating a vector, rotation matrices still work by matrix multiplication (or matrix-vector multiplication, if you consider that different):

However, quaternions use an operation known as “conjugation”:

where the second equality follows from the equality of the quaternion inverse (reciprocal) and the quaternion conjugate (computed by negating its vector part) for unit quaternions. The first holds even for non-unit quaternions, though I will not use that fact here.

Typically, the way that these operations are performed for axis-angle rotations is to first compute a corresponding rotation matrix or quaternion, then apply one of the above procedures. This can be an efficient approach if the derived matrix or quaternion is going to be used many times, but doing so once and then throwing it away can be wasteful.

First, I’ll look at rotating a vector according to an angular velocity vector and a delta-time scale factor . The total scaled-axis representation of the rotation is then , with total angle and axis the unit vector parallel to the angular velocity. This is a common representation in applications.

The quaternion form of this scaled-axis rotation has real (scalar) part and imaginary (vector) part . Because the variables we have to work with are the angular velocity and the time step, not the unit axis , computing this requires normalising the angular velocity (involving a square root) as well as computing a sine and cosine. Not ideal.

How about the rotation matrix representation? Rodrigues’ rotation formula gives us:

Here, is the identity matrix and the antisymmetric matrix is such that is the usual cross product. Therefore, the effect on a vector is:

This also involves normalising the angular velocity and computing some trig functions. Can we do better?

One approach is “first order and normalise”: take the rotation matrix to first order in and then normalise the resulting vector to the same length as the input:

This looks okay at first glance - now just a square root is necessary and no trig functions, but there’s a problem: This doesn’t actually rotate around . Notice that only appears in the combination , which is the same as for any , because cross products between a vector and itself are zero. This operation actually rotates around , orthogonal to .

It’s notable that this same sort of problem doesn’t occur in the quaternion domain: The first-order approximation to the quaternion is:

Dividing by the magnitude of this denormalised quaternion gives:

which has its imaginary vector component in the same direction as . It’s not too hard to figure out the angle this quaternion turns through:

Basically, the angle saturates according to the arctangent of the desired angle, leading to absolute angles between positive and negative

Avoiding the square root

I’ll look back at Rodrigues’ rotation formula now, and write it in terms of the half-angle rather than the full angle . This will turn out helpful later, trust me.

Now I will intentionally apply the same approximation as for the quaternion case above, that is , and so :

and so:

And there we have it: an approximate rotation of a vector by a given angular velocity and timestep, without needing any non-elementary arithmetic operations! While the angle turned through is only correct to second-order:

the axis is the same as the exact rotation, and the length-preserving property of rotation operators is reproduced without requiring a square root evaluation to compute the length of an intermediate result.

Now, this is all good for operating on individual vectors, but what if we want to update an entire orientation? That ideally needs a quaternion to multiply by, not a rotation matrix. I’ll try applying the same half-angle trick to the expression for a quaternion:

This time the approximation is , which is also second-order accurate but with a third-order coefficient 4 times smaller:

This would be the form I would recommend for updating quaternion orientations if speed is of the essence, with the above form used for rotating vectors.

Benchmarking

In some basic testing I have carried out, the methods I have suggested are as fast or faster than others, and can be substantially faster than constructing a full quaternion.

For rotating a vector, doing so by computing the full quaternion and performing a quaternion-vector conjugation took 28.9 nanoseconds. Computing my recommended approximate quaternion and conjugating took 22.3 ns. Doing the “first-order and normalise” took 20.1 ns and my recommended approximate vector rotation took 18.4 ns. My suggested method is both the fastest, and has accuracy advantages (i.e. it reproduces the correct axis) over the next fastest method, the “first-order and normalise”. It’s over a third faster than the full calculation, so is a good option where rotation rates are relatively low or execution speed is the priority over maximum accuracy.

For computing an orientation quaternion (not including multiplying with another quaternion, as this would be the same for all of the methods), the “exact” method took 21.8 ns, “first-order and normalise” took 16.7 ns and my approximation took 15.0 ns. Here, the “first-order and normalise” method does reproduce the correct axis, but its angle error is slightly worse than that of my method, as well as being marginally slower.