rustmatrixgraphicsvulkan

Projection matrix causes vertical offset


I am trying to render a quad in 3d space, but there is a vertical offset when I apply my projection matrix, when I expect it to still be centered on the screen. I feel like I've eliminated all other variables (no other matrices, 1/1 screen ratio, etc), so there must be something wrong with the projection matrix itself. The only difference in the pictures below is whether I multiply by a projection matrix or by an identity matrix.

The rendering engine's coordinate system is X to the right, Y forward and Z up. This is converted to the Vulkan coordinate system in both examples by multiplying the projection matrix with a matrix that flips the axes.

So my question is, where did I make a mistake in the projection matrix calculations?

Also imporant, the matrices are row major, and the NDC is (-1, -1, -1) to (1, 1, 1).

Edit: Some more testing indeed shows that there is an unwanted offset happening on the Z axis. I made a simple projection matrix,

Matrix4x4f::perspective_near_plane(-1.0, 1.0, 1.0, -1.0, 1.0, 100.0)

and multiplied the coordinates of the corners of the near and far plane by the projection. This should simply give the corners of the NDC, but instead returns this:

[-1, 1, 1, 1]        -> [-1, -0.9999999, 2, 1] -> [-1, -0.9999999, 2, 1]
[1, 1, 1, 1]         -> [1, -0.9999999, 2, 1]  -> [1, -0.9999999, 2, 1]
[1, 1, -1, 1]        -> [1, -0.9999999, 0, 1]  -> [1, -0.9999999, 0, 1]
[-1, 1, -1, 1]       -> [-1, -0.9999999, 0, 1] -> [-1, -0.9999999, 0, 1]
[-100, 100, 100, 1]  -> [-100, 100, 101, 100]  -> [-1, 1, 1.01, 1]
[100, 100, 100, 1]   -> [100, 100, 101, 100]   -> [1, 1, 1.01, 1]
[100, 100, -100, 1]  -> [100, 100, -99, 100]   -> [1, 1, -0.99, 1]
[-100, 100, -100, 1] -> [-100, 100, -99, 100]  -> [-1, 1, -0.99, 1]

Without projection matrix

Without projection matrix

With projection matrix

With projection matrix

Projection matrix calculations

impl<T> Matrix4x4<T> {
    pub fn perspective_near_plane(left: T, top: T, right: T, bottom: T, near: T, far: T) -> Self
        where T: Real {
        let m11 = ((T::one() + T::one()) * near) / (right - left);
        let m12 = T::zero();
        let m13 = T::zero();
        let m14 = T::zero();
    
        let m21 = -(right + left) / (right - left);
        let m22 = (far + near) / (far - near);
        let m23 = -(top + bottom) / (top - bottom);
        let m24 = T::one();
    
        let m31 = T::zero();
        let m32 = T::zero();
        let m33 = ((T::one() + T::one()) * near) / (top - bottom);
        let m34 = T::zero();
    
        let m41 = T::zero();
        let m42 = (-(T::one() + T::one()) * far * near) / (far - near);
        let m43 = T::one();
        let m44 = T::zero();
    
        Self::new(
            m11, m12, m13, m14,
            m21, m22, m23, m24,
            m31, m32, m33, m34,
            m41, m42, m43, m44)
    }
    
    pub fn perspective_horizontal_fov(horizontal_fov: T, aspect_ratio: T, near: T, far: T) -> Self
        where T: Real {
        let right = (horizontal_fov / (T::one() + T::one())).tan() * near;
        let left = -right;
        let top = right / aspect_ratio;
        let bottom = -top;
        Self::perspective_near_plane(left, top, right, bottom, near, far)
    }
}

Creating the projection matrix

let projection = Matrix4x4f::perspective_horizontal_fov(
    60.0 / 180.0 * std::f32::consts::PI,
    window_size.x as f32 / window_size.y as f32,
    0.1,
    1000.0);

Coordinate adjustment matrix

Matrix4x4f::new(
    1.0, 0.0, 0.0, 0.0,
    0.0, 0.0, 1.0, 0.0,
    0.0, -1.0, 0.0, 0.0,
    0.0, 0.0, 0.0, 1.0
)

Hard coded vertex positions

static const float3 positions[6] = {
    float3(-0.5, 1, -0.5),
    float3(0.5, 1, 0.5),
    float3(-0.5, 1, 0.5),
    float3(-0.5, 1, -0.5),
    float3(0.5, 1, -0.5),
    float3(0.5, 1, 0.5),
};

Solution

  • I found the culprit! I was setting both m24 and m43 to a value of one. But as you can see in this example, only one element of the matrix should have a non zero constant value.

    Since m43 is the only element of the two that affects the Z axis, it has to be incorrect one. And indeed, after setting it to zero:

    Fixed projection