fixed projectile prediction for double

This commit is contained in:
2026-04-25 21:05:00 +03:00
parent 4c65781c6f
commit 9234704010
17 changed files with 260 additions and 236 deletions

View File

@@ -12,7 +12,7 @@ namespace omath::cry_engine
class PredEngineTrait final class PredEngineTrait final
{ {
public: public:
constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile& projectile, constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile<float>& projectile,
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
@@ -26,7 +26,7 @@ namespace omath::cry_engine
return current_pos; return current_pos;
} }
[[nodiscard]] [[nodiscard]]
static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target& target, static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target<float>& target,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto predicted = target.m_origin + target.m_velocity * time; auto predicted = target.m_origin + target.m_velocity * time;
@@ -49,7 +49,7 @@ namespace omath::cry_engine
} }
[[nodiscard]] [[nodiscard]]
static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile<float>& projectile,
Vector3<float> predicted_target_position, Vector3<float> predicted_target_position,
const std::optional<float> projectile_pitch) noexcept const std::optional<float> projectile_pitch) noexcept
{ {

View File

@@ -12,7 +12,7 @@ namespace omath::frostbite_engine
class PredEngineTrait final class PredEngineTrait final
{ {
public: public:
constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile& projectile, constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile<float>& projectile,
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
@@ -26,7 +26,7 @@ namespace omath::frostbite_engine
return current_pos; return current_pos;
} }
[[nodiscard]] [[nodiscard]]
static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target& target, static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target<float>& target,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto predicted = target.m_origin + target.m_velocity * time; auto predicted = target.m_origin + target.m_velocity * time;
@@ -49,7 +49,7 @@ namespace omath::frostbite_engine
} }
[[nodiscard]] [[nodiscard]]
static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile<float>& projectile,
Vector3<float> predicted_target_position, Vector3<float> predicted_target_position,
const std::optional<float> projectile_pitch) noexcept const std::optional<float> projectile_pitch) noexcept
{ {

View File

@@ -13,7 +13,7 @@ namespace omath::iw_engine
class PredEngineTrait final class PredEngineTrait final
{ {
public: public:
constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile& projectile, constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile<float>& projectile,
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
@@ -27,7 +27,7 @@ namespace omath::iw_engine
return current_pos; return current_pos;
} }
[[nodiscard]] [[nodiscard]]
static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target& target, static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target<float>& target,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto predicted = target.m_origin + target.m_velocity * time; auto predicted = target.m_origin + target.m_velocity * time;
@@ -50,7 +50,7 @@ namespace omath::iw_engine
} }
[[nodiscard]] [[nodiscard]]
static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile<float>& projectile,
Vector3<float> predicted_target_position, Vector3<float> predicted_target_position,
const std::optional<float> projectile_pitch) noexcept const std::optional<float> projectile_pitch) noexcept
{ {

View File

@@ -12,7 +12,7 @@ namespace omath::opengl_engine
class PredEngineTrait final class PredEngineTrait final
{ {
public: public:
constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile& projectile, constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile<float>& projectile,
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
@@ -26,7 +26,7 @@ namespace omath::opengl_engine
return current_pos; return current_pos;
} }
[[nodiscard]] [[nodiscard]]
static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target& target, static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target<float>& target,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto predicted = target.m_origin + target.m_velocity * time; auto predicted = target.m_origin + target.m_velocity * time;
@@ -49,7 +49,7 @@ namespace omath::opengl_engine
} }
[[nodiscard]] [[nodiscard]]
static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile<float>& projectile,
Vector3<float> predicted_target_position, Vector3<float> predicted_target_position,
const std::optional<float> projectile_pitch) noexcept const std::optional<float> projectile_pitch) noexcept
{ {

View File

@@ -13,7 +13,7 @@ namespace omath::source_engine
class PredEngineTrait final class PredEngineTrait final
{ {
public: public:
constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile& projectile, constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile<float>& projectile,
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
@@ -27,7 +27,7 @@ namespace omath::source_engine
return current_pos; return current_pos;
} }
[[nodiscard]] [[nodiscard]]
static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target& target, static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target<float>& target,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto predicted = target.m_origin + target.m_velocity * time; auto predicted = target.m_origin + target.m_velocity * time;
@@ -50,7 +50,7 @@ namespace omath::source_engine
} }
[[nodiscard]] [[nodiscard]]
static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile<float>& projectile,
Vector3<float> predicted_target_position, Vector3<float> predicted_target_position,
const std::optional<float> projectile_pitch) noexcept const std::optional<float> projectile_pitch) noexcept
{ {

View File

@@ -12,7 +12,7 @@ namespace omath::unity_engine
class PredEngineTrait final class PredEngineTrait final
{ {
public: public:
constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile& projectile, constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile<float>& projectile,
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
@@ -26,7 +26,7 @@ namespace omath::unity_engine
return current_pos; return current_pos;
} }
[[nodiscard]] [[nodiscard]]
static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target& target, static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target<float>& target,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto predicted = target.m_origin + target.m_velocity * time; auto predicted = target.m_origin + target.m_velocity * time;
@@ -49,7 +49,7 @@ namespace omath::unity_engine
} }
[[nodiscard]] [[nodiscard]]
static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile<float>& projectile,
Vector3<float> predicted_target_position, Vector3<float> predicted_target_position,
const std::optional<float> projectile_pitch) noexcept const std::optional<float> projectile_pitch) noexcept
{ {

View File

@@ -12,69 +12,72 @@ namespace omath::unreal_engine
class PredEngineTrait final class PredEngineTrait final
{ {
public: public:
constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile& projectile, static Vector3<double> predict_projectile_position(const projectile_prediction::Projectile<double>& projectile,
const float pitch, const float yaw, const double pitch, const double yaw,
const float time, const float gravity) noexcept const double time, const double gravity) noexcept
{ {
const auto launch_pos = projectile.m_origin + projectile.m_launch_offset; const auto launch_pos = projectile.m_origin + projectile.m_launch_offset;
const auto fwd_d = forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), const auto fwd_d = forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw),
RollAngle::from_degrees(0)}); RollAngle::from_degrees(0)});
auto current_pos = launch_pos auto current_pos = launch_pos
+ Vector3<float>{static_cast<float>(fwd_d.x), static_cast<float>(fwd_d.y), + Vector3<double>{fwd_d.x, fwd_d.y, fwd_d.z}
static_cast<float>(fwd_d.z)}
* projectile.m_launch_speed * time; * projectile.m_launch_speed * time;
current_pos.y -= (gravity * projectile.m_gravity_scale) * (time * time) * 0.5f; current_pos.y -= (gravity * projectile.m_gravity_scale) * (time * time) * 0.5;
return current_pos; return current_pos;
} }
[[nodiscard]] [[nodiscard]]
static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target& target, static Vector3<double> predict_target_position(const projectile_prediction::Target<double>& target,
const float time, const float gravity) noexcept const double time, const double gravity) noexcept
{ {
auto predicted = target.m_origin + target.m_velocity * time; auto predicted = target.m_origin + target.m_velocity * time;
if (target.m_is_airborne) if (target.m_is_airborne)
predicted.y -= gravity * (time * time) * 0.5f; predicted.y -= gravity * (time * time) * 0.5;
return predicted; return predicted;
} }
[[nodiscard]] [[nodiscard]]
static float calc_vector_2d_distance(const Vector3<float>& delta) noexcept static double calc_vector_2d_distance(const Vector3<double>& delta) noexcept
{ {
return std::sqrt(delta.x * delta.x + delta.z * delta.z); return std::sqrt(delta.x * delta.x + delta.z * delta.z);
} }
[[nodiscard]] [[nodiscard]]
constexpr static float get_vector_height_coordinate(const Vector3<float>& vec) noexcept static double get_vector_height_coordinate(const Vector3<double>& vec) noexcept
{ {
return vec.y; return vec.y;
} }
[[nodiscard]] [[nodiscard]]
static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, static Vector3<double> calc_viewpoint_from_angles(const projectile_prediction::Projectile<double>& projectile,
Vector3<float> predicted_target_position, Vector3<double> predicted_target_position,
const std::optional<float> projectile_pitch) noexcept const std::optional<double> projectile_pitch) noexcept
{ {
const auto delta2d = calc_vector_2d_distance(predicted_target_position - projectile.m_origin); const auto delta2d = calc_vector_2d_distance(predicted_target_position - projectile.m_origin);
const auto height = delta2d * std::tan(angles::degrees_to_radians(projectile_pitch.value())); const auto height = delta2d * std::tan(angles::degrees_to_radians(projectile_pitch.value()));
return {predicted_target_position.x, predicted_target_position.y, projectile.m_origin.z + height}; return {predicted_target_position.x, predicted_target_position.y, projectile.m_origin.z + height};
} }
// Due to specification of maybe_calculate_projectile_launch_pitch_angle, pitch angle must be: // Due to specification of maybe_calculate_projectile_launch_pitch_angle, pitch angle must be:
// 89 look up, -89 look down // 89 look up, -89 look down
[[nodiscard]] [[nodiscard]]
static float calc_direct_pitch_angle(const Vector3<float>& origin, const Vector3<float>& view_to) noexcept static double calc_direct_pitch_angle(const Vector3<double>& origin, const Vector3<double>& view_to) noexcept
{ {
const auto direction = (view_to - origin).normalized(); const auto direction = (view_to - origin).normalized();
return angles::radians_to_degrees(std::asin(direction.z)); return angles::radians_to_degrees(std::asin(direction.z));
} }
[[nodiscard]] [[nodiscard]]
static float calc_direct_yaw_angle(const Vector3<float>& origin, const Vector3<float>& view_to) noexcept static double calc_direct_yaw_angle(const Vector3<double>& origin, const Vector3<double>& view_to) noexcept
{ {
const auto direction = (view_to - origin).normalized(); const auto direction = (view_to - origin).normalized();
return angles::radians_to_degrees(std::atan2(direction.y, direction.x)); return angles::radians_to_degrees(std::atan2(direction.y, direction.x));
}; }
}; };
} // namespace omath::unreal_engine } // namespace omath::unreal_engine

View File

@@ -8,22 +8,24 @@
namespace omath::projectile_prediction namespace omath::projectile_prediction
{ {
template<class ArithmeticType = float>
struct AimAngles struct AimAngles
{ {
float pitch{}; ArithmeticType pitch{};
float yaw{}; ArithmeticType yaw{};
}; };
template<class ArithmeticType = float>
class ProjPredEngineInterface class ProjPredEngineInterface
{ {
public: public:
[[nodiscard]] [[nodiscard]]
virtual std::optional<Vector3<float>> maybe_calculate_aim_point(const Projectile& projectile, virtual std::optional<Vector3<ArithmeticType>> maybe_calculate_aim_point(
const Target& target) const = 0; const Projectile<ArithmeticType>& projectile, const Target<ArithmeticType>& target) const = 0;
[[nodiscard]] [[nodiscard]]
virtual std::optional<AimAngles> maybe_calculate_aim_angles(const Projectile& projectile, virtual std::optional<AimAngles<ArithmeticType>> maybe_calculate_aim_angles(
const Target& target) const = 0; const Projectile<ArithmeticType>& projectile, const Target<ArithmeticType>& target) const = 0;
virtual ~ProjPredEngineInterface() = default; virtual ~ProjPredEngineInterface() = default;
}; };

View File

@@ -6,14 +6,14 @@
namespace omath::projectile_prediction namespace omath::projectile_prediction
{ {
class ProjPredEngineAvx2 final : public ProjPredEngineInterface class ProjPredEngineAvx2 final : public ProjPredEngineInterface<float>
{ {
public: public:
[[nodiscard]] std::optional<Vector3<float>> [[nodiscard]] std::optional<Vector3<float>>
maybe_calculate_aim_point(const Projectile& projectile, const Target& target) const override; maybe_calculate_aim_point(const Projectile<float>& projectile, const Target<float>& target) const override;
[[nodiscard]] std::optional<AimAngles> [[nodiscard]] std::optional<AimAngles<float>>
maybe_calculate_aim_angles(const Projectile& projectile, const Target& target) const override; maybe_calculate_aim_angles(const Projectile<float>& projectile, const Target<float>& target) const override;
ProjPredEngineAvx2(float gravity_constant, float simulation_time_step, float maximum_simulation_time); ProjPredEngineAvx2(float gravity_constant, float simulation_time_step, float maximum_simulation_time);
~ProjPredEngineAvx2() override = default; ~ProjPredEngineAvx2() override = default;
@@ -21,7 +21,7 @@ namespace omath::projectile_prediction
private: private:
[[nodiscard]] static std::optional<float> calculate_pitch(const Vector3<float>& proj_origin, [[nodiscard]] static std::optional<float> calculate_pitch(const Vector3<float>& proj_origin,
const Vector3<float>& target_pos, const Vector3<float>& target_pos,
float bullet_gravity, float v0, float time) ; float bullet_gravity, float v0, float time);
// We use [[maybe_unused]] here since AVX2 is not available for ARM and ARM64 CPU // We use [[maybe_unused]] here since AVX2 is not available for ARM and ARM64 CPU
[[maybe_unused]] const float m_gravity_constant; [[maybe_unused]] const float m_gravity_constant;

View File

@@ -13,24 +13,23 @@
namespace omath::projectile_prediction namespace omath::projectile_prediction
{ {
template<class T> template<class T, class ArithmeticType>
concept PredEngineConcept = concept PredEngineConcept =
requires(const Projectile& projectile, const Target& target, const Vector3<float>& vec_a, requires(const Projectile<ArithmeticType>& projectile, const Target<ArithmeticType>& target,
const Vector3<float>& vec_b, const Vector3<ArithmeticType>& vec_a, const Vector3<ArithmeticType>& vec_b,
Vector3<float> v3, // by-value for calc_viewpoint_from_angles Vector3<ArithmeticType> v3,
float pitch, float yaw, float time, float gravity, std::optional<float> maybe_pitch) { ArithmeticType pitch, ArithmeticType yaw, ArithmeticType time, ArithmeticType gravity,
// Presence + return types std::optional<ArithmeticType> maybe_pitch) {
{ {
T::predict_projectile_position(projectile, pitch, yaw, time, gravity) T::predict_projectile_position(projectile, pitch, yaw, time, gravity)
} -> std::same_as<Vector3<float>>; } -> std::same_as<Vector3<ArithmeticType>>;
{ T::predict_target_position(target, time, gravity) } -> std::same_as<Vector3<float>>; { T::predict_target_position(target, time, gravity) } -> std::same_as<Vector3<ArithmeticType>>;
{ T::calc_vector_2d_distance(vec_a) } -> std::same_as<float>; { T::calc_vector_2d_distance(vec_a) } -> std::same_as<ArithmeticType>;
{ T::get_vector_height_coordinate(vec_b) } -> std::same_as<float>; { T::get_vector_height_coordinate(vec_b) } -> std::same_as<ArithmeticType>;
{ T::calc_viewpoint_from_angles(projectile, v3, maybe_pitch) } -> std::same_as<Vector3<float>>; { T::calc_viewpoint_from_angles(projectile, v3, maybe_pitch) } -> std::same_as<Vector3<ArithmeticType>>;
{ T::calc_direct_pitch_angle(vec_a, vec_b) } -> std::same_as<float>; { T::calc_direct_pitch_angle(vec_a, vec_b) } -> std::same_as<ArithmeticType>;
{ T::calc_direct_yaw_angle(vec_a, vec_b) } -> std::same_as<float>; { T::calc_direct_yaw_angle(vec_a, vec_b) } -> std::same_as<ArithmeticType>;
// Enforce noexcept as in PredEngineTrait
requires noexcept(T::predict_projectile_position(projectile, pitch, yaw, time, gravity)); requires noexcept(T::predict_projectile_position(projectile, pitch, yaw, time, gravity));
requires noexcept(T::predict_target_position(target, time, gravity)); requires noexcept(T::predict_target_position(target, time, gravity));
requires noexcept(T::calc_vector_2d_distance(vec_a)); requires noexcept(T::calc_vector_2d_distance(vec_a));
@@ -39,21 +38,24 @@ namespace omath::projectile_prediction
requires noexcept(T::calc_direct_pitch_angle(vec_a, vec_b)); requires noexcept(T::calc_direct_pitch_angle(vec_a, vec_b));
requires noexcept(T::calc_direct_yaw_angle(vec_a, vec_b)); requires noexcept(T::calc_direct_yaw_angle(vec_a, vec_b));
}; };
template<class EngineTrait = source_engine::PredEngineTrait>
requires PredEngineConcept<EngineTrait> template<class EngineTrait = source_engine::PredEngineTrait, class ArithmeticType = float>
class ProjPredEngineLegacy final : public ProjPredEngineInterface requires PredEngineConcept<EngineTrait, ArithmeticType>
class ProjPredEngineLegacy final : public ProjPredEngineInterface<ArithmeticType>
{ {
public: public:
explicit ProjPredEngineLegacy(const float gravity_constant, const float simulation_time_step, explicit ProjPredEngineLegacy(const ArithmeticType gravity_constant,
const float maximum_simulation_time, const float distance_tolerance) const ArithmeticType simulation_time_step,
const ArithmeticType maximum_simulation_time,
const ArithmeticType distance_tolerance)
: m_gravity_constant(gravity_constant), m_simulation_time_step(simulation_time_step), : m_gravity_constant(gravity_constant), m_simulation_time_step(simulation_time_step),
m_maximum_simulation_time(maximum_simulation_time), m_distance_tolerance(distance_tolerance) m_maximum_simulation_time(maximum_simulation_time), m_distance_tolerance(distance_tolerance)
{ {
} }
[[nodiscard]] [[nodiscard]]
std::optional<Vector3<float>> maybe_calculate_aim_point(const Projectile& projectile, std::optional<Vector3<ArithmeticType>> maybe_calculate_aim_point(
const Target& target) const override const Projectile<ArithmeticType>& projectile, const Target<ArithmeticType>& target) const override
{ {
const auto solution = find_solution(projectile, target); const auto solution = find_solution(projectile, target);
if (!solution) if (!solution)
@@ -64,28 +66,31 @@ namespace omath::projectile_prediction
} }
[[nodiscard]] [[nodiscard]]
std::optional<AimAngles> maybe_calculate_aim_angles(const Projectile& projectile, std::optional<AimAngles<ArithmeticType>> maybe_calculate_aim_angles(
const Target& target) const override const Projectile<ArithmeticType>& projectile, const Target<ArithmeticType>& target) const override
{ {
const auto solution = find_solution(projectile, target); const auto solution = find_solution(projectile, target);
if (!solution) if (!solution)
return std::nullopt; return std::nullopt;
const auto yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin + projectile.m_launch_offset, solution->predicted_target_position); const auto yaw = EngineTrait::calc_direct_yaw_angle(
return AimAngles{solution->pitch, yaw}; projectile.m_origin + projectile.m_launch_offset, solution->predicted_target_position);
return AimAngles<ArithmeticType>{solution->pitch, yaw};
} }
private: private:
struct Solution struct Solution
{ {
Vector3<float> predicted_target_position; Vector3<ArithmeticType> predicted_target_position;
float pitch; ArithmeticType pitch;
}; };
[[nodiscard]] [[nodiscard]]
std::optional<Solution> find_solution(const Projectile& projectile, const Target& target) const std::optional<Solution> find_solution(const Projectile<ArithmeticType>& projectile,
const Target<ArithmeticType>& target) const
{ {
for (float time = 0.f; time < m_maximum_simulation_time; time += m_simulation_time_step) for (ArithmeticType time = ArithmeticType{0}; time < m_maximum_simulation_time;
time += m_simulation_time_step)
{ {
const auto predicted_target_position = const auto predicted_target_position =
EngineTrait::predict_target_position(target, time, m_gravity_constant); EngineTrait::predict_target_position(target, time, m_gravity_constant);
@@ -105,10 +110,10 @@ namespace omath::projectile_prediction
return std::nullopt; return std::nullopt;
} }
const float m_gravity_constant; const ArithmeticType m_gravity_constant;
const float m_simulation_time_step; const ArithmeticType m_simulation_time_step;
const float m_maximum_simulation_time; const ArithmeticType m_maximum_simulation_time;
const float m_distance_tolerance; const ArithmeticType m_distance_tolerance;
// Realization of this formula: // Realization of this formula:
// https://stackoverflow.com/questions/54917375/how-to-calculate-the-angle-to-shoot-a-bullet-in-order-to-hit-a-moving-target // https://stackoverflow.com/questions/54917375/how-to-calculate-the-angle-to-shoot-a-bullet-in-order-to-hit-a-moving-target
@@ -123,15 +128,15 @@ namespace omath::projectile_prediction
\] \]
*/ */
[[nodiscard]] [[nodiscard]]
std::optional<float> std::optional<ArithmeticType>
maybe_calculate_projectile_launch_pitch_angle(const Projectile& projectile, maybe_calculate_projectile_launch_pitch_angle(const Projectile<ArithmeticType>& projectile,
const Vector3<float>& target_position) const noexcept const Vector3<ArithmeticType>& target_position) const noexcept
{ {
const auto bullet_gravity = m_gravity_constant * projectile.m_gravity_scale; const auto bullet_gravity = m_gravity_constant * projectile.m_gravity_scale;
const auto launch_origin = projectile.m_origin + projectile.m_launch_offset; const auto launch_origin = projectile.m_origin + projectile.m_launch_offset;
if (bullet_gravity == 0.f) if (bullet_gravity == ArithmeticType{0})
return EngineTrait::calc_direct_pitch_angle(launch_origin, target_position); return EngineTrait::calc_direct_pitch_angle(launch_origin, target_position);
const auto delta = target_position - launch_origin; const auto delta = target_position - launch_origin;
@@ -140,24 +145,28 @@ namespace omath::projectile_prediction
const auto distance2d_sqr = distance2d * distance2d; const auto distance2d_sqr = distance2d * distance2d;
const auto launch_speed_sqr = projectile.m_launch_speed * projectile.m_launch_speed; const auto launch_speed_sqr = projectile.m_launch_speed * projectile.m_launch_speed;
float root = launch_speed_sqr * launch_speed_sqr ArithmeticType root = launch_speed_sqr * launch_speed_sqr
- bullet_gravity - bullet_gravity
* (bullet_gravity * distance2d_sqr * (bullet_gravity * distance2d_sqr
+ 2.0f * EngineTrait::get_vector_height_coordinate(delta) * launch_speed_sqr); + ArithmeticType{2} * EngineTrait::get_vector_height_coordinate(delta)
* launch_speed_sqr);
if (root < 0.0f) [[unlikely]] if (root < ArithmeticType{0}) [[unlikely]]
return std::nullopt; return std::nullopt;
root = std::sqrt(root); root = std::sqrt(root);
const float angle = std::atan((launch_speed_sqr - root) / (bullet_gravity * distance2d)); const ArithmeticType angle = std::atan((launch_speed_sqr - root) / (bullet_gravity * distance2d));
return angles::radians_to_degrees(angle); return angles::radians_to_degrees(angle);
} }
[[nodiscard]] [[nodiscard]]
bool is_projectile_reached_target(const Vector3<float>& target_position, const Projectile& projectile, bool is_projectile_reached_target(const Vector3<ArithmeticType>& target_position,
const float pitch, const float time) const noexcept const Projectile<ArithmeticType>& projectile,
const ArithmeticType pitch, const ArithmeticType time) const noexcept
{ {
const auto yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin + projectile.m_launch_offset, target_position); const auto yaw = EngineTrait::calc_direct_yaw_angle(
projectile.m_origin + projectile.m_launch_offset, target_position);
const auto projectile_position = const auto projectile_position =
EngineTrait::predict_projectile_position(projectile, pitch, yaw, time, m_gravity_constant); EngineTrait::predict_projectile_position(projectile, pitch, yaw, time, m_gravity_constant);

View File

@@ -7,12 +7,13 @@
namespace omath::projectile_prediction namespace omath::projectile_prediction
{ {
template <class ArithmeticType = float>
class Projectile final class Projectile final
{ {
public: public:
Vector3<float> m_origin; Vector3<ArithmeticType> m_origin;
Vector3<float> m_launch_offset{0.f, 0.f, 0.f}; Vector3<ArithmeticType> m_launch_offset{};
float m_launch_speed{}; ArithmeticType m_launch_speed{};
float m_gravity_scale{}; ArithmeticType m_gravity_scale{};
}; };
} // namespace omath::projectile_prediction } // namespace omath::projectile_prediction

View File

@@ -7,11 +7,12 @@
namespace omath::projectile_prediction namespace omath::projectile_prediction
{ {
template <class ArithmeticType = float>
class Target final class Target final
{ {
public: public:
Vector3<float> m_origin; Vector3<ArithmeticType> m_origin;
Vector3<float> m_velocity; Vector3<ArithmeticType> m_velocity;
bool m_is_airborne{}; bool m_is_airborne{};
}; };
} // namespace omath::projectile_prediction } // namespace omath::projectile_prediction

View File

@@ -14,8 +14,8 @@
namespace omath::projectile_prediction namespace omath::projectile_prediction
{ {
std::optional<Vector3<float>> std::optional<Vector3<float>>
ProjPredEngineAvx2::maybe_calculate_aim_point([[maybe_unused]] const Projectile& projectile, ProjPredEngineAvx2::maybe_calculate_aim_point([[maybe_unused]] const Projectile<float>& projectile,
[[maybe_unused]] const Target& target) const [[maybe_unused]] const Target<float>& target) const
{ {
#if defined(OMATH_USE_AVX2) && defined(__i386__) && defined(__x86_64__) #if defined(OMATH_USE_AVX2) && defined(__i386__) && defined(__x86_64__)
const float bullet_gravity = m_gravity_constant * projectile.m_gravity_scale; const float bullet_gravity = m_gravity_constant * projectile.m_gravity_scale;
@@ -124,9 +124,9 @@ namespace omath::projectile_prediction
std::format("{} AVX2 feature is not enabled!", std::source_location::current().function_name())); std::format("{} AVX2 feature is not enabled!", std::source_location::current().function_name()));
#endif #endif
} }
std::optional<AimAngles> std::optional<AimAngles<float>>
ProjPredEngineAvx2::maybe_calculate_aim_angles([[maybe_unused]] const Projectile& projectile, ProjPredEngineAvx2::maybe_calculate_aim_angles([[maybe_unused]] const Projectile<float>& projectile,
[[maybe_unused]] const Target& target) const [[maybe_unused]] const Target<float>& target) const
{ {
#if defined(OMATH_USE_AVX2) && defined(__i386__) && defined(__x86_64__) #if defined(OMATH_USE_AVX2) && defined(__i386__) && defined(__x86_64__)
const float bullet_gravity = m_gravity_constant * projectile.m_gravity_scale; const float bullet_gravity = m_gravity_constant * projectile.m_gravity_scale;
@@ -201,7 +201,7 @@ namespace omath::projectile_prediction
const Vector3 delta = target_pos - projectile.m_origin; const Vector3 delta = target_pos - projectile.m_origin;
const float yaw = angles::radians_to_degrees(std::atan2(delta.y, delta.x)); const float yaw = angles::radians_to_degrees(std::atan2(delta.y, delta.x));
return AimAngles{*pitch, yaw}; return AimAngles<float>{*pitch, yaw};
} }
} }
} }

View File

@@ -44,46 +44,46 @@ static void expect_matrix_near(const MatT& a, const MatT& b, float eps = 1e-5f)
#include <omath/engines/cry_engine/traits/pred_engine_trait.hpp> #include <omath/engines/cry_engine/traits/pred_engine_trait.hpp>
// Helper: verify that zero offset matches default-initialized offset behavior // Helper: verify that zero offset matches default-initialized offset behavior
template<typename Trait> template<typename Trait, typename AT = float>
static void verify_launch_offset_at_time_zero(const Vector3<float>& origin, const Vector3<float>& offset) static void verify_launch_offset_at_time_zero(const Vector3<AT>& origin, const Vector3<AT>& offset)
{ {
projectile_prediction::Projectile p; projectile_prediction::Projectile<AT> p;
p.m_origin = origin; p.m_origin = origin;
p.m_launch_offset = offset; p.m_launch_offset = offset;
p.m_launch_speed = 100.f; p.m_launch_speed = static_cast<AT>(100);
p.m_gravity_scale = 1.f; p.m_gravity_scale = static_cast<AT>(1);
const auto pos = Trait::predict_projectile_position(p, 0.f, 0.f, 0.f, 9.81f); const auto pos = Trait::predict_projectile_position(p, AT{0}, AT{0}, AT{0}, static_cast<AT>(9.81));
const auto expected = origin + offset; const auto expected = origin + offset;
EXPECT_NEAR(pos.x, expected.x, 1e-4f); EXPECT_NEAR(static_cast<double>(pos.x), static_cast<double>(expected.x), 1e-4);
EXPECT_NEAR(pos.y, expected.y, 1e-4f); EXPECT_NEAR(static_cast<double>(pos.y), static_cast<double>(expected.y), 1e-4);
EXPECT_NEAR(pos.z, expected.z, 1e-4f); EXPECT_NEAR(static_cast<double>(pos.z), static_cast<double>(expected.z), 1e-4);
} }
template<typename Trait> template<typename Trait, typename AT = float>
static void verify_zero_offset_matches_default() static void verify_zero_offset_matches_default()
{ {
projectile_prediction::Projectile p; projectile_prediction::Projectile<AT> p;
p.m_origin = {10.f, 20.f, 30.f}; p.m_origin = {static_cast<AT>(10), static_cast<AT>(20), static_cast<AT>(30)};
p.m_launch_offset = {0.f, 0.f, 0.f}; p.m_launch_offset = {};
p.m_launch_speed = 50.f; p.m_launch_speed = static_cast<AT>(50);
p.m_gravity_scale = 1.f; p.m_gravity_scale = static_cast<AT>(1);
projectile_prediction::Projectile p2; projectile_prediction::Projectile<AT> p2;
p2.m_origin = {10.f, 20.f, 30.f}; p2.m_origin = {static_cast<AT>(10), static_cast<AT>(20), static_cast<AT>(30)};
p2.m_launch_speed = 50.f; p2.m_launch_speed = static_cast<AT>(50);
p2.m_gravity_scale = 1.f; p2.m_gravity_scale = static_cast<AT>(1);
const auto pos1 = Trait::predict_projectile_position(p, 15.f, 30.f, 1.f, 9.81f); const auto pos1 = Trait::predict_projectile_position(p, static_cast<AT>(15), static_cast<AT>(30), static_cast<AT>(1), static_cast<AT>(9.81));
const auto pos2 = Trait::predict_projectile_position(p2, 15.f, 30.f, 1.f, 9.81f); const auto pos2 = Trait::predict_projectile_position(p2, static_cast<AT>(15), static_cast<AT>(30), static_cast<AT>(1), static_cast<AT>(9.81));
#if defined(__x86_64__) || defined(_M_X64) || defined(__aarch64__) || defined(_M_ARM64) #if defined(__x86_64__) || defined(_M_X64) || defined(__aarch64__) || defined(_M_ARM64)
constexpr float tol = 1e-6f; constexpr double tol = 1e-6;
#else #else
constexpr float tol = 1e-4f; constexpr double tol = 1e-4;
#endif #endif
EXPECT_NEAR(pos1.x, pos2.x, tol); EXPECT_NEAR(static_cast<double>(pos1.x), static_cast<double>(pos2.x), tol);
EXPECT_NEAR(pos1.y, pos2.y, tol); EXPECT_NEAR(static_cast<double>(pos1.y), static_cast<double>(pos2.y), tol);
EXPECT_NEAR(pos1.z, pos2.z, tol); EXPECT_NEAR(static_cast<double>(pos1.z), static_cast<double>(pos2.z), tol);
} }
TEST(LaunchOffsetTests, Source_OffsetAtTimeZero) TEST(LaunchOffsetTests, Source_OffsetAtTimeZero)
@@ -128,11 +128,11 @@ TEST(LaunchOffsetTests, Unity_ZeroOffsetMatchesDefault)
} }
TEST(LaunchOffsetTests, Unreal_OffsetAtTimeZero) TEST(LaunchOffsetTests, Unreal_OffsetAtTimeZero)
{ {
verify_launch_offset_at_time_zero<unreal_engine::PredEngineTrait>({0, 0, 0}, {5, 3, -2}); verify_launch_offset_at_time_zero<unreal_engine::PredEngineTrait, double>({0, 0, 0}, {5, 3, -2});
} }
TEST(LaunchOffsetTests, Unreal_ZeroOffsetMatchesDefault) TEST(LaunchOffsetTests, Unreal_ZeroOffsetMatchesDefault)
{ {
verify_zero_offset_matches_default<unreal_engine::PredEngineTrait>(); verify_zero_offset_matches_default<unreal_engine::PredEngineTrait, double>();
} }
TEST(LaunchOffsetTests, CryEngine_OffsetAtTimeZero) TEST(LaunchOffsetTests, CryEngine_OffsetAtTimeZero)
{ {
@@ -401,38 +401,38 @@ TEST(TraitTests, Unreal_Pred_And_Mesh_And_Camera)
{ {
namespace e = omath::unreal_engine; namespace e = omath::unreal_engine;
projectile_prediction::Projectile p; projectile_prediction::Projectile<double> p;
p.m_origin = {0.f, 0.f, 0.f}; p.m_origin = {0.0, 0.0, 0.0};
p.m_launch_speed = 10.f; p.m_launch_speed = 10.0;
p.m_gravity_scale = 1.f; p.m_gravity_scale = 1.0;
const auto pos = e::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f); const auto pos = e::PredEngineTrait::predict_projectile_position(p, 0.0, 0.0, 1.0, 9.81);
EXPECT_NEAR(pos.x, 10.f, 1e-4f); EXPECT_NEAR(pos.x, 10.0, 1e-4);
EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f); EXPECT_NEAR(pos.y, -9.81 * 0.5, 1e-4);
projectile_prediction::Target t; projectile_prediction::Target<double> t;
t.m_origin = {0.f, 5.f, 0.f}; t.m_origin = {0.0, 5.0, 0.0};
t.m_velocity = {2.f, 0.f, 0.f}; t.m_velocity = {2.0, 0.0, 0.0};
t.m_is_airborne = true; t.m_is_airborne = true;
const auto pred = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); const auto pred = e::PredEngineTrait::predict_target_position(t, 2.0, 9.81);
EXPECT_NEAR(pred.x, 4.f, 1e-6f); EXPECT_NEAR(pred.x, 4.0, 1e-6);
EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f); EXPECT_NEAR(pred.y, 5.0 - 9.81 * (2.0 * 2.0) * 0.5, 1e-6);
EXPECT_NEAR(e::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f); EXPECT_NEAR(e::PredEngineTrait::calc_vector_2d_distance({3.0, 0.0, 4.0}), 5.0, 1e-6);
EXPECT_NEAR(e::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f); EXPECT_NEAR(e::PredEngineTrait::get_vector_height_coordinate({1.0, 2.5, 3.0}), 2.5, 1e-6);
std::optional<float> pitch = 45.f; std::optional<double> pitch = 45.0;
auto vp = e::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch); auto vp = e::PredEngineTrait::calc_viewpoint_from_angles(p, Vector3<double>{10.0, 0.0, 0.0}, pitch);
EXPECT_NEAR(vp.z, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f); EXPECT_NEAR(vp.z, 0.0 + 10.0 * std::tan(angles::degrees_to_radians(45.0)), 1e-6);
Vector3<float> origin{0.f, 0.f, 0.f}; Vector3<double> origin{0.0, 0.0, 0.0};
Vector3<float> view_to{1.f, 1.f, 1.f}; Vector3<double> view_to{1.0, 1.0, 1.0};
const auto pitch_calc = e::PredEngineTrait::calc_direct_pitch_angle(origin, view_to); const auto pitch_calc = e::PredEngineTrait::calc_direct_pitch_angle(origin, view_to);
const auto dir = (view_to - origin).normalized(); const auto dir = (view_to - origin).normalized();
EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.z)), 1e-3f); EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.z)), 1e-3);
const auto yaw_calc = e::PredEngineTrait::calc_direct_yaw_angle(origin, view_to); const auto yaw_calc = e::PredEngineTrait::calc_direct_yaw_angle(origin, view_to);
EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(dir.y, dir.x)), 1e-3f); EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(dir.y, dir.x)), 1e-3);
e::ViewAngles va; e::ViewAngles va;
expect_matrix_near(e::MeshTrait::rotation_matrix(va), e::rotation_matrix(va)); expect_matrix_near(e::MeshTrait::rotation_matrix(va), e::rotation_matrix(va));
@@ -448,8 +448,8 @@ TEST(TraitTests, Unreal_Pred_And_Mesh_And_Camera)
// non-airborne // non-airborne
t.m_is_airborne = false; t.m_is_airborne = false;
const auto pred_ground_unreal = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); const auto pred_ground_unreal = e::PredEngineTrait::predict_target_position(t, 2.0, 9.81);
EXPECT_NEAR(pred_ground_unreal.x, 4.f, 1e-6f); EXPECT_NEAR(pred_ground_unreal.x, 4.0, 1e-6);
} }
// ── NDC Depth Range tests for Source and CryEngine camera traits ──────────── // ── NDC Depth Range tests for Source and CryEngine camera traits ────────────

View File

@@ -7,9 +7,12 @@
using namespace omath; using namespace omath;
using namespace omath::source_engine; using namespace omath::source_engine;
using Projectile = projectile_prediction::Projectile<float>;
using Target = projectile_prediction::Target<float>;
TEST(PredEngineTrait, PredictProjectilePositionBasic) TEST(PredEngineTrait, PredictProjectilePositionBasic)
{ {
projectile_prediction::Projectile p; Projectile p;
p.m_origin = {0.f, 0.f, 0.f}; p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_speed = 10.f; p.m_launch_speed = 10.f;
p.m_gravity_scale = 1.f; p.m_gravity_scale = 1.f;
@@ -23,7 +26,7 @@ TEST(PredEngineTrait, PredictProjectilePositionBasic)
TEST(PredEngineTrait, PredictTargetPositionAirborne) TEST(PredEngineTrait, PredictTargetPositionAirborne)
{ {
projectile_prediction::Target t; Target t;
t.m_origin = {0.f, 0.f, 10.f}; t.m_origin = {0.f, 0.f, 10.f};
t.m_velocity = {1.f, 0.f, 0.f}; t.m_velocity = {1.f, 0.f, 0.f};
t.m_is_airborne = true; t.m_is_airborne = true;
@@ -42,7 +45,7 @@ TEST(PredEngineTrait, CalcVector2dDistance)
TEST(PredEngineTrait, CalcViewpointFromAngles) TEST(PredEngineTrait, CalcViewpointFromAngles)
{ {
projectile_prediction::Projectile p; Projectile p;
p.m_origin = {0.f, 0.f, 0.f}; p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_speed = 10.f; p.m_launch_speed = 10.f;
@@ -55,7 +58,7 @@ TEST(PredEngineTrait, CalcViewpointFromAngles)
TEST(PredEngineTrait, PredictProjectilePositionWithLaunchOffset) TEST(PredEngineTrait, PredictProjectilePositionWithLaunchOffset)
{ {
projectile_prediction::Projectile p; Projectile p;
p.m_origin = {0.f, 0.f, 0.f}; p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_offset = {5.f, 3.f, -2.f}; p.m_launch_offset = {5.f, 3.f, -2.f};
p.m_launch_speed = 10.f; p.m_launch_speed = 10.f;
@@ -76,13 +79,13 @@ TEST(PredEngineTrait, PredictProjectilePositionWithLaunchOffset)
TEST(PredEngineTrait, ZeroLaunchOffsetMatchesOriginalBehavior) TEST(PredEngineTrait, ZeroLaunchOffsetMatchesOriginalBehavior)
{ {
projectile_prediction::Projectile p; Projectile p;
p.m_origin = {10.f, 20.f, 30.f}; p.m_origin = {10.f, 20.f, 30.f};
p.m_launch_offset = {0.f, 0.f, 0.f}; p.m_launch_offset = {0.f, 0.f, 0.f};
p.m_launch_speed = 15.f; p.m_launch_speed = 15.f;
p.m_gravity_scale = 0.5f; p.m_gravity_scale = 0.5f;
projectile_prediction::Projectile p_no_offset; Projectile p_no_offset;
p_no_offset.m_origin = {10.f, 20.f, 30.f}; p_no_offset.m_origin = {10.f, 20.f, 30.f};
p_no_offset.m_launch_speed = 15.f; p_no_offset.m_launch_speed = 15.f;
p_no_offset.m_gravity_scale = 0.5f; p_no_offset.m_gravity_scale = 0.5f;

View File

@@ -1,14 +1,19 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <omath/projectile_prediction/proj_pred_engine_legacy.hpp> #include <omath/projectile_prediction/proj_pred_engine_legacy.hpp>
#include <omath/engines/source_engine/traits/camera_trait.hpp> #include <omath/engines/source_engine/traits/camera_trait.hpp>
using Projectile = omath::projectile_prediction::Projectile<float>;
using Target = omath::projectile_prediction::Target<float>;
using Engine = omath::projectile_prediction::ProjPredEngineLegacy<>;
TEST(UnitTestPrediction, PredictionTest) TEST(UnitTestPrediction, PredictionTest)
{ {
constexpr omath::projectile_prediction::Target target{ constexpr Target target{
.m_origin = {100, 0, 90}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; .m_origin = {100, 0, 90}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = { constexpr Projectile proj = {
.m_origin = {3, 2, 1}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; .m_origin = {3, 2, 1}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
const auto viewPoint = const auto viewPoint =
omath::projectile_prediction::ProjPredEngineLegacy(400, 1.f / 1000.f, 50, 5.f).maybe_calculate_aim_point(proj, target); Engine(400.f, 1.f / 1000.f, 50.f, 5.f).maybe_calculate_aim_point(proj, target);
const auto [pitch, yaw, _] =omath::source_engine::CameraTrait::calc_look_at_angle(proj.m_origin, viewPoint.value()); const auto [pitch, yaw, _] =omath::source_engine::CameraTrait::calc_look_at_angle(proj.m_origin, viewPoint.value());
@@ -18,12 +23,12 @@ TEST(UnitTestPrediction, PredictionTest)
} }
// Helper: verify aim_angles match angles derived from aim_point via CameraTrait // 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, static void expect_angles_match_aim_point(const Projectile& proj,
const omath::projectile_prediction::Target& target, const Target& target,
float gravity, float step, float max_time, float tolerance, float gravity, float step, float max_time, float tolerance,
float angle_eps = 0.01f) float angle_eps = 0.01f)
{ {
const omath::projectile_prediction::ProjPredEngineLegacy engine(gravity, step, max_time, tolerance); const Engine engine(gravity, step, max_time, tolerance);
const auto aim_point = engine.maybe_calculate_aim_point(proj, target); const auto aim_point = engine.maybe_calculate_aim_point(proj, target);
const auto aim_angles = engine.maybe_calculate_aim_angles(proj, target); const auto aim_angles = engine.maybe_calculate_aim_angles(proj, target);
@@ -45,30 +50,30 @@ static void expect_angles_match_aim_point(const omath::projectile_prediction::Pr
TEST(UnitTestPrediction, AimAnglesMatchAimPoint_StaticTarget) TEST(UnitTestPrediction, AimAnglesMatchAimPoint_StaticTarget)
{ {
constexpr omath::projectile_prediction::Target target{ constexpr Target target{
.m_origin = {100, 0, 90}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; .m_origin = {100, 0, 90}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = { constexpr Projectile proj = {
.m_origin = {3, 2, 1}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; .m_origin = {3, 2, 1}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f); expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f);
} }
TEST(UnitTestPrediction, AimAnglesMatchAimPoint_MovingTarget) TEST(UnitTestPrediction, AimAnglesMatchAimPoint_MovingTarget)
{ {
constexpr omath::projectile_prediction::Target target{ constexpr Target target{
.m_origin = {500, 100, 0}, .m_velocity = {-50, 20, 0}, .m_is_airborne = false}; .m_origin = {500, 100, 0}, .m_velocity = {-50, 20, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = { constexpr Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 3000, .m_gravity_scale = 1.0}; .m_origin = {0, 0, 0}, .m_launch_speed = 3000.f, .m_gravity_scale = 1.0f};
expect_angles_match_aim_point(proj, target, 800, 1.f / 500.f, 30, 10.f); expect_angles_match_aim_point(proj, target, 800, 1.f / 500.f, 30, 10.f);
} }
TEST(UnitTestPrediction, AimAnglesMatchAimPoint_AirborneTarget) TEST(UnitTestPrediction, AimAnglesMatchAimPoint_AirborneTarget)
{ {
constexpr omath::projectile_prediction::Target target{ constexpr Target target{
.m_origin = {200, 50, 300}, .m_velocity = {10, -5, -20}, .m_is_airborne = true}; .m_origin = {200, 50, 300}, .m_velocity = {10, -5, -20}, .m_is_airborne = true};
constexpr omath::projectile_prediction::Projectile proj = { constexpr Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 4000, .m_gravity_scale = 0.5}; .m_origin = {0, 0, 0}, .m_launch_speed = 4000.f, .m_gravity_scale = 0.5f};
expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 10.f); expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 10.f);
} }
@@ -76,10 +81,10 @@ TEST(UnitTestPrediction, AimAnglesMatchAimPoint_AirborneTarget)
TEST(UnitTestPrediction, AimAnglesMatchAimPoint_HighArc) TEST(UnitTestPrediction, AimAnglesMatchAimPoint_HighArc)
{ {
// Target nearly directly above — high pitch angle // Target nearly directly above — high pitch angle
constexpr omath::projectile_prediction::Target target{ constexpr Target target{
.m_origin = {10, 0, 500}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; .m_origin = {10, 0, 500}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = { constexpr Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.3}; .m_origin = {0, 0, 0}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.3f};
expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f); expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f);
} }
@@ -87,20 +92,20 @@ TEST(UnitTestPrediction, AimAnglesMatchAimPoint_HighArc)
TEST(UnitTestPrediction, AimAnglesMatchAimPoint_NegativeYaw) TEST(UnitTestPrediction, AimAnglesMatchAimPoint_NegativeYaw)
{ {
// Target behind and to the left — negative yaw quadrant // Target behind and to the left — negative yaw quadrant
constexpr omath::projectile_prediction::Target target{ constexpr Target target{
.m_origin = {-200, -150, 10}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; .m_origin = {-200, -150, 10}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = { constexpr Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; .m_origin = {0, 0, 0}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f); expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f);
} }
TEST(UnitTestPrediction, AimAnglesMatchAimPoint_WithLaunchOffset) TEST(UnitTestPrediction, AimAnglesMatchAimPoint_WithLaunchOffset)
{ {
constexpr omath::projectile_prediction::Target target{ constexpr Target target{
.m_origin = {200, 0, 50}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; .m_origin = {200, 0, 50}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = { const Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {5, 0, -3}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; .m_origin = {0, 0, 0}, .m_launch_offset = {5, 0, -3}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f); expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f);
} }
@@ -108,13 +113,13 @@ TEST(UnitTestPrediction, AimAnglesMatchAimPoint_WithLaunchOffset)
// Helper: simulate projectile flight using aim_angles and verify it reaches the target. // Helper: simulate projectile flight using aim_angles and verify it reaches the target.
// Steps the projectile forward in small increments, simultaneously predicts target position, // Steps the projectile forward in small increments, simultaneously predicts target position,
// and checks that the minimum distance is within hit_tolerance. // and checks that the minimum distance is within hit_tolerance.
static void expect_projectile_hits_target(const omath::projectile_prediction::Projectile& proj, static void expect_projectile_hits_target(const Projectile& proj,
const omath::projectile_prediction::Target& target, const Target& target,
float gravity, float engine_step, float max_time, float engine_tolerance, float gravity, float engine_step, float max_time, float engine_tolerance,
float hit_tolerance, float sim_step = 1.f / 2000.f) float hit_tolerance, float sim_step = 1.f / 2000.f)
{ {
using Trait = omath::source_engine::PredEngineTrait; using Trait = omath::source_engine::PredEngineTrait;
const omath::projectile_prediction::ProjPredEngineLegacy engine(gravity, engine_step, max_time, engine_tolerance); const Engine engine(gravity, engine_step, max_time, engine_tolerance);
const auto aim_angles = engine.maybe_calculate_aim_angles(proj, target); const auto aim_angles = engine.maybe_calculate_aim_angles(proj, target);
ASSERT_TRUE(aim_angles.has_value()) << "engine must find a solution"; ASSERT_TRUE(aim_angles.has_value()) << "engine must find a solution";
@@ -148,50 +153,50 @@ static void expect_projectile_hits_target(const omath::projectile_prediction::Pr
TEST(ProjectileSimulation, HitsStaticTarget_NoOffset) TEST(ProjectileSimulation, HitsStaticTarget_NoOffset)
{ {
constexpr omath::projectile_prediction::Target target{ constexpr Target target{
.m_origin = {100, 0, 90}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; .m_origin = {100, 0, 90}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = { constexpr Projectile proj = {
.m_origin = {3, 2, 1}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; .m_origin = {3, 2, 1}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
} }
TEST(ProjectileSimulation, HitsMovingTarget_NoOffset) TEST(ProjectileSimulation, HitsMovingTarget_NoOffset)
{ {
constexpr omath::projectile_prediction::Target target{ constexpr Target target{
.m_origin = {500, 100, 0}, .m_velocity = {-50, 20, 0}, .m_is_airborne = false}; .m_origin = {500, 100, 0}, .m_velocity = {-50, 20, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = { constexpr Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 3000, .m_gravity_scale = 1.0}; .m_origin = {0, 0, 0}, .m_launch_speed = 3000.f, .m_gravity_scale = 1.0f};
expect_projectile_hits_target(proj, target, 800, 1.f / 500.f, 30, 10.f, 15.f); expect_projectile_hits_target(proj, target, 800, 1.f / 500.f, 30, 10.f, 15.f);
} }
TEST(ProjectileSimulation, HitsAirborneTarget_NoOffset) TEST(ProjectileSimulation, HitsAirborneTarget_NoOffset)
{ {
constexpr omath::projectile_prediction::Target target{ constexpr Target target{
.m_origin = {200, 50, 300}, .m_velocity = {10, -5, -20}, .m_is_airborne = true}; .m_origin = {200, 50, 300}, .m_velocity = {10, -5, -20}, .m_is_airborne = true};
constexpr omath::projectile_prediction::Projectile proj = { constexpr Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 4000, .m_gravity_scale = 0.5}; .m_origin = {0, 0, 0}, .m_launch_speed = 4000.f, .m_gravity_scale = 0.5f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 10.f, 15.f); expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 10.f, 15.f);
} }
TEST(ProjectileSimulation, HitsHighTarget_NoOffset) TEST(ProjectileSimulation, HitsHighTarget_NoOffset)
{ {
constexpr omath::projectile_prediction::Target target{ constexpr Target target{
.m_origin = {10, 0, 500}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; .m_origin = {10, 0, 500}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = { constexpr Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.3}; .m_origin = {0, 0, 0}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.3f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
} }
TEST(ProjectileSimulation, HitsNegativeYawTarget_NoOffset) TEST(ProjectileSimulation, HitsNegativeYawTarget_NoOffset)
{ {
constexpr omath::projectile_prediction::Target target{ constexpr Target target{
.m_origin = {-200, -150, 10}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; .m_origin = {-200, -150, 10}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = { constexpr Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; .m_origin = {0, 0, 0}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
} }
@@ -200,92 +205,92 @@ TEST(ProjectileSimulation, HitsNegativeYawTarget_NoOffset)
TEST(ProjectileSimulation, HitsStaticTarget_SmallOffset) TEST(ProjectileSimulation, HitsStaticTarget_SmallOffset)
{ {
constexpr omath::projectile_prediction::Target target{ constexpr Target target{
.m_origin = {200, 0, 50}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; .m_origin = {200, 0, 50}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = { const Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {5, 0, -3}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; .m_origin = {0, 0, 0}, .m_launch_offset = {5, 0, -3}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
} }
TEST(ProjectileSimulation, HitsStaticTarget_LargeXOffset) TEST(ProjectileSimulation, HitsStaticTarget_LargeXOffset)
{ {
constexpr omath::projectile_prediction::Target target{ constexpr Target target{
.m_origin = {300, 100, 0}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; .m_origin = {300, 100, 0}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = { const Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {20, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; .m_origin = {0, 0, 0}, .m_launch_offset = {20, 0, 0}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
} }
TEST(ProjectileSimulation, HitsStaticTarget_LargeYOffset) TEST(ProjectileSimulation, HitsStaticTarget_LargeYOffset)
{ {
constexpr omath::projectile_prediction::Target target{ constexpr Target target{
.m_origin = {150, -200, 30}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; .m_origin = {150, -200, 30}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = { const Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {0, 15, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; .m_origin = {0, 0, 0}, .m_launch_offset = {0, 15, 0}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
} }
TEST(ProjectileSimulation, HitsStaticTarget_LargeZOffset) TEST(ProjectileSimulation, HitsStaticTarget_LargeZOffset)
{ {
constexpr omath::projectile_prediction::Target target{ constexpr Target target{
.m_origin = {100, 0, 200}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; .m_origin = {100, 0, 200}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = { const Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {0, 0, -10}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; .m_origin = {0, 0, 0}, .m_launch_offset = {0, 0, -10}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
} }
TEST(ProjectileSimulation, HitsStaticTarget_AllAxesOffset) TEST(ProjectileSimulation, HitsStaticTarget_AllAxesOffset)
{ {
constexpr omath::projectile_prediction::Target target{ constexpr Target target{
.m_origin = {250, 80, 60}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; .m_origin = {250, 80, 60}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = { const Projectile proj = {
.m_origin = {10, 5, 20}, .m_launch_offset = {8, -4, -6}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; .m_origin = {10, 5, 20}, .m_launch_offset = {8, -4, -6}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
} }
TEST(ProjectileSimulation, HitsMovingTarget_WithOffset) TEST(ProjectileSimulation, HitsMovingTarget_WithOffset)
{ {
constexpr omath::projectile_prediction::Target target{ constexpr Target target{
.m_origin = {400, 0, 50}, .m_velocity = {-30, 10, 5}, .m_is_airborne = false}; .m_origin = {400, 0, 50}, .m_velocity = {-30, 10, 5}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = { const Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {10, -5, 2}, .m_launch_speed = 3000, .m_gravity_scale = 0.8}; .m_origin = {0, 0, 0}, .m_launch_offset = {10, -5, 2}, .m_launch_speed = 3000.f, .m_gravity_scale = 0.8f};
expect_projectile_hits_target(proj, target, 800, 1.f / 500.f, 30, 10.f, 15.f); expect_projectile_hits_target(proj, target, 800, 1.f / 500.f, 30, 10.f, 15.f);
} }
TEST(ProjectileSimulation, HitsAirborneTarget_WithOffset) TEST(ProjectileSimulation, HitsAirborneTarget_WithOffset)
{ {
constexpr omath::projectile_prediction::Target target{ constexpr Target target{
.m_origin = {150, 80, 250}, .m_velocity = {5, -10, -30}, .m_is_airborne = true}; .m_origin = {150, 80, 250}, .m_velocity = {5, -10, -30}, .m_is_airborne = true};
const omath::projectile_prediction::Projectile proj = { const Projectile proj = {
.m_origin = {0, 0, 50}, .m_launch_offset = {3, 7, -5}, .m_launch_speed = 4000, .m_gravity_scale = 0.5}; .m_origin = {0, 0, 50}, .m_launch_offset = {3, 7, -5}, .m_launch_speed = 4000.f, .m_gravity_scale = 0.5f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 10.f, 15.f); expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 10.f, 15.f);
} }
TEST(ProjectileSimulation, HitsNegativeYawTarget_WithOffset) TEST(ProjectileSimulation, HitsNegativeYawTarget_WithOffset)
{ {
constexpr omath::projectile_prediction::Target target{ constexpr Target target{
.m_origin = {-200, -150, 10}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; .m_origin = {-200, -150, 10}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = { const Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {-5, 3, 2}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; .m_origin = {0, 0, 0}, .m_launch_offset = {-5, 3, 2}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
} }
TEST(UnitTestPrediction, AimAnglesReturnsNulloptWhenNoSolution) TEST(UnitTestPrediction, AimAnglesReturnsNulloptWhenNoSolution)
{ {
constexpr omath::projectile_prediction::Target target{ constexpr Target target{
.m_origin = {100000, 0, 0}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; .m_origin = {100000, 0, 0}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = { constexpr Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 1, .m_gravity_scale = 1}; .m_origin = {0, 0, 0}, .m_launch_speed = 1.f, .m_gravity_scale = 1.f};
const omath::projectile_prediction::ProjPredEngineLegacy engine(9.81f, 0.1f, 2.f, 5.f); const Engine engine(9.81f, 0.1f, 2.f, 5.f);
const auto aim_point = engine.maybe_calculate_aim_point(proj, target); const auto aim_point = engine.maybe_calculate_aim_point(proj, target);
const auto aim_angles = engine.maybe_calculate_aim_angles(proj, target); const auto aim_angles = engine.maybe_calculate_aim_angles(proj, target);

View File

@@ -4,8 +4,8 @@
#include <omath/projectile_prediction/target.hpp> #include <omath/projectile_prediction/target.hpp>
#include <omath/linear_algebra/vector3.hpp> #include <omath/linear_algebra/vector3.hpp>
using omath::projectile_prediction::Projectile; using Projectile = omath::projectile_prediction::Projectile<float>;
using omath::projectile_prediction::Target; using Target = omath::projectile_prediction::Target<float>;
using omath::Vector3; using omath::Vector3;
// Fake engine trait where gravity is effectively zero and projectile prediction always hits the target // Fake engine trait where gravity is effectively zero and projectile prediction always hits the target