 | Lesson: Quaternion Camera Class |
 | Hi! My name if Vic Hollis and I've been coming to NeHe for a couple of years now. And I think its time I finally gave something back. Lately
I have been studying Quaternions for doing rotations and to be entirely honest I still don't understand them quite as well as I should. Since
I have started using them I can tell you that using Quaternions for 3D rotations and finding positions in your scene can really make things
easier. Of course you don't have to use Quaternions for this sort of thing you can always deal with matrices and vector math. And by using
Quaternions you will still have to deal with them somewhat. In this Lesson I'm going to create a simple class to represent a Quaternion and
a Camera. Anyone who is interested in these classes is welcome to use them in your source. I will not be discussing the math behind the
Quaternion so I am going to avoid that subject all together. There is no shortage of information out there about the mathematics behind
Quaternions. What I am mostly interested in as a developer (and I bet most everyone else reading this) is getting results. In this lesson
I will create a height map that we can use for some terrain so we will have something to fly around and I will make a Camera class that uses
Quaternions so we can set our perspective on things and conviently get World coordinates to translate to based on orientation and speed. The
model that I will be using for flying is a Wing Commander style of flying that you find in most space sims. I'll leave it to you guys to find
other ways to fly around your scene but after this lesson that shouldn't be to much of a problem for you with a little bit of work.
A Quaternion is really an odd thing to think about. After months of trying to visually understand them I just gave up and accepted them as
something that just was. That leap of faith actually help me understand them all the better believe it or not and I would recommend anyone
struggling with them to do the same. I'm sure some of you have heard of the effects of gimbal lock. Well its something that happens when your
start multiplying lots of rotations together. Quaternions give us a way around this and thats why they are so useful. Well I think I have
rambled enough below is the function we will be using the most from the Quaternion class.
|  |
void glQuaternion::CreateFromAxisAngle(GLfloat x, GLfloat y, GLfloat z, GLfloat degrees)
{
// First we want to convert the degrees to radians
// since the angle is assumed to be in radians
GLfloat angle = GLfloat((degrees / 180.0f) * PI);
// Here we calculate the sin( theta / 2) once for optimization
GLfloat result = (GLfloat)sin( angle / 2.0f );
// Calcualte the w value by cos( theta / 2 )
m_w = (GLfloat)cos( angle / 2.0f );
// Calculate the x, y and z of the quaternion
m_x = GLfloat(x * result);
m_y = GLfloat(y * result);
m_z = GLfloat(z * result);
}
 |
You can think of this function just like you would a call to glRotatef(). All the parameters are the same with the exception of the order.
Also you will see later in the lesson that you can use them interchangable in OpenGL. As you can see from the above the only member varibles
we need to represent a Quaternion are an x, y, z, and w coordinates. The x, y, and z coordinates are the axis of the rotation and the degrees
is the number of degrees you want to rotate around that axis. Below is the second most usefull function in the class CreateMatrix()
|  |
void glQuaternion::CreateMatrix(GLfloat *pMatrix)
{
// Make sure the matrix has allocated memory to store the rotation data
if(!pMatrix) return;
// First row
pMatrix[ 0] = 1.0f - 2.0f * ( m_y * m_y + m_z * m_z );
pMatrix[ 1] = 2.0f * (m_x * m_y + m_z * m_w);
pMatrix[ 2] = 2.0f * (m_x * m_z - m_y * m_w);
pMatrix[ 3] = 0.0f;
// Second row
pMatrix[ 4] = 2.0f * ( m_x * m_y - m_z * m_w );
pMatrix[ 5] = 1.0f - 2.0f * ( m_x * m_x + m_z * m_z );
pMatrix[ 6] = 2.0f * (m_z * m_y + m_x * m_w );
pMatrix[ 7] = 0.0f;
// Third row
pMatrix[ 8] = 2.0f * ( m_x * m_z + m_y * m_w );
pMatrix[ 9] = 2.0f * ( m_y * m_z - m_x * m_w );
pMatrix[10] = 1.0f - 2.0f * ( m_x * m_x + m_y * m_y );
pMatrix[11] = 0.0f;
// Fourth row
pMatrix[12] = 0;
pMatrix[13] = 0;
pMatrix[14] = 0;
pMatrix[15] = 1.0f;
// Now pMatrix[] is a 4x4 homogeneous matrix that can be applied to an OpenGL Matrix
}
 |
This function, as its name implies, Creates a 4x4 homogeneous matrix that can be sent to glMultMatrixf(). That means that you will never
have to use glRotatef() directly when using this class to represent your rotations. You must first call the CreateFromAxisAngle() if you
want this function to give you a valid matrix to use with glMultMatrixf(). The next function is an operator and that is the multiplication
operator. This class is not going to do us much good unless we can combine rotations. In order to do that we need the ability to multiply
Quaternions.
|  |
glQuaternion glQuaternion::operator *(glQuaternion q)
{
glQuaternion r;
r.m_w = m_w*q.m_w - m_x*q.m_x - m_y*q.m_y - m_z*q.m_z;
r.m_x = m_w*q.m_x + m_x*q.m_w + m_y*q.m_z - m_z*q.m_y;
r.m_y = m_w*q.m_y + m_y*q.m_w + m_z*q.m_x - m_x*q.m_z;
r.m_z = m_w*q.m_z + m_z*q.m_w + m_x*q.m_y - m_y*q.m_x;
return(r);
}
 |
That is all there is too multiplying Quaternions. One thing to note about multiplication with Quaternions is the results are not
communitive. Which basically means that Quaternion a * Quaternion b does not equal Quaternion b * Quaternion a. The same applies with
matrices as well. This will actually come into play later in the tutorial when we find our World coordinates to translate to. Thats all
we need for the Quaternion class. Next up is the very basic Camera class.
|  |
void glCamera::SetPrespective()
{
GLfloat Matrix[16];
glQuaternion q;
// Make the Quaternions that will represent our rotations
m_qPitch.CreateFromAxisAngle(1.0f, 0.0f, 0.0f, m_PitchDegrees);
m_qHeading.CreateFromAxisAngle(0.0f, 1.0f, 0.0f, m_HeadingDegrees);
// Combine the pitch and heading rotations and store the results in q
q = m_qPitch * m_qHeading;
q.CreateMatrix(Matrix);
// Let OpenGL set our new prespective on the world!
glMultMatrixf(Matrix);
// Create a matrix from the pitch Quaternion and get the j vector
// for our direction.
m_qPitch.CreateMatrix(Matrix);
m_DirectionVector.j = Matrix[9];
// Combine the heading and pitch rotations and make a matrix to get
// the i and j vectors for our direction.
q = m_qHeading * m_qPitch;
q.CreateMatrix(Matrix);
m_DirectionVector.i = Matrix[8];
m_DirectionVector.k = Matrix[10];
// Scale the direction by our speed.
m_DirectionVector *= m_ForwardVelocity;
// Increment our position by the vector
m_Position.x += m_DirectionVector.i;
m_Position.y += m_DirectionVector.j;
m_Position.z += m_DirectionVector.k;
// Translate to our new position.
glTranslatef(-m_Position.x, -m_Position.y, m_Position.z);
}
 |
Ok this part of the code needs a bit of explaining. First off we need to declare an array of floats because OpenGL represents matrices as
a 1 dimensional array. We also need a temporary Quaternion 'q' to store the results of our Quaternion multiplication. What we are doing here
is defining 2 Quaternions to represent rotations about the X and Y axises. Then we multiply these two Quaternions together to get our
Orientation in the scene (IE which direction we are facing). The next thing we need is a direction vector based on our Orientation so that
we can translate to a position in the scene. It just so happens that the matrix we created based on the m_Pitch contains a value that we
can use for the 'j' vector coordinate of our direction vector. An interesting thing to note here is that the 3rd row of our matrix (elements
8, 9, 10) will alwasy contain translation coordinates! So we don't have to use computations to re-figure our direction vector. Remember when
I said that multiplication is not communative with Quaternions? Well here we are using that to our advantage to get our 'i' and 'k'
coordinates for our vector. We multiply the m_Heading * m_Pitch which will give us a different Matrix. The 'i' and 'k' coordinates are
stored in this matrix and since we are using unit length Quaternions to represent our rotations then the values in the 3rd row of the Matrix
can be used as unit length vectors. Now that we have a vector we can scale that vector by our speed and add that back to the position. Now
all thats left is to translate to that position. Now bear in mind that this function models the Wing Commander style of flying around. It
will not work for a Microsoft Flight Sim flying style. Next up is the InitGL() function.
|  |
// Try to load our height map
if(!hMap.LoadRawFile("Art/Terrain1.raw", MAP_SIZE * MAP_SIZE))
{
MessageBox(NULL, "Failed to load Terrain1.raw.", "Error", MB_OK);
}
// Try to load our texture for the height map
if(!hMap.LoadTexture("Art/Dirt1.bmp"))
{
MessageBox(NULL, "Failed to load terrain texture.", "Error", MB_OK);
}
// Now set up our max values for the camera
Cam.m_MaxForwardVelocity = 5.0f;
Cam.m_MaxPitchRate = 5.0f;
Cam.m_MaxHeadingRate = 5.0f;
Cam.m_PitchDegrees = 0.0f;
Cam.m_HeadingDegrees = 0.0f;
 |
The only difference between our Init function and the NeHe base code init is I added the above code. All it does is load a raw file created
to generate the height map and then loads a texture for the terrain. Oh yea we are also setting the Cameras maximum allowed velocity, pitch,
and heading. Lets take a look at keyboard handler below.
|  |
void CheckKeys(void)
{
if(keys[VK_UP])
{
Cam.ChangePitch(5.0f);
}
if(keys[VK_DOWN])
{
Cam.ChangePitch(-5.0f);
}
if(keys[VK_LEFT])
{
Cam.ChangeHeading(-5.0f);
}
if(keys[VK_RIGHT])
{
Cam.ChangeHeading(5.0f);
}
if(keys['W'] == TRUE)
{
Cam.ChangeVelocity(0.1f);
}
if(keys['S'] == TRUE)
{
Cam.ChangeVelocity(-0.1f);
}
}
 |
All we need to do here is check for a key press and then take an appropriate action like change the camera heading. The Camera class has
the following member functions to do this for you: ChangeVelocity(), ChangeHeading(), and ChangePitch(). These guys handle all the details
of changing degrees we want to rotate by. We need these because we have to do a little checking for cases like if we are upside down or not
when rotating. I will not be adding the code here because its pretty basic. Next is the mouse handler.
|  |
void CheckMouse(void)
{
GLfloat DeltaMouse;
POINT pt;
GetCursorPos(&pt);
MouseX = pt.x;
MouseY = pt.y;
if(MouseX < CenterX)
{
DeltaMouse = GLfloat(CenterX - MouseX);
Cam.ChangeHeading(-0.2f * DeltaMouse);
}
else if(MouseX > CenterX)
{
DeltaMouse = GLfloat(MouseX - CenterX);
Cam.ChangeHeading(0.2f * DeltaMouse);
}
if(MouseY < CenterY)
{
DeltaMouse = GLfloat(CenterY - MouseY);
Cam.ChangePitch(-0.2f * DeltaMouse);
}
else if(MouseY > CenterY)
{
DeltaMouse = GLfloat(MouseY - CenterY);
Cam.ChangePitch(0.2f * DeltaMouse);
}
MouseX = CenterX;
MouseY = CenterY;
SetCursorPos(CenterX, CenterY);
}
 |
This function basically does the same as the keyboard handler with the exception of DeltaMouse. DeltaMouse just holds the change of Mouse
coordinates. If you move the mouse fast then DeltaMouse will be larger. If you were to move the mouse slowly then DeltaMouse would be small.
This will allow us to make a nice smooth transition when rotating instead of very jerky movements as with the keyboard handler. Finally the
only thing that remains is the DrawGLScene().
|  |
int DrawGLScene(GLvoid) // Here's Where We Do All The Drawing
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear Screen And Depth Buffer
glLoadIdentity(); // Reset The Current Modelview Matrix
Cam.SetPrespective();
// Lets make the height map really big since we move so fast.
glScalef(hMap.m_ScaleValue, hMap.m_ScaleValue * HEIGHT_RATIO, hMap.m_ScaleValue);
hMap.DrawHeightMap();
CheckKeys();
CheckMouse();
return TRUE; // Everything Went OK
}
 |
Here we load the identity matrix and set our prespective on things by way of the camera class we made. Next scale and draw the height map.
Check the mouse and keyboard for changes and that is it. There are a few other things worth mentioning here about Quaternions and OpenGL
in general. I could have written this entire lesson with only glRotatef() to do my rotations and glGetFloatv() to get the model view matrix
for my direction vector. So whats the point of using Quaternions? Well you don't really need too, at least not for something like this. The
only reason I wrote this lesson is to illustrate that single point. I think its a common misconception among people that you need really
high tech math classes in order to get something like a flight sim of some kind up and running. OpenGL keeps track of most of the
information for you so there is really no need to put extra computations for this sort of thing in your code it will only slow things down.
I've seen lots of code that does all sorts of crazy vector math to do this stuff as well as matrices and what not. I believed at one time
that Quaternions would allow me to do all this stuff if I could just understand them. After finally understanding how Quaternions worked it
only served to show me that I never needed them in the first place. Don't get me wrong. I'm not saying Quaternions are useless or anything.
They do have their uses I am sure there are instances where you might want to use them and now you can. I just wanted to point out that you
do not really need them to fly around your scene and that just plain old OpenGL code would suffice. If you were to change the SetPrespective
function in the glCamera class to the code below you would get the exact same result as you would if you were using the above SetPrespective
function using Quaternions, but you might be wondering about the gimbal lock that I mentioned earlier. Well to put it plainly it will never
happen in this code! The reason we don't have to worry about the nasty effects of gimbal lock with the below function is because we are
loading the identity matrix every time we draw the scene in our DrawGLScene() function like all good GL programmers are supposed to do.
Gimbal lock will only reer its ugly head when you are combing several rotations and you do not load the Identity matrix every time you load
your scene.
|  |
void glCamera::SetPrespective()
{
GLfloat Matrix[16];
glRotatef(m_HeadingDegrees, 0.0f, 1.0f, 0.0f);
glRotatef(m_PitchDegrees, 1.0f, 0.0f, 0.0f);
glGetFloatv(GL_MODELVIEW_MATRIX, Matrix);
m_DirectionVector.i = Matrix[8];
m_DirectionVector.k = Matrix[10];
glLoadIdentity();
glRotatef(m_PitchDegrees, 1.0f, 0.0f, 0.0f);
glGetFloatv(GL_MODELVIEW_MATRIX, Matrix);
m_DirectionVector.j = Matrix[9];
glRotatef(m_HeadingDegrees, 0.0f, 1.0f, 0.0f);
// Scale the direction by our speed.
m_DirectionVector *= m_ForwardVelocity;
// Increment our position by the vector
m_Position.x += m_DirectionVector.i;
m_Position.y += m_DirectionVector.j;
m_Position.z += m_DirectionVector.k;
// Translate to our new position.
glTranslatef(-m_Position.x, -m_Position.y, m_Position.z);
}
 |
I hope you guys can use this and I also hope this helps some of you guys out with your own projects. Just want to thank Jeff for making an
awsome site where information is not hard to come by. Also I need to thank DigiBen at
http://www.gametutorials.com for his tutorial on Quaternions. I had to learn this
stuff from somewhere :)
- Cheers Vic
* DOWNLOAD Visual C++ Code For This Lesson.
* DOWNLOAD Borland C++ Builder 6 Code For This Lesson. ( Conversion by Dominique Louis )
* DOWNLOAD Code Warrior 5.3 Code For This Lesson. ( Conversion by Scott Lupton )
* DOWNLOAD Dev C++ Code For This Lesson. ( Conversion by Mike )
* DOWNLOAD Linux Code For This Lesson. ( Conversion by Kimmo Karlsson )
|  |