Add XMMatrixPerspectiveFovInfiniteReverseZLH/RH - #334
Open
RohithPariki wants to merge 1 commit into
Open
Conversation
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: There may be pipelines that require an authorized user to comment /azp run to run. |
Collaborator
|
If you have example test code for this, please attach and I can integrate it into https://github.com/walbourn/directxmathtest |
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;
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds dedicated functions for infinite reversed-Z perspective projections, addressing Issue #158.
Passing
INFINITYas the far plane to the existingXMMatrixPerspectiveFovLH/XMMatrixPerspectiveFovRHfunctions is not sufficient because the internal calculations contain floating-point operations such as0 * infinity, which result inNaNvalues.This change derives the limiting form of the perspective projection matrix when the far plane approaches infinity and introduces:
XMMatrixPerspectiveFovInfiniteReverseZLHXMMatrixPerspectiveFovInfiniteReverseZRHThe generated matrices match the layout discussed in Issue #158:
Changes
Added infinite reversed-Z perspective projection functions for:
_XM_NO_INTRINSICS_)_XM_ARM_NEON_INTRINSICS_)_XM_SSE_INTRINSICS_)Added local validation tests covering:
The existing implementation was also tested with
INFINITYas the far plane and confirmed to produceNaNvalues.Screenshots: