Skip to content

Add XMMatrixPerspectiveFovInfiniteReverseZLH/RH - #334

Open
RohithPariki wants to merge 1 commit into
microsoft:mainfrom
RohithPariki:feature/infinite-reversez-projection
Open

RohithPariki wants to merge 1 commit into
microsoft:mainfrom
RohithPariki:feature/infinite-reversez-projection

Conversation

@RohithPariki

Copy link
Copy Markdown

Summary

This PR adds dedicated functions for infinite reversed-Z perspective projections, addressing Issue #158.

Passing INFINITY as the far plane to the existing XMMatrixPerspectiveFovLH / XMMatrixPerspectiveFovRH functions is not sufficient because the internal calculations contain floating-point operations such as 0 * infinity, which result in NaN values.

This change derives the limiting form of the perspective projection matrix when the far plane approaches infinity and introduces:

  • XMMatrixPerspectiveFovInfiniteReverseZLH
  • XMMatrixPerspectiveFovInfiniteReverseZRH

The generated matrices match the layout discussed in Issue #158:

[ xScale,   0,        0,     0 ]
[   0,    yScale,     0,     0 ]
[   0,      0,        0,     1 ]
[   0,      0,      zNear,   0 ]

Changes

  • Added infinite reversed-Z perspective projection functions for:

    • Scalar implementation (_XM_NO_INTRINSICS_)
    • ARM NEON implementation (_XM_ARM_NEON_INTRINSICS_)
    • SSE implementation (_XM_SSE_INTRINSICS_)
  • Added local validation tests covering:

    • LH and RH matrix generation
    • Near-plane depth mapping
    • Infinite far-plane depth mapping
    • Consistency between LH and RH implementations

The existing implementation was also tested with INFINITY as the far plane and confirmed to produce NaN values.

Screenshots:

image image
Implements infinite reversed-z perspective projection matrices, resolving
issue microsoft#158. These functions complement the existing XMMatrixPerspectiveFovLH/RH
pair for the increasingly common rendering technique of reversed-z with an
infinite far plane, which provides optimal floating-point depth precision and
eliminates Z-fighting at large distances.

The algebraic derivation takes the limit of XMMatrixPerspectiveFovLH as
FarZ -> +infinity. Using the standard row-major DX NDC mapping:
  NDC_z = M22 + M32 / z
Setting NDC_z(NearZ) = 1 and NDC_z(inf) = 0 gives:
  M22 = 0,  M32 = NearZ  (LH)
  M22 = 0,  M32 = -NearZ (RH)

All three code paths are implemented:
  - Scalar (_XM_NO_INTRINSICS_)
  - ARM NEON (_XM_ARM_NEON_INTRINSICS_)
  - SSE/AVX (_XM_SSE_INTRINSICS_)

Unlike substituting infinity into the existing FovLH/RH functions, these
functions produce well-defined, NaN-free results.
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.
@walbourn

Copy link
Copy Markdown
Collaborator

If you have example test code for this, please attach and I can integrate it into https://github.com/walbourn/directxmathtest

@RohithPariki

Copy link
Copy Markdown
Author

I realized my earlier test code was written as a standalone executable rather than following the math3 harness format. I've corrected it. The attached file uses the HRESULT TestNNN(LogProxy* pLog) pattern and printe/printi from math3.h. Please assign the appropriate test numbers.

Click to expand test code (XMMatrixInfiniteReverseZ.cpp)
//-------------------------------------------------------------------------------------
// XMMatrixInfiniteReverseZ.cpp - DirectXMath Test Suite
//
// Tests for XMMatrixPerspectiveFovInfiniteReverseZLH and
//            XMMatrixPerspectiveFovInfiniteReverseZRH
//
// Resolves: https://github.com/microsoft/DirectXMath/issues/158
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//
// https://go.microsoft.com/fwlink/?LinkID=615560
//-------------------------------------------------------------------------------------
//
// NOTE TO MAINTAINER:
//   Please assign test numbers and register these functions in math3.cpp /
//   AssignTests(). Two functions are provided:
//     TestNNN  -> XMMatrixPerspectiveFovInfiniteReverseZLH
//     TestNNN1 -> XMMatrixPerspectiveFovInfiniteReverseZRH
//

#include "math3.h"

#include <cmath>
#include <limits>

using namespace DirectX;

// Helper: compute NDC z from a stored 4x4 matrix and a view-space z value.
// NDC_z = (M[2][2]*z + M[3][2]) / (M[2][3]*z + M[3][3])
// Uses XMFLOAT4X4 row-major element names (_33 = row2,col2, etc.)
static float ComputeNDCz(const XMFLOAT4X4& m, float z)
{
    float num = m._33 * z + m._43;
    float den = m._34 * z + m._44;
    return num / den;
}

//------------------------------------------------------------------------------
// TestNNN - XMMatrixPerspectiveFovInfiniteReverseZLH
//
// Verifies the left-handed infinite reversed-Z projection matrix:
//   [xScale  0       0   0]
//   [0       yScale  0   0]
//   [0       0       0   1]
//   [0       0     zNear 0]
//
// Key properties:
//   M[2][2] == 0        (no finite far plane term)
//   M[3][2] == NearZ    (reversed-Z: near maps to NDC 1.0)
//   M[2][3] == 1        (left-handed)
//   NDC_z(NearZ) == 1.0
//   NDC_z(inf)   ~= 0.0
//------------------------------------------------------------------------------
HRESULT TestNNN(LogProxy* pLog)
{
    // XMMatrixPerspectiveFovInfiniteReverseZLH
    HRESULT ret = S_OK;

    const float fovY   = XM_PIDIV4;        // 45 degrees
    const float aspect = 16.0f / 9.0f;     // 16:9 widescreen
    const float nearZ  = 0.1f;

    XMMATRIX M = XMMatrixPerspectiveFovInfiniteReverseZLH(fovY, aspect, nearZ);
    XMFLOAT4X4 f;
    XMStoreFloat4x4(&f, M);

    // --- Structure checks ---

    // M[2][2] must be exactly 0 (the infinite far-plane term cancels out)
    if (f._33 != 0.0f)
    {
        printe("%s: LH M[2][2] expected 0.0, got %f\n", TestName, f._33);
        ret = MATH_FAIL;
    }
    else
    {
        printi("%s: LH M[2][2]==0 OK\n", TestName);
    }

    // M[3][2] must equal NearZ (reversed-Z: near plane clips to NDC 1.0)
    if (f._43 != nearZ)
    {
        printe("%s: LH M[3][2] expected %f, got %f\n", TestName, nearZ, f._43);
        ret = MATH_FAIL;
    }
    else
    {
        printi("%s: LH M[3][2]==NearZ OK\n", TestName);
    }

    // M[2][3] must be 1.0 for a left-handed projection
    if (f._34 != 1.0f)
    {
        printe("%s: LH M[2][3] expected 1.0, got %f\n", TestName, f._34);
        ret = MATH_FAIL;
    }
    else
    {
        printi("%s: LH M[2][3]==1 OK\n", TestName);
    }

    // M[3][3] must be 0.0
    if (f._44 != 0.0f)
    {
        printe("%s: LH M[3][3] expected 0.0, got %f\n", TestName, f._44);
        ret = MATH_FAIL;
    }
    else
    {
        printi("%s: LH M[3][3]==0 OK\n", TestName);
    }

    // X and Y scale must be positive
    if (f._11 <= 0.0f || f._22 <= 0.0f)
    {
        printe("%s: LH scale must be positive: _11=%f _22=%f\n", TestName, f._11, f._22);
        ret = MATH_FAIL;
    }
    else
    {
        printi("%s: LH XY scale positive OK\n", TestName);
    }

    // Off-diagonal elements must be zero
    if (f._12 != 0.f || f._13 != 0.f || f._14 != 0.f ||
        f._21 != 0.f || f._23 != 0.f || f._24 != 0.f ||
        f._31 != 0.f || f._32 != 0.f ||
        f._41 != 0.f || f._42 != 0.f)
    {
        printe("%s: LH off-diagonal elements are not zero\n", TestName);
        ret = MATH_FAIL;
    }
    else
    {
        printi("%s: LH off-diagonal zeros OK\n", TestName);
    }

    // --- Depth mapping checks ---

    // At the near plane, NDC z must be exactly 1.0 (reversed-Z maps near to 1)
    {
        float ndc = ComputeNDCz(f, nearZ);
        if (fabsf(ndc - 1.0f) > 1e-5f)
        {
            printe("%s: LH NDC_z(NearZ=%.2f) expected 1.0, got %f\n", TestName, nearZ, ndc);
            ret = MATH_FAIL;
        }
        else
        {
            printi("%s: LH NDC_z(NearZ)==1.0 OK\n", TestName);
        }
    }

    // At z approaching infinity, NDC z must approach 0.0
    {
        float ndc = ComputeNDCz(f, 1e9f);
        if (fabsf(ndc) > 1e-5f)
        {
            printe("%s: LH NDC_z(inf) expected ~0.0, got %f\n", TestName, ndc);
            ret = MATH_FAIL;
        }
        else
        {
            printi("%s: LH NDC_z(inf)~=0 OK\n", TestName);
        }
    }

    return ret;
}

//------------------------------------------------------------------------------
// TestNNN1 - XMMatrixPerspectiveFovInfiniteReverseZRH
//
// Verifies the right-handed infinite reversed-Z projection matrix:
//   [xScale  0        0   0 ]
//   [0       yScale   0   0 ]
//   [0       0        0  -1 ]
//   [0       0      -zNear 0]
//
// Key properties:
//   M[2][2] == 0         (no finite far plane term)
//   M[3][2] == -NearZ    (reversed-Z, right-handed)
//   M[2][3] == -1        (right-handed)
//   NDC_z(NearZ) == 1.0
//   NDC_z(inf)   ~= 0.0
//   X/Y scaling matches the LH variant for same FOV+aspect
//
// Also confirms that the naive workaround (passing INFINITY to the existing
// XMMatrixPerspectiveFovLH) produces NaN, justifying these new functions.
//------------------------------------------------------------------------------
HRESULT TestNNN1(LogProxy* pLog)
{
    // XMMatrixPerspectiveFovInfiniteReverseZRH
    HRESULT ret = S_OK;

    const float fovY   = XM_PIDIV4;
    const float aspect = 16.0f / 9.0f;
    const float nearZ  = 0.1f;

    XMMATRIX M = XMMatrixPerspectiveFovInfiniteReverseZRH(fovY, aspect, nearZ);
    XMFLOAT4X4 f;
    XMStoreFloat4x4(&f, M);

    // --- Structure checks ---

    if (f._33 != 0.0f)
    {
        printe("%s: RH M[2][2] expected 0.0, got %f\n", TestName, f._33);
        ret = MATH_FAIL;
    }
    else
    {
        printi("%s: RH M[2][2]==0 OK\n", TestName);
    }

    if (f._43 != -nearZ)
    {
        printe("%s: RH M[3][2] expected %f (-NearZ), got %f\n", TestName, -nearZ, f._43);
        ret = MATH_FAIL;
    }
    else
    {
        printi("%s: RH M[3][2]==-NearZ OK\n", TestName);
    }

    if (f._34 != -1.0f)
    {
        printe("%s: RH M[2][3] expected -1.0, got %f\n", TestName, f._34);
        ret = MATH_FAIL;
    }
    else
    {
        printi("%s: RH M[2][3]==-1 OK\n", TestName);
    }

    if (f._44 != 0.0f)
    {
        printe("%s: RH M[3][3] expected 0.0, got %f\n", TestName, f._44);
        ret = MATH_FAIL;
    }
    else
    {
        printi("%s: RH M[3][3]==0 OK\n", TestName);
    }

    if (f._11 <= 0.0f || f._22 <= 0.0f)
    {
        printe("%s: RH scale must be positive: _11=%f _22=%f\n", TestName, f._11, f._22);
        ret = MATH_FAIL;
    }
    else
    {
        printi("%s: RH XY scale positive OK\n", TestName);
    }

    if (f._12 != 0.f || f._13 != 0.f || f._14 != 0.f ||
        f._21 != 0.f || f._23 != 0.f || f._24 != 0.f ||
        f._31 != 0.f || f._32 != 0.f ||
        f._41 != 0.f || f._42 != 0.f)
    {
        printe("%s: RH off-diagonal elements are not zero\n", TestName);
        ret = MATH_FAIL;
    }
    else
    {
        printi("%s: RH off-diagonal zeros OK\n", TestName);
    }

    // --- Depth mapping checks ---

    // At near plane (RH looks down -Z, so z = nearZ positive here for formula)
    {
        float ndc = ComputeNDCz(f, nearZ);
        if (fabsf(ndc - 1.0f) > 1e-5f)
        {
            printe("%s: RH NDC_z(NearZ=%.2f) expected 1.0, got %f\n", TestName, nearZ, ndc);
            ret = MATH_FAIL;
        }
        else
        {
            printi("%s: RH NDC_z(NearZ)==1.0 OK\n", TestName);
        }
    }

    // At z approaching -infinity (RH far), NDC z must approach 0.0
    {
        float ndc = ComputeNDCz(f, -1e9f);
        if (fabsf(ndc) > 1e-5f)
        {
            printe("%s: RH NDC_z(-inf) expected ~0.0, got %f\n", TestName, ndc);
            ret = MATH_FAIL;
        }
        else
        {
            printi("%s: RH NDC_z(-inf)~=0 OK\n", TestName);
        }
    }

    // --- LH / RH consistency: same FOV+aspect must produce same X/Y scale ---
    {
        XMMATRIX lhM = XMMatrixPerspectiveFovInfiniteReverseZLH(fovY, aspect, nearZ);
        XMFLOAT4X4 lhF;
        XMStoreFloat4x4(&lhF, lhM);

        if (lhF._11 != f._11 || lhF._22 != f._22)
        {
            printe("%s: LH/RH X scale mismatch: LH=%f RH=%f\n", TestName, lhF._11, f._11);
            printe("%s: LH/RH Y scale mismatch: LH=%f RH=%f\n", TestName, lhF._22, f._22);
            ret = MATH_FAIL;
        }
        else
        {
            printi("%s: LH/RH XY scale match OK\n", TestName);
        }
    }

    // --- Confirm the naive workaround produces NaN (justifies these functions) ---
    // Passing INFINITY as FarZ to the existing function computes 0 * INFINITY = NaN.
    {
        const float inf = std::numeric_limits<float>::infinity();
        XMMATRIX broken = XMMatrixPerspectiveFovLH(fovY, aspect, nearZ, inf);
        XMFLOAT4X4 b;
        XMStoreFloat4x4(&b, broken);

        if (std::isnan(b._43) || std::isnan(b._33))
        {
            printi("%s: naive INFINITY workaround produces NaN as expected OK\n", TestName);
        }
        else
        {
            // Not a failure of the new functions; just informational for this platform.
            printi("%s: note - naive INFINITY workaround did not produce NaN on this platform\n", TestName);
        }
    }

    return ret;
}
@RohithPariki

Copy link
Copy Markdown
Author

Closing stale PR.

@walbourn

Copy link
Copy Markdown
Collaborator

You are welcome to keep this open. I just don't have access to an ARM64 device atm.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.
@Ossama-MOUSTAFA

Copy link
Copy Markdown

Quick question on XMMatrixPerspectiveFovInfiniteReverseZRH:

in Right-Handed view space,
the near plane is at z = -NearZ
(so w = -z = +NearZ).

Should M.m[3][2] be: +NearZ
instead of: -NearZ
so that: NDC_z = +1.0?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

3 participants