// Copyright Epic Games, Inc. All Rights Reserved. #include "Channels/BezierChannelCurveModel.h" #include "Cache/MovieSceneCachedCurve.h" #include "Cache/MovieSceneUpdateCachedCurveData.h" #include "Channels/CurveModelHelpers.h" #include "Channels/MovieSceneChannelData.h" #include "Channels/MovieSceneCurveChannelCommon.h" #include "Channels/MovieSceneDoubleChannel.h" #include "Channels/MovieSceneFloatChannel.h" #include "Containers/EnumAsByte.h" #include "CurveDataAbstraction.h" #include "CurveDrawInfo.h" #include "CurveEditorScreenSpace.h" #include "Curves/RealCurve.h" #include "Curves/RichCurve.h" #include "Delegates/Delegate.h" #include "HAL/PlatformCrt.h" #include "Math/Color.h" #include "Math/UnrealMathUtility.h" #include "Math/Vector2D.h" #include "Misc/FrameNumber.h" #include "Misc/FrameRate.h" #include "Misc/Optional.h" #include "MovieScene.h" #include "MovieSceneSection.h" #include "Styling/AppStyle.h" #include "Templates/Casts.h" #include "Templates/UnrealTemplate.h" #include "UObject/WeakObjectPtr.h" class FCurveEditor; template FBezierChannelBufferedCurveModel::FBezierChannelBufferedCurveModel( const ChannelType* InChannel, TWeakObjectPtr InWeakSection, TArray&& InKeyPositions, TArray&& InKeyAttributes, const FString& InLongDisplayName, const double InValueMin, const double InValueMax) : IBufferedCurveModel(MoveTemp(InKeyPositions), MoveTemp(InKeyAttributes), InLongDisplayName, InValueMin, InValueMax) , Channel(*InChannel) , WeakSection(InWeakSection) {} template void FBezierChannelBufferedCurveModel::DrawCurve(const FCurveEditor& InCurveEditor, const FCurveEditorScreenSpace& InScreenSpace, TArray>& OutInterpolatingPoints) const { UMovieSceneSection* Section = WeakSection.Get(); if (Section && Section->GetTypedOuter()) { FFrameRate TickResolution = Section->GetTypedOuter()->GetTickResolution(); const double StartTimeSeconds = InScreenSpace.GetInputMin(); const double EndTimeSeconds = InScreenSpace.GetInputMax(); const double TimeThreshold = FMath::Max(0.0001, 1.0 / InScreenSpace.PixelsPerInput()); const double ValueThreshold = FMath::Max(0.0001, 1.0 / InScreenSpace.PixelsPerOutput()); Channel.PopulateCurvePoints(StartTimeSeconds, EndTimeSeconds, TimeThreshold, ValueThreshold, TickResolution, OutInterpolatingPoints); } } template bool FBezierChannelBufferedCurveModel::Evaluate(double InTime, double& OutValue) const { return UE::MovieSceneTools::CurveHelpers::Evaluate(InTime, OutValue, Channel, WeakSection); } template class FBezierChannelBufferedCurveModel; template class FBezierChannelBufferedCurveModel; template FBezierChannelCurveModel::FBezierChannelCurveModel(TMovieSceneChannelHandle InChannel, UMovieSceneSection* OwningSection, TWeakPtr InWeakSequencer) : FChannelCurveModel(InChannel, OwningSection, InWeakSequencer) { ChannelType* Channel = InChannel.Get(); if (Channel && OwningSection && OwningSection->GetTypedOuter()) { Channel->SetTickResolution(OwningSection->GetTypedOuter()->GetTickResolution()); } } template FBezierChannelCurveModel::FBezierChannelCurveModel(TMovieSceneChannelHandle InChannel, UMovieSceneSection* OwningSection, UObject* InOwningObject, TWeakPtr InWeakSequencer) : FChannelCurveModel(InChannel, OwningSection, InOwningObject, InWeakSequencer) { ChannelType* Channel = InChannel.Get(); if (Channel && OwningSection && OwningSection->GetTypedOuter()) { Channel->SetTickResolution(OwningSection->GetTypedOuter()->GetTickResolution()); } } template void FBezierChannelCurveModel::DrawCurve(const FCurveEditor& InCurveEditor, const FCurveEditorScreenSpace& InScreenSpace, TArray>& OutInterpolatingPoints) const { ChannelType* Channel = this->GetChannelHandle().Get(); UMovieSceneSection* Section = this->template GetOwningObjectOrOuter(); if (Channel && Section && Section->GetTypedOuter()) { FFrameRate TickResolution = Section->GetTypedOuter()->GetTickResolution(); const double StartTimeSeconds = InScreenSpace.GetInputMin(); const double EndTimeSeconds = InScreenSpace.GetInputMax(); const double TimeThreshold = FMath::Max(0.0001, 1.0 / InScreenSpace.PixelsPerInput()); const double ValueThreshold = FMath::Max(0.0001, 1.0 / InScreenSpace.PixelsPerOutput()); Channel->PopulateCurvePoints(StartTimeSeconds, EndTimeSeconds, TimeThreshold, ValueThreshold, TickResolution, OutInterpolatingPoints); } } template UE::CurveEditor::ICurveEditorCurveCachePool* FBezierChannelCurveModel::DrawCurveToCachePool( const TSharedRef& CurveEditor, const UE::CurveEditor::FCurveDrawParamsHandle& CurveDrawParamsHandle, const FCurveEditorScreenSpace& ScreenSpace) { ChannelType* Channel = this->GetChannelHandle().Get(); UMovieSceneSection* Section = this->template GetOwningObjectOrOuter(); if (!CachedCurve.IsValid()) { CachedCurve = MakeShared>(CurveDrawParamsHandle.GetID()); CachedCurve->Initialize(CurveEditor); } if (Channel && Section && Section->GetTypedOuter()) { const FFrameRate TickResolution = Section->GetTypedOuter()->GetTickResolution(); UE::MovieSceneTools::FMovieSceneUpdateCachedCurveData UpdateData(*CurveEditor, *this, *Channel, ScreenSpace, TickResolution); CachedCurve->UpdateCachedCurve(UpdateData, CurveDrawParamsHandle); } return &UE::MovieSceneTools::FMovieSceneCurveCachePool::Get(); } template bool FBezierChannelCurveModel::HasChangedAndResetTest() { if (CachedCurve.IsValid()) { return CachedCurve->HasChanged() || Super::HasChangedAndResetTest(); } return Super::HasChangedAndResetTest(); } template void FBezierChannelCurveModel::GetKeyDrawInfo(ECurvePointType PointType, const FKeyHandle InKeyHandle, FKeyDrawInfo& OutDrawInfo) const { if (PointType == ECurvePointType::ArriveTangent || PointType == ECurvePointType::LeaveTangent) { OutDrawInfo.Brush = FAppStyle::GetBrush("GenericCurveEditor.TangentHandle"); OutDrawInfo.ScreenSize = FVector2D(8, 8); } else { // All keys are the same size by default OutDrawInfo.ScreenSize = FVector2D(11, 11); ERichCurveInterpMode KeyInterpType = RCIM_None; ERichCurveTangentWeightMode KeyTWType = RCTWM_WeightedNone; // Get the key type from the supplied key handle if it's valid ChannelType* Channel = this->GetChannelHandle().Get(); if (Channel && InKeyHandle != FKeyHandle::Invalid()) { TMovieSceneChannelData ChannelData = Channel->GetData(); const int32 KeyIndex = ChannelData.GetIndex(InKeyHandle); if (KeyIndex != INDEX_NONE) { KeyInterpType = ChannelData.GetValues()[KeyIndex].InterpMode; KeyTWType = ChannelData.GetValues()[KeyIndex].Tangent.TangentWeightMode; } } switch (KeyInterpType) { case ERichCurveInterpMode::RCIM_Constant: OutDrawInfo.Brush = FAppStyle::GetBrush("GenericCurveEditor.ConstantKey"); break; case ERichCurveInterpMode::RCIM_Linear: OutDrawInfo.Brush = FAppStyle::GetBrush("GenericCurveEditor.LinearKey"); break; case ERichCurveInterpMode::RCIM_Cubic: if (KeyTWType == ERichCurveTangentWeightMode::RCTWM_WeightedBoth) { OutDrawInfo.Brush = FAppStyle::GetBrush("GenericCurveEditor.WeightedTangentCubicKey"); } else { OutDrawInfo.Brush = FAppStyle::GetBrush("GenericCurveEditor.CubicKey"); } break; default: OutDrawInfo.Brush = FAppStyle::GetBrush("GenericCurveEditor.Key"); break; } if (this->IsReadOnly()) { OutDrawInfo.Tint = OutDrawInfo.Tint.IsSet() ? OutDrawInfo.Tint.GetValue() * 0.5f : FLinearColor(0.5f, 0.5f, 0.5f); } } } template TPair FBezierChannelCurveModel::GetInterpolationMode(const double& InTime, ERichCurveInterpMode DefaultInterpolationMode, ERichCurveTangentMode DefaultTangentMode) const { ChannelType* Channel = this->GetChannelHandle().Get(); UMovieSceneSection* Section = this->template GetOwningObjectOrOuter(); if (Channel && Section) { FFrameRate TickResolution = Section->GetTypedOuter()->GetTickResolution(); TMovieSceneChannelData ChannelData = Channel->GetData(); TArrayView Times = ChannelData.GetTimes(); TArrayView Values = ChannelData.GetValues(); const FFrameNumber InFrame = (InTime * TickResolution).RoundToFrame(); if (Times.Num() > 0) { int32 InterpolationIndex = Algo::LowerBound(Times, InFrame) - 1; if (InterpolationIndex < 0) { InterpolationIndex = 0; } const FKeyHandle KeyHandle = ChannelData.GetHandle(InterpolationIndex); TArrayView InKey(&KeyHandle, 1); TArray KeyAttributes; KeyAttributes.SetNum(1); GetKeyAttributes(InKey, KeyAttributes); ERichCurveInterpMode InterpMode = KeyAttributes[0].GetInterpMode(); ERichCurveTangentMode TangentMode = KeyAttributes[0].HasTangentMode() ? KeyAttributes[0].GetTangentMode() : DefaultTangentMode; //if we are cubic, with anything but auto tangents we use the default instead, since they will give us flat tangents which aren't good if (InterpMode == ERichCurveInterpMode::RCIM_Cubic && (TangentMode != ERichCurveTangentMode::RCTM_Auto && TangentMode != ERichCurveTangentMode::RCTM_SmartAuto)) { TangentMode = DefaultTangentMode; } return TPair(InterpMode, TangentMode); } } return TPair(DefaultInterpolationMode, DefaultTangentMode); } static bool IsAuto(ERichCurveTangentMode TangentMode) { return (TangentMode == RCTM_Auto || TangentMode == RCTM_SmartAuto); } /** * Shared implementation for getting keys. * * Typically, attributes reflect the settings that the user has manually configured for the keys, so certain attributes may remain unset. * For instance, when TangentMode == RCTM_Auto, tangents and weights are automatically computed, meaning attributes like ArriveTangent are not explicitly set. * Setting ArriveTangent would imply a user-defined value, which is incompatible with TangentMode == RCTM_Auto. * * Sometimes API users still need a way to get the auto-computed values, like those of tangents, though. * For this, use bAllAttributes parameter: * - true: get all attributes, even the auto-computed ones. The values are useful for e.g. UI visualization. Do not pass them to SetKeyAttributes. * - false: get only the attributes that were set by the user (i.e. skip returning auto-computed values). You can pass them to SetKeyAttributes, e.g. for copy paste. */ template static void GetKeyAttributesDetail( ChannelType* InChannel, UMovieSceneSection* InSection, UMovieScene* InMovieScene, TArrayView InKeys, TArrayView OutAttributes ) { if (!InChannel || !InSection || !InMovieScene) { return; } TMovieSceneChannelData ChannelData = InChannel->GetData(); TArrayView Values = ChannelData.GetValues(); const float TimeInterval = InMovieScene->GetTickResolution().AsInterval(); for (int32 Index = 0; Index < InKeys.Num(); ++Index) { const int32 KeyIndex = ChannelData.GetIndex(InKeys[Index]); if (KeyIndex != INDEX_NONE) { const ChannelValue& KeyValue = Values[KeyIndex]; FKeyAttributes& Attributes = OutAttributes[Index]; Attributes.SetInterpMode(KeyValue.InterpMode); // If the previous key is cubic, show the arrive tangent handle even if this key is constant const int32 PreviousKeyIndex = KeyIndex - 1; const bool bGetArriveTangent = Values.IsValidIndex(PreviousKeyIndex) && Values[PreviousKeyIndex].InterpMode == RCIM_Cubic; if (bAllAttributes && bGetArriveTangent) { Attributes.SetArriveTangent(KeyValue.Tangent.ArriveTangent / TimeInterval); } if ((KeyValue.InterpMode != RCIM_Constant && KeyValue.InterpMode != RCIM_Linear)) { Attributes.SetTangentMode(KeyValue.TangentMode); // The remaining settings (arrive / leave tangent, arrive / leave weight, weight mode) can only be user specified, i.e. cannot be set when in auto. // Setting any of them would imply they specified by the user, which would contradict the setting that it's auto. if (!bAllAttributes && IsAuto(KeyValue.TangentMode)) { continue; } Attributes.SetArriveTangent(KeyValue.Tangent.ArriveTangent / TimeInterval); Attributes.SetLeaveTangent(KeyValue.Tangent.LeaveTangent / TimeInterval); if (KeyValue.InterpMode == RCIM_Cubic) { Attributes.SetTangentWeightMode(KeyValue.Tangent.TangentWeightMode); if (KeyValue.Tangent.TangentWeightMode != RCTWM_WeightedNone) { Attributes.SetArriveTangentWeight(KeyValue.Tangent.ArriveTangentWeight); Attributes.SetLeaveTangentWeight(KeyValue.Tangent.LeaveTangentWeight); } } } } } } template void FBezierChannelCurveModel::GetKeyAttributes(TArrayView InKeys, TArrayView OutAttributes) const { ChannelType* Channel = this->GetChannelHandle().Get(); UMovieSceneSection* Section = this->template GetOwningObjectOrOuter(); UMovieScene* MovieScene = Section ? Section->GetTypedOuter() : nullptr; GetKeyAttributesDetail(Channel, Section, MovieScene, InKeys, OutAttributes); } template void FBezierChannelCurveModel::GetKeyAttributesExcludingAutoComputed(TArrayView InKeys, TArrayView OutAttributes) const { ChannelType* Channel = this->GetChannelHandle().Get(); UMovieSceneSection* Section = this->template GetOwningObjectOrOuter(); UMovieScene* MovieScene = Section ? Section->GetTypedOuter() : nullptr; GetKeyAttributesDetail(Channel, Section, MovieScene, InKeys, OutAttributes); } template void FBezierChannelCurveModel::SetKeyAttributes(TArrayView InKeys, TArrayView InAttributes, EPropertyChangeType::Type ChangeType) { UE::MovieScene::FScopedSignedObjectModifyDefer Defer(false /*do not force upate*/); ChannelType* Channel = this->GetChannelHandle().Get(); UMovieSceneSignedObject* SignedOwner = this->template GetOwningObjectOrOuter(); UMovieScene* MovieScene = SignedOwner ? SignedOwner->GetTypedOuter() : nullptr; // Rule: Auto means tangents and weights are auto computed. // This is used below to correct invalid input InAttributes, e.g. if InAttributes says TangentMode is RCTM_Auto, then ArriveTangent cannot be set. const auto ResetModeAndWeightForAuto = [](ChannelValue& KeyValue) { if (IsAuto(KeyValue.TangentMode)) { KeyValue.TangentMode = RCTM_User; KeyValue.Tangent.TangentWeightMode = RCTWM_WeightedNone; } }; if (Channel && SignedOwner && MovieScene && !this->IsReadOnly()) { bool bAutoSetTangents = false; TMovieSceneChannelData ChannelData = Channel->GetData(); TArrayView Values = ChannelData.GetValues(); FFrameRate TickResolution = MovieScene->GetTickResolution(); float TimeInterval = TickResolution.AsInterval(); for (int32 Index = 0; Index < InKeys.Num(); ++Index) { const int32 KeyIndex = ChannelData.GetIndex(InKeys[Index]); if (KeyIndex != INDEX_NONE) { const FKeyAttributes& Attributes = InAttributes[Index]; ChannelValue& KeyValue = Values[KeyIndex]; if (Attributes.HasInterpMode()) { KeyValue.InterpMode = Attributes.GetInterpMode(); bAutoSetTangents = true; } if (Attributes.HasTangentMode()) { KeyValue.TangentMode = Attributes.GetTangentMode(); if (IsAuto(KeyValue.TangentMode)) { KeyValue.Tangent.TangentWeightMode = RCTWM_WeightedNone; } bAutoSetTangents = true; } if (Attributes.HasTangentWeightMode()) { if (KeyValue.Tangent.TangentWeightMode == RCTWM_WeightedNone) //set tangent weights to default use { TArrayView Times = Channel->GetTimes(); const float OneThird = 1.0f / 3.0f; //calculate a tangent weight based upon tangent and time difference //calculate arrive tangent weight if (KeyIndex > 0 ) { const float X = TickResolution.AsSeconds(Times[KeyIndex].Value - Times[KeyIndex - 1].Value); const float ArriveTangentNormal = KeyValue.Tangent.ArriveTangent / (TimeInterval); const float Y = ArriveTangentNormal * X; KeyValue.Tangent.ArriveTangentWeight = FMath::Sqrt(X*X + Y * Y) * OneThird; } //calculate leave weight if(KeyIndex < ( Times.Num() - 1)) { const float X = TickResolution.AsSeconds(Times[KeyIndex].Value - Times[KeyIndex + 1].Value); const float LeaveTangentNormal = KeyValue.Tangent.LeaveTangent / (TimeInterval); const float Y = LeaveTangentNormal * X; KeyValue.Tangent.LeaveTangentWeight = FMath::Sqrt(X*X + Y*Y) * OneThird; } } KeyValue.Tangent.TangentWeightMode = Attributes.GetTangentWeightMode(); if( KeyValue.Tangent.TangentWeightMode != RCTWM_WeightedNone ) { if (KeyValue.TangentMode != RCTM_User && KeyValue.TangentMode != RCTM_Break) { // Input attribute is invalid: Weights can only be set for user or break tangent modes. Correct it. KeyValue.TangentMode = RCTM_User; } } bAutoSetTangents = true; } if (Attributes.HasArriveTangent()) { // If TangentMode is auto, then the input attribute is invalid: it implies arrive tangent is user specified. Correct TangentMode. ResetModeAndWeightForAuto(KeyValue); KeyValue.Tangent.ArriveTangent = Attributes.GetArriveTangent() * TimeInterval; if (KeyValue.InterpMode == RCIM_Cubic && KeyValue.TangentMode != RCTM_Break) { KeyValue.Tangent.LeaveTangent = KeyValue.Tangent.ArriveTangent; } bAutoSetTangents = true; } if (Attributes.HasLeaveTangent()) { // If TangentMode is auto, then the input attribute is invalid: it implies leave tangent is user specified. Correct TangentMode. ResetModeAndWeightForAuto(KeyValue); KeyValue.Tangent.LeaveTangent = Attributes.GetLeaveTangent() * TimeInterval; if (KeyValue.InterpMode == RCIM_Cubic && KeyValue.TangentMode != RCTM_Break) { KeyValue.Tangent.ArriveTangent = KeyValue.Tangent.LeaveTangent; } bAutoSetTangents = true; } if (Attributes.HasArriveTangentWeight()) { // If TangentMode is auto, then the input attribute is invalid: it implies arrive weight is user specified. Correct TangentMode. ResetModeAndWeightForAuto(KeyValue); KeyValue.Tangent.ArriveTangentWeight = Attributes.GetArriveTangentWeight(); if (KeyValue.InterpMode == RCIM_Cubic && KeyValue.TangentMode != RCTM_Break) { KeyValue.Tangent.LeaveTangentWeight = KeyValue.Tangent.ArriveTangentWeight; } bAutoSetTangents = true; } if (Attributes.HasLeaveTangentWeight()) { // If TangentMode is auto, then the input attribute is invalid: it implies leave weight is user specified. Correct TangentMode. ResetModeAndWeightForAuto(KeyValue); KeyValue.Tangent.LeaveTangentWeight = Attributes.GetLeaveTangentWeight(); if (KeyValue.InterpMode == RCIM_Cubic && KeyValue.TangentMode != RCTM_Break) { KeyValue.Tangent.ArriveTangentWeight = KeyValue.Tangent.LeaveTangentWeight; } bAutoSetTangents = true; } } } if (bAutoSetTangents) { Channel->AutoSetTangents(); } SignedOwner->MarkAsChanged(); this->CurveModifiedDelegate.Broadcast(); } } template void FBezierChannelCurveModel::GetCurveAttributes(FCurveAttributes& OutCurveAttributes) const { ChannelType* Channel = this->GetChannelHandle().Get(); if (Channel) { OutCurveAttributes.SetPreExtrapolation(Channel->PreInfinityExtrap); OutCurveAttributes.SetPostExtrapolation(Channel->PostInfinityExtrap); } } template void FBezierChannelCurveModel::SetCurveAttributes(const FCurveAttributes& InCurveAttributes) { ChannelType* Channel = this->GetChannelHandle().Get(); UMovieSceneSignedObject* SignedOwner = this->template GetOwningObjectOrOuter(); if (Channel && SignedOwner && !this->IsReadOnly()) { if (InCurveAttributes.HasPreExtrapolation()) { Channel->PreInfinityExtrap = InCurveAttributes.GetPreExtrapolation(); } if (InCurveAttributes.HasPostExtrapolation()) { Channel->PostInfinityExtrap = InCurveAttributes.GetPostExtrapolation(); } SignedOwner->MarkAsChanged(); this->CurveModifiedDelegate.Broadcast(); } } /* Finds min/max for cubic curves: Looks for feature points in the signal(determined by change in direction of local tangent), these locations are then re-examined in closer detail recursively Similar to function in RichCurve but usees the Channel::Evaluate function, instead of CurveModel::Eval*/ template void FBezierChannelCurveModel::FeaturePointMethod(double StartTime, double EndTime, double StartValue, double Mu, int Depth, int MaxDepth, double& MaxV, double& MinVal) const { if (Depth >= MaxDepth) { return; } double PrevValue = StartValue; double EvalValue; this->Evaluate(StartTime - Mu, EvalValue); double PrevTangent = StartValue - EvalValue; EndTime += Mu; for (double f = StartTime + Mu; f < EndTime; f += Mu) { double Value; this->Evaluate(f, Value); MaxV = FMath::Max(Value, MaxV); MinVal = FMath::Min(Value, MinVal); double CurTangent = Value - PrevValue; if (FMath::Sign(CurTangent) != FMath::Sign(PrevTangent)) { //feature point centered around the previous tangent double FeaturePointTime = f - Mu * 2.0f; double NewVal; this->Evaluate(FeaturePointTime, NewVal); FeaturePointMethod(FeaturePointTime, f,NewVal, Mu*0.4f, Depth + 1, MaxDepth, MaxV, MinVal); } PrevTangent = CurTangent; PrevValue = Value; } } template void FBezierChannelCurveModel::GetValueRange(double InMinTime, double InMaxTime, double& MinValue, double& MaxValue) const { ChannelType* Channel = this->GetChannelHandle().Get(); UMovieSceneSignedObject* SignedOwner = this->template GetOwningObjectOrOuter(); UMovieScene* MovieScene = SignedOwner ? SignedOwner->GetTypedOuter() : nullptr; if (Channel && SignedOwner && MovieScene && !this->IsReadOnly()) { TArrayView Times = Channel->GetData().GetTimes(); TArrayView Values = Channel->GetData().GetValues(); if (Times.Num() == 0) { // If there are no keys we just use the default value for the channel, defaulting to zero if there is no default. MinValue = MaxValue = Channel->GetDefault().Get(0.f); } else { FFrameRate TickResolution = MovieScene->GetTickResolution(); double ToTime = TickResolution.AsInterval(); int32 LastKeyIndex = Values.Num() - 1; MinValue = TNumericLimits::Max(); MaxValue = TNumericLimits::Lowest(); for (int32 i = 0; i < Values.Num(); i++) { double KeyTime = static_cast(Times[i].Value) * ToTime; if (KeyTime < InMinTime) { continue; } else if (KeyTime > InMaxTime) { break; } const ChannelValue& Key = Values[i]; MinValue = FMath::Min(MinValue, (double)Key.Value); MaxValue = FMath::Max(MaxValue, (double)Key.Value); if (Key.InterpMode == RCIM_Cubic && i != LastKeyIndex) { const ChannelValue& NextKey = Values[i + 1]; double NextTime = static_cast(Times[i + 1].Value) * ToTime; double TimeStep = (NextTime - KeyTime) * 0.2f; FeaturePointMethod(KeyTime, NextTime, Key.Value, TimeStep, 0, 3, MaxValue, MinValue); } } } } //if nothing found just set to zero if (MinValue == TNumericLimits::Max()) { MinValue = 0.0; } if (MaxValue == TNumericLimits::Lowest()) { MaxValue = 0.0; } } template void FBezierChannelCurveModel::GetValueRange(double& MinValue, double& MaxValue) const { const double InMinTime = TNumericLimits::Lowest(); const double InMaxTime = TNumericLimits::Max(); GetValueRange(InMinTime, InMaxTime, MinValue, MaxValue); } template double FBezierChannelCurveModel::GetKeyValue(TArrayView Values, int32 Index) const { return Values[Index].Value; } template void FBezierChannelCurveModel::SetKeyValue(int32 Index, double KeyValue) const { ChannelType* Channel = this->GetChannelHandle().Get(); if (Channel) { TMovieSceneChannelData ChannelData = Channel->GetData(); ChannelData.GetValues()[Index].Value = KeyValue; } } template class FBezierChannelCurveModel; template class FBezierChannelCurveModel;