diff --git a/include/omath/projection/camera.hpp b/include/omath/projection/camera.hpp index 6675297..35bb1f0 100644 --- a/include/omath/projection/camera.hpp +++ b/include/omath/projection/camera.hpp @@ -7,13 +7,17 @@ #include "omath/linear_algebra/mat.hpp" #include "omath/linear_algebra/vector3.hpp" #include "omath/projection/error_codes.hpp" -#include #include +#include #include #ifdef OMATH_BUILD_TESTS -// ReSharper disable once CppInconsistentNaming +// ReSharper disable CppInconsistentNaming class UnitTestProjection_Projection_Test; +class UnitTestProjection_ScreenToNdcTopLeft_Test; +class UnitTestProjection_ScreenToNdcBottomLeft_Test; +// ReSharper restore CppInconsistentNaming + #endif namespace omath::projection @@ -52,6 +56,8 @@ namespace omath::projection { #ifdef OMATH_BUILD_TESTS friend UnitTestProjection_Projection_Test; + friend UnitTestProjection_ScreenToNdcTopLeft_Test; + friend UnitTestProjection_ScreenToNdcBottomLeft_Test; #endif public: enum class ScreenStart @@ -152,7 +158,6 @@ namespace omath::projection return m_origin; } - template [[nodiscard]] std::expected, Error> world_to_screen(const Vector3& world_position) const noexcept @@ -206,17 +211,19 @@ namespace omath::projection inverted_projection.at(2, 0)}; } + template [[nodiscard]] std::expected, Error> screen_to_world(const Vector3& screen_pos) const noexcept { - return view_port_to_screen(screen_to_ndc(screen_pos)); + return view_port_to_screen(screen_to_ndc(screen_pos)); } + template [[nodiscard]] std::expected, Error> screen_to_world(const Vector2& screen_pos) const noexcept { const auto& [x, y] = screen_pos; - return screen_to_world({x, y, 1.f}); + return screen_to_world({x, y, 1.f}); } protected: @@ -286,10 +293,17 @@ namespace omath::projection return {(ndc.x + 1.f) / 2.f * m_view_port.m_width, (ndc.y / 2.f + 0.5f) * m_view_port.m_height, ndc.z}; } + template [[nodiscard]] Vector3 screen_to_ndc(const Vector3& screen_pos) const noexcept { - return {screen_pos.x / m_view_port.m_width * 2.f - 1.f, 1.f - screen_pos.y / m_view_port.m_height * 2.f, - screen_pos.z}; + if constexpr (screen_start == ScreenStart::TOP_LEFT_CORNER) + return {screen_pos.x / m_view_port.m_width * 2.f - 1.f, 1.f - screen_pos.y / m_view_port.m_height * 2.f, + screen_pos.z}; + else if (screen_start == ScreenStart::BOTTOM_LEFT_CORNER) + return {screen_pos.x / m_view_port.m_width * 2.f - 1.f, + (screen_pos.y / m_view_port.m_height - 0.5f) * 2.f, screen_pos.z}; + else + std::unreachable(); } }; } // namespace omath::projection diff --git a/tests/general/unit_test_projection.cpp b/tests/general/unit_test_projection.cpp index b2ba6d7..f507b01 100644 --- a/tests/general/unit_test_projection.cpp +++ b/tests/general/unit_test_projection.cpp @@ -1,11 +1,13 @@ // // Created by Vlad on 27.08.2024. // +#include "omath/engines/unity_engine/camera.hpp" #include #include #include #include #include +#include TEST(UnitTestProjection, Projection) { @@ -22,4 +24,76 @@ TEST(UnitTestProjection, Projection) EXPECT_NEAR(projected->x, 960.f, 0.001f); EXPECT_NEAR(projected->y, 504.f, 0.001f); EXPECT_NEAR(projected->z, 1.f, 0.001f); +} +TEST(UnitTestProjection, ScreenToNdcTopLeft) +{ + constexpr auto fov = omath::Angle::from_degrees(90.f); + const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov, + 0.01f, 1000.f); + using ScreenStart = omath::source_engine::Camera::ScreenStart; + + const auto ndc_top_left = cam.screen_to_ndc({1500, 300, 1.f}); + EXPECT_NEAR(ndc_top_left.x, 0.5625f, 0.0001f); + EXPECT_NEAR(ndc_top_left.y, 0.4444f, 0.0001f); +} + +TEST(UnitTestProjection, ScreenToNdcBottomLeft) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(60.f); + + const auto cam = omath::unity_engine::Camera({0, 0, 0}, {}, {1280.f, 720.f}, fov, 0.03f, 1000.f); + using ScreenStart = omath::unity_engine::Camera::ScreenStart; + + const auto ndc_bottom_left = + cam.screen_to_ndc({1263.53833f, 547.061523f, 0.99405992f}); + EXPECT_NEAR(ndc_bottom_left.x, 0.974278628f, 0.0001f); + EXPECT_NEAR(ndc_bottom_left.y, 0.519615293f, 0.0001f); +} + +TEST(UnitTestProjection, ScreenToWorldTopLeftCorner) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + + std::uniform_real_distribution dist_x(1.f, 1900.f); + std::uniform_real_distribution dist_y(1.f, 1070.f); + + constexpr auto fov = omath::Angle::from_degrees(90.f); + const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov, + 0.01f, 1000.f); + using ScreenStart = omath::source_engine::Camera::ScreenStart; + + for (int i = 0; i < 100; i++) + { + const auto initial_screen_cords = omath::Vector2{dist_x(gen), dist_y(gen)}; + + const auto world_cords = cam.screen_to_world(initial_screen_cords); + const auto screen_cords = cam.world_to_screen(world_cords.value()); + + EXPECT_NEAR(screen_cords->x, initial_screen_cords.x, 0.001f); + EXPECT_NEAR(screen_cords->y, initial_screen_cords.y, 0.001f); + } +} + +TEST(UnitTestProjection, ScreenToWorldBottomLeftCorner) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + + std::uniform_real_distribution dist_x(1.f, 1900.f); + std::uniform_real_distribution dist_y(1.f, 1070.f); + + constexpr auto fov = omath::Angle::from_degrees(90.f); + const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov, + 0.01f, 1000.f); + using ScreenStart = omath::source_engine::Camera::ScreenStart; + + for (int i = 0; i < 100; i++) + { + const auto initial_screen_cords = omath::Vector2{dist_x(gen), dist_y(gen)}; + + const auto world_cords = cam.screen_to_world(initial_screen_cords); + const auto screen_cords = cam.world_to_screen(world_cords.value()); + + EXPECT_NEAR(screen_cords->x, initial_screen_cords.x, 0.001f); + EXPECT_NEAR(screen_cords->y, initial_screen_cords.y, 0.001f); + } } \ No newline at end of file