Icon Unrolling Rotations


Icon Animation Blend Spaces without Triangulation


Icon Quaternion Weighted Average


Icon BVHView


Icon Dead Blending Node in Unreal Engine


Icon Propagating Velocities through Animation Systems


Icon Cubic Interpolation of Quaternions


Icon Dead Blending


Icon Perfect Tracking with Springs


Icon Creating Looping Animations from Motion Capture


Icon My Favourite Things


Icon Inertialization Transition Cost


Icon Scalar Velocity


Icon Tags, Ranges and Masks


Icon Fitting Code Driven Displacement


Icon atoi and Trillions of Whales


Icon SuperTrack: Motion Tracking for Physically Simulated Characters using Supervised Learning


Icon Joint Limits


Icon Code vs Data Driven Displacement


Icon Exponential Map, Angle Axis, and Angular Velocity


Icon Encoding Events for Neural Networks


Icon Visualizing Rotation Spaces


Icon Spring-It-On: The Game Developer's Spring-Roll-Call


Icon Interviewing Advice from the Other Side of the Table


Icon Saguaro


Icon Learned Motion Matching


Icon Why Can't I Reproduce Their Results?


Icon Latinendian vs Arabendian


Icon Machine Learning, Kolmogorov Complexity, and Squishy Bunnies


Icon Subspace Neural Physics: Fast Data-Driven Interactive Simulation


Icon Software for Rent


Icon Naraleian Caterpillars


Icon The Scientific Method is a Virus


Icon Local Minima, Saddle Points, and Plateaus


Icon Robust Solving of Optical Motion Capture Data by Denoising


Icon Simple Concurrency in Python


Icon The Software Thief


Icon ASCII : A Love Letter


Icon My Neural Network isn't working! What should I do?


Icon Phase-Functioned Neural Networks for Character Control


Icon 17 Line Markov Chain


Icon 14 Character Random Number Generator


Icon Simple Two Joint IK


Icon Generating Icons with Pixel Sorting


Icon Neural Network Ambient Occlusion


Icon Three Short Stories about the East Coast Main Line


Icon The New Alphabet


Icon "The Color Munifni Exists"


Icon A Deep Learning Framework For Character Motion Synthesis and Editing


Icon The Halting Problem and The Moral Arbitrator


Icon The Witness


Icon Four Seasons Crisp Omelette


Icon At the Bottom of the Elevator


Icon Tracing Functions in Python


Icon Still Things and Moving Things


Icon water.cpp


Icon Making Poetry in Piet


Icon Learning Motion Manifolds with Convolutional Autoencoders


Icon Learning an Inverse Rig Mapping for Character Animation


Icon Infinity Doesn't Exist


Icon Polyconf


Icon Raleigh


Icon The Skagerrak


Icon Printing a Stack Trace with MinGW


Icon The Border Pines


Icon You could have invented Parser Combinators


Icon Ready for the Fight


Icon Earthbound


Icon Turing Drawings


Icon Lost Child Announcement


Icon Shelter


Icon Data Science, how hard can it be?


Icon Denki Furo


Icon In Defence of the Unitype


Icon Maya Velocity Node


Icon Sandy Denny


Icon What type of Machine is the C Preprocessor?


Icon Which AI is more human?


Icon Gone Home


Icon Thoughts on Japan


Icon Can Computers Think?


Icon Counting Sheep & Infinity


Icon How Nature Builds Computers


Icon Painkillers


Icon Correct Box Sphere Intersection


Icon Avoiding Shader Conditionals


Icon Writing Portable OpenGL


Icon The Only Cable Car in Ireland


Icon Is the C Preprocessor Turing Complete?


Icon The aesthetics of code


Icon Issues with SDL on iOS and Android


Icon How I learned to stop worrying and love statistics


Icon PyMark


Icon AutoC Tools


Icon Scripting xNormal with Python


Icon Six Myths About Ray Tracing


Icon The Web Giants Will Fall


Icon PyAutoC


Icon The Pirate Song


Icon Dear Esther


Icon Unsharp Anti Aliasing


Icon The First Boy


Icon Parallel programming isn't hard, optimisation is.


Icon Skyrim


Icon Recognizing a language is solving a problem


Icon Could an animal learn to program?




Icon Pure Depth SSAO


Icon Synchronized in Python


Icon 3d Printing


Icon Real Time Graphics is Virtual Reality


Icon Painting Style Renderer


Icon A very hard problem


Icon Indie Development vs Modding


Icon Corange


Icon 3ds Max PLY Exporter


Icon A Case for the Technical Artist


Icon Enums


Icon Scorpions have won evolution


Icon Dirt and Ashes


Icon Lazy Python


Icon Subdivision Modelling


Icon The Owl


Icon Mouse Traps


Icon Updated Art Reel


Icon Tech Reel


Icon Graphics Aren't the Enemy


Icon On Being A Games Artist


Icon The Bluebird


Icon Everything2


Icon Duck Engine


Icon Boarding Preview


Icon Sailing Preview


Icon Exodus Village Flyover


Icon Art Reel




Icon One Cat Just Leads To Another

Unrolling Rotations

Created on March 26, 2024, 7:41 p.m.

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).


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.


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.

github twitter rss