Skip to content

Add XMMatrixPerspectiveFovInfiniteReverseZLH/RH - #334

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

Add XMMatrixPerspectiveFovInfiniteReverseZLH/RH#334
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

walbourn commented Aug 1, 2026

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;
}

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add matrix functions for generating infinite reversed-z perspective projection matrix

2 participants