University of Wales Trinity Saint David
ACCA6021 - Graphics and Games Programming

Lab M05 - The Look-At Matrix

Overview

This worksheet derives the view matrix from first principles. You will compute the three camera basis vectors from an eye position and a look-at point using cross products, assemble the look-at matrix, and verify the results numerically. Understanding what glm::lookAt computes explains why the view matrix appears to move the world rather than the camera, and why its translation column is negated. This worksheet builds on the Homogeneous Coordinates lab (WS-M03) and the Column-Major Convention lab (WS-M04).

Learning Objectives

By the end of this lab you will be able to:

Concept

The camera is positioned at a point in the scene and aimed at a target. The vertex shader needs all scene vertices expressed relative to the camera. Students often think the view matrix moves the camera to where it belongs, but it actually does the opposite: it moves the entire world so the camera ends up at the origin looking down -Z. The vertices do not move in world space. The coordinate frame shifts.

Three basis vectors describe the camera's orientation. GLM computes them as follows. The forward vector f is the normalised direction from eye to centre: f = normalise(centre - eye). The right vector s is perpendicular to both forward and worldUp: s = normalise(cross(f, worldUp)). The corrected up vector u is then recomputed as u = cross(s, f) to ensure perfect orthogonality. The view matrix combines a rotation block (rows built from s, u, -f) with a negated translation that shifts the world so the camera sits at the origin. The translation values are negated because the world moves opposite to the camera's position.

The function glm::lookAt(eye, centre, worldUp) computes this matrix. Its three arguments map directly to the three inputs of the derivation. One critical edge case: if the camera is aimed straight up or straight down, f and worldUp are parallel. Their cross product is the zero vector and normalising it is undefined. This gimbal singularity must be avoided in camera code.

Tasks

Task 1 - Deriving a Look-At Matrix Step by Step

Use: eye = (0, 0, 5), centre = (0, 0, 0), worldUp = (0, 1, 0). These values produce clean integer results.

Step 1 - Forward vector f

f = normalise(centre - eye)
  = normalise((0,0,0) - (0,0,5))
  = normalise((0, 0, -5))
  = (0, 0, -1)

Step 2 - Right vector s

s = normalise(cross(f, worldUp))
  = normalise(cross((0,0,-1), (0,1,0)))

Cross product expansion (u x v = (uy*vz - uz*vy, uz*vx - ux*vz, ux*vy - uy*vx)):

cross((0,0,-1), (0,1,0)):
  x = (0)(0)  - (-1)(1) =  0 + 1 = 1
  y = (-1)(0) - (0)(0)  =  0 - 0 = 0
  z = (0)(1)  - (0)(0)  =  0 - 0 = 0
Result: (1, 0, 0)  -- already unit length, so s = (1, 0, 0)

Step 3 - Corrected up vector u

u = cross(s, f)
  = cross((1,0,0), (0,0,-1)):
  x = (0)(-1) - (0)(0)  = 0
  y = (0)(0)  - (1)(-1) = 1
  z = (1)(0)  - (0)(0)  = 0
Result: u = (0, 1, 0)

Step 4 - Assemble the view matrix

The look-at matrix has the form:
|  s.x   s.y   s.z   -dot(s, eye) |
|  u.x   u.y   u.z   -dot(u, eye) |
| -f.x  -f.y  -f.z    dot(f, eye) |
|   0     0     0         1       |

Compute the translation column:
  -dot(s, eye) = -(1*0 + 0*0 + 0*5) = 0
  -dot(u, eye) = -(0*0 + 1*0 + 0*5) = 0
   dot(f, eye) = (0*0 + 0*0 + (-1)*5) = -5

Final look-at matrix:
| 1  0   0   0 |
| 0  1   0   0 |
| 0  0   1  -5 |
| 0  0   0   1 |

Interpretation: this is a pure translation of -5 on Z. The camera is at z = 5, so the world must shift by -5 to bring the camera to the origin. Because the camera has no rotation relative to the world axes, the rotation block of the matrix is the identity. The -5 in the translation column comes from dot(f, eye) = (0)(0)+(0)(0)+(-1)(5) = -5, placed at row 2, column 3 directly.

Task 2 - Derive a Different Configuration

Use: eye = (4, 0, 0), centre = (0, 0, 0), worldUp = (0, 1, 0). The camera is to the right of the scene, looking left toward the origin. Follow the same four steps and fill in the blanks.

Step 1: f = normalise(centre - eye) = normalise((0,0,0) - (4,0,0)) = normalise((-4,0,0)) = (, 0, 0)

Step 2: cross(f, worldUp) = cross((-1,0,0), (0,1,0)). x component = (0)(0) - (0)(1) =

Step 2 continued: y component = (0)(0) - (-1)(0) =

Step 2 continued: z component = (-1)(1) - (0)(0) =

So s = normalise((0, 0, -1)) = (0, 0, )

Step 3: u = cross(s, f) = cross((0,0,-1), (-1,0,0)). x component = (0)(0) - (-1)(0) =

Step 3 continued: y component = (-1)(-1) - (0)(0) =

Step 3 continued: z component = (0)(0) - (0)(-1) = . So u = (0, 1, 0).

Step 4: -dot(s, eye) = -(0*4 + 0*0 + (-1)*0) =

Step 4: -dot(u, eye) = -(0*4 + 1*0 + 0*0) =

Step 4: dot(f, eye) = (-1)(4) + (0)(0) + (0)(0) =

The completed view matrix for eye = (4, 0, 0), centre = (0, 0, 0), worldUp = (0, 1, 0):

|  0   0  -1   0 |
|  0   1   0   0 |
|  1   0   0  -4 |
|  0   0   0   1 |

The right vector s = (0, 0, -1): the camera looks left (-X), so its right side points into the screen (-Z in world space). Row 2 of the view matrix is (-f.x, -f.y, -f.z, dot(f,eye)) = (1, 0, 0, -4): f = (-1,0,0) so -f = (1,0,0), and dot(f,eye) = (-1)(4) = -4, encoding that the origin is -4 units along the camera's forward axis.

Task 3 - Conceptual Questions

The camera is pointing straight up. worldUp = (0,1,0) and (centre - eye) points in the same direction (0,1,0). What goes wrong in Step 2, and what does this failure mean physically?

In Step 2, f = (0,1,0) and worldUp = (0,1,0). The cross product of two parallel vectors is the zero vector. Normalising the zero vector is undefined (division by zero). Physically, the camera is aiming straight up and there is no unique horizontal direction for "right" - the camera could be rotated any amount around the vertical and the view would look the same. This is a gimbal singularity. Camera code must avoid aiming the camera directly along the worldUp direction.

Why is the translation column of the look-at matrix negated?

The view matrix moves the world, not the camera. If the camera is at position e in world space, the world must shift by -e so the camera ends up at the origin. The translation values in the view matrix are -dot(s, eye), -dot(u, eye), and dot(f, eye) (with a sign flip because f is used negated in the matrix row). These values encode the negated camera position projected onto each basis axis, producing exactly the world-shift needed to bring the camera to the origin.

What does the right vector s represent, and why is the cross product the correct tool for computing it?

The right vector s points in the direction to the camera's right. It must be perpendicular to both the forward direction f and the worldUp vector. The cross product of two vectors produces a vector that is perpendicular to both of them - this is exactly the constraint needed. No other common operation gives this perpendicularity property directly.

Solution

Try the tasks above before revealing the solution.