// Copyright Epic Games, Inc. All Rights Reserved. #include "SMorphTargetViewer.h" #include "Animation/AnimInstance.h" #include "Animation/DebugSkelMeshComponent.h" #include "Animation/MorphTarget.h" #include "Engine/RendererSettings.h" #include "Framework/Commands/UIAction.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "GPUSkinCache.h" #include "HAL/PlatformApplicationMisc.h" #include "IPersonaPreviewScene.h" #include "InterchangeMeshUtilities.h" #include "InterchangeManager.h" #include "Misc/App.h" #include "Rendering/SkeletalMeshLODImporterData.h" #include "ScopedTransaction.h" #include "SkeletalMeshAttributes.h" #include "SkeletalRenderPublic.h" #include "SkinnedAssetCompiler.h" #include "SRenameMorphTargetDialog.h" #include "Textures/SlateIcon.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SCheckBox.h" #include "Widgets/Input/SSearchBox.h" #include "Widgets/Input/SSpinBox.h" #include "Editor.h" #define LOCTEXT_NAMESPACE "SMorphTargetViewer" static const FName ColumnId_MorphTargetNameLabel( "MorphTargetName" ); static const FName ColumnID_MorphTargetWeightLabel( "Weight" ); static const FName ColumnID_MorphTargetEditLabel( "Edit" ); static const FName ColumnID_MorphTargetVertCountLabel( "NumberOfVerts" ); ////////////////////////////////////////////////////////////////////////// // SMorphTargetListRow typedef TSharedPtr< FDisplayedMorphTargetInfo > FDisplayedMorphTargetInfoPtr; class SMorphTargetListRow : public SMultiColumnTableRow< FDisplayedMorphTargetInfoPtr > { public: SLATE_BEGIN_ARGS( SMorphTargetListRow ) {} /** The item for this row **/ SLATE_ARGUMENT( FDisplayedMorphTargetInfoPtr, Item ) /* The SMorphTargetViewer that we push the morph target weights into */ SLATE_ARGUMENT( class SMorphTargetViewer*, MorphTargetViewer ) /* Widget used to display the list of morph targets */ SLATE_ARGUMENT( TSharedPtr, MorphTargetListView ) SLATE_END_ARGS() void Construct( const FArguments& InArgs, const TSharedRef& InPreviewScene, const TSharedRef& OwnerTableView ); /** Overridden from SMultiColumnTableRow. Generates a widget for this column of the tree row. */ virtual TSharedRef GenerateWidgetForColumn( const FName& ColumnName ) override; private: /** * Called when the user begins/ends dragging the slider on the SSpinBox */ void OnBeginSlideMorphTargetWeight(); void OnEndSlideMorphTargetWeight(float Value); /** * Called when the user changes the value of the SSpinBox * * @param NewWeight - The new number the SSpinBox is set to * */ void OnMorphTargetWeightChanged( float NewWeight ); /** * Called when the user types the value and enters * * @param NewWeight - The new number the SSpinBox is set to * */ void OnMorphTargetWeightValueCommitted( float NewWeight, ETextCommit::Type CommitType); /** * Called to know if we enable or disable the weight sliders */ bool IsMorphTargetWeightSliderEnabled() const; /** * Show the tooltip for the weight widget */ FText GetMorphTargetWeightSliderToolTip() const; /** Auto fill check call back functions */ void OnMorphTargetAutoFillChecked(ECheckBoxState InState); ECheckBoxState IsMorphTargetAutoFillChangedChecked() const; /** * Returns the weight of this morph target * * @return SearchText - The new number the SSpinBox is set to * */ float GetWeight() const; /* The SMorphTargetViewer that we push the morph target weights into */ SMorphTargetViewer* MorphTargetViewer; /** Widget used to display the list of morph targets */ TSharedPtr MorphTargetListView; /** The name and weight of the morph target */ FDisplayedMorphTargetInfoPtr Item; /** Preview scene - we invalidate this etc. */ TWeakPtr PreviewScenePtr; }; void SMorphTargetListRow::Construct( const FArguments& InArgs, const TSharedRef& InPreviewScene, const TSharedRef& InOwnerTableView ) { Item = InArgs._Item; MorphTargetViewer = InArgs._MorphTargetViewer; MorphTargetListView = InArgs._MorphTargetListView; PreviewScenePtr = InPreviewScene; check( Item.IsValid() ); SMultiColumnTableRow< FDisplayedMorphTargetInfoPtr >::Construct( FSuperRowType::FArguments(), InOwnerTableView ); } TSharedRef< SWidget > SMorphTargetListRow::GenerateWidgetForColumn( const FName& ColumnName ) { if ( ColumnName == ColumnId_MorphTargetNameLabel ) { FText SourceFilenamesTooltip; FText MorphNameText = FText::FromName(Item->Name); FText CarriageReturn = FText::FromString(TEXT("\n")); for (int32 LodIndex = 0; LodIndex < Item->MorphLodInfos.Num(); ++LodIndex) { const FDisplayedMorphTargetInfo::FMorphLodInfo& MorphLodInfo = Item->MorphLodInfos[LodIndex]; const bool bLodCustomImported = !MorphLodInfo.SourceFilename.IsEmpty(); const bool bIsValidLodMorph = MorphLodInfo.bIsValidLodMorph; const int32 GeneratedFromLodIndex = MorphLodInfo.GeneratedFromLodIndex; if (LodIndex == 0) { if(!bIsValidLodMorph) { MorphNameText = FText::Format(LOCTEXT("MorphRowNameInvalid", "{0} (Empty or invalid Morph Data)"), MorphNameText); } else if (bLodCustomImported) { MorphNameText = FText::Format(LOCTEXT("MorphRowNameCustomImport", "{0} (Imported by File)"), MorphNameText); SourceFilenamesTooltip = FText::Format(LOCTEXT("Lod0Tooltip_ImportByFile", "This morph target was imported from source filename: {0}"), FText::FromString(MorphLodInfo.SourceFilename)); } else if(GeneratedFromLodIndex != INDEX_NONE) { //LOD 0 could be generated only from it's own import data (inline generated) ensure(GeneratedFromLodIndex == 0); MorphNameText = FText::Format(LOCTEXT("MorphRowNameGeneratedFromLOD", "{0} (Generated From Source Data)"), MorphNameText); SourceFilenamesTooltip = LOCTEXT("Lod0Tooltip_GeneratedFromLOD", "This morph target was generated from is base mesh source data."); } else if(MorphLodInfo.bIsGeneratedByEngine) { MorphNameText = FText::Format(LOCTEXT("MorphRowNameGenerated", "{0} (Generated By Engine)"), MorphNameText); SourceFilenamesTooltip = LOCTEXT("Lod0Tooltip_Generated", "This morph target was generated by an engine tool."); } else { MorphNameText = FText::Format(LOCTEXT("MorphRowNameImportWithLodGeometry", "{0} (Imported with LOD geometry)"), MorphNameText); SourceFilenamesTooltip = LOCTEXT("Lod0Tooltip_ImportWithLodGeometry", "This morph target was imported with the LOD geometry."); } } else if (bIsValidLodMorph) //Do not add invalid morph to the tooltip { if (bLodCustomImported) { SourceFilenamesTooltip = FText::Format(LOCTEXT("LodXTooltip_ImportByFile", "{0}{1}LOD {2} was imported from source filename: {3}"), SourceFilenamesTooltip, CarriageReturn, FText::AsNumber(LodIndex), FText::FromString(MorphLodInfo.SourceFilename)); } else if (GeneratedFromLodIndex != INDEX_NONE) { SourceFilenamesTooltip = FText::Format(LOCTEXT("LodXTooltip_GeneratedFromLOD", "{0}{1}LOD {2} was generated from a lower LOD {3}"), SourceFilenamesTooltip, CarriageReturn, FText::AsNumber(LodIndex), GeneratedFromLodIndex); } else if(MorphLodInfo.bIsGeneratedByEngine) { SourceFilenamesTooltip = FText::Format(LOCTEXT("LodXTooltip_Generated", "{0}{1}LOD {2} was generated by an engine tool"), SourceFilenamesTooltip, CarriageReturn, FText::AsNumber(LodIndex)); } else { SourceFilenamesTooltip = FText::Format(LOCTEXT("LodXTooltip_ImportWithLodGeometry", "{0}{1}LOD {2} was imported with the LOD geometry"), SourceFilenamesTooltip, CarriageReturn, FText::AsNumber(LodIndex)); } } } return SNew( SVerticalBox ) + SVerticalBox::Slot() .AutoHeight() .Padding( 0.0f, 4.0f ) .VAlign( VAlign_Center ) [ SNew( STextBlock ) .Text( MorphNameText ) .ToolTipText(SourceFilenamesTooltip) .HighlightText( MorphTargetViewer->GetFilterText() ) ]; } else if ( ColumnName == ColumnID_MorphTargetWeightLabel ) { // Encase the SSpinbox in an SVertical box so we can apply padding. Setting ItemHeight on the containing SListView has no effect :-( return SNew( SVerticalBox ) + SVerticalBox::Slot() .AutoHeight() .Padding( 0.0f, 1.0f ) .VAlign( VAlign_Center ) [ SNew( SSpinBox ) .MinSliderValue(-1.f) .MaxSliderValue(1.f) .Value( this, &SMorphTargetListRow::GetWeight ) .OnBeginSliderMovement(this, &SMorphTargetListRow::OnBeginSlideMorphTargetWeight) .OnEndSliderMovement(this, &SMorphTargetListRow::OnEndSlideMorphTargetWeight) .OnValueChanged( this, &SMorphTargetListRow::OnMorphTargetWeightChanged ) .OnValueCommitted( this, &SMorphTargetListRow::OnMorphTargetWeightValueCommitted ) .IsEnabled(this, &SMorphTargetListRow::IsMorphTargetWeightSliderEnabled) .ToolTipText(this, &SMorphTargetListRow::GetMorphTargetWeightSliderToolTip) ]; } else if ( ColumnName == ColumnID_MorphTargetEditLabel ) { return SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() .Padding(0.0f, 1.0f) .VAlign(VAlign_Center) .HAlign(HAlign_Center) [ SNew(SCheckBox) .OnCheckStateChanged(this, &SMorphTargetListRow::OnMorphTargetAutoFillChecked) .IsChecked(this, &SMorphTargetListRow::IsMorphTargetAutoFillChangedChecked) ]; } else { return SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() .Padding(4.0f, 4.0f) .VAlign(VAlign_Center) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .HAlign(HAlign_Right) [ SNew(STextBlock) .Text(FText::AsNumber(Item->NumberOfVerts)) .HighlightText(MorphTargetViewer->GetFilterText()) ] ]; } } void SMorphTargetListRow::OnBeginSlideMorphTargetWeight() { GEditor->BeginTransaction(LOCTEXT("OverrideMorphTargetWeight", "Override Morph Target Weight")); } void SMorphTargetListRow::OnEndSlideMorphTargetWeight(float Value) { GEditor->EndTransaction(); } void SMorphTargetListRow::OnMorphTargetAutoFillChecked(ECheckBoxState InState) { Item->bAutoFillData = InState == ECheckBoxState::Checked; if (Item->bAutoFillData) { // clear value so that it can be filled up MorphTargetViewer->AddMorphTargetOverride(Item->Name, 0.f, true); } else { // Setting value, add the override MorphTargetViewer->AddMorphTargetOverride(Item->Name, Item->Weight, false); } } ECheckBoxState SMorphTargetListRow::IsMorphTargetAutoFillChangedChecked() const { return (Item->bAutoFillData)? ECheckBoxState::Checked: ECheckBoxState::Unchecked; } void SMorphTargetListRow::OnMorphTargetWeightChanged( float NewWeight ) { // First change this item... // the delta feature is a bit confusing when debugging morphtargets, and you're not sure why it's changing, so I'm disabling it for now. // I think in practice, you want each morph target to move independentaly. It is very unlikely you'd like to move multiple things together. const float MorphTargetMaxBlendWeight = UE::SkeletalRender::Settings::GetMorphTargetMaxBlendWeight(); NewWeight = FMath::Clamp(NewWeight, -MorphTargetMaxBlendWeight, MorphTargetMaxBlendWeight); #if 0 float Delta = NewWeight - GetWeight(); #endif Item->Weight = NewWeight; Item->bAutoFillData = false; MorphTargetViewer->AddMorphTargetOverride( Item->Name, Item->Weight, false ); PreviewScenePtr.Pin()->InvalidateViews(); #if 0 TArray< TSharedPtr< FDisplayedMorphTargetInfo > > SelectedRows = MorphTargetListView->GetSelectedItems(); // ...then any selected rows need changing by the same delta for ( auto ItemIt = SelectedRows.CreateIterator(); ItemIt; ++ItemIt ) { TSharedPtr< FDisplayedMorphTargetInfo > RowItem = ( *ItemIt ); if ( RowItem != Item ) // Don't do "this" row again if it's selected { RowItem->Weight = FMath::Clamp(RowItem->Weight + Delta, -MorphTargetMaxBlendWeight, MorphTargetMaxBlendWeight); RowItem->bAutoFillData = false; MorphTargetViewer->AddMorphTargetOverride( RowItem->Name, RowItem->Weight, false ); } } #endif } void SMorphTargetListRow::OnMorphTargetWeightValueCommitted( float NewWeight, ETextCommit::Type CommitType) { if (CommitType == ETextCommit::OnEnter || CommitType == ETextCommit::OnUserMovedFocus) { const float MorphTargetMaxBlendWeight = UE::SkeletalRender::Settings::GetMorphTargetMaxBlendWeight(); NewWeight = FMath::Clamp(NewWeight, -MorphTargetMaxBlendWeight, MorphTargetMaxBlendWeight); Item->Weight = NewWeight; Item->bAutoFillData = false; MorphTargetViewer->AddMorphTargetOverride(Item->Name, Item->Weight, false); TArray< TSharedPtr< FDisplayedMorphTargetInfo > > SelectedRows = MorphTargetListView->GetSelectedItems(); // ...then any selected rows need changing by the same delta for(auto ItemIt = SelectedRows.CreateIterator(); ItemIt; ++ItemIt) { TSharedPtr< FDisplayedMorphTargetInfo > RowItem = (*ItemIt); if(RowItem != Item) // Don't do "this" row again if it's selected { RowItem->Weight = NewWeight; RowItem->bAutoFillData = false; MorphTargetViewer->AddMorphTargetOverride(RowItem->Name, RowItem->Weight, false); } } PreviewScenePtr.Pin()->InvalidateViews(); } } bool SMorphTargetListRow::IsMorphTargetWeightSliderEnabled() const { const uint32 CVarMorphTargetModeValue = GetDefault()->bUseGPUMorphTargets; return GEnableGPUSkinCache > 0 ? CVarMorphTargetModeValue > 0 : true; } FText SMorphTargetListRow::GetMorphTargetWeightSliderToolTip() const { if (!IsMorphTargetWeightSliderEnabled()) { return LOCTEXT("MorphTargetWeightSliderTooltip", "When using skin cache, the morph target must use the GPU to affect the mesh"); } return FText(); } float SMorphTargetListRow::GetWeight() const { if (Item->bAutoFillData) { float CurrentWeight = 0.f; USkeletalMeshComponent* SkelComp = PreviewScenePtr.Pin()->GetPreviewMeshComponent(); UAnimInstance* AnimInstance = (SkelComp) ? SkelComp->GetAnimInstance() : nullptr; if (AnimInstance) { // make sure if they have value that's not same as saved value const TMap& MorphCurves = AnimInstance->GetAnimationCurveList(EAnimCurveType::MorphTargetCurve); const float* CurrentWeightPtr = MorphCurves.Find(Item->Name); if (CurrentWeightPtr) { CurrentWeight = *CurrentWeightPtr; } } return CurrentWeight; } else { USkeletalMeshComponent* SkelComp = PreviewScenePtr.Pin()->GetPreviewMeshComponent(); return SkelComp->GetMorphTargetCurves()[Item->Name]; } } ////////////////////////////////////////////////////////////////////////// // SMorphTargetViewer void SMorphTargetViewer::Construct(const FArguments& InArgs, const TSharedRef& InPreviewScene, FSimpleMulticastDelegate& OnPostUndo) { PreviewScenePtr = InPreviewScene; SkeletalMesh = InPreviewScene->GetPreviewMeshComponent()->GetSkeletalMeshAsset(); InPreviewScene->RegisterOnPreviewMeshChanged( FOnPreviewMeshChanged::CreateSP( this, &SMorphTargetViewer::OnPreviewMeshChanged ) ); InPreviewScene->RegisterOnMorphTargetsChanged(FSimpleDelegate::CreateSP(this, &SMorphTargetViewer::OnMorphTargetsChanged)); OnPostUndo.Add(FSimpleDelegate::CreateSP(this, &SMorphTargetViewer::OnPostUndo)); const FText SkeletalMeshName = SkeletalMesh ? FText::FromString( SkeletalMesh->GetName() ) : LOCTEXT( "MorphTargetMeshNameLabel", "No Skeletal Mesh Present" ); SkeletalMesh->GetOnMeshChanged().Add(FSimpleDelegate::CreateSP(this, &SMorphTargetViewer::OnMeshChanged)); ChildSlot [ SNew( SVerticalBox ) + SVerticalBox::Slot() .AutoHeight() [ SNew( STextBlock ) .Text( SkeletalMeshName ) ] + SVerticalBox::Slot() .AutoHeight() .Padding(0,2) [ SNew(SHorizontalBox) // Import morph target +SHorizontalBox::Slot() .AutoWidth() [ SNew( SButton ) .OnClicked(this, &SMorphTargetViewer::OnImportMorphTargetButton) [ SNew(STextBlock) .ToolTipText(LOCTEXT("ImportCustomMorphTargetButtonTooltip", "Import a new morph target from a file.")) .Text(LOCTEXT("ImportCustomMorphTargetButtonText", "Import Morph Target")) ] ] ] + SVerticalBox::Slot() .AutoHeight() .Padding(0,2) [ SNew(SHorizontalBox) // Filter entry +SHorizontalBox::Slot() .FillWidth( 1 ) [ SAssignNew( NameFilterBox, SSearchBox ) .SelectAllTextWhenFocused( true ) .OnTextChanged( this, &SMorphTargetViewer::OnFilterTextChanged ) .OnTextCommitted( this, &SMorphTargetViewer::OnFilterTextCommitted ) ] ] + SVerticalBox::Slot() .FillHeight( 1.0f ) // This is required to make the scrollbar work, as content overflows Slate containers by default [ SAssignNew( MorphTargetListView, SMorphTargetListType ) .ListItemsSource( &MorphTargetList ) .OnGenerateRow( this, &SMorphTargetViewer::GenerateMorphTargetRow ) .OnContextMenuOpening( this, &SMorphTargetViewer::OnGetContextMenuContent ) .OnSelectionChanged( this, &SMorphTargetViewer::OnRowsSelectedChanged ) .HeaderRow ( SNew( SHeaderRow ) + SHeaderRow::Column( ColumnId_MorphTargetNameLabel ) .DefaultLabel( LOCTEXT( "MorphTargetNameLabel", "Morph Target Name" ) ) + SHeaderRow::Column( ColumnID_MorphTargetWeightLabel ) .DefaultLabel( LOCTEXT( "MorphTargetWeightLabel", "Weight" ) ) + SHeaderRow::Column(ColumnID_MorphTargetEditLabel) .DefaultLabel(LOCTEXT("MorphTargetEditLabel", "Auto")) + SHeaderRow::Column( ColumnID_MorphTargetVertCountLabel ) .DefaultLabel( LOCTEXT("MorphTargetVertCountLabel", "Vert Count") ) ) ] ]; CreateMorphTargetList(); } TArray SMorphTargetViewer::GetSelectedMorphTargetNames() const { TArray< TSharedPtr< FDisplayedMorphTargetInfo > > SelectedRows = MorphTargetListView->GetSelectedItems(); TArray SelectedMorphTargetNames; for (auto ItemIt = SelectedRows.CreateIterator(); ItemIt; ++ItemIt) { TSharedPtr< FDisplayedMorphTargetInfo > RowItem = (*ItemIt); SelectedMorphTargetNames.AddUnique(RowItem->Name); } return SelectedMorphTargetNames; } TSharedRef SMorphTargetViewer::AsWidget() { return this->AsShared(); } void SMorphTargetViewer::OnPreviewMeshChanged(class USkeletalMesh* OldPreviewMesh, class USkeletalMesh* NewPreviewMesh) { SkeletalMesh = NewPreviewMesh; CreateMorphTargetList( NameFilterBox->GetText().ToString() ); } void SMorphTargetViewer::OnMorphTargetsChanged() { CreateMorphTargetList(NameFilterBox->GetText().ToString()); } void SMorphTargetViewer::OnFilterTextChanged( const FText& SearchText ) { FilterText = SearchText; CreateMorphTargetList( SearchText.ToString() ); } void SMorphTargetViewer::OnFilterTextCommitted( const FText& SearchText, ETextCommit::Type CommitInfo ) { // Just do the same as if the user typed in the box OnFilterTextChanged( SearchText ); } TSharedRef SMorphTargetViewer::GenerateMorphTargetRow(TSharedPtr InInfo, const TSharedRef& OwnerTable) { check( InInfo.IsValid() ); return SNew( SMorphTargetListRow, PreviewScenePtr.Pin().ToSharedRef(), OwnerTable ) .Item( InInfo ) .MorphTargetViewer( this ) .MorphTargetListView( MorphTargetListView ); } TSharedPtr SMorphTargetViewer::OnGetContextMenuContent() const { const bool bShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder( bShouldCloseWindowAfterMenuSelection, NULL); MenuBuilder.BeginSection("MorphTargetAction", LOCTEXT( "MorphsAction", "Selected Item Actions" ) ); { TArray< TSharedPtr< FDisplayedMorphTargetInfo > > SelectedRows = MorphTargetListView->GetSelectedItems(); const int32 SelectionCount = SelectedRows.Num(); if (SelectionCount > 0) { const int32 LodCount = SkeletalMesh->GetLODNum(); bool bShowImportMenu = false; struct FLodMorphTargetImportMenuInfo { bool bShowImportMenu = true; bool bShowReimportMenu = true; bool bShowReimportWithNewFileMenu = true; bool IsMenuShow() const { return bShowImportMenu || bShowReimportMenu || bShowReimportWithNewFileMenu; } void HideMenus() { bShowImportMenu = false; bShowReimportMenu = false; bShowReimportWithNewFileMenu = false; } }; TMap MenuInforPerLods; for (int32 LodIndex = 0; LodIndex < LodCount; ++LodIndex) { FLodMorphTargetImportMenuInfo& MenuInfo = MenuInforPerLods.FindOrAdd(LodIndex); if (!SkeletalMesh->HasMeshDescription(LodIndex)) { MenuInfo.HideMenus(); continue; } for (int32 RowIndex = 0; RowIndex < SelectionCount; ++RowIndex) { UMorphTarget* MorphTarget = SkeletalMesh->FindMorphTarget(SelectedRows[RowIndex]->Name); if (MorphTarget) { //Look at the lod 0 to see if the morph target is an imported morph target constexpr int32 LodIndex0 = 0; if (!MorphTarget->IsCustomImported(LodIndex0)) { MenuInfo.bShowImportMenu = false; MenuInfo.bShowReimportMenu = false; MenuInfo.bShowReimportWithNewFileMenu = false; } else { const bool bIsCustomImportedLod = MorphTarget->IsCustomImported(LodIndex); MenuInfo.bShowImportMenu &= (SelectionCount == 1 && !bIsCustomImportedLod); MenuInfo.bShowReimportMenu &= bIsCustomImportedLod; MenuInfo.bShowReimportWithNewFileMenu &= (SelectionCount == 1 && bIsCustomImportedLod); } } } bShowImportMenu |= MenuInfo.IsMenuShow(); } if (bShowImportMenu) { //Create the import menu for every lods for (int32 LodIndex = 0; LodIndex < LodCount; ++LodIndex) { const FLodMorphTargetImportMenuInfo& MenuInfo = MenuInforPerLods.FindOrAdd(LodIndex); //We can import a morph only if the lod is custom imported if (MenuInfo.IsMenuShow()) { FText SubMenuLabel = FText::Format(LOCTEXT("LodSubMenu", "LOD {0}"), FText::AsNumber(LodIndex)); MenuBuilder.AddSubMenu(SubMenuLabel, FText(), FNewMenuDelegate::CreateLambda([this, LodIndex, MenuInfo](FMenuBuilder& InSubMenuBuilder) { FUIAction Action; //Import Morph target if (MenuInfo.bShowImportMenu) { Action.ExecuteAction = FExecuteAction::CreateSP(const_cast(this), &SMorphTargetViewer::OnReimportMorphTargets, LodIndex); Action.CanExecuteAction = nullptr; const FText Label = LOCTEXT("ImportMorphTargetLabel", "Import"); const FText ToolTipText = LOCTEXT("ImportMorphTargetTooltip", "Import all selected custom imported morph target"); InSubMenuBuilder.AddMenuEntry(Label, ToolTipText, FSlateIcon(), Action); } //Reimport Morph target if (MenuInfo.bShowReimportMenu) { Action.ExecuteAction = FExecuteAction::CreateSP(const_cast(this), &SMorphTargetViewer::OnReimportMorphTargets, LodIndex); Action.CanExecuteAction = nullptr; const FText Label = LOCTEXT("ReimportMorphTargetLabel", "Reimport"); const FText ToolTipText = LOCTEXT("ReimportMorphTargetTooltip", "Reimport all selected custom imported morph target"); InSubMenuBuilder.AddMenuEntry(Label, ToolTipText, FSlateIcon(), Action); } //Reimport Morph target with new file if (MenuInfo.bShowReimportWithNewFileMenu) { Action.ExecuteAction = FExecuteAction::CreateSP(const_cast(this), &SMorphTargetViewer::OnReimportMorphTargetsWithNewFile, LodIndex); Action.CanExecuteAction = nullptr; const FText Label = LOCTEXT("ReimportWithNewFileMorphTargetLabel", "Reimport With New File"); const FText ToolTipText = LOCTEXT("ReimportWithNewFileMorphTargetTooltip", "Ask a file and re-import every selected morph target."); InSubMenuBuilder.AddMenuEntry(Label, ToolTipText, FSlateIcon(), Action); } }) ); } } MenuBuilder.AddMenuSeparator(); } //Basic morph target context menu { FUIAction Action; //Rename morph target { Action.ExecuteAction = FExecuteAction::CreateSP(const_cast(this), &SMorphTargetViewer::OnRenameMorphTargets); Action.CanExecuteAction = nullptr; const FText Label = LOCTEXT("RenameMorphTargetLabel", "Rename"); const FText ToolTipText = LOCTEXT("RenameMorphTargetTooltip", "Rename the selected morph targets"); MenuBuilder.AddMenuEntry(Label, ToolTipText, FSlateIcon(), Action); } //Delete morph target { Action.ExecuteAction = FExecuteAction::CreateSP(const_cast(this), &SMorphTargetViewer::OnDeleteMorphTargets); Action.CanExecuteAction = FCanExecuteAction::CreateSP(this, &SMorphTargetViewer::CanPerformDelete); const FText Label = LOCTEXT("DeleteMorphTargetButtonLabel", "Delete"); const FText ToolTipText = LOCTEXT("DeleteMorphTargetButtonTooltip", "Deletes the selected morph targets."); MenuBuilder.AddMenuEntry(Label, ToolTipText, FSlateIcon(), Action); } //Copy morph target name { Action.ExecuteAction = FExecuteAction::CreateSP(const_cast(this), &SMorphTargetViewer::OnCopyMorphTargetNames); Action.CanExecuteAction = nullptr; const FText Label = LOCTEXT("CopyMorphTargetNamesButtonLabel", "Copy Names"); const FText ToolTipText = LOCTEXT("CopyMorphTargetNamesButtonTooltip", "Copy the names of selected morph targets to clipboard"); MenuBuilder.AddMenuEntry(Label, ToolTipText, FSlateIcon(), Action); } } } } MenuBuilder.EndSection(); return MenuBuilder.MakeWidget(); } void SMorphTargetViewer::CreateMorphTargetList( const FString& SearchText ) { TArray SelectedMorphTargets = GetSelectedMorphTargetNames(); MorphTargetList.Empty(); if ( SkeletalMesh ) { UDebugSkelMeshComponent* MeshComponent = PreviewScenePtr.Pin()->GetPreviewMeshComponent(); TArray>& MorphTargets = SkeletalMesh->GetMorphTargets(); bool bDoFiltering = !SearchText.IsEmpty(); for ( int32 I = 0; I < MorphTargets.Num(); ++I ) { if ( bDoFiltering && !MorphTargets[I]->GetName().Contains( SearchText ) ) { continue; // Skip items that don't match our filter } int32 NumberOfVerts = (MorphTargets[I]->GetMorphLODModels().Num() > 0)? MorphTargets[I]->GetMorphLODModels()[0].Vertices.Num() : 0; TArray MorphLodInfos; MorphLodInfos.AddDefaulted(SkeletalMesh->GetLODNum()); for (int32 LodIndex = 0; LodIndex < SkeletalMesh->GetLODNum(); ++LodIndex) { //Only query the data if the morph lod has some data (is valid) MorphLodInfos[LodIndex].bIsValidLodMorph = MorphTargets[I]->HasDataForLOD(LodIndex); if(MorphLodInfos[LodIndex].bIsValidLodMorph) { const bool bIsGeneratedByEngine = MorphTargets[I]->IsGeneratedByEngine(LodIndex); // Morph target generated by the LOD reduction should be tag properly in the UI // We will keep the generated lod index to be able to output it in the UI if(LodIndex > 0 && bIsGeneratedByEngine && SkeletalMesh->IsReductionActive(LodIndex)) { FSkeletalMeshOptimizationSettings ReductionSettings = SkeletalMesh->GetReductionSettings(LodIndex); const int32 ReductionBaseLodIndex = ReductionSettings.BaseLOD; MorphLodInfos[LodIndex].GeneratedFromLodIndex = ReductionBaseLodIndex; } MorphLodInfos[LodIndex].SourceFilename = MorphTargets[I]->GetCustomImportedSourceFilename(LodIndex); MorphLodInfos[LodIndex].bIsGeneratedByEngine = bIsGeneratedByEngine; } } TSharedRef Info = FDisplayedMorphTargetInfo::Make( MorphTargets[I]->GetFName(), NumberOfVerts, MorphLodInfos); if(MeshComponent) { const float *CurveValPtr = MeshComponent->GetMorphTargetCurves().Find( MorphTargets[I]->GetFName() ); if(CurveValPtr) { Info.Get().Weight = (*CurveValPtr); Info.Get().bAutoFillData = false; } } MorphTargetList.Add( Info ); } } for (const FDisplayedMorphTargetInfoPtr& Item : MorphTargetList) { if (SelectedMorphTargets.Contains(Item->Name)) { MorphTargetListView->SetItemSelection(Item, true); } } NotifySelectionChange(); MorphTargetListView->RequestListRefresh(); } void SMorphTargetViewer::AddMorphTargetOverride( FName& Name, float Weight, bool bRemoveZeroWeight ) { UDebugSkelMeshComponent* Mesh = PreviewScenePtr.Pin()->GetPreviewMeshComponent(); if ( Mesh ) { GEditor->BeginTransaction(LOCTEXT("MorphTargetOverrideChanged", "Changed Morph Target Override")); Mesh->SetFlags(RF_Transactional); Mesh->Modify(); Mesh->SetMorphTarget( Name, Weight, bRemoveZeroWeight ); GEditor->EndTransaction(); } } bool SMorphTargetViewer::CanPerformDelete() const { TArray< TSharedPtr< FDisplayedMorphTargetInfo > > SelectedRows = MorphTargetListView->GetSelectedItems(); return SelectedRows.Num() > 0; } void SMorphTargetViewer::OnRenameMorphTargets() { auto RenameMorphTarget = [this](UMorphTarget* SelectMorphTarget) { TSharedRef RenameWidgetDialog = SNew(SRenameMorphTargetDialog) .SkeletalMesh(SkeletalMesh) .MorphTarget(SelectMorphTarget); TSharedRef RenameWindowDialog = SNew(SWindow) .Title(LOCTEXT("RenameMorphTargetWindowTitle", "Rename Morph target")) .SizingRule(ESizingRule::Autosized) .SupportsMaximize(false) .SupportsMinimize(false); RenameWindowDialog->SetContent(SNew(SBox) .MinDesiredWidth(320.0f) [ RenameWidgetDialog ]); TSharedPtr CurrentWindow = FSlateApplication::Get().FindWidgetWindow(AsShared()); FSlateApplication::Get().AddModalWindow(RenameWindowDialog, CurrentWindow); }; { FScopedSkeletalMeshPostEditChange PostEditChangeScope(SkeletalMesh); TArray< TSharedPtr< FDisplayedMorphTargetInfo > > SelectedRows = MorphTargetListView->GetSelectedItems(); for (int32 RowIndex = 0; RowIndex < SelectedRows.Num(); ++RowIndex) { UMorphTarget* MorphTarget = SkeletalMesh->FindMorphTarget(SelectedRows[RowIndex]->Name); if (MorphTarget) { RenameMorphTarget(MorphTarget); } } } //Wait until the skeletal mesh compilation is done FSkinnedAssetCompilingManager::Get().FinishCompilation({ SkeletalMesh }); CreateMorphTargetList(NameFilterBox->GetText().ToString()); } void SMorphTargetViewer::OnDeleteMorphTargets() { TArray< TSharedPtr< FDisplayedMorphTargetInfo > > SelectedRows = MorphTargetListView->GetSelectedItems(); // Clean up override usage TArray MorphTargetNames; for (int32 RowIndex = 0; RowIndex < SelectedRows.Num(); ++RowIndex) { UMorphTarget* MorphTarget = SkeletalMesh->FindMorphTarget(SelectedRows[RowIndex]->Name); if(MorphTarget) { AddMorphTargetOverride(SelectedRows[RowIndex]->Name, 0.0f, true); MorphTargetNames.Add(SelectedRows[RowIndex]->Name); } } //Scope a skeletal mesh build { FScopedSkeletalMeshPostEditChange ScopePostEditChange(SkeletalMesh); // Remove from mesh SkeletalMesh->RemoveMorphTargets(MorphTargetNames); } //Wait until the skeletal mesh compilation is done FSkinnedAssetCompilingManager::Get().FinishCompilation({ SkeletalMesh }); CreateMorphTargetList( NameFilterBox->GetText().ToString() ); } void SMorphTargetViewer::OnCopyMorphTargetNames() { FString CopyText; TArray< TSharedPtr< FDisplayedMorphTargetInfo > > SelectedRows = MorphTargetListView->GetSelectedItems(); for (int RowIndex = 0; RowIndex < SelectedRows.Num(); ++RowIndex) { UMorphTarget* MorphTarget = SkeletalMesh->FindMorphTarget(SelectedRows[RowIndex]->Name); if (MorphTarget) { CopyText += FString::Printf(TEXT("%s\r\n"), *MorphTarget->GetName()); } } if(!CopyText.IsEmpty()) { FPlatformApplicationMisc::ClipboardCopy(*CopyText); } } FReply SMorphTargetViewer::OnImportMorphTargetButton() { constexpr int32 LodIndex0 = 0; constexpr bool bWithNewFileTrue = true; constexpr bool bRecreateMorphTargetListTrue = true; InternalImportMorphTarget(LodIndex0, bWithNewFileTrue, nullptr, bRecreateMorphTargetListTrue); return FReply::Handled(); } void SMorphTargetViewer::OnReimportMorphTargets(int32 LodIndex) { constexpr bool bWithNewFileFalse = false; TArray< TSharedPtr< FDisplayedMorphTargetInfo > > SelectedRows = MorphTargetListView->GetSelectedItems(); for (int RowIndex = 0; RowIndex < SelectedRows.Num(); ++RowIndex) { UMorphTarget* MorphTarget = SkeletalMesh->FindMorphTarget(SelectedRows[RowIndex]->Name); if (MorphTarget) { constexpr bool bRecreateMorphTargetListFalse = false; InternalImportMorphTarget(LodIndex, bWithNewFileFalse, MorphTarget, bRecreateMorphTargetListFalse); } } CreateMorphTargetList(); } void SMorphTargetViewer::OnReimportMorphTargetsWithNewFile(int32 LodIndex) { constexpr bool bWithNewFileTrue = true; TArray< TSharedPtr< FDisplayedMorphTargetInfo > > SelectedRows = MorphTargetListView->GetSelectedItems(); for (int RowIndex = 0; RowIndex < SelectedRows.Num(); ++RowIndex) { UMorphTarget* MorphTarget = SkeletalMesh->FindMorphTarget(SelectedRows[RowIndex]->Name); if (MorphTarget) { constexpr bool bRecreateMorphTargetListFalse = false; InternalImportMorphTarget(LodIndex, bWithNewFileTrue, MorphTarget, bRecreateMorphTargetListFalse); } } CreateMorphTargetList(); } void SMorphTargetViewer::InternalImportMorphTarget(int32 LodIndex, bool bWithNewFile, UMorphTarget* ReimportMorphTarget, bool bRecreateMorphTargetList) { FString Filename; bool bInternalWithNewFile = bWithNewFile || !ReimportMorphTarget || !ReimportMorphTarget->IsCustomImported(LodIndex); if (bInternalWithNewFile) { FText PickerTitle = FText::Format(NSLOCTEXT("SMorphTargetViewer", "OnImportNewMorphTarget_PickerTitle", "Choose a file to import a morph target for LOD{0}"), FText::AsNumber(LodIndex)); if (!UInterchangeMeshUtilities::ShowMeshFilePicker(Filename, PickerTitle)) { return; } } else if(ensure(ReimportMorphTarget && ReimportMorphTarget->IsCustomImported(LodIndex))) { Filename = ReimportMorphTarget->GetCustomImportedSourceFilename(LodIndex); } constexpr bool bAsyncFalse = false; UInterchangeManager& InterchangeManager = UInterchangeManager::GetInterchangeManager(); const UInterchangeSourceData* SourceData = InterchangeManager.CreateSourceData(Filename); //Import a new morph target TFuture FutureResult = UInterchangeMeshUtilities::ImportMorphTarget(SkeletalMesh, LodIndex, SourceData, bAsyncFalse, ReimportMorphTarget ? ReimportMorphTarget->GetName() : FString()); ensure(FutureResult.IsReady()); if (bRecreateMorphTargetList) { CreateMorphTargetList(); } } SMorphTargetViewer::~SMorphTargetViewer() { if (PreviewScenePtr.IsValid()) { UDebugSkelMeshComponent* Mesh = PreviewScenePtr.Pin()->GetPreviewMeshComponent(); if (Mesh) { Mesh->ClearMorphTargets(); } } } void SMorphTargetViewer::OnPostUndo() { CreateMorphTargetList(); NotifySelectionChange(); } void SMorphTargetViewer::OnMeshChanged() { CreateMorphTargetList(); NotifySelectionChange(); } void SMorphTargetViewer::NotifySelectionChange() const { TArray SelectedMorphTargetNames = GetSelectedMorphTargetNames(); // stil have to call this even if empty, otherwise it won't clear it PreviewMorphTargets(SelectedMorphTargetNames); } void SMorphTargetViewer::OnRowsSelectedChanged(TSharedPtr Item, ESelectInfo::Type SelectInfo) { NotifySelectionChange(); } void SMorphTargetViewer::PreviewMorphTargets(const TArray& SelectedMorphTargetNames) const { UDebugSkelMeshComponent* PreviewComponent = PreviewScenePtr.Pin()->GetPreviewMeshComponent(); if (PreviewComponent) { PreviewComponent->MorphTargetOfInterests.Reset(); if (SelectedMorphTargetNames.Num() > 0) { if (SkeletalMesh) { for (const FName& MorphTargetName : SelectedMorphTargetNames) { int32 MorphtargetIdx; UMorphTarget* MorphTarget = SkeletalMesh->FindMorphTargetAndIndex(MorphTargetName, MorphtargetIdx); if (MorphTarget != nullptr) { PreviewComponent->MorphTargetOfInterests.AddUnique(MorphTarget); } } } PreviewScenePtr.Pin()->InvalidateViews(); PreviewComponent->PostInitMeshObject(PreviewComponent->MeshObject); } } } #undef LOCTEXT_NAMESPACE