Euler Angles are Evil.

By Steve Baker

Introduction

Most people's first attempt at writing a camera/player movement routine do this kind of thing:

  while ( 1 )
  {
    ...read some angular velocities from a joystick or something ;
    ...add the angular velocities to the current angles ;
    ...convert current angles into a matrix ;
    ...invert the matrix ;
    ...put it onto the GL_MODELVIEW stack ;
 
    ...draw the scene ;
  }

This doesn't work - or at least, it seems to work - but only for heading changes - and perhaps small changes in pitch and roll. You have just discovered that - and that's why you are reading this page - right?

Why doesn't it work?

Well, when you convert those three angles into a matrix (or when you use three glRotate commands to do it for you), the routine that build that matrix has to make some assumption about the ORDER of those three rotations.

Imagine an aeroplane. The three rotations are called 'Heading', 'Pitch' and 'Roll'. Heading is sometimes called 'Yaw' - but I hate that choice since the abbreviation is 'Y' and that gets confused with the Y axis - so I'll stick with 'Heading'.

...well, that's what you'd like them to do. Let's also define our rotations to be positive in the anticlockwise direction for the sake of argument - and let's assume that zero rotations correspond to flying straight and level with the nose pointing due North.

(When you use three angles to represent a rotation, this is often referred to as 'Euler Angle Representation' or just 'Eulers' for short. I usually write an euler as (H,P,R).)

In fact, the problem is that when you compose these rotations, you MUST do so in some particular order - you can choose what that order is - but your mathematics will always force you to make a choice. That is to say, you might choose to perform Roll first, then Pitch and then Heading. Let's look at what can happen with this sequence of operations - and to keep it simple, let's consider just roll and pitch:

(If you are right-handed, you'll find this discussion much easier to follow if you are holding a paper plane. Fold one now! Left handed people usually have better spatial reasoning skills and can grok this using their brains alone :-)

  1. The plane (with no rotations) is straight and level and pointing North.
  2. Let's give it a Heading/Pitch/Roll of (0,90,90) and see what happens:
  3. The plane now has 90 degrees of roll (so the right wingtip points at the sky, and the left points at the ground).
  4. ...and 90 degrees of pitch (which means that the nose pitched UP)...so the nose points West and the tail is off to the East, the right wingtip still points at the sky, and the left still points at the ground.
  5. Now, the pilot attempts to roll the plane back to level by pushing his joystick to the right - which with the code above will progressively subtract roll angle until it's zero again. You might expect this to result in the nose pointing to the West still - but now flying nice and level...
  6. So at the end of this manouver, the HPR angle went from (0,90,90) to (0,90,0) - where is the plane actually pointing?
  7. Let's go back to zero rotations (straight and level - pointing north) and see where (0,90,0) puts us:
  8. Well, zero roll puts the wings nice and level - left wing to the west and right wing to the east...and 90 pitch points the nose up to the sky!
So undoing the roll resulted in us flying vertically up instead of level and to the west. From the point of view of the pilot, who was in a steep bank and flying west, moving the joystick to the right to roll back level, he will get the impression that the plane actually changed heading as it moved from flying due west to pointing straight up - since the nose moved to his right and the tail to his left and that's our definition of heading! Attempting to level out the plane - whilst still flying due west using either heading change (the rudder pedals) or the pitch axis will also fail!

This class of effect is often termed 'gimbal lock' - after an effect that happens with real-world mechanisms that have three single-axis rotational joints. When two of those joints end up parallel to each other, you have lost one degree of freedom in the system. I have a car spark-plug wrench that is flexible enough to reach into tight corners - but which relies on this effect to allow you to actually turn the spark plug.

Add in the third rotation angle and matters can get seriously out of hand.

How do we fix this?

But you knew all that...What do we do to fix it?

We need to understand what went wrong. The problem is that the initial roll happened BEFORE the pitch, but the 'unroll' at the end is something that you hoped would operate on the results of the first roll+pitch - not simply an undoing of the initial roll which is what the naive code at the top of this document actually does.

Well, it's tempting to modify our main loop as follows:


  ...convert initial position into a 'position' matrix ;

  while ( 1 )
  {
    ...read some angular velocities from a joystick or something ;
    ...convert angular velocities into a 'velocity' matrix ;
    ...post-multiply the position matrix by the velocity matrix
    ...invert the position matrix ;
    ...put the result onto the GL_MODELVIEW stack ;
 
    ...draw the scene ;
  }

...this certainly seems to work - and mathematically, it's a correct implementation. Since we preserve the 'current' rotation as a matrix, any concept of the 'order' of operations prior to the formation of the matrix has been forgotten - and providing the angular velocities remain relatively small (which is usually the case), this works well in theory.

Annoyingly though, there is a practical problem with this approach and that is that there is roundoff error in the system.

When you store rotation as Euler angles, there can be tiny amounts of roundoff error - if you change heading by 0.1 degrees for 3600 frames, the resulting net heading won't quite be zero. However, to a player steering a character with the joystick, this error isn't noticable - and is naturally corrected as a result of human input.

Unfortunately, a 3x3 or 4x4 matrix can represent a lot more operations than just rotation. By choosing the right set of numbers, a matrix can represent a shear or a stretch or something. What happens when you repeatedly multiply rotation matrices is that the matrix gradually starts to shear and stretch - after tens of minutes of action, you'll start to see the scene distort visibly. Since the user has no control over this, he has no way to unconciously correct for it - and we are screwed.

There are mechanisms to 'renormalize' a matrix. If you read my companion document "Matrices can be your Friends" then you'll realise that you can normalize the three rows of the matrix and jiggle them such that they are mutually perpendicular - and you'll have a 'pure' rotation matrix once more. Do that operation ever frame - or perhaps every few frames and nobody will notice any sudden 'correction' of the matrix.

That works acceptably well - but I observe that many people find it hard to work with a system in which the current rotation is known only as a matrix. If you wanted to implement a compass for example - it would be nice to know the players heading. That information isn't 'obviously' available in the matrix. Hence, many people conver the matrix back into euler angles:


  ...convert initial position into a 'position' matrix ;

  while ( 1 )
  {
    ...read some angular velocities from a joystick or something ;
    ...convert angular velocities into a 'velocity' matrix ;
    ...post-multiply the position matrix by the velocity matrix ;

    ...renormalize the current position matrix ; 
    ...convert the current position matrix into Euler angles ;

    ...invert the position matrix ;
    ...put the result onto the GL_MODELVIEW stack ;
 
    ...draw the scene ;
  }

If you are going to do that - then you can do this instead:


  while ( 1 )
  {
    ...read some angular velocities from a joystick or something ;
    ...convert angular velocities into a 'velocity' matrix ;
    ...convert current position Eulers into a 'position' matrix ;
    ...post-multiply the position matrix by the velocity matrix ;
    ...convert the current position matrix back into Euler angles ;

    ...invert the position matrix ;
    ...put the result onto the GL_MODELVIEW stack ;
 
    ...draw the scene ;
  }

This saves you the messy matrix renormalization step since the matrix is regenerated from angles each frame. However, care has to be taken since the step of converting a matrix back into Euler angles results in some 'unknown' values at some orientations. If the eyepoint it pitched by 90 degrees - then roll and heading do the same thing. Hence, although roll+heading==constant, the split between the two is ill-defined and could change dramatically from one frame to the next. If you use heading alone to generate (say) a compass reading then expect it to go crazy at certain pitch or roll angles. That happens in the real world too - so don't feel too bad about it!

Quaternions.

In addition to Euler Angler representations and the Matrix representation, there is a third useful way to represent rotation. Quaternions (as their name implies) are sets of four numbers that can be thought of as being an (X,Y,Z) vector of length 1.0 - and an angle to rotate about that vector. A combination of moving the vector and changing the angle can encode any possible rotation.

This is the same idea as the four parameters of the glRotate command (although the four numbers in a Quaternion are scaled differently for reasons of mathematical convenience).

I don't want to go into quaternions in any detail here, but they are pretty cool since (like matrices) they encode rotation in an order-independent manner - yet (like Euler angles) can ONLY encode rotation - and are hence much less susceptable to ugly roundoff error issues (although you do have to ensure the length of the vector part stays nailed at 1.0 at all times - that's a much simpler idea than renormalizing a matrix.