Models And Cameras, Oh My!
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!