Unrolling Rotations

26/03/2024

Today I'd like to talk about unrolling rotations. Not only is this something I wish I'd been told about a lot earlier when I started doing animation programming (for example, I'm pretty sure some of the artefacts in the animation of my PFNN character were caused by the fact I did not unroll my rotations properly!), but it's also because I think it's a really good way to get a deeper understanding of the different representations of rotations, how they work, and what their properties are (similar to visualizing rotation spaces).

And while this is something I mentioned briefly in my article on Joint Limits, I feel it requires a bit more depth. So let's start at the beginning:


Euler Angles

If you open some animation data in Maya or MotionBuilder and look at the keyframed curves for one of the joints, you might see something like this:

keyframe jump

It appears that once a joint's rotation reaches +180 degrees it seems to jump instantly to -180. Similarly, it will jump from -180 to +180 instantly when moving in the other direction.

The reason for this is simple: a rotation of +181 degrees and -179 degrees produce the same result and this data has been "normalized" i.e. all the angles have been put into the range of -180 degrees to +180 degrees, even if this means discontinuities. This might simply be because these euler angles were generated from some other representation such as quaternions.

When we're processing animation data these discontinuities can be annoying. For example, if we wanted to give this data as input to a neural network or other statistical technique we'd be in the situation where two extremely similar poses for the character would have two very different numerical representations - which is going to make learning anything very challenging. Similarly, if we tried to interpolate between these two rotation values naively we would pass through zero rather than taking the shortest route.

What we need to do is "unroll" this data. By allowing for rotations of greater than +180 degrees or less than -180 degrees we can remove the discontinuities. Here is a simple function that does this by basically integrating the differences between consecutive rotations:

// Put an angle (represented in radians) into range -180 to +180 degrees
float angle_normalize(float x)
{
    float y = fmod(x + M_PI, 2.0f * M_PI);
    return y < 0.0f ? y + M_PI : y - M_PI;
}

// Put set of three euler angles into range -180 to +180 degrees
vec3 angle_normalize(vec3 v)
{
    return vec3(angle_normalize(v.x), angle_normalize(v.y), angle_normalize(v.z));
}

// Compute the angular difference between two sets of euler angles
vec3 angle_sub(vec3 a, vec3 b)
{
    return angle_normalize(a - b);
}

// Unroll an array of euler angles in-place
void euler_unroll_inplace(slice1d<vec3> rotations)
{
    // Make initial euler angles be in the range -180 to +180 degrees
    rotations(0) = angle_normalize(rotations(0));
    
    // Loop over following rotations
    for (int i = 1; i < rotations.size; i++)
    {
        // Angular difference between the previous and current rotation
        vec3 rotation_difference = angle_sub(rotations(i), rotations(i - 1));
        
        // Accumulate the angular difference
        rotations(i) = rotations(i-1) + rotation_difference;
    }
}

And here is the result on some animation data. This is what it looks like before unrolling (you can see the constant swaps between +180 and -180 degrees):

angles euler

and here it is after:

angles euler unrolled

So while these rotations go over +180 and under -180 degrees, in doing so we have removed the discontinuities.

(Aside: This can be achieved in MotionBuilder by using the unroll filter when plotting your data)

If we have a rotation in our data which is doing multiple revolutions (such as a wheel spinning around), then this unrolling will effectively capture these multiple wrap-arounds, even if it means the resulting euler angle values become very large. For example this:

angles euler

will become this:

angles euler

That's a double-edged sword. We can remove discontinuities, but if we have something that is truly spinning around fast and making multiple loops it may produce some very large rotation values! A wheel which has done zero revolutions and a wheel which has done 100 revolutions (even if locally they have the same orientation) will have vastly different rotation values. That's a bit weird - almost like the wheel has a memory of how many times it has turned. Is there a way to avoid that?


Rotation Matrices

What happens if we take the previous rotations in euler angles...

angles euler

and convert them to rotation matrices, plotting the 9 values of the rotation matrix instead:

angles matrix

Look! No discontinuities. This is because for a rotation matrix, a rotation of -180 degrees and +180 degrees are represented by exactly the same value, so there is naturally no jump or discontinuity whatever the configuration. If we apply the same thing to our unrolled euler angles the result would be the same - no discontinuities and rotation values which don't grow to be arbitrarily large. For example, here is what the rotation matrix plot looks like for our example of doing multiple revolutions like a wheel:

angles matrix wheel

This is the reason why you see lots of animation research papers using two-columns of the rotation matrix as their rotation representation for input and output to neural networks. This representation is pretty much fool-proof: it always produces continuous values which are within a fixed range whatever you throw at it. The reason we tend to not include the third column of the matrix is because it can be easily reconstructed from the other two using the cross product so adding it in would just be a waste of the Neural Network's capacity (for an in-depth comparison check out this paper).


Quaternions

What about quaternions? Do they need unrolling? The answer is yes, but the situation is different again.

Let's convert our Euler angles to quaternions and see what the result looks like. Here is our euler angles again:

angles euler

And here are the quaternion values:

angles euler

We can see that there are still discontinuities. So yes, just like euler angles, quaternions can have a discontinuity when a rotation goes over +180 degrees and switches to -180 degrees. To unroll quaternions we pick the quaternion on the hemisphere closest to our previous rotation. This unrolls the quaterions by allowing for rotations of less than -180 degrees and greater than +180 degrees:

quat quat_abs(quat x)
{
    return x.w < 0.0 ? -x : x;
}

void quat_unroll_inplace(slice1d<quat> rotations)
{
    // Make initial rotation be the "short way around"
    rotations(0) = quat_abs(rotations(0));
    
    // Loop over following rotations
    for (int i = 1; i < rotations.size; i++)
    {
        // If more than 180 degrees away from previous frame 
        // rotation then flip to opposite hemisphere
        if (quat_dot(rotations(i), rotations(i - 1)) < 0.0f)
        {
            rotations(i) = -rotations(i);
        }
    }
}

And this is how it looks after unrolling:

angles quat unroll

So far it might seem like this is exactly the same situation as with euler angles, but what about our wheel example where we have a rotation making multiple revolutions? Converting directly to quaternions we see the discontinuities just like with the euler angles:

angles quat wheel

And here it is after unrolling.

angles quat wheel unroll

Now we have a difference...unlike the euler angles we can see that this time we don't get values that grow arbitrarily large. The values appear grow, but then they loop back again...

This might seem a little unintuitive but one way to think about it is this: a rotation of +360 degrees and -360 degrees are represented by the same quaternion value, so after we rotate from 0 to +360 degrees, we then start to rotate back from -360 to 0 degrees, to zero to complete a full 720 degree loop.

This property of quaternions where you can complete a 720 degree rotation to end up back in the state you started is sometimes explained with "the belt trick". However I like to think about it like this: that doing two full rotations (the first from 0 to 360, and the second from -360 back to 0) gives the appearance of a continuous 720 degree rotation that puts you back in your starting state.

Here is a video of me rotating an object through +360 degrees:

And here is another different video of me rotating the same object through -360 degrees:

If I reverse the second video (to turn it into a rotation from -360 degrees back to 0) and stitch it to the first, then it does indeed give the appearance that the object is making a full 720 degree rotation and ending up back in its original state:

I think that gives some intuition for why the quaternion curves can go up but then come down again while only (appearing) to rotate continuously in one direction.

This property of quaternions is either desirable or undesirable depending on your application. Like rotation matrices, unrolled quaternions will not grow larger and larger in value as you spin them around multiple times - but unlike rotation matrices, quaternions can distinguish between rotations of more than +180 or less than -180 degrees (up to +360 or -360 degrees, at which point they loop around again).


Exponential Map

What about the exponential map? Things are different again here...

If we start with our raw, not-unrolled quaternion values and put them into the exponential map we can see that the discontinuities are there just like with the quaternions:

angles exp

While if we take our unrolled quaternions and put those into the exponential map, at first it might look like we get a result with no discontinuities and we are therefore done:

angles exp unroll

And as long as our rotations are not rotating around more than 360 degrees from their starting point this would be the case (which actually is often the case in character animation when we are dealing with characters with limbs that we don't expect to be spinning around like wheels!)

But if we take our quaternions from our wheel example where we have an object rotating multiple times and put this into the exponential map we see something odd. Here is it without unrolling the quaternions:

angles exp wheel

And here it is after unrolling the quaternions:

angles exp wheel unroll

Now, we can see that there is not exactly a pure discontinuity, but there is a large jump from a positive to a negative value nonetheless.

This is because unlike quaternions, the exponential map does not represent a rotation of +360 degrees and -360 degrees using the same value. We can see this really clearly if we just create an artificial signal representing a continuous rotation from -720 degrees to +720 degrees on the X axis. Here is what it looks like in (unrolled) quaternions:

angles quat test

No discontinuity - everything is smooth since we are looping around as explained in the previous section. Yet here is the result when we convert this to the exponential map:

angles exp test unroll

Now we can see the discontinuity clearly. We jump from positive rotations of +360 degrees to negative rotations of -360 degrees.

Okay so how do we fix this one? Unfortunately this time I don't have an answer for you! I may well be wrong but I think that unrolling the exponential map in a way that avoids sharp changes is not actually possible as when you move to the outer "shells" the configuration gets into an unstable state similar to that of gimbal lock.

Nonetheless, I could not find anything online about this, so please let me know if I am wrong and you know a way to unroll the exponential map or have a deeper insight into this.


Conclusion

Rotations are weird! I find it fascinating how different representations have both commonalities and differences when it comes to unrolling. I hope you've found this post insightful and useful too, and as usual thanks for reading.