Skip to content

Commit

Permalink
Add quaternion type unit tests. Add quaternion::swap(). Remove quater…
Browse files Browse the repository at this point in the history
…nion to vector typecast.
  • Loading branch information
cjhoward committed Sep 4, 2024
1 parent ef6fc5b commit 5b6b02b
Show file tree
Hide file tree
Showing 3 changed files with 244 additions and 50 deletions.
105 changes: 58 additions & 47 deletions src/engine/math/quaternion-types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <engine/math/matrix.hpp>
#include <format>
#include <type_traits>
#include <utility>

// export module math.quaternion:type;
// import math.constants;
Expand Down Expand Up @@ -53,7 +54,7 @@ struct quaternion

/// @}

/// @name Element access
/// @name Part access
/// @{

/// @{
Expand Down Expand Up @@ -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`.
Expand All @@ -154,13 +123,12 @@ struct quaternion
return {static_cast<U>(r), vec3<U>(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();
Expand All @@ -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<T>() 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
/// @{
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/game/systems/constraint-system.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
186 changes: 184 additions & 2 deletions test/test-math.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<float> / 4.0f), std::sin(math::pi<float> / 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<float> / 4.0f), 0.0f, std::sin(math::pi<float> / 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<float> / 4.0f), 0.0f, 0.0f, std::sin(math::pi<float> / 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<float>::infinity()};

auto str = std::format("{:.4f}", q);
ASSERT_EQ(str, "{-9999.9600, {0.0000, 0.6667, inf}}");
});

return suite.run();
}

0 comments on commit 5b6b02b

Please sign in to comment.