Placing a Camera: the LookAt Function
Keywords: Matrix, LookAt, camera, cross product, transformation matrix, transform, camera-to-world, look-at, Gimbal lock.Want to report bugs, errors or send feedback? Write at You can also contact us on Twitter and Discord. We do this for free, please be kind. Thank you.
11 mns read.
In this short lesson we will study a simple but useful method for moving 3D cameras around. You won't understand this lesson easily if you are not familiar with the concept of transformation matrix and cross product between vectors. Hopefully if that's not already the case, we recommend you to read the lesson called Geometry (entirely).

Moving the Camera

Being able to move the camera in a 3D scene is really essential. However in most of the lessons from Scratchapixel we usually set the camera position and rotation in space (remember that cameras shouldn't be scaled), using a 4x4 matrix which is often labelled camToWorld. Remember that the camera in its default position is assumed to be centred at the origin and aligned along the negative z-axis. This is explained in detail in the lesson Ray Tracing: Generating Camera Rays. However, using a 4x4 matrix to set the camera position in the scene, is just not very friendly, unless we can access a 3D animation system such as for example Maya or Blender to set the camera and export its transformation matrix.

Hopefully, we can use another method which is better than setting up the matrix directly and doesn't require an editor (though this is of course always better). This technique doesn't really have a name but programmers usually refer to it to it as the Look-At method. The idea of the method is simple. In order to set a camera position and orientation, all you really need is a point to set the camera position in space which we will refer to as the from point, and a point that defines what the camera is looking at. We will refer to this point as the to point.

What's interesting is that from this pair of points, we can create a camera 4x4 matrix as we will demonstrate in this lesson.

The camera is aligned along the negative z-axis. Does it mean I need to rotate the camera by 180 degrees along the y-axis or scale it up by -1 along the z-axis? Not at all. Transforming a camera is no different from transforming any other object in a scene. Keep in mind that in ray-tracing, we build the primary rays as if the camera was located in its default position. This is explained in the lesson Ray Tracing: Generating Camera Rays. This is when we actually reverse the direction of the rays. In other words, the z-coordinates of the rays' directions at this point are always negative: the camera in its default position, looks down along the negative z-axis. Those primary rays are then transformed by the camera-to-world matrix. Therefore, there is no need to account for the default orientation of the camera when the 4x4 camera-to-world matrix is built.

The Method

Figure 1: the local coordinate system of the camera aimed at a point.

Figure 2: computing the forward vector from the position of the camera and target point.

Remember that a 4x4 matrix encodes the 3-axis of a Cartesian coordinate system. Again if this is not obvious to you, please read the lesson on Geometry. Remember that there are two conventions you need to pay attention to when you deal with matrices and coordinate systems. For matrices you need to choose between the row-major and column-major representation. At Scratchapixel, we use a row-major notation. As for coordinate system, you need to choose between a right-hand and left-hand coordinate system. We use a right-hand coordinate system. The fourth row of the 4x4 matrix (in a row-major matrix) encodes the translation values.

$$ \begin{matrix} \color{red}{Right_x}&\color{red}{Right_y}&\color{red}{Right_z}&0\\ \color{green}{Up_x}&\color{green}{Up_y}&\color{green}{Up_z}&0\\ \color{blue}{Forward_x}&\color{blue}{Forward_y}&\color{blue}{Forward_z}&0\\ T_x&T_y&T_z&1 \end{matrix} $$

How you name the axis of a Cartesian coordinate system depends on your preference, you can call them x, y and z but in this lesson for clarity, we will name them right (for x-axis), up (for the y-axis) and forward for the (z-axis). This is illustrated in figure 1. The method from building a 4x4 matrix from the from-to pair of points can be broken down in four steps:

Again, if you are unsure about why we do that, check the lesson on Geometry. Finally here is the source code of the complete function. It computes and return a camera-to-world matrix from two arguments, the from and to points. Note that the function third parameter (called tmp in the following code) is the arbitrary vector used in the computation of the right vector. It is set with the default value of (0,1,0) but it can be changed if desired (hence the need to normalize it when used).

Matrix44f lookAt(const Vec3f& from, const Vec3f& to, const Vec3f& tmp = Vec3f(0, 1, 0)) { Vec3f forward = normalize(to - from); Vec3f right = crossProduct(normalize(tmp), forward); Vec3f up = crossProduct(forward, right); Matrix44f camToWorld; camToWorld[0][0] = right.x; camToWorld[0][1] = right.y; camToWorld[0][2] = right.z; camToWorld[1][0] = up.x; camToWorld[1][1] = up.y; camToWorld[1][2] = up.z; camToWorld[2][0] = forward.x; camToWorld[2][1] = forward.y; camToWorld[2][2] = forward.z; camToWorld[3][0] = from.x; camToWorld[3][1] = from.y; camToWorld[3][2] = from.z; return camToWorld; }

The Look-At Method Limitations

The method is very simple and works generally well. Though it has an Achilles heels (a weakness). When the camera is vertical looking straight down or straight up, the forward axis gets very close to the arbitrary axis used to compute the right axis. The extreme case is of course when the froward axis and this arbitrary axis are perfectly parallel e.g. when the forward vector is either (0,1,0) or (0,-1,0). Unfortunately in this particular case, the cross product fails producing a result for the right vector. There is actually no real solution to this problem. You can either detect this case, and choose to set the vectors by hand (since you know what the configuration of the vectors should be anyway). A more elegant solution can be developed using quaternion interpolation.