From a5c0ca0cbd7b0cf9087995b699a249d7824d1de9 Mon Sep 17 00:00:00 2001 From: orange Date: Tue, 17 Mar 2026 20:31:46 +0300 Subject: [PATCH 1/3] added stuff --- .../cry_engine/traits/pred_engine_trait.hpp | 3 +- .../traits/pred_engine_trait.hpp | 3 +- .../iw_engine/traits/pred_engine_trait.hpp | 3 +- .../traits/pred_engine_trait.hpp | 3 +- .../traits/pred_engine_trait.hpp | 3 +- .../unity_engine/traits/pred_engine_trait.hpp | 3 +- .../traits/pred_engine_trait.hpp | 3 +- .../proj_pred_engine.hpp | 11 ++ .../proj_pred_engine_avx2.hpp | 3 + .../proj_pred_engine_legacy.hpp | 33 ++++- .../projectile_prediction/projectile.hpp | 1 + .../proj_pred_engine_avx2.cpp | 106 ++++++++++++++- tests/engines/unit_test_traits_engines.cpp | 123 ++++++++++++++++++ tests/general/unit_test_pred_engine_trait.cpp | 41 ++++++ tests/general/unit_test_prediction.cpp | 104 +++++++++++++++ ...unit_test_proj_pred_engine_legacy_more.cpp | 19 +++ 16 files changed, 452 insertions(+), 10 deletions(-) diff --git a/include/omath/engines/cry_engine/traits/pred_engine_trait.hpp b/include/omath/engines/cry_engine/traits/pred_engine_trait.hpp index 1e3621fb..fc28b539 100644 --- a/include/omath/engines/cry_engine/traits/pred_engine_trait.hpp +++ b/include/omath/engines/cry_engine/traits/pred_engine_trait.hpp @@ -16,7 +16,8 @@ namespace omath::cry_engine const float pitch, const float yaw, const float time, const float gravity) noexcept { - auto current_pos = projectile.m_origin + const auto launch_pos = projectile.m_origin + projectile.m_launch_offset; + auto current_pos = launch_pos + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), RollAngle::from_degrees(0)}) * projectile.m_launch_speed * time; diff --git a/include/omath/engines/frostbite_engine/traits/pred_engine_trait.hpp b/include/omath/engines/frostbite_engine/traits/pred_engine_trait.hpp index a1e681e5..0a5ad757 100644 --- a/include/omath/engines/frostbite_engine/traits/pred_engine_trait.hpp +++ b/include/omath/engines/frostbite_engine/traits/pred_engine_trait.hpp @@ -16,7 +16,8 @@ namespace omath::frostbite_engine const float pitch, const float yaw, const float time, const float gravity) noexcept { - auto current_pos = projectile.m_origin + const auto launch_pos = projectile.m_origin + projectile.m_launch_offset; + auto current_pos = launch_pos + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), RollAngle::from_degrees(0)}) * projectile.m_launch_speed * time; diff --git a/include/omath/engines/iw_engine/traits/pred_engine_trait.hpp b/include/omath/engines/iw_engine/traits/pred_engine_trait.hpp index 7961268a..728fd754 100644 --- a/include/omath/engines/iw_engine/traits/pred_engine_trait.hpp +++ b/include/omath/engines/iw_engine/traits/pred_engine_trait.hpp @@ -17,7 +17,8 @@ namespace omath::iw_engine const float pitch, const float yaw, const float time, const float gravity) noexcept { - auto current_pos = projectile.m_origin + const auto launch_pos = projectile.m_origin + projectile.m_launch_offset; + auto current_pos = launch_pos + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), RollAngle::from_degrees(0)}) * projectile.m_launch_speed * time; diff --git a/include/omath/engines/opengl_engine/traits/pred_engine_trait.hpp b/include/omath/engines/opengl_engine/traits/pred_engine_trait.hpp index 2567d84b..83e758c4 100644 --- a/include/omath/engines/opengl_engine/traits/pred_engine_trait.hpp +++ b/include/omath/engines/opengl_engine/traits/pred_engine_trait.hpp @@ -16,7 +16,8 @@ namespace omath::opengl_engine const float pitch, const float yaw, const float time, const float gravity) noexcept { - auto current_pos = projectile.m_origin + const auto launch_pos = projectile.m_origin + projectile.m_launch_offset; + auto current_pos = launch_pos + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), RollAngle::from_degrees(0)}) * projectile.m_launch_speed * time; diff --git a/include/omath/engines/source_engine/traits/pred_engine_trait.hpp b/include/omath/engines/source_engine/traits/pred_engine_trait.hpp index ca9771e1..7f8cc8ef 100644 --- a/include/omath/engines/source_engine/traits/pred_engine_trait.hpp +++ b/include/omath/engines/source_engine/traits/pred_engine_trait.hpp @@ -17,7 +17,8 @@ namespace omath::source_engine const float pitch, const float yaw, const float time, const float gravity) noexcept { - auto current_pos = projectile.m_origin + const auto launch_pos = projectile.m_origin + projectile.m_launch_offset; + auto current_pos = launch_pos + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), RollAngle::from_degrees(0)}) * projectile.m_launch_speed * time; diff --git a/include/omath/engines/unity_engine/traits/pred_engine_trait.hpp b/include/omath/engines/unity_engine/traits/pred_engine_trait.hpp index 77f5781d..52044b8f 100644 --- a/include/omath/engines/unity_engine/traits/pred_engine_trait.hpp +++ b/include/omath/engines/unity_engine/traits/pred_engine_trait.hpp @@ -16,7 +16,8 @@ namespace omath::unity_engine const float pitch, const float yaw, const float time, const float gravity) noexcept { - auto current_pos = projectile.m_origin + const auto launch_pos = projectile.m_origin + projectile.m_launch_offset; + auto current_pos = launch_pos + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), RollAngle::from_degrees(0)}) * projectile.m_launch_speed * time; diff --git a/include/omath/engines/unreal_engine/traits/pred_engine_trait.hpp b/include/omath/engines/unreal_engine/traits/pred_engine_trait.hpp index dbc04f05..f2a2214b 100644 --- a/include/omath/engines/unreal_engine/traits/pred_engine_trait.hpp +++ b/include/omath/engines/unreal_engine/traits/pred_engine_trait.hpp @@ -16,7 +16,8 @@ namespace omath::unreal_engine const float pitch, const float yaw, const float time, const float gravity) noexcept { - auto current_pos = projectile.m_origin + const auto launch_pos = projectile.m_origin + projectile.m_launch_offset; + auto current_pos = launch_pos + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), RollAngle::from_degrees(0)}) * projectile.m_launch_speed * time; diff --git a/include/omath/projectile_prediction/proj_pred_engine.hpp b/include/omath/projectile_prediction/proj_pred_engine.hpp index bbd5a546..8b2b6f7b 100644 --- a/include/omath/projectile_prediction/proj_pred_engine.hpp +++ b/include/omath/projectile_prediction/proj_pred_engine.hpp @@ -8,12 +8,23 @@ namespace omath::projectile_prediction { + struct AimAngles + { + float pitch{}; + float yaw{}; + }; + class ProjPredEngineInterface { public: [[nodiscard]] virtual std::optional> maybe_calculate_aim_point(const Projectile& projectile, const Target& target) const = 0; + + [[nodiscard]] + virtual std::optional maybe_calculate_aim_angles(const Projectile& projectile, + const Target& target) const = 0; + virtual ~ProjPredEngineInterface() = default; }; } // namespace omath::projectile_prediction diff --git a/include/omath/projectile_prediction/proj_pred_engine_avx2.hpp b/include/omath/projectile_prediction/proj_pred_engine_avx2.hpp index e4a7dc51..da41c645 100644 --- a/include/omath/projectile_prediction/proj_pred_engine_avx2.hpp +++ b/include/omath/projectile_prediction/proj_pred_engine_avx2.hpp @@ -12,6 +12,9 @@ namespace omath::projectile_prediction [[nodiscard]] std::optional> maybe_calculate_aim_point(const Projectile& projectile, const Target& target) const override; + [[nodiscard]] std::optional + maybe_calculate_aim_angles(const Projectile& projectile, const Target& target) const override; + ProjPredEngineAvx2(float gravity_constant, float simulation_time_step, float maximum_simulation_time); ~ProjPredEngineAvx2() override = default; diff --git a/include/omath/projectile_prediction/proj_pred_engine_legacy.hpp b/include/omath/projectile_prediction/proj_pred_engine_legacy.hpp index bd75554e..3740248d 100644 --- a/include/omath/projectile_prediction/proj_pred_engine_legacy.hpp +++ b/include/omath/projectile_prediction/proj_pred_engine_legacy.hpp @@ -54,6 +54,36 @@ namespace omath::projectile_prediction [[nodiscard]] std::optional> maybe_calculate_aim_point(const Projectile& projectile, const Target& target) const override + { + const auto solution = find_solution(projectile, target); + if (!solution) + return std::nullopt; + + return EngineTrait::calc_viewpoint_from_angles(projectile, solution->predicted_target_position, + solution->pitch); + } + + [[nodiscard]] + std::optional maybe_calculate_aim_angles(const Projectile& projectile, + const Target& target) const override + { + const auto solution = find_solution(projectile, target); + if (!solution) + return std::nullopt; + + const auto yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin, solution->predicted_target_position); + return AimAngles{solution->pitch, yaw}; + } + + private: + struct Solution + { + Vector3 predicted_target_position; + float pitch; + }; + + [[nodiscard]] + std::optional find_solution(const Projectile& projectile, const Target& target) const { for (float time = 0.f; time < m_maximum_simulation_time; time += m_simulation_time_step) { @@ -70,12 +100,11 @@ namespace omath::projectile_prediction time)) continue; - return EngineTrait::calc_viewpoint_from_angles(projectile, predicted_target_position, projectile_pitch); + return Solution{predicted_target_position, projectile_pitch.value()}; } return std::nullopt; } - private: const float m_gravity_constant; const float m_simulation_time_step; const float m_maximum_simulation_time; diff --git a/include/omath/projectile_prediction/projectile.hpp b/include/omath/projectile_prediction/projectile.hpp index c4560ed4..92537ed4 100644 --- a/include/omath/projectile_prediction/projectile.hpp +++ b/include/omath/projectile_prediction/projectile.hpp @@ -11,6 +11,7 @@ namespace omath::projectile_prediction { public: Vector3 m_origin; + Vector3 m_launch_offset{0.f, 0.f, 0.f}; float m_launch_speed{}; float m_gravity_scale{}; }; diff --git a/source/projectile_prediction/proj_pred_engine_avx2.cpp b/source/projectile_prediction/proj_pred_engine_avx2.cpp index 9605db6d..b942a87d 100644 --- a/source/projectile_prediction/proj_pred_engine_avx2.cpp +++ b/source/projectile_prediction/proj_pred_engine_avx2.cpp @@ -21,7 +21,7 @@ namespace omath::projectile_prediction const float bullet_gravity = m_gravity_constant * projectile.m_gravity_scale; const float v0 = projectile.m_launch_speed; const float v0_sqr = v0 * v0; - const Vector3 proj_origin = projectile.m_origin; + const Vector3 proj_origin = projectile.m_origin + projectile.m_launch_offset; constexpr int SIMD_FACTOR = 8; float current_time = m_simulation_time_step; @@ -124,6 +124,110 @@ namespace omath::projectile_prediction std::format("{} AVX2 feature is not enabled!", std::source_location::current().function_name())); #endif } + std::optional + ProjPredEngineAvx2::maybe_calculate_aim_angles([[maybe_unused]] const Projectile& projectile, + [[maybe_unused]] const Target& target) const + { +#if defined(OMATH_USE_AVX2) && defined(__i386__) && defined(__x86_64__) + const float bullet_gravity = m_gravity_constant * projectile.m_gravity_scale; + const float v0 = projectile.m_launch_speed; + const Vector3 proj_origin = projectile.m_origin + projectile.m_launch_offset; + + constexpr int SIMD_FACTOR = 8; + float current_time = m_simulation_time_step; + + for (; current_time <= m_maximum_simulation_time; current_time += m_simulation_time_step * SIMD_FACTOR) + { + const __m256 times + = _mm256_setr_ps(current_time, current_time + m_simulation_time_step, + current_time + m_simulation_time_step * 2, current_time + m_simulation_time_step * 3, + current_time + m_simulation_time_step * 4, current_time + m_simulation_time_step * 5, + current_time + m_simulation_time_step * 6, current_time + m_simulation_time_step * 7); + + const __m256 target_x + = _mm256_fmadd_ps(_mm256_set1_ps(target.m_velocity.x), times, _mm256_set1_ps(target.m_origin.x)); + const __m256 target_y + = _mm256_fmadd_ps(_mm256_set1_ps(target.m_velocity.y), times, _mm256_set1_ps(target.m_origin.y)); + const __m256 times_sq = _mm256_mul_ps(times, times); + const __m256 target_z = _mm256_fmadd_ps(_mm256_set1_ps(target.m_velocity.z), times, + _mm256_fnmadd_ps(_mm256_set1_ps(0.5f * m_gravity_constant), times_sq, + _mm256_set1_ps(target.m_origin.z))); + + const __m256 delta_x = _mm256_sub_ps(target_x, _mm256_set1_ps(proj_origin.x)); + const __m256 delta_y = _mm256_sub_ps(target_y, _mm256_set1_ps(proj_origin.y)); + + const __m256 d_sqr = _mm256_add_ps(_mm256_mul_ps(delta_x, delta_x), _mm256_mul_ps(delta_y, delta_y)); + const __m256 delta_z = _mm256_sub_ps(target_z, _mm256_set1_ps(proj_origin.z)); + + const __m256 bg_times_sq = _mm256_mul_ps(_mm256_set1_ps(bullet_gravity), times_sq); + const __m256 term = _mm256_add_ps(delta_z, _mm256_mul_ps(_mm256_set1_ps(0.5f), bg_times_sq)); + const __m256 term_sq = _mm256_mul_ps(term, term); + const __m256 numerator = _mm256_add_ps(d_sqr, term_sq); + const __m256 denominator = _mm256_add_ps(times_sq, _mm256_set1_ps(1e-8f)); + const __m256 required_v0_sqr = _mm256_div_ps(numerator, denominator); + + const __m256 v0_sqr_vec = _mm256_set1_ps(v0 * v0 + 1e-3f); + const __m256 mask = _mm256_cmp_ps(required_v0_sqr, v0_sqr_vec, _CMP_LE_OQ); + + const unsigned valid_mask = _mm256_movemask_ps(mask); + if (!valid_mask) + continue; + + alignas(32) float valid_times[SIMD_FACTOR]; + _mm256_store_ps(valid_times, times); + + for (int i = 0; i < SIMD_FACTOR; ++i) + { + if (!(valid_mask & (1 << i))) + continue; + + const float candidate_time = valid_times[i]; + if (candidate_time > m_maximum_simulation_time) + continue; + + for (float fine_time = candidate_time - m_simulation_time_step * 2; + fine_time <= candidate_time + m_simulation_time_step * 2; fine_time += m_simulation_time_step) + { + if (fine_time < 0) + continue; + + Vector3 target_pos = target.m_origin + target.m_velocity * fine_time; + if (target.m_is_airborne) + target_pos.z -= 0.5f * m_gravity_constant * fine_time * fine_time; + + const auto pitch = calculate_pitch(proj_origin, target_pos, bullet_gravity, v0, fine_time); + if (!pitch) + continue; + + const Vector3 delta = target_pos - projectile.m_origin; + const float yaw = angles::radians_to_degrees(std::atan2(delta.y, delta.x)); + return AimAngles{*pitch, yaw}; + } + } + } + + for (; current_time <= m_maximum_simulation_time; current_time += m_simulation_time_step) + { + Vector3 target_pos = target.m_origin + target.m_velocity * current_time; + if (target.m_is_airborne) + target_pos.z -= 0.5f * m_gravity_constant * current_time * current_time; + + const auto pitch = calculate_pitch(proj_origin, target_pos, bullet_gravity, v0, current_time); + if (!pitch) + continue; + + const Vector3 delta = target_pos - projectile.m_origin; + const float yaw = angles::radians_to_degrees(std::atan2(delta.y, delta.x)); + return AimAngles{*pitch, yaw}; + } + + return std::nullopt; +#else + throw std::runtime_error( + std::format("{} AVX2 feature is not enabled!", std::source_location::current().function_name())); +#endif + } + ProjPredEngineAvx2::ProjPredEngineAvx2(const float gravity_constant, const float simulation_time_step, const float maximum_simulation_time) : m_gravity_constant(gravity_constant), m_simulation_time_step(simulation_time_step), diff --git a/tests/engines/unit_test_traits_engines.cpp b/tests/engines/unit_test_traits_engines.cpp index f891a3e3..7f5fd793 100644 --- a/tests/engines/unit_test_traits_engines.cpp +++ b/tests/engines/unit_test_traits_engines.cpp @@ -20,6 +20,8 @@ #include #include +#include + #include #include #include @@ -35,6 +37,127 @@ static void expect_matrix_near(const MatT& a, const MatT& b, float eps = 1e-5f) EXPECT_NEAR(a.at(r, c), b.at(r, c), eps); } +// ── Launch offset tests for all engines ────────────────────────────────────── +#include + +// Helper: verify that zero offset matches default-initialized offset behavior +template +static void verify_launch_offset_at_time_zero(const Vector3& origin, const Vector3& offset) +{ + projectile_prediction::Projectile p; + p.m_origin = origin; + p.m_launch_offset = offset; + p.m_launch_speed = 100.f; + p.m_gravity_scale = 1.f; + + const auto pos = Trait::predict_projectile_position(p, 0.f, 0.f, 0.f, 9.81f); + const auto expected = origin + offset; + EXPECT_NEAR(pos.x, expected.x, 1e-4f); + EXPECT_NEAR(pos.y, expected.y, 1e-4f); + EXPECT_NEAR(pos.z, expected.z, 1e-4f); +} + +template +static void verify_zero_offset_matches_default() +{ + projectile_prediction::Projectile p; + p.m_origin = {10.f, 20.f, 30.f}; + p.m_launch_offset = {0.f, 0.f, 0.f}; + p.m_launch_speed = 50.f; + p.m_gravity_scale = 1.f; + + projectile_prediction::Projectile p2; + p2.m_origin = {10.f, 20.f, 30.f}; + p2.m_launch_speed = 50.f; + p2.m_gravity_scale = 1.f; + + const auto pos1 = Trait::predict_projectile_position(p, 15.f, 30.f, 1.f, 9.81f); + const auto pos2 = Trait::predict_projectile_position(p2, 15.f, 30.f, 1.f, 9.81f); + EXPECT_NEAR(pos1.x, pos2.x, 1e-6f); + EXPECT_NEAR(pos1.y, pos2.y, 1e-6f); + EXPECT_NEAR(pos1.z, pos2.z, 1e-6f); +} + +TEST(LaunchOffsetTests, Source_OffsetAtTimeZero) +{ + verify_launch_offset_at_time_zero({0, 0, 0}, {5, 3, -2}); +} +TEST(LaunchOffsetTests, Source_ZeroOffsetMatchesDefault) +{ + verify_zero_offset_matches_default(); +} +TEST(LaunchOffsetTests, Frostbite_OffsetAtTimeZero) +{ + verify_launch_offset_at_time_zero({0, 0, 0}, {5, 3, -2}); +} +TEST(LaunchOffsetTests, Frostbite_ZeroOffsetMatchesDefault) +{ + verify_zero_offset_matches_default(); +} +TEST(LaunchOffsetTests, IW_OffsetAtTimeZero) +{ + verify_launch_offset_at_time_zero({0, 0, 0}, {5, 3, -2}); +} +TEST(LaunchOffsetTests, IW_ZeroOffsetMatchesDefault) +{ + verify_zero_offset_matches_default(); +} +TEST(LaunchOffsetTests, OpenGL_OffsetAtTimeZero) +{ + verify_launch_offset_at_time_zero({0, 0, 0}, {5, 3, -2}); +} +TEST(LaunchOffsetTests, OpenGL_ZeroOffsetMatchesDefault) +{ + verify_zero_offset_matches_default(); +} +TEST(LaunchOffsetTests, Unity_OffsetAtTimeZero) +{ + verify_launch_offset_at_time_zero({0, 0, 0}, {5, 3, -2}); +} +TEST(LaunchOffsetTests, Unity_ZeroOffsetMatchesDefault) +{ + verify_zero_offset_matches_default(); +} +TEST(LaunchOffsetTests, Unreal_OffsetAtTimeZero) +{ + verify_launch_offset_at_time_zero({0, 0, 0}, {5, 3, -2}); +} +TEST(LaunchOffsetTests, Unreal_ZeroOffsetMatchesDefault) +{ + verify_zero_offset_matches_default(); +} +TEST(LaunchOffsetTests, CryEngine_OffsetAtTimeZero) +{ + verify_launch_offset_at_time_zero({0, 0, 0}, {5, 3, -2}); +} +TEST(LaunchOffsetTests, CryEngine_ZeroOffsetMatchesDefault) +{ + verify_zero_offset_matches_default(); +} + +// Test that offset shifts the projectile position at t>0 as well +TEST(LaunchOffsetTests, OffsetShiftsTrajectory) +{ + projectile_prediction::Projectile p_no_offset; + p_no_offset.m_origin = {0.f, 0.f, 0.f}; + p_no_offset.m_launch_speed = 100.f; + p_no_offset.m_gravity_scale = 1.f; + + projectile_prediction::Projectile p_with_offset; + p_with_offset.m_origin = {0.f, 0.f, 0.f}; + p_with_offset.m_launch_offset = {10.f, 5.f, -3.f}; + p_with_offset.m_launch_speed = 100.f; + p_with_offset.m_gravity_scale = 1.f; + + const auto pos1 = source_engine::PredEngineTrait::predict_projectile_position(p_no_offset, 20.f, 45.f, 2.f, 9.81f); + const auto pos2 = source_engine::PredEngineTrait::predict_projectile_position(p_with_offset, 20.f, 45.f, 2.f, 9.81f); + + // The difference should be exactly the launch offset + EXPECT_NEAR(pos2.x - pos1.x, 10.f, 1e-4f); + EXPECT_NEAR(pos2.y - pos1.y, 5.f, 1e-4f); + EXPECT_NEAR(pos2.z - pos1.z, -3.f, 1e-4f); +} + // Generic tests for PredEngineTrait behaviour across engines TEST(TraitTests, Frostbite_Pred_And_Mesh_And_Camera) { diff --git a/tests/general/unit_test_pred_engine_trait.cpp b/tests/general/unit_test_pred_engine_trait.cpp index 8ea766b7..eb9defcb 100644 --- a/tests/general/unit_test_pred_engine_trait.cpp +++ b/tests/general/unit_test_pred_engine_trait.cpp @@ -53,6 +53,47 @@ TEST(PredEngineTrait, CalcViewpointFromAngles) EXPECT_NEAR(vp.z, 10.f, 1e-6f); } +TEST(PredEngineTrait, PredictProjectilePositionWithLaunchOffset) +{ + projectile_prediction::Projectile p; + p.m_origin = {0.f, 0.f, 0.f}; + p.m_launch_offset = {5.f, 3.f, -2.f}; + p.m_launch_speed = 10.f; + p.m_gravity_scale = 1.f; + + // At time=0, projectile should be at launch_pos = origin + offset + const auto pos_t0 = PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 0.f, 9.81f); + EXPECT_NEAR(pos_t0.x, 5.f, 1e-4f); + EXPECT_NEAR(pos_t0.y, 3.f, 1e-4f); + EXPECT_NEAR(pos_t0.z, -2.f, 1e-4f); + + // At time=1 with zero pitch/yaw, should travel along X from the offset position + const auto pos_t1 = PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f); + EXPECT_NEAR(pos_t1.x, 5.f + 10.f, 1e-3f); + EXPECT_NEAR(pos_t1.y, 3.f, 1e-3f); + EXPECT_NEAR(pos_t1.z, -2.f - 9.81f * 0.5f, 1e-3f); +} + +TEST(PredEngineTrait, ZeroLaunchOffsetMatchesOriginalBehavior) +{ + projectile_prediction::Projectile p; + p.m_origin = {10.f, 20.f, 30.f}; + p.m_launch_offset = {0.f, 0.f, 0.f}; + p.m_launch_speed = 15.f; + p.m_gravity_scale = 0.5f; + + projectile_prediction::Projectile p_no_offset; + p_no_offset.m_origin = {10.f, 20.f, 30.f}; + p_no_offset.m_launch_speed = 15.f; + p_no_offset.m_gravity_scale = 0.5f; + + const auto pos1 = PredEngineTrait::predict_projectile_position(p, 30.f, 45.f, 2.f, 9.81f); + const auto pos2 = PredEngineTrait::predict_projectile_position(p_no_offset, 30.f, 45.f, 2.f, 9.81f); + EXPECT_NEAR(pos1.x, pos2.x, 1e-6f); + EXPECT_NEAR(pos1.y, pos2.y, 1e-6f); + EXPECT_NEAR(pos1.z, pos2.z, 1e-6f); +} + TEST(PredEngineTrait, DirectAngles) { constexpr Vector3 origin{0.f, 0.f, 0.f}; diff --git a/tests/general/unit_test_prediction.cpp b/tests/general/unit_test_prediction.cpp index a0c22d5b..e3153edb 100644 --- a/tests/general/unit_test_prediction.cpp +++ b/tests/general/unit_test_prediction.cpp @@ -16,3 +16,107 @@ TEST(UnitTestPrediction, PredictionTest) EXPECT_NEAR(-42.547142, pitch.as_degrees(), 0.01f); EXPECT_NEAR(-1.181189, yaw.as_degrees(), 0.01f); } + +// Helper: verify aim_angles match angles derived from aim_point via CameraTrait +static void expect_angles_match_aim_point(const omath::projectile_prediction::Projectile& proj, + const omath::projectile_prediction::Target& target, + float gravity, float step, float max_time, float tolerance, + float angle_eps = 0.01f) +{ + const omath::projectile_prediction::ProjPredEngineLegacy engine(gravity, step, max_time, tolerance); + + const auto aim_point = engine.maybe_calculate_aim_point(proj, target); + const auto aim_angles = engine.maybe_calculate_aim_angles(proj, target); + + ASSERT_TRUE(aim_point.has_value()) << "aim_point should have a solution"; + ASSERT_TRUE(aim_angles.has_value()) << "aim_angles should have a solution"; + + // Source engine CameraTrait: pitch = -asin(dir.z), yaw = atan2(dir.y, dir.x) + // PredEngineTrait: pitch = asin(delta.z / dist), yaw = atan2(delta.y, delta.x) + // So aim_angles.pitch == -camera_pitch, aim_angles.yaw == camera_yaw + const auto [cam_pitch, cam_yaw, cam_roll] = + omath::source_engine::CameraTrait::calc_look_at_angle(proj.m_origin, aim_point.value()); + + EXPECT_NEAR(aim_angles->pitch, -cam_pitch.as_degrees(), angle_eps) + << "pitch from aim_angles must match pitch derived from aim_point"; + EXPECT_NEAR(aim_angles->yaw, cam_yaw.as_degrees(), angle_eps) + << "yaw from aim_angles must match yaw derived from aim_point"; +} + +TEST(UnitTestPrediction, AimAnglesMatchAimPoint_StaticTarget) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {100, 0, 90}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + constexpr omath::projectile_prediction::Projectile proj = { + .m_origin = {3, 2, 1}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; + + expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f); +} + +TEST(UnitTestPrediction, AimAnglesMatchAimPoint_MovingTarget) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {500, 100, 0}, .m_velocity = {-50, 20, 0}, .m_is_airborne = false}; + constexpr omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_speed = 3000, .m_gravity_scale = 1.0}; + + expect_angles_match_aim_point(proj, target, 800, 1.f / 500.f, 30, 10.f); +} + +TEST(UnitTestPrediction, AimAnglesMatchAimPoint_AirborneTarget) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {200, 50, 300}, .m_velocity = {10, -5, -20}, .m_is_airborne = true}; + constexpr omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_speed = 4000, .m_gravity_scale = 0.5}; + + expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 10.f); +} + +TEST(UnitTestPrediction, AimAnglesMatchAimPoint_HighArc) +{ + // Target nearly directly above — high pitch angle + constexpr omath::projectile_prediction::Target target{ + .m_origin = {10, 0, 500}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + constexpr omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.3}; + + expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f); +} + +TEST(UnitTestPrediction, AimAnglesMatchAimPoint_NegativeYaw) +{ + // Target behind and to the left — negative yaw quadrant + constexpr omath::projectile_prediction::Target target{ + .m_origin = {-200, -150, 10}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + constexpr omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; + + expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f); +} + +TEST(UnitTestPrediction, AimAnglesMatchAimPoint_WithLaunchOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {200, 0, 50}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + const omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_offset = {5, 0, -3}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; + + expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f); +} + +TEST(UnitTestPrediction, AimAnglesReturnsNulloptWhenNoSolution) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {100000, 0, 0}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + constexpr omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_speed = 1, .m_gravity_scale = 1}; + + const omath::projectile_prediction::ProjPredEngineLegacy engine(9.81f, 0.1f, 2.f, 5.f); + + const auto aim_point = engine.maybe_calculate_aim_point(proj, target); + const auto aim_angles = engine.maybe_calculate_aim_angles(proj, target); + + EXPECT_FALSE(aim_point.has_value()); + EXPECT_FALSE(aim_angles.has_value()); +} diff --git a/tests/general/unit_test_proj_pred_engine_legacy_more.cpp b/tests/general/unit_test_proj_pred_engine_legacy_more.cpp index 26080b42..73011caa 100644 --- a/tests/general/unit_test_proj_pred_engine_legacy_more.cpp +++ b/tests/general/unit_test_proj_pred_engine_legacy_more.cpp @@ -46,6 +46,22 @@ TEST(ProjPredLegacyMore, ZeroGravityUsesDirectPitchAndReturnsViewpoint) EXPECT_NEAR(v.z, 3.f, 1e-6f); } +TEST(ProjPredLegacyMore, ZeroGravityAimAnglesReturnsPitchAndYaw) +{ + constexpr Projectile proj{ .m_origin = {0.f, 0.f, 0.f}, .m_launch_speed = 10.f, .m_gravity_scale = 0.f }; + constexpr Target target{ .m_origin = {100.f, 0.f, 0.f}, .m_velocity = {0.f,0.f,0.f}, .m_is_airborne = false }; + + using Engine = omath::projectile_prediction::ProjPredEngineLegacy; + const Engine engine(9.8f, 0.1f, 5.f, 1e-3f); + + const auto res = engine.maybe_calculate_aim_angles(proj, target); + ASSERT_TRUE(res.has_value()); + // FakeEngineZeroGravity::calc_direct_pitch_angle returns 12.5f + EXPECT_NEAR(res->pitch, 12.5f, 1e-6f); + // FakeEngineZeroGravity::calc_direct_yaw_angle returns 0.f + EXPECT_NEAR(res->yaw, 0.f, 1e-6f); +} + // Fake trait producing no valid launch angle (root < 0) struct FakeEngineNoSolution { @@ -69,6 +85,9 @@ TEST(ProjPredLegacyMore, NoSolutionRootReturnsNullopt) const auto res = engine.maybe_calculate_aim_point(proj, target); EXPECT_FALSE(res.has_value()); + + const auto angles_res = engine.maybe_calculate_aim_angles(proj, target); + EXPECT_FALSE(angles_res.has_value()); } // Fake trait where an angle exists but the projectile does not reach target (miss) From aa08c7cb653f4a84a6368b9f92c89388ea959ab3 Mon Sep 17 00:00:00 2001 From: orange Date: Tue, 17 Mar 2026 20:43:26 +0300 Subject: [PATCH 2/3] improved projectile prediction --- .../proj_pred_engine_legacy.hpp | 10 +- tests/general/unit_test_prediction.cpp | 173 ++++++++++++++++++ 2 files changed, 179 insertions(+), 4 deletions(-) diff --git a/include/omath/projectile_prediction/proj_pred_engine_legacy.hpp b/include/omath/projectile_prediction/proj_pred_engine_legacy.hpp index 3740248d..eb2b9833 100644 --- a/include/omath/projectile_prediction/proj_pred_engine_legacy.hpp +++ b/include/omath/projectile_prediction/proj_pred_engine_legacy.hpp @@ -71,7 +71,7 @@ namespace omath::projectile_prediction if (!solution) return std::nullopt; - const auto yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin, solution->predicted_target_position); + const auto yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin + projectile.m_launch_offset, solution->predicted_target_position); return AimAngles{solution->pitch, yaw}; } @@ -129,10 +129,12 @@ namespace omath::projectile_prediction { const auto bullet_gravity = m_gravity_constant * projectile.m_gravity_scale; + const auto launch_origin = projectile.m_origin + projectile.m_launch_offset; + if (bullet_gravity == 0.f) - return EngineTrait::calc_direct_pitch_angle(projectile.m_origin, target_position); + return EngineTrait::calc_direct_pitch_angle(launch_origin, target_position); - const auto delta = target_position - projectile.m_origin; + const auto delta = target_position - launch_origin; const auto distance2d = EngineTrait::calc_vector_2d_distance(delta); const auto distance2d_sqr = distance2d * distance2d; @@ -155,7 +157,7 @@ namespace omath::projectile_prediction bool is_projectile_reached_target(const Vector3& target_position, const Projectile& projectile, const float pitch, const float time) const noexcept { - const auto yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin, target_position); + const auto yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin + projectile.m_launch_offset, target_position); const auto projectile_position = EngineTrait::predict_projectile_position(projectile, pitch, yaw, time, m_gravity_constant); diff --git a/tests/general/unit_test_prediction.cpp b/tests/general/unit_test_prediction.cpp index e3153edb..dc66d416 100644 --- a/tests/general/unit_test_prediction.cpp +++ b/tests/general/unit_test_prediction.cpp @@ -105,6 +105,179 @@ TEST(UnitTestPrediction, AimAnglesMatchAimPoint_WithLaunchOffset) expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f); } +// Helper: simulate projectile flight using aim_angles and verify it reaches the target. +// Steps the projectile forward in small increments, simultaneously predicts target position, +// and checks that the minimum distance is within hit_tolerance. +static void expect_projectile_hits_target(const omath::projectile_prediction::Projectile& proj, + const omath::projectile_prediction::Target& target, + float gravity, float engine_step, float max_time, float engine_tolerance, + float hit_tolerance, float sim_step = 1.f / 2000.f) +{ + using Trait = omath::source_engine::PredEngineTrait; + const omath::projectile_prediction::ProjPredEngineLegacy engine(gravity, engine_step, max_time, engine_tolerance); + + const auto aim_angles = engine.maybe_calculate_aim_angles(proj, target); + ASSERT_TRUE(aim_angles.has_value()) << "engine must find a solution"; + + float min_dist = std::numeric_limits::max(); + float best_time = 0.f; + + for (float t = 0.f; t <= max_time; t += sim_step) + { + const auto proj_pos = Trait::predict_projectile_position(proj, aim_angles->pitch, aim_angles->yaw, t, gravity); + const auto tgt_pos = Trait::predict_target_position(target, t, gravity); + const float dist = proj_pos.distance_to(tgt_pos); + + if (dist < min_dist) + { + min_dist = dist; + best_time = t; + } + + // Early exit once distance starts increasing significantly after approaching + if (dist > min_dist + hit_tolerance * 10.f && min_dist < hit_tolerance * 100.f) + break; + } + + EXPECT_LE(min_dist, hit_tolerance) + << "Projectile must reach target. Closest approach: " << min_dist + << " at t=" << best_time; +} + +// ── Simulation hit tests: no launch offset ───────────────────────────────── + +TEST(ProjectileSimulation, HitsStaticTarget_NoOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {100, 0, 90}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + constexpr omath::projectile_prediction::Projectile proj = { + .m_origin = {3, 2, 1}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); +} + +TEST(ProjectileSimulation, HitsMovingTarget_NoOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {500, 100, 0}, .m_velocity = {-50, 20, 0}, .m_is_airborne = false}; + constexpr omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_speed = 3000, .m_gravity_scale = 1.0}; + + expect_projectile_hits_target(proj, target, 800, 1.f / 500.f, 30, 10.f, 15.f); +} + +TEST(ProjectileSimulation, HitsAirborneTarget_NoOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {200, 50, 300}, .m_velocity = {10, -5, -20}, .m_is_airborne = true}; + constexpr omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_speed = 4000, .m_gravity_scale = 0.5}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 10.f, 15.f); +} + +TEST(ProjectileSimulation, HitsHighTarget_NoOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {10, 0, 500}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + constexpr omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.3}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); +} + +TEST(ProjectileSimulation, HitsNegativeYawTarget_NoOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {-200, -150, 10}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + constexpr omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); +} + +// ── Simulation hit tests: with launch offset ──────────────────────────────── + +TEST(ProjectileSimulation, HitsStaticTarget_SmallOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {200, 0, 50}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + const omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_offset = {5, 0, -3}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); +} + +TEST(ProjectileSimulation, HitsStaticTarget_LargeXOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {300, 100, 0}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + const omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_offset = {20, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); +} + +TEST(ProjectileSimulation, HitsStaticTarget_LargeYOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {150, -200, 30}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + const omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_offset = {0, 15, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); +} + +TEST(ProjectileSimulation, HitsStaticTarget_LargeZOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {100, 0, 200}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + const omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_offset = {0, 0, -10}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); +} + +TEST(ProjectileSimulation, HitsStaticTarget_AllAxesOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {250, 80, 60}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + const omath::projectile_prediction::Projectile proj = { + .m_origin = {10, 5, 20}, .m_launch_offset = {8, -4, -6}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); +} + +TEST(ProjectileSimulation, HitsMovingTarget_WithOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {400, 0, 50}, .m_velocity = {-30, 10, 5}, .m_is_airborne = false}; + const omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_offset = {10, -5, 2}, .m_launch_speed = 3000, .m_gravity_scale = 0.8}; + + expect_projectile_hits_target(proj, target, 800, 1.f / 500.f, 30, 10.f, 15.f); +} + +TEST(ProjectileSimulation, HitsAirborneTarget_WithOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {150, 80, 250}, .m_velocity = {5, -10, -30}, .m_is_airborne = true}; + const omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 50}, .m_launch_offset = {3, 7, -5}, .m_launch_speed = 4000, .m_gravity_scale = 0.5}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 10.f, 15.f); +} + +TEST(ProjectileSimulation, HitsNegativeYawTarget_WithOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {-200, -150, 10}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + const omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_offset = {-5, 3, 2}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); +} + TEST(UnitTestPrediction, AimAnglesReturnsNulloptWhenNoSolution) { constexpr omath::projectile_prediction::Target target{ From 89bd8791871f3f22dae4e704a0d0abfedb32f6a4 Mon Sep 17 00:00:00 2001 From: orange Date: Tue, 17 Mar 2026 21:15:39 +0300 Subject: [PATCH 3/3] added tolerance depending on arch --- tests/engines/unit_test_traits_engines.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/engines/unit_test_traits_engines.cpp b/tests/engines/unit_test_traits_engines.cpp index 7f5fd793..35d5b6fe 100644 --- a/tests/engines/unit_test_traits_engines.cpp +++ b/tests/engines/unit_test_traits_engines.cpp @@ -73,9 +73,14 @@ static void verify_zero_offset_matches_default() const auto pos1 = Trait::predict_projectile_position(p, 15.f, 30.f, 1.f, 9.81f); const auto pos2 = Trait::predict_projectile_position(p2, 15.f, 30.f, 1.f, 9.81f); - EXPECT_NEAR(pos1.x, pos2.x, 1e-6f); - EXPECT_NEAR(pos1.y, pos2.y, 1e-6f); - EXPECT_NEAR(pos1.z, pos2.z, 1e-6f); +#if defined(__x86_64__) || defined(_M_X64) || defined(__aarch64__) || defined(_M_ARM64) + constexpr float tol = 1e-6f; +#else + constexpr float tol = 1e-4f; +#endif + EXPECT_NEAR(pos1.x, pos2.x, tol); + EXPECT_NEAR(pos1.y, pos2.y, tol); + EXPECT_NEAR(pos1.z, pos2.z, tol); } TEST(LaunchOffsetTests, Source_OffsetAtTimeZero)