// Copyright Epic Games, Inc. All Rights Reserved. #include "MuCOE/SMutableCurveViewer.h" #include "CurveEditor.h" #include "CurveEditorScreenSpace.h" #include "Framework/Views/TableViewMetadata.h" #include "ICurveEditorModule.h" #include "MuT/TypeInfo.h" #include "SCurveEditorPanel.h" #include "Widgets/Views/SListView.h" class ITableRow; class STableViewBase; class SWidget; #define LOCTEXT_NAMESPACE "SMutableCurveViewer" #pragma region FMutableCurveModel method definitions void FMutablePreviewerCurveModel::DrawCurve(const FCurveEditor& CurveEditor, const FCurveEditorScreenSpace& ScreenSpace, TArray>& InterpolatingPoints) const { const double StartTimeSeconds = ScreenSpace.GetInputMin(); const double EndTimeSeconds = ScreenSpace.GetInputMax(); const double TimeThreshold = FMath::Max(0.0001, 1.0 / ScreenSpace.PixelsPerInput()); const double ValueThreshold = FMath::Max(0.0001, 1.0 / ScreenSpace.PixelsPerOutput()); InterpolatingPoints.Add(MakeTuple(StartTimeSeconds, double(RichCurve.Eval(StartTimeSeconds)))); const TArray& CurveKeys = RichCurve.GetConstRefOfKeys(); for (const FRichCurveKey& Key : CurveKeys) { if (Key.Time > StartTimeSeconds && Key.Time < EndTimeSeconds) { InterpolatingPoints.Add(MakeTuple(double(Key.Time), double(Key.Value))); } } InterpolatingPoints.Add(MakeTuple(EndTimeSeconds, double(RichCurve.Eval(EndTimeSeconds)))); // Generate the points between the key positions we have already set int32 OldSize = InterpolatingPoints.Num(); do { OldSize = InterpolatingPoints.Num(); RefineCurvePoints(RichCurve, TimeThreshold, ValueThreshold, InterpolatingPoints); } while (OldSize != InterpolatingPoints.Num()); } void FMutablePreviewerCurveModel::RefineCurvePoints(const FRichCurve& InRichCurve, double TimeThreshold, float ValueThreshold, TArray>& InOutPoints) const { 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]); float Value = InRichCurve.Eval(EvalTime); const float 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; } } } } void FMutablePreviewerCurveModel::AddKeys(TArrayView InPositions, TArrayView InAttributes, TArrayView>* OutKeyHandles) { check(InPositions.Num() == InAttributes.Num()); // Iterate over the positions and store their data for (int32 KeyIndex = 0; KeyIndex < InPositions.Num(); KeyIndex++) { // Add a new key on our rich curve and cache the handle const float KeyTime = InPositions[KeyIndex].InputValue; const float KeyValue = InPositions[KeyIndex].OutputValue; // RichCurve add key is the one that sets the index to whatever it wants!! KeyHandles.Add( RichCurve.AddKey(KeyTime,KeyValue)); // Load attributes onto the key const FKeyAttributes KeyAttributes = InAttributes[KeyIndex]; { // Set interpolation and weight modes RichCurve.SetKeyInterpMode(KeyHandles.Last(),KeyAttributes.GetInterpMode()); RichCurve.SetKeyTangentMode(KeyHandles.Last(),KeyAttributes.GetTangentMode()); RichCurve.SetKeyTangentWeightMode(KeyHandles.Last(),KeyAttributes.GetTangentWeightMode()); // Set the tangent data FRichCurveKey& RichKey = RichCurve.GetKey(KeyHandles.Last()); RichKey.ArriveTangentWeight = KeyAttributes.GetArriveTangentWeight(); RichKey.LeaveTangentWeight = KeyAttributes.GetLeaveTangentWeight(); RichKey.ArriveTangent = KeyAttributes.GetArriveTangent(); RichKey.LeaveTangent = KeyAttributes.GetLeaveTangent(); } } } void FMutablePreviewerCurveModel::GetKeyPositions(TArrayView InKeys, TArrayView OutKeyPositions) const { if (!InKeys.Num()) { return; } TArray LocatedPositions; // Retrieve the keys by looking for those same key handles on our array of key handles for (int32 InKeyIndex = 0; InKeyIndex < InKeys.Num(); InKeyIndex++) { // Index of the provided key on our array of keys const int32 ActualIndex = KeyHandles.IndexOfByKey(InKeys[InKeyIndex]); const FRichCurveKey Key = RichCurve.GetKey(KeyHandles[ActualIndex] ); FKeyPosition KeyPosition; { KeyPosition.InputValue = Key.Time; KeyPosition.OutputValue = Key.Value; } LocatedPositions.Add(KeyPosition); } OutKeyPositions = TArrayView(LocatedPositions); } void FMutablePreviewerCurveModel::GetTimeRange(double& MinTime, double& MaxTime) const { if (RichCurve.Keys.Num() > 1) { MinTime = RichCurve.Keys[0].Time; MaxTime = RichCurve.Keys[RichCurve.Keys.Num()-1].Time; } else { MinTime = 0; MaxTime = 1; } } void FMutablePreviewerCurveModel::GetValueRange(double& MinValue, double& MaxValue) const { MinValue = 0; MaxValue = 1; // Get max and min values for (int32 RichCurveKeyIndex = 0; RichCurveKeyIndex < RichCurve.Keys.Num(); RichCurveKeyIndex++) { const double PositionValue = RichCurve.Keys[RichCurveKeyIndex].Value; if (PositionValue > MaxValue) { MaxValue = PositionValue; } if (PositionValue < MinValue) { MinValue = PositionValue; } } } void FMutablePreviewerCurveModel::GetNeighboringKeys(const FKeyHandle InKeyHandle, TOptional& OutPreviousKeyHandle, TOptional& OutNextKeyHandle) const { const int32 IndexOfTargetKey = KeyHandles.IndexOfByKey(InKeyHandle); check(IndexOfTargetKey >= 0); if (IndexOfTargetKey > 0) { OutPreviousKeyHandle = KeyHandles[IndexOfTargetKey - 1]; } if (IndexOfTargetKey < KeyHandles.Num() - 1) { OutNextKeyHandle = KeyHandles[IndexOfTargetKey + 1]; } } #pragma endregion namespace MutableCurveKeyFramesListColumns { static const FName KeyFrameIdColumnID("Keyframe"); static const FName KeyFrameTimeColumnID ("Time"); static const FName KeyFrameValueColumnID ("Value"); static const FName KeyFrameInTangentColumnID ("In Tangent"); static const FName KeyFrameInTangentWeightColumnID ("In Tangent Weight"); static const FName KeyFrameOutTangentColumnID ("Out Tangent"); static const FName KeyFrameOutTangentWeightColumnID ("Out Tangent Weight"); static const FName KeyFrameInterpolationModeColumnID ("Interpolation Mode"); static const FName KeyFrameTangentModeColumnID ("Tangent Mode"); static const FName KeyFrameTangentWeightModeColumnID ("Weight Mode"); } class SMutableCurveKeyFrameTableRow : public SMultiColumnTableRow> { public: void Construct(const FArguments& Args, const TSharedRef& InOwnerTableView, const TSharedPtr& InRowItem) { RowItem = InRowItem; SMultiColumnTableRow< TSharedPtr >::Construct( STableRow::FArguments() .ShowSelection(true) , InOwnerTableView ); } virtual TSharedRef GenerateWidgetForColumn( const FName& InColumnName ) override { // INDEX if (InColumnName == MutableCurveKeyFramesListColumns::KeyFrameIdColumnID) { return SNew(SHorizontalBox)+SHorizontalBox::Slot() [ SNew(STextBlock). Text(FText::FromString( FString::FromInt(RowItem->KeyFrameIndex))) ]; } // time if (InColumnName == MutableCurveKeyFramesListColumns::KeyFrameTimeColumnID) { return SNew(SHorizontalBox)+SHorizontalBox::Slot() [ SNew(STextBlock). Text(FText::FromString( FString::SanitizeFloat(RowItem->CurveKeyFrame.Time) )) ]; } // value if (InColumnName == MutableCurveKeyFramesListColumns::KeyFrameValueColumnID) { return SNew(SHorizontalBox)+SHorizontalBox::Slot() [ SNew(STextBlock). Text(FText::FromString( FString::SanitizeFloat(RowItem->CurveKeyFrame.Value) )) ]; } // In tangent if (InColumnName == MutableCurveKeyFramesListColumns::KeyFrameInTangentColumnID) { return SNew(SHorizontalBox)+SHorizontalBox::Slot() [ SNew(STextBlock). Text(FText::FromString( FString::SanitizeFloat(RowItem->CurveKeyFrame.ArriveTangent) )) ]; } // In tangent weight if (InColumnName == MutableCurveKeyFramesListColumns::KeyFrameInTangentWeightColumnID) { return SNew(SHorizontalBox)+SHorizontalBox::Slot() [ SNew(STextBlock). Text(FText::FromString( FString::SanitizeFloat(RowItem->CurveKeyFrame.ArriveTangentWeight) )) ]; } // out tangent if (InColumnName == MutableCurveKeyFramesListColumns::KeyFrameOutTangentColumnID) { return SNew(SHorizontalBox)+SHorizontalBox::Slot() [ SNew(STextBlock). Text(FText::FromString( FString::SanitizeFloat(RowItem->CurveKeyFrame.LeaveTangent) )) ]; } // Out tangent weight if (InColumnName == MutableCurveKeyFramesListColumns::KeyFrameOutTangentWeightColumnID) { return SNew(SHorizontalBox)+SHorizontalBox::Slot() [ SNew(STextBlock). Text(FText::FromString( FString::SanitizeFloat(RowItem->CurveKeyFrame.LeaveTangentWeight) )) ]; } // interp_mode if (InColumnName == MutableCurveKeyFramesListColumns::KeyFrameInterpolationModeColumnID) { const uint8 InterpolationMode = RowItem->CurveKeyFrame.InterpMode; return SNew(SHorizontalBox)+SHorizontalBox::Slot() [ SNew(STextBlock). Text(FText::FromString(* FString::Printf(TEXT("%d"),InterpolationMode))) ]; } // tangent mode if (InColumnName == MutableCurveKeyFramesListColumns::KeyFrameTangentModeColumnID) { const uint8 TangentMode = RowItem->CurveKeyFrame.TangentMode; return SNew(SHorizontalBox)+SHorizontalBox::Slot() [ SNew(STextBlock). Text(FText::FromString(*FString::Printf(TEXT("%d"), TangentMode))) ]; } // tangent weight mode if (InColumnName == MutableCurveKeyFramesListColumns::KeyFrameTangentWeightModeColumnID) { const uint8 TangentWeightMode = RowItem->CurveKeyFrame.TangentWeightMode; return SNew(SHorizontalBox)+SHorizontalBox::Slot() [ SNew(STextBlock). Text(FText::FromString(*FString::Printf(TEXT("%d"), TangentWeightMode))) ]; } // Invalid column name so no widget will be produced checkNoEntry(); return SNullWidget::NullWidget; } private: TSharedPtr RowItem; }; void SMutableCurveViewer::Construct(const FArguments& InArgs) { // Create the Curve Editor { this->CurveEditor = MakeShared(); CurveEditor = MakeShared(); FCurveEditorInitParams InitParams; CurveEditor->InitCurveEditor(InitParams); CurveEditor->GridLineLabelFormatXAttribute = LOCTEXT("GridXLabelFormat", "{0}"); } // Formatting constexpr float VerticalPadding = 30.0f; ChildSlot [ SNew(SVerticalBox) + SVerticalBox::Slot() .FillHeight(0.6f) [ // Curve display SAssignNew(CurveEditorPanel, SCurveEditorPanel, CurveEditor.ToSharedRef()) ] + SVerticalBox::Slot() .Padding(0,VerticalPadding) .AutoHeight() [ // Curve table with all the mutable key data SAssignNew(CurveListView, SListView>) .ListItemsSource(&CurveElements) .OnGenerateRow(this, &SMutableCurveViewer::OnGenerateCurveTableRow) .SelectionMode(ESelectionMode::None) .HeaderRow ( SNew(SHeaderRow) + SHeaderRow::Column(MutableCurveKeyFramesListColumns::KeyFrameIdColumnID) .DefaultLabel(LOCTEXT("KeyframeID", "ID")) .FillWidth(0.22) + SHeaderRow::Column(MutableCurveKeyFramesListColumns::KeyFrameTimeColumnID) .DefaultLabel(LOCTEXT("KeyframeTime", "Time")) + SHeaderRow::Column(MutableCurveKeyFramesListColumns::KeyFrameValueColumnID) .DefaultLabel(LOCTEXT("KeyframeValue", "Value")) + SHeaderRow::Column(MutableCurveKeyFramesListColumns::KeyFrameInTangentColumnID) .DefaultLabel(LOCTEXT("InTangent", "In-Tangent")) + SHeaderRow::Column(MutableCurveKeyFramesListColumns::KeyFrameInTangentWeightColumnID) .DefaultLabel(LOCTEXT("InTangentWeight", "In-Tangent Weight")) + SHeaderRow::Column(MutableCurveKeyFramesListColumns::KeyFrameOutTangentColumnID) .DefaultLabel(LOCTEXT("OutTangent", "Out-Tangent")) + SHeaderRow::Column(MutableCurveKeyFramesListColumns::KeyFrameOutTangentWeightColumnID) .DefaultLabel(LOCTEXT("OutTangentWeight", "Out-Tangent Weight")) + SHeaderRow::Column(MutableCurveKeyFramesListColumns::KeyFrameInterpolationModeColumnID) .DefaultLabel(LOCTEXT("InterpolationMode", "Interpolation Mode")) + SHeaderRow::Column(MutableCurveKeyFramesListColumns::KeyFrameTangentModeColumnID) .DefaultLabel(LOCTEXT("TangentWeight", "Tangent Mode")) + SHeaderRow::Column(MutableCurveKeyFramesListColumns::KeyFrameTangentWeightModeColumnID) .DefaultLabel(LOCTEXT("TangentWeightMode", "Weight Mode")) ) ] ]; SetCurve(InArgs._MutableCurve); } void SMutableCurveViewer::SetCurve(const FRichCurve& InMutableCurve) { this->MutableCurve = InMutableCurve; // Load the Curve Editor with the data found on the mu::Curve SetupMutableCurveGraph(); // Load the data required by the SListView to display the data on the mu::Curve SetupMutableCurveListView(); } void SMutableCurveViewer::SetupMutableCurveListView() { const int32 KeyFrameCount = MutableCurve.Keys.Num() ; CurveElements.SetNum(KeyFrameCount); for (int32 KeyFrameIndex = 0; KeyFrameIndex < KeyFrameCount; KeyFrameIndex++) { const FRichCurveKey& CurrentKeyFrame = MutableCurve.Keys[KeyFrameIndex]; const TSharedPtr NewCurveElement = MakeShareable(new FMutableCurveElement(KeyFrameIndex,CurrentKeyFrame)); CurveElements[KeyFrameIndex] = NewCurveElement; } CurveListView->RequestListRefresh(); } TSharedRef SMutableCurveViewer::OnGenerateCurveTableRow(TSharedPtr InElement, const TSharedRef& OwnerTable) const { TSharedRef Row = SNew(SMutableCurveKeyFrameTableRow, OwnerTable, InElement); return Row; } void SMutableCurveViewer::SetupMutableCurveGraph() const { // Clear all possible curves set on previous iterations if (CurveEditor->GetCurves().Num()) { CurveEditor->RemoveAllCurves(); } // Add a default curve TUniquePtr CurveEditorModule = MakeUnique(); CurveEditorModule->SetColor(FLinearColor(FColor::Yellow)); // Cache the last key time to be able to compare it with the one being currently processed float PreviousFrameTime = TNumericLimits::Lowest(); // Fill the curve with data const int32 KeyFrameCount = MutableCurve.Keys.Num() ; for (int32 KeyframeIndex = 0; KeyframeIndex < KeyFrameCount; KeyframeIndex++) { // Load the mutable data const FRichCurveKey& CurrentKeyframe = MutableCurve.Keys[KeyframeIndex]; // Setup basic data (x and y axis) FKeyPosition KeyPosition; { // time and value KeyPosition.InputValue = CurrentKeyframe.Time; KeyPosition.OutputValue = CurrentKeyframe.Value; } /* * Important : The time value for each key MUST be different. nearly equal values can produce ill-formed graphs * depending on the order of the keys. * * Check out FRichCurve::AddKey for more information (for at the beginning of the method setting the index). */ { // Base amount of time delta to be applied to correct the time as index behaviour of FRichCurve // @note TNumericLimits::Min() is too small; constexpr float ApplicableTimeDelta = 0.00000005f; // Shift the position of the current element if we are too close to the last processed element or if // the previous element for whatever reason is now in front of us (previousTime > currentTime>. if (FMath::IsNearlyEqual(PreviousFrameTime,CurrentKeyframe.Time,ApplicableTimeDelta) || PreviousFrameTime > CurrentKeyframe.Time) { KeyPosition.InputValue += ApplicableTimeDelta; } } // Update previousFrameTime to represent the updated value of the current keyframe PreviousFrameTime = KeyPosition.InputValue; // Setup the curve behaviour for this key of the curve. FKeyAttributes KeyAttributes; { // tangents KeyAttributes.SetArriveTangent(CurrentKeyframe.ArriveTangent); KeyAttributes.SetLeaveTangent(CurrentKeyframe.LeaveTangent); // Weights KeyAttributes.SetArriveTangentWeight(CurrentKeyframe.ArriveTangentWeight); KeyAttributes.SetLeaveTangentWeight(CurrentKeyframe.LeaveTangentWeight); // Interp mode const TEnumAsByte UnrealInterpolationMode = static_cast(CurrentKeyframe.InterpMode); KeyAttributes.SetInterpMode(UnrealInterpolationMode); // Tangent mode const TEnumAsByte UnrealTangentMode = static_cast(CurrentKeyframe.TangentMode); KeyAttributes.SetTangentMode(UnrealTangentMode); // Tangent weight const TEnumAsByte UnrealTangentWeightMode = static_cast(CurrentKeyframe.TangentWeightMode); KeyAttributes.SetTangentWeightMode(UnrealTangentWeightMode); } CurveEditorModule->AddKey(KeyPosition,KeyAttributes); } // Add the curve to the editor const FCurveModelID CurveId = CurveEditor->AddCurve(MoveTemp(CurveEditorModule)); CurveEditor->PinCurve(CurveId); } #undef LOCTEXT_NAMESPACE