From b3ba9eaadf78dd95b50016d407aaf081d33588dc Mon Sep 17 00:00:00 2001 From: Orange Date: Thu, 23 Apr 2026 19:48:55 +0300 Subject: [PATCH] updated formulas --- include/omath/linear_algebra/mat.hpp | 53 +++++++++++++++++++++++ source/engines/source_engine/formulas.cpp | 29 +++++-------- source/engines/unreal_engine/formulas.cpp | 28 ++++-------- 3 files changed, 72 insertions(+), 38 deletions(-) diff --git a/include/omath/linear_algebra/mat.hpp b/include/omath/linear_algebra/mat.hpp index d82dea5..a262b69 100644 --- a/include/omath/linear_algebra/mat.hpp +++ b/include/omath/linear_algebra/mat.hpp @@ -707,6 +707,59 @@ namespace omath else std::unreachable(); } + + // Horizontal-FOV variants โ€” use these when the engine reports FOV as + // horizontal (UE's FMinimalViewInfo::FOV, Quake-family fov_x, etc.). + // X and Y scales derived as: X = 1 / tan(hfov/2), Y = aspect / tan(hfov/2). + template + [[nodiscard]] + Mat<4, 4, Type, St> mat_perspective_left_handed_horizontal_fov(const float horizontal_fov, + const float aspect_ratio, const float near, + const float far) noexcept + { + const float inv_tan_half_hfov = 1.f / std::tan(angles::degrees_to_radians(horizontal_fov) / 2.f); + const float x_axis = inv_tan_half_hfov; + const float y_axis = inv_tan_half_hfov * aspect_ratio; + + if constexpr (DepthRange == NDCDepthRange::ZERO_TO_ONE) + return {{x_axis, 0.f, 0.f, 0.f}, + {0.f, y_axis, 0.f, 0.f}, + {0.f, 0.f, far / (far - near), -(near * far) / (far - near)}, + {0.f, 0.f, 1.f, 0.f}}; + else if constexpr (DepthRange == NDCDepthRange::NEGATIVE_ONE_TO_ONE) + return {{x_axis, 0.f, 0.f, 0.f}, + {0.f, y_axis, 0.f, 0.f}, + {0.f, 0.f, (far + near) / (far - near), -(2.f * near * far) / (far - near)}, + {0.f, 0.f, 1.f, 0.f}}; + else + std::unreachable(); + } + + template + [[nodiscard]] + Mat<4, 4, Type, St> mat_perspective_right_handed_horizontal_fov(const float horizontal_fov, + const float aspect_ratio, const float near, + const float far) noexcept + { + const float inv_tan_half_hfov = 1.f / std::tan(angles::degrees_to_radians(horizontal_fov) / 2.f); + const float x_axis = inv_tan_half_hfov; + const float y_axis = inv_tan_half_hfov * aspect_ratio; + + if constexpr (DepthRange == NDCDepthRange::ZERO_TO_ONE) + return {{x_axis, 0.f, 0.f, 0.f}, + {0.f, y_axis, 0.f, 0.f}, + {0.f, 0.f, -far / (far - near), -(near * far) / (far - near)}, + {0.f, 0.f, -1.f, 0.f}}; + else if constexpr (DepthRange == NDCDepthRange::NEGATIVE_ONE_TO_ONE) + return {{x_axis, 0.f, 0.f, 0.f}, + {0.f, y_axis, 0.f, 0.f}, + {0.f, 0.f, -(far + near) / (far - near), -(2.f * near * far) / (far - near)}, + {0.f, 0.f, -1.f, 0.f}}; + else + std::unreachable(); + } template [[nodiscard]] diff --git a/source/engines/source_engine/formulas.cpp b/source/engines/source_engine/formulas.cpp index 8bb1afd..b7968cc 100644 --- a/source/engines/source_engine/formulas.cpp +++ b/source/engines/source_engine/formulas.cpp @@ -39,30 +39,23 @@ namespace omath::source_engine const float far, const NDCDepthRange ndc_depth_range) noexcept { // Source (inherited from Quake) stores FOV as horizontal FOV at a 4:3 - // reference aspect. Convert to vertical FOV first, then use the - // standard vfov-based projection against the caller's actual aspect. + // reference aspect. Convert to true vertical FOV, then delegate to the + // standard vertical-FOV left-handed builder with the caller's actual + // aspect ratio. // vfov = 2 ยท atan( tan(hfov_4:3 / 2) / (4/3) ) constexpr float k_source_reference_aspect = 4.f / 3.f; const float half_hfov_4_3 = angles::degrees_to_radians(field_of_view) / 2.f; - const float tan_half_vfov = std::tan(half_hfov_4_3) / k_source_reference_aspect; - - const float x_axis = 1.f / (aspect_ratio * tan_half_vfov); - const float y_axis = 1.f / tan_half_vfov; + const float vfov_deg = angles::radians_to_degrees( + 2.f * std::atan(std::tan(half_hfov_4_3) / k_source_reference_aspect)); if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE) - return { - {x_axis, 0, 0, 0}, - {0, y_axis, 0, 0}, - {0, 0, far / (far - near), -(near * far) / (far - near)}, - {0, 0, 1, 0}, - }; + return mat_perspective_left_handed< + float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>( + vfov_deg, aspect_ratio, near, far); if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE) - return { - {x_axis, 0, 0, 0}, - {0, y_axis, 0, 0}, - {0, 0, (far + near) / (far - near), -(2.f * far * near) / (far - near)}, - {0, 0, 1, 0}, - }; + return mat_perspective_left_handed< + float, MatStoreType::ROW_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>( + vfov_deg, aspect_ratio, near, far); std::unreachable(); } } // namespace omath::source_engine diff --git a/source/engines/unreal_engine/formulas.cpp b/source/engines/unreal_engine/formulas.cpp index 7e0b24c..f8bd02b 100644 --- a/source/engines/unreal_engine/formulas.cpp +++ b/source/engines/unreal_engine/formulas.cpp @@ -38,28 +38,16 @@ namespace omath::unreal_engine Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near, const float far, const NDCDepthRange ndc_depth_range) noexcept { - // UE stores horizontal FOV in FMinimalViewInfo โ€” mirror engine's - // FMinimalViewInfo::CalculateProjectionMatrixGivenViewRectangle: - // XAxisMultiplier = 1 / tan(hfov/2) - // YAxisMultiplier = aspect / tan(hfov/2) - const float inv_tan_half_hfov = 1.f / std::tan(angles::degrees_to_radians(field_of_view) / 2.f); - const float x_axis = inv_tan_half_hfov; - const float y_axis = inv_tan_half_hfov * aspect_ratio; - + // UE stores horizontal FOV in FMinimalViewInfo โ€” use the left-handed + // horizontal-FOV builder directly. if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE) - return { - {x_axis, 0, 0, 0}, - {0, y_axis, 0, 0}, - {0, 0, far / (far - near), -(near * far) / (far - near)}, - {0, 0, 1, 0}, - }; + return mat_perspective_left_handed_horizontal_fov< + float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>( + field_of_view, aspect_ratio, near, far); if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE) - return { - {x_axis, 0, 0, 0}, - {0, y_axis, 0, 0}, - {0, 0, (far + near) / (far - near), -(2.f * far * near) / (far - near)}, - {0, 0, 1, 0}, - }; + return mat_perspective_left_handed_horizontal_fov< + float, MatStoreType::ROW_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>( + field_of_view, aspect_ratio, near, far); std::unreachable(); } } // namespace omath::unreal_engine