// Copyright Epic Games, Inc. All Rights Reserved. #include "Channels/MovieSceneDoubleChannel.h" #include "Channels/MovieSceneFloatChannel.h" #include "Curves/RealCurve.h" #include "Misc/FrameRate.h" #include "Templates/UnrealTemplate.h" #include namespace UE::MovieScene { /** Currently, conceptually double and float channels support bezier curve painting */ template concept CSupportsBezierCurvePainting = std::is_same_v || std::is_same_v; // Fwd namespace Private { template requires CSupportsBezierCurvePainting struct FMovieSceneDrawInfiniteBezierCurve; template requires CSupportsBezierCurvePainting static void PopulateCurvePoints(const ChannelType& InChannel, const FFrameRate& InTickResolution, const double InTimeThreshold, const CurveValueType InValueThreshold, const double InStartTimeSeconds, const double InEndTimeSeconds, TArray>& OutPoints); template requires CSupportsBezierCurvePainting static void RefineCurvePoints(const ChannelType& InChannel, FFrameRate InTickResolution, double TimeThreshold, CurveValueType ValueThreshold, TArray>& OutPoints); } /** * Draws bezier curves, correctly handling and fast painting complex pre-/post-infinity extrapolation. * * @param InChannel The Movie Scene Channel for which the curve should be painted. * @param InTickResolution The Movie Scene's current tick resolution * @param InTimeThreshold The time that is visible as pixel delta on screen * @param InValueThreshold The value that is visible as pixel delta on screen * @param InStartTimeSeconds The desired start time in seconds * @param InEndTimeSeconds The desired end time in seconds * @param OutInterpolatingPoints The resulting interpolating points of the curve */ template requires CSupportsBezierCurvePainting static void DrawBezierCurve( const ChannelType& InChannel, const FFrameRate& InTickResolution, const double InTimeThreshold, const double InValueThreshold, const double InStartTimeSeconds, const double InEndTimeSeconds, TArray>& OutInterpolatingPoints) { /** The type of channel value structs */ using ChannelValueType = typename ChannelType::ChannelValueType; /** The type of curve values (float or double) */ using CurveValueType = typename ChannelType::CurveValueType; const ERichCurveExtrapolation PreInfinityExtrapolation = InChannel.PreInfinityExtrap; const ERichCurveExtrapolation PostInfinityExtrapolation = InChannel.PostInfinityExtrap; const TArrayView Times = InChannel.GetTimes(); const TArrayView Values = InChannel.GetValues(); const bool bComplexPreInfinityExtrapolation = PreInfinityExtrapolation == ERichCurveExtrapolation::RCCE_Cycle || PreInfinityExtrapolation == ERichCurveExtrapolation::RCCE_CycleWithOffset || PreInfinityExtrapolation == ERichCurveExtrapolation::RCCE_Oscillate; const bool bComplexPostInfinityExtrapolation = PostInfinityExtrapolation == ERichCurveExtrapolation::RCCE_Cycle || PostInfinityExtrapolation == ERichCurveExtrapolation::RCCE_CycleWithOffset || PostInfinityExtrapolation == ERichCurveExtrapolation::RCCE_Oscillate; const bool bDrawComplexPrePostInfinityExtrapolation = (bComplexPreInfinityExtrapolation || bComplexPostInfinityExtrapolation) && (Times.Num() > 1 && Values.Num() > 1); if (bDrawComplexPrePostInfinityExtrapolation) { UE::MovieScene::Private::FMovieSceneDrawInfiniteBezierCurve( InChannel, InTickResolution, InTimeThreshold, InValueThreshold, InStartTimeSeconds, InEndTimeSeconds, OutInterpolatingPoints); } else { Private::PopulateCurvePoints( InChannel, InTickResolution, InTimeThreshold, InValueThreshold, InStartTimeSeconds, InEndTimeSeconds, OutInterpolatingPoints); } } namespace Private { /* * Populate the specified array with times and values that represent the smooth interpolation of * the given channel across the specified range. * * Mind this algo only works correctly within in curve range, from the first to the last key. * Mind when pre-/post-infinity is complex, this may not draw the first and the last point correctly when time is near bounds. * * @param InChannel The Movie Scene Channel for which the curve should be painted. * @param InTickResolution The Movie Scene's current tick resolution * @param InTimeThreshold The time that is visible as pixel delta on screen * @param InValueThreshold The value that is visible as pixel delta on screen * @param InStartTimeSeconds The desired start time in seconds * @param InEndTimeSeconds The desired end time in seconds * @param OutInterpolatingPoints The resulting interpolating points of the curve */ template requires CSupportsBezierCurvePainting static void PopulateCurvePoints( const ChannelType& InChannel, const FFrameRate& InTickResolution, const double InTimeThreshold, const CurveValueType InValueThreshold, const double InStartTimeSeconds, const double InEndTimeSeconds, TArray>& OutPoints) { const FFrameNumber StartFrame = (InStartTimeSeconds * InTickResolution).FloorToFrame(); const FFrameNumber EndFrame = (InEndTimeSeconds * InTickResolution).CeilToFrame(); const TArrayView Times = InChannel.GetTimes(); const TArrayView Values = InChannel.GetValues(); const int32 StartingIndex = Algo::UpperBound(Times, StartFrame); const int32 EndingIndex = Algo::LowerBound(Times, EndFrame); // Add the lower bound of the visible space CurveValueType EvaluatedValue; if (InChannel.Evaluate(StartFrame, EvaluatedValue)) { OutPoints.Emplace(StartFrame / InTickResolution, static_cast(EvaluatedValue)); } // Add all keys in-between for (int32 KeyIndex = StartingIndex; KeyIndex < EndingIndex; ++KeyIndex) { OutPoints.Emplace(Times[KeyIndex] / InTickResolution, static_cast(Values[KeyIndex].Value)); } // Add the upper bound of the visible space if (InChannel.Evaluate(EndFrame, EvaluatedValue)) { OutPoints.Emplace(EndFrame / InTickResolution, static_cast(EvaluatedValue)); } int32 OldSize = OutPoints.Num(); do { OldSize = OutPoints.Num(); Private::RefineCurvePoints(InChannel, InTickResolution, InTimeThreshold, InValueThreshold, OutPoints); } while (OldSize != OutPoints.Num()); } /** * Adds median points between each of the supplied points if their evaluated value is * significantly different than the linear interpolation of those points. * * Mind this algo only works in curve range, from the first to the last key. * * @param TickResolution The tick resolution with which to interpret this channel's times * @param TimeThreshold A small time threshold in seconds below which we should stop adding new points * @param ValueThreshold A small value threshold below which we should stop adding new points where the linear interpolation would suffice * @param InOutPoints An array to populate with the evaluated points */ template requires CSupportsBezierCurvePainting static void RefineCurvePoints( const ChannelType& InChannel, FFrameRate InTickResolution, double InTimeThreshold, CurveValueType InValueThreshold, TArray>& OutPoints) { const float InterpTimes[] = { 0.25f, 0.5f, 0.6f }; for (int32 Index = 0; Index < OutPoints.Num() - 1; ++Index) { TTuple Lower = OutPoints[Index]; TTuple Upper = OutPoints[Index + 1]; if ((Upper.Get<0>() - Lower.Get<0>()) >= InTimeThreshold) { bool bSegmentIsLinear = true; TTuple Evaluated[UE_ARRAY_COUNT(InterpTimes)] = { TTuple(0, 0) }; for (int32 InterpIndex = 0; InterpIndex < UE_ARRAY_COUNT(InterpTimes); ++InterpIndex) { double& EvalTime = Evaluated[InterpIndex].Get<0>(); EvalTime = FMath::Lerp(Lower.Get<0>(), Upper.Get<0>(), InterpTimes[InterpIndex]); CurveValueType Value = 0.0; InChannel.Evaluate(EvalTime * InTickResolution, Value); const CurveValueType LinearValue = FMath::Lerp(Lower.Get<1>(), Upper.Get<1>(), InterpTimes[InterpIndex]); if (bSegmentIsLinear) { bSegmentIsLinear = FMath::IsNearlyEqual(Value, LinearValue, InValueThreshold); } Evaluated[InterpIndex].Get<1>() = Value; } if (!bSegmentIsLinear) { // Add the point OutPoints.Insert(Evaluated, UE_ARRAY_COUNT(Evaluated), Index + 1); --Index; } } } } /** * Draws bezier curves, correctly handling and fast painting complex pre-/post-infinity extrapolation. * Should only be used when curve data result in complex pre-/post-infinity extrapolation (ensured). */ template requires CSupportsBezierCurvePainting struct FMovieSceneDrawInfiniteBezierCurve : FNoncopyable { public: /** * Draws complex pre-/post-infinity extrapolation. * Should only be used when pre- or post-infinity extrapolation is complex (cycle, cycle with offset or oscillate). * * @param InChannel The Movie Scene Channel for which the curve should be painted. * @param InTickResolution The Movie Scene's current tick resolution * @param InStartTimeSeconds The desired start time in seconds * @param InEndTimeSeconds The desired end time in seconds * @param OutInterpolatingPoints The resulting interpolating points of the curve */ FMovieSceneDrawInfiniteBezierCurve( const ChannelType& InChannel, const FFrameRate& InTickResolution, const double& InTimeThreshold, const double& InValueThreshold, const double& InStartTimeSeconds, const double& InEndTimeSeconds, TArray>& OutInterpolatingPoints) : Channel(InChannel) , TickResolution(InTickResolution) , TimeThreshold(InTimeThreshold) , ValueThreshold(InValueThreshold) , StartTimeSeconds(InStartTimeSeconds) , EndTimeSeconds(InEndTimeSeconds) { PreInfinityExtrapolation = InChannel.PreInfinityExtrap; PostInfinityExtrapolation = InChannel.PostInfinityExtrap; const TArrayView Times = InChannel.GetTimes(); const TArrayView Values = InChannel.GetValues(); bComplexPreInfinityExtrapolation = PreInfinityExtrapolation == ERichCurveExtrapolation::RCCE_Cycle || PreInfinityExtrapolation == ERichCurveExtrapolation::RCCE_CycleWithOffset || PreInfinityExtrapolation == ERichCurveExtrapolation::RCCE_Oscillate; bComplexPostInfinityExtrapolation = PostInfinityExtrapolation == ERichCurveExtrapolation::RCCE_Cycle || PostInfinityExtrapolation == ERichCurveExtrapolation::RCCE_CycleWithOffset || PostInfinityExtrapolation == ERichCurveExtrapolation::RCCE_Oscillate; const bool bDrawComplexPrePostInfinityExtrapolation = (bComplexPreInfinityExtrapolation || bComplexPostInfinityExtrapolation) && (Times.Num() > 1 && Values.Num() > 1); if (!ensure(bDrawComplexPrePostInfinityExtrapolation)) { PopulateCurvePoints( InChannel, InTickResolution, InTimeThreshold, InValueThreshold, InStartTimeSeconds, InEndTimeSeconds, OutInterpolatingPoints); return; } CurveStartSeconds = Times[0] / TickResolution; CurveEndSeconds = Times.Last() / TickResolution; CurveDurationSeconds = CurveEndSeconds - CurveStartSeconds; VisibleCurveStartSeconds = FMath::Max(CurveStartSeconds, StartTimeSeconds); VisibleCurveEndSeconds = FMath::Min(CurveEndSeconds, EndTimeSeconds); PreInfinityDuration = CurveStartSeconds - StartTimeSeconds; PostInfinityDuration = EndTimeSeconds - CurveEndSeconds; bPreInfinityVisible = PreInfinityDuration > 0.0; bPostInfinityVisible = PostInfinityDuration > 0.0; bPreInfinityOscillates = PreInfinityExtrapolation == ERichCurveExtrapolation::RCCE_Oscillate; bPostInfinityOscillates = PostInfinityExtrapolation == ERichCurveExtrapolation::RCCE_Oscillate; // Find the interpolation range that is required to draw the curve, but also pre- and post-infinity extrapolation double InterpolationStartSeconds; double InterpolationEndSeconds; GetInterpolationRangeSeconds(InterpolationStartSeconds, InterpolationEndSeconds); TArray> CurveInterpolatingPoints; PopulateCurvePoints( InChannel, TickResolution, TimeThreshold, ValueThreshold, InterpolationStartSeconds, InterpolationEndSeconds, CurveInterpolatingPoints); // Handle the case where PopulateCurvePoints overdraws to frame time. This would be problematic as it can hold // evaluated values from pre-/post-infinity, e.g. a cycled value, instead of the last value of the curve. if (!CurveInterpolatingPoints.IsEmpty()) { if (CurveInterpolatingPoints[0].Get<0>() < CurveStartSeconds) { CurveInterpolatingPoints[0] = MakeTuple(CurveStartSeconds, static_cast(Values[0].Value)); } if (CurveInterpolatingPoints.Last().Get<0>() > CurveEndSeconds) { CurveInterpolatingPoints.Last() = MakeTuple(CurveEndSeconds, static_cast(Values.Last().Value)); } } // Draw the curve if (bPreInfinityVisible) { if (bComplexPreInfinityExtrapolation) { DrawPreInfinityExtrapolationComplex(CurveInterpolatingPoints, OutInterpolatingPoints); } else { DrawPreInfinityExtrapolationLine(CurveInterpolatingPoints, OutInterpolatingPoints); } } OutInterpolatingPoints.Append(CurveInterpolatingPoints); if (bPostInfinityVisible) { if (bComplexPostInfinityExtrapolation) { DrawPostInfinityExtrapolationComplex(CurveInterpolatingPoints, OutInterpolatingPoints); } else { DrawPostInfinityExtrapolationLine(CurveInterpolatingPoints, OutInterpolatingPoints); } } } private: /** The channel which is painted */ const ChannelType& Channel; /** The current tick resolution */ const FFrameRate TickResolution; /** The time threshold that is visible on screen as pixel delta */ const double TimeThreshold = 0.0; /** The value threshold that is visible on screen as pixel delta */ const double ValueThreshold = 0.0; /** The start time of the painted range */ const double StartTimeSeconds = 0.0; /** The end time of the painted range */ const double EndTimeSeconds = 0.0; /** The pre-infinity extrapolation type */ ERichCurveExtrapolation PreInfinityExtrapolation = ERichCurveExtrapolation::RCCE_None; /** The post-infinity extrapolation type */ ERichCurveExtrapolation PostInfinityExtrapolation = ERichCurveExtrapolation::RCCE_None; /** If true the pre-infinity extrapolation is non-linear (cycling, cycling with offset or oscillating) */ bool bComplexPreInfinityExtrapolation = false; /** If true the post-infinity extrapolation is non-linear (cycling, cycling with offset or oscillating) */ bool bComplexPostInfinityExtrapolation = false; /** The start of the curve (the time of the first key) in seconds */ double CurveStartSeconds = 0.0; /** The end of the curve (the time of the last key) in seconds */ double CurveEndSeconds = 0.0; /** The duration of the curve, in seconds */ double CurveDurationSeconds = 0.0; /** The lower visible bound of the curve, in seconds */ double VisibleCurveStartSeconds = 0.0; /** The upper visible bound of the curve, in seconds */ double VisibleCurveEndSeconds = 0.0; /** Duration of the pre-infinity extrapolation (the time before the first key) */ double PreInfinityDuration = 0.0; /** Duration of the post-infinity extrapolation (the time after the last key) */ double PostInfinityDuration = 0.0; /** True if pre-infinity is visible */ bool bPreInfinityVisible = false; /** True if post-infinity is visible */ bool bPostInfinityVisible = false; /** True if pre-infinity oscillates */ bool bPreInfinityOscillates = false; /** True if post-infinity oscillates */ bool bPostInfinityOscillates = false; /** * Returns the interpolation range to draw the curve as well as pre- and post-infinity extrapolation. * Mind a larger part of the curve might be visible in pre- or post-infinity extrapolation, while only a small part of the curve is visilbe. */ void GetInterpolationRangeSeconds(double& OutInterpolationStartSeconds, double& OutInterpolationEndSeconds) { // Translate pre- and post-infinity ranges to curve range const double TranslatedPreInfinityStartSeconds = [this]() { if (bPreInfinityVisible && bComplexPreInfinityExtrapolation) { return bPreInfinityOscillates ? CurveStartSeconds : FMath::Max(CurveStartSeconds, CurveEndSeconds - PreInfinityDuration); } else { return VisibleCurveStartSeconds; } }(); const double TranslatedPreInfinityEndSeconds = [this]() { if (bPreInfinityVisible && bComplexPreInfinityExtrapolation) { return bPreInfinityOscillates ? FMath::Min(CurveEndSeconds, CurveStartSeconds + PreInfinityDuration) : CurveEndSeconds; } else { return VisibleCurveEndSeconds; } }(); const double TranslatedPostInfinityStartSeconds = [this]() { if (bPostInfinityVisible && bComplexPostInfinityExtrapolation) { return bPostInfinityOscillates ? FMath::Max(CurveStartSeconds, CurveEndSeconds - PostInfinityDuration) : CurveStartSeconds; } else { return VisibleCurveStartSeconds; } }(); const double TranslatedPostInfinityEndSeconds = [this]() { if (bPostInfinityVisible && bComplexPostInfinityExtrapolation) { return bPostInfinityOscillates ? CurveEndSeconds : FMath::Min(CurveEndSeconds, CurveStartSeconds + PostInfinityDuration); } else { return VisibleCurveEndSeconds; } }(); OutInterpolationStartSeconds = FMath::Min3(VisibleCurveStartSeconds, TranslatedPreInfinityStartSeconds, TranslatedPostInfinityStartSeconds); OutInterpolationEndSeconds = FMath::Max3(VisibleCurveEndSeconds, TranslatedPreInfinityEndSeconds, TranslatedPostInfinityEndSeconds); } /** Draws pre-infinity extrapolation as a straight line */ void DrawPreInfinityExtrapolationLine(const TArray>& InCurveInterpolatingPoints, TArray>& OutInterpolatingPoints) { const FVector2D FirstKey(InCurveInterpolatingPoints[0].Get<0>(), InCurveInterpolatingPoints[0].Get<1>()); if (PreInfinityExtrapolation == ERichCurveExtrapolation::RCCE_None || PreInfinityExtrapolation == ERichCurveExtrapolation::RCCE_Constant) { OutInterpolatingPoints.Emplace(StartTimeSeconds, FirstKey.Y); } else if (PreInfinityExtrapolation == ERichCurveExtrapolation::RCCE_Linear) { const FFrameNumber StartFrame = (StartTimeSeconds * TickResolution).FloorToFrame(); CurveValueType EvaluatedValue; if (Channel.Evaluate(StartFrame, EvaluatedValue)) { OutInterpolatingPoints.Emplace(StartTimeSeconds, EvaluatedValue); } else { OutInterpolatingPoints.Emplace(StartTimeSeconds, FirstKey.Y); } } else { ensureMsgf(0, TEXT("Unhandled enum value")); } } /** Draws complex pre-infinity extrapolation */ void DrawPreInfinityExtrapolationComplex(const TArray>& InCurveInterpolatingPoints, TArray>& OutInterpolatingPoints) { const FVector2D FirstKey(InCurveInterpolatingPoints[0].Get<0>(), InCurveInterpolatingPoints[0].Get<1>()); const FVector2D LastKey(InCurveInterpolatingPoints.Last().Get<0>(), InCurveInterpolatingPoints.Last().Get<1>()); const double InfinityOffset = FirstKey.X - StartTimeSeconds; if (FMath::IsNearlyZero(CurveDurationSeconds)) { // Draw a single line in the odd case where there are many keys with a nearly zero duration. OutInterpolatingPoints.Emplace( StartTimeSeconds, FirstKey.Y); OutInterpolatingPoints.Emplace( FirstKey.X, FirstKey.Y); } else { // Cycle or oscillate const double StartTime = FirstKey.X; const double ValueOffset = PreInfinityExtrapolation == ERichCurveExtrapolation::RCCE_CycleWithOffset ? LastKey.Y - FirstKey.Y : 0.0; const int32 NumIterations = static_cast(InfinityOffset / CurveDurationSeconds) + 1; for (int32 Iteration = NumIterations; Iteration > 0; Iteration--) { const bool bReverse = PreInfinityExtrapolation == ERichCurveExtrapolation::RCCE_Oscillate && Iteration % 2 != 0; if (bReverse) { for (int32 PointIndex = InCurveInterpolatingPoints.Num() - 1; PointIndex >= 0; PointIndex--) { // Mirror around start time const double Time = 2 * StartTime + CurveDurationSeconds - InCurveInterpolatingPoints[PointIndex].Get<0>() - CurveDurationSeconds * Iteration; const double Value = InCurveInterpolatingPoints[PointIndex].Get<1>() - ValueOffset * Iteration; OutInterpolatingPoints.Emplace(Time, Value); } } else { for (const TTuple& Point : InCurveInterpolatingPoints) { const double Time = Point.Get<0>() - CurveDurationSeconds * Iteration; const double Value = Point.Get<1>() - ValueOffset * Iteration; OutInterpolatingPoints.Emplace(Time, Value); } } } } } /** Draws post-infinity extrapolation as a straight line */ void DrawPostInfinityExtrapolationLine(const TArray>& InCurveInterpolatingPoints, TArray>& OutInterpolatingPoints) { const FVector2D LastKey(InCurveInterpolatingPoints.Last().Get<0>(), InCurveInterpolatingPoints.Last().Get<1>()); if (PostInfinityExtrapolation == ERichCurveExtrapolation::RCCE_None || PostInfinityExtrapolation == ERichCurveExtrapolation::RCCE_Constant) { OutInterpolatingPoints.Emplace(EndTimeSeconds, LastKey.Y); } else if (PostInfinityExtrapolation == ERichCurveExtrapolation::RCCE_Linear) { const FFrameNumber EndFrame = (EndTimeSeconds * TickResolution).CeilToFrame(); CurveValueType EvaluatedValue; if (Channel.Evaluate(EndFrame, EvaluatedValue)) { OutInterpolatingPoints.Emplace(EndTimeSeconds, EvaluatedValue); } else { OutInterpolatingPoints.Emplace(EndTimeSeconds, LastKey.Y); } } else { ensureMsgf(0, TEXT("Unhandled enum value")); } } /** Draws complex post-infinity extrapolation */ void DrawPostInfinityExtrapolationComplex(const TArray>& InCurveInterpolatingPoints, TArray>& OutInterpolatingPoints) { const FVector2D FirstKey(InCurveInterpolatingPoints[0].Get<0>(), InCurveInterpolatingPoints[0].Get<1>()); const FVector2D LastKey(InCurveInterpolatingPoints.Last().Get<0>(), InCurveInterpolatingPoints.Last().Get<1>()); const double InfinityOffset = EndTimeSeconds - LastKey.X; if (FMath::IsNearlyZero(CurveDurationSeconds)) { // Draw a single line in the odd case where there are many keys with a nearly zero duration. OutInterpolatingPoints.Emplace( LastKey.X, LastKey.Y); OutInterpolatingPoints.Emplace( EndTimeSeconds, LastKey.Y); } else { // Cycle or oscillate const double StartTime = FirstKey.X; const double ValueOffset = PostInfinityExtrapolation == ERichCurveExtrapolation::RCCE_CycleWithOffset ? LastKey.Y - FirstKey.Y : 0.0; const int32 NumIterations = static_cast(InfinityOffset / CurveDurationSeconds) + 1; for (int32 Iteration = 1; Iteration <= NumIterations; Iteration++) { const bool bReverse = PostInfinityExtrapolation == ERichCurveExtrapolation::RCCE_Oscillate && Iteration % 2 != 0; if (bReverse) { for (int32 PointIndex = InCurveInterpolatingPoints.Num() - 1; PointIndex >= 0; PointIndex--) { // Mirror around start time const double Time = 2 * StartTime + CurveDurationSeconds - InCurveInterpolatingPoints[PointIndex].Get<0>() + CurveDurationSeconds * Iteration; const double Value = InCurveInterpolatingPoints[PointIndex].Get<1>() - ValueOffset * Iteration; OutInterpolatingPoints.Emplace(Time, Value); } } else { for (const TTuple& Point : InCurveInterpolatingPoints) { const double Time = Point.Get<0>() + CurveDurationSeconds * Iteration; const double Value = Point.Get<1>() + ValueOffset * Iteration; OutInterpolatingPoints.Emplace(Time, Value); } } } } } }; } }