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?
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'.
(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 :-)
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.
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!
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.