Search code examples
openglmatrixperspectivecamera

The math behind the perspective matrix


I want to create my own mvp matrix as an exercise.

auto lookAt(Vec)(const Vec eye, const Vec center, const Vec up)
if(isVector!Vec && Vec.dimension is 3){
    auto z = (eye - center).unit;
    auto x = cross(up.unit, z);
    auto y = cross(z, x);
    alias Mat = Matrix!(Vec.Type, 4, 4);
    alias Vec4 = Vector!(Vec.Type, 4);
    return Mat(Vec4(x, 0),
               Vec4(y, 0),
               Vec4(z, 0),
               Vec4(0, 0, 0, 1)) * translate(-eye);
}

The view transformation was rather simple, I set it up so that I now look along the negative Z axis.

But I have a few troubles with the perspective matrix.

y' = 1 / (tan(fov/2) * z
x' = 1 / (ar * tan(fov/2) * z

Where x' and y' are the new distorted values. So I came up with this matrix

Mat4f projection(float fov, float ar, float near, float far){
  import std.math: tan;
  auto tanHalfAngle = tan(fov/2.0f);
  return Mat4f(
      Vec4f(1.0f / (ar * tanHalfAngle) , 0, 0, 0),
      Vec4f(0, 1.0f / tanHalfAngle, 0, 0),
      Vec4f(0, 0, 1, 0),
      Vec4f(0, 0, -1, 0));
}

I currently just set it up so that the Z value is always -1. But I made a small mistake because now I am dividing by -Z.

The thing is the Matrix above works. I am currently render a spinning cube and besides the depth values everything looks correct.

But I really wanted this matrix

Mat4f projection(float fov, float ar, float near, float far){
  import std.math: tan;
  auto tanHalfAngle = tan(fov/2.0f);
  return Mat4f(
      Vec4f(1.0f / (ar * tanHalfAngle) , 0, 0, 0),
      Vec4f(0, 1.0f / tanHalfAngle, 0, 0),
      Vec4f(0, 0, -1, 0),
      Vec4f(0, 0, 1, 0));
}

But the cube doesn't appear on the screen any more and I don't understand why.

auto view = lookAt(Vec3f(0, 0, -5), Vec3f(0, 0, 0), Vec3f(0, 1, 0));
auto proj = projection(PI/2, 4.0f/3.0f, 0.1f, 100);

I setup test values

auto v = proj * view * Vec4f(0.5,0.5,0.5,1);

This is the output I get with the first matrix where I divide by -Z

Vec(-0.375, 0.5, -5.5, 5.5)
Vec(-0.0681818, 0.0909091, -1) // divided by w

This is the output I get with the second matrix where I divide by Z

Vec(-0.375, 0.5, 5.5, -5.5)
Vec(0.0681818, -0.0909091, -1) //divided by w

The only thing that has changed is the sign.

But why can I see a cube with the first matrix but nothing with the second matrix?


Solution

  • The default clipping rule of OpenGL is formulated in clip space as all points satisfying the following inequailty to lie inside the viewing frustum:

    -w <= x, y, z <= w
    

    Note that this trivially rules out any points with w < 0, since -w will be > w then, so it can't be satisfied.

    The view transformation was rather simple, I set it up so that I now look along the negative Z axis.

    That is not really true. The view matrix does not define the viewing direction alone, the projection matrix also has an influence. When using a standard perspective projection, the projection matrix' last row can simply be interpreted as the direction vector of the principal axis for the projection - so it is defining the viewing direction vector in eye space. Classic GL projection uses (0 0 -1 0), so -z will be the viewing direction.

    The view matrix' job is now to place the camera and to rotate the scene such that the intented world space viewing direction is mapped to the eye space viewing direction the projection matrix is using.

    When you flip your projection matrix to divide by z instead of -z, this of course means that it will look in that direction. Since you did not adjust your view matrix to follow that convenction, your camera is now viewing exactly in the opposite direction your view matrix is setting up. The points which lie in "front" of your camera at z_eye < 0 will all have a negative w_clip, and are clipped by the near plane.