Advertisement

Trouble with my Perspective Projection Matrix

Started by March 07, 2018 01:50 AM
7 comments, last by Hashbrown 6 years, 6 months ago

I'm trying to learn how to make my own model, view, projection setup. I've managed to translate, rotate, and scale my models, but have an issue with my perspective projection matrix.

Even though I'm multiplying halfFOV with my aspect ratio, the image looks squished unless my window is a perfect square like this:

perfect.png

If it's not a perfect square: the wider my window the more stretched my object looks in the Z axis like an egg.

Screen_Shot_2018-03-06_at_8.32.02_PM.png

So it definitely has to do with with my projection matrix, specifically related to my aspect ratio. The way I'm multiplying my matrices is as follows, I'll show you my translation and projection keep it simple:


Translation

[1, 0, 0, 0,
 0, 1  0, 0
 0, 0, 1, 0,
 x, y, z, 1]
 
Projection

let halfFOV = Math.tan(toRad(FOV/2.0));
let zRange = (NEAR - FAR);

let x = 1.0 / (halfFOV * aspect);
let y = 1.0 / (halfFOV);
let z = (NEAR + FAR) / zRange;
let w = 2 * FAR * NEAR / zRange;

 [x, 0, 0, 0,
  0, y, 0, 0, 
  0, 0, z, -1,
  0, 0, w, 0]

I normally see the -1 where the w is, but for some reason I need to set it up the way you see it in my matrix, if not it won't work. I'll also share how I'm multiplying matrices very quickly:


function (r, a, b) 
	
	r.mat[0]  = (a.mat[0] * b.mat[0])  + (a.mat[1] * b.mat[4])  + (a.mat[2]  * b.mat[8])  + (a.mat[3] * b.mat[12]);
    r.mat[1]  = (a.mat[0] * b.mat[1])  + (a.mat[1] * b.mat[5])  + (a.mat[2]  * b.mat[9])  + (a.mat[3] * b.mat[13]);
    r.mat[2]  = (a.mat[0] * b.mat[2])  + (a.mat[1] * b.mat[6])  + (a.mat[2]  * b.mat[10]) + (a.mat[3] * b.mat[14]);
    r.mat[3]  = (a.mat[0] * b.mat[3])  + (a.mat[1] * b.mat[7])  + (a.mat[2]  * b.mat[11]) + (a.mat[3] * b.mat[15]);

	// don't need to add the rest of it...
    
   	// How I use it
    Mathf.mul(resultMat, position, projection);

So I take the left row and multiply it against the right column. I then get rows as a result as you can see. If you want to check out the full implementation it's here. I've also checked that I'm getting the correct window size. I divide width/height to get the aspect ratio too. Not sure what I'm doing wrong. I also tried multiplying my matrices on the gpu (glsl) and I gt the same results, so it's definitely my projection matrix :(

Hope this all makes sense.

Edit: I probably should have posted this thread in the Math categories, my apologies.

 

 

From my C++ code, it seems as if you are missing 2 components in the third row


float x = (2.0f * zNear) / (right - left);
float y = (2.0f * zNear) / (top - bottom);
float a = (right + left) / (right - left);
float b = (top + bottom) / (top - bottom);
float z = -(zFar + zNear) / (zFar - zNear);
float w = -(2.0f * zFar * zNear) / (zFar - zNear);
            
result = new Matrix4(x, 0, 0,  0,
                     0, y, 0,  0,
                     a, b, z, -1,
                     0, 0, w, 0);

This would take the window bounds into account, dont know if you have access to them

Advertisement
1 hour ago, Shaarigan said:

From my C++ code, it seems as if you are missing 2 components in the third row



float x = (2.0f * zNear) / (right - left);
float y = (2.0f * zNear) / (top - bottom);
float a = (right + left) / (right - left);
float b = (top + bottom) / (top - bottom);
float z = -(zFar + zNear) / (zFar - zNear);
float w = -(2.0f * zFar * zNear) / (zFar - zNear);
            
result = new Matrix4(x, 0, 0,  0,
                     0, y, 0,  0,
                     a, b, z, -1,
                     0, 0, w, 0);

This would take the window bounds into account, dont know if you have access to them

Thanks for the quick answer Shaarigan. I'm afraid I'm not following :( 
left would be the -width of my window, and right would be +width? I was also wondering if I can still use FOV with your implementation.

Strange, the only problem I have is the depth of the object. The width and height is great, but the z I guess stays the same, leaving that egg form.

You're definitely right, I'm missing properties that consider the width and height in my depth.

Sorry so I explain,


float bottom = zNear * Math::Tan(0.5f * fovy);
float top = -bottom;
float left = top * aspect;
float right = bottom * aspect;

(I hope to wrote it correctly from memory :D )

This calculates from field of view and aspect ration the display rectangle your window will hold. We assume that the center point of the window is the [0, 0, 0] coord, left is -half of the field of view * aspect where right is half of the field of view * aspect.

normalisedCoords.gif

The problem you have in your matrix is that you dont take the precentage width and percentage height of your window into account. This means that you render assuming that the points [1, 0, 0] and [-1, 0, 0] are always the same regardless of the real size of your window. This will result in mapping a square into your rectangular projection plane what will become stretched from the hardware driver.

filling the matrix with the above values at a and b (or directly as I do)


result.Value[0] = (2.0f * zNear) / (right - left);
result.Value[5] = (2.0f * zNear) / (top - bottom);
result.Value[8] = (right + left) / (right - left);
result.Value[9] = (top + bottom) / (top - bottom);
result.Value[10] = -(zFar + zNear) / (zFar - zNear);
result.Value[11] = -1;
result.Value[14] = -(2.0f * zFar * zNear) / (zFar - zNear);

Adds and additional multiplication factor on the X and Y coords that will scale the points [1, 0, 0] and [-1, 0, 0] (your window corners) so that they become a factor above 0 and below 1 your vertices will be multiplied with to stay always on the same direction

Hi, Hashbrown! The math actually looks fine to me, are you sure there's a problem?

I've always thought that this kind of distortion is to be expected from a perspective projection. Something to do with the fact that we're projecting a 3D scene onto a 2D plane. In the second image the spheres are closer to the edge of the screen, which results in the more apparent distortion, but they aren't perfectly circular in the first image either. Here's an illustration:

https://en.wikipedia.org/wiki/File:Fig_3._After_pivoting_object_to_remain_facing_the_viewer,_the_image_is_distorted,_but_to_the_viewer_the_image_is_unchanged..png

Shaarigan, could you please explain, what a and b coefficients in your implementation do? I'm using basically the same matrix as Hashbrown, so I'm guessing, yours is a more general form? I mean, if left == -right and top == -bottom, then both a and b are 0, yielding the same matrix as Hashbrown has. Setting those to different values results in some kind of skewed/asymmetrical frustum?

1 hour ago, dietrich said:

Hi, Hashbrown! The math actually looks fine to me, are you sure there's a problem?

I've always thought that this kind of distortion is to be expected from a perspective projection. Something to do with the fact that we're projecting a 3D scene onto a 2D plane. In the second image the spheres are closer to the edge of the screen, which results in the more apparent distortion, but they aren't perfectly circular in the first image either. Here's an illustration:

https://en.wikipedia.org/wiki/File:Fig_3._After_pivoting_object_to_remain_facing_the_viewer,_the_image_is_distorted,_but_to_the_viewer_the_image_is_unchanged..png

Shaarigan, could you please explain, what a and b coefficients in your implementation do? I'm using basically the same matrix as Hashbrown, so I'm guessing, yours is a more general form? I mean, if left == -right and top == -bottom, then both a and b are 0, yielding the same matrix as Hashbrown has. Setting those to different values results in some kind of skewed/asymmetrical frustum?

 

Thanks dietrich, I'm still a little confused but after Shaarigan's explanation, I'm going to work it out step by step on paper until I get it. I'm definitely messing up on something. I'll also check your link.

 

1 hour ago, Shaarigan said:

Sorry so I explain,



float bottom = zNear * Math::Tan(0.5f * fovy);
float top = -bottom;
float left = top * aspect;
float right = bottom * aspect;

(I hope to wrote it correctly from memory :D )

This calculates from field of view and aspect ration the display rectangle your window will hold. We assume that the center point of the window is the [0, 0, 0] coord, left is -half of the field of view * aspect where right is half of the field of view * aspect.

normalisedCoords.gif

The problem you have in your matrix is that you dont take the precentage width and percentage height of your window into account. This means that you render assuming that the points [1, 0, 0] and [-1, 0, 0] are always the same regardless of the real size of your window. This will result in mapping a square into your rectangular projection plane what will become stretched from the hardware driver.

filling the matrix with the above values at a and b (or directly as I do)



result.Value[0] = (2.0f * zNear) / (right - left);
result.Value[5] = (2.0f * zNear) / (top - bottom);
result.Value[8] = (right + left) / (right - left);
result.Value[9] = (top + bottom) / (top - bottom);
result.Value[10] = -(zFar + zNear) / (zFar - zNear);
result.Value[11] = -1;
result.Value[14] = -(2.0f * zFar * zNear) / (zFar - zNear);

Adds and additional multiplication factor on the X and Y coords that will scale the points [1, 0, 0] and [-1, 0, 0] (your window corners) so that they become a factor above 0 and below 1 your vertices will be multiplied with to stay always on the same direction

 

Sharrigan, thank you again. As I mentioned above, I'm going to go through your solution step by step until I get it intuitively. I did try your matrix but unfortunately I still get that egg shape in the z axis :(  I'm clearly doing something wrong. I also had to switch back to my perspective matrix because with yours made my objects look insideout.

Screen_Shot_2018-03-07_at_6.20.42_AM.png

(perspective matrix calculations results in the screenshot)

This is the window setup I tried with:

Window Size 1160 x 310,
Window Aspect 3.7419~
Near: 0.1 | Far: 1000

My new perspective matrix


    let b = NEAR * Math.tan(toRad(0.5 * FOV));
    let t = -b;
    let l = t * aspect;
    let r = b * aspect;

    p.mat[0]  = (2.0 * NEAR) / (r - l);
    p.mat[5]  = (2.0 * NEAR) / (t - b);

    p.mat[8] = (r + l) / (r - l);
    p.mat[9] = (t + b) / (t - b);

    p.mat[10] = -(FAR + NEAR) / (FAR - NEAR);
    p.mat[11] = -1;
    p.mat[14] = -(2.0 * FAR * NEAR) / (FAR - NEAR);
    p.mat[15] = 0;

...but still the same egg z axis issue. I'm going to look over what you explained to get a better understanding.

 

Advertisement

Sorry (for you and everyone else) that I'm so worse in transporting my thougths to other people :)

 

What is your FOV parameter for the calculation? I usually go for


fov : 90 degree //you need to transform them to radiance (PI / 4)
asp : Width / Height //your example 1160 / 310 = 3.7419
znr : 0.1
zfr : 1000

 

No, no, you explained very nicely. I'm actually the slow one here to be honest lol. But it's a longer learning process for some of us :)

Update (Solved!):

I got it! I went through all 16 slots in the array and changed them to see what each one does, also did the multiplication on paper. Here are the results, no warping, just slightly due to the FOV which is normal. If I want no warping I can go down to 45degs, but again, it's very minor and normal.

nice1.gif

 

nice2gif.gif

(excuse the low framerate, had to lower the GIF size)

Explanation

No warping! :D Here's what I learned just in case somebody runs into a similar issue. Just as Shaarigan wisely indicated, I wasn't considering the aspect ratio in the Z axis.


[
  x, 0, 0, 0,
  0, y, 0, 0,
  a, b, z, -1,
  0, 0, w, 0
]
// Note: I did not use A and B, but I'll explain them.

I won't explain the obvious ones like NEAR, FAR, and FOV, but I will get into the NEAR PLANE a little below in my DEPTH explanation.

Scale

x: Scales the width.  Divide by the Aspect Ratio IF height > width.
y: Scales the height. Divide by the Aspect Ratio IF width  > height.
z: How "deep" the depth will be, a ratio dividing by the zRange (NEAR - FAR)

Depth

w: I noticed the lower the number (into the screen), the further the NEAR PLANE would be. It's like if you had a screen a few units in front of you and once an object touches it, the object disappears.


-1: Im not 100% sure about this one, but I noticed I can use it as a Z axis offset. It pushes the object back -1, and if I go lower, it pushes it further back. Leave it -1 though.

Extra (Position?)

a: This property was not very useful for me, It moves the object in the X axis. Note, it will move it with no perspective projection applied. So you won't see the side of a cube...it will move to the left or right with its face completely in front of you. Orthographic style.

b: Same as A, but the Y Axis. 

(Also, if you use a or b, both axis are "inverted", as in +x will go left. -x will go right. Same with the y axis.

So there you go. Shaarigan thanks again for helping out and pointing me towards the right direction. If anybody finds something wrong in my explanation, please let me know and I'll correct it.

Also, here's the final working perspective projection matrix:


    const halfFOV = Math.tan(toRad(FOV/2.0));
    let xScale, yScale;

    if (viewport.width > viewport.height) {
        xScale = 1.0 / (halfFOV);
        yScale = 1.0 / (halfFOV / viewport.aspect);
    }
    else {
        xScale = 1.0 / (halfFOV / viewport.aspect);
        yScale = 1.0 / (halfFOV);        
    }

    const zRange = (NEAR - FAR);
    const zScale = (NEAR + FAR) / zRange
    const NEAR_PLANE = (2 * NEAR * FAR) / zRange;

    p.mat[0]  = xScale;
    p.mat[5]  = yScale;
    p.mat[10] = zScale;

    p.mat[11] = -1.0;
    p.mat[14] = NEAR_PLANE
    p.mat[15] = 0;

 

 

This topic is closed to new replies.

Advertisement