// 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()) { if (GroomAsset != nullptr) { if(!GroomAsset->GetDataflowSettings().GetDataflowAsset()) { TSharedRef NewCustomAssetEditor(new FGroomCustomAssetEditorToolkit()); NewCustomAssetEditor->InitCustomAssetEditor(OpenArgs.GetToolkitMode(), OpenArgs.ToolkitHost, GroomAsset); } else { UAssetEditorSubsystem* const AssetEditorSubsystem = GEditor->GetEditorSubsystem(); UDataflowEditor* const AssetEditor = NewObject(AssetEditorSubsystem, NAME_None, RF_Transient); AssetEditor->RegisterToolCategories({"General"}); const TSubclassOf ActorClass = StaticLoadClass(AActor::StaticClass(), nullptr, TEXT("/HairStrands/BP_PreviewGroom.BP_PreviewGroom_C"), nullptr, LOAD_None, nullptr); UMaterial* HairMaterial = Cast(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& TypeAssets, TArray& OutSourceFilePaths) const { for (UObject* Asset : TypeAssets) { const UGroomAsset* GroomAsset = CastChecked(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()) { 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()) { if (GroomAsset && GroomAsset->IsValid() && GroomAsset->CanRebuildFromDescription() && GroomAsset->AssetImportData) { UGroomAssetImportData* GroomAssetImportData = Cast(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(GroomAssetImportData->ImportOptions, nullptr); const uint32 GroupCount = GroomAsset->GetNumHairGroups(); UGroomHairGroupsPreview* GroupsPreview = NewObject(); { 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 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()) { if (GroomAsset && GroomAsset->IsValid()) { // Duplicate the options to prevent dirtying the asset when they are modified but the rebuild is cancelled UGroomCreateBindingOptions* CurrentOptions = NewObject(); if (CurrentOptions) { CurrentOptions->GroomAsset = GroomAsset; } TSharedPtr 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 CreatedObjects; CreatedObjects.Add(BindingAsset); FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); ContentBrowserModule.Get().SyncBrowserToAssets(CreatedObjects); #if WITH_EDITOR GEditor->GetEditorSubsystem()->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 GroomAssets = CBContext->LoadSelectedObjects(); 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(); 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 GroomOptionWindow = SGroomCreateFollicleMaskOptionsWindow::DisplayCreateFollicleMaskOptions(CurrentOptions); if (!GroomOptionWindow->ShouldCreate()) { return; } else { TArray 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 GroomAssets = CBContext->LoadSelectedObjects(); 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(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(GroomAssetImportData->HairStrandsTexturesOptions, nullptr); } } else { // Create UGroomAssetImportData and copy existing values if any GroomAssetImportData = NewObject(); GroomAssetImportData->Rename(nullptr, GroomAsset); GroomAssetImportData->SourceData = GroomAsset->AssetImportData->SourceData; // Create/Initialize import setings const uint32 GroupCount = GroomAsset->GetNumHairGroups(); GroomAssetImportData->ImportOptions = NewObject(); 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(); } TSharedPtr 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 Label = LOCTEXT("RebuildGroom", "Rebuild"); const TAttribute 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 Label = LOCTEXT("CreateBindingAsset", "Create Binding"); const TAttribute 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 Label = LOCTEXT("CreateFollicleTexture", "Create Follicle Texture"); const TAttribute 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 Label = LOCTEXT("CreateStrandsTextures", "Create Strands Textures"); const TAttribute 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