If you find this article contains errors or problems rendering it unreadable (missing images or files, mangled code, improper text formatting, etc) please contact the editor so corrections can be made. Thank you for helping us improve this resource |
So, what is a Quaternion?
Quaternions aren't actually as scary as they sound. Everything I read regarding quaternions talked about imaginary numbers, hyper-complex numbers, spinors, and other scary sounding things. There was too much maths jargon for my liking. If you understand what a vector is, it isn't very hard to understand what a quaternion is. One way to represent a quaternion is
Q = xi + yj + zk + w
where i,j & k are coordinate basis vectors for three dimensions.
I don't particularly like this representation for computer graphics, especially where cameras are concerned. I prefer thinking of a quaternion as an object that contains a vector and a scalar. We'll call the vector v and keep the scalar as w.
Q = [ w, v ]
where v = xi + yj + zk.
For use in the sample code below, here's a quaternion data structure:
struct quaternion { double x, y, z, w; };
This is a much easier representation to comprehend for me. Now for our purposes, quaternion addition, subtraction, etc., aren't needed. We only need to know how to normalize (scale to length=1), multiply and compute the conjugate of a quaternion in order to generate a rotation. These tasks are actually very simple and are described below.
Normalizing a quaternion
Normalizing a quaternion is almost the same as normalizing a vector. We start by computing the length of the quaternion to be normalized, which is done using the Euclidean distance forumula:
|Q| = sqrt( w^2 + x^2 + y^2 + z^2)
A function to implement this formula might look like this:
double length(quaternion quat) { return sqrt(quat.x * quat.x + quat.y * quat.y + quat.z * quat.z + quat.w * quat.w); }
Not too hard is it? Now to get the normalized vector Q, which we'll call Q*, we just divide every element of Q by |Q|.
Q* = Q/|Q| = [w/|Q|, v/|Q|]
Here's some sample code for this function:
quaternion normalize(quaternion quat) { double L = length(quat); quat.x /= L; quat.y /= L; quat.z /= L; quat.w /= L; return quat; }
This will give us a quaternion of length 1 (which is very important for rotations). Still nothing scary right? Well, it doesn't really get much harder than this.
The conjugate of a quaternion
Let's compute the conjugate of a quaternion and call it Q'. The conjugate is simply
Q' = [ w, -v ]
Here's the code for the conjugate:
quaternion conjugate(quaternion quat) { quat.x = -quat.x; quat.y = -quat.y; quat.z = -quat.z; return quat; }
The last thing you need to know is quaternion multiplication. Then you'll be ready to make a quaternion based camera.
Multiplying quaternions
Multiplication with quaternions is a little complicated as it involves dot-products cross-products. However, if you just use the following forumula that expands these operations, it isn't too hard. To multiply quaternion A by quaternion B, just do the following:
C = A * B
such that:
C.x = | A.w*B.x + A.x*B.w + A.y*B.z - A.z*B.y | C.y = | A.w*B.y - A.x*B.z + A.y*B.w + A.z*B.x | C.z = | A.w*B.z + A.x*B.y - A.y*B.x + A.z*B.w | C.w = | A.w*B.w - A.x*B.x - A.y*B.y - A.z*B.z |
Here's some code for multiplication:
quaternion mult(quaternion A, quaternion B) { quaternion C; C.x = A.w*B.x + A.x*B.w + A.y*B.z - A.z*B.y; C.y = A.w*B.y - A.x*B.z + A.y*B.w + A.z*B.x; C.z = A.w*B.z + A.x*B.y - A.y*B.x + A.z*B.w; C.w = A.w*B.w - A.x*B.x - A.y*B.y - A.z*B.z; return C; }
That's not too that hard is it? Now we'll look at how to use these operations to make a quaternion based camera.
The quaternion camera
To make a camera you typically use three vectors: Position, View, and Up (or you may call them what you like). For a first person camera - which we will be using - we're only going to consider rotating the View vector. With quaternions we can rotate a vector around an arbitrary axis (same as with axis-angles) very easily.
To achieve this, first we need to turn our View vector into a quaternion, then define a rotation quaternion and lastly, apply the rotation quaternion to the View quaternion to make the rotation.
To make the View quaternion, V, the x, y, and z values are taken from the View vector and we simply add a 0 for the scalar component w. Thus,
V = [0, View]
Then you need to make a quaternion to represent the rotation. To do this, you need the vector you want to rotate about, and the angle you wish to rotate by. We'll just simply term the vector to rotate about A, and the angle theta. Here is the formula to build your rotation quaternion, which we'll call R.
vector A = [x, y, z] R.x = A.x * sin(theta/2) R.y = A.y * sin(theta/2) R.z = A.z * sin(theta/2) R.w = cos(theta/2)
So now we have the vector (View) and its quaternion V that we want to rotate by an angle theta about the vector A. The rotation quaternion R defines this rotation. After the rotation, we'll have the new quaternion representing our view, given by W. The rotation operation is simply
W = R * V * R'
where R' is the conjugate of R. To get our new view vector, we just take the vector components out of W.
NewView = [W.x W.y W.z]
The following function (using SDL, use glut or whatever you like) sets the view based on the distance from the current mouse coordinates to the centre of the screen. I learned how to do this from gametutorials.com, and modified the code for my purposes. In this code, positive x is to the right and positive y is down the screen.
void Camera::SetViewByMouse(void) { // the coordinates of our mouse coordinates int MouseX, MouseY; // the middle of the screen in the x direction int MiddleX = SCREENWIDTH/2; // the middle of the screen in the y direction int MiddleY = SCREENHEIGHT/2; // vector that describes mouseposition - center Vector MouseDirection(0, 0, 0); // static variable to store the rotation about the x-axis, since // we want to limit how far up or down we can look. // We don't need to cap the rotation about the y-axis as we // want to be able to turn around 360 degrees static double CurrentRotationAboutX = 0.0; // The maximum angle we can look up or down, in radians double maxAngle = 1; // This function gets the position of the mouse SDL_GetMouseState(&MouseX, &MouseY); // if the mouse hasn't moved, return without doing // anything to our view if((MouseX == MiddleX) && (MouseY == MiddleY)) return; // otherwise move the mouse back to the middle of the screen SDL_WarpMouse(MiddleX, MiddleY); // get the distance and direction the mouse moved in x (in // pixels). We can't use the actual number of pixels in radians, // as only six pixels would cause a full 360 degree rotation. // So we use a mousesensitivity variable that can be changed to // vary how many radians we want to turn in the x-direction for // a given mouse movement distance // We have to remember that positive rotation is counter-clockwise. // Moving the mouse down is a negative rotation about the x axis // Moving the mouse right is a negative rotation about the y axis MouseDirection.x = (MiddleX - MouseX)/MouseSensitivity; MouseDirection.y = (MiddleY - MouseY)/MouseSensitivity; CurrentRotationX += MouseDirection.y; // We don't want to rotate up more than one radian, so we cap it. if(CurrentRotationX > 1) { CurrentRotationX = 1; return; } // We don't want to rotate down more than one radian, so we cap it. if(CurrentRotationX < -1) { CurrentRotationX = -1; return; } else { // get the axis to rotate around the x-axis. Vector Axis = CrossProduct(View - Position, Up); // To be able to use the quaternion conjugate, the axis to // rotate around must be normalized. Axis = Normalize(Axis); // Rotate around the y axis RotateCamera(MouseDirection.y, Axis.x, Axis.y, Axis.z); // Rotate around the x axis RotateCamera(MouseDirection.x, 0, 1, 0); } }
This function actually rotates our view. After we are done, just plug your camera vectors (Position, View, and Up) into gluLookAt(Position.x, Position.y, Position.z, View.x, View.y, View.z, Up.x, Up.y, Up.z). Here is the code for the rotation.
void RotateCamera(double Angle, double x, double y, double z) { quaternion temp, quat_view, result; temp.x = x * sin(Angle/2); temp.y = y * sin(Angle/2); temp.z = z * sin(Angle/2); temp.w = cos(Angle/2); quat_view.x = View.x; quat_view.y = View.y; quat_view.z = View.z; quat_view.w = 0; result = mult(mult(temp, quat_view), conjugate(temp)); View.x = result.x; View.y = result.y; View.z = result.z; }
Again, at the end of the above functions, you should call
gluLookAt(Position.x, Position.y, Position.z, View.x, View.y, View.z, Up.x, Up.y, Up.z).
and your camera should work just perfectly. Maybe some other time, I'll do a third person camera tutorial, and explain how to use SLERP.