In the previous post, we explored how to move a 3D model using angles. However, a camera is not a model. It does not have a generic transform component, so we need to find another way to rotate it.

“raylib” defines a data structure called a “Quaternion”. It is a mathematical construct used to represent rotations in 3D space safely, without suffering from gimbal lock - a notorious problem that occurs when relying solely on Euler angles.

What is a Quaternion?

In mathematics, the quaternion number system extends complex numbers and is heavily used in 3D graphics to calculate smooth rotations. Simply put, a quaternion is a four-dimensional vector that elegantly represents an orientation in three-dimensional space.

Quaternions are generally represented in the following form:

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

where the coefficients $a, b, c, d$ are real numbers, and $1, i, j, k$ are the basis elements (read more on Wikipedia).

The Camera’s Anatomy

The camera in “raylib” does not store its own rotation state. Instead, its orientation is defined by three vectors:

  • Position: The exact point where the camera is located in the world.

  • Target (Forward): The point the camera is looking at.

  • Up: The direction that tells the camera which way is “up” (preventing the world from rendering upside down).

To effectively control the camera, we can maintain its internal orientation using a quaternion. This way, we can apply rotations to the quaternion and easily extract the new forward and up vectors to update the camera.

Rotating the Camera: Step-by-Step

Let’s see an example of how to rotate the camera using quaternions.

1. Setting Up the Reference Vectors

First, we need to define our “relative world” coordinate system. These are constant vectors that represent the baseline axes. We also need to set up the camera’s initial rotation state.

// Reference vectors representing the "relative world" coordinate system
const auto worldUp = (Vector3){0.0f, 1.0f, 0.0f}; 
const auto worldForward = (Vector3){0.0f, 0.0f, 1.0f};
const auto worldRight = (Vector3){-1.0f, 0.0f, 0.0f};

// Start with the identity quaternion (represents no rotation)
auto matrix = QuaternionIdentity();

// Extract the current camera directional vectors
auto right = worldRight; 
auto up = camera.up;
auto forward = Vector3Normalize(camera.target); // Simplified for this example

// Arbitrary angles to rotate the camera (in radians)
float pitch = 0.2f; // Rotation around the right vector
float roll = 0.5f;  // Rotation around the forward vector
float yaw = 0.1f;   // Rotation around the up vector

2. Calculating the Rotations

Just like with the model, we want to apply changes to the pitch, roll, and yaw angles. We calculate a separate quaternion for each axis rotation:

const auto qPitch = QuaternionFromAxisAngle(right, pitch);
const auto qRoll = QuaternionFromAxisAngle(forward, roll);
const auto qYaw = QuaternionFromAxisAngle(up, yaw);

Next, we combine them to get the final orientation of the camera for this frame:

const auto orientation = QuaternionMultiply(
    qYaw, 
    QuaternionMultiply(
        qPitch, 
        qRoll));

3. Updating the State and Applying to Camera

Now, we update our main rotation matrix by multiplying the new orientation quaternion with the current rotation state:

matrix = QuaternionNormalize(QuaternionMultiply(orientation, matrix));

Finally, we calculate the new forward, up, and right vectors based on our updated quaternion matrix, and feed those values back into the “raylib” camera:

// Calculate the new directional vectors after applying the rotation
forward = Vector3Normalize(Vector3RotateByQuaternion(matrix, worldForward));
up = Vector3Normalize(Vector3RotateByQuaternion(matrix, worldUp));
right = Vector3Normalize(Vector3RotateByQuaternion(matrix, worldRight));

// Apply the new orientation to the camera
camera.target = forward;
camera.up = up;

By calculating the camera’s orientation this way, we can rotate it freely in any direction without ever worrying about gimbal lock or broken axes!