replaced enum

This commit is contained in:
2026-03-21 14:53:04 +03:00
parent 8063c1697a
commit 5a4c042fec
2 changed files with 130 additions and 4 deletions

View File

@@ -36,7 +36,11 @@ namespace omath::projection
} }
}; };
using FieldOfView = Angle<float, 0.f, 180.f, AngleFlags::Clamped>; using FieldOfView = Angle<float, 0.f, 180.f, AngleFlags::Clamped>;
enum class ViewPortClipping
{
AUTO,
MANUAL,
};
template<class T, class MatType, class ViewAnglesType> template<class T, class MatType, class ViewAnglesType>
concept CameraEngineConcept = concept CameraEngineConcept =
requires(const Vector3<float>& cam_origin, const Vector3<float>& look_at, const ViewAnglesType& angles, requires(const Vector3<float>& cam_origin, const Vector3<float>& look_at, const ViewAnglesType& angles,
@@ -222,7 +226,7 @@ namespace omath::projection
[[nodiscard]] std::expected<Vector3<float>, Error> [[nodiscard]] std::expected<Vector3<float>, Error>
not_clip_world_to_screen(const Vector3<float>& world_position) const noexcept not_clip_world_to_screen(const Vector3<float>& world_position) const noexcept
{ {
const auto normalized_cords = world_to_view_port(world_position, false); const auto normalized_cords = world_to_view_port(world_position, ViewPortClipping::MANUAL);
if (!normalized_cords.has_value()) if (!normalized_cords.has_value())
return std::unexpected{normalized_cords.error()}; return std::unexpected{normalized_cords.error()};
@@ -283,7 +287,8 @@ namespace omath::projection
} }
[[nodiscard]] std::expected<Vector3<float>, Error> [[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() auto projected = get_view_projection_matrix()
* mat_column_from_vector<float, Mat4X4Type::get_store_ordering()>(world_position); * mat_column_from_vector<float, Mat4X4Type::get_store_ordering()>(world_position);
@@ -294,7 +299,7 @@ namespace omath::projection
projected /= w; projected /= w;
if (auto_clip && is_ndc_out_of_bounds(projected)) if (clipping == ViewPortClipping::MANUAL && is_ndc_out_of_bounds(projected))
return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS); 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)}; return Vector3<float>{projected.at(0, 0), projected.at(1, 0), projected.at(2, 0)};

View File

@@ -50,6 +50,127 @@ TEST(UnitTestProjection, ScreenToNdcBottomLeft)
EXPECT_NEAR(ndc_bottom_left.y, 0.519615293f, 0.0001f); EXPECT_NEAR(ndc_bottom_left.y, 0.519615293f, 0.0001f);
} }
TEST(UnitTestProjection, NotClipWorldToScreenInBounds)
{
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.not_clip_world_to_screen({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, NotClipWorldToScreenMatchesWorldToScreenWhenInBounds)
{
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.not_clip_world_to_screen({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, NotClipWorldToScreenRejectsBehindCamera)
{
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.not_clip_world_to_screen({-1000.f, 0, 0});
EXPECT_FALSE(projected.has_value());
EXPECT_EQ(projected.error(), omath::projection::Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS);
}
TEST(UnitTestProjection, NotClipWorldToScreenRejectsOutOfBoundsNdc)
{
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 should exceed NDC [-1,1] bounds
const auto projected = cam.not_clip_world_to_screen({100.f, 5000.f, 0});
EXPECT_FALSE(projected.has_value());
EXPECT_EQ(projected.error(), omath::projection::Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS);
}
TEST(UnitTestProjection, WorldToScreenAllowsOutOfBoundsNdc)
{
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 not_clip rejects should succeed with world_to_screen
const auto projected = cam.world_to_screen({100.f, 5000.f, 0});
EXPECT_TRUE(projected.has_value());
}
TEST(UnitTestProjection, NotClipWorldToScreenBottomLeftCorner)
{
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.not_clip_world_to_screen<ScreenStart::TOP_LEFT_CORNER>({1000.f, 0, 50.f});
const auto bottom_left = cam.not_clip_world_to_screen<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, NotClipWorldToScreenRoundTrip)
{
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.not_clip_world_to_screen(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.not_clip_world_to_screen(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, NotClipWorldToScreenUnityEngine)
{
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.not_clip_world_to_screen<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) TEST(UnitTestProjection, ScreenToWorldTopLeftCorner)
{ {
std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source