Let's remove Quaternions from every 3D Engine


(An Interactive Introduction to Rotors from Geometric Algebra)



To represent 3D rotations graphics programmers use Quaternions. However, Quaternions are hard to understand because they are taught at face value. We sort of just accept their odd multiplication tables and other arcane definitions and use them as black boxes that rotate vectors in the ways we want. Why does $\mathbf{i}^2=\mathbf{j}^2=\mathbf{k}^2=-1$ and $\mathbf{i} \mathbf{j} = \mathbf{k}$? Why do we take a vector and upgrade it to an "imaginary" vector in order to transform it, like $\mathbf{q} (x\mathbf{i} + y\mathbf{j} + z \mathbf{k}) \mathbf{q}^{*}$? Who cares as long as it rotates my vectors the right way, right?

There is a way to represents rotations called a Rotor that subsumes both Complex Numbers (in 2D) and Quaternions (in 3D) and even generalizes to any number of dimensions.

We can build Rotors almost entirely from scratch, instead of defining quaternions out of nowhere and trying to explain how they work retroactively. This takes more time, but I find it is very much worth it because it makes them much easier to understand!

Also, 3D Rotors do not require the use of a fourth dimension of space in order to be visualized and understood.

It would be great if we could start phasing out the use and teaching of Quaternions and replace them with Rotors. The change is simple and the code remains almost the same. Anything you can do with a Quaternion, such as Interpolation and avoiding Gimbal lock, you can do on a Rotor. But the understanding grows a lot.

    (In the following article, every diagram is interactive. The video follows the article, and you can press the buttons to play the relevant section of video. Conversely, you can press the button to go to the section of the article that corresponds to what the video is playing at this moment. You can maximize your window to have more space for the video, or you can press the button to set it to a fixed size.)

    Planes of Rotations

    Rotations happen in 2D planes

    In 3D, we usually think of rotations as happening around an axis, like a wheel turning around its axle, but instead of thinking about the axle a more correct way is to think about the plane that the wheel lies on, perpendicular to the axle.

    Old Lady shows off rotations
    This old lady is spinning wheel in the $\mathbf{xz}$ plane, perpendicular to the $\mathbf{y}$ axis.

    This is because if we split a vector into two pieces, one lying inside the plane ($\mathbf{v}_\parallel$) and one lying outside the plane ($\mathbf{v}_\perp$), the rotation rotates the inside part while keeping the outside part the same.

    Rotation in the $yx$ plane [Drag anywhere to move the camera]

    In 2D there is only one single plane to rotate in (there is no outside part). Therefore considering rotations to happen around a third axis (perpendicular to the 2D plane) is technically incorrect, since we shouldnít need to introduce another dimension to perform rotations.

    If you told a 2D "flatlander" (who lives inside a 2D plane and has only ever experienced 2D) about a perpendicular rotation axis they would look at you and ask "which direction does the axis point along? I can't picture it!"

    Explicit Sense of Rotation

    In addition, when thinking about rotation around an axis, the sense of the rotation is undefined, and so needs to be defined by convention (via the so-called "right hand rule").

    However, if we think about rotations as happening inside planes, the sense is clear: rotation in the $\mathbf{xy}$ plane means a rotation that takes the (unit) vector $\mathbf{x}$ to the (unit) vector $\mathbf{y}$, inside the plane they form together. Rotation in the $\mathbf{yx}$ plane is the opposite rotation: it takes the vector $\mathbf{y}$ to the vector $\mathbf{x}$.

    Bivectors

    The Outer Product

    To compute the axis of rotation to rotate one vector $\mathbf{a}$ to another vector $\mathbf{b}$, we take the cross product of the two vectors to get a vector that is perpendicular to both. But why "leave" the plane, since a rotation is fundamentally a 2D thing?

    Instead we take what is called the outer product (also called exterior, or wedge product) of the two vectors, building a new element called a bivector (or 2-vector) $\mathbf{B}$ that represents the plane the two vectors form together. If the cross product creates the normal vector to a plane, the outer product creates the plane itself. Taking the normal to the plane is extraneous.

    $$\mathbf{B} = \mathbf{a} \wedge \mathbf{b}$$

    $\mathbf{B}$ can be represented as the parallelogram built from the vectors $\mathbf{a}$ and $\mathbf{b}$, in the plane they form together.

    The idea of a bivector might seem a bit strange at first, but they are pretty much as fundamental as vectors, as we will see. If a vector is like a line, then a bivector is like a plane... The properties of the outer product are suited to capture the properties of planes.

    Basis for Bivectors

    Bivectors have components, just like vectors. But they are defined in terms of basis planes instead of basis lines like vectors.

    The three orthogonal basis planes are $\mathbf{x} \wedge \mathbf{y}$, $\mathbf{x} \wedge \mathbf{z}$, and $\mathbf{y} \wedge \mathbf{z}$, as seen on the diagram to the right.

    But first let's look at the simpler 2D case...


    2D Bivectors

    In 2D there is only one plane, the $\mathbf{xy}$ plane. So a 2D bivector only has one component. For a bivector built from vectors $\mathbf{a}$ and $\mathbf{b}$, this number $B_{xy}$ is equal to the (signed) area of the parallelogram the two vectors form together.

    $$\mathbf{B}=\mathbf{a} \wedge \mathbf{b} = B_{xy} (\mathbf{x} \wedge \mathbf{y})$$

    You can play with a 2D bivector in the following interactive diagram, by adjusting the (unit) vectors it is made from:

    You can see that by changing the angle between the vectors the area of the parallelogram changes (according to the sine of the angle).

    If the vectors are the same, or if they are parallel, they don't form a proper plane and the result is zero. This simple property defines what a bivector is:

    $$\mathbf{a} \wedge \mathbf{a} = 0$$

    By looking at the sum of two vectors, we can see that this property implies the following:

    $$\begin{eqnarray}(\mathbf{a}+\mathbf{b}) \wedge (\mathbf{a}+\mathbf{b}) &=& 0 \\ \mathbf{a} \wedge \mathbf{a} + \mathbf{b} \wedge \mathbf{a} + \mathbf{a} \wedge \mathbf{b} + \mathbf{b} \wedge \mathbf{b} &=& 0 \\ \mathbf{b} \wedge \mathbf{a} + \mathbf{a} \wedge \mathbf{b} &=& 0 \end{eqnarray} $$

    Therefore:

    $$\mathbf{a} \wedge \mathbf{b} = -\mathbf{b} \wedge \mathbf{a}$$

    Just like the sense of a rotation matters, the order of the arguments to the outer product matters. Swapping the arguments changes the sign of the result (this is called "anti-symmetric").

    In the diagram, the sign is represented using the color, which changes from blue to green. The sign changes whenever the rotation from $\mathbf{a}$ to $\mathbf{b}$ goes from being clockwise to being anticlockwise (i.e. if it matches the ($\mathbf{x}$ to $\mathbf{y}$) direction or the ($\mathbf{y}$ to $\mathbf{x}$) direction).

    You can see how the properties of the outer product are suited to capture the properties of planes and rotations.

    2D Bivectors from non-unit vectors

    The vectors obviously don't have to be unit lengths, and in this diagram the restriction is removed:

    The signed area of the parallelogram is proportional to the lengths of both vectors: $B_{xy} = sin(\alpha)\|a\|\|b\|$ where $\alpha$ is the angle between $\mathbf{a}$ and $\mathbf{b}$. So for example doubling the length of one vector doubles the area.

    We can get the actual value by plugging in the vectors in component form:

    $$\begin{eqnarray}\mathbf{a} \wedge \mathbf{b} &=& (a_x \mathbf{x} + a_y \mathbf{y}) \wedge (b_x \mathbf{x} + b_y \mathbf{y}) \\ &=& a_x b_x (\mathbf{x} \wedge \mathbf{x}) + a_x b_y (\mathbf{x} \wedge \mathbf{y}) + a_y b_x (\mathbf{y} \wedge \mathbf{x}) + a_y b_y (\mathbf{y} \wedge \mathbf{y}) \\ &=& a_x b_y (\mathbf{x} \wedge \mathbf{y}) + a_y b_x (\mathbf{y} \wedge \mathbf{x}) \\ &=& a_x b_y (\mathbf{x} \wedge \mathbf{y}) - a_y b_x (\mathbf{x} \wedge \mathbf{y}) \\ &=& (a_x b_y - a_y b_x) (\mathbf{x} \wedge \mathbf{y}) \end{eqnarray}$$

    $$B_{xy} = a_x b_y - b_x a_y$$

    3D Bivectors

    Just like the coordinates of a vector $\mathbf{v}$ can be thought of as the projections of the vector onto the three orthogonal basis axes ($\mathbf{x},\mathbf{y},\mathbf{z}$), the coordinates of a bivector $\mathbf{B}$ can be thought of as the projections of the small plane onto the three orthogonal basis planes.

    The projections of the vector are the lengths of that vector along each basis vector, while the projections of the bivector are the areas of the plane on each basis plane.

    For a vector:

    $$\mathbf{v} = \bbox[5px,border-bottom:2px solid red]{v_x} \mathbf{x} + \bbox[5px,border-bottom:2px solid green]{v_y} \mathbf{y} + \bbox[5px,border-bottom:2px solid blue]{v_z} \mathbf{z}$$

    For a bivector:

    $$\mathbf{B} = \bbox[5px,border-bottom:2px solid coral]{B_{xy}} (\mathbf{x} \wedge \mathbf{y}) + \bbox[5px,border-bottom:2px solid gold]{B_{xz}} (\mathbf{x} \wedge \mathbf{z}) + \bbox[5px,border-bottom:2px solid DarkViolet]{B_{yz}} (\mathbf{y} \wedge \mathbf{z})$$

    Where $B_{xy}, B_{xz}, B_{yz}$ are just numbers like $v_x, v_y, v_z$ (they are underlined to match the diagram colors).

    The components of a 3D bivector are just the three 2D projections of the bivector onto the 2D basis planes.

    Using the same method as before we find that the actual values of the components look a lot like the XY component from the 2D case, but applied to all three planes:

    $$B_{xy} = a_x b_y - b_x a_y$$

    $$B_{xz} = a_x b_z - b_x a_z$$

    $$B_{yz} = a_y b_z - b_y a_z$$

    You can play with a 3D bivector in the following interactive diagram:

    Does the exterior product remind you of anything? In 3D, the definition of the outer product is very similar to that of the cross product. In fact, in 3D a vector that comes from a cross product (such as a normal vector) will have three components which are equal to the components of the bivector (the numbers are the same, but the basis is different).

    $$\begin{eqnarray}\mathbf{a} \wedge \mathbf{b} &=& & (a_x b_y - b_x a_y)(\mathbf{x} \wedge \mathbf{y}) \\ & & + & (a_x b_z - b_x a_z)(\mathbf{x} \wedge \mathbf{z}) \\ & & + & (a_y b_z - b_y a_z)(\mathbf{y} \wedge \mathbf{z}) \\ \\ \mathbf{a} \times \mathbf{b} &=& & (a_x b_y - b_x a_y) \ \mathbf{z} \\ & & - & (a_x b_z - b_x a_z) \ \mathbf{y} \\ & & + & (a_y b_z - b_y a_z) \ \mathbf{x}\end{eqnarray}$$

    The bivector definition makes sense geometrically, instead of appearing out of thin air. I remember thinking when I was learning the cross product, why the hell does it return a vector that has length equal to the area of the parallelogram formed by the two vectors? That feels so arbitrary. And why would you be allowed to turn the area of the parallelogram into the length of the vector?

    Semantics of Vectors and Bivectors

    In 3D, a bivector has three coordinates, one per plane: ($\mathbf{xy}$, $\mathbf{xz}$, and $\mathbf{yz}$). Vectors also have three coordinates, one per axis ($\mathbf{x}$, $\mathbf{y}$ and $\mathbf{z}$). Each plane is perpendicular to one axis. This is a coincidence that only happens in three dimensions (*) and it is why historically we have been confusing bivectors with vectors.

    In programming terms, they both have the same memory layout, but different operations. Using a 3D vector instead of a 3D bivector is like "type-casting" the bivector.

    Here's an example: you might have seen how normal vectors transform differently than regular vectors, using the "inverse transpose" of the matrix $(\mathbf{M}^{T})^{-1}$ instead of the matrix itself. That's because they are not really vectors, but actually bivectors, which we have "type-cast" to vectors. In physics, there's a hack called an "axial vector," which has been introduced to differentiate vectors that come from cross products from regular vectors. Bivector is the actual "type" of the object and it should be thought of and manipulated as such.

    The Geometric Product

    Multiplying Vectors together

    The geometric product $\mathbf{a b}$ (denoted without a symbol) is another operation one can do on vectors. The geometric product is defined so that vectors have inverses (i.e $\mathbf{a} \mathbf{a}^{-1}= 1$ where 1 is just the number $1$!) and have nice properties like associativity ($\mathbf{a} (\mathbf{b} \mathbf{c}) = (\mathbf{a} \mathbf{b}) \mathbf{c}$). The goal is to be able to multiply vectors together so that —just like for matrices— multiplication corresponds to geometric operations.

    To define the product, first note that it is possible to split a product (or any function that takes two arguments) into the sum of a part that does not change if we swap the arguments and one that does change, in the following way:

    $$\begin{eqnarray}\mathbf{a} \mathbf{b} &=& \frac{1}{2} (\mathbf{a} \mathbf{b} + \mathbf{a} \mathbf{b} + \mathbf{b} \mathbf{a} - \mathbf{b} \mathbf{a}) \\ &=& \frac{1}{2} (\mathbf{a} \mathbf{b} + \mathbf{b} \mathbf{a}) + \frac{1}{2} (\mathbf{a} \mathbf{b} - \mathbf{b} \mathbf{a})\end{eqnarray}$$

    The first term does not depend on the order of the arguments $\mathbf{a}$ and $\mathbf{b}$ anymore (it is called the "symmetric" part), while the second term changes sign when the arguments are swapped (it is called the "antisymmetric" part).

    The dot product of two vectors (also called inner product) is symmetric and is a measure of distance ($\mathbf{a} \cdot \mathbf{a} = \|\mathbf{a}\|^2 $), so it sounds useful geometrically to set it equal to the symmetric part:

    $$\frac{1}{2} (\mathbf{a} \mathbf{b} + \mathbf{b} \mathbf{a}) = \mathbf{a} \cdot \mathbf{b}$$

    Similarity, the outer product of two vectors is antisymmetric, so it sounds useful geometrically to set it equal to the antisymmetric part:

    $$\frac{1}{2} (\mathbf{a} \mathbf{b} - \mathbf{b} \mathbf{a}) = \mathbf{a} \wedge \mathbf{b}$$

    In addition, the dot product contains the cosine of the angle between the two vectors ($\mathbf{a} \cdot \mathbf{b} = \|\mathbf{a}\|\|\mathbf{b}\|cos(\alpha)$), while the outer product contains the sine of the angle. Together they fully describe the angle between the vectors, as well the plane they form.

    So the geometric product is:

    $$\mathbf{a} \mathbf{b} = \mathbf{a} \cdot \mathbf{b} + \mathbf{a} \wedge \mathbf{b}$$

    It is strange because multiplying two vectors together gives the sum of two different things: a scalar and a bivector. However this is similar to how a complex number is the sum of a scalar and an "imaginary" number, so you might be used to it already. Here the bivector part corresponds to the "imaginary" part of the complex number. Except it is not "imaginary," itís just a bivector, which we have a concrete picture of!

    Basically, by multiplying two vectors together we compute useful properties about them (the "length of their projections onto each other" / "cosine of the angle" ($\mathbf{a} \cdot \mathbf{b}$), and the "plane they form together" / "sine of the angle" ($\mathbf{a} \wedge \mathbf{b}$)), which we keep bundled together via the "plus" sign. The geometric product also gives these "property bundles" operations that can be applied to them, and these operations have geometric interpretations (for example: rotating and reflecting vectors), as we shall see now .

    Multiplication Table

    The multiplication table helps make this product more concrete: let's see what happens if we take products of the basis vectors ($\mathbf{x}$,$\mathbf{y}$,$\mathbf{z}$).

    For any basis vector, such as the $\mathbf{x}$ axis, the result is $1$:

    $$\mathbf{x} \mathbf{x} = \mathbf{x} \cdot \mathbf{x} + \mathbf{x} \wedge \mathbf{x} = 1$$

    For any pair of basis vectors, such as the $\mathbf{x}$ and $\mathbf{y}$ axes, the result is just the bivector they form together:

    $$\mathbf{x} \mathbf{y} = \mathbf{x} \cdot \mathbf{y} + \mathbf{x} \wedge \mathbf{y} = \mathbf{x} \wedge \mathbf{y}$$

    (so we can call $\mathbf{x} \wedge \mathbf{y}$ simply $\mathbf{x} \mathbf{y}$ since they are the same thing! This is true for basis vectors, as well vectors which are perpendicular i.e. have their dot product equal to zero)

    This gives the following table:

    $\mathbf{a} \mathbf{b}$
    $\mathbf{b}$
    $\mathbf{x}$ $\mathbf{y}$ $\mathbf{z}$
    $\mathbf{a}$ $\mathbf{x}$
    $1$
    $\mathbf{x} \mathbf{y}$
    $\mathbf{x} \mathbf{z}$
    $\mathbf{y}$
    $-\mathbf{x} \mathbf{y}$
    $1$
    $\mathbf{y} \mathbf{z}$
    $\mathbf{z}$
    $-\mathbf{x} \mathbf{z}$
    $-\mathbf{y} \mathbf{z}$
    $1$

    It is basically trivial, unlike the quaternion table for example.

    The Reflection Formula (Traditional Version)

    Reflection by a vector [you can move each vector]

    If we have a unit vector $\mathbf{a}$ and a vector $\mathbf{v}$ we can reflect $\mathbf{v}$ by the plane perpendicular to $\mathbf{a}$.

    This is done the usual way: we decompose $\mathbf{v}$ into a part perpendicular to the plane $\mathbf{v}_\perp = (\mathbf{v} \cdot \mathbf{a}) \mathbf{a}$, and a part parallel to the plane $\mathbf{v}_\parallel = \mathbf{v} - \mathbf{v}_\perp = \mathbf{v} - (\mathbf{v} \cdot \mathbf{a})\mathbf{a}$.

    Then, to reflect the vector, flip the perpendicular part while keeping the parallel part unchanged:

    $$\begin{eqnarray}R_{\mathbf{a}}(\mathbf{v}) &=& \mathbf{v}_\parallel - \mathbf{v}_\perp \\ &=& ( \mathbf{v} - (\mathbf{v} \cdot \mathbf{a})\mathbf{a} ) - ((\mathbf{v} \cdot \mathbf{a}) \mathbf{a}) \\ &=&\mathbf{v} - 2 (\mathbf{v} \cdot \mathbf{a}) \mathbf{a}\end{eqnarray}$$

    The Reflection Formula (Geometric Product Version)

    At this point we can replace the dot product $\mathbf{v} \cdot \mathbf{a}$ by its geometric product version $\frac{1}{2} (\mathbf{v} \mathbf{a} + \mathbf{a} \mathbf{v})$ to get the following:

    $$\begin{eqnarray}R_{\mathbf{a}}(\mathbf{v}) &=& \mathbf{v} - 2(\frac{1}{2}( \mathbf{v} \mathbf{a} + \mathbf{a} \mathbf{v})) \mathbf{a} \\ &= & \mathbf{v} - \mathbf{v} \mathbf{a}^2 - \mathbf{a} \mathbf{v} \mathbf{a} \\ &= & - \mathbf{a} \mathbf{v} \mathbf{a}\end{eqnarray}$$

    ($\mathbf{a}^2 = \mathbf{a} \cdot \mathbf{a} = 1$ since $\mathbf{a}$ is a unit vector)

    This is saying the exact same thing but in a different notation. Using a simple product notation instead of a formula to encode a fundamental operation such as a reflection is going to prove very useful!

    Two Reflections is a Rotation: 2D case

    It turns out that if we apply two successive reflections to $\mathbf{v}$ (using vector $\mathbf{a}$ followed by vector $\mathbf{b}$) we get a rotation by twice the angle between the vectors $\mathbf{a}$ and $\mathbf{b}$.

    You can apply each successive Reflection Step in the diagram below :

    You can also change the vectors $\mathbf{a}$, $\mathbf{b}$, and $\mathbf{v}$, but the initial configuration of vectors in the diagram (click the "Reset Vector Positions" button) should make it especially clear why the rotation ends up being twice the angle. Another configuration that is not bad is to set $\mathbf{a}$ and $\mathbf{b}$ to the $\mathbf{x}$ and $\mathbf{y}$ axes.

    Two Reflections is a Rotation: 3D case

    In the 3D case the vector $\mathbf{v}$ can be split into two different parts, one lying inside the plane defined by $\mathbf{a}$ and $\mathbf{b}$, and one lying outside (perpendicular to) the plane. As seen in the following diagram, when the vector gets reflected by each plane its outside part stays the same. So for the inside part, we are back to the 2D case, and it just gets rotated by twice the angle!

    Rotors

    In terms of the Geometric Product, the two reflections simply correspond to:

    $$R_{\mathbf{b}}(R_{\mathbf{a}}(\mathbf{v})) = - \mathbf{b} (-\mathbf{a} \mathbf{v} \mathbf{a}) \mathbf{b} = \mathbf{b} \mathbf{a} \: \mathbf{v} \: \mathbf{a} \mathbf{b}$$

    We call $\mathbf{a} \mathbf{b} = \mathbf{a} \cdot \mathbf{b} + \mathbf{a} \wedge \mathbf{b}$ a Rotor because by multiplying by $\mathbf{a} \mathbf{b}$ on both sides of a vector we perform a rotation ($\mathbf{b} \mathbf{a}$ the same as $\mathbf{a} \mathbf{b}$ except the bivector part is flipped).

    Applying a Rotor $\mathbf{a} \mathbf{b}$ to both sides of a vector rotates this vector in the plane of vectors $\mathbf{a}$ and $\mathbf{b}$ by twice the angle between $\mathbf{a}$ and $\mathbf{b}$.

    ✨⭐💖 That's all there is to it! 💖⭐✨ź




    3D Rotors vs Quaternions

    We can notice that 3D Rotors look a lot like Quaternions:

    $$a + B_{xy} \ \mathbf{x} \wedge \mathbf{y} + B_{xz} \ \mathbf{x} \wedge \mathbf{z} + B_{yz} \ \mathbf{y} \wedge \mathbf{z}$$

    $$a + b \ \mathbf{i} + c \ \mathbf{j} + d \ \mathbf{k}$$

    In fact the code/math is basically the same! The main difference is that $\mathbf{i}$, $\mathbf{j}$ and $\mathbf{k}$ get replaced by $\mathbf{y} \wedge \mathbf{z}$, $\mathbf{x} \wedge \mathbf{z}$ and $\mathbf{x} \wedge \mathbf{y}$, but they work mostly the same way. Here is the code comparison. I did not include everything, such as log/exp for interpolation, but they are easy to make.

    Earth-Centric planetary motion

    However, as we have seen, 3D Rotors are a 3D concept that does not require the use of "4D double rotations" or "stereographic projection" to visualize. Trying to visualize quaternions as operating in 4D just to explain 3D rotations is a bit like trying to understand planetary motion from an earth-centric perspective i.e. overly complex because you are looking at it from the wrong viewpoint.

    As we have seen, representing rotations as operating inside planes instead of around vectors helps a lot. For example the basis bivectors square to $-1$, just like the basis quaternions ($\mathbf{i}^2=\mathbf{j}^2=\mathbf{k}^2 = -1$) :

    $$(\mathbf{x} \mathbf{y})^2 = (\mathbf{x} \mathbf{y}) (\mathbf{x} \mathbf{y}) = - (\mathbf{y} \mathbf{x}) (\mathbf{x} \mathbf{y}) = -\mathbf{y} (\mathbf{x} \mathbf{x}) \mathbf{y} = - \mathbf{y} \mathbf{y} = -1$$

    Multiplying two bivectors together gives a third bivector, but this is basically trivial, and we don't have to remember how $\mathbf{i} \mathbf{j} = \mathbf{k}$:

    $$(\mathbf{x} \mathbf{y}) (\mathbf{y} \mathbf{z}) = \mathbf{x} (\mathbf{y} \mathbf{y}) \mathbf{z} = \mathbf{x} \mathbf{z}$$

    (Note that we have used $\mathbf{x} \wedge \mathbf{y} = \mathbf{x} \mathbf{y}$)

    These properties are a consequence of the geometric product instead of appearing out of thin air!

    Further Reading

    (by the way, Geometric Algebra contains a lot more cool stuff than rotors!)

    Credit

    Made by Marc ten Bosch. If you enjoyed this video/article combo thing, you might consider donating.