diff --git a/doc/changelog.dox b/doc/changelog.dox index 1c22b3fce..410c51cfe 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -47,7 +47,9 @@ See also: @subsubsection changelog-latest-new-math Math library -- New @ref Math::CubicHermite class for cubic Hermite spline interpolation +- New @ref Math::CubicHermite class for cubic Hermite spline interpolation, + convertible to and from cubic Bézier curves using + @ref Math::Bezier::fromCubicHermite() and @ref Math::CubicHermite::fromBezier() - Added @ref Math::Intersection::rangeFrustum(), @ref Math::Intersection::aabbFrustum(), @ref Math::Intersection::sphereFrustum(), diff --git a/doc/snippets/MagnumMath.cpp b/doc/snippets/MagnumMath.cpp index 2b9f3ca11..39f0584cb 100644 --- a/doc/snippets/MagnumMath.cpp +++ b/doc/snippets/MagnumMath.cpp @@ -25,6 +25,8 @@ #include "Magnum/Magnum.h" #include "Magnum/Math/Color.h" +#include "Magnum/Math/Bezier.h" +#include "Magnum/Math/CubicHermite.h" #include "Magnum/Math/DualComplex.h" #include "Magnum/Math/DualQuaternion.h" #include "Magnum/Math/Half.h" @@ -839,6 +841,18 @@ Color4 a = 0x33b27fcc_srgbaf; // {0.0331048f, 0.445201f, 0.212231f, 0.8f} static_cast(a); } +{ +/* [CubicHermite-fromBezier] */ +CubicBezier2D segment; +auto startPoint = CubicHermite2D::fromBezier( + {Vector2{}, Vector2{}, Vector2{}, segment[3]}, segment); +auto endPoint = CubicHermite2D::fromBezier(segment, + {segment[0], Vector2{}, Vector2{}, Vector2{}}); +/* [CubicHermite-fromBezier] */ +static_cast(startPoint); +static_cast(endPoint); +} + { /* [Half-usage] */ using namespace Math::Literals; diff --git a/src/Magnum/Math/Bezier.h b/src/Magnum/Math/Bezier.h index 60668a626..3909332c3 100644 --- a/src/Magnum/Math/Bezier.h +++ b/src/Magnum/Math/Bezier.h @@ -46,8 +46,12 @@ namespace Implementation { @tparam dimensions Dimensions of control points @tparam T Underlying data type -Implementation of M-order N-dimensional -[Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve). +Represents a M-order N-dimensional +[Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve) segment. + +Cubic Bézier curves are fully interchangeable with cubic Hermite splines, use +@ref fromCubicHermite() and @ref CubicHermite::fromBezier() for the conversion. + @see @ref QuadraticBezier, @ref CubicBezier, @ref QuadraticBezier2D, @ref QuadraticBezier3D, @ref CubicBezier2D, @ref CubicBezier3D */ @@ -64,6 +68,37 @@ template class Bezier { Dimensions = dimensions /**< Dimensions of control points */ }; + /** + * @brief Create cubic Hermite spline point from adjacent Bézier curve segments + * + * Given two cubic Hermite spline points defined by points + * @f$ \boldsymbol{p}_i @f$, in-tangents @f$ \boldsymbol{m}_i @f$ and + * out-tangents @f$ \boldsymbol{n}_i @f$, the corresponding cubic + * Bezier curve segment with points @f$ \boldsymbol{c}_0 @f$, + * @f$ \boldsymbol{c}_1 @f$, @f$ \boldsymbol{c}_2 @f$ and + * @f$ \boldsymbol{c}_3 @f$ is defined as: @f[ + * \begin{array}{rcl} + * \boldsymbol{c}_0 & = & \boldsymbol{p}_a \\ + * \boldsymbol{c}_1 & = & \frac{1}{3} \boldsymbol{n}_a - \boldsymbol{p}_a \\ + * \boldsymbol{c}_2 & = & \boldsymbol{p}_b - \frac{1}{3} \boldsymbol{m}_b \\ + * \boldsymbol{c}_3 & = & \boldsymbol{p}_b + * \end{array} + * @f] + * + * Enabled only on @ref CubicBezier for @ref CubicHermite with vector + * underlying types. See @ref CubicHermite::fromBezier() for the + * inverse operation. + */ + template static + #ifndef DOXYGEN_GENERATING_OUTPUT + typename std::enable_if, VectorType>::value && order == 3, Bezier>::type + #else + Bezier + #endif + fromCubicHermite(const CubicHermite& a, const CubicHermite& b) { + return {a.point(), a.outTangent()/T(3) - a.point(), b.point() - b.inTangent()/T(3), b.point()}; + } + /** * @brief Default constructor * diff --git a/src/Magnum/Math/CubicHermite.h b/src/Magnum/Math/CubicHermite.h index e14d909e1..6d4022fb0 100644 --- a/src/Magnum/Math/CubicHermite.h +++ b/src/Magnum/Math/CubicHermite.h @@ -46,6 +46,9 @@ animation keyframe representation. The structure assumes the in/out tangents to be in their final form, i.e. already normalized by length of their adjacent segments. +Cubic Hermite splines are fully interchangeable with cubic Bézier curves, use +@ref fromBezier() and @ref Bezier::fromCubicHermite() for the conversion. + @see @ref CubicHermite2D, @ref CubicHermite3D, @ref Magnum::CubicHermite2D, @ref Magnum::CubicHermite2Dd, @ref Magnum::CubicHermite3D, @ref Magnum::CubicHermite3Dd, @@ -56,6 +59,46 @@ template class CubicHermite { public: typedef T Type; /**< @brief Underlying data type */ + /** + * @brief Create cubic Hermite spline point from adjacent Bézier curve segments + * + * Given two adjacent cubic Bézier curve segments defined by points + * @f$ \boldsymbol{a}_i @f$ and @f$ \boldsymbol{b}_i @f$, + * @f$ i \in \{ 0, 1, 2, 3 \} @f$, the corresponding cubic Hermite + * spline point @f$ \boldsymbol{p} @f$, in-tangent @f$ \boldsymbol{m} @f$ + * and out-tangent @f$ \boldsymbol{n} @f$ is defined as: @f[ + * \begin{array}{rcl} + * \boldsymbol{m} & = & 3 (\boldsymbol{a}_3 - \boldsymbol{a}_2) + * = 3 (\boldsymbol{b}_0 - \boldsymbol{a}_2) \\ + * \boldsymbol{p} & = & \boldsymbol{a}_3 = \boldsymbol{b}_0 \\ + * \boldsymbol{n} & = & 3 (\boldsymbol{b}_1 - \boldsymbol{a}_3) + * = 3 (\boldsymbol{b}_1 - \boldsymbol{b}_0) + * \end{array} + * @f] + * + * Expects that the two segments are adjacent (i.e., the endpoint of + * first segment is the start point of the second). If you need to + * create a cubic Hermite spline point that's at the beginning or at + * the end of a curve, simply pass a dummy Bézier segment that + * satisfies this constraint as the other parameter: + * + * @snippet MagnumMath.cpp CubicHermite-fromBezier + * + * Enabled only on vector underlying types. See + * @ref Bezier::fromCubicHermite() for the inverse operation. + */ + template static + #ifdef DOXYGEN_GENERATING_OUTPUT + CubicHermite + #else + typename std::enable_if, T>::value, CubicHermite>::type + #endif + fromBezier(const CubicBezier& a, const CubicBezier& b) { + return CORRADE_CONSTEXPR_ASSERT(a[3] == b[0], + "Math::CubicHermite::fromBezier(): segments are not adjacent"), + CubicHermite{3*(a[3] - a[2]), a[3], 3*(b[1] - a[3])}; + } + /** * @brief Default constructor * diff --git a/src/Magnum/Math/Test/BezierTest.cpp b/src/Magnum/Math/Test/BezierTest.cpp index 3080afea2..6bc198d08 100644 --- a/src/Magnum/Math/Test/BezierTest.cpp +++ b/src/Magnum/Math/Test/BezierTest.cpp @@ -29,6 +29,7 @@ #include #include "Magnum/Math/Bezier.h" +#include "Magnum/Math/CubicHermite.h" #include "Magnum/Math/Vector2.h" #include "Magnum/Math/Functions.h" @@ -60,6 +61,7 @@ typedef Math::Bezier<1, 2, Float> LinearBezier2D; typedef Math::QuadraticBezier2D QuadraticBezier2D; typedef Math::QuadraticBezier2D QuadraticBezier2Dd; typedef Math::CubicBezier2D CubicBezier2D; +typedef Math::CubicHermite2D CubicHermite2D; struct BezierTest: Corrade::TestSuite::Tester { explicit BezierTest(); @@ -68,6 +70,7 @@ struct BezierTest: Corrade::TestSuite::Tester { void constructDefault(); void constructNoInit(); void constructConversion(); + void constructFromCubicHermite(); void constructCopy(); void convert(); @@ -91,6 +94,7 @@ BezierTest::BezierTest() { &BezierTest::constructDefault, &BezierTest::constructNoInit, &BezierTest::constructConversion, + &BezierTest::constructFromCubicHermite, &BezierTest::constructCopy, &BezierTest::convert, @@ -160,6 +164,16 @@ void BezierTest::constructConversion() { CORRADE_VERIFY((std::is_nothrow_constructible::value)); } +void BezierTest::constructFromCubicHermite() { + /* See CubicHermiterTest::constructFromBezier() for the inverse. Expected + value the same as in valueCubic() to test also interpolation with it. */ + CubicHermite2D a{{}, Vector2{0.0f, 0.0f}, Vector2{30.0f, 45.0f}}; + CubicHermite2D b{Vector2{-45, -72}, Vector2{5.0f, -20.0f}, {}}; + auto bezier = CubicBezier2D::fromCubicHermite(a, b); + + CORRADE_COMPARE(bezier, (CubicBezier2D{Vector2{0.0f, 0.0f}, Vector2{10.0f, 15.0f}, Vector2{20.0f, 4.0f}, Vector2{5.0f, -20.0f}})); +} + void BezierTest::constructCopy() { constexpr QuadraticBezier2D a{Vector2{0.5f, 1.0f}, Vector2{1.1f, 0.3f}, Vector2{0.1f, 1.2f}}; constexpr QuadraticBezier2D b{a}; @@ -237,6 +251,7 @@ void BezierTest::valueQuadratic() { void BezierTest::valueCubic() { CubicBezier2D bezier{Vector2{0.0f, 0.0f}, Vector2{10.0f, 15.0f}, Vector2{20.0f, 4.0f}, Vector2{5.0f, -20.0f}}; + /* Values should be exactly the same as in CubicHermiterTest::splerpVectorFromBezier() */ CORRADE_COMPARE(bezier.value(0.0f), (Vector2{0.0f, 0.0f})); CORRADE_COMPARE(bezier.value(0.2f), (Vector2{5.8f, 5.984f})); CORRADE_COMPARE(bezier.value(0.5f), (Vector2{11.875f, 4.625f})); diff --git a/src/Magnum/Math/Test/CubicHermiteTest.cpp b/src/Magnum/Math/Test/CubicHermiteTest.cpp index ea3fd5213..d27dc94cb 100644 --- a/src/Magnum/Math/Test/CubicHermiteTest.cpp +++ b/src/Magnum/Math/Test/CubicHermiteTest.cpp @@ -26,6 +26,7 @@ #include #include +#include "Magnum/Math/Bezier.h" #include "Magnum/Math/CubicHermite.h" #include "Magnum/Math/Functions.h" #include "Magnum/Math/Vector2.h" @@ -65,6 +66,8 @@ struct CubicHermiteTest: Corrade::TestSuite::Tester { void constructConversionComplex(); void constructConversionQuaternion(); + void constructFromBezier(); + void constructCopyScalar(); void constructCopyVector(); void constructCopyComplex(); @@ -94,6 +97,7 @@ struct CubicHermiteTest: Corrade::TestSuite::Tester { void splerpScalar(); void splerpVector(); + void splerpVectorFromBezier(); void splerpComplex(); void splerpComplexNotNormalized(); void splerpQuaternion(); @@ -136,6 +140,8 @@ CubicHermiteTest::CubicHermiteTest() { &CubicHermiteTest::constructConversionComplex, &CubicHermiteTest::constructConversionQuaternion, + &CubicHermiteTest::constructFromBezier, + &CubicHermiteTest::constructCopyScalar, &CubicHermiteTest::constructCopyVector, &CubicHermiteTest::constructCopyComplex, @@ -165,6 +171,7 @@ CubicHermiteTest::CubicHermiteTest() { &CubicHermiteTest::splerpScalar, &CubicHermiteTest::splerpVector, + &CubicHermiteTest::splerpVectorFromBezier, &CubicHermiteTest::splerpComplex, &CubicHermiteTest::splerpComplexNotNormalized, &CubicHermiteTest::splerpQuaternion, @@ -179,6 +186,7 @@ CubicHermiteTest::CubicHermiteTest() { typedef Math::Vector2 Vector2; typedef Math::Complex Complex; typedef Math::Quaternion Quaternion; +typedef Math::CubicBezier2D CubicBezier2D; typedef Math::CubicHermite1D CubicHermite1D; typedef Math::CubicHermite2D CubicHermite2D; typedef Math::CubicHermiteComplex CubicHermiteComplex; @@ -499,6 +507,20 @@ void CubicHermiteTest::constructConversionQuaternion() { CORRADE_VERIFY((std::is_nothrow_constructible::value)); } +void CubicHermiteTest::constructFromBezier() { + /* Taken from BezierTest::valueCubic() -- we're testing the same values + also in splerpVectorFromBezier(). See + BezierTest::constructFromCubicHermite() for the inverse. */ + CubicBezier2D bezier{Vector2{0.0f, 0.0f}, Vector2{10.0f, 15.0f}, Vector2{20.0f, 4.0f}, Vector2{5.0f, -20.0f}}; + auto a = CubicHermite2D::fromBezier({Vector2{}, Vector2{}, Vector2{}, bezier[0]}, bezier); + auto b = CubicHermite2D::fromBezier(bezier, {bezier[3], Vector2{}, Vector2{}, Vector2{}}); + + CORRADE_COMPARE(a.point(), bezier[0]); + CORRADE_COMPARE(a.outTangent(), (Vector2{30.0f, 45.0f})); + CORRADE_COMPARE(b.inTangent(), (Vector2{-45, -72})); + CORRADE_COMPARE(b.point(), bezier[3]); +} + void CubicHermiteTest::constructCopyScalar() { constexpr CubicHermite1D a{2.0f, -2.0f, -0.5f}; constexpr CubicHermite1D b{a}; @@ -815,6 +837,25 @@ void CubicHermiteTest::splerpVector() { CORRADE_COMPARE(Math::splerp(a, b, 0.8f), (Vector2{-2.152f, 0.9576f})); } +void CubicHermiteTest::splerpVectorFromBezier() { + /* Taken from BezierTest::valueCubic() */ + CubicBezier2D bezier{Vector2{0.0f, 0.0f}, Vector2{10.0f, 15.0f}, Vector2{20.0f, 4.0f}, Vector2{5.0f, -20.0f}}; + auto a = CubicHermite2D::fromBezier({Vector2{}, Vector2{}, Vector2{}, bezier[0]}, bezier); + auto b = CubicHermite2D::fromBezier(bezier, {bezier[3], Vector2{}, Vector2{}, Vector2{}}); + + CORRADE_COMPARE(bezier.value(0.0f), (Vector2{0.0f, 0.0f})); + CORRADE_COMPARE(Math::splerp(a, b, 0.0f), (Vector2{0.0f, 0.0f})); + + CORRADE_COMPARE(bezier.value(0.2f), (Vector2{5.8f, 5.984f})); + CORRADE_COMPARE(Math::splerp(a, b, 0.2f), (Vector2{5.8f, 5.984f})); + + CORRADE_COMPARE(bezier.value(0.5f), (Vector2{11.875f, 4.625f})); + CORRADE_COMPARE(Math::splerp(a, b, 0.5f), (Vector2{11.875f, 4.625f})); + + CORRADE_COMPARE(bezier.value(1.0f), (Vector2{5.0f, -20.0f})); + CORRADE_COMPARE(Math::splerp(a, b, 1.0f), (Vector2{5.0f, -20.0f})); +} + void CubicHermiteTest::splerpComplex() { CubicHermiteComplex a{{2.0f, 1.5f}, {0.999445f, 0.0333148f}, {-1.0f, 0.0f}}; CubicHermiteComplex b{{5.0f, 0.3f}, {-0.876216f, 0.481919f}, {1.5f, 0.3f}};