From 5b6b02bfd4b12e9eec33166c671aac5185ce91f8 Mon Sep 17 00:00:00 2001 From: "C. J. Howard" Date: Tue, 3 Sep 2024 19:27:34 -0700 Subject: [PATCH] Add quaternion type unit tests. Add quaternion::swap(). Remove quaternion to vector typecast. --- src/engine/math/quaternion-types.hpp | 105 +++++++------- src/game/systems/constraint-system.cpp | 3 +- test/test-math.cpp | 186 ++++++++++++++++++++++++- 3 files changed, 244 insertions(+), 50 deletions(-) diff --git a/src/engine/math/quaternion-types.hpp b/src/engine/math/quaternion-types.hpp index 39ebcc49..c49001bd 100644 --- a/src/engine/math/quaternion-types.hpp +++ b/src/engine/math/quaternion-types.hpp @@ -9,6 +9,7 @@ #include #include #include +#include // export module math.quaternion:type; // import math.constants; @@ -53,7 +54,7 @@ struct quaternion /// @} - /// @name Element access + /// @name Part access /// @{ /// @{ @@ -105,41 +106,9 @@ struct quaternion /// @} /// @} - - /** - * Returns a quaternion representing a rotation about the x-axis. - * - * @param angle Angle of rotation, in radians. - * - * @return Quaternion representing an x-axis rotation. - */ - [[nodiscard]] static quaternion rotate_x(scalar_type angle) - { - return {cos(angle * T{0.5}), sin(angle * T{0.5}), T{0}, T{0}}; - } - - /** - * Returns a quaternion representing a rotation about the y-axis. - * - * @param angle Angle of rotation, in radians. - * - * @return Quaternion representing an y-axis rotation. - */ - [[nodiscard]] static quaternion rotate_y(scalar_type angle) - { - return {cos(angle * T{0.5}), T{0}, sin(angle * T{0.5}), T{0}}; - } - - /** - * Returns a quaternion representing a rotation about the z-axis. - * - * @param angle Angle of rotation, in radians. - * @return Quaternion representing an z-axis rotation. - */ - [[nodiscard]] static quaternion rotate_z(scalar_type angle) - { - return {cos(angle * T{0.5}), T{0}, T{0}, sin(angle * T{0.5})}; - } + + /// @name Conversion + /// @{ /** * Type-casts the quaternion scalars using `static_cast`. @@ -154,13 +123,12 @@ struct quaternion return {static_cast(r), vec3(i)}; } - /// @{ /** * Constructs a matrix representing the rotation described by the quaternion. * * @return Rotation matrix. */ - [[nodiscard]] constexpr explicit operator matrix_type() const noexcept + [[nodiscard]] inline constexpr matrix_type matrix() const noexcept { const T xx = x() * x(); const T xy = x() * y(); @@ -179,22 +147,30 @@ struct quaternion {(xz + yw) * T{2}, (yz - xw) * T{2}, T{1} - (xx + yy) * T{2}} }}; } - - [[nodiscard]] inline constexpr matrix_type matrix() const noexcept + + /// @copydoc matrix() + [[nodiscard]] constexpr explicit operator matrix_type() const noexcept { - return matrix_type(*this); + return matrix(); } + /// @} + + /// @name Operations + /// @{ /** - * Casts the quaternion to a 4-element vector, with the real part as the first element and the imaginary part as the following three elements. + * Exchanges the parts of this quaternion with the parts of another. * - * @return Vector containing the real and imaginary parts of the quaternion. + * @param other Quaternion with which to exchange parts. */ - [[nodiscard]] inline constexpr explicit operator vec4() const noexcept + [[nodiscard]] inline constexpr void swap(quaternion& other) noexcept { - return {r, i.x(), i.y(), i.z()}; - } + std::swap(r, other.r); + std::swap(i, other.i); + }; + + /// @} /// @name Comparison /// @{ @@ -207,13 +183,48 @@ struct quaternion [[nodiscard]] inline constexpr friend bool operator==(const quaternion&, const quaternion&) noexcept = default; /** - * Compares the elements of two quaternions lexicographically. + * Compares the parts of two quaternions lexicographically. * * @return Lexicographical ordering of the two quaternions. */ [[nodiscard]] inline constexpr friend auto operator<=>(const quaternion&, const quaternion&) noexcept = default; /// @} + + /** + * Returns a quaternion representing a rotation about the x-axis. + * + * @param angle Angle of rotation, in radians. + * + * @return Quaternion representing an x-axis rotation. + */ + [[nodiscard]] static quaternion rotate_x(scalar_type angle) + { + return {cos(angle * T{0.5}), sin(angle * T{0.5}), T{0}, T{0}}; + } + + /** + * Returns a quaternion representing a rotation about the y-axis. + * + * @param angle Angle of rotation, in radians. + * + * @return Quaternion representing an y-axis rotation. + */ + [[nodiscard]] static quaternion rotate_y(scalar_type angle) + { + return {cos(angle * T{0.5}), T{0}, sin(angle * T{0.5}), T{0}}; + } + + /** + * Returns a quaternion representing a rotation about the z-axis. + * + * @param angle Angle of rotation, in radians. + * @return Quaternion representing an z-axis rotation. + */ + [[nodiscard]] static quaternion rotate_z(scalar_type angle) + { + return {cos(angle * T{0.5}), T{0}, T{0}, sin(angle * T{0.5})}; + } }; /// @name Tuple-like interface diff --git a/src/game/systems/constraint-system.cpp b/src/game/systems/constraint-system.cpp index 83d7ebfe..1a107d47 100644 --- a/src/game/systems/constraint-system.cpp +++ b/src/game/systems/constraint-system.cpp @@ -290,7 +290,8 @@ void constraint_system::handle_spring_to_constraint(transform_component& transfo if (constraint.spring_rotation) { // Update rotation spring target - constraint.rotation.set_target_value(math::fvec4(target_transform->world.rotation)); + const auto& r = target_transform->world.rotation; + constraint.rotation.set_target_value(math::fvec4{r.w(), r.x(), r.y(), r.z()}); // Solve rotation spring constraint.rotation.solve(dt); diff --git a/test/test-math.cpp b/test/test-math.cpp index 6f56d98c..1b691b75 100644 --- a/test/test-math.cpp +++ b/test/test-math.cpp @@ -197,7 +197,7 @@ int main(int argc, char* argv[]) suite.tests.emplace_back("vector comparison", []() { ivec3 a{1, 2, 3}; - ivec3 b{2, 3, 4}; + ivec3 b{1, 2, 4}; ivec3 c{1, 2, 3}; ASSERT_EQ(a, c); @@ -470,7 +470,7 @@ int main(int argc, char* argv[]) suite.tests.emplace_back("matrix comparison", []() { imat2 a{1, 2, 3, 4}; - imat2 b{2, 3, 4, 5}; + imat2 b{1, 2, 3, 5}; imat2 c{1, 2, 3, 4}; ASSERT_EQ(a, c); @@ -523,6 +523,188 @@ int main(int argc, char* argv[]) ASSERT_EQ(str, "{{-0.4700, 0.0000, 0.6667}, {inf, 1000.3457, -0.0000}}"); }); + suite.tests.emplace_back("quaternion initialization", []() + { + fquat a{}; + ASSERT_EQ(a.w(), 0.0f); + ASSERT_EQ(a.x(), 0.0f); + ASSERT_EQ(a.y(), 0.0f); + ASSERT_EQ(a.z(), 0.0f); + + fquat b{1.0f, 2.0f, 3.0f, 4.0f}; + ASSERT_EQ(b.w(), 1.0f); + ASSERT_EQ(b.x(), 2.0f); + ASSERT_EQ(b.y(), 3.0f); + ASSERT_EQ(b.z(), 4.0f); + + fquat c{5.0f, {6.0f, 7.0f, 8.0f}}; + ASSERT_EQ(c.w(), 5.0f); + ASSERT_EQ(c.x(), 6.0f); + ASSERT_EQ(c.y(), 7.0f); + ASSERT_EQ(c.z(), 8.0f); + + fvec3 di{-2.0f, -3.0f, -4.0f}; + fquat d{-1.0f, di}; + ASSERT_EQ(d.w(), -1.0f); + ASSERT_EQ(d.x(), -2.0f); + ASSERT_EQ(d.y(), -3.0f); + ASSERT_EQ(d.z(), -4.0f); + }); + + suite.tests.emplace_back("quaternion part access", []() + { + fquat a{1.0f, 2.0f, 3.0f, 4.0f}; + ASSERT_EQ(a.w(), a.r); + ASSERT_EQ(a.x(), a.i.x()); + ASSERT_EQ(a.y(), a.i.y()); + ASSERT_EQ(a.z(), a.i.z()); + + a.w() = 5.0f; + a.x() = 6.0f; + a.y() = 7.0f; + a.z() = 8.0f; + ASSERT_EQ(a.r, 5.0f); + ASSERT_EQ(a.i.x(), 6.0f); + ASSERT_EQ(a.i.y(), 7.0f); + ASSERT_EQ(a.i.z(), 8.0f); + }); + + suite.tests.emplace_back("quaternion conversion", []() + { + // Scalar type conversion + fquat q = fquat(dquat{1.0, 2.0, 3.0, 4.0}); + ASSERT_NEAR(q.w(), 1.0f, 1e-6f); + ASSERT_NEAR(q.x(), 2.0f, 1e-6f); + ASSERT_NEAR(q.y(), 3.0f, 1e-6f); + ASSERT_NEAR(q.z(), 4.0f, 1e-6f); + + // Matrix conversion (identity) + q = {1.0f, 0.0f, 0.0f, 0.0f}; + fmat3 m = fmat3(q); + ASSERT_NEAR(m[0][0], 1.0f, 1e-6); + ASSERT_NEAR(m[0][1], 0.0f, 1e-6); + ASSERT_NEAR(m[0][2], 0.0f, 1e-6); + ASSERT_NEAR(m[1][0], 0.0f, 1e-6); + ASSERT_NEAR(m[1][1], 1.0f, 1e-6); + ASSERT_NEAR(m[1][2], 0.0f, 1e-6); + ASSERT_NEAR(m[2][0], 0.0f, 1e-6); + ASSERT_NEAR(m[2][1], 0.0f, 1e-6); + ASSERT_NEAR(m[2][2], 1.0f, 1e-6); + + // Matrix conversion (X-axis, 90 degrees) + q = {std::cos(math::pi / 4.0f), std::sin(math::pi / 4.0f), 0.0f, 0.0f}; + m = fmat3(q); + ASSERT_NEAR(m[0][0], 1.0f, 1e-6); + ASSERT_NEAR(m[0][1], 0.0f, 1e-6); + ASSERT_NEAR(m[0][2], 0.0f, 1e-6); + ASSERT_NEAR(m[1][0], 0.0f, 1e-6); + ASSERT_NEAR(m[1][1], 0.0f, 1e-6); + ASSERT_NEAR(m[1][2], 1.0f, 1e-6); + ASSERT_NEAR(m[2][0], 0.0f, 1e-6); + ASSERT_NEAR(m[2][1], -1.0f, 1e-6); + ASSERT_NEAR(m[2][2], 0.0f, 1e-6); + + // Matrix conversion (Y-axis, 90 degrees) + q = {std::cos(math::pi / 4.0f), 0.0f, std::sin(math::pi / 4.0f), 0.0f}; + m = fmat3(q); + ASSERT_NEAR(m[0][0], 0.0f, 1e-6); + ASSERT_NEAR(m[0][1], 0.0f, 1e-6); + ASSERT_NEAR(m[0][2], -1.0f, 1e-6); + ASSERT_NEAR(m[1][0], 0.0f, 1e-6); + ASSERT_NEAR(m[1][1], 1.0f, 1e-6); + ASSERT_NEAR(m[1][2], 0.0f, 1e-6); + ASSERT_NEAR(m[2][0], 1.0f, 1e-6); + ASSERT_NEAR(m[2][1], 0.0f, 1e-6); + ASSERT_NEAR(m[2][2], 0.0f, 1e-6); + + // Matrix conversion (Z-axis, 90 degrees) + q = {std::cos(math::pi / 4.0f), 0.0f, 0.0f, std::sin(math::pi / 4.0f)}; + m = fmat3(q); + ASSERT_NEAR(m[0][0], 0.0f, 1e-6); + ASSERT_NEAR(m[0][1], 1.0f, 1e-6); + ASSERT_NEAR(m[0][2], 0.0f, 1e-6); + ASSERT_NEAR(m[1][0], -1.0f, 1e-6); + ASSERT_NEAR(m[1][1], 0.0f, 1e-6); + ASSERT_NEAR(m[1][2], 0.0f, 1e-6); + ASSERT_NEAR(m[2][0], 0.0f, 1e-6); + ASSERT_NEAR(m[2][1], 0.0f, 1e-6); + ASSERT_NEAR(m[2][2], 1.0f, 1e-6); + }); + + suite.tests.emplace_back("quaternion operations", []() + { + fquat a{1.0f, 2.0f, 3.0f, 4.0f}; + fquat b{5.0f, 6.0f, 7.0f, 8.0f}; + + a.swap(b); + + ASSERT_EQ(a.w(), 5.0f); + ASSERT_EQ(a.x(), 6.0f); + ASSERT_EQ(a.y(), 7.0f); + ASSERT_EQ(a.z(), 8.0f); + ASSERT_EQ(b.w(), 1.0f); + ASSERT_EQ(b.x(), 2.0f); + ASSERT_EQ(b.y(), 3.0f); + ASSERT_EQ(b.z(), 4.0f); + }); + + suite.tests.emplace_back("quaternion comparison", []() + { + fquat a{1.0f, 2.0f, 3.0f, 4.0f}; + fquat b{1.0f, 2.0f, 3.0f, 5.0f}; + fquat c{1.0f, 2.0f, 3.0f, 4.0f}; + + ASSERT_EQ(a, c); + ASSERT_NE(a, b); + ASSERT_LT(a, b); + ASSERT_LE(a, b); + ASSERT_LE(a, c); + ASSERT_GT(b, a); + ASSERT_GE(b, a); + ASSERT_GE(a, c); + }); + + suite.tests.emplace_back("quaternion tuple-like interface", []() + { + fquat q{1.0f, 2.0f, 3.0f, 4.0f}; + + auto [r, i] = q; + + ASSERT_EQ(r, 1.0f); + ASSERT_EQ(i.x(), 2.0f); + ASSERT_EQ(i.y(), 3.0f); + ASSERT_EQ(i.z(), 4.0f); + + ASSERT_EQ(get<0>(q), 1.0f); + ASSERT_EQ(get<1>(q).x(), 2.0f); + ASSERT_EQ(get<1>(q).y(), 3.0f); + ASSERT_EQ(get<1>(q).z(), 4.0f); + + auto& [rr, ri] = q; + rr = 5.0f; + ri.x() = 6.0f; + ri.y() = 7.0f; + ri.z() = 8.0f; + + ASSERT_EQ(q.w(), 5.0f); + ASSERT_EQ(q.x(), 6.0f); + ASSERT_EQ(q.y(), 7.0f); + ASSERT_EQ(q.z(), 8.0f); + + ASSERT_NE(r, rr); + ASSERT_NE(i.x(), ri.x()); + ASSERT_NE(i.y(), ri.y()); + ASSERT_NE(i.z(), ri.z()); + }); + + suite.tests.emplace_back("quaternion formatter", []() + { + fquat q{-9999.96f, 0.0f, 2.0f / 3.0f, std::numeric_limits::infinity()}; + + auto str = std::format("{:.4f}", q); + ASSERT_EQ(str, "{-9999.9600, {0.0000, 0.6667, inf}}"); + }); + return suite.run(); }