diff --git a/include/omath/engines/rage_engine/camera.hpp b/include/omath/engines/rage_engine/camera.hpp new file mode 100644 index 0000000..2000bdc --- /dev/null +++ b/include/omath/engines/rage_engine/camera.hpp @@ -0,0 +1,13 @@ +// +// Created by Codex on 6/3/2026. +// + +#pragma once +#include "omath/engines/rage_engine/constants.hpp" +#include "omath/projection/camera.hpp" +#include "traits/camera_trait.hpp" + +namespace omath::rage_engine +{ + using Camera = projection::Camera; +} // namespace omath::rage_engine diff --git a/include/omath/engines/rage_engine/constants.hpp b/include/omath/engines/rage_engine/constants.hpp new file mode 100644 index 0000000..b60860b --- /dev/null +++ b/include/omath/engines/rage_engine/constants.hpp @@ -0,0 +1,25 @@ +// +// Created by Codex on 6/3/2026. +// + +#pragma once +#include "omath/linear_algebra/mat.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include +#include + +namespace omath::rage_engine +{ + constexpr Vector3 k_abs_up = {0, 0, 1}; + constexpr Vector3 k_abs_right = {1, 0, 0}; + constexpr Vector3 k_abs_forward = {0, 1, 0}; + + using Mat4X4 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; + using Mat3X3 = Mat<3, 3, float, MatStoreType::ROW_MAJOR>; + using Mat1X3 = Mat<1, 3, float, MatStoreType::ROW_MAJOR>; + using PitchAngle = Angle; + using YawAngle = Angle; + using RollAngle = Angle; + + using ViewAngles = omath::ViewAngles; +} // namespace omath::rage_engine diff --git a/include/omath/engines/rage_engine/formulas.hpp b/include/omath/engines/rage_engine/formulas.hpp new file mode 100644 index 0000000..a0a888c --- /dev/null +++ b/include/omath/engines/rage_engine/formulas.hpp @@ -0,0 +1,85 @@ +// +// Created by Codex on 6/3/2026. +// + +#pragma once +#include "omath/engines/rage_engine/constants.hpp" +#include + +namespace omath::rage_engine +{ + [[nodiscard]] + Vector3 forward_vector(const ViewAngles& angles) noexcept; + + [[nodiscard]] + Vector3 right_vector(const ViewAngles& angles) noexcept; + + [[nodiscard]] + Vector3 up_vector(const ViewAngles& angles) noexcept; + + [[nodiscard]] Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept; + + [[nodiscard]] + Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept; + + [[nodiscard]] + Vector3 extract_origin(const Mat4X4& mat) noexcept; + + [[nodiscard]] + Vector3 extract_scale(const Mat4X4& mat) noexcept; + + [[nodiscard]] + ViewAngles extract_rotation_angles(const Mat4X4& mat) noexcept; + + [[nodiscard]] + Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far, + NDCDepthRange ndc_depth_range = NDCDepthRange::ZERO_TO_ONE) noexcept; + + template + requires std::is_floating_point_v + [[nodiscard]] + constexpr FloatingType units_to_centimeters(const FloatingType& units) + { + return units / static_cast(100); + } + + template + requires std::is_floating_point_v + [[nodiscard]] + constexpr FloatingType units_to_meters(const FloatingType& units) + { + return units; + } + + template + requires std::is_floating_point_v + [[nodiscard]] + constexpr FloatingType units_to_kilometers(const FloatingType& units) + { + return units_to_meters(units) / static_cast(1000); + } + + template + requires std::is_floating_point_v + [[nodiscard]] + constexpr FloatingType centimeters_to_units(const FloatingType& centimeters) + { + return centimeters * static_cast(100); + } + + template + requires std::is_floating_point_v + [[nodiscard]] + constexpr FloatingType meters_to_units(const FloatingType& meters) + { + return meters; + } + + template + requires std::is_floating_point_v + [[nodiscard]] + constexpr FloatingType kilometers_to_units(const FloatingType& kilometers) + { + return meters_to_units(kilometers * static_cast(1000)); + } +} // namespace omath::rage_engine diff --git a/include/omath/engines/rage_engine/mesh.hpp b/include/omath/engines/rage_engine/mesh.hpp new file mode 100644 index 0000000..27ad1ba --- /dev/null +++ b/include/omath/engines/rage_engine/mesh.hpp @@ -0,0 +1,13 @@ +// +// Created by Codex on 6/3/2026. +// + +#pragma once +#include "constants.hpp" +#include "omath/3d_primitives/mesh.hpp" +#include "traits/mesh_trait.hpp" + +namespace omath::rage_engine +{ + using Mesh = primitives::Mesh; +} // namespace omath::rage_engine diff --git a/include/omath/engines/rage_engine/traits/camera_trait.hpp b/include/omath/engines/rage_engine/traits/camera_trait.hpp new file mode 100644 index 0000000..773c38e --- /dev/null +++ b/include/omath/engines/rage_engine/traits/camera_trait.hpp @@ -0,0 +1,24 @@ +// +// Created by Codex on 6/3/2026. +// + +#pragma once +#include "omath/engines/rage_engine/formulas.hpp" +#include "omath/projection/camera.hpp" + +namespace omath::rage_engine +{ + class CameraTrait final + { + public: + [[nodiscard]] + static ViewAngles calc_look_at_angle(const Vector3& cam_origin, const Vector3& look_at) noexcept; + + [[nodiscard]] + static Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept; + [[nodiscard]] + static Mat4X4 calc_projection_matrix(const projection::FieldOfView& fov, const projection::ViewPort& view_port, + float near, float far, NDCDepthRange ndc_depth_range) noexcept; + }; + +} // namespace omath::rage_engine diff --git a/include/omath/engines/rage_engine/traits/mesh_trait.hpp b/include/omath/engines/rage_engine/traits/mesh_trait.hpp new file mode 100644 index 0000000..5cc57e9 --- /dev/null +++ b/include/omath/engines/rage_engine/traits/mesh_trait.hpp @@ -0,0 +1,20 @@ +// +// Created by Codex on 6/3/2026. +// + +#pragma once +#include +#include + +namespace omath::rage_engine +{ + class MeshTrait final + { + public: + [[nodiscard]] + static Mat4X4 rotation_matrix(const ViewAngles& rotation) + { + return rage_engine::rotation_matrix(rotation); + } + }; +} // namespace omath::rage_engine diff --git a/include/omath/hud/renderer_realizations/imgui_renderer.hpp b/include/omath/hud/renderer_realizations/imgui_renderer.hpp index 8b8190b..a7e1364 100644 --- a/include/omath/hud/renderer_realizations/imgui_renderer.hpp +++ b/include/omath/hud/renderer_realizations/imgui_renderer.hpp @@ -27,7 +27,7 @@ namespace omath::hud const Color& tint = Color{1.f, 1.f, 1.f, 1.f}) override; void add_text(const Vector2& position, const Color& color, const std::string_view& text) override; [[nodiscard]] - virtual Vector2 calc_text_size(const std::string_view& text) override; + Vector2 calc_text_size(const std::string_view& text) override; }; } // namespace omath::hud #endif // OMATH_IMGUI_INTEGRATION \ No newline at end of file diff --git a/include/omath/omath.hpp b/include/omath/omath.hpp index bcf503a..f249eac 100644 --- a/include/omath/omath.hpp +++ b/include/omath/omath.hpp @@ -87,6 +87,12 @@ #include "omath/engines/frostbite_engine/traits/camera_trait.hpp" #include "omath/engines/frostbite_engine/traits/pred_engine_trait.hpp" +// RAGE Engine +#include "omath/engines/rage_engine/constants.hpp" +#include "omath/engines/rage_engine/formulas.hpp" +#include "omath/engines/rage_engine/camera.hpp" +#include "omath/engines/rage_engine/traits/camera_trait.hpp" +#include "omath/engines/rage_engine/traits/pred_engine_trait.hpp" // Unreal Engine #include "omath/engines/unreal_engine/constants.hpp" @@ -101,4 +107,4 @@ // Utility #include "omath/utility/pattern_scan.hpp" -#include "omath/utility/pe_pattern_scan.hpp" \ No newline at end of file +#include "omath/utility/pe_pattern_scan.hpp" diff --git a/source/engines/rage_engine/formulas.cpp b/source/engines/rage_engine/formulas.cpp new file mode 100644 index 0000000..ff2b3ce --- /dev/null +++ b/source/engines/rage_engine/formulas.cpp @@ -0,0 +1,71 @@ +// +// Created by Codex on 6/3/2026. +// +#include "omath/engines/rage_engine/formulas.hpp" + +namespace omath::rage_engine +{ + Vector3 forward_vector(const ViewAngles& angles) noexcept + { + const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_forward); + + return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; + } + Vector3 right_vector(const ViewAngles& angles) noexcept + { + const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_right); + + return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; + } + Vector3 up_vector(const ViewAngles& angles) noexcept + { + const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_up); + + return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; + } + Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept + { + return mat_camera_view(forward_vector(angles), right_vector(angles), + up_vector(angles), cam_origin); + } + Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept + { + return mat_rotation_axis_z(angles.yaw) + * mat_rotation_axis_y(angles.roll) + * mat_rotation_axis_x(angles.pitch); + } + + Vector3 extract_origin(const Mat4X4& mat) noexcept + { + return mat_extract_origin(mat); + } + + Vector3 extract_scale(const Mat4X4& mat) noexcept + { + return mat_extract_scale(mat); + } + + ViewAngles extract_rotation_angles(const Mat4X4& mat) noexcept + { + const auto angles = mat_extract_rotation_zyx(mat); + return { + PitchAngle::from_degrees(angles.x), + YawAngle::from_degrees(angles.z), + RollAngle::from_degrees(angles.y), + }; + } + + Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near, + const float far, const NDCDepthRange ndc_depth_range) noexcept + { + if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE) + return mat_perspective_left_handed_vertical_fov( + field_of_view, aspect_ratio, near, far); + + if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE) + return mat_perspective_left_handed_vertical_fov( + field_of_view, aspect_ratio, near, far); + std::unreachable(); + } +} // namespace omath::rage_engine diff --git a/source/engines/rage_engine/traits/camera_trait.cpp b/source/engines/rage_engine/traits/camera_trait.cpp new file mode 100644 index 0000000..8e1f5cd --- /dev/null +++ b/source/engines/rage_engine/traits/camera_trait.cpp @@ -0,0 +1,27 @@ +// +// Created by Codex on 6/3/2026. +// +#include "omath/engines/rage_engine/traits/camera_trait.hpp" + +namespace omath::rage_engine +{ + + ViewAngles CameraTrait::calc_look_at_angle(const Vector3& cam_origin, const Vector3& look_at) noexcept + { + const auto direction = (look_at - cam_origin).normalized(); + + return {PitchAngle::from_radians(std::asin(direction.z)), + YawAngle::from_radians(-std::atan2(direction.x, direction.y)), RollAngle::from_radians(0.f)}; + } + Mat4X4 CameraTrait::calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept + { + return rage_engine::calc_view_matrix(angles, cam_origin); + } + Mat4X4 CameraTrait::calc_projection_matrix(const projection::FieldOfView& fov, + const projection::ViewPort& view_port, const float near, const float far, + const NDCDepthRange ndc_depth_range) noexcept + { + return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far, + ndc_depth_range); + } +} // namespace omath::rage_engine diff --git a/source/lua/lua_engines.cpp b/source/lua/lua_engines.cpp index 31a4999..f64cae2 100644 --- a/source/lua/lua_engines.cpp +++ b/source/lua/lua_engines.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -181,6 +182,12 @@ namespace using ViewAngles = omath::source_engine::ViewAngles; using Camera = omath::source_engine::Camera; }; + struct RageEngineTraits + { + using PitchAngle = omath::rage_engine::PitchAngle; + using ViewAngles = omath::rage_engine::ViewAngles; + using Camera = omath::rage_engine::Camera; + }; struct UnityEngineTraits { using PitchAngle = omath::unity_engine::PitchAngle; @@ -254,6 +261,7 @@ namespace omath::lua register_engine(omath_table, "frostbite"); register_engine(omath_table, "iw"); register_engine(omath_table, "source"); + register_engine(omath_table, "rage"); register_engine(omath_table, "unity"); register_engine(omath_table, "unreal"); register_engine(omath_table, "cry"); diff --git a/tests/engines/unit_test_rage_engine.cpp b/tests/engines/unit_test_rage_engine.cpp new file mode 100644 index 0000000..59cbaa6 --- /dev/null +++ b/tests/engines/unit_test_rage_engine.cpp @@ -0,0 +1,101 @@ +// +// Created by Codex on 6/3/2026. +// +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace omath; + +static_assert(std::is_same_v); + +static void expect_rage_vector_near(const Vector3& actual, const Vector3& expected) +{ + for (const auto& [result, etalon] : std::views::zip(actual.as_array(), expected.as_array())) + EXPECT_NEAR(result, etalon, 0.0001f); +} + +TEST(unit_test_rage_engine, ForwardVector) +{ + const auto forward = rage_engine::forward_vector({}); + + EXPECT_EQ(forward, rage_engine::k_abs_forward); +} + +TEST(unit_test_rage_engine, RightVector) +{ + const auto right = rage_engine::right_vector({}); + + EXPECT_EQ(right, rage_engine::k_abs_right); +} + +TEST(unit_test_rage_engine, UpVector) +{ + const auto up = rage_engine::up_vector({}); + EXPECT_EQ(up, rage_engine::k_abs_up); +} + +TEST(unit_test_rage_engine, LookAtForward) +{ + const auto angles = rage_engine::CameraTrait::calc_look_at_angle({}, rage_engine::k_abs_forward); + + expect_rage_vector_near(rage_engine::forward_vector(angles), rage_engine::k_abs_forward); +} + +TEST(unit_test_rage_engine, LookAtRight) +{ + const auto angles = rage_engine::CameraTrait::calc_look_at_angle({}, rage_engine::k_abs_right); + + expect_rage_vector_near(rage_engine::forward_vector(angles), rage_engine::k_abs_right); +} + +TEST(unit_test_rage_engine, LookAtUp) +{ + const auto angles = rage_engine::CameraTrait::calc_look_at_angle({}, rage_engine::k_abs_up); + + expect_rage_vector_near(rage_engine::forward_vector(angles), rage_engine::k_abs_up); +} + +TEST(unit_test_rage_engine, ProjectTargetMovedFromCamera) +{ + constexpr auto fov = projection::FieldOfView::from_degrees(60.f); + const auto cam = rage_engine::Camera({0, 0, 0}, {}, {1280.f, 720.f}, fov, 0.01f, 1000.f); + + const auto projected = cam.world_to_screen({0, 10.f, 0}); + + ASSERT_TRUE(projected.has_value()); + EXPECT_NEAR(projected->x, 640.f, 0.0001f); + EXPECT_NEAR(projected->y, 360.f, 0.0001f); +} + +TEST(unit_test_rage_engine, PredEngineTraitUsesZAsHeight) +{ + projectile_prediction::Projectile projectile; + projectile.m_origin = {0.f, 0.f, 0.f}; + projectile.m_launch_speed = 10.f; + projectile.m_gravity_scale = 1.f; + + const auto pos = rage_engine::PredEngineTrait::predict_projectile_position(projectile, 0.f, 0.f, 1.f, 9.81f); + + EXPECT_NEAR(pos.x, 0.f, 0.0001f); + EXPECT_NEAR(pos.y, 10.f, 0.0001f); + EXPECT_NEAR(pos.z, -9.81f * 0.5f, 0.0001f); + EXPECT_NEAR(rage_engine::PredEngineTrait::get_vector_height_coordinate({1.f, 2.f, 3.f}), 3.f, 0.0001f); +} + +TEST(unit_test_rage_engine, MeshTraitForwardsRotationMatrix) +{ + const rage_engine::ViewAngles angles{ + rage_engine::PitchAngle::from_degrees(20.f), + rage_engine::YawAngle::from_degrees(-35.f), + rage_engine::RollAngle::from_degrees(15.f), + }; + + EXPECT_EQ(rage_engine::MeshTrait::rotation_matrix(angles), rage_engine::rotation_matrix(angles)); +} diff --git a/tests/general/unit_test_prediction.cpp b/tests/general/unit_test_prediction.cpp index 6e856d8..ecc2136 100644 --- a/tests/general/unit_test_prediction.cpp +++ b/tests/general/unit_test_prediction.cpp @@ -283,6 +283,34 @@ TEST(ProjectileSimulation, HitsNegativeYawTarget_WithOffset) expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); } +TEST(ProjectileSimulation, HitsStaticTarget_WithAirFriction) +{ + constexpr Target target{ + .m_origin = {75, 0, 0}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + constexpr Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_speed = 100.f, .m_gravity_scale = 0.f, .m_air_friction = 0.5f}; + + const Engine engine(0.f, 1.f / 1000.f, 10.f, 0.1f); + const auto aim_angles = engine.maybe_calculate_aim_angles(proj, target); + + ASSERT_TRUE(aim_angles.has_value()); + EXPECT_NEAR(aim_angles->pitch, 0.f, 0.1f); + EXPECT_NEAR(aim_angles->yaw, 0.f, 0.1f); +} + +TEST(ProjectileSimulation, AirFrictionLimitsMaximumReach) +{ + constexpr Target target{ + .m_origin = {150, 0, 0}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + constexpr Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_speed = 100.f, .m_gravity_scale = 0.f, .m_air_friction = 1.f}; + + const Engine engine(0.f, 1.f / 1000.f, 10.f, 0.1f); + + EXPECT_FALSE(engine.maybe_calculate_aim_point(proj, target).has_value()); + EXPECT_FALSE(engine.maybe_calculate_aim_angles(proj, target).has_value()); +} + TEST(UnitTestPrediction, AimAnglesReturnsNulloptWhenNoSolution) { constexpr Target target{