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.
This commit is contained in:
2025-11-08 13:51:56 +03:00
parent d4a16a94e6
commit b48160e1b7
2 changed files with 95 additions and 7 deletions

View File

@@ -7,13 +7,17 @@
#include "omath/linear_algebra/mat.hpp"
#include "omath/linear_algebra/vector3.hpp"
#include "omath/projection/error_codes.hpp"
#include <omath/trigonometry/angle.hpp>
#include <expected>
#include <omath/trigonometry/angle.hpp>
#include <type_traits>
#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<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
[[nodiscard]] std::expected<Vector3<float>, Error>
world_to_screen(const Vector3<float>& world_position) const noexcept
@@ -206,17 +211,19 @@ namespace omath::projection
inverted_projection.at(2, 0)};
}
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
[[nodiscard]]
std::expected<Vector3<float>, Error> screen_to_world(const Vector3<float>& screen_pos) const noexcept
{
return view_port_to_screen(screen_to_ndc(screen_pos));
return view_port_to_screen(screen_to_ndc<screen_start>(screen_pos));
}
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
[[nodiscard]]
std::expected<Vector3<float>, Error> screen_to_world(const Vector2<float>& screen_pos) const noexcept
{
const auto& [x, y] = screen_pos;
return screen_to_world({x, y, 1.f});
return screen_to_world<screen_start>({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<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
[[nodiscard]] Vector3<float> screen_to_ndc(const Vector3<float>& 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

View File

@@ -1,11 +1,13 @@
//
// Created by Vlad on 27.08.2024.
//
#include "omath/engines/unity_engine/camera.hpp"
#include <complex>
#include <gtest/gtest.h>
#include <omath/engines/source_engine/camera.hpp>
#include <omath/projection/camera.hpp>
#include <print>
#include <random>
TEST(UnitTestProjection, Projection)
{
@@ -23,3 +25,75 @@ TEST(UnitTestProjection, Projection)
EXPECT_NEAR(projected->y, 504.f, 0.001f);
EXPECT_NEAR(projected->z, 1.f, 0.001f);
}
TEST(UnitTestProjection, ScreenToNdcTopLeft)
{
constexpr auto fov = omath::Angle<float, 0.f, 180.f, omath::AngleFlags::Clamped>::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<ScreenStart::TOP_LEFT_CORNER>({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<ScreenStart::BOTTOM_LEFT_CORNER>({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<float, 0.f, 180.f, omath::AngleFlags::Clamped>::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<ScreenStart::TOP_LEFT_CORNER>(initial_screen_cords);
const auto screen_cords = cam.world_to_screen<ScreenStart::TOP_LEFT_CORNER>(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<float, 0.f, 180.f, omath::AngleFlags::Clamped>::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<ScreenStart::BOTTOM_LEFT_CORNER>(initial_screen_cords);
const auto screen_cords = cam.world_to_screen<ScreenStart::BOTTOM_LEFT_CORNER>(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);
}
}