/* * Copyright (c) <2021> Side Effects Software Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. The name of Side Effects Software may not be used to endorse or * promote products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN * NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "HoudiniEngineEditorUtils.h" #include "HoudiniEngineEditorPrivatePCH.h" #include "HoudiniEngine.h" #include "HoudiniEngineEditor.h" #include "HoudiniRuntimeSettings.h" #include "HoudiniAssetActor.h" #include "HoudiniGeoPartObject.h" #include "HoudiniAsset.h" #include "HoudiniOutput.h" #include "HoudiniTool.h" #include "ContentBrowserModule.h" #include "IContentBrowserSingleton.h" #include "Engine/Selection.h" #include "AssetRegistryModule.h" #include "EditorViewportClient.h" #include "ActorFactories/ActorFactory.h" #include "FileHelpers.h" #include "PropertyPathHelpers.h" #define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE int32 FHoudiniEngineEditorUtils::GetContentBrowserSelection(TArray< UObject* >& ContentBrowserSelection) { ContentBrowserSelection.Empty(); // Get the current Content browser selection FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked< FContentBrowserModule >("ContentBrowser"); TArray SelectedAssets; ContentBrowserModule.Get().GetSelectedAssets(SelectedAssets); for (int32 n = 0; n < SelectedAssets.Num(); n++) { // Get the current object UObject * Object = SelectedAssets[n].GetAsset(); if (!Object || Object->IsPendingKill()) continue; // Only static meshes are supported if (Object->GetClass() != UStaticMesh::StaticClass()) continue; ContentBrowserSelection.Add(Object); } return ContentBrowserSelection.Num(); } int32 FHoudiniEngineEditorUtils::GetWorldSelection(TArray& WorldSelection, bool bHoudiniAssetActorsOnly) { WorldSelection.Empty(); // Get the current editor selection if (GEditor) { USelection* SelectedActors = GEditor->GetSelectedActors(); if (SelectedActors && !SelectedActors->IsPendingKill()) { for (FSelectionIterator It(*SelectedActors); It; ++It) { AActor * Actor = Cast(*It); if (!IsValid(Actor)) continue; // Ignore the SkySphere? FString ClassName = Actor->GetClass() ? Actor->GetClass()->GetName() : FString(); if (ClassName == TEXT("BP_Sky_Sphere_C")) continue; // We're normally only selecting actors with StaticMeshComponents and SplineComponents // Heightfields? Filter here or later? also allow HoudiniAssets? WorldSelection.Add(Actor); } } } // If we only want Houdini Actors... if (bHoudiniAssetActorsOnly) { // ... remove all but them for (int32 Idx = WorldSelection.Num() - 1; Idx >= 0; Idx--) { AHoudiniAssetActor * HoudiniAssetActor = Cast(WorldSelection[Idx]); if (!HoudiniAssetActor || HoudiniAssetActor->IsPendingKill()) WorldSelection.RemoveAt(Idx); } } return WorldSelection.Num(); } FString FHoudiniEngineEditorUtils::HoudiniCurveTypeToString(const EHoudiniCurveType& HoudiniCurveType) { FString HoudiniCurveTypeStr; switch (HoudiniCurveType) { case EHoudiniCurveType::Invalid: { HoudiniCurveTypeStr = TEXT("Invalid"); } break; case EHoudiniCurveType::Polygon: { HoudiniCurveTypeStr = TEXT("Polygon"); } break; case EHoudiniCurveType::Nurbs: { HoudiniCurveTypeStr = TEXT("Nurbs"); } break; case EHoudiniCurveType::Bezier: { HoudiniCurveTypeStr = TEXT("Bezier"); } break; case EHoudiniCurveType::Points: { HoudiniCurveTypeStr = TEXT("Points"); } break; } return HoudiniCurveTypeStr; } FString FHoudiniEngineEditorUtils::HoudiniCurveMethodToString(const EHoudiniCurveMethod& CurveMethod) { FString HoudiniCurveMethodStr; switch (CurveMethod) { case EHoudiniCurveMethod::Invalid: { HoudiniCurveMethodStr = TEXT("Invalid"); } break; case EHoudiniCurveMethod::CVs: { HoudiniCurveMethodStr = TEXT("CVs"); } break; case EHoudiniCurveMethod::Breakpoints: { HoudiniCurveMethodStr = TEXT("Breakpoints"); } break; case EHoudiniCurveMethod::Freehand: { HoudiniCurveMethodStr = TEXT("Freehand"); } break; } return HoudiniCurveMethodStr; } FString FHoudiniEngineEditorUtils::HoudiniCurveMethodToUnrealCurveTypeString(const EHoudiniCurveType& HoudiniCurveType) { // Temporary, we need to figure out a way to access the output curve's info later FString UnrealCurveType; switch (HoudiniCurveType) { case EHoudiniCurveType::Polygon: case EHoudiniCurveType::Points: { UnrealCurveType = TEXT("Linear"); } break; case EHoudiniCurveType::Nurbs: case EHoudiniCurveType::Bezier: { UnrealCurveType = TEXT("Curve"); } break; } return UnrealCurveType; } FString FHoudiniEngineEditorUtils::HoudiniLandscapeOutputBakeTypeToString(const EHoudiniLandscapeOutputBakeType& LandscapeBakeType) { FString LandscapeBakeTypeString; switch (LandscapeBakeType) { case EHoudiniLandscapeOutputBakeType::Detachment: LandscapeBakeTypeString = "To Current Level"; break; case EHoudiniLandscapeOutputBakeType::BakeToImage: LandscapeBakeTypeString = "To Image"; break; case EHoudiniLandscapeOutputBakeType::BakeToWorld: LandscapeBakeTypeString = "To New World"; break; } return LandscapeBakeTypeString; } FTransform FHoudiniEngineEditorUtils::GetDefaulAssetSpawnTransform() { FTransform SpawnTransform = FTransform::Identity; // Get the editor viewport LookAt position to spawn the new objects if (GEditor && GEditor->GetActiveViewport()) { FEditorViewportClient* ViewportClient = (FEditorViewportClient*)GEditor->GetActiveViewport()->GetClient(); if (ViewportClient) { // We need to toggle the orbit camera on to get the proper LookAtLocation to spawn our asset ViewportClient->ToggleOrbitCamera(true); SpawnTransform.SetLocation(ViewportClient->GetLookAtLocation()); ViewportClient->ToggleOrbitCamera(false); } } return SpawnTransform; } FTransform FHoudiniEngineEditorUtils::GetMeanWorldSelectionTransform() { FTransform SpawnTransform = GetDefaulAssetSpawnTransform(); if (GEditor && (GEditor->GetSelectedActorCount() > 0)) { // Get the current Level Editor Selection USelection* SelectedActors = GEditor->GetSelectedActors(); int NumAppliedTransform = 0; for (FSelectionIterator It(*SelectedActors); It; ++It) { AActor * Actor = Cast< AActor >(*It); if (!Actor) continue; // Just Ignore the SkySphere... FString ClassName = Actor->GetClass() ? Actor->GetClass()->GetName() : FString(); if (ClassName == TEXT("BP_Sky_Sphere_C")) continue; FTransform CurrentTransform = Actor->GetTransform(); ALandscapeProxy* Landscape = Cast< ALandscapeProxy >(Actor); if (Landscape) { // We need to offset Landscape's transform in X/Y to center them properly FVector Origin, Extent; Actor->GetActorBounds(false, Origin, Extent); // Use the origin's XY Position FVector Location = CurrentTransform.GetLocation(); Location.X = Origin.X; Location.Y = Origin.Y; CurrentTransform.SetLocation(Location); } // Accumulate all the actor transforms... if (NumAppliedTransform == 0) SpawnTransform = CurrentTransform; else SpawnTransform.Accumulate(CurrentTransform); NumAppliedTransform++; } if (NumAppliedTransform > 0) { // "Mean" all the accumulated Transform SpawnTransform.SetScale3D(FVector::OneVector); SpawnTransform.NormalizeRotation(); if (NumAppliedTransform > 1) SpawnTransform.SetLocation(SpawnTransform.GetLocation() / (float)NumAppliedTransform); } } return SpawnTransform; } void FHoudiniEngineEditorUtils::InstantiateHoudiniAsset(UHoudiniAsset* InHoudiniAsset, const EHoudiniToolType& InType, const EHoudiniToolSelectionType& InSelectionType) { if (!InHoudiniAsset) return; FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); // Load the asset UObject* AssetObj = Cast(InHoudiniAsset);//InHoudiniAsset->LoadSynchronous(); if (!AssetObj) return; // Get the asset Factory UActorFactory* Factory = GEditor->FindActorFactoryForActorClass(AHoudiniAssetActor::StaticClass()); if (!Factory) return; // Get the current Level Editor Selection TArray WorldSelection; int32 WorldSelectionCount = FHoudiniEngineEditorUtils::GetWorldSelection(WorldSelection); // Get the current Content browser selection TArray ContentBrowserSelection; int32 ContentBrowserSelectionCount = FHoudiniEngineEditorUtils::GetContentBrowserSelection(ContentBrowserSelection); // By default, Content browser selection has a priority over the world selection bool UseCBSelection = ContentBrowserSelectionCount > 0; if (InSelectionType == EHoudiniToolSelectionType::HTOOL_SELECTION_CB_ONLY) UseCBSelection = true; else if (InSelectionType == EHoudiniToolSelectionType::HTOOL_SELECTION_WORLD_ONLY) UseCBSelection = false; // Modify the created actor's position from the current editor world selection FTransform SpawnTransform = GetDefaulAssetSpawnTransform(); if (WorldSelectionCount > 0) { // Get the "mean" transform of all the selected actors SpawnTransform = GetMeanWorldSelectionTransform(); } // If the current tool is a batch one, we'll need to create multiple instances of the HDA if (InType == EHoudiniToolType::HTOOLTYPE_OPERATOR_BATCH) { // Unselect the current selection to select the created actor after if (GEditor) GEditor->SelectNone(true, true, false); // An instance of the asset will be created for each selected object for (int32 SelecIndex = 0; SelecIndex < (UseCBSelection ? ContentBrowserSelectionCount : WorldSelectionCount); SelecIndex++) { // Get the current object UObject* CurrentSelectedObject = nullptr; if (UseCBSelection && ContentBrowserSelection.IsValidIndex(SelecIndex)) CurrentSelectedObject = ContentBrowserSelection[SelecIndex]; if (!UseCBSelection && WorldSelection.IsValidIndex(SelecIndex)) CurrentSelectedObject = WorldSelection[SelecIndex]; if (!CurrentSelectedObject) continue; // If it's an actor, use its Transform to spawn the HDA AActor* CurrentSelectedActor = Cast(CurrentSelectedObject); if (CurrentSelectedActor) SpawnTransform = CurrentSelectedActor->GetTransform(); else SpawnTransform = GetDefaulAssetSpawnTransform(); // Create the actor for the HDA AActor* CreatedActor = Factory->CreateActor(AssetObj, GEditor->GetEditorWorldContext().World()->GetCurrentLevel(), SpawnTransform); if (!CreatedActor) continue; // Get the HoudiniAssetActor / HoudiniAssetComponent we just created AHoudiniAssetActor* HoudiniAssetActor = (AHoudiniAssetActor*)CreatedActor; if (!HoudiniAssetActor) continue; UHoudiniAssetComponent* HoudiniAssetComponent = HoudiniAssetActor->GetHoudiniAssetComponent(); if (!HoudiniAssetComponent) continue; // Create and set the input preset for this HDA and selected Object TMap InputPreset; InputPreset.Add(CurrentSelectedObject, 0); HoudiniAssetComponent->SetInputPresets(InputPreset); // Select the Actor we just created if (GEditor && GEditor->CanSelectActor(CreatedActor, true, false)) GEditor->SelectActor(CreatedActor, true, true, true); } } else { // We only need to create a single instance of the asset, regarding of the selection AActor* CreatedActor = Factory->CreateActor(AssetObj, GEditor->GetEditorWorldContext().World()->GetCurrentLevel(), SpawnTransform); if (!CreatedActor) return; // Generator tools don't need to preset their input if (InType != EHoudiniToolType::HTOOLTYPE_GENERATOR) { TMap InputPresets; AHoudiniAssetActor* HoudiniAssetActor = (AHoudiniAssetActor*)CreatedActor; UHoudiniAssetComponent* HoudiniAssetComponent = HoudiniAssetActor ? HoudiniAssetActor->GetHoudiniAssetComponent() : nullptr; if (HoudiniAssetComponent) { // Build the preset map int InputIndex = 0; for (auto CurrentObject : (UseCBSelection ? ContentBrowserSelection : WorldSelection)) { if (!CurrentObject) continue; if (InType == EHoudiniToolType::HTOOLTYPE_OPERATOR_MULTI) { // The selection will be applied individually to multiple inputs // (first object to first input, second object to second input etc...) InputPresets.Add(CurrentObject, InputIndex++); } else { // All the selection will be applied to the asset's first input InputPresets.Add(CurrentObject, 0); } } // Set the input preset on the HoudiniAssetComponent if (InputPresets.Num() > 0) HoudiniAssetComponent->SetInputPresets(InputPresets); } } // Select the Actor we just created if (GEditor->CanSelectActor(CreatedActor, true, true)) { GEditor->SelectNone(true, true, false); GEditor->SelectActor(CreatedActor, true, true, true); } } } AActor* FHoudiniEngineEditorUtils::InstantiateHoudiniAssetAt(UHoudiniAsset* InHoudiniAsset, const FTransform& InTransform, UWorld* InSpawnInWorld, ULevel* InSpawnInLevelOverride) { if (!InHoudiniAsset) return nullptr; FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); // Load the asset UObject* AssetObj = Cast(InHoudiniAsset);//InHoudiniAsset->LoadSynchronous(); if (!AssetObj) return nullptr; // Get the asset Factory UActorFactory* Factory = GEditor->FindActorFactoryForActorClass(AHoudiniAssetActor::StaticClass()); if (!Factory) return nullptr; // Determine the level to spawn in from the supplied parameters // InSpawnInLevelOverride if valid, else if InSpawnInWorld is valid its current level // lastly, get the editor world's current level ULevel* LevelToSpawnIn = InSpawnInLevelOverride; if (!IsValid(LevelToSpawnIn)) { if (IsValid(InSpawnInWorld)) LevelToSpawnIn = InSpawnInWorld->GetCurrentLevel(); else LevelToSpawnIn = GEditor->GetEditorWorldContext().World()->GetCurrentLevel(); } // We only need to create a single instance of the asset, regarding of the selection AActor* CreatedActor = Factory->CreateActor(AssetObj, LevelToSpawnIn, InTransform); if (!CreatedActor) return nullptr; // Select the Actor we just created if (GEditor->CanSelectActor(CreatedActor, true, true)) { GEditor->SelectNone(true, true, false); GEditor->SelectActor(CreatedActor, true, true, true); } return CreatedActor; } void FHoudiniEngineEditorUtils::SaveAllHoudiniTemporaryCookData(UWorld *InSaveWorld) { // Add a slate notification FString Notification = TEXT("Saving all Houdini temporary cook data..."); // FHoudiniEngineUtils::CreateSlateNotification(Notification); TArray PackagesToSave; for (TObjectIterator Itr; Itr; ++Itr) { UHoudiniAssetComponent * HAC = *Itr; if (!HAC || HAC->IsPendingKill()) continue; if (InSaveWorld && InSaveWorld != HAC->GetWorld()) continue; const int32 NumOutputs = HAC->GetNumOutputs(); for (int32 Index = 0; Index < NumOutputs; ++Index) { UHoudiniOutput *Output = HAC->GetOutputAt(Index); if (!Output || Output->IsPendingKill()) continue; // TODO: Also save landscape layer info objects? if (Output->GetType() != EHoudiniOutputType::Mesh) continue; for (auto &OutputObjectPair : Output->GetOutputObjects()) { UObject *Obj = OutputObjectPair.Value.OutputObject; if (!Obj || Obj->IsPendingKill()) continue; UStaticMesh *SM = Cast(Obj); if (!SM) continue; UPackage *Package = SM->GetOutermost(); if (!Package || Package->IsPendingKill()) continue; if (Package->IsDirty() && Package->IsFullyLoaded() && Package != GetTransientPackage()) { PackagesToSave.Add(Package); } } for (auto& MaterialAssignementPair : Output->GetAssignementMaterials()) { UMaterialInterface* MatInterface = MaterialAssignementPair.Value; if (!MatInterface || MatInterface->IsPendingKill()) continue; UPackage *Package = MatInterface->GetOutermost(); if (!Package || Package->IsPendingKill()) continue; if (Package->IsDirty() && Package->IsFullyLoaded() && Package != GetTransientPackage()) { PackagesToSave.Add(Package); } } } } UEditorLoadingAndSavingUtils::SavePackages(PackagesToSave, true); } void FHoudiniEngineEditorUtils::ReselectSelectedActors() { // TODO: Duplicate with FHoudiniEngineUtils::UpdateEditorProperties ?? USelection* Selection = GEditor->GetSelectedActors(); TArray SelectedActors; SelectedActors.SetNumUninitialized(GEditor->GetSelectedActorCount()); Selection->GetSelectedObjects(SelectedActors); GEditor->SelectNone(false, false, false); for (AActor* NextSelected : SelectedActors) { GEditor->SelectActor(NextSelected, true, true, true, true); } } FString FHoudiniEngineEditorUtils::GetNodeNamePaddedByPathDepth(const FString& InNodeName, const FString& InNodePath, const uint8 Padding, const TCHAR PathSep) { int32 Depth = 0; for (const TCHAR Char : InNodePath) { if (Char == PathSep) Depth++; } FString Trimmed = InNodeName; Trimmed.TrimStartInline(); return Trimmed.LeftPad(Trimmed.Len() + (Depth * Padding)); } void FHoudiniEngineEditorUtils::NotifyPostEditChangeProperty(FName InPropertyPath, UObject* InRootObject) { if (!IsValid(InRootObject)) { HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineEditorUtils::NotifyPostEditChangeProperty]: InRootObject is null.")); return; } const FCachedPropertyPath CachedPath(InPropertyPath.ToString()); if (CachedPath.Resolve(InRootObject)) { // Notify that we have changed the property // FPropertyChangedEvent Evt = CachedPath.ToPropertyChangedEvent(EPropertyChangeType::Unspecified); // Construct FPropertyChangedEvent from the cached property path const int32 NumSegments = CachedPath.GetNumSegments(); FPropertyChangedEvent Evt( CastFieldChecked(CachedPath.GetLastSegment().GetField().ToField()), EPropertyChangeType::Unspecified, { InRootObject }); if(NumSegments > 1) { Evt.SetActiveMemberProperty(CastFieldChecked(CachedPath.GetSegment(NumSegments - 2).GetField().ToField())); } // Set the array of indices to the changed property TArray> ArrayIndicesPerObject; ArrayIndicesPerObject.AddDefaulted(1); for (int32 SegmentIdx = 0; SegmentIdx < NumSegments; ++SegmentIdx) { const FPropertyPathSegment& Segment = CachedPath.GetSegment(SegmentIdx); const int32 ArrayIndex = Segment.GetArrayIndex(); if (ArrayIndex != INDEX_NONE) { ArrayIndicesPerObject[0].Add(Segment.GetName().ToString(), ArrayIndex); } } Evt.SetArrayIndexPerObject(ArrayIndicesPerObject); FEditPropertyChain Chain; CachedPath.ToEditPropertyChain(Chain); FPropertyChangedChainEvent ChainEvent(Chain, Evt); ChainEvent.ObjectIteratorIndex = 0; InRootObject->PostEditChangeChainProperty(ChainEvent); } else { HOUDINI_LOG_WARNING(TEXT("Could not resolve property path '%s' on %s."), *InPropertyPath.ToString(), *(InRootObject->GetFullName())); } } #undef LOCTEXT_NAMESPACE