How to rotate a point with a Quaternion
Tags: gamedev, math, beginners, learning
Published: 2023-12-20 08:38:28

You have a point, say {% katex inline %}\vec{p}=[1.5,0,0]{% endkatex %}, which describes a location in {% katex inline %}x, y, z{% endkatex %} coordinates. How do you create and use a Quaternion {% katex inline %}\bold{q}{% endkatex %} to rotate this vector by some angle {% katex inline %}\theta{% endkatex %} around an axis?

First, we create our Quaternion (also called a Rotor). For a rotation around the axis {% katex inline %}\vec{a}{% endkatex %}, we can use the formula

{% katex %}
\bold q = \cos(\frac\theta2) + \vec{a}\sin(\frac\theta2)I
{% endkatex %}

which produces for us a normalised Quaternion {% katex inline %}\bold q{% endkatex %} (also called a Versor). We'll see an example later. Next, we apply this Rotor to our point {% katex inline %}\vec{p}{% endkatex %} with

{% katex %}
\bold{p_{r}} = \bold q \cdot \bold p \cdot \bar{\bold q}
{% endkatex %}

and from that we can derive our result {% katex inline %}\vec{p_{r}}{% endkatex %}. Confused?

## Step by Step Example

For this example our point is {% katex inline %}\vec{p} = [1.5, 0, 0]{% endkatex %}, or one and a half units in the {% katex inline %}x{% endkatex %} direction. We will rotate about the {% katex inline %}z{% endkatex %}-axis, giving {% katex inline %} \vec{a} = [0, 0, 1]{% endkatex %}. If we choose an angle of {% katex inline %}\theta=\frac\pi2{% endkatex %}, which is {% katex inline %}90{% endkatex %} degrees, we will expect an answer of {% katex inline %}\vec{p_r} = [0, 1.5, 0]{% endkatex %} which is one and a half units in the {% katex inline %}y{% endkatex %} direction.

1. Calculate the Rotor {% katex inline %}\bold q{% endkatex %}. From the formula above, we get

    {% katex %}
    \bold q = [\cos(\frac\pi4),\, \sin(\frac\pi4)a_x,\, 
    \sin(\frac\pi4)a_y,\, \sin(\frac\pi4)a_z]
    {% endkatex %}
    {% katex %}
    = [\frac1{\sqrt2},\, 0,\, 0,\, \frac1{\sqrt2}]
    {% endkatex %}

    Note that this result is in {% katex inline %}w, x, y, z{% endkatex %} order. You may need to re-order these values for use with a software package or when comparing to other examples. Also note that {% katex inline %}\vec a{% endkatex %} must be normalised (have length 1).

2. Express the point as a Quaternion. In this case, our point 
    {% katex inline %}\vec p{% endkatex %} converts to

    {% katex %}
    \bold p = [0,\, 1.5,\, 0,\, 0]
    {% endkatex %}

    where we simply copy the {% katex inline %}x, y, z{% endkatex %} entries of our point to the {% katex inline %}w, x, y, z{% endkatex %} entries in our Quaternion. Again, the order of the software library or convention you are using may be different.

3. Calculate the conjugate of the Rotor {% katex inline %}\bold q{% endkatex %}. Simply negate the {% katex inline %}x, y, z{% endkatex %} entries, giving

    {% katex %}
    \bar{\bold q}= [\frac1{\sqrt2},\, 0,\, 0,\, - \frac1{\sqrt2}]
    {% endkatex %}

4. Apply the Rotor to the encoded point (perhaps using an online tool or a software library).

    {% katex %}
    \bold p_r = \bold q \cdot \bold p \cdot \bar{\bold q}
    {% endkatex %}
    {% katex %}
    = [\frac1{\sqrt2},\, 0,\, 0,\, \frac1{\sqrt2}] 
    \cdot [0,\, 1.5,\, 0,\, 0] \cdot [\frac1{\sqrt2},\, 0,\, 0,\, -\frac1{\sqrt2}]
    {% endkatex %}
    {% katex %}
    = [0,\, 0,\, 1.5,\, 0] 
    {% endkatex %}

    Note that quaternion multiplication is not simply multiplying each entry. Use software someone else has written or borrow from the code samples below.

5. Read off the {% katex inline %}x, y, z{% endkatex %} entries from {% katex inline %}\bold p_r{% endkatex %}, giving

    {% katex %}
    \vec {p_r} = [0,\, 1.5,\, 0] 
    {% endkatex %}

    as we expected!

## Example Code

### Create a Quaternion (Rotor)

```python
import math
def rotor(axis, angle):
  '''
  Create a Rotor that, when applied to a point by q * p * conjugate(q),
  performs a rotation around `axis` by `angle` (in radians).
  '''
  x, y, z = axis
  n = math.sqrt(x * x + y * y + z * z)
  s = math.sin(angle / 2) / n
  w = math.cos(angle / 2)
  return w, x * s, y * s, z * s
```
The extra `n` calculation ensures the axis vector is normalised, which is a requirement of Versors.

### Encode/decode a Point to/from a Quaternion (Rotor)

```python
def encode_point(p):
    x, y, z = p
    return 0, x, y, z

def decode_point(rotor):
    w, x, y, z = rotor
    return x, y, z
```

Note that the {% katex inline %}w{% endkatex %} component is zero.

### Calculate the conjugate of a Quaternion (Rotor)

```python
def conjugate(rotor):
  '''
  Take a Rotor in w,x,y,z order and return its conjugate.
  '''
  w, x, y, z = rotor
  return w, -x, -y, -z
```

### Multiply two Quaternions (Rotors)

```python
def cross(g, h):
  '''
  Right-handed cross product between two vectors.
  '''
  # ijk = -1
  g1, g2, g3 = g
  h1, h2, h3 = h
  
  i = g2*h3 - g3*h2
  j = g3*h1 - g1*h3
  k = g1*h2 - g2*h1

  return i, j, k

def multiply(g, h):
  '''
  Multiply two rotors g x h.
  '''
  # i,j,k = g1,g2,g3
  gs, g1, g2, g3 = g
  hs, h1, h2, h3 = h
  w = gs*hs - g3*h3 - g2*h2 - g1*h1
  # assuming ijk = -1
  i, j, k = cross((g1,g2,g3), (h1,h2,h3))
  x = i + gs*h1 + hs*g1
  y = j + gs*h2 + hs*g2
  z = k + gs*h3 + hs*g3
  return w, x, y, z
```

### Example in code

```python
point = 1.5, 0, 0
axis = 0, 0, 1  # z-axis
angle = math.pi / 2  # 90 degrees
point_encoded = encode_point(point)
r = rotor(axis, angle)
result_encoded = multiply(r, multiply(point_encoded, conjugate(r)))
print(decode_point(result_encoded))
```

Which prints

```
(2.220446049250313e-16 1.5 0.0)
```

where `2.220446049250313e-16` is an ignorable numerical error, meaning we get the point {% katex inline %}[0,\, 1.5,\, 0]{% endkatex %} as expected. Try some other angles and axes!

## Note about conventions

Sometimes you'll find that you are getting different results and you don't know why. An annoying aspect of using quaternions is that there are a bunch of choices involved in how we calculate with them and different people/software use different conventions. For example, here I have used the definition of Quaternions where {% katex inline %}ijk=-1{% endkatex %} (the right-hand convension), others might use {% katex inline %}ijk=1{% endkatex %} although it's less common. Some also prefer to apply a Rotor with {% katex inline %} \bar{\bold q} \cdot \bold p \cdot \bold q {% endkatex %} whereas I've gone with the convention {% katex inline %} \bold q \cdot \bold p \cdot \bar{\bold q} {% endkatex %}. Finally, the order of values can differ. Here, I've used {% katex inline %}w, x, y, z{% endkatex %} order but other orderings such as {% katex inline %}x, y, z, w{% endkatex %} and {% katex inline %}z, y, x, w{% endkatex %} are also used. If any of these choices are made differently, you may get a different answer than you expect so be careful and read the documentation.

Hope this helps someone wondering how to get from equations to code!