From de61f7a5d8b8f4d1950849ea0347c84a5409af76 Mon Sep 17 00:00:00 2001 From: Orange Date: Tue, 9 Sep 2025 01:31:23 +0300 Subject: [PATCH 1/3] Adds screen to world space conversion Adds functionality to convert screen coordinates to world space, including handling for cases where the inverse view projection matrix is singular or when the world position is out of screen bounds. Also exposes Camera class to unit tests. --- CMakeLists.txt | 1 + include/omath/projection/camera.hpp | 63 ++++++++++++++++++------ include/omath/projection/error_codes.hpp | 1 + tests/general/unit_test_projection.cpp | 5 +- 4 files changed, 54 insertions(+), 16 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ad862534..e98cb975 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,7 @@ target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23) if (OMATH_BUILD_TESTS) add_subdirectory(extlibs) add_subdirectory(tests) + target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_BUILD_TESTS) endif () if (OMATH_BUILD_EXAMPLES) diff --git a/include/omath/projection/camera.hpp b/include/omath/projection/camera.hpp index be44e8e8..b2c25150 100644 --- a/include/omath/projection/camera.hpp +++ b/include/omath/projection/camera.hpp @@ -4,13 +4,15 @@ #pragma once -#include "omath/projection/error_codes.hpp" #include "omath/linear_algebra/mat.hpp" #include "omath/linear_algebra/vector3.hpp" +#include "omath/projection/error_codes.hpp" #include #include #include - +#ifdef OMATH_BUILD_TESTS +class UnitTestProjection_Projection_Test; +#endif namespace omath::projection { class ViewPort final @@ -45,6 +47,8 @@ namespace omath::projection requires CameraEngineConcept class Camera final { + friend UnitTestProjection_Projection_Test; + public: ~Camera() = default; Camera(const Vector3& position, const ViewAnglesType& view_angles, const ViewPort& view_port, @@ -164,6 +168,31 @@ namespace omath::projection return Vector3{projected.at(0, 0), projected.at(1, 0), projected.at(2, 0)}; } + [[nodiscard]] + std::expected, Error> view_port_to_screen(const Vector3& ndc) const noexcept + { + const auto inv_view_proj = get_view_projection_matrix().inverted(); + + if (!inv_view_proj) + return std::unexpected(Error::INV_VIEW_PROJ_MAT_DET_EQ_ZERO); + + auto inverted_projection = + inv_view_proj.value() * mat_column_from_vector(ndc); + + if (!inverted_projection.at(3, 0)) + return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS); + + inverted_projection /= inverted_projection.at(3, 0); + + return Vector3{inverted_projection.at(0, 0), inverted_projection.at(1, 0), + inverted_projection.at(2, 0)}; + } + + [[nodiscard]] + std::expected, Error> screen_to_world(const Vector3& screen_pos) const noexcept + { + return view_port_to_screen(screen_to_dnc(screen_pos)); + } protected: ViewPort m_view_port{}; @@ -186,19 +215,25 @@ namespace omath::projection [[nodiscard]] Vector3 ndc_to_screen_position(const Vector3& ndc) const noexcept { -/* - ^ - | y - 1 | - | - | - -1 ---------0--------- 1 --> x - | - | - -1 | - v -*/ + /* + ^ + | y + 1 | + | + | + -1 ---------0--------- 1 --> x + | + | + -1 | + v + */ return {(ndc.x + 1.f) / 2.f * m_view_port.m_width, (1.f - ndc.y) / 2.f * m_view_port.m_height, ndc.z}; } + + [[nodiscard]] Vector3 screen_to_dnc(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}; + } }; } // namespace omath::projection diff --git a/include/omath/projection/error_codes.hpp b/include/omath/projection/error_codes.hpp index ae29fc07..0129af2c 100644 --- a/include/omath/projection/error_codes.hpp +++ b/include/omath/projection/error_codes.hpp @@ -10,5 +10,6 @@ namespace omath::projection enum class Error : uint16_t { WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS, + INV_VIEW_PROJ_MAT_DET_EQ_ZERO, }; } \ No newline at end of file diff --git a/tests/general/unit_test_projection.cpp b/tests/general/unit_test_projection.cpp index a5ca9a74..8d55d1a1 100644 --- a/tests/general/unit_test_projection.cpp +++ b/tests/general/unit_test_projection.cpp @@ -13,8 +13,9 @@ TEST(UnitTestProjection, Projection) const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); - const auto projected = cam.world_to_screen({1000, 0, 50}); - + const auto projected = cam.world_to_screen({1000, 0, 50.f}); + const auto result = cam.screen_to_world(projected.value()); + const auto result2 = cam.world_to_screen(result.value()); EXPECT_NEAR(projected->x, 960.f, 0.001f); EXPECT_NEAR(projected->y, 504.f, 0.001f); EXPECT_NEAR(projected->z, 1.f, 0.001f); From 69f46abce1d5affde35d52cf6ef4d22df1bdab1b Mon Sep 17 00:00:00 2001 From: Orange Date: Tue, 9 Sep 2025 01:37:38 +0300 Subject: [PATCH 2/3] Adds projection test for world-to-screen consistency Adds a test to verify the consistency of world-to-screen and screen-to-world projections. This ensures that projecting a point from world to screen and back results in the same point, thereby validating the correctness of the camera projection transformations. --- tests/general/unit_test_projection.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/general/unit_test_projection.cpp b/tests/general/unit_test_projection.cpp index 8d55d1a1..03c5e42a 100644 --- a/tests/general/unit_test_projection.cpp +++ b/tests/general/unit_test_projection.cpp @@ -16,6 +16,9 @@ TEST(UnitTestProjection, Projection) const auto projected = cam.world_to_screen({1000, 0, 50.f}); const auto result = cam.screen_to_world(projected.value()); const auto result2 = cam.world_to_screen(result.value()); + + EXPECT_EQ(static_cast>(projected.value()), + static_cast>(result2.value())); EXPECT_NEAR(projected->x, 960.f, 0.001f); EXPECT_NEAR(projected->y, 504.f, 0.001f); EXPECT_NEAR(projected->z, 1.f, 0.001f); From 418b7c0e7e189fdec016a9aa4797a1e992e47898 Mon Sep 17 00:00:00 2001 From: Orange Date: Tue, 9 Sep 2025 02:13:45 +0300 Subject: [PATCH 3/3] Fixes float type conversion in world_to_screen Fixes a potential type conversion issue by explicitly casting the x-coordinate to float in the world_to_screen test. This prevents possible compiler warnings and ensures the intended behavior. --- include/omath/projection/camera.hpp | 6 +++++- tests/general/unit_test_projection.cpp | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/include/omath/projection/camera.hpp b/include/omath/projection/camera.hpp index b2c25150..6d84cb62 100644 --- a/include/omath/projection/camera.hpp +++ b/include/omath/projection/camera.hpp @@ -10,9 +10,12 @@ #include #include #include + #ifdef OMATH_BUILD_TESTS +// ReSharper disable once CppInconsistentNaming class UnitTestProjection_Projection_Test; #endif + namespace omath::projection { class ViewPort final @@ -47,8 +50,9 @@ namespace omath::projection requires CameraEngineConcept class Camera final { +#ifdef OMATH_BUILD_TESTS friend UnitTestProjection_Projection_Test; - +#endif public: ~Camera() = default; Camera(const Vector3& position, const ViewAnglesType& view_angles, const ViewPort& view_port, diff --git a/tests/general/unit_test_projection.cpp b/tests/general/unit_test_projection.cpp index 03c5e42a..b2ba6d7f 100644 --- a/tests/general/unit_test_projection.cpp +++ b/tests/general/unit_test_projection.cpp @@ -13,7 +13,7 @@ TEST(UnitTestProjection, Projection) const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); - const auto projected = cam.world_to_screen({1000, 0, 50.f}); + const auto projected = cam.world_to_screen({1000.f, 0, 50.f}); const auto result = cam.screen_to_world(projected.value()); const auto result2 = cam.world_to_screen(result.value());