diff --git a/doc/changelog.dox b/doc/changelog.dox index 346b59f5e..dd81f0fca 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -66,6 +66,9 @@ See also: @ref Math::Color4::fromSrgb(UnsignedInt, T), @ref Math::Color3::toSrgbInt(), and @ref Math::Color4::toSrgbAlphaInt() for easier conversion of packed 24-/32-bit sRGB colors to and from @ref Math::Color3 / @ref Math::Color4 +- 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::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/Complex.h b/src/Magnum/Math/Complex.h index cc9cb73b7..465d70b7b 100644 --- a/src/Magnum/Math/Complex.h +++ b/src/Magnum/Math/Complex.h @@ -456,6 +456,53 @@ template inline Complex operator/(T scalar, const Complex& comple return {scalar/complex.real(), scalar/complex.imaginary()}; } +/** @relatesalso Complex +@brief Linear interpolation of two complex numbers +@param normalizedA First complex number +@param normalizedB Second complex number +@param t Interpolation phase (from range @f$ [0; 1] @f$) + +Expects that both complex numbers are normalized. @f[ + c_{LERP} = \frac{(1 - t) c_A + t c_B}{|(1 - t) c_A + t c_B|} +@f] +@see @ref Complex::isNormalized(), @ref slerp(const Complex&, const Complex&, T), + @ref lerp(const Quaternion&, const Quaternion&, T), + @ref lerp(const T&, const T&, U) +*/ +template inline Complex lerp(const Complex& normalizedA, const Complex& normalizedB, T t) { + CORRADE_ASSERT(normalizedA.isNormalized() && normalizedB.isNormalized(), + "Math::lerp(): complex numbers must be normalized", {}); + return ((T(1) - t)*normalizedA + t*normalizedB).normalized(); +} + +/** @relatesalso Complex +@brief Spherical linear interpolation of two complex numbers +@param normalizedA First complex number +@param normalizedB Second complex number +@param t Interpolation phase (from range @f$ [0; 1] @f$) + +Expects that both complex numbers are normalized. If the complex numbers are +the same, returns the first argument. @f[ + c_{SLERP} = \frac{sin((1 - t) \theta) c_A + sin(t \theta) c_B}{sin \theta} + ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ + \theta = acos \left( \frac{c_A \cdot c_B}{|c_A| \cdot |c_B|} \right) = acos(c_A \cdot c_B) +@f] +@see @ref Complex::isNormalized(), @ref lerp(const Complex&, const Complex&, T), + @ref slerp(const Quaternion&, const Quaternion&, T) + */ +template inline Complex slerp(const Complex& normalizedA, const Complex& normalizedB, T t) { + CORRADE_ASSERT(normalizedA.isNormalized() && normalizedB.isNormalized(), + "Math::slerp(): complex numbers must be normalized", {}); + const T cosAngle = dot(normalizedA, normalizedB); + + /* Avoid division by zero */ + if(std::abs(cosAngle) >= T(1)) return Complex{normalizedA}; + + /** @todo couldn't this be done somewhat simpler? */ + const T a = std::acos(cosAngle); + return (std::sin((T(1) - t)*a)*normalizedA + std::sin(t*a)*normalizedB)/std::sin(a); +} + /** @debugoperator{Complex} */ template Corrade::Utility::Debug& operator<<(Corrade::Utility::Debug& debug, const Complex& value) { return debug << "Complex(" << Corrade::Utility::Debug::nospace diff --git a/src/Magnum/Math/Functions.h b/src/Magnum/Math/Functions.h index 80af3f9dd..0612b91c0 100644 --- a/src/Magnum/Math/Functions.h +++ b/src/Magnum/Math/Functions.h @@ -560,7 +560,8 @@ The interpolation for vectors is done as in following, similarly for scalars: @f @f] See @ref select() for constant interpolation using the same API. -@see @ref lerpInverted(), @ref lerp(const Quaternion&, const Quaternion&, T) +@see @ref lerpInverted(), @ref lerp(const Complex&, const Complex&, T), + @ref lerp(const Quaternion&, const Quaternion&, T) @m_keyword{mix(),GLSL mix(),} */ template inline diff --git a/src/Magnum/Math/Quaternion.h b/src/Magnum/Math/Quaternion.h index 9075b2798..7286c33ac 100644 --- a/src/Magnum/Math/Quaternion.h +++ b/src/Magnum/Math/Quaternion.h @@ -87,8 +87,10 @@ 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] -@see @ref Quaternion::isNormalized(), @ref slerp(const Quaternion&, const Quaternion&, T), - @ref lerp(const T&, const T&, U), @ref sclerp() +@see @ref Quaternion::isNormalized(), + @ref slerp(const Quaternion&, const Quaternion&, T), @ref sclerp(), + @ref lerp(const T&, const T&, U), + @ref lerp(const Complex&, const Complex&, T) */ template inline Quaternion lerp(const Quaternion& normalizedA, const Quaternion& normalizedB, T t) { CORRADE_ASSERT(normalizedA.isNormalized() && normalizedB.isNormalized(), @@ -105,12 +107,12 @@ template inline Quaternion lerp(const Quaternion& normalizedA, co 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) @f] @see @ref Quaternion::isNormalized(), @ref lerp(const Quaternion&, const Quaternion&, T), - @ref sclerp() - */ + @ref slerp(const Complex&, const Complex&, T), @ref sclerp() +*/ template inline Quaternion slerp(const Quaternion& normalizedA, const Quaternion& normalizedB, T t) { CORRADE_ASSERT(normalizedA.isNormalized() && normalizedB.isNormalized(), "Math::slerp(): quaternions must be normalized", {}); diff --git a/src/Magnum/Math/Test/ComplexTest.cpp b/src/Magnum/Math/Test/ComplexTest.cpp index c5540a1bc..c434f57ec 100644 --- a/src/Magnum/Math/Test/ComplexTest.cpp +++ b/src/Magnum/Math/Test/ComplexTest.cpp @@ -86,6 +86,10 @@ struct ComplexTest: Corrade::TestSuite::Tester { void angle(); void rotation(); void matrix(); + void lerp(); + void lerpNotNormalized(); + void slerp(); + void slerpNotNormalized(); void transformVector(); void debug(); @@ -128,6 +132,10 @@ ComplexTest::ComplexTest() { &ComplexTest::angle, &ComplexTest::rotation, &ComplexTest::matrix, + &ComplexTest::lerp, + &ComplexTest::lerpNotNormalized, + &ComplexTest::slerp, + &ComplexTest::slerpNotNormalized, &ComplexTest::transformVector, &ComplexTest::debug, @@ -141,6 +149,8 @@ typedef Math::Vector2 Vector2; typedef Math::Matrix3 Matrix3; typedef Math::Matrix2x2 Matrix2x2; +using namespace Math::Literals; + void ComplexTest::construct() { constexpr Complex a = {0.5f, -3.7f}; CORRADE_COMPARE(a, Complex(0.5f, -3.7f)); @@ -411,6 +421,57 @@ void ComplexTest::matrix() { CORRADE_COMPARE(b, a); } +void ComplexTest::lerp() { + /* Results should be consistent with QuaternionTest::lerp2D() (but not + equivalent, probably because quaternions double cover and complex + numbers not) */ + Complex a = Complex::rotation(15.0_degf); + Complex b = Complex::rotation(57.0_degf); + Complex lerp = Math::lerp(a, b, 0.35f); + + CORRADE_VERIFY(lerp.isNormalized()); + CORRADE_COMPARE(lerp.angle(), 29.4308_degf); /* almost but not quite 29.7 */ + CORRADE_COMPARE(lerp, (Complex{0.87095f, 0.491372f})); +} + +void ComplexTest::lerpNotNormalized() { + std::ostringstream out; + Error redirectError{&out}; + + Complex a; + Math::lerp(a*3.0f, a, 0.35f); + Math::lerp(a, a*-3.0f, 0.35f); + CORRADE_COMPARE(out.str(), + "Math::lerp(): complex numbers must be normalized\n" + "Math::lerp(): complex numbers must be normalized\n"); +} + +void ComplexTest::slerp() { + /* Result angle should be equivalent to QuaternionTest::slerp2D() */ + Complex a = Complex::rotation(15.0_degf); + Complex b = Complex::rotation(57.0_degf); + Complex slerp = Math::slerp(a, b, 0.35f); + + CORRADE_VERIFY(slerp.isNormalized()); + CORRADE_COMPARE(slerp.angle(), 29.7_degf); /* 15 + (57-15)*0.35 */ + CORRADE_COMPARE(slerp, (Complex{0.868632f, 0.495459f})); + + /* Avoid division by zero */ + CORRADE_COMPARE(Math::slerp(a, a, 0.25f), a); +} + +void ComplexTest::slerpNotNormalized() { + std::ostringstream out; + Error redirectError{&out}; + + Complex a; + Math::slerp(a*3.0f, a, 0.35f); + Math::slerp(a, a*-3.0f, 0.35f); + CORRADE_COMPARE(out.str(), + "Math::slerp(): complex numbers must be normalized\n" + "Math::slerp(): complex numbers must be normalized\n"); +} + void ComplexTest::transformVector() { Complex a = Complex::rotation(Deg(23.0f)); Matrix3 m = Matrix3::rotation(Deg(23.0f)); diff --git a/src/Magnum/Math/Test/QuaternionTest.cpp b/src/Magnum/Math/Test/QuaternionTest.cpp index f779d477d..662e0a7d4 100644 --- a/src/Magnum/Math/Test/QuaternionTest.cpp +++ b/src/Magnum/Math/Test/QuaternionTest.cpp @@ -88,8 +88,10 @@ struct QuaternionTest: Corrade::TestSuite::Tester { void angle(); void matrix(); void lerp(); + void lerp2D(); void lerpNotNormalized(); void slerp(); + void slerp2D(); void slerpNotNormalized(); void transformVector(); void transformVectorNormalized(); @@ -145,8 +147,10 @@ QuaternionTest::QuaternionTest() { &QuaternionTest::angle, &QuaternionTest::matrix, &QuaternionTest::lerp, + &QuaternionTest::lerp2D, &QuaternionTest::lerpNotNormalized, &QuaternionTest::slerp, + &QuaternionTest::slerp2D, &QuaternionTest::slerpNotNormalized, &QuaternionTest::transformVector, &QuaternionTest::transformVectorNormalized, @@ -469,6 +473,17 @@ void QuaternionTest::lerp() { CORRADE_COMPARE(lerp, Quaternion({0.119127f, 0.049134f, 0.049134f}, 0.990445f)); } +void QuaternionTest::lerp2D() { + /* Results should be consistent with ComplexTest::lerp() */ + Quaternion a = Quaternion::rotation(15.0_degf, Vector3::zAxis()); + Quaternion b = Quaternion::rotation(57.0_degf, Vector3::zAxis()); + Quaternion lerp = Math::lerp(a, b, 0.35f); + + CORRADE_VERIFY(lerp.isNormalized()); + CORRADE_COMPARE(lerp.angle(), 29.6351_degf); /* almost but not quite 29.7 */ + CORRADE_COMPARE(lerp, (Quaternion{{0.0f, 0.0f, 0.255742f}, 0.966745f})); +} + void QuaternionTest::lerpNotNormalized() { std::ostringstream out; Error redirectError{&out}; @@ -494,6 +509,17 @@ void QuaternionTest::slerp() { CORRADE_COMPARE(Math::slerp(a, -a, 0.42f), a); } +void QuaternionTest::slerp2D() { + /* Result angle should be equivalent to ComplexTest::slerp() */ + Quaternion a = Quaternion::rotation(15.0_degf, Vector3::zAxis()); + Quaternion b = Quaternion::rotation(57.0_degf, Vector3::zAxis()); + Quaternion slerp = Math::slerp(a, b, 0.35f); + + CORRADE_VERIFY(slerp.isNormalized()); + CORRADE_COMPARE(slerp.angle(), 29.7_degf); /* 15 + (57-15)*0.35 */ + CORRADE_COMPARE(slerp, (Quaternion{{0.0f, 0.0f, 0.256289f}, 0.9666f})); +} + void QuaternionTest::slerpNotNormalized() { std::ostringstream out; Error redirectError{&out};