mirror of
https://github.com/orange-cpp/omath.git
synced 2026-04-19 05:43:26 +00:00
Compare commits
8 Commits
7567501f00
...
v5.1.0.rc6
| Author | SHA1 | Date | |
|---|---|---|---|
| 308f7ed481 | |||
| 8802ad9af1 | |||
| 2ac508d6e8 | |||
| eb1ca6055b | |||
| b528e41de3 | |||
| 8615ab2b7c | |||
| 5a4c042fec | |||
| 8063c1697a |
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(ls:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,11 @@ namespace omath::projection
|
||||
}
|
||||
};
|
||||
using FieldOfView = Angle<float, 0.f, 180.f, AngleFlags::Clamped>;
|
||||
|
||||
enum class ViewPortClipping
|
||||
{
|
||||
AUTO,
|
||||
MANUAL,
|
||||
};
|
||||
template<class T, class MatType, class ViewAnglesType>
|
||||
concept CameraEngineConcept =
|
||||
requires(const Vector3<float>& cam_origin, const Vector3<float>& look_at, const ViewAnglesType& angles,
|
||||
@@ -204,9 +208,25 @@ namespace omath::projection
|
||||
|
||||
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
|
||||
[[nodiscard]] std::expected<Vector3<float>, Error>
|
||||
world_to_screen(const Vector3<float>& world_position, const bool auto_clip = true) const noexcept
|
||||
world_to_screen(const Vector3<float>& world_position) const noexcept
|
||||
{
|
||||
const auto normalized_cords = world_to_view_port(world_position, auto_clip);
|
||||
const auto normalized_cords = world_to_view_port(world_position);
|
||||
|
||||
if (!normalized_cords.has_value())
|
||||
return std::unexpected{normalized_cords.error()};
|
||||
|
||||
if constexpr (screen_start == ScreenStart::TOP_LEFT_CORNER)
|
||||
return ndc_to_screen_position_from_top_left_corner(*normalized_cords);
|
||||
else if constexpr (screen_start == ScreenStart::BOTTOM_LEFT_CORNER)
|
||||
return ndc_to_screen_position_from_bottom_left_corner(*normalized_cords);
|
||||
else
|
||||
std::unreachable();
|
||||
}
|
||||
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
|
||||
[[nodiscard]] std::expected<Vector3<float>, Error>
|
||||
world_to_screen_unclipped(const Vector3<float>& world_position) const noexcept
|
||||
{
|
||||
const auto normalized_cords = world_to_view_port(world_position, ViewPortClipping::MANUAL);
|
||||
|
||||
if (!normalized_cords.has_value())
|
||||
return std::unexpected{normalized_cords.error()};
|
||||
@@ -267,18 +287,28 @@ namespace omath::projection
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<Vector3<float>, Error>
|
||||
world_to_view_port(const Vector3<float>& world_position, const bool auto_clip = true) const noexcept
|
||||
world_to_view_port(const Vector3<float>& world_position,
|
||||
const ViewPortClipping& clipping = ViewPortClipping::AUTO) const noexcept
|
||||
{
|
||||
auto projected = get_view_projection_matrix()
|
||||
* mat_column_from_vector<float, Mat4X4Type::get_store_ordering()>(world_position);
|
||||
|
||||
const auto& w = projected.at(3, 0);
|
||||
if (w <= std::numeric_limits<float>::epsilon())
|
||||
return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS);
|
||||
constexpr auto eps = std::numeric_limits<float>::epsilon();
|
||||
if (w <= eps)
|
||||
return std::unexpected(Error::PERSPECTIVE_DIVIDER_LESS_EQ_ZERO);
|
||||
|
||||
projected /= w;
|
||||
|
||||
if (auto_clip && is_ndc_out_of_bounds(projected))
|
||||
// ReSharper disable once CppTooWideScope
|
||||
const auto clipped_automatically = clipping == ViewPortClipping::AUTO && is_ndc_out_of_bounds(projected);
|
||||
if (clipped_automatically)
|
||||
return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS);
|
||||
|
||||
// ReSharper disable once CppTooWideScope
|
||||
const auto clipped_manually = clipping == ViewPortClipping::MANUAL && (projected.at(2, 0) < 0.0f - eps
|
||||
|| projected.at(2, 0) > 1.0f + eps);
|
||||
if (clipped_manually)
|
||||
return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS);
|
||||
|
||||
return Vector3<float>{projected.at(0, 0), projected.at(1, 0), projected.at(2, 0)};
|
||||
|
||||
@@ -11,5 +11,6 @@ namespace omath::projection
|
||||
{
|
||||
WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS,
|
||||
INV_VIEW_PROJ_MAT_DET_EQ_ZERO,
|
||||
PERSPECTIVE_DIVIDER_LESS_EQ_ZERO,
|
||||
};
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
//
|
||||
#ifdef OMATH_ENABLE_LUA
|
||||
#include "omath/lua/lua.hpp"
|
||||
#include "omath/omath.hpp"
|
||||
#include "omath/projection/error_codes.hpp"
|
||||
#include <omath/engines/cry_engine/camera.hpp>
|
||||
#include <omath/engines/frostbite_engine/camera.hpp>
|
||||
#include <omath/engines/iw_engine/camera.hpp>
|
||||
@@ -33,6 +35,8 @@ namespace
|
||||
return "world position is out of screen bounds";
|
||||
case omath::projection::Error::INV_VIEW_PROJ_MAT_DET_EQ_ZERO:
|
||||
return "inverse view-projection matrix determinant is zero";
|
||||
case omath::projection::Error::PERSPECTIVE_DIVIDER_LESS_EQ_ZERO:
|
||||
return "perspective divider is less or equal to zero";
|
||||
}
|
||||
return "unknown error";
|
||||
}
|
||||
|
||||
@@ -50,6 +50,126 @@ TEST(UnitTestProjection, ScreenToNdcBottomLeft)
|
||||
EXPECT_NEAR(ndc_bottom_left.y, 0.519615293f, 0.0001f);
|
||||
}
|
||||
|
||||
TEST(UnitTestProjection, UnclippedWorldToScreenInBounds)
|
||||
{
|
||||
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);
|
||||
|
||||
const auto projected = cam.world_to_screen_unclipped({1000.f, 0, 50.f});
|
||||
ASSERT_TRUE(projected.has_value());
|
||||
EXPECT_NEAR(projected->x, 960.f, 0.001f);
|
||||
EXPECT_NEAR(projected->y, 504.f, 0.001f);
|
||||
}
|
||||
|
||||
TEST(UnitTestProjection, UnclippedWorldToScreenMatchesWorldToScreenWhenInBounds)
|
||||
{
|
||||
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);
|
||||
|
||||
const auto w2s = cam.world_to_screen({1000.f, 0, 50.f});
|
||||
const auto no_clip = cam.world_to_screen_unclipped({1000.f, 0, 50.f});
|
||||
|
||||
ASSERT_TRUE(w2s.has_value());
|
||||
ASSERT_TRUE(no_clip.has_value());
|
||||
EXPECT_NEAR(w2s->x, no_clip->x, 0.001f);
|
||||
EXPECT_NEAR(w2s->y, no_clip->y, 0.001f);
|
||||
EXPECT_NEAR(w2s->z, no_clip->z, 0.001f);
|
||||
}
|
||||
|
||||
TEST(UnitTestProjection, UnclippedWorldToScreenRejectsBehindCamera)
|
||||
{
|
||||
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);
|
||||
|
||||
const auto projected = cam.world_to_screen_unclipped({-1000.f, 0, 0});
|
||||
EXPECT_FALSE(projected.has_value());
|
||||
EXPECT_EQ(projected.error(), omath::projection::Error::PERSPECTIVE_DIVIDER_LESS_EQ_ZERO);
|
||||
}
|
||||
|
||||
TEST(UnitTestProjection, UnclippedWorldToScreenAllowsOutOfBoundsNdc)
|
||||
{
|
||||
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);
|
||||
|
||||
// Point far to the side exceeds NDC [-1,1] bounds but unclipped returns it anyway
|
||||
const auto projected = cam.world_to_screen_unclipped({100.f, 5000.f, 0});
|
||||
EXPECT_TRUE(projected.has_value());
|
||||
}
|
||||
|
||||
TEST(UnitTestProjection, WorldToScreenRejectsOutOfBoundsNdc)
|
||||
{
|
||||
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);
|
||||
|
||||
// Same point that unclipped allows — clipped world_to_screen rejects it
|
||||
const auto projected = cam.world_to_screen({100.f, 5000.f, 0});
|
||||
EXPECT_FALSE(projected.has_value());
|
||||
}
|
||||
|
||||
TEST(UnitTestProjection, UnclippedWorldToScreenBottomLeftCorner)
|
||||
{
|
||||
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 top_left = cam.world_to_screen_unclipped<ScreenStart::TOP_LEFT_CORNER>({1000.f, 0, 50.f});
|
||||
const auto bottom_left = cam.world_to_screen_unclipped<ScreenStart::BOTTOM_LEFT_CORNER>({1000.f, 0, 50.f});
|
||||
|
||||
ASSERT_TRUE(top_left.has_value());
|
||||
ASSERT_TRUE(bottom_left.has_value());
|
||||
// X should be identical, Y should differ (mirrored around center)
|
||||
EXPECT_NEAR(top_left->x, bottom_left->x, 0.001f);
|
||||
EXPECT_NEAR(top_left->y + bottom_left->y, 1080.f, 0.001f);
|
||||
}
|
||||
|
||||
TEST(UnitTestProjection, UnclippedWorldToScreenRoundTrip)
|
||||
{
|
||||
std::mt19937 gen(42);
|
||||
std::uniform_real_distribution dist_fwd(100.f, 900.f);
|
||||
std::uniform_real_distribution dist_side(-400.f, 400.f);
|
||||
std::uniform_real_distribution dist_up(-200.f, 200.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);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
const omath::Vector3<float> world_pos{dist_fwd(gen), dist_side(gen), dist_up(gen)};
|
||||
const auto screen = cam.world_to_screen_unclipped(world_pos);
|
||||
if (!screen.has_value())
|
||||
continue;
|
||||
|
||||
const auto back_to_world = cam.screen_to_world(screen.value());
|
||||
ASSERT_TRUE(back_to_world.has_value());
|
||||
|
||||
const auto back_to_screen = cam.world_to_screen_unclipped(back_to_world.value());
|
||||
ASSERT_TRUE(back_to_screen.has_value());
|
||||
|
||||
EXPECT_NEAR(screen->x, back_to_screen->x, 0.01f);
|
||||
EXPECT_NEAR(screen->y, back_to_screen->y, 0.01f);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(UnitTestProjection, UnclippedWorldToScreenUnityEngine)
|
||||
{
|
||||
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;
|
||||
|
||||
// Point directly in front
|
||||
const auto projected = cam.world_to_screen_unclipped<ScreenStart::BOTTOM_LEFT_CORNER>({0, 0, 500.f});
|
||||
ASSERT_TRUE(projected.has_value());
|
||||
EXPECT_NEAR(projected->x, 640.f, 0.5f);
|
||||
EXPECT_NEAR(projected->y, 360.f, 0.5f);
|
||||
}
|
||||
|
||||
TEST(UnitTestProjection, ScreenToWorldTopLeftCorner)
|
||||
{
|
||||
std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source
|
||||
|
||||
Reference in New Issue
Block a user