// // Created by Vlad on 27.08.2024. // #pragma once #include "omath/linear_algebra/mat.hpp" #include "omath/linear_algebra/triangle.hpp" #include "omath/linear_algebra/vector3.hpp" #include "omath/projection/error_codes.hpp" #include #include #include #include #ifdef OMATH_BUILD_TESTS // ReSharper disable CppInconsistentNaming class UnitTestProjection_Projection_Test; class UnitTestProjection_ScreenToNdcTopLeft_Test; class UnitTestProjection_ScreenToNdcBottomLeft_Test; // ReSharper restore CppInconsistentNaming #endif namespace omath::projection { class ViewPort final { public: float m_width; float m_height; [[nodiscard]] constexpr float aspect_ratio() const { return m_width / m_height; } }; using FieldOfView = Angle; template concept CameraEngineConcept = requires(const Vector3& cam_origin, const Vector3& look_at, const ViewAnglesType& angles, const FieldOfView& fov, const ViewPort& viewport, float znear, float zfar) { // Presence + return types { T::calc_look_at_angle(cam_origin, look_at) } -> std::same_as; { T::calc_view_matrix(angles, cam_origin) } -> std::same_as; { T::calc_projection_matrix(fov, viewport, znear, zfar) } -> std::same_as; // Enforce noexcept as in the trait declaration requires noexcept(T::calc_look_at_angle(cam_origin, look_at)); requires noexcept(T::calc_view_matrix(angles, cam_origin)); requires noexcept(T::calc_projection_matrix(fov, viewport, znear, zfar)); }; template requires CameraEngineConcept class Camera final { #ifdef OMATH_BUILD_TESTS friend UnitTestProjection_Projection_Test; friend UnitTestProjection_ScreenToNdcTopLeft_Test; friend UnitTestProjection_ScreenToNdcBottomLeft_Test; #endif public: enum class ScreenStart { TOP_LEFT_CORNER, BOTTOM_LEFT_CORNER, }; ~Camera() = default; Camera(const Vector3& position, const ViewAnglesType& view_angles, const ViewPort& view_port, const FieldOfView& fov, const float near, const float far) noexcept : m_view_port(view_port), m_field_of_view(fov), m_far_plane_distance(far), m_near_plane_distance(near), m_view_angles(view_angles), m_origin(position) { } void look_at(const Vector3& target) { m_view_angles = TraitClass::calc_look_at_angle(m_origin, target); m_view_projection_matrix = std::nullopt; m_view_matrix = std::nullopt; } [[nodiscard]] ViewAnglesType calc_look_at_angles(const Vector3& look_to) const { return TraitClass::calc_look_at_angle(m_origin, look_to); } [[nodiscard]] Vector3 get_forward() const noexcept { const auto& view_matrix = get_view_matrix(); if constexpr (inverted_z) return -Vector3{view_matrix[2, 0], view_matrix[2, 1], view_matrix[2, 2]}; return {view_matrix[2, 0], view_matrix[2, 1], view_matrix[2, 2]}; } [[nodiscard]] Vector3 get_right() const noexcept { const auto& view_matrix = get_view_matrix(); return {view_matrix[0, 0], view_matrix[0, 1], view_matrix[0, 2]}; } [[nodiscard]] Vector3 get_up() const noexcept { const auto& view_matrix = get_view_matrix(); return {view_matrix[1, 0], view_matrix[1, 1], view_matrix[1, 2]}; } [[nodiscard]] const Mat4X4Type& get_view_projection_matrix() const noexcept { if (!m_view_projection_matrix.has_value()) m_view_projection_matrix = get_projection_matrix() * get_view_matrix(); return m_view_projection_matrix.value(); } [[nodiscard]] const Mat4X4Type& get_view_matrix() const noexcept { if (!m_view_matrix.has_value()) m_view_matrix = TraitClass::calc_view_matrix(m_view_angles, m_origin); return m_view_matrix.value(); } [[nodiscard]] const Mat4X4Type& get_projection_matrix() const noexcept { if (!m_projection_matrix.has_value()) m_projection_matrix = TraitClass::calc_projection_matrix(m_field_of_view, m_view_port, m_near_plane_distance, m_far_plane_distance); return m_projection_matrix.value(); } void set_field_of_view(const FieldOfView& fov) noexcept { m_field_of_view = fov; m_view_projection_matrix = std::nullopt; m_projection_matrix = std::nullopt; } void set_near_plane(const float near_plane) noexcept { m_near_plane_distance = near_plane; m_view_projection_matrix = std::nullopt; m_projection_matrix = std::nullopt; } void set_far_plane(const float far_plane) noexcept { m_far_plane_distance = far_plane; m_view_projection_matrix = std::nullopt; m_projection_matrix = std::nullopt; } void set_view_angles(const ViewAnglesType& view_angles) noexcept { m_view_angles = view_angles; m_view_projection_matrix = std::nullopt; m_view_matrix = std::nullopt; } void set_origin(const Vector3& origin) noexcept { m_origin = origin; m_view_projection_matrix = std::nullopt; m_view_matrix = std::nullopt; } void set_view_port(const ViewPort& view_port) noexcept { m_view_port = view_port; m_view_projection_matrix = std::nullopt; m_projection_matrix = std::nullopt; } [[nodiscard]] const FieldOfView& get_field_of_view() const noexcept { return m_field_of_view; } [[nodiscard]] const float& get_near_plane() const noexcept { return m_near_plane_distance; } [[nodiscard]] const float& get_far_plane() const noexcept { return m_far_plane_distance; } [[nodiscard]] const ViewAnglesType& get_view_angles() const noexcept { return m_view_angles; } [[nodiscard]] const Vector3& get_origin() const noexcept { return m_origin; } template [[nodiscard]] std::expected, Error> world_to_screen(const Vector3& world_position) const noexcept { 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(); } [[nodiscard]] bool is_culled_by_frustum(const Triangle>& triangle) const noexcept { // Transform to clip space (before perspective divide) auto to_clip = [this](const Vector3& point) { auto clip = get_view_projection_matrix() * mat_column_from_vector(point); return std::array{ clip.at(0, 0), // x clip.at(1, 0), // y clip.at(2, 0), // z clip.at(3, 0) // w }; }; const auto c0 = to_clip(triangle.m_vertex1); const auto c1 = to_clip(triangle.m_vertex2); const auto c2 = to_clip(triangle.m_vertex3); // If all vertices are behind the camera (w <= 0), trivially reject if (c0[3] <= 0.f && c1[3] <= 0.f && c2[3] <= 0.f) return true; // Helper: all three vertices outside the same clip plane auto all_outside_plane = [](const int axis, const std::array& a, const std::array& b, const std::array& c, const bool positive_side) { if (positive_side) return a[axis] > a[3] && b[axis] > b[3] && c[axis] > c[3]; return a[axis] < -a[3] && b[axis] < -b[3] && c[axis] < -c[3]; }; // Clip volume in clip space (OpenGL-style): // -w <= x <= w // -w <= y <= w // -w <= z <= w for (int i = 0; i < 3; i++) { if (all_outside_plane(i, c0, c1, c2, false)) return true; // x < -w (left) if (all_outside_plane(i, c0, c1, c2, true)) return true; // x > w (right) } return false; } [[nodiscard]] std::expected, Error> world_to_view_port(const Vector3& world_position) const noexcept { auto projected = get_view_projection_matrix() * mat_column_from_vector(world_position); const auto& w = projected.at(3, 0); if (w <= std::numeric_limits::epsilon()) return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS); projected /= w; if (is_ndc_out_of_bounds(projected)) return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS); 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); const auto& w = inverted_projection.at(3, 0); if (std::abs(w) < std::numeric_limits::epsilon()) return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS); inverted_projection /= w; return Vector3{inverted_projection.at(0, 0), inverted_projection.at(1, 0), 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)); } 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}); } protected: ViewPort m_view_port{}; Angle m_field_of_view; mutable std::optional m_view_projection_matrix; mutable std::optional m_projection_matrix; mutable std::optional m_view_matrix; float m_far_plane_distance; float m_near_plane_distance; ViewAnglesType m_view_angles; Vector3 m_origin; private: template [[nodiscard]] constexpr static bool is_ndc_out_of_bounds(const Type& ndc) noexcept { constexpr auto eps = std::numeric_limits::epsilon(); return std::ranges::any_of(ndc.raw_array(), [](const auto& val) { return val < -1.0f - eps || val > 1.0f + eps; }); } // NDC REPRESENTATION: /* ^ | y 1 | | | -1 ---------0--------- 1 --> x | | -1 | v */ [[nodiscard]] Vector3 ndc_to_screen_position_from_top_left_corner(const Vector3& ndc) const noexcept { /* +------------------------> | (0, 0) | | | | | | ⌄ */ 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}; } [[nodiscard]] Vector3 ndc_to_screen_position_from_bottom_left_corner(const Vector3& ndc) const noexcept { /* ^ | | | | | | | (0, 0) +------------------------> */ 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 { 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 constexpr (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