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.</para>
<para>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 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
+ 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.</para>
<para>Pitch is the rotation that raises or lowers the front of the object. Yaw is the
<para>You can control the orientation of each gimbal separately. The <keycap>w</keycap> and
- <keycap>s</keycap> keys control the blue gimbal, the <keycap>a</keycap> and
- <keycap>d</keycap> keys control the green gimbal, and the <keycap>q</keycap> and
- <keycap>e</keycap> keys control the red gimbal.</para>
- <para>With these three gimbals, you can cause the innermost gimbal (the red one) to have any
- arbitrary orientation. That isn't the problem. The problem happens when two of the
- gimbals are parallel with one another:</para>
+ <keycap>s</keycap> keys control the outer gimbal, the <keycap>a</keycap> and
+ <keycap>d</keycap> keys control the middle gimbal, and the <keycap>q</keycap> and
+ <keycap>e</keycap> keys control the inner gimbal. If you just want to see (and
+ affect) the orientation of the ship, press the <keycap>SpaceBar</keycap> to toggle
+ drawing the gimbal rings.</para>
+ <para>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
+ <emphasis>only</emphasis> the pitch. Orienting the ship is very unintuitive.</para>
+ <para>The bigger is what happens when two of the gimbals are parallel with one
<title>Parallel Gimbals</title>
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
- <para>The player's theoretical ship is pointed in the direction of the center gimbal's
- plane. Given the controls you have here, can you cause the center gimbal to rotate
- around the axis that it is facing? No. You can't even do this somewhat; you can only
- rotate it in two directions. But we have three gimbals, which means we should have three
- axes of rotation. Why can't we rotate the red gimbal in the Z (forward) axis?</para>
- <para>Because the outer and inner gimbals are now rotating about the <emphasis>same
- axis</emphasis>. 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.</para>
+ <para>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.</para>
+ <para>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 <emphasis>same axis</emphasis>. 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
<para>When gimbals are in such a position, you have what is known as <glossterm>gimbal
lock</glossterm>; you have locked one of the gimbals to another, and now both cause
- <title>Complex Mesh Space</title>
- <para>The way this tutorial renders each gimbal serves as an object lesson in the use of
- mesh space. The gimbals are composed of 6 pieces, each of which ultimately derives
- from a simple 3D cube who's sides are of unit length. The cube is scaled and
- translated to form the rectangular prisms used to build each the gimbal.</para>
- <para>The <function>DrawGimbal</function> function takes a matrix stack, an axis (one of
- the three kinds of gimbals), a size for the gimbal, and a color. The width and
- height of a gimbal are the same, so this code only takes a single size.</para>
- <para>There is ultimately only one gimbal <quote>mesh</quote>: the difference between
- them is what their base orientation is. That is computed in this function:</para>
+ <title>Rendering</title>
+ <para>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.</para>
+ <para>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
+ <para>The rendering function looks like this:</para>
- <title>DrawGimbal Function</title>
- <programlisting language="cpp">void DrawGimbal(MatrixStack &currMatrix, GimbalAxis eAxis, float fSize, glm::vec4 baseColor)
- currMatrix.RotateZ(90.0f);
- currMatrix.RotateX(90.0f);
- currMatrix.RotateY(90.0f);
- currMatrix.RotateX(90.0f);
- DrawBaseGimbal(currMatrix, fSize, baseColor);
- <para>The rotation matrices change the orientation of the gimbal to its neutral
- position. These matrices are chosen to allow the full array of 3 gimbals to connect
- <para>The <function>DrawBaseGimbal</function> function performs the basic computations
- necessary to draw the pieces. It delegates the actual drawing to other
- <title>DrawBaseGimbal Function</title>
- <programlisting language="cpp">void DrawBaseGimbal(MatrixStack &currMatrix, float fSize, glm::vec4 baseColor)
- //A Gimbal can only be 4 units in size or more.
- glUseProgram(theProgram);
- //Set the base color for this object.
- glUniform4fv(baseColorUnif, 1, glm::value_ptr(baseColor));
- glBindVertexArray(vao);
- float fGimbalSidesOffset = (fSize / 2.0f) - 1.5f;
- float fGimbalSidesScale = fSize - 2.0f;
- DrawGimbalSides(currMatrix, fGimbalSidesOffset, fGimbalSidesScale);
- float fGimbalAttachOffset = (fSize / 2.0f) - 0.5f;
- DrawGimbalAttachments(currMatrix, fGimbalAttachOffset);
- <para>The color is stored into a uniform in the program. The fragment shader uses this
- color along with the interpolated per-vertex color to compute the final output
- <title>ColorMultUniform Fragment Shader</title>
- <programlisting language="glsl">#version 330
-smooth in vec4 theColor;
- outputColor = theColor * baseColor;
- <para>The interpolated per-vertex color is multiplied with the base color. The colors
- from the mesh are not really colors in this case; they serve only to darken certain
- faces. The front and back faces use full white (1.0, 1.0, 1.0, 1.0); multiplying
- this by <varname>baseColor</varname> will simply return
- <varname>baseColor</varname>. Thus the front and back faces of the object are the
- intended color. The top and bottom sides use (0.75, 0.75, 0.75, 1.0), which serves
- to make <varname>baseColor</varname> closer to zero, but not by a lot. The left and
- right sides use a smaller value (0.5, 0.5, 0.5, 1.0), which makes the resulting
- color even smaller.</para>
- <para>The <function>DrawBaseGimbal</function> function computes the length and
- positional offset for each of the main pieces of the ring of the gimbal. Since the
- gimbal is square, it only needs a single length and offset. The
- <function>DrawGimbalSides</function> draws the gimbal's square ring.</para>
- <para>The two attachment points are rendered with ; the sizes for these are fixed, so
- they don't vary with the size of the gimbal. The <emphasis>location</emphasis> of
- these do vary, so <function>DrawBaseGimbal</function> must compute that
- <para>All of this code uses multiple spaces in multiple different places. The most
- important thing that it shows is how each layer in the hierarchy of functions
- doesn't care what the space in the matrix stack currently is. DrawBaseGimbal draws
- the gimbal in a certain space; it is up to whatever happens to be on the matrix
- stack as to how it gets transformed into the final space.</para>
- <para>Furthermore, the lower parts of the code don't care about how the gimbal drawing
- gets done. The <function>display</function> function that ultimately calls
- <function>DrawGimbal</function> doesn't care if it is drawing multiple meshes,
- or a single scaled mesh, or whatever. It simply says to draw the Y-axis gimbal of a
- certain size in a certain color at a transform given by the matrix stack. Similarly,
- <function>DrawGimbal</function> only expects <function>DrawBaseGimbal</function>
- to draw a X-oriented gimbal; that this requires 6 rendering calls and some matrix
- stack work is not something <function>DrawGimbal</function> is concerned
- <para>This is the ideal for dealing with complex objects that are positioned via various
- transforms. The higher level code should be insulated from the fact that low level
- code may be performing complex transformations, and low-level code should be
- insulated from the specific uses of that particular object. Look at how simple the
- display function is:</para>
- <title>Gimbal Lock's Display Function</title>
+ <title>Gimbal Lock Display Code</title>
<programlisting language="cpp">void display()
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
- MatrixStack currMatrix;
- currMatrix.Translate(glm::vec3(0.0f, 0.0f, -60.0f));
+ Framework::MatrixStack currMatrix;
+ currMatrix.Translate(glm::vec3(0.0f, 0.0f, -200.0f));
currMatrix.RotateX(g_angles.fAngleX);
- DrawGimbal(currMatrix, GIMBAL_X_AXIS, 30.0f, glm::vec4(0.4f, 0.4f, 1.0f, 1.0f));
+ 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, 26.0f, glm::vec4(0.0f, 1.0f, 0.0f, 1.0f));
+ 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, 22.0f, glm::vec4(1.0f, 0.3f, 0.3f, 1.0f));
+ 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");
- <para>It performs a translation, so that the gimbal is positioned in front of the
- camera. Then it performs the 3 successive rotations, drawing each gimbal after each
- one. So long as <function>DrawGimbal</function> is doing the job that
- <function>display</function> expects, everything works out fine.</para>
+ <para>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.</para>
+ <para>The <function>DrawGimbal</function> 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
+ <function>DrawGimbal.</function></para>
+ <para>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.</para>
<title>Quaternions</title>
- <para>How do you fix this problem? There are several possible solutions to the
- <para>Perhaps the most optimal solution is to simply not use gimbals. After all, if you
- don't have gimbals, you can't gimbal lock. Instead of storing the orientation as a
- series of rotations, store the orientation as an <emphasis>orientation.</emphasis> That
- is, maintain the current orientation as a matrix. When you need to modify the
- orientation, you apply a transformation to this matrix, storing the result as the new
- current orientation.</para>
+ <para>So gimbals, 3 accumulated axial rotations, don't really work very well for orienting
+ an object. How do we fix this problem?</para>
+ <para>Part of the problem is that we are trying to store an orientation as a series of 3
+ accumulated axial rotations. Orientations are <emphasis>orientations,</emphasis> not
+ rotations. And certainly not a series of them. So we need to treat the orientation of
+ the ship as an orientation.</para>
+ <para>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.</para>
<para>This means that every yaw, pitch, and roll applied to the current orientation will be
- relative to that current orientation. Which is usually exactly what you want. If the
- user applies a positive yaw, you want that yaw to rotate them relative to where they are
- current pointing.</para>
- <para>A downside of this approach, besides the size penalty of having to store a 4x4 matrix
- rather than 3 floating-point angles, is that 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.</para>
+ 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.</para>
+ <para>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.</para>
<para>The solution here is to re-orthonormalize the matrix after applying each transform. A
- transform (space) is said to be <glossterm>orthonormal</glossterm> if the basis vectors
- are of unit length (no scale) and each axis is perpendicular to all of the
+ coordinate system (which a matrix defines) is said to be
+ <glossterm>orthonormal</glossterm> if the basis vectors are of unit length (no
+ scale) and each axis is perpendicular to all of the others.</para>
<para>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.</para>
- <para>So instead, we need to use a different way of storing an orientation. To do this, we
- use something called a <glossterm>quaternion.</glossterm></para>
+ <para>Orthonormalization is certainly possible. But there are better solutions. Such as using
+ something called a <glossterm>quaternion.</glossterm></para>
<para>A quaternion is (for the purposes of this conversation) a 4-dimensional vector that is
- treated in a special way. Any change of orientation from one coordinate system to
+ 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:</para>
- <!--TODO: Equation: build XYZW of a quaternion from an angle/axis rotation.-->
- <para>This will produce a <glossterm>unit quaternion.</glossterm> That is, a quaternion with
+ <title>Angle/Axis to Quaternion</title>
+ <imagedata fileref="AngleAxisToQuaternion.svg" format="SVG"/>
+ <para>Assuming the axis itself is a unit vector, this will produce a <glossterm>unit
+ quaternion.</glossterm> That is, a quaternion with a length of 1.</para>
+ <para>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.</para>
<title>Quaternion Math</title>
<para>Quaternions are equivalent to orientation matrices. You can compose two
orientation quaternions using a special operation called <glossterm>quaternion
- multiplication</glossterm>.</para>
- <!--TODO: Equation: two quaternions and the multiplication of them.-->
+ multiplication</glossterm>. Given the quaternions <literal>a</literal> and
+ <literal>b</literal>, the product of them is:</para>
+ <title>Quaternion Multiplication</title>
+ <imagedata fileref="QuaternionMultiplication.svg" format="SVG"/>
<para>If the two quaternions being multiplied represent orientations, then the product
- of them is a composite orientation. Just as with matrix multiplication, only
- specifically for orientations. Like matrix multiplication, quaternion multiplication
- is associative (<informalequation>
+ of them is a composite orientation. This works like matrix multiplication, except
+ only for orientations. Like matrix multiplication, quaternion multiplication is
+ associative (<informalequation>
<mathphrase>(a*b) * c = a * (b*c)</mathphrase>
</informalequation>), but not commutative (<informalequation>
<mathphrase>a*b != b*a</mathphrase>
</informalequation>).</para>
- <para>The main difference that helps us solve the Gimbal lock problem 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.</para>
+ <para>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.</para>
<para>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:</para>
- <!--TODO: Equation: compute rotation matrix from a quaternion.-->
+ <title>Quaternion to Matrix</title>
+ <imagedata fileref="QuaternionToMatrix.svg" format="SVG"/>
+ <para>This does look suspiciously similar to the formula for generating a matrix from an
+ angle/axis rotation.</para>
<title>Composition Type</title>
<para>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
- <para>If you multiply the pitch increase on the left side, then it changes the pitch
- relative to the current orientation. If you multiply the pitch on the right side, it
- changes the pitch relative to the initial space of the vertices. Which is usually
- model space. Since the yaw, pitch, and roll are usually defined relative to the
- current orientation, that is what we need to do.</para>
+ <para>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 <inlineequation>
+ <mathphrase>O*p</mathphrase>
+ </inlineequation></para>
+ <para>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 <inlineequation>
+ <mathphrase>O*R*p</mathphrase>
+ </inlineequation>; the new orientation is <inlineequation>
+ <mathphrase>O*R</mathphrase>
+ </inlineequation>.</para>
+ <title>Yaw Pitch Roll</title>
+ <para>We implement this in the <phrase role="propername">Quaternion YPR</phrase>
+ tutorial. This tutorial doesn't show gimbals, but the same controls exist for yaw,
+ pitch, and roll transformations. Here, pressing the <keycap>SpaceBar</keycap> will
+ switch between right-multiplying the YPR values to the current orientation and
+ left-multiplying them. Post-multiplication will apply the YPR transforms from
+ <title>Quaternion YPR Project</title>
+ <imagedata fileref="Quaternion%20YPR.png"/>
+ <para>The rendering code is pretty straightforward.</para>
+ <title>Quaternion YPR Display</title>
+ <programlisting language="cpp">void display()
+ glClearColor(0.0f, 0.0f, 0.0f, 0.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");
+ <para>Though GLSL does not have quaternion types or quaternion arithmetic, the GLM math
+ library provides both. The <varname>g_orientation</varname> variable is of the type
+ <classname>glm::fquat</classname>, which is a floating-point quaternion. The
+ <function>glm::mat4_cast</function> function converts a quaternion into a 4x4
+ rotation matrix. This stands in place of the series of 3 rotations used in the last
+ <para>In response to keypresses, <varname>g_orientation</varname> is modified, applying
+ a transform to it. This is done with the <function>OffsetOrientation</function>
+ <title>OffsetOrientation Function</title>
+ <programlisting language="cpp">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);
+ g_orientation = g_orientation * offset;
+ g_orientation = offset * g_orientation;
+ g_orientation = glm::normalize(g_orientation);
+ <para>This generates the offset quaternion from an angle and axis. Since the axis is
+ normalized, there is no need to normalize the resulting <varname>offset</varname>
+ quaternion. Then the offset is multiplied into the orientation, and the result is
+ <para>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 <emphasis>world</emphasis> space.
+ Both of these can be useful for different purposes.</para>
<title>Camera-Relative Orientation</title>
+ <para>As useful as model and world space offsetting is, there is one more space that it
+ might be useful to orient from. Camera-space.</para>
+ <para>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.</para>
+ <para>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 <inlineequation>
+ <mathphrase>C*O*p</mathphrase>
+ </inlineequation>.</para>
+ <para>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: <inlineequation>
+ <mathphrase>R*C*O*p</mathphrase>
+ </inlineequation>. That's nice and all, but we want to apply a transform to O, not to
+ <para>Therefore, we need to use our transforms to generate a new orientation offset (N),
+ which will produce the same effect:</para>
+ <para><informalequation>
+ <mathphrase>C*N*O = R*C*O</mathphrase>
+ </informalequation></para>
- <title>Transformation Spaces</title>
- <para>Our scaling transform is interesting, but it does have certain flaws. The most
- important problem is that it causes a scale based on the X, Y and Z axes of the
- initial coordinate system. So what do you do if you want to scale something along
- <para>This is quite simple, really: you use successive transforms. First, you transform
- into a space where the <quote>different axes</quote>
- <emphasis>are</emphasis> the X, Y and Z axes. Then you do your scaling transform.
- After that, you transform back to the original space.</para>
- <para>If the different axes represent an orientation change, you can use a rotation
- matrix. Simply rotate, apply the scale, and rotate back. The last part is done by
- generating the rotation matrix using the same axis, but negating the angle.</para>
- <para>Transforming into a space that is convenient for another transform, applying that
- transform, and then undoing the first transform is a general-purpose operation. The
- matrix that results from this is a transformation in a different space. In the
- scaling case, we want to do the scaling in a different space, so we apply a rotation
- matrix to get into that space, then undo the rotation after applying the
- <para>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 <inlineequation>
- <mathphrase>M<superscript>-1</superscript>TM</mathphrase>
- </inlineequation>.</para>
- <para>The matrix M<superscript>-1</superscript> is the <glossterm>inverse
- matrix</glossterm> of M. The symbol <superscript>-1</superscript> does not
- (strictly) mean to raise the matrix to the -1 power. It means to invert it.</para>
- <para>The inverse of a matrix M is the matrix N such that <inlineequation>
+ <title>Inversion</title>
+ <para>In order to solve this, we need to introduce a new concept. Given a matrix M,
+ there may be a matrix N such that <inlineequation>
<mathphrase>MN = I</mathphrase>
- </inlineequation>, where I is the identity matrix. This can be analogized to the
- scalar multiplicative inverse (ie: reciprocal). The scalar multiplicative inverse of
- X is the number Y such that <inlineequation>
+ </inlineequation>, where I is the identity matrix. If there is such a matrix, the
+ matrix N is called the <glossterm>inverse matrix</glossterm> of M. The notation for
+ the inverse matrix of M is M<superscript>-1</superscript>. The symbol
+ <quote><superscript>-1</superscript></quote> does not mean to raise the
+ matrix to the -1 power; it means to invert it.</para>
+ <para>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 <inlineequation>
<mathphrase>XY = 1</mathphrase>
</inlineequation>.</para>
<para>In the case of the scalar inverse, this is very easy to solve for: <inlineequation>
<mathphrase>Y = 1/X</mathphrase>
- </inlineequation>. Even in this case, there are values of X for which there is no
- multiplicative inverse. OK, there's <emphasis>one</emphasis> such value of X:
+ </inlineequation>. Easy though this may be, there are values of X for which there is
+ no multiplicative inverse. OK, there's <emphasis>one</emphasis> such real value of
<para>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 <emphasis>lot</emphasis> of matrices with no inverse. Also, computing the inverse
the component matrices. But you have to do the matrix multiplication in
<emphasis>reverse</emphasis> order. So if we have <inlineequation>
<mathphrase>M = TRS</mathphrase>
- </inlineequation>, then M<superscript>-1</superscript> is <inlineequation>
+ </inlineequation>, then <inlineequation>
<mathphrase>M<superscript>-1</superscript> =
S<superscript>-1</superscript>R<superscript>-1</superscript>T<superscript>-1</superscript></mathphrase>
</inlineequation>.</para>
+ <para>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 <inlineequation>
+ <mathphrase>sin(-θ) = -sin(θ)</mathphrase>
+ </inlineequation>. It is also the case that <inlineequation>
+ <mathphrase>cos(-θ) = cos(θ)</mathphrase>
+ </inlineequation>. 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
+ <para>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.</para>
+ <para>In quaternion lingo, the inverse quaternion is more correctly called the
+ <glossterm>conjugate quaternion.</glossterm> We use the same inverse notation,
+ <quote><superscript>-1</superscript>,</quote> to denote conjugate
+ <para>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.</para>
+ <title>Solution</title>
+ <para>Given our new knowledge of inverse matrices, we can solve our problem.</para>
+ <para><informalequation>
+ <mathphrase>C*N*O = R*C*O</mathphrase>
+ </informalequation></para>
+ <para>We can right-multiply both sides of this equation by the inverse transform of
+ <mathphrase>(C*N*O)*O<superscript>-1</superscript> =
+ (R*C*O)*O<superscript>-1</superscript></mathphrase>
+ <mathphrase>C*N*I = R*C*I</mathphrase>
+ <para>The I is the identity transform. From here, we can left-multiply both sides by the
+ inverse transform of C:</para>
+ <mathphrase>C<superscript>-1</superscript> *(C*N) =
+ C<superscript>-1</superscript>*(R*C)</mathphrase>
+ <mathphrase>N = C<superscript>-1</superscript>*(R*C)</mathphrase>
+ <para>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
+ <title>Transformation Spaces</title>
+ <para>It turns out that this is a generalized operation. It can be used for much more
+ than just orientation changes.</para>
+ <para>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?</para>
+ <para>You rotate the object into a coordinate system where the axis you want to scale
+ <emphasis>is</emphasis> one of the basis axes, perform your scale, then rotate
+ it back with the inverse of the previous rotation.</para>
+ <para>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.</para>
+ <para>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 <inlineequation>
+ <mathphrase>M<superscript>-1</superscript>*T*M</mathphrase>
+ </inlineequation>.</para>
+ <title>Final Orientation</title>
+ <para>Let's look at how this all works out in code, with the <phrase role="propername"
+ >Camera Relative</phrase> tutorial. This works very similarly to the last
+ tutorial, but with a few differences.</para>
+ <para>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 <keycap>I</keycap> and <keycap>K</keycap>
+ keys will move the camera up and down, relative to a center point. The
+ <keycap>J</keycap> and <keycap>K</keycap> keys will move the camera left and
+ right around the center point. Holding <keycap>Shift</keycap> with these keys will
+ move the camera in smaller increments.</para>
+ <para>The <keycap>SpaceBar</keycap> will toggle between three transforms: model-relative
+ (yaw/pitch/roll-style), world-relative, and camera-relative.</para>
+ <para>Our scene also includes a ground plane as a reference.</para>
+ <title>Camera Relative Project</title>
+ <imagedata fileref="Camera%20Relative.png"/>
+ <para>The <function>display</function> 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.</para>
+ <para>The substantive changes were in the <function>OffsetOrientation</function>
+ <title>Camera Relative OffsetOrientation</title>
+ <programlisting language="cpp">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);
+ g_orientation = g_orientation * offset;
+ g_orientation = offset * g_orientation;
+ 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;
+ g_orientation = glm::normalize(g_orientation);
+ <para>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
+ <function>glm::quat_cast</function>.</para>
+ <para>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.</para>
<title>Interpolation</title>
- <para>One other trick you can do with quaternions is this.</para>
+ <para>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.</para>
+ <para>What if we want to generate an orientation that is halfway between them, for some
+ definition of <quote>halfway</quote>? 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.</para>
+ <para>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.</para>
+ <para>The <phrase role="propername">Interpolation</phrase> tutorial demonstrates this. The
+ <keycap>Q</keycap>, <keycap>W</keycap>, <keycap>E</keycap>, <keycap>R</keycap>,
+ <keycap>T</keycap>, <keycap>Y</keycap>, and <keycap>U</keycap> keys cause the ship
+ to interpolate to a new orientation. Each key corresponds to a particular orientation,
+ and the <keycap>Q</keycap> key is the initial orientation.</para>
+ <para>We can see that there are some pretty reasonable looking transitions. The transition
+ from <keycap>Q</keycap> to <keycap>W</keycap>, for example. However, there are some
+ other transitions that don't look so good; the <keycap>Q</keycap> to <keycap>E</keycap>
+ transition. What exactly is going on?</para>
+ <title>The Long Path</title>
+ <para>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.</para>
+ <para>While unit quaternions do represent orientations, a quaternion is not a
+ <emphasis>unique</emphasis> representation of an orientation. That is, there are
+ multiple quaternions that represent the same orientation. Well, there are
+ <para>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.</para>
+ <para>While the two quaternions represent the same orientation, they aren't the same as
+ far as interpolation is concerned. Consider a two-dimensional case:</para>
+ <!--TODO: Picture of a vector q1, a vector q2, and -q1. The angle between -q1 and q2 should be less than 90 degrees.-->
+ <para>If the angle between the two quaternions is greater than 90°, then the
+ interpolation between them will take the <quote>long path</quote> between the two
+ orientations. Which is what we see in the <keycap>Q</keycap> to <keycap>E</keycap>
+ transition. The orientation <keycap>R</keycap> is the negation of
+ <keycap>E</keycap>; if you try to interpolate between them, nothing changes. The
+ <keycap>Q</keycap> to <keycap>R</keycap> transition looks much better
+ <para>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.</para>
+ <title>Interpolation Speed</title>
+ <para>There is another problem. Notice how fast the <keycap>Q</keycap> to
+ <keycap>E</keycap> interpolation is. It starts off slow, then rapidly spins
+ around, then slows down towards the end. Why does this happen?</para>
+ <para>The linear interpolation code looks like this:</para>
+ <title>Quaternion Linear Interpolation</title>
+ <programlisting language="cpp">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);
+ <para>GLM's quaternion support does something unusual. The W component is given
+ first to the <type>fquat</type> constructor. Be aware of that when looking
+ through the code.</para>
+ <para>The <function>Vectorize</function> function simply takes a quaternion and returns
+ a <type>vec4</type>; this is necessary because GLM <type>fquat</type> don't support
+ many of the operations that GLM <type>vec4</type>'s do. In this case, the
+ <type>glm::mix</type> function, which performs component-wise linear
+ <para>Each component of the vector is interpolated separately from the rest. The
+ quaternion for <keycap>Q</keycap> is (0.7071f, 0.7071f, 0.0f, 0.0f), while the
+ quaternion for <keycap>E</keycap> 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
+ <para>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 <emphasis>direction</emphasis> of the 4D vector is. Which is why it
+ moves very fast in the middle: the direction is changing rapidly.</para>
+ <para>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 <glossterm>spherical linear
+ interpolation</glossterm> or <glossterm>slerp</glossterm>.</para>
+ <para>To see the difference this makes, press the <keycap>SpaceBar</keycap>; this
+ toggles between regular linear interpolation and slerp. The slerp version is much
+ <para>The code for slerp is rather complex:</para>
+ <title>Spherical Linear Interpolation</title>
+ <programlisting>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);
+ <title>Slerp and Performance</title>
+ <para>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
+ <para>The <function>acos</function> 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.</para>
<para>Quaternions work almost identically to matrices, in so far as they specify
- orientations. They can be constructed directly from an angle/axis
+ orientations. They can be constructed directly from an angle/axis rotation, and
+ they can be composed with one another via quaternion multiplication.</para>
<para>One can transform a matrix, or a quaternion with another matrix or quaternion,
camera-space,, while the object's orientation remains a model-to-world
+ <para>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.</para>
+ <title>Further Study</title>
+ <para>Try doing the following with the orientation tutorials.</para>
+ <para>Modify the Interpolation tutorial to allow multiple animations to be
+ active simultaneously. The <classname>Orientation</classname> class is
+ already close to being able to allow this. Instead of storing a single
+ <classname>Orientation::Animation</classname> object, it should store a
+ <classname>std::deque</classname> 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 <classname>Orientation</classname>
+ class. To get the orientation, just call each animation's orientation
+ function, feeding the previous result into the next one.</para>
+ <para>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.</para>
+ <title>Further Research</title>
+ <para>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
+ <emphasis>is.</emphasis> 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.</para>
+ <para>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.</para>
<?dbhtml filename="Tut08 Glossary.html" ?>
+ <glossterm>quaternion</glossterm>
+ <para>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.</para>
+ <glossterm>inverse matrix</glossterm>
+ <para>The inverse matrix of the matrix M is the matrix N for which the following
+ equation is true: <inlineequation>
+ <mathphrase>MN = I</mathphrase>
+ </inlineequation>, where I is the identity matrix. The inverse of M is
+ usually denoted as M<superscript>-1</superscript>.</para>
+ <glossterm>conjugate quaternion</glossterm>
+ <para>Analogous to the inverse matrix. It is computed by negating the vector
+ part of the quatenrion.</para>
+ <glossterm>spherical linear interpolation, slerp</glossterm>
+ <para>Interpolation between two unit vectors that is linear based on the angle
+ between them, rather than the vectors themselves.</para>