diff --git a/src/unittest/CMakeLists.txt b/src/unittest/CMakeLists.txt index fb3e1f97a..8a635c8e8 100644 --- a/src/unittest/CMakeLists.txt +++ b/src/unittest/CMakeLists.txt @@ -14,7 +14,7 @@ set (UNITTEST_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/test_inventory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_irrptr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_irr_matrix4.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/test_irr_quaternion.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_irr_rotation.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_logging.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_lua.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_map.cpp diff --git a/src/unittest/test_irr_quaternion.cpp b/src/unittest/test_irr_quaternion.cpp deleted file mode 100644 index 61ddcad7c..000000000 --- a/src/unittest/test_irr_quaternion.cpp +++ /dev/null @@ -1,57 +0,0 @@ -// Luanti -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "catch.h" -#include "irrMath.h" -#include "matrix4.h" -#include "quaternion.h" -#include "irr_v3d.h" - -using matrix4 = core::matrix4; - -static bool matrix_equals(const matrix4 &a, const matrix4 &b) { - return a.equals(b, 0.00001f); -} - -TEST_CASE("quaternion") { - -// Make sure that the conventions are consistent -SECTION("equivalence to euler rotations") { - auto test_rotation = [](v3f rad) { - matrix4 R; - R.setRotationRadians(rad); - v3f rad2; - core::quaternion(rad).toEuler(rad2); - matrix4 R2; - R2.setRotationRadians(rad2); - CHECK(matrix_equals(R, R2)); - }; - - Catch::Generators::RandomFloatingGenerator gen(0.0f, 2 * core::PI, Catch::getSeed()); - for (int i = 0; i < 1000; ++i) - test_rotation(v3f{gen.get(), gen.get(), gen.get()}); - for (int i = 0; i < 4; i++) - for (int j = 0; j < 4; j++) - for (int k = 0; k < 4; k++) - test_rotation(core::PI / 4.0f * v3f(i, j, k)); -} - -SECTION("equivalence to rotation matrices") { - auto test_rotation = [](v3f rad) { - matrix4 R; - R.setRotationRadians(rad); - matrix4 R2; - core::quaternion(R).getMatrix(R2); - CHECK(matrix_equals(R, R2)); - }; - - Catch::Generators::RandomFloatingGenerator gen(0.0f, 2 * core::PI, Catch::getSeed()); - for (int i = 0; i < 1000; ++i) - test_rotation(v3f{gen.get(), gen.get(), gen.get()}); - for (int i = 0; i < 4; i++) - for (int j = 0; j < 4; j++) - for (int k = 0; k < 4; k++) - test_rotation(core::PI / 4.0f * v3f(i, j, k)); -} - -} \ No newline at end of file diff --git a/src/unittest/test_irr_rotation.cpp b/src/unittest/test_irr_rotation.cpp new file mode 100644 index 000000000..1da0462fe --- /dev/null +++ b/src/unittest/test_irr_rotation.cpp @@ -0,0 +1,109 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "catch.h" +#include "catch_amalgamated.hpp" +#include "irrMath.h" +#include "matrix4.h" +#include "irrMath.h" +#include "matrix4.h" +#include "irr_v3d.h" +#include "quaternion.h" +#include + +// Irrlicht provides three different representations of rotations: +// - Euler angles in radians (or degrees, but that doesn't matter much); +// - Quaternions; +// - Rotation matrices. +// These tests ensure that converting between these representations is rotation-preserving. + +using matrix4 = core::matrix4; +using quaternion = core::quaternion; + +// Despite the internal usage of doubles, matrix4::setRotationRadians +// simply incurs component-wise errors of the order 1e-3. +const f32 tolerance = 1e-2f; + +static bool matrix_equals(const matrix4 &mat, const matrix4 &mat2) +{ + return mat.equals(mat2, tolerance); +} + +static bool euler_angles_equiv(v3f rad, v3f rad2) +{ + matrix4 mat, mat2; + mat.setRotationRadians(rad); + mat2.setRotationRadians(rad2); + return matrix_equals(mat, mat2); +} + +static void test_euler_angles_rad(const std::function &test_euler_radians) +{ + Catch::Generators::RandomFloatingGenerator gen(0.0f, 2 * core::PI, Catch::getSeed()); + auto random_angle = [&gen]() { + f32 f = gen.get(); + gen.next(); + return f; + }; + for (int i = 0; i < 1000; ++i) + test_euler_radians(v3f{random_angle(), random_angle(), random_angle()}); + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + for (int k = 0; k < 4; k++) { + v3f rad = core::PI / 4.0f * v3f(i, j, k); + test_euler_radians(rad); + // Test very slightly nudged, "almost-perfect" rotations to make sure + // that the conversions are relatively stable at extremal points + for (int l = 0; l < 10; ++l) { + v3f jitter = v3f{random_angle(), random_angle(), random_angle()} * 0.001f; + test_euler_radians(rad + jitter); + } + } +} + +TEST_CASE("rotations") { + +SECTION("euler-to-quaternion conversion") { + test_euler_angles_rad([](v3f rad) { + core::matrix4 rot, rot_quat; + rot.setRotationRadians(rad); + quaternion q(rad); + q.getMatrix(rot_quat); + // Check equivalence of the rotations via matrices + CHECK(matrix_equals(rot, rot_quat)); + }); +} + +// Now that we've already tested the conversion to quaternions, +// this essentially primarily tests the quaternion to euler conversion +SECTION("quaternion-euler roundtrip") { + test_euler_angles_rad([](v3f rad) { + quaternion q(rad); + v3f rad2; + q.toEuler(rad2); + CHECK(euler_angles_equiv(rad, rad2)); + }); +} + +SECTION("matrix-quaternion roundtrip") { + test_euler_angles_rad([](v3f rad) { + matrix4 mat; + mat.setRotationRadians(rad); + quaternion q(mat); + matrix4 mat2; + q.getMatrix(mat2); + CHECK(matrix_equals(mat, mat2)); + }); +} + +SECTION("matrix-euler roundtrip") { + test_euler_angles_rad([](v3f rad) { + matrix4 mat, mat2; + mat.setRotationRadians(rad); + v3f rad2 = mat.getRotationDegrees() * core::DEGTORAD; + mat2.setRotationRadians(rad2); + CHECK(matrix_equals(mat, mat2)); + }); +} + +}