diff --git a/include/omath/engines/cry_engine/constants.hpp b/include/omath/engines/cry_engine/constants.hpp index 762d0404..29175707 100644 --- a/include/omath/engines/cry_engine/constants.hpp +++ b/include/omath/engines/cry_engine/constants.hpp @@ -15,7 +15,7 @@ namespace omath::cry_engine constexpr Vector3 k_abs_forward = {0, 1, 0}; using Mat4X4 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; - using Mat3X3 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; + using Mat3X3 = Mat<3, 3, float, MatStoreType::ROW_MAJOR>; using Mat1X3 = Mat<1, 3, float, MatStoreType::ROW_MAJOR>; using PitchAngle = Angle; using YawAngle = Angle; diff --git a/include/omath/engines/cry_engine/formulas.hpp b/include/omath/engines/cry_engine/formulas.hpp index 555a5f68..b09cb8b5 100644 --- a/include/omath/engines/cry_engine/formulas.hpp +++ b/include/omath/engines/cry_engine/formulas.hpp @@ -23,7 +23,7 @@ namespace omath::cry_engine [[nodiscard]] Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far, - NDCDepthRange ndc_depth_range = NDCDepthRange::NEGATIVE_ONE_TO_ONE) noexcept; + NDCDepthRange ndc_depth_range = NDCDepthRange::ZERO_TO_ONE) noexcept; template requires std::is_floating_point_v diff --git a/include/omath/linear_algebra/mat.hpp b/include/omath/linear_algebra/mat.hpp index ca04b8c5..21c70535 100644 --- a/include/omath/linear_algebra/mat.hpp +++ b/include/omath/linear_algebra/mat.hpp @@ -667,7 +667,7 @@ namespace omath template [[nodiscard]] - Mat<4, 4, Type, St> mat_perspective_left_handed(const Type field_of_view, const Type aspect_ratio, + Mat<4, 4, Type, St> mat_perspective_left_handed_vertical_fov(const Type field_of_view, const Type aspect_ratio, const Type near, const Type far) noexcept { const auto fov_half_tan = std::tan(angles::degrees_to_radians(field_of_view) / Type{2}); @@ -689,7 +689,7 @@ namespace omath template [[nodiscard]] - Mat<4, 4, Type, St> mat_perspective_right_handed(const Type field_of_view, const Type aspect_ratio, + Mat<4, 4, Type, St> mat_perspective_right_handed_vertical_fov(const Type field_of_view, const Type aspect_ratio, const Type near, const Type far) noexcept { const auto fov_half_tan = std::tan(angles::degrees_to_radians(field_of_view) / Type{2}); @@ -730,7 +730,7 @@ namespace omath else if constexpr (DepthRange == NDCDepthRange::NEGATIVE_ONE_TO_ONE) return {{x_axis, Type{0}, Type{0}, Type{0}}, {Type{0}, y_axis, Type{0}, Type{0}}, - {Type{0}, Type{0}, (far + near) / (far - near), -(2.f * near * far) / (far - near)}, + {Type{0}, Type{0}, (far + near) / (far - near), -(Type{2} * near * far) / (far - near)}, {Type{0}, Type{0}, Type{1}, Type{0}}}; else std::unreachable(); @@ -755,7 +755,7 @@ namespace omath else if constexpr (DepthRange == NDCDepthRange::NEGATIVE_ONE_TO_ONE) return {{x_axis, Type{0}, Type{0}, Type{0}}, {Type{0}, y_axis, Type{0}, Type{0}}, - {Type{0}, Type{0}, -(far + near) / (far - near), -(2.f * near * far) / (far - near)}, + {Type{0}, Type{0}, -(far + near) / (far - near), -(Type{2} * near * far) / (far - near)}, {Type{0}, Type{0}, -Type{1}, Type{0}}}; else std::unreachable(); diff --git a/source/engines/cry_engine/formulas.cpp b/source/engines/cry_engine/formulas.cpp index 301016b9..c70b9c0d 100644 --- a/source/engines/cry_engine/formulas.cpp +++ b/source/engines/cry_engine/formulas.cpp @@ -38,12 +38,12 @@ namespace omath::cry_engine const float far, const NDCDepthRange ndc_depth_range) noexcept { if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE) - return mat_perspective_left_handed( + return mat_perspective_left_handed_vertical_fov( field_of_view, aspect_ratio, near, far); if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE) - return mat_perspective_left_handed( + return mat_perspective_left_handed_vertical_fov( field_of_view, aspect_ratio, near, far); std::unreachable(); } -} // namespace omath::unity_engine +} // namespace omath::cry_engine diff --git a/source/engines/cry_engine/traits/camera_trait.cpp b/source/engines/cry_engine/traits/camera_trait.cpp index a8ea6180..da260f5b 100644 --- a/source/engines/cry_engine/traits/camera_trait.cpp +++ b/source/engines/cry_engine/traits/camera_trait.cpp @@ -24,4 +24,4 @@ namespace omath::cry_engine return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far, ndc_depth_range); } -} // namespace omath::unity_engine \ No newline at end of file +} // namespace omath::cry_engine \ No newline at end of file diff --git a/source/engines/frostbite_engine/formulas.cpp b/source/engines/frostbite_engine/formulas.cpp index c3193cb1..981ca1c4 100644 --- a/source/engines/frostbite_engine/formulas.cpp +++ b/source/engines/frostbite_engine/formulas.cpp @@ -38,11 +38,11 @@ namespace omath::frostbite_engine const float far, const NDCDepthRange ndc_depth_range) noexcept { if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE) - return mat_perspective_left_handed( + return mat_perspective_left_handed_vertical_fov( field_of_view, aspect_ratio, near, far); if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE) - return mat_perspective_left_handed( + return mat_perspective_left_handed_vertical_fov( field_of_view, aspect_ratio, near, far); std::unreachable(); diff --git a/source/engines/iw_engine/formulas.cpp b/source/engines/iw_engine/formulas.cpp index 09ce04aa..25926493 100644 --- a/source/engines/iw_engine/formulas.cpp +++ b/source/engines/iw_engine/formulas.cpp @@ -47,11 +47,11 @@ namespace omath::iw_engine const auto vertical_fov = angles::horizontal_fov_to_vertical(field_of_view, k_source_reference_aspect); if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE) - return mat_perspective_left_handed< + return mat_perspective_left_handed_vertical_fov< float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>( vertical_fov, aspect_ratio, near, far); if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE) - return mat_perspective_left_handed< + return mat_perspective_left_handed_vertical_fov< float, MatStoreType::ROW_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>( vertical_fov, aspect_ratio, near, far); std::unreachable(); diff --git a/source/engines/opengl_engine/formulas.cpp b/source/engines/opengl_engine/formulas.cpp index 93eec900..01d41b3e 100644 --- a/source/engines/opengl_engine/formulas.cpp +++ b/source/engines/opengl_engine/formulas.cpp @@ -40,11 +40,11 @@ namespace omath::opengl_engine const float far, const NDCDepthRange ndc_depth_range) noexcept { if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE) - return mat_perspective_right_handed( + return mat_perspective_right_handed_vertical_fov( field_of_view, aspect_ratio, near, far); if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE) - return mat_perspective_right_handed( + return mat_perspective_right_handed_vertical_fov( field_of_view, aspect_ratio, near, far); std::unreachable(); diff --git a/source/engines/source_engine/formulas.cpp b/source/engines/source_engine/formulas.cpp index c8c5bc7a..7718fdef 100644 --- a/source/engines/source_engine/formulas.cpp +++ b/source/engines/source_engine/formulas.cpp @@ -47,11 +47,11 @@ namespace omath::source_engine const auto vertical_fov = angles::horizontal_fov_to_vertical(field_of_view, k_source_reference_aspect); if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE) - return mat_perspective_left_handed< + return mat_perspective_left_handed_vertical_fov< float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>( vertical_fov, aspect_ratio, near, far); if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE) - return mat_perspective_left_handed< + return mat_perspective_left_handed_vertical_fov< float, MatStoreType::ROW_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>( vertical_fov, aspect_ratio, near, far); std::unreachable(); diff --git a/source/engines/unity_engine/formulas.cpp b/source/engines/unity_engine/formulas.cpp index 51e4c4c8..b10fa82b 100644 --- a/source/engines/unity_engine/formulas.cpp +++ b/source/engines/unity_engine/formulas.cpp @@ -38,10 +38,10 @@ namespace omath::unity_engine const float far, const NDCDepthRange ndc_depth_range) noexcept { if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE) - return omath::mat_perspective_right_handed( + return omath::mat_perspective_right_handed_vertical_fov( field_of_view, aspect_ratio, near, far); if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE) - return omath::mat_perspective_right_handed(field_of_view, aspect_ratio, near, far); std::unreachable(); diff --git a/tests/engines/unit_test_cry_engine.cpp b/tests/engines/unit_test_cry_engine.cpp index 2a80c5e5..1888c278 100644 --- a/tests/engines/unit_test_cry_engine.cpp +++ b/tests/engines/unit_test_cry_engine.cpp @@ -29,11 +29,11 @@ TEST(unit_test_cry_engine, look_at_right) } TEST(unit_test_cry_engine, look_at_up) { - const auto angles = cry_engine::CameraTrait::calc_look_at_angle({}, cry_engine::k_abs_right); + const auto angles = cry_engine::CameraTrait::calc_look_at_angle({}, cry_engine::k_abs_up); // ReSharper disable once CppTooWideScopeInitStatement const auto dir_vector = cry_engine::forward_vector(angles); - for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), cry_engine::k_abs_right.as_array())) + for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), cry_engine::k_abs_up.as_array())) EXPECT_NEAR(result, etalon, 0.0001f); } diff --git a/tests/general/unit_test_line_tracer_obb.cpp b/tests/general/unit_test_line_tracer_obb.cpp index 5331f76c..d4a269d7 100644 --- a/tests/general/unit_test_line_tracer_obb.cpp +++ b/tests/general/unit_test_line_tracer_obb.cpp @@ -34,6 +34,13 @@ namespace const auto s = std::sin(radians); return OBB{center, {c, s, 0.f}, {-s, c, 0.f}, {0.f, 0.f, 1.f}, half_extents}; } + + OBB rotated_around_y(const Vec3& center, const Vec3& half_extents, const float radians) noexcept + { + const auto c = std::cos(radians); + const auto s = std::sin(radians); + return OBB{center, {c, 0.f, -s}, {0.f, 1.f, 0.f}, {s, 0.f, c}, half_extents}; + } } // namespace // --- axis-aligned OBB behaves like AABB --- @@ -126,6 +133,18 @@ TEST(LineTracerOBBTests, RotatedBoxHitOnRotatedFace) EXPECT_NEAR(hit.z, 0.f, 1e-4f); } +TEST(LineTracerOBBTests, RotatedAroundYBoxHitOnRotatedFace) +{ + const auto box = rotated_around_y({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}, std::numbers::pi_v / 4.f); + const auto ray = make_ray({0.f, 0.f, 5.f}, {0.f, 0.f, -5.f}); + + const auto hit = LineTracer::get_ray_hit_point(ray, box); + EXPECT_NE(hit, ray.end); + EXPECT_NEAR(hit.x, 0.f, 1e-4f); + EXPECT_NEAR(hit.y, 0.f, 1e-4f); + EXPECT_NEAR(hit.z, std::numbers::sqrt2_v, 1e-4f); +} + TEST(LineTracerOBBTests, RotatedBoxMissesWhereAabbWouldHit) { // A unit cube rotated 45° around Z has an XY footprint that is a diamond reaching @@ -170,6 +189,43 @@ TEST(LineTracerOBBTests, RotatedAndTranslatedBoxHit) EXPECT_NEAR(hit.y, 5.f, 1e-4f); } +TEST(LineTracerOBBTests, RayStartsInsideRotatedBox) +{ + const auto box = rotated_around_z({2.f, 3.f, 0.f}, {2.f, 1.f, 1.f}, std::numbers::pi_v / 6.f); + const auto ray = make_ray({2.f, 3.f, 0.f}, {10.f, 3.f, 0.f}); + + const auto hit = LineTracer::get_ray_hit_point(ray, box); + EXPECT_NE(hit, ray.end); + EXPECT_NEAR(hit.x, 2.f, 1e-4f); + EXPECT_NEAR(hit.y, 3.f, 1e-4f); + EXPECT_NEAR(hit.z, 0.f, 1e-4f); +} + +TEST(LineTracerOBBTests, TangentRayHitsRotatedBox) +{ + const auto box = rotated_around_z({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}, std::numbers::pi_v / 4.f); + const auto ray = make_ray({-5.f, std::numbers::sqrt2_v, 0.f}, + {5.f, std::numbers::sqrt2_v, 0.f}); + + const auto hit = LineTracer::get_ray_hit_point(ray, box); + EXPECT_NE(hit, ray.end); + EXPECT_NEAR(hit.x, 0.f, 1e-4f); + EXPECT_NEAR(hit.y, std::numbers::sqrt2_v, 1e-4f); + EXPECT_NEAR(hit.z, 0.f, 1e-4f); +} + +TEST(LineTracerOBBTests, DegeneratePlaneBoxCanBeHit) +{ + const auto box = axis_aligned_obb({0.f, 0.f, 0.f}, {1.f, 1.f, 0.f}); + const auto ray = make_ray({0.f, 0.f, -5.f}, {0.f, 0.f, 5.f}); + + const auto hit = LineTracer::get_ray_hit_point(ray, box); + EXPECT_NE(hit, ray.end); + EXPECT_NEAR(hit.x, 0.f, 1e-4f); + EXPECT_NEAR(hit.y, 0.f, 1e-4f); + EXPECT_NEAR(hit.z, 0.f, 1e-4f); +} + TEST(LineTracerOBBTests, ParallelRayOutsideMisses) { // Ray runs parallel to a slab face, completely outside the slab. diff --git a/tests/general/unit_test_mat.cpp b/tests/general/unit_test_mat.cpp index 0059b127..0cc51c35 100644 --- a/tests/general/unit_test_mat.cpp +++ b/tests/general/unit_test_mat.cpp @@ -1,6 +1,7 @@ // UnitTestMat.cpp #include "omath/linear_algebra/mat.hpp" #include "omath/linear_algebra/vector3.hpp" +#include "omath/trigonometry/angles.hpp" #include using namespace omath; @@ -220,8 +221,8 @@ TEST(UnitTestMatStandalone, Equanity) constexpr omath::Vector3 left_handed = {0, 2, 10}; constexpr omath::Vector3 right_handed = {0, 2, -10}; - const auto proj_left_handed = omath::mat_perspective_left_handed(90.f, 16.f / 9.f, 0.1f, 1000.f); - const auto proj_right_handed = omath::mat_perspective_right_handed(90.f, 16.f / 9.f, 0.1f, 1000.f); + const auto proj_left_handed = omath::mat_perspective_left_handed_vertical_fov(90.f, 16.f / 9.f, 0.1f, 1000.f); + const auto proj_right_handed = omath::mat_perspective_right_handed_vertical_fov(90.f, 16.f / 9.f, 0.1f, 1000.f); auto ndc_left_handed = proj_left_handed * omath::mat_column_from_vector(left_handed); auto ndc_right_handed = proj_right_handed * omath::mat_column_from_vector(right_handed); @@ -233,7 +234,7 @@ TEST(UnitTestMatStandalone, Equanity) } TEST(UnitTestMatStandalone, MatPerspectiveLeftHanded) { - const auto perspective_proj = mat_perspective_left_handed(90.f, 16.f/9.f, 0.1f, 1000.f); + const auto perspective_proj = mat_perspective_left_handed_vertical_fov(90.f, 16.f/9.f, 0.1f, 1000.f); auto projected = perspective_proj * mat_column_from_vector({0, 0, 0.1001}); @@ -244,7 +245,7 @@ TEST(UnitTestMatStandalone, MatPerspectiveLeftHanded) TEST(UnitTestMatStandalone, MatPerspectiveLeftHandedZeroToOne) { - const auto proj = mat_perspective_left_handed( + const auto proj = mat_perspective_left_handed_vertical_fov( 90.f, 16.f / 9.f, 0.1f, 1000.f); // Near plane point should map to z ~ 0 @@ -266,7 +267,7 @@ TEST(UnitTestMatStandalone, MatPerspectiveLeftHandedZeroToOne) TEST(UnitTestMatStandalone, MatPerspectiveRightHandedZeroToOne) { - const auto proj = mat_perspective_right_handed( + const auto proj = mat_perspective_right_handed_vertical_fov( 90.f, 16.f / 9.f, 0.1f, 1000.f); // Near plane point (negative z for right-handed) should map to z ~ 0 @@ -289,8 +290,8 @@ TEST(UnitTestMatStandalone, MatPerspectiveRightHandedZeroToOne) TEST(UnitTestMatStandalone, MatPerspectiveNegativeOneToOneRange) { // Verify existing [-1, 1] behavior with explicit template arg matches default - const auto proj_default = mat_perspective_left_handed(90.f, 16.f / 9.f, 0.1f, 1000.f); - const auto proj_explicit = mat_perspective_left_handed(90.f, 16.f / 9.f, 0.1f, 1000.f); EXPECT_EQ(proj_default, proj_explicit); @@ -306,15 +307,174 @@ TEST(UnitTestMatStandalone, MatPerspectiveNegativeOneToOneRange) EXPECT_NEAR(far_pt.at(2, 0), 1.0f, 1e-3f); } +TEST(UnitTestMatStandalone, MatPerspectiveRightHandedNegOneToOne) +{ + const auto proj = mat_perspective_right_handed_vertical_fov(90.f, 16.f / 9.f, 0.1f, 1000.f); + + // Near plane (negative z for RH) should map to z ~ -1 + auto near_pt = proj * mat_column_from_vector({0, 0, -0.1f}); + near_pt /= near_pt.at(3, 0); + EXPECT_NEAR(near_pt.at(2, 0), -1.0f, 1e-3f); + + // Far plane should map to z ~ 1 + auto far_pt = proj * mat_column_from_vector({0, 0, -1000.f}); + far_pt /= far_pt.at(3, 0); + EXPECT_NEAR(far_pt.at(2, 0), 1.0f, 1e-3f); + + // Mid-range point should be in (-1, 1) + auto mid_pt = proj * mat_column_from_vector({0, 0, -500.f}); + mid_pt /= mid_pt.at(3, 0); + EXPECT_GT(mid_pt.at(2, 0), -1.0f); + EXPECT_LT(mid_pt.at(2, 0), 1.0f); +} + +TEST(UnitTestMatStandalone, MatPerspectiveLeftHandedHorizontalFovZeroToOne) +{ + // hfov=90 deg, aspect=16/9 => tan(hfov/2)=1, so x_axis=1 and y_axis=aspect + const auto proj = mat_perspective_left_handed_horizontal_fov(90.f, 16.f / 9.f, 0.1f, 1000.f); + + // Near plane should map to z ~ 0 + auto near_pt = proj * mat_column_from_vector({0, 0, 0.1f}); + near_pt /= near_pt.at(3, 0); + EXPECT_NEAR(near_pt.at(2, 0), 0.0f, 1e-4f); + + // Far plane should map to z ~ 1 + auto far_pt = proj * mat_column_from_vector({0, 0, 1000.f}); + far_pt /= far_pt.at(3, 0); + EXPECT_NEAR(far_pt.at(2, 0), 1.0f, 1e-4f); + + // Right edge of horizontal frustum at near plane (view_x = tan(hfov/2)*near = 0.1) + auto right_edge = proj * mat_column_from_vector({0.1f, 0, 0.1f}); + right_edge /= right_edge.at(3, 0); + EXPECT_NEAR(right_edge.at(0, 0), 1.0f, 1e-4f); +} + +TEST(UnitTestMatStandalone, MatPerspectiveLeftHandedHorizontalFovNegOneToOne) +{ + const auto proj = mat_perspective_left_handed_horizontal_fov(90.f, 16.f / 9.f, 0.1f, 1000.f); + + auto near_pt = proj * mat_column_from_vector({0, 0, 0.1f}); + near_pt /= near_pt.at(3, 0); + EXPECT_NEAR(near_pt.at(2, 0), -1.0f, 1e-3f); + + auto far_pt = proj * mat_column_from_vector({0, 0, 1000.f}); + far_pt /= far_pt.at(3, 0); + EXPECT_NEAR(far_pt.at(2, 0), 1.0f, 1e-3f); + + auto right_edge = proj * mat_column_from_vector({0.1f, 0, 0.1f}); + right_edge /= right_edge.at(3, 0); + EXPECT_NEAR(right_edge.at(0, 0), 1.0f, 1e-4f); +} + +TEST(UnitTestMatStandalone, MatPerspectiveRightHandedHorizontalFovZeroToOne) +{ + const auto proj = mat_perspective_right_handed_horizontal_fov(90.f, 16.f / 9.f, 0.1f, 1000.f); + + auto near_pt = proj * mat_column_from_vector({0, 0, -0.1f}); + near_pt /= near_pt.at(3, 0); + EXPECT_NEAR(near_pt.at(2, 0), 0.0f, 1e-4f); + + auto far_pt = proj * mat_column_from_vector({0, 0, -1000.f}); + far_pt /= far_pt.at(3, 0); + EXPECT_NEAR(far_pt.at(2, 0), 1.0f, 1e-4f); + + auto right_edge = proj * mat_column_from_vector({0.1f, 0, -0.1f}); + right_edge /= right_edge.at(3, 0); + EXPECT_NEAR(right_edge.at(0, 0), 1.0f, 1e-4f); +} + +TEST(UnitTestMatStandalone, MatPerspectiveRightHandedHorizontalFovNegOneToOne) +{ + const auto proj = mat_perspective_right_handed_horizontal_fov(90.f, 16.f / 9.f, 0.1f, 1000.f); + + auto near_pt = proj * mat_column_from_vector({0, 0, -0.1f}); + near_pt /= near_pt.at(3, 0); + EXPECT_NEAR(near_pt.at(2, 0), -1.0f, 1e-3f); + + auto far_pt = proj * mat_column_from_vector({0, 0, -1000.f}); + far_pt /= far_pt.at(3, 0); + EXPECT_NEAR(far_pt.at(2, 0), 1.0f, 1e-3f); + + auto right_edge = proj * mat_column_from_vector({0.1f, 0, -0.1f}); + right_edge /= right_edge.at(3, 0); + EXPECT_NEAR(right_edge.at(0, 0), 1.0f, 1e-4f); +} + +TEST(UnitTestMatStandalone, MatPerspectiveHorizontalVsVerticalFovEquivalence) +{ + constexpr float hfov_deg = 90.f; + constexpr float aspect = 16.f / 9.f; + const float vfov_deg = angles::horizontal_fov_to_vertical(hfov_deg, aspect); + + const auto proj_h = mat_perspective_left_handed_horizontal_fov(hfov_deg, aspect, 0.1f, 1000.f); + const auto proj_v = mat_perspective_left_handed_vertical_fov(vfov_deg, aspect, 0.1f, 1000.f); + + for (size_t i = 0; i < 4; ++i) + for (size_t j = 0; j < 4; ++j) + EXPECT_NEAR(proj_h.at(i, j), proj_v.at(i, j), 1e-4f); +} + +// Handedness contract: clip_w sign tells front-of-camera vs behind. +// LH: +z view-space is in front (clip_w > 0), -z is behind (clip_w < 0). +// RH: -z view-space is in front (clip_w > 0), +z is behind (clip_w < 0). +TEST(UnitTestMatStandalone, MatPerspectiveLeftHandedVerticalFovHandedness) +{ + const auto proj = mat_perspective_left_handed_vertical_fov(90.f, 16.f / 9.f, 0.1f, 1000.f); + + const auto in_front = proj * mat_column_from_vector({0, 0, 1.f}); + const auto behind = proj * mat_column_from_vector({0, 0, -1.f}); + + EXPECT_GT(in_front.at(3, 0), 0.0f); + EXPECT_LT(behind.at(3, 0), 0.0f); +} + +TEST(UnitTestMatStandalone, MatPerspectiveRightHandedVerticalFovHandedness) +{ + const auto proj = mat_perspective_right_handed_vertical_fov(90.f, 16.f / 9.f, 0.1f, 1000.f); + + const auto in_front = proj * mat_column_from_vector({0, 0, -1.f}); + const auto behind = proj * mat_column_from_vector({0, 0, 1.f}); + + EXPECT_GT(in_front.at(3, 0), 0.0f); + EXPECT_LT(behind.at(3, 0), 0.0f); +} + +TEST(UnitTestMatStandalone, MatPerspectiveLeftHandedHorizontalFovHandedness) +{ + const auto proj = mat_perspective_left_handed_horizontal_fov(90.f, 16.f / 9.f, 0.1f, 1000.f); + + const auto in_front = proj * mat_column_from_vector({0, 0, 1.f}); + const auto behind = proj * mat_column_from_vector({0, 0, -1.f}); + + EXPECT_GT(in_front.at(3, 0), 0.0f); + EXPECT_LT(behind.at(3, 0), 0.0f); +} + +TEST(UnitTestMatStandalone, MatPerspectiveRightHandedHorizontalFovHandedness) +{ + const auto proj = mat_perspective_right_handed_horizontal_fov(90.f, 16.f / 9.f, 0.1f, 1000.f); + + const auto in_front = proj * mat_column_from_vector({0, 0, -1.f}); + const auto behind = proj * mat_column_from_vector({0, 0, 1.f}); + + EXPECT_GT(in_front.at(3, 0), 0.0f); + EXPECT_LT(behind.at(3, 0), 0.0f); +} + TEST(UnitTestMatStandalone, MatPerspectiveZeroToOneEquanity) { // LH and RH should produce same NDC for mirrored z constexpr omath::Vector3 left_handed = {0, 2, 10}; constexpr omath::Vector3 right_handed = {0, 2, -10}; - const auto proj_lh = mat_perspective_left_handed( + const auto proj_lh = mat_perspective_left_handed_vertical_fov( 90.f, 16.f / 9.f, 0.1f, 1000.f); - const auto proj_rh = mat_perspective_right_handed( + const auto proj_rh = mat_perspective_right_handed_vertical_fov( 90.f, 16.f / 9.f, 0.1f, 1000.f); auto ndc_lh = proj_lh * mat_column_from_vector(left_handed); diff --git a/tests/general/unit_test_obb.cpp b/tests/general/unit_test_obb.cpp index 5b4ac14a..c02e43cc 100644 --- a/tests/general/unit_test_obb.cpp +++ b/tests/general/unit_test_obb.cpp @@ -35,6 +35,13 @@ namespace const auto s = std::sin(radians); return ObbF{center, {c, 0.f, -s}, {0.f, 1.f, 0.f}, {s, 0.f, c}, half_extents}; } + + void expect_vec_near(const Vec3F& actual, const Vec3F& expected, const float epsilon = 1e-5f) + { + EXPECT_NEAR(actual.x, expected.x, epsilon); + EXPECT_NEAR(actual.y, expected.y, epsilon); + EXPECT_NEAR(actual.z, expected.z, epsilon); + } } // namespace // --- struct-level tests --- @@ -42,7 +49,7 @@ namespace TEST(ObbTests, VerticesOfAxisAlignedUnitBox) { constexpr auto box = axis_aligned_obb({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}); - const auto v = box.vertices(); + constexpr auto v = box.vertices(); EXPECT_EQ(v[0], (Vec3F{-1.f, -1.f, -1.f})); EXPECT_EQ(v[1], (Vec3F{1.f, -1.f, -1.f})); @@ -57,7 +64,7 @@ TEST(ObbTests, VerticesOfAxisAlignedUnitBox) TEST(ObbTests, VerticesOfTranslatedBox) { constexpr auto box = axis_aligned_obb({10.f, 20.f, 30.f}, {1.f, 2.f, 3.f}); - const auto v = box.vertices(); + constexpr auto v = box.vertices(); EXPECT_EQ(v[0], (Vec3F{9.f, 18.f, 27.f})); EXPECT_EQ(v[7], (Vec3F{11.f, 22.f, 33.f})); @@ -80,10 +87,41 @@ TEST(ObbTests, VerticesOfRotatedBox) } } +TEST(ObbTests, VerticesOfTranslatedNonUniformRotatedBox) +{ + const auto box = rotated_around_z({2.f, 3.f, 4.f}, {2.f, 1.f, 0.5f}, std::numbers::pi_v / 2.f); + const auto v = box.vertices(); + + expect_vec_near(v[0], {3.f, 1.f, 3.5f}); + expect_vec_near(v[1], {3.f, 5.f, 3.5f}); + expect_vec_near(v[2], {1.f, 1.f, 3.5f}); + expect_vec_near(v[3], {1.f, 5.f, 3.5f}); + expect_vec_near(v[4], {3.f, 1.f, 4.5f}); + expect_vec_near(v[5], {3.f, 5.f, 4.5f}); + expect_vec_near(v[6], {1.f, 1.f, 4.5f}); + expect_vec_near(v[7], {1.f, 5.f, 4.5f}); +} + +TEST(ObbTests, VerticesCollapseWhenOneExtentIsZero) +{ + constexpr auto box = axis_aligned_obb({1.f, 2.f, 3.f}, {2.f, 0.f, 4.f}); + constexpr auto v = box.vertices(); + + EXPECT_EQ(v[0], v[2]); + EXPECT_EQ(v[1], v[3]); + EXPECT_EQ(v[4], v[6]); + EXPECT_EQ(v[5], v[7]); + + EXPECT_EQ(v[0], (Vec3F{-1.f, 2.f, -1.f})); + EXPECT_EQ(v[1], (Vec3F{3.f, 2.f, -1.f})); + EXPECT_EQ(v[4], (Vec3F{-1.f, 2.f, 7.f})); + EXPECT_EQ(v[5], (Vec3F{3.f, 2.f, 7.f})); +} + TEST(ObbTests, DoublePrecisionInstantiation) { constexpr ObbD box{{0.0, 0.0, 0.0}, {1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}, {2.0, 3.0, 4.0}}; - const auto v = box.vertices(); + constexpr auto v = box.vertices(); EXPECT_DOUBLE_EQ(v[0].x, -2.0); EXPECT_DOUBLE_EQ(v[0].y, -3.0); EXPECT_DOUBLE_EQ(v[0].z, -4.0); @@ -100,7 +138,7 @@ TEST(ObbTests, AxisAlignedInFrontNotCulled) const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); - const auto obb = axis_aligned_obb({100.f, 0.f, 0.f}, {10.f, 1.f, 1.f}); + constexpr auto obb = axis_aligned_obb({100.f, 0.f, 0.f}, {10.f, 1.f, 1.f}); EXPECT_FALSE(cam.is_obb_culled_by_frustum(obb)); } @@ -110,7 +148,7 @@ TEST(ObbTests, AxisAlignedBehindCameraCulled) const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); - const auto obb = axis_aligned_obb({-150.f, 0.f, 0.f}, {50.f, 1.f, 1.f}); + constexpr auto obb = axis_aligned_obb({-150.f, 0.f, 0.f}, {50.f, 1.f, 1.f}); EXPECT_TRUE(cam.is_obb_culled_by_frustum(obb)); } @@ -120,17 +158,27 @@ TEST(ObbTests, AxisAlignedBeyondFarPlaneCulled) const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); - const auto obb = axis_aligned_obb({1750.f, 0.f, 0.f}, {250.f, 1.f, 1.f}); + constexpr auto obb = axis_aligned_obb({1750.f, 0.f, 0.f}, {250.f, 1.f, 1.f}); EXPECT_TRUE(cam.is_obb_culled_by_frustum(obb)); } +TEST(ObbTests, AxisAlignedStraddlingFarPlaneNotCulled) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov, + 0.01f, 1000.f); + + constexpr auto obb = axis_aligned_obb({1005.f, 0.f, 0.f}, {10.f, 1.f, 1.f}); + EXPECT_FALSE(cam.is_obb_culled_by_frustum(obb)); +} + TEST(ObbTests, AxisAlignedFarLeftCulled) { constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); - const auto obb = axis_aligned_obb({100.f, 4500.f, 0.f}, {10.f, 500.f, 1.f}); + constexpr auto obb = axis_aligned_obb({100.f, 4500.f, 0.f}, {10.f, 500.f, 1.f}); EXPECT_TRUE(cam.is_obb_culled_by_frustum(obb)); } @@ -140,7 +188,7 @@ TEST(ObbTests, AxisAlignedFarRightCulled) const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); - const auto obb = axis_aligned_obb({100.f, -4500.f, 0.f}, {10.f, 500.f, 1.f}); + constexpr auto obb = axis_aligned_obb({100.f, -4500.f, 0.f}, {10.f, 500.f, 1.f}); EXPECT_TRUE(cam.is_obb_culled_by_frustum(obb)); } @@ -150,7 +198,7 @@ TEST(ObbTests, AxisAlignedAboveCulled) const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); - const auto obb = axis_aligned_obb({100.f, 0.f, 5500.f}, {10.f, 1.f, 500.f}); + constexpr auto obb = axis_aligned_obb({100.f, 0.f, 5500.f}, {10.f, 1.f, 500.f}); EXPECT_TRUE(cam.is_obb_culled_by_frustum(obb)); } @@ -160,7 +208,7 @@ TEST(ObbTests, AxisAlignedBelowCulled) const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); - const auto obb = axis_aligned_obb({100.f, 0.f, -5500.f}, {10.f, 1.f, 500.f}); + constexpr auto obb = axis_aligned_obb({100.f, 0.f, -5500.f}, {10.f, 1.f, 500.f}); EXPECT_TRUE(cam.is_obb_culled_by_frustum(obb)); } @@ -204,10 +252,10 @@ TEST(ObbTests, RotationCanPullBoxIntoFrustum) const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); - const Vec3F center{50.f, 100.f, 0.f}; - const Vec3F half{1.f, 1.f, 50.f}; + constexpr Vec3F center{50.f, 100.f, 0.f}; + constexpr Vec3F half{1.f, 1.f, 50.f}; - const auto axis_aligned = axis_aligned_obb(center, half); + constexpr auto axis_aligned = axis_aligned_obb(center, half); EXPECT_TRUE(cam.is_obb_culled_by_frustum(axis_aligned)); const auto rotated = rotated_around_y(center, half, std::numbers::pi_v / 2.f); @@ -225,10 +273,10 @@ TEST(ObbTests, RotationCanPushBoxOutOfFrustum) const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); - const Vec3F center{50.f, 130.f, 0.f}; - const Vec3F half{50.f, 1.f, 1.f}; + constexpr Vec3F center{50.f, 130.f, 0.f}; + constexpr Vec3F half{50.f, 1.f, 1.f}; - const auto axis_aligned = axis_aligned_obb(center, half); + constexpr auto axis_aligned = axis_aligned_obb(center, half); EXPECT_FALSE(cam.is_obb_culled_by_frustum(axis_aligned)); const auto rotated = rotated_around_z(center, half, std::numbers::pi_v / 2.f); @@ -263,10 +311,20 @@ TEST(ObbTests, OpenGlEngineBehindCulled) constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); const auto cam = omath::opengl_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); - const auto obb = axis_aligned_obb({0.f, 0.f, 100.f}, {5.f, 5.f, 5.f}); + constexpr auto obb = axis_aligned_obb({0.f, 0.f, 100.f}, {5.f, 5.f, 5.f}); EXPECT_TRUE(cam.is_obb_culled_by_frustum(obb)); } +TEST(ObbTests, OpenGlEngineStraddlingFarPlaneNotCulled) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + const auto cam = omath::opengl_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.01f, 100.f); + + const auto obb = rotated_around_z({0.f, 0.f, -105.f}, {5.f, 5.f, 10.f}, + std::numbers::pi_v / 4.f); + EXPECT_FALSE(cam.is_obb_culled_by_frustum(obb)); +} + TEST(ObbTests, UnityEngineBeyondFarCulled) { constexpr auto fov = omath::projection::FieldOfView::from_degrees(60.f); @@ -284,7 +342,7 @@ TEST(ObbTests, DegenerateZeroVolumeInsideNotCulled) 0.01f, 1000.f); // Zero-extent OBB — collapses to a point, but still must not be culled if the centre is inside. - const auto obb = axis_aligned_obb({100.f, 0.f, 0.f}, {0.f, 0.f, 0.f}); + constexpr auto obb = axis_aligned_obb({100.f, 0.f, 0.f}, {0.f, 0.f, 0.f}); EXPECT_FALSE(cam.is_obb_culled_by_frustum(obb)); }