diff --git a/include/omath/Mat.h b/include/omath/Mat.h new file mode 100644 index 0000000..2923f90 --- /dev/null +++ b/include/omath/Mat.h @@ -0,0 +1,303 @@ +// +// Created by vlad on 9/29/2024. +// +#pragma once +#include +#include +#include +#include "Vector3.h" +#include +#include "Angles.h" + + +namespace omath +{ + template + class Mat final + { + public: + + constexpr Mat() + { + Clear(); + } + + + constexpr Mat(const std::initializer_list>& rows) + { + if (rows.size() != Rows) + throw std::invalid_argument("Initializer list rows size does not match template parameter Rows"); + + auto rowIt = rows.begin(); + for (size_t i = 0; i < Rows; ++i, ++rowIt) + { + if (rowIt->size() != Columns) + throw std::invalid_argument("All rows must have the same number of columns as template parameter Columns"); + + auto colIt = rowIt->begin(); + for (size_t j = 0; j < Columns; ++j, ++colIt) + { + At(i, j) = *colIt; + } + } + } + + + constexpr Mat(const Mat& other) + { + for (size_t i = 0; i < Rows; ++i) + for (size_t j = 0; j < Columns; ++j) + At(i, j) = other.At(i, j); + } + + + constexpr Mat(Mat&& other) noexcept + { + for (size_t i = 0; i < Rows; ++i) + for (size_t j = 0; j < Columns; ++j) + At(i, j) = other.At(i, j) ; + } + + [[nodiscard]] + static constexpr size_t RowCount() noexcept { return Rows; } + + [[nodiscard]] + static constexpr size_t ColumnsCount() noexcept { return Columns; } + + [[nodiscard]] + static constexpr std::pair Size() noexcept { return { Rows, Columns }; } + + + [[nodiscard]] constexpr const float& At(const size_t rowIndex, const size_t columnIndex) const + { + if (rowIndex >= Rows || columnIndex >= Columns) + throw std::out_of_range("Index out of range"); + + return m_data[rowIndex * Columns + columnIndex]; + } + [[nodiscard]] constexpr float& At(const size_t rowIndex, const size_t columnIndex) + { + return const_cast(std::as_const(*this).At(rowIndex, columnIndex)); + } + [[nodiscard]] + constexpr float Sum() const + { + float sum = 0.f; + for (size_t i = 0; i < Rows; ++i) + for (size_t j = 0; j < Columns; ++j) + sum += At(i, j); + + return sum; + } + + constexpr void Clear() + { + for (size_t i = 0; i < Rows; ++i) + for (size_t j = 0; j < Columns; ++j) + At(i, j) = 0.f; + } + + // Operator overloading for multiplication with another Mat + template + constexpr Mat operator*(const Mat& other) const + { + Mat result; + + for (size_t i = 0; i < Rows; ++i) + for (size_t j = 0; j < OtherColumns; ++j) + { + float sum = 0.f; + for (size_t k = 0; k < Columns; ++k) + sum += At(i, k) * other.At(k, j); + result.At(i, j) = sum; + } + return result; + } + + constexpr Mat& operator*=(float f) + { + for (size_t i = 0; i < Rows; ++i) + for (size_t j = 0; j < Columns; ++j) + At(i, j) *= f; + return *this; + } + + template + constexpr Mat operator*=(const Mat& other) + { + return *this = *this * other; + } + + constexpr Mat operator*(float f) const + { + Mat result(*this); + result *= f; + return result; + } + + constexpr Mat& operator/=(float f) + { + for (size_t i = 0; i < Rows; ++i) + for (size_t j = 0; j < Columns; ++j) + At(i, j) /= f; + return *this; + } + + constexpr Mat operator/(float f) const + { + Mat result(*this); + result /= f; + return result; + } + + constexpr Mat& operator=(const Mat& other) + { + if (this == &other) + return *this; + for (size_t i = 0; i < Rows; ++i) + for (size_t j = 0; j < Columns; ++j) + At(i, j) = other.At(i, j); + return *this; + } + + constexpr Mat& operator=(Mat&& other) noexcept + { + if (this == &other) + return *this; + + for (size_t i = 0; i < Rows; ++i) + for (size_t j = 0; j < Columns; ++j) + At(i, j) = other.At(i, j); + + return *this; + } + + [[nodiscard]] + constexpr Mat Transpose() const + { + Mat transposed; + for (size_t i = 0; i < Rows; ++i) + for (size_t j = 0; j < Columns; ++j) + transposed.At(j, i) = At(i, j); + + return transposed; + } + + [[nodiscard]] + constexpr float Determinant() const + { + static_assert(Rows == Columns, "Determinant is only defined for square matrices."); + + if constexpr (Rows == 1) + return At(0, 0); + + else if constexpr (Rows == 2) + return At(0, 0) * At(1, 1) - At(0, 1) * At(1, 0); + else + { + float det = 0.f; + for (size_t i = 0; i < Columns; ++i) + { + const float cofactor = (i % 2 == 0 ? 1.f : -1.f) * At(0, i) * Minor(0, i).Determinant(); + det += cofactor; + } + return det; + } + } + + [[nodiscard]] + constexpr Mat Minor(const size_t row, const size_t column) const + { + Mat result; + for (size_t i = 0, m = 0; i < Rows; ++i) + { + if (i == row) + continue; + for (size_t j = 0, n = 0; j < Columns; ++j) + { + if (j == column) + continue; + result.At(m, n) = At(i, j); + ++n; + } + ++m; + } + return result; + } + + [[nodiscard]] + std::string ToString() const + { + std::ostringstream oss; + for (size_t i = 0; i < Rows; ++i) + { + for (size_t j = 0; j < Columns; ++j) + { + oss << At(i, j); + if (j != Columns - 1) + oss << ' '; + } + oss << '\n'; + } + return oss.str(); + } + + // Static methods that return fixed-size matrices + [[nodiscard]] + constexpr static Mat<4, 4> ToScreenMat(const float screenWidth, const float screenHeight) + { + return + { + {screenWidth / 2.f, 0.f, 0.f, 0.f}, + {0.f, -screenHeight / 2.f, 0.f, 0.f}, + {0.f, 0.f, 1.f, 0.f}, + {screenWidth / 2.f, screenHeight / 2.f, 0.f, 1.f}, + }; + } + + [[nodiscard]] + constexpr static Mat<4, 4> TranslationMat(const Vector3& diff) + { + return + { + {1.f, 0.f, 0.f, 0.f}, + {0.f, 1.f, 0.f, 0.f}, + {0.f, 0.f, 1.f, 0.f}, + {diff.x, diff.y, diff.z, 1.f}, + }; + } + + [[nodiscard]] + constexpr static Mat<4, 4> OrientationMat(const Vector3& forward, const Vector3& right, const Vector3& up) + { + Mat<4, 4> mat; + + return + { + {right.x, up.x, forward.x, 0.f}, + {right.y, up.y, forward.y, 0.f}, + {right.z, up.z, forward.z, 0.f}, + {0.f, 0.f, 0.f, 1.f}, + }; + + return mat; + } + + [[nodiscard]] + constexpr static Mat<4, 4> ProjectionMat(const float fieldOfView, const float aspectRatio, const float near, const float far) + { + const float fovHalfTan = std::tan(angles::DegreesToRadians(fieldOfView) / 2.f); + + return + { + {1.f / (aspectRatio * fovHalfTan), 0.f, 0.f, 0.f}, + {0.f, 1.f / fovHalfTan, 0.f, 0.f}, + {0.f, 0.f, (far + near) / (far - near), 2.f * near * far / (far - near)}, + {0.f, 0.f, -1.f, 0.f} + }; + } + + private: + std::array m_data; + }; +} \ No newline at end of file diff --git a/include/omath/projection/Camera.h b/include/omath/projection/Camera.h index a3bfbe1..4d37e67 100644 --- a/include/omath/projection/Camera.h +++ b/include/omath/projection/Camera.h @@ -6,7 +6,7 @@ #include #include -#include +#include #include #include "ErrorCodes.h" @@ -28,7 +28,7 @@ namespace omath::projection Camera(const Vector3& position, const Vector3& viewAngles, const ViewPort& viewPort, float fov, float near, float far); void SetViewAngles(const Vector3& viewAngles); - [[nodiscard]] Matrix GetViewMatrix() const; + [[nodiscard]] Mat<4, 4> GetViewMatrix() const; [[nodiscard]] std::expected WorldToScreen(const Vector3& worldPosition) const; diff --git a/source/projection/Camera.cpp b/source/projection/Camera.cpp index 5f67a6e..0b49e70 100644 --- a/source/projection/Camera.cpp +++ b/source/projection/Camera.cpp @@ -21,24 +21,24 @@ namespace omath::projection m_farPlaneDistance = far; } - Matrix Camera::GetViewMatrix() const + Mat<4, 4> Camera::GetViewMatrix() const { const auto forward = Vector3::ForwardVector(m_viewAngles.x, m_viewAngles.y); const auto right = Vector3::RightVector(m_viewAngles.x, m_viewAngles.y, m_viewAngles.z); const auto up = Vector3::UpVector(m_viewAngles.x, m_viewAngles.y, m_viewAngles.z); - return Matrix::TranslationMatrix(-m_origin) * Matrix::OrientationMatrix(forward, right, up); + return Mat<4, 4>::TranslationMat(-m_origin) * Mat<4, 4>::OrientationMat(forward, right, up); } std::expected Camera::WorldToScreen(const Vector3 &worldPosition) const { - const auto posVecAsMatrix = Matrix({{worldPosition.x, worldPosition.y, worldPosition.z, 1.f}}); + const auto posVecAsMatrix = Mat<1, 4>({{worldPosition.x, worldPosition.y, worldPosition.z, 1.f}}); - const auto projectionMatrix = Matrix::ProjectionMatrix(m_fieldOfView, m_viewPort.AspectRatio(), + const auto projectionMatrix = Mat<4, 4>::ProjectionMat(m_fieldOfView, m_viewPort.AspectRatio(), m_nearPlaneDistance, m_farPlaneDistance); - auto projected = posVecAsMatrix * (GetViewMatrix() * projectionMatrix); + Mat<1, 4> projected = posVecAsMatrix * (GetViewMatrix() * projectionMatrix); if (projected.At(0, 3) <= 0.f) return std::unexpected(Error::WORLD_POSITION_IS_BEHIND_CAMERA); @@ -49,7 +49,7 @@ namespace omath::projection projected.At(0, 1) < -1.f || projected.At(0, 1) > 1.f) return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS); - projected *= Matrix::ToScreenMatrix(m_viewPort.m_width, m_viewPort.m_height); + projected *= Mat<4, 4>::ToScreenMat(m_viewPort.m_width, m_viewPort.m_height); return Vector2{projected.At(0, 0), projected.At(0, 1)}; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 920fae4..90f0bea 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,6 +7,7 @@ include(GoogleTest) add_executable(unit-tests UnitTestPrediction.cpp UnitTestMatrix.cpp + UnitTestMat.cpp UnitTestAstar.cpp UnitTestProjection.cpp UnitTestVector3.cpp diff --git a/tests/UnitTestMat.cpp b/tests/UnitTestMat.cpp new file mode 100644 index 0000000..7a0048c --- /dev/null +++ b/tests/UnitTestMat.cpp @@ -0,0 +1,245 @@ +// UnitTestMat.cpp +#include +#include "omath/Mat.h" +#include "omath/Vector3.h" + +using namespace omath; + +class UnitTestMat : public ::testing::Test +{ +protected: + Mat<2, 2> m1; + Mat<2, 2> m2; + + void SetUp() override + { + m1 = Mat<2, 2>(); + m2 = Mat<2, 2>{{1.0f, 2.0f}, {3.0f, 4.0f}}; + } +}; + +// Test constructors +TEST_F(UnitTestMat, Constructor_Default) +{ + Mat<3, 3> m; + EXPECT_EQ(m.RowCount(), 3); + EXPECT_EQ(m.ColumnsCount(), 3); + for (size_t i = 0; i < 3; ++i) + for (size_t j = 0; j < 3; ++j) + EXPECT_FLOAT_EQ(m.At(i, j), 0.0f); +} + +TEST_F(UnitTestMat, Constructor_InitializerList) +{ + constexpr Mat<2, 2> m{{1.0f, 2.0f}, {3.0f, 4.0f}}; + EXPECT_EQ(m.RowCount(), 2); + EXPECT_EQ(m.ColumnsCount(), 2); + EXPECT_FLOAT_EQ(m.At(0, 0), 1.0f); + EXPECT_FLOAT_EQ(m.At(0, 1), 2.0f); + EXPECT_FLOAT_EQ(m.At(1, 0), 3.0f); + EXPECT_FLOAT_EQ(m.At(1, 1), 4.0f); +} + +TEST_F(UnitTestMat, Constructor_Copy) +{ + Mat<2, 2> m3 = m2; + EXPECT_EQ(m3.RowCount(), m2.RowCount()); + EXPECT_EQ(m3.ColumnsCount(), m2.ColumnsCount()); + EXPECT_FLOAT_EQ(m3.At(0, 0), m2.At(0, 0)); + EXPECT_FLOAT_EQ(m3.At(1, 1), m2.At(1, 1)); +} + +TEST_F(UnitTestMat, Constructor_Move) +{ + Mat<2, 2> m3 = std::move(m2); + EXPECT_EQ(m3.RowCount(), 2); + EXPECT_EQ(m3.ColumnsCount(), 2); + EXPECT_FLOAT_EQ(m3.At(0, 0), 1.0f); + EXPECT_FLOAT_EQ(m3.At(1, 1), 4.0f); + // m2 is in a valid but unspecified state after move +} + +// Test matrix operations +TEST_F(UnitTestMat, Operator_Multiplication_Matrix) +{ + Mat<2, 2> m3 = m2 * m2; + EXPECT_EQ(m3.RowCount(), 2); + EXPECT_EQ(m3.ColumnsCount(), 2); + EXPECT_FLOAT_EQ(m3.At(0, 0), 7.0f); + EXPECT_FLOAT_EQ(m3.At(0, 1), 10.0f); + EXPECT_FLOAT_EQ(m3.At(1, 0), 15.0f); + EXPECT_FLOAT_EQ(m3.At(1, 1), 22.0f); +} + +TEST_F(UnitTestMat, Operator_Multiplication_Scalar) +{ + Mat<2, 2> m3 = m2 * 2.0f; + EXPECT_FLOAT_EQ(m3.At(0, 0), 2.0f); + EXPECT_FLOAT_EQ(m3.At(1, 1), 8.0f); +} + +TEST_F(UnitTestMat, Operator_Division_Scalar) +{ + Mat<2, 2> m3 = m2 / 2.0f; + EXPECT_FLOAT_EQ(m3.At(0, 0), 0.5f); + EXPECT_FLOAT_EQ(m3.At(1, 1), 2.0f); +} + +// Test matrix functions +TEST_F(UnitTestMat, Transpose) +{ + Mat<2, 2> m3 = m2.Transpose(); + EXPECT_FLOAT_EQ(m3.At(0, 0), m2.At(0, 0)); + EXPECT_FLOAT_EQ(m3.At(0, 1), m2.At(1, 0)); + EXPECT_FLOAT_EQ(m3.At(1, 0), m2.At(0, 1)); + EXPECT_FLOAT_EQ(m3.At(1, 1), m2.At(1, 1)); +} + +TEST_F(UnitTestMat, Determinant) +{ + const float det = m2.Determinant(); + EXPECT_FLOAT_EQ(det, -2.0f); +} + +TEST_F(UnitTestMat, Sum) +{ + const float sum = m2.Sum(); + EXPECT_FLOAT_EQ(sum, 10.0f); +} + +TEST_F(UnitTestMat, Clear) +{ + m2.Clear(); + for (size_t i = 0; i < m2.RowCount(); ++i) + for (size_t j = 0; j < m2.ColumnsCount(); ++j) + EXPECT_FLOAT_EQ(m2.At(i, j), 0.0f); +} + +TEST_F(UnitTestMat, ToString) +{ + const std::string str = m2.ToString(); + EXPECT_FALSE(str.empty()); + EXPECT_EQ(str, "1 2\n3 4\n"); +} + +// Test assignment operators +TEST_F(UnitTestMat, AssignmentOperator_Copy) +{ + Mat<2, 2> m3; + m3 = m2; + EXPECT_EQ(m3.RowCount(), m2.RowCount()); + EXPECT_EQ(m3.ColumnsCount(), m2.ColumnsCount()); + EXPECT_FLOAT_EQ(m3.At(0, 0), m2.At(0, 0)); +} + +TEST_F(UnitTestMat, AssignmentOperator_Move) +{ + Mat<2, 2> m3; + m3 = std::move(m2); + EXPECT_EQ(m3.RowCount(), 2); + EXPECT_EQ(m3.ColumnsCount(), 2); + EXPECT_FLOAT_EQ(m3.At(0, 0), 1.0f); + EXPECT_FLOAT_EQ(m3.At(1, 1), 4.0f); + // m2 is in a valid but unspecified state after move +} + +// Test static methods +TEST_F(UnitTestMat, StaticMethod_ToScreenMat) +{ + Mat<4, 4> screenMat = Mat<4, 4>::ToScreenMat(800.0f, 600.0f); + EXPECT_FLOAT_EQ(screenMat.At(0, 0), 400.0f); + EXPECT_FLOAT_EQ(screenMat.At(1, 1), -300.0f); + EXPECT_FLOAT_EQ(screenMat.At(3, 0), 400.0f); + EXPECT_FLOAT_EQ(screenMat.At(3, 1), 300.0f); + EXPECT_FLOAT_EQ(screenMat.At(3, 3), 1.0f); +} + +// Test static method: TranslationMat +TEST_F(UnitTestMat, StaticMethod_TranslationMat) +{ + Vector3 diff{10.0f, 20.0f, 30.0f}; + Mat<4, 4> transMat = Mat<4, 4>::TranslationMat(diff); + EXPECT_FLOAT_EQ(transMat.At(0, 0), 1.0f); + EXPECT_FLOAT_EQ(transMat.At(3, 0), diff.x); + EXPECT_FLOAT_EQ(transMat.At(3, 1), diff.y); + EXPECT_FLOAT_EQ(transMat.At(3, 2), diff.z); + EXPECT_FLOAT_EQ(transMat.At(3, 3), 1.0f); +} + +// Test static method: OrientationMat +TEST_F(UnitTestMat, StaticMethod_OrientationMat) +{ + constexpr Vector3 forward{0.0f, 0.0f, 1.0f}; + constexpr Vector3 right{1.0f, 0.0f, 0.0f}; + constexpr Vector3 up{0.0f, 1.0f, 0.0f}; + constexpr Mat<4, 4> orientMat = Mat<4, 4>::OrientationMat(forward, right, up); + EXPECT_FLOAT_EQ(orientMat.At(0, 0), right.x); + EXPECT_FLOAT_EQ(orientMat.At(0, 1), up.x); + EXPECT_FLOAT_EQ(orientMat.At(0, 2), forward.x); + EXPECT_FLOAT_EQ(orientMat.At(1, 0), right.y); + EXPECT_FLOAT_EQ(orientMat.At(1, 1), up.y); + EXPECT_FLOAT_EQ(orientMat.At(1, 2), forward.y); + EXPECT_FLOAT_EQ(orientMat.At(2, 0), right.z); + EXPECT_FLOAT_EQ(orientMat.At(2, 1), up.z); + EXPECT_FLOAT_EQ(orientMat.At(2, 2), forward.z); +} + +// Test static method: ProjectionMat +TEST_F(UnitTestMat, StaticMethod_ProjectionMat) +{ + constexpr float fieldOfView = 45.0f; + constexpr float aspectRatio = 1.33f; + constexpr float near = 0.1f; + constexpr float far = 100.0f; + const Mat<4, 4> projMat = Mat<4, 4>::ProjectionMat(fieldOfView, aspectRatio, near, far); + + const float fovHalfTan = std::tan(angles::DegreesToRadians(fieldOfView) / 2.f); + + EXPECT_FLOAT_EQ(projMat.At(0, 0), 1.f / (aspectRatio * fovHalfTan)); + EXPECT_FLOAT_EQ(projMat.At(1, 1), 1.f / fovHalfTan); + EXPECT_FLOAT_EQ(projMat.At(2, 2), (far + near) / (far - near)); + EXPECT_FLOAT_EQ(projMat.At(2, 3), (2.f * near * far) / (far - near)); + EXPECT_FLOAT_EQ(projMat.At(3, 2), -1.f); +} + +// Test exception handling in At() method +TEST_F(UnitTestMat, Method_At_OutOfRange) +{ + EXPECT_THROW(std::ignore = m2.At(2, 0), std::out_of_range); + EXPECT_THROW(std::ignore = m2.At(0, 2), std::out_of_range); +} + +// Test Determinant for 3x3 matrix +TEST(UnitTestMatStandalone, Determinant_3x3) +{ + constexpr auto det = Mat<3, 3>{{6, 1, 1}, {4, -2, 5}, {2, 8, 7}}.Determinant(); + EXPECT_FLOAT_EQ(det, -306.0f); +} + +// Test Minor for 3x3 matrix +TEST(UnitTestMatStandalone, Minor_3x3) +{ + constexpr Mat<3, 3> m{{3, 0, 2}, {2, 0, -2}, {0, 1, 1}}; + auto minor = m.Minor(0, 0); + EXPECT_EQ(minor.RowCount(), 2); + EXPECT_EQ(minor.ColumnsCount(), 2); + EXPECT_FLOAT_EQ(minor.At(0, 0), 0.0f); + EXPECT_FLOAT_EQ(minor.At(0, 1), -2.0f); + EXPECT_FLOAT_EQ(minor.At(1, 0), 1.0f); + EXPECT_FLOAT_EQ(minor.At(1, 1), 1.0f); +} + +// Test Transpose for non-square matrix +TEST(UnitTestMatStandalone, Transpose_NonSquare) +{ + constexpr Mat<2, 3> m{{1.0f, 2.0f, 3.0f}, {4.0f, 5.0f, 6.0f}}; + auto transposed = m.Transpose(); + EXPECT_EQ(transposed.RowCount(), 3); + EXPECT_EQ(transposed.ColumnsCount(), 2); + EXPECT_FLOAT_EQ(transposed.At(0, 0), 1.0f); + EXPECT_FLOAT_EQ(transposed.At(1, 0), 2.0f); + EXPECT_FLOAT_EQ(transposed.At(2, 0), 3.0f); + EXPECT_FLOAT_EQ(transposed.At(0, 1), 4.0f); + EXPECT_FLOAT_EQ(transposed.At(1, 1), 5.0f); + EXPECT_FLOAT_EQ(transposed.At(2, 1), 6.0f); +}