From 737104f2c7401a05d873accc4c6dd4188736f221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Mon, 3 Sep 2018 15:25:35 +0200 Subject: [PATCH] Math: added "shortest path" alternatives to lerp(), slerp() and sclerp(). Before neither of the lerp(), slerp() had the shortest path check, while sclerp() had it. Now, to be consistent, none of them has it and there are lerpShortestPath(), slerpShortestPath() and sclerpShortestPath() functions that have the shortest path check. This is different from other engines, where there's usually only the shortest path interpolation by default and either an optional "non-shortest-path" interpolation or no alternative at all. I like to give the users a choice, so there's both versions and the non-shortest-path version is the default, because -- at least in case of lerp() -- this results in a quite significant perf difference (15% faster), so why not have it. Preprocess your data instead ;) --- doc/changelog.dox | 5 + src/Magnum/Math/DualQuaternion.h | 107 +++++++++++++++++--- src/Magnum/Math/Quaternion.h | 102 ++++++++++++++++++- src/Magnum/Math/Test/DualQuaternionTest.cpp | 84 ++++++++++++--- src/Magnum/Math/Test/QuaternionTest.cpp | 62 +++++++++++- 5 files changed, 328 insertions(+), 32 deletions(-) diff --git a/doc/changelog.dox b/doc/changelog.dox index e67f6c82f..f9a8a8deb 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -72,6 +72,11 @@ See also: - Added @ref Math::lerp(const Complex&, const Complex&, T) and @ref Math::slerp(const Complex&, const Complex&, T) for feature parity with @ref Math::Quaternion +- Added @ref Math::lerpShortestPath(const Quaternion&, const Quaternion&, T), + @ref Math::slerpShortestPath(const Quaternion&, const Quaternion&, T) + and @ref Math::sclerpShortestPath(const DualQuaternion&, const DualQuaternion&, T) + variants; clarified that the original versions are explicitly *not* + shortest-path - Added @ref Math::Range2D::x(), @ref Math::Range3D::x(), @ref Math::Range2D::y(), @ref Math::Range3D::y(), @ref Math::Range3D::z() and @ref Math::Range3D::y() for slicing ranges into lower dimensions diff --git a/src/Magnum/Math/DualQuaternion.h b/src/Magnum/Math/DualQuaternion.h index d8f45269b..5191b563d 100644 --- a/src/Magnum/Math/DualQuaternion.h +++ b/src/Magnum/Math/DualQuaternion.h @@ -48,26 +48,107 @@ namespace Implementation { @param normalizedB Second dual quaternion @param t Interpolation phase (from range @f$ [0; 1] @f$) -Expects that both dual quaternions are normalized. @f[ -\begin{array}{rcl} - l + \epsilon m & = & \hat q_A^* \hat q_B \\ - \frac{\hat a} 2 & = & acos \left( l_S \right) - \epsilon m_S \frac 1 {|l_V|} \\ - \hat {\boldsymbol n} & = & \boldsymbol n_0 + \epsilon \boldsymbol n_\epsilon - ~~~~~~~~ \boldsymbol n_0 = l_V \frac 1 {|l_V|} - ~~~~~~~~ \boldsymbol n_\epsilon = \left( m_V - {\boldsymbol n}_0 \frac {a_\epsilon} 2 l_S \right)\frac 1 {|l_V|} \\ - {\hat q}_{ScLERP} & = & \hat q_A (\hat q_A^* \hat q_B)^t = - \hat q_A \left[ \hat {\boldsymbol n} sin \left( t \frac {\hat a} 2 \right), cos \left( t \frac {\hat a} 2 \right) \right] \\ -\end{array} +Expects that both dual quaternions are normalized. If the real parts are the +same or one is a negation of the other, returns the @ref DualQuaternion::rotation() +(real) part combined with interpolated @ref DualQuaternion::translation(): @f[ + \begin{array}{rcl} + d & = & q_{A_0} \cdot q_{B_0} \\[5pt] + {\hat q}_{ScLERP} & = & 2 \left[(1 - t)(q_{A_\epsilon} q_{A_0}^*)_V + t (q_{B_\epsilon} q_{B_0}^*)_V \right] q_A, ~ {\color{m-primary} \text{if} ~ d \ge 1} + \end{array} @f] -@see @ref DualQuaternion::isNormalized(), - @ref slerp(const Quaternion&, const Quaternion&, T), - @ref lerp(const T&, const T&, U) + +@m_class{m-noindent} + +otherwise, the interpolation is performed as: @f[ + \begin{array}{rcl} + l + \epsilon m & = & \hat q_A^* \hat q_B \\[5pt] + \frac{\hat a} 2 & = & \arccos \left( l_S \right) - \epsilon m_S \frac 1 {|\boldsymbol{l}_V|} \\[5pt] + \hat {\boldsymbol n} & = & \boldsymbol n_0 + \epsilon \boldsymbol n_\epsilon, + ~~~~~~~~ \boldsymbol n_0 = \boldsymbol{l}_V \frac 1 {|\boldsymbol{l}_V|}, + ~~~~~~~~ \boldsymbol n_\epsilon = \left(\boldsymbol{m}_V - {\boldsymbol n}_0 \frac {a_\epsilon} 2 l_S \right)\frac 1 {|\boldsymbol{l}_V|} \\[5pt] + {\hat q}_{ScLERP} & = & \hat q_A (\hat q_A^* \hat q_B)^t = + \hat q_A \left[ \hat {\boldsymbol n} \sin \left( t \frac {\hat a} 2 \right), \cos \left( t \frac {\hat a} 2 \right) \right] + \end{array} +@f] + +Note that this function does not check for shortest path interpolation, see +@ref sclerpShortestPath() for an alternative. +@see @ref DualQuaternion::isNormalized(), @ref DualQuaternion::quaternionConjugated(), + @ref lerp(const Quaternion&, const Quaternion&, T), + @ref slerp(const Quaternion&, const Quaternion&, T) */ template inline DualQuaternion sclerp(const DualQuaternion& normalizedA, const DualQuaternion& normalizedB, const T t) { CORRADE_ASSERT(normalizedA.isNormalized() && normalizedB.isNormalized(), "Math::sclerp(): dual quaternions must be normalized", {}); const T cosHalfAngle = dot(normalizedA.real(), normalizedB.real()); + /* Avoid division by zero: interpolate just the translation part */ + /** @todo could this be optimized somehow? */ + if(std::abs(cosHalfAngle) >= T(1)) + return DualQuaternion::translation(Implementation::lerp(normalizedA.translation(), normalizedB.translation(), t))*DualQuaternion{normalizedA.real()}; + + /* l + εm = q_A^**q_B */ + const DualQuaternion diff = normalizedA.quaternionConjugated()*normalizedB; + const Quaternion& l = diff.real(); + const Quaternion& m = diff.dual(); + + /* a/2 = acos(l_S) - εm_S/|l_V| */ + const T invr = l.vector().lengthInverted(); + const Dual aHalf{std::acos(l.scalar()), -m.scalar()*invr}; + + /* direction = n_0 = l_V/|l_V| + moment = n_ε = (m_V - n_0*(a_ε/2)*l_S)/|l_V| */ + const Vector3 direction = l.vector()*invr; + const Vector3 moment = (m.vector() - direction*(aHalf.dual()*l.scalar()))*invr; + const Dual> n{direction, moment}; + + /* q_ScLERP = q_A*(cos(t*a/2) + n*sin(t*a/2)) */ + Dual sin, cos; + std::tie(sin, cos) = Math::sincos(t*Dual>(aHalf)); + return normalizedA*DualQuaternion{n*sin, cos}; +} + +/** @relatesalso DualQuaternion +@brief Screw linear shortest-path interpolation of two dual quaternions +@param normalizedA First dual quaternion +@param normalizedB Second dual quaternion +@param t Interpolation phase (from range @f$ [0; 1] @f$) + +Unlike @ref sclerp(const DualQuaternion&, const DualQuaternion&, T) this +function interpolates on the shortest path. Expects that both dual quaternions +are normalized. If the real parts are the same or one is a negation of the +other, returns the @ref DualQuaternion::rotation() (real) part combined with +interpolated @ref DualQuaternion::translation(): @f[ + \begin{array}{rcl} + d & = & q_{A_0} \cdot q_{B_0} \\[5pt] + {\hat q}_{ScLERP} & = & 2 \left((1 - t)(q_{A_\epsilon} q_{A_0}^*)_V + t (q_{B_\epsilon} q_{B_0}^*)_V \right) (q_{A_0} + \epsilon [\boldsymbol{0}, 0]), ~ {\color{m-primary} \text{if} ~ d \ge 1} + \end{array} +@f] + +@m_class{m-noindent} + +otherwise, the interpolation is performed as: @f[ + \begin{array}{rcl} + l + \epsilon m & = & \begin{cases} + \phantom{-}\hat q_A^* \hat q_B, & d \ge 0 \\ + -\hat q_A^* \hat q_B, & d < 0 \\ + \end{cases} \\[15pt] + \frac{\hat a} 2 & = & \arccos \left( l_S \right) - \epsilon m_S \frac 1 {|\boldsymbol{l}_V|} \\[5pt] + \hat {\boldsymbol n} & = & \boldsymbol n_0 + \epsilon \boldsymbol n_\epsilon, + ~~~~~~~~ \boldsymbol n_0 = \boldsymbol{l}_V \frac 1 {|\boldsymbol{l}_V|}, + ~~~~~~~~ \boldsymbol n_\epsilon = \left(\boldsymbol{m}_V - {\boldsymbol n}_0 \frac {a_\epsilon} 2 l_S \right)\frac 1 {|\boldsymbol{l}_V|} \\[5pt] + {\hat q}_{ScLERP} & = & \hat q_A (\hat q_A^* \hat q_B)^t = + \hat q_A \left[ \hat {\boldsymbol n} \sin \left( t \frac {\hat a} 2 \right), \cos \left( t \frac {\hat a} 2 \right) \right] + \end{array} +@f] +@see @ref DualQuaternion::isNormalized(), @ref lerpShortestPath(), + @ref slerpShortestPath() +*/ +template inline DualQuaternion sclerpShortestPath(const DualQuaternion& normalizedA, const DualQuaternion& normalizedB, const T t) { + CORRADE_ASSERT(normalizedA.isNormalized() && normalizedB.isNormalized(), + "Math::sclerp(): dual quaternions must be normalized", {}); + const T cosHalfAngle = dot(normalizedA.real(), normalizedB.real()); + /* Avoid division by zero: interpolate just the translation part */ /** @todo could this be optimized somehow? */ if(std::abs(cosHalfAngle) >= T(1)) diff --git a/src/Magnum/Math/Quaternion.h b/src/Magnum/Math/Quaternion.h index 8917a5549..528bbbbea 100644 --- a/src/Magnum/Math/Quaternion.h +++ b/src/Magnum/Math/Quaternion.h @@ -87,6 +87,9 @@ template inline Rad angle(const Quaternion& normalizedA, const Qu Expects that both quaternions are normalized. @f[ q_{LERP} = \frac{(1 - t) q_A + t q_B}{|(1 - t) q_A + t q_B|} @f] + +Note that this function does not check for shortest path interpolation, see +@ref lerpShortestPath() for an alternative. @see @ref Quaternion::isNormalized(), @ref slerp(const Quaternion&, const Quaternion&, T), @ref sclerp(), @ref lerp(const T&, const T&, U), @@ -101,6 +104,31 @@ template inline Quaternion lerp(const Quaternion& normalizedA, co return ((T(1) - t)*normalizedA + t*normalizedB).normalized(); } +/** @relatesalso Quaternion +@brief Linear shortest-path interpolation of two quaternions +@param normalizedA First quaternion +@param normalizedB Second quaternion +@param t Interpolation phase (from range @f$ [0; 1] @f$) + +Unlike @ref lerp(const Quaternion&, const Quaternion&, T), this +interpolates on the shortest path at some performance expense. Expects that +both quaternions are normalized. @f[ + \begin{array}{rcl} + d & = & q_A \cdot q_B \\[5pt] + q'_A & = & \begin{cases} + \phantom{-}q_A, & d \ge 0 \\ + -q_A, & d < 0 + \end{cases} \\[15pt] + q_{LERP} & = & \cfrac{(1 - t) q'_A + t q_B}{|(1 - t) q'_A + t q_B|} + \end{array} +@f] +@see @ref Quaternion::isNormalized(), @ref slerpShortestPath(), + @ref sclerpShortestPath() +*/ +template inline Quaternion lerpShortestPath(const Quaternion& normalizedA, const Quaternion& normalizedB, T t) { + return lerp(dot(normalizedA, normalizedB) < T(0) ? -normalizedA : normalizedA, normalizedB, t); +} + /** @relatesalso Quaternion @brief Spherical linear interpolation of two quaternions @param normalizedA First quaternion @@ -108,11 +136,24 @@ template inline Quaternion lerp(const Quaternion& normalizedA, co @param t Interpolation phase (from range @f$ [0; 1] @f$) Expects that both quaternions are normalized. If the quaternions are the same -or one is a negation of the other, returns the first argument. @f[ - q_{SLERP} = \frac{sin((1 - t) \theta) q_A + sin(t \theta) q_B}{sin \theta} - ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ - \theta = acos \left( \frac{q_A \cdot q_B}{|q_A| \cdot |q_B|} \right) = acos(q_A \cdot q_B) +or one is a negation of the other, it just returns the first argument: @f[ + \begin{array}{rcl} + d & = & q_A \cdot q_B \\[5pt] + q_{SLERP} & = & q_A, ~ {\color{m-primary} \text{if} ~ d \ge 1} + \end{array} @f] + +@m_class{m-noindent} + +otherwise, the interpolation is performed as: @f[ + \begin{array}{rcl} + \theta & = & \arccos \left( \frac{q_A \cdot q_B}{|q_A| |q_B|} \right) = \arccos(q_A \cdot q_B) = \arccos(d) \\[5pt] + q_{SLERP} & = & \cfrac{\sin((1 - t) \theta) q_A + \sin(t \theta) q_B}{\sin(\theta)} + \end{array} +@f] + +Note that this function does not check for shortest path interpolation, see +@ref slerpShortestPath() for an alternative. @see @ref Quaternion::isNormalized(), @ref lerp(const Quaternion&, const Quaternion&, T), @ref slerp(const Complex&, const Complex&, T), @ref sclerp() */ @@ -128,11 +169,62 @@ template inline Quaternion slerp(const Quaternion& normalizedA, c return (std::sin((T(1) - t)*a)*normalizedA + std::sin(t*a)*normalizedB)/std::sin(a); } +/** @relatesalso Quaternion +@brief Spherical linear shortest-path interpolation of two quaternions +@param normalizedA First quaternion +@param normalizedB Second quaternion +@param t Interpolation phase (from range @f$ [0; 1] @f$) + +Unlike @ref slerp(const Quaternion&, const Quaternion&, T) this function +interpolates on the shortest path. Expects that both quaternions are +normalized. If the quaternions are the same or one is a negation of the other, +it just returns the first argument: @f[ + \begin{array}{rcl} + d & = & q_A \cdot q_B \\ + q_{SLERP} & = & q_A, ~ {\color{m-primary} \text{if} ~ d \ge 1} + \end{array} +@f] + +@m_class{m-noindent} + +otherwise, the interpolation is performed as: @f[ + \begin{array}{rcl} + q'_A & = & \begin{cases} + \phantom{-}q_A, & d \ge 0 \\ + -q_A, & d < 0 + \end{cases} \\[15pt] + \theta & = & \arccos \left( \frac{|q'_A \cdot q_B|}{|q'_A| |q_B|} \right) = \arccos(|q'_A \cdot q_B|) = \arccos(|d|) \\[5pt] + q_{SLERP} & = & \cfrac{\sin((1 - t) \theta) q'_A + \sin(t \theta) q_B}{\sin(\theta)} + \end{array} +@f] +@see @ref Quaternion::isNormalized(), @ref lerpShortestPath(), + @ref sclerpShortestPath() +*/ +template inline Quaternion slerpShortestPath(const Quaternion& normalizedA, const Quaternion& normalizedB, T t) { + CORRADE_ASSERT(normalizedA.isNormalized() && normalizedB.isNormalized(), + "Math::slerp(): quaternions must be normalized", {}); + const T cosHalfAngle = dot(normalizedA, normalizedB); + + /* Avoid division by zero */ + if(std::abs(cosHalfAngle) >= T(1)) return Quaternion{normalizedA}; + + const Quaternion shortestNormalizedA = cosHalfAngle < 0 ? -normalizedA : normalizedA; + + const T a = std::acos(std::abs(cosHalfAngle)); + return (std::sin((T(1) - t)*a)*shortestNormalizedA + std::sin(t*a)*normalizedB)/std::sin(a); +} + /** @brief Quaternion @tparam T Underlying data type -Represents 3D rotation. See @ref transformations for brief introduction. +Represents 3D rotation. Usually denoted as the following in equations, with +@f$ \boldsymbol{q}_V @f$ being the @ref vector() part and @f$ q_S @f$ being the +@ref scalar() part: @f[ + q = [\boldsymbol{q}_V, q_S] +@f] + +See @ref transformations for a brief introduction. @see @ref Magnum::Quaternion, @ref Magnum::Quaterniond, @ref DualQuaternion, @ref Matrix4 */ diff --git a/src/Magnum/Math/Test/DualQuaternionTest.cpp b/src/Magnum/Math/Test/DualQuaternionTest.cpp index 9b241b99d..23875683d 100644 --- a/src/Magnum/Math/Test/DualQuaternionTest.cpp +++ b/src/Magnum/Math/Test/DualQuaternionTest.cpp @@ -90,6 +90,7 @@ struct DualQuaternionTest: Corrade::TestSuite::Tester { void transformPointNormalized(); void sclerp(); + void sclerpShortestPath(); void debug(); void configuration(); @@ -144,6 +145,7 @@ DualQuaternionTest::DualQuaternionTest() { &DualQuaternionTest::transformPointNormalized, &DualQuaternionTest::sclerp, + &DualQuaternionTest::sclerpShortestPath, &DualQuaternionTest::debug, &DualQuaternionTest::configuration}); @@ -480,40 +482,98 @@ void DualQuaternionTest::transformPointNormalized() { void DualQuaternionTest::sclerp() { auto from = DualQuaternion::translation({20.0f, 0.0f, 0.0f})* - DualQuaternion::rotation(180.0_degf, Vector3::yAxis()); + DualQuaternion::rotation(65.0_degf, Vector3::yAxis()); auto to = DualQuaternion::translation({42.0f, 42.0f, 42.0f})* DualQuaternion::rotation(75.0_degf, Vector3::xAxis()); + const DualQuaternion begin = Math::sclerp(from, to, 0.0f); + const DualQuaternion beginShortestPath = Math::sclerpShortestPath(from, to, 0.0f); + const DualQuaternion end = Math::sclerp(from, to, 1.0f); + const DualQuaternion endShortestPath = Math::sclerpShortestPath(from, to, 1.0f); + CORRADE_COMPARE(begin, from); + CORRADE_COMPARE(beginShortestPath, from); + CORRADE_COMPARE(end, to); + CORRADE_COMPARE(endShortestPath, to); + DualQuaternion expected1{ - {{0.23296291f, 0.92387953f, 0.0f}, 0.30360317f}, - {{2.23561910f, 2.81697198f, 10.72224091f}, -10.28763633f}}; + {{0.170316f, 0.424975f, 0.0f}, 0.889038f}, + {{10.689f, 7.47059f, 5.33428f}, -5.61881f}}; DualQuaternion expected2{ - {{0.44376798f, 0.68454710f, 0.0f}, 0.57832969f}, - {{5.76439487f, 11.16130665f, 9.67126701f}, -17.63439459f}}; + {{0.34568f, 0.282968f, 0.0f}, 0.89467f}, + {{12.8764f, 15.8357f, 5.03088f}, -9.98371f}}; DualQuaternion expected3{ - {{0.59797859f, 0.18738131f, 0.0f}, 0.77930087f}, - {{13.40962790f, 25.45212445f, 5.68158104f}, -16.40948111f}}; + {{0.550678f, 0.072563f, 0.0f}, 0.831558f}, + {{15.6916f, 26.3477f, 4.23219f}, -12.6905f}}; const DualQuaternion interp1 = Math::sclerp(from, to, 0.25f); + const DualQuaternion interp1ShortestPath = Math::sclerpShortestPath(from, to, 0.25f); const DualQuaternion interp2 = Math::sclerp(from, to, 0.52f); + const DualQuaternion interp2ShortestPath = Math::sclerpShortestPath(from, to, 0.52f); const DualQuaternion interp3 = Math::sclerp(from, to, 0.88f); + const DualQuaternion interp3ShortestPath = Math::sclerpShortestPath(from, to, 0.88f); CORRADE_COMPARE(interp1, expected1); + CORRADE_COMPARE(interp1ShortestPath, expected1); CORRADE_COMPARE(interp2, expected2); + CORRADE_COMPARE(interp2ShortestPath, expected2); CORRADE_COMPARE(interp3, expected3); + CORRADE_COMPARE(interp3ShortestPath, expected3); /* Edge cases: */ /* Dual quaternions with identical rotation */ CORRADE_COMPARE(Math::sclerp(from, from, 0.42f), from); + CORRADE_COMPARE(Math::sclerpShortestPath(from, from, 0.42f), from); CORRADE_COMPARE(Math::sclerp(from, -from, 0.42f), from); + CORRADE_COMPARE(Math::sclerpShortestPath(from, -from, 0.42f), from); /* No difference in rotation, but in translation */ - auto rotation = DualQuaternion::rotation(35.0_degf, Vector3{0.3f, 0.2f, 0.1f}.normalized()); - auto interpolateTranslation = Math::sclerp( - DualQuaternion::translation({1.0f, 2.0f, 4.0f})*rotation, DualQuaternion::translation({5.0f, -6.0f, 2.0f})*rotation, 0.25f); - CORRADE_VERIFY(interpolateTranslation.isNormalized()); - CORRADE_COMPARE(interpolateTranslation, DualQuaternion::translation({2.0f, 0.0f, 3.5f})*rotation); + { + auto rotation = DualQuaternion::rotation(35.0_degf, Vector3{0.3f, 0.2f, 0.1f}.normalized()); + auto a = DualQuaternion::translation({1.0f, 2.0f, 4.0f})*rotation; + auto b = DualQuaternion::translation({5.0f, -6.0f, 2.0f})*rotation; + auto expected = DualQuaternion::translation({2.0f, 0.0f, 3.5f})*rotation; + + auto interpolateTranslation = Math::sclerp(a, b, 0.25f); + auto interpolateTranslationShortestPath = Math::sclerpShortestPath(a, b, 0.25f); + CORRADE_VERIFY(interpolateTranslation.isNormalized()); + CORRADE_VERIFY(interpolateTranslationShortestPath.isNormalized()); + CORRADE_COMPARE(interpolateTranslation, expected); + CORRADE_COMPARE(interpolateTranslationShortestPath, expected); + } +} + +void DualQuaternionTest::sclerpShortestPath() { + DualQuaternion a = DualQuaternion::translation({1.5f, 0.3f, 0.0f})* + DualQuaternion::rotation(0.0_degf, Vector3::zAxis()); + DualQuaternion b = DualQuaternion::translation({3.5f, 0.3f, 1.0f})* + DualQuaternion::rotation(225.0_degf, Vector3::zAxis()); + + DualQuaternion sclerp = Math::sclerp(a, b, 0.25f); + DualQuaternion sclerpShortestPath = Math::sclerpShortestPath(a, b, 0.25f); + + CORRADE_VERIFY(sclerp.isNormalized()); + CORRADE_VERIFY(sclerpShortestPath.isNormalized()); + CORRADE_COMPARE(sclerp.rotation().axis(), Vector3::zAxis()); + /** @todo why is this inverted compared to QuaternionTest::slerpShortestPath()? */ + CORRADE_COMPARE(sclerpShortestPath.rotation().axis(), -Vector3::zAxis()); + CORRADE_COMPARE(sclerp.rotation().angle(), 56.25_degf); + /* Because the axis is inverted, this is also inverted compared to + QuaternionTest::slerpShortestPath() */ + CORRADE_COMPARE(sclerpShortestPath.rotation().angle(), 360.0_degf - 326.25_degf); + + CORRADE_COMPARE(sclerp, (DualQuaternion{ + {{0.0f, 0.0f, 0.471397f}, 0.881921f}, + {{0.536892f, -0.692656f, 0.11024f}, -0.0589246f}})); + /* Also inverted compared to QuaternionTest::slerpShortestPath() */ + CORRADE_COMPARE(sclerpShortestPath, (DualQuaternion{ + {{0.0f, 0.0f, -0.290285f}, 0.95694f}, + {{0.794402f, 0.651539f, 0.119618f}, 0.0362856f}})); + + /* Translation along Z should be the same in both, in 25% of the way. + Translation in the XY plane is along a screw, so that's different. */ + CORRADE_COMPARE(sclerpShortestPath.translation().z(), 0.25f); + CORRADE_COMPARE(sclerpShortestPath.translation().z(), 0.25f); } void DualQuaternionTest::debug() { diff --git a/src/Magnum/Math/Test/QuaternionTest.cpp b/src/Magnum/Math/Test/QuaternionTest.cpp index 202697898..d29b2997d 100644 --- a/src/Magnum/Math/Test/QuaternionTest.cpp +++ b/src/Magnum/Math/Test/QuaternionTest.cpp @@ -89,12 +89,16 @@ struct QuaternionTest: Corrade::TestSuite::Tester { void rotation(); void angle(); void matrix(); + void lerp(); + void lerpShortestPath(); void lerp2D(); void lerpNotNormalized(); void slerp(); + void slerpShortestPath(); void slerp2D(); void slerpNotNormalized(); + void transformVector(); void transformVectorNormalized(); @@ -150,12 +154,16 @@ QuaternionTest::QuaternionTest() { &QuaternionTest::rotation, &QuaternionTest::angle, &QuaternionTest::matrix, + &QuaternionTest::lerp, + &QuaternionTest::lerpShortestPath, &QuaternionTest::lerp2D, &QuaternionTest::lerpNotNormalized, &QuaternionTest::slerp, + &QuaternionTest::slerpShortestPath, &QuaternionTest::slerp2D, &QuaternionTest::slerpNotNormalized, + &QuaternionTest::transformVector, &QuaternionTest::transformVectorNormalized, @@ -481,10 +489,34 @@ void QuaternionTest::matrix() { void QuaternionTest::lerp() { Quaternion a = Quaternion::rotation(15.0_degf, Vector3(1.0f/Constants::sqrt3())); Quaternion b = Quaternion::rotation(23.0_degf, Vector3::xAxis()); + Quaternion lerp = Math::lerp(a, b, 0.35f); + Quaternion lerpShortestPath = Math::lerpShortestPath(a, b, 0.35f); + Quaternion expected{{0.119127f, 0.049134f, 0.049134f}, 0.990445f}; + /* Both should give the same result */ CORRADE_VERIFY(lerp.isNormalized()); - CORRADE_COMPARE(lerp, Quaternion({0.119127f, 0.049134f, 0.049134f}, 0.990445f)); + CORRADE_VERIFY(lerpShortestPath.isNormalized()); + CORRADE_COMPARE(lerp, expected); + CORRADE_COMPARE(lerpShortestPath, expected); +} + +void QuaternionTest::lerpShortestPath() { + Quaternion a = Quaternion::rotation(0.0_degf, Vector3::zAxis()); + Quaternion b = Quaternion::rotation(225.0_degf, Vector3::zAxis()); + + Quaternion slerp = Math::lerp(a, b, 0.25f); + Quaternion slerpShortestPath = Math::lerpShortestPath(a, b, 0.25f); + + CORRADE_VERIFY(slerp.isNormalized()); + CORRADE_VERIFY(slerpShortestPath.isNormalized()); + CORRADE_COMPARE(slerp.axis(), Vector3::zAxis()); + CORRADE_COMPARE(slerpShortestPath.axis(), Vector3::zAxis()); + CORRADE_COMPARE(slerp.angle(), 38.8848_degf); + CORRADE_COMPARE(slerpShortestPath.angle(), 329.448_degf); + + CORRADE_COMPARE(slerp, (Quaternion{{0.0f, 0.0f, 0.332859f}, 0.942977f})); + CORRADE_COMPARE(slerpShortestPath, (Quaternion{{0.0f, 0.0f, 0.26347f}, -0.964667f})); } void QuaternionTest::lerp2D() { @@ -513,14 +545,40 @@ void QuaternionTest::lerpNotNormalized() { void QuaternionTest::slerp() { Quaternion a = Quaternion::rotation(15.0_degf, Vector3(1.0f/Constants::sqrt3())); Quaternion b = Quaternion::rotation(23.0_degf, Vector3::xAxis()); + Quaternion slerp = Math::slerp(a, b, 0.35f); + Quaternion slerpShortestPath = Math::slerpShortestPath(a, b, 0.35f); + Quaternion expected{{0.1191653f, 0.0491109f, 0.0491109f}, 0.9904423f}; + /* Both should give the same result */ CORRADE_VERIFY(slerp.isNormalized()); - CORRADE_COMPARE(slerp, Quaternion({0.1191653f, 0.0491109f, 0.0491109f}, 0.9904423f)); + CORRADE_COMPARE(slerp, expected); + CORRADE_VERIFY(slerpShortestPath.isNormalized()); + CORRADE_COMPARE(slerpShortestPath, expected); /* Avoid division by zero */ CORRADE_COMPARE(Math::slerp(a, a, 0.25f), a); CORRADE_COMPARE(Math::slerp(a, -a, 0.42f), a); + CORRADE_COMPARE(Math::slerpShortestPath(a, a, 0.25f), a); + CORRADE_COMPARE(Math::slerpShortestPath(a, -a, 0.25f), a); +} + +void QuaternionTest::slerpShortestPath() { + Quaternion a = Quaternion::rotation(0.0_degf, Vector3::zAxis()); + Quaternion b = Quaternion::rotation(225.0_degf, Vector3::zAxis()); + + Quaternion slerp = Math::slerp(a, b, 0.25f); + Quaternion slerpShortestPath = Math::slerpShortestPath(a, b, 0.25f); + + CORRADE_VERIFY(slerp.isNormalized()); + CORRADE_VERIFY(slerpShortestPath.isNormalized()); + CORRADE_COMPARE(slerp.axis(), Vector3::zAxis()); + CORRADE_COMPARE(slerpShortestPath.axis(), Vector3::zAxis()); + CORRADE_COMPARE(slerp.angle(), 56.25_degf); + CORRADE_COMPARE(slerpShortestPath.angle(), 326.25_degf); + + CORRADE_COMPARE(slerp, (Quaternion{{0.0f, 0.0f, 0.471397f}, 0.881921f})); + CORRADE_COMPARE(slerpShortestPath, (Quaternion{{0.0f, 0.0f, 0.290285f}, -0.95694f})); } void QuaternionTest::slerp2D() {