Files
UnrealEngine/Engine/Plugins/Runtime/HairStrands/Source/HairStrandsEditor/Private/GroomActions.cpp
2025-05-18 13:04:45 +08:00

583 lines
23 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "GroomActions.h"
#include "GroomAsset.h"
#include "EditorFramework/AssetImportData.h"
#include "HairStrandsCore.h"
#include "GeometryCache.h"
#include "GroomAssetImportData.h"
#include "GroomBuilder.h"
#include "GroomDeformerBuilder.h"
#include "GroomImportOptions.h"
#include "GroomImportOptionsWindow.h"
#include "GroomCustomAssetEditorToolkit.h"
#include "GroomCreateBindingOptions.h"
#include "GroomCreateBindingOptionsWindow.h"
#include "GroomCreateFollicleMaskOptions.h"
#include "GroomCreateFollicleMaskOptionsWindow.h"
#include "GroomCreateStrandsTexturesOptions.h"
#include "GroomCreateStrandsTexturesOptionsWindow.h"
#include "AssetCompilingManager.h"
#include "HairStrandsImporter.h"
#include "HairStrandsTranslator.h"
#include "ToolMenuSection.h"
#include "Misc/ScopedSlowTask.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "GroomBindingBuilder.h"
#include "GroomTextureBuilder.h"
#include "GroomBindingAsset.h"
#include "ContentBrowserModule.h"
#include "IContentBrowserSingleton.h"
#include "Subsystems/AssetEditorSubsystem.h"
#include "ObjectTools.h"
#include "Widgets/Notifications/SNotificationList.h"
#include "Framework/Notifications/NotificationManager.h"
#include "Materials/Material.h"
#include "AssetToolsModule.h"
#include "ContentBrowserMenuContexts.h"
#include "ToolMenus.h"
#include "Dataflow/DataflowEditor.h"
#include "ThumbnailRendering/SceneThumbnailInfo.h"
#define LOCTEXT_NAMESPACE "AssetTypeActions"
FLinearColor UAssetDefinition_GroomAsset::GetAssetColor() const
{
return FColor::White;
}
UThumbnailInfo* UAssetDefinition_GroomAsset::LoadThumbnailInfo(const FAssetData& InAssetData) const
{
return UE::Editor::FindOrCreateThumbnailInfo(InAssetData.GetAsset(), USceneThumbnailInfo::StaticClass());
}
EAssetCommandResult UAssetDefinition_GroomAsset::OpenAssets(const FAssetOpenArgs& OpenArgs) const
{
// #ueent_todo: Will need a custom editor at some point, for now just use the Properties editor
for (UGroomAsset* GroomAsset : OpenArgs.LoadObjects<UGroomAsset>())
{
if (GroomAsset != nullptr)
{
if(!GroomAsset->GetDataflowSettings().GetDataflowAsset())
{
TSharedRef<FGroomCustomAssetEditorToolkit> NewCustomAssetEditor(new FGroomCustomAssetEditorToolkit());
NewCustomAssetEditor->InitCustomAssetEditor(OpenArgs.GetToolkitMode(), OpenArgs.ToolkitHost, GroomAsset);
}
else
{
UAssetEditorSubsystem* const AssetEditorSubsystem = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
UDataflowEditor* const AssetEditor = NewObject<UDataflowEditor>(AssetEditorSubsystem, NAME_None, RF_Transient);
AssetEditor->RegisterToolCategories({"General"});
const TSubclassOf<AActor> ActorClass = StaticLoadClass(AActor::StaticClass(), nullptr,
TEXT("/HairStrands/BP_PreviewGroom.BP_PreviewGroom_C"), nullptr, LOAD_None, nullptr);
UMaterial* HairMaterial = Cast<UMaterial>(StaticLoadObject(UMaterial::StaticClass(),nullptr,
TEXT("/HairStrands/Materials/HairDataflowMaterial.HairDataflowMaterial"), nullptr, LOAD_None, nullptr));
GroomAsset->GetDataflowSettings().GetDataflowAsset()->Material = HairMaterial;
AssetEditor->Initialize({ GroomAsset}, ActorClass);
}
}
}
return EAssetCommandResult::Handled;
}
void UAssetDefinition_GroomAsset::GetResolvedSourceFilePaths(const TArray<UObject*>& TypeAssets, TArray<FString>& OutSourceFilePaths) const
{
for (UObject* Asset : TypeAssets)
{
const UGroomAsset* GroomAsset = CastChecked<UGroomAsset>(Asset);
if (GroomAsset && GroomAsset->AssetImportData)
{
GroomAsset->AssetImportData->ExtractFilenames(OutSourceFilePaths);
}
}
}
namespace MenuExtension_GroomAsset
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Groom build/rebuild
bool CanRebuild(const FToolMenuContext& InContext)
{
const UContentBrowserAssetContextMenuContext* CBContext = UContentBrowserAssetContextMenuContext::FindContextWithAssets(InContext);
for (UGroomAsset* GroomAsset : CBContext->LoadSelectedObjects<UGroomAsset>())
{
if (GroomAsset && GroomAsset->IsValid() && GroomAsset->CanRebuildFromDescription())
{
return true;
}
}
return false;
}
void ExecuteRebuild(const FToolMenuContext& InContext)
{
const UContentBrowserAssetContextMenuContext* CBContext = UContentBrowserAssetContextMenuContext::FindContextWithAssets(InContext);
for (UGroomAsset* GroomAsset : CBContext->LoadSelectedObjects<UGroomAsset>())
{
if (GroomAsset && GroomAsset->IsValid() && GroomAsset->CanRebuildFromDescription() && GroomAsset->AssetImportData)
{
UGroomAssetImportData* GroomAssetImportData = Cast<UGroomAssetImportData>(GroomAsset->AssetImportData);
if (GroomAssetImportData && GroomAssetImportData->ImportOptions)
{
FString Filename(GroomAssetImportData->GetFirstFilename());
// Duplicate the options to prevent dirtying the asset when they are modified but the rebuild is cancelled
UGroomImportOptions* CurrentOptions = DuplicateObject<UGroomImportOptions>(GroomAssetImportData->ImportOptions, nullptr);
const uint32 GroupCount = GroomAsset->GetNumHairGroups();
UGroomHairGroupsPreview* GroupsPreview = NewObject<UGroomHairGroupsPreview>();
{
FHairDescription HairDescription = GroomAsset->GetHairDescription();
FHairDescriptionGroups HairDescriptionGroups;
FGroomBuilder::BuildHairDescriptionGroups(HairDescription, HairDescriptionGroups);
for (uint32 GroupIndex = 0; GroupIndex < GroupCount; GroupIndex++)
{
FGroomHairGroupPreview& OutGroup = GroupsPreview->Groups.AddDefaulted_GetRef();
OutGroup.GroupIndex = GroupIndex;
OutGroup.GroupID = GroomAsset->GetHairGroupsInfo()[GroupIndex].GroupID;
OutGroup.GroupName = GroomAsset->GetHairGroupsInfo()[GroupIndex].GroupName;
OutGroup.CurveCount = GroomAsset->GetHairGroupsPlatformData()[GroupIndex].Strands.BulkData.GetNumCurves();
OutGroup.GuideCount = GroomAsset->GetHairGroupsPlatformData()[GroupIndex].Guides.BulkData.GetNumCurves();
OutGroup.Attributes = HairDescriptionGroups.HairGroups[GroupIndex].GetHairAttributes();
OutGroup.AttributeFlags = HairDescriptionGroups.HairGroups[GroupIndex].GetHairAttributeFlags();
OutGroup.InterpolationSettings = GroomAsset->GetHairGroupsInterpolation()[GroupIndex];
}
}
TSharedPtr<SGroomImportOptionsWindow> GroomOptionWindow = SGroomImportOptionsWindow::DisplayRebuildOptions(CurrentOptions, GroupsPreview, nullptr/*GroupsMapping*/, Filename);
if (!GroomOptionWindow->ShouldImport())
{
continue;
}
// Apply new interpolation settings to the groom, prior to rebuilding the groom
bool bEnableRigging = false;
for (uint32 GroupIndex = 0; GroupIndex < GroupCount; ++GroupIndex)
{
GroomAsset->GetHairGroupsInterpolation()[GroupIndex] = GroupsPreview->Groups[GroupIndex].InterpolationSettings;
bEnableRigging |= GroomAsset->GetHairGroupsInterpolation()[GroupIndex].InterpolationSettings.GuideType == EGroomGuideType::Rigged;
}
bool bSucceeded = GroomAsset->CacheDerivedDatas();
if (bSucceeded)
{
if(bEnableRigging)
{
GroomAsset->SetRiggedSkeletalMesh(FGroomDeformerBuilder::CreateSkeletalMesh(GroomAsset));
}
// Move the transient ImportOptions to the asset package and set it on the GroomAssetImportData for serialization
CurrentOptions->Rename(nullptr, GroomAssetImportData);
for (const FGroomHairGroupPreview& GroupPreview : GroupsPreview->Groups)
{
CurrentOptions->InterpolationSettings[GroupPreview.GroupIndex] = GroupPreview.InterpolationSettings;
}
GroomAssetImportData->ImportOptions = CurrentOptions;
GroomAsset->MarkPackageDirty();
}
}
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Binding
bool CanCreateBindingAsset(const FToolMenuContext& InContext)
{
const UContentBrowserAssetContextMenuContext* CBContext = UContentBrowserAssetContextMenuContext::FindContextWithAssets(InContext);
for (const FAssetData& SelectedAsset : CBContext->SelectedAssets)
{
if (SelectedAsset.IsValid())
{
return true;
}
}
return false;
}
void ExecuteCreateBindingAsset(const FToolMenuContext& InContext)
{
const UContentBrowserAssetContextMenuContext* CBContext = UContentBrowserAssetContextMenuContext::FindContextWithAssets(InContext);
for (UGroomAsset* GroomAsset : CBContext->LoadSelectedObjects<UGroomAsset>())
{
if (GroomAsset && GroomAsset->IsValid())
{
// Duplicate the options to prevent dirtying the asset when they are modified but the rebuild is cancelled
UGroomCreateBindingOptions* CurrentOptions = NewObject<UGroomCreateBindingOptions>();
if (CurrentOptions)
{
CurrentOptions->GroomAsset = GroomAsset;
}
TSharedPtr<SGroomCreateBindingOptionsWindow> GroomOptionWindow = SGroomCreateBindingOptionsWindow::DisplayCreateBindingOptions(CurrentOptions);
if (!GroomOptionWindow->ShouldCreate())
{
continue;
}
else if (CurrentOptions &&
((CurrentOptions->GroomBindingType == EGroomBindingMeshType::SkeletalMesh && CurrentOptions->TargetSkeletalMesh) ||
(CurrentOptions->GroomBindingType == EGroomBindingMeshType::GeometryCache && CurrentOptions->TargetGeometryCache)))
{
GroomAsset->ConditionalPostLoad();
UGroomBindingAsset* BindingAsset = nullptr;
if (CurrentOptions->GroomBindingType == EGroomBindingMeshType::SkeletalMesh)
{
CurrentOptions->TargetSkeletalMesh->ConditionalPostLoad();
if (CurrentOptions->SourceSkeletalMesh)
{
CurrentOptions->SourceSkeletalMesh->ConditionalPostLoad();
}
BindingAsset = FHairStrandsCore::CreateGroomBindingAsset(CurrentOptions->GroomBindingType, GroomAsset, CurrentOptions->SourceSkeletalMesh, CurrentOptions->TargetSkeletalMesh, CurrentOptions->NumInterpolationPoints, CurrentOptions->MatchingSection, CurrentOptions->TargetBindingAttribute);
}
else
{
CurrentOptions->TargetGeometryCache->ConditionalPostLoad();
if (CurrentOptions->SourceGeometryCache)
{
CurrentOptions->SourceGeometryCache->ConditionalPostLoad();
}
BindingAsset = FHairStrandsCore::CreateGroomBindingAsset(CurrentOptions->GroomBindingType, GroomAsset, CurrentOptions->SourceGeometryCache, CurrentOptions->TargetGeometryCache, CurrentOptions->NumInterpolationPoints, CurrentOptions->MatchingSection, CurrentOptions->TargetBindingAttribute);
}
if (BindingAsset)
{
BindingAsset->Build();
#if WITH_EDITOR
FAssetCompilingManager::Get().FinishCompilationForObjects({BindingAsset});
#endif
if (BindingAsset->IsValid())
{
TArray<UObject*> CreatedObjects;
CreatedObjects.Add(BindingAsset);
FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>("ContentBrowser");
ContentBrowserModule.Get().SyncBrowserToAssets(CreatedObjects);
#if WITH_EDITOR
GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->OpenEditorForAssets(CreatedObjects);
#endif
}
else
{
FNotificationInfo Info(LOCTEXT("FailedToCreateBinding", "Failed to create groom binding. See Output Log for details"));
Info.ExpireDuration = 5.0f;
FSlateNotificationManager::Get().AddNotification(Info);
if (ObjectTools::DeleteSingleObject(BindingAsset))
{
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
}
}
}
}
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Follicle
bool CanCreateFollicleTexture(const FToolMenuContext& InContext)
{
const UContentBrowserAssetContextMenuContext* CBContext = UContentBrowserAssetContextMenuContext::FindContextWithAssets(InContext);
for (const FAssetData& SelectedAsset : CBContext->SelectedAssets)
{
if (SelectedAsset.IsValid())
{
return true;
}
}
return false;
}
void ExecuteCreateFollicleTexture(const FToolMenuContext& InContext)
{
const UContentBrowserAssetContextMenuContext* CBContext = UContentBrowserAssetContextMenuContext::FindContextWithAssets(InContext);
TArray<UGroomAsset*> GroomAssets = CBContext->LoadSelectedObjects<UGroomAsset>();
if (GroomAssets.Num() == 0)
{
return;
}
// Duplicate the options to prevent dirtying the asset when they are modified but the rebuild is cancelled
UGroomCreateFollicleMaskOptions* CurrentOptions = NewObject<UGroomCreateFollicleMaskOptions>();
if (!CurrentOptions)
{
return;
}
for (UGroomAsset* GroomAsset : GroomAssets)
{
if (GroomAsset && GroomAsset->IsValid())
{
FFollicleMaskOptions& Items = CurrentOptions->Grooms.AddDefaulted_GetRef();;
Items.Groom = GroomAsset;
Items.Channel = EFollicleMaskChannel::R;
}
}
if (CurrentOptions->Grooms.Num() == 0)
{
return;
}
TSharedPtr<SGroomCreateFollicleMaskOptionsWindow> GroomOptionWindow = SGroomCreateFollicleMaskOptionsWindow::DisplayCreateFollicleMaskOptions(CurrentOptions);
if (!GroomOptionWindow->ShouldCreate())
{
return;
}
else
{
TArray<FFollicleInfo> Infos;
for (FFollicleMaskOptions& Option : CurrentOptions->Grooms)
{
if (Option.Groom)
{
Option.Groom->ConditionalPostLoad();
FFollicleInfo& Info = Infos.AddDefaulted_GetRef();
Info.GroomAsset = Option.Groom;
Info.Channel = FFollicleInfo::EChannel(uint8(Option.Channel));
Info.KernelSizeInPixels = FMath::Max(2, CurrentOptions->RootRadius);
}
}
const uint32 Resolution = FMath::RoundUpToPowerOfTwo(CurrentOptions->Resolution);
UTexture2D* FollicleTexture = FGroomTextureBuilder::CreateGroomFollicleMaskTexture(CurrentOptions->Grooms[0].Groom, Resolution);
if (FollicleTexture)
{
FGroomTextureBuilder::BuildFollicleTexture(Infos, FollicleTexture, false);
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Strands Textures
bool CanCreateStrandsTextures(const FToolMenuContext& InContext)
{
const UContentBrowserAssetContextMenuContext* CBContext = UContentBrowserAssetContextMenuContext::FindContextWithAssets(InContext);
for (const FAssetData& SelectedAsset : CBContext->SelectedAssets)
{
if (SelectedAsset.IsValid())
{
return true;
}
}
return false;
}
void ExecuteCreateStrandsTextures(const FToolMenuContext& InContext)
{
const UContentBrowserAssetContextMenuContext* CBContext = UContentBrowserAssetContextMenuContext::FindContextWithAssets(InContext);
TArray<UGroomAsset*> GroomAssets = CBContext->LoadSelectedObjects<UGroomAsset>();
if (GroomAssets.Num() == 0)
{
return;
}
// Duplicate the options to prevent dirtying the asset when they are modified but the rebuild is cancelled
for (UGroomAsset* GroomAsset : GroomAssets)
{
if (GroomAsset && GroomAsset->IsValid())
{
UGroomCreateStrandsTexturesOptions* CurrentOptions = nullptr;
UGroomAssetImportData* GroomAssetImportData = Cast<UGroomAssetImportData>(GroomAsset->AssetImportData);
if (GroomAssetImportData)
{
// Duplicate the options to prevent dirtying the asset when they are modified but the rebuild is cancelled
if (GroomAssetImportData->HairStrandsTexturesOptions)
{
CurrentOptions = DuplicateObject<UGroomCreateStrandsTexturesOptions>(GroomAssetImportData->HairStrandsTexturesOptions, nullptr);
}
}
else
{
// Create UGroomAssetImportData and copy existing values if any
GroomAssetImportData = NewObject<UGroomAssetImportData>();
GroomAssetImportData->Rename(nullptr, GroomAsset);
GroomAssetImportData->SourceData = GroomAsset->AssetImportData->SourceData;
// Create/Initialize import setings
const uint32 GroupCount = GroomAsset->GetNumHairGroups();
GroomAssetImportData->ImportOptions = NewObject<UGroomImportOptions>();
GroomAssetImportData->ImportOptions->Rename(nullptr, GroomAssetImportData);
GroomAssetImportData->ImportOptions->InterpolationSettings.SetNum(GroupCount);
for (uint32 GroupIndex = 0; GroupIndex < GroupCount; ++GroupIndex)
{
GroomAssetImportData->ImportOptions->InterpolationSettings[GroupIndex] = GroomAsset->GetHairGroupsInterpolation()[GroupIndex];
}
}
if (CurrentOptions == nullptr)
{
CurrentOptions = NewObject<UGroomCreateStrandsTexturesOptions>();
}
TSharedPtr<SGroomCreateStrandsTexturesOptionsWindow> GroomOptionWindow = SGroomCreateStrandsTexturesOptionsWindow::DisplayCreateStrandsTexturesOptions(CurrentOptions);
if (!GroomOptionWindow->ShouldCreate())
{
return;
}
else
{
GroomAsset->ConditionalPostLoad();
// Create debug data for the groom asset for tracing hair geometry when redering strands texture.
if (!GroomAsset->HasDebugData())
{
GroomAsset->CreateDebugData();
}
float SignDirection = 1;
float MaxDistance = CurrentOptions->TraceDistance;
switch (CurrentOptions->TraceType)
{
case EStrandsTexturesTraceType::TraceOuside: SignDirection = 1; break;
case EStrandsTexturesTraceType::TraceInside: SignDirection = -1; break;
case EStrandsTexturesTraceType::TraceBidirectional: SignDirection = 0; MaxDistance *= 2; break;
}
UStaticMesh* StaticMesh = nullptr;
USkeletalMesh* SkeletalMesh = nullptr;
switch (CurrentOptions->MeshType)
{
case EStrandsTexturesMeshType::Static: StaticMesh = CurrentOptions->StaticMesh; break;
case EStrandsTexturesMeshType::Skeletal: SkeletalMesh = CurrentOptions->SkeletalMesh; break;
}
if (SkeletalMesh == nullptr && StaticMesh == nullptr)
{
return;
}
FStrandsTexturesInfo Info;
Info.Layout = CurrentOptions->Layout;
Info.GroomAsset = GroomAsset;
Info.TracingDirection = SignDirection;
Info.MaxTracingDistance = MaxDistance;
Info.Resolution = FMath::RoundUpToPowerOfTwo(FMath::Max(256, CurrentOptions->Resolution));
Info.LODIndex = FMath::Max(0, CurrentOptions->LODIndex);
Info.SectionIndex = FMath::Max(0, CurrentOptions->SectionIndex);
Info.UVChannelIndex= FMath::Max(0, CurrentOptions->UVChannelIndex);
Info.SkeletalMesh = SkeletalMesh;
Info.StaticMesh = StaticMesh;
Info.Dilation = FMath::Clamp(CurrentOptions->Dilation, 0, 64);
if (CurrentOptions->GroupIndex.Num())
{
Info.GroupIndices = CurrentOptions->GroupIndex;
}
else
{
for (int32 GroupIndex = 0; GroupIndex < GroomAsset->GetNumHairGroups(); ++GroupIndex)
{
Info.GroupIndices.Add(GroupIndex);
}
}
FStrandsTexturesOutput Output;
const uint32 TextureCount = GetHairTextureLayoutTextureCount(Info.Layout);
if (CurrentOptions->GeneratedTextures.Num() != TextureCount)
{
Output = FGroomTextureBuilder::CreateGroomStrandsTexturesTexture(GroomAsset, Info.Resolution, Info.Layout);
CurrentOptions->GeneratedTextures = Output.Textures;
}
else
{
Output.Textures = CurrentOptions->GeneratedTextures;
}
if (Output.IsValid())
{
FGroomTextureBuilder::BuildStrandsTextures(Info, Output);
// Save settings used for generating these textures
if (GroomAssetImportData)
{
if (!GroomAssetImportData->HairStrandsTexturesOptions || GroomAssetImportData->HairStrandsTexturesOptions != CurrentOptions)
{
// Move the transient ImportOptions to the asset package and set it on the GroomAssetImportData for serialization
CurrentOptions->Rename(nullptr, GroomAssetImportData);
GroomAssetImportData->HairStrandsTexturesOptions = CurrentOptions;
GroomAsset->AssetImportData = GroomAssetImportData;
GroomAsset->MarkPackageDirty();
}
}
}
}
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Actions registration
static FDelayedAutoRegisterHelper DelayedAutoRegister(EDelayedRegisterRunPhase::EndOfEngineInit, []{
UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateLambda([]()
{
FToolMenuOwnerScoped OwnerScoped(UE_MODULE_NAME);
UToolMenu* Menu = UE::ContentBrowser::ExtendToolMenu_AssetContextMenu(UGroomAsset::StaticClass());
FToolMenuSection& Section = Menu->FindOrAddSection("GetAssetActions");
Section.AddDynamicEntry(NAME_None, FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection)
{
{
const TAttribute<FText> Label = LOCTEXT("RebuildGroom", "Rebuild");
const TAttribute<FText> ToolTip = LOCTEXT("RebuildGroomTooltip", "Rebuild the groom with new build settings");
const FSlateIcon Icon = FSlateIcon(FAppStyle::GetAppStyleSetName(), "ContentBrowser.AssetActions");
FToolUIAction UIAction;
UIAction.ExecuteAction = FToolMenuExecuteAction::CreateStatic(&ExecuteRebuild);
UIAction.CanExecuteAction = FToolMenuCanExecuteAction::CreateStatic(&CanRebuild);
InSection.AddMenuEntry("GroomAsset_RebuildGroom", Label, ToolTip, Icon, UIAction);
}
{
const TAttribute<FText> Label = LOCTEXT("CreateBindingAsset", "Create Binding");
const TAttribute<FText> ToolTip = LOCTEXT("CreateBindingAssetTooltip", "Create a binding asset between a skeletal mesh and a groom asset");
const FSlateIcon Icon = FSlateIcon(FAppStyle::GetAppStyleSetName(), "ContentBrowser.AssetActions");
FToolUIAction UIAction;
UIAction.ExecuteAction = FToolMenuExecuteAction::CreateStatic(&ExecuteCreateBindingAsset);
UIAction.CanExecuteAction = FToolMenuCanExecuteAction::CreateStatic(&CanCreateBindingAsset);
InSection.AddMenuEntry("GroomAsset_CreateBindingAsset", Label, ToolTip, Icon, UIAction);
}
{
const TAttribute<FText> Label = LOCTEXT("CreateFollicleTexture", "Create Follicle Texture");
const TAttribute<FText> ToolTip = LOCTEXT("CreateFollicleTextureTooltip", "Create a follicle texture for the selected groom assets");
const FSlateIcon Icon = FSlateIcon(FAppStyle::GetAppStyleSetName(), "ContentBrowser.AssetActions");
FToolUIAction UIAction;
UIAction.ExecuteAction = FToolMenuExecuteAction::CreateStatic(&ExecuteCreateFollicleTexture);
UIAction.CanExecuteAction = FToolMenuCanExecuteAction::CreateStatic(&CanCreateFollicleTexture);
InSection.AddMenuEntry("GroomAsset_CreateFollicleTexture", Label, ToolTip, Icon, UIAction);
}
{
const TAttribute<FText> Label = LOCTEXT("CreateStrandsTextures", "Create Strands Textures");
const TAttribute<FText> ToolTip = LOCTEXT("CreateStrandsTexturesTooltip", "Create projected strands textures onto meshes");
const FSlateIcon Icon = FSlateIcon(FAppStyle::GetAppStyleSetName(), "ContentBrowser.AssetActions");
FToolUIAction UIAction;
UIAction.ExecuteAction = FToolMenuExecuteAction::CreateStatic(&ExecuteCreateStrandsTextures);
UIAction.CanExecuteAction = FToolMenuCanExecuteAction::CreateStatic(&CanCreateStrandsTextures);
InSection.AddMenuEntry("GroomAsset_CreateStrandsTextures", Label, ToolTip, Icon, UIAction);
}
}));
}));
});
} // namespace MenuExtension_GroomAsset
#undef LOCTEXT_NAMESPACE