10. Quaternions#
We saw in 5. Transformations that we can calculate a transformation matrix to rotate about a vector. This matrix was derived by compositing three individual rotations about the three co-ordinate \(x\), \(y\) and \(z\) axes.
Fig. 10.1 The pitch, yaw and roll Euler angles.#
The angles that we use to define the rotation around each of the axes are known as Euler angles and we use the names pitch, yaw and roll for the rotation around the \(x\), \(y\) and \(z\) axes respectively. The problem with using a composite of Euler angles rotations is that for certain alignments we can experience gimbal lock where two of the rotation axes are aligned leading to a loss of a degree of freedom causing the composite rotation to be locked into a 2D rotation.
Quaternions are a mathematical object that can be used to perform rotation operations that do not suffer from gimbal lock and require fewer floating point calculations. There is quite a lot of maths used here but in this page I’ve focussed only on the bits you need to know to apply quaternions. If you are interested in the derivations of the various equations see Appendices - Complex Numbers and Quaternions.
Compile and run the project and you will see that we have the scene consisting of the cubes last seen in 7. Moving the Camera.

10.1. Complex Numbers#
Before we delve into quaternions we must first look at complex numbers. Consider the following equation
Solving using simple algebra gives
Here we have a problem since the square of a negative number always returns a positive value, e.g., \((-1) \times (-1) = 1\), so there does not exist a real number to satisfy the solution to this equation. Not being satisfied with this, mathematicians invented another type of number called the imaginary number that is defined by \(i = \sqrt{-1}\) so the solution to the equation above is \(x = i\).
Note
Some students find the concept of an imaginary number difficult to grasp. However, you have been using negative numbers for a while now and these are similar to the imaginary number since they do not represent a physical quantity, e.g., you can show me 5 coins but you cannot show me negative 5 coins. We developed negative numbers to help us solve problems, as we have also done with the imaginary number.
Imaginary numbers can be combined with real numbers to give us a complex number where a real number is added to a multiple of the imaginary number
where \(x\) and \(y\) are real numbers, \(x\) is known as the real part and \(y\) is known as the imaginary part of a complex number.
Since a complex number consists of two parts we can plot them on a 2D space called the complex plane where the horizontal axis is used to represent the real part and the vertical axis is used to represent the imaginary part (Fig. 10.2).
Fig. 10.2 The complex number \(z = x + yi\) plotted on the complex plane.#
We can see from Fig. 10.2 that a complex number \(z = x + yi\) can be thought of as a 2D vector pointing from \((0,0)\) to \((x, y)\). The length of this vector is known as the magnitude of \(z\) denoted by \(|z|\) and calculated using
10.1.1. Rotation using complex numbers#
A very useful property of complex numbers, and the reason why we are interested in them, is that multiplying a number by \(i\) rotates the number by \(90^\circ\) in the complex plane. For example consider the complex number \(2 + i\), multiplying repeatedly by \(i\)
So after mulitplying \(2 + i\) by \(i\) four times we are back to where we started. Fig. 10.3 shows these complex numbers plotted on the complex plane. Note how they have been rotated by \(90^\circ\) each time.
Fig. 10.3 Rotation of the complex number \(2 + i\) by repeated multiplying by \(i\).#
So we have seen that multiplying a number by \(i\) rotates it by 90\(^\circ\), so how do we rotate a number by a different angle? Fig. 10.4 shows the rotation of the number 1 by \(\theta\) anti-clockwise in the complex plane.
Fig. 10.4 The complex number \(z\) is the real number 1 rotated \(\theta\) anti-clockwise in the complex plane.#
Recall that \(\cos(\theta) = \dfrac{adjacent}{hypotenuse}\) and \(\sin(\theta) = \dfrac{opposite}{hypotenuse}\) and since the hypotenuse is 1 then
This means we can rotate by an arbitrary angle \(\theta\) in the complex plane by multiplying by \(z\).
10.2. Quaternions#
A quaternion is an extension of a complex number where two additional imaginary numbers are used to extend from a 2D space to a 4D space. The general form of a quaternion is
where \(w\), \(x\), \(y\) and \(z\) are real numbers and \(i\), \(j\) and \(k\) are imaginary numbers which are related to \(-1\) and each other by
Quaternions are more commonly represented in scalar-vector form
where \(\mathbf{v} = (x, y, z)\).
We are going to create a Quaternion class so that we can work with quaternions. In the maths.hpp header file, add the following class declaration before the Maths class declaration (it needs to come before the Maths class since later we will be adding commands to the Maths class that use quaternions).
// Quaternion class
class Quaternion
{
public:
float w, x, y, z;
// Constructors
Quaternion();
Quaternion(const float w, const float x, const float y, const float z);
};
Here we have declared a class with four attributes fo the \(w\), \(x\), \(y\) and \(z\) parts of a quaternion along with two constructors.
Then, in the maths.cpp file, define the constructors
// Quaternions
Quaternion::Quaternion() {}
Quaternion::Quaternion(const float w, const float x, const float y, const float z)
{
this->w = w;
this->x = x;
this->y = y;
this->z = z;
}
10.3. Rotations using quaternions#
We saw above that we can rotate a number in the complex plane by multiplying by the complex number
We can do similar in 3D space where the rotation of the vector \(\mathbf{p}\) around a unit vector \(\hat{\mathbf{v}}\) by the angle \(\theta\) can be achieved by calculating \(qpq^*\) using the following quaternions (see Appendix: Multiplying quaternions for the how to multiply quaternions)
See Appendix: Quaternion rotation for the derivation of this formula.
Fig. 10.5 Axis-angle rotation.#
We have been using \(4 \times 4\) matrices to compute the transformations to convert between model, view and screen spaces so in order to use quaternions for rotations we need to calculate a \(4 \times 4\) rotation matrix that is equivalent to \(qpq^*\). If \(q = [\cos(\tfrac{1}{2}\theta), \sin(\tfrac{1}{2}\theta) \hat{\mathbf{v}}] = [w, (x, y, z)]\) is the rotation quaternion, then the corresponding rotation matrix is
where \(s = \dfrac{2}{w^2 + x^2 + y^2 + z^2}\) (see Appendix: Rotation matrix for the derivation of this matrix).
In the maths.hpp file add the following method declaration to the Quaternion class
glm::mat4 matrix();
Then in the maths.cpp file add the following method definition
glm::mat4 Quaternion::matrix()
{
float s = 2.0f / (w * w + x * x + y * y + z * z);
float xs = x * s, ys = y * s, zs = z * s;
float xx = x * xs, xy = x * ys, xz = x * zs;
float yy = y * ys, yz = y * zs, zz = z * zs;
float xw = w * xs, yw = w * ys, zw = w * zs;
glm::mat4 rotate;
rotate[0][0] = 1.0f - (yy + zz);
rotate[0][1] = xy + zw;
rotate[0][2] = xz - yw;
rotate[1][0] = xy - zw;
rotate[1][1] = 1.0f - (xx + zz);
rotate[1][2] = yz + xw;
rotate[2][0] = xz + yw;
rotate[2][1] = yz - xw;
rotate[2][2] = 1.0f - (xx + yy);
return rotate;
}
We can now calculate the rotation matrix for a rotation quaternion q
using q.matrix()
. Comparing this code to the definition of rotate()
in the maths.cpp file we can see the quaternion rotation matrix requires 16 multiplications compared to 24 multiplications to calculate the rotation matrix based on the composite of three separate rotations about the \(x\), \(y\) and \(z\) axes and a translation. Efficiency is always a bonus, but the main advantage is the quaternion rotation matrix does not suffer from gimbal lock.
So it makes sense to use the quaternion rotation matrix for our axis-angle rotations. Edit the rotate()
function definition, so that is looks like the following.
glm::mat4 Maths::rotate(const float &angle, glm::vec3 v)
{
v = glm::normalize(v);
float c = cos(0.5f * angle);
float s = sin(0.5f * angle);
Quaternion q(c, s * v.x, s * v.y, s * v.z);
return q.matrix();
}
Here we normalise the vector which we are rotating around before calculating the rotation quaternion q
and returning its rotation matrix using equation (10.2)
Compile and run your program, and you should see that nothing has changed. This is good news as we are now using efficient quaternion rotation to rotate the cubes and don’t have to worry about gimbal lock.
10.3.1. Calculating a quaternion from Euler angles#
Quaternions can be thought of as an orientation in 3D space. Imagine a camera in the world space that is pointing in a particular direction. The direction in which the camera is pointing can be described with reference to the \(x\), \(y\) and \(z\) axes in terms of the \(pitch\) and \(yaw\) Euler angles. Using the following abbreviations
then the quaternion that represents the camera orientation is
See Appendix: Euler angles to quaternion for the derivation of this equation. We are going to add constructor to our quaternion class to create a quaternion from Euler angles. Add the following to the Quaternion class declaration in maths.hpp
Quaternion(const float pitch, const float yaw);
and in the maths.cpp define the constructor
Quaternion::Quaternion(const float pitch, const float yaw)
{
float cosPitch = cos(0.5f * pitch);
float sinPitch = sin(0.5f * pitch);
float cosYaw = cos(0.5f * yaw);
float sinYaw = sin(0.5f * yaw);
this->w = cosPitch * cosYaw;
this->x = sinPitch * cosYaw;
this->y = cosPitch * sinYaw;
this->z = sinPitch * sinYaw;
}
10.4. A Quaternion camera#
We are currently using Euler angles rotation to calculate the view matrix in the calculateMatrices()
Camera class function (see 6. 3D worlds). As such our camera may suffer from gimbal lock, and it also does not allow us to move the camera through 90\(^\circ\) or 270\(^\circ\) (try looking at the cubes from directly above or below, you will notice the orientation suddenly flipping around – see the video below). So it would be advantageous to use quaternion rotations to calculate the view matrix.
To implement a quaternion camera we calculate a quaternion from the camera Euler angles that represents the current orientation of the camera. We can then use the rotation matrix for this quaternion, along with a translation transformation to move the camera to \((0, 0, 0)\), to calculate the view matrix, i.e.,
In the camera.hpp header file declare the camera orientation quaternion attribute.
// Quaternion camera
Quaternion orientation = Quaternion(pitch, yaw);
We are going to write a Camera class method for a quaternion camera, add the method declaration to the Camera class
void quaternionCamera();
and define the method in the camera.cpp file
void Camera::quaternionCamera()
{
// Calculate camera orientation quaternion from the Euler angles
Quaternion orientation(-pitch, yaw);
// Calculate the view matrix
view = orientation.matrix() * Maths::translate(-eye);
// Calculate the projection matrix
projection = glm::perspective(fov, aspect, near, far);
// Calculate camera vectors from view matrix
right = glm::vec3(view[0][0], view[1][0], view[2][0]);
up = glm::vec3(view[0][1], view[1][1], view[2][1]);
front = -glm::vec3(view[0][2], view[1][2], view[2][2]);
}
Here the camera orientation quaternion is calculated from the \(pitch\) and \(yaw\) Euler angles. We then combine a translation by \(-\mathbf{eye}\) so that the camera is at the origin and then rotate using the rotation matrix for the orientation quaternion (remember this is how the view matrix was derived in 6. 3D Worlds). We also need to calculate the \(\mathbf{right}\), \(\mathbf{up}\) and \(\mathbf{front}\) camera vectors using the orientation quaternion. Recall that the view matrix given in equation (6.1) is
So we just extract \(\mathbf{right}\), \(\mathbf{up}\) and \(\mathbf{front}\) from the first three columns of the view matrix.
We also need to change the initial \(yaw\) angle from \(-90^\circ\) to \(0^\circ\). In the camera.hpp change the \(yaw\) angle declaration to the following
float yaw = 0.0f;
Finally, replace the call to the calculateMatrices()
method in the Lab10_Quaternions.cpp file with the following so that we are now using our quaternion camera.
camera.quaternionCamera();
Compile and run the code and you will see that you can move the camera in any orientation and we can move the camera through 90\(^\circ\) or 270\(^\circ\) without the orientation flipping around.
10.5. SLERP#
The another advantage that quaternions have over Euler angles is that we can interpolate between two quaternions smoothly and without encountering the problem of gimble lock. Standard Linear intERPolation (LERP) is used to calculate an intermediate position on the straight line between two points.
Fig. 10.6 Linear interpolation between two points.#
If \(\mathbf{v}_1\) and \(\mathbf{v}_2\) are two points then another point, \(\mathbf{v}_t\), that lies on the line between \(\mathbf{v}_1\) and \(\mathbf{v}_2\) is calculated using
where \(t\) is a value between 0 and 1.
SLERP stands for Spherical Linear intERPpolation and is a method used to interpolate between two quaternions across a surface of a sphere.
Fig. 10.7 SLERP interpolation between two points on a sphere.#
Consider Fig. 10.7 where \(q_1\) and \(q_2\) are two quaternions emanating from the centre of a sphere (note that this diagram is a bit misleading as quaternions exist in 4 dimensions but since it’s very difficult to visualize 4D on a 2D screen this will have to do). The interpolated quaternion \(q_t\) represents another quaternion that is partway between \(q_1\) and \(q_2\) calculated using
where \(t\) is a value between 0 and 1 and \(\theta\) is the angle between the two quaternions and is calculated using
where \(q_1 \cdot q_2\) is the dot product between the two quaternions and calculated in the same way as the dot product between two 4-element vectors. Sometimes \(q_1 \cdot q_2\) returns a negative result meaning that \(\theta\) we will be interpolating the long way round the sphere. To overcome this we negate the values of one of the quaternions, this is fine since the quaternion \(-q\) is the same orientation as \(q\).
Another consideration is when \(\theta\) is very small then \(\sin(\theta)\) in equation (10.4) can be rounded to zero causing a divide by zero error. To get around this we can use LERP between \(q_1\) and \(q_2\).
Add a method declaration to the Maths class in the maths.hpp
file
static Quaternion SLERP(const Quaternion q1, const Quaternion q2, const float t);
and define the method in the maths.cpp
file
// SLERP
Quaternion Maths::SLERP(Quaternion q1, Quaternion q2, const float t)
{
// Calculate cos(theta)
float cosTheta = q1.w * q2.w + q1.x * q2.x + q1.y * q2.y + q1.z * q2.z;
// If q1 and q2 are close together return q2 to avoid divide by zero errors
if (cosTheta > 0.9999f)
return q2;
// Avoid taking the long path around the sphere by reversing sign of q2
if (cosTheta < 0)
{
q2 = Quaternion(-q2.w, -q2.x, -q2.y, -q2.z);
cosTheta = -cosTheta;
}
// Calculate SLERP
Quaternion q;
float theta = acos(cosTheta);
float a = sin((1.0f - t) * theta) / sin(theta);
float b = sin(t * theta) / sin(theta);
q.w = a * q1.w + b * q2.w;
q.x = a * q1.x + b * q2.x;
q.y = a * q1.y + b * q2.y;
q.z = a * q1.z + b * q2.z;
return q;
}
Then to apply SLERP replace the code used to calculate the orientation
quaternion in the quaternionCamera()
method with the following.
// Calculate camera orientation quaternion from the Euler angles
Quaternion newOrientation(-pitch, yaw);
// Apply SLERP
orientation = Maths::SLERP(orientation, newOrientation, 0.2f);
Here we use a temporary quaternion newOrientation
which is calculated using the \(pitch\), \(yaw\) and \(roll\) Euler angles of the camera and then used SLERP to interpolate between orientation
and newOrientation
. Note that here we are using \(t = 0.2\). This parameter determines how far towards the new orientation we are interpolating. Compile and run your program and you should see that the camera rotation is much smoother and more satisfying to use.
10.6. Third person camera#
The use of quaternions allows game developers to implement third person camera view in 3D games where the camera follows the character that the player is controlling. This was first done for the Playstation game Tomb Raider released by Core Design in 1996 and has become popular with game developers with game franchises such as God of War, Horizon Zero Dawn, Assassins Creed and Red Dead Redemption to name a few all using third person camera view.
Fig. 10.8 A third person camera that follows a character.#
To implement a simple third person camera, we calculate the view matrix as usual and then move the camera back by translating by an \(\mathbf{offset}\) vector Fig. 10.8.
The result of a third-person camera view can be seen below. Here we are using Suzanne the Blender mascot to act as our character model, and we can switch from first-person to third-person view using keyboard input.
Moving the camera around we see that our character model is always facing in the same direction. To make it face in the same direction as the camera we combine pitch and yaw rotations, and use them in the model matrix calculation for the character model.
Implementations of a third-person camera can vary. For example, you may want the character movement to be independent of the camera movement so that the camera is not always behind the character. To do this we would calculate the view matrix for a third-person camera as seen above, but calculate a different orientation for the character based on a different \(yaw\) angle that can be altered using keyboard inputs (Fig. 10.9).
Fig. 10.9 A third person camera that is independent of the character orientation.#
10.7. Exercises#
Add the ability for the user to switch between view modes where pressing the 1 key selects first-person camera and pressing the 2 key selects a third person camera. In third-person camera mode the camera should follow the character and point in the same direction as the character is facing.
The Suzanne model and textures can be downloaded from the GitHub repository (this was only added recently so you might not have it).
Add the ability for the user to select a different third-person camera mode by pressing the 3 key. In this mode, the camera should be independent of the character movement where it can rotate around the character based on the camera \(yaw\) and \(pitch\) angles. The character movement direction should be governed by a character \(yaw\) angle that can be altered by the A and D keys.
10.8. Video walkthrough#
The video below walks you through these lab materials.