// Copyright Epic Games, Inc. All Rights Reserved. #include "Channels/MovieSceneCurveChannelImpl.h" #include "Channels/MovieSceneDoubleChannel.h" #include "Channels/MovieSceneFloatChannel.h" #include "Channels/MovieSceneInterpolation.h" #include "HAL/ConsoleManager.h" #include "MovieSceneFrameMigration.h" #include "Curves/CurveEvaluation.h" #include "UObject/FortniteMainBranchObjectVersion.h" #include "UObject/SequencerObjectVersion.h" int32 GSequencerLinearCubicInterpolation = 1; static FAutoConsoleVariableRef CVarSequencerLinearCubicInterpolation( TEXT("Sequencer.LinearCubicInterpolation"), GSequencerLinearCubicInterpolation, TEXT("If 1 Linear Keys Act As Cubic Interpolation with Linear Tangents, if 0 Linear Key Forces Linear Interpolation to Next Key."), ECVF_Default); int32 GCachedSequencerAutoTangentInterpolation = 2; static FAutoConsoleVariableRef CVarSequencerAutoTangentInterpolation( TEXT("Sequencer.AutoTangentNew"), GCachedSequencerAutoTangentInterpolation, TEXT("If 2 Sequencer will flatten tangents with no overshoot, if 1 Auto Tangent will use new algorithm to gradually flatten maximum/minimum keys, if 0 Auto Tangent will average all keys (pre 4.23 behavior)."), ECVF_Default); static TAutoConsoleVariable CVarSequencerSmartAutoBlendLocationPercentage( TEXT("Sequencer.SmartAutoBlendLocationPercentage"), 0.8, TEXT("Percentage near the next value that the tangent will blend to the adjacent tangent, if over 1.0 we won't blend. Default to 0.8"), ECVF_Default); namespace UE { namespace MovieScene { template static typename ChannelType::CurveValueType EvalForTwoKeys( const typename ChannelType::ChannelValueType& Key1, FFrameNumber Key1Time, const typename ChannelType::ChannelValueType& Key2, FFrameNumber Key2Time, FFrameNumber InTime, FFrameRate DisplayRate) { using CurveValueType = typename ChannelType::CurveValueType; double DecimalRate = DisplayRate.AsDecimal(); double Diff = (double)(Key2Time - Key1Time).Value; Diff /= DecimalRate; const int CheckBothLinear = GSequencerLinearCubicInterpolation; if (Diff > 0) { if (Key1.InterpMode != RCIM_Constant) { const double Alpha = ((double)(InTime - Key1Time).Value / DecimalRate) / Diff; const CurveValueType P0 = Key1.Value; const CurveValueType P3 = Key2.Value; if (Key1.InterpMode == RCIM_Linear && (!CheckBothLinear || Key2.InterpMode != RCIM_Cubic)) { return FMath::Lerp(P0, P3, Alpha); } else { double LeaveTangent = Key1.Tangent.LeaveTangent * DecimalRate; double ArriveTangent = Key2.Tangent.ArriveTangent * DecimalRate; const double OneThird = 1.0 / 3.0; const CurveValueType P1 = P0 + (LeaveTangent * Diff * OneThird); const CurveValueType P2 = P3 - (ArriveTangent * Diff * OneThird); return UE::Curves::BezierInterp(P0, P1, P2, P3, Alpha); } } else { return InTime < Key2Time ? Key1.Value : Key2.Value; } } else { return Key1.Value; } } FCycleParams CycleTime(FFrameNumber MinFrame, FFrameNumber MaxFrame, FFrameTime InTime) { int64 Duration = int64(MaxFrame.Value) - int64(MinFrame.Value); if (Duration > MAX_int32) { return FCycleParams(InTime, 0); } FCycleParams Params(InTime, static_cast(Duration)); if (Params.Duration == 0) { Params.Time = MaxFrame; Params.CycleCount = 0; } else if (InTime < MinFrame) { const int64 CycleCount = (int64(MaxFrame.Value) - int64(InTime.FrameNumber.Value)) / Params.Duration; Params.CycleCount = static_cast(-CycleCount); Params.Time = InTime; Params.Time.FrameNumber.Value = static_cast(int64(Params.Time.FrameNumber.Value) + int64(Params.Duration)*CycleCount); } else if (InTime > MaxFrame) { const int64 CycleCount = (int64(InTime.FrameNumber.Value) - int64(MinFrame.Value)) / Params.Duration; Params.CycleCount = static_cast(CycleCount); Params.Time = InTime; Params.Time.FrameNumber.Value = static_cast(int64(Params.Time.FrameNumber.Value) - int64(Params.Duration) * CycleCount); } return Params; } } } template void TMovieSceneCurveChannelImpl::Set(ChannelType* InChannel, TArray InTimes, TArray InValues) { check(InTimes.Num() == InValues.Num()); InChannel->Times = MoveTemp(InTimes); InChannel->Values = MoveTemp(InValues); InChannel->KeyHandles.Reset(); for (int32 Index = 0; Index < InChannel->Times.Num(); ++Index) { InChannel->KeyHandles.AllocateHandle(Index); } } template int32 TMovieSceneCurveChannelImpl::InsertKeyInternal(ChannelType* InChannel, FFrameNumber InTime) { const int32 InsertIndex = Algo::UpperBound(InChannel->Times, InTime); InChannel->Times.Insert(InTime, InsertIndex); InChannel->Values.Insert(ChannelValueType(), InsertIndex); InChannel->KeyHandles.AllocateHandle(InsertIndex); return InsertIndex; } template int32 TMovieSceneCurveChannelImpl::AddConstantKey(ChannelType* InChannel, FFrameNumber InTime, CurveValueType InValue) { const int32 Index = InsertKeyInternal(InChannel, InTime); ChannelValueType& Value = InChannel->Values[Index]; Value.Value = InValue; Value.InterpMode = RCIM_Constant; AutoSetTangents(InChannel); return Index; } template int32 TMovieSceneCurveChannelImpl::AddLinearKey(ChannelType* InChannel, FFrameNumber InTime, CurveValueType InValue) { const int32 Index = InsertKeyInternal(InChannel, InTime); ChannelValueType& Value = InChannel->Values[Index]; Value.Value = InValue; Value.InterpMode = RCIM_Linear; AutoSetTangents(InChannel); return Index; } template int32 TMovieSceneCurveChannelImpl::AddCubicKey(ChannelType* InChannel, FFrameNumber InTime, CurveValueType InValue, ERichCurveTangentMode TangentMode, const FMovieSceneTangentData& Tangent) { const int32 Index = InsertKeyInternal(InChannel, InTime); ChannelValueType& Value = InChannel->Values[Index]; Value.Value = InValue; Value.InterpMode = RCIM_Cubic; Value.TangentMode = TangentMode; Value.Tangent = Tangent; AutoSetTangents(InChannel); return Index; } template bool TMovieSceneCurveChannelImpl::CacheExtrapolation(const ChannelType* InChannel, FFrameTime InTime, UE::MovieScene::Interpolation::FCachedInterpolation& OutValue) { using namespace UE::MovieScene::Interpolation; // If the time is outside of the curve, deal with extrapolation if (InTime < InChannel->Times[0]) { if (InChannel->PreInfinityExtrap == RCCE_None) { return false; } FCachedInterpolationRange Range = FCachedInterpolationRange::Until(InChannel->Times[0]); if (InChannel->PreInfinityExtrap == RCCE_Constant) { OutValue = FCachedInterpolation(Range, FConstantValue(InChannel->Times[0], InChannel->Values[0].Value)); return true; } if (InChannel->PreInfinityExtrap == RCCE_Linear) { const ChannelValueType FirstValue = InChannel->Values[0]; if (FirstValue.InterpMode == RCIM_Constant) { OutValue = FCachedInterpolation(Range, FConstantValue(InChannel->Times[0], FirstValue.Value)); } else if(FirstValue.InterpMode == RCIM_Cubic) { OutValue = FCachedInterpolation(Range, FLinearInterpolation(InChannel->Times[0], FirstValue.Tangent.ArriveTangent, FirstValue.Value)); } else if(FirstValue.InterpMode == RCIM_Linear) { const int32 InterpStartFrame = InChannel->Times[1].Value; const double DY = InChannel->Values[1].Value - FirstValue.Value; const double DX = InChannel->Times[1].Value - InChannel->Times[0].Value; const double Coefficient = DX == 0.0 ? 0.0 : DY/DX; OutValue = FCachedInterpolation(Range, FLinearInterpolation(InChannel->Times[0], Coefficient, FirstValue.Value)); } return true; } } else if (InTime > InChannel->Times.Last()) { if (InChannel->PostInfinityExtrap == RCCE_None) { return false; } FCachedInterpolationRange Range = FCachedInterpolationRange::From(InChannel->Times.Last()); if (InChannel->PostInfinityExtrap == RCCE_Constant) { OutValue = FCachedInterpolation(Range, FConstantValue(InChannel->Times.Last(), InChannel->Values.Last().Value)); return true; } if (InChannel->PostInfinityExtrap == RCCE_Linear) { const ChannelValueType LastValue = InChannel->Values.Last(); if (LastValue.InterpMode == RCIM_Constant) { OutValue = FCachedInterpolation(Range, FConstantValue(InChannel->Times.Last(), LastValue.Value)); } else if(LastValue.InterpMode == RCIM_Cubic) { OutValue = FCachedInterpolation(Range, FLinearInterpolation(InChannel->Times.Last(), LastValue.Tangent.LeaveTangent, LastValue.Value)); } else if(LastValue.InterpMode == RCIM_Linear) { const int32 NumKeys = InChannel->Times.Num(); const int32 InterpStartFrame = InChannel->Times[NumKeys-2].Value; const int32 DeltaFrame = InChannel->Times.Last().Value - InterpStartFrame; const double DY = LastValue.Value - InChannel->Values[NumKeys-2].Value; const double DX = InChannel->Times.Last().Value - InChannel->Times[NumKeys-2].Value; const double Coefficient = DX == 0.0 ? 0.0 : DY/DX; OutValue = FCachedInterpolation(Range, FLinearInterpolation(InChannel->Times.Last(), Coefficient, LastValue.Value)); } return true; } } return false; } template UE::MovieScene::Interpolation::FCachedInterpolation TMovieSceneCurveChannelImpl::GetInterpolationForTime(const ChannelType* InChannel, FFrameTime InTime) { return GetInterpolationForTime(InChannel, nullptr, InTime); } template UE::MovieScene::Interpolation::FCachedInterpolation TMovieSceneCurveChannelImpl::GetInterpolationForTime(const ChannelType* InChannel, FTimeEvaluationCache* InOutEvaluationCache, FFrameTime InTime) { using namespace UE::MovieScene; using namespace UE::MovieScene::Interpolation; const int32 NumKeys = InChannel->Times.Num(); // No keys means default value, or nothing if (NumKeys == 0) { if (InChannel->bHasDefaultValue) { return FCachedInterpolation(FCachedInterpolationRange::Infinite(), FConstantValue(0, InChannel->DefaultValue)); } return FCachedInterpolation(); } // For single keys, we can only ever return that value if (NumKeys == 1) { return FCachedInterpolation(FCachedInterpolationRange::Infinite(), FConstantValue(InChannel->Times[0], InChannel->Values[0].Value)); } // Cache extrapolation if we're outside the bounds of the curve // for any non-piecewise extrapolation algorithms (constant or linear) { FCachedInterpolation ExtrapolatedCache; if (CacheExtrapolation(InChannel, InTime, ExtrapolatedCache)) { return ExtrapolatedCache; } } // Piecewise spline evaluation from this point const FFrameNumber MinFrame = InChannel->Times[0]; const FFrameNumber MaxFrame = InChannel->Times.Last(); // Compute the cycled time based on extrapolation FCycleParams Params = CycleTime(MinFrame, MaxFrame, InTime); // Deal with offset cycles and oscillation if (InTime < FFrameTime(MinFrame)) { switch (InChannel->PreInfinityExtrap) { case RCCE_CycleWithOffset: Params.ComputePreValueOffset(InChannel->Values[0].Value, InChannel->Values[NumKeys-1].Value); break; case RCCE_Oscillate: Params.Oscillate(MinFrame.Value, MaxFrame.Value); break; } } else if (InTime > FFrameTime(MaxFrame)) { switch (InChannel->PostInfinityExtrap) { case RCCE_CycleWithOffset: Params.ComputePostValueOffset(InChannel->Values[0].Value, InChannel->Values[NumKeys-1].Value); break; case RCCE_Oscillate: Params.Oscillate(MinFrame.Value, MaxFrame.Value); break; } } if (!ensureMsgf(Params.Time.FrameNumber >= MinFrame && Params.Time.FrameNumber <= MaxFrame, TEXT("Invalid time computed for float channel evaluation"))) { return FCachedInterpolation(); } // Find the pair of keys that we need to evaluate double Interp = 0.0; int32 Index1 = INDEX_NONE, Index2 = INDEX_NONE; // Initialize cache if not yet performed if(InOutEvaluationCache && InOutEvaluationCache->CachedNumFrames == INDEX_NONE) { UE::MovieScene::EvaluateTime(InChannel->Times, Params.Time, InOutEvaluationCache->Index1, InOutEvaluationCache->Index2, InOutEvaluationCache->InterpValue); InOutEvaluationCache->CachedNumFrames = InChannel->Times.Num(); InOutEvaluationCache->CacheFrameTime = Params.Time; } // If cache matches contained number of frames, copy data out rather than evaluating time data again if (InOutEvaluationCache && InOutEvaluationCache->CachedNumFrames == InChannel->Times.Num() && InOutEvaluationCache->CacheFrameTime == Params.Time) { Interp = InOutEvaluationCache->InterpValue; Index1 = InOutEvaluationCache->Index1; Index2 = InOutEvaluationCache->Index2; } else { UE::MovieScene::EvaluateTime(InChannel->Times, Params.Time, Index1, Index2, Interp); } if (Index1 == INDEX_NONE) { // No starting key - we are probably evaluating directly on the first or last key // we explicitly only cache this for the current time to ensure that subsequent caches can cache the correct pair FCachedInterpolationRange Range = FCachedInterpolationRange::Only(Params.Time.GetFrame()); return FCachedInterpolation(Range, FConstantValue(InChannel->Times[Index2], Params.ValueOffset + InChannel->Values[Index2].Value)); } else if (Index2 == INDEX_NONE) { // No ending key - we are probably evaluating directly on the first or last key // we explicitly only cache this for the current time to ensure that subsequent caches can cache the correct pair FCachedInterpolationRange Range = FCachedInterpolationRange::Only(Params.Time.GetFrame()); return FCachedInterpolation(Range, FConstantValue(InChannel->Times[Index1], Params.ValueOffset + InChannel->Values[Index1].Value)); } else { return GetInterpolationForKey(InChannel, Index1, Index2, &Params); } } template UE::MovieScene::Interpolation::FCachedInterpolation TMovieSceneCurveChannelImpl::GetInterpolationForKey(const ChannelType* InChannel, int32 Index, const UE::MovieScene::FCycleParams* Params) { using namespace UE::MovieScene::Interpolation; check(InChannel->Times.IsValidIndex(Index)); if (Index == InChannel->Times.Num() - 1) { FCachedInterpolationRange Range = FCachedInterpolationRange::Only(InChannel->Times.Last()); return FCachedInterpolation(Range, FConstantValue(Range.Start, InChannel->Values.Last().Value)); } return GetInterpolationForKey(InChannel, Index, Index+1, Params); } template UE::MovieScene::Interpolation::FCachedInterpolation TMovieSceneCurveChannelImpl::GetInterpolationForKey(const ChannelType* InChannel, int32 Index1, int32 Index2, const UE::MovieScene::FCycleParams* Params) { using namespace UE::MovieScene::Interpolation; check(InChannel->Times.IsValidIndex(Index1) && InChannel->Times.IsValidIndex(Index2)); // We have a valid pair of keys to cache on the spline. const double DX = InChannel->Times[Index2].Value - InChannel->Times[Index1].Value; // Cache the pair of keys from the spline at the correct time ChannelValueType Key1 = InChannel->Values[Index1]; ChannelValueType Key2 = InChannel->Values[Index2]; // Manipulate they keys by translating them by the cycle count in the time-domain. // By caching the control points in this translated state we can avoid having to // do any manipulation on the input time const FFrameNumber CycleOffset = Params ? Params->CycleCount*Params->Duration : 0; const double ValueOffset = Params ? Params->ValueOffset : 0.0; const FFrameNumber MinFrame = InChannel->Times[0]; const FFrameNumber MaxFrame = InChannel->Times.Last(); // Compute the start and end values and tangents. FFrameNumber Time1, Time2; // Control point 1 double V1 = ValueOffset + Key1.Value; double T1 = Key1.Tangent.LeaveTangent; double W1 = Key1.Tangent.LeaveTangentWeight; bool bIsWeighted1 = (Key1.Tangent.TangentWeightMode == RCTWM_WeightedBoth || Key1.Tangent.TangentWeightMode == RCTWM_WeightedLeave); // Control point 2 double V2 = ValueOffset + Key2.Value; double T2 = Key2.Tangent.ArriveTangent; double W2 = Key2.Tangent.ArriveTangentWeight; bool bIsWeighted2 = (Key2.Tangent.TangentWeightMode == RCTWM_WeightedBoth || Key2.Tangent.TangentWeightMode == RCTWM_WeightedArrive); // Mirror the curve for oscillation by swapping the control points // and mirroring the times based on the width. if (Params && Params->ShouldMirrorCurve()) { Swap(V1, V2); Swap(T1, T2); Swap(W1, W2); Swap(bIsWeighted1, bIsWeighted2); T1 = -T1; T2 = -T2; // Mirror the times of the control points such that // MinFrame MaxFrame // | x1 x2 x3 | // | becomes | // | x3 x2 x1 | // and offset them by the cycle offset Time1 = MinFrame + (MaxFrame - InChannel->Times[Index2]) + CycleOffset; Time2 = MinFrame + (MaxFrame - InChannel->Times[Index1]) + CycleOffset; } else { // No oscillation so just offset the control points to be in their final positions Time1 = InChannel->Times[Index1] + CycleOffset; Time2 = InChannel->Times[Index2] + CycleOffset; } // Cache this interpolation for any time between the two control points FCachedInterpolationRange Range = FCachedInterpolationRange::Finite(Time1, Time2); // Careful: We use the original Key1 rather than the oscillated interpmode to ensure that // we use the correct key to produce a mirror image TEnumAsByte InterpMode = Key1.InterpMode; const int CheckBothLinear = GSequencerLinearCubicInterpolation; if(InterpMode == RCIM_Linear && (CheckBothLinear && Key2.InterpMode == RCIM_Cubic)) { InterpMode = RCIM_Cubic; } switch (InterpMode) { case RCIM_Cubic: { if (V1 == V2 && T1 == 0 && T2 == 0 && W1 == 0 && W2 == 0) { return FCachedInterpolation(Range, FConstantValue(Time1, V1)); } // Weighted if either control point has weight on their tangent else if (bIsWeighted1 || bIsWeighted2) { return FCachedInterpolation(Range, FWeightedCubicInterpolation( InChannel->TickResolution, Time1, Time1, V1, T1, W1, bIsWeighted1, Time2, V2, T2, W2, bIsWeighted2 )); } else { return FCachedInterpolation(Range, FCubicBezierInterpolation(Time1, DX, V1, V2, T1, T2)); } } break; case RCIM_Linear: { const double DY = V2 - V1; if (DY == 0.0) { return FCachedInterpolation(Range, FConstantValue(Time1, V1)); } else { return FCachedInterpolation(Range, FLinearInterpolation(Time1, DY/DX, V1)); } } default: return FCachedInterpolation(Range, FConstantValue(Time1, V1)); } } template bool TMovieSceneCurveChannelImpl::Evaluate(const ChannelType* InChannel, FFrameTime InTime, CurveValueType& OutValue) { return EvaluateCached(InChannel, nullptr, InTime, OutValue); } template bool TMovieSceneCurveChannelImpl::EvaluateWithCache(const ChannelType* InChannel, FTimeEvaluationCache* InOutEvaluationCache, FFrameTime InTime, CurveValueType& OutValue) { using namespace UE::MovieScene; return EvaluateCached(InChannel, InOutEvaluationCache, InTime, OutValue); } template bool TMovieSceneCurveChannelImpl::EvaluateCached(const ChannelType* InChannel, FTimeEvaluationCache* InOutEvaluationCache, FFrameTime InTime, CurveValueType& OutValue) { double ResultValue = 0.0; if (GetInterpolationForTime(InChannel, InOutEvaluationCache, InTime).Evaluate(InTime, ResultValue)) { OutValue = static_cast(ResultValue); return true; } return false; } template int signNoZero(ChannelValueType val) { return (ChannelValueType(0) < val) ? -1 : 1; }; template ChannelValueType ClampTangent(ChannelValueType NewTangent, ChannelValueType PreviousSlope, ChannelValueType NextSlope) { if (signNoZero(PreviousSlope) != signNoZero(NextSlope) || signNoZero(NewTangent) != signNoZero(NextSlope)) { NewTangent = 0.0; } else if (NextSlope >= 0) { NewTangent = FMath::Min(FMath::Min(NewTangent, NextSlope), PreviousSlope); } else { NewTangent = FMath::Max(FMath::Max(NewTangent, NextSlope), PreviousSlope); } return NewTangent; }; template float TMovieSceneCurveChannelImpl::CalcSmartTangent(ChannelType* InChannel, int32 Index) { ChannelValueType& ThisKey = InChannel->Values[Index]; int32 PrevIndex = Index - 1; ChannelValueType PrevKey = InChannel->Values[PrevIndex]; int32 NextIndex = Index + 1; ChannelValueType NextKey = InChannel->Values[NextIndex]; float NewTangent = 0.0f; // if key doesn't lie between we keep it flat(0.0), except if using auto tangent option 2 since that handles it automatically // and let's us do blending to tangents that have overshoot if ((GCachedSequencerAutoTangentInterpolation == 2)|| (ThisKey.Value > PrevKey.Value && ThisKey.Value < NextKey.Value) || (ThisKey.Value < PrevKey.Value && ThisKey.Value > NextKey.Value)) { if (GCachedSequencerAutoTangentInterpolation != 2) //for older versions we try to match over a longer period of tie, not needed with improved flattening { while (NextIndex <= (InChannel->Values.Num() - 2) && FMath::IsNearlyZero(NextKey.Tangent.ArriveTangent) && NextKey.InterpMode == RCIM_Cubic && (NextKey.TangentMode == RCTM_Auto || NextKey.TangentMode == RCTM_SmartAuto) && ((NextKey.Value > ThisKey.Value && NextKey.Value < InChannel->Values[NextIndex + 1].Value) || (NextKey.Value < ThisKey.Value && NextKey.Value > InChannel->Values[NextIndex + 1].Value))) { ++NextIndex; NextKey = InChannel->Values[NextIndex]; } } const CurveValueType OneThird = 1.0 / 3.0; const CurveValueType TwoThird = 2.0 / 3.0; //we calculate the tangent one third from the previous and next tangents const double TimeToPrevious = FMath::Max(KINDA_SMALL_NUMBER, InChannel->Times[Index].Value - InChannel->Times[PrevIndex].Value); const double OneThirdTimeToPrevious = OneThird * TimeToPrevious; CurveValueType PrevY = PrevKey.Value + (PrevKey.Tangent.LeaveTangent * OneThirdTimeToPrevious); const double TimeToNext = FMath::Max(KINDA_SMALL_NUMBER, InChannel->Times[NextIndex].Value - InChannel->Times[Index].Value); const double OneThirdTimeToNext = OneThird * TimeToNext; CurveValueType NextY = NextKey.Value - (NextKey.Tangent.ArriveTangent * OneThirdTimeToNext); //leaving ThisKey.Value - ThisKey.Value there since it is needed if we decide to add any weighting NewTangent = ((ThisKey.Value - PrevY) + (NextY - ThisKey.Value)) / (TwoThird * TimeToPrevious + TwoThird * TimeToNext); if (GCachedSequencerAutoTangentInterpolation == 2) //use flattening, no overshoot { //if two keys are equivalent in value and both auto tangent is zero if (FMath::IsNearlyEqual(ThisKey.Value, NextKey.Value) && NextKey.InterpMode == RCIM_Cubic && (NextKey.TangentMode == RCTM_Auto || NextKey.TangentMode == RCTM_SmartAuto)) { NewTangent = 0.0; } else { const double PreviousSlope = (ThisKey.Value - PrevY) / (TwoThird * TimeToPrevious); const double NextSlope = (NextY - ThisKey.Value) / (TwoThird * TimeToNext); NewTangent = ClampTangent(NewTangent, PreviousSlope, NextSlope); } } else { const float BlendToNextRange = CVarSequencerSmartAutoBlendLocationPercentage->GetFloat(); const double ValDiff = FMath::Abs(NextKey.Value - PrevKey.Value); const double OurDiff = FMath::Abs(ThisKey.Value - PrevKey.Value); //ValDiff won't be zero ever due to previous check double PercDiff = OurDiff / ValDiff; float NextTangent = (NextKey.InterpMode == RCIM_Cubic && (NextKey.TangentMode == RCTM_Auto || NextKey.TangentMode == RCTM_SmartAuto)) ? 0.0 : NextKey.Tangent.ArriveTangent; float PrevTangent = (PrevKey.InterpMode == RCIM_Cubic && (PrevKey.TangentMode == RCTM_Auto || PrevKey.TangentMode == RCTM_SmartAuto)) ? 0.0 : PrevKey.Tangent.LeaveTangent; if (BlendToNextRange >= 0.0f && BlendToNextRange <= 1.0f) { NextTangent = PrevTangent = 0.0; if (PercDiff > BlendToNextRange) { PercDiff = (PercDiff - BlendToNextRange) / (1.0 - BlendToNextRange); NewTangent = NewTangent * (1.0 - PercDiff) + (PercDiff * NextTangent); } else if (PercDiff < (1.0 - BlendToNextRange)) { PercDiff = PercDiff / (1.0 - BlendToNextRange); NewTangent = NewTangent * PercDiff + (1.0 - PercDiff) * PrevTangent; } } } } return NewTangent; } template void TMovieSceneCurveChannelImpl::AutoSetTangents(ChannelType* InChannel, float Tension) { if (InChannel->Values.Num() < 2) { return; } { ChannelValueType& FirstValue = InChannel->Values[0]; if (FirstValue.InterpMode == RCIM_Linear) { FirstValue.Tangent.TangentWeightMode = RCTWM_WeightedNone; ChannelValueType& NextKey = InChannel->Values[1]; const double NextTimeDiff = FMath::Max(KINDA_SMALL_NUMBER, InChannel->Times[1].Value - InChannel->Times[0].Value); const double NewTangent = (NextKey.Value - FirstValue.Value) / NextTimeDiff; FirstValue.Tangent.LeaveTangent = NewTangent; } else if (FirstValue.InterpMode == RCIM_Cubic && (FirstValue.TangentMode == RCTM_Auto || FirstValue.TangentMode == RCTM_SmartAuto)) { FirstValue.Tangent.LeaveTangent = FirstValue.Tangent.ArriveTangent = 0.0f; FirstValue.Tangent.TangentWeightMode = RCTWM_WeightedNone; } } { ChannelValueType& LastValue = InChannel->Values.Last(); if (LastValue.InterpMode == RCIM_Linear) { LastValue.Tangent.TangentWeightMode = RCTWM_WeightedNone; int32 Index = InChannel->Values.Num() - 1; ChannelValueType& PrevKey = InChannel->Values[Index-1]; const double PrevTimeDiff = FMath::Max(KINDA_SMALL_NUMBER, InChannel->Times[Index].Value - InChannel->Times[Index - 1].Value); const double NewTangent = (LastValue.Value - PrevKey.Value) / PrevTimeDiff; LastValue.Tangent.ArriveTangent = NewTangent; } else if (LastValue.InterpMode == RCIM_Cubic && (LastValue.TangentMode == RCTM_Auto || LastValue.TangentMode == RCTM_SmartAuto)) { LastValue.Tangent.LeaveTangent = LastValue.Tangent.ArriveTangent = 0.0f; LastValue.Tangent.TangentWeightMode = RCTWM_WeightedNone; } } const int32 MaxSmartAutoIterations = 10; // with smart auto we may need to iterate tangent calculations since it depends // upon next tangent value which may be 0 since it is new. int32 SmartAutoCount = 0; for (int32 Index = 1; Index < InChannel->Values.Num() - 1; ++Index) { ChannelValueType PrevKey = InChannel->Values[Index-1]; ChannelValueType& ThisKey = InChannel->Values[Index ]; if (ThisKey.InterpMode == RCIM_Cubic && (ThisKey.TangentMode == RCTM_Auto || ThisKey.TangentMode == RCTM_SmartAuto)) { ChannelValueType NextKey = InChannel->Values[Index + 1]; float NewTangent = 0.0f; //flat tangent by default double PrevToNextTimeDiff = FMath::Max(KINDA_SMALL_NUMBER, InChannel->Times[Index + 1].Value - InChannel->Times[Index - 1].Value); if (ThisKey.TangentMode == RCTM_SmartAuto) { NewTangent = CalcSmartTangent(InChannel, Index); //okay we changed our tangent so we need to recalc the previous tangent //but only if it's non zero if (FMath::IsNearlyZero(ThisKey.Tangent.ArriveTangent,1e-3f) == false && FMath::IsNearlyEqual(ThisKey.Tangent.ArriveTangent, NewTangent,1e-3f) == false) { if (SmartAutoCount < MaxSmartAutoIterations && Index >= 2) { ++SmartAutoCount; Index -= 2; //go back 2 since we will increment by one } else { SmartAutoCount = 0; } } else { SmartAutoCount = 0; } } else if (GCachedSequencerAutoTangentInterpolation == 0) { //need to pass in the curve value type since though unfortunately tangents are just always floats (for doubles or floats), //the AutoCalcTangent works with either. CurveValueType ValueNewTangent = 0.0; AutoCalcTangent(PrevKey.Value, ThisKey.Value, NextKey.Value, Tension, ValueNewTangent); NewTangent = ValueNewTangent; NewTangent /= PrevToNextTimeDiff; } else { if (GCachedSequencerAutoTangentInterpolation >= 1) { // if key doesn't lie between we keep it flat(0.0). if ((ThisKey.Value > PrevKey.Value && ThisKey.Value < NextKey.Value) || (ThisKey.Value < PrevKey.Value && ThisKey.Value > NextKey.Value)) { CurveValueType ValueNewTangent = 0.0; AutoCalcTangent(PrevKey.Value, ThisKey.Value, NextKey.Value, Tension, ValueNewTangent); NewTangent = ValueNewTangent; NewTangent /= PrevToNextTimeDiff; if (GCachedSequencerAutoTangentInterpolation < 2) { //if within 0 to 15% or 85% to 100% range we gradually weight tangent to zero const double AverageToZeroRange = 0.85; const double ValDiff = FMath::Abs(NextKey.Value - PrevKey.Value); const double OurDiff = FMath::Abs(ThisKey.Value - PrevKey.Value); //ValDiff won't be zero ever due to previous check double PercDiff = OurDiff / ValDiff; if (PercDiff > AverageToZeroRange) { PercDiff = (PercDiff - AverageToZeroRange) / (1.0 - AverageToZeroRange); NewTangent = NewTangent * (1.0 - PercDiff); } else if (PercDiff < (1.0 - AverageToZeroRange)) { PercDiff = PercDiff / (1.0 - AverageToZeroRange); NewTangent = NewTangent * PercDiff; } } else if (GCachedSequencerAutoTangentInterpolation == 2) //use flattening, no overshoot { const double TwoThird = 2.0 / 3.0; const double TimeToPrevious = FMath::Max(KINDA_SMALL_NUMBER, InChannel->Times[Index].Value - InChannel->Times[Index - 1].Value); const double TimeToNext = FMath::Max(KINDA_SMALL_NUMBER, InChannel->Times[Index + 1].Value - InChannel->Times[Index].Value); const double PreviousSlope = (ThisKey.Value - PrevKey.Value) / (TwoThird * TimeToPrevious); const double NextSlope = (NextKey.Value - ThisKey.Value) / (TwoThird * TimeToNext); NewTangent = ClampTangent(NewTangent, PreviousSlope, NextSlope); } } } } // In 'auto' mode, arrive and leave tangents are always the same ThisKey.Tangent.LeaveTangent = ThisKey.Tangent.ArriveTangent = (double)NewTangent; ThisKey.Tangent.TangentWeightMode = RCTWM_WeightedNone; } else if (ThisKey.InterpMode == RCIM_Linear) { ThisKey.Tangent.TangentWeightMode = RCTWM_WeightedNone; ChannelValueType& NextKey = InChannel->Values[Index + 1]; const double PrevTimeDiff = FMath::Max(KINDA_SMALL_NUMBER, InChannel->Times[Index].Value - InChannel->Times[Index - 1].Value); double NewTangent = (ThisKey.Value - PrevKey.Value) / PrevTimeDiff; ThisKey.Tangent.ArriveTangent = NewTangent; const double NextTimeDiff = FMath::Max(KINDA_SMALL_NUMBER, InChannel->Times[Index + 1].Value - InChannel->Times[Index].Value); NewTangent = (NextKey.Value - ThisKey.Value) / NextTimeDiff; ThisKey.Tangent.LeaveTangent = NewTangent; } } } template void TMovieSceneCurveChannelImpl::DeleteKeysFrom(ChannelType* InChannel, FFrameNumber InTime, bool bDeleteKeysBefore) { // Insert a key at the current time to maintain evaluation TMovieSceneChannelData ChannelData(InChannel->GetData()); if (ChannelData.GetTimes().Num() > 0) { int32 KeyHandleIndex = ChannelData.FindKey(InTime); if (KeyHandleIndex == INDEX_NONE) { CurveValueType Value = 0; if (Evaluate(InChannel, InTime, Value)) { AddCubicKey(InChannel, InTime, Value); } } } ChannelData.DeleteKeysFrom(InTime, bDeleteKeysBefore); } template void TMovieSceneCurveChannelImpl::ChangeFrameResolution(ChannelType* InChannel, FFrameRate SourceRate, FFrameRate DestinationRate) { RemapTimes(InChannel, UE::MovieScene::FFrameRateRetiming(SourceRate, DestinationRate)); } template void TMovieSceneCurveChannelImpl::RemapTimes(ChannelType* InChannel, const UE::MovieScene::IRetimingInterface& Retimer) { check(InChannel->Times.Num() == InChannel->Values.Num()); double IntervalFactor = Retimer.GetScale(); InChannel->GetData().RemapTimes(Retimer); for (int32 Index = 0; Index < InChannel->Times.Num(); ++Index) { ChannelValueType& Value = InChannel->Values[Index]; Value.Tangent.ArriveTangent *= IntervalFactor; Value.Tangent.LeaveTangent *= IntervalFactor; } } template void TMovieSceneCurveChannelImpl::Optimize(ChannelType* InChannel, const FKeyDataOptimizationParams& InParameters) { TMovieSceneChannelData ChannelData = InChannel->GetData(); TArray OutKeyTimes; TArray OutKeyHandles; InChannel->GetKeys(InParameters.Range, &OutKeyTimes, &OutKeyHandles); if (OutKeyHandles.Num() > 2) { int32 MostRecentKeepKeyIndex = 0; TArray KeysToRemove; for (int32 TestIndex = 1; TestIndex < OutKeyHandles.Num() - 1; ++TestIndex) { int32 Index = ChannelData.GetIndex(OutKeyHandles[TestIndex]); int32 NextIndex = ChannelData.GetIndex(OutKeyHandles[TestIndex+1]); const CurveValueType KeyValue = ChannelData.GetValues()[Index].Value; const CurveValueType ValueWithoutKey = UE::MovieScene::EvalForTwoKeys( ChannelData.GetValues()[MostRecentKeepKeyIndex], ChannelData.GetTimes()[MostRecentKeepKeyIndex].Value, ChannelData.GetValues()[NextIndex], ChannelData.GetTimes()[NextIndex].Value, ChannelData.GetTimes()[Index].Value, InParameters.DisplayRate); if (FMath::Abs(ValueWithoutKey - KeyValue) > InParameters.Tolerance) // Is this key needed { MostRecentKeepKeyIndex = Index; } else { KeysToRemove.Add(OutKeyHandles[TestIndex]); } } ChannelData.DeleteKeys(KeysToRemove); if (InParameters.bAutoSetInterpolation) { AutoSetTangents(InChannel); } } } template EMovieSceneKeyInterpolation TMovieSceneCurveChannelImpl::GetInterpolationMode(ChannelType* InChannel, const FFrameNumber& InTime, EMovieSceneKeyInterpolation DefaultInterpolationMode) { auto ChannelData = InChannel->GetData(); const TArrayView Values = ChannelData.GetValues(); const TArrayView Times = ChannelData.GetTimes(); if (Times.Num() > 0) { //get previous key or first key if first int32 Index = Algo::LowerBound(Times, InTime) - 1; if (Index < 0) { Index = 0; } const ChannelValueType& Value = Values[Index]; //-V758 switch (Value.InterpMode.GetValue()) { case RCIM_Linear: return EMovieSceneKeyInterpolation::Linear; break; case RCIM_Constant: return EMovieSceneKeyInterpolation::Constant; break; case RCIM_Cubic: switch (Value.TangentMode.GetValue()) { case RCTM_Auto: return EMovieSceneKeyInterpolation::Auto; break; case RCTM_SmartAuto: return EMovieSceneKeyInterpolation::SmartAuto; break; case RCTM_Break: return EMovieSceneKeyInterpolation::Break; break; case RCTM_User: return EMovieSceneKeyInterpolation::User; break; } break; } } return DefaultInterpolationMode; } template double TMovieSceneCurveChannelImpl::GetTangentValue(ChannelType* InChannel, const FFrameNumber InFrameTime, const float InFloatValue, double DeltaTime) { //if zero set to .1 default if (FMath::IsNearlyZero(DeltaTime)) { DeltaTime = 0.1; } // Time as seconds double InValue = InFloatValue; FFrameRate TickResolution = InChannel->GetTickResolution(); const double InTime = TickResolution.AsSeconds(InFrameTime); double TargetTime = InTime + DeltaTime; // The time to get tangent value. Could be left or right depending on is DeltaTime is negative or positive CurveValueType CurveTargetValue = 0; // The helper value to get Tangent value CurveValueType CurveValue = 0; Evaluate(InChannel, InFrameTime, CurveValue); Evaluate(InChannel, TargetTime * TickResolution, CurveTargetValue); // Initialize TargetValue by TargetTime double TargetValue = CurveTargetValue; double Value = CurveValue; double TangentValue = (TargetValue - InValue) / FMath::Abs(DeltaTime); // The tangent value to return double PrevTangent = DBL_MAX; // Used for determine whether the tangent is close to the limit int32 Count = 10; // Preventing we stuck in this function for too long // Logic // While the tangents not close enough and we haven't reach the max iteration time while (!FMath::IsNearlyEqual(FMath::Abs(TangentValue), FMath::Abs(PrevTangent)) && Count > 0) { // Update previous tangent value and make delta time smaller PrevTangent = TangentValue; DeltaTime /= 2.0; TargetTime = InTime + DeltaTime; // Calculate a more precise tangent value Evaluate(InChannel, TargetTime * TickResolution, CurveTargetValue); TargetValue = CurveTargetValue; TangentValue = (TargetValue - InValue) / FMath::Abs(DeltaTime); --Count; } return TangentValue * TickResolution.AsInterval(); } template FKeyHandle TMovieSceneCurveChannelImpl::AddKeyToChannel(ChannelType* InChannel, FFrameNumber InFrameNumber, float InValue, EMovieSceneKeyInterpolation Interpolation) { TMovieSceneChannelData ChannelData = InChannel->GetData(); int32 ExistingIndex = ChannelData.FindKey(InFrameNumber); if (ExistingIndex != INDEX_NONE) { ChannelValueType& Value = ChannelData.GetValues()[ExistingIndex]; //-V758 Value.Value = InValue; AutoSetTangents(InChannel); } else { FMovieSceneTangentData TangentData; if ((Interpolation == EMovieSceneKeyInterpolation::User || Interpolation == EMovieSceneKeyInterpolation::Break) && ChannelData.GetTimes().Num() >= 2) { //if we are within the range of existing keys set the tangent to be that of the slope of the curve, otherwise //just set it as flat(leave as default) TRange Range = ChannelData.GetTotalRange(); if (Range.Contains(InFrameNumber)) { const double DeltaTime = 0.1; // Left const float ArriveTangent = -GetTangentValue(InChannel, InFrameNumber, InValue, -DeltaTime); // Right const float LeaveTangent = GetTangentValue(InChannel, InFrameNumber, InValue, DeltaTime); TangentData.ArriveTangent = TangentData.LeaveTangent = (ArriveTangent + LeaveTangent) * 0.5f; } } switch (Interpolation) { case EMovieSceneKeyInterpolation::SmartAuto: ExistingIndex = InChannel->AddCubicKey(InFrameNumber, InValue, RCTM_SmartAuto); break; case EMovieSceneKeyInterpolation::Auto: ExistingIndex = InChannel->AddCubicKey(InFrameNumber, InValue, RCTM_Auto); break; case EMovieSceneKeyInterpolation::User: ExistingIndex = InChannel->AddCubicKey(InFrameNumber, InValue, RCTM_User,TangentData); break; case EMovieSceneKeyInterpolation::Break: ExistingIndex = InChannel->AddCubicKey(InFrameNumber, InValue, RCTM_Break,TangentData); break; case EMovieSceneKeyInterpolation::Linear: ExistingIndex = InChannel->AddLinearKey(InFrameNumber, InValue); break; case EMovieSceneKeyInterpolation::Constant: ExistingIndex = InChannel->AddConstantKey(InFrameNumber, InValue); break; } } return InChannel->GetData().GetHandle(ExistingIndex); } template void TMovieSceneCurveChannelImpl::Dilate(ChannelType* InChannel, FFrameNumber Origin, float DilationFactor) { TArrayView Times = InChannel->GetData().GetTimes(); for (FFrameNumber& Time : Times) { Time = Origin + FFrameNumber(FMath::FloorToInt((Time - Origin).Value * DilationFactor)); } AutoSetTangents(InChannel); } template void TMovieSceneCurveChannelImpl::AssignValue(ChannelType* InChannel, FKeyHandle InKeyHandle, typename ChannelType::CurveValueType InValue) { TMovieSceneChannelData ChannelData = InChannel->GetData(); int32 ValueIndex = ChannelData.GetIndex(InKeyHandle); if (ValueIndex != INDEX_NONE) { ChannelData.GetValues()[ValueIndex].Value = InValue; } } template void TMovieSceneCurveChannelImpl::PopulateCurvePoints(const ChannelType* InChannel, double StartTimeSeconds, double EndTimeSeconds, double TimeThreshold, CurveValueType ValueThreshold, FFrameRate InTickResolution, TArray>& InOutPoints) { const FFrameNumber StartFrame = (StartTimeSeconds * InTickResolution).FloorToFrame(); const FFrameNumber EndFrame = (EndTimeSeconds * InTickResolution).CeilToFrame(); const int32 StartingIndex = Algo::UpperBound(InChannel->Times, StartFrame); const int32 EndingIndex = Algo::LowerBound(InChannel->Times, EndFrame); // Add the lower bound of the visible space CurveValueType EvaluatedValue = 0; if (Evaluate(InChannel, StartFrame, EvaluatedValue)) { InOutPoints.Add(MakeTuple(StartFrame / InTickResolution, double(EvaluatedValue))); } // Add all keys in-between for (int32 KeyIndex = StartingIndex; KeyIndex < EndingIndex; ++KeyIndex) { InOutPoints.Add(MakeTuple(InChannel->Times[KeyIndex] / InTickResolution, double(InChannel->Values[KeyIndex].Value))); } // Add the upper bound of the visible space if (Evaluate(InChannel, EndFrame, EvaluatedValue)) { InOutPoints.Add(MakeTuple(EndFrame / InTickResolution, double(EvaluatedValue))); } int32 OldSize = InOutPoints.Num(); do { OldSize = InOutPoints.Num(); RefineCurvePoints(InChannel, InTickResolution, TimeThreshold, ValueThreshold, InOutPoints); } while(OldSize != InOutPoints.Num()); } template void TMovieSceneCurveChannelImpl::RefineCurvePoints(const ChannelType* InChannel, FFrameRate InTickResolution, double TimeThreshold, CurveValueType ValueThreshold, TArray>& InOutPoints) { const float InterpTimes[] = { 0.25f, 0.5f, 0.6f }; for (int32 Index = 0; Index < InOutPoints.Num() - 1; ++Index) { TTuple Lower = InOutPoints[Index]; TTuple Upper = InOutPoints[Index + 1]; if ((Upper.Get<0>() - Lower.Get<0>()) >= TimeThreshold) { 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; Evaluate(InChannel, EvalTime * InTickResolution, Value); const CurveValueType LinearValue = FMath::Lerp(Lower.Get<1>(), Upper.Get<1>(), InterpTimes[InterpIndex]); if (bSegmentIsLinear) { bSegmentIsLinear = FMath::IsNearlyEqual(Value, LinearValue, ValueThreshold); } Evaluated[InterpIndex].Get<1>() = Value; } if (!bSegmentIsLinear) { // Add the point InOutPoints.Insert(Evaluated, UE_ARRAY_COUNT(Evaluated), Index+1); --Index; } } } } template bool TMovieSceneCurveChannelImpl::ValueExistsAtTime(const ChannelType* Channel, FFrameNumber InFrameNumber, typename ChannelType::CurveValueType Value) { const FFrameTime FrameTime(InFrameNumber); CurveValueType ExistingValue = 0.0; return Channel->Evaluate(FrameTime, ExistingValue) && FMath::IsNearlyEqual(ExistingValue, Value, (CurveValueType)KINDA_SMALL_NUMBER); } template bool TMovieSceneCurveChannelImpl::ValueExistsAtTime(const ChannelType* Channel, FFrameNumber InFrameNumber, const typename ChannelType::ChannelValueType& InValue) { return ValueExistsAtTime(Channel, InFrameNumber, InValue.Value); } template bool TMovieSceneCurveChannelImpl::Serialize(ChannelType* InChannel, FArchive& Ar) { Ar.UsingCustomVersion(FSequencerObjectVersion::GUID); Ar.UsingCustomVersion(FFortniteMainBranchObjectVersion::GUID); if (Ar.CustomVer(FSequencerObjectVersion::GUID) < FSequencerObjectVersion::SerializeFloatChannelCompletely && Ar.CustomVer(FFortniteMainBranchObjectVersion::GUID) < FFortniteMainBranchObjectVersion::SerializeFloatChannelShowCurve) { return false; } const bool bSerializeShowCurve = ( Ar.CustomVer(FFortniteMainBranchObjectVersion::GUID) >= FFortniteMainBranchObjectVersion::SerializeFloatChannelShowCurve); Ar << InChannel->PreInfinityExtrap; Ar << InChannel->PostInfinityExtrap; // Save FFrameNumber(int32) and channel value arrays. // We try to save and load the full array data, unless we are // ByteSwapping or the Size has a mismatch on load, then we do normal save/load if (Ar.IsLoading()) { int32 CurrentSerializedElementSize = sizeof(FFrameNumber); int32 SerializedElementSize = 0; Ar << SerializedElementSize; if (SerializedElementSize != CurrentSerializedElementSize || Ar.IsByteSwapping()) { Ar << InChannel->Times; } else { InChannel->Times.CountBytes(Ar); int32 NewArrayNum = 0; Ar << NewArrayNum; InChannel->Times.Empty(NewArrayNum); if (NewArrayNum > 0) { InChannel->Times.AddUninitialized(NewArrayNum); Ar.Serialize(InChannel->Times.GetData(), NewArrayNum * SerializedElementSize); } } CurrentSerializedElementSize = sizeof(ChannelValueType); Ar << SerializedElementSize; if (SerializedElementSize != CurrentSerializedElementSize || Ar.IsByteSwapping()) { Ar << InChannel->Values; } else { InChannel->Values.CountBytes(Ar); int32 NewArrayNum = 0; Ar << NewArrayNum; InChannel->Values.Empty(NewArrayNum); if (NewArrayNum > 0) { InChannel->Values.AddUninitialized(NewArrayNum); Ar.Serialize(InChannel->Values.GetData(), NewArrayNum * SerializedElementSize); } } } else if (Ar.IsSaving()) { int32 SerializedElementSize = sizeof(FFrameNumber); Ar << SerializedElementSize; InChannel->Times.CountBytes(Ar); int32 ArrayCount = InChannel->Times.Num(); Ar << ArrayCount; if (ArrayCount > 0) { Ar.Serialize(InChannel->Times.GetData(), ArrayCount * SerializedElementSize); } InChannel->Values.CountBytes(Ar); SerializedElementSize = sizeof(ChannelValueType); Ar << SerializedElementSize; ArrayCount = InChannel->Values.Num(); Ar << ArrayCount; if (ArrayCount > 0) { Ar.Serialize(InChannel->Values.GetData(), ArrayCount * SerializedElementSize); } } Ar << InChannel->DefaultValue; Ar << InChannel->bHasDefaultValue; Ar << InChannel->TickResolution.Numerator; Ar << InChannel->TickResolution.Denominator; if (Ar.IsTransacting()) { Ar << InChannel->KeyHandles; } if (bSerializeShowCurve) { #if WITH_EDITOR Ar << InChannel->bShowCurve; #else bool bUnused = false; Ar << bUnused; #endif } return true; } template bool TMovieSceneCurveChannelImpl::SerializeFromRichCurve(ChannelType* InChannel, const FPropertyTag& Tag, FStructuredArchive::FSlot Slot) { static const FName RichCurveName("RichCurve"); check(InChannel); if (Tag.GetType().IsStruct(RichCurveName)) { FRichCurve RichCurve; FRichCurve::StaticStruct()->SerializeItem(Slot, &RichCurve, nullptr); if (RichCurve.GetDefaultValue() != MAX_flt) { InChannel->bHasDefaultValue = true; InChannel->DefaultValue = RichCurve.GetDefaultValue(); } InChannel->PreInfinityExtrap = RichCurve.PreInfinityExtrap; InChannel->PostInfinityExtrap = RichCurve.PostInfinityExtrap; InChannel->Times.Reserve(RichCurve.GetNumKeys()); InChannel->Values.Reserve(RichCurve.GetNumKeys()); const FFrameRate LegacyFrameRate = GetLegacyConversionFrameRate(); const float Interval = LegacyFrameRate.AsInterval(); int32 Index = 0; for (auto It = RichCurve.GetKeyIterator(); It; ++It) { const FRichCurveKey& Key = *It; FFrameNumber KeyTime = UpgradeLegacyMovieSceneTime(nullptr, LegacyFrameRate, It->Time); ChannelValueType NewValue; NewValue.Value = Key.Value; NewValue.InterpMode = Key.InterpMode; NewValue.TangentMode = Key.TangentMode; NewValue.Tangent.ArriveTangent = Key.ArriveTangent * Interval; NewValue.Tangent.LeaveTangent = Key.LeaveTangent * Interval; ConvertInsertAndSort(Index++, KeyTime, NewValue, InChannel->Times, InChannel->Values); } return true; } return false; } template bool TMovieSceneCurveChannelImpl::SerializeChannelValue(ChannelValueType& InValue, FArchive& Ar) { Ar.UsingCustomVersion(FSequencerObjectVersion::GUID); if (Ar.CustomVer(FSequencerObjectVersion::GUID) < FSequencerObjectVersion::SerializeFloatChannel) { return false; } if constexpr(std::is_same_v) { if(Ar.UEVer() >= EUnrealEngineObjectUE5Version::LARGE_WORLD_COORDINATES) { Ar << InValue.Value; } else { // Serialize as float and convert to doubles. checkf(Ar.IsLoading(), TEXT("float -> double conversion applied outside of load!")); float TempValue = (float)InValue.Value; Ar << TempValue; InValue.Value = (double)TempValue; } } else { Ar << InValue.Value; } if (Ar.CustomVer(FSequencerObjectVersion::GUID) < FSequencerObjectVersion::SerializeFloatChannelCompletely) { // Serialization is handled manually to avoid the extra size overhead of FProperty tagging. // Otherwise with many keys in a FMovieSceneFloatValue the size can become quite large. Ar << InValue.InterpMode; Ar << InValue.TangentMode; Ar << InValue.Tangent; } else { Ar << InValue.Tangent.ArriveTangent; Ar << InValue.Tangent.LeaveTangent; Ar << InValue.Tangent.ArriveTangentWeight; Ar << InValue.Tangent.LeaveTangentWeight; Ar << InValue.Tangent.TangentWeightMode; Ar << InValue.InterpMode; Ar << InValue.TangentMode; Ar << InValue.PaddingByte; } return true; } template struct MOVIESCENE_API TMovieSceneCurveChannelImpl; template struct MOVIESCENE_API TMovieSceneCurveChannelImpl;