diff --git a/tests/general/unit_test_line_tracer_obb.cpp b/tests/general/unit_test_line_tracer_obb.cpp index 5331f76..d4a269d 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_obb.cpp b/tests/general/unit_test_obb.cpp index 5b4ac14..7773622 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 --- @@ -80,6 +87,37 @@ 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}); + const 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}}; @@ -124,6 +162,16 @@ TEST(ObbTests, AxisAlignedBeyondFarPlaneCulled) 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); + + const 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); @@ -267,6 +315,16 @@ TEST(ObbTests, OpenGlEngineBehindCulled) 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);