From f26f48cc5374eda7b5cd56aea47132a70d23b66a Mon Sep 17 00:00:00 2001 From: Orange Date: Tue, 21 Oct 2025 00:22:00 +0300 Subject: [PATCH 1/2] Defines constants for Frostbite engine Introduces a constants header file for the Frostbite engine, including common vectors, matrices, and angle types. This provides a centralized location for defining and accessing essential mathematical constants used throughout the engine. --- .../engines/frostbite_engine/constants.hpp | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 include/omath/engines/frostbite_engine/constants.hpp diff --git a/include/omath/engines/frostbite_engine/constants.hpp b/include/omath/engines/frostbite_engine/constants.hpp new file mode 100644 index 0000000..6c99c99 --- /dev/null +++ b/include/omath/engines/frostbite_engine/constants.hpp @@ -0,0 +1,25 @@ +// +// Created by Vlad on 10/21/2025. +// + +#pragma once +#include "omath/linear_algebra/mat.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include +#include + +namespace omath::frostbite_engine +{ + constexpr Vector3 k_abs_up = {0, 1, 0}; + constexpr Vector3 k_abs_right = {1, 0, 0}; + constexpr Vector3 k_abs_forward = {0, 0, 1}; + + using Mat4X4 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; + using Mat3X3 = Mat<4, 4, 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::frostbite_engine \ No newline at end of file From 404c7de594321419a1629e7af2b4abf104e19676 Mon Sep 17 00:00:00 2001 From: Orange Date: Thu, 23 Oct 2025 00:08:17 +0300 Subject: [PATCH 2/2] added frostbite sources --- .../omath/engines/frostbite_engine/camera.hpp | 13 + .../engines/frostbite_engine/constants.hpp | 2 +- .../engines/frostbite_engine/formulas.hpp | 26 ++ .../frostbite_engine/traits/camera_trait.hpp | 24 ++ .../traits/pred_engine_trait.hpp | 76 ++++++ source/engines/frostbite_engine/formulas.cpp | 42 ++++ .../frostbite_engine/traits/camera_trait.cpp | 26 ++ tests/engines/unit_test_frostbite_engine.cpp | 236 ++++++++++++++++++ 8 files changed, 444 insertions(+), 1 deletion(-) create mode 100644 include/omath/engines/frostbite_engine/camera.hpp create mode 100644 include/omath/engines/frostbite_engine/formulas.hpp create mode 100644 include/omath/engines/frostbite_engine/traits/camera_trait.hpp create mode 100644 include/omath/engines/frostbite_engine/traits/pred_engine_trait.hpp create mode 100644 source/engines/frostbite_engine/formulas.cpp create mode 100644 source/engines/frostbite_engine/traits/camera_trait.cpp create mode 100644 tests/engines/unit_test_frostbite_engine.cpp diff --git a/include/omath/engines/frostbite_engine/camera.hpp b/include/omath/engines/frostbite_engine/camera.hpp new file mode 100644 index 0000000..e16c8b6 --- /dev/null +++ b/include/omath/engines/frostbite_engine/camera.hpp @@ -0,0 +1,13 @@ +// +// Created by Vlad on 3/22/2025. +// + +#pragma once +#include "omath/engines/frostbite_engine/constants.hpp" +#include "omath/projection/camera.hpp" +#include "traits/camera_trait.hpp" + +namespace omath::frostbite_engine +{ + using Camera = projection::Camera; +} // namespace omath::unity_engine \ No newline at end of file diff --git a/include/omath/engines/frostbite_engine/constants.hpp b/include/omath/engines/frostbite_engine/constants.hpp index 6c99c99..8c0aff1 100644 --- a/include/omath/engines/frostbite_engine/constants.hpp +++ b/include/omath/engines/frostbite_engine/constants.hpp @@ -17,7 +17,7 @@ namespace omath::frostbite_engine using Mat4X4 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; using Mat3X3 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; using Mat1X3 = Mat<1, 3, float, MatStoreType::ROW_MAJOR>; - using PitchAngle = Angle; + using PitchAngle = Angle; using YawAngle = Angle; using RollAngle = Angle; diff --git a/include/omath/engines/frostbite_engine/formulas.hpp b/include/omath/engines/frostbite_engine/formulas.hpp new file mode 100644 index 0000000..89a9274 --- /dev/null +++ b/include/omath/engines/frostbite_engine/formulas.hpp @@ -0,0 +1,26 @@ +// +// Created by Vlad on 3/22/2025. +// + +#pragma once +#include "omath/engines/frostbite_engine/constants.hpp" + +namespace omath::frostbite_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]] + Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far) noexcept; +} // namespace omath::unity_engine diff --git a/include/omath/engines/frostbite_engine/traits/camera_trait.hpp b/include/omath/engines/frostbite_engine/traits/camera_trait.hpp new file mode 100644 index 0000000..8152178 --- /dev/null +++ b/include/omath/engines/frostbite_engine/traits/camera_trait.hpp @@ -0,0 +1,24 @@ +// +// Created by Vlad on 8/10/2025. +// + +#pragma once +#include "omath/engines/frostbite_engine/formulas.hpp" +#include "omath/projection/camera.hpp" + +namespace omath::frostbite_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) noexcept; + }; + +} // namespace omath::unreal_engine \ No newline at end of file diff --git a/include/omath/engines/frostbite_engine/traits/pred_engine_trait.hpp b/include/omath/engines/frostbite_engine/traits/pred_engine_trait.hpp new file mode 100644 index 0000000..70314f7 --- /dev/null +++ b/include/omath/engines/frostbite_engine/traits/pred_engine_trait.hpp @@ -0,0 +1,76 @@ +// +// Created by Vlad on 8/6/2025. +// +#pragma once +#include "omath/engines/frostbite_engine/formulas.hpp" +#include "omath/projectile_prediction/projectile.hpp" +#include "omath/projectile_prediction/target.hpp" +#include + +namespace omath::frostbite_engine +{ + class PredEngineTrait final + { + public: + constexpr static Vector3 predict_projectile_position(const projectile_prediction::Projectile& projectile, + const float pitch, const float yaw, + const float time, const float gravity) noexcept + { + auto current_pos = projectile.m_origin + + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), + RollAngle::from_degrees(0)}) + * projectile.m_launch_speed * time; + current_pos.y -= (gravity * projectile.m_gravity_scale) * (time * time) * 0.5f; + + return current_pos; + } + [[nodiscard]] + static constexpr Vector3 predict_target_position(const projectile_prediction::Target& target, + const float time, const float gravity) noexcept + { + auto predicted = target.m_origin + target.m_velocity * time; + + if (target.m_is_airborne) + predicted.y -= gravity * (time * time) * 0.5f; + + return predicted; + } + [[nodiscard]] + static float calc_vector_2d_distance(const Vector3& delta) noexcept + { + return std::sqrt(delta.x * delta.x + delta.z * delta.z); + } + + [[nodiscard]] + constexpr static float get_vector_height_coordinate(const Vector3& vec) noexcept + { + return vec.y; + } + + [[nodiscard]] + static Vector3 calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, + Vector3 predicted_target_position, + const std::optional projectile_pitch) noexcept + { + 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())); + + return {predicted_target_position.x, predicted_target_position.y + height, projectile.m_origin.z}; + } + // Due to specification of maybe_calculate_projectile_launch_pitch_angle, pitch angle must be: + // 89 look up, -89 look down + [[nodiscard]] + static float calc_direct_pitch_angle(const Vector3& origin, const Vector3& view_to) noexcept + { + const auto direction = (view_to - origin).normalized(); + return angles::radians_to_degrees(std::asin(direction.y)); + } + [[nodiscard]] + static float calc_direct_yaw_angle(const Vector3& origin, const Vector3& view_to) noexcept + { + const auto direction = (view_to - origin).normalized(); + + return angles::radians_to_degrees(std::atan2(direction.x, direction.z)); + }; + }; +} // namespace omath::unity_engine diff --git a/source/engines/frostbite_engine/formulas.cpp b/source/engines/frostbite_engine/formulas.cpp new file mode 100644 index 0000000..021cdc7 --- /dev/null +++ b/source/engines/frostbite_engine/formulas.cpp @@ -0,0 +1,42 @@ +// +// Created by Vlad on 3/22/2025. +// +#include "omath/engines/frostbite_engine/formulas.hpp" + +namespace omath::frostbite_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.roll) + * mat_rotation_axis_y(angles.yaw) + * mat_rotation_axis_x(angles.pitch); + } + Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near, + const float far) noexcept + { + return mat_perspective_left_handed(field_of_view, aspect_ratio, near, far); + } +} // namespace omath::unity_engine diff --git a/source/engines/frostbite_engine/traits/camera_trait.cpp b/source/engines/frostbite_engine/traits/camera_trait.cpp new file mode 100644 index 0000000..8b44d4f --- /dev/null +++ b/source/engines/frostbite_engine/traits/camera_trait.cpp @@ -0,0 +1,26 @@ +// +// Created by Vlad on 8/11/2025. +// +#include "omath/engines/frostbite_engine/traits/camera_trait.hpp" + +namespace omath::frostbite_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.y)), + YawAngle::from_radians(std::atan2(direction.x, direction.z)), RollAngle::from_radians(0.f)}; + } + Mat4X4 CameraTrait::calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept + { + return frostbite_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) noexcept + { + return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far); + } +} // namespace omath::unity_engine \ No newline at end of file diff --git a/tests/engines/unit_test_frostbite_engine.cpp b/tests/engines/unit_test_frostbite_engine.cpp new file mode 100644 index 0000000..a10e8c1 --- /dev/null +++ b/tests/engines/unit_test_frostbite_engine.cpp @@ -0,0 +1,236 @@ +// +// Created by Vlad on 10/23/2025. +// +#include +#include +#include +#include +#include +#include + +TEST(unit_test_frostbite_engine, ForwardVector) +{ + const auto forward = omath::frostbite_engine::forward_vector({}); + + EXPECT_EQ(forward, omath::frostbite_engine::k_abs_forward); +} + +TEST(unit_test_frostbite_engine, ForwardVectorRotationYaw) +{ + omath::frostbite_engine::ViewAngles angles; + + angles.yaw = omath::frostbite_engine::YawAngle::from_degrees(90.f); + + const auto forward = omath::frostbite_engine::forward_vector(angles); + EXPECT_NEAR(forward.x, omath::frostbite_engine::k_abs_right.x, 0.00001f); + EXPECT_NEAR(forward.y, omath::frostbite_engine::k_abs_right.y, 0.00001f); + EXPECT_NEAR(forward.z, omath::frostbite_engine::k_abs_right.z, 0.00001f); +} + +TEST(unit_test_frostbite_engine, ForwardVectorRotationPitch) +{ + omath::frostbite_engine::ViewAngles angles; + + angles.pitch = omath::frostbite_engine::PitchAngle::from_degrees(-90.f); + + const auto forward = omath::frostbite_engine::forward_vector(angles); + EXPECT_NEAR(forward.x, omath::frostbite_engine::k_abs_up.x, 0.00001f); + EXPECT_NEAR(forward.y, omath::frostbite_engine::k_abs_up.y, 0.00001f); + EXPECT_NEAR(forward.z, omath::frostbite_engine::k_abs_up.z, 0.00001f); +} + +TEST(unit_test_frostbite_engine, ForwardVectorRotationRoll) +{ + omath::frostbite_engine::ViewAngles angles; + + angles.roll = omath::frostbite_engine::RollAngle::from_degrees(-90.f); + + const auto forward = omath::frostbite_engine::up_vector(angles); + EXPECT_NEAR(forward.x, omath::frostbite_engine::k_abs_right.x, 0.00001f); + EXPECT_NEAR(forward.y, omath::frostbite_engine::k_abs_right.y, 0.00001f); + EXPECT_NEAR(forward.z, omath::frostbite_engine::k_abs_right.z, 0.00001f); +} + +TEST(unit_test_frostbite_engine, RightVector) +{ + const auto right = omath::frostbite_engine::right_vector({}); + + EXPECT_EQ(right, omath::frostbite_engine::k_abs_right); +} + +TEST(unit_test_frostbite_engine, UpVector) +{ + const auto up = omath::frostbite_engine::up_vector({}); + EXPECT_EQ(up, omath::frostbite_engine::k_abs_up); +} + +TEST(unit_test_frostbite_engine, ProjectTargetMovedFromCamera) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(60.f); + const auto cam = omath::frostbite_engine::Camera({0, 0, 0}, {}, {1280.f, 720.f}, fov, 0.01f, 1000.f); + + for (float distance = 0.02f; distance < 100.f; distance += 0.01f) + { + const auto projected = cam.world_to_screen({0, 0, distance}); + + EXPECT_TRUE(projected.has_value()); + + if (!projected.has_value()) + continue; + + EXPECT_NEAR(projected->x, 640, 0.00001f); + EXPECT_NEAR(projected->y, 360, 0.00001f); + } +} +TEST(unit_test_frostbite_engine, Project) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(60.f); + + const auto cam = omath::frostbite_engine::Camera({0, 0, 0}, {}, {1280.f, 720.f}, fov, 0.03f, 1000.f); + const auto proj = cam.world_to_screen({10.f, 3, 10.f}); + + EXPECT_NEAR(proj->x, 1263.538, 0.001f); + EXPECT_NEAR(proj->y, 547.061f, 0.001f); +} + +TEST(unit_test_frostbite_engine, CameraSetAndGetFov) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::frostbite_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + EXPECT_EQ(cam.get_field_of_view().as_degrees(), 90.f); + cam.set_field_of_view(omath::projection::FieldOfView::from_degrees(50.f)); + + EXPECT_EQ(cam.get_field_of_view().as_degrees(), 50.f); +} + +TEST(unit_test_frostbite_engine, CameraSetAndGetOrigin) +{ + auto cam = omath::frostbite_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, {}, 0.01f, 1000.f); + + EXPECT_EQ(cam.get_origin(), omath::Vector3{}); + cam.set_field_of_view(omath::projection::FieldOfView::from_degrees(50.f)); + + EXPECT_EQ(cam.get_field_of_view().as_degrees(), 50.f); +} +TEST(unit_test_frostbite_engine, loook_at_random_all_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::frostbite_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{dist(gen), dist(gen), dist(gen)}; + + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.0001f || std::abs(projected_pos->y - 0.f) >= 0.0001f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} + +TEST(unit_test_frostbite_engine, loook_at_random_x_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::frostbite_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{dist(gen), 0.f, 0.f}; + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.001f || std::abs(projected_pos->y - 0.f) >= 0.001f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} + +TEST(unit_test_frostbite_engine, loook_at_random_y_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::frostbite_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{0.f, dist(gen), 0.f}; + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.01f || std::abs(projected_pos->y - 0.f) >= 0.01f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} + +TEST(unit_test_frostbite_engine, loook_at_random_z_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::frostbite_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{0.f, 0.f, dist(gen)}; + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.01f || std::abs(projected_pos->y - 0.f) >= 0.01f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} \ No newline at end of file