From 2eccb4023f1fe4fd0525b2e7d53356b2a9dc1f47 Mon Sep 17 00:00:00 2001 From: Orange Date: Thu, 23 Apr 2026 18:33:00 +0300 Subject: [PATCH 1/6] fix --- source/engines/unreal_engine/formulas.cpp | 31 ++++++++++++++++++----- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/source/engines/unreal_engine/formulas.cpp b/source/engines/unreal_engine/formulas.cpp index 1890a13..7e0b24c 100644 --- a/source/engines/unreal_engine/formulas.cpp +++ b/source/engines/unreal_engine/formulas.cpp @@ -2,7 +2,6 @@ // Created by Vlad on 3/22/2025. // #include "omath/engines/unreal_engine/formulas.hpp" - namespace omath::unreal_engine { Vector3 forward_vector(const ViewAngles& angles) noexcept @@ -25,7 +24,7 @@ namespace omath::unreal_engine } Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept { - return mat_camera_view(forward_vector(angles), -right_vector(angles), + return mat_camera_view(forward_vector(angles), right_vector(angles), up_vector(angles), cam_origin); } Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept @@ -34,13 +33,33 @@ namespace omath::unreal_engine * mat_rotation_axis_z(angles.yaw) * mat_rotation_axis_y(-angles.pitch); } + + 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 { - if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE) - return mat_perspective_left_handed( - field_of_view, aspect_ratio, near, far); + // 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; - return mat_perspective_left_handed(field_of_view, aspect_ratio, near, far); + 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}, + }; + 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}, + }; + std::unreachable(); } } // namespace omath::unreal_engine From 42a8a5a763afbf812a872f5494e4877d8c310d5f Mon Sep 17 00:00:00 2001 From: Orange Date: Thu, 23 Apr 2026 19:23:13 +0300 Subject: [PATCH 2/6] also fixed for source --- source/engines/source_engine/formulas.cpp | 28 ++++++++++++++--------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/source/engines/source_engine/formulas.cpp b/source/engines/source_engine/formulas.cpp index 85f7055..8bb1afd 100644 --- a/source/engines/source_engine/formulas.cpp +++ b/source/engines/source_engine/formulas.cpp @@ -38,24 +38,30 @@ namespace omath::source_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 { - // NOTE: Need magic number to fix fov calculation, since source inherit Quake proj matrix calculation - constexpr auto k_multiply_factor = 0.75f; + // 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. + // 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 fov_half_tan = std::tan(angles::degrees_to_radians(field_of_view) / 2.f) * k_multiply_factor; + const float x_axis = 1.f / (aspect_ratio * tan_half_vfov); + const float y_axis = 1.f / tan_half_vfov; if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE) return { - {1.f / (aspect_ratio * fov_half_tan), 0, 0, 0}, - {0, 1.f / (fov_half_tan), 0, 0}, - {0, 0, far / (far - near), -(near * far) / (far - near)}, - {0, 0, 1, 0}, + {x_axis, 0, 0, 0}, + {0, y_axis, 0, 0}, + {0, 0, far / (far - near), -(near * far) / (far - near)}, + {0, 0, 1, 0}, }; if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE) return { - {1.f / (aspect_ratio * fov_half_tan), 0, 0, 0}, - {0, 1.f / (fov_half_tan), 0, 0}, - {0, 0, (far + near) / (far - near), -(2.f * far * near) / (far - near)}, - {0, 0, 1, 0}, + {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}, }; std::unreachable(); } From b3ba9eaadf78dd95b50016d407aaf081d33588dc Mon Sep 17 00:00:00 2001 From: Orange Date: Thu, 23 Apr 2026 19:48:55 +0300 Subject: [PATCH 3/6] 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 From 56ebc475531d626ddf86cadcb721f6b35f4a157c Mon Sep 17 00:00:00 2001 From: Orange Date: Thu, 23 Apr 2026 20:02:34 +0300 Subject: [PATCH 4/6] remove axys invertion --- include/omath/engines/unreal_engine/camera.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/omath/engines/unreal_engine/camera.hpp b/include/omath/engines/unreal_engine/camera.hpp index 4b5283e..1f62c3c 100644 --- a/include/omath/engines/unreal_engine/camera.hpp +++ b/include/omath/engines/unreal_engine/camera.hpp @@ -9,5 +9,5 @@ namespace omath::unreal_engine { - using Camera = projection::Camera; + using Camera = projection::Camera; } // namespace omath::unreal_engine \ No newline at end of file From 11c053e28c6e9eeafb71eb55de7081319d0193dd Mon Sep 17 00:00:00 2001 From: Orange Date: Thu, 23 Apr 2026 21:24:46 +0300 Subject: [PATCH 5/6] fixed rotation ordering --- source/engines/unreal_engine/formulas.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/source/engines/unreal_engine/formulas.cpp b/source/engines/unreal_engine/formulas.cpp index f8bd02b..9405824 100644 --- a/source/engines/unreal_engine/formulas.cpp +++ b/source/engines/unreal_engine/formulas.cpp @@ -29,9 +29,13 @@ namespace omath::unreal_engine } Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept { - return mat_rotation_axis_x(angles.roll) - * mat_rotation_axis_z(angles.yaw) - * mat_rotation_axis_y(-angles.pitch); + // UE FRotator is intrinsic Z-Y-X (Yaw → Pitch → Roll applied in local + // frame), which for column-vector composition is Rz·Ry·Rx. + // Pitch and roll axes in omath spin opposite to UE's convention, so + // both carry a sign flip. + return mat_rotation_axis_z(angles.yaw) + * mat_rotation_axis_y(-angles.pitch) + * mat_rotation_axis_x(-angles.roll); } From e62e8672b3210d2280d20d3f58ce8ae08966cc06 Mon Sep 17 00:00:00 2001 From: Orange Date: Thu, 23 Apr 2026 22:32:44 +0300 Subject: [PATCH 6/6] fixed tests --- tests/engines/unit_test_unreal_engine.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/engines/unit_test_unreal_engine.cpp b/tests/engines/unit_test_unreal_engine.cpp index 2ad6819..cb41716 100644 --- a/tests/engines/unit_test_unreal_engine.cpp +++ b/tests/engines/unit_test_unreal_engine.cpp @@ -44,7 +44,7 @@ TEST(unit_test_unreal_engine, ForwardVectorRotationRoll) { omath::unreal_engine::ViewAngles angles; - angles.roll = omath::unreal_engine::RollAngle::from_degrees(-90.f); + angles.roll = omath::unreal_engine::RollAngle::from_degrees(90.f); const auto forward = omath::unreal_engine::up_vector(angles); EXPECT_NEAR(forward.x, omath::unreal_engine::k_abs_right.x, 0.00001f);