Posted by: Entropy
Date posted: May 18 2003 User Rating: 5 out of 5.0  Number of views: 7927 Number of comments: 5  Description: Changing view angles with quaternions 
The goal of this tutorial is to show you how to modify the HalfLife input code to allow changing the player's view angles around any arbitrary axis by using quaternions to change the view angles. These changes will allow you to create "flightsimulator" type controls (that is, the view rotates in "the same" way no matter what your current angles are, rather than the standard mechanism in which yaw changes always cause rotations around the z (vertical) axis). The uses are not limited to flight simulation, however. This technique will also permit such things as wallwalking or any other nonstandard orientation you can dream up.
I'm not going to discuss the mathematics of rotations in general or quaternions in particular, since there are better explanations already out there than I could produce. Discussions of those topics can be found in such places as Gamedev, Mathworld, and other places around the internet (Google is your friend).
The basic idea here is to take the control inputs which normally directly change the pitch, yaw or roll and instead define our own axes around which to rotate the view. For this example, the axes we'll use are the axes defined by the current view angles, which will result in "flightsimlike" behavior. The first order of business is to change the input code to remove all the direct references to the pitch, yaw and roll, and save all those changes to apply later. To do that, we'll add an extra parameter, deltaAngles, to some of the input functions to hold the input until we're ready to use it. Open input.cpp and change the prototype for IN_Move (line 50) to this:
  void IN_Move ( float frametime, usercmd_t *cmd, float * deltaAngles); 
Next, find the function CL_AdjustAngles. Remove this line:
  viewangles[YAW] = anglemod(viewangles[YAW]); 
and these:
  if (viewangles[PITCH] > cl_pitchdown>value) viewangles[PITCH] = cl_pitchdown>value; if (viewangles[PITCH] < cl_pitchup>value) viewangles[PITCH] = cl_pitchup>value;
if (viewangles[ROLL] > 50) viewangles[ROLL] = 50; if (viewangles[ROLL] < 50) viewangles[ROLL] = 50;

Because we're going to accumulate all the different control inputs that affect the angles, we won't want the angles to "wrap around" (from 360 to 0 degrees, for example) until all the forms of input have had their "say". The anglemod call keeps the yaw in the 0360 range, which is not what we want. The other lines limit pitch and roll, which is also not what we want to do at this point. Next, in CL_CreateMove, add these lines at the beginning of the function:
  vec3_t deltaAngles; gEngfuncs.GetViewAngles( (float *)viewangles ); 
and replace this section:
  //memset( viewangles, 0, sizeof( vec3_t ) ); //viewangles[ 0 ] = viewangles[ 1 ] = viewangles[ 2 ] = 0.0; gEngfuncs.GetViewAngles( (float *)viewangles );
CL_AdjustAngles ( frametime, viewangles );

with this:
  deltaAngles[ 0 ] = deltaAngles[ 1 ] = deltaAngles[ 2 ] = 0.0; // Get change in angles due to keyboard input CL_AdjustAngles ( frametime, deltaAngles );

Also delete the call to gEngfuncs.SetViewAngles that you see just below this. Our goal here is to keep the view angles separate from the input changes in the angles (which we'll store in deltaAngles). CL_AdjustAngles processes keyboard input that affects view angles. Rather than passing it the actual viewangles, deltaAngles is passed in, so the function ends up giving us the changes caused by keyboard commands. Next, change the call to IN_Move (around line 726) to add deltaAngles so that it will be available to the mouse and joystick code:
  IN_Move ( frametime, cmd, deltaAngles );

Scroll down until you find   gEngfuncs.GetViewAngles( (float *)viewangles );  (around line 756) and delete that line (we only want to fetch the view angles once  at the beginning of the function). In its place, put this section:
  float angles[3]; VectorCopy(viewangles, angles); QuaternionRotate(angles, deltaAngles, viewangles); if (viewangles[PITCH] > 180) viewangles[PITCH] = 360; if (viewangles[PITCH] < 180) viewangles[PITCH] += 360; if (viewangles[ROLL] > 180) viewangles[ROLL] = 360; if (viewangles[ROLL] < 180) viewangles[ROLL] += 360;

also add the prototype for QuaternionRotate at the beginning of input.cpp:
  extern "C" void QuaternionRotate(float anglesIn[3], float deltaAngles[3], float anglesOut[3]);

The QuaternionRotate function takes the input (deltaAngles) and applies it to the view angles (angles), returning the result in viewangles  more on this later. The resulting view angles are then bounded, and at the end of the function we pass the view angles back to the engine with this as the last line in the function:
  gEngfuncs.SetViewAngles( (float *)viewangles );

The rest of the input code changes will be in inputw32.cpp. These changes will basically do the same thing as those above  changing references to 'viewangles' to use 'deltaAngles' and removing any code that limits the angle changes. Find the function IN_MouseMove (line 300) and change the first line to:
  void IN_MouseMove ( float frametime, usercmd_t *cmd, float * deltaAngles)

then delete these lines:
  vec3_t viewangles; gEngfuncs.GetViewAngles( (float *)viewangles );

Replace 'viewangles' in these lines (lines 353 & 357) with 'deltaAngles':
  viewangles[YAW] = m_yaw>value * mouse_x; ... viewangles[PITCH] += m_pitch>value * mouse_y;

...and delete these lines:
  if (viewangles[PITCH] > cl_pitchdown>value) viewangles[PITCH] = cl_pitchdown>value; if (viewangles[PITCH] < cl_pitchup>value) viewangles[PITCH] = cl_pitchup>value;

Further down, delete this line:
  gEngfuncs.SetViewAngles( (float *)viewangles );

Now find the function IN_JoyMove and add deltaAngles:
  void IN_JoyMove ( float frametime, usercmd_t *cmd, float * deltaAngles )

and delete these lines:
  vec3_t viewangles;
gEngfuncs.GetViewAngles( (float *)viewangles );

Change the references to 'viewangles' in the rest of the function to 'deltaAngles'  there are 6, on lines 788, 792, 841, 845, 860, and 864. Finally, delete this section:
  // bounds check pitch if (viewangles[PITCH] > cl_pitchdown>value) viewangles[PITCH] = cl_pitchdown>value; if (viewangles[PITCH] < cl_pitchup>value) viewangles[PITCH] = cl_pitchup>value;
gEngfuncs.SetViewAngles( (float *)viewangles );

Now change the IN_Move function to:
  void IN_Move ( float frametime, usercmd_t *cmd, float * deltaAngles) { if ( !iMouseInUse && mouseactive ) { IN_MouseMove ( frametime, cmd, deltaAngles); }
IN_JoyMove ( frametime, cmd, deltaAngles); }

OK, we've revamped the input system to cause all processing of the viewangle input to go through our function 'QuaternionRotate'. It is this function that really demonstrates the power of using quaternions. You should add quaternion.c and quaternion.h to your common folder, and add quaternion.c to the client project. These two files are my implementation of quaternions. You should at least look these files over and try to understand where the equations come from  the Matrix and Quaternion FAQ is quite helpful. There are also functions in these files which are not used in this tutorial, but which you might find useful in writing your own code which uses quaternions. Here is the QuaternionRotate function, which does most of the work to make the viewangles change:
  void QuaternionRotate(const float * anglesIn, const float * deltaAngles, float * anglesOut) { static quaternion_t qIn, qYaw, qPitch, qRoll, qDelta, qResult, qTemp; vec3_t forward, right, up;
// Convert the input angles to quaternion notation and find the axes EulerToQuaternion(anglesIn, &qIn); AngleVectors(anglesIn, forward, right, up);
// Calculate the additional rotations from the delta angles and the axes QuaternionFromAxisAndAngle(up, deltaAngles[YAW], &qYaw); QuaternionFromAxisAndAngle(right, deltaAngles[PITCH], &qPitch); QuaternionFromAxisAndAngle(forward, deltaAngles[ROLL], &qRoll);
// Get the total additional rotation QuaternionMultiply(qPitch, qYaw, &qTemp); QuaternionMultiply(qTemp, qRoll, &qDelta);
// Calculate the new angles QuaternionMultiply(qDelta, qIn, &qResult); QuaternionToEuler(qResult, anglesOut); }

The first step in this function is to convert the view angles passed from CL_CreateMove as anglesIn (Euler angles) into a quaternion with a call to EulerToQuaternion. Then vectors representing the player's local axes (forward, right and up) are calculated. These axes are then used with deltaAngles to calculate quaternions representing the "pitch", "yaw" and "roll" rotations around the player's axes (as opposed to the absolute axes of the world). Multiplying all the individual quaternions yields a quaternion for the desired change in the view angles, qDelta. Multiplying this by the quaternion for the original view angles (qIn) yields a quaternion representing the final rotation (qResult), which is then converted back to Euler angles for use by the rest of the game. We're not restricted to using the player's current view angles to determine the axes around which to rotate the view. Any axis can be used. For example, the normal vector of a wall (or other plane) could be used as the axis for "yaw" rotations and axes at right angles to the normal for "pitch" and "roll"  so that the controls would behave just like the standard controls, except that the player would seem to be walking on a wall. Or, by using a combination of the player's view angles and a fixed "up" vector of (0, 0, 1) you'd get something like the standard HalfLife controls. The possibilities are endless.
[edit] Note from omega: Entropy is hereby known as "God of the Evil Angles" [/edit] 

User Comments
Showing comments 15
great tutorial, very clever!
the links to the files quaternion.c and quaternion.h are broken tho 

me too!
i mean.. they're working now :) 

broken again :( anyone have these files? 

If you're modding HL2 now, take a look at /public/vector.h It contains a Quaternion class! And if you're doing HL1.. just google for the files. 

You must register to post a comment. If you have already registered, you must login.

297 Approved Articless
8 Pending Articles
3940 Registered Members
0 People Online (8 guests)

