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 RAGE

 

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 LOL I DREW THIS DRAGON

 

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


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.

github twitter rss