Files
UnrealEngine/Engine/Plugins/Editor/EngineAssetDefinitions/Source/Private/Animation/SkeletalMeshUtilities.cpp
2025-05-18 13:04:45 +08:00

492 lines
14 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Animation/SkeletalMeshUtilities.h"
#include "Engine/SkeletalMesh.h"
#include "Misc/MessageDialog.h"
#include "SlateOptMacros.h"
#include "Framework/Application/SlateApplication.h"
#include "Widgets/Layout/SSeparator.h"
#include "Widgets/Layout/SScrollBox.h"
#include "Widgets/Input/SCheckBox.h"
#include "SSkeletonWidget.h"
#include "AssetNotifications.h"
#include "Editor.h"
#define LOCTEXT_NAMESPACE "AssetTypeActions"
namespace UE::SkeletalMeshUtilities
{
/**
* FBoneCheckboxInfo
*
* Context data for the SDlgMergeSkeleton panel check boxes
*/
struct FBoneCheckboxInfo
{
FName BoneName;
int32 BoneID;
bool bUsed;
};
/**
* FDlgMergeSkeleton
*
* Wrapper class for SDlgMergeSkeleton. This class creates and launches a dialog then awaits the
* result to return to the user.
*/
class FDlgMergeSkeleton
{
public:
enum EResult
{
Cancel = 0, // No/Cancel, normal usage would stop the current action
Confirm = 1, // Yes/Ok/Etc, normal usage would continue with action
};
FDlgMergeSkeleton( USkeletalMesh* InMesh, USkeleton* InSkeleton );
/** Shows the dialog box and waits for the user to respond. */
EResult ShowModal();
// List of required bones for skeleton
TArray<int32> RequiredBones;
private:
/** Cached pointer to the modal window */
TSharedPtr<SWindow> DialogWindow;
/** Cached pointer to the merge skeleton widget */
TSharedPtr<class SDlgMergeSkeleton> DialogWidget;
/** The SkeletalMesh to merge bones from*/
USkeletalMesh* Mesh;
/** The Skeleton to merge bones to */
USkeleton* Skeleton;
};
/**
* Slate panel for choosing which bones to merge into the skeleton
*/
class SDlgMergeSkeleton : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SDlgMergeSkeleton)
{}
/** Window in which this widget resides */
SLATE_ATTRIBUTE(TSharedPtr<SWindow>, ParentWindow)
SLATE_END_ARGS()
/**
* Constructs this widget
*
* @param InArgs The declaration data for this widget
*/
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void Construct( const FArguments& InArgs )
{
UserResponse = FDlgMergeSkeleton::Cancel;
ParentWindow = InArgs._ParentWindow.Get();
this->ChildSlot[
SNew(SVerticalBox)
+SVerticalBox::Slot()
.AutoHeight()
.Padding(8.0f, 4.0f, 8.0f, 4.0f)
[
SNew( STextBlock )
.Text( LOCTEXT("MergeSkeletonDlgDescription", "Would you like to add following bones to the skeleton?"))
]
+SVerticalBox::Slot()
.AutoHeight()
.Padding(8.0f, 4.0f, 8.0f, 4.0f)
[
SNew(SSeparator)
]
+SVerticalBox::Slot()
.Padding(8.0f, 4.0f, 8.0f, 4.0f)
[
SNew(SBorder)
[
SNew(SScrollBox)
+SScrollBox::Slot()
[
//Save this widget so we can populate it later with check boxes
SAssignNew(CheckBoxContainer, SVerticalBox)
]
]
]
+SVerticalBox::Slot()
.AutoHeight()
.HAlign(HAlign_Left)
.Padding(8.0f, 4.0f, 8.0f, 4.0f)
[
SNew(SUniformGridPanel)
.SlotPadding(FAppStyle::GetMargin("StandardDialog.SlotPadding"))
.MinDesiredSlotWidth(FAppStyle::GetFloat("StandardDialog.MinDesiredSlotWidth"))
.MinDesiredSlotHeight(FAppStyle::GetFloat("StandardDialog.MinDesiredSlotHeight"))
+SUniformGridPanel::Slot(0,0)
[
SNew(SButton)
.HAlign(HAlign_Center)
.ContentPadding(FAppStyle::GetMargin("StandardDialog.ContentPadding"))
.OnClicked(this, &SDlgMergeSkeleton::ChangeAllOptions, true)
.Text(LOCTEXT("SkeletonMergeSelectAll", "Select All"))
]
+SUniformGridPanel::Slot(1,0)
[
SNew(SButton)
.HAlign(HAlign_Center)
.ContentPadding(FAppStyle::GetMargin("StandardDialog.ContentPadding"))
.OnClicked(this, &SDlgMergeSkeleton::ChangeAllOptions, false)
.Text(LOCTEXT("SkeletonMergeDeselectAll", "Deselect All"))
]
]
+SVerticalBox::Slot()
.AutoHeight()
.Padding(8.0f, 4.0f, 8.0f, 4.0f)
[
SNew(SSeparator)
]
+SVerticalBox::Slot()
.AutoHeight()
.HAlign(HAlign_Right)
.Padding(8.0f, 4.0f, 8.0f, 4.0f)
[
SNew(SUniformGridPanel)
.SlotPadding(FAppStyle::GetMargin("StandardDialog.SlotPadding"))
.MinDesiredSlotWidth(FAppStyle::GetFloat("StandardDialog.MinDesiredSlotWidth"))
.MinDesiredSlotHeight(FAppStyle::GetFloat("StandardDialog.MinDesiredSlotHeight"))
+SUniformGridPanel::Slot(0,0)
[
SNew(SButton)
.HAlign(HAlign_Center)
.ContentPadding(FAppStyle::GetMargin("StandardDialog.ContentPadding"))
.OnClicked( this, &SDlgMergeSkeleton::OnButtonClick, FDlgMergeSkeleton::Confirm )
.Text(LOCTEXT("SkeletonMergeOk", "OK"))
]
+SUniformGridPanel::Slot(1,0)
[
SNew(SButton)
.HAlign(HAlign_Center)
.ContentPadding(FAppStyle::GetMargin("StandardDialog.ContentPadding"))
.OnClicked( this, &SDlgMergeSkeleton::OnButtonClick, FDlgMergeSkeleton::Cancel )
.Text(LOCTEXT("SkeletonMergeCancel", "Cancel"))
]
]
];
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
/**
* Creates a Slate check box
*
* @param Label Text label for the check box
* @param ButtonId The ID for the check box
* @return The created check box widget
*/
TSharedRef<SWidget> CreateCheckBox( const FString& Label, int32 ButtonId )
{
return
SNew(SCheckBox)
.IsChecked( this, &SDlgMergeSkeleton::IsCheckboxChecked, ButtonId )
.OnCheckStateChanged( this, &SDlgMergeSkeleton::OnCheckboxChanged, ButtonId )
[
SNew(STextBlock).Text(FText::FromString(Label))
];
}
/**
* Returns the state of the check box
*
* @param ButtonId The ID for the check box
* @return The status of the check box
*/
ECheckBoxState IsCheckboxChecked( int32 ButtonId ) const
{
return CheckBoxInfoMap.FindChecked(ButtonId).bUsed ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
}
/**
* Handler for all check box clicks
*
* @param NewCheckboxState The new state of the check box
* @param CheckboxThatChanged The ID of the radio button that has changed.
*/
void OnCheckboxChanged( ECheckBoxState NewCheckboxState, int32 CheckboxThatChanged )
{
FBoneCheckboxInfo& Info = CheckBoxInfoMap.FindChecked(CheckboxThatChanged);
Info.bUsed = !Info.bUsed;
}
/**
* Handler for the Select All and Deselect All buttons
*
* @param bNewCheckedState The new state of the check boxes
*/
FReply ChangeAllOptions(bool bNewCheckedState)
{
for(auto Iter = CheckBoxInfoMap.CreateIterator(); Iter; ++Iter)
{
FBoneCheckboxInfo& Info = Iter.Value();
Info.bUsed = bNewCheckedState;
}
return FReply::Handled();
}
/**
* Populated the dialog with multiple check boxes, each corresponding to a bone
*
* @param BoneInfos The list of Bones to populate the dialog with
*/
void PopulateOptions(TArray<FBoneCheckboxInfo>& BoneInfos)
{
for(auto Iter = BoneInfos.CreateIterator(); Iter; ++Iter)
{
FBoneCheckboxInfo& Info = (*Iter);
Info.bUsed = true;
CheckBoxInfoMap.Add(Info.BoneID, Info);
CheckBoxContainer->AddSlot()
.AutoHeight()
[
CreateCheckBox(Info.BoneName.GetPlainNameString(), Info.BoneID)
];
}
}
/**
* Returns the EResult of the button which the user pressed. Closing of the dialog
* in any other way than clicking "Ok" results in this returning a "Cancel" value
*/
FDlgMergeSkeleton::EResult GetUserResponse() const
{
return UserResponse;
}
/**
* Returns whether the user selected that bone to be used (checked its respective check box)
*/
bool IsBoneIncluded(int32 BoneID)
{
auto* Item = CheckBoxInfoMap.Find(BoneID);
return Item ? Item->bUsed : false;
}
private:
/**
* Handles when a button is pressed, should be bound with appropriate EResult Key
*
* @param ButtonID - The return type of the button which has been pressed.
*/
FReply OnButtonClick(FDlgMergeSkeleton::EResult ButtonID)
{
ParentWindow->RequestDestroyWindow();
UserResponse = ButtonID;
return FReply::Handled();
}
/** Stores the users response to this dialog */
FDlgMergeSkeleton::EResult UserResponse;
/** The slate container that the bone check boxes get added to */
TSharedPtr<SVerticalBox> CheckBoxContainer;
/** Store the check box state for each bone */
TMap<int32,FBoneCheckboxInfo> CheckBoxInfoMap;
/** Pointer to the window which holds this Widget, required for modal control */
TSharedPtr<SWindow> ParentWindow;
};
FDlgMergeSkeleton::FDlgMergeSkeleton( USkeletalMesh* InMesh, USkeleton* InSkeleton )
{
Mesh = InMesh;
Skeleton = InSkeleton;
if (FSlateApplication::IsInitialized())
{
DialogWindow = SNew(SWindow)
.Title( LOCTEXT("MergeSkeletonDlgTitle", "Merge Bones") )
.SupportsMinimize(false) .SupportsMaximize(false)
.ClientSize(FVector2D(350, 500));
TSharedPtr<SBorder> DialogWrapper =
SNew(SBorder)
.BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder"))
.Padding(4.0f)
[
SAssignNew(DialogWidget, SDlgMergeSkeleton)
.ParentWindow(DialogWindow)
];
DialogWindow->SetContent(DialogWrapper.ToSharedRef());
}
}
FDlgMergeSkeleton::EResult FDlgMergeSkeleton::ShowModal()
{
RequiredBones.Empty();
TMap<FName, int32> BoneIndicesMap;
TArray<FBoneCheckboxInfo> BoneInfos;
// Make a list of all skeleton bone list
const FReferenceSkeleton& RefSkeleton = Skeleton->GetReferenceSkeleton();
for ( int32 BoneTreeId=0; BoneTreeId<RefSkeleton.GetRawBoneNum(); ++BoneTreeId )
{
const FName& BoneName = RefSkeleton.GetBoneName(BoneTreeId);
BoneIndicesMap.Add(BoneName, BoneTreeId);
}
for ( int32 RefBoneId=0 ; RefBoneId< Mesh->GetRefSkeleton().GetRawBoneNum() ; ++RefBoneId )
{
const FName& BoneName = Mesh->GetRefSkeleton().GetBoneName(RefBoneId);
// if I can't find this from Skeleton
if (BoneIndicesMap.Find(BoneName)==NULL)
{
FBoneCheckboxInfo Info;
Info.BoneID = RefBoneId;
Info.BoneName = BoneName;
BoneInfos.Add(Info);
}
}
if (BoneInfos.Num() == 0)
{
// it's all identical, but still need to return RequiredBones
// for the case, where they'd like to replace the one exactly same hierarchy but different skeleton
for ( int32 RefBoneId= 0 ; RefBoneId< Mesh->GetRefSkeleton().GetRawBoneNum() ; ++RefBoneId )
{
const FName& BoneName = Mesh->GetRefSkeleton().GetBoneName(RefBoneId);
RequiredBones.Add(RefBoneId);
}
return EResult::Confirm;
}
DialogWidget->PopulateOptions(BoneInfos);
//Show Dialog
GEditor->EditorAddModalWindow(DialogWindow.ToSharedRef());
EResult UserResponse = (EResult)DialogWidget->GetUserResponse();
if(UserResponse == EResult::Confirm)
{
for ( int32 RefBoneId= 0 ; RefBoneId< Mesh->GetRefSkeleton().GetRawBoneNum() ; ++RefBoneId )
{
if ( DialogWidget->IsBoneIncluded(RefBoneId) )
{
TArray<int32> ParentList;
// I need to make sure parent exists first
int32 ParentIndex = Mesh->GetRefSkeleton().GetParentIndex(RefBoneId);
// make sure RequiredBones already have ParentIndex
while (ParentIndex >= 0 )
{
// if I don't have it yet
if ( RequiredBones.Contains(ParentIndex)==false )
{
ParentList.Add(ParentIndex);
}
ParentIndex = Mesh->GetRefSkeleton().GetParentIndex(ParentIndex);
}
if ( ParentList.Num() > 0 )
{
// if we need to add parent list
// add from back to front (since it's added from child to up
for (int32 I=ParentList.Num()-1; I>=0; --I)
{
RequiredBones.Add(ParentList[I]);
}
}
RequiredBones.Add(RefBoneId);
}
}
}
return UserResponse;
}
void AssignSkeletonToMesh(USkeletalMesh* SkelMesh)
{
if (!SkelMesh || SkelMesh->GetOutermost()->bIsCookedForEditor)
{
FAssetNotifications::CannotEditCookedAsset(SkelMesh);
return;
}
// Create a skeleton asset from the selected skeletal mesh. Defaults to being in the same package/group as the skeletal mesh.
TSharedRef<SWindow> WidgetWindow = SNew(SWindow)
.Title(LOCTEXT("ChooseSkeletonWindowTitle", "Choose Skeleton"))
.ClientSize(FVector2D(400,600));
TSharedPtr<SSkeletonSelectorWindow> SkeletonSelectorWindow;
WidgetWindow->SetContent
(
SNew(SBorder)
.BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder"))
[
SAssignNew(SkeletonSelectorWindow, SSkeletonSelectorWindow)
.Object(SkelMesh)
.WidgetWindow(WidgetWindow)
]
);
GEditor->EditorAddModalWindow(WidgetWindow);
USkeleton* SelectedSkeleton = SkeletonSelectorWindow->GetSelectedSkeleton();
// only do this if not same
if ( SelectedSkeleton )
{
FDlgMergeSkeleton AssetDlg( SkelMesh, SelectedSkeleton );
if(AssetDlg.ShowModal() == FDlgMergeSkeleton::Confirm)
{
TArray<int32> RequiredBones;
RequiredBones.Append(AssetDlg.RequiredBones);
if ( RequiredBones.Num() > 0 )
{
// Do automatic asset generation.
bool bSuccess = SelectedSkeleton->MergeBonesToBoneTree( SkelMesh, RequiredBones );
if ( bSuccess )
{
if (SkelMesh->GetSkeleton() != SelectedSkeleton)
{
SkelMesh->SetSkeleton(SelectedSkeleton);
SkelMesh->MarkPackageDirty();
}
FAssetNotifications::SkeletonNeedsToBeSaved(SelectedSkeleton);
}
else
{
// if failed, ask if user would like to regenerate skeleton hierarchy
if ( EAppReturnType::Yes == FMessageDialog::Open( EAppMsgType::YesNo,
LOCTEXT("SkeletonMergeBones_Override", "FAILED TO MERGE BONES: \n\nThis could happen if significant hierarchical changes have been made,\ne.g. inserting a bone between nodes.\nWould you like to regenerate the skeleton from this mesh? \n\n***WARNING: THIS WILL INVALIDATE ALL ANIMATION DATA THAT IS LINKED TO THIS SKELETON***\n")))
{
if ( SelectedSkeleton->RecreateBoneTree( SkelMesh ) )
{
FAssetNotifications::SkeletonNeedsToBeSaved( SelectedSkeleton );
}
}
else
{
FMessageDialog::Open( EAppMsgType::Ok, LOCTEXT("SkeletonMergeBonesFailure", "Failed to merge bones to Skeleton") );
}
}
}
else
{
FMessageDialog::Open( EAppMsgType::Ok, LOCTEXT("SkeletonMergeBonesFailure", "Failed to merge bones to Skeleton") );
}
}
}
}
}
#undef LOCTEXT_NAMESPACE