c++mathopengl3dglm-math

Limiting FOV both horizontally and vertically


How can I create a perspective matrix that limits maximum FOV?

What I means is that, if I for example have a scene that looks like this:

/-------\
|#######|
|#######|
\-------/

And the screen is currently size 16:10, I can see:

/-------\
 #######
  #####
   ---

Now, if I resize the screen to 16:9, I see this (b is black):

b/-------\b
 |#######|
  #######
   -----

When I would rather it cut more of the top/bottom off.

How can I do this? I'm using OpenGL and GLM. I'm creating that current matrix with:

glm::perspective(yFov, width / height, zNear, zFar)

(Attempted) Clarification: The problem is that in my application, I want the required amount of "margin" for the terrain on each side to be fixed. The problem I'm trying to solve is that depending on the aspect ratio of the screen, this value currently changes. I can set a yFov, so that I'll always be able to see the same amount vertically. However, as the screen gets wider, I can see more and more horizontally. What I want instead is to be able to see less vertically. Thanks for any advice :)

Solution I went with:

projMat = glm::perspective(fov, aspect.x / aspect.y, zRange.x, zRange.y);
// ^ could be any other maths lib / code

if (aspect.x > aspect.y * 1.6f) {
    float tanFov = std::tan(0.5f * fov);
    projMat[0][0] = 1.f / tanFov / 1.6f;
    projMat[1][1] = 1.f / (aspect.y / aspect.x * tanFov) / 1.6f;
}

The 1.6 is because I'd like the the "standard" ratio to be 16:10. This works great :)


Solution

  • You could change the definition of your projection to use the field-of-view angle for the larger dimension, instead of always for the y dimension. This means that you see a given range of your object in the larger dimension, and less in the smaller dimension.

    While this is doable by combining the projection matrix you get from your matrix library with an additional scaling, the cleaner method is to calculate your own projection matrix.

    The calculation of a standard projection matrix looks like this:

    float tanFov = tan(0.5f * fov);
    float aspRat = (float)width / (float)height;
    
    mat[0][0] = 1.0f / (aspRat * tanFov);
    mat[0][1] = 0.0f;
    mat[0][2] = 0.0f;
    mat[0][3] = 0.0f;
    
    mat[1][0] = 0.0f;
    mat[1][1] = 1.0f / tanFov;
    mat[1][2] = 0.0f;
    mat[1][3] = 0.0f;
    
    mat[2][0] = 0.0f;
    mat[2][1] = 0.0f;
    mat[2][2] = (zNear + zFar) / (zNear - zFar);
    mat[2][3] = -1.0f;
    
    mat[3][0] = 0.0f;
    mat[3][1] = 0.0f;
    mat[3][2] = 2.0f * zNear * zFar / (zNear - zFar);
    mat[3][3] = 0.0f;
    

    Most of this will stay the same if you use the proposed logic to calculate the projection matrix. Only elements [0][0] and [1][1], which determine the scaling in the x and y dimensions, are calculated differently if width is greater than height:

    if (width > height) {
        float aspRat = (float)height/ (float)width;
        mat[0][0] = 1.0f / tanFov;
        mat[1][1] = 1.0f / (aspRat * tanFov);
    } else {
        float aspRat = (float)width / (float)height;
        mat[0][0] = 1.0f / (aspRat * tanFov);
        mat[1][1] = 1.0f / tanFov;
    }
    

    This is the same as before if height is greater than width, and swaps the calculation logic for the two directions otherwise.