/* * 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 "HoudiniEngineBakeUtils.h" #include "HoudiniEngineEditorPrivatePCH.h" #include "HoudiniEngineUtils.h" #include "HoudiniAssetActor.h" #include "HoudiniAsset.h" #include "HoudiniAssetComponent.h" #include "HoudiniOutput.h" #include "HoudiniSplineComponent.h" #include "HoudiniGeoPartObject.h" #include "HoudiniPackageParams.h" #include "HoudiniEnginePrivatePCH.h" #include "HoudiniRuntimeSettings.h" #include "HoudiniEngineUtils.h" #include "UnrealLandscapeTranslator.h" #include "HoudiniInstanceTranslator.h" #include "HoudiniInstancedActorComponent.h" #include "HoudiniMeshSplitInstancerComponent.h" #include "HoudiniPDGAssetLink.h" #include "HoudiniStringResolver.h" #include "HoudiniEngineCommands.h" #include "HoudiniEngineRuntimeUtils.h" #include "Engine/StaticMesh.h" #include "Engine/World.h" #include "RawMesh.h" #include "UObject/Package.h" #include "PackageTools.h" #include "UObject/MetaData.h" #include "AssetRegistryModule.h" #include "Materials/Material.h" #include "LandscapeProxy.h" #include "LandscapeStreamingProxy.h" #include "LandscapeInfo.h" #include "Factories/WorldFactory.h" #include "AssetToolsModule.h" #include "InstancedFoliageActor.h" #include "Components/SplineComponent.h" #include "GameFramework/Actor.h" #include "Engine/StaticMeshActor.h" #include "Components/StaticMeshComponent.h" #include "PhysicsEngine/BodySetup.h" #include "ActorFactories/ActorFactoryStaticMesh.h" #include "ActorFactories/ActorFactoryEmptyActor.h" #include "BusyCursor.h" #include "Editor.h" #include "Kismet2/KismetEditorUtilities.h" #include "FileHelpers.h" #include "HoudiniEngineEditor.h" #include "HoudiniEngine.h" #include "HoudiniLandscapeTranslator.h" #include "HoudiniOutputTranslator.h" #include "Editor/EditorEngine.h" #include "Factories/BlueprintFactory.h" #include "Engine/SimpleConstructionScript.h" #include "Misc/Paths.h" #include "HAL/FileManager.h" #include "LandscapeEdit.h" #include "Containers/UnrealString.h" #include "Components/AudioComponent.h" #include "Engine/WorldComposition.h" #include "Kismet2/BlueprintEditorUtils.h" #include "MaterialEditor/Public/MaterialEditingLibrary.h" #include "MaterialGraph/MaterialGraph.h" #include "Materials/MaterialInstance.h" #include "Particles/ParticleSystemComponent.h" #include "Sound/SoundBase.h" #include "UObject/UnrealType.h" #include "Math/Box.h" #include "Misc/ScopedSlowTask.h" HOUDINI_BAKING_DEFINE_LOG_CATEGORY(); #define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE FHoudiniEngineBakedActor::FHoudiniEngineBakedActor() : Actor(nullptr) , OutputIndex(INDEX_NONE) , OutputObjectIdentifier() , ActorBakeName(NAME_None) , BakedObject(nullptr) , SourceObject(nullptr) , BakeFolderPath() , bInstancerOutput(false) , bPostBakeProcessPostponed(false) { } FHoudiniEngineBakedActor::FHoudiniEngineBakedActor( AActor* InActor, FName InActorBakeName, FName InWorldOutlinerFolder, int32 InOutputIndex, const FHoudiniOutputObjectIdentifier& InOutputObjectIdentifier, UObject* InBakedObject, UObject* InSourceObject, UObject* InBakedComponent, const FString& InBakeFolderPath, const FHoudiniPackageParams& InBakedObjectPackageParams) : Actor(InActor) , OutputIndex(InOutputIndex) , OutputObjectIdentifier(InOutputObjectIdentifier) , ActorBakeName(InActorBakeName) , WorldOutlinerFolder(InWorldOutlinerFolder) , BakedObject(InBakedObject) , SourceObject(InSourceObject) , BakedComponent(InBakedComponent) , BakeFolderPath(InBakeFolderPath) , BakedObjectPackageParams(InBakedObjectPackageParams) , bInstancerOutput(false) , bPostBakeProcessPostponed(false) { } bool FHoudiniEngineBakeUtils::BakeHoudiniAssetComponent( UHoudiniAssetComponent* InHACToBake, bool bInReplacePreviousBake, EHoudiniEngineBakeOption InBakeOption, bool bInRemoveHACOutputOnSuccess, bool bInRecenterBakedActors) { if (!IsValid(InHACToBake)) return false; // Handle proxies: if the output has any current proxies, first refine them bool bHACNeedsToReCook; if (!CheckForAndRefineHoudiniProxyMesh(InHACToBake, bInReplacePreviousBake, InBakeOption, bInRemoveHACOutputOnSuccess, bInRecenterBakedActors, bHACNeedsToReCook)) { // Either the component is invalid, or needs a recook to refine a proxy mesh return false; } bool bSuccess = false; switch (InBakeOption) { case EHoudiniEngineBakeOption::ToActor: { bSuccess = FHoudiniEngineBakeUtils::BakeHoudiniActorToActors(InHACToBake, bInReplacePreviousBake, bInReplacePreviousBake, bInRecenterBakedActors); } break; case EHoudiniEngineBakeOption::ToBlueprint: { bSuccess = FHoudiniEngineBakeUtils::BakeBlueprints(InHACToBake, bInReplacePreviousBake, bInRecenterBakedActors); } break; case EHoudiniEngineBakeOption::ToFoliage: { TMap AlreadyBakedMaterialsMap; bSuccess = FHoudiniEngineBakeUtils::BakeHoudiniActorToFoliage(InHACToBake, bInReplacePreviousBake, AlreadyBakedMaterialsMap); } break; case EHoudiniEngineBakeOption::ToWorldOutliner: { //Todo bSuccess = false; } break; } if (bSuccess && bInRemoveHACOutputOnSuccess) { TArray DeferredClearOutputs; FHoudiniOutputTranslator::ClearAndRemoveOutputs(InHACToBake, DeferredClearOutputs, true); } return bSuccess; } bool FHoudiniEngineBakeUtils::BakeHoudiniActorToActors( UHoudiniAssetComponent* HoudiniAssetComponent, bool bInReplaceActors, bool bInReplaceAssets, bool bInRecenterBakedActors) { if (!HoudiniAssetComponent || HoudiniAssetComponent->IsPendingKill()) return false; TArray NewActors; TArray PackagesToSave; FHoudiniEngineOutputStats BakeStats; const bool bBakedWithErrors = !FHoudiniEngineBakeUtils::BakeHoudiniActorToActors( HoudiniAssetComponent, bInReplaceActors, bInReplaceAssets, NewActors, PackagesToSave, BakeStats); if (bBakedWithErrors) { // TODO ? HOUDINI_LOG_WARNING(TEXT("Errors when baking")); } // Save the created packages FHoudiniEngineBakeUtils::SaveBakedPackages(PackagesToSave); // Recenter and select the baked actors if (GEditor && NewActors.Num() > 0) GEditor->SelectNone(false, true); for (const FHoudiniEngineBakedActor& Entry : NewActors) { if (!IsValid(Entry.Actor)) continue; if (bInRecenterBakedActors) CenterActorToBoundingBoxCenter(Entry.Actor); if (GEditor) GEditor->SelectActor(Entry.Actor, true, false); } if (GEditor && NewActors.Num() > 0) GEditor->NoteSelectionChange(); { const FString FinishedTemplate = TEXT("Baking finished. Created {0} packages. Updated {1} packages."); FString Msg = FString::Format(*FinishedTemplate, { BakeStats.NumPackagesCreated, BakeStats.NumPackagesUpdated } ); FHoudiniEngine::Get().FinishTaskSlateNotification( FText::FromString(Msg) ); } // Broadcast that the bake is complete HoudiniAssetComponent->HandleOnPostBake(!bBakedWithErrors); return true; } bool FHoudiniEngineBakeUtils::BakeHoudiniActorToActors( UHoudiniAssetComponent* HoudiniAssetComponent, bool bInReplaceActors, bool bInReplaceAssets, TArray& OutNewActors, TArray& OutPackagesToSave, FHoudiniEngineOutputStats& OutBakeStats, TArray const* InOutputTypesToBake, TArray const* InInstancerComponentTypesToBake, AActor* InFallbackActor, const FString& InFallbackWorldOutlinerFolder) { if (!HoudiniAssetComponent || HoudiniAssetComponent->IsPendingKill()) return false; AActor* OwnerActor = HoudiniAssetComponent->GetOwner(); if (!IsValid(OwnerActor)) return false; const FString HoudiniAssetName = OwnerActor->GetName(); // Get an array of the outputs const int32 NumOutputs = HoudiniAssetComponent->GetNumOutputs(); TArray Outputs; Outputs.Reserve(NumOutputs); for (int32 OutputIdx = 0; OutputIdx < NumOutputs; ++OutputIdx) { Outputs.Add(HoudiniAssetComponent->GetOutputAt(OutputIdx)); } // Get the previous bake objects and grow/shrink to match asset outputs TArray& BakedOutputs = HoudiniAssetComponent->GetBakedOutputs(); // Ensure we have an entry for each output if (BakedOutputs.Num() != NumOutputs) BakedOutputs.SetNum(NumOutputs); return BakeHoudiniOutputsToActors( HoudiniAssetComponent, Outputs, BakedOutputs, HoudiniAssetName, HoudiniAssetComponent->GetComponentTransform(), HoudiniAssetComponent->BakeFolder, HoudiniAssetComponent->TemporaryCookFolder, bInReplaceActors, bInReplaceAssets, OutNewActors, OutPackagesToSave, OutBakeStats, InOutputTypesToBake, InInstancerComponentTypesToBake, InFallbackActor, InFallbackWorldOutlinerFolder); } bool FHoudiniEngineBakeUtils::BakeHoudiniOutputsToActors( const UHoudiniAssetComponent* HoudiniAssetComponent, const TArray& InOutputs, TArray& InBakedOutputs, const FString& InHoudiniAssetName, const FTransform& InParentTransform, const FDirectoryPath& InBakeFolder, const FDirectoryPath& InTempCookFolder, bool bInReplaceActors, bool bInReplaceAssets, TArray& OutNewActors, TArray& OutPackagesToSave, FHoudiniEngineOutputStats& OutBakeStats, TArray const* InOutputTypesToBake, TArray const* InInstancerComponentTypesToBake, AActor* InFallbackActor, const FString& InFallbackWorldOutlinerFolder) { const int32 NumOutputs = InOutputs.Num(); const FString MsgTemplate = TEXT("Baking output: {0}/{1}."); FString Msg = FString::Format(*MsgTemplate, { 0, NumOutputs }); FHoudiniEngine::Get().CreateTaskSlateNotification(FText::FromString(Msg)); TArray BakedActors; // Ensure that InBakedOutputs is the same size as InOutputs if (InBakedOutputs.Num() != NumOutputs) InBakedOutputs.SetNum(NumOutputs); // First bake everything except instancers, then bake instancers. Since instancers might use meshes in // from the other outputs. bool bHasAnyInstancers = false; int32 NumProcessedOutputs = 0; TMap AlreadyBakedMaterialsMap; for (int32 OutputIdx = 0; OutputIdx < NumOutputs; ++OutputIdx) { UHoudiniOutput* Output = InOutputs[OutputIdx]; if (!Output || Output->IsPendingKill()) { NumProcessedOutputs++; continue; } Msg = FString::Format(*MsgTemplate, { NumProcessedOutputs + 1, NumOutputs }); FHoudiniEngine::Get().UpdateTaskSlateNotification(FText::FromString(Msg)); const EHoudiniOutputType OutputType = Output->GetType(); // Check if we should skip this output type if (InOutputTypesToBake && InOutputTypesToBake->Find(OutputType) == INDEX_NONE) { NumProcessedOutputs++; continue; } switch (OutputType) { case EHoudiniOutputType::Mesh: { FHoudiniEngineBakeUtils::BakeStaticMeshOutputToActors( HoudiniAssetComponent, OutputIdx, InOutputs, InBakedOutputs, InHoudiniAssetName, InBakeFolder, InTempCookFolder, bInReplaceActors, bInReplaceAssets, BakedActors, OutPackagesToSave, AlreadyBakedMaterialsMap, InFallbackActor, InFallbackWorldOutlinerFolder); } break; case EHoudiniOutputType::Instancer: { if (!bHasAnyInstancers) bHasAnyInstancers = true; NumProcessedOutputs--; } break; case EHoudiniOutputType::Landscape: { const bool bResult = BakeLandscape( HoudiniAssetComponent, OutputIdx, InOutputs, InBakedOutputs, bInReplaceActors, bInReplaceAssets, InBakeFolder.Path, InHoudiniAssetName, OutBakeStats); } break; case EHoudiniOutputType::Skeletal: break; case EHoudiniOutputType::Curve: { FHoudiniEngineBakeUtils::BakeHoudiniCurveOutputToActors( HoudiniAssetComponent, OutputIdx, InOutputs, InBakedOutputs, InHoudiniAssetName, InBakeFolder, bInReplaceActors, bInReplaceAssets, BakedActors, InFallbackActor, InFallbackWorldOutlinerFolder); } break; case EHoudiniOutputType::Invalid: break; } NumProcessedOutputs++; } if (bHasAnyInstancers) { for (int32 OutputIdx = 0; OutputIdx < NumOutputs; ++OutputIdx) { UHoudiniOutput* Output = InOutputs[OutputIdx]; if (!Output || Output->IsPendingKill()) { NumProcessedOutputs++; continue; } Msg = FString::Format(*MsgTemplate, { NumProcessedOutputs + 1, NumOutputs }); FHoudiniEngine::Get().UpdateTaskSlateNotification(FText::FromString(Msg)); if (Output->GetType() == EHoudiniOutputType::Instancer) { FHoudiniEngineBakeUtils::BakeInstancerOutputToActors( HoudiniAssetComponent, OutputIdx, InOutputs, InBakedOutputs, InParentTransform, InHoudiniAssetName, InBakeFolder, InTempCookFolder, bInReplaceActors, bInReplaceAssets, BakedActors, OutPackagesToSave, AlreadyBakedMaterialsMap, InInstancerComponentTypesToBake, InFallbackActor, InFallbackWorldOutlinerFolder); } NumProcessedOutputs++; } } // Only do the post bake post-process once per Actor TSet UniqueActors; for (FHoudiniEngineBakedActor& BakedActor : BakedActors) { if (BakedActor.bPostBakeProcessPostponed && BakedActor.Actor) { BakedActor.bPostBakeProcessPostponed = false; AActor* Actor = BakedActor.Actor; bool bIsAlreadyInSet = false; UniqueActors.Add(Actor, &bIsAlreadyInSet); if (!bIsAlreadyInSet) { Actor->InvalidateLightingCache(); Actor->PostEditMove(true); Actor->MarkPackageDirty(); } } } OutNewActors.Append(BakedActors); return true; } bool FHoudiniEngineBakeUtils::BakeInstancerOutputToFoliage( const UHoudiniAssetComponent* HoudiniAssetComponent, int32 InOutputIndex, const TArray& InAllOutputs, const FHoudiniOutputObjectIdentifier& InOutputObjectIdentifier, const FHoudiniOutputObject& InOutputObject, FHoudiniBakedOutputObject& InBakedOutputObject, const FString& InHoudiniAssetName, const FDirectoryPath& InBakeFolder, const FDirectoryPath& InTempCookFolder, bool bInReplaceActors, bool bInReplaceAssets, TArray& OutActors, TArray& OutPackagesToSave, TMap& InOutAlreadyBakedMaterialsMap) { UHoudiniOutput* Output = InAllOutputs[InOutputIndex]; if (!Output || Output->IsPendingKill()) return false; if (Output->GetType() != EHoudiniOutputType::Instancer) return false; if (!IsValid(InOutputObject.OutputComponent)) return false; UStaticMeshComponent* SMC = Cast(InOutputObject.OutputComponent); if (!IsValid(SMC)) { HOUDINI_LOG_WARNING( TEXT("Unsupported component for foliage: %s"),*(InOutputObject.OutputComponent->GetClass()->GetName())); return false; } UStaticMesh* InstancedStaticMesh = SMC->GetStaticMesh(); if (!IsValid(InstancedStaticMesh)) { // No mesh, skip this instancer return false; } const EPackageReplaceMode AssetPackageReplaceMode = bInReplaceAssets ? EPackageReplaceMode::ReplaceExistingAssets : EPackageReplaceMode::CreateNewAssets; UWorld* DesiredWorld = Output ? Output->GetWorld() : GWorld; // Determine if the incoming mesh is temporary by looking for it in the mesh outputs. Populate mesh package params // for baking from it. // If not temporary set the ObjectName from the its package. (Also use this as a fallback default) FString ObjectName = FHoudiniPackageParams::GetPackageNameExcludingGUID(InstancedStaticMesh); UStaticMesh* PreviousStaticMesh = Cast(InBakedOutputObject.GetBakedObjectIfValid()); UStaticMesh* BakedStaticMesh = nullptr; int32 MeshOutputIndex = INDEX_NONE; FHoudiniOutputObjectIdentifier MeshIdentifier; FHoudiniAttributeResolver MeshResolver; FHoudiniPackageParams MeshPackageParams; const bool bFoundMeshOutput = FindOutputObject(InstancedStaticMesh, EHoudiniOutputType::Mesh, InAllOutputs, MeshOutputIndex, MeshIdentifier); if (bFoundMeshOutput) { // Found the mesh in the mesh outputs, is temporary const FHoudiniOutputObject& MeshOutputObject = InAllOutputs[MeshOutputIndex]->GetOutputObjects().FindChecked(MeshIdentifier); FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( DesiredWorld, HoudiniAssetComponent, MeshIdentifier, MeshOutputObject, ObjectName, InHoudiniAssetName, MeshPackageParams, MeshResolver, InBakeFolder.Path, AssetPackageReplaceMode); // Update with resolved object name ObjectName = MeshPackageParams.ObjectName; // This will bake/duplicate the mesh if temporary, or return the input one if it is not BakedStaticMesh = FHoudiniEngineBakeUtils::DuplicateStaticMeshAndCreatePackageIfNeeded( InstancedStaticMesh, PreviousStaticMesh, MeshPackageParams, InAllOutputs, OutActors, InTempCookFolder.Path, OutPackagesToSave, InOutAlreadyBakedMaterialsMap); } else { BakedStaticMesh = InstancedStaticMesh; } // Update the baked object InBakedOutputObject.BakedObject = FSoftObjectPath(BakedStaticMesh).ToString(); // const FString InstancerName = FString::Printf(TEXT("%s_foliage_%s"), *ObjectName, *(InOutputObjectIdentifier.SplitIdentifier)); // Construct PackageParams for the instancer itself. When baking to actor we technically won't create a stand-alone // disk package for the instancer, but certain attributes (such as level path) use tokens populated from the // package params. FHoudiniPackageParams InstancerPackageParams; FHoudiniAttributeResolver InstancerResolver; FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( DesiredWorld, HoudiniAssetComponent, InOutputObjectIdentifier, InOutputObject, ObjectName, InHoudiniAssetName, InstancerPackageParams, InstancerResolver, InBakeFolder.Path, AssetPackageReplaceMode); // By default spawn in the current level unless specified via the unreal_level_path attribute ULevel* DesiredLevel = GWorld->GetCurrentLevel(); bool bHasLevelPathAttribute = InOutputObject.CachedAttributes.Contains(HAPI_UNREAL_ATTRIB_LEVEL_PATH); if (bHasLevelPathAttribute) { // Get the package path from the unreal_level_path attribute FString LevelPackagePath = InstancerResolver.ResolveFullLevelPath(); bool bCreatedPackage = false; if (!FHoudiniEngineBakeUtils::FindOrCreateDesiredLevelFromLevelPath( LevelPackagePath, DesiredLevel, DesiredWorld, bCreatedPackage)) { // TODO: LOG ERROR IF NO LEVEL HOUDINI_LOG_ERROR(TEXT("Could not find or create a level: %s"), *LevelPackagePath); return false; } // If we have created a new level, add it to the packages to save // TODO: ? always add? if (bCreatedPackage && DesiredLevel) { // We can now save the package again, and unload it. OutPackagesToSave.Add(DesiredLevel->GetOutermost()); } } if (!DesiredLevel) return false; // Get foliage actor for the level const bool bCreateIfNone = true; AInstancedFoliageActor* InstancedFoliageActor = AInstancedFoliageActor::GetInstancedFoliageActorForLevel(DesiredLevel, bCreateIfNone); if (!InstancedFoliageActor || InstancedFoliageActor->IsPendingKill()) { HOUDINI_LOG_ERROR(TEXT("Could not find or create an instanced foliage actor for level %s"), *(DesiredLevel->GetPathName())); return false; } // Get the previous bake data for this instancer InBakedOutputObject.BakedObject = FSoftObjectPath(BakedStaticMesh).ToString(); // Foliage type is replaced in replacement mode if: // the previous baked object is this foliage type // and we haven't bake this foliage type during this bake (BakeResults) // NOTE: foliage type is only recorded as the previous bake object if we created the foliage type // TODO: replacement mode should probably only affect the instances themselves and not the foliage type // since the foliage type is already linked to whatever mesh we are using (which will be replaced // incremented already). To track instances it looks like we would have to use the locations of the // baked instances (likely cannot use the indices, since the user might modify/add/remove instances // after the bake). // See if we already have a FoliageType for that static mesh UFoliageType* FoliageType = InstancedFoliageActor->GetLocalFoliageTypeForSource(BakedStaticMesh); if (!FoliageType || FoliageType->IsPendingKill()) { // We need to create a new FoliageType for this Static Mesh // TODO: Add foliage default settings InstancedFoliageActor->AddMesh(BakedStaticMesh, &FoliageType); // Update the previous bake results with the foliage type we created InBakedOutputObject.BakedComponent = FSoftObjectPath(FoliageType).ToString(); } else { const FString FoliageTypePath = FSoftObjectPath(FoliageType).ToString(); if (bInReplaceAssets && InBakedOutputObject.BakedComponent == FoliageTypePath && !OutActors.FindByPredicate([FoliageType](const FHoudiniEngineBakedActor& Entry) { return Entry.BakedComponent == FoliageType; })) { InstancedFoliageActor->RemoveFoliageType(&FoliageType, 1); // Update the previous bake results with the foliage type InBakedOutputObject.BakedComponent = FoliageTypePath; } else { // If we didn't create the foliage type, don't set the baked component InBakedOutputObject.BakedComponent.Empty(); } } // Record the foliage bake in the current results FHoudiniEngineBakedActor& NewResult = OutActors.Add_GetRef(FHoudiniEngineBakedActor()); NewResult.OutputIndex = InOutputIndex; NewResult.OutputObjectIdentifier = InOutputObjectIdentifier; NewResult.SourceObject = InstancedStaticMesh; NewResult.BakedObject = BakedStaticMesh; NewResult.BakedComponent = FoliageType; // Get the FoliageMeshInfo for this Foliage type so we can add the instance to it FFoliageInfo* FoliageInfo = InstancedFoliageActor->FindOrAddMesh(FoliageType); if (!FoliageInfo) return false; int32 CurrentInstanceCount = 0; if (SMC->IsA()) { UInstancedStaticMeshComponent* ISMC = Cast(SMC); const int32 NumInstances = ISMC->GetInstanceCount(); for (int32 InstanceIndex = 0; InstanceIndex < NumInstances; ++InstanceIndex) { FTransform InstanceTransform; const bool bWorldSpace = true; if (ISMC->GetInstanceTransform(InstanceIndex, InstanceTransform, bWorldSpace)) { FFoliageInstance FoliageInstance; FoliageInstance.Location = InstanceTransform.GetLocation(); FoliageInstance.Rotation = InstanceTransform.GetRotation().Rotator(); FoliageInstance.DrawScale3D = InstanceTransform.GetScale3D(); FoliageInfo->AddInstance(InstancedFoliageActor, FoliageType, FoliageInstance); CurrentInstanceCount++; } } } else { const FTransform ComponentToWorldTransform = SMC->GetComponentToWorld(); FFoliageInstance FoliageInstance; FoliageInstance.Location = ComponentToWorldTransform.GetLocation(); FoliageInstance.Rotation = ComponentToWorldTransform.GetRotation().Rotator(); FoliageInstance.DrawScale3D = ComponentToWorldTransform.GetScale3D(); FoliageInfo->AddInstance(InstancedFoliageActor, FoliageType, FoliageInstance); CurrentInstanceCount++; } // TODO: This was due to a bug in UE4.22-20, check if still needed! if (FoliageInfo->GetComponent()) FoliageInfo->GetComponent()->BuildTreeIfOutdated(true, true); // Notify the user that we succesfully bake the instances to foliage FString Notification = TEXT("Successfully baked ") + FString::FromInt(CurrentInstanceCount) + TEXT(" instances of ") + BakedStaticMesh->GetName() + TEXT(" to Foliage"); FHoudiniEngineUtils::CreateSlateNotification(Notification); InstancedFoliageActor->RegisterAllComponents(); // Update / repopulate the foliage editor mode's mesh list if (CurrentInstanceCount > 0) FHoudiniEngineUtils::RepopulateFoliageTypeListInUI(); return true; } bool FHoudiniEngineBakeUtils::CanHoudiniAssetComponentBakeToFoliage(UHoudiniAssetComponent* HoudiniAssetComponent) { if (!HoudiniAssetComponent || HoudiniAssetComponent->IsPendingKill()) return false; for (int32 n = 0; n < HoudiniAssetComponent->GetNumOutputs(); ++n) { UHoudiniOutput* Output = HoudiniAssetComponent->GetOutputAt(n); if (!Output || Output->IsPendingKill()) continue; if (Output->GetType() != EHoudiniOutputType::Instancer) continue; if (Output->GetInstancedOutputs().Num() > 0) return true; /* // TODO: Is this needed? check we have components to bake? for (auto& OutputObjectPair : Output->GetOutputObjects()) { if (OutputObjectPair.Value.OutputCompoent!= nullpt) return true; } */ } return false; } bool FHoudiniEngineBakeUtils::BakeHoudiniActorToFoliage(UHoudiniAssetComponent* HoudiniAssetComponent, bool bInReplaceAssets, TMap& InOutAlreadyBakedMaterialsMap) { if (!HoudiniAssetComponent || HoudiniAssetComponent->IsPendingKill()) return false; AActor * OwnerActor = HoudiniAssetComponent->GetOwner(); if (!OwnerActor || OwnerActor->IsPendingKill()) return false; TArray PackagesToSave; TArray BakedResults; FTransform HoudiniAssetTransform = HoudiniAssetComponent->GetComponentTransform(); const FString HoudiniAssetName = OwnerActor->GetName(); // Build an array of the outputs so that we can search for meshes/previous baked meshes TArray Outputs; HoudiniAssetComponent->GetOutputs(Outputs); const int32 NumOutputs = HoudiniAssetComponent->GetNumOutputs(); // Get the previous bake outputs and match the output array size TArray& BakedOutputs = HoudiniAssetComponent->GetBakedOutputs(); if (BakedOutputs.Num() != NumOutputs) BakedOutputs.SetNum(NumOutputs); bool bSuccess = true; // Map storing original and baked Static Meshes for (int32 OutputIdx = 0; OutputIdx < NumOutputs; OutputIdx++) { UHoudiniOutput* Output = Outputs[OutputIdx]; if (!Output || Output->IsPendingKill()) continue; if (Output->GetType() != EHoudiniOutputType::Instancer) continue; TMap& OutputObjects = Output->GetOutputObjects(); const TMap& OldBakedOutputObjects = BakedOutputs[OutputIdx].BakedOutputObjects; TMap NewBakedOutputObjects; for (auto & Pair : OutputObjects) { const FHoudiniOutputObjectIdentifier& Identifier = Pair.Key; FHoudiniOutputObject& OutputObject = Pair.Value; FHoudiniBakedOutputObject& BakedOutputObject = NewBakedOutputObjects.Add(Identifier); if (OldBakedOutputObjects.Contains(Identifier)) BakedOutputObject = OldBakedOutputObjects.FindChecked(Identifier); const bool bInReplaceActors = false; bSuccess &= BakeInstancerOutputToFoliage( HoudiniAssetComponent, OutputIdx, Outputs, Identifier, OutputObject, BakedOutputObject, HoudiniAssetName, HoudiniAssetComponent->BakeFolder, HoudiniAssetComponent->TemporaryCookFolder, bInReplaceActors, bInReplaceAssets, BakedResults, PackagesToSave, InOutAlreadyBakedMaterialsMap); } // Update the cached baked output data BakedOutputs[OutputIdx].BakedOutputObjects = NewBakedOutputObjects; } if (PackagesToSave.Num() > 0) { FHoudiniEngineBakeUtils::SaveBakedPackages(PackagesToSave); } // Broadcast that the bake is complete HoudiniAssetComponent->HandleOnPostBake(bSuccess); return bSuccess; } bool FHoudiniEngineBakeUtils::BakeInstancerOutputToActors( const UHoudiniAssetComponent* HoudiniAssetComponent, int32 InOutputIndex, const TArray& InAllOutputs, TArray& InBakedOutputs, const FTransform& InTransform, const FString& InHoudiniAssetName, const FDirectoryPath& InBakeFolder, const FDirectoryPath& InTempCookFolder, bool bInReplaceActors, bool bInReplaceAssets, TArray& OutActors, TArray& OutPackagesToSave, TMap& InOutAlreadyBakedMaterialsMap, TArray const* InInstancerComponentTypesToBake, AActor* InFallbackActor, const FString& InFallbackWorldOutlinerFolder) { if (!InAllOutputs.IsValidIndex(InOutputIndex)) return false; UHoudiniOutput* InOutput = InAllOutputs[InOutputIndex]; if (!InOutput || InOutput->IsPendingKill()) return false; // Ensure we have the same number of baked outputs and asset outputs if (InBakedOutputs.Num() != InAllOutputs.Num()) InBakedOutputs.SetNum(InAllOutputs.Num()); TMap& OutputObjects = InOutput->GetOutputObjects(); const TMap& OldBakedOutputObjects = InBakedOutputs[InOutputIndex].BakedOutputObjects; TMap NewBakedOutputObjects; // Iterate on the output objects, baking their object/component as we go for (auto& Pair : OutputObjects) { const FHoudiniOutputObjectIdentifier& Identifier = Pair.Key; FHoudiniOutputObject& CurrentOutputObject = Pair.Value; FHoudiniBakedOutputObject& BakedOutputObject = NewBakedOutputObjects.Add(Identifier); if (OldBakedOutputObjects.Contains(Identifier)) BakedOutputObject = OldBakedOutputObjects.FindChecked(Identifier); if (CurrentOutputObject.bProxyIsCurrent) { // TODO: we need to refine the SM first! // ?? } if (!CurrentOutputObject.OutputComponent || CurrentOutputObject.OutputComponent->IsPendingKill()) continue; if (CurrentOutputObject.OutputComponent->IsA()) { // Bake foliage as foliage if (!InInstancerComponentTypesToBake || InInstancerComponentTypesToBake->Contains(EHoudiniInstancerComponentType::FoliageInstancedStaticMeshComponent)) { BakeInstancerOutputToFoliage( HoudiniAssetComponent, InOutputIndex, InAllOutputs, // InBakedOutputs, Pair.Key, CurrentOutputObject, BakedOutputObject, InHoudiniAssetName, InBakeFolder, InTempCookFolder, bInReplaceActors, bInReplaceAssets, OutActors, OutPackagesToSave, InOutAlreadyBakedMaterialsMap); } else if (!InInstancerComponentTypesToBake || InInstancerComponentTypesToBake->Contains(EHoudiniInstancerComponentType::FoliageAsHierarchicalInstancedStaticMeshComponent)) { BakeInstancerOutputToActors_ISMC( HoudiniAssetComponent, InOutputIndex, InAllOutputs, // InBakedOutputs, Pair.Key, CurrentOutputObject, BakedOutputObject, InTransform, InBakeFolder, InTempCookFolder, bInReplaceActors, bInReplaceAssets, OutActors, OutPackagesToSave, InOutAlreadyBakedMaterialsMap, InFallbackActor, InFallbackWorldOutlinerFolder); } } else if (CurrentOutputObject.OutputComponent->IsA() && (!InInstancerComponentTypesToBake || InInstancerComponentTypesToBake->Contains(EHoudiniInstancerComponentType::InstancedStaticMeshComponent))) { BakeInstancerOutputToActors_ISMC( HoudiniAssetComponent, InOutputIndex, InAllOutputs, // InBakedOutputs, Pair.Key, CurrentOutputObject, BakedOutputObject, InTransform, InBakeFolder, InTempCookFolder, bInReplaceActors, bInReplaceAssets, OutActors, OutPackagesToSave, InOutAlreadyBakedMaterialsMap, InFallbackActor, InFallbackWorldOutlinerFolder); } else if (CurrentOutputObject.OutputComponent->IsA() && (!InInstancerComponentTypesToBake || InInstancerComponentTypesToBake->Contains(EHoudiniInstancerComponentType::InstancedActorComponent))) { BakeInstancerOutputToActors_IAC( HoudiniAssetComponent, InOutputIndex, Pair.Key, CurrentOutputObject, BakedOutputObject, InBakeFolder, bInReplaceActors, bInReplaceAssets, OutActors, OutPackagesToSave); } else if (CurrentOutputObject.OutputComponent->IsA() && (!InInstancerComponentTypesToBake || InInstancerComponentTypesToBake->Contains(EHoudiniInstancerComponentType::MeshSplitInstancerComponent))) { BakeInstancerOutputToActors_MSIC( HoudiniAssetComponent, InOutputIndex, InAllOutputs, // InBakedOutputs, Pair.Key, CurrentOutputObject, BakedOutputObject, InTransform, InBakeFolder, InTempCookFolder, bInReplaceActors, bInReplaceAssets, OutActors, OutPackagesToSave, InOutAlreadyBakedMaterialsMap, InFallbackActor, InFallbackWorldOutlinerFolder); } else if (CurrentOutputObject.OutputComponent->IsA() && (!InInstancerComponentTypesToBake || InInstancerComponentTypesToBake->Contains(EHoudiniInstancerComponentType::StaticMeshComponent))) { BakeInstancerOutputToActors_SMC( HoudiniAssetComponent, InOutputIndex, InAllOutputs, // InBakedOutputs, Pair.Key, CurrentOutputObject, BakedOutputObject, InBakeFolder, InTempCookFolder, bInReplaceActors, bInReplaceAssets, OutActors, OutPackagesToSave, InOutAlreadyBakedMaterialsMap, InFallbackActor, InFallbackWorldOutlinerFolder); } else { // Unsupported component! } } // Update the cached baked output data InBakedOutputs[InOutputIndex].BakedOutputObjects = NewBakedOutputObjects; return true; } bool FHoudiniEngineBakeUtils::BakeInstancerOutputToActors_ISMC( const UHoudiniAssetComponent* HoudiniAssetComponent, int32 InOutputIndex, const TArray& InAllOutputs, // const TArray& InAllBakedOutputs, const FHoudiniOutputObjectIdentifier& InOutputObjectIdentifier, const FHoudiniOutputObject& InOutputObject, FHoudiniBakedOutputObject& InBakedOutputObject, const FTransform& InTransform, const FDirectoryPath& InBakeFolder, const FDirectoryPath& InTempCookFolder, bool bInReplaceActors, bool bInReplaceAssets, TArray& OutActors, TArray& OutPackagesToSave, TMap& InOutAlreadyBakedMaterialsMap, AActor* InFallbackActor, const FString& InFallbackWorldOutlinerFolder) { UInstancedStaticMeshComponent * InISMC = Cast(InOutputObject.OutputComponent); if (!InISMC || InISMC->IsPendingKill()) return false; AActor * OwnerActor = InISMC->GetOwner(); if (!OwnerActor || OwnerActor->IsPendingKill()) return false; UStaticMesh * StaticMesh = InISMC->GetStaticMesh(); if (!StaticMesh || StaticMesh->IsPendingKill()) return false; // Certain SMC materials may need to be duplicated if we didn't generate the mesh object. TArray DuplicatedISMCOverrideMaterials; const EPackageReplaceMode AssetPackageReplaceMode = bInReplaceAssets ? EPackageReplaceMode::ReplaceExistingAssets : EPackageReplaceMode::CreateNewAssets; UWorld* DesiredWorld = OwnerActor ? OwnerActor->GetWorld() : GWorld; // Determine if the incoming mesh is temporary by looking for it in the mesh outputs. Populate mesh package params // for baking from it. // If not temporary set the ObjectName from the its package. (Also use this as a fallback default) FString ObjectName = FHoudiniPackageParams::GetPackageNameExcludingGUID(StaticMesh); UStaticMesh* PreviousStaticMesh = Cast(InBakedOutputObject.GetBakedObjectIfValid()); UStaticMesh* BakedStaticMesh = nullptr; int32 MeshOutputIndex = INDEX_NONE; FHoudiniOutputObjectIdentifier MeshIdentifier; FHoudiniAttributeResolver MeshResolver; FHoudiniPackageParams MeshPackageParams; // Construct PackageParams for the instancer itself. When baking to actor we technically won't create a stand-alone // disk package for the instancer, but certain attributes (such as level path) use tokens populated from the // package params. FHoudiniPackageParams InstancerPackageParams; FHoudiniAttributeResolver InstancerResolver; FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( DesiredWorld, HoudiniAssetComponent, InOutputObjectIdentifier, InOutputObject, ObjectName, OwnerActor->GetName(), InstancerPackageParams, InstancerResolver, InBakeFolder.Path, AssetPackageReplaceMode); const bool bFoundMeshOutput = FindOutputObject(StaticMesh, EHoudiniOutputType::Mesh, InAllOutputs, MeshOutputIndex, MeshIdentifier); if (bFoundMeshOutput) { // Found the mesh in the mesh outputs, is temporary const FHoudiniOutputObject& MeshOutputObject = InAllOutputs[MeshOutputIndex]->GetOutputObjects().FindChecked(MeshIdentifier); FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( DesiredWorld, HoudiniAssetComponent, MeshIdentifier, MeshOutputObject, ObjectName, OwnerActor->GetName(), MeshPackageParams, MeshResolver, InBakeFolder.Path, AssetPackageReplaceMode); // Update with resolved object name ObjectName = MeshPackageParams.ObjectName; // This will bake/duplicate the mesh if temporary, or return the input one if it is not BakedStaticMesh = FHoudiniEngineBakeUtils::DuplicateStaticMeshAndCreatePackageIfNeeded( StaticMesh, PreviousStaticMesh, MeshPackageParams, InAllOutputs, OutActors, InTempCookFolder.Path, OutPackagesToSave, InOutAlreadyBakedMaterialsMap); } else { BakedStaticMesh = StaticMesh; // We still need to duplicate materials, if they are temporary. TArray Materials = InISMC->GetMaterials(); for (int32 MaterialIdx = 0; MaterialIdx < Materials.Num(); ++MaterialIdx) { UMaterialInterface* MaterialInterface = Materials[MaterialIdx]; if (!MaterialInterface || MaterialInterface->IsPendingKill()) continue; // Only duplicate the material if it is temporary if (IsObjectTemporary(MaterialInterface, EHoudiniOutputType::Invalid, InAllOutputs, InTempCookFolder.Path)) { UMaterialInterface * DuplicatedMaterial = BakeSingleMaterialToPackage(MaterialInterface, InstancerPackageParams, OutPackagesToSave, InOutAlreadyBakedMaterialsMap); DuplicatedISMCOverrideMaterials.Add(DuplicatedMaterial); } } } // Update the baked object InBakedOutputObject.BakedObject = FSoftObjectPath(BakedStaticMesh).ToString(); // Instancer name adds the split identifier (INSTANCERNUM_VARIATIONNUM) const FString BaseName = OwnerActor->GetName(); const FString InstancerName = ObjectName + "_instancer_" + InOutputObjectIdentifier.SplitIdentifier; const FName WorldOutlinerFolderPath = GetOutlinerFolderPath(InOutputObject, FName(InFallbackWorldOutlinerFolder.IsEmpty() ? BaseName : InFallbackWorldOutlinerFolder)); // By default spawn in the current level unless specified via the unreal_level_path attribute ULevel* DesiredLevel = GWorld->GetCurrentLevel(); bool bHasLevelPathAttribute = InOutputObject.CachedAttributes.Contains(HAPI_UNREAL_ATTRIB_LEVEL_PATH); if (bHasLevelPathAttribute) { // Get the package path from the unreal_level_apth attribute FString LevelPackagePath = InstancerResolver.ResolveFullLevelPath(); bool bCreatedPackage = false; if (!FHoudiniEngineBakeUtils::FindOrCreateDesiredLevelFromLevelPath( LevelPackagePath, DesiredLevel, DesiredWorld, bCreatedPackage)) { // TODO: LOG ERROR IF NO LEVEL return false; } // If we have created a new level, add it to the packages to save // TODO: ? always add? if (bCreatedPackage && DesiredLevel) { // We can now save the package again, and unload it. OutPackagesToSave.Add(DesiredLevel->GetOutermost()); } } if(!DesiredLevel) return false; // Try to find the unreal_bake_actor, if specified, or fallback to the default named actor FName BakeActorName; AActor* FoundActor = nullptr; bool bHasBakeActorName = false; if (!FindUnrealBakeActor(InOutputObject, InBakedOutputObject, OutActors, DesiredLevel, *InstancerName, bInReplaceActors, InFallbackActor, FoundActor, bHasBakeActorName, BakeActorName)) return false; /* // TODO: Get the bake name! // Bake override, the output name // The bake name override has priority FString InstancerName = InOutputObject.BakeName; if (InstancerName.IsEmpty()) { // .. then use the output name InstancerName = Resolver.ResolveOutputName(); } */ // Should we create one actor with an ISMC or multiple actors with one SMC? bool bSpawnMultipleSMC = false; if (bSpawnMultipleSMC) { // TODO: Double check, Has a crash here! // Get the StaticMesh ActorFactory UActorFactory* SMFactory = nullptr; if (!FoundActor) { SMFactory = GEditor ? GEditor->FindActorFactoryByClass(UActorFactoryStaticMesh::StaticClass()) : nullptr; if (!SMFactory) return false; } // Split the instances to multiple StaticMeshActors for (int32 InstanceIdx = 0; InstanceIdx < InISMC->GetInstanceCount(); InstanceIdx++) { FTransform InstanceTransform; InISMC->GetInstanceTransform(InstanceIdx, InstanceTransform, true); if (!FoundActor) { FoundActor = SMFactory->CreateActor(BakedStaticMesh, DesiredLevel, InstanceTransform); if (!FoundActor || FoundActor->IsPendingKill()) continue; } const FString NewNameStr = MakeUniqueObjectNameIfNeeded(DesiredLevel, SMFactory->NewActorClass, BakeActorName.ToString(), FoundActor); RenameAndRelabelActor(FoundActor, NewNameStr, false); // The folder is named after the original actor and contains all generated actors SetOutlinerFolderPath(FoundActor, InOutputObject, WorldOutlinerFolderPath); AStaticMeshActor* SMActor = Cast(FoundActor); if (!SMActor || SMActor->IsPendingKill()) continue; // Copy properties from the existing component CopyPropertyToNewActorAndComponent(FoundActor, SMActor->GetStaticMeshComponent(), InISMC); FHoudiniEngineBakedActor& OutputEntry = OutActors.Add_GetRef(FHoudiniEngineBakedActor( FoundActor, BakeActorName, WorldOutlinerFolderPath, InOutputIndex, InOutputObjectIdentifier, BakedStaticMesh, StaticMesh, SMActor->GetStaticMeshComponent(), bFoundMeshOutput ? MeshPackageParams.BakeFolder : FString(), MeshPackageParams)); OutputEntry.bInstancerOutput = true; OutputEntry.InstancerPackageParams = InstancerPackageParams; } } else { bool bSpawnedActor = false; if (!FoundActor) { // Only create one actor FActorSpawnParameters SpawnInfo; SpawnInfo.OverrideLevel = DesiredLevel; SpawnInfo.ObjectFlags = RF_Transactional; SpawnInfo.Name = FName(MakeUniqueObjectNameIfNeeded(DesiredLevel, AActor::StaticClass(), BakeActorName.ToString())); SpawnInfo.bDeferConstruction = true; // Spawn the new Actor FoundActor = DesiredLevel->OwningWorld->SpawnActor(SpawnInfo); if (!FoundActor || FoundActor->IsPendingKill()) return false; bSpawnedActor = true; FHoudiniEngineRuntimeUtils::SetActorLabel(FoundActor, /*DesiredLevel->bUseExternalActors ? BakeActorName.ToString() : */FoundActor->GetName()); FoundActor->SetActorHiddenInGame(InISMC->bHiddenInGame); } else { // If there is a previously baked component, and we are in replace mode, remove it if (bInReplaceAssets) { USceneComponent* InPrevComponent = Cast(InBakedOutputObject.GetBakedComponentIfValid()); if (IsValid(InPrevComponent) && InPrevComponent->GetOwner() == FoundActor) RemovePreviouslyBakedComponent(InPrevComponent); } const FString UniqueActorNameStr = MakeUniqueObjectNameIfNeeded(DesiredLevel, AActor::StaticClass(), BakeActorName.ToString(), FoundActor); RenameAndRelabelActor(FoundActor, UniqueActorNameStr, false); } // The folder is named after the original actor and contains all generated actors SetOutlinerFolderPath(FoundActor, InOutputObject, WorldOutlinerFolderPath); // Get/create the actor's root component const bool bCreateIfMissing = true; USceneComponent* RootComponent = GetActorRootComponent(FoundActor, bCreateIfMissing); if (bSpawnedActor && IsValid(RootComponent)) RootComponent->SetWorldTransform(InTransform); // Duplicate the instancer component, create a Hierarchical ISMC if needed UInstancedStaticMeshComponent* NewISMC = nullptr; UHierarchicalInstancedStaticMeshComponent* InHISMC = Cast(InISMC); if (InHISMC) { // Handle foliage: don't duplicate foliage component, create a new hierarchical one and copy what we can // from the foliage component if (InHISMC->IsA()) { NewISMC = NewObject( FoundActor, FName(MakeUniqueObjectNameIfNeeded(FoundActor, InHISMC->GetClass(), InISMC->GetName()))); CopyPropertyToNewActorAndComponent(FoundActor, NewISMC, InISMC); } else { NewISMC = DuplicateObject( InHISMC, FoundActor, FName(MakeUniqueObjectNameIfNeeded(FoundActor, InHISMC->GetClass(), InISMC->GetName()))); } } else { NewISMC = DuplicateObject( InISMC, FoundActor, FName(MakeUniqueObjectNameIfNeeded(FoundActor, InISMC->GetClass(), InISMC->GetName()))); } if (!NewISMC) { //DesiredLevel->OwningWorld-> return false; } InBakedOutputObject.BakedComponent = FSoftObjectPath(NewISMC).ToString(); NewISMC->RegisterComponent(); // NewISMC->SetupAttachment(nullptr); NewISMC->SetStaticMesh(BakedStaticMesh); FoundActor->AddInstanceComponent(NewISMC); if (DuplicatedISMCOverrideMaterials.Num() > 0) { UMaterialInterface * InstancerMaterial = DuplicatedISMCOverrideMaterials[0]; if (InstancerMaterial) { NewISMC->OverrideMaterials.Empty(); int32 MeshMaterialCount = BakedStaticMesh->StaticMaterials.Num(); for (int32 Idx = 0; Idx < MeshMaterialCount; ++Idx) NewISMC->SetMaterial(Idx, InstancerMaterial); } } // NewActor->SetRootComponent(NewISMC); if (IsValid(RootComponent)) NewISMC->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform); NewISMC->SetWorldTransform(InISMC->GetComponentTransform()); // TODO: do we need to copy properties here, we duplicated the component // // Copy properties from the existing component // CopyPropertyToNewActorAndComponent(FoundActor, NewISMC, InISMC); if (bSpawnedActor) FoundActor->FinishSpawning(InTransform); InBakedOutputObject.Actor = FSoftObjectPath(FoundActor).ToString(); FHoudiniEngineBakedActor& OutputEntry = OutActors.Add_GetRef(FHoudiniEngineBakedActor( FoundActor, BakeActorName, WorldOutlinerFolderPath, InOutputIndex, InOutputObjectIdentifier, BakedStaticMesh, StaticMesh, NewISMC, bFoundMeshOutput ? MeshPackageParams.BakeFolder : FString(), MeshPackageParams)); OutputEntry.bInstancerOutput = true; OutputEntry.InstancerPackageParams = InstancerPackageParams; // Postpone post-bake calls to do them once per actor OutActors.Last().bPostBakeProcessPostponed = true; } // If we are baking in replace mode, remove previously baked components/instancers if (bInReplaceActors && bInReplaceAssets) { const bool bInDestroyBakedComponent = false; const bool bInDestroyBakedInstancedActors = true; const bool bInDestroyBakedInstancedComponents = true; DestroyPreviousBakeOutput( InBakedOutputObject, bInDestroyBakedComponent, bInDestroyBakedInstancedActors, bInDestroyBakedInstancedComponents); } return true; } bool FHoudiniEngineBakeUtils::BakeInstancerOutputToActors_SMC( const UHoudiniAssetComponent* HoudiniAssetComponent, int32 InOutputIndex, const TArray& InAllOutputs, // const TArray& InAllBakedOutputs, const FHoudiniOutputObjectIdentifier& InOutputObjectIdentifier, const FHoudiniOutputObject& InOutputObject, FHoudiniBakedOutputObject& InBakedOutputObject, const FDirectoryPath& InBakeFolder, const FDirectoryPath& InTempCookFolder, bool bInReplaceActors, bool bInReplaceAssets, TArray& OutActors, TArray& OutPackagesToSave, TMap& InOutAlreadyBakedMaterialsMap, AActor* InFallbackActor, const FString& InFallbackWorldOutlinerFolder) { UStaticMeshComponent* InSMC = Cast(InOutputObject.OutputComponent); if (!InSMC || InSMC->IsPendingKill()) return false; AActor* OwnerActor = InSMC->GetOwner(); if (!OwnerActor || OwnerActor->IsPendingKill()) return false; UStaticMesh* StaticMesh = InSMC->GetStaticMesh(); if (!StaticMesh || StaticMesh->IsPendingKill()) return false; UWorld* DesiredWorld = OwnerActor ? OwnerActor->GetWorld() : GWorld; const EPackageReplaceMode AssetPackageReplaceMode = bInReplaceAssets ? EPackageReplaceMode::ReplaceExistingAssets : EPackageReplaceMode::CreateNewAssets; // Certain SMC materials may need to be duplicated if we didn't generate the mesh object. TArray DuplicatedSMCOverrideMaterials; // Determine if the incoming mesh is temporary by looking for it in the mesh outputs. Populate mesh package params // for baking from it. // If not temporary set the ObjectName from the its package. (Also use this as a fallback default) FString ObjectName = FHoudiniPackageParams::GetPackageNameExcludingGUID(StaticMesh); UStaticMesh* PreviousStaticMesh = Cast(InBakedOutputObject.GetBakedObjectIfValid()); UStaticMesh* BakedStaticMesh = nullptr; int32 MeshOutputIndex = INDEX_NONE; FHoudiniOutputObjectIdentifier MeshIdentifier; FHoudiniAttributeResolver MeshResolver; FHoudiniPackageParams MeshPackageParams; // Package params for the instancer // See if the instanced static mesh is still a temporary Houdini created Static Mesh // If it is, we need to bake the StaticMesh first FHoudiniPackageParams InstancerPackageParams; // Configure FHoudiniAttributeResolver and fill the package params with resolved object name and bake folder. // The resolver is then also configured with the package params for subsequent resolving (level_path etc) FHoudiniAttributeResolver InstancerResolver; FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( DesiredWorld, HoudiniAssetComponent, InOutputObjectIdentifier, InOutputObject, ObjectName, OwnerActor->GetName(), InstancerPackageParams, InstancerResolver, InBakeFolder.Path, AssetPackageReplaceMode); const bool bFoundMeshOutput = FindOutputObject(StaticMesh, EHoudiniOutputType::Mesh, InAllOutputs, MeshOutputIndex, MeshIdentifier); if (bFoundMeshOutput) { // Found the mesh in the mesh outputs, is temporary const FHoudiniOutputObject& MeshOutputObject = InAllOutputs[MeshOutputIndex]->GetOutputObjects().FindChecked(MeshIdentifier); FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( DesiredWorld, HoudiniAssetComponent, MeshIdentifier, MeshOutputObject, ObjectName, OwnerActor->GetName(), MeshPackageParams, MeshResolver, InBakeFolder.Path, AssetPackageReplaceMode); // Update with resolved object name ObjectName = MeshPackageParams.ObjectName; // This will bake/duplicate the mesh if temporary, or return the input one if it is not BakedStaticMesh = FHoudiniEngineBakeUtils::DuplicateStaticMeshAndCreatePackageIfNeeded( StaticMesh, PreviousStaticMesh, MeshPackageParams, InAllOutputs, OutActors, InTempCookFolder.Path, OutPackagesToSave, InOutAlreadyBakedMaterialsMap); } else { BakedStaticMesh = StaticMesh; // We still need to duplicate materials, if they are temporary. TArray Materials = InSMC->GetMaterials(); for (int32 MaterialIdx = 0; MaterialIdx < Materials.Num(); ++MaterialIdx) { UMaterialInterface* MaterialInterface = Materials[MaterialIdx]; if (!MaterialInterface || MaterialInterface->IsPendingKill()) continue; // Only duplicate the material if it is temporary if (IsObjectTemporary(MaterialInterface, EHoudiniOutputType::Invalid, InAllOutputs, InTempCookFolder.Path)) { UMaterialInterface * DuplicatedMaterial = BakeSingleMaterialToPackage(MaterialInterface, InstancerPackageParams, OutPackagesToSave, InOutAlreadyBakedMaterialsMap); DuplicatedSMCOverrideMaterials.Add(DuplicatedMaterial); } } } // Update the previous baked object InBakedOutputObject.BakedObject = FSoftObjectPath(BakedStaticMesh).ToString(); // BaseName holds the Actor / HDA name // Instancer name adds the split identifier (INSTANCERNUM_VARIATIONNUM) const FString BaseName = OwnerActor->GetName(); const FString InstancerName = ObjectName + "_instancer_" + InOutputObjectIdentifier.SplitIdentifier; const FName WorldOutlinerFolderPath = GetOutlinerFolderPath(InOutputObject, FName(InFallbackWorldOutlinerFolder.IsEmpty() ? BaseName : InFallbackWorldOutlinerFolder)); // By default spawn in the current level unless specified via the unreal_level_path attribute ULevel* DesiredLevel = GWorld->GetCurrentLevel(); bool bHasLevelPathAttribute = InOutputObject.CachedAttributes.Contains(HAPI_UNREAL_ATTRIB_LEVEL_PATH); if (bHasLevelPathAttribute) { // Get the package path from the unreal_level_apth attribute FString LevelPackagePath = InstancerResolver.ResolveFullLevelPath(); bool bCreatedPackage = false; if (!FHoudiniEngineBakeUtils::FindOrCreateDesiredLevelFromLevelPath( LevelPackagePath, DesiredLevel, DesiredWorld, bCreatedPackage)) { // TODO: LOG ERROR IF NO LEVEL return false; } // If we have created a level, add it to the packages to save // TODO: ? always add? if (bCreatedPackage && DesiredLevel) { // We can now save the package again, and unload it. OutPackagesToSave.Add(DesiredLevel->GetOutermost()); } } if (!DesiredLevel) return false; // Try to find the unreal_bake_actor, if specified FName BakeActorName; AActor* FoundActor = nullptr; bool bHasBakeActorName = false; if (!FindUnrealBakeActor(InOutputObject, InBakedOutputObject, OutActors, DesiredLevel, *InstancerName, bInReplaceActors, InFallbackActor, FoundActor, bHasBakeActorName, BakeActorName)) return false; UStaticMeshComponent* StaticMeshComponent = nullptr; // Create an actor if we didn't find one if (!FoundActor) { // Get the StaticMesh ActorFactory UActorFactory* SMFactory = GEditor ? GEditor->FindActorFactoryByClass(UActorFactoryStaticMesh::StaticClass()) : nullptr; if (!SMFactory) return false; FoundActor = SMFactory->CreateActor(BakedStaticMesh, DesiredLevel, InSMC->GetComponentTransform()); if (!FoundActor || FoundActor->IsPendingKill()) return false; AStaticMeshActor* SMActor = Cast(FoundActor); if (!SMActor || SMActor->IsPendingKill()) return false; StaticMeshComponent = SMActor->GetStaticMeshComponent(); } else { USceneComponent* RootComponent = GetActorRootComponent(FoundActor); if (!IsValid(RootComponent)) return false; if (bInReplaceAssets) { // Check if we have a previous bake component and that it belongs to FoundActor, if so, reuse it UStaticMeshComponent* PrevSMC = Cast(InBakedOutputObject.GetBakedComponentIfValid()); if (IsValid(PrevSMC) && (PrevSMC->GetOwner() == FoundActor)) { StaticMeshComponent = PrevSMC; } } if (!IsValid(StaticMeshComponent)) { // Create a new static mesh component StaticMeshComponent = NewObject(FoundActor, NAME_None, RF_Transactional); FoundActor->AddInstanceComponent(StaticMeshComponent); StaticMeshComponent->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform); StaticMeshComponent->RegisterComponent(); } } const FString NewNameStr = MakeUniqueObjectNameIfNeeded(DesiredLevel, FoundActor->GetClass(), BakeActorName.ToString(), FoundActor); RenameAndRelabelActor(FoundActor, NewNameStr, false); // The folder is named after the original actor and contains all generated actors SetOutlinerFolderPath(FoundActor, InOutputObject, WorldOutlinerFolderPath); // Update the previous baked component InBakedOutputObject.BakedComponent = FSoftObjectPath(StaticMeshComponent).ToString(); if (!IsValid(StaticMeshComponent)) return false; // Copy properties from the existing component const bool bCopyWorldTransform = true; CopyPropertyToNewActorAndComponent(FoundActor, StaticMeshComponent, InSMC, bCopyWorldTransform); StaticMeshComponent->SetStaticMesh(BakedStaticMesh); if (DuplicatedSMCOverrideMaterials.Num() > 0) { UMaterialInterface * InstancerMaterial = DuplicatedSMCOverrideMaterials[0]; if (InstancerMaterial) { StaticMeshComponent->OverrideMaterials.Empty(); int32 MeshMaterialCount = BakedStaticMesh->StaticMaterials.Num(); for (int32 Idx = 0; Idx < MeshMaterialCount; ++Idx) StaticMeshComponent->SetMaterial(Idx, InstancerMaterial); } } InBakedOutputObject.Actor = FSoftObjectPath(FoundActor).ToString(); FHoudiniEngineBakedActor& OutputEntry = OutActors.Add_GetRef(FHoudiniEngineBakedActor( FoundActor, BakeActorName, WorldOutlinerFolderPath, InOutputIndex, InOutputObjectIdentifier, BakedStaticMesh, StaticMesh, StaticMeshComponent, MeshPackageParams.BakeFolder, MeshPackageParams)); OutputEntry.bInstancerOutput = true; OutputEntry.InstancerPackageParams = InstancerPackageParams; // If we are baking in replace mode, remove previously baked components/instancers if (bInReplaceActors && bInReplaceAssets) { const bool bInDestroyBakedComponent = false; const bool bInDestroyBakedInstancedActors = true; const bool bInDestroyBakedInstancedComponents = true; DestroyPreviousBakeOutput( InBakedOutputObject, bInDestroyBakedComponent, bInDestroyBakedInstancedActors, bInDestroyBakedInstancedComponents); } return true; } bool FHoudiniEngineBakeUtils::BakeInstancerOutputToActors_IAC( const UHoudiniAssetComponent* HoudiniAssetComponent, int32 InOutputIndex, const FHoudiniOutputObjectIdentifier& InOutputObjectIdentifier, const FHoudiniOutputObject& InOutputObject, FHoudiniBakedOutputObject& InBakedOutputObject, const FDirectoryPath& InBakeFolder, bool bInReplaceActors, bool bInReplaceAssets, TArray& OutActors, TArray& OutPackagesToSave) { UHoudiniInstancedActorComponent* InIAC = Cast(InOutputObject.OutputComponent); if (!InIAC || InIAC->IsPendingKill()) return false; AActor * OwnerActor = InIAC->GetOwner(); if (!OwnerActor || OwnerActor->IsPendingKill()) return false; // BaseName holds the Actor / HDA name const FName BaseName = FName(OwnerActor->GetName()); // Get the object instanced by this IAC UObject* InstancedObject = InIAC->GetInstancedObject(); if (!InstancedObject || InstancedObject->IsPendingKill()) return false; FHoudiniPackageParams PackageParams; const EPackageReplaceMode AssetPackageReplaceMode = bInReplaceAssets ? EPackageReplaceMode::ReplaceExistingAssets : EPackageReplaceMode::CreateNewAssets; // Configure FHoudiniAttributeResolver and fill the package params with resolved object name and bake folder. // The resolver is then also configured with the package params for subsequent resolving (level_path etc) FHoudiniAttributeResolver Resolver; UWorld* DesiredWorld = OwnerActor ? OwnerActor->GetWorld() : GWorld; FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( DesiredWorld, HoudiniAssetComponent, InOutputObjectIdentifier, InOutputObject, BaseName.ToString(), OwnerActor->GetName(), PackageParams, Resolver, InBakeFolder.Path, AssetPackageReplaceMode); // By default spawn in the current level unless specified via the unreal_level_path attribute ULevel* DesiredLevel = GWorld->GetCurrentLevel(); bool bHasLevelPathAttribute = InOutputObject.CachedAttributes.Contains(HAPI_UNREAL_ATTRIB_LEVEL_PATH); if (bHasLevelPathAttribute) { // Get the package path from the unreal_level_apth attribute FString LevelPackagePath = Resolver.ResolveFullLevelPath(); bool bCreatedPackage = false; if (!FHoudiniEngineBakeUtils::FindOrCreateDesiredLevelFromLevelPath( LevelPackagePath, DesiredLevel, DesiredWorld, bCreatedPackage)) { // TODO: LOG ERROR IF NO LEVEL return false; } // If we have created a level, add it to the packages to save // TODO: ? always add? if (bCreatedPackage && DesiredLevel) { // We can now save the package again, and unload it. OutPackagesToSave.Add(DesiredLevel->GetOutermost()); } } if (!DesiredLevel) return false; // If we are baking in actor replacement mode, remove any previously baked instanced actors for this output if (bInReplaceActors && InBakedOutputObject.InstancedActors.Num() > 0) { UWorld* LevelWorld = DesiredLevel->GetWorld(); if (IsValid(LevelWorld)) { for (const FString& ActorPathStr : InBakedOutputObject.InstancedActors) { const FSoftObjectPath ActorPath(ActorPathStr); if (!ActorPath.IsValid()) continue; AActor* Actor = Cast(ActorPath.TryLoad()); // Destroy Actor if it is valid and part of DesiredLevel if (IsValid(Actor) && Actor->GetLevel() == DesiredLevel) { #if WITH_EDITOR LevelWorld->EditorDestroyActor(Actor, true); #else LevelWorld->DestroyActor(Actor); #endif } } } } // Empty and reserve enough space for new instanced actors InBakedOutputObject.InstancedActors.Empty(InIAC->GetInstancedActors().Num()); // Iterates on all the instances of the IAC for (AActor* CurrentInstancedActor : InIAC->GetInstancedActors()) { if (!CurrentInstancedActor || CurrentInstancedActor->IsPendingKill()) continue; const FString NewNameStr = MakeUniqueObjectNameIfNeeded(DesiredLevel, InstancedObject->StaticClass(), BaseName.ToString()); FTransform CurrentTransform = CurrentInstancedActor->GetTransform(); AActor* NewActor = FHoudiniInstanceTranslator::SpawnInstanceActor(CurrentTransform, DesiredLevel, InIAC); if (!NewActor || NewActor->IsPendingKill()) continue; const auto CopyOptions = (EditorUtilities::ECopyOptions::Type) (EditorUtilities::ECopyOptions::OnlyCopyEditOrInterpProperties | EditorUtilities::ECopyOptions::PropagateChangesToArchetypeInstances); EditorUtilities::CopyActorProperties(CurrentInstancedActor, NewActor); const FName WorldOutlinerFolderPath = GetOutlinerFolderPath(InOutputObject, BaseName); FHoudiniEngineRuntimeUtils::SetActorLabel(NewActor, NewNameStr); SetOutlinerFolderPath(NewActor, InOutputObject, WorldOutlinerFolderPath); NewActor->SetActorTransform(CurrentTransform); InBakedOutputObject.InstancedActors.Add(FSoftObjectPath(NewActor).ToString()); FHoudiniEngineBakedActor& OutputEntry = OutActors.Add_GetRef(FHoudiniEngineBakedActor( NewActor, BaseName, WorldOutlinerFolderPath, InOutputIndex, InOutputObjectIdentifier, nullptr, InstancedObject, nullptr, PackageParams.BakeFolder, PackageParams)); OutputEntry.bInstancerOutput = true; OutputEntry.InstancerPackageParams = PackageParams; } // TODO: // Move Actors to DesiredLevel if needed?? // If we are baking in replace mode, remove previously baked components/instancers if (bInReplaceActors && bInReplaceAssets) { const bool bInDestroyBakedComponent = true; const bool bInDestroyBakedInstancedActors = false; const bool bInDestroyBakedInstancedComponents = true; DestroyPreviousBakeOutput( InBakedOutputObject, bInDestroyBakedComponent, bInDestroyBakedInstancedActors, bInDestroyBakedInstancedComponents); } return true; } bool FHoudiniEngineBakeUtils::BakeInstancerOutputToActors_MSIC( const UHoudiniAssetComponent* HoudiniAssetComponent, int32 InOutputIndex, const TArray& InAllOutputs, // const TArray& InAllBakedOutputs, const FHoudiniOutputObjectIdentifier& InOutputObjectIdentifier, const FHoudiniOutputObject& InOutputObject, FHoudiniBakedOutputObject& InBakedOutputObject, const FTransform& InTransform, const FDirectoryPath& InBakeFolder, const FDirectoryPath& InTempCookFolder, bool bInReplaceActors, bool bInReplaceAssets, TArray& OutActors, TArray& OutPackagesToSave, TMap& InOutAlreadyBakedMaterialsMap, AActor* InFallbackActor, const FString& InFallbackWorldOutlinerFolder) { UHoudiniMeshSplitInstancerComponent * InMSIC = Cast(InOutputObject.OutputComponent); if (!InMSIC || InMSIC->IsPendingKill()) return false; AActor * OwnerActor = InMSIC->GetOwner(); if (!OwnerActor || OwnerActor->IsPendingKill()) return false; UStaticMesh * StaticMesh = InMSIC->GetStaticMesh(); if (!StaticMesh || StaticMesh->IsPendingKill()) return false; // Certain SMC materials may need to be duplicated if we didn't generate the mesh object. TArray DuplicatedMSICOverrideMaterials; UWorld* DesiredWorld = OwnerActor ? OwnerActor->GetWorld() : GWorld; const EPackageReplaceMode AssetPackageReplaceMode = bInReplaceAssets ? EPackageReplaceMode::ReplaceExistingAssets : EPackageReplaceMode::CreateNewAssets; // Determine if the incoming mesh is temporary by looking for it in the mesh outputs. Populate mesh package params // for baking from it. // If not temporary set the ObjectName from the its package. (Also use this as a fallback default) FString ObjectName = FHoudiniPackageParams::GetPackageNameExcludingGUID(StaticMesh); UStaticMesh* PreviousStaticMesh = Cast(InBakedOutputObject.GetBakedObjectIfValid()); UStaticMesh* BakedStaticMesh = nullptr; int32 MeshOutputIndex = INDEX_NONE; FHoudiniOutputObjectIdentifier MeshIdentifier; FHoudiniAttributeResolver MeshResolver; FHoudiniPackageParams MeshPackageParams; // See if the instanced static mesh is still a temporary Houdini created Static Mesh // If it is, we need to bake the StaticMesh first FHoudiniPackageParams InstancerPackageParams; // Configure FHoudiniAttributeResolver and fill the package params with resolved object name and bake folder. // The resolver is then also configured with the package params for subsequent resolving (level_path etc) FHoudiniAttributeResolver InstancerResolver; FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( DesiredWorld, HoudiniAssetComponent, InOutputObjectIdentifier, InOutputObject, ObjectName, OwnerActor->GetName(), InstancerPackageParams, InstancerResolver, InBakeFolder.Path, AssetPackageReplaceMode); const bool bFoundMeshOutput = FindOutputObject(StaticMesh, EHoudiniOutputType::Mesh, InAllOutputs, MeshOutputIndex, MeshIdentifier); if (bFoundMeshOutput) { // Found the mesh in the mesh outputs, is temporary const FHoudiniOutputObject& MeshOutputObject = InAllOutputs[MeshOutputIndex]->GetOutputObjects().FindChecked(MeshIdentifier); FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( DesiredWorld, HoudiniAssetComponent, MeshIdentifier, MeshOutputObject, ObjectName, OwnerActor->GetName(), MeshPackageParams, MeshResolver, InBakeFolder.Path, AssetPackageReplaceMode); // Update with resolved object name ObjectName = MeshPackageParams.ObjectName; // This will bake/duplicate the mesh if temporary, or return the input one if it is not BakedStaticMesh = FHoudiniEngineBakeUtils::DuplicateStaticMeshAndCreatePackageIfNeeded( StaticMesh, PreviousStaticMesh, MeshPackageParams, InAllOutputs, OutActors, InTempCookFolder.Path, OutPackagesToSave, InOutAlreadyBakedMaterialsMap); } else { BakedStaticMesh = StaticMesh; // We still need to duplicate materials, if they are temporary. TArray Materials = InMSIC->GetOverrideMaterials(); for (int32 MaterialIdx = 0; MaterialIdx < Materials.Num(); ++MaterialIdx) { UMaterialInterface* MaterialInterface = Materials[MaterialIdx]; if (!MaterialInterface || MaterialInterface->IsPendingKill()) continue; // Only duplicate the material if it is temporary if (IsObjectTemporary(MaterialInterface, EHoudiniOutputType::Invalid, InAllOutputs, InTempCookFolder.Path)) { UMaterialInterface * DuplicatedMaterial = BakeSingleMaterialToPackage(MaterialInterface, InstancerPackageParams, OutPackagesToSave, InOutAlreadyBakedMaterialsMap); DuplicatedMSICOverrideMaterials.Add(DuplicatedMaterial); } } } // Update the baked output InBakedOutputObject.BakedObject = FSoftObjectPath(BakedStaticMesh).ToString(); // Instancer name adds the split identifier (INSTANCERNUM_VARIATIONNUM) const FString BaseName = OwnerActor->GetName(); const FString InstancerName = ObjectName + "_instancer_" + InOutputObjectIdentifier.SplitIdentifier; const FName WorldOutlinerFolderPath = GetOutlinerFolderPath(InOutputObject, FName(InFallbackWorldOutlinerFolder.IsEmpty() ? BaseName : InFallbackWorldOutlinerFolder)); // By default spawn in the current level unless specified via the unreal_level_path attribute ULevel* DesiredLevel = GWorld->GetCurrentLevel(); bool bHasLevelPathAttribute = InOutputObject.CachedAttributes.Contains(HAPI_UNREAL_ATTRIB_LEVEL_PATH); if (bHasLevelPathAttribute) { // Get the package path from the unreal_level_path attribute FString LevelPackagePath = InstancerResolver.ResolveFullLevelPath(); bool bCreatedPackage = false; if (!FHoudiniEngineBakeUtils::FindOrCreateDesiredLevelFromLevelPath( LevelPackagePath, DesiredLevel, DesiredWorld, bCreatedPackage)) { // TODO: LOG ERROR IF NO LEVEL return false; } // If we have created a level, add it to the packages to save // TODO: ? always add? if (bCreatedPackage && DesiredLevel) { // We can now save the package again, and unload it. OutPackagesToSave.Add(DesiredLevel->GetOutermost()); } } if (!DesiredLevel) return false; // Try to find the unreal_bake_actor, if specified FName BakeActorName; AActor* FoundActor = nullptr; bool bHasBakeActorName = false; bool bSpawnedActor = false; if (!FindUnrealBakeActor(InOutputObject, InBakedOutputObject, OutActors, DesiredLevel, *InstancerName, bInReplaceActors, InFallbackActor, FoundActor, bHasBakeActorName, BakeActorName)) return false; if (!FoundActor) { // This is a split mesh instancer component - we will create a generic AActor with a bunch of SMC FActorSpawnParameters SpawnInfo; SpawnInfo.OverrideLevel = DesiredLevel; SpawnInfo.ObjectFlags = RF_Transactional; SpawnInfo.Name = FName(MakeUniqueObjectNameIfNeeded(DesiredLevel, AActor::StaticClass(), BakeActorName.ToString())); SpawnInfo.bDeferConstruction = true; // Spawn the new Actor FoundActor = DesiredLevel->OwningWorld->SpawnActor(SpawnInfo); if (!FoundActor || FoundActor->IsPendingKill()) return false; bSpawnedActor = true; FHoudiniEngineRuntimeUtils::SetActorLabel(FoundActor, /*DesiredLevel->bUseExternalActors ? BakeActorName.ToString() : */FoundActor->GetName()); FoundActor->SetActorHiddenInGame(InMSIC->bHiddenInGame); } else { // If we are baking in replacement mode, remove the previous components (if they belong to FoundActor) for (const FString& PrevComponentPathStr : InBakedOutputObject.InstancedComponents) { const FSoftObjectPath PrevComponentPath(PrevComponentPathStr); if (!PrevComponentPath.IsValid()) continue; UActorComponent* PrevComponent = Cast(PrevComponentPath.TryLoad()); if (!IsValid(PrevComponent) || PrevComponent->GetOwner() != FoundActor) continue; RemovePreviouslyBakedComponent(PrevComponent); } const FString UniqueActorNameStr = MakeUniqueObjectNameIfNeeded(DesiredLevel, AActor::StaticClass(), BakeActorName.ToString(), FoundActor); RenameAndRelabelActor(FoundActor, UniqueActorNameStr, false); } // The folder is named after the original actor and contains all generated actors SetOutlinerFolderPath(FoundActor, InOutputObject, WorldOutlinerFolderPath); // Get/create the actor's root component const bool bCreateIfMissing = true; USceneComponent* RootComponent = GetActorRootComponent(FoundActor, bCreateIfMissing); if (bSpawnedActor && IsValid(RootComponent)) RootComponent->SetWorldTransform(InTransform); // Empty and reserve enough space in the baked components array for the new components InBakedOutputObject.InstancedComponents.Empty(InMSIC->GetInstances().Num()); // Now add s SMC component for each of the SMC's instance for (UStaticMeshComponent* CurrentSMC : InMSIC->GetInstances()) { if (!CurrentSMC || CurrentSMC->IsPendingKill()) continue; UStaticMeshComponent* NewSMC = DuplicateObject( CurrentSMC, FoundActor, FName(MakeUniqueObjectNameIfNeeded(FoundActor, CurrentSMC->GetClass(), CurrentSMC->GetName()))); if (!NewSMC || NewSMC->IsPendingKill()) continue; InBakedOutputObject.InstancedComponents.Add(FSoftObjectPath(NewSMC).ToString()); NewSMC->RegisterComponent(); // NewSMC->SetupAttachment(nullptr); NewSMC->SetStaticMesh(BakedStaticMesh); FoundActor->AddInstanceComponent(NewSMC); NewSMC->SetWorldTransform(CurrentSMC->GetComponentTransform()); if (DuplicatedMSICOverrideMaterials.Num() > 0) { UMaterialInterface * InstancerMaterial = DuplicatedMSICOverrideMaterials[0]; if (InstancerMaterial) { NewSMC->OverrideMaterials.Empty(); int32 MeshMaterialCount = BakedStaticMesh->StaticMaterials.Num(); for (int32 Idx = 0; Idx < MeshMaterialCount; ++Idx) NewSMC->SetMaterial(Idx, InstancerMaterial); } } if (IsValid(RootComponent)) NewSMC->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepWorldTransform); // TODO: Do we need to copy properties here, we duplicated the component // // Copy properties from the existing component // CopyPropertyToNewActorAndComponent(FoundActor, NewSMC, CurrentSMC); } if (bSpawnedActor) FoundActor->FinishSpawning(InTransform); InBakedOutputObject.Actor = FSoftObjectPath(FoundActor).ToString(); FHoudiniEngineBakedActor& OutputEntry = OutActors.Add_GetRef(FHoudiniEngineBakedActor( FoundActor, BakeActorName, WorldOutlinerFolderPath, InOutputIndex, InOutputObjectIdentifier, BakedStaticMesh, StaticMesh, nullptr, MeshPackageParams.BakeFolder, MeshPackageParams)); OutputEntry.bInstancerOutput = true; OutputEntry.InstancerPackageParams = InstancerPackageParams; // Postpone these calls to do them once per actor OutActors.Last().bPostBakeProcessPostponed = true; // If we are baking in replace mode, remove previously baked components/instancers if (bInReplaceActors && bInReplaceAssets) { const bool bInDestroyBakedComponent = true; const bool bInDestroyBakedInstancedActors = true; const bool bInDestroyBakedInstancedComponents = false; DestroyPreviousBakeOutput( InBakedOutputObject, bInDestroyBakedComponent, bInDestroyBakedInstancedActors, bInDestroyBakedInstancedComponents); } return true; } bool FHoudiniEngineBakeUtils::FindHGPO( const FHoudiniOutputObjectIdentifier& InIdentifier, const TArray& InHGPOs, FHoudiniGeoPartObject const*& OutHGPO) { // Find the HGPO that matches this output identifier const FHoudiniGeoPartObject* FoundHGPO = nullptr; for (auto & NextHGPO : InHGPOs) { // We use Matches() here as it handles the case where the HDA was loaded, // which likely means that the the obj/geo/part ids dont match the output identifier if(InIdentifier.Matches(NextHGPO)) { FoundHGPO = &NextHGPO; break; } } OutHGPO = FoundHGPO; return !OutHGPO; } void FHoudiniEngineBakeUtils::GetTemporaryOutputObjectBakeName( const UObject* InObject, const FHoudiniOutputObject& InMeshOutputObject, FString& OutBakeName) { // The bake name override has priority OutBakeName = InMeshOutputObject.BakeName; if (OutBakeName.IsEmpty()) { FHoudiniAttributeResolver Resolver; Resolver.SetCachedAttributes(InMeshOutputObject.CachedAttributes); Resolver.SetTokensFromStringMap(InMeshOutputObject.CachedTokens); const FString DefaultObjectName = FHoudiniPackageParams::GetPackageNameExcludingGUID(InObject); // The default output name (if not set via attributes) is {object_name}, which look for an object_name // key-value token if (!Resolver.GetCachedTokens().Contains(TEXT("object_name"))) Resolver.SetToken(TEXT("object_name"), DefaultObjectName); OutBakeName = Resolver.ResolveOutputName(); // const TArray& HGPOs = InAllOutputs[MeshOutputIdx]->GetHoudiniGeoPartObjects(); // const FHoudiniGeoPartObject* FoundHGPO = nullptr; // FindHGPO(MeshIdentifier, HGPOs, FoundHGPO); // // ... finally the part name // if (FoundHGPO && FoundHGPO->bHasCustomPartName) // OutBakeName = FoundHGPO->PartName; if (OutBakeName.IsEmpty()) OutBakeName = DefaultObjectName; } } bool FHoudiniEngineBakeUtils::GetTemporaryOutputObjectBakeName( const UObject* InObject, EHoudiniOutputType InOutputType, const TArray& InAllOutputs, FString& OutBakeName) { if (!IsValid(InObject)) return false; OutBakeName.Empty(); int32 MeshOutputIdx = INDEX_NONE; FHoudiniOutputObjectIdentifier MeshIdentifier; if (FindOutputObject(InObject, InOutputType, InAllOutputs, MeshOutputIdx, MeshIdentifier)) { // Found the mesh, get its name const FHoudiniOutputObject& MeshOutputObject = InAllOutputs[MeshOutputIdx]->GetOutputObjects().FindChecked(MeshIdentifier); GetTemporaryOutputObjectBakeName(InObject, MeshOutputObject, OutBakeName); return true; } return false; } bool FHoudiniEngineBakeUtils::BakeStaticMeshOutputToActors( const UHoudiniAssetComponent* HoudiniAssetComponent, int32 InOutputIndex, const TArray& InAllOutputs, TArray& InBakedOutputs, const FString& InHoudiniAssetName, const FDirectoryPath& InBakeFolder, const FDirectoryPath& InTempCookFolder, bool bInReplaceActors, bool bInReplaceAssets, TArray& OutActors, TArray& OutPackagesToSave, TMap& InOutAlreadyBakedMaterialsMap, AActor* InFallbackActor, const FString& InFallbackWorldOutlinerFolder) { // Check that index is not negative if (InOutputIndex < 0) return false; if (!InAllOutputs.IsValidIndex(InOutputIndex)) return false; UHoudiniOutput* InOutput = InAllOutputs[InOutputIndex]; if (!InOutput || InOutput->IsPendingKill()) return false; UActorFactory* Factory = GEditor ? GEditor->FindActorFactoryByClass(UActorFactoryStaticMesh::StaticClass()) : nullptr; if (!Factory) return false; TMap& OutputObjects = InOutput->GetOutputObjects(); const TArray& HGPOs = InOutput->GetHoudiniGeoPartObjects(); // Get the previous bake objects if (!InBakedOutputs.IsValidIndex(InOutputIndex)) InBakedOutputs.SetNum(InOutputIndex + 1); const TMap& OldBakedOutputObjects = InBakedOutputs[InOutputIndex].BakedOutputObjects; TMap NewBakedOutputObjects; for (auto& Pair : OutputObjects) { const FHoudiniOutputObjectIdentifier& Identifier = Pair.Key; const FHoudiniOutputObject& OutputObject = Pair.Value; // Add a new baked output object entry and update it with the previous bake's data, if available FHoudiniBakedOutputObject& BakedOutputObject = NewBakedOutputObjects.Add(Identifier); if (OldBakedOutputObjects.Contains(Identifier)) BakedOutputObject = OldBakedOutputObjects.FindChecked(Identifier); UStaticMesh* StaticMesh = Cast(OutputObject.OutputObject); if (!StaticMesh || StaticMesh->IsPendingKill()) continue; UStaticMeshComponent* InSMC = Cast(OutputObject.OutputComponent); if (!InSMC || InSMC->IsPendingKill()) continue; // Find the HGPO that matches this output identifier const FHoudiniGeoPartObject* FoundHGPO = nullptr; FindHGPO(Identifier, HGPOs, FoundHGPO); // We do not bake templated geos if (FoundHGPO && FoundHGPO->bIsTemplated) continue; const FString DefaultObjectName = FHoudiniPackageParams::GetPackageNameExcludingGUID(StaticMesh); UWorld* DesiredWorld = InOutput ? InOutput->GetWorld() : GWorld; ULevel* DesiredLevel = GWorld->GetCurrentLevel(); FHoudiniPackageParams PackageParams; const FName WorldOutlinerFolderPath = GetOutlinerFolderPath(OutputObject, FName(InFallbackWorldOutlinerFolder.IsEmpty() ? InHoudiniAssetName : InFallbackWorldOutlinerFolder)); if (!ResolvePackageParams( HoudiniAssetComponent, InOutput, Identifier, OutputObject, InHoudiniAssetName, DefaultObjectName, InBakeFolder, bInReplaceAssets, PackageParams, OutPackagesToSave)) { continue; } // Bake the static mesh if it is still temporary UStaticMesh* BakedSM = FHoudiniEngineBakeUtils::DuplicateStaticMeshAndCreatePackageIfNeeded( StaticMesh, Cast(BakedOutputObject.GetBakedObjectIfValid()), PackageParams, InAllOutputs, OutActors, InTempCookFolder.Path, OutPackagesToSave, InOutAlreadyBakedMaterialsMap); if (!BakedSM || BakedSM->IsPendingKill()) continue; // Record the baked object BakedOutputObject.BakedObject = FSoftObjectPath(BakedSM).ToString(); // Make sure we have a level to spawn to if (!DesiredLevel || DesiredLevel->IsPendingKill()) continue; // Try to find the unreal_bake_actor, if specified FName BakeActorName; AActor* FoundActor = nullptr; bool bHasBakeActorName = false; if (!FindUnrealBakeActor(OutputObject, BakedOutputObject, OutActors, DesiredLevel, *(PackageParams.ObjectName), bInReplaceActors, InFallbackActor, FoundActor, bHasBakeActorName, BakeActorName)) return false; UStaticMeshComponent* SMC = nullptr; if (!FoundActor) { // Spawn the new actor FoundActor = Factory->CreateActor(BakedSM, DesiredLevel, InSMC->GetComponentTransform()); if (!FoundActor || FoundActor->IsPendingKill()) continue; // Copy properties to new actor AStaticMeshActor* SMActor = Cast(FoundActor); if (!SMActor || SMActor->IsPendingKill()) continue; SMC = SMActor->GetStaticMeshComponent(); } else { if (bInReplaceAssets) { // Check if we have a previous bake component and that it belongs to FoundActor, if so, reuse it UStaticMeshComponent* PrevSMC = Cast(BakedOutputObject.GetBakedComponentIfValid()); if (IsValid(PrevSMC) && (PrevSMC->GetOwner() == FoundActor)) { SMC = PrevSMC; } } const bool bCreateIfMissing = true; USceneComponent* RootComponent = GetActorRootComponent(FoundActor, bCreateIfMissing); if (!IsValid(SMC)) { // Create a new static mesh component on the existing actor SMC = NewObject(FoundActor, NAME_None, RF_Transactional); FoundActor->AddInstanceComponent(SMC); if (IsValid(RootComponent)) SMC->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform); else FoundActor->SetRootComponent(SMC); SMC->RegisterComponent(); } } // We need to make a unique name for the actor, renaming an object on top of another is a fatal error const FString NewNameStr = MakeUniqueObjectNameIfNeeded(DesiredLevel, Factory->NewActorClass, BakeActorName.ToString(), FoundActor); RenameAndRelabelActor(FoundActor, NewNameStr, false); SetOutlinerFolderPath(FoundActor, OutputObject, WorldOutlinerFolderPath); if (IsValid(SMC)) { const bool bCopyWorldTransform = true; CopyPropertyToNewActorAndComponent(FoundActor, SMC, InSMC, bCopyWorldTransform); SMC->SetStaticMesh(BakedSM); BakedOutputObject.BakedComponent = FSoftObjectPath(SMC).ToString(); } BakedOutputObject.Actor = FSoftObjectPath(FoundActor).ToString(); OutActors.Add(FHoudiniEngineBakedActor( FoundActor, BakeActorName, WorldOutlinerFolderPath, InOutputIndex, Identifier, BakedSM, StaticMesh, SMC, PackageParams.BakeFolder, PackageParams)); // If we are baking in replace mode, remove previously baked components/instancers if (bInReplaceActors && bInReplaceAssets) { const bool bInDestroyBakedComponent = false; const bool bInDestroyBakedInstancedActors = true; const bool bInDestroyBakedInstancedComponents = true; DestroyPreviousBakeOutput( BakedOutputObject, bInDestroyBakedComponent, bInDestroyBakedInstancedActors, bInDestroyBakedInstancedComponents); } } // Update the cached baked output data InBakedOutputs[InOutputIndex].BakedOutputObjects = NewBakedOutputObjects; return true; } bool FHoudiniEngineBakeUtils::ResolvePackageParams( const UHoudiniAssetComponent* HoudiniAssetComponent, UHoudiniOutput* InOutput, const FHoudiniOutputObjectIdentifier& Identifier, const FHoudiniOutputObject& InOutputObject, const FString& InHoudiniAssetName, const FString& DefaultObjectName, const FDirectoryPath& InBakeFolder, const bool bInReplaceAssets, FHoudiniPackageParams& OutPackageParams, TArray& OutPackagesToSave) { FHoudiniAttributeResolver Resolver; UWorld* DesiredWorld = InOutput ? InOutput->GetWorld() : GWorld; ULevel* DesiredLevel = GWorld->GetCurrentLevel(); // Set the replace mode based on if we are doing a replacement or incremental asset bake const EPackageReplaceMode AssetPackageReplaceMode = bInReplaceAssets ? EPackageReplaceMode::ReplaceExistingAssets : EPackageReplaceMode::CreateNewAssets; // Configure FHoudiniAttributeResolver and fill the package params with resolved object name and bake folder. // The resolver is then also configured with the package params for subsequent resolving (level_path etc) FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( DesiredWorld, HoudiniAssetComponent, Identifier, InOutputObject, DefaultObjectName, InHoudiniAssetName, OutPackageParams, Resolver, InBakeFolder.Path, AssetPackageReplaceMode); // See if this output object has an unreal_level_path attribute specified // In which case, we need to create/find the desired level for baking instead of using the current one bool bHasLevelPathAttribute = InOutputObject.CachedAttributes.Contains(HAPI_UNREAL_ATTRIB_LEVEL_PATH); if (bHasLevelPathAttribute) { // Get the package path from the unreal_level_path attribute FString LevelPackagePath = Resolver.ResolveFullLevelPath(); bool bCreatedPackage = false; if (!FHoudiniEngineBakeUtils::FindOrCreateDesiredLevelFromLevelPath( LevelPackagePath, DesiredLevel, DesiredWorld, bCreatedPackage)) { // TODO: LOG ERROR IF NO LEVEL return false; } // If we have created a level, add it to the packages to save // TODO: ? always add the level to the packages to save? if (bCreatedPackage && DesiredLevel) { // We can now save the package again, and unload it. OutPackagesToSave.Add(DesiredLevel->GetOutermost()); } } return true; } bool FHoudiniEngineBakeUtils::BakeHoudiniCurveOutputToActors( const UHoudiniAssetComponent* HoudiniAssetComponent, int32 InOutputIndex, const TArray& InAllOutputs, TArray& InBakedOutputs, const FString& InHoudiniAssetName, const FDirectoryPath& InBakeFolder, bool bInReplaceActors, bool bInReplaceAssets, TArray& OutActors, AActor* InFallbackActor, const FString& InFallbackWorldOutlinerFolder) { // Check that index is not negative if (InOutputIndex < 0) return false; if (!InAllOutputs.IsValidIndex(InOutputIndex)) return false; UHoudiniOutput* const Output = InAllOutputs[InOutputIndex]; if (!Output || Output->IsPendingKill()) return false; TArray PackagesToSave; // Find the previous baked output data for this output index. If an entry // does not exist, create entries up to and including this output index if (!InBakedOutputs.IsValidIndex(InOutputIndex)) InBakedOutputs.SetNum(InOutputIndex + 1); TMap& OutputObjects = Output->GetOutputObjects(); FHoudiniBakedOutput& BakedOutput = InBakedOutputs[InOutputIndex]; const TMap& OldBakedOutputObjects = BakedOutput.BakedOutputObjects; TMap NewBakedOutputObjects; const TArray & HGPOs = Output->GetHoudiniGeoPartObjects(); for (auto & Pair : OutputObjects) { FHoudiniOutputObject& OutputObject = Pair.Value; USplineComponent* SplineComponent = Cast(OutputObject.OutputComponent); if (!SplineComponent || SplineComponent->IsPendingKill()) continue; const FHoudiniOutputObjectIdentifier& Identifier = Pair.Key; FHoudiniBakedOutputObject& BakedOutputObject = NewBakedOutputObjects.Add(Identifier); if (OldBakedOutputObjects.Contains(Identifier)) BakedOutputObject = OldBakedOutputObjects.FindChecked(Identifier); // TODO: FIX ME!! May not work 100% const FHoudiniGeoPartObject* FoundHGPO = nullptr; for (auto & NextHGPO : HGPOs) { if (Identifier.GeoId == NextHGPO.GeoId && Identifier.ObjectId == NextHGPO.ObjectId && Identifier.PartId == NextHGPO.PartId) { FoundHGPO = &NextHGPO; break; } } if (!FoundHGPO) continue; const FString DefaultObjectName = InHoudiniAssetName + "_" + SplineComponent->GetName(); FHoudiniPackageParams PackageParams; // Set the replace mode based on if we are doing a replacement or incremental asset bake const EPackageReplaceMode AssetPackageReplaceMode = bInReplaceAssets ? EPackageReplaceMode::ReplaceExistingAssets : EPackageReplaceMode::CreateNewAssets; // Configure FHoudiniAttributeResolver and fill the package params with resolved object name and bake folder. // The resolver is then also configured with the package params for subsequent resolving (level_path etc) FHoudiniAttributeResolver Resolver; UWorld* const DesiredWorld = SplineComponent ? SplineComponent->GetWorld() : GWorld; FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( DesiredWorld, HoudiniAssetComponent, Identifier, OutputObject, DefaultObjectName, InHoudiniAssetName, PackageParams, Resolver, InBakeFolder.Path, AssetPackageReplaceMode); BakeCurve( OutputObject, BakedOutputObject, PackageParams, Resolver, bInReplaceActors, bInReplaceAssets, OutActors, PackagesToSave, InFallbackActor, InFallbackWorldOutlinerFolder); } // Update the cached bake output results BakedOutput.BakedOutputObjects = NewBakedOutputObjects; SaveBakedPackages(PackagesToSave); return true; } bool FHoudiniEngineBakeUtils::CopyActorContentsToBlueprint(AActor * InActor, UBlueprint * OutBlueprint) { if (!InActor || InActor->IsPendingKill()) return false; if (!OutBlueprint || OutBlueprint->IsPendingKill()) return false; if (InActor->GetInstanceComponents().Num() > 0) FKismetEditorUtilities::AddComponentsToBlueprint( OutBlueprint, InActor->GetInstanceComponents()); if (OutBlueprint->GeneratedClass) { AActor * CDO = Cast< AActor >(OutBlueprint->GeneratedClass->GetDefaultObject()); if (!CDO || CDO->IsPendingKill()) return false; const auto CopyOptions = (EditorUtilities::ECopyOptions::Type) (EditorUtilities::ECopyOptions::OnlyCopyEditOrInterpProperties | EditorUtilities::ECopyOptions::PropagateChangesToArchetypeInstances); EditorUtilities::CopyActorProperties(InActor, CDO, CopyOptions); USceneComponent * Scene = CDO->GetRootComponent(); if (Scene && !Scene->IsPendingKill()) { Scene->SetRelativeLocation(FVector::ZeroVector); Scene->SetRelativeRotation(FRotator::ZeroRotator); // Clear out the attachment info after having copied the properties from the source actor Scene->SetupAttachment(nullptr); while (true) { const int32 ChildCount = Scene->GetAttachChildren().Num(); if (ChildCount < 1) break; USceneComponent * Component = Scene->GetAttachChildren()[ChildCount - 1]; if (Component && !Component->IsPendingKill()) Component->DetachFromComponent(FDetachmentTransformRules::KeepRelativeTransform); } check(Scene->GetAttachChildren().Num() == 0); // Ensure the light mass information is cleaned up Scene->InvalidateLightingCache(); // Copy relative scale from source to target. if (USceneComponent* SrcSceneRoot = InActor->GetRootComponent()) { Scene->SetRelativeScale3D_Direct(SrcSceneRoot->GetRelativeScale3D()); } } } // Compile our blueprint and notify asset system about blueprint. //FKismetEditorUtilities::CompileBlueprint(OutBlueprint); //FAssetRegistryModule::AssetCreated(OutBlueprint); return true; } bool FHoudiniEngineBakeUtils::BakeBlueprints(UHoudiniAssetComponent* HoudiniAssetComponent, bool bInReplaceAssets, bool bInRecenterBakedActors) { FHoudiniEngineOutputStats BakeStats; TArray PackagesToSave; TArray Blueprints; const bool bSuccess = BakeBlueprints(HoudiniAssetComponent, bInReplaceAssets, bInRecenterBakedActors, BakeStats, Blueprints, PackagesToSave); if (!bSuccess) { // TODO: ? HOUDINI_LOG_WARNING(TEXT("Errors while baking to blueprints.")); } FHoudiniEngineBakeUtils::SaveBakedPackages(PackagesToSave); // Sync the CB to the baked objects if(GEditor && Blueprints.Num() > 0) { TArray Assets; Assets.Reserve(Blueprints.Num()); for (UBlueprint* Blueprint : Blueprints) { Assets.Add(Blueprint); } GEditor->SyncBrowserToObjects(Assets); } { const FString FinishedTemplate = TEXT("Baking finished. Created {0} packages. Updated {1} packages."); FString Msg = FString::Format(*FinishedTemplate, { BakeStats.NumPackagesCreated, BakeStats.NumPackagesUpdated } ); FHoudiniEngine::Get().FinishTaskSlateNotification( FText::FromString(Msg) ); } TryCollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); // Broadcast that the bake is complete HoudiniAssetComponent->HandleOnPostBake(bSuccess); return bSuccess; } bool FHoudiniEngineBakeUtils::BakeBlueprints( UHoudiniAssetComponent* HoudiniAssetComponent, bool bInReplaceAssets, bool bInRecenterBakedActors, FHoudiniEngineOutputStats& InBakeStats, TArray& OutBlueprints, TArray& OutPackagesToSave) { if (!HoudiniAssetComponent || HoudiniAssetComponent->IsPendingKill()) return false; AActor* OwnerActor = HoudiniAssetComponent->GetOwner(); const bool bIsOwnerActorValid = IsValid(OwnerActor); TArray Actors; // Don't process outputs that are not supported in blueprints TArray OutputsToBake = { EHoudiniOutputType::Mesh, EHoudiniOutputType::Instancer, EHoudiniOutputType::Curve }; TArray InstancerComponentTypesToBake = { EHoudiniInstancerComponentType::StaticMeshComponent, EHoudiniInstancerComponentType::InstancedStaticMeshComponent, EHoudiniInstancerComponentType::MeshSplitInstancerComponent, EHoudiniInstancerComponentType::FoliageAsHierarchicalInstancedStaticMeshComponent }; // When baking blueprints we always create new actors since they are deleted from the world once copied into the // blueprint const bool bReplaceActors = false; bool bBakeSuccess = BakeHoudiniActorToActors( HoudiniAssetComponent, bReplaceActors, bInReplaceAssets, Actors, OutPackagesToSave, InBakeStats, &OutputsToBake, &InstancerComponentTypesToBake); if (!bBakeSuccess) { HOUDINI_LOG_ERROR(TEXT("Could not create output actors for baking to blueprint.")); return false; } // Get the previous baked outputs TArray& BakedOutputs = HoudiniAssetComponent->GetBakedOutputs(); bBakeSuccess = BakeBlueprintsFromBakedActors( Actors, bInRecenterBakedActors, bInReplaceAssets, bIsOwnerActorValid ? OwnerActor->GetName() : FString(), HoudiniAssetComponent->BakeFolder, &BakedOutputs, nullptr, OutBlueprints, OutPackagesToSave); return bBakeSuccess; } UStaticMesh* FHoudiniEngineBakeUtils::BakeStaticMesh( UStaticMesh * StaticMesh, const FHoudiniPackageParams& PackageParams, const TArray& InAllOutputs, const FDirectoryPath& InTempCookFolder, TMap& InOutAlreadyBakedMaterialsMap) { if (!StaticMesh || StaticMesh->IsPendingKill()) return nullptr; TArray PackagesToSave; TArray Outputs; const TArray BakedResults; UStaticMesh* BakedStaticMesh = FHoudiniEngineBakeUtils::DuplicateStaticMeshAndCreatePackageIfNeeded( StaticMesh, nullptr, PackageParams, InAllOutputs, BakedResults, InTempCookFolder.Path, PackagesToSave, InOutAlreadyBakedMaterialsMap); if (BakedStaticMesh) { FHoudiniEngineBakeUtils::SaveBakedPackages(PackagesToSave); // Sync the CB to the baked objects if(GEditor) { TArray Objects; Objects.Add(BakedStaticMesh); GEditor->SyncBrowserToObjects(Objects); } } return BakedStaticMesh; } bool FHoudiniEngineBakeUtils::BakeLandscape( const UHoudiniAssetComponent* HoudiniAssetComponent, int32 InOutputIndex, const TArray& InAllOutputs, TArray& InBakedOutputs, bool bInReplaceActors, bool bInReplaceAssets, FString BakePath, FString HoudiniAssetName, FHoudiniEngineOutputStats& BakeStats ) { // Check that index is not negative if (InOutputIndex < 0) return false; if (!InAllOutputs.IsValidIndex(InOutputIndex)) return false; UHoudiniOutput* const Output = InAllOutputs[InOutputIndex]; if (!IsValid(Output)) return false; // Find the previous baked output data for this output index. If an entry // does not exist, create entries up to and including this output index if (!InBakedOutputs.IsValidIndex(InOutputIndex)) InBakedOutputs.SetNum(InOutputIndex + 1); TMap& OutputObjects = Output->GetOutputObjects(); FHoudiniBakedOutput& BakedOutput = InBakedOutputs[InOutputIndex]; const TMap& OldBakedOutputObjects = BakedOutput.BakedOutputObjects; TMap NewBakedOutputObjects; TArray PackagesToSave; TArray LandscapeWorldsToUpdate; FHoudiniPackageParams PackageParams; for (auto& Elem : OutputObjects) { const FHoudiniOutputObjectIdentifier& ObjectIdentifier = Elem.Key; FHoudiniOutputObject& OutputObject = Elem.Value; FHoudiniBakedOutputObject& BakedOutputObject = NewBakedOutputObjects.Add(ObjectIdentifier); if (OldBakedOutputObjects.Contains(ObjectIdentifier)) BakedOutputObject = OldBakedOutputObjects.FindChecked(ObjectIdentifier); // Populate the package params for baking this output object. if (!IsValid(OutputObject.OutputObject)) continue; if (!OutputObject.OutputObject->IsA()) continue; UHoudiniLandscapePtr* LandscapePtr = Cast(OutputObject.OutputObject); ALandscapeProxy* Landscape = LandscapePtr->GetRawPtr(); if (!IsValid(Landscape)) continue; FString ObjectName = Landscape->GetName(); // Set the replace mode based on if we are doing a replacement or incremental asset bake const EPackageReplaceMode AssetPackageReplaceMode = bInReplaceAssets ? EPackageReplaceMode::ReplaceExistingAssets : EPackageReplaceMode::CreateNewAssets; // Configure FHoudiniAttributeResolver and fill the package params with resolved object name and bake folder. // The resolver is then also configured with the package params for subsequent resolving (level_path etc) FHoudiniAttributeResolver Resolver; UWorld* const DesiredWorld = Landscape ? Landscape->GetWorld() : GWorld; FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( DesiredWorld, HoudiniAssetComponent, ObjectIdentifier, OutputObject, ObjectName, HoudiniAssetName, PackageParams, Resolver, BakePath, AssetPackageReplaceMode); BakeLandscapeObject(OutputObject, BakedOutputObject, bInReplaceActors, bInReplaceAssets, PackageParams, Resolver, LandscapeWorldsToUpdate, PackagesToSave, BakeStats); } // Update the cached baked output data BakedOutput.BakedOutputObjects = NewBakedOutputObjects; if (PackagesToSave.Num() > 0) { FEditorFileUtils::PromptForCheckoutAndSave(PackagesToSave, true, false); } for(UWorld* LandscapeWorld : LandscapeWorldsToUpdate) { if (!LandscapeWorld) continue; FHoudiniEngineUtils::RescanWorldPath(LandscapeWorld); ULandscapeInfo::RecreateLandscapeInfo(LandscapeWorld, true); if (LandscapeWorld->WorldComposition) { UWorldComposition::WorldCompositionChangedEvent.Broadcast(LandscapeWorld); } } if (PackagesToSave.Num() > 0) { // These packages were either created during the Bake process or they weren't // loaded in the first place so be sure to unload them again to preserve their "state". TArray PackagesToUnload; for (UPackage* Package : PackagesToSave) { if (!Package->IsDirty()) PackagesToUnload.Add(Package); } UPackageTools::UnloadPackages(PackagesToUnload); } #if WITH_EDITOR FEditorDelegates::RefreshLevelBrowser.Broadcast(); FEditorDelegates::RefreshAllBrowsers.Broadcast(); #endif return true; } bool FHoudiniEngineBakeUtils::BakeLandscapeObject( FHoudiniOutputObject& InOutputObject, FHoudiniBakedOutputObject& InBakedOutputObject, bool bInReplaceActors, bool bInReplaceAssets, FHoudiniPackageParams& PackageParams, FHoudiniAttributeResolver& InResolver, TArray& WorldsToUpdate, TArray& OutPackagesToSave, FHoudiniEngineOutputStats& BakeStats) { UHoudiniLandscapePtr* LandscapePointer = Cast(InOutputObject.OutputObject); if (!LandscapePointer) return false; ALandscapeProxy* TileActor = LandscapePointer->GetRawPtr(); if (!TileActor) return false; // Fetch the previous bake's pointer and proxy (if available) ALandscapeProxy* PreviousTileActor = Cast(InBakedOutputObject.GetBakedObjectIfValid()); UWorld* TileWorld = TileActor->GetWorld(); ULevel* TileLevel = TileActor->GetLevel(); ULandscapeInfo::RecreateLandscapeInfo(TileWorld, true); // If this actor has a shared landscape, ensure the shared landscape gets detached from the HAC // and has the appropriate name. ALandscape* SharedLandscapeActor = TileActor->GetLandscapeActor(); check(SharedLandscapeActor); // Fetch the previous bake's shared landscape actor (if available) ALandscape* PreviousSharedLandscapeActor = nullptr; if (IsValid(PreviousTileActor)) PreviousSharedLandscapeActor = PreviousTileActor->GetLandscapeActor(); const bool bHasSharedLandscape = SharedLandscapeActor != TileActor; bool bHasPreviousSharedLandscape = PreviousSharedLandscapeActor && PreviousSharedLandscapeActor != PreviousTileActor; if (bHasPreviousSharedLandscape) { // Ignore the previous shared landscape if the world's are different // Typically in baking we treat completely different asset/output names in a bake as detached from the "previous" bake if (PreviousSharedLandscapeActor->GetWorld() != SharedLandscapeActor->GetWorld()) bHasPreviousSharedLandscape = false; } bool bLandscapeReplaced = false; if (bHasSharedLandscape) { // If we are baking in replace mode and we have a previous shared landscape actor, use the name of that // actor FString SharedLandscapeName = InResolver.ResolveAttribute( HAPI_UNREAL_ATTRIB_LANDSCAPE_SHARED_ACTOR_NAME, SharedLandscapeActor->GetName()); // If the shared landscape is still attached, or it's base name does not match the desired name, "bake" it AActor* const AttachedParent = SharedLandscapeActor->GetAttachParentActor(); if (AttachedParent || SharedLandscapeActor->GetFName().GetPlainNameString() != SharedLandscapeName) { if (bHasPreviousSharedLandscape && bInReplaceActors && PreviousSharedLandscapeActor->GetFName().GetPlainNameString() == SharedLandscapeName) { SharedLandscapeName = PreviousSharedLandscapeActor->GetName(); } else if (!bInReplaceActors) { // If we are not baking in replacement mode, create a unique name if the name is already in use SharedLandscapeName = MakeUniqueObjectNameIfNeeded( SharedLandscapeActor->GetOuter(), SharedLandscapeActor->GetClass(), *SharedLandscapeName, SharedLandscapeActor); } if (SharedLandscapeActor->GetName() != SharedLandscapeName) { AActor* FoundActor = nullptr; ALandscape* ExistingLandscape = FHoudiniEngineUtils::FindOrRenameInvalidActor(TileWorld, SharedLandscapeName, FoundActor); if (ExistingLandscape && bInReplaceActors) { // Even though we found an existing landscape with the desired type, we're just going to destroy/replace // it for now. FHoudiniEngineUtils::RenameToUniqueActor(ExistingLandscape, SharedLandscapeName+"_0"); ExistingLandscape->Destroy(); bLandscapeReplaced = true; } // Fix name of shared landscape FHoudiniEngineUtils::SafeRenameActor(SharedLandscapeActor, *SharedLandscapeName); } SharedLandscapeActor->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform); WorldsToUpdate.AddUnique(SharedLandscapeActor->GetWorld()); } } // Find the world where the landscape tile should be placed. TArray ValidLandscapes; FString ActorName = InResolver.ResolveOutputName(); // If the unreal_level_path was not specified, then fallback to the tile world's package FString PackagePath = TileWorld->GetOutermost() ? TileWorld->GetOutermost()->GetPathName() : FString(); bool bHasLevelPathAttribute = InOutputObject.CachedAttributes.Contains(HAPI_UNREAL_ATTRIB_LEVEL_PATH); if (bHasLevelPathAttribute) PackagePath = InResolver.ResolveFullLevelPath(); // Get the previous baked actor (if available) name, but only if it is in the // same target level, and it's plain name (no numeric suffix) matches ActorName // In replacement mode we'll then replace the previous tile actor. if (bInReplaceActors && IsValid(PreviousTileActor)) { UPackage* PreviousPackage = PreviousTileActor->GetOutermost();//GetPackage(); if (IsValid(PreviousPackage) && PreviousPackage->GetPathName() == PackagePath && PreviousTileActor->GetFName().GetPlainNameString() == ActorName) { ActorName = PreviousTileActor->GetName(); } } bool bCreatedPackage = false; UWorld* TargetWorld = nullptr; ULevel* TargetLevel = nullptr; ALandscapeProxy* TargetActor = FHoudiniLandscapeTranslator::FindExistingLandscapeActor_Bake( TileActor->GetWorld(), nullptr, //unused in bake mode ValidLandscapes,//unused in bake mode -1, //unused in bake mode -1, //unused in bake mode ActorName, PackagePath, TargetWorld, TargetLevel, bCreatedPackage ); check(TargetLevel) check(TargetWorld) if (TargetActor && TargetActor != TileActor) { if (bInReplaceActors && (!PreviousTileActor || PreviousTileActor == TargetActor)) { // We found an target matching the name that we want. For now, rename it and then nuke it, so that // at the very least we can spawn a new actor with the desired name. At a later stage we'll implement // a content update, if possible. FHoudiniEngineUtils::RenameToUniqueActor(TargetActor, ActorName + TEXT("_0")); TargetActor->Destroy(); } else { // incremental, keep existing actor and create a unique name for the new one ActorName = MakeUniqueObjectNameIfNeeded(TargetActor->GetOuter(), TargetActor->GetClass(), ActorName, TileActor); } TargetActor = nullptr; } if (TargetLevel != TileActor->GetLevel()) { bool bLevelInWorld = TileWorld->ContainsLevel(TargetLevel); ALandscape* SharedLandscape = TileActor->GetLandscapeActor(); ULandscapeInfo* LandscapeInfo = TileActor->GetLandscapeInfo(); check(LandscapeInfo); // We can now move the current landscape to the new world / level // if (TileActor->GetClass()->IsChildOf()) { // We can only move streaming proxies to sublevels for now. TArray ActorsToMove = {TileActor}; ALandscapeProxy* NewLandscapeProxy = LandscapeInfo->MoveComponentsToLevel(TileActor->LandscapeComponents, TargetLevel); // We have now moved the landscape components into the new level. We can (hopefully) safely delete the // old tile actor. TileActor->Destroy(); TargetLevel->MarkPackageDirty(); TileActor = NewLandscapeProxy; } } else { // Ensure the landscape actor is detached. TileActor->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform); } UPackage * CreatedPackage = TargetLevel->GetOutermost(); TMap AlreadyBakedMaterialsMap; // Replace materials if (TileActor->LandscapeMaterial) { UMaterialInterface * DuplicatedMaterial = BakeSingleMaterialToPackage(TileActor->LandscapeMaterial, PackageParams, OutPackagesToSave, AlreadyBakedMaterialsMap); TileActor->LandscapeMaterial = DuplicatedMaterial; } if (TileActor->LandscapeHoleMaterial) { UMaterialInterface * DuplicatedMaterial = BakeSingleMaterialToPackage(TileActor->LandscapeHoleMaterial, PackageParams, OutPackagesToSave, AlreadyBakedMaterialsMap); TileActor->LandscapeHoleMaterial = DuplicatedMaterial; } // Ensure the tile actor has the desired name. FHoudiniEngineUtils::SafeRenameActor(TileActor, ActorName); if (TileActor->GetClass()->IsChildOf(ALandscape::StaticClass())) { // This is not a shared landscape. Be sure to update this landscape's world when // baking is done. WorldsToUpdate.AddUnique(TileActor->GetWorld()); } if (bCreatedPackage) { // We can now save the package again, and unload it. OutPackagesToSave.Add(TargetLevel->GetOutermost()); } // Record the landscape in the baked output object via a new UHoudiniLandscapePtr // UHoudiniLandscapePtr* BakedLandscapePtr = NewObject(LandscapePointer->GetOuter()); // if (IsValid(BakedLandscapePtr)) // { // BakedLandscapePtr->SetSoftPtr(TileActor); InBakedOutputObject.BakedObject = FSoftObjectPath(TileActor).ToString(); // } // else // { // InBakedOutputObject.BakedObject = nullptr; // } // Bake the landscape layer uassets ULandscapeInfo* const LandscapeInfo = TileActor->GetLandscapeInfo(); if (IsValid(LandscapeInfo) && LandscapeInfo->Layers.Num() > 0) { TSet TempLayers; const int32 NumLayers = LandscapeInfo->Layers.Num(); TempLayers.Reserve(NumLayers); for (int32 LayerIndex = 0; LayerIndex < NumLayers; ++LayerIndex) { const FLandscapeInfoLayerSettings& Layer = LandscapeInfo->Layers[LayerIndex]; if (!IsValid(Layer.LayerInfoObj)) continue; if (!IsObjectInTempFolder(Layer.LayerInfoObj, PackageParams.TempCookFolder)) continue; if (!TempLayers.Contains(Layer.LayerInfoObj)) TempLayers.Add(Layer.LayerInfoObj); } // Setup package params to duplicate each layer FHoudiniPackageParams LayerPackageParams = PackageParams; const EPackageReplaceMode AssetPackageReplaceMode = bInReplaceAssets ? EPackageReplaceMode::ReplaceExistingAssets : EPackageReplaceMode::CreateNewAssets; LayerPackageParams.ReplaceMode = AssetPackageReplaceMode; // Determine the final bake name of the "owning" landscape (shared landscape in tiled mode, or just the // landscape actor itself in non-tiled mode FString OwningLandscapeActorBakeName; if (bHasSharedLandscape && IsValid(SharedLandscapeActor)) { SharedLandscapeActor->GetName(OwningLandscapeActorBakeName); } else { TileActor->GetName(OwningLandscapeActorBakeName); } // Keep track of the landscape layers we are baking this time around, and replace in the baked output object // at the end. TMap ThisBakedLandscapeLayers; // Bake/duplicate temp layers and replace temp layers via LandscapeInfo for (ULandscapeLayerInfoObject* const LayerInfo : TempLayers) { const FString SanitizedLayerName = ObjectTools::SanitizeObjectName(LayerInfo->LayerName.ToString()); LayerPackageParams.SplitStr = SanitizedLayerName; LayerPackageParams.ObjectName = OwningLandscapeActorBakeName + TEXT("_layer_") + SanitizedLayerName; // Get the previously baked layer info for this layer, if any ULandscapeLayerInfoObject* const PreviousBakedLayerInfo = InBakedOutputObject.GetLandscapeLayerInfoIfValid( LayerInfo->LayerName); // If our name is the base name (no number) of the previous, then we can fetch the bake counter for // replacement / incrementing from it int32 BakeCounter = 0; if (IsValid(PreviousBakedLayerInfo) && LayerPackageParams.MatchesPackagePathNameExcludingBakeCounter(PreviousBakedLayerInfo)) { // Get the bake counter from the previous bake FHoudiniPackageParams::GetBakeCounterFromBakedAsset(PreviousBakedLayerInfo, BakeCounter); } FString LayerPackageName; UPackage* const LayerPackage = LayerPackageParams.CreatePackageForObject(LayerPackageName, BakeCounter); if (IsValid(LayerPackage)) { BakeStats.NotifyPackageCreated(1); ULandscapeLayerInfoObject* BakedLayer = DuplicateObject( LayerInfo, LayerPackage, *LayerPackageName); if (IsValid(BakedLayer)) { OutPackagesToSave.Add(LayerPackage); // Trigger update of the Layer Info BakedLayer->PreEditChange(nullptr); BakedLayer->PostEditChange(); BakedLayer->MarkPackageDirty(); // Mark the package dirty... LayerPackage->MarkPackageDirty(); LandscapeInfo->ReplaceLayer(LayerInfo, BakedLayer); // Record as the new baked result for the LayerName ThisBakedLandscapeLayers.Add(LayerInfo->LayerName, FSoftObjectPath(BakedLayer).ToString()); } } } // Update the baked landscape layers in InBakedOutputObject InBakedOutputObject.LandscapeLayers = ThisBakedLandscapeLayers; } // Remove the landscape from the InOutputObject since it should no longer be used/reused/updated by temp cooks InOutputObject.OutputObject = nullptr; DestroyPreviousBakeOutput(InBakedOutputObject, true, true, true); // ---------------------------------------------------- // Collect baking stats // ---------------------------------------------------- if (bLandscapeReplaced) BakeStats.NotifyObjectsReplaced(EHoudiniOutputType::Landscape, 1); else BakeStats.NotifyObjectsCreated(EHoudiniOutputType::Landscape, 1); if (bCreatedPackage) BakeStats.NotifyPackageCreated(1); else if (TileLevel != TargetLevel) BakeStats.NotifyPackageUpdated(1); return true; } UStaticMesh * FHoudiniEngineBakeUtils::DuplicateStaticMeshAndCreatePackageIfNeeded( UStaticMesh * InStaticMesh, UStaticMesh * InPreviousBakeStaticMesh, const FHoudiniPackageParams &PackageParams, const TArray& InParentOutputs, const TArray& InCurrentBakedActors, const FString& InTemporaryCookFolder, TArray & OutCreatedPackages, TMap& InOutAlreadyBakedMaterialsMap) { if (!InStaticMesh || InStaticMesh->IsPendingKill()) return nullptr; const bool bIsTemporaryStaticMesh = IsObjectTemporary(InStaticMesh, EHoudiniOutputType::Mesh, InParentOutputs, InTemporaryCookFolder); if (!bIsTemporaryStaticMesh) { // The Static Mesh is not a temporary one/already baked, we can simply reuse it // instead of duplicating it return InStaticMesh; } // Look for InStaticMesh as the SourceObject in InCurrentBakedActors (it could have already been baked along with // a previous output: instancers etc) for (const FHoudiniEngineBakedActor& BakedActor : InCurrentBakedActors) { if (BakedActor.SourceObject == InStaticMesh && IsValid(BakedActor.BakedObject) && BakedActor.BakedObject->IsA(InStaticMesh->GetClass())) { // We have found a bake result where InStaticMesh was the source object and we have a valid BakedObject // of a compatible class return Cast(BakedActor.BakedObject); } } // InStaticMesh is temporary and we didn't find a baked version of it in our current bake output, we need to bake it // If we have a previously baked static mesh, get the bake counter from it so that both replace and increment // is consistent with the bake counter int32 BakeCounter = 0; bool bPreviousBakeStaticMeshValid = IsValid(InPreviousBakeStaticMesh); TArray PreviousBakeMaterials; if (bPreviousBakeStaticMeshValid) { bPreviousBakeStaticMeshValid = PackageParams.MatchesPackagePathNameExcludingBakeCounter(InPreviousBakeStaticMesh); if (bPreviousBakeStaticMeshValid) { PackageParams.GetBakeCounterFromBakedAsset(InPreviousBakeStaticMesh, BakeCounter); PreviousBakeMaterials = InPreviousBakeStaticMesh->StaticMaterials;//GetStaticMaterials(); } } FString CreatedPackageName; UPackage* MeshPackage = PackageParams.CreatePackageForObject(CreatedPackageName, BakeCounter); if (!MeshPackage || MeshPackage->IsPendingKill()) return nullptr; OutCreatedPackages.Add(MeshPackage); // We need to be sure the package has been fully loaded before calling DuplicateObject if (!MeshPackage->IsFullyLoaded()) { FlushAsyncLoading(); if (!MeshPackage->GetOuter()) { MeshPackage->FullyLoad(); } else { MeshPackage->GetOutermost()->FullyLoad(); } } // If the a UStaticMesh with that name already exists then detach it from all of its components before replacing // it so that its render resources can be safely replaced/updated, and then reattach it UStaticMesh * DuplicatedStaticMesh = nullptr; UStaticMesh* ExistingMesh = FindObject(MeshPackage, *CreatedPackageName); bool bFoundExistingMesh = false; if (IsValid(ExistingMesh)) { FStaticMeshComponentRecreateRenderStateContext SMRecreateContext(ExistingMesh); DuplicatedStaticMesh = DuplicateObject(InStaticMesh, MeshPackage, *CreatedPackageName); bFoundExistingMesh = true; } else { DuplicatedStaticMesh = DuplicateObject(InStaticMesh, MeshPackage, *CreatedPackageName); } if (!DuplicatedStaticMesh || DuplicatedStaticMesh->IsPendingKill()) return nullptr; // Add meta information. FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( MeshPackage, DuplicatedStaticMesh, HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT, TEXT("true")); FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( MeshPackage, DuplicatedStaticMesh, HAPI_UNREAL_PACKAGE_META_GENERATED_NAME, *CreatedPackageName); // See if we need to duplicate materials and textures. TArrayDuplicatedMaterials; TArray& Materials = DuplicatedStaticMesh->StaticMaterials; for (int32 MaterialIdx = 0; MaterialIdx < Materials.Num(); ++MaterialIdx) { UMaterialInterface* MaterialInterface = Materials[MaterialIdx].MaterialInterface; if (!MaterialInterface || MaterialInterface->IsPendingKill()) continue; // Only duplicate the material if it is temporary if (IsObjectTemporary(MaterialInterface, EHoudiniOutputType::Invalid, InParentOutputs, InTemporaryCookFolder)) { UPackage * MaterialPackage = Cast(MaterialInterface->GetOuter()); if (MaterialPackage && !MaterialPackage->IsPendingKill()) { FString MaterialName; if (FHoudiniEngineBakeUtils::GetHoudiniGeneratedNameFromMetaInformation( MeshPackage, DuplicatedStaticMesh, MaterialName)) { MaterialName = MaterialName + "_Material" + FString::FromInt(MaterialIdx + 1); // We only deal with materials. if (!MaterialInterface->IsA(UMaterial::StaticClass()) && !MaterialInterface->IsA(UMaterialInstance::StaticClass())) { continue; } UMaterialInterface * Material = MaterialInterface; if (Material && !Material->IsPendingKill()) { // Look for a previous bake material at this index UMaterialInterface* PreviousBakeMaterial = nullptr; if (bPreviousBakeStaticMeshValid && PreviousBakeMaterials.IsValidIndex(MaterialIdx)) { PreviousBakeMaterial = Cast(PreviousBakeMaterials[MaterialIdx].MaterialInterface); } // Duplicate material resource. UMaterialInterface * DuplicatedMaterial = FHoudiniEngineBakeUtils::DuplicateMaterialAndCreatePackage( Material, PreviousBakeMaterial, MaterialName, PackageParams, OutCreatedPackages, InOutAlreadyBakedMaterialsMap); if (!DuplicatedMaterial || DuplicatedMaterial->IsPendingKill()) continue; // Store duplicated material. FStaticMaterial DupeStaticMaterial = Materials[MaterialIdx]; DupeStaticMaterial.MaterialInterface = DuplicatedMaterial; DuplicatedMaterials.Add(DupeStaticMaterial); continue; } } } } // We can simply reuse the source material DuplicatedMaterials.Add(Materials[MaterialIdx]); } // Assign duplicated materials. DuplicatedStaticMesh->StaticMaterials = DuplicatedMaterials; // Notify registry that we have created a new duplicate mesh. if (!bFoundExistingMesh) FAssetRegistryModule::AssetCreated(DuplicatedStaticMesh); // Dirty the static mesh package. DuplicatedStaticMesh->MarkPackageDirty(); return DuplicatedStaticMesh; } ALandscapeProxy* FHoudiniEngineBakeUtils::BakeHeightfield( ALandscapeProxy * InLandscapeProxy, const FHoudiniPackageParams & PackageParams, const EHoudiniLandscapeOutputBakeType & LandscapeOutputBakeType) { if (!InLandscapeProxy || InLandscapeProxy->IsPendingKill()) return nullptr; const FString & BakeFolder = PackageParams.BakeFolder; const FString & AssetName = PackageParams.HoudiniAssetName; TArray PackagesToSave; switch (LandscapeOutputBakeType) { case EHoudiniLandscapeOutputBakeType::Detachment: { // Detach the landscape from the Houdini Asset Actor InLandscapeProxy->DetachFromActor(FDetachmentTransformRules::KeepRelativeTransform); } break; case EHoudiniLandscapeOutputBakeType::BakeToImage: { // Create heightmap image to the bake folder ULandscapeInfo * InLandscapeInfo = InLandscapeProxy->GetLandscapeInfo(); if (!InLandscapeInfo || InLandscapeInfo->IsPendingKill()) return nullptr; // bake to image must use absoluate path, // and the file name has a file extension (.png) FString BakeFolderInFullPath = BakeFolder; // Figure absolute path, if (!BakeFolderInFullPath.EndsWith("/")) BakeFolderInFullPath += "/"; if (BakeFolderInFullPath.StartsWith("/Game")) BakeFolderInFullPath = BakeFolderInFullPath.Mid(5, BakeFolderInFullPath.Len() - 5); if (BakeFolderInFullPath.StartsWith("/")) BakeFolderInFullPath = BakeFolderInFullPath.Mid(1, BakeFolderInFullPath.Len() - 1); FString FullPath = FPaths::ProjectContentDir() + BakeFolderInFullPath + AssetName + "_" + InLandscapeProxy->GetName() + ".png"; InLandscapeInfo->ExportHeightmap(FullPath); // TODO: // We should update this to have an asset/package.. } break; case EHoudiniLandscapeOutputBakeType::BakeToWorld: { ULandscapeInfo * InLandscapeInfo = InLandscapeProxy->GetLandscapeInfo(); if (!InLandscapeInfo || InLandscapeInfo->IsPendingKill()) return nullptr; // 0. Get Landscape Data // // Extract landscape height data TArray InLandscapeHeightData; int32 XSize, YSize; FVector Min, Max; if (!FUnrealLandscapeTranslator::GetLandscapeData(InLandscapeProxy, InLandscapeHeightData, XSize, YSize, Min, Max)) return nullptr; // Extract landscape Layers data TArray InLandscapeImportLayerInfos; for (int32 n = 0; n < InLandscapeInfo->Layers.Num(); ++n) { TArray CurrentLayerIntData; FLinearColor LayerUsageDebugColor; FString LayerName; if (!FUnrealLandscapeTranslator::GetLandscapeLayerData(InLandscapeProxy, InLandscapeInfo, n, CurrentLayerIntData, LayerUsageDebugColor, LayerName)) continue; FLandscapeImportLayerInfo CurrentLayerInfo; CurrentLayerInfo.LayerName = FName(LayerName); CurrentLayerInfo.LayerInfo = InLandscapeInfo->Layers[n].LayerInfoObj; CurrentLayerInfo.LayerData = CurrentLayerIntData; CurrentLayerInfo.LayerInfo->LayerUsageDebugColor = LayerUsageDebugColor; InLandscapeImportLayerInfos.Add(CurrentLayerInfo); } // 1. Create package // FString PackagePath = PackageParams.GetPackagePath(); FString PackageName = PackageParams.GetPackageName(); UPackage *CreatedPackage = nullptr; FString CreatedPackageName; CreatedPackage = PackageParams.CreatePackageForObject(CreatedPackageName); if (!CreatedPackage) return nullptr; // 2. Create a new world asset with dialog // UWorldFactory* Factory = NewObject(); FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); UObject* Asset = AssetToolsModule.Get().CreateAssetWithDialog( PackageName, PackagePath, UWorld::StaticClass(), Factory, FName("ContentBrowserNewAsset")); UWorld* NewWorld = Cast(Asset); if (!NewWorld) return nullptr; NewWorld->SetCurrentLevel(NewWorld->PersistentLevel); // 4. Spawn a landscape proxy actor in the created world ALandscapeStreamingProxy * BakedLandscapeProxy = NewWorld->SpawnActor(); if (!BakedLandscapeProxy) return nullptr; // Create a new GUID FGuid currentGUID = FGuid::NewGuid(); BakedLandscapeProxy->SetLandscapeGuid(currentGUID); // Deactivate CastStaticShadow on the landscape to avoid "grid shadow" issue BakedLandscapeProxy->bCastStaticShadow = false; // 5. Import data to the created landscape proxy TMap> HeightmapDataPerLayers; TMap> MaterialLayerDataPerLayer; HeightmapDataPerLayers.Add(FGuid(), InLandscapeHeightData); MaterialLayerDataPerLayer.Add(FGuid(), InLandscapeImportLayerInfos); ELandscapeImportAlphamapType ImportLayerType = ELandscapeImportAlphamapType::Additive; BakedLandscapeProxy->Import( currentGUID, 0, 0, XSize-1, YSize-1, InLandscapeInfo->ComponentNumSubsections, InLandscapeInfo->SubsectionSizeQuads, HeightmapDataPerLayers, NULL, MaterialLayerDataPerLayer, ImportLayerType); BakedLandscapeProxy->StaticLightingLOD = FMath::DivideAndRoundUp(FMath::CeilLogTwo((XSize * YSize) / (2048 * 2048) + 1), (uint32)2); TMap AlreadyBakedMaterialsMap; if (BakedLandscapeProxy->LandscapeMaterial) { UMaterialInterface * DuplicatedMaterial = BakeSingleMaterialToPackage(BakedLandscapeProxy->LandscapeMaterial, PackageParams, PackagesToSave, AlreadyBakedMaterialsMap); BakedLandscapeProxy->LandscapeMaterial = DuplicatedMaterial; } if (BakedLandscapeProxy->LandscapeHoleMaterial) { UMaterialInterface * DuplicatedMaterial = BakeSingleMaterialToPackage(BakedLandscapeProxy->LandscapeHoleMaterial, PackageParams, PackagesToSave, AlreadyBakedMaterialsMap); BakedLandscapeProxy->LandscapeHoleMaterial = DuplicatedMaterial; } // 6. Register all the landscape components, and set landscape actor transform BakedLandscapeProxy->RegisterAllComponents(); BakedLandscapeProxy->SetActorTransform(InLandscapeProxy->GetTransform()); // 7. Save Package PackagesToSave.Add(CreatedPackage); FHoudiniEngineBakeUtils::SaveBakedPackages(PackagesToSave); // Sync the CB to the baked objects if(GEditor) { TArray Objects; Objects.Add(NewWorld); GEditor->SyncBrowserToObjects(Objects); } } break; } return InLandscapeProxy; } bool FHoudiniEngineBakeUtils::BakeCurve( USplineComponent* InSplineComponent, ULevel* InLevel, const FHoudiniPackageParams &PackageParams, AActor*& OutActor, USplineComponent*& OutSplineComponent, FName InOverrideFolderPath, AActor* InActor) { if (!IsValid(InActor)) { UActorFactory* Factory = GEditor ? GEditor->FindActorFactoryByClass(UActorFactoryEmptyActor::StaticClass()) : nullptr; if (!Factory) return false; OutActor = Factory->CreateActor(nullptr, InLevel, InSplineComponent->GetComponentTransform()); } else { OutActor = InActor; } // The default name will be based on the static mesh package, we would prefer it to be based on the Houdini asset const FName BaseActorName(PackageParams.ObjectName); const FString NewNameStr = MakeUniqueObjectNameIfNeeded(InLevel, OutActor->GetClass(), BaseActorName.ToString(), OutActor); RenameAndRelabelActor(OutActor, NewNameStr, false); OutActor->SetFolderPath(InOverrideFolderPath.IsNone() ? FName(PackageParams.HoudiniAssetName) : InOverrideFolderPath); USplineComponent* DuplicatedSplineComponent = DuplicateObject( InSplineComponent, OutActor, FName(MakeUniqueObjectNameIfNeeded(OutActor, InSplineComponent->GetClass(), PackageParams.ObjectName))); OutActor->AddInstanceComponent(DuplicatedSplineComponent); const bool bCreateIfMissing = true; USceneComponent* RootComponent = GetActorRootComponent(OutActor, bCreateIfMissing); DuplicatedSplineComponent->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform); // We duplicated the InSplineComponent, so we don't have to copy all of its properties, but we must set the // world transform DuplicatedSplineComponent->SetWorldTransform(InSplineComponent->GetComponentTransform()); FAssetRegistryModule::AssetCreated(DuplicatedSplineComponent); DuplicatedSplineComponent->RegisterComponent(); OutSplineComponent = DuplicatedSplineComponent; return true; } bool FHoudiniEngineBakeUtils::BakeCurve( const FHoudiniOutputObject& InOutputObject, FHoudiniBakedOutputObject& InBakedOutputObject, // const TArray& InAllBakedOutputs, const FHoudiniPackageParams &PackageParams, FHoudiniAttributeResolver& InResolver, bool bInReplaceActors, bool bInReplaceAssets, TArray& OutActors, TArray& OutPackagesToSave, AActor* InFallbackActor, const FString& InFallbackWorldOutlinerFolder) { USplineComponent* SplineComponent = Cast(InOutputObject.OutputComponent); if (!IsValid(SplineComponent)) return false; // By default spawn in the current level unless specified via the unreal_level_path attribute ULevel* DesiredLevel = GWorld->GetCurrentLevel(); bool bHasLevelPathAttribute = InOutputObject.CachedAttributes.Contains(HAPI_UNREAL_ATTRIB_LEVEL_PATH); if (bHasLevelPathAttribute) { UWorld* DesiredWorld = SplineComponent ? SplineComponent->GetWorld() : GWorld; // Get the package path from the unreal_level_apth attribute FString LevelPackagePath = InResolver.ResolveFullLevelPath(); bool bCreatedPackage = false; if (!FHoudiniEngineBakeUtils::FindOrCreateDesiredLevelFromLevelPath( LevelPackagePath, DesiredLevel, DesiredWorld, bCreatedPackage)) { // TODO: LOG ERROR IF NO LEVEL return false; } // If we have created a new level, add it to the packages to save // TODO: ? always add? if (bCreatedPackage && DesiredLevel) { // We can now save the package again, and unload it. OutPackagesToSave.Add(DesiredLevel->GetOutermost()); } } if(!DesiredLevel) return false; // Try to find the unreal_bake_actor, if specified, or fallback to the default named actor FName BakeActorName; AActor* FoundActor = nullptr; bool bHasBakeActorName = false; if (!FindUnrealBakeActor(InOutputObject, InBakedOutputObject, OutActors, DesiredLevel, *(PackageParams.ObjectName), bInReplaceActors, InFallbackActor, FoundActor, bHasBakeActorName, BakeActorName)) return false; // If we are baking in replace mode, remove the previous bake component if (bInReplaceAssets && !InBakedOutputObject.BakedComponent.IsEmpty()) { UActorComponent* PrevComponent = Cast(InBakedOutputObject.GetBakedComponentIfValid()); if (PrevComponent && PrevComponent->GetOwner() == FoundActor) { RemovePreviouslyBakedComponent(PrevComponent); } } FHoudiniPackageParams CurvePackageParams = PackageParams; CurvePackageParams.ObjectName = BakeActorName.ToString(); USplineComponent* NewSplineComponent = nullptr; const FName OutlinerFolderPath = GetOutlinerFolderPath(InOutputObject, *(CurvePackageParams.HoudiniAssetName)); if (!BakeCurve(SplineComponent, DesiredLevel, CurvePackageParams, FoundActor, NewSplineComponent, OutlinerFolderPath, FoundActor)) return false; InBakedOutputObject.Actor = FSoftObjectPath(FoundActor).ToString(); InBakedOutputObject.BakedComponent = FSoftObjectPath(NewSplineComponent).ToString(); // If we are baking in replace mode, remove previously baked components/instancers if (bInReplaceActors && bInReplaceAssets) { const bool bInDestroyBakedComponent = false; const bool bInDestroyBakedInstancedActors = true; const bool bInDestroyBakedInstancedComponents = true; DestroyPreviousBakeOutput( InBakedOutputObject, bInDestroyBakedComponent, bInDestroyBakedInstancedActors, bInDestroyBakedInstancedComponents); } FHoudiniEngineBakedActor Result; Result.Actor = FoundActor; Result.ActorBakeName = BakeActorName; Result.BakeFolderPath = PackageParams.BakeFolder; Result.BakedObjectPackageParams = PackageParams; OutActors.Add(Result); return true; } AActor* FHoudiniEngineBakeUtils::BakeInputHoudiniCurveToActor( UHoudiniSplineComponent * InHoudiniSplineComponent, const FHoudiniPackageParams & PackageParams, UWorld* WorldToSpawn, const FTransform & SpawnTransform) { if (!InHoudiniSplineComponent || InHoudiniSplineComponent->IsPendingKill()) return nullptr; TArray & DisplayPoints = InHoudiniSplineComponent->DisplayPoints; if (DisplayPoints.Num() < 2) return nullptr; ULevel* DesiredLevel = GWorld->GetCurrentLevel(); UActorFactory* Factory = GEditor ? GEditor->FindActorFactoryByClass(UActorFactoryEmptyActor::StaticClass()) : nullptr; if (!Factory) return nullptr; // Remove the actor if it exists for (auto & Actor : DesiredLevel->Actors) { if (!Actor) continue; if (Actor->GetName() == PackageParams.ObjectName) { UWorld* World = Actor->GetWorld(); if (!World) World = GWorld; Actor->RemoveFromRoot(); Actor->ConditionalBeginDestroy(); World->EditorDestroyActor(Actor, true); break; } } AActor* NewActor = Factory->CreateActor(nullptr, DesiredLevel, InHoudiniSplineComponent->GetComponentTransform()); USplineComponent* BakedUnrealSplineComponent = NewObject(NewActor); if (!BakedUnrealSplineComponent) return nullptr; // add display points to created unreal spline component for (int32 n = 0; n < DisplayPoints.Num(); ++n) { FVector & NextPoint = DisplayPoints[n]; BakedUnrealSplineComponent->AddSplinePoint(NextPoint, ESplineCoordinateSpace::Local); // Set the curve point type to be linear, since we are using display points BakedUnrealSplineComponent->SetSplinePointType(n, ESplinePointType::Linear); } NewActor->AddInstanceComponent(BakedUnrealSplineComponent); BakedUnrealSplineComponent->AttachToComponent(NewActor->GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform); FAssetRegistryModule::AssetCreated(NewActor); FAssetRegistryModule::AssetCreated(BakedUnrealSplineComponent); BakedUnrealSplineComponent->RegisterComponent(); // The default name will be based on the static mesh package, we would prefer it to be based on the Houdini asset const FString NewNameStr = MakeUniqueObjectNameIfNeeded(DesiredLevel, Factory->NewActorClass, *(PackageParams.ObjectName), NewActor); RenameAndRelabelActor(NewActor, NewNameStr, false); NewActor->SetFolderPath(FName(PackageParams.HoudiniAssetName)); return NewActor; } UBlueprint* FHoudiniEngineBakeUtils::BakeInputHoudiniCurveToBlueprint( UHoudiniSplineComponent * InHoudiniSplineComponent, const FHoudiniPackageParams & PackageParams, UWorld* WorldToSpawn, const FTransform & SpawnTransform) { if (!InHoudiniSplineComponent || InHoudiniSplineComponent->IsPendingKill()) return nullptr; FGuid BakeGUID = FGuid::NewGuid(); if (!BakeGUID.IsValid()) BakeGUID = FGuid::NewGuid(); // We only want half of generated guid string. FString BakeGUIDString = BakeGUID.ToString().Left(FHoudiniEngineUtils::PackageGUIDItemNameLength); // Generate Blueprint name. FString BlueprintName = PackageParams.ObjectName + "_BP"; // Generate unique package name. FString PackageName = PackageParams.BakeFolder + "/" + BlueprintName; PackageName = UPackageTools::SanitizePackageName(PackageName); // See if package exists, if it does, we need to regenerate the name. UPackage * Package = FindPackage(nullptr, *PackageName); if (Package && !Package->IsPendingKill()) { // Package does exist, there's a collision, we need to generate a new name. BakeGUID.Invalidate(); } else { // Create actual package. Package = CreatePackage(nullptr, *PackageName); } AActor * CreatedHoudiniSplineActor = FHoudiniEngineBakeUtils::BakeInputHoudiniCurveToActor( InHoudiniSplineComponent, PackageParams, WorldToSpawn, SpawnTransform); TArray PackagesToSave; UBlueprint * Blueprint = nullptr; if (CreatedHoudiniSplineActor && !CreatedHoudiniSplineActor->IsPendingKill()) { UObject* Asset = nullptr; Asset = StaticFindObjectFast(UObject::StaticClass(), Package, FName(*BlueprintName)); if (!Asset) { UBlueprintFactory* Factory = NewObject(); FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); Asset = AssetToolsModule.Get().CreateAsset( BlueprintName, PackageParams.BakeFolder, UBlueprint::StaticClass(), Factory, FName("ContentBrowserNewAsset")); } TArray Components; for (auto & Next : CreatedHoudiniSplineActor->GetComponents()) { Components.Add(Next); } Blueprint = Cast(Asset); // Clear old Blueprint Node tree USimpleConstructionScript* SCS = Blueprint->SimpleConstructionScript; int32 NodeSize = SCS->GetAllNodes().Num(); for (int32 n = NodeSize - 1; n >= 0; --n) SCS->RemoveNode(SCS->GetAllNodes()[n]); FKismetEditorUtilities::AddComponentsToBlueprint(Blueprint, Components); CreatedHoudiniSplineActor->RemoveFromRoot(); CreatedHoudiniSplineActor->ConditionalBeginDestroy(); GWorld->EditorDestroyActor(CreatedHoudiniSplineActor, true); Package->MarkPackageDirty(); PackagesToSave.Add(Package); } // Save the created BP package. FHoudiniEngineBakeUtils::SaveBakedPackages (PackagesToSave); return Blueprint; } void FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( UPackage * Package, UObject * Object, const TCHAR * Key, const TCHAR * Value) { if (!Package || Package->IsPendingKill()) return; UMetaData * MetaData = Package->GetMetaData(); if (MetaData && !MetaData->IsPendingKill()) MetaData->SetValue(Object, Key, Value); } bool FHoudiniEngineBakeUtils:: GetHoudiniGeneratedNameFromMetaInformation( UPackage * Package, UObject * Object, FString & HoudiniName) { if (!Package || Package->IsPendingKill()) return false; UMetaData * MetaData = Package->GetMetaData(); if (!MetaData || MetaData->IsPendingKill()) return false; if (MetaData->HasValue(Object, HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT)) { // Retrieve name used for package generation. const FString NameFull = MetaData->GetValue(Object, HAPI_UNREAL_PACKAGE_META_GENERATED_NAME); //HoudiniName = NameFull.Left(FMath::Min(NameFull.Len(), FHoudiniEngineUtils::PackageGUIDItemNameLength)); HoudiniName = NameFull; return true; } return false; } UMaterialInterface * FHoudiniEngineBakeUtils::DuplicateMaterialAndCreatePackage( UMaterialInterface * Material, UMaterialInterface* PreviousBakeMaterial, const FString & MaterialName, const FHoudiniPackageParams& ObjectPackageParams, TArray & OutGeneratedPackages, TMap& InOutAlreadyBakedMaterialsMap) { if (InOutAlreadyBakedMaterialsMap.Contains(Material)) { return InOutAlreadyBakedMaterialsMap[Material]; } UMaterialInterface * DuplicatedMaterial = nullptr; FString CreatedMaterialName; // Create material package. Use the same package params as static mesh, but with the material's name FHoudiniPackageParams MaterialPackageParams = ObjectPackageParams; MaterialPackageParams.ObjectName = MaterialName; // Check if there is a valid previous material. If so, get the bake counter for consistency in // replace or iterative package naming bool bIsPreviousBakeMaterialValid = IsValid(PreviousBakeMaterial); int32 BakeCounter = 0; TArray PreviousBakeMaterialExpressions; if (bIsPreviousBakeMaterialValid && PreviousBakeMaterial->IsA(UMaterial::StaticClass())) { UMaterial * PreviousMaterialCast = Cast(PreviousBakeMaterial); bIsPreviousBakeMaterialValid = MaterialPackageParams.MatchesPackagePathNameExcludingBakeCounter(PreviousBakeMaterial); if (bIsPreviousBakeMaterialValid && PreviousMaterialCast) { MaterialPackageParams.GetBakeCounterFromBakedAsset(PreviousBakeMaterial, BakeCounter); PreviousBakeMaterialExpressions = PreviousMaterialCast->Expressions; } } UPackage * MaterialPackage = MaterialPackageParams.CreatePackageForObject(CreatedMaterialName, BakeCounter); if (!MaterialPackage || MaterialPackage->IsPendingKill()) return nullptr; // Clone material. DuplicatedMaterial = DuplicateObject< UMaterialInterface >(Material, MaterialPackage, *CreatedMaterialName); if (!DuplicatedMaterial || DuplicatedMaterial->IsPendingKill()) return nullptr; // Add meta information. FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( MaterialPackage, DuplicatedMaterial, HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT, TEXT("true")); FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( MaterialPackage, DuplicatedMaterial, HAPI_UNREAL_PACKAGE_META_GENERATED_NAME, *CreatedMaterialName); // Retrieve and check various sampling expressions. If they contain textures, duplicate (and bake) them. UMaterial * DuplicatedMaterialCast = Cast(DuplicatedMaterial); if (DuplicatedMaterialCast) { const int32 NumExpressions = DuplicatedMaterialCast->Expressions.Num(); for (int32 ExpressionIdx = 0; ExpressionIdx < NumExpressions; ++ExpressionIdx) { UMaterialExpression* Expression = DuplicatedMaterialCast->Expressions[ExpressionIdx]; UMaterialExpression* PreviousBakeExpression = nullptr; if (bIsPreviousBakeMaterialValid && PreviousBakeMaterialExpressions.IsValidIndex(ExpressionIdx)) { PreviousBakeExpression = PreviousBakeMaterialExpressions[ExpressionIdx]; } FHoudiniEngineBakeUtils::ReplaceDuplicatedMaterialTextureSample( Expression, PreviousBakeExpression, MaterialPackageParams, OutGeneratedPackages); } } // Notify registry that we have created a new duplicate material. FAssetRegistryModule::AssetCreated(DuplicatedMaterial); // Dirty the material package. DuplicatedMaterial->MarkPackageDirty(); // Recompile the baked material // DuplicatedMaterial->ForceRecompileForRendering(); // Use UMaterialEditingLibrary::RecompileMaterial since it correctly updates texture references in the material // which ForceRecompileForRendering does not do if (DuplicatedMaterialCast) { UMaterialEditingLibrary::RecompileMaterial(DuplicatedMaterialCast); } OutGeneratedPackages.Add(MaterialPackage); InOutAlreadyBakedMaterialsMap.Add(Material, DuplicatedMaterial); return DuplicatedMaterial; } void FHoudiniEngineBakeUtils::ReplaceDuplicatedMaterialTextureSample( UMaterialExpression * MaterialExpression, UMaterialExpression* PreviousBakeMaterialExpression, const FHoudiniPackageParams& PackageParams, TArray & OutCreatedPackages) { UMaterialExpressionTextureSample * TextureSample = Cast< UMaterialExpressionTextureSample >(MaterialExpression); if (!TextureSample || TextureSample->IsPendingKill()) return; UTexture2D * Texture = Cast< UTexture2D >(TextureSample->Texture); if (!Texture || Texture->IsPendingKill()) return; UPackage * TexturePackage = Cast< UPackage >(Texture->GetOuter()); if (!TexturePackage || TexturePackage->IsPendingKill()) return; // Try to get the previous bake's texture UTexture2D* PreviousBakeTexture = nullptr; if (IsValid(PreviousBakeMaterialExpression)) { UMaterialExpressionTextureSample* PreviousBakeTextureSample = Cast< UMaterialExpressionTextureSample >(PreviousBakeMaterialExpression); if (IsValid(PreviousBakeTextureSample)) PreviousBakeTexture = Cast< UTexture2D >(PreviousBakeTextureSample->Texture); } FString GeneratedTextureName; if (FHoudiniEngineBakeUtils::GetHoudiniGeneratedNameFromMetaInformation( TexturePackage, Texture, GeneratedTextureName)) { // Duplicate texture. UTexture2D * DuplicatedTexture = FHoudiniEngineBakeUtils::DuplicateTextureAndCreatePackage( Texture, PreviousBakeTexture, GeneratedTextureName, PackageParams, OutCreatedPackages); // Re-assign generated texture. TextureSample->Texture = DuplicatedTexture; } } UTexture2D * FHoudiniEngineBakeUtils::DuplicateTextureAndCreatePackage( UTexture2D * Texture, UTexture2D* PreviousBakeTexture, const FString & SubTextureName, const FHoudiniPackageParams& PackageParams, TArray & OutCreatedPackages) { UTexture2D* DuplicatedTexture = nullptr; #if WITH_EDITOR // Retrieve original package of this texture. UPackage * TexturePackage = Cast< UPackage >(Texture->GetOuter()); if (!TexturePackage || TexturePackage->IsPendingKill()) return nullptr; FString GeneratedTextureName; if (FHoudiniEngineBakeUtils::GetHoudiniGeneratedNameFromMetaInformation(TexturePackage, Texture, GeneratedTextureName)) { UMetaData * MetaData = TexturePackage->GetMetaData(); if (!MetaData || MetaData->IsPendingKill()) return nullptr; // Retrieve texture type. const FString & TextureType = MetaData->GetValue(Texture, HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_TYPE); FString CreatedTextureName; // Create texture package. Use the same package params as material's, but with object name appended by generated texture's name FHoudiniPackageParams TexturePackageParams = PackageParams; TexturePackageParams.ObjectName = TexturePackageParams.ObjectName + "_" + GeneratedTextureName; // Determine the bake counter of the previous bake's texture (if exists/valid) for naming consistency when // replacing/iterating bool bIsPreviousBakeTextureValid = IsValid(PreviousBakeTexture); int32 BakeCounter = 0; if (bIsPreviousBakeTextureValid) { bIsPreviousBakeTextureValid = TexturePackageParams.MatchesPackagePathNameExcludingBakeCounter(PreviousBakeTexture); if (bIsPreviousBakeTextureValid) { TexturePackageParams.GetBakeCounterFromBakedAsset(PreviousBakeTexture, BakeCounter); } } UPackage * NewTexturePackage = TexturePackageParams.CreatePackageForObject(CreatedTextureName, BakeCounter); if (!NewTexturePackage || NewTexturePackage->IsPendingKill()) return nullptr; // Clone texture. DuplicatedTexture = DuplicateObject< UTexture2D >(Texture, NewTexturePackage, *CreatedTextureName); if (!DuplicatedTexture || DuplicatedTexture->IsPendingKill()) return nullptr; // Add meta information. FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( NewTexturePackage, DuplicatedTexture, HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT, TEXT("true")); FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( NewTexturePackage, DuplicatedTexture, HAPI_UNREAL_PACKAGE_META_GENERATED_NAME, *CreatedTextureName); FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( NewTexturePackage, DuplicatedTexture, HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_TYPE, *TextureType); // Notify registry that we have created a new duplicate texture. FAssetRegistryModule::AssetCreated(DuplicatedTexture); // Dirty the texture package. DuplicatedTexture->MarkPackageDirty(); OutCreatedPackages.Add(NewTexturePackage); } #endif return DuplicatedTexture; } bool FHoudiniEngineBakeUtils::DeleteBakedHoudiniAssetActor(UHoudiniAssetComponent* HoudiniAssetComponent) { if (!HoudiniAssetComponent || HoudiniAssetComponent->IsPendingKill()) return false; AActor * ActorOwner = HoudiniAssetComponent->GetOwner(); if (!ActorOwner || ActorOwner->IsPendingKill()) return false; UWorld* World = ActorOwner->GetWorld(); if (!World) World = GWorld; World->EditorDestroyActor(ActorOwner, false); return true; } void FHoudiniEngineBakeUtils::SaveBakedPackages(TArray & PackagesToSave, bool bSaveCurrentWorld) { UWorld * CurrentWorld = nullptr; if (bSaveCurrentWorld && GEditor) CurrentWorld = GEditor->GetEditorWorldContext().World(); if (CurrentWorld) { // Save the current map FString CurrentWorldPath = FPaths::GetBaseFilename(CurrentWorld->GetPathName(), false); UPackage* CurrentWorldPackage = CreatePackage(nullptr, *CurrentWorldPath); if (CurrentWorldPackage) { CurrentWorldPackage->MarkPackageDirty(); PackagesToSave.Add(CurrentWorldPackage); } } FEditorFileUtils::PromptForCheckoutAndSave(PackagesToSave, true, false); } bool FHoudiniEngineBakeUtils::FindOutputObject(const UObject* InObjectToFind, EHoudiniOutputType InOutputType, const TArray InOutputs, int32& OutOutputIndex, FHoudiniOutputObjectIdentifier &OutIdentifier) { if (!InObjectToFind || InObjectToFind->IsPendingKill()) return false; const int32 NumOutputs = InOutputs.Num(); for (int32 OutputIdx = 0; OutputIdx < NumOutputs; ++OutputIdx) { const UHoudiniOutput* CurOutput = InOutputs[OutputIdx]; if (!IsValid(CurOutput)) continue; if (CurOutput->GetType() != InOutputType) continue; for (auto& CurOutputObject : CurOutput->GetOutputObjects()) { if (CurOutputObject.Value.OutputObject == InObjectToFind || CurOutputObject.Value.OutputComponent == InObjectToFind || CurOutputObject.Value.ProxyObject == InObjectToFind || CurOutputObject.Value.ProxyComponent == InObjectToFind) { OutOutputIndex = OutputIdx; OutIdentifier = CurOutputObject.Key; return true; } } } return false; } bool FHoudiniEngineBakeUtils::IsObjectTemporary(UObject* InObject, EHoudiniOutputType InOutputType, UHoudiniAssetComponent* InHAC) { if (!InObject || InObject->IsPendingKill()) return false; FString TempPath = FString(); // TODO: Get the HAC outputs in a better way? TArray Outputs; if (InHAC && !InHAC->IsPendingKill()) { const int32 NumOutputs = InHAC->GetNumOutputs(); Outputs.Reserve(NumOutputs); for (int32 OutputIdx = 0; OutputIdx < NumOutputs; ++OutputIdx) { Outputs.Add(InHAC->GetOutputAt(OutputIdx)); } TempPath = InHAC->TemporaryCookFolder.Path; } return IsObjectTemporary(InObject, InOutputType, Outputs, TempPath); } bool FHoudiniEngineBakeUtils::IsObjectInTempFolder(UObject* const InObject, const FString& InTemporaryCookFolder) { if (!IsValid(InObject)) return false; // Check the package path for this object // If it is in the HAC temp directory, assume it is temporary, and will need to be duplicated UPackage* ObjectPackage = InObject->GetOutermost(); if (ObjectPackage && !ObjectPackage->IsPendingKill()) { const FString PathName = ObjectPackage->GetPathName(); if (PathName.StartsWith(InTemporaryCookFolder)) return true; // Also check the default temp folder const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault(); if (PathName.StartsWith(HoudiniRuntimeSettings->DefaultTemporaryCookFolder)) return true; } return false; } bool FHoudiniEngineBakeUtils::IsObjectTemporary( UObject* InObject, EHoudiniOutputType InOutputType, const TArray& InParentOutputs, const FString& InTemporaryCookFolder) { if (!InObject || InObject->IsPendingKill()) return false; int32 ParentOutputIndex = -1; FHoudiniOutputObjectIdentifier Identifier; if (FindOutputObject(InObject, InOutputType, InParentOutputs, ParentOutputIndex, Identifier)) return true; // Check the package path for this object // If it is in the HAC temp directory, assume it is temporary, and will need to be duplicated if (IsObjectInTempFolder(InObject, InTemporaryCookFolder)) return true; /* UPackage* ObjectPackage = InObject->GetOutermost(); if (ObjectPackage && !ObjectPackage->IsPendingKill()) { // TODO: this just indicates that the object was generated by H // it could as well have been baked before... // we should probably add a "temp" metadata // Look in the meta info as well?? UMetaData * MetaData = ObjectPackage->GetMetaData(); if (!MetaData || MetaData->IsPendingKill()) return false; if (MetaData->HasValue(InObject, HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT)) return true; } */ return false; } void FHoudiniEngineBakeUtils::CopyPropertyToNewActorAndComponent( AActor* NewActor, UStaticMeshComponent* NewSMC, UStaticMeshComponent* InSMC, bool bInCopyWorldTransform) { if (!NewSMC || NewSMC->IsPendingKill()) return; if (!InSMC || InSMC->IsPendingKill()) return; // Copy properties to new actor //UStaticMeshComponent* OtherSMC_NonConst = const_cast(StaticMeshComponent); NewSMC->SetCollisionProfileName(InSMC->GetCollisionProfileName()); NewSMC->SetCollisionEnabled(InSMC->GetCollisionEnabled()); NewSMC->LightmassSettings = InSMC->LightmassSettings; NewSMC->CastShadow = InSMC->CastShadow; NewSMC->SetMobility(InSMC->Mobility); UBodySetup* InBodySetup = InSMC->GetBodySetup(); UBodySetup* NewBodySetup = NewSMC->GetBodySetup(); if (InBodySetup && NewBodySetup) { // Copy the BodySetup NewBodySetup->CopyBodyPropertiesFrom(InBodySetup); // We need to recreate the physics mesh for the new body setup NewBodySetup->InvalidatePhysicsData(); NewBodySetup->CreatePhysicsMeshes(); // Only copy the physical material if it's different from the default one, // As this was causing crashes on BakeToActors in some cases if (GEngine != NULL && NewBodySetup->GetPhysMaterial() != GEngine->DefaultPhysMaterial) NewSMC->SetPhysMaterialOverride(InBodySetup->GetPhysMaterial()); } if (NewActor && !NewActor->IsPendingKill()) NewActor->SetActorHiddenInGame(InSMC->bHiddenInGame); NewSMC->SetVisibility(InSMC->IsVisible()); // TODO: // // Reapply the uproperties modified by attributes on the new component // FHoudiniEngineUtils::UpdateAllPropertyAttributesOnObject(InSMC, InHGPO); // The below code is from EditorUtilities::CopyActorProperties and modified to only copy from one component to another UClass* ComponentClass = nullptr; if (InSMC->GetClass()->IsChildOf(NewSMC->GetClass())) { ComponentClass = NewSMC->GetClass(); } else if (NewSMC->GetClass()->IsChildOf(InSMC->GetClass())) { ComponentClass = InSMC->GetClass(); } else { HOUDINI_LOG_WARNING( TEXT("Incompatible component classes in CopyPropertyToNewActorAndComponent: %s vs %s"), *(InSMC->GetName()), *(NewSMC->GetClass()->GetName())); NewSMC->PostEditChange(); return; } TSet SourceUCSModifiedProperties; InSMC->GetUCSModifiedProperties(SourceUCSModifiedProperties); AActor* SourceActor = InSMC->GetOwner(); if (!IsValid(SourceActor)) { NewSMC->PostEditChange(); return; } TArray ModifiedObjects; const EditorUtilities::FCopyOptions Options(EditorUtilities::ECopyOptions::CallPostEditChangeProperty); // Copy component properties for( FProperty* Property = ComponentClass->PropertyLink; Property != nullptr; Property = Property->PropertyLinkNext ) { const bool bIsTransient = !!( Property->PropertyFlags & CPF_Transient ); const bool bIsIdentical = Property->Identical_InContainer( InSMC, NewSMC ); const bool bIsComponent = !!( Property->PropertyFlags & ( CPF_InstancedReference | CPF_ContainsInstancedReference ) ); const bool bIsTransform = Property->GetFName() == USceneComponent::GetRelativeScale3DPropertyName() || Property->GetFName() == USceneComponent::GetRelativeLocationPropertyName() || Property->GetFName() == USceneComponent::GetRelativeRotationPropertyName(); // auto SourceComponentIsRoot = [&]() // { // USceneComponent* RootComponent = SourceActor->GetRootComponent(); // if (InSMC == RootComponent) // { // return true; // } // return false; // }; // if( !bIsTransient && !bIsIdentical && !bIsComponent && !SourceUCSModifiedProperties.Contains(Property) // && ( !bIsTransform || !SourceComponentIsRoot() ) ) if( !bIsTransient && !bIsIdentical && !bIsComponent && !SourceUCSModifiedProperties.Contains(Property) && !bIsTransform ) { // const bool bIsSafeToCopy = (!(Options.Flags & EditorUtilities::ECopyOptions::OnlyCopyEditOrInterpProperties) || (Property->HasAnyPropertyFlags(CPF_Edit | CPF_Interp))) // && (!(Options.Flags & EditorUtilities::ECopyOptions::SkipInstanceOnlyProperties) || (!Property->HasAllPropertyFlags(CPF_DisableEditOnTemplate))); const bool bIsSafeToCopy = true; if( bIsSafeToCopy ) { if (!Options.CanCopyProperty(*Property, *SourceActor)) { continue; } if( !ModifiedObjects.Contains(NewSMC) ) { NewSMC->SetFlags(RF_Transactional); NewSMC->Modify(); ModifiedObjects.Add(NewSMC); } if (Options.Flags & EditorUtilities::ECopyOptions::CallPostEditChangeProperty) { // @todo simulate: Should we be calling this on the component instead? NewActor->PreEditChange( Property ); } EditorUtilities::CopySingleProperty(InSMC, NewSMC, Property); if (Options.Flags & EditorUtilities::ECopyOptions::CallPostEditChangeProperty) { FPropertyChangedEvent PropertyChangedEvent( Property ); NewActor->PostEditChangeProperty( PropertyChangedEvent ); } } } } if (bInCopyWorldTransform) { NewSMC->SetWorldTransform(InSMC->GetComponentTransform()); } NewSMC->PostEditChange(); }; bool FHoudiniEngineBakeUtils::RemovePreviouslyBakedActor( AActor* InNewBakedActor, ULevel* InLevel, const FHoudiniPackageParams& InPackageParams) { // Remove a previous bake actor if it exists for (auto & Actor : InLevel->Actors) { if (!Actor) continue; if (Actor != InNewBakedActor && Actor->GetName() == InPackageParams.ObjectName) { UWorld* World = Actor->GetWorld(); if (!World) World = GWorld; Actor->RemoveFromRoot(); Actor->ConditionalBeginDestroy(); World->EditorDestroyActor(Actor, true); return true; } } return false; } bool FHoudiniEngineBakeUtils::RemovePreviouslyBakedComponent(UActorComponent* InComponent) { if (!IsValid(InComponent)) return false; // Remove from its actor first if (InComponent->GetOwner()) InComponent->GetOwner()->RemoveOwnedComponent(InComponent); // Detach from its parent component if attached USceneComponent* SceneComponent = Cast(InComponent); if (IsValid(SceneComponent)) SceneComponent->DetachFromComponent(FDetachmentTransformRules::KeepRelativeTransform); InComponent->UnregisterComponent(); InComponent->DestroyComponent(); return true; } FName FHoudiniEngineBakeUtils::GetOutputFolderPath(UObject* InOutputOwner) { // Get an output folder path for PDG outputs generated by InOutputOwner. // The folder path is: / FString FolderName; FName FolderDirName; AActor* OuterActor = Cast(InOutputOwner); if (OuterActor) { FolderName = OuterActor->GetActorLabel(); FolderDirName = OuterActor->GetFolderPath(); } else { FolderName = InOutputOwner->GetName(); } if (!FolderDirName.IsNone()) return FName(FString::Printf(TEXT("%s/%s"), *FolderDirName.ToString(), *FolderName)); else return FName(FolderName); } void FHoudiniEngineBakeUtils::RenameAsset(UObject* InAsset, const FString& InNewName, bool bMakeUniqueIfNotUnique) { FAssetToolsModule& AssetToolsModule = FModuleManager::GetModuleChecked(TEXT("AssetTools")); const FSoftObjectPath OldPath = FSoftObjectPath(InAsset); FString NewName; if (bMakeUniqueIfNotUnique) NewName = MakeUniqueObjectNameIfNeeded(InAsset->GetOutermost()/*GetPackage()*/, InAsset->GetClass(), InNewName, InAsset); else NewName = InNewName; FHoudiniEngineUtils::RenameObject(InAsset, *NewName); const FSoftObjectPath NewPath = FSoftObjectPath(InAsset); if (OldPath != NewPath) { TArray RenameData; RenameData.Add(FAssetRenameData(OldPath, NewPath, true)); AssetToolsModule.Get().RenameAssets(RenameData); } } void FHoudiniEngineBakeUtils::RenameAndRelabelActor(AActor* InActor, const FString& InNewName, bool bMakeUniqueIfNotUnique) { if (!IsValid(InActor)) return; FAssetToolsModule& AssetToolsModule = FModuleManager::GetModuleChecked(TEXT("AssetTools")); const FSoftObjectPath OldPath = FSoftObjectPath(InActor); FString NewName; if (bMakeUniqueIfNotUnique) NewName = MakeUniqueObjectNameIfNeeded(InActor->GetOuter(), InActor->GetClass(), InNewName, InActor); else NewName = InNewName; FHoudiniEngineUtils::RenameObject(InActor, *NewName); FHoudiniEngineRuntimeUtils::SetActorLabel(InActor, NewName); const FSoftObjectPath NewPath = FSoftObjectPath(InActor); if (OldPath != NewPath) { TArray RenameData; RenameData.Add(FAssetRenameData(OldPath, NewPath, true)); AssetToolsModule.Get().RenameAssets(RenameData); } } bool FHoudiniEngineBakeUtils::DetachAndRenameBakedPDGOutputActor( AActor* InActor, const FString& InNewName, const FName& InFolderPath) { if (!IsValid(InActor)) { HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineUtils::DetachAndRenameBakedPDGOutputActor]: InActor is null.")); return false; } if (InNewName.TrimStartAndEnd().IsEmpty()) { HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineUtils::DetachAndRenameBakedPDGOutputActor]: A valid actor name must be specified.")); return false; } // Detach from parent InActor->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform); // Rename const bool bMakeUniqueIfNotUnique = true; RenameAndRelabelActor(InActor, InNewName, bMakeUniqueIfNotUnique); InActor->SetFolderPath(InFolderPath); return true; } bool FHoudiniEngineBakeUtils::BakePDGWorkResultObject( UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InNode, int32 InWorkResultArrayIndex, int32 InWorkResultObjectArrayIndex, bool bInReplaceActors, bool bInReplaceAssets, bool bInBakeToWorkResultActor, bool bInIsAutoBake, TArray& OutBakedActors, TArray& OutPackagesToSave, FHoudiniEngineOutputStats& OutBakeStats, TArray const* InOutputTypesToBake, TArray const* InInstancerComponentTypesToBake, const FString& InFallbackWorldOutlinerFolder) { if (!IsValid(InPDGAssetLink)) return false; if (!IsValid(InNode)) return false; if (!InNode->WorkResult.IsValidIndex(InWorkResultArrayIndex)) return false; FTOPWorkResult& WorkResult = InNode->WorkResult[InWorkResultArrayIndex]; if (!WorkResult.ResultObjects.IsValidIndex(InWorkResultObjectArrayIndex)) return false; FTOPWorkResultObject& WorkResultObject = WorkResult.ResultObjects[InWorkResultObjectArrayIndex]; TArray& Outputs = WorkResultObject.GetResultOutputs(); if (Outputs.Num() == 0) return true; if (WorkResultObject.State != EPDGWorkResultState::Loaded) { if (bInIsAutoBake && WorkResultObject.AutoBakedSinceLastLoad()) { HOUDINI_LOG_MESSAGE(TEXT("[FHoudiniEngineBakeUtils::BakePDGTOPNodeOutputsKeepActors]: WorkResultObject (%s) is not loaded but was auto-baked since its last load."), *WorkResultObject.Name); return true; } HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineBakeUtils::BakePDGTOPNodeOutputsKeepActors]: WorkResultObject (%s) is not loaded, cannot bake it."), *WorkResultObject.Name); return false; } AActor* WorkResultObjectActor = WorkResultObject.GetOutputActorOwner().GetOutputActor(); if (!IsValid(WorkResultObjectActor)) { HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineBakeUtils::BakePDGTOPNodeOutputsKeepActors]: WorkResultObjectActor (%s) is null (unexpected since # Outputs > 0)"), *WorkResultObject.Name); return false; } // OutBakedActors contains each actor that contains baked PDG results. Actors may // appear in the array more than once if they have more than one baked result/component associated with // them // TArray BakedActorsForWorkResultObject; const int32 NumBakedPre = OutBakedActors.Num(); const FString HoudiniAssetName(WorkResultObject.Name); // Find the previous bake output for this work result object FString Key; InNode->GetBakedWorkResultObjectOutputsKey(InWorkResultArrayIndex, InWorkResultObjectArrayIndex, Key); FHoudiniPDGWorkResultObjectBakedOutput& BakedOutputContainer = InNode->GetBakedWorkResultObjectsOutputs().FindOrAdd(Key); const UHoudiniAssetComponent* HoudiniAssetComponent = FHoudiniEngineUtils::GetOuterHoudiniAssetComponent(InPDGAssetLink); check(IsValid(HoudiniAssetComponent)); BakeHoudiniOutputsToActors( HoudiniAssetComponent, Outputs, BakedOutputContainer.BakedOutputs, HoudiniAssetName, WorkResultObjectActor->GetActorTransform(), InPDGAssetLink->BakeFolder, InPDGAssetLink->GetTemporaryCookFolder(), bInReplaceActors, bInReplaceAssets, OutBakedActors, OutPackagesToSave, OutBakeStats, InOutputTypesToBake, InInstancerComponentTypesToBake, bInBakeToWorkResultActor ? WorkResultObjectActor : nullptr, InFallbackWorldOutlinerFolder); // Set the PDG indices on the output baked actor entries FOutputActorOwner& OutputActorOwner = WorkResultObject.GetOutputActorOwner(); AActor* const WROActor = OutputActorOwner.GetOutputActor(); FHoudiniEngineBakedActor const * BakedWROActorEntry = nullptr; const int32 NumBakedPost = OutBakedActors.Num(); if (NumBakedPost > NumBakedPre) { for (int32 Index = FMath::Max(0, NumBakedPre); Index < NumBakedPost; ++Index) { FHoudiniEngineBakedActor& BakedActorEntry = OutBakedActors[Index]; BakedActorEntry.PDGWorkResultArrayIndex = InWorkResultArrayIndex; BakedActorEntry.PDGWorkItemIndex = WorkResult.WorkItemIndex; BakedActorEntry.PDGWorkResultObjectArrayIndex = InWorkResultObjectArrayIndex; if (WROActor && BakedActorEntry.Actor == WROActor) { BakedWROActorEntry = &BakedActorEntry; } } } // If anything was baked to WorkResultObjectActor, detach it from its parent if (bInBakeToWorkResultActor) { // if we re-used the temp actor as a bake actor, then remove its temp outputs WorkResultObject.DestroyResultOutputs(); if (WROActor) { if (BakedWROActorEntry) { OutputActorOwner.SetOutputActor(nullptr); const FString OldActorPath = FSoftObjectPath(WROActor).ToString(); DetachAndRenameBakedPDGOutputActor( WROActor, BakedWROActorEntry->ActorBakeName.ToString(), BakedWROActorEntry->WorldOutlinerFolder); const FString NewActorPath = FSoftObjectPath(WROActor).ToString(); if (OldActorPath != NewActorPath) { // Fix cached string reference in baked outputs to WROActor for (FHoudiniBakedOutput& BakedOutput : BakedOutputContainer.BakedOutputs) { for (auto& Entry : BakedOutput.BakedOutputObjects) { if (Entry.Value.Actor == OldActorPath) Entry.Value.Actor = NewActorPath; } } } } else { OutputActorOwner.DestroyOutputActor(); } } } if (bInIsAutoBake) WorkResultObject.SetAutoBakedSinceLastLoad(true); // OutBakedActors.Append(BakedActorsForWorkResultObject); return true; } void FHoudiniEngineBakeUtils::CheckPDGAutoBakeAfterResultObjectLoaded( UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InNode, int32 InWorkItemHAPIIndex, int32 InWorkItemResultInfoIndex) { if (!IsValid(InPDGAssetLink)) return; if (!InPDGAssetLink->bBakeAfterAllWorkResultObjectsLoaded) return; if (!IsValid(InNode)) return; // Check if the node is ready for baking: all work items must be complete bool bDoNotBake = false; bool bPendingBakeItems = false; if (!InNode->AreAllWorkItemsComplete() || InNode->AnyWorkItemsFailed()) bDoNotBake = true; // Check if the node is ready for baking: all work items must be loaded if (!bDoNotBake) { for (const FTOPWorkResult& WorkResult : InNode->WorkResult) { for (const FTOPWorkResultObject& WRO : WorkResult.ResultObjects) { if (WRO.State != EPDGWorkResultState::Loaded && !WRO.AutoBakedSinceLastLoad()) { bDoNotBake = true; break; } } if (bDoNotBake) break; } } if (!bDoNotBake) { // Check which outputs are selected for baking: selected node, selected network or all // And only bake if the node falls within the criteria UTOPNetwork const * const SelectedTOPNetwork = InPDGAssetLink->GetSelectedTOPNetwork(); UTOPNode const * const SelectedTOPNode = InPDGAssetLink->GetSelectedTOPNode(); switch (InPDGAssetLink->PDGBakeSelectionOption) { case EPDGBakeSelectionOption::SelectedNetwork: if (!IsValid(SelectedTOPNetwork) || !InNode->IsParentTOPNetwork(SelectedTOPNetwork)) { HOUDINI_LOG_WARNING( TEXT("Not baking Node %s (Net %s): not in selected network"), InNode ? *InNode->GetName() : TEXT(""), SelectedTOPNetwork ? *SelectedTOPNetwork->GetName() : TEXT("")); bDoNotBake = true; } break; case EPDGBakeSelectionOption::SelectedNode: if (InNode != SelectedTOPNode) { HOUDINI_LOG_WARNING( TEXT("Not baking Node %s (Net %s): not the selected node"), InNode ? *InNode->GetName() : TEXT(""), SelectedTOPNetwork ? *SelectedTOPNetwork->GetName() : TEXT("")); bDoNotBake = true; } break; case EPDGBakeSelectionOption::All: default: break; } } // If there are no nodes left to auto-bake, broadcast the onpostbake delegate if (bDoNotBake && !InPDGAssetLink->AnyRemainingAutoBakeNodes()) InPDGAssetLink->HandleOnPostBake(true); if (bDoNotBake) return; bool bSuccess = false; const bool bIsAutoBake = true; switch (InPDGAssetLink->HoudiniEngineBakeOption) { case EHoudiniEngineBakeOption::ToActor: bSuccess = FHoudiniEngineBakeUtils::BakePDGTOPNodeOutputsKeepActors(InPDGAssetLink, InNode, bIsAutoBake, InPDGAssetLink->PDGBakePackageReplaceMode, InPDGAssetLink->bRecenterBakedActors); break; case EHoudiniEngineBakeOption::ToBlueprint: bSuccess = FHoudiniEngineBakeUtils::BakePDGTOPNodeBlueprints(InPDGAssetLink, InNode, bIsAutoBake, InPDGAssetLink->PDGBakePackageReplaceMode, InPDGAssetLink->bRecenterBakedActors); break; default: HOUDINI_LOG_WARNING(TEXT("Unsupported HoudiniEngineBakeOption %i"), InPDGAssetLink->HoudiniEngineBakeOption); } if (!InPDGAssetLink->AnyRemainingAutoBakeNodes()) InPDGAssetLink->HandleOnPostBake(bSuccess); } bool FHoudiniEngineBakeUtils::BakePDGTOPNodeOutputsKeepActors( UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InNode, bool bInBakeForBlueprint, bool bInIsAutoBake, const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, TArray& OutBakedActors, TArray& OutPackagesToSave, FHoudiniEngineOutputStats& OutBakeStats) { if (!InPDGAssetLink || InPDGAssetLink->IsPendingKill()) return false; if (!IsValid(InNode)) return false; // Determine the output world outliner folder path via the PDG asset link's // owner's folder path and name UObject* PDGOwner = InPDGAssetLink->GetOwnerActor(); if (!PDGOwner) PDGOwner = InPDGAssetLink->GetOuter(); const FName& FallbackWorldOutlinerFolderPath = GetOutputFolderPath(PDGOwner); // Determine the actor/package replacement settings const bool bReplaceActors = !bInBakeForBlueprint && InPDGBakePackageReplaceMode == EPDGBakePackageReplaceModeOption::ReplaceExistingAssets; const bool bReplaceAssets = InPDGBakePackageReplaceMode == EPDGBakePackageReplaceModeOption::ReplaceExistingAssets; // Determine the output types to bake: don't bake landscapes in blueprint baking mode TArray OutputTypesToBake; TArray InstancerComponentTypesToBake; if (bInBakeForBlueprint) { OutputTypesToBake.Add(EHoudiniOutputType::Mesh); OutputTypesToBake.Add(EHoudiniOutputType::Instancer); OutputTypesToBake.Add(EHoudiniOutputType::Curve); InstancerComponentTypesToBake.Add(EHoudiniInstancerComponentType::StaticMeshComponent); InstancerComponentTypesToBake.Add(EHoudiniInstancerComponentType::InstancedStaticMeshComponent); InstancerComponentTypesToBake.Add(EHoudiniInstancerComponentType::MeshSplitInstancerComponent); InstancerComponentTypesToBake.Add(EHoudiniInstancerComponentType::FoliageAsHierarchicalInstancedStaticMeshComponent); } const int32 NumWorkResults = InNode->WorkResult.Num(); FScopedSlowTask Progress(NumWorkResults, FText::FromString(FString::Printf(TEXT("Baking PDG Node Output %s ..."), *InNode->GetName()))); Progress.MakeDialog(); for (int32 WorkResultArrayIdx = 0; WorkResultArrayIdx < NumWorkResults; ++WorkResultArrayIdx) { FTOPWorkResult& WorkResult = InNode->WorkResult[WorkResultArrayIdx]; const int32 NumWorkResultObjects = WorkResult.ResultObjects.Num(); for (int32 WorkResultObjectArrayIdx = 0; WorkResultObjectArrayIdx < NumWorkResultObjects; ++WorkResultObjectArrayIdx) { Progress.EnterProgressFrame(1.0f); BakePDGWorkResultObject( InPDGAssetLink, InNode, WorkResultArrayIdx, WorkResultObjectArrayIdx, bReplaceActors, bReplaceAssets, !bInBakeForBlueprint, bInIsAutoBake, OutBakedActors, OutPackagesToSave, OutBakeStats, OutputTypesToBake.Num() > 0 ? &OutputTypesToBake : nullptr, InstancerComponentTypesToBake.Num() > 0 ? &InstancerComponentTypesToBake : nullptr, FallbackWorldOutlinerFolderPath.ToString() ); } } return true; } bool FHoudiniEngineBakeUtils::BakePDGTOPNodeOutputsKeepActors(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode, bool bInIsAutoBake, const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, bool bInRecenterBakedActors) { TArray PackagesToSave; FHoudiniEngineOutputStats BakeStats; TArray BakedActors; const bool bBakeBlueprints = false; bool bSuccess = BakePDGTOPNodeOutputsKeepActors( InPDGAssetLink, InTOPNode, bBakeBlueprints, bInIsAutoBake, InPDGBakePackageReplaceMode, BakedActors, PackagesToSave, BakeStats); SaveBakedPackages(PackagesToSave); // Recenter and select the baked actors if (GEditor && BakedActors.Num() > 0) GEditor->SelectNone(false, true); for (const FHoudiniEngineBakedActor& Entry : BakedActors) { if (!IsValid(Entry.Actor)) continue; if (bInRecenterBakedActors) CenterActorToBoundingBoxCenter(Entry.Actor); if (GEditor) GEditor->SelectActor(Entry.Actor, true, false); } if (GEditor && BakedActors.Num() > 0) GEditor->NoteSelectionChange(); { const FString FinishedTemplate = TEXT("Baking finished. Created {0} packages. Updated {1} packages."); FString Msg = FString::Format(*FinishedTemplate, { BakeStats.NumPackagesCreated, BakeStats.NumPackagesUpdated } ); FHoudiniEngine::Get().FinishTaskSlateNotification( FText::FromString(Msg) ); } return bSuccess; } bool FHoudiniEngineBakeUtils::BakePDGTOPNetworkOutputsKeepActors( UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNetwork* InNetwork, bool bInBakeForBlueprint, bool bInIsAutoBake, const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, TArray& BakedActors, TArray& OutPackagesToSave, FHoudiniEngineOutputStats& OutBakeStats) { if (!InPDGAssetLink || InPDGAssetLink->IsPendingKill()) return false; if (!IsValid(InNetwork)) return false; bool bSuccess = true; for (UTOPNode* Node : InNetwork->AllTOPNodes) { if (!IsValid(Node)) continue; bSuccess &= BakePDGTOPNodeOutputsKeepActors(InPDGAssetLink, Node, bInBakeForBlueprint, bInIsAutoBake, InPDGBakePackageReplaceMode, BakedActors, OutPackagesToSave, OutBakeStats); } return bSuccess; } bool FHoudiniEngineBakeUtils::BakePDGAssetLinkOutputsKeepActors(UHoudiniPDGAssetLink* InPDGAssetLink, const EPDGBakeSelectionOption InBakeSelectionOption, const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, bool bInRecenterBakedActors) { if (!InPDGAssetLink || InPDGAssetLink->IsPendingKill()) return false; TArray PackagesToSave; FHoudiniEngineOutputStats BakeStats; TArray BakedActors; const bool bBakeBlueprints = false; const bool bIsAutoBake = false; bool bSuccess = true; switch(InBakeSelectionOption) { case EPDGBakeSelectionOption::All: for (UTOPNetwork* Network : InPDGAssetLink->AllTOPNetworks) { if (!IsValid(Network)) continue; for (UTOPNode* Node : Network->AllTOPNodes) { if (!IsValid(Node)) continue; bSuccess &= BakePDGTOPNodeOutputsKeepActors(InPDGAssetLink, Node, bBakeBlueprints, bIsAutoBake, InPDGBakePackageReplaceMode, BakedActors, PackagesToSave, BakeStats); } } break; case EPDGBakeSelectionOption::SelectedNetwork: bSuccess = BakePDGTOPNetworkOutputsKeepActors(InPDGAssetLink, InPDGAssetLink->GetSelectedTOPNetwork(), bBakeBlueprints, bIsAutoBake, InPDGBakePackageReplaceMode, BakedActors, PackagesToSave, BakeStats); case EPDGBakeSelectionOption::SelectedNode: bSuccess = BakePDGTOPNodeOutputsKeepActors(InPDGAssetLink, InPDGAssetLink->GetSelectedTOPNode(), bBakeBlueprints, bIsAutoBake, InPDGBakePackageReplaceMode, BakedActors, PackagesToSave, BakeStats); } SaveBakedPackages(PackagesToSave); // Recenter and select the baked actors if (GEditor && BakedActors.Num() > 0) GEditor->SelectNone(false, true); for (const FHoudiniEngineBakedActor& Entry : BakedActors) { if (!IsValid(Entry.Actor)) continue; if (bInRecenterBakedActors) CenterActorToBoundingBoxCenter(Entry.Actor); if (GEditor) GEditor->SelectActor(Entry.Actor, true, false); } if (GEditor && BakedActors.Num() > 0) GEditor->NoteSelectionChange(); { const FString FinishedTemplate = TEXT("Baking finished. Created {0} packages. Updated {1} packages."); FString Msg = FString::Format(*FinishedTemplate, { BakeStats.NumPackagesCreated, BakeStats.NumPackagesUpdated } ); FHoudiniEngine::Get().FinishTaskSlateNotification( FText::FromString(Msg) ); } // Broadcast that the bake is complete InPDGAssetLink->HandleOnPostBake(bSuccess); return bSuccess; } bool FHoudiniEngineBakeUtils::BakeBlueprintsFromBakedActors( const TArray& InBakedActors, bool bInRecenterBakedActors, bool bInReplaceAssets, const FString& InAssetName, const FDirectoryPath& InBakeFolder, TArray* const InNonPDGBakedOutputs, TMap* const InPDGBakedOutputs, TArray& OutBlueprints, TArray& OutPackagesToSave) { // // Clear selection // if (GEditor) // { // GEditor->SelectNone(false, true); // GEditor->NoteSelectionChange(); // } // Iterate over the baked actors. An actor might appear multiple times if multiple OutputComponents were // baked to the same actor, so keep track of actors we have already processed in BakedActorSet UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); const bool bIsAssetEditorSubsystemValid = IsValid(AssetEditorSubsystem); TArray AssetsToReOpenEditors; TSet BakedActorSet; for (const FHoudiniEngineBakedActor& Entry : InBakedActors) { AActor *Actor = Entry.Actor; if (!Actor || Actor->IsPendingKill()) continue; if (BakedActorSet.Contains(Actor)) continue; BakedActorSet.Add(Actor); UObject* Asset = nullptr; // Recenter the actor to its bounding box center if (bInRecenterBakedActors) CenterActorToBoundingBoxCenter(Actor); // Create package for out Blueprint FString BlueprintName; // For instancers we determine the bake folder from the instancer, // for everything else we use the baked object's bake folder // If all of that is blank, we fall back to InBakeFolder. FString BakeFolderPath; if (Entry.bInstancerOutput) BakeFolderPath = Entry.InstancerPackageParams.BakeFolder; else BakeFolderPath = Entry.BakeFolderPath; if (BakeFolderPath.IsEmpty()) BakeFolderPath = InBakeFolder.Path; FHoudiniPackageParams PackageParams; // Set the replace mode based on if we are doing a replacement or incremental asset bake const EPackageReplaceMode AssetPackageReplaceMode = bInReplaceAssets ? EPackageReplaceMode::ReplaceExistingAssets : EPackageReplaceMode::CreateNewAssets; FHoudiniEngineUtils::FillInPackageParamsForBakingOutput( PackageParams, FHoudiniOutputObjectIdentifier(), BakeFolderPath, Entry.ActorBakeName.ToString() + "_BP", InAssetName, AssetPackageReplaceMode); // If we have a previously baked a blueprint, get the bake counter from it so that both replace and increment // is consistent with the bake counter int32 BakeCounter = 0; UBlueprint* InPreviousBlueprint = nullptr; FHoudiniBakedOutputObject* BakedOutputObject = nullptr; FHoudiniPDGWorkResultObjectBakedOutput* WorkResultObjectBakedOutput = nullptr; // Get the baked output object if (Entry.PDGWorkResultArrayIndex >= 0 && Entry.PDGWorkItemIndex >= 0 && Entry.PDGWorkResultObjectArrayIndex >= 0 && InPDGBakedOutputs) { const FString Key = UTOPNode::GetBakedWorkResultObjectOutputsKey(Entry.PDGWorkItemIndex, Entry.PDGWorkResultObjectArrayIndex); WorkResultObjectBakedOutput = InPDGBakedOutputs->Find(Key); if (WorkResultObjectBakedOutput) { if (Entry.OutputIndex >= 0 && WorkResultObjectBakedOutput->BakedOutputs.IsValidIndex(Entry.OutputIndex)) { BakedOutputObject = WorkResultObjectBakedOutput->BakedOutputs[Entry.OutputIndex].BakedOutputObjects.Find(Entry.OutputObjectIdentifier); } } } else if (Entry.OutputIndex >= 0 && InNonPDGBakedOutputs) { if (Entry.OutputIndex >= 0 && InNonPDGBakedOutputs->IsValidIndex(Entry.OutputIndex)) { BakedOutputObject = (*InNonPDGBakedOutputs)[Entry.OutputIndex].BakedOutputObjects.Find(Entry.OutputObjectIdentifier); } } if (BakedOutputObject) { InPreviousBlueprint = BakedOutputObject->GetBlueprintIfValid(); if (IsValid(InPreviousBlueprint)) { if (PackageParams.MatchesPackagePathNameExcludingBakeCounter(InPreviousBlueprint)) { PackageParams.GetBakeCounterFromBakedAsset(InPreviousBlueprint, BakeCounter); } } } UPackage* Package = PackageParams.CreatePackageForObject(BlueprintName, BakeCounter); if (!Package || Package->IsPendingKill()) { HOUDINI_LOG_WARNING(TEXT("Could not find or create a package for the blueprint of %s"), *(Actor->GetPathName())); continue; } if (!Package->IsFullyLoaded()) Package->FullyLoad(); //Blueprint = FKismetEditorUtilities::CreateBlueprintFromActor(*BlueprintName, Package, Actor, false); // Find existing asset first (only relevant if we are in replacement mode). If the existing asset has a // different base class than the incoming actor, we reparent the blueprint to the new base class before // clearing the SCS graph and repopulating it from the temp actor. Asset = StaticFindObjectFast(UBlueprint::StaticClass(), Package, FName(*BlueprintName)); if (IsValid(Asset)) { UBlueprint* Blueprint = Cast(Asset); if (IsValid(Blueprint)) { if (Blueprint->GeneratedClass && Blueprint->GeneratedClass != Actor->GetClass()) { // Close editors opened on existing asset if applicable if (Asset && bIsAssetEditorSubsystemValid && AssetEditorSubsystem->FindEditorForAsset(Asset, false) != nullptr) { AssetEditorSubsystem->CloseAllEditorsForAsset(Asset); AssetsToReOpenEditors.Add(Asset); } Blueprint->ParentClass = Actor->GetClass(); FBlueprintEditorUtils::RefreshAllNodes(Blueprint); FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint); FKismetEditorUtilities::CompileBlueprint(Blueprint); } } } else if (Asset && Asset->IsPendingKill()) { // Rename to pending kill so that we can use the desired name const FString AssetPendingKillName(BlueprintName + "_PENDING_KILL"); // Asset->Rename(*MakeUniqueObjectNameIfNeeded(Package, UBlueprint::StaticClass(), AssetPendingKillName).ToString()); RenameAsset(Asset, AssetPendingKillName, true); Asset = nullptr; } if (!Asset) { UBlueprintFactory* Factory = NewObject(); Factory->ParentClass = Actor->GetClass(); FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); Asset = AssetToolsModule.Get().CreateAsset( BlueprintName, PackageParams.GetPackagePath(), UBlueprint::StaticClass(), Factory, FName("ContentBrowserNewAsset")); } UBlueprint* Blueprint = Cast(Asset); if (!Blueprint || Blueprint->IsPendingKill()) { HOUDINI_LOG_WARNING( TEXT("Found an asset at %s/%s, but it was not a blueprint or was pending kill."), *(InBakeFolder.Path), *BlueprintName); continue; } // Close editors opened on existing asset if applicable if (Blueprint && bIsAssetEditorSubsystemValid && AssetEditorSubsystem->FindEditorForAsset(Blueprint, false) != nullptr) { AssetEditorSubsystem->CloseAllEditorsForAsset(Blueprint); AssetsToReOpenEditors.Add(Blueprint); } // Record the blueprint as the previous bake blueprint if (BakedOutputObject) BakedOutputObject->Blueprint = FSoftObjectPath(Blueprint).ToString(); OutBlueprints.Add(Blueprint); // Clear old Blueprint Node tree { USimpleConstructionScript* SCS = Blueprint->SimpleConstructionScript; int32 NodeSize = SCS->GetAllNodes().Num(); for (int32 n = NodeSize - 1; n >= 0; --n) SCS->RemoveNode(SCS->GetAllNodes()[n]); } FHoudiniEngineBakeUtils::CopyActorContentsToBlueprint(Actor, Blueprint); UWorld* World = Actor->GetWorld(); if (!World) World = GWorld; World->EditorDestroyActor(Actor, true); // Save the created BP package. Package->MarkPackageDirty(); OutPackagesToSave.Add(Package); } // Re-open asset editors for updated blueprints that were open in editors if (bIsAssetEditorSubsystemValid && AssetsToReOpenEditors.Num() > 0) { for (UObject* Asset : AssetsToReOpenEditors) { if (IsValid(Asset)) { AssetEditorSubsystem->OpenEditorForAsset(Asset); } } } return true; } bool FHoudiniEngineBakeUtils::BakePDGTOPNodeBlueprints( UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InNode, bool bInIsAutoBake, const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, bool bInRecenterBakedActors, TArray& OutBlueprints, TArray& OutPackagesToSave, FHoudiniEngineOutputStats& OutBakeStats) { TArray BPActors; if (!IsValid(InPDGAssetLink)) { HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineBakeUtils::BakePDGBlueprint]: InPDGAssetLink is null")); return false; } if (!IsValid(InNode)) { HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineBakeUtils::BakePDGBlueprint]: InNode is null")); return false; } const bool bReplaceAssets = InPDGBakePackageReplaceMode == EPDGBakePackageReplaceModeOption::ReplaceExistingAssets; // Bake PDG output to new actors // bInBakeForBlueprint == true will skip landscapes and instanced actor components const bool bInBakeForBlueprint = true; TArray BakedActors; bool bSuccess = BakePDGTOPNodeOutputsKeepActors( InPDGAssetLink, InNode, bInBakeForBlueprint, bInIsAutoBake, InPDGBakePackageReplaceMode, BakedActors, OutPackagesToSave, OutBakeStats ); if (bSuccess) { bSuccess = BakeBlueprintsFromBakedActors( BakedActors, bInRecenterBakedActors, bReplaceAssets, InPDGAssetLink->AssetName, InPDGAssetLink->BakeFolder, nullptr, &InNode->GetBakedWorkResultObjectsOutputs(), OutBlueprints, OutPackagesToSave); } return bSuccess; } bool FHoudiniEngineBakeUtils::BakePDGTOPNodeBlueprints(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode, bool bInIsAutoBake, const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, bool bInRecenterBakedActors) { TArray Blueprints; TArray PackagesToSave; FHoudiniEngineOutputStats BakeStats; if (!InPDGAssetLink || InPDGAssetLink->IsPendingKill()) return false; const bool bSuccess = BakePDGTOPNodeBlueprints( InPDGAssetLink, InTOPNode, bInIsAutoBake, InPDGBakePackageReplaceMode, bInRecenterBakedActors, Blueprints, PackagesToSave, BakeStats); FHoudiniEngineBakeUtils::SaveBakedPackages(PackagesToSave); // Sync the CB to the baked objects if(GEditor && Blueprints.Num() > 0) { TArray Assets; Assets.Reserve(Blueprints.Num()); for (UBlueprint* Blueprint : Blueprints) { Assets.Add(Blueprint); } GEditor->SyncBrowserToObjects(Assets); } { const FString FinishedTemplate = TEXT("Baking finished. Created {0} packages. Updated {1} packages."); FString Msg = FString::Format(*FinishedTemplate, { BakeStats.NumPackagesCreated, BakeStats.NumPackagesUpdated } ); FHoudiniEngine::Get().FinishTaskSlateNotification( FText::FromString(Msg) ); } TryCollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); return bSuccess; } bool FHoudiniEngineBakeUtils::BakePDGTOPNetworkBlueprints( UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNetwork* InNetwork, const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, bool bInRecenterBakedActors, TArray& OutBlueprints, TArray& OutPackagesToSave, FHoudiniEngineOutputStats& OutBakeStats) { if (!InPDGAssetLink || InPDGAssetLink->IsPendingKill()) return false; if (!IsValid(InNetwork)) return false; const bool bIsAutoBake = false; bool bSuccess = true; for (UTOPNode* Node : InNetwork->AllTOPNodes) { if (!IsValid(Node)) continue; bSuccess &= BakePDGTOPNodeBlueprints(InPDGAssetLink, Node, bIsAutoBake, InPDGBakePackageReplaceMode, bInRecenterBakedActors, OutBlueprints, OutPackagesToSave, OutBakeStats); } return bSuccess; } bool FHoudiniEngineBakeUtils::BakePDGAssetLinkBlueprints(UHoudiniPDGAssetLink* InPDGAssetLink, const EPDGBakeSelectionOption InBakeSelectionOption, const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, bool bInRecenterBakedActors) { TArray Blueprints; TArray PackagesToSave; FHoudiniEngineOutputStats BakeStats; if (!InPDGAssetLink || InPDGAssetLink->IsPendingKill()) return false; const bool bIsAutoBake = false; bool bSuccess = true; switch(InBakeSelectionOption) { case EPDGBakeSelectionOption::All: for (UTOPNetwork* Network : InPDGAssetLink->AllTOPNetworks) { if (!IsValid(Network)) continue; for (UTOPNode* Node : Network->AllTOPNodes) { if (!IsValid(Node)) continue; bSuccess &= BakePDGTOPNodeBlueprints(InPDGAssetLink, Node, bIsAutoBake, InPDGBakePackageReplaceMode, bInRecenterBakedActors, Blueprints, PackagesToSave, BakeStats); } } break; case EPDGBakeSelectionOption::SelectedNetwork: bSuccess &= BakePDGTOPNetworkBlueprints( InPDGAssetLink, InPDGAssetLink->GetSelectedTOPNetwork(), InPDGBakePackageReplaceMode, bInRecenterBakedActors, Blueprints, PackagesToSave, BakeStats); case EPDGBakeSelectionOption::SelectedNode: bSuccess &= BakePDGTOPNodeBlueprints( InPDGAssetLink, InPDGAssetLink->GetSelectedTOPNode(), bIsAutoBake, InPDGBakePackageReplaceMode, bInRecenterBakedActors, Blueprints, PackagesToSave, BakeStats); } FHoudiniEngineBakeUtils::SaveBakedPackages(PackagesToSave); // Sync the CB to the baked objects if(GEditor && Blueprints.Num() > 0) { TArray Assets; Assets.Reserve(Blueprints.Num()); for (UBlueprint* Blueprint : Blueprints) { Assets.Add(Blueprint); } GEditor->SyncBrowserToObjects(Assets); } { const FString FinishedTemplate = TEXT("Baking finished. Created {0} packages. Updated {1} packages."); FString Msg = FString::Format(*FinishedTemplate, { BakeStats.NumPackagesCreated, BakeStats.NumPackagesUpdated } ); FHoudiniEngine::Get().FinishTaskSlateNotification( FText::FromString(Msg) ); } TryCollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); // Broadcast that the bake is complete InPDGAssetLink->HandleOnPostBake(bSuccess); return bSuccess; } bool FHoudiniEngineBakeUtils::FindOrCreateDesiredLevelFromLevelPath( const FString& InLevelPath, ULevel*& OutDesiredLevel, UWorld*& OutDesiredWorld, bool& OutCreatedPackage) { OutDesiredLevel = nullptr; OutDesiredWorld = nullptr; if (InLevelPath.IsEmpty()) { OutDesiredWorld = GWorld; OutDesiredLevel = GWorld->GetCurrentLevel(); } else { OutCreatedPackage = false; UWorld* FoundWorld = nullptr; ULevel* FoundLevel = nullptr; bool bActorInWorld = false; if (FHoudiniEngineUtils::FindWorldAndLevelForSpawning( GWorld, InLevelPath, true, FoundWorld, FoundLevel, OutCreatedPackage, bActorInWorld)) { OutDesiredLevel = FoundLevel; OutDesiredWorld = FoundWorld; } } return ((OutDesiredWorld != nullptr) && (OutDesiredLevel != nullptr)); } bool FHoudiniEngineBakeUtils::FindDesiredBakeActorFromBakeActorName( const FString& InBakeActorName, ULevel* InLevel, AActor*& OutActor, bool bInNoPendingKillActors, bool bRenamePendingKillActor) { OutActor = nullptr; if (!IsValid(InLevel)) return false; UWorld* const World = InLevel->GetWorld(); if (!IsValid(World)) return false; // Look for an actor with the given name in the world const FName BakeActorFName(InBakeActorName); AActor* FoundActor = Cast(StaticFindObjectFast(AActor::StaticClass(), InLevel, BakeActorFName)); // for (TActorIterator Iter(World, AActor::StaticClass(), EActorIteratorFlags::AllActors); Iter; ++Iter) // { // AActor* const Actor = *Iter; // if (Actor->GetFName() == BakeActorFName && Actor->GetLevel() == InLevel) // { // // Found the actor // FoundActor = Actor; // break; // } // } // If we found an actor and it is pending kill, rename it and don't use it if (FoundActor) { if (FoundActor->IsPendingKill()) { if (bRenamePendingKillActor) { // FoundActor->Rename( // *MakeUniqueObjectNameIfNeeded( // FoundActor->GetOuter(), // FoundActor->GetClass(), // FName(FoundActor->GetName() + "_Pending_Kill")).ToString()); RenameAndRelabelActor( FoundActor, *MakeUniqueObjectNameIfNeeded( FoundActor->GetOuter(), FoundActor->GetClass(), FoundActor->GetName() + "_Pending_Kill", FoundActor), false); } if (bInNoPendingKillActors) FoundActor = nullptr; else OutActor = FoundActor; } else { OutActor = FoundActor; } } return true; } bool FHoudiniEngineBakeUtils::FindUnrealBakeActor( const FHoudiniOutputObject& InOutputObject, const FHoudiniBakedOutputObject& InBakedOutputObject, const TArray& InAllBakedActors, ULevel* InLevel, FName InDefaultActorName, bool bInReplaceActorBakeMode, AActor* InFallbackActor, AActor*& OutFoundActor, bool& bOutHasBakeActorName, FName& OutBakeActorName) { // Determine desired actor name via unreal_output_actor, fallback to InDefaultActorName OutBakeActorName = NAME_None; OutFoundActor = nullptr; bOutHasBakeActorName = InOutputObject.CachedAttributes.Contains(HAPI_UNREAL_ATTRIB_BAKE_ACTOR); if (bOutHasBakeActorName) { const FString& BakeActorNameStr = InOutputObject.CachedAttributes[HAPI_UNREAL_ATTRIB_BAKE_ACTOR]; if (BakeActorNameStr.IsEmpty()) { OutBakeActorName = NAME_None; bOutHasBakeActorName = false; } else { OutBakeActorName = FName(BakeActorNameStr, NAME_NO_NUMBER_INTERNAL); // We have a bake actor name, look for the actor AActor* BakeNameActor = nullptr; if (FindDesiredBakeActorFromBakeActorName(BakeActorNameStr, InLevel, BakeNameActor)) { // Found an actor with that name, check that we "own" it (we created in during baking previously) AActor* IncrementedBakedActor = nullptr; for (const FHoudiniEngineBakedActor& BakedActor : InAllBakedActors) { if (!IsValid(BakedActor.Actor)) continue; if (BakedActor.Actor == BakeNameActor) { OutFoundActor = BakeNameActor; break; } else if (!IncrementedBakedActor && BakedActor.ActorBakeName == OutBakeActorName) { // Found an actor we have baked named OutBakeActorName_# (incremental version of our desired name) IncrementedBakedActor = BakedActor.Actor; } } if (!OutFoundActor && IncrementedBakedActor) OutFoundActor = IncrementedBakedActor; } } } // If unreal_actor_name is not set, or is blank, fallback to InDefaultActorName if (!bOutHasBakeActorName || (OutBakeActorName.IsNone() || OutBakeActorName.ToString().TrimStartAndEnd().IsEmpty())) OutBakeActorName = InDefaultActorName; if (!OutFoundActor) { // If in replace mode, use previous bake actor if valid and in InLevel if (bInReplaceActorBakeMode) { const FSoftObjectPath PrevActorPath(InBakedOutputObject.Actor); const FString ActorPath = PrevActorPath.IsSubobject() ? PrevActorPath.GetAssetPathString() + ":" + PrevActorPath.GetSubPathString() : PrevActorPath.GetAssetPathString(); const FString LevelPath = IsValid(InLevel) ? InLevel->GetPathName() : ""; if (PrevActorPath.IsValid() && (LevelPath.IsEmpty() || ActorPath.StartsWith(LevelPath))) OutFoundActor = InBakedOutputObject.GetActorIfValid(); } // Fallback to InFallbackActor if valid and in InLevel if (!OutFoundActor && IsValid(InFallbackActor) && (!InLevel || InFallbackActor->GetLevel() == InLevel)) OutFoundActor = InFallbackActor; } return true; } AActor* FHoudiniEngineBakeUtils::FindExistingActor_Bake( UWorld* InWorld, UHoudiniOutput* InOutput, const FString& InActorName, const FString& InPackagePath, UWorld*& OutWorld, ULevel*& OutLevel, bool& bCreatedPackage) { bCreatedPackage = false; // Try to Locate a previous actor AActor* FoundActor = FHoudiniEngineUtils::FindOrRenameInvalidActor(InWorld, InActorName, FoundActor); if (FoundActor) FoundActor->Destroy(); // nuke it! if (FoundActor) { // TODO: make sure that the found is actor is actually assigned to the level defined by package path. // If the found actor is not from that level, it should be moved there. OutWorld = FoundActor->GetWorld(); OutLevel = FoundActor->GetLevel(); } else { // Actor is not present, BUT target package might be loaded. Find the appropriate World and Level for spawning. bool bActorInWorld = false; const bool bResult = FHoudiniEngineUtils::FindWorldAndLevelForSpawning( InWorld, InPackagePath, true, OutWorld, OutLevel, bCreatedPackage, bActorInWorld); if (!bResult) { return nullptr; } if (!bActorInWorld) { // The OutLevel is not present in the current world which means we might // still find the tile actor in OutWorld. FoundActor = FHoudiniEngineUtils::FindActorInWorld(OutWorld, FName(InActorName)); } } return FoundActor; } bool FHoudiniEngineBakeUtils::CheckForAndRefineHoudiniProxyMesh( UHoudiniAssetComponent* InHoudiniAssetComponent, bool bInReplacePreviousBake, EHoudiniEngineBakeOption InBakeOption, bool bInRemoveHACOutputOnSuccess, bool bInRecenterBakedActors, bool& bOutNeedsReCook) { if (!IsValid(InHoudiniAssetComponent)) { return false; } // Handle proxies: if the output has any current proxies, first refine them bOutNeedsReCook = false; if (InHoudiniAssetComponent->HasAnyCurrentProxyOutput()) { bool bNeedsRebuildOrDelete; bool bInvalidState; const bool bCookedDataAvailable = InHoudiniAssetComponent->IsHoudiniCookedDataAvailable(bNeedsRebuildOrDelete, bInvalidState); if (bCookedDataAvailable) { // Cook data is available, refine the mesh AHoudiniAssetActor* HoudiniActor = Cast(InHoudiniAssetComponent->GetOwner()); if (IsValid(HoudiniActor)) { FHoudiniEngineCommands::RefineHoudiniProxyMeshActorArrayToStaticMeshes({ HoudiniActor }); } } else if (!bNeedsRebuildOrDelete && !bInvalidState) { // A cook is needed: request the cook, but with no proxy and with a bake after cook InHoudiniAssetComponent->SetNoProxyMeshNextCookRequested(true); // Only if (!InHoudiniAssetComponent->IsBakeAfterNextCookEnabled() || !InHoudiniAssetComponent->GetOnPostCookBakeDelegate().IsBound()) { InHoudiniAssetComponent->GetOnPostCookBakeDelegate().BindLambda([bInReplacePreviousBake, InBakeOption, bInRemoveHACOutputOnSuccess, bInRecenterBakedActors](UHoudiniAssetComponent* InHAC) { return FHoudiniEngineBakeUtils::BakeHoudiniAssetComponent(InHAC, bInReplacePreviousBake, InBakeOption, bInRemoveHACOutputOnSuccess, bInRecenterBakedActors); }); } InHoudiniAssetComponent->MarkAsNeedCook(); bOutNeedsReCook = true; // The cook has to complete first (asynchronously) before the bake can happen // The SetBakeAfterNextCookEnabled flag will result in a bake after cook return false; } else { // The HAC is in an unsupported state const EHoudiniAssetState AssetState = InHoudiniAssetComponent->GetAssetState(); HOUDINI_LOG_ERROR(TEXT("Could not refine (in order to bake) %s, the asset is in an unsupported state: %s"), *(InHoudiniAssetComponent->GetPathName()), *(UEnum::GetValueAsString(AssetState))); return false; } } return true; } void FHoudiniEngineBakeUtils::CenterActorToBoundingBoxCenter(AActor* InActor) { if (!IsValid(InActor)) return; USceneComponent * const RootComponent = InActor->GetRootComponent(); if (!IsValid(RootComponent)) return; // If the root component does not have any child components, then there is nothing to recenter if (RootComponent->GetNumChildrenComponents() <= 0) return; const bool bOnlyCollidingComponents = false; const bool bIncludeFromChildActors = true; FVector Origin; FVector BoxExtent; // InActor->GetActorBounds(bOnlyCollidingComponents, Origin, BoxExtent, bIncludeFromChildActors); FBox Box(ForceInit); InActor->ForEachComponent(bIncludeFromChildActors, [&](const UPrimitiveComponent* InPrimComp) { // Only use non-editor-only components for the bounds calculation (to exclude things like editor only sprite/billboard components) if (InPrimComp->IsRegistered() && !InPrimComp->IsEditorOnly() && (!bOnlyCollidingComponents || InPrimComp->IsCollisionEnabled())) { Box += InPrimComp->Bounds.GetBox(); } }); Box.GetCenterAndExtents(Origin, BoxExtent); const FVector Delta = Origin - RootComponent->GetComponentLocation(); // Actor->SetActorLocation(Origin); RootComponent->SetWorldLocation(Origin); for (USceneComponent* SceneComponent : RootComponent->GetAttachChildren()) { if (!IsValid(SceneComponent)) continue; SceneComponent->SetWorldLocation(SceneComponent->GetComponentLocation() - Delta); } } void FHoudiniEngineBakeUtils::CenterActorsToBoundingBoxCenter(const TArray& InActors) { for (AActor* Actor : InActors) { if (!IsValid(Actor)) continue; CenterActorToBoundingBoxCenter(Actor); } } USceneComponent* FHoudiniEngineBakeUtils::GetActorRootComponent(AActor* InActor, bool bCreateIfMissing, EComponentMobility::Type InMobilityIfCreated) { USceneComponent* RootComponent = InActor->GetRootComponent(); if (!IsValid(RootComponent)) { RootComponent = NewObject(InActor, USceneComponent::GetDefaultSceneRootVariableName(), RF_Transactional); // Change the creation method so the component is listed in the details panels InActor->SetRootComponent(RootComponent); InActor->AddInstanceComponent(RootComponent); RootComponent->RegisterComponent(); RootComponent->SetMobility(InMobilityIfCreated); } return RootComponent; } FString FHoudiniEngineBakeUtils::MakeUniqueObjectNameIfNeeded(UObject* InOuter, const UClass* InClass, const FString& InName, UObject* InObjectThatWouldBeRenamed) { if (IsValid(InObjectThatWouldBeRenamed)) { const FName CurrentName = InObjectThatWouldBeRenamed->GetFName(); if (CurrentName.ToString() == InName) return InName; // Check if the prefix matches (without counter suffix) the new name // In other words, if InName is 'my_actor' and the object is already an increment of it, 'my_actor_5' then // don't we can just keep the current name if (CurrentName.GetPlainNameString() == InName) return CurrentName.ToString(); } UObject* ExistingObject = nullptr; FName CandidateName(InName); bool bAppendedNumber = false; // Do our own loop for generate suffixes as sequentially as possible. If this turns out to be expensive we can // revert to MakeUniqueObjectName. // return MakeUniqueObjectName(InOuter, InClass, CandidateName).ToString(); do { if (InOuter == ANY_PACKAGE) { ExistingObject = StaticFindObject(nullptr, ANY_PACKAGE, *(CandidateName.ToString())); } else { ExistingObject = StaticFindObjectFast(nullptr, InOuter, CandidateName); } if (ExistingObject) { if (!bAppendedNumber) { const bool bSplitName = false; CandidateName = FName(*InName, NAME_EXTERNAL_TO_INTERNAL(1), FNAME_Add, bSplitName); bAppendedNumber = true; } else { CandidateName.SetNumber(CandidateName.GetNumber() + 1); } // CandidateName = FString::Printf(TEXT("%s_%d"), *InName, ++Counter); } } while (ExistingObject); return CandidateName.ToString(); } FName FHoudiniEngineBakeUtils::GetOutlinerFolderPath(const FHoudiniOutputObject& InOutputObject, FName InDefaultFolder) { const FString* FolderPathPtr = InOutputObject.CachedAttributes.Find(HAPI_UNREAL_ATTRIB_BAKE_OUTLINER_FOLDER); if (FolderPathPtr && !FolderPathPtr->IsEmpty()) return FName(*FolderPathPtr); else return InDefaultFolder; } bool FHoudiniEngineBakeUtils::SetOutlinerFolderPath(AActor* InActor, const FHoudiniOutputObject& InOutputObject, FName InDefaultFolder) { if (!IsValid(InActor)) return false; InActor->SetFolderPath(GetOutlinerFolderPath(InOutputObject, InDefaultFolder)); return true; } uint32 FHoudiniEngineBakeUtils::DestroyPreviousBakeOutput( FHoudiniBakedOutputObject& InBakedOutputObject, bool bInDestroyBakedComponent, bool bInDestroyBakedInstancedActors, bool bInDestroyBakedInstancedComponents) { uint32 NumDeleted = 0; if (bInDestroyBakedComponent) { UActorComponent* Component = Cast(InBakedOutputObject.GetBakedComponentIfValid()); if (Component) { if (RemovePreviouslyBakedComponent(Component)) { InBakedOutputObject.BakedComponent = nullptr; NumDeleted++; } } } if (bInDestroyBakedInstancedActors) { for (const FString& ActorPathStr : InBakedOutputObject.InstancedActors) { const FSoftObjectPath ActorPath(ActorPathStr); if (!ActorPath.IsValid()) continue; AActor* Actor = Cast(ActorPath.TryLoad()); if (IsValid(Actor)) { UWorld* World = Actor->GetWorld(); if (IsValid(World)) { #if WITH_EDITOR World->EditorDestroyActor(Actor, true); #else World->DestroyActor(Actor); #endif NumDeleted++; } } } InBakedOutputObject.InstancedActors.Empty(); } if (bInDestroyBakedInstancedComponents) { for (const FString& ComponentPathStr : InBakedOutputObject.InstancedComponents) { const FSoftObjectPath ComponentPath(ComponentPathStr); if (!ComponentPath.IsValid()) continue; UActorComponent* Component = Cast(ComponentPath.TryLoad()); if (IsValid(Component)) { if (RemovePreviouslyBakedComponent(Component)) NumDeleted++; } } InBakedOutputObject.InstancedComponents.Empty(); } return NumDeleted; } UMaterialInterface* FHoudiniEngineBakeUtils::BakeSingleMaterialToPackage(UMaterialInterface* InOriginalMaterial, const FHoudiniPackageParams & InPackageParams, TArray& OutPackagesToSave, TMap& InOutAlreadyBakedMaterialsMap) { if (!InOriginalMaterial || InOriginalMaterial->IsPendingKill()) { return nullptr; } // We only deal with materials. if (!InOriginalMaterial->IsA(UMaterial::StaticClass()) && !InOriginalMaterial->IsA(UMaterialInstance::StaticClass())) { return nullptr; } FString MaterialName = InOriginalMaterial->GetName(); // Duplicate material resource. UMaterialInterface * DuplicatedMaterial = FHoudiniEngineBakeUtils::DuplicateMaterialAndCreatePackage( InOriginalMaterial, nullptr, MaterialName, InPackageParams, OutPackagesToSave, InOutAlreadyBakedMaterialsMap); if (!DuplicatedMaterial || DuplicatedMaterial->IsPendingKill()) return nullptr; return DuplicatedMaterial; } #undef LOCTEXT_NAMESPACE