// Copyright Epic Games, Inc. All Rights Reserved. #include "Channels/MovieSceneInterpolation.h" #include "Curves/CurveEvaluation.h" #include "Algo/Partition.h" namespace UE::MovieScene::Interpolation { bool FInterpolationExtents::IsValid() const { return MinValue != std::numeric_limits::max() && MaxValue != std::numeric_limits::lowest(); } void FInterpolationExtents::AddPoint(double Value, FFrameTime Time) { if (Value < MinValue) { MinValue = Value; MinValueTime = Time; } if (Value > MaxValue) { MaxValue = Value; MaxValueTime = Time; } } void FInterpolationExtents::Combine(const FInterpolationExtents& Other) { if (Other.IsValid()) { AddPoint(Other.MinValue, Other.MinValueTime); AddPoint(Other.MaxValue, Other.MaxValueTime); } } FCachedInterpolationRange FCachedInterpolationRange::Empty() { return FCachedInterpolationRange{ 0, -1 }; } FCachedInterpolationRange FCachedInterpolationRange::Finite(FFrameNumber InStart, FFrameNumber InEnd) { return FCachedInterpolationRange{ InStart, InEnd }; } FCachedInterpolationRange FCachedInterpolationRange::Infinite() { return FCachedInterpolationRange{ TNumericLimits::Lowest(), TNumericLimits::Max() }; } FCachedInterpolationRange FCachedInterpolationRange::Only(FFrameNumber InTime) { const FFrameNumber EndTime = InTime < TNumericLimits::Max() ? InTime + 1 : InTime; return FCachedInterpolationRange{ InTime, EndTime }; } FCachedInterpolationRange FCachedInterpolationRange::From(FFrameNumber InStart) { return FCachedInterpolationRange{ InStart, TNumericLimits::Max() }; } FCachedInterpolationRange FCachedInterpolationRange::Until(FFrameNumber InEnd) { return FCachedInterpolationRange{ TNumericLimits::Lowest(), InEnd }; } bool FCachedInterpolationRange::Contains(FFrameNumber FrameNumber) const { return FrameNumber >= Start && (FrameNumber < End || End == TNumericLimits::Max()); } bool FCachedInterpolationRange::IsEmpty() const { return Start >= End; } FCachedInterpolation::FCachedInterpolation() : Data(TInPlaceType()) , Range(FCachedInterpolationRange::Empty()) { } FCachedInterpolation::FCachedInterpolation(const FCachedInterpolationRange& InRange, const FConstantValue& Constant) : Data(TInPlaceType(), Constant) , Range(InRange) { } FCachedInterpolation::FCachedInterpolation(const FCachedInterpolationRange& InRange, const FLinearInterpolation& Linear) : Data(TInPlaceType(), Linear) , Range(InRange) { } FCachedInterpolation::FCachedInterpolation(const FCachedInterpolationRange& InRange, const FQuadraticInterpolation& Quadratic) : Data(TInPlaceType(), Quadratic) , Range(InRange) { } FCachedInterpolation::FCachedInterpolation(const FCachedInterpolationRange& InRange, const FCubicInterpolation& Cubic) : Data(TInPlaceType(), Cubic) , Range(InRange) { } FCachedInterpolation::FCachedInterpolation(const FCachedInterpolationRange& InRange, const FQuarticInterpolation& Quartic) : Data(TInPlaceType(), Quartic) , Range(InRange) { } FCachedInterpolation::FCachedInterpolation(const FCachedInterpolationRange& InRange, const FCubicBezierInterpolation& Cubic) : Data(TInPlaceType(), Cubic) , Range(InRange) { } FCachedInterpolation::FCachedInterpolation(const FCachedInterpolationRange& InRange, const FWeightedCubicInterpolation& WeightedCubic) : Data(TInPlaceType(), WeightedCubic) , Range(InRange) { } bool FCachedInterpolation::IsValid() const { return !Range.IsEmpty(); } bool FCachedInterpolation::IsCacheValidForTime(FFrameNumber FrameNumber) const { return Range.Contains(FrameNumber); } FCachedInterpolationRange FCachedInterpolation::GetRange() const { return Range; } FInterpolationExtents FCachedInterpolation::ComputeExtents() const { return ComputeExtents(Range.Start, Range.End); } FInterpolationExtents FCachedInterpolation::ComputeExtents(FFrameTime From, FFrameTime To) const { FInterpolationExtents Extents; if (const FConstantValue* Constant = Data.TryGet()) { Extents.AddPoint(Constant->Value, From); Extents.AddPoint(Constant->Value, To); } else if (const FLinearInterpolation* Linear = Data.TryGet()) { Extents.AddPoint(Linear->Evaluate(From), From); Extents.AddPoint(Linear->Evaluate(To), To); } else if (const FQuadraticInterpolation* Quadratic = Data.TryGet()) { Extents.AddPoint(Quadratic->Evaluate(From), From); Extents.AddPoint(Quadratic->Evaluate(To), To); FLinearInterpolation Derivative = Quadratic->Derivative(); FFrameTime Solutions[1]; const int32 NumSolutions = Derivative.SolveWithin(From, To, 0.0, Solutions); check(NumSolutions <= 1); for (int32 Index = 0; Index < NumSolutions; ++Index) { Extents.AddPoint(Quadratic->Evaluate(Solutions[Index]), Solutions[Index]); } } else if (const FCubicInterpolation* Cubic = Data.TryGet()) { Extents.AddPoint(Cubic->Evaluate(From), From); Extents.AddPoint(Cubic->Evaluate(To), To); FQuadraticInterpolation Derivative = Cubic->Derivative(); FFrameTime Solutions[2]; const int32 NumSolutions = Derivative.SolveWithin(From, To, 0.0, Solutions); check(NumSolutions <= 2); for (int32 Index = 0; Index < NumSolutions; ++Index) { Extents.AddPoint(Cubic->Evaluate(Solutions[Index]), Solutions[Index]); } } else if (const FQuarticInterpolation* Quartic = Data.TryGet()) { Extents.AddPoint(Quartic->Evaluate(From), From); Extents.AddPoint(Quartic->Evaluate(To), To); FCubicInterpolation Derivative = Quartic->Derivative(); FFrameTime Solutions[3]; const int32 NumSolutions = Derivative.SolveWithin(From, To, 0.0, Solutions); check(NumSolutions <= 3); for (int32 Index = 0; Index < NumSolutions; ++Index) { Extents.AddPoint(Quartic->Evaluate(Solutions[Index]), Solutions[Index]); } } else if (const FCubicBezierInterpolation* CubicBezier = Data.TryGet()) { const double Origin = CubicBezier->Origin.Value; const double DX = CubicBezier->DX; Extents.AddPoint(CubicBezier->Evaluate(From), From); Extents.AddPoint(CubicBezier->Evaluate(To), To); FQuadraticInterpolation Derivative = CubicBezier->Derivative(); FFrameTime Solutions[2]; const int32 NumSolutions = Derivative.SolveWithin(From, To, 0.0, Solutions); check(NumSolutions <= 2); for (int32 Index = 0; Index < NumSolutions; ++Index) { Extents.AddPoint(CubicBezier->Evaluate(Solutions[Index]), Solutions[Index]); } } else if (const FWeightedCubicInterpolation* WeightedCubic = Data.TryGet()) { Extents.AddPoint(WeightedCubic->Evaluate(From), From); Extents.AddPoint(WeightedCubic->Evaluate(To), To); } ensureMsgf( Extents.MinValueTime >= From && Extents.MinValueTime <= To && Extents.MaxValueTime >= From && Extents.MaxValueTime <= To, TEXT("ComputeExtents resulted in Min: %f and Max: %f, but expected to be between From: %f To: %f"), Extents.MinValueTime.AsDecimal(), Extents.MaxValueTime.AsDecimal(), From.AsDecimal(), To.AsDecimal()); return Extents; } bool FCachedInterpolation::Evaluate(FFrameTime Time, double& OutResult) const { if (const FConstantValue* Constant = Data.TryGet()) { OutResult = Constant->Value; return true; } else if (const FLinearInterpolation* Linear = Data.TryGet()) { OutResult = Linear->Evaluate(Time); return true; } else if (const FQuadraticInterpolation* Quadratic = Data.TryGet()) { OutResult = Quadratic->Evaluate(Time); return true; } else if (const FCubicInterpolation* Cubic = Data.TryGet()) { OutResult = Cubic->Evaluate(Time); return true; } else if (const FQuarticInterpolation* Quartic = Data.TryGet()) { OutResult = Quartic->Evaluate(Time); return true; } else if (const FCubicBezierInterpolation* CubicBezier = Data.TryGet()) { OutResult = CubicBezier->Evaluate(Time); return true; } else if (const FWeightedCubicInterpolation* WeightedCubic = Data.TryGet()) { OutResult = WeightedCubic->Evaluate(Time); return true; } return false; } void FCachedInterpolation::Offset(double Amount) { if (FConstantValue* Constant = Data.TryGet()) { Constant->Value += Amount; } else if (FLinearInterpolation* Linear = Data.TryGet()) { Linear->Constant += Amount; } else if (FQuadraticInterpolation* Quadratic = Data.TryGet()) { Quadratic->Constant += Amount; } else if (FCubicInterpolation* Cubic = Data.TryGet()) { Cubic->Constant += Amount; } else if (FQuarticInterpolation* Quartic = Data.TryGet()) { Quartic->Constant += Amount; } else if (FCubicBezierInterpolation* CubicBezier = Data.TryGet()) { CubicBezier->P0 += Amount; CubicBezier->P1 += Amount; CubicBezier->P2 += Amount; CubicBezier->P3 += Amount; } else if (FWeightedCubicInterpolation* WeightedCubic = Data.TryGet()) { WeightedCubic->StartKeyValue += Amount; WeightedCubic->EndKeyValue += Amount; } } TOptional FCachedInterpolation::ComputeIntegral(double ConstantOffset) const { if (const FConstantValue* Constant = Data.TryGet()) { return FCachedInterpolation(Range, Constant->Integral(ConstantOffset)); } else if (const FLinearInterpolation* Linear = Data.TryGet()) { return FCachedInterpolation(Range, Linear->Integral(ConstantOffset)); } else if (const FQuadraticInterpolation* Quadratic = Data.TryGet()) { return FCachedInterpolation(Range, Quadratic->Integral(ConstantOffset)); } else if (const FCubicInterpolation* Cubic = Data.TryGet()) { return FCachedInterpolation(Range, Cubic->Integral(ConstantOffset)); } else if (const FQuarticInterpolation* Quartic = Data.TryGet()) { checkf(false, TEXT("Unable to compute the integral of a quartic. Although the math is easy, quintic curves are practically impossible to solve.")); } else if (const FCubicBezierInterpolation* CubicBezier = Data.TryGet()) { return FCachedInterpolation(Range, CubicBezier->Integral(ConstantOffset)); } else if (const FWeightedCubicInterpolation* WeightedCubic = Data.TryGet()) { } return TOptional(); } TOptional FCachedInterpolation::ComputeDerivative() const { if (const FConstantValue* Constant = Data.TryGet()) { return FCachedInterpolation(Range, Constant->Derivative()); } else if (const FLinearInterpolation* Linear = Data.TryGet()) { return FCachedInterpolation(Range, Linear->Derivative()); } else if (const FQuadraticInterpolation* Quadratic = Data.TryGet()) { return FCachedInterpolation(Range, Quadratic->Derivative()); } else if (const FCubicInterpolation* Cubic = Data.TryGet()) { return FCachedInterpolation(Range, Cubic->Derivative()); } else if (const FQuarticInterpolation* Quartic = Data.TryGet()) { return FCachedInterpolation(Range, Quartic->Derivative()); } else if (const FCubicBezierInterpolation* CubicBezier = Data.TryGet()) { return FCachedInterpolation(Range, CubicBezier->Derivative()); } else if (const FWeightedCubicInterpolation* WeightedCubic = Data.TryGet()) { } return TOptional(); } int32 FCachedInterpolation::InverseEvaluate(double InValue, TInterpSolutions OutResults) const { int32 NumSolutions = 0; if (const FConstantValue* Constant = Data.TryGet()) { if (InValue == Constant->Value) { if (Range.Start == TNumericLimits::Lowest()) { if (Range.End == TNumericLimits::Lowest()) { OutResults[0] = FFrameTime(); } else { OutResults[0] = Range.End; } } else { OutResults[0] = Range.Start; } NumSolutions = 1; } check(NumSolutions <= 1); } else if (const FLinearInterpolation* Linear = Data.TryGet()) { NumSolutions = Linear->Solve(InValue, OutResults); check(NumSolutions <= 1); } else if (const FQuadraticInterpolation* Quadratic = Data.TryGet()) { NumSolutions = Quadratic->Solve(InValue, OutResults); check(NumSolutions <= 2); } else if (const FCubicInterpolation* Cubic = Data.TryGet()) { NumSolutions = Cubic->Solve(InValue, OutResults); check(NumSolutions <= 3); } else if (const FQuarticInterpolation* Quartic = Data.TryGet()) { NumSolutions = Quartic->Solve(InValue, OutResults); check(NumSolutions <= 4); } else if (const FCubicBezierInterpolation* CubicBezier = Data.TryGet()) { NumSolutions = CubicBezier->Solve(InValue, OutResults); check(NumSolutions <= 2); } else if (const FWeightedCubicInterpolation* WeightedCubic = Data.TryGet()) { NumSolutions = WeightedCubic->Solve(InValue, OutResults); check(NumSolutions <= 2); } // Only accept solutions within our acceptable range int32 WritePos = 0; for (int32 Index = 0; Index < NumSolutions; ++Index) { if (OutResults[Index] >= Range.Start && OutResults[Index] <= Range.End) { if (Index != WritePos) { OutResults[WritePos] = OutResults[Index]; } ++WritePos; } } return WritePos; } FConstantValue FConstantValue::Derivative() const { return FConstantValue(Origin, 0.0); } FLinearInterpolation FConstantValue::Integral(double ConstantOffset) const { return FLinearInterpolation(Origin, Value, ConstantOffset); } double FLinearInterpolation::Evaluate(FFrameTime InTime) const { return Coefficient*(InTime - Origin).AsDecimal() + Constant; } int32 FLinearInterpolation::Solve(double Value, TInterpSolutions OutResults) const { if (Coefficient != 0.0) { OutResults[0] = FFrameTime::FromDecimal((Value - Constant) / Coefficient) + Origin; return 1; } else if (Value == Constant) { OutResults[0] = FFrameTime::FromDecimal(Constant); return 1; } return 0; } int32 FLinearInterpolation::SolveWithin(FFrameTime Start, FFrameTime End, double Value, TInterpSolutions OutResults) const { if (Value == Constant) { OutResults[0] = Start; return 1; } if (Solve(Value, OutResults) == 1 && OutResults[0] >= Start && OutResults[0] < End) { return 1; } return 0; } FConstantValue FLinearInterpolation::Derivative() const { return FConstantValue(Origin, Coefficient); } FQuadraticInterpolation FLinearInterpolation::Integral(double ConstantOffset) const { return FQuadraticInterpolation(Origin, 0.5*Coefficient, Constant, ConstantOffset); } double FQuadraticInterpolation::Evaluate(FFrameTime InTime) const { const double X = (InTime - Origin).AsDecimal(); return A*X*X + B*X + Constant; } int32 FQuadraticInterpolation::Solve(double Value, TInterpSolutions OutResults) const { const double C = Constant - Value; // Solve using the quadratic formula OutResults[0] = FFrameTime::FromDecimal( (-B + FMath::Sqrt(B*B - 4.0*A*C)) / (2.0*A) ) + Origin; OutResults[1] = FFrameTime::FromDecimal( (-B - FMath::Sqrt(B*B - 4.0*A*C)) / (2.0*A) ) + Origin; return 2; } int32 FQuadraticInterpolation::SolveWithin(FFrameTime Start, FFrameTime End, double Value, TInterpSolutions OutResults) const { Solve(Value, OutResults); if (OutResults[0] < Start || OutResults[0] >= End) { if (OutResults[1] < Start || OutResults[1] >= End) { return 0; } OutResults[0] = OutResults[1]; return 1; } if (OutResults[1] < Start || OutResults[1] >= End) { return 1; } return 2; } FLinearInterpolation FQuadraticInterpolation::Derivative() const { return FLinearInterpolation(Origin, 2.0*A, B); } FCubicInterpolation FQuadraticInterpolation::Integral(double ConstantOffset) const { return FCubicInterpolation(Origin, A/3.0, B/2.0, Constant, ConstantOffset); } double FCubicInterpolation::Evaluate(FFrameTime InTime) const { if (FMath::IsNearlyEqual(DX, 0.0)) { return A; } const double X = (InTime - Origin).AsDecimal() / DX; return A*X*X*X + B*X*X + C*X + Constant; } int32 FCubicInterpolation::Solve(double Value, TInterpSolutions OutResults) const { double Coefficients[4] = { Constant-Value, C, B, A, }; double Solutions[3]; const int32 NumRealSolutions = UE::Curves::SolveCubic(Coefficients, Solutions); for (int32 Index = 0; Index < NumRealSolutions; ++Index) { OutResults[Index] = DX*FFrameTime::FromDecimal(Solutions[Index])+Origin; } return NumRealSolutions; } int32 FCubicInterpolation::SolveWithin(FFrameTime Start, FFrameTime End, double Value, TInterpSolutions OutResults) const { int32 NumSolutions = Solve(Value, OutResults); // Only accept solutions within our acceptable range int32 WritePos = 0; for (int32 Index = 0; Index < NumSolutions; ++Index) { if (OutResults[Index] >= Start && OutResults[Index] <= End) { if (Index != WritePos) { OutResults[WritePos] = OutResults[Index]; } ++WritePos; } } return WritePos; } FQuadraticInterpolation FCubicInterpolation::Derivative() const { return FQuadraticInterpolation(Origin, 3.0*A/(DX*DX*DX), 2.0*B/(DX*DX), C/DX); } FQuarticInterpolation FCubicInterpolation::Integral(double ConstantOffset) const { return FQuarticInterpolation(Origin, A*DX/4.0, B*DX/3.0, C*DX/2.0, DX*Constant, ConstantOffset, DX); } double FQuarticInterpolation::Evaluate(FFrameTime InTime) const { if (FMath::IsNearlyEqual(DX, 0.0)) { return A; } const double X = (InTime - Origin).AsDecimal() / DX; return A*X*X*X*X + B*X*X*X + C*X*X + D*X + Constant; } FCubicInterpolation FQuarticInterpolation::Derivative() const { return FCubicInterpolation(Origin, 4.0*A, 3.0*B, 2.0*C, D, DX); } int32 FQuarticInterpolation::Solve(double Value, TInterpSolutions OutResults) const { // Solving ax^4 + bx^3 + cx^2 + dx + e = 0 is very difficult, but we can solve it using some tricks. // First off, convert our coefficients to make this a monic quartic of the form X^4 + bx^3 + cx^2 + dx + e: const double A_ = 1.0; const double B_ = B/A; const double C_ = C/A; const double D_ = D/A; const double E_ = (Constant - Value)/A; // Now convert the monic quartic to a depressed quartic of the form y^4 + py^2 + qy + r by substituting x = y - b/4 (since a=1.0) const double P = C_ - (3.0*B_*B_) / 8.0; const double Q = D_ - (B_*C_)/2.0 + (B_*B_*B_)/8.0; const double R = E_ - (B_*D_)/4.0 + (B_*B_*C_)/16.0 - (3.0*B_*B_*B_*B_)/256.0; // Step 2: Factor the depressed quartic into two quadratics: // (y^2 + sy + t)(y^2 + uy + v) // // which results in // y^4 + (s+u)y^3 + (t+v+su)y^2 + (sv+tu)y + tv // // Since y^3 is 0 in our depressed quartic, we can deduce that // s+u = 0 // p = t + v + su // q = sv + tu // r = tv // // From this we can deduce that s = -u: // p + u^2 = t + v // q = u(t-v) // r = tv // // Eliminating t and v by substitution and substituting U = u^2: // U^3 + 2pU^2 + (p^2 - 4r)U - q^2 = 0 // Solve the cubic for U using Cardano's algorithm: // SolveCubic expects coefficients in increasing exponent order // ie d + cx + bx^2 + ax^3 double Coefficients[4] = { -(Q*Q), // d (P*P - 4.0*R), // c (2.0*P), // b 1.0, // a }; double Solutions[3]; const int32 NumSolutions = UE::Curves::SolveCubic(Coefficients, Solutions); int32 SolutionToUse = INDEX_NONE; // Any real solution will do, but fallback to a complex solution if there's only one for (int32 Index = 0; Index < NumSolutions; ++Index) { SolutionToUse = Index; // Use first real solution if possible if (Solutions[Index] > 0.0) { break; } } int32 SolutionIndex = 0; if (SolutionToUse != INDEX_NONE) { const double U = FMath::Sqrt(FMath::Abs(Solutions[0])); const double S = -U; const double T = (U*U*U + P*U + Q) / (2.0*U); const double V = T - (Q / U); const double USquaredMinusFourV = U*U - 4.0*V; const double SSquaredMinusFourT = S*S - 4.0*T; const double BOverFourA = B_ / (4.0*A_); if (USquaredMinusFourV >= 0.0) { OutResults[SolutionIndex++] = FFrameTime::FromDecimal(( (-U + FMath::Sqrt(USquaredMinusFourV)) / 2.0 ) - BOverFourA) + Origin; OutResults[SolutionIndex++] = FFrameTime::FromDecimal(( (-U - FMath::Sqrt(USquaredMinusFourV)) / 2.0 ) - BOverFourA) + Origin; } if (SSquaredMinusFourT >= 0.0) { OutResults[SolutionIndex++] = FFrameTime::FromDecimal(( (-S + FMath::Sqrt(SSquaredMinusFourT)) / 2.0 ) - BOverFourA) + Origin; OutResults[SolutionIndex++] = FFrameTime::FromDecimal(( (-S - FMath::Sqrt(SSquaredMinusFourT)) / 2.0 ) - BOverFourA) + Origin; } } return SolutionIndex; } int32 FQuarticInterpolation::SolveWithin(FFrameTime Start, FFrameTime End, double Value, TInterpSolutions OutResults) const { int32 NumSolutions = Solve(Value, OutResults); // Only accept solutions within our acceptable range int32 WritePos = 0; for (int32 Index = 0; Index < NumSolutions; ++Index) { if (OutResults[Index] >= Start && OutResults[Index] <= End) { if (Index != WritePos) { OutResults[WritePos] = OutResults[Index]; } ++WritePos; } } return WritePos; } FCubicBezierInterpolation::FCubicBezierInterpolation(FFrameNumber InOrigin, double InDX, double InStartValue, double InEndValue, double InStartTangent, double InEndTangent) : DX(InDX) , Origin(InOrigin) { constexpr double OneThird = 1.0 / 3.0; P0 = InStartValue; P1 = P0 + (InStartTangent * DX * OneThird); P3 = InEndValue; P2 = P3 - (InEndTangent * DX * OneThird); } FCubicInterpolation FCubicBezierInterpolation::AsCubic() const { // Beziers are interpolated using a normalized input so we have to factor out that normalization to // each term of the resulting polynomial. const double OneOverDxCubed = 1.0 / (DX * DX * DX); const double OneOverDxSquared = 1.0 / (DX * DX); const double OneOverDx = 1.0 / (DX); const double A = OneOverDxCubed*(-P0 + 3.0*P1 - 3.0*P2 + P3); const double B = OneOverDxSquared*(3.0*P0 - 6.0*P1 + 3.0*P2); const double C = OneOverDx*3.0*(-P0 + P1); const double D = P0; return FCubicInterpolation( Origin, A, B, C, D, 1.0);// DX); } double FCubicBezierInterpolation::Evaluate(FFrameTime InTime) const { if (FMath::IsNearlyEqual(DX, 0.0)) { return P3; } const float Interp = static_cast((InTime - Origin).AsDecimal() / DX); return UE::Curves::BezierInterp(P0, P1, P2, P3, Interp); } FQuadraticInterpolation FCubicBezierInterpolation::Derivative() const { return AsCubic().Derivative(); } FQuarticInterpolation FCubicBezierInterpolation::Integral(double ConstantOffset) const { return AsCubic().Integral(ConstantOffset); } int32 FCubicBezierInterpolation::Solve(double InValue, TInterpSolutions OutResults) const { // Offset the curve by InValue to find roots // SolveCubic expects coefficients in increasing exponent order // ie d + cx + bx^2 + ax^3 double Coefficients[4] = { (P0-InValue), // d (-3.0 * P0 + 3.0 * P1), // c (3.0 * P0 - 6.0 * P1 + 3.0 * P2), // b (-P0 + 3.0 * P1 - 3.0 * P2 + P3), // a }; double Solutions[3]; const int32 NumRealSolutions = UE::Curves::SolveCubic(Coefficients, Solutions); int32 NumSolutionsWithinRange = 0; for (int32 Index = 0; Index < NumRealSolutions; ++Index) { double Solution = Solutions[Index]; // Only accept solutions within the 0-1 range, allowing for some rounding discrepancies if (Solution >= -UE_SMALL_NUMBER && Solution <= 1.0 + UE_SMALL_NUMBER) { Solution = FMath::Clamp(Solution, 0.0, 1.0); OutResults[NumSolutionsWithinRange++] = FFrameTime::FromDecimal(Solution*DX) + Origin; } } return NumSolutionsWithinRange; } FWeightedCubicInterpolation::FWeightedCubicInterpolation( FFrameRate TickResolution, FFrameNumber InOrigin, FFrameNumber StartTime, double StartValue, double StartTangent, double StartTangentWeight, bool bStartIsWeighted, FFrameNumber EndTime, double EndValue, double EndTangent, double EndTangentWeight, bool bEndIsWeighted) { constexpr double OneThird = 1.0 / 3.0; const float TimeInterval = TickResolution.AsInterval(); const float ToSeconds = 1.0f / TimeInterval; const double Time1 = TickResolution.AsSeconds(StartTime); const double Time2 = TickResolution.AsSeconds(EndTime); const double DXInSeconds = Time2 - Time1; Origin = InOrigin; DX = (EndTime - StartTime).Value; StartKeyValue = StartValue; EndKeyValue = EndValue; double CosAngle, SinAngle, Angle; // --------------------------------------------------------------------------------- // Initialize the start key parameters Angle = FMath::Atan(StartTangent * ToSeconds); FMath::SinCos(&SinAngle, &CosAngle, Angle); if (bStartIsWeighted) { StartWeight = StartTangentWeight; } else { const double LeaveTangentNormalized = StartTangent / TimeInterval; const double DY = LeaveTangentNormalized * DXInSeconds; StartWeight = FMath::Sqrt(DXInSeconds*DXInSeconds + DY*DY) * OneThird; } const double StartKeyTanX = CosAngle * StartWeight + Time1; StartKeyTanY = SinAngle * StartWeight + StartValue; NormalizedStartTanDX = (StartKeyTanX - Time1) / DXInSeconds; // --------------------------------------------------------------------------------- // Initialize the end key parameters Angle = FMath::Atan(EndTangent * ToSeconds); FMath::SinCos(&SinAngle, &CosAngle, Angle); if (bEndIsWeighted) { EndWeight = EndTangentWeight; } else { const double ArriveTangentNormalized = EndTangent / TimeInterval; const double DY = ArriveTangentNormalized * DXInSeconds; EndWeight = FMath::Sqrt(DXInSeconds*DXInSeconds + DY*DY) * OneThird; } const double EndKeyTanX = -CosAngle * EndWeight + Time2; EndKeyTanY = -SinAngle * EndWeight + EndValue; NormalizedEndTanDX = (EndKeyTanX - Time1) / DXInSeconds; } double FWeightedCubicInterpolation::Evaluate(FFrameTime InTime) const { const double Interp = (InTime - Origin).AsDecimal() / DX; double Coeff[4]; double Results[3]; //Convert Bezier to Power basis, also float to double for precision for root finding. UE::Curves::BezierToPower( 0.0, NormalizedStartTanDX, NormalizedEndTanDX, 1.0, &(Coeff[3]), &(Coeff[2]), &(Coeff[1]), &(Coeff[0]) ); Coeff[0] = Coeff[0] - Interp; const int32 NumResults = UE::Curves::SolveCubic(Coeff, Results); double NewInterp = Interp; if (NumResults == 1) { NewInterp = Results[0]; } else { NewInterp = TNumericLimits::Lowest(); //just need to be out of range for (double Result : Results) { if ((Result > 0.0 || FMath::IsNearlyEqual(Result, 0.0)) && (Result < 1.0 || FMath::IsNearlyEqual(Result, 1.0))) { if (NewInterp < 0.0 || Result > NewInterp) { NewInterp = Result; } } } if (NewInterp == TNumericLimits::Lowest()) { NewInterp = 0.0; } } //now use NewInterp and adjusted tangents plugged into the Y (Value) part of the graph. const double P0 = StartKeyValue; const double P1 = StartKeyTanY; const double P3 = EndKeyValue; const double P2 = EndKeyTanY; return UE::Curves::BezierInterp(P0, P1, P2, P3, static_cast(NewInterp)); } int32 FWeightedCubicInterpolation::Solve(double InValue, TInterpSolutions OutResults) const { // Solve the control points first const double P0 = StartKeyValue; const double P1 = StartKeyTanY; const double P3 = EndKeyValue; const double P2 = EndKeyTanY; // Offset the curve by InValue to find roots // SolveCubic expects coefficients in increasing exponent order // ie d + cx + bx^2 + ax^3 double Coefficients[4] = { (P0-InValue), // d (-3.0 * P0 + 3.0 * P1), // c (3.0 * P0 - 6.0 * P1 + 3.0 * P2), // b (-P0 + 3.0 * P1 - 3.0 * P2 + P3), // a }; double Solutions[3]; const int32 NumRealSolutions = UE::Curves::SolveCubic(Coefficients, Solutions); FFrameTime InitialSolutions[3]; int32 NumSolutionsWithinRange = 0; for (int32 Index = 0; Index < NumRealSolutions; ++Index) { double Solution = Solutions[Index]; // Only accept solutions within the 0-1 range, allowing for some rounding discrepancies if (Solution >= -UE_SMALL_NUMBER && Solution <= 1.0 + UE_SMALL_NUMBER) { Solution = FMath::Clamp(Solution, 0.0, 1.0); // Now solve this interp for the power basis Solution = UE::Curves::BezierInterp(0.0, NormalizedStartTanDX, NormalizedEndTanDX, 1.0, static_cast(Solution)); OutResults[NumSolutionsWithinRange++] = FFrameTime::FromDecimal(Solution*DX) + Origin; } } return NumSolutionsWithinRange; } } // namespace UE::MovieScene