Search code examples
matrixopenglgraphicsglm-math

Confusion about zFar and zNear plane offsets using glm::perspective


I have been using glm to help build a software rasterizer for self education. In my camera class I am using glm::lookat() to create my view matrix and glm::perspective() to create my perspective matrix.

I seem to be getting what I expect for my left, right top and bottom clipping planes. However, I seem to be either doing something wrong for my near/far planes of there is an error in my understanding. I have reached a point in which my "google-fu" has failed me.

Operating under the assumption that I am correctly extracting clip planes from my glm::perspective matrix, and using the general plane equation:

aX+bY+cZ+d = 0

I am getting strange d or "offset" values for my zNear and zFar planes. It is my understanding that the d value is the value of which I would be shifting/translatin the point P0 of a plane along the normal vector.

They are 0.200200200 and -0.200200200 respectively. However, my normals are correct orientated at +1.0f and -1.f along the z-axis as expected for a plane perpendicular to my z basis vector.

So when testing a point such as the (0, 0, -5) world space against these planes, it is transformed by my view matrix to:

(0, 0, 5.81181192)

so testing it against these plane in a clip chain, said example vertex would be culled.

Here is the start of a camera class establishing the relevant matrices:

static constexpr glm::vec3 UPvec(0.f, 1.f, 0.f);
static constexpr auto zFar = 100.f;
static constexpr auto zNear = 0.1f;


Camera::Camera(glm::vec3 eye, glm::vec3 center, float fovY, float w, float h) :

viewMatrix{ glm::lookAt(eye, center, UPvec) },
perspectiveMatrix{ glm::perspective(glm::radians<float>(fovY), w/h, zNear, zFar) },

frustumLeftPlane {setPlane(0, 1)},
frustumRighPlane {setPlane(0, 0)},
frustumBottomPlane {setPlane(1, 1)},
frustumTopPlane {setPlane(1, 0)},
frstumNearPlane  {setPlane(2, 0)},
frustumFarPlane {setPlane(2, 1)},

The frustum objects are based off the following struct:

struct Plane
{
    glm::vec4 normal;
    float offset;
};

I have extracted the 6 clipping planes from the perspective matrix as below:

Plane Camera::setPlane(const int& row, const bool& sign)
{
    float temp[4]{};
    Plane plane{};
    if (sign == 0)
    {
        for (int i = 0; i < 4; ++i)
        {
            temp[i] = perspectiveMatrix[i][3] + perspectiveMatrix[i][row];
        }
    }
    else
    {
        for (int i = 0; i < 4; ++i)
        {
            temp[i] = perspectiveMatrix[i][3] - perspectiveMatrix[i][row];
        }
    }

    plane.normal.x = temp[0];
    plane.normal.y = temp[1];
    plane.normal.z = temp[2];
    plane.normal.w = 0.f;
    plane.offset = temp[3];
    plane.normal = glm::normalize(plane.normal);

    return plane;
}

Any help would be appreciated, as now I am at a loss.

Many thanks.


Solution

  • The d parameter of a plane equation describes how much the plane is offset from the origin along the plane normal. This also takes into account the length of the normal.

    One can't just normalize the normal without also adjusting the d parameter since normalizing changes the length of the normal. If you want to normalize a plane equation then you also have to apply the division step to the d coordinate:

    float normalLength = sqrt(temp[0] * temp[0] + temp[1] * temp[1] + temp[2] * temp[2]);
    
    plane.normal.x = temp[0] / normalLength;
    plane.normal.y = temp[1] / normalLength;
    plane.normal.z = temp[2] / normalLength;
    plane.normal.w = 0.f;
    plane.offset = temp[3] / normalLength;
    

    Side note 1: Usually, one would store the offset of a plane equation in the w-coordinate of a vec4 instead of a separate variable. The reason is that the typical operation you perform with it is a point to plane distance check like dist = n * x - d (for a given point x, normal n, offset d, * is dot product), which can then be written as dist = [n, d] * [x, -1].

    Side note 2: Most software and also hardware rasterizer perform clipping after the projection step since it's cheaper and easier to implement.