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.

Exercise 1

In the render loop (in main()), we do three things:

  • Update time: currentTime
  • Render: display()
  • Handle events: while (SDL_PollEvent(&event)) {...}
Test the following (do not read documentation this time), which of the following units is currentTime measured in? (nano/micro/milli seconds/minutes/years):
Which types of events do we poll for:
Which types of events do we catch/handle:
How can we test if the right button is pressed on a mouse event:
How can we test if both the left and the right button are pressed simultaneously on a mouse event:

Task 1: Moving the car

In this laboration, 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.

The first matrix we focus on is the model matrix, which tranform 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, but 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. 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} $$ First make a hardcoded translation, e.g. by $t = (0.0, 0.0, 5.0)$, and make sure that the car move while the city stays the same. Then add keyboard controls for the translation, e.g. the arrow keys. One way is to add 'speed' translation to the $z$-component for each frame that Up is pressed down (try speed 0.3f for this scene). Now 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
float speed = 0.3f;
static mat4 T(1.0f), R(1.0f);
if (state[SDL_SCANCODE_UP]) {
	T[3] += speed * vec4(0.0f, 0.0f, 1.0f, 0.0f);
}
if (state[SDL_SCANCODE_DOWN]) { 
	T[3] -= speed * vec4(0.0f, 0.0f, 1.0f, 0.0f);
}
if (state[SDL_SCANCODE_LEFT]) {
	T[3] += speed * vec4(1.0f, 0.0f, 0.0f, 0.0f);
}
if (state[SDL_SCANCODE_RIGHT]) {
	T[3] -= speed * vec4(1.0f, 0.0f, 0.0f, 0.0f);
}
And use $T$ as the model matrix for the car:
carModelMatrix = T;

Next up, we will add some steering to the car by rotating its orientation. A pure rotation around origo (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 pair-wise orthogonal). $$ 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} $$ drive direction update From start, we will use an identity matrix for rotation matrix, and we will replace the side-way translation when we press Left/Right for a rotation of the car. We can perform the rotation in terms of an angle, but we will make it quick and dirty for now. We will keep $y$ and only rotate in the $x$-$z$ plane. Add/subtract a portion of the third base vector to the first base vector with Left/Right:

if (state[SDL_SCANCODE_LEFT]) {
	R[0] -= 0.03f * R[2];
}
if (state[SDL_SCANCODE_RIGHT]) {
	R[0] += 0.03f * R[2];
}
And then we make $R$ orthonormal again by normalizing $x$ and recomputing $z$: $$ R_0 = \frac{R_0}{||R_0||} $$ $$ R_2 = R_0 \times R_1 $$ Which becomes:
// Make R orthonormal again
R[0] = normalize(R[0]);
R[2] = vec4(cross(vec3(R[0]), vec3(R[1])),  0.0f)
Try $R$ for model matrix:
carModelMatrix = R;
Then try the concatenation of $T$ and $R$ for model matrix:
carModelMatrix = T * R;
After steering (rotating) the car, we still are 'driving' in the same direction. Now make the translation happen in the direction of the car.

Hint: Instead of adding a small translation in a fixed direction, use the 'forward direction' as one of the base vectors we have changed into with the multiplication of $R$.

What does the model matrix transform? (strictly speaking)

Will $RT$ produce the same transformation as $TR$? Why/Why not?

Task 2: 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 the second car such that it drives in a circle around your starting position. 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);

Task 3: 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 in origo:

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 rotate the world space coordinate (after the translation). We also need to decide what direction that should be the cameraUp. We can choose the direction closest to the worldUp direction, but still 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.

Task 4: Perspective Transform

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. Uncomment the gui() call to do it interactively.

Optional task 5: 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.

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.