Our Friend, The Quaternion

(or, Why The World Goes To Hell When I Look Up And Down)

(or, Why The Phrase "Euler Angle" Causes Cort To Run Around For Hours With Murder In His Eyes)

Cort Stratton (Mister Programmer, Sir)

Forgive me in advance for the incoherent rant you're about to be subjected to; futzing with Rotators in UnrealScript consumed the best years of my life.

Okay, here goes. We're projecting UnrealTournament onto five screens, forming a 210-degree horizontal field of view. One system is actually connected to the level as a player, and the other four are spectators. We need some way of offseting the view angle of the spectators, so they look off to the left or right at the appropriate angle. Right now, the way we've kludged it up is putting an appropriate yaw offset in the .cfg files of each of the spectator computers, and applying the offset to the current rotation's yaw every frame in the PlayerPawn.CalcPlayerView() function.

A bit of an explanation of how Unreal handles rotations: it represents a rotation using a (Roll, Pitch, Yaw) structure. Each of these three values is an integer variable ranging from 0 to 65536 (though Yaw only ever ranges between 0 and 32768). An excellent description of how Unreal interprets a Rotator can be found here. Basically, the roll pitch and yaw values are applied in sequence. Such rotations are known as "Euler Angles".

Euler Angles Are Evil.

Why are they evil? Well, if you're too lazy to follow any of the above links, I'll lay it out for you: Euler Angle based rotation systems are susceptible to a problem called gimbal lock, whereby two of your degrees of freedom collapse into one following a certain combination of rotations. In Unreal, this happens when the player looks straight up or straight down, at which point rolling and yawing become essentially the same rotation.

Now, that doesn't really sound like it be all that important, so let me go into why Euler angles make life in the Earth Theater a neckbrace-encumbered hell. As I mentioned earlier, the way we have things set up right now is to add a constant offset to the each Spectator's Yaw value. This works, but only if you restrict your rotations to looking left or looking right. Rolling and pitching do not behave as desired, however, because each of the five screens is rotating independently around the "center point". This is best illustrated visually -- I wrote a simple application in OpenGL to simulate the problem we're seeing, as well as demonstrate one proposed solution.

Download the demo here. Fire it up, and you'll see five rectangles representing five "windows" into the Earth Theater world. These windows use an Euler Angle based rotation system, with a constant yaw offset for the four side-facing windows (just like in the Earth Theater). Use the A and D keys to yaw left and right -- you'll notice that the five screens remain perfectly side by side, the way we want. Now use the Q and E keys to adjust the roll value. AAAAAAAACK! that's not what we wanted at all! But if you think about, what it's doing makes perfect sense -- each window is rotating independently around the center point, so when you say roll right, they all roll right. Fortunately, we don't do a lot of rolling in UT, so this problem doesn't come up much.

Finally, use the W and S keys to pitch (look up and down). This is a more subtle problem -- everything looks fine at first, but the more you pitch up or down, the more the windows start to overlap. Again, if you think about it for a moment, this makes sense, once you realize that the windows are rotating independently around the center point. This is why we can't look up and down. And it's all because Unreal's rotation system is based upon FUCKING Euler Angles. Pardon my French.

I think I know how to fix this problem, and the solution involves quaternions. Quaternions are absolutely amazing, they're fabulous, and they just WORK. Every rotation system should be based on quaternions. Mathematically, a quaternion is a four-dimensional vector quantity, which can be used to encode a three-dimensional rotation. This site contains an EXCELLENT description of what quaternions are and how to use them. Read it before you go any further. The important thing to remember, though, is that combining multiple quaternion rotations just magically WORKS, whereas combining multiple Euler Angle rotations (which is, essentially, what we're doing -- combining the original view rotation with our constant yaw offset) almost NEVER works the way you want it to.

So, the solution seems to be something quaternion based. By converting an Unreal rotator into a quaternion, and then concatanting that quaternion with an appropriate yaw offset quaternion, you can come up with a correct rotation for all five screens such that they stay in perfect alignment no matter what rotation you throw at them. To prove that this works, open up the quatdemo program again and hit the M key. The windows turn yellow, and you're now in Quaternion Mode! Use the same commands as before to roll, pitch and yaw to your heart's content -- using quaternion rotations instead of Euler Angles to apply our yaw offset actually works!

Well, it works in OpenGL and C, anyway. Unfortunately, the situation in UnrealScript is slightly more difficult than C. I ported all my quaternion code to UnrealScript, tested it extensively, and you'll have to trust me when I say that it works -- it produces the correct quaternion rotation for each screen every frame. I ran my UnrealScript quaternion code and my OpenGL/C code on the same input rotation (and just to be safe, I did the calculations by hand as well). All three methods came up with the same answer. However, in C I could just convert the quaternion back to a rotation matrix, pass the matrix to glMultMatrix(), and I'm done. In Unreal, though, I have to convert my lovely quaternion back into an Euler Angle...and therein lies my problem. I've tried several different methods for converting quaternions to Euler Angles, and none of them seem to work (the resulting roll/pitch/yaw is nowhere near the same rotation as the quaternion describes).

My latest theory makes use of an UnrealScript function called OrthoRotation, which takes as input three vectors corresponding to the X, Y and Z axes of a rotated coordinate system, and returns the UnrealScript Rotator (Euler Angle) corresponding to that rotation. Here's a rundown of exactly what I'm doing to arrive at the final rotation:

  1. convert the UnrealScript Rotator (an Euler Angle) to the equivilant quaternion.
  2. concatenate the result of step 1 with the quaternion representing our yaw offset rotation.
  3. convert the quaternion result of step 2 to a 4x4 rotation matrix.
  4. manually use the rotation matrix to transform three unit vectors along the X, Y and Z axes. This essentially rotates the default coordinate axes by the rotation stored in the quaternion.
  5. Pass these three vectors to Unreal's OrthoRotation() function, and get back an equivilant rotation in UnrealScript Rotator format.

For debugging, I'm using a yaw offset of zero, so I can basically skip step 2. And then, assuming that all goes well, the final output rotation should be identical to the input rotation.

Now, let us peel back the foreskin of Assumption, and apply the harsh steel-wool brush of Reality. The output rotation does NOT equal the input rotation, even with a yaw offset of zero. My world is a crotch!

The problem seems to be this...I figured, hey, if two rotations are equivilant, then when you call GetAxes() on them they should return the same X/Y/Z-axes. After proving that my code was generating the correctly rotated X/Y/Z axes for the input rotation, I tried calling GetAxes() on the input rotation directly, to compare. And sure enough, the results weren't the same. What I don't understand here is that the axes I got from calling GetAxes() on the input rotation aren't even at right angles to each other! They're all unit length, yes, but only two of the axes are ever at 90 degrees to each other (and it's not even consistent WHICH axis is the odd man out).

So, I'm at a loss here. Out of time, out of ideas. I present for posterity the (non-functional) quaternion code I mentioned earlier, in the form of an overloaded PlayerPawn class called QuatPlayer. It's all quite heavily commented, it just doesn't work. If you decide to stick with the quaternion approach, though, this would be a great place to start. Download QuatPlayer.uc here

Suggestions for Future Work


Last modified: Wed Oct 17 02:07:50 Eastern Daylight Time 2001