Third-person Camera

How to 3D

Chapter 7: Camera

Third-person Camera

A first-person camera gives a first-person perspective of the virtual world. The avatar and eye are one and the same. Sometimes we want to see the avatar, especially in games in which the primary mechanic isn't firing projectiles at enemies from afar but rather melee combat or athletic maneuvers. On these occasions we need a third-person camera. Like the one in this renderer, which can be moved with the mouse and WASD keys:

Let's examine what a third-person camera abstraction might look like. In the following discussion, we assume that the third-person camera is associated with an avatar model that is defined in model space to reside at the origin, stand upright, and look along the negative z-axis.

State

The ThirdPersonCamera maintains the following state in order to orient itself and the avatar to which it is attached:

A first-person camera positions and orients just a camera. In contrast, a third-person camera positions and orients the avatar—which is visible—and the camera is situated behind at some distance. When the avatar moves or turns, the camera tags along. The abstraction therefore maintains two matrices. The worldFromModel matrix is used to transform the avatar model. This matrix applies only to the avatar; other models in the scene will have their own worldFromModel matrices not maintained by the camera. The eyeFromWorld matrix puts the viewer looking over the avatar's shoulder.

Behaviors

The ThirdPersonCamera class provides several behaviors for initializing the camera, moving and turning it, and building its matrices.

Constructor

The constructor receives the avatar's position, the position at which it's looking, and the viewer's position in the avatar's model space. From the two positions, it computes the avatar's forward vector and focal distance.

The matrices must be rebuilt whenever the avatar's position or orientation changes, so it would not be wise to build the matrix in the constructor. Instead, the matrices are built in a method named reorient, which the constructor calls.

Reorient

The reorient method is responsible for assembling the eyeFromWorld and worldFromModel matrices whenever the avatar is moved. Let's examine each of these matrices in turn.

We want to rotate the avatar so that it is looking in the desired direction, so we build a rotation matrix out of the avatar's three world space axes. We have the avatar's forward vector as part of the camera state, and we compute the other two vectors using cross products:

function reorient()
  // Find the avatar's axes
  avatarRight = cross avatarForward with worldUp vector
  normalize avatarRight
  avatarUp = cross avatarRight with avatarForward
  ...
function reorient()
  // Find the avatar's axes
  avatarRight = cross avatarForward with worldUp vector
  normalize avatarRight
  avatarUp = cross avatarRight with avatarForward
  ...

Earlier we formed the rotation matrix of a first-person camera by dropping into its rows the axes of the incoming world space that were to become the x-, y-, and z-axes of the outgoing eye space. That same trick won't work here—not exactly. The vectors we just computed are the outgoing world space vectors that we want the incoming model space x-, y-, and z-axes to become. That's the reverse of the situation we had with the first-person camera.

There's a related law of rotation matrices that can help us out: the columns of a rotation matrix represent the outgoing vectors that the x-, y-, and z-axes of the incoming space become. For example, this rotation matrix makes the avatar's right arm, which points along the x-axis in model space, point along the world space vector \(\mathrm{right}\):

$$\begin{bmatrix} \mathrm{right}_x \\ \mathrm{right}_y \\ \mathrm{right}_z \\ 0 \end{bmatrix} = \begin{bmatrix} \mathrm{right}_x & \ldots & \ldots & 0 \\ \mathrm{right}_y & \ldots & \ldots & 0 \\ \mathrm{right}_z & \ldots & \ldots & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \times \begin{bmatrix} 1 \\ 0 \\ 0 \\ 0 \end{bmatrix}$$

Altogether, this matrix rotates our avatar into the desired orientation:

$$\begin{bmatrix} \mathrm{right}_x & \mathrm{up}_x & -\mathrm{forward}_x & 0 \\ \mathrm{right}_y & \mathrm{up}_y & -\mathrm{forward}_y & 0 \\ \mathrm{right}_z & \mathrm{up}_z & -\mathrm{forward}_z & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix}$$

We also need to translate the avatar from its origin in model space to its position in world space. Together the translation and rotation matrices form the avatar's worldFromModel matrix:

function reorient()
  ...
  // Build worldFromModel
  rotateAvatar = matrix whose columns are avatar's right,
                 up, and negative forward vectors
  translateAvatar = matrix that translates avatar to
                    position
  worldFromModel = translateAvatar * rotateAvatar
  ...
function reorient()
  ...
  // Build worldFromModel
  rotateAvatar = matrix whose columns are avatar's right,
                 up, and negative forward vectors
  translateAvatar = matrix that translates avatar to
                    position
  worldFromModel = translateAvatar * rotateAvatar
  ...

Next up is the eyeFromWorld matrix. It is assembled in much the same way as the first-person camera's matrix. But this time the camera's position is derived from the avatar. We compute it by transforming the camera's offset by the worldFromEye matrix:

function reorient()
  ...
  // Compute camera properties
  cameraFrom = worldFromEye * offset
  ...
function reorient()
  ...
  // Compute camera properties
  cameraFrom = worldFromEye * offset
  ...

Next we determine the camera's right, up, and forward vectors:

function reorient()
  ...
  focalPoint = anchor + forward * focalDistance
  cameraForward = normalize focalPoint - cameraFrom
  cameraRight = cross cameraForward and worldUp
  normalize cameraRight
  cameraUp = cross cameraRight with cameraForward
  ...
function reorient()
  ...
  focalPoint = anchor + forward * focalDistance
  cameraForward = normalize focalPoint - cameraFrom
  cameraRight = cross cameraForward and worldUp
  normalize cameraRight
  cameraUp = cross cameraRight with cameraForward
  ...

These vectors for the rows of the eyeFromWorld matrix, just as they did in the first-person camera:

function reorient()
  ...
  // Build eyeFromWorld
  translateCamera = matrix that translates camera to origin
  rotateCamera = matrix whose rows are camera's right,
                 up, and negative forward vectors
  eyeFromWorld = rotateCamera * translateCamera
function reorient()
  ...
  // Build eyeFromWorld
  translateCamera = matrix that translates camera to origin
  rotateCamera = matrix whose rows are camera's right,
                 up, and negative forward vectors
  eyeFromWorld = rotateCamera * translateCamera

Strafe

As with a first-person camera, we have the avatar strafe by pushing its position along its right vector and then rebuilding the transformation matrices, as shown in this pseudocode:

function strafe(distance)
  anchor = anchor + avatarRight * distance
  reorient camera
function strafe(distance)
  anchor = anchor + avatarRight * distance
  reorient camera

Since the camera is positioned by a relative offset from the avatar, moving the avatar also moves the camera.

Advance

To make the avatar walk forward or backward, we move it along the forward vector:

function advance(distance)
  anchor = anchor + avatarForward * distance
  reorient camera
function advance(distance)
  anchor = anchor + avatarForward * distance
  reorient camera

Rotation

Since a third-person camera has a more expansive view than a first-person camera, we'll omit pitching and support only yawing. As with the first-person camera, we implement yawing by rotating the avatar's forward around the world's up axis:

function yaw(degrees)
  forward = rotateAround(worldUp, degrees) * forward
  reorient camera
function yaw(degrees)
  forward = rotateAround(worldUp, degrees) * forward
  reorient camera
← Looking with the MouseLab: First-person Camera →