Blindly following a 3D engine tutorial: MVP matrix

I started a tutorial to learn about 3D engines, so far so good. The thing is I decided to do it on nalgebra an rust instead of using tools I feel comfortable with. 2x experience but 3x difficulty.

https://www.davrous.com/2013/06/13/tutorial-series-learning-how-to-write-a-3d-soft-engine-from-scratch-in-c-typescript-or-javascript/

During the MVP matrix creation I did something like this:

  1. mapped vertices and positions data to Point3, and rotation data to Rotation3
  2. Didn’t use Vector3 at all

Now I translated the bottom part of the tutorial (Called << The most important part: the Device object>>) to this rust code:

        let world_matrix =  mesh.rotation * Translation3::new(mesh.position.x,mesh.position.y,mesh.position.z);
        
        // The combination of the model with the view is still an isometry.
        let model_view = view * world_matrix;
        
        // Convert everything to a `Matrix4` so that they can be combined.
        let mat_model_view = model_view.to_homogeneous();
        
        // Combine everything.
        let model_view_projection = projection.as_matrix() * mat_model_view;

the rotation and the translation give me an isometry, which -by reading the documentation- I guess is correct. The problem is that I don’t have a way to multiply view* world_matrix.
This is the error I get:

error[E0277]: cannot multiply soft_engine::na::Isometry<f32, soft_engine::na::U3, soft_engine::na::Rotation<f32, soft_engine::na::U3>> to soft_engine::na::Isometry<f32, soft_engine::na::U3, soft_engine::na::Unit<soft_engine::na::Quaternion<f32>>>
–> src/soft_engine.rs:86:35
|
86 | let model_view = view * world_matrix;
| ^ no implementation for soft_engine::na::Isometry<f32, soft_engine::na::U3, soft_engine::na::Unit<soft_engine::na::Quaternion<f32>>> * soft_engine::na::Isometry<f32, soft_engine::na::U3, soft_engine::na::Rotation<f32, soft_engine::na::U3>>
|
= help: the trait std::ops::Mul<soft_engine::na::Isometry<f32, soft_engine::na::U3, soft_engine::na::Rotation<f32, soft_engine::na::U3>>> is not implemented for soft_engine::na::Isometry<f32, soft_engine::na::U3, soft_engine::na::Unit<soft_engine::na::Quaternion<f32>>>

Do I have to call .to_homogeneous() on both (view and world_matrix) and remove the mat_model_view declaration?

is there a simpler way of representing this and I’m just thinking it wrong from the beggining?

And final question. once I have the model_view_projection matrix, how do I project the Point3? (is Point3 correct or would it be better to use Vector3?)

Thank you in advance

Hi!

It appears that you are mixing several rotation representations. Your view appears to be using a na::Rotation (i.e. a rotation matrix) for its rotational part while your world_matrix is using a UnitQuaternion (i.e. a quaternion rotation) for its rotational part. This mixup in rotation representations is what makes the multiplication fails. So you should settle on a single representation for all your rotation.

So one solution would be to map rotations to UnitQuaternion instead of Rotation3.

is there a simpler way of representing this and I’m just thinking it wrong from the beggining?

What you are doing looks right to me.

Though if you would prefer to work directly with plain 4x4 matrices instead of using nalgebra’s dedicated types for transformations, you may use nalgebra-glm which will have everything you need for manipulating raw matices (and everything you need to follow closely any tutorial for building MVP matrices with the GLM C++ library).

And final question. once I have the model_view_projection matrix, how do I project the Point3? (is Point3 correct or would it be better to use Vector3?)

You can use model_view_projection.transform_point(&point).

Thank you very much, that was exactly the problem. I also had the inverse order in the matrix multiplication, so that was rotated incorrectly and I was not seeing my sample cube.

I had to do some adjustements:

let view   = Isometry3::look_at_lh(&camera.position, &camera.target, &Vector3::y());
let mut projection = Matrix4::new_perspective(self.width as f32 /self.height as f32, 0.78, 0.1, 1.0);
projection[(2,2)] *= -1.0; // !!!??? I had to make this so
projection[(3,2)] *= -1.0; // the perspective matrix would match the tutorial
let world_matrix =  UnitQuaternion::from(mesh.rotation) * Translation3::new(mesh.position.x,mesh.position.y,mesh.position.z);
let model_view = view * world_matrix;
let mat_model_view = model_view.to_homogeneous();
let model_view_projection : Matrix4<f32> = projection * mat_model_view;
for vertex in &mesh.vertices {
    let point = self.project(&vertex, &model_view_projection);
    self.draw_point(point.x, point.y);
}

I don’t quite understand the z sign that I had to flip. I saw that there are left and right handed coordinate system, but the tutorial (in the repository, not in the page) and the code is using left handed system, so I don’t quite follow why it’s different. The rest of the matrix, and all other matrices useed are the same as the tutorial.

For future reference, if anyone else needs a hint, here is the project code:

fn project(&self, coord:&Point3<f32>, mat : &Matrix4<f32>) -> Point2<u32>{
        let point = mat.transform_point(&coord);
        // The transformed coordinates will be based on coordinate system
        // starting on the center of the screen. But drawing on screen normally starts
        // from top left. We then need to transform them again to have x:0, y:0 on top left.
        let x = point.x * self.width as f32 + self.width as f32 / 2.0;
        let y = -point.y * self.height as f32 + self.height as f32 / 2.0;
        Point2::new(x as u32, y as u32)
}

so, other than the z sign, I’d like to know if there is any way to easily apply a rotation. I’d like to use the most idiomatic nalgebra possible.

right now I’m doing this on each render pass:

    cube.rotation *= UnitQuaternion::from_scaled_axis(&Vector3::x() * 0.01).to_rotation_matrix();
    cube.rotation *= UnitQuaternion::from_scaled_axis(&Vector3::y() * 0.01).to_rotation_matrix();

Is there an easier way to rotate my demo cube? rotation is Rotation3 right now, and I experimented with UnitQuaternion as you mentioned, but I didn’t find a clean way of slowly increasing the rotation on an axis like that, or at least I didn’t find a way that looks better on code.

You may wonder why I fight and experiment like this. Other than learning, I’d love to share the tutorial code translated into rust and nalgebra, so it’s useful to the next person who tries to understand 3D basics and operations for the first time like me, so I want to make it as clean and idiomatic as possible, and I’d also like to avoid nalgebra-glm. I found nalgebra to require me to be super explicit, and that’s a good thing when you are learning.

Thank you in advance

nalgebra’s perspective projection is based on a right-handed coordinate system. The multiplications by -1.0 that you are doing is what makes it left-handed like the tutorial.

It appears that in the tutorial cube.rotation is just a 3D vector which is converted to a rotation matrix in the render loop. So you could just have cube.rotation be a Vector3 and do:

    cube.rotation += Vector3::new(0.01, 0.01, 0.0);
    // ...
    let world_matrix  = Translation::from(mesh.position) * UnitQuaternion::from_local_axis(&mesh.rotation);

This is closer to what the tutorial does.

Also, the tutorial is using Euler angles so perhaps UnitQuaternion::from_euler_angles(mesh.rotation.z, mesh.rotation.x, mesh.rotation.y) will give you a more similar result.

1 Like

Thank you very much. Your advice has been vital and allowed me to move forward.

Since I’m new to this whole subject I got confused with the documentation and assumed it was left-handed:

The actual shape to be transformed depends on the projection itself. Note that projections implemented on nalgebra also flip the z axis. This is a common convention in computer graphics applications for rendering with, e.g., OpenGL, because the coordinate system of the screen is left-handed.

https://www.nalgebra.org/projections/

Looks like our documentation is not accurate enough. To be more precise, the nalgebra projection will transform vector/points expressed in a right-handed coordinate system into vectors/points expressed in a left-handed coordinate system. Your tutorial’s projection transforms vectors/points expressed in a left-handed coordinate system into vector/points expressed in a left-handed coordinate system.

In short:

  • nalgebra projection: right-handed input, left-handed output.
  • tutorial projection: left-handed input, left-handed output.

And this transition between two handedness is why you have the different signs for nalgebra’s projection.

1 Like

For future reference, this is the final 3D render. 60FPS @ 1920x1080 on a Ryzen 7 1800x.

It loads babylon exported files, and renders them with shadows and texture using nalgebra and minifb:

I tried to translate the code as close to the original tutorial as possible too.

Thanks for your help!

By the way… what would be the equivalent to “transformNormal”? I will find it eventually, it’s just that I haven’t studied it in detail yet. transform seems to work OK though. It’s for the back-face culling stage. (I’m on it!)
What I did so far is

let model_view = view * world_matrix;
let mat_model_view = model_view.to_homogeneous();
let inverse_transpose_view = mat_model_view.try_inverse().unwrap().transpose();

Then I simply call inverse_transpose_view.transform_vector(&face.normal) but it’s not really working out. Even calling transform_vector on the mat_model_view directly works better.

Update: I simply debugged it and it seems like transform_vector gets me the vector I need… and I don’t seem to find any big performance penalty, so I’ll stick to that. Thanks!

Thank you for sharing your work!

One small detail, instead of doing:

let model_view = view * world_matrix;
let mat_model_view = model_view.to_homogeneous();
let inverse_transpose_view = mat_model_view.try_inverse().unwrap().transpose();

you should call inverse before to_homogeneous. It will be more efficient to inverse an Isomety3 than a whole Matrix4.

let inverse_model_view = (view * world_matrix).inverse();
let inverse_transpose_view = model_view.inverse().to_homogeneous().transpose();
1 Like