From e99ca0bc2b7bf634cc90ec15f7cc8299f749762b Mon Sep 17 00:00:00 2001 From: Orange Date: Thu, 19 Mar 2026 19:19:42 +0300 Subject: [PATCH] update --- include/omath/algorithm/targeting.hpp | 50 +++++++++ include/omath/projection/camera.hpp | 5 + tests/general/unit_test_targeting.cpp | 154 ++++++++++++++++++++++++++ 3 files changed, 209 insertions(+) create mode 100644 include/omath/algorithm/targeting.hpp create mode 100644 tests/general/unit_test_targeting.cpp diff --git a/include/omath/algorithm/targeting.hpp b/include/omath/algorithm/targeting.hpp new file mode 100644 index 0000000..3c06ea0 --- /dev/null +++ b/include/omath/algorithm/targeting.hpp @@ -0,0 +1,50 @@ +// +// Created by Vladislav on 19.03.2026. +// + +#pragma once +#include "omath/linear_algebra/vector3.hpp" +#include +#include +#include + +namespace omath::algorithm +{ + template + requires std::is_invocable_r_v, std::iter_reference_t> + [[nodiscard]] + IteratorType get_closest_target_by_fov(const IteratorType& begin, const IteratorType& end, const CameraType& camera, + auto get_position, + const std::optional>& filter_func = std::nullopt) + { + auto best_target = end; + const auto& camera_angles = camera.get_view_angles(); + const Vector2 camera_angles_vec = {camera_angles.pitch.as_degrees(), camera_angles.yaw.as_degrees()}; + + for (auto current = begin; current != end; current = std::next(current)) + { + if (filter_func && !filter_func.value()(*current)) + continue; + + if (best_target == end) + { + best_target = current; + continue; + } + const auto current_target_angles = camera.calc_look_at_angles(get_position(*current)); + const auto best_target_angles = camera.calc_look_at_angles(get_position(*best_target)); + + const Vector2 current_angles_vec = {current_target_angles.pitch.as_degrees(), + current_target_angles.yaw.as_degrees()}; + const Vector2 best_angles_vec = {best_target_angles.pitch.as_degrees(), + best_target_angles.yaw.as_degrees()}; + + const auto current_target_distance = camera_angles_vec.distance_to(current_angles_vec); + const auto best_target_distance = camera_angles_vec.distance_to(best_angles_vec); + if (current_target_distance < best_target_distance) + best_target = current; + } + return best_target; + } + +} // namespace omath::algorithm \ No newline at end of file diff --git a/include/omath/projection/camera.hpp b/include/omath/projection/camera.hpp index c2d82f6..1678d4b 100644 --- a/include/omath/projection/camera.hpp +++ b/include/omath/projection/camera.hpp @@ -82,6 +82,11 @@ namespace omath::projection m_view_projection_matrix = std::nullopt; m_view_matrix = std::nullopt; } + [[nodiscard]] + ViewAnglesType calc_look_at_angles(const Vector3& look_to) const + { + return TraitClass::calc_look_at_angle(m_origin, look_to); + } [[nodiscard]] Vector3 get_forward() const noexcept diff --git a/tests/general/unit_test_targeting.cpp b/tests/general/unit_test_targeting.cpp new file mode 100644 index 0000000..8386290 --- /dev/null +++ b/tests/general/unit_test_targeting.cpp @@ -0,0 +1,154 @@ +// +// Created by claude on 19.03.2026. +// +#include +#include +#include +#include + +namespace +{ + using Camera = omath::source_engine::Camera; + using ViewAngles = omath::source_engine::ViewAngles; + using Targets = std::vector>; + using Iter = Targets::const_iterator; + using FilterSig = bool(const omath::Vector3&); + + constexpr auto k_fov = omath::Angle::from_degrees(90.f); + + Camera make_camera(const omath::Vector3& origin, float pitch_deg, float yaw_deg) + { + ViewAngles angles{ + omath::source_engine::PitchAngle::from_degrees(pitch_deg), + omath::source_engine::YawAngle::from_degrees(yaw_deg), + omath::source_engine::RollAngle::from_degrees(0.f), + }; + return Camera{origin, angles, {1920.f, 1080.f}, k_fov, 0.01f, 1000.f}; + } + + auto get_pos = [](const omath::Vector3& v) -> const omath::Vector3& { return v; }; + + Iter find_closest(const Iter begin, const Iter end, const Camera& camera) + { + return omath::algorithm::get_closest_target_by_fov( + begin, end, camera, get_pos); + } +} + +TEST(unit_test_targeting, returns_end_for_empty_range) +{ + const auto camera = make_camera({0, 0, 0}, 0.f, 0.f); + Targets targets; + + EXPECT_EQ(find_closest(targets.cbegin(), targets.cend(), camera), targets.cend()); +} + +TEST(unit_test_targeting, single_target_returns_that_target) +{ + const auto camera = make_camera({0, 0, 0}, 0.f, 0.f); + Targets targets = {{100.f, 0.f, 0.f}}; + + EXPECT_EQ(find_closest(targets.cbegin(), targets.cend(), camera), targets.cbegin()); +} + +TEST(unit_test_targeting, picks_closest_to_crosshair) +{ + // Camera looking forward along +X (yaw=0, pitch=0 in source engine) + const auto camera = make_camera({0, 0, 0}, 0.f, 0.f); + + Targets targets = { + {100.f, 50.f, 0.f}, // off to the side + {100.f, 1.f, 0.f}, // nearly on crosshair + {100.f, -30.f, 0.f}, // off to the other side + }; + + const auto result = find_closest(targets.cbegin(), targets.cend(), camera); + + ASSERT_NE(result, targets.cend()); + EXPECT_EQ(result, targets.cbegin() + 1); +} + +TEST(unit_test_targeting, picks_closest_with_vertical_offset) +{ + const auto camera = make_camera({0, 0, 0}, 0.f, 0.f); + + Targets targets = { + {100.f, 0.f, 50.f}, // high above + {100.f, 0.f, 2.f}, // slightly above + {100.f, 0.f, 30.f}, // moderately above + }; + + const auto result = find_closest(targets.cbegin(), targets.cend(), camera); + + ASSERT_NE(result, targets.cend()); + EXPECT_EQ(result, targets.cbegin() + 1); +} + +TEST(unit_test_targeting, respects_camera_direction) +{ + // Camera looking along +Y (yaw=90) + const auto camera = make_camera({0, 0, 0}, 0.f, 90.f); + + Targets targets = { + {100.f, 0.f, 0.f}, // to the side relative to camera facing +Y + {0.f, 100.f, 0.f}, // directly in front + }; + + const auto result = find_closest(targets.cbegin(), targets.cend(), camera); + + ASSERT_NE(result, targets.cend()); + EXPECT_EQ(result, targets.cbegin() + 1); +} + +TEST(unit_test_targeting, equidistant_targets_returns_first) +{ + const auto camera = make_camera({0, 0, 0}, 0.f, 0.f); + + // Two targets symmetric about the forward axis — same angular distance + Targets targets = { + {100.f, 10.f, 0.f}, + {100.f, -10.f, 0.f}, + }; + + const auto result = find_closest(targets.cbegin(), targets.cend(), camera); + + ASSERT_NE(result, targets.cend()); + // First target should be selected (strict < means first wins on tie) + EXPECT_EQ(result, targets.cbegin()); +} + +TEST(unit_test_targeting, camera_pitch_affects_selection) +{ + // Camera looking upward (pitch < 0) + const auto camera = make_camera({0, 0, 0}, -40.f, 0.f); + + Targets targets = { + {100.f, 0.f, 0.f}, // on the horizon + {100.f, 0.f, 40.f}, // above, closer to where camera is looking + }; + + const auto result = find_closest(targets.cbegin(), targets.cend(), camera); + + ASSERT_NE(result, targets.cend()); + EXPECT_EQ(result, targets.cbegin() + 1); +} + +TEST(unit_test_targeting, many_targets_picks_best) +{ + const auto camera = make_camera({0, 0, 0}, 0.f, 0.f); + + Targets targets = { + {100.f, 80.f, 80.f}, + {100.f, 60.f, 60.f}, + {100.f, 40.f, 40.f}, + {100.f, 20.f, 20.f}, + {100.f, 0.5f, 0.5f}, // closest to crosshair + {100.f, 10.f, 10.f}, + {100.f, 30.f, 30.f}, + }; + + const auto result = find_closest(targets.cbegin(), targets.cend(), camera); + + ASSERT_NE(result, targets.cend()); + EXPECT_EQ(result, targets.cbegin() + 4); +}