From b48160e1b701e980827690b0a2a3c2bb8a18ddb2 Mon Sep 17 00:00:00 2001 From: Orange Date: Sat, 8 Nov 2025 13:51:56 +0300 Subject: [PATCH] Improves screen to world conversion accuracy Adds support for different screen origin configurations. This change allows for more accurate conversion from screen coordinates to world coordinates by correctly handling different screen origin positions (top-left and bottom-left). Includes new unit tests to verify the functionality with both configurations. --- include/omath/projection/camera.hpp | 28 +++++++--- tests/general/unit_test_projection.cpp | 74 ++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 7 deletions(-) diff --git a/include/omath/projection/camera.hpp b/include/omath/projection/camera.hpp index 6675297a..35bb1f07 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 b2ba6d7f..f507b01d 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