Introduction

In this tutorial we are going to make things move, or animate. The goals are to be able to move the camera to look at things from different angles, and to make a part of the scene move as time progresses, and even interact with user input.

We will focus on transforming geometry so we abstract away the setup of vertex data and textures. We will import a 3D model from an .OBJ file (wavefront) instead of specifying the data manually.

Run the program, press mouse buttons and move the mouse around and see what happens. Then look through the code and make sure you understand it all so far.

The lecture on vectors and transforms explains the model-/world-/view-space frames of reference, so it's useful to have gone through it.

Moving the car

In this lab we finally move completely into the 3d space. That is, we will have a Model-, a View- and a Projection matrix which will be used to transform all vertices from their model-space coordinates to their window coordinates.

In OpenGL and, by extension, in the GLM library we use to handle math types, matrices are stored in column-major order. That way, to create the matrix

\[ M = \begin{bmatrix} a & b & c & d\\ e & f & g & h\\ i & j & k & l\\ m & n & o & p \end{bmatrix} \]

One would write

mat4 M(a, e, i, m,
	   b, f, j, n,
	   c, g, k, o,
	   d, h, l, p);

The first matrix we focus on is the model matrix, which tranforms points from the model space (specific to a model) to the world space (common for all models).

As of now, the model matrices for the city and the car are both the identity matrix, which means that the vertex coordinates will have the same position in world space as in model space, so we will start by applying a translation to the car (but keeping the city fixed).

Replace the modelMatrix of the car with a translation matrix that you construct yourself, or use the glm function translate(). A translation matrix looks like a 4 by 4 identity matrix, but the last column contains the translation. When we apply the translation matrix \(T\) on a vertex position \(p\) we actually make this computation:

\[ T p = \begin{bmatrix} 1 & 0 & 0 & t_x\\ 0 & 1 & 0 & t_y\\ 0 & 0 & 1 & t_z\\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} = \begin{bmatrix} x + t_x \\ y + t_y \\ z + t_z \\ 1 \end{bmatrix} \]

This could be used to add a minor optimisation to the code, but for the sake of clarity we will keep using matrix multiplication to update transformations.

Try it out yourself by setting the model matrix of the car to be a translation matrix that moves it by 5 along the Y axis, making the car float high over the ground.

Now lets add keyboard controls for the translation, i.e. the arrow keys. One way is to add 'speed' translation to the \(z\)-component for each frame that Up is held pressed.

Add controls for translation along the \(x\)-\(z\) plane and try it out:

// check keyboard state (which keys are still pressed)
const uint8_t *state = SDL_GetKeyboardState(nullptr);

// implement controls based on key states
const float speed = 10.f;
vec3 car_forward = vec3( 0, 0, 1 );
if(state[SDL_SCANCODE_UP])
{
	T = translate(car_forward * speed * deltaTime) * T;
}
if(state[SDL_SCANCODE_DOWN])
{
	T = translate(-car_forward * speed * deltaTime) * T;
}
if(state[SDL_SCANCODE_LEFT])
{
	T = translate(vec3(1, 0, 0) * speed * deltaTime) * T;
}
if(state[SDL_SCANCODE_RIGHT])
{
	T = translate(-vec3(1, 0, 0) * speed * deltaTime) * T;
}

And use \(T\) as the model matrix for the car:

carModelMatrix = T;
Click here to see solution (But try yourself first! You have to understand each step.)

Steering

Next up, we will add some steering to the car by rotating its orientation. A pure rotation around the origin (no scaling, no translation) is just a change of base and the new base vectors are the columns of the upper-left 3 by 3 matrix. Pure rotations should be an orthonormal base (each base vector should have unit length, and all base vectors should be orthogonal to each other).

\[ R = \begin{bmatrix} r_{00} & r_{01} & r_{02} & 0\\ r_{10} & r_{11} & r_{12} & 0\\ r_{20} & r_{21} & r_{22} & 0\\ 0 & 0 & 0 & 1 \end{bmatrix} \]

From start, we will use an identity matrix for rotation matrix, and we will replace the sideway translation when we press Left/Right for a rotation of the car. We can use glm's rotate() function to calculate a rotation matrix we can multiply with our current matrix to update it.

const float rotateSpeed = 2.f;
if(state[SDL_SCANCODE_LEFT])
{
	R = glm::rotate(rotateSpeed * deltaTime, glm::vec3(0, 1, 0)) * R;
}
if(state[SDL_SCANCODE_RIGHT])
{
	R = glm::rotate(-rotateSpeed * deltaTime, glm::vec3(0, 1, 0)) * R;
}

Try \(R\) as the model matrix:

carModelMatrix = R;

Then try the concatenation of \(T\) and \(R\) as the model matrix:

carModelMatrix = T * R;

After steering (rotating) the car, we are still 'driving' in the same direction. To fix that, you need to make the translation happen in the direction of the car.

Hint: You need to apply the car's current rotation to the car's "untransformed velocity vector", so that the translation will follow that direction. The "untransformed velocity vector" should be the same as the original forward direction of the car.

Click here to see solution (But try yourself first! You have to understand each step.)

Building the Model Matrix

What does the model matrix transform? (strictly speaking)

Will \(RT\) produce the same transformation as \(TR\)? Why/Why not?

Time-dependent animations

Render the car another time (a second carModel->render() call), with another model matrix, effectively reusing the car model and adding another car to our application. Make the model matrix of this second car such that it drives in a circle around the roundabout. The position in the track should depend on the float variable currentTime. For this task, use glm's functions for transformations.

Translate:

mat4 translate(vec3 t);

Rotate (around an axis):

mat4 rotate(float radians, vec3 axis);

And if you like, the scale matrix (one scale factor per axis):

mat4 scale(vec3 scale);

The roundabout has a radius of 10 units, and its center is located at (25, 0, 0). Remember from the previous task that \(TR\) is not the same as \(RT\) when creating the transformation matrix and that, usually, the correct way to create it is using \(TR\), but you can approach this task in basically 2 ways: you can try to build the \(R\) and \(T\) matrices separately and then multiply them as \(TR\); or you can think of it in a hyerarchical way and compound a few rotations and translations that accomplish the same result.

Click here to see solution (But try yourself first! You have to understand each step.)

Adding camera control

The camera is defined by the two 3D vectors cameraPosition and cameraDirection that are declared as global variables. We will control these with the mouse and keyboard. We rotate the camera direction based on the mouse motion when the left mouse botton is pressed down. With a horizontal mouse movement, we will rotate the camera direction around the worldUp direction, and with a vertical mouse movement, we will rotate around an axis orthogonal to both the worldUp and cameraDirection. Replace the code for the mouse motion event:

if (event.button.button & SDL_BUTTON(SDL_BUTTON_LEFT)) {
    float rotationSpeed = 0.005f;
    mat4 yaw = rotate(rotationSpeed * -delta_x, worldUp);
    mat4 pitch = rotate(rotationSpeed * -delta_y, normalize(cross(cameraDirection, worldUp)));
    cameraDirection = vec3(pitch * yaw * vec4(cameraDirection, 0.0f));
}

For translation, we will use the W,S keys. Your task is to make the cameraPosition move a small amount in the cameraDirection when W is pressed, and in the opposite direction when S is pressed.

We will now replace the constant view matrix (in display) with one we control with our keyboard and mouse. The view matrix transform world space coordinates into view space coordinates. In view space, the cameras position is at the origin. So the first transform (the right-most) is a translation that puts the cameras world space position as the origin:

mat4 viewMatrix = cameraRotation * translate(-cameraPosition);

In view space, we look down the negative z-axis (at least in OpenGL). So we need to make a rotation matrix that rotates the world space coordinate (after the translation). We also need to decide what should be the up direction of the camera, cameraUp. We can choose the direction that's closest to the worldUp direction and orthogonal to the cameraDirection. The third base vector is then fixed since it has to be an orthonormal base. The desired base vectors are [cameraRight, cameraUp, -cameraDirection] and we compute the base vectors as:

// use camera direction as -z axis and compute the x (cameraRight) and y (cameraUp) base vectors
vec3 cameraRight = normalize(cross(cameraDirection, worldUp));
vec3 cameraUp = normalize(cross(cameraRight, cameraDirection));

mat3 cameraBaseVectorsWorldSpace(cameraRight, cameraUp, -cameraDirection);

To get the rotation matrix that rotate the world into this base, we take the inverse of the matrix. Since the rotation is an orthonormal base, the inverse is just the transpose. We can now put together the final view matrix:

\[ \mathrm{Camera Base Vectors_\mathrm{ws}} = \begin{bmatrix} \mathrm{cameraRight}_x & \mathrm{cameraUp}_x & \mathrm{-cameraDir}_x\\ \mathrm{cameraRight}_y & \mathrm{cameraUp}_y & \mathrm{-cameraDir}_y\\ \mathrm{cameraRight}_z & \mathrm{cameraUp}_z & \mathrm{-cameraDir}_z \end{bmatrix}_{\mathrm{ws}} \]
\[ R_{\mathrm{camera}} = \mathrm{Camera Base Vectors_\mathrm{ws}}^{-1} = \begin{bmatrix} \mathrm{cameraRight_x} & \mathrm{cameraRight_y} & \mathrm{cameraRight_z} & 0\\ \mathrm{cameraUp_x} & \mathrm{cameraUp_y} & \mathrm{cameraUp_z} & 0\\ -\mathrm{cameraDir_x} & -\mathrm{cameraDir_y} & -\mathrm{cameraDir_z} & 0\\ 0 & 0 & 0 & 1 \end{bmatrix} \]
\[ M_\mathrm{view} = R_{\mathrm{camera}} T_{\mathrm{camera}} \]
mat4 cameraRotation = mat4(transpose(cameraBaseVectorsWorldSpace));
mat4 viewMatrix = cameraRotation * translate(-cameraPosition);

Move around in the world and see if the camera controls are working properly.

Click here to see solution (But try yourself first! You have to understand each step.)

Perspective Transform and Z-Fighting

The third component of the modelViewProjection transform is the projection transform. We use the glm function perspective() to generate this matrix. Have a look at the arguments to this function and play around with them. You can do it interactively with the gui, which you can toggle by pressing G (you can also comment out the if surrounding the call to gui() in main()).

Now uncomment the drawGround() call in the display() function. You will see that in the middle of the scene there's a new flat square. You will also notice that it looks like it's broken, and when you rotate the camera it will look like the brokenness changes (if it doesn't look broken right away, just try rotating the camera and you should see what we mean). This is what we call z-fighting.

Z-Fighting happens when two triangles are touching, or very close together, so close that the depth buffer doesn't have enough precision to determine which one is closer to the camera. In the case of the flat square we added, they are very close together, but not touching. If they were touching, only moving them away would prevent z-fighting, but since they are not, we can explore how different camera parameters can change the results.

Play around a bit with the values in the gui and try to find out which setting helps reduce or remove this effect.

What parameter did you change to reduce the z-fighting effect at the default camera distance?

What value did you set it to?

Note: z-fighting is not usually solved by changing the camera parameters. The value you found might work for this specific setup, but if you move the camera closer or farther away from the scene, you will see that the value you need is a different one.

This is because the depth buffer stores the depth in a non-linear way (more precision in some areas than others). In a real application, z-fighting will instead be avoided by moving things far apart enough that they don't z-fight anymore, removing parts of the geometry, or hiding it in some other way.

[OPTIONAL] Add your own camera control

Think about how games control the camera with W,A,S,D (or arrow keys) and the mouse. Try to implement something similar. You can add a checkbox to the GUI to select if the camera should be the initial one or this new one you create.

Suggestions: First person while driving the car. Third person while driving the car.


When done, show your result to one of the assistants. Have the finished program running and be prepared to explain what you have done.