From 4186ae8d763fc031f427bedaace88543e608a4da Mon Sep 17 00:00:00 2001 From: Orange Date: Mon, 20 Apr 2026 01:17:06 +0300 Subject: [PATCH] added more tests --- .../unit_test_cry_pred_engine_trait.cpp | 275 ++++++++++++++++++ tests/general/unit_test_projection.cpp | 245 ++++++++++++++++ 2 files changed, 520 insertions(+) create mode 100644 tests/general/unit_test_cry_pred_engine_trait.cpp diff --git a/tests/general/unit_test_cry_pred_engine_trait.cpp b/tests/general/unit_test_cry_pred_engine_trait.cpp new file mode 100644 index 0000000..ed66e35 --- /dev/null +++ b/tests/general/unit_test_cry_pred_engine_trait.cpp @@ -0,0 +1,275 @@ +// +// Created by Vladislav on 20.04.2026. +// +#include +#include +#include +#include + +using namespace omath; +using namespace omath::cry_engine; + +// ---- predict_projectile_position ---- + +TEST(CryPredEngineTrait, PredictProjectilePositionAtTimeZero) +{ + projectile_prediction::Projectile p; + p.m_origin = {1.f, 2.f, 3.f}; + p.m_launch_offset = {4.f, 5.f, 6.f}; + p.m_launch_speed = 100.f; + p.m_gravity_scale = 1.f; + + const auto pos = PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 0.f, 9.81f); + + // At t=0 no velocity is applied, just origin+offset + EXPECT_NEAR(pos.x, 5.f, 1e-4f); + EXPECT_NEAR(pos.y, 7.f, 1e-4f); + EXPECT_NEAR(pos.z, 9.f, 1e-4f); +} + +TEST(CryPredEngineTrait, PredictProjectilePositionZeroAnglesForwardIsY) +{ + // Cry engine forward = +Y. At pitch=0, yaw=0 the projectile travels along +Y. + projectile_prediction::Projectile p; + p.m_origin = {0.f, 0.f, 0.f}; + p.m_launch_speed = 10.f; + p.m_gravity_scale = 0.f; // no gravity so we isolate direction + + const auto pos = PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f); + + EXPECT_NEAR(pos.x, 0.f, 1e-4f); + EXPECT_NEAR(pos.y, 10.f, 1e-4f); + EXPECT_NEAR(pos.z, 0.f, 1e-4f); +} + +TEST(CryPredEngineTrait, PredictProjectilePositionGravityDropsZ) +{ + projectile_prediction::Projectile p; + p.m_origin = {0.f, 0.f, 0.f}; + p.m_launch_speed = 10.f; + p.m_gravity_scale = 1.f; + + const auto pos = PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 2.f, 9.81f); + + // z = 0 - (9.81 * 1) * (4) * 0.5 = -19.62 + EXPECT_NEAR(pos.z, -9.81f * 4.f * 0.5f, 1e-3f); +} + +TEST(CryPredEngineTrait, PredictProjectilePositionGravityScaleZeroNoZDrop) +{ + projectile_prediction::Projectile p; + p.m_origin = {0.f, 0.f, 0.f}; + p.m_launch_speed = 10.f; + p.m_gravity_scale = 0.f; + + const auto pos = PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 3.f, 9.81f); + + EXPECT_NEAR(pos.z, 0.f, 1e-4f); +} + +TEST(CryPredEngineTrait, PredictProjectilePositionWithLaunchOffset) +{ + projectile_prediction::Projectile p; + p.m_origin = {5.f, 0.f, 0.f}; + p.m_launch_offset = {0.f, 0.f, 2.f}; + p.m_launch_speed = 10.f; + p.m_gravity_scale = 0.f; + + const auto pos = PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 0.f); + + // launch position = {5, 0, 2}, travels along +Y by 10 + EXPECT_NEAR(pos.x, 5.f, 1e-4f); + EXPECT_NEAR(pos.y, 10.f, 1e-4f); + EXPECT_NEAR(pos.z, 2.f, 1e-4f); +} + +// ---- predict_target_position ---- + +TEST(CryPredEngineTrait, PredictTargetPositionGroundedStationary) +{ + projectile_prediction::Target t; + t.m_origin = {10.f, 20.f, 5.f}; + t.m_velocity = {0.f, 0.f, 0.f}; + t.m_is_airborne = false; + + const auto pred = PredEngineTrait::predict_target_position(t, 5.f, 9.81f); + + EXPECT_NEAR(pred.x, 10.f, 1e-6f); + EXPECT_NEAR(pred.y, 20.f, 1e-6f); + EXPECT_NEAR(pred.z, 5.f, 1e-6f); +} + +TEST(CryPredEngineTrait, PredictTargetPositionGroundedMoving) +{ + projectile_prediction::Target t; + t.m_origin = {0.f, 0.f, 0.f}; + t.m_velocity = {3.f, 4.f, 0.f}; + t.m_is_airborne = false; + + const auto pred = PredEngineTrait::predict_target_position(t, 2.f, 9.81f); + + EXPECT_NEAR(pred.x, 6.f, 1e-6f); + EXPECT_NEAR(pred.y, 8.f, 1e-6f); + EXPECT_NEAR(pred.z, 0.f, 1e-6f); // grounded — no gravity +} + +TEST(CryPredEngineTrait, PredictTargetPositionAirborneGravityDropsZ) +{ + projectile_prediction::Target t; + t.m_origin = {0.f, 0.f, 20.f}; + t.m_velocity = {0.f, 0.f, 0.f}; + t.m_is_airborne = true; + + const auto pred = PredEngineTrait::predict_target_position(t, 2.f, 9.81f); + + // z = 20 - 9.81 * 4 * 0.5 = 20 - 19.62 = 0.38 + EXPECT_NEAR(pred.z, 20.f - 9.81f * 4.f * 0.5f, 1e-4f); +} + +TEST(CryPredEngineTrait, PredictTargetPositionAirborneMovingWithGravity) +{ + projectile_prediction::Target t; + t.m_origin = {0.f, 0.f, 50.f}; + t.m_velocity = {10.f, 5.f, 0.f}; + t.m_is_airborne = true; + + const auto pred = PredEngineTrait::predict_target_position(t, 3.f, 9.81f); + + EXPECT_NEAR(pred.x, 30.f, 1e-4f); + EXPECT_NEAR(pred.y, 15.f, 1e-4f); + EXPECT_NEAR(pred.z, 50.f - 9.81f * 9.f * 0.5f, 1e-4f); +} + +// ---- calc_vector_2d_distance ---- + +TEST(CryPredEngineTrait, CalcVector2dDistance_3_4_5) +{ + EXPECT_NEAR(PredEngineTrait::calc_vector_2d_distance({3.f, 4.f, 999.f}), 5.f, 1e-5f); +} + +TEST(CryPredEngineTrait, CalcVector2dDistance_ZeroVector) +{ + EXPECT_NEAR(PredEngineTrait::calc_vector_2d_distance({0.f, 0.f, 0.f}), 0.f, 1e-6f); +} + +TEST(CryPredEngineTrait, CalcVector2dDistance_ZIgnored) +{ + // Z does not affect the 2D distance + EXPECT_NEAR(PredEngineTrait::calc_vector_2d_distance({0.f, 5.f, 100.f}), + PredEngineTrait::calc_vector_2d_distance({0.f, 5.f, 0.f}), 1e-6f); +} + +// ---- get_vector_height_coordinate ---- + +TEST(CryPredEngineTrait, GetVectorHeightCoordinate_ReturnsZ) +{ + // Cry engine up = +Z + EXPECT_FLOAT_EQ(PredEngineTrait::get_vector_height_coordinate({1.f, 2.f, 7.f}), 7.f); +} + +// ---- calc_direct_pitch_angle ---- + +TEST(CryPredEngineTrait, CalcDirectPitchAngle_Flat) +{ + // Target at same height → pitch = 0 + EXPECT_NEAR(PredEngineTrait::calc_direct_pitch_angle({0.f, 0.f, 0.f}, {0.f, 100.f, 0.f}), 0.f, 1e-4f); +} + +TEST(CryPredEngineTrait, CalcDirectPitchAngle_LookingUp) +{ + // Target at 45° above (equal XY distance and Z height) + // direction to {0, 1, 1} normalized = {0, 0.707, 0.707}, asin(0.707) = 45° + EXPECT_NEAR(PredEngineTrait::calc_direct_pitch_angle({0.f, 0.f, 0.f}, {0.f, 1.f, 1.f}), 45.f, 1e-3f); +} + +TEST(CryPredEngineTrait, CalcDirectPitchAngle_LookingDown) +{ + // Target directly below + EXPECT_NEAR(PredEngineTrait::calc_direct_pitch_angle({0.f, 0.f, 10.f}, {0.f, 0.f, 0.f}), -90.f, 1e-3f); +} + +TEST(CryPredEngineTrait, CalcDirectPitchAngle_LookingDirectlyUp) +{ + EXPECT_NEAR(PredEngineTrait::calc_direct_pitch_angle({0.f, 0.f, 0.f}, {0.f, 0.f, 100.f}), 90.f, 1e-3f); +} + +// ---- calc_direct_yaw_angle ---- + +TEST(CryPredEngineTrait, CalcDirectYawAngle_ForwardAlongY) +{ + // Cry engine forward = +Y → yaw = 0 + EXPECT_NEAR(PredEngineTrait::calc_direct_yaw_angle({0.f, 0.f, 0.f}, {0.f, 100.f, 0.f}), 0.f, 1e-4f); +} + +TEST(CryPredEngineTrait, CalcDirectYawAngle_AlongPositiveX) +{ + // direction = {1, 0, 0}, yaw = -atan2(1, 0) = -90° + EXPECT_NEAR(PredEngineTrait::calc_direct_yaw_angle({0.f, 0.f, 0.f}, {100.f, 0.f, 0.f}), -90.f, 1e-3f); +} + +TEST(CryPredEngineTrait, CalcDirectYawAngle_AlongNegativeX) +{ + // direction = {-1, 0, 0}, yaw = -atan2(-1, 0) = 90° + EXPECT_NEAR(PredEngineTrait::calc_direct_yaw_angle({0.f, 0.f, 0.f}, {-100.f, 0.f, 0.f}), 90.f, 1e-3f); +} + +TEST(CryPredEngineTrait, CalcDirectYawAngle_BackwardAlongNegY) +{ + // direction = {0, -1, 0}, yaw = -atan2(0, -1) = ±180° + const float yaw = PredEngineTrait::calc_direct_yaw_angle({0.f, 0.f, 0.f}, {0.f, -100.f, 0.f}); + EXPECT_NEAR(std::abs(yaw), 180.f, 1e-3f); +} + +TEST(CryPredEngineTrait, CalcDirectYawAngle_OffOriginCamera) +{ + // Same relative direction regardless of camera position + const float yaw_a = PredEngineTrait::calc_direct_yaw_angle({0.f, 0.f, 0.f}, {0.f, 100.f, 0.f}); + const float yaw_b = PredEngineTrait::calc_direct_yaw_angle({50.f, 50.f, 0.f}, {50.f, 150.f, 0.f}); + EXPECT_NEAR(yaw_a, yaw_b, 1e-4f); +} + +// ---- calc_viewpoint_from_angles ---- + +TEST(CryPredEngineTrait, CalcViewpointFromAngles_45Degrees) +{ + projectile_prediction::Projectile p; + p.m_origin = {0.f, 0.f, 0.f}; + p.m_launch_speed = 10.f; + + // Target along +Y at distance 10; pitch=45° → height = 10 * tan(45°) = 10 + const Vector3 target{0.f, 10.f, 0.f}; + const auto vp = PredEngineTrait::calc_viewpoint_from_angles(p, target, 45.f); + + EXPECT_NEAR(vp.x, 0.f, 1e-4f); + EXPECT_NEAR(vp.y, 10.f, 1e-4f); + EXPECT_NEAR(vp.z, 10.f, 1e-3f); +} + +TEST(CryPredEngineTrait, CalcViewpointFromAngles_ZeroPitch) +{ + projectile_prediction::Projectile p; + p.m_origin = {0.f, 0.f, 5.f}; + p.m_launch_speed = 1.f; + + const Vector3 target{3.f, 4.f, 0.f}; + const auto vp = PredEngineTrait::calc_viewpoint_from_angles(p, target, 0.f); + + // tan(0) = 0 → viewpoint Z = origin.z + 0 = 5 + EXPECT_NEAR(vp.x, 3.f, 1e-4f); + EXPECT_NEAR(vp.y, 4.f, 1e-4f); + EXPECT_NEAR(vp.z, 5.f, 1e-4f); +} + +TEST(CryPredEngineTrait, CalcViewpointXYMatchesPredictedTargetXY) +{ + projectile_prediction::Projectile p; + p.m_origin = {1.f, 2.f, 3.f}; + p.m_launch_speed = 50.f; + + const Vector3 target{10.f, 20.f, 5.f}; + const auto vp = PredEngineTrait::calc_viewpoint_from_angles(p, target, 30.f); + + // X and Y always match the predicted target position + EXPECT_NEAR(vp.x, target.x, 1e-4f); + EXPECT_NEAR(vp.y, target.y, 1e-4f); +} diff --git a/tests/general/unit_test_projection.cpp b/tests/general/unit_test_projection.cpp index 5a0ed2e..50c4031 100644 --- a/tests/general/unit_test_projection.cpp +++ b/tests/general/unit_test_projection.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -941,3 +942,247 @@ TEST(UnitTestProjection, IWEngine_ZeroAngles_BasisVectors) EXPECT_NEAR(up.y, omath::iw_engine::k_abs_up.y, k_eps); EXPECT_NEAR(up.z, omath::iw_engine::k_abs_up.z, k_eps); } + +// ---- extract_projection_params ---- + +TEST(UnitTestProjection, ExtractProjectionParams_FovRoundTrip) +{ + // Source engine applies a 0.75 scale factor to its projection matrix, so + // extract_projection_params (standard formula) does not round-trip with it. + // Use Unity engine, which uses a standard projection matrix. + constexpr float k_eps = 1e-4f; + constexpr auto fov = omath::projection::FieldOfView::from_degrees(75.f); + const auto cam = omath::unity_engine::Camera({}, {}, {1280.f, 720.f}, fov, 0.03f, 1000.f); + + const auto params = omath::unity_engine::Camera::extract_projection_params(cam.get_projection_matrix()); + + EXPECT_NEAR(params.fov.as_degrees(), 75.f, k_eps); +} + +TEST(UnitTestProjection, ExtractProjectionParams_AspectRatioRoundTrip) +{ + constexpr float k_eps = 1e-4f; + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + const auto cam = omath::source_engine::Camera({}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + const auto params = omath::source_engine::Camera::extract_projection_params(cam.get_projection_matrix()); + + EXPECT_NEAR(params.aspect_ratio, 1920.f / 1080.f, k_eps); +} + +TEST(UnitTestProjection, ExtractProjectionParams_UnityEngine) +{ + constexpr float k_eps = 1e-4f; + constexpr auto fov = omath::projection::FieldOfView::from_degrees(60.f); + const auto cam = omath::unity_engine::Camera({}, {}, {1280.f, 720.f}, fov, 0.03f, 1000.f); + + const auto params = omath::unity_engine::Camera::extract_projection_params(cam.get_projection_matrix()); + + EXPECT_NEAR(params.fov.as_degrees(), 60.f, k_eps); + EXPECT_NEAR(params.aspect_ratio, 1280.f / 720.f, k_eps); +} + +// ---- Accessors ---- + +TEST(UnitTestProjection, Accessors_GetFovNearFarOrigin) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + const omath::Vector3 origin{10.f, 20.f, 30.f}; + const auto cam = omath::source_engine::Camera(origin, {}, {1920.f, 1080.f}, fov, 0.1f, 500.f); + + EXPECT_NEAR(cam.get_field_of_view().as_degrees(), 90.f, 1e-4f); + EXPECT_FLOAT_EQ(cam.get_near_plane(), 0.1f); + EXPECT_FLOAT_EQ(cam.get_far_plane(), 500.f); + EXPECT_FLOAT_EQ(cam.get_origin().x, 10.f); + EXPECT_FLOAT_EQ(cam.get_origin().y, 20.f); + EXPECT_FLOAT_EQ(cam.get_origin().z, 30.f); +} + +// ---- Setters + cache invalidation ---- + +TEST(UnitTestProjection, SetFieldOfView_InvalidatesProjection) +{ + constexpr auto fov_a = omath::projection::FieldOfView::from_degrees(90.f); + constexpr auto fov_b = omath::projection::FieldOfView::from_degrees(45.f); + auto cam = omath::source_engine::Camera({}, {}, {1920.f, 1080.f}, fov_a, 0.01f, 1000.f); + + const auto proj_before = cam.get_projection_matrix(); + cam.set_field_of_view(fov_b); + const auto proj_after = cam.get_projection_matrix(); + + EXPECT_NE(proj_before.at(0, 0), proj_after.at(0, 0)); + EXPECT_NEAR(cam.get_field_of_view().as_degrees(), 45.f, 1e-4f); +} + +TEST(UnitTestProjection, SetNearPlane_InvalidatesProjection) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::source_engine::Camera({}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + const auto proj_before = cam.get_projection_matrix(); + cam.set_near_plane(1.f); + const auto proj_after = cam.get_projection_matrix(); + + EXPECT_FLOAT_EQ(cam.get_near_plane(), 1.f); + EXPECT_NE(proj_before.at(2, 2), proj_after.at(2, 2)); +} + +TEST(UnitTestProjection, SetFarPlane_InvalidatesProjection) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::source_engine::Camera({}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + const auto proj_before = cam.get_projection_matrix(); + cam.set_far_plane(100.f); + const auto proj_after = cam.get_projection_matrix(); + + EXPECT_FLOAT_EQ(cam.get_far_plane(), 100.f); + EXPECT_NE(proj_before.at(2, 2), proj_after.at(2, 2)); +} + +TEST(UnitTestProjection, SetOrigin_InvalidatesViewMatrix) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::source_engine::Camera({0.f, 0.f, 0.f}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + // Target is off to the side — stays at the same world position while camera + // moves laterally, so the projected X must change. + const auto screen_before = cam.world_to_screen({500.f, 100.f, 0.f}); + cam.set_origin({0.f, 100.f, 0.f}); // lateral shift + const auto screen_after = cam.world_to_screen({500.f, 100.f, 0.f}); + + ASSERT_TRUE(screen_before.has_value()); + ASSERT_TRUE(screen_after.has_value()); + EXPECT_NE(screen_before->x, screen_after->x); + EXPECT_FLOAT_EQ(cam.get_origin().y, 100.f); +} + +TEST(UnitTestProjection, SetViewPort_InvalidatesProjection) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::source_engine::Camera({}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + const auto proj_before = cam.get_projection_matrix(); + // 1280x800 is 8:5, different aspect ratio from 1920x1080 (16:9) + cam.set_view_port({1280.f, 800.f}); + const auto proj_after = cam.get_projection_matrix(); + + EXPECT_NE(proj_before.at(0, 0), proj_after.at(0, 0)); +} + +TEST(UnitTestProjection, SetViewAngles_InvalidatesViewMatrix) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::source_engine::Camera({}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + const auto view_before = cam.get_view_matrix(); + const omath::source_engine::ViewAngles rotated{ + omath::source_engine::PitchAngle::from_degrees(30.f), + omath::source_engine::YawAngle::from_degrees(45.f), + omath::source_engine::RollAngle::from_degrees(0.f) + }; + cam.set_view_angles(rotated); + const auto view_after = cam.get_view_matrix(); + + EXPECT_NE(view_before.at(0, 0), view_after.at(0, 0)); +} + +// ---- calc_look_at_angles / look_at ---- + +TEST(UnitTestProjection, CalcLookAtAngles_ForwardTarget) +{ + // Source engine: +X is forward. Camera at origin, target on +X axis. + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + const auto cam = omath::source_engine::Camera({0.f, 0.f, 0.f}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + const auto angles = cam.calc_look_at_angles({100.f, 0.f, 0.f}); + + EXPECT_NEAR(angles.pitch.as_degrees(), 0.f, 1e-4f); + EXPECT_NEAR(angles.yaw.as_degrees(), 0.f, 1e-4f); +} + +TEST(UnitTestProjection, LookAt_ForwardVectorPointsAtTarget) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::source_engine::Camera({0.f, 0.f, 0.f}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + cam.look_at({200.f, 0.f, 0.f}); + const auto fwd = cam.get_abs_forward(); + + // After pointing at +X target the forward vector should be ~(1,0,0) + EXPECT_NEAR(fwd.x, 1.f, 1e-4f); + EXPECT_NEAR(fwd.y, 0.f, 1e-4f); + EXPECT_NEAR(fwd.z, 0.f, 1e-4f); +} + +// ---- is_culled_by_frustum (triangle) ---- + +TEST(UnitTestProjection, TriangleInsideFrustumNotCulled) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + const auto cam = omath::source_engine::Camera({0.f, 0.f, 0.f}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + // Small triangle directly in front (Source: +X forward) + const omath::Triangle> tri{ + {100.f, 0.f, 1.f}, + {100.f, 1.f, -1.f}, + {100.f, -1.f, -1.f} + }; + EXPECT_FALSE(cam.is_culled_by_frustum(tri)); +} + +TEST(UnitTestProjection, TriangleBehindCameraCulled) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + const auto cam = omath::source_engine::Camera({0.f, 0.f, 0.f}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + // Triangle entirely behind the camera (-X) + const omath::Triangle> tri{ + {-100.f, 0.f, 1.f}, + {-100.f, 1.f, -1.f}, + {-100.f, -1.f, -1.f} + }; + EXPECT_TRUE(cam.is_culled_by_frustum(tri)); +} + +TEST(UnitTestProjection, TriangleBeyondFarPlaneCulled) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + const auto cam = omath::source_engine::Camera({0.f, 0.f, 0.f}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + // Triangle beyond the 1000-unit far plane + const omath::Triangle> tri{ + {2000.f, 0.f, 1.f}, + {2000.f, 1.f, -1.f}, + {2000.f, -1.f, -1.f} + }; + EXPECT_TRUE(cam.is_culled_by_frustum(tri)); +} + +TEST(UnitTestProjection, TriangleFarToSideCulled) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + const auto cam = omath::source_engine::Camera({0.f, 0.f, 0.f}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + // Triangle far outside the side frustum planes + const omath::Triangle> tri{ + {100.f, 5000.f, 0.f}, + {100.f, 5001.f, 1.f}, + {100.f, 5001.f, -1.f} + }; + EXPECT_TRUE(cam.is_culled_by_frustum(tri)); +} + +TEST(UnitTestProjection, TriangleStraddlingFrustumNotCulled) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + const auto cam = omath::source_engine::Camera({0.f, 0.f, 0.f}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + // Large triangle with vertices on both sides of the frustum — should not be culled + const omath::Triangle> tri{ + { 100.f, 0.f, 0.f}, + { 100.f, 5000.f, 0.f}, + { 100.f, 0.f, 5000.f} + }; + EXPECT_FALSE(cam.is_culled_by_frustum(tri)); +}