1606 lines
46 KiB
C++
1606 lines
46 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "SClothAssetSelector.h"
|
|
|
|
#include "Animation/DebugSkelMeshComponent.h"
|
|
#include "AssetRegistry/ARFilter.h"
|
|
#include "AssetRegistry/AssetData.h"
|
|
#include "ClothLODData.h"
|
|
#include "ClothPhysicalMeshData.h"
|
|
#include "ClothingAsset.h"
|
|
#include "ClothingAssetBase.h"
|
|
#include "ClothingAssetExporter.h"
|
|
#include "ClothingAssetFactoryInterface.h"
|
|
#include "ClothingAssetListCommands.h"
|
|
#include "ClothingSimulationFactory.h"
|
|
#include "ClothingSystemEditorInterfaceModule.h"
|
|
#include "Containers/ContainersFwd.h"
|
|
#include "Containers/IndirectArray.h"
|
|
#include "Containers/Map.h"
|
|
#include "Containers/UnrealString.h"
|
|
#include "ContentBrowserDelegates.h"
|
|
#include "ContentBrowserModule.h"
|
|
#include "DetailLayoutBuilder.h"
|
|
#include "Editor.h"
|
|
#include "Editor/EditorEngine.h"
|
|
#include "Engine/SkeletalMesh.h"
|
|
#include "Fonts/SlateFontInfo.h"
|
|
#include "Framework/Application/MenuStack.h"
|
|
#include "Framework/Application/SlateApplication.h"
|
|
#include "Framework/Commands/GenericCommands.h"
|
|
#include "Framework/Commands/UIAction.h"
|
|
#include "Framework/Commands/UICommandList.h"
|
|
#include "Framework/MultiBox/MultiBoxBuilder.h"
|
|
#include "Framework/Views/ITypedTableView.h"
|
|
#include "IContentBrowserSingleton.h"
|
|
#include "Input/Events.h"
|
|
#include "InputCoreTypes.h"
|
|
#include "Internationalization/Internationalization.h"
|
|
#include "Layout/Children.h"
|
|
#include "Layout/Margin.h"
|
|
#include "Layout/WidgetPath.h"
|
|
#include "Math/Vector2D.h"
|
|
#include "Misc/AssertionMacros.h"
|
|
#include "Misc/Attribute.h"
|
|
#include "Modules/ModuleManager.h"
|
|
#include "PointWeightMap.h"
|
|
#include "Rendering/SkeletalMeshLODModel.h"
|
|
#include "Rendering/SkeletalMeshModel.h"
|
|
#include "SCopyVertexColorSettingsPanel.h"
|
|
#include "SPositiveActionButton.h"
|
|
#include "ScopedTransaction.h"
|
|
#include "SkeletalMeshTypes.h"
|
|
#include "SlotBase.h"
|
|
#include "Styling/AppStyle.h"
|
|
#include "Styling/ISlateStyle.h"
|
|
#include "Styling/SlateColor.h"
|
|
#include "Templates/Casts.h"
|
|
#include "Templates/SubclassOf.h"
|
|
#include "Textures/SlateIcon.h"
|
|
#include "Types/SlateStructs.h"
|
|
#include "UObject/Class.h"
|
|
#include "UObject/NameTypes.h"
|
|
#include "UObject/TopLevelAssetPath.h"
|
|
#include "UObject/UObjectGlobals.h"
|
|
#include "UObject/UnrealNames.h"
|
|
#include "Utils/ClothingMeshUtils.h"
|
|
#include "Widgets/Images/SImage.h"
|
|
#include "Widgets/Input/SButton.h"
|
|
#include "Widgets/Input/SCheckBox.h"
|
|
#include "Widgets/Input/SComboButton.h"
|
|
#include "Widgets/Input/SNumericEntryBox.h"
|
|
#include "Widgets/Layout/SBox.h"
|
|
#include "Widgets/Layout/SExpandableArea.h"
|
|
#include "Widgets/Layout/SSplitter.h"
|
|
#include "Widgets/SBoxPanel.h"
|
|
#include "Widgets/SNullWidget.h"
|
|
#include "Widgets/Text/SInlineEditableTextBlock.h"
|
|
#include "Widgets/Text/STextBlock.h"
|
|
#include "Widgets/Views/SHeaderRow.h"
|
|
#include "Widgets/Views/STableRow.h"
|
|
|
|
class FUICommandInfo;
|
|
class ITableRow;
|
|
class STableViewBase;
|
|
class SWidget;
|
|
class UObject;
|
|
struct FGeometry;
|
|
|
|
#define LOCTEXT_NAMESPACE "ClothAssetSelector"
|
|
|
|
FPointWeightMap* FClothingMaskListItem::GetMask()
|
|
{
|
|
if(UClothingAssetCommon* Asset = ClothingAsset.Get())
|
|
{
|
|
if(Asset->IsValidLod(LodIndex))
|
|
{
|
|
FClothLODDataCommon& LodData = Asset->LodData[LodIndex];
|
|
if(LodData.PointWeightMaps.IsValidIndex(MaskIndex))
|
|
{
|
|
return &LodData.PointWeightMaps[MaskIndex];
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
FClothPhysicalMeshData* FClothingMaskListItem::GetMeshData()
|
|
{
|
|
UClothingAssetCommon* Asset = ClothingAsset.Get();
|
|
return Asset && Asset->IsValidLod(LodIndex) ? &Asset->LodData[LodIndex].PhysicalMeshData : nullptr;
|
|
}
|
|
|
|
USkeletalMesh* FClothingMaskListItem::GetOwningMesh()
|
|
{
|
|
UClothingAssetCommon* Asset = ClothingAsset.Get();
|
|
return Asset ? Cast<USkeletalMesh>(Asset->GetOuter()) : nullptr;
|
|
}
|
|
|
|
class SAssetListRow : public STableRow<TSharedPtr<FClothingAssetListItem>>
|
|
{
|
|
public:
|
|
|
|
SLATE_BEGIN_ARGS(SAssetListRow)
|
|
{}
|
|
SLATE_EVENT(FSimpleDelegate, OnInvalidateList)
|
|
SLATE_END_ARGS()
|
|
|
|
void Construct(const FArguments& InArgs, const TSharedRef<STableViewBase>& InOwnerTable, TSharedPtr<FClothingAssetListItem> InItem)
|
|
{
|
|
Item = InItem;
|
|
OnInvalidateList = InArgs._OnInvalidateList;
|
|
|
|
BindCommands();
|
|
|
|
STableRow<TSharedPtr<FClothingAssetListItem>>::Construct(
|
|
STableRow<TSharedPtr<FClothingAssetListItem>>::FArguments()
|
|
.Content()
|
|
[
|
|
SNew(SBox)
|
|
.Padding(2.0f)
|
|
[
|
|
SAssignNew(EditableText, SInlineEditableTextBlock)
|
|
.Text(this, &SAssetListRow::GetAssetName)
|
|
.OnTextCommitted(this, &SAssetListRow::OnCommitAssetName)
|
|
.IsSelected(this, &SAssetListRow::IsSelected)
|
|
]
|
|
],
|
|
InOwnerTable
|
|
);
|
|
}
|
|
|
|
FText GetAssetName() const
|
|
{
|
|
if(Item.IsValid())
|
|
{
|
|
return FText::FromString(Item->ClothingAsset->GetName());
|
|
}
|
|
|
|
return FText::GetEmpty();
|
|
}
|
|
|
|
void OnCommitAssetName(const FText& InText, ETextCommit::Type CommitInfo)
|
|
{
|
|
if(Item.IsValid())
|
|
{
|
|
if(UClothingAssetCommon* Asset = Item->ClothingAsset.Get())
|
|
{
|
|
FText TrimText = FText::TrimPrecedingAndTrailing(InText);
|
|
|
|
if(Asset->GetName() != TrimText.ToString())
|
|
{
|
|
FName NewName(*TrimText.ToString());
|
|
|
|
// Check for an existing object, and if we find one build a unique name based on the request
|
|
if(UObject* ExistingObject = StaticFindObject(UClothingAssetCommon::StaticClass(), Asset->GetOuter(), *NewName.ToString()))
|
|
{
|
|
NewName = MakeUniqueObjectName(Asset->GetOuter(), UClothingAssetCommon::StaticClass(), FName(*TrimText.ToString()));
|
|
}
|
|
|
|
Asset->Rename(*NewName.ToString(), Asset->GetOuter());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void BindCommands()
|
|
{
|
|
check(!UICommandList.IsValid());
|
|
|
|
UICommandList = MakeShareable(new FUICommandList);
|
|
|
|
const FClothingAssetListCommands& Commands = FClothingAssetListCommands::Get();
|
|
|
|
UICommandList->MapAction(
|
|
FGenericCommands::Get().Delete,
|
|
FExecuteAction::CreateSP(this, &SAssetListRow::DeleteAsset)
|
|
);
|
|
|
|
UICommandList->MapAction(
|
|
Commands.RebuildAssetParams,
|
|
FExecuteAction::CreateSP(this, &SAssetListRow::RebuildLODParameters),
|
|
FCanExecuteAction::CreateSP(this, &SAssetListRow::CanRebuildLODParameters)
|
|
);
|
|
|
|
// Add clothing asset exporters
|
|
ForEachClothingAssetExporter([this, &Commands](UClass* ExportedType)
|
|
{
|
|
if (const TSharedPtr<FUICommandInfo>* const CommandId = Commands.ExportAssets.Find(ExportedType->GetFName()))
|
|
{
|
|
UICommandList->MapAction(
|
|
*CommandId,
|
|
FExecuteAction::CreateSP(this, &SAssetListRow::ExportAsset, ExportedType));
|
|
}
|
|
});
|
|
}
|
|
|
|
virtual FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override
|
|
{
|
|
if(Item.IsValid() && MouseEvent.GetEffectingButton() == EKeys::RightMouseButton)
|
|
{
|
|
const FClothingAssetListCommands& Commands = FClothingAssetListCommands::Get();
|
|
FMenuBuilder Builder(true, UICommandList);
|
|
|
|
Builder.BeginSection(NAME_None, LOCTEXT("AssetActions_SectionName", "Actions"));
|
|
{
|
|
Builder.AddMenuEntry(FGenericCommands::Get().Delete);
|
|
Builder.AddMenuEntry(Commands.RebuildAssetParams);
|
|
|
|
for (const TPair<FName, TSharedPtr<FUICommandInfo>>& CommandId : Commands.ExportAssets)
|
|
{
|
|
Builder.AddMenuEntry(CommandId.Value);
|
|
}
|
|
}
|
|
Builder.EndSection();
|
|
|
|
FWidgetPath Path = MouseEvent.GetEventPath() != nullptr ? *MouseEvent.GetEventPath() : FWidgetPath();
|
|
|
|
FSlateApplication::Get().PushMenu(AsShared(), Path, Builder.MakeWidget(), MouseEvent.GetScreenSpacePosition(), FPopupTransitionEffect::ContextMenu);
|
|
|
|
return FReply::Handled();
|
|
}
|
|
|
|
return STableRow<TSharedPtr<FClothingAssetListItem>>::OnMouseButtonUp(MyGeometry, MouseEvent);
|
|
}
|
|
|
|
private:
|
|
|
|
void DeleteAsset()
|
|
{
|
|
//Lambda use to sync one of the UserSectionData section from one LOD Model
|
|
auto SetSkelMeshSourceSectionUserData = [](FSkeletalMeshLODModel& LODModel, const int32 SectionIndex, const int32 OriginalSectionIndex)
|
|
{
|
|
FSkelMeshSourceSectionUserData& SourceSectionUserData = LODModel.UserSectionsData.FindOrAdd(OriginalSectionIndex);
|
|
SourceSectionUserData.bDisabled = LODModel.Sections[SectionIndex].bDisabled;
|
|
SourceSectionUserData.bCastShadow = LODModel.Sections[SectionIndex].bCastShadow;
|
|
SourceSectionUserData.bVisibleInRayTracing = LODModel.Sections[SectionIndex].bVisibleInRayTracing;
|
|
SourceSectionUserData.bRecomputeTangent = LODModel.Sections[SectionIndex].bRecomputeTangent;
|
|
SourceSectionUserData.GenerateUpToLodIndex = LODModel.Sections[SectionIndex].GenerateUpToLodIndex;
|
|
SourceSectionUserData.CorrespondClothAssetIndex = LODModel.Sections[SectionIndex].CorrespondClothAssetIndex;
|
|
SourceSectionUserData.ClothingData = LODModel.Sections[SectionIndex].ClothingData;
|
|
};
|
|
|
|
if(UClothingAssetCommon* Asset = Item->ClothingAsset.Get())
|
|
{
|
|
if(USkeletalMesh* SkelMesh = Cast<USkeletalMesh>(Asset->GetOuter()))
|
|
{
|
|
FScopedSuspendAlternateSkinWeightPreview ScopedSuspendAlternateSkinnWeightPreview(SkelMesh);
|
|
int32 AssetIndex;
|
|
if(SkelMesh->GetMeshClothingAssets().Find(Asset, AssetIndex))
|
|
{
|
|
// Need to unregister our components so they shut down their current clothing simulation
|
|
FScopedSkeletalMeshPostEditChange ScopedPostEditChange(SkelMesh);
|
|
SkelMesh->PreEditChange(nullptr);
|
|
|
|
Asset->UnbindFromSkeletalMesh(SkelMesh);
|
|
SkelMesh->GetMeshClothingAssets().RemoveAt(AssetIndex);
|
|
|
|
// Need to fix up asset indices on sections.
|
|
if(FSkeletalMeshModel* Model = SkelMesh->GetImportedModel())
|
|
{
|
|
for(FSkeletalMeshLODModel& LodModel : Model->LODModels)
|
|
{
|
|
for (int32 SectionIndex = 0; SectionIndex < LodModel.Sections.Num(); ++SectionIndex)
|
|
{
|
|
FSkelMeshSection& Section = LodModel.Sections[SectionIndex];
|
|
if(Section.CorrespondClothAssetIndex > AssetIndex)
|
|
{
|
|
--Section.CorrespondClothAssetIndex;
|
|
//Keep the user section data (build source data) in sync
|
|
SetSkelMeshSourceSectionUserData(LodModel, SectionIndex, Section.OriginalDataSectionIndex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
OnInvalidateList.ExecuteIfBound();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Using LOD0 of an asset, rebuild the other LOD masks by mapping the LOD0 parameters onto their meshes
|
|
void RebuildLODParameters()
|
|
{
|
|
if(!Item.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if(UClothingAssetCommon* Asset = Item->ClothingAsset.Get())
|
|
{
|
|
const int32 NumLods = Asset->GetNumLods();
|
|
|
|
for(int32 CurrIndex = 0; CurrIndex < NumLods - 1; ++CurrIndex)
|
|
{
|
|
FClothLODDataCommon& SourceLod = Asset->LodData[CurrIndex];
|
|
FClothLODDataCommon& DestLod = Asset->LodData[CurrIndex + 1];
|
|
|
|
DestLod.PointWeightMaps.Reset();
|
|
|
|
for(FPointWeightMap& SourceMask : SourceLod.PointWeightMaps)
|
|
{
|
|
DestLod.PointWeightMaps.AddDefaulted();
|
|
FPointWeightMap& DestMask = DestLod.PointWeightMaps.Last();
|
|
|
|
DestMask.Name = SourceMask.Name;
|
|
DestMask.bEnabled = SourceMask.bEnabled;
|
|
DestMask.CurrentTarget = SourceMask.CurrentTarget;
|
|
|
|
ClothingMeshUtils::FVertexParameterMapper ParameterMapper(
|
|
DestLod.PhysicalMeshData.Vertices,
|
|
DestLod.PhysicalMeshData.Normals,
|
|
SourceLod.PhysicalMeshData.Vertices,
|
|
SourceLod.PhysicalMeshData.Normals,
|
|
SourceLod.PhysicalMeshData.Indices
|
|
);
|
|
|
|
ParameterMapper.Map(SourceMask.Values, DestMask.Values);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ExportAsset(UClass* ExportedType)
|
|
{
|
|
if (!Item.IsValid() || !ExportedType)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (const UClothingAssetCommon* const ClothingAsset = Item->ClothingAsset.Get())
|
|
{
|
|
ExportClothingAsset(ClothingAsset, ExportedType);
|
|
}
|
|
}
|
|
|
|
bool CanRebuildLODParameters() const
|
|
{
|
|
if(!Item.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if(UClothingAssetCommon* Asset = Item->ClothingAsset.Get())
|
|
{
|
|
if(Asset->GetNumLods() > 1)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
TSharedPtr<FClothingAssetListItem> Item;
|
|
TSharedPtr<SInlineEditableTextBlock> EditableText;
|
|
FSimpleDelegate OnInvalidateList;
|
|
TSharedPtr<FUICommandList> UICommandList;
|
|
};
|
|
|
|
class SMaskListRow : public SMultiColumnTableRow<TSharedPtr<FClothingMaskListItem>>
|
|
{
|
|
public:
|
|
|
|
SLATE_BEGIN_ARGS(SMaskListRow)
|
|
{}
|
|
|
|
SLATE_EVENT(FSimpleDelegate, OnInvalidateList)
|
|
|
|
SLATE_END_ARGS()
|
|
|
|
void Construct(const FArguments& InArgs, const TSharedRef<STableViewBase>& InOwnerTable, TSharedPtr<FClothingMaskListItem> InItem, TSharedPtr<SClothAssetSelector> InAssetSelector )
|
|
{
|
|
OnInvalidateList = InArgs._OnInvalidateList;
|
|
Item = InItem;
|
|
AssetSelectorPtr = InAssetSelector;
|
|
|
|
BindCommands();
|
|
|
|
SMultiColumnTableRow<TSharedPtr<FClothingMaskListItem>>::Construct(FSuperRowType::FArguments(), InOwnerTable);
|
|
}
|
|
|
|
static FName Column_Enabled;
|
|
static FName Column_MaskName;
|
|
static FName Column_CurrentTarget;
|
|
|
|
virtual TSharedRef<SWidget> GenerateWidgetForColumn(const FName& InColumnName) override
|
|
{
|
|
if(InColumnName == Column_Enabled)
|
|
{
|
|
return SNew(SBox)
|
|
.Padding(2.f)
|
|
[
|
|
SNew(SCheckBox)
|
|
.IsEnabled(this, &SMaskListRow::IsMaskCheckboxEnabled, Item)
|
|
.IsChecked(this, &SMaskListRow::IsMaskEnabledChecked, Item)
|
|
.OnCheckStateChanged(this, &SMaskListRow::OnMaskEnabledCheckboxChanged, Item)
|
|
.ToolTipText(LOCTEXT("MaskEnableCheckBox_ToolTip", "Sets whether this mask is enabled and can affect final parameters for its target parameter."))
|
|
];
|
|
}
|
|
|
|
if(InColumnName == Column_MaskName)
|
|
{
|
|
return SAssignNew(InlineText, SInlineEditableTextBlock)
|
|
.Text(this, &SMaskListRow::GetMaskName)
|
|
.OnTextCommitted(this, &SMaskListRow::OnCommitMaskName)
|
|
.IsSelected(this, &SMultiColumnTableRow<TSharedPtr<FClothingMaskListItem>>::IsSelectedExclusively);
|
|
}
|
|
|
|
if(InColumnName == Column_CurrentTarget)
|
|
{
|
|
// Retrieve the mask names for the current clothing simulation factory
|
|
const TSubclassOf<class UClothingSimulationFactory> ClothingSimulationFactory = UClothingSimulationFactory::GetDefaultClothingSimulationFactoryClass();
|
|
if (ClothingSimulationFactory.Get() != nullptr)
|
|
{
|
|
const UEnum* const Enum = ClothingSimulationFactory.GetDefaultObject()->GetWeightMapTargetEnum();
|
|
const FPointWeightMap* const Mask = Item->GetMask();
|
|
if(Enum && Mask)
|
|
{
|
|
return SNew(STextBlock).Text(Enum->GetDisplayNameTextByValue((int64)Mask->CurrentTarget));
|
|
}
|
|
}
|
|
}
|
|
|
|
return SNullWidget::NullWidget;
|
|
}
|
|
|
|
FText GetMaskName() const
|
|
{
|
|
if(Item.IsValid())
|
|
{
|
|
if(FPointWeightMap* Mask = Item->GetMask())
|
|
{
|
|
return FText::FromName(Mask->Name);
|
|
}
|
|
}
|
|
|
|
return LOCTEXT("MaskName_Invalid", "Invalid Mask");
|
|
}
|
|
|
|
void OnCommitMaskName(const FText& InText, ETextCommit::Type CommitInfo)
|
|
{
|
|
if(Item.IsValid())
|
|
{
|
|
if(FPointWeightMap* Mask = Item->GetMask())
|
|
{
|
|
FText TrimText = FText::TrimPrecedingAndTrailing(InText);
|
|
Mask->Name = FName(*TrimText.ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
virtual FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override
|
|
{
|
|
// Spawn menu
|
|
if(MouseEvent.GetEffectingButton() == EKeys::RightMouseButton && Item.IsValid())
|
|
{
|
|
if(FPointWeightMap* Mask = Item->GetMask())
|
|
{
|
|
FMenuBuilder Builder(true, UICommandList);
|
|
|
|
FUIAction DeleteAction(FExecuteAction::CreateSP(this, &SMaskListRow::OnDeleteMask));
|
|
|
|
Builder.BeginSection(NAME_None, LOCTEXT("MaskActions_SectionName", "Actions"));
|
|
{
|
|
Builder.AddSubMenu(LOCTEXT("MaskActions_SetTarget", "Set Target"), LOCTEXT("MaskActions_SetTarget_Tooltip", "Choose the target for this mask"), FNewMenuDelegate::CreateSP(this, &SMaskListRow::BuildTargetSubmenu));
|
|
Builder.AddMenuEntry(FGenericCommands::Get().Delete);
|
|
Builder.AddSubMenu(LOCTEXT("MaskActions_CopyFromMask", "Copy From Mask"), LOCTEXT("MaskActions_CopyFromMask_Tooltip", "Replace this mask with values from another mask on this cloth"), FNewMenuDelegate::CreateSP(this, &SMaskListRow::BuildCopyMaskSubmenu));
|
|
Builder.AddSubMenu(LOCTEXT("MaskActions_CopyFromVertexColor", "Copy From Vertex Color"), LOCTEXT("MaskActions_CopyFromVertexColor_Tooltip", "Replace this mask with values from vertex color channel on sim mesh"), FNewMenuDelegate::CreateSP(this, &SMaskListRow::BuildCopyVertexColorSubmenu));
|
|
}
|
|
Builder.EndSection();
|
|
|
|
FWidgetPath Path = MouseEvent.GetEventPath() != nullptr ? *MouseEvent.GetEventPath() : FWidgetPath();
|
|
|
|
FSlateApplication::Get().PushMenu(AsShared(), Path, Builder.MakeWidget(), MouseEvent.GetScreenSpacePosition(), FPopupTransitionEffect::ContextMenu);
|
|
|
|
return FReply::Handled();
|
|
}
|
|
}
|
|
|
|
return SMultiColumnTableRow<TSharedPtr<FClothingMaskListItem>>::OnMouseButtonUp(MyGeometry, MouseEvent);
|
|
}
|
|
|
|
void EditName()
|
|
{
|
|
if(InlineText.IsValid())
|
|
{
|
|
InlineText->EnterEditingMode();
|
|
}
|
|
}
|
|
|
|
private:
|
|
|
|
void BindCommands()
|
|
{
|
|
check(!UICommandList.IsValid());
|
|
|
|
UICommandList = MakeShareable(new FUICommandList);
|
|
|
|
UICommandList->MapAction(
|
|
FGenericCommands::Get().Delete,
|
|
FExecuteAction::CreateSP(this, &SMaskListRow::OnDeleteMask)
|
|
);
|
|
}
|
|
|
|
FClothLODDataCommon* GetCurrentLod() const
|
|
{
|
|
if(Item.IsValid())
|
|
{
|
|
if(UClothingAssetCommon* Asset = Item->ClothingAsset.Get())
|
|
{
|
|
if(Asset->LodData.IsValidIndex(Item->LodIndex))
|
|
{
|
|
return &Asset->LodData[Item->LodIndex];
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void OnDeleteMask()
|
|
{
|
|
USkeletalMesh* CurrentMesh = Item->GetOwningMesh();
|
|
|
|
if(CurrentMesh)
|
|
{
|
|
FScopedTransaction CurrTransaction(LOCTEXT("DeleteMask_Transaction", "Delete clothing parameter mask."));
|
|
Item->ClothingAsset->Modify();
|
|
|
|
if(FClothLODDataCommon* LodData = GetCurrentLod())
|
|
{
|
|
if(LodData->PointWeightMaps.IsValidIndex(Item->MaskIndex))
|
|
{
|
|
LodData->PointWeightMaps.RemoveAt(Item->MaskIndex);
|
|
|
|
// We've removed a mask, so it will need to be applied to the clothing data
|
|
if(Item.IsValid())
|
|
{
|
|
if(UClothingAssetCommon* Asset = Item->ClothingAsset.Get())
|
|
{
|
|
Asset->ApplyParameterMasks();
|
|
}
|
|
}
|
|
|
|
OnInvalidateList.ExecuteIfBound();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void OnSetTarget(int32 InTargetEntryIndex)
|
|
{
|
|
USkeletalMesh* CurrentMesh = Item->GetOwningMesh();
|
|
|
|
if(Item.IsValid() && CurrentMesh)
|
|
{
|
|
FScopedTransaction CurrTransaction(LOCTEXT("SetMaskTarget_Transaction", "Set clothing parameter mask target."));
|
|
Item->ClothingAsset->Modify();
|
|
|
|
if(FPointWeightMap* Mask = Item->GetMask())
|
|
{
|
|
Mask->CurrentTarget = (uint8)InTargetEntryIndex;
|
|
if(Mask->CurrentTarget == (uint8)EWeightMapTargetCommon::None)
|
|
{
|
|
// Make sure to disable this mask if it has no valid target
|
|
Mask->bEnabled = false;
|
|
}
|
|
|
|
OnInvalidateList.ExecuteIfBound();
|
|
}
|
|
}
|
|
}
|
|
|
|
void OnCopyMask(int32 InMaskIndex)
|
|
{
|
|
if (AssetSelectorPtr.IsValid() && Item.IsValid())
|
|
{
|
|
if (USkeletalMesh* CurrentMesh = Item->GetOwningMesh())
|
|
{
|
|
if (FPointWeightMap* Mask = Item->GetMask())
|
|
{
|
|
if (UClothingAssetCommon* const ClothingAsset = AssetSelectorPtr.Pin()->GetSelectedAsset().Get())
|
|
{
|
|
const int32 LOD = AssetSelectorPtr.Pin()->GetSelectedLod();
|
|
if (ClothingAsset->LodData.IsValidIndex(LOD))
|
|
{
|
|
const FClothLODDataCommon& LodData = ClothingAsset->LodData[LOD];
|
|
if (LodData.PointWeightMaps.IsValidIndex(InMaskIndex))
|
|
{
|
|
{
|
|
FScopedTransaction CurrTransaction(LOCTEXT("CopyMask_Transaction", "Copy Mask."));
|
|
Item->ClothingAsset->Modify();
|
|
Mask->Values = LodData.PointWeightMaps[InMaskIndex].Values;
|
|
}
|
|
|
|
const bool bUpdateFixedVertData = (Mask->CurrentTarget == (uint8)EWeightMapTargetCommon::MaxDistance);
|
|
ClothingAsset->ApplyParameterMasks(bUpdateFixedVertData);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void BuildTargetSubmenu(FMenuBuilder& Builder)
|
|
{
|
|
Builder.BeginSection(NAME_None, LOCTEXT("MaskTargets_SectionName", "Targets"));
|
|
{
|
|
// Retrieve the mask names for the current clothing simulation factory
|
|
const TSubclassOf<class UClothingSimulationFactory> ClothingSimulationFactory = UClothingSimulationFactory::GetDefaultClothingSimulationFactoryClass();
|
|
if (ClothingSimulationFactory.Get() != nullptr)
|
|
{
|
|
if (const UEnum* const Enum = ClothingSimulationFactory.GetDefaultObject()->GetWeightMapTargetEnum())
|
|
{
|
|
const int32 NumEntries = Enum->NumEnums();
|
|
|
|
// Iterate to -1 to skip the _MAX entry appended to the end of the enum
|
|
for(int32 Index = 0; Index < NumEntries - 1; ++Index)
|
|
{
|
|
FUIAction EntryAction(FExecuteAction::CreateSP(this, &SMaskListRow::OnSetTarget, (int32)Enum->GetValueByIndex(Index)));
|
|
|
|
FText EntryText = Enum->GetDisplayNameTextByIndex(Index);
|
|
|
|
Builder.AddMenuEntry(EntryText, FText::GetEmpty(), FSlateIcon(), EntryAction);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Builder.EndSection();
|
|
}
|
|
|
|
/** Build sub menu for choosing which vertex color channel to copy to selected mask */
|
|
void BuildCopyMaskSubmenu(FMenuBuilder& Builder)
|
|
{
|
|
if (AssetSelectorPtr.IsValid() && Item.IsValid())
|
|
{
|
|
if (const UClothingAssetCommon* const ClothingAsset = AssetSelectorPtr.Pin()->GetSelectedAsset().Get())
|
|
{
|
|
const int32 LOD = AssetSelectorPtr.Pin()->GetSelectedLod();
|
|
if (ClothingAsset->LodData.IsValidIndex(LOD))
|
|
{
|
|
const FClothLODDataCommon& LodData = ClothingAsset->LodData[LOD];
|
|
Builder.BeginSection(NAME_None, LOCTEXT("Masks_SectionName", "Masks"));
|
|
for (int32 MaskIndex = 0; MaskIndex < LodData.PointWeightMaps.Num(); ++MaskIndex)
|
|
{
|
|
if (MaskIndex == Item->MaskIndex)
|
|
{
|
|
continue;
|
|
}
|
|
FUIAction EntryAction(FExecuteAction::CreateSP(this, &SMaskListRow::OnCopyMask, MaskIndex));
|
|
|
|
Builder.AddMenuEntry(FText::FromName(LodData.PointWeightMaps[MaskIndex].Name), FText::GetEmpty(), FSlateIcon(), EntryAction);
|
|
|
|
}
|
|
Builder.EndSection();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Build sub menu for choosing which vertex color channel to copy to selected mask */
|
|
void BuildCopyVertexColorSubmenu(FMenuBuilder& Builder)
|
|
{
|
|
if (AssetSelectorPtr.IsValid())
|
|
{
|
|
UClothingAssetCommon* ClothingAsset = AssetSelectorPtr.Pin()->GetSelectedAsset().Get();
|
|
int32 LOD = AssetSelectorPtr.Pin()->GetSelectedLod();
|
|
FPointWeightMap* Mask = Item->GetMask();
|
|
|
|
TSharedRef<SWidget> Widget = SNew(SCopyVertexColorSettingsPanel, ClothingAsset, LOD, Mask);
|
|
|
|
Builder.AddWidget(Widget, FText::GetEmpty(), true, false);
|
|
}
|
|
}
|
|
|
|
// Mask enabled checkbox handling
|
|
ECheckBoxState IsMaskEnabledChecked(TSharedPtr<FClothingMaskListItem> InItem) const
|
|
{
|
|
if(InItem.IsValid())
|
|
{
|
|
if(FPointWeightMap* Mask = InItem->GetMask())
|
|
{
|
|
return Mask->bEnabled ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
|
|
}
|
|
}
|
|
|
|
return ECheckBoxState::Unchecked;
|
|
}
|
|
|
|
bool IsMaskCheckboxEnabled(TSharedPtr<FClothingMaskListItem> InItem) const
|
|
{
|
|
if(InItem.IsValid())
|
|
{
|
|
if(FPointWeightMap* Mask = InItem->GetMask())
|
|
{
|
|
return Mask->CurrentTarget != (uint8)EWeightMapTargetCommon::None;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void OnMaskEnabledCheckboxChanged(ECheckBoxState InState, TSharedPtr<FClothingMaskListItem> InItem)
|
|
{
|
|
if(InItem.IsValid())
|
|
{
|
|
if(FPointWeightMap* Mask = InItem->GetMask())
|
|
{
|
|
const bool bNewEnableState = (InState == ECheckBoxState::Checked);
|
|
|
|
if(Mask->bEnabled != bNewEnableState)
|
|
{
|
|
if(bNewEnableState)
|
|
{
|
|
// Disable all other masks that affect this target (there can only be one mask enabled of the same target type at the same time)
|
|
if(UClothingAssetCommon* Asset = InItem->ClothingAsset.Get())
|
|
{
|
|
if(Asset->LodData.IsValidIndex(InItem->LodIndex))
|
|
{
|
|
FClothLODDataCommon& LodData = Asset->LodData[InItem->LodIndex];
|
|
|
|
TArray<FPointWeightMap*> AllTargetMasks;
|
|
LodData.GetParameterMasksForTarget(Mask->CurrentTarget, AllTargetMasks);
|
|
|
|
for(FPointWeightMap* TargetMask : AllTargetMasks)
|
|
{
|
|
if(TargetMask && TargetMask != Mask)
|
|
{
|
|
TargetMask->bEnabled = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set the flag
|
|
Mask->bEnabled = bNewEnableState;
|
|
|
|
if(UClothingAssetCommon* Asset = InItem->ClothingAsset.Get())
|
|
{
|
|
const bool bUpdateFixedVertData = (Mask->CurrentTarget == (uint8)EWeightMapTargetCommon::MaxDistance);
|
|
Asset->ApplyParameterMasks(bUpdateFixedVertData);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
FSimpleDelegate OnInvalidateList;
|
|
TSharedPtr<FClothingMaskListItem> Item;
|
|
TSharedPtr<SInlineEditableTextBlock> InlineText;
|
|
TSharedPtr<FUICommandList> UICommandList;
|
|
TWeakPtr<SClothAssetSelector> AssetSelectorPtr;
|
|
};
|
|
|
|
FName SMaskListRow::Column_Enabled(TEXT("Enabled"));
|
|
FName SMaskListRow::Column_MaskName(TEXT("Name"));
|
|
FName SMaskListRow::Column_CurrentTarget(TEXT("CurrentTarget"));
|
|
|
|
SClothAssetSelector::~SClothAssetSelector()
|
|
{
|
|
if(Mesh)
|
|
{
|
|
Mesh->UnregisterOnClothingChange(MeshClothingChangedHandle);
|
|
}
|
|
|
|
if(GEditor)
|
|
{
|
|
GEditor->UnregisterForUndo(this);
|
|
}
|
|
}
|
|
|
|
void SClothAssetSelector::Construct(const FArguments& InArgs, USkeletalMesh* InMesh)
|
|
{
|
|
FClothingAssetListCommands::Register();
|
|
|
|
Mesh = InMesh;
|
|
OnSelectionChanged = InArgs._OnSelectionChanged;
|
|
|
|
// Register callback for external changes to clothing items
|
|
if(Mesh)
|
|
{
|
|
MeshClothingChangedHandle = Mesh->RegisterOnClothingChange(FSimpleMulticastDelegate::FDelegate::CreateSP(this, &SClothAssetSelector::OnRefresh));
|
|
}
|
|
|
|
if(GEditor)
|
|
{
|
|
GEditor->RegisterForUndo(this);
|
|
}
|
|
|
|
ChildSlot
|
|
[
|
|
SNew(SVerticalBox)
|
|
|
|
+SVerticalBox::Slot()
|
|
.Padding(FMargin(0.0f, 0.0f, 0.0f, 1.0f))
|
|
.AutoHeight()
|
|
[
|
|
SNew(SExpandableArea)
|
|
.BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryTop"))
|
|
.HeaderContent()
|
|
[
|
|
SAssignNew(AssetHeaderBox, SHorizontalBox)
|
|
+SHorizontalBox::Slot()
|
|
.VAlign(VAlign_Center)
|
|
.Padding(10.f, 0.f, 0.f, 0.f)
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(LOCTEXT("AssetExpander_Title", "Clothing Data"))
|
|
.TransformPolicy(ETextTransformPolicy::ToUpper)
|
|
.TextStyle(FAppStyle::Get(), "DetailsView.CategoryTextStyle")
|
|
.Font(FAppStyle::Get().GetFontStyle("PropertyWindow.BoldFont"))
|
|
]
|
|
+ SHorizontalBox::Slot()
|
|
.AutoWidth()
|
|
.VAlign(VAlign_Center)
|
|
.HAlign(HAlign_Right)
|
|
.Padding(0.0f, 0.0f, 4.0f, 0.0f)
|
|
[
|
|
SNew(SPositiveActionButton)
|
|
.Text(LOCTEXT("CopyClothingFromMeshText_TEXT", "Add Clothing"))
|
|
.Icon(FAppStyle::Get().GetBrush("Icons.Plus"))
|
|
.ToolTipText(LOCTEXT("CopyClothingFromMeshText_TOOLTIP", "Copy Clothing from SkeletalMesh"))
|
|
.OnGetMenuContent(this, &SClothAssetSelector::OnGenerateSkeletalMeshPickerForClothCopy)
|
|
]
|
|
|
|
+ SHorizontalBox::Slot()
|
|
.AutoWidth()
|
|
.VAlign(VAlign_Center)
|
|
.HAlign(HAlign_Right)
|
|
[
|
|
SNew(SComboButton)
|
|
.ForegroundColor(FSlateColor::UseStyle())
|
|
.OnGetMenuContent(this, &SClothAssetSelector::OnGetLodMenu)
|
|
.HasDownArrow(true)
|
|
.ButtonContent()
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(this, &SClothAssetSelector::GetLodButtonText)
|
|
]
|
|
]
|
|
]
|
|
.BodyContent()
|
|
[
|
|
SNew(SBox)
|
|
.Padding(FMargin(0.f, 2.0f))
|
|
.MinDesiredHeight(100.0f)
|
|
[
|
|
SAssignNew(AssetList, SAssetList)
|
|
.ListItemsSource(&AssetListItems)
|
|
.OnGenerateRow(this, &SClothAssetSelector::OnGenerateWidgetForClothingAssetItem)
|
|
.OnSelectionChanged(this, &SClothAssetSelector::OnAssetListSelectionChanged)
|
|
.ClearSelectionOnClick(false)
|
|
.SelectionMode(ESelectionMode::Single)
|
|
]
|
|
]
|
|
]
|
|
|
|
+ SVerticalBox::Slot()
|
|
.Padding(FMargin(0.0f, 0.0f, 0.0f, 1.0f))
|
|
.AutoHeight()
|
|
[
|
|
SNew(SExpandableArea)
|
|
.BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryTop"))
|
|
.Padding(FMargin(0.f, 2.0f))
|
|
.HeaderContent()
|
|
[
|
|
SAssignNew(MaskHeaderBox, SHorizontalBox)
|
|
+SHorizontalBox::Slot()
|
|
.VAlign(VAlign_Center)
|
|
.Padding(10.f, 0.f, 0.f, 0.f)
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(LOCTEXT("MaskExpander_Title", "Masks"))
|
|
.TransformPolicy(ETextTransformPolicy::ToUpper)
|
|
.TextStyle(FAppStyle::Get(), "DetailsView.CategoryTextStyle")
|
|
.Font(FAppStyle::Get().GetFontStyle("PropertyWindow.BoldFont"))
|
|
]
|
|
+SHorizontalBox::Slot()
|
|
.AutoWidth()
|
|
.VAlign(VAlign_Center)
|
|
.HAlign(HAlign_Right)
|
|
[
|
|
SAssignNew(NewMaskButton, SButton)
|
|
.ButtonStyle( FAppStyle::Get(), "SimpleButton" )
|
|
.OnClicked(this, &SClothAssetSelector::AddNewMask)
|
|
.IsEnabled(this, &SClothAssetSelector::CanAddNewMask)
|
|
.ToolTipText(LOCTEXT("AddMask_Tooltip", "Add a Mask"))
|
|
.HAlign(HAlign_Center)
|
|
.VAlign(VAlign_Center)
|
|
[
|
|
SNew(SImage)
|
|
.ColorAndOpacity(FSlateColor::UseForeground())
|
|
.Image(FAppStyle::Get().GetBrush("Icons.PlusCircle"))
|
|
]
|
|
]
|
|
]
|
|
.BodyContent()
|
|
[
|
|
SNew(SBox)
|
|
.MinDesiredHeight(100.0f)
|
|
[
|
|
SAssignNew(MaskList, SMaskList)
|
|
.ListItemsSource(&MaskListItems)
|
|
.OnGenerateRow(this, &SClothAssetSelector::OnGenerateWidgetForMaskItem)
|
|
.OnSelectionChanged(this, &SClothAssetSelector::OnMaskSelectionChanged)
|
|
.ClearSelectionOnClick(false)
|
|
.SelectionMode(ESelectionMode::Single)
|
|
.HeaderRow
|
|
(
|
|
SNew(SHeaderRow)
|
|
|
|
+ SHeaderRow::Column(SMaskListRow::Column_Enabled)
|
|
.FixedWidth(40)
|
|
.HAlignCell(HAlign_Right) .DefaultLabel(FText::GetEmpty())
|
|
|
|
+ SHeaderRow::Column(SMaskListRow::Column_MaskName)
|
|
.FillWidth(0.5f)
|
|
.DefaultLabel(LOCTEXT("MaskListHeader_Name", "Name"))
|
|
|
|
+ SHeaderRow::Column(SMaskListRow::Column_CurrentTarget)
|
|
.FillWidth(0.3f)
|
|
.DefaultLabel(LOCTEXT("MaskListHeader_Target", "Target"))
|
|
)
|
|
]
|
|
]
|
|
]
|
|
+ SVerticalBox::Slot() // Mesh to mesh skinning
|
|
.AutoHeight()
|
|
.Padding(FMargin(0.0f, 0.0f, 0.0f, 1.0f))
|
|
[
|
|
SNew(SExpandableArea)
|
|
.BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryTop"))
|
|
.Padding(FMargin(0.f, 2.0f))
|
|
.HeaderContent()
|
|
[
|
|
SNew(SHorizontalBox)
|
|
+ SHorizontalBox::Slot()
|
|
.VAlign(VAlign_Center)
|
|
.Padding(10, 8, 0, 8)
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(LOCTEXT("MeshSkinning_Title", "Mesh Skinning"))
|
|
.TransformPolicy(ETextTransformPolicy::ToUpper)
|
|
.TextStyle(FAppStyle::Get(), "DetailsView.CategoryTextStyle")
|
|
.Font(FAppStyle::Get().GetFontStyle("PropertyWindow.BoldFont"))
|
|
]
|
|
]
|
|
.BodyContent()
|
|
[
|
|
// TODO: Replace this with a table view or something more suitable. UETOOL-2341
|
|
|
|
SNew(SSplitter)
|
|
.Orientation(EOrientation::Orient_Horizontal)
|
|
.PhysicalSplitterHandleSize(1.0)
|
|
|
|
+ SSplitter::Slot()
|
|
.Value(.3f)
|
|
[
|
|
SNew(SVerticalBox)
|
|
|
|
+ SVerticalBox::Slot()
|
|
.VAlign(VAlign_Center)
|
|
.Padding(16.0f, 2.0f, 0.0f, 2.0f)
|
|
[
|
|
SNew(STextBlock)
|
|
.Font(FAppStyle::Get().GetFontStyle("PropertyWindow.NormalFont"))
|
|
.Text(LOCTEXT("MultipleInfluences", "Use Multiple Influences"))
|
|
]
|
|
|
|
+ SVerticalBox::Slot()
|
|
.VAlign(VAlign_Center)
|
|
.Padding(16.0f, 2.0f, 0.0f, 2.0f)
|
|
[
|
|
SNew(STextBlock)
|
|
.Font(FAppStyle::Get().GetFontStyle("PropertyWindow.NormalFont"))
|
|
.Text(LOCTEXT("CurrentRadius", "Kernel Radius"))
|
|
]
|
|
|
|
+ SVerticalBox::Slot()
|
|
.Padding(0.0f, 2.0f, 0.0f, 2.0f)
|
|
.AutoHeight()
|
|
[
|
|
SNew(STextBlock)
|
|
.Font(IDetailLayoutBuilder::GetDetailFontBold())
|
|
.Text(LOCTEXT("SmoothTransition", "Smooth Transition From Skin to Cloth"))
|
|
.ShadowOffset(FVector2D(1, 1))
|
|
]
|
|
]
|
|
|
|
+ SSplitter::Slot()
|
|
[
|
|
SNew(SVerticalBox)
|
|
|
|
+ SVerticalBox::Slot()
|
|
.VAlign(VAlign_Center)
|
|
.Padding(12.0f, 4.0f, 0.0f, 4.0f)
|
|
[
|
|
SNew(SCheckBox)
|
|
.IsChecked(this, &SClothAssetSelector::GetCurrentUseMultipleInfluences)
|
|
.OnCheckStateChanged(this, &SClothAssetSelector::OnCurrentUseMultipleInfluencesChanged)
|
|
.IsEnabled(this, &SClothAssetSelector::IsValidClothLodSelected)
|
|
]
|
|
|
|
+ SVerticalBox::Slot()
|
|
.VAlign(VAlign_Center)
|
|
.Padding(12.0f, 4.0f, 0.0f, 4.0f)
|
|
[
|
|
SNew(SNumericEntryBox<float>)
|
|
.AllowSpin(true)
|
|
.MinSliderValue(0)
|
|
.MinValue(0)
|
|
.MaxSliderValue(TOptional<float>(1000.0f))
|
|
.IsEnabled(this, &SClothAssetSelector::CurrentKernelRadiusIsEnabled)
|
|
.UndeterminedString(FText::FromString("????"))
|
|
.Value(this, &SClothAssetSelector::GetCurrentKernelRadius)
|
|
.OnValueCommitted(this, &SClothAssetSelector::OnCurrentKernelRadiusCommitted)
|
|
.OnValueChanged(this, &SClothAssetSelector::OnCurrentKernelRadiusChanged)
|
|
.LabelPadding(0)
|
|
]
|
|
|
|
+ SVerticalBox::Slot()
|
|
.Padding(0.0f, 2.0f, 0.0f, 2.0f)
|
|
.AutoHeight()
|
|
[
|
|
SNew(SCheckBox)
|
|
.IsChecked(this, &SClothAssetSelector::GetCurrentSmoothTransition)
|
|
.OnCheckStateChanged(this, &SClothAssetSelector::OnCurrentSmoothTransitionChanged)
|
|
.IsEnabled(this, &SClothAssetSelector::IsValidClothLodSelected)
|
|
]
|
|
]
|
|
|
|
]
|
|
]
|
|
];
|
|
|
|
RefreshAssetList();
|
|
RefreshMaskList();
|
|
}
|
|
|
|
|
|
TOptional<float> SClothAssetSelector::GetCurrentKernelRadius() const
|
|
{
|
|
UClothingAssetCommon* Asset = SelectedAsset.Get();
|
|
if (Asset && Asset->IsValidLod(SelectedLod))
|
|
{
|
|
const FClothLODDataCommon& LodData = Asset->LodData[SelectedLod];
|
|
return LodData.SkinningKernelRadius;
|
|
}
|
|
|
|
return TOptional<float>();
|
|
}
|
|
|
|
void SClothAssetSelector::OnCurrentKernelRadiusChanged(float InValue)
|
|
{
|
|
UClothingAssetCommon* Asset = SelectedAsset.Get();
|
|
if (Asset && Asset->IsValidLod(SelectedLod))
|
|
{
|
|
FClothLODDataCommon& LodData = Asset->LodData[SelectedLod];
|
|
LodData.SkinningKernelRadius = InValue;
|
|
}
|
|
}
|
|
|
|
void SClothAssetSelector::OnCurrentKernelRadiusCommitted(float InValue, ETextCommit::Type CommitType)
|
|
{
|
|
UClothingAssetCommon* Asset = SelectedAsset.Get();
|
|
if (Asset && Asset->IsValidLod(SelectedLod))
|
|
{
|
|
FClothLODDataCommon& LodData = Asset->LodData[SelectedLod];
|
|
LodData.SkinningKernelRadius = InValue;
|
|
|
|
// Recompute weights
|
|
if (USkeletalMesh* SkeletalMesh = Cast<USkeletalMesh>(Asset->GetOuter()))
|
|
{
|
|
FScopedSkeletalMeshPostEditChange ScopedSkeletalMeshPostEditChange(SkeletalMesh);
|
|
SkeletalMesh->InvalidateDeriveDataCacheGUID();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool SClothAssetSelector::CurrentKernelRadiusIsEnabled() const
|
|
{
|
|
return (this->GetCurrentUseMultipleInfluences() == ECheckBoxState::Checked);
|
|
}
|
|
|
|
ECheckBoxState SClothAssetSelector::GetCurrentUseMultipleInfluences() const
|
|
{
|
|
UClothingAssetCommon* Asset = SelectedAsset.Get();
|
|
if (Asset && Asset->IsValidLod(SelectedLod))
|
|
{
|
|
const FClothLODDataCommon& LodData = Asset->LodData[SelectedLod];
|
|
return LodData.bUseMultipleInfluences ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
|
|
}
|
|
|
|
return ECheckBoxState::Undetermined;
|
|
}
|
|
|
|
void SClothAssetSelector::OnCurrentUseMultipleInfluencesChanged(ECheckBoxState InValue)
|
|
{
|
|
if (InValue == ECheckBoxState::Undetermined)
|
|
{
|
|
return;
|
|
}
|
|
|
|
UClothingAssetCommon* Asset = SelectedAsset.Get();
|
|
if (Asset && Asset->IsValidLod(SelectedLod))
|
|
{
|
|
FClothLODDataCommon& LodData = Asset->LodData[SelectedLod];
|
|
LodData.bUseMultipleInfluences = (InValue == ECheckBoxState::Checked);
|
|
|
|
// Recompute weights
|
|
if (USkeletalMesh* SkeletalMesh = Cast<USkeletalMesh>(Asset->GetOuter()))
|
|
{
|
|
FScopedSkeletalMeshPostEditChange ScopedSkeletalMeshPostEditChange(SkeletalMesh);
|
|
SkeletalMesh->InvalidateDeriveDataCacheGUID();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
ECheckBoxState SClothAssetSelector::GetCurrentSmoothTransition() const
|
|
{
|
|
UClothingAssetCommon* Asset = SelectedAsset.Get();
|
|
if (Asset && Asset->IsValidLod(SelectedLod))
|
|
{
|
|
const FClothLODDataCommon& LodData = Asset->LodData[SelectedLod];
|
|
return LodData.bSmoothTransition ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
|
|
}
|
|
|
|
return ECheckBoxState::Undetermined;
|
|
}
|
|
|
|
|
|
void SClothAssetSelector::OnCurrentSmoothTransitionChanged(ECheckBoxState InValue)
|
|
{
|
|
if (InValue == ECheckBoxState::Undetermined)
|
|
{
|
|
return;
|
|
}
|
|
|
|
UClothingAssetCommon* Asset = SelectedAsset.Get();
|
|
if (Asset && Asset->IsValidLod(SelectedLod))
|
|
{
|
|
FClothLODDataCommon& LodData = Asset->LodData[SelectedLod];
|
|
LodData.bSmoothTransition = (InValue == ECheckBoxState::Checked);
|
|
|
|
// Recompute weights
|
|
if (USkeletalMesh* SkeletalMesh = Cast<USkeletalMesh>(Asset->GetOuter()))
|
|
{
|
|
FScopedSkeletalMeshPostEditChange ScopedSkeletalMeshPostEditChange(SkeletalMesh);
|
|
SkeletalMesh->InvalidateDeriveDataCacheGUID();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool SClothAssetSelector::IsValidClothLodSelected() const
|
|
{
|
|
UClothingAssetCommon* Asset = SelectedAsset.Get();
|
|
return (Asset && Asset->IsValidLod(SelectedLod));
|
|
}
|
|
|
|
|
|
TWeakObjectPtr<UClothingAssetCommon> SClothAssetSelector::GetSelectedAsset() const
|
|
{
|
|
return SelectedAsset;
|
|
|
|
}
|
|
|
|
int32 SClothAssetSelector::GetSelectedLod() const
|
|
{
|
|
return SelectedLod;
|
|
}
|
|
|
|
int32 SClothAssetSelector::GetSelectedMask() const
|
|
{
|
|
return SelectedMask;
|
|
}
|
|
|
|
void SClothAssetSelector::PostUndo(bool bSuccess)
|
|
{
|
|
OnRefresh();
|
|
}
|
|
|
|
void SClothAssetSelector::OnCopyClothingAssetSelected(const FAssetData& AssetData)
|
|
{
|
|
USkeletalMesh* SourceSkelMesh = Cast<USkeletalMesh>(AssetData.GetAsset());
|
|
|
|
if (Mesh && SourceSkelMesh && Mesh != SourceSkelMesh)
|
|
{
|
|
FScopedTransaction Transaction(LOCTEXT("CopiedClothingAssetsFromSkelMesh", "Copied clothing assets from another SkelMesh"));
|
|
Mesh->Modify();
|
|
FClothingSystemEditorInterfaceModule& ClothingEditorModule = FModuleManager::LoadModuleChecked<FClothingSystemEditorInterfaceModule>("ClothingSystemEditorInterface");
|
|
UClothingAssetFactoryBase* AssetFactory = ClothingEditorModule.GetClothingAssetFactory();
|
|
|
|
for (UClothingAssetBase* ClothingAsset : SourceSkelMesh->GetMeshClothingAssets())
|
|
{
|
|
UClothingAssetCommon* NewAsset = Cast<UClothingAssetCommon>(AssetFactory->CreateFromExistingCloth(Mesh, SourceSkelMesh, ClothingAsset));
|
|
Mesh->AddClothingAsset(NewAsset);
|
|
}
|
|
OnRefresh();
|
|
}
|
|
FSlateApplication::Get().DismissAllMenus();
|
|
}
|
|
|
|
TSharedRef<SWidget> SClothAssetSelector::OnGenerateSkeletalMeshPickerForClothCopy()
|
|
{
|
|
FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser"));
|
|
|
|
FAssetPickerConfig AssetPickerConfig;
|
|
AssetPickerConfig.Filter.ClassPaths.Add(USkeletalMesh::StaticClass()->GetClassPathName());
|
|
AssetPickerConfig.OnAssetSelected = FOnAssetSelected::CreateSP(this, &SClothAssetSelector::OnCopyClothingAssetSelected);
|
|
AssetPickerConfig.bAllowNullSelection = true;
|
|
AssetPickerConfig.InitialAssetViewType = EAssetViewType::List;
|
|
AssetPickerConfig.bFocusSearchBoxWhenOpened = true;
|
|
AssetPickerConfig.bShowBottomToolbar = false;
|
|
AssetPickerConfig.SelectionMode = ESelectionMode::Single;
|
|
|
|
return SNew(SBox)
|
|
.WidthOverride(300)
|
|
.HeightOverride(400)
|
|
[
|
|
ContentBrowserModule.Get().CreateAssetPicker(AssetPickerConfig)
|
|
];
|
|
}
|
|
|
|
EVisibility SClothAssetSelector::GetAssetHeaderButtonTextVisibility() const
|
|
{
|
|
bool bShow = AssetHeaderBox.IsValid() && AssetHeaderBox->IsHovered();
|
|
|
|
return bShow ? EVisibility::HitTestInvisible : EVisibility::Collapsed;
|
|
}
|
|
|
|
EVisibility SClothAssetSelector::GetMaskHeaderButtonTextVisibility() const
|
|
{
|
|
bool bShow = MaskHeaderBox.IsValid() && MaskHeaderBox->IsHovered();
|
|
|
|
return bShow ? EVisibility::HitTestInvisible : EVisibility::Collapsed;
|
|
}
|
|
|
|
TSharedRef<SWidget> SClothAssetSelector::OnGetLodMenu()
|
|
{
|
|
FMenuBuilder Builder(true, nullptr);
|
|
|
|
int32 NumLods = 0;
|
|
|
|
if(UClothingAssetCommon* CurrAsset = SelectedAsset.Get())
|
|
{
|
|
NumLods = CurrAsset->GetNumLods();
|
|
}
|
|
|
|
if(NumLods == 0)
|
|
{
|
|
Builder.AddMenuEntry(LOCTEXT("LodMenu_NoLods", "Select an asset..."), FText::GetEmpty(), FSlateIcon(), FUIAction());
|
|
}
|
|
else
|
|
{
|
|
for(int32 LodIdx = 0; LodIdx < NumLods; ++LodIdx)
|
|
{
|
|
FText ItemText = FText::Format(LOCTEXT("LodMenuItem", "LOD{0}"), FText::AsNumber(LodIdx));
|
|
FText ToolTipText = FText::Format(LOCTEXT("LodMenuItemToolTip", "Select LOD{0}"), FText::AsNumber(LodIdx));
|
|
|
|
FUIAction Action;
|
|
Action.ExecuteAction.BindSP(this, &SClothAssetSelector::OnClothingLodSelected, LodIdx);
|
|
|
|
Builder.AddMenuEntry(ItemText, ToolTipText, FSlateIcon(), Action);
|
|
}
|
|
}
|
|
|
|
return Builder.MakeWidget();
|
|
}
|
|
|
|
FText SClothAssetSelector::GetLodButtonText() const
|
|
{
|
|
if(SelectedLod == INDEX_NONE)
|
|
{
|
|
return LOCTEXT("LodButtonGenTextEmpty", "LOD");
|
|
}
|
|
|
|
return FText::Format(LOCTEXT("LodButtonGenText", "LOD{0}"), FText::AsNumber(SelectedLod));
|
|
}
|
|
|
|
TSharedRef<ITableRow> SClothAssetSelector::OnGenerateWidgetForClothingAssetItem(TSharedPtr<FClothingAssetListItem> InItem, const TSharedRef<STableViewBase>& OwnerTable)
|
|
{
|
|
if(UClothingAssetCommon* Asset = InItem->ClothingAsset.Get())
|
|
{
|
|
return SNew(SAssetListRow, OwnerTable, InItem)
|
|
.OnInvalidateList(this, &SClothAssetSelector::OnRefresh);
|
|
}
|
|
|
|
return SNew(STableRow<TSharedPtr<FClothingAssetListItem>>, OwnerTable)
|
|
.Content()
|
|
[
|
|
SNew(STextBlock).Text(FText::FromString(TEXT("No Assets Available")))
|
|
];
|
|
}
|
|
|
|
void SClothAssetSelector::OnAssetListSelectionChanged(TSharedPtr<FClothingAssetListItem> InSelectedItem, ESelectInfo::Type InSelectInfo)
|
|
{
|
|
if(InSelectedItem.IsValid() && InSelectInfo != ESelectInfo::Direct)
|
|
{
|
|
SetSelectedAsset(InSelectedItem->ClothingAsset);
|
|
}
|
|
}
|
|
|
|
TSharedRef<ITableRow> SClothAssetSelector::OnGenerateWidgetForMaskItem(TSharedPtr<FClothingMaskListItem> InItem, const TSharedRef<STableViewBase>& OwnerTable)
|
|
{
|
|
if(FPointWeightMap* Mask = InItem->GetMask())
|
|
{
|
|
return SNew(SMaskListRow, OwnerTable, InItem, SharedThis(this))
|
|
.OnInvalidateList(this, &SClothAssetSelector::OnRefresh);
|
|
}
|
|
|
|
return SNew(STableRow<TSharedPtr<FClothingMaskListItem>>, OwnerTable)
|
|
[
|
|
SNew(STextBlock).Text(LOCTEXT("MaskList_NoMasks", "No masks available"))
|
|
];
|
|
}
|
|
|
|
void SClothAssetSelector::OnMaskSelectionChanged(TSharedPtr<FClothingMaskListItem> InSelectedItem, ESelectInfo::Type InSelectInfo)
|
|
{
|
|
if(InSelectedItem.IsValid()
|
|
&& InSelectedItem->ClothingAsset.IsValid()
|
|
&& InSelectedItem->LodIndex != INDEX_NONE
|
|
&& InSelectedItem->MaskIndex != INDEX_NONE
|
|
&& InSelectedItem->MaskIndex != SelectedMask
|
|
&& InSelectInfo != ESelectInfo::Direct)
|
|
{
|
|
SetSelectedMask(InSelectedItem->MaskIndex);
|
|
}
|
|
}
|
|
|
|
FReply SClothAssetSelector::AddNewMask()
|
|
{
|
|
if(UClothingAssetCommon* Asset = SelectedAsset.Get())
|
|
{
|
|
if(Asset->LodData.IsValidIndex(SelectedLod))
|
|
{
|
|
FClothLODDataCommon& LodData = Asset->LodData[SelectedLod];
|
|
const int32 NumRequiredValues = LodData.PhysicalMeshData.Vertices.Num();
|
|
|
|
LodData.PointWeightMaps.AddDefaulted();
|
|
|
|
FPointWeightMap& NewMask = LodData.PointWeightMaps.Last();
|
|
|
|
NewMask.Name = TEXT("New Mask");
|
|
NewMask.CurrentTarget = (uint8)EWeightMapTargetCommon::None;
|
|
NewMask.Values.AddZeroed(NumRequiredValues);
|
|
|
|
OnRefresh();
|
|
}
|
|
}
|
|
|
|
return FReply::Handled();
|
|
}
|
|
|
|
bool SClothAssetSelector::CanAddNewMask() const
|
|
{
|
|
return SelectedAsset.Get() != nullptr;
|
|
}
|
|
|
|
void SClothAssetSelector::OnRefresh()
|
|
{
|
|
RefreshAssetList();
|
|
RefreshMaskList();
|
|
}
|
|
|
|
void SClothAssetSelector::RefreshAssetList()
|
|
{
|
|
UClothingAssetCommon* CurrSelectedAsset = nullptr;
|
|
int32 SelectedItem = INDEX_NONE;
|
|
|
|
if(AssetList.IsValid())
|
|
{
|
|
TArray<TSharedPtr<FClothingAssetListItem>> SelectedItems;
|
|
AssetList->GetSelectedItems(SelectedItems);
|
|
|
|
if(SelectedItems.Num() > 0)
|
|
{
|
|
CurrSelectedAsset = SelectedItems[0]->ClothingAsset.Get();
|
|
}
|
|
}
|
|
|
|
AssetListItems.Empty();
|
|
|
|
for(UClothingAssetBase* Asset : Mesh->GetMeshClothingAssets())
|
|
{
|
|
UClothingAssetCommon* ConcreteAsset = Cast<UClothingAssetCommon>(Asset);
|
|
|
|
TSharedPtr<FClothingAssetListItem> Entry = MakeShareable(new FClothingAssetListItem);
|
|
|
|
Entry->ClothingAsset = ConcreteAsset;
|
|
|
|
AssetListItems.Add(Entry);
|
|
|
|
if(ConcreteAsset == CurrSelectedAsset)
|
|
{
|
|
SelectedItem = AssetListItems.Num() - 1;
|
|
}
|
|
}
|
|
|
|
if(AssetListItems.Num() == 0)
|
|
{
|
|
// Add an invalid entry so we can show a "none" line
|
|
AssetListItems.Add(MakeShareable(new FClothingAssetListItem));
|
|
}
|
|
|
|
if(AssetList.IsValid())
|
|
{
|
|
AssetList->RequestListRefresh();
|
|
|
|
if(SelectedItem != INDEX_NONE)
|
|
{
|
|
AssetList->SetSelection(AssetListItems[SelectedItem]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SClothAssetSelector::RefreshMaskList()
|
|
{
|
|
int32 CurrSelectedLod = INDEX_NONE;
|
|
int32 CurrSelectedMask = INDEX_NONE;
|
|
int32 SelectedItem = INDEX_NONE;
|
|
|
|
if(MaskList.IsValid())
|
|
{
|
|
TArray<TSharedPtr<FClothingMaskListItem>> SelectedItems;
|
|
|
|
MaskList->GetSelectedItems(SelectedItems);
|
|
|
|
if(SelectedItems.Num() > 0)
|
|
{
|
|
CurrSelectedLod = SelectedItems[0]->LodIndex;
|
|
CurrSelectedMask = SelectedItems[0]->MaskIndex;
|
|
}
|
|
}
|
|
|
|
MaskListItems.Empty();
|
|
|
|
UClothingAssetCommon* Asset = SelectedAsset.Get();
|
|
if(Asset && Asset->IsValidLod(SelectedLod))
|
|
{
|
|
const FClothLODDataCommon& LodData = Asset->LodData[SelectedLod];
|
|
const int32 NumMasks = LodData.PointWeightMaps.Num();
|
|
|
|
for(int32 Index = 0; Index < NumMasks; ++Index)
|
|
{
|
|
TSharedPtr<FClothingMaskListItem> NewItem = MakeShareable(new FClothingMaskListItem);
|
|
NewItem->ClothingAsset = SelectedAsset;
|
|
NewItem->LodIndex = SelectedLod;
|
|
NewItem->MaskIndex = Index;
|
|
MaskListItems.Add(NewItem);
|
|
|
|
if(NewItem->LodIndex == CurrSelectedLod &&
|
|
NewItem->MaskIndex == CurrSelectedMask)
|
|
{
|
|
SelectedItem = MaskListItems.Num() - 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(MaskListItems.Num() == 0)
|
|
{
|
|
// Add invalid entry so we can make a widget for "none"
|
|
TSharedPtr<FClothingMaskListItem> NewItem = MakeShareable(new FClothingMaskListItem);
|
|
MaskListItems.Add(NewItem);
|
|
}
|
|
|
|
if(MaskList.IsValid())
|
|
{
|
|
MaskList->RequestListRefresh();
|
|
|
|
if(SelectedItem != INDEX_NONE)
|
|
{
|
|
MaskList->SetSelection(MaskListItems[SelectedItem]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SClothAssetSelector::OnClothingLodSelected(int32 InNewLod)
|
|
{
|
|
if(InNewLod == INDEX_NONE)
|
|
{
|
|
SetSelectedLod(InNewLod);
|
|
//ClothPainterSettings->OnAssetSelectionChanged.Broadcast(SelectedAsset.Get(), SelectedLod, SelectedMask);
|
|
}
|
|
|
|
if(SelectedAsset.IsValid())
|
|
{
|
|
SetSelectedLod(InNewLod);
|
|
|
|
int32 NewMaskSelection = INDEX_NONE;
|
|
if(SelectedAsset->LodData.IsValidIndex(SelectedLod))
|
|
{
|
|
const FClothLODDataCommon& LodData = SelectedAsset->LodData[SelectedLod];
|
|
|
|
if(LodData.PointWeightMaps.Num() > 0)
|
|
{
|
|
NewMaskSelection = 0;
|
|
}
|
|
}
|
|
|
|
SetSelectedMask(NewMaskSelection);
|
|
}
|
|
}
|
|
|
|
void SClothAssetSelector::SetSelectedAsset(TWeakObjectPtr<UClothingAssetCommon> InSelectedAsset)
|
|
{
|
|
SelectedAsset = InSelectedAsset;
|
|
|
|
RefreshMaskList();
|
|
|
|
if(UClothingAssetCommon* NewAsset = SelectedAsset.Get())
|
|
{
|
|
if(NewAsset->GetNumLods() > 0)
|
|
{
|
|
SetSelectedLod(0);
|
|
|
|
const FClothLODDataCommon& LodData = NewAsset->LodData[SelectedLod];
|
|
if(LodData.PointWeightMaps.Num() > 0)
|
|
{
|
|
SetSelectedMask(0);
|
|
}
|
|
else
|
|
{
|
|
SetSelectedMask(INDEX_NONE);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SetSelectedLod(INDEX_NONE);
|
|
SetSelectedMask(INDEX_NONE);
|
|
}
|
|
|
|
OnSelectionChanged.ExecuteIfBound(SelectedAsset, SelectedLod, SelectedMask);
|
|
}
|
|
}
|
|
|
|
void SClothAssetSelector::SetSelectedLod(int32 InLodIndex, bool bRefreshMasks /*= true*/)
|
|
{
|
|
if(InLodIndex != SelectedLod)
|
|
{
|
|
SelectedLod = InLodIndex;
|
|
|
|
if(MaskList.IsValid() && bRefreshMasks)
|
|
{
|
|
// New LOD means new set of masks, refresh that list
|
|
RefreshMaskList();
|
|
}
|
|
|
|
OnSelectionChanged.ExecuteIfBound(SelectedAsset, SelectedLod, SelectedMask);
|
|
}
|
|
}
|
|
|
|
void SClothAssetSelector::SetSelectedMask(int32 InMaskIndex)
|
|
{
|
|
SelectedMask = InMaskIndex;
|
|
|
|
if(MaskList.IsValid())
|
|
{
|
|
TSharedPtr<FClothingMaskListItem>* FoundItem = nullptr;
|
|
if(InMaskIndex != INDEX_NONE)
|
|
{
|
|
// Find the item so we can select it in the list
|
|
FoundItem = MaskListItems.FindByPredicate([&](const TSharedPtr<FClothingMaskListItem>& InItem)
|
|
{
|
|
return InItem->MaskIndex == InMaskIndex;
|
|
});
|
|
}
|
|
|
|
if(FoundItem)
|
|
{
|
|
MaskList->SetSelection(*FoundItem);
|
|
}
|
|
else
|
|
{
|
|
MaskList->ClearSelection();
|
|
}
|
|
}
|
|
|
|
OnSelectionChanged.ExecuteIfBound(SelectedAsset, SelectedLod, SelectedMask);
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|