Getting Oriented In this tutorial, we will investigate specific problems with regard to orienting objects.
Gimbal Lock Remember a few tutorials back, when we said that a rotation matrix isn't a rotation matrix at all, that it is an orientation matrix? We also said that forgetting this can come back to bite you. Well, here's likely the most common way. Normally, when dealing with orienting an object like a plane or spaceship in 3D space, you want to orient it based on 3 rotations about the 3 main axes. The obvious way to do this is with a series of 3 rotations. This means that the program stores 3 angles, and you generate a rotation matrix by creating 3 rotation matrices based on these angles and concatenating them. Indeed, the 3 angles often have special names, based on common flight terminology: yaw, pitch, and roll. Pitch is the rotation that raises or lowers the front of the object. Yaw is the rotation that turns the object left and right. Roll is the rotation that spins it around the direction of motion. These terms neatly duck the question of what the axes technically are; they are defined in terms of semantic axes (front, left, direction of motion, etc), rather than a specific model space. So in one model space, roll might be a rotation about the X axis, but in another, it might be a rotation about the Z axis. One of the first problems you will note is that the order you apply these rotations matter. As previously stated, a rotation matrix is an orientation transform. Each transform defines a new coordinate system, and the next transform is based on an object in the new space. For example, if we apply the roll first, we have now changed what the axis for the subsequent yaw is. You can use any order that you like, so long as you understand what these angles mean. If you apply the roll first, your yaw and pitch must be in terms of the new roll coordinate system, and not the original coordinate system. That is, a change that is intended to only affect the roll of the final space may need yaw or pitch changes to allow it to have the same apparent orientation (except for the new roll, of course). But there is a far more insidious problem lurking in here. And this problem happens anytime you compute a final rotation from a series of 3 rotations about axes perpendicular to each other. The tutorial project Gimbal Lock illustrates this problem. Because the problem was first diagnosed with a physical device called a gimbal, the problem has become known as gimbal lock. A gimbal is a pivoted support that provides the ability to rotate in one axis. A gimbal can be mounted within another gimbal. The Gimbal Lock project has a set of 3 square gimbals, each with a pivot axis that is perpendicular to the other two. This effectively mimics the common yaw/pitch/roll angle setup. You can control the orientation of each gimbal separately. The w and s keys control the outer gimbal, the a and d keys control the middle gimbal, and the q and e keys control the inner gimbal. If you just want to see (and affect) the orientation of the ship, press the SpaceBar to toggle drawing the gimbal rings. The first thing you discover when attempting to use the gimbals to orient the ship is that the yaw, pitch, and roll controls of the gimbal change each time you move one of them. That is, when the gimbal arrangement is in the original position, the outer gimbal controls the pitch. But if you move the middle gimbal, it no longer controls only the pitch. Orienting the ship is very unintuitive. The bigger is what happens when two of the gimbals are parallel with one another: Recall that the purpose of the three gimbals is to be able to adjust one of the three angles and orient the object in a particular direction. In a flight-simulation game, the player would have controls that would change their yaw, pitch, and roll. However, look at this picture. Given the controls of these gimbals, can you cause the object to pitch up and down? That is, move its nose up and down from where it is currently? Only slightly; you can use the middle gimbal, which has a bit of pitch rotation. But that isn't much. The reason we don't have as much freedom to orient the object is because the outer and inner gimbals are now rotating about the same axis. Which means you really only have two gimbals to manipulate in order to orient the red gimbal. And 3D orientation cannot be fully controlled with only 2 axial rotations, with only 2 gimbals. When gimbals are in such a position, you have what is known as gimbal lock; you have locked one of the gimbals to another, and now both cause the same effect.
Rendering Before we find a solution to the problem, let's review the code. Most of it is nothing you haven't seen elsewhere, so this will be quick. There is no explicit camera matrix set up for this example; it is too simple for that. The three gimbals are loaded from mesh files as we saw in our last tutorial. They are built to fit into the above array. The ship is also from a mesh file. The rendering function looks like this: Gimbal Lock Display Code void display() { glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClearDepth(1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); Framework::MatrixStack currMatrix; currMatrix.Translate(glm::vec3(0.0f, 0.0f, -200.0f)); currMatrix.RotateX(g_angles.fAngleX); DrawGimbal(currMatrix, GIMBAL_X_AXIS, glm::vec4(0.4f, 0.4f, 1.0f, 1.0f)); currMatrix.RotateY(g_angles.fAngleY); DrawGimbal(currMatrix, GIMBAL_Y_AXIS, glm::vec4(0.0f, 1.0f, 0.0f, 1.0f)); currMatrix.RotateZ(g_angles.fAngleZ); DrawGimbal(currMatrix, GIMBAL_Z_AXIS, glm::vec4(1.0f, 0.3f, 0.3f, 1.0f)); glUseProgram(theProgram); currMatrix.Scale(3.0, 3.0, 3.0); currMatrix.RotateX(-90); //Set the base color for this object. glUniform4f(baseColorUnif, 1.0, 1.0, 1.0, 1.0); glUniformMatrix4fv(modelToCameraMatrixUnif, 1, GL_FALSE, glm::value_ptr(currMatrix.Top())); g_pObject->Render("tint"); glUseProgram(0); glutSwapBuffers(); } The translation done first acts as our camera matrix: positioning the objects far enough away to be comfortably visible. From there, the three gimbals are drawn, with their appropriate rotations. Since each rotation applies to the previous one, the final orientation is given to the last gimbal. The DrawGimbal function does some rotations of its own, but this is just to position the gimbals properly in the array. The gimbals are given a color programmatically, which is the 3rd parameter to DrawGimbal. After building up the rotation matrix, we draw the ship. We use a scale to make it reasonably large, and then rotate it so that it points in the correct direction relative to the final gimbal. In model space, the ship faces the +Z axis, but the gimbal faces the +Y axis. So we needed a change of coordinate system.
Quaternions So gimbals, 3 accumulated axial rotations, don't really work very well for orienting an object. How do we fix this problem? Part of the problem is that we are trying to store an orientation as a series of 3 accumulated axial rotations. Orientations are orientations, not rotations. And certainly not a series of them. So we need to treat the orientation of the ship as an orientation. The first thought towards this end would be to keep the orientation as a matrix. When the time comes to modify the orientation, we simply apply a transformation to this matrix, storing the result as the new current orientation. This means that every yaw, pitch, and roll applied to the current orientation will be relative to that current orientation. Which is precisely what we need. If the user applies a positive yaw, you want that yaw to rotate them relative to where they are current pointing, not relative to some fixed coordinate system. There are a few downsides to this approach. First, a 4x4 matrix is rather larger than 3 floating-point angles. But a much more difficult issue is that successive floating-point math can lead to errors. If you keep accumulating successive transformations of an object, once every 1/30th of a second for a period of several minutes or hours, these floating-point errors start accumulating. Eventually, the orientation stops being a pure rotation and starts incorporating scale and skewing characteristics. The solution here is to re-orthonormalize the matrix after applying each transform. A coordinate system (which a matrix defines) is said to be orthonormal if the basis vectors are of unit length (no scale) and each axis is perpendicular to all of the others. Unfortunately, re-orthonormalizing a matrix is not a simple operation. You could try to normalize each of the axis vectors with typical vector normalization, but that wouldn't ensure that the matrix was orthonormal. It would remove scaling, but the axes wouldn't be guaranteed to be perpendicular. Orthonormalization is certainly possible. But there are better solutions. Such as using something called a quaternion. A quaternion is (for the purposes of this conversation) a 4-dimensional vector that is treated in a special way. Any pure orientation change from one coordinate system to another can be represented by a rotation about some axis by some angle. A quaternion is a way of encoding this angle/axis rotation: Angle/Axis to Quaternion Assuming the axis itself is a unit vector, this will produce a unit quaternion. That is, a quaternion with a length of 1. Quaternions can be considered to be two parts: a vector part and a scalar part. The vector part are the first three components, when displayed in the order above. The scalar part is the last part.
Quaternion Math Quaternions are equivalent to orientation matrices. You can compose two orientation quaternions using a special operation called quaternion multiplication. Given the quaternions a and b, the product of them is: Quaternion Multiplication If the two quaternions being multiplied represent orientations, then the product of them is a composite orientation. This works like matrix multiplication, except only for orientations. Like matrix multiplication, quaternion multiplication is associative ( (a*b) * c = a * (b*c) ), but not commutative ( a*b != b*a ). The main difference between matrices and quaternions that matters for our needs is that it is easy to keep a quaternion normalized. Simply perform a vector normalization on it after every few multiplications. This enables us to add numerous small rotations together without numerical precision problems showing up. There is one more thing we need to be able to do: convert a quaternion into a rotation matrix. While we could convert a unit quaternion back into angle/axis rotations, it's much preferable to do it directly: Quaternion to Matrix This does look suspiciously similar to the formula for generating a matrix from an angle/axis rotation.
Composition Type So our goal is to compose successive rotations into a final orientation. When we want to increase the pitch, for example, we will take the current orientation and multiply into it a quaternion that represents a pitch rotation of a few degrees. The result becomes the new orientation. But which side do we do the multiplication on? Quaternion multiplication is not commutative, so this will have an affect on the output. Well, it works exactly like matrix math. Our positions (p) are in model space. We are transforming them into world space. The current transformation matrix is represented by the orientation O. Thus, to transform points, we use O*p Now, we want to adjust the orientation O by applying some small pitch change. Well, the pitch of the model is defined by model space. Therefore, the pitch change (R) is a transformation that takes coordinates in model space and transforms them to the pitch space. So our total transformation is O*R*p ; the new orientation is O*R .
Yaw Pitch Roll We implement this in the Quaternion YPR tutorial. This tutorial doesn't show gimbals, but the same controls exist for yaw, pitch, and roll transformations. Here, pressing the SpaceBar will switch between right-multiplying the YPR values to the current orientation and left-multiplying them. Post-multiplication will apply the YPR transforms from world-space. The rendering code is pretty straightforward. Quaternion YPR Display void display() { glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClearDepth(1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); Framework::MatrixStack currMatrix; currMatrix.Translate(glm::vec3(0.0f, 0.0f, -200.0f)); currMatrix.ApplyMatrix(glm::mat4_cast(g_orientation)); glUseProgram(theProgram); currMatrix.Scale(3.0, 3.0, 3.0); currMatrix.RotateX(-90); //Set the base color for this object. glUniform4f(baseColorUnif, 1.0, 1.0, 1.0, 1.0); glUniformMatrix4fv(modelToCameraMatrixUnif, 1, GL_FALSE, glm::value_ptr(currMatrix.Top())); g_pShip->Render("tint"); glUseProgram(0); glutSwapBuffers(); } Though GLSL does not have quaternion types or quaternion arithmetic, the GLM math library provides both. The g_orientation variable is of the type glm::fquat, which is a floating-point quaternion. The glm::mat4_cast function converts a quaternion into a 4x4 rotation matrix. This stands in place of the series of 3 rotations used in the last tutorial. In response to keypresses, g_orientation is modified, applying a transform to it. This is done with the OffsetOrientation function. OffsetOrientation Function void OffsetOrientation(const glm::vec3 &_axis, float fAngDeg) { float fAngRad = Framework::DegToRad(fAngDeg); glm::vec3 axis = glm::normalize(_axis); axis = axis * sinf(fAngRad / 2.0f); float scalar = cosf(fAngRad / 2.0f); glm::fquat offset(scalar, axis.x, axis.y, axis.z); if(g_bRightMultiply) g_orientation = g_orientation * offset; else g_orientation = offset * g_orientation; g_orientation = glm::normalize(g_orientation); } This generates the offset quaternion from an angle and axis. Since the axis is normalized, there is no need to normalize the resulting offset quaternion. Then the offset is multiplied into the orientation, and the result is normalized. In particular, pay attention to the difference between right multiplication and left multiplication. When you right-multiply, the offset orientation is in model space. When you left-multiply, the offset is in world space. Both of these can be useful for different purposes.
Camera-Relative Orientation As useful as model and world space offsetting is, there is one more space that it might be useful to orient from. Camera-space. This is primarily useful in modelling applications, but it can have other applications as well. In such programs, as the user spins the camera around to different angles, the user may want to transform the object relative to the direction of the view. In order to understand the solution to doing this, let's frame the problem explicitly. We have positions (p) in model space. These positions will be transformed by a current model-to-world orientation (O), and then by a final camera matrix (C). Thus, our transform equation is C*O*p . We want to apply an orientation offset (R), which takes points in camera-space. If we wanted to apply this to the camera matrix, it would simply be multiplied by the camera matrix: R*C*O*p . That's nice and all, but we want to apply a transform to O, not to C. Therefore, we need to use our transforms to generate a new orientation offset (N), which will produce the same effect: C*N*O = R*C*O
Inversion In order to solve this, we need to introduce a new concept. Given a matrix M, there may be a matrix N such that MN = I , where I is the identity matrix. If there is such a matrix, the matrix N is called the inverse matrix of M. The notation for the inverse matrix of M is M-1. The symbol -1 does not mean to raise the matrix to the -1 power; it means to invert it. The matrix inverse can be analogized to the scalar multiplicative inverse (ie: reciprocal). The scalar multiplicative inverse of X is the number Y such that XY = 1 . In the case of the scalar inverse, this is very easy to solve for: Y = 1/X . Easy though this may be, there are values of X for which there is no multiplicative inverse. OK, there's one such real value of X: 0. The case of the inverse matrix is much more complicated. Just as with the scalar inverse, there are matrices that have no inverse. Unlike the scalar case, there are a lot of matrices with no inverse. Also, computing the inverse matrix is a lot more complicated than simply taking the reciprocal of a value. Most common transformation matrices do have an inverse. And for the basic transformation matrices, the inverse matrix is very easy to compute. For a pure rotation matrix, simply compute a new rotation matrix by negating the angle that the old one was generated with. For a translation matrix, negate the origin value in the matrix. For a scale matrix, take the reciprocal of the scale along each axis. To take the inverse of a sequence of matrices, you can take the inverse of each of the component matrices. But you have to do the matrix multiplication in reverse order. So if we have M = TRS , then M-1 = S-1R-1T-1 . Quaternions, like matrices, have a multiplicative inverse. The inverse of a pure rotation matrix, which quaternions represent, is a rotation about the same axis with the negative of the angle. For any angle θ, it is the case that sin(-θ) = -sin(θ) . It is also the case that cos(-θ) = cos(θ) . Since the vector part of a quaternion is built by multiplying the axis by the sine of the half-angle, and the scalar part is the cosine of the half-angle, the inverse of any quaternion is just the negation of the vector part. You can also infer this to mean that, if you negate the axis of rotation, you are effectively rotating about the old axis but negating the angle. Which is true, since the direction of the axis of rotation defines what direction the rotation angle moves the points in. Negate the axis's direction, and you're rotating in the opposite direction. In quaternion lingo, the inverse quaternion is more correctly called the conjugate quaternion. We use the same inverse notation, -1, to denote conjugate quaternions. Incidentally, the identity quaternion is a quaternion who's rotation angle is zero. The cosine of 0 is one, and the sine of 0 is zero, so the vector part of the identity quaternion is zero and the scalar part is one.
Solution Given our new knowledge of inverse matrices, we can solve our problem. C*N*O = R*C*O We can right-multiply both sides of this equation by the inverse transform of O. (C*N*O)*O-1 = (R*C*O)*O-1 C*N*I = R*C*I The I is the identity transform. From here, we can left-multiply both sides by the inverse transform of C: C-1 *(C*N) = C-1*(R*C) N = C-1*(R*C) Therefore, given an offset that is in camera space, we can generate the world-space equivalent by multiplying it between the camera and inverse camera transforms.
Transformation Spaces It turns out that this is a generalized operation. It can be used for much more than just orientation changes. Consider a scale operation. Scales apply along the main axes of their space. But if you want to scale something along a different axis, how do you do that? You rotate the object into a coordinate system where the axis you want to scale is one of the basis axes, perform your scale, then rotate it back with the inverse of the previous rotation. Effectively, what we are doing is transforming, not positions, but other transformation matrices into different spaces. A transformation matrix has some input space and defines an output space. If we want to apply that transformation in a different space, we perform this operation. The general form of this sequence is as follows. Suppose you have a transformation matrix T, which operates on points in a space called F. We have some positions in the space P. What we want is to create a matrix that applies T's transformation operation, except that it needs to operate on points in the space of P. Given a matrix M that transforms from P space to F space, that matrix is M-1*T*M .
Final Orientation Let's look at how this all works out in code, with the Camera Relative tutorial. This works very similarly to the last tutorial, but with a few differences. Since we are doing camera-relative rotation, we need to have an actual camera that can move independently of the world. So we incorporate our camera code from our world space into this one. As before, the I and K keys will move the camera up and down, relative to a center point. The J and K keys will move the camera left and right around the center point. Holding Shift with these keys will move the camera in smaller increments. The SpaceBar will toggle between three transforms: model-relative (yaw/pitch/roll-style), world-relative, and camera-relative. Our scene also includes a ground plane as a reference. The display function only changed where needed to deal with drawing the ground plane and to handle the camera. Either way, it's nothing that hasn't been seen elsewhere. The substantive changes were in the OffsetOrientation function: Camera Relative OffsetOrientation void OffsetOrientation(const glm::vec3 &_axis, float fAngDeg) { float fAngRad = Framework::DegToRad(fAngDeg); glm::vec3 axis = glm::normalize(_axis); axis = axis * sinf(fAngRad / 2.0f); float scalar = cosf(fAngRad / 2.0f); glm::fquat offset(scalar, axis.x, axis.y, axis.z); switch(g_iOffset) { case MODEL_RELATIVE: g_orientation = g_orientation * offset; break; case WORLD_RELATIVE: g_orientation = offset * g_orientation; break; case CAMERA_RELATIVE: { const glm::vec3 &camPos = ResolveCamPosition(); const glm::mat4 &camMat = CalcLookAtMatrix(camPos, g_camTarget, glm::vec3(0.0f, 1.0f, 0.0f)); glm::fquat viewQuat = glm::quat_cast(camMat); glm::fquat invViewQuat = glm::conjugate(viewQuat); const glm::fquat &worldQuat = (invViewQuat * offset * viewQuat); g_orientation = worldQuat * g_orientation; } break; } g_orientation = glm::normalize(g_orientation); } The change here is the addition of the camera-relative condition. To do this in quaternion math, we must first convert the world-to-camera matrix into a quaternion representing that orientation. This is done here using glm::quat_cast. The conjugate quaternion is computed with GLM. Then, we simply multiply them with the offset to compute the world-space offset orientation. This gets left-multiplied into the current orientation, and we're done.
Interpolation A quaternion represents an orientation; it defines a coordinate system relative to another. If we have two orientations, we can consider the orientation of the same object represented in both coordinate systems. What if we want to generate an orientation that is halfway between them, for some definition of halfway? Or even better, consider an arbitrary interpolation between two orientations, so that we can watch an object move from one orientation to another. This would allow us to see an object smoothly moving from one orientation to another. This is one more trick we can play with quaternions that we can't with matrices. Linearly-interpolating the components of matrices does not create anything that resembles an inbetween transformation. However, linearly interpolating a pair of quaternions does. As long as you normalize the results. The Interpolation tutorial demonstrates this. The Q, W, E, R, T, Y, and U keys cause the ship to interpolate to a new orientation. Each key corresponds to a particular orientation, and the Q key is the initial orientation. We can see that there are some pretty reasonable looking transitions. The transition from Q to W, for example. However, there are some other transitions that don't look so good; the Q to E transition. What exactly is going on?
The Long Path Unit quaternions represent orientations, but they are also vector directions. Specifically, directions in a four-dimensional space. Being unit vectors, they represent points on a 4D sphere of radius one. Therefore, the path between two orientations can be considered to be simply moving from one direction to another on the surface of the 4D sphere. While unit quaternions do represent orientations, a quaternion is not a unique representation of an orientation. That is, there are multiple quaternions that represent the same orientation. Well, there are two. The conjugate of a quaternion, its inverse orientation, is the negation of the vector part of the quaternion. If you negate all four components however, you get something quite different: the same orientation as before. Negating a quaternion does not affect its orientation. While the two quaternions represent the same orientation, they aren't the same as far as interpolation is concerned. Consider a two-dimensional case: If the angle between the two quaternions is greater than 90°, then the interpolation between them will take the long path between the two orientations. Which is what we see in the Q to E transition. The orientation R is the negation of E; if you try to interpolate between them, nothing changes. The Q to R transition looks much better behaved. This can be detected easily enough. If the 4-vector dot product between the two quaternions is less than zero, then the long path will be taken. If you want to prevent the long path from being used, simply negate one of the quaternions before interpolating if you detect this. Similarly, if you want to force the long path, then ensure that the angle is greater than 90° by negating a quaternion if the dot product is greater than zero.
Interpolation Speed There is another problem. Notice how fast the Q to E interpolation is. It starts off slow, then rapidly spins around, then slows down towards the end. Why does this happen? The linear interpolation code looks like this: Quaternion Linear Interpolation glm::fquat Lerp(const glm::fquat &v0, const glm::fquat &v1, float alpha) { glm::vec4 start = Vectorize(v0); glm::vec4 end = Vectorize(v1); glm::vec4 interp = glm::mix(start, end, alpha); interp = glm::normalize(interp); return glm::fquat(interp.w, interp.x, interp.y, interp.z); } GLM's quaternion support does something unusual. The W component is given first to the fquat constructor. Be aware of that when looking through the code. The Vectorize function simply takes a quaternion and returns a vec4; this is necessary because GLM fquat don't support many of the operations that GLM vec4's do. In this case, the glm::mix function, which performs component-wise linear interpolation. Each component of the vector is interpolated separately from the rest. The quaternion for Q is (0.7071f, 0.7071f, 0.0f, 0.0f), while the quaternion for E is (-0.4895f, -0.7892f, -0.3700f, -0.02514f). In order for the first componet of Q to get to E's first component, it will have to go through zero. When the alpha is around 0.5, half-way through the movement, the resultant vector before normalization is very small. But the vector itself isn't what provides the orientation; the direction of the 4D vector is. Which is why it moves very fast in the middle: the direction is changing rapidly. In order to get smooth interpolation, we need to interpolate based on the direction of the vectors. That is, we interpolate along the angle between the two vectors. This kind of interpolation is called spherical linear interpolation or slerp. To see the difference this makes, press the SpaceBar; this toggles between regular linear interpolation and slerp. The slerp version is much smoother. The code for slerp is rather complex: Spherical Linear Interpolation glm::fquat Slerp(const glm::fquat &v0, const glm::fquat &v1, float alpha) { float dot = glm::dot(v0, v1); const float DOT_THRESHOLD = 0.9995f; if (dot > DOT_THRESHOLD) return Lerp(v0, v1, alpha); glm::clamp(dot, -1.0f, 1.0f); float theta_0 = acosf(dot); float theta = theta_0*alpha; glm::fquat v2 = v1 - v0*dot; v2 = glm::normalize(v2); return v0*cos(theta) + v2*sin(theta); } Slerp and Performance It's important to know what kind of problems slerp is intended to solve and what kind it is not. Slerp becomes increasingly more important the more disparate the two quaternions being interpolated are. If you know that two quaternions are always quite close to one another, then slerp isn't worth the expense. The acos call in the slerp code alone is pretty substantial in terms of performance. Whereas lerp is typically just a vector/scalar multiply followed by a vector/vector addition. Even on the CPU, the performance difference is important, particularly if you're doing thousands of these per frame. As you might be in an animation system.
In Review In this tutorial, you have learned the following: A fixed sequence of successive rotations can prevent other rotations from contributing to the object's orientation. Quaternions are 4-dimensional vectors that can encode an orientation. They can be used for successively applying small rotations to an orientation. Matrices fail at this because of the difficulty of orthonormalizing them to avoid floating-point error accumulation. Quaternions work almost identically to matrices, in so far as they specify orientations. They can be constructed directly from an angle/axis rotation, and they can be composed with one another via quaternion multiplication. One can transform a matrix, or a quaternion with another matrix or quaternion, such that the resulting transform is specified in a different space. This is useful for applying rotations to an object's orientation that are in camera-space,, while the object's orientation remains a model-to-world transform. Quaternions can be interpolated, either with component-wise linear interpolation or with spherical linear interpolation. If the angle between two quaternion vectors is greater than 90°, then the interpolation between them will move indirectly between the two.
Further Study Try doing the following with the orientation tutorials. Modify the Interpolation tutorial to allow multiple animations to be active simultaneously. The Orientation class is already close to being able to allow this. Instead of storing a single Orientation::Animation object, it should store a std::deque of them. When the external code adds a new one, it gets pushed onto the end of the deque. During update, the front-most entries can end and be popped off, recording its destination index as the new current one in the Orientation class. To get the orientation, just call each animation's orientation function, feeding the previous result into the next one. Change the Interpolation tutorial to allow one to specify whether the long path or short path between two orientations should be taken. This can work for both linear and spherical interpolation.
Further Research This discussion has focused on the utility of quaternions in orienting objects, and it has deftly avoided answering the question of exactly what a quaternion is. After all, saying that a quaternion is a four-dimensional complex number does not explain why they are useful in graphics. They are a quite fascinating subject for those who like oddball math things. This discussion has also glossed over a few uses of quaternions in graphics, such as how to directly rotate a position or direction by a quaternion. Such information is readily available online.
Glossary gimbal lock When applying 3 or more successive rotations about axes that are orthogonal to each other, gimbal lock occurs when a degree of rotational freedom is lost due to two or more axes that cause the same effective rotation. orthonormal A transform has the property of being orthonormal if the three basis axes are all orthogonal and the three axes are normal (have a length of 1). quaternion A four-dimensional vector that represents an orientation. The first three components of the quaternion, the X, Y and Z, are considered the vector part of the quaternion. The fourth component is the scalar part. inverse matrix The inverse matrix of the matrix M is the matrix N for which the following equation is true: MN = I , where I is the identity matrix. The inverse of M is usually denoted as M-1. conjugate quaternion Analogous to the inverse matrix. It is computed by negating the vector part of the quatenrion. spherical linear interpolation, slerp Interpolation between two unit vectors that is linear based on the angle between them, rather than the vectors themselves.