Placing a Camera: the LookAt Function
Keywords: Matrix, LookAt, camera, cross product, transformation matrix, transform, camera-to-world, look-at, Gimbal lock.
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:

• Step 1: compute the forward axis. In figure 1 and 2 it is quite easy to see that the forward axis of the camera local coordinate system is aligned along the line segment defined by the points from and to. A little bit of geometry suffices to compute this vector. You just need to normalize the vector ${To-From}$ (mind the direction of this vector: it is ${To-From}$ not ${From-To}$). This can be done with the following code snippet:
Vec3f forward = Normalize(from - to);
We found one vector. Two left!
• Step 2: compute the right vector. Recall from the lesson on Geometry that Cartesian coordinates are defined by three unit vectors that are perpendicular to each other. We also know that if we take two vectors $A$ and $B$, they can be seen as lying into a plane, and that the cross product of these two vectors create a third vector $C$ perpendicular to that plane and thus also obviously perpendicular to $A$ and $B$. We can use this property to create our right vector. This idea here is to use some arbitrary vector and compute the cross vector between the forward vector and this arbitrary vector. The result is a vector that is necessarily perpendicular to the forward vector and that can be used in the construction of our Cartesian coordinate system as the right vector. The code for computing this vector is simple since it only implies a cross product between the forward vector and this arbitrary vector:
Vec3f right = crossProduct(randomVec, forward);

Figure 3: the vector (0,1,0) is in the plane defined by the forward and up vector. The vector perpendicular to this plane is thus the right vector.

The question is now, how do we choose this arbitrary vector? Well this vector can't really be totally arbitrary which is the reason why we wrote the word in italic. Think about this: if the forward vector is (0,0,1), then the right vector ought to be (1,0,0). This can only be done if we choose as our arbitrary vector, the vector (0,1,0). Indeed: (0,1,0) x (0,0,1) = (1,0,0) where the sign x here accounts for the cross product. Remember that the equations to compute the cross-product are: $$\begin{array}{l} c_x = a_y * b_z - a_z * b_y,\\ c_y = a_z * b_x - a_x * b_z,\\ c_z = a_x * b_y - a_y * b_x\\ \end{array}$$ where $a$ and $b$ are two vectors and $c$ is the result of the cross product of $a$ and $b$. When you look at figure 3, you can also notice that regardless of the forward vector's direction, the vector perpendicular to the plane defined by the forward vector and the vector (0,1,0) is always the right vector of the camera's Cartesian coordinate system. That is because the up vector of that coordinate system lies within that same plane as showed in figure 4. That's great because the vector (0,1,0) can clearly be used in place of what we called earlier our arbitrary vector.
Note also from that observation that the right vector always lie within the xz-plane. How come you may ask? If the cameras has a roll wouldn't the right vector be in a different plane? That's actually true, but applying a roll to the camera is not something you can do directly with the look-at method. To add a camera roll, you would first need to create a matrix to roll the camera (rotate the camera around the z-axis) and then multiply this matrix by the camera-to-world matrix built with the look-at method.
Finally, here is the code to compute the right vector:
Vec3f tmp(0, 1, 0); Vec3f right = crossProduct(Normalize(tmp), forward);
Note that we normalize the arbitrary vector just in case you would actually be using a vector that is different from (0,1,0). So in order to be on the safe side, we will normalise it. Also pay attention to the order of the vectors in the cross product. Keep in mind that the cross product is not commutative (it is actually anti-commutative, check the lesson on Geometry for more details). The best mnemonic way of remembering the right order, is to think of the cross product of the forward vector (0,0,1) with the up vector (0,1,0) we know it should give (1,0,0) and not (-1,0,0). If you know the equations of the cross-product, you should easily find out that the order is $up \times forward$ and not the other way around. Great we have the forward and right vector. How do now find the up vector?
• Step 4: compute the up vector. Well this is very simple, we have two orthogonal vectors, the forward and right vector, so computing the cross product between this two vectors will just give us the missing third vector, the up vector. Note that if the forward and right vector are normalized, then the resulting up vector computed from the cross product will be normalized too:
Vec3f up = crossProduct(forward, right);
Here again, you need to be careful about the order of the vectors involved in the cross product. Great we now have the three vectors defining the camera coordinate system. Let's now build our final 4x4 camera-to-world matrix.
• Step 4: set the 4x4 matrix using the right, up and forward vector as from point. All there is to do to complete the process is to build the camera-to-world matrix itself. For that, we just replace each row of the matrix with the right data:
• Row 1: replace the first three coefficients of the row with the coordinates of the right vector,
• Row 2: replace the first three coefficients of the row with the coordinates of the up vector,
• Row 3: replace the first three coefficients of the row with the coordinates of the forward vector,
• Row 4: replace the first three coefficients of the row with the coordinates of the from point.

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(from - to); 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 Limitation

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.