Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 32

Creating the MVP Matrix

Recommended resources

Introduction

Welcome back! In our last lesson, we built the perspective projection matrix, the 'P' in our 3D transformation pipeline. This matrix is essential for creating the illusion of depth by projecting a 3D viewing volume (the frustum) into a 2D space.

In this lesson, we will complete the transformation pipeline by achieving the following learning outcome: Combine model, view, and projection matrices into a single MVP matrix.

We'll start by defining the roles of the Model ('M') and View ('V') matrices. Then, we'll see why combining all three into a single Model-View-Projection (MVP) matrix is a crucial optimization. Most importantly, you will learn the correct order for multiplying these matrices—a critical and often misunderstood step. Finally, we'll see how to implement this on the CPU in C and how to use the resulting MVP matrix in our GLSL vertex shader.

This lesson ties together all the 3D math we've covered, allowing us to transform a vertex from its original design coordinates all the way to the final coordinates ready for the screen.


The Full Transformation Pipeline

Before we combine the matrices, let's understand the journey a single vertex takes from being part of a model file to appearing on your screen. This journey involves moving through several coordinate systems, or "spaces."

3D Rendering Pipeline: Coordinate Space Transformations
This diagram shows the full pipeline. Our focus today is on combining the Model, View, and Projection matrices to handle the first three transformations in a single step.

As the diagram shows, the core steps are:

  1. Model Matrix: From Local Space (the model's own coordinate system) to World Space (the global scene).
  2. View Matrix: From World Space to View Space (relative to the camera).
  3. Projection Matrix: From View Space to Clip Space (the normalized cube we discussed previously).

To gain a more intuitive feel for what each of these matrices does, let's watch a short, conceptual video.

Model View Projection Matrices

This video by Jamie King provides an excellent visual demonstration of the Model, View, and Projection transformations. He uses the clearer terms 'Model-to-World', 'World-to-View', and 'View-to-Projection' to describe their roles.

Watch from 02:13 to 14:03. As you watch, focus on understanding: Model-to-World (Model Matrix): How separate scale, rotation, and translation sliders are used to place individual objects (a box, a sphere) in the world. Notice that each object gets its own unique model matrix. World-to-View (View Matrix): How moving the camera is conceptually the same as moving the entire world in the opposite direction. Notice that all objects in the scene share the same view matrix. The final result of these two transformations, which positions the objects correctly from the camera's point of view.


Combining Matrices: The Order of Operations

As the video explained, to render an object, we need to apply the Model, then the View, and finally the Projection matrix to each of its vertices. We could do this by sending three separate matrices to the vertex shader and performing three matrix-vector multiplications for every vertex.

// Inefficient approach
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

// ...
void main() {
    vec4 worldPos = model * vec4(position, 1.0);
    vec4 viewPos = view * worldPos;
    gl_Position = projection * viewPos;
}

However, matrix multiplication is an expensive operation. A more efficient way is to pre-multiply the three matrices on the CPU once per object per frame and send a single, combined MVP matrix to the shader.

This is possible because matrix multiplication is associative: (A * B) * C = A * (B * C). This allows us to group the matrices together before multiplying by the vertex vector.

The transformation of a vertex v is:
v_final = Projection * (View * (Model * v_initial))

We can pre-calculate the combined matrix:
MVP = Projection * View * Model

And then the calculation in the shader becomes a single, efficient operation:
v_final = MVP * v_initial

The multiplication order is critical. Since matrix multiplication is not commutative (A * B ≠ B * A), the order must be Projection * View * Model. The transformation that is applied first to the vertex (the Model matrix) must appear on the rightmost side of the multiplication chain.

The following resource confirms this and shows how the final code should look.

Tutorial 2 Part A: Vertex Transformation

This section from a tutorial by Newcastle University directly shows the GLSL code for creating and applying the combined MVP matrix. It explicitly highlights the correct multiplication order.

Read the short section titled 'Vertex Shader'. Pay close attention to line 15: mat4 mvp = projMatrix * viewMatrix * modelMatrix;. This line is the central concept of today's lesson.


Implementation in C and GLSL

Now, let's put this into code. The process involves creating each matrix individually on the CPU and then multiplying them together in the correct order.

1. Creating the Individual Matrices

You already have a function mat4_perspective from the last lesson. We now need functions to create the Model and View matrices.

Model Matrix:
The model matrix is typically a combination of scaling, rotation, and translation. The order of these operations matters as well. The standard order is Scale, then Rotate, then Translate. This ensures that we rotate and move the object after it has been scaled in its own local space.

// Assume you have mat4_scale, mat4_rotate_z, mat4_translate, and mat4_mul from previous lessons.

mat4 model_matrix;
mat4_identity(model_matrix); // Start with identity

mat4 scale_matrix, rot_matrix, trans_matrix;

// Create individual transformation matrices
mat4_scale(scale_matrix, scale_vector);
mat4_rotate_z(rot_matrix, rotation_angle_in_radians); // Example: rotate around Z
mat4_translate(trans_matrix, position_vector);

// Combine them: model = translate * rotate * scale
mat4 temp;
mat4_mul(temp, rot_matrix, scale_matrix);
mat4_mul(model_matrix, trans_matrix, temp);

View Matrix:
The view matrix positions the camera. As the video explained, it's the inverse of the camera's own transformation in the world. A common way to create this is with a lookAt function, which builds the matrix from a camera position, a target position, and an "up" vector.

Deriving the lookAt matrix involves constructing an orthonormal basis (a set of three mutually perpendicular unit vectors) and is a great exercise. The video below explains this derivation in detail, which will connect well with your mathematical background.

OpenGL with C++ 5: Transformations

Let's watch a section of the 'GetIntoGameDev' video that derives the View matrix from scratch. This gives a deep understanding of how a lookAt function works.

Watch from 36:45 to 57:18. The explanation is quite dense, so focus on the high-level concepts: The view transform is the inverse of the camera's model transform. It's built from the camera's forward, right, and up vectors. These vectors are calculated using cross products and normalization to form an orthonormal basis. Don't worry about memorizing the full derivation; absorb the principles. I will provide a simplified lookAt implementation below.

Here's a simplified C implementation of mat4_look_at based on that logic. You'll need vector helper functions (vec3_sub, vec3_normalize, vec3_cross, vec3_dot) which you should have from Module 2.

// Add to your math library
void mat4_look_at(mat4 out, vec3 eye, vec3 center, vec3 up) {
    vec3 f, s, u;

    // f (forward) = normalize(center - eye)
    vec3_sub(f, center, eye);
    vec3_normalize(f);

    // s (right) = normalize(cross(f, up))
    vec3_cross(s, f, up);
    vec3_normalize(s);

    // u (up) = cross(s, f)
    vec3_cross(u, s, f);

    out[0][0] = s[0];
    out[0][1] = u[0];
    out[0][2] = -f[0];
    out[0][3] = 0.0f;

    out[1][0] = s[1];
    out[1][1] = u[1];
    out[1][2] = -f[1];
    out[1][3] = 0.0f;

    out[2][0] = s[2];
    out[2][1] = u[2];
    out[2][2] = -f[2];
    out[2][3] = 0.0f;

    out[3][0] = -vec3_dot(s, eye);
    out[3][1] = -vec3_dot(u, eye);
    out[3][2] = vec3_dot(f, eye);
    out[3][3] = 1.0f;
}

2. Combining into MVP and Updating the Shader

Now you can bring everything together in your main loop.

// --- In your application setup ---
// Get uniform location for the combined MVP matrix
GLuint mvp_location = glGetUniformLocation(shader_program, "mvp");

// --- In your main loop ---
// 1. Create Model matrix (unique for each object)
mat4 model_matrix;
// ... create model matrix using scale, rotate, translate ...

// 2. Create View matrix (same for all objects in the scene)
mat4 view_matrix;
vec3 camera_pos   = {0.0f, 0.0f, 3.0f}; // Move camera back 3 units
vec3 camera_target = {0.0f, 0.0f, 0.0f}; // Look at the origin
vec3 up_vector    = {0.0f, 1.0f, 0.0f};
mat4_look_at(view_matrix, camera_pos, camera_target, up_vector);

// 3. Create Projection matrix (same for the frame)
mat4 projection_matrix;
mat4_perspective(projection_matrix, fov, aspect_ratio, near, far);

// 4. Combine into MVP = P * V * M
mat4 mvp_matrix;
mat4 view_projection;
mat4_mul(view_projection, projection_matrix, view_matrix);
mat4_mul(mvp_matrix, view_projection, model_matrix);

// 5. Send the final MVP matrix to the shader
glUseProgram(shader_program);
glUniformMatrix4fv(mvp_location, 1, GL_FALSE, &mvp_matrix[0][0]);

// 6. Draw your object
// ... glDrawElements call ...

And your vertex shader becomes beautifully simple:




#version 330 core
layout (location = 0) in vec3 aPos;

// Single uniform for the combined transformation
uniform mat4 mvp;

void main() {
    // Transform vertex to clip space in one step
    gl_Position = mvp * vec4(aPos, 1.0);
}

This approach is highly efficient because the heavy matrix multiplications are done once on the CPU per object, and the GPU only has to do one matrix-vector multiplication for each of the thousands (or millions) of vertices.


Conclusion

In this lesson, you've assembled the complete transformation chain that takes a 3D model from its local space all the way to clip space, ready for rendering.

Key Takeaways:

  • The Model matrix positions, rotates, and scales an object in the world.
  • The View matrix positions and orients the camera in the world.
  • The Projection matrix defines the camera's viewing volume and creates perspective.
  • These are combined into a single MVP matrix for efficiency.
  • The correct multiplication order is MVP = Projection * View * Model. This is because transformations are applied to vertices from right to left.
  • This combination is done on the CPU, and the resulting single matrix is sent to the vertex shader as a uniform.

Preview of the Next Lesson:
We now have all the theoretical and practical tools needed to render a proper 3D object. In the next lesson, we will apply the MVP transformation to vertex coordinates in the vertex shader by defining the vertices for a 3D cube and drawing it using the MVP matrix we learned to construct today.