Do you want to rotate objects in 3D space without running into gimbal lock issues? Relatable.

Do you want to show off a complex mathematical concept that spans imaginary numbers and four-dimensional space to your loved one and have them say “Erm… that’s nice dear? It’s just turning around?”. Relatable.

In any case, join me in this blog post as I will try my best at giving a full on explanation about Quaternions, what they are and how to use them for rotating objects in 3D space.

What are Quaternions?

Quaternions are defined using four components: one real part and three imaginary parts. Their general form is:

where

But you may also see them as ordered pairs in the literature:

where

What do they represent?

Stick with me for that looong explanation, for it will be worth it. Let’s go back for a moment to complex numbers.

Complex numbers are made up of a real and an imaginary part. For example:

where and is the imaginary unit with the property that .

Initially mathematicians came up with complex numbers to solve equations that didn’t have solutions in the real number system like

???

So what did they do? They invented the imaginary unit such that:

And now we can solve the equation:

So is a number that when squared gives -1. Cool, right? It even is periodic:

… and so on.

It has a nice cyclical behavior, doesn’t it? What if we graphed that on a 2D plane? We would get something like this:

Complex Plane

Here the intuition is that multiplying by corresponds to a 90-degree rotation in the complex plane. So complex numbers can represent rotations in 2D space. In fact, we can represent any rotation in 2D space using a rotor:

where is the angle of rotation in radians. Multiplying a complex number by this rotor rotates the point by

radians around the origin. Which leads us to the formula for rotating a point :

Translating that back to the normal 2D plane, we have the first real part as the new x coordinate and the second imaginary part as the new y coordinate. Note that for rotating around an arbitrary point, you would need to translate the point to the origin, perform the rotation, and then translate it back.

Okay cool but what about Quaternions?

Quaternions extend this idea to 3D space. While complex numbers use one imaginary unit () for 2D rotations, quaternions use three imaginary units () for 3D rotations. It’s the natural next step up in dimensions.

Let’s dive in. Hamilton’s work establishes that the quaternions are defined by the following multiplication rules:

And this is what you’ll first see on the wikipedia page about quaternions. But what does that even mean? For the first part, it’s easy, we have seen that before with complex numbers. (). The other two are similar, just for the other imaginary units. But now:

Where does THAT come from? It was Hamilton “creative leap” that bridged the gap from complex numbers to quaternions, treat it like a “chicken and egg” situation where he needed the relationship to define the units and now he had the units to define the relationship. How he got there is beyond me. But that relationship implies the following multiplication rules:

and and and

Notice that multiplication of quaternions units is not commutative. The order matters. Because first rotating around one axis and then another is not the same as doing it in the opposite order. Which is exactly what we want for 3D rotations.

Now with those rules we can develop an intuition again. If we plot the imaginary parts as axes in 3D space, as unit vectors, we can see that multiplying two of them results in a third one, perpendicular to the first two, as we would expect from the established multiplication rules, which for vectors would be the cross product:

Quaternion Axes

If you aren’t familiar with cross products, the quick explanation is that the cross product of two vectors results in a third vector that is perpendicular to both of the original vectors. The direction of the resulting vector is determined by the right-hand rule.

Right Hand Rule

Keep in mind that this is just an intuition aid. Quaternions are not vectors, they are 4D numbers. But the analogy helps in understanding how the imaginary parts relate to 3D space. So as we did with complex numbers, we can think of those imaginary parts as representing directions in 3D space as we multiply them together.

Okay let’s go skate on the 4th dimension now 🛹

Because we are going to flow over many definitions and concepts which are beyond the scope of this blog post to explain in full detail. Read this resource if you want a deep dive into the math behind quaternions.

To represent no rotation, we need a quaternion that acts like the identity element in multiplication. According to wikipedia, that is:

Building on our previous intuition from complex numbers, and much like how complex numbers can represent 2D rotations with a general formula, quaternions can represent 3D rotations using the general form of rotation quaternion:

where:

Seems familiar?

Yep, it’s the same idea. So multiplying the identity quaternion by this rotation quaternion will give a new quaternion that represents a rotation of radians around the axis . And much like the sick grinds we do in skateboarding, we can chain multiple rotations together by multiplying the corresponding rotation quaternions. The order of multiplication matters, as we saw earlier when building our intuition about the non-commutative nature of quaternion multiplication.

While this would work to say rotate something on itself, it’s not all we need. We want to rotate points in 3D space around the origin. And we can do just that using the general form of rotation quaternion:

where:

Okay so several things to unpack here. Which I won’t go into full detail because this would get outside the scope of this blog post:

Now why do we “sandwich” the point between and ? Basically multiplying by only gives a resulting quaternion that is not pure (has a scalar part) which represent something outside 3D space. But as Hamilton figured out, by multiplying by the inverse we “undo” that extra part and get back to a pure quaternion that represents a point in 3D space. That’s also why we need to use half angle in the rotation quaternion, because the sandwich product effectively doubles the angle of rotation.

You still with me? Not too rough of a landing? Okay, let’s ride on the next more concrete part.

In the code!

So I’m not going to implement quaternions from scratch here. In my Rust project I use cgmath which has built-in support for quaternions. But the concepts are the same in any language or library that supports quaternions. My usecase was to move my camera using quaternions for orientation to avoid gimbal lock issues. Because, when you encounter those issues, well:

// TODO insert gimbal example here and the code that causes it

But with quaternions, we can avoid that. Here’s how I did it:

pub fn rotate_on_self(&mut self, axis: cgmath::Vector3<f32>, angle_radians: f32) {
let half_angle = angle_radians * 0.5;
let axis_norm = axis.normalize();
let rotation = cgmath::Quaternion::new(
half_angle.cos(),
half_angle.sin() * axis_norm.x,
half_angle.sin() * axis_norm.y,
half_angle.sin() * axis_norm.z,
);
self.orientation = (rotation * self.orientation).normalize();
}

This function rotates the camera on itself around a given axis by a given angle in radians:

Here are the results of using quaternions for camera rotation when I call it like so every frame:

camera.rotate_on_self(
cgmath::Vector3::unit_y(),
3.5f32.to_radians(),
);

Self rotation Y axis

Amazing.

Now to rotate an object around an arbitrary point in space, we can use the sandwich product we discussed earlier. Here’s how I implemented it:

pub fn rotate_around(&mut self, target: cgmath::Point3<f32>, axis: cgmath::Vector3<f32>, angle_radians: f32) {
let half_angle = angle_radians * 0.5;
let axis_norm = axis.normalize();
let rotation = cgmath::Quaternion::new(
half_angle.cos(),
half_angle.sin() * axis_norm.x,
half_angle.sin() * axis_norm.y,
half_angle.sin() * axis_norm.z,
);

// Translate position to be relative to target, apply rotation, then translate back
let relative_pos = self.position - target;
let new_relative_position = (rotation
* cgmath::Quaternion::new(0.0, relative_pos.x, relative_pos.y, relative_pos.z)
* rotation.conjugate()).v;
let new_position = new_relative_position + target.to_vec();
self.position = cgmath::Point3::new(new_position.x, new_position.y, new_position.z);

// Also rotate the orientation of the camera by the same rotation to have it face the correct direction
// Note here that we always multiply the new local rotation on the left side of the existing orientation.
// Otherwise, we would be rotating in world space instead of local space.
self.orientation = (rotation * self.orientation).normalize();
}

This function rotates the camera around a target point in space:

Rotate around x

Here we rotate the camera around the x axis of the target point which is at the center of the screen.

Rotate around y

Here we rotate the camera around the y axis of the target point which is at the center of the screen.

Rotate around z

Here we rotate the camera around the z axis of the target point which is at the center of the screen.

Rotate around x and y

Here we rotate the camera around both the x and y axes of the target point which is at the center of the screen. I just applied the two rotations sequentially in the same frame:

camera.rotate_around(
center,
cgmath::Vector3::unit_y(),
0.5f32.to_radians(),
);
camera.rotate_around(
center,
cgmath::Vector3::unit_x(),
0.5f32.to_radians(),
);

Rotate around a specific point

Here we rotate the camera around the y axis of a target point that is offset from the center of the screen by (-0.5, 0, 0).

And there you have it! Quaternions in action for smooth, gimbal lock-free 3D rotations. I hope first it made sense then it wasn’t too painful. If you have any questions or want to discuss more about quaternions or 3D math in general, feel free to reach out at inoukakis@gmail.com.

Cheers!

References