Path: blob/master/tests/core/math/test_quaternion.cpp
23450 views
/**************************************************************************/1/* test_quaternion.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "tests/test_macros.h"3132TEST_FORCE_LINK(test_quaternion)3334#include "core/math/math_defs.h"35#include "core/math/math_funcs.h"36#include "core/math/quaternion.h"37#include "core/math/vector3.h"3839namespace TestQuaternion {4041Quaternion quat_euler_yxz_deg(Vector3 angle) {42double yaw = Math::deg_to_rad(angle[1]);43double pitch = Math::deg_to_rad(angle[0]);44double roll = Math::deg_to_rad(angle[2]);4546// Generate YXZ (Z-then-X-then-Y) Quaternion using single-axis Euler47// constructor and quaternion product, both tested separately.48Quaternion q_y = Quaternion::from_euler(Vector3(0.0, yaw, 0.0));49Quaternion q_p = Quaternion::from_euler(Vector3(pitch, 0.0, 0.0));50Quaternion q_r = Quaternion::from_euler(Vector3(0.0, 0.0, roll));51// Roll-Z is followed by Pitch-X, then Yaw-Y.52Quaternion q_yxz = q_y * q_p * q_r;5354return q_yxz;55}5657TEST_CASE("[Quaternion] Default Construct") {58constexpr Quaternion q;5960CHECK(q[0] == 0.0);61CHECK(q[1] == 0.0);62CHECK(q[2] == 0.0);63CHECK(q[3] == 1.0);64}6566TEST_CASE("[Quaternion] Construct x,y,z,w") {67// Values are taken from actual use in another project & are valid (except roundoff error).68constexpr Quaternion q(0.2391, 0.099, 0.3696, 0.8924);6970CHECK(q[0] == doctest::Approx(0.2391));71CHECK(q[1] == doctest::Approx(0.099));72CHECK(q[2] == doctest::Approx(0.3696));73CHECK(q[3] == doctest::Approx(0.8924));74}7576TEST_CASE("[Quaternion] Construct AxisAngle 1") {77// Easy to visualize: 120 deg about X-axis.78Quaternion q(Vector3(1.0, 0.0, 0.0), Math::deg_to_rad(120.0));7980// 0.866 isn't close enough; doctest::Approx doesn't cut much slack!81CHECK(q[0] == doctest::Approx(0.866025)); // Sine of half the angle.82CHECK(q[1] == doctest::Approx(0.0));83CHECK(q[2] == doctest::Approx(0.0));84CHECK(q[3] == doctest::Approx(0.5)); // Cosine of half the angle.85}8687TEST_CASE("[Quaternion] Construct AxisAngle 2") {88// Easy to visualize: 30 deg about Y-axis.89Quaternion q(Vector3(0.0, 1.0, 0.0), Math::deg_to_rad(30.0));9091CHECK(q[0] == doctest::Approx(0.0));92CHECK(q[1] == doctest::Approx(0.258819)); // Sine of half the angle.93CHECK(q[2] == doctest::Approx(0.0));94CHECK(q[3] == doctest::Approx(0.965926)); // Cosine of half the angle.95}9697TEST_CASE("[Quaternion] Construct AxisAngle 3") {98// Easy to visualize: 60 deg about Z-axis.99Quaternion q(Vector3(0.0, 0.0, 1.0), Math::deg_to_rad(60.0));100101CHECK(q[0] == doctest::Approx(0.0));102CHECK(q[1] == doctest::Approx(0.0));103CHECK(q[2] == doctest::Approx(0.5)); // Sine of half the angle.104CHECK(q[3] == doctest::Approx(0.866025)); // Cosine of half the angle.105}106107TEST_CASE("[Quaternion] Construct AxisAngle 4") {108// More complex & hard to visualize, so test w/ data from online calculator.109constexpr Vector3 axis(1.0, 2.0, 0.5);110Quaternion q(axis.normalized(), Math::deg_to_rad(35.0));111112CHECK(q[0] == doctest::Approx(0.131239));113CHECK(q[1] == doctest::Approx(0.262478));114CHECK(q[2] == doctest::Approx(0.0656194));115CHECK(q[3] == doctest::Approx(0.953717));116}117118TEST_CASE("[Quaternion] Construct from Quaternion") {119constexpr Vector3 axis(1.0, 2.0, 0.5);120Quaternion q_src(axis.normalized(), Math::deg_to_rad(35.0));121Quaternion q(q_src);122123CHECK(q[0] == doctest::Approx(0.131239));124CHECK(q[1] == doctest::Approx(0.262478));125CHECK(q[2] == doctest::Approx(0.0656194));126CHECK(q[3] == doctest::Approx(0.953717));127}128129TEST_CASE("[Quaternion] Construct Euler SingleAxis") {130double yaw = Math::deg_to_rad(45.0);131double pitch = Math::deg_to_rad(30.0);132double roll = Math::deg_to_rad(10.0);133134Vector3 euler_y(0.0, yaw, 0.0);135Quaternion q_y = Quaternion::from_euler(euler_y);136CHECK(q_y[0] == doctest::Approx(0.0));137CHECK(q_y[1] == doctest::Approx(0.382684));138CHECK(q_y[2] == doctest::Approx(0.0));139CHECK(q_y[3] == doctest::Approx(0.923879));140141Vector3 euler_p(pitch, 0.0, 0.0);142Quaternion q_p = Quaternion::from_euler(euler_p);143CHECK(q_p[0] == doctest::Approx(0.258819));144CHECK(q_p[1] == doctest::Approx(0.0));145CHECK(q_p[2] == doctest::Approx(0.0));146CHECK(q_p[3] == doctest::Approx(0.965926));147148Vector3 euler_r(0.0, 0.0, roll);149Quaternion q_r = Quaternion::from_euler(euler_r);150CHECK(q_r[0] == doctest::Approx(0.0));151CHECK(q_r[1] == doctest::Approx(0.0));152CHECK(q_r[2] == doctest::Approx(0.0871558));153CHECK(q_r[3] == doctest::Approx(0.996195));154}155156TEST_CASE("[Quaternion] Construct Euler YXZ dynamic axes") {157double yaw = Math::deg_to_rad(45.0);158double pitch = Math::deg_to_rad(30.0);159double roll = Math::deg_to_rad(10.0);160161// Generate YXZ comparison data (Z-then-X-then-Y) using single-axis Euler162// constructor and quaternion product, both tested separately.163Vector3 euler_y(0.0, yaw, 0.0);164Quaternion q_y = Quaternion::from_euler(euler_y);165Vector3 euler_p(pitch, 0.0, 0.0);166Quaternion q_p = Quaternion::from_euler(euler_p);167Vector3 euler_r(0.0, 0.0, roll);168Quaternion q_r = Quaternion::from_euler(euler_r);169170// Instrinsically, Yaw-Y then Pitch-X then Roll-Z.171// Extrinsically, Roll-Z is followed by Pitch-X, then Yaw-Y.172Quaternion check_yxz = q_y * q_p * q_r;173174// Test construction from YXZ Euler angles.175Vector3 euler_yxz(pitch, yaw, roll);176Quaternion q = Quaternion::from_euler(euler_yxz);177CHECK(q[0] == doctest::Approx(check_yxz[0]));178CHECK(q[1] == doctest::Approx(check_yxz[1]));179CHECK(q[2] == doctest::Approx(check_yxz[2]));180CHECK(q[3] == doctest::Approx(check_yxz[3]));181182CHECK(q.is_equal_approx(check_yxz));183CHECK(q.get_euler().is_equal_approx(euler_yxz));184CHECK(check_yxz.get_euler().is_equal_approx(euler_yxz));185}186187TEST_CASE("[Quaternion] Construct Basis Euler") {188double yaw = Math::deg_to_rad(45.0);189double pitch = Math::deg_to_rad(30.0);190double roll = Math::deg_to_rad(10.0);191Vector3 euler_yxz(pitch, yaw, roll);192Quaternion q_yxz = Quaternion::from_euler(euler_yxz);193Basis basis_axes = Basis::from_euler(euler_yxz);194Quaternion q(basis_axes);195CHECK(q.is_equal_approx(q_yxz));196}197198TEST_CASE("[Quaternion] Construct Basis Axes") {199// Arbitrary Euler angles.200const Vector3 euler_yxz(Math::deg_to_rad(31.41), Math::deg_to_rad(-49.16), Math::deg_to_rad(12.34));201// Basis vectors from online calculation of rotation matrix.202constexpr Vector3 i_unit(0.5545787, 0.1823950, 0.8118957);203constexpr Vector3 j_unit(-0.5249245, 0.8337420, 0.1712555);204constexpr Vector3 k_unit(-0.6456754, -0.5211586, 0.5581192);205// Quaternion from online calculation.206constexpr Quaternion q_calc(0.2016913, -0.4245716, 0.206033, 0.8582598);207// Quaternion from local calculation.208const Quaternion q_local = quat_euler_yxz_deg(Vector3(31.41, -49.16, 12.34));209// Quaternion from Euler angles constructor.210const Quaternion q_euler = Quaternion::from_euler(euler_yxz);211CHECK(q_calc.is_equal_approx(q_local));212CHECK(q_local.is_equal_approx(q_euler));213214// Calculate Basis and construct Quaternion.215// When this is written, C++ Basis class does not construct from basis vectors.216// This is by design, but may be subject to change.217// Workaround by constructing Basis from Euler angles.218// basis_axes = Basis(i_unit, j_unit, k_unit);219Basis basis_axes = Basis::from_euler(euler_yxz);220Quaternion q(basis_axes);221222CHECK(basis_axes.get_column(0).is_equal_approx(i_unit));223CHECK(basis_axes.get_column(1).is_equal_approx(j_unit));224CHECK(basis_axes.get_column(2).is_equal_approx(k_unit));225226CHECK(q.is_equal_approx(q_calc));227CHECK_FALSE(q.inverse().is_equal_approx(q_calc));228CHECK(q.is_equal_approx(q_local));229CHECK(q.is_equal_approx(q_euler));230CHECK(q[0] == doctest::Approx(0.2016913));231CHECK(q[1] == doctest::Approx(-0.4245716));232CHECK(q[2] == doctest::Approx(0.206033));233CHECK(q[3] == doctest::Approx(0.8582598));234}235236TEST_CASE("[Quaternion] Construct Shortest Arc For 180 Degree Arc") {237Vector3 up(0, 1, 0);238Vector3 down(0, -1, 0);239Vector3 left(-1, 0, 0);240Vector3 right(1, 0, 0);241Vector3 forward(0, 0, -1);242Vector3 back(0, 0, 1);243244// When we have a 180 degree rotation quaternion which was defined as245// A to B, logically when we transform A we expect to get B.246Quaternion left_to_right(left, right);247Quaternion right_to_left(right, left);248CHECK(left_to_right.xform(left).is_equal_approx(right));249CHECK(Quaternion(right, left).xform(right).is_equal_approx(left));250CHECK(Quaternion(up, down).xform(up).is_equal_approx(down));251CHECK(Quaternion(down, up).xform(down).is_equal_approx(up));252CHECK(Quaternion(forward, back).xform(forward).is_equal_approx(back));253CHECK(Quaternion(back, forward).xform(back).is_equal_approx(forward));254255// With (arbitrary) opposite vectors that are not axis-aligned as parameters.256Vector3 diagonal_up = Vector3(1.2, 2.3, 4.5).normalized();257Vector3 diagonal_down = -diagonal_up;258Quaternion q1(diagonal_up, diagonal_down);259CHECK(q1.xform(diagonal_down).is_equal_approx(diagonal_up));260CHECK(q1.xform(diagonal_up).is_equal_approx(diagonal_down));261262// For the consistency of the rotation direction, they should be symmetrical to the plane.263CHECK(left_to_right.is_equal_approx(right_to_left.inverse()));264265// If vectors are same, no rotation.266CHECK(Quaternion(diagonal_up, diagonal_up).is_equal_approx(Quaternion()));267}268269TEST_CASE("[Quaternion] Get Euler Orders") {270double x = Math::deg_to_rad(30.0);271double y = Math::deg_to_rad(45.0);272double z = Math::deg_to_rad(10.0);273Vector3 euler(x, y, z);274for (int i = 0; i < 6; i++) {275EulerOrder order = (EulerOrder)i;276Basis basis = Basis::from_euler(euler, order);277Quaternion q = Quaternion(basis);278Vector3 check = q.get_euler(order);279CHECK_MESSAGE(check.is_equal_approx(euler),280"Quaternion get_euler method should return the original angles.");281CHECK_MESSAGE(check.is_equal_approx(basis.get_euler(order)),282"Quaternion get_euler method should behave the same as Basis get_euler.");283}284}285286TEST_CASE("[Quaternion] Product (book)") {287// Example from "Quaternions and Rotation Sequences" by Jack Kuipers, p. 108.288constexpr Quaternion p(1.0, -2.0, 1.0, 3.0);289constexpr Quaternion q(-1.0, 2.0, 3.0, 2.0);290291constexpr Quaternion pq = p * q;292CHECK(pq[0] == doctest::Approx(-9.0));293CHECK(pq[1] == doctest::Approx(-2.0));294CHECK(pq[2] == doctest::Approx(11.0));295CHECK(pq[3] == doctest::Approx(8.0));296}297298TEST_CASE("[Quaternion] Product") {299double yaw = Math::deg_to_rad(45.0);300double pitch = Math::deg_to_rad(30.0);301double roll = Math::deg_to_rad(10.0);302303Vector3 euler_y(0.0, yaw, 0.0);304Quaternion q_y = Quaternion::from_euler(euler_y);305CHECK(q_y[0] == doctest::Approx(0.0));306CHECK(q_y[1] == doctest::Approx(0.382684));307CHECK(q_y[2] == doctest::Approx(0.0));308CHECK(q_y[3] == doctest::Approx(0.923879));309310Vector3 euler_p(pitch, 0.0, 0.0);311Quaternion q_p = Quaternion::from_euler(euler_p);312CHECK(q_p[0] == doctest::Approx(0.258819));313CHECK(q_p[1] == doctest::Approx(0.0));314CHECK(q_p[2] == doctest::Approx(0.0));315CHECK(q_p[3] == doctest::Approx(0.965926));316317Vector3 euler_r(0.0, 0.0, roll);318Quaternion q_r = Quaternion::from_euler(euler_r);319CHECK(q_r[0] == doctest::Approx(0.0));320CHECK(q_r[1] == doctest::Approx(0.0));321CHECK(q_r[2] == doctest::Approx(0.0871558));322CHECK(q_r[3] == doctest::Approx(0.996195));323324// Test ZYX dynamic-axes since test data is available online.325// Rotate first about X axis, then new Y axis, then new Z axis.326// (Godot uses YXZ Yaw-Pitch-Roll order).327Quaternion q_yp = q_y * q_p;328CHECK(q_yp[0] == doctest::Approx(0.239118));329CHECK(q_yp[1] == doctest::Approx(0.369644));330CHECK(q_yp[2] == doctest::Approx(-0.099046));331CHECK(q_yp[3] == doctest::Approx(0.892399));332333Quaternion q_ryp = q_r * q_yp;334CHECK(q_ryp[0] == doctest::Approx(0.205991));335CHECK(q_ryp[1] == doctest::Approx(0.389078));336CHECK(q_ryp[2] == doctest::Approx(-0.0208912));337CHECK(q_ryp[3] == doctest::Approx(0.897636));338}339340TEST_CASE("[Quaternion] xform unit vectors") {341// Easy to visualize: 120 deg about X-axis.342// Transform the i, j, & k unit vectors.343Quaternion q(Vector3(1.0, 0.0, 0.0), Math::deg_to_rad(120.0));344Vector3 i_t = q.xform(Vector3(1.0, 0.0, 0.0));345Vector3 j_t = q.xform(Vector3(0.0, 1.0, 0.0));346Vector3 k_t = q.xform(Vector3(0.0, 0.0, 1.0));347//348CHECK(i_t.is_equal_approx(Vector3(1.0, 0.0, 0.0)));349CHECK(j_t.is_equal_approx(Vector3(0.0, -0.5, 0.866025)));350CHECK(k_t.is_equal_approx(Vector3(0.0, -0.866025, -0.5)));351CHECK(i_t.length_squared() == doctest::Approx(1.0));352CHECK(j_t.length_squared() == doctest::Approx(1.0));353CHECK(k_t.length_squared() == doctest::Approx(1.0));354355// Easy to visualize: 30 deg about Y-axis.356q = Quaternion(Vector3(0.0, 1.0, 0.0), Math::deg_to_rad(30.0));357i_t = q.xform(Vector3(1.0, 0.0, 0.0));358j_t = q.xform(Vector3(0.0, 1.0, 0.0));359k_t = q.xform(Vector3(0.0, 0.0, 1.0));360//361CHECK(i_t.is_equal_approx(Vector3(0.866025, 0.0, -0.5)));362CHECK(j_t.is_equal_approx(Vector3(0.0, 1.0, 0.0)));363CHECK(k_t.is_equal_approx(Vector3(0.5, 0.0, 0.866025)));364CHECK(i_t.length_squared() == doctest::Approx(1.0));365CHECK(j_t.length_squared() == doctest::Approx(1.0));366CHECK(k_t.length_squared() == doctest::Approx(1.0));367368// Easy to visualize: 60 deg about Z-axis.369q = Quaternion(Vector3(0.0, 0.0, 1.0), Math::deg_to_rad(60.0));370i_t = q.xform(Vector3(1.0, 0.0, 0.0));371j_t = q.xform(Vector3(0.0, 1.0, 0.0));372k_t = q.xform(Vector3(0.0, 0.0, 1.0));373//374CHECK(i_t.is_equal_approx(Vector3(0.5, 0.866025, 0.0)));375CHECK(j_t.is_equal_approx(Vector3(-0.866025, 0.5, 0.0)));376CHECK(k_t.is_equal_approx(Vector3(0.0, 0.0, 1.0)));377CHECK(i_t.length_squared() == doctest::Approx(1.0));378CHECK(j_t.length_squared() == doctest::Approx(1.0));379CHECK(k_t.length_squared() == doctest::Approx(1.0));380}381382TEST_CASE("[Quaternion] xform vector") {383// Arbitrary quaternion rotates an arbitrary vector.384const Vector3 euler_yzx(Math::deg_to_rad(31.41), Math::deg_to_rad(-49.16), Math::deg_to_rad(12.34));385const Basis basis_axes = Basis::from_euler(euler_yzx);386const Quaternion q(basis_axes);387388constexpr Vector3 v_arb(3.0, 4.0, 5.0);389const Vector3 v_rot = q.xform(v_arb);390const Vector3 v_compare = basis_axes.xform(v_arb);391392CHECK(v_rot.length_squared() == doctest::Approx(v_arb.length_squared()));393CHECK(v_rot.is_equal_approx(v_compare));394}395396// Test vector xform for a single combination of Quaternion and Vector.397void test_quat_vec_rotate(Vector3 euler_yzx, Vector3 v_in) {398const Basis basis_axes = Basis::from_euler(euler_yzx);399const Quaternion q(basis_axes);400401const Vector3 v_rot = q.xform(v_in);402const Vector3 v_compare = basis_axes.xform(v_in);403404CHECK(v_rot.length_squared() == doctest::Approx(v_in.length_squared()));405CHECK(v_rot.is_equal_approx(v_compare));406}407408TEST_CASE("[Quaternion] Finite number checks") {409constexpr real_t x = Math::NaN;410411CHECK_MESSAGE(412Quaternion(0, 1, 2, 3).is_finite(),413"Quaternion with all components finite should be finite");414415CHECK_FALSE_MESSAGE(416Quaternion(x, 1, 2, 3).is_finite(),417"Quaternion with one component infinite should not be finite.");418CHECK_FALSE_MESSAGE(419Quaternion(0, x, 2, 3).is_finite(),420"Quaternion with one component infinite should not be finite.");421CHECK_FALSE_MESSAGE(422Quaternion(0, 1, x, 3).is_finite(),423"Quaternion with one component infinite should not be finite.");424CHECK_FALSE_MESSAGE(425Quaternion(0, 1, 2, x).is_finite(),426"Quaternion with one component infinite should not be finite.");427428CHECK_FALSE_MESSAGE(429Quaternion(x, x, 2, 3).is_finite(),430"Quaternion with two components infinite should not be finite.");431CHECK_FALSE_MESSAGE(432Quaternion(x, 1, x, 3).is_finite(),433"Quaternion with two components infinite should not be finite.");434CHECK_FALSE_MESSAGE(435Quaternion(x, 1, 2, x).is_finite(),436"Quaternion with two components infinite should not be finite.");437CHECK_FALSE_MESSAGE(438Quaternion(0, x, x, 3).is_finite(),439"Quaternion with two components infinite should not be finite.");440CHECK_FALSE_MESSAGE(441Quaternion(0, x, 2, x).is_finite(),442"Quaternion with two components infinite should not be finite.");443CHECK_FALSE_MESSAGE(444Quaternion(0, 1, x, x).is_finite(),445"Quaternion with two components infinite should not be finite.");446447CHECK_FALSE_MESSAGE(448Quaternion(0, x, x, x).is_finite(),449"Quaternion with three components infinite should not be finite.");450CHECK_FALSE_MESSAGE(451Quaternion(x, 1, x, x).is_finite(),452"Quaternion with three components infinite should not be finite.");453CHECK_FALSE_MESSAGE(454Quaternion(x, x, 2, x).is_finite(),455"Quaternion with three components infinite should not be finite.");456CHECK_FALSE_MESSAGE(457Quaternion(x, x, x, 3).is_finite(),458"Quaternion with three components infinite should not be finite.");459460CHECK_FALSE_MESSAGE(461Quaternion(x, x, x, x).is_finite(),462"Quaternion with four components infinite should not be finite.");463}464465} // namespace TestQuaternion466467468