/* * 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 "HoudiniEngineCommands.h" #include "HoudiniEngineEditorPrivatePCH.h" #include "HoudiniEngine.h" #include "HoudiniEngineUtils.h" #include "HoudiniEngineBakeUtils.h" #include "HoudiniEngineEditorUtils.h" #include "HoudiniEngineRuntime.h" #include "HoudiniAssetActor.h" #include "HoudiniAssetComponent.h" #include "HoudiniOutputTranslator.h" #include "HoudiniStaticMesh.h" #include "HoudiniOutput.h" #include "HoudiniEngineStyle.h" #include "DesktopPlatformModule.h" #include "Interfaces/IMainFrameModule.h" #include "EditorDirectories.h" #include "Misc/ScopedSlowTask.h" #include "Async/Async.h" #include "FileHelpers.h" #include "AssetRegistryModule.h" #include "Engine/ObjectLibrary.h" #include "ObjectTools.h" #include "CoreGlobals.h" #include "HoudiniEngineOutputStats.h" #include "Misc/FeedbackContext.h" #include "HAL/FileManager.h" #include "Modules/ModuleManager.h" #include "ISettingsModule.h" #define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE FDelegateHandle FHoudiniEngineCommands::OnPostSaveWorldRefineProxyMeshesHandle = FDelegateHandle(); FHoudiniEngineCommands::FOnHoudiniProxyMeshesRefinedDelegate FHoudiniEngineCommands::OnHoudiniProxyMeshesRefinedDelegate = FHoudiniEngineCommands::FOnHoudiniProxyMeshesRefinedDelegate(); FHoudiniEngineCommands::FHoudiniEngineCommands() : TCommands (TEXT("HoudiniEngine"), NSLOCTEXT("Contexts", "HoudiniEngine", "Houdini Engine Plugin"), NAME_None, FHoudiniEngineStyle::GetStyleSetName()) { } void FHoudiniEngineCommands::RegisterCommands() { UI_COMMAND(_CreateSession, "Create Session", "Creates a new Houdini Engine session.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(_ConnectSession, "Connect Session", "Connects to an existing Houdini Engine session.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(_StopSession, "Stop Session", "Stops the current Houdini Engine session.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(_RestartSession, "Restart Session", "Restarts the current Houdini Engine session.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(_OpenSessionSync, "Open Houdini Session Sync", "Opens Houdini with Session Sync and connect to it.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(_CloseSessionSync, "Close Houdini Session Sync", "Close the Session Sync Houdini.", EUserInterfaceActionType::Button, FInputChord()); // Viewport Sync UI_COMMAND(_ViewportSyncNone, "Disabled", "Do not sync viewports.", EUserInterfaceActionType::Check, FInputChord()); UI_COMMAND(_ViewportSyncUnreal, "Sync Unreal to Houdini.", "Sync the Unreal viewport to Houdini's.", EUserInterfaceActionType::Check, FInputChord()); UI_COMMAND(_ViewportSyncHoudini, "Sync Houdini to Unreal", "Sync the Houdini viewport to Unreal's.", EUserInterfaceActionType::Check, FInputChord()); UI_COMMAND(_ViewportSyncBoth, "Both", "Sync both Unreal and Houdini's viewport.", EUserInterfaceActionType::Check, FInputChord()); // PDG Import Commandlet UI_COMMAND(_StartPDGCommandlet, "Start Async Importer", "Start the commandlet that imports PDG BGEO results in the background.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(_StopPDGCommandlet, "Stop Async Importer", "Stops the commandlet that imports PDG BGEO results in the background.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(_IsPDGCommandletEnabled, "Enable Async Importer", "Enables the commandlet that imports PDG BGEO results in the background.", EUserInterfaceActionType::Check, FInputChord()); UI_COMMAND(_InstallInfo, "Installation Info", "Display information on the current Houdini Engine installation", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(_PluginSettings, "PluginSettings", "Displays the Houdini Engine plugin settings", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(_OpenInHoudini, "Open scene in Houdini", "Opens the current Houdini scene in Houdini.", EUserInterfaceActionType::Button, FInputChord(EKeys::O, EModifierKey::Control | EModifierKey::Alt)); UI_COMMAND(_SaveHIPFile, "Save Houdini scene (HIP)", "Saves a .hip file of the current Houdini scene.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(_OnlineDoc, "Online Documentation", "Go to the plugin's online documentation.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(_OnlineForum, "Online Forum", "Go to the plugin's online forum.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(_ReportBug, "Report a bug", "Report a bug for Houdini Engine for Unreal plugin.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(_CookAll, "Recook All", "Recooks all Houdini Assets Actors in the current level.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(_CookSelected, "Recook Selection", "Recooks selected Houdini Asset Actors in the current level.", EUserInterfaceActionType::Button, FInputChord(EKeys::C, EModifierKey::Control | EModifierKey::Alt)); UI_COMMAND(_RebuildAll, "Rebuild All", "Rebuilds all Houdini Assets Actors in the current level.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(_RebuildSelected, "Rebuild Selection", "Rebuilds selected Houdini Asset Actors in the current level.", EUserInterfaceActionType::Button, FInputChord(EKeys::R, EModifierKey::Control | EModifierKey::Alt)); UI_COMMAND(_BakeAll, "Bake And Replace All Houdini Assets", "Bakes and replaces with blueprints all Houdini Assets in the scene.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(_BakeSelected, "Bake And Replace Selection", "Bakes and replaces with blueprints selected Houdini Asset Actors in the current level.", EUserInterfaceActionType::Button, FInputChord(EKeys::B, EModifierKey::Control | EModifierKey::Alt)); UI_COMMAND(_RefineAll, "Refine all Houdini Proxy Meshes To Static Meshes", "Builds and replaces all Houdini proxy meshes with UStaticMesh instances.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(_RefineSelected, "Refine selected Houdini Proxy Meshes To Static Meshes", "Builds and replaces selected Houdini proxy meshes with UStaticMesh instances.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(_CleanUpTempFolder, "Clean Houdini Engine Temp Folder", "Deletes the unused temporary files in the Temporary Cook Folder.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(_PauseAssetCooking, "Pause Houdini Engine Cooking", "When activated, prevents Houdini Engine from cooking assets until unpaused.", EUserInterfaceActionType::Check, FInputChord(EKeys::P, EModifierKey::Control | EModifierKey::Alt)); } void FHoudiniEngineCommands::SaveHIPFile() { if (!FHoudiniEngine::IsInitialized() || FHoudiniEngine::Get().GetSession() == nullptr) { HOUDINI_LOG_ERROR(TEXT("Cannot save the Houdini scene, the Houdini Engine session hasn't been started.")); return; } IDesktopPlatform * DesktopPlatform = FDesktopPlatformModule::Get(); if (!DesktopPlatform || !FHoudiniEngineUtils::IsInitialized()) return; TArray< FString > SaveFilenames; bool bSaved = false; void * ParentWindowWindowHandle = NULL; IMainFrameModule & MainFrameModule = FModuleManager::LoadModuleChecked< IMainFrameModule >(TEXT("MainFrame")); const TSharedPtr< SWindow > & MainFrameParentWindow = MainFrameModule.GetParentWindow(); if (MainFrameParentWindow.IsValid() && MainFrameParentWindow->GetNativeWindow().IsValid()) ParentWindowWindowHandle = MainFrameParentWindow->GetNativeWindow()->GetOSWindowHandle(); bSaved = DesktopPlatform->SaveFileDialog( ParentWindowWindowHandle, NSLOCTEXT("SaveHIPFile", "SaveHIPFile", "Saves a .hip file of the current Houdini scene.").ToString(), *(FEditorDirectories::Get().GetLastDirectory(ELastDirectory::GENERIC_EXPORT)), TEXT(""), TEXT("Houdini HIP file|*.hip"), EFileDialogFlags::None, SaveFilenames); if (bSaved && SaveFilenames.Num()) { // Add a slate notification FString Notification = TEXT("Saving internal Houdini scene..."); FHoudiniEngineUtils::CreateSlateNotification(Notification); // ... and a log message HOUDINI_LOG_MESSAGE(TEXT("Saved Houdini scene to %s"), *SaveFilenames[0]); // Get first path. std::string HIPPathConverted(TCHAR_TO_UTF8(*SaveFilenames[0])); // Save HIP file through Engine. FHoudiniApi::SaveHIPFile(FHoudiniEngine::Get().GetSession(), HIPPathConverted.c_str(), false); } } void FHoudiniEngineCommands::OpenInHoudini() { if(!FHoudiniEngine::IsInitialized() || FHoudiniEngine::Get().GetSession() == nullptr) { HOUDINI_LOG_ERROR(TEXT("Cannot open the scene in Houdini, the Houdini Engine session hasn't been started.")); return; } // First, saves the current scene as a hip file // Creates a proper temporary file name FString UserTempPath = FPaths::CreateTempFilename( FPlatformProcess::UserTempDir(), TEXT("HoudiniEngine"), TEXT(".hip")); // Save HIP file through Engine. std::string TempPathConverted(TCHAR_TO_UTF8(*UserTempPath)); FHoudiniApi::SaveHIPFile( FHoudiniEngine::Get().GetSession(), TempPathConverted.c_str(), false); if (!FPaths::FileExists(UserTempPath)) return; // Add a slate notification FString Notification = TEXT("Opening scene in Houdini..."); FHoudiniEngineUtils::CreateSlateNotification(Notification); // Add quotes to the path to avoid issues with spaces UserTempPath = TEXT("\"") + UserTempPath + TEXT("\""); // Then open the hip file in Houdini FString LibHAPILocation = FHoudiniEngine::Get().GetLibHAPILocation(); FString HoudiniExecutable = FHoudiniEngine::Get().GetHoudiniExecutable(); FString HoudiniLocation = LibHAPILocation + TEXT("//") + HoudiniExecutable; FProcHandle ProcHandle = FPlatformProcess::CreateProc( *HoudiniLocation, *UserTempPath, true, false, false, nullptr, 0, FPlatformProcess::UserTempDir(), nullptr, nullptr); if (!ProcHandle.IsValid()) { // Try with the steam version executable instead HoudiniLocation = LibHAPILocation + TEXT("//hindie.steam"); ProcHandle = FPlatformProcess::CreateProc( *HoudiniLocation, *UserTempPath, true, false, false, nullptr, 0, FPlatformProcess::UserTempDir(), nullptr, nullptr); if (!ProcHandle.IsValid()) { HOUDINI_LOG_ERROR(TEXT("Failed to open scene in Houdini.")); } } // ... and a log message HOUDINI_LOG_MESSAGE(TEXT("Opened scene in Houdini.")); } void FHoudiniEngineCommands::ReportBug() { FPlatformProcess::LaunchURL(HAPI_UNREAL_BUG_REPORT_URL, nullptr, nullptr); } void FHoudiniEngineCommands::ShowInstallInfo() { // TODO } void FHoudiniEngineCommands::ShowPluginSettings() { FModuleManager::LoadModuleChecked("Settings").ShowViewer(FName("Project"), FName("Plugins"), FName("HoudiniEngine")); } void FHoudiniEngineCommands::OnlineDocumentation() { FPlatformProcess::LaunchURL(HAPI_UNREAL_ONLINE_DOC_URL, nullptr, nullptr); } void FHoudiniEngineCommands::OnlineForum() { FPlatformProcess::LaunchURL(HAPI_UNREAL_ONLINE_FORUM_URL, nullptr, nullptr); } void FHoudiniEngineCommands::CleanUpTempFolder() { // TODO: Improve me! slow now that we also have SM saved in the temp directory // Due to the ref, we probably iterate a little too much, and should maybe do passes following the order of refs: // mesh first, then materials, then textures. // have a look at UWrangleContentCommandlet as well // Add a slate notification FString Notification = TEXT("Cleaning up Houdini Engine temporary folder..."); FHoudiniEngineUtils::CreateSlateNotification(Notification); GWarn->BeginSlowTask(LOCTEXT("CleanUpTemp", "Cleaning up the Houdini Engine Temp Folder"), false, false); // Get the default temp cook folder FString TempCookFolder = FHoudiniEngineRuntime::Get().GetDefaultTemporaryCookFolder(); TArray TempCookFolders; TempCookFolders.Add(FHoudiniEngineRuntime::Get().GetDefaultTemporaryCookFolder()); for (TObjectIterator It; It; ++It) { FString CookFolder = It->TemporaryCookFolder.Path; if (CookFolder.IsEmpty()) continue; TempCookFolders.AddUnique(CookFolder); } // The Asset registry will help us finding if the content of the asset is referenced FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); int32 DeletedCount = 0; bool bDidDeleteAsset = true; while (bDidDeleteAsset) { // To correctly clean the temp folder, we need to iterate multiple times, because some of the temp assets // might be referenced by other temp assets.. (ie Textures are referenced by Materials) // We'll stop looking for assets to delete when no deletion occured. bDidDeleteAsset = false; TArray AssetDataList; for (auto& TempFolder : TempCookFolders) { // The Object library will list all UObjects found in the TempFolder auto ObjectLibrary = UObjectLibrary::CreateLibrary(UObject::StaticClass(), false, true); ObjectLibrary->LoadAssetDataFromPath(TempFolder); // Get all the assets found in the TEMP folder TArray CurrentAssetDataList; ObjectLibrary->GetAssetDataList(CurrentAssetDataList); AssetDataList.Append(CurrentAssetDataList); } // All the assets we're going to delete TArray AssetDataToDelete; for (FAssetData Data : AssetDataList) { UPackage* CurrentPackage = Data.GetPackage(); if (!CurrentPackage || CurrentPackage->IsPendingKill()) continue; // Do not try to delete the package if it's referenced anywhere TArray ReferenceNames; AssetRegistryModule.Get().GetReferencers(CurrentPackage->GetFName(), ReferenceNames, EAssetRegistryDependencyType::All); if (ReferenceNames.Num() > 0) continue; bool bAssetDataSafeToDelete = true; TArray AssetsInPackage; AssetRegistryModule.Get().GetAssetsByPackageName(CurrentPackage->GetFName(), AssetsInPackage); for (const auto& AssetInfo : AssetsInPackage) { // Check if the objects contained in the package are referenced by something that won't be garbage collected (*including* the undo buffer) UObject* AssetInPackage = AssetInfo.GetAsset(); if (!AssetInPackage || AssetInPackage->IsPendingKill()) continue; FReferencerInformationList ReferencesIncludingUndo; bool bReferencedInMemoryOrUndoStack = IsReferenced(AssetInPackage, GARBAGE_COLLECTION_KEEPFLAGS, EInternalObjectFlags::GarbageCollectionKeepFlags, true, &ReferencesIncludingUndo); if (!bReferencedInMemoryOrUndoStack) continue; // We do have external references, check if the external references are in our ObjectToDelete list // If they are, we can delete the asset because its references are going to be deleted as well. for (auto ExtRef : ReferencesIncludingUndo.ExternalReferences) { UObject* Outer = ExtRef.Referencer->GetOuter(); if (!Outer || Outer->IsPendingKill()) continue; bool bOuterFound = false; for (auto DataToDelete : AssetDataToDelete) { if (DataToDelete.GetPackage() == Outer) { bOuterFound = true; break; } else if (DataToDelete.GetAsset() == Outer) { bOuterFound = true; break; } } // We have at least one reference that's not going to be deleted, we have to keep the asset if (!bOuterFound) { bAssetDataSafeToDelete = false; break; } } } if (bAssetDataSafeToDelete) AssetDataToDelete.Add(Data); } // Nothing to delete if (AssetDataToDelete.Num() <= 0) break; int32 CurrentDeleted = ObjectTools::DeleteAssets(AssetDataToDelete, false); if (CurrentDeleted > 0) { DeletedCount += CurrentDeleted; bDidDeleteAsset = true; } } // Now, go through all the directories in the temp directories and delete all the empty ones IFileManager& FM = IFileManager::Get(); // Lambda that parses a directory recursively and returns true if it is empty auto IsEmptyFolder = [&FM](FString PathToDeleteOnDisk) { struct FEmptyFolderVisitor : public IPlatformFile::FDirectoryVisitor { bool bIsEmpty; FEmptyFolderVisitor() : bIsEmpty(true) { } virtual bool Visit(const TCHAR* FilenameOrDirectory, bool bIsDirectory) override { if (!bIsDirectory) { bIsEmpty = false; return false; // abort searching } return true; // continue searching } }; // Look for files on disk in case the folder contains things not tracked by the asset registry FEmptyFolderVisitor EmptyFolderVisitor; IFileManager::Get().IterateDirectoryRecursively(*PathToDeleteOnDisk, EmptyFolderVisitor); return EmptyFolderVisitor.bIsEmpty; }; // Iterates on all the temporary cook directories recursively, // And keep not of all the empty directories FString TempCookPathOnDisk; TArray FoldersToDelete; if (FPackageName::TryConvertLongPackageNameToFilename(TempCookFolder, TempCookPathOnDisk)) { FM.IterateDirectoryRecursively(*TempCookPathOnDisk, [&FM, &FoldersToDelete, &IsEmptyFolder](const TCHAR* InFilenameOrDirectory, const bool InIsDirectory) -> bool { // Skip Files if (!InIsDirectory) return true; FString CurrentDirectoryPath = FString(InFilenameOrDirectory); if (IsEmptyFolder(CurrentDirectoryPath)) FoldersToDelete.Add(CurrentDirectoryPath); // keep iterating return true; }); } int32 DeletedDirectories = 0; for (auto& FolderPath : FoldersToDelete) { FString PathToDelete; if (!FPackageName::TryConvertFilenameToLongPackageName(FolderPath, PathToDelete)) continue; if (IFileManager::Get().DeleteDirectory(*FolderPath, false, true)) { AssetRegistryModule.Get().RemovePath(PathToDelete); DeletedDirectories++; } } GWarn->EndSlowTask(); // Add a slate notification Notification = TEXT("Deleted ") + FString::FromInt(DeletedCount) + TEXT(" temporary files and ") + FString::FromInt(DeletedDirectories) + TEXT(" directories."); FHoudiniEngineUtils::CreateSlateNotification(Notification); // ... and a log message HOUDINI_LOG_MESSAGE(TEXT("Deleted %d temporary files and %d directories."), DeletedCount, DeletedDirectories); } void FHoudiniEngineCommands::BakeAllAssets() { // Add a slate notification FString Notification = TEXT("Baking all assets in the current level..."); FHoudiniEngineUtils::CreateSlateNotification(Notification); // Bakes and replaces with blueprints all Houdini Assets in the current level int32 BakedCount = 0; for (TObjectIterator Itr; Itr; ++Itr) { UHoudiniAssetComponent * HoudiniAssetComponent = *Itr; if (!HoudiniAssetComponent || HoudiniAssetComponent->IsPendingKill()) { HOUDINI_LOG_ERROR(TEXT("Failed to bake a Houdini Asset in the scene! - Invalid Houdini Asset Component")); continue; } if (!HoudiniAssetComponent->IsComponentValid()) { FString AssetName = HoudiniAssetComponent->GetOuter() ? HoudiniAssetComponent->GetOuter()->GetName() : HoudiniAssetComponent->GetName(); if (AssetName != "Default__HoudiniAssetActor") HOUDINI_LOG_ERROR(TEXT("Failed to bake a Houdini Asset in the scene! - %s is invalid"), *AssetName); continue; } // If component is not cooking or instancing, we can bake blueprint. if (HoudiniAssetComponent->IsInstantiatingOrCooking()) { FString AssetName = HoudiniAssetComponent->GetOuter() ? HoudiniAssetComponent->GetOuter()->GetName() : HoudiniAssetComponent->GetName(); HOUDINI_LOG_ERROR(TEXT("Failed to bake a Houdini Asset in the scene! - %s is actively instantiating or cooking"), *AssetName); continue; } bool bSuccess = false; bool BakeToBlueprints = true; if (BakeToBlueprints) { // if (FHoudiniEngineBakeUtils::ReplaceWithBlueprint(HoudiniAssetComponent) != nullptr) // bSuccess = true; FHoudiniEngineOutputStats BakeStats; TArray PackagesToSave; TArray Blueprints; const bool bInReplaceAssets = true; bSuccess = FHoudiniEngineBakeUtils::BakeBlueprints(HoudiniAssetComponent, bInReplaceAssets, HoudiniAssetComponent->bRecenterBakedActors, BakeStats, Blueprints, PackagesToSave); FHoudiniEngineBakeUtils::SaveBakedPackages(PackagesToSave); if (bSuccess) { // Instantiate blueprints in component's level, then remove houdini asset actor bSuccess = false; ULevel* Level = HoudiniAssetComponent->GetComponentLevel(); if (IsValid(Level)) { UWorld* World = Level->GetWorld(); if (IsValid(World)) { FActorSpawnParameters SpawnParams; SpawnParams.OverrideLevel = Level; FTransform Transform = HoudiniAssetComponent->GetComponentTransform(); for (UBlueprint* Blueprint : Blueprints) { if (!IsValid(Blueprint)) continue; World->SpawnActor(Blueprint->GetBlueprintClass(), &Transform, SpawnParams); } FHoudiniEngineBakeUtils::DeleteBakedHoudiniAssetActor(HoudiniAssetComponent); bSuccess = true; } } } } else { // TODO: this used to have a way to not select in v1 // if (FHoudiniEngineBakeUtils::ReplaceHoudiniActorWithActors(HoudiniAssetComponent)) // bSuccess = true; const bool bReplaceActors = true; const bool bReplaceAssets = true; if (FHoudiniEngineBakeUtils::BakeHoudiniActorToActors(HoudiniAssetComponent, bReplaceActors, bReplaceAssets, HoudiniAssetComponent->bRecenterBakedActors)) { bSuccess = true; FHoudiniEngineBakeUtils::DeleteBakedHoudiniAssetActor(HoudiniAssetComponent); } } if (bSuccess) BakedCount++; } // Add a slate notification Notification = TEXT("Baked ") + FString::FromInt(BakedCount) + TEXT(" Houdini assets."); FHoudiniEngineUtils::CreateSlateNotification(Notification); // ... and a log message HOUDINI_LOG_MESSAGE(TEXT("Baked all %d Houdini assets in the current level."), BakedCount); } void FHoudiniEngineCommands::PauseAssetCooking() { // Revert the global flag bool bCurrentCookingEnabled = !FHoudiniEngine::Get().IsCookingEnabled(); FHoudiniEngine::Get().SetCookingEnabled(bCurrentCookingEnabled); // We need to refresh UI when pause cooking. Set refresh UI counter to be the number of current registered HACs. if (!bCurrentCookingEnabled) FHoudiniEngine::Get().SetUIRefreshCountWhenPauseCooking( FHoudiniEngineRuntime::Get().GetRegisteredHoudiniComponentCount() ); // Add a slate notification FString Notification = TEXT("Houdini Engine cooking paused"); if (bCurrentCookingEnabled) Notification = TEXT("Houdini Engine cooking resumed"); FHoudiniEngineUtils::CreateSlateNotification(Notification); // ... and a log message if (bCurrentCookingEnabled) HOUDINI_LOG_MESSAGE(TEXT("Houdini Engine cooking resumed.")); else HOUDINI_LOG_MESSAGE(TEXT("Houdini Engine cooking paused.")); if (!bCurrentCookingEnabled) return; /* // If we are unpausing, tick each asset component to "update" them for (TObjectIterator Itr; Itr; ++Itr) { UHoudiniAssetComponent * HoudiniAssetComponent = *Itr; if (!HoudiniAssetComponent || HoudiniAssetComponent->IsPendingKill() || !HoudiniAssetComponent->IsValidLowLevel()) { HOUDINI_LOG_ERROR(TEXT("Failed to cook a Houdini Asset in the scene!")); continue; } HoudiniAssetComponent->StartHoudiniTicking(); } */ } bool FHoudiniEngineCommands::IsAssetCookingPaused() { return !FHoudiniEngine::Get().IsCookingEnabled(); } void FHoudiniEngineCommands::RecookSelection() { // Get current world selection TArray WorldSelection; int32 SelectedHoudiniAssets = FHoudiniEngineEditorUtils::GetWorldSelection(WorldSelection, true); if (SelectedHoudiniAssets <= 0) { HOUDINI_LOG_MESSAGE(TEXT("No Houdini Assets selected in the world outliner")); return; } // Add a slate notification FString Notification = TEXT("Cooking selected Houdini Assets..."); FHoudiniEngineUtils::CreateSlateNotification(Notification); // Iterates over the selection and cook the assets if they're in a valid state int32 CookedCount = 0; for (int32 Idx = 0; Idx < SelectedHoudiniAssets; Idx++) { AHoudiniAssetActor * HoudiniAssetActor = Cast(WorldSelection[Idx]); if (!HoudiniAssetActor || HoudiniAssetActor->IsPendingKill()) continue; UHoudiniAssetComponent * HoudiniAssetComponent = HoudiniAssetActor->GetHoudiniAssetComponent(); if (!HoudiniAssetComponent || HoudiniAssetComponent->IsPendingKill()) continue; HoudiniAssetComponent->MarkAsNeedCook(); CookedCount++; } // Add a slate notification Notification = TEXT("Re-cooking ") + FString::FromInt(CookedCount) + TEXT(" Houdini assets."); FHoudiniEngineUtils::CreateSlateNotification(Notification); // ... and a log message HOUDINI_LOG_MESSAGE(TEXT("Re-cooking %d selected Houdini assets."), CookedCount); } void FHoudiniEngineCommands::RecookAllAssets() { // Add a slate notification FString Notification = TEXT("Cooking all assets in the current level..."); FHoudiniEngineUtils::CreateSlateNotification(Notification); // Bakes and replaces with blueprints all Houdini Assets in the current level int32 CookedCount = 0; for (TObjectIterator Itr; Itr; ++Itr) { UHoudiniAssetComponent * HoudiniAssetComponent = *Itr; if (!HoudiniAssetComponent || HoudiniAssetComponent->IsPendingKill()) continue; HoudiniAssetComponent->MarkAsNeedCook(); CookedCount++; } // Add a slate notification Notification = TEXT("Re-cooked ") + FString::FromInt(CookedCount) + TEXT(" Houdini assets."); FHoudiniEngineUtils::CreateSlateNotification(Notification); // ... and a log message HOUDINI_LOG_MESSAGE(TEXT("Re-cooked %d Houdini assets in the current level."), CookedCount); } void FHoudiniEngineCommands::RebuildAllAssets() { // Add a slate notification FString Notification = TEXT("Re-building all assets in the current level..."); FHoudiniEngineUtils::CreateSlateNotification(Notification); // Bakes and replaces with blueprints all Houdini Assets in the current level int32 RebuiltCount = 0; for (TObjectIterator Itr; Itr; ++Itr) { UHoudiniAssetComponent * HoudiniAssetComponent = *Itr; if (!HoudiniAssetComponent || HoudiniAssetComponent->IsPendingKill()) continue; HoudiniAssetComponent->MarkAsNeedRebuild(); RebuiltCount++; } // Add a slate notification Notification = TEXT("Rebuilt ") + FString::FromInt(RebuiltCount) + TEXT(" Houdini assets."); FHoudiniEngineUtils::CreateSlateNotification(Notification); // ... and a log message HOUDINI_LOG_MESSAGE(TEXT("Rebuilt %d Houdini assets in the current level."), RebuiltCount); } void FHoudiniEngineCommands::RebuildSelection() { // Get current world selection TArray WorldSelection; int32 SelectedHoudiniAssets = FHoudiniEngineEditorUtils::GetWorldSelection(WorldSelection, true); if (SelectedHoudiniAssets <= 0) { HOUDINI_LOG_MESSAGE(TEXT("No Houdini Assets selected in the world outliner")); return; } // Add a slate notification FString Notification = TEXT("Rebuilding selected Houdini Assets..."); FHoudiniEngineUtils::CreateSlateNotification(Notification); // Iterates over the selection and rebuilds the assets if they're in a valid state int32 RebuiltCount = 0; for (int32 Idx = 0; Idx < SelectedHoudiniAssets; Idx++) { AHoudiniAssetActor * HoudiniAssetActor = Cast(WorldSelection[Idx]); if (!HoudiniAssetActor || HoudiniAssetActor->IsPendingKill()) continue; UHoudiniAssetComponent * HoudiniAssetComponent = HoudiniAssetActor->GetHoudiniAssetComponent(); if (!HoudiniAssetComponent || HoudiniAssetComponent->IsPendingKill())// || !HoudiniAssetComponent->IsComponentValid()) continue; HoudiniAssetComponent->MarkAsNeedRebuild(); RebuiltCount++; } // Add a slate notification Notification = TEXT("Rebuilt ") + FString::FromInt(RebuiltCount) + TEXT(" Houdini assets."); FHoudiniEngineUtils::CreateSlateNotification(Notification); // ... and a log message HOUDINI_LOG_MESSAGE(TEXT("Rebuilt %d selected Houdini assets."), RebuiltCount); } void FHoudiniEngineCommands::BakeSelection() { // Get current world selection TArray WorldSelection; int32 SelectedHoudiniAssets = FHoudiniEngineEditorUtils::GetWorldSelection(WorldSelection, true); if (SelectedHoudiniAssets <= 0) { HOUDINI_LOG_MESSAGE(TEXT("No Houdini Assets selected in the world outliner")); return; } // Add a slate notification FString Notification = TEXT("Baking selected Houdini Asset Actors in the current level..."); FHoudiniEngineUtils::CreateSlateNotification(Notification); // Iterates over the selection and rebuilds the assets if they're in a valid state int32 BakedCount = 0; for (int32 Idx = 0; Idx < SelectedHoudiniAssets; Idx++) { AHoudiniAssetActor * HoudiniAssetActor = Cast(WorldSelection[Idx]); if (!HoudiniAssetActor || HoudiniAssetActor->IsPendingKill()) continue; UHoudiniAssetComponent* HoudiniAssetComponent = HoudiniAssetActor->GetHoudiniAssetComponent(); if (!HoudiniAssetComponent || HoudiniAssetComponent->IsPendingKill()) { HOUDINI_LOG_ERROR(TEXT("Failed to export a Houdini Asset in the scene!")); continue; } if (!HoudiniAssetComponent->IsComponentValid()) { FString AssetName = HoudiniAssetComponent->GetOuter() ? HoudiniAssetComponent->GetOuter()->GetName() : HoudiniAssetComponent->GetName(); HOUDINI_LOG_ERROR(TEXT("Failed to export Houdini Asset: %s in the scene!"), *AssetName); continue; } // If component is not cooking or instancing, we can bake blueprint. if (!HoudiniAssetComponent->IsInstantiatingOrCooking()) { // if (FHoudiniEngineBakeUtils::ReplaceWithBlueprint(HoudiniAssetComponent) != nullptr) // BakedCount++; // if (FHoudiniEngineBakeUtils::ReplaceWithBlueprint(HoudiniAssetComponent) != nullptr) // bSuccess = true; FHoudiniEngineOutputStats BakeStats; TArray PackagesToSave; TArray Blueprints; const bool bReplaceAssets = true; const bool bSuccess = FHoudiniEngineBakeUtils::BakeBlueprints(HoudiniAssetComponent, bReplaceAssets, HoudiniAssetComponent->bRecenterBakedActors, BakeStats, Blueprints, PackagesToSave); FHoudiniEngineBakeUtils::SaveBakedPackages(PackagesToSave); if (bSuccess) { // Instantiate blueprints in component's level, then remove houdini asset actor ULevel* Level = HoudiniAssetComponent->GetComponentLevel(); if (IsValid(Level)) { UWorld* World = Level->GetWorld(); if (IsValid(World)) { FActorSpawnParameters SpawnParams; SpawnParams.OverrideLevel = Level; FTransform Transform = HoudiniAssetComponent->GetComponentTransform(); for (UBlueprint* Blueprint : Blueprints) { if (!IsValid(Blueprint)) continue; World->SpawnActor(Blueprint->GetBlueprintClass(), &Transform, SpawnParams); } FHoudiniEngineBakeUtils::DeleteBakedHoudiniAssetActor(HoudiniAssetComponent); BakedCount++; } } } } } // Add a slate notification Notification = TEXT("Baked ") + FString::FromInt(BakedCount) + TEXT(" Houdini assets."); FHoudiniEngineUtils::CreateSlateNotification(Notification); // ... and a log message HOUDINI_LOG_MESSAGE(TEXT("Baked all %d Houdini assets in the current level."), BakedCount); } // Recentre HoudiniAsset actors' pivots to their input / cooked static-mesh average centre. void FHoudiniEngineCommands::RecentreSelection() { /* #if WITH_EDITOR //Get current world selection TArray WorldSelection; int32 SelectedHoudiniAssets = FHoudiniEngineEditorUtils::GetWorldSelection(WorldSelection, true); if (SelectedHoudiniAssets <= 0) { HOUDINI_LOG_MESSAGE(TEXT("No Houdini Assets selected in the world outliner")); return; } // Add a slate notification FString Notification = TEXT("Recentering selected Houdini Assets..."); FHoudiniEngineUtils::CreateSlateNotification(Notification); // Iterates over the selection and cook the assets if they're in a valid state int32 RecentreCount = 0; for (int32 Idx = 0; Idx < SelectedHoudiniAssets; Idx++) { AHoudiniAssetActor * HoudiniAssetActor = Cast(WorldSelection[Idx]); if (!HoudiniAssetActor || HoudiniAssetActor->IsPendingKill()) continue; UHoudiniAssetComponent * HoudiniAssetComponent = HoudiniAssetActor->GetHoudiniAssetComponent(); if (!HoudiniAssetComponent || !HoudiniAssetComponent->IsComponentValid()) continue; // Get the average centre of all the created Static Meshes FVector AverageBoundsCentre = FVector::ZeroVector; int32 NumBounds = 0; const FVector CurrentLocation = HoudiniAssetComponent->GetComponentLocation(); { //Check Static Meshes TArray StaticMeshes; StaticMeshes.Reserve(16); HoudiniAssetComponent->GetAllUsedStaticMeshes(StaticMeshes); //Get average centre of all the static meshes. for (const UStaticMesh* pMesh : StaticMeshes) { if (!pMesh) continue; //to world space AverageBoundsCentre += (pMesh->GetBounds().Origin + CurrentLocation); NumBounds++; } } //Check Inputs if (0 == NumBounds) { const TArray< UHoudiniInput* >& AssetInputs = HoudiniAssetComponent->Inputs; for (const UHoudiniInput* pInput : AssetInputs) { if (!pInput || pInput->IsPendingKill()) continue; // to world space FBox Bounds = pInput->GetInputBounds(CurrentLocation); if (Bounds.IsValid) { AverageBoundsCentre += Bounds.GetCenter(); NumBounds++; } } } //if we have more than one, get the average centre if (NumBounds > 1) { AverageBoundsCentre /= (float)NumBounds; } //if we need to move... float fDist = FVector::DistSquared(CurrentLocation, AverageBoundsCentre); if (NumBounds && fDist > 1.0f) { // Move actor to average centre and recook // This will refresh the static mesh under the HoudiniAssestComponent ( undoing the translation ). HoudiniAssetActor->SetActorLocation(AverageBoundsCentre, false, nullptr, ETeleportType::TeleportPhysics); // Recook now the houdini-static-mesh has a new origin HoudiniAssetComponent->StartTaskAssetCookingManual(); RecentreCount++; } } if (RecentreCount) { // UE4 Editor doesn't refresh the translation-handles until they are re-selected, confusing the user, deselect the objects. GEditor->SelectNone(true, false); } // Add a slate notification Notification = TEXT("Re-centred ") + FString::FromInt(RecentreCount) + TEXT(" Houdini assets."); FHoudiniEngineUtils::CreateSlateNotification(Notification); // ... and a log message HOUDINI_LOG_MESSAGE(TEXT("Re-centred %d selected Houdini assets."), RecentreCount); #endif //WITH_EDITOR */ } void FHoudiniEngineCommands::OpenSessionSync() { //if (!FHoudiniEngine::IsInitialized()) // return; if (!FHoudiniEngine::Get().StopSession()) { // StopSession returns false only if Houdini is not initialized HOUDINI_LOG_ERROR(TEXT("Failed to start Session Sync - HAPI Not initialized")); return; } // Get the runtime settings to get the session/type and settings const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault(); EHoudiniRuntimeSettingsSessionType SessionType = HoudiniRuntimeSettings->SessionType; FString ServerPipeName = HoudiniRuntimeSettings->ServerPipeName; int32 ServerPort = HoudiniRuntimeSettings->ServerPort; FString SessionSyncArgs = TEXT("-hess="); if (SessionType == EHoudiniRuntimeSettingsSessionType::HRSST_NamedPipe) { // Add the -hess=pipe:hapi argument SessionSyncArgs += TEXT("pipe:") + ServerPipeName; } else if (SessionType == EHoudiniRuntimeSettingsSessionType::HRSST_Socket) { // Add the -hess=port:9090 argument SessionSyncArgs += TEXT("port:") + FString::FromInt(ServerPort); } else { // Invalid session type HOUDINI_LOG_ERROR(TEXT("Failed to start Session Sync - Invalid session type")); return; } // Add a slate notification FString Notification = TEXT("Opening Houdini Session Sync..."); FHoudiniEngineUtils::CreateSlateNotification(Notification); // ... and a log message HOUDINI_LOG_MESSAGE(TEXT("Opening Houdini Session Sync.")); // Only launch Houdini in Session sync if we havent started it already! FProcHandle PreviousHESS = FHoudiniEngine::Get().GetHESSProcHandle(); if (!FPlatformProcess::IsProcRunning(PreviousHESS)) { // Start houdini with the -hess commandline args const FString LibHAPILocation = FHoudiniEngine::Get().GetLibHAPILocation(); # if PLATFORM_MAC const FString HoudiniExeLocationRelativeToLibHAPI = TEXT("/../Resources/bin"); # elif PLATFORM_LINUX const FString HoudiniExeLocationRelativeToLibHAPI = TEXT("/../bin"); # elif PLATFORM_WINDOWS const FString HoudiniExeLocationRelativeToLibHAPI; # else // Treat an unknown platform the same as Windows for now const FString HoudiniExeLocationRelativeToLibHAPI; # endif FString HoudiniExecutable = FHoudiniEngine::Get().GetHoudiniExecutable(); FString HoudiniLocation = LibHAPILocation + HoudiniExeLocationRelativeToLibHAPI + TEXT("/") + HoudiniExecutable; HOUDINI_LOG_MESSAGE(TEXT("Path to houdini executable: %s"), *HoudiniLocation); FProcHandle HESSHandle = FPlatformProcess::CreateProc( *HoudiniLocation, *SessionSyncArgs, true, false, false, nullptr, 0, FPlatformProcess::UserTempDir(), nullptr, nullptr); if (!HESSHandle.IsValid()) { // Try with the steam version executable instead HoudiniLocation = LibHAPILocation + HoudiniExeLocationRelativeToLibHAPI + TEXT("/hindie.steam"); HOUDINI_LOG_MESSAGE(TEXT("Path to hindie.steam executable: %s"), *HoudiniLocation); HESSHandle = FPlatformProcess::CreateProc( *HoudiniLocation, *SessionSyncArgs, true, false, false, nullptr, 0, FPlatformProcess::UserTempDir(), nullptr, nullptr); if (!HESSHandle.IsValid()) { HOUDINI_LOG_ERROR(TEXT("Failed to launch Houdini in Session Sync mode.")); return; } } // Keep track of the SessionSync ProcHandle FHoudiniEngine::Get().SetHESSProcHandle(HESSHandle); } // Start an Async task to connect to Session Sync Async(EAsyncExecution::TaskGraphMainThread, [SessionType, ServerPipeName, ServerPort]() { // Use a timeout to avoid waiting indefinitely for H to start in session sync mode const double Timeout = 180.0; // 3min const double StartTimestamp = FPlatformTime::Seconds(); FString ServerHost = TEXT("localhost"); while (!FHoudiniEngine::Get().SessionSyncConnect(SessionType, ServerPipeName, ServerHost, ServerPort)) { // Houdini might not be done loading, sleep for one second FPlatformProcess::Sleep(1); // Check for the timeout if (FPlatformTime::Seconds() - StartTimestamp > Timeout) { // ... and a log message HOUDINI_LOG_ERROR(TEXT("Failed to start SessionSync - Timeout...")); return false; } } // Initialize HAPI with this session if (!FHoudiniEngine::Get().InitializeHAPISession()) { FHoudiniEngine::Get().StopTicking(); return false; } // Notify all HACs that they need to instantiate in the new session MarkAllHACsAsNeedInstantiation(); // Start ticking FHoudiniEngine::Get().StartTicking(); // Add a slate notification FString Notification = TEXT("Succesfully connected to Session Sync..."); FHoudiniEngineUtils::CreateSlateNotification(Notification); // ... and a log message HOUDINI_LOG_MESSAGE(TEXT("Succesfully connected to Session Sync...")); return true; }); } void FHoudiniEngineCommands::CloseSessionSync() { if (!FHoudiniEngine::Get().StopSession()) { // StopSession returns false only if Houdini is not initialized HOUDINI_LOG_ERROR(TEXT("Failed to stop Session Sync - HAPI Not initialized")); return; } // Add a slate notification FString Notification = TEXT("Stopping Houdini Session Sync..."); FHoudiniEngineUtils::CreateSlateNotification(Notification); // ... and a log message HOUDINI_LOG_MESSAGE(TEXT("Stopping Houdini Session Sync.")); // Stop Houdini Session sync if it is still running! FProcHandle PreviousHESS = FHoudiniEngine::Get().GetHESSProcHandle(); if (FPlatformProcess::IsProcRunning(PreviousHESS)) { FPlatformProcess::TerminateProc(PreviousHESS, true); } } void FHoudiniEngineCommands::SetViewportSync(const int32& ViewportSync) { if (ViewportSync == 1) { FHoudiniEngine::Get().SetSyncViewportEnabled(true); FHoudiniEngine::Get().SetSyncHoudiniViewportEnabled(true); FHoudiniEngine::Get().SetSyncUnrealViewportEnabled(false); } else if (ViewportSync == 2) { FHoudiniEngine::Get().SetSyncViewportEnabled(true); FHoudiniEngine::Get().SetSyncHoudiniViewportEnabled(false); FHoudiniEngine::Get().SetSyncUnrealViewportEnabled(true); } else if (ViewportSync == 3) { FHoudiniEngine::Get().SetSyncViewportEnabled(true); FHoudiniEngine::Get().SetSyncHoudiniViewportEnabled(true); FHoudiniEngine::Get().SetSyncUnrealViewportEnabled(true); } else { FHoudiniEngine::Get().SetSyncViewportEnabled(false); FHoudiniEngine::Get().SetSyncHoudiniViewportEnabled(false); FHoudiniEngine::Get().SetSyncUnrealViewportEnabled(false); } } int32 FHoudiniEngineCommands::GetViewportSync() { if(!FHoudiniEngine::Get().IsSyncViewportEnabled()) return 0; bool bSyncH = FHoudiniEngine::Get().IsSyncHoudiniViewportEnabled(); bool bSyncU = FHoudiniEngine::Get().IsSyncUnrealViewportEnabled(); if (bSyncH && !bSyncU) return 1; else if (!bSyncH && bSyncU) return 2; else if (bSyncH && bSyncU) return 3; else return 0; } void FHoudiniEngineCommands::RestartSession() { // Restart the current Houdini Engine Session if (!FHoudiniEngine::Get().RestartSession()) return; // We've successfully restarted the Houdini Engine session, // We now need to notify all the HoudiniAssetComponent that they need to re instantiate // themselves in the new Houdini engine session. MarkAllHACsAsNeedInstantiation(); } void FHoudiniEngineCommands::CreateSession() { const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); // Restart the current Houdini Engine Session if (!FHoudiniEngine::Get().CreateSession(HoudiniRuntimeSettings->SessionType)) return; // We've successfully created the Houdini Engine session, // We now need to notify all the HoudiniAssetComponent that they need to re instantiate // themselves in the new Houdini engine session. MarkAllHACsAsNeedInstantiation(); } void FHoudiniEngineCommands::ConnectSession() { const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); // Restart the current Houdini Engine Session if (!FHoudiniEngine::Get().ConnectSession(HoudiniRuntimeSettings->SessionType)) return; // We've successfully connected to a Houdini Engine session, // We now need to notify all the HoudiniAssetComponent that they need to re instantiate // themselves in the new Houdini engine session. MarkAllHACsAsNeedInstantiation(); } void FHoudiniEngineCommands::MarkAllHACsAsNeedInstantiation() { // Notify all the HoudiniAssetComponents that they need to re instantiate themselves in the new Houdini engine session. for (TObjectIterator Itr; Itr; ++Itr) { UHoudiniAssetComponent * HoudiniAssetComponent = *Itr; if (!HoudiniAssetComponent || HoudiniAssetComponent->IsPendingKill()) continue; HoudiniAssetComponent->MarkAsNeedInstantiation(); } } bool FHoudiniEngineCommands::IsSessionValid() { return FHoudiniEngine::IsInitialized(); } bool FHoudiniEngineCommands::IsSessionSyncProcessValid() { // Only launch Houdini in Session sync if we havent started it already! FProcHandle PreviousHESS = FHoudiniEngine::Get().GetHESSProcHandle(); return FPlatformProcess::IsProcRunning(PreviousHESS); } void FHoudiniEngineCommands::StopSession() { // Restart the current Houdini Engine Session if (!FHoudiniEngine::Get().StopSession()) { // StopSession returns false only if Houdini is not initialized HOUDINI_LOG_ERROR(TEXT("Failed to restart the Houdini Engine session - HAPI Not initialized")); } else { HOUDINI_LOG_MESSAGE(TEXT("Houdini Engine session stopped.")); } } EHoudiniProxyRefineRequestResult FHoudiniEngineCommands::RefineHoudiniProxyMeshesToStaticMeshes(bool bOnlySelectedActors, bool bSilent, bool bRefineAll, bool bOnPreSaveWorld, UWorld *OnPreSaveWorld, bool bOnPreBeginPIE) { // Get current world selection TArray WorldSelection; int32 NumSelectedHoudiniAssets = 0; if (bOnlySelectedActors) { NumSelectedHoudiniAssets = FHoudiniEngineEditorUtils::GetWorldSelection(WorldSelection, true); if (NumSelectedHoudiniAssets <= 0) { HOUDINI_LOG_MESSAGE(TEXT("No Houdini Assets selected in the world outliner")); return EHoudiniProxyRefineRequestResult::Invalid; } } // Add a slate notification FString Notification = TEXT("Refining Houdini proxy meshes to static meshes..."); // FHoudiniEngineUtils::CreateSlateNotification(Notification); // First find the components that have meshes that we must refine TArray ComponentsToRefine; TArray ComponentsToCook; // Components that would be candidates for refinement/cooking, but have errors TArray SkippedComponents; if (bOnlySelectedActors) { for (int32 Index = 0; Index < NumSelectedHoudiniAssets; ++Index) { AHoudiniAssetActor * HoudiniAssetActor = Cast(WorldSelection[Index]); if (!HoudiniAssetActor || HoudiniAssetActor->IsPendingKill()) continue; UHoudiniAssetComponent * HoudiniAssetComponent = HoudiniAssetActor->GetHoudiniAssetComponent(); if (!HoudiniAssetComponent || HoudiniAssetComponent->IsPendingKill()) continue; // Check if we should consider this component for proxy mesh refinement or cooking, based on its settings and // flags passed to the function. TriageHoudiniAssetComponentsForProxyMeshRefinement(HoudiniAssetComponent, bRefineAll, bOnPreSaveWorld, OnPreSaveWorld, bOnPreBeginPIE, ComponentsToRefine, ComponentsToCook, SkippedComponents); } } else { for (TObjectIterator Itr; Itr; ++Itr) { UHoudiniAssetComponent * HoudiniAssetComponent = *Itr; if (!HoudiniAssetComponent || HoudiniAssetComponent->IsPendingKill()) continue; if (bOnPreSaveWorld && OnPreSaveWorld && OnPreSaveWorld != HoudiniAssetComponent->GetWorld()) continue; // Check if we should consider this component for proxy mesh refinement or cooking, based on its settings and // flags passed to the function. TriageHoudiniAssetComponentsForProxyMeshRefinement(HoudiniAssetComponent, bRefineAll, bOnPreSaveWorld, OnPreSaveWorld, bOnPreBeginPIE, ComponentsToRefine, ComponentsToCook, SkippedComponents); } } return RefineTriagedHoudiniProxyMesehesToStaticMeshes( ComponentsToRefine, ComponentsToCook, SkippedComponents, bSilent, bRefineAll, bOnPreSaveWorld, OnPreSaveWorld, bOnPreBeginPIE ); } EHoudiniProxyRefineRequestResult FHoudiniEngineCommands::RefineHoudiniProxyMeshActorArrayToStaticMeshes(const TArray& InActorsToRefine, bool bSilent) { const bool bRefineAll = true; const bool bOnPreSaveWorld = false; UWorld* OnPreSaveWorld = nullptr; const bool bOnPreBeginPIE = false; // First find the components that have meshes that we must refine TArray ComponentsToRefine; TArray ComponentsToCook; // Components that would be candidates for refinement/cooking, but have errors TArray SkippedComponents; for (const AHoudiniAssetActor* HoudiniAssetActor : InActorsToRefine) { if (!HoudiniAssetActor || HoudiniAssetActor->IsPendingKill()) continue; UHoudiniAssetComponent* HoudiniAssetComponent = HoudiniAssetActor->GetHoudiniAssetComponent(); if (!HoudiniAssetComponent || HoudiniAssetComponent->IsPendingKill()) continue; // Check if we should consider this component for proxy mesh refinement or cooking, based on its settings and // flags passed to the function. TriageHoudiniAssetComponentsForProxyMeshRefinement(HoudiniAssetComponent, bRefineAll, bOnPreSaveWorld, OnPreSaveWorld, bOnPreBeginPIE, ComponentsToRefine, ComponentsToCook, SkippedComponents); } return RefineTriagedHoudiniProxyMesehesToStaticMeshes( ComponentsToRefine, ComponentsToCook, SkippedComponents, bSilent, bRefineAll, bOnPreSaveWorld, OnPreSaveWorld, bOnPreBeginPIE ); } void FHoudiniEngineCommands::StartPDGCommandlet() { FHoudiniEngine::Get().StartPDGCommandlet(); } void FHoudiniEngineCommands::StopPDGCommandlet() { FHoudiniEngine::Get().StopPDGCommandlet(); } bool FHoudiniEngineCommands::IsPDGCommandletRunningOrConnected() { return FHoudiniEngine::Get().IsPDGCommandletRunningOrConnected(); } bool FHoudiniEngineCommands::IsPDGCommandletEnabled() { const UHoudiniRuntimeSettings* const Settings = GetDefault(); if (IsValid(Settings)) { return Settings->bPDGAsyncCommandletImportEnabled; } return false; } bool FHoudiniEngineCommands::SetPDGCommandletEnabled(bool InEnabled) { UHoudiniRuntimeSettings* const Settings = GetMutableDefault(); if (IsValid(Settings)) { Settings->bPDGAsyncCommandletImportEnabled = InEnabled; return true; } return false; } void FHoudiniEngineCommands::TriageHoudiniAssetComponentsForProxyMeshRefinement(UHoudiniAssetComponent* InHAC, bool bRefineAll, bool bOnPreSaveWorld, UWorld *OnPreSaveWorld, bool bOnPreBeginPIE, TArray &OutToRefine, TArray &OutToCook, TArray &OutSkipped) { if (!InHAC || InHAC->IsPendingKill()) return; // Make sure that the component's World and Owner are valid AActor *Owner = InHAC->GetOwner(); if (!Owner || Owner->IsPendingKill()) return; UWorld *World = InHAC->GetWorld(); if (!World || World->IsPendingKill()) return; if (bOnPreSaveWorld && OnPreSaveWorld && OnPreSaveWorld != World) return; // Check if we should consider this component for proxy mesh refinement based on its settings and // flags passed to the function if (bRefineAll || (bOnPreSaveWorld && InHAC->IsProxyStaticMeshRefinementOnPreSaveWorldEnabled()) || (bOnPreBeginPIE && InHAC->IsProxyStaticMeshRefinementOnPreBeginPIEEnabled())) { TArray ProxyMeshPackagesToSave; TArray ComponentsWithProxiesToSave; if (InHAC->HasAnyCurrentProxyOutput()) { // Get the state of the asset and check if it is cooked // If it is not cook, request a cook. We can only build the UStaticMesh // if the data from the cook is available // If the state is not pre-cook, or None (cooked), then the state is invalid, // log an error and skip the component bool bNeedsRebuildOrDelete = false; bool bUnsupportedState = false; const bool bCookedDataAvailable = InHAC->IsHoudiniCookedDataAvailable(bNeedsRebuildOrDelete, bUnsupportedState); if (bCookedDataAvailable) { OutToRefine.Add(InHAC); ComponentsWithProxiesToSave.Add(InHAC); } else if (!bUnsupportedState && !bNeedsRebuildOrDelete) { InHAC->MarkAsNeedCook(); // Force the output of the cook to be directly created as a UStaticMesh and not a proxy InHAC->SetNoProxyMeshNextCookRequested(true); OutToCook.Add(InHAC); ComponentsWithProxiesToSave.Add(InHAC); } else { OutSkipped.Add(InHAC); const EHoudiniAssetState AssetState = InHAC->GetAssetState(); HOUDINI_LOG_ERROR(TEXT("Could not refine %s, the asset is in an unsupported state: %s"), *(InHAC->GetPathName()), *(UEnum::GetValueAsString(AssetState))); } } else if (InHAC->HasAnyProxyOutput()) { // If the HAC has non-current proxies, destroy them // TODO: Make this its own command? const uint32 NumOutputs = InHAC->GetNumOutputs(); for (uint32 Index = 0; Index < NumOutputs; ++Index) { UHoudiniOutput *Output = InHAC->GetOutputAt(Index); if (!Output || Output->IsPendingKill()) continue; TMap& OutputObjects = Output->GetOutputObjects(); for (auto& CurrentPair : OutputObjects) { FHoudiniOutputObject& CurrentOutputObject = CurrentPair.Value; if (!CurrentOutputObject.bProxyIsCurrent) { // The proxy is not current, delete it and its component USceneComponent* FoundProxyComponent = Cast(CurrentOutputObject.ProxyComponent); if (FoundProxyComponent && !FoundProxyComponent->IsPendingKill()) { // Remove from the HoudiniAssetActor if (FoundProxyComponent->GetOwner()) FoundProxyComponent->GetOwner()->RemoveOwnedComponent(FoundProxyComponent); FoundProxyComponent->DetachFromComponent(FDetachmentTransformRules::KeepRelativeTransform); FoundProxyComponent->UnregisterComponent(); FoundProxyComponent->DestroyComponent(); } UObject* ProxyObject = CurrentOutputObject.ProxyObject; if (!ProxyObject || ProxyObject->IsPendingKill()) continue; ProxyObject->MarkPendingKill(); ProxyObject->MarkPackageDirty(); UPackage* const Package = ProxyObject->GetOutermost();//GetPackage(); if (IsValid(Package)) ProxyMeshPackagesToSave.Add(Package); } } } } for (UHoudiniAssetComponent* const HAC : ComponentsWithProxiesToSave) { const uint32 NumOutputs = HAC->GetNumOutputs(); for (uint32 Index = 0; Index < NumOutputs; ++Index) { UHoudiniOutput *Output = HAC->GetOutputAt(Index); if (!Output || Output->IsPendingKill()) continue; TMap& OutputObjects = Output->GetOutputObjects(); for (auto& CurrentPair : OutputObjects) { FHoudiniOutputObject& CurrentOutputObject = CurrentPair.Value; if (CurrentOutputObject.bProxyIsCurrent && CurrentOutputObject.ProxyObject) { UPackage* const Package = CurrentOutputObject.ProxyObject->GetOutermost();//GetPackage(); if (IsValid(Package) && Package->IsDirty()) ProxyMeshPackagesToSave.Add(Package); } } } } if (ProxyMeshPackagesToSave.Num() > 0) { TryCollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); FEditorFileUtils::PromptForCheckoutAndSave(ProxyMeshPackagesToSave, true, false); } } } EHoudiniProxyRefineRequestResult FHoudiniEngineCommands::RefineTriagedHoudiniProxyMesehesToStaticMeshes( const TArray& InComponentsToRefine, const TArray& InComponentsToCook, const TArray& InSkippedComponents, bool bInSilent, bool bInRefineAll, bool bInOnPreSaveWorld, UWorld* InOnPreSaveWorld, bool bInOnPrePIEBeginPlay) { // Slate notification text FString Notification = TEXT("Refining Houdini proxy meshes to static meshes..."); const uint32 NumComponentsToCook = InComponentsToCook.Num(); const uint32 NumComponentsToRefine = InComponentsToRefine.Num(); const uint32 NumComponentsToProcess = NumComponentsToCook + NumComponentsToRefine; TArray SuccessfulComponents; TArray FailedComponents; TArray SkippedComponents(InSkippedComponents); if (NumComponentsToProcess > 0) { // The task progress pointer is potentially going to be shared with a background thread and tasks // on the main thread, so make it thread safe TSharedPtr TaskProgress = MakeShareable(new FSlowTask((float)NumComponentsToProcess, FText::FromString(Notification))); TaskProgress->Initialize(); if (!bInSilent) TaskProgress->MakeDialog(/*bShowCancelButton=*/true); // Iterate over the components for which we can build UStaticMesh, and build the meshes bool bCancelled = false; for (uint32 ComponentIndex = 0; ComponentIndex < NumComponentsToRefine; ++ComponentIndex) { UHoudiniAssetComponent* HoudiniAssetComponent = InComponentsToRefine[ComponentIndex]; TaskProgress->EnterProgressFrame(1.0f); const bool bDestroyProxies = true; FHoudiniOutputTranslator::BuildStaticMeshesOnHoudiniProxyMeshOutputs(HoudiniAssetComponent, bDestroyProxies); SuccessfulComponents.Add(HoudiniAssetComponent); bCancelled = TaskProgress->ShouldCancel(); if (bCancelled) { for (uint32 SkippedIndex = ComponentIndex + 1; SkippedIndex < NumComponentsToRefine; ++SkippedIndex) { SkippedComponents.Add(InComponentsToRefine[ComponentIndex]); } break; } } if (bCancelled && NumComponentsToCook > 0) { for (UHoudiniAssetComponent* const HAC : InComponentsToCook) { SkippedComponents.Add(HAC); } } if (NumComponentsToCook > 0 && !bCancelled) { // Now use an async task to check on the progress of the cooking components Async(EAsyncExecution::Thread, [InComponentsToCook, TaskProgress, NumComponentsToProcess, bInOnPreSaveWorld, InOnPreSaveWorld, SuccessfulComponents, FailedComponents, SkippedComponents]() { RefineHoudiniProxyMeshesToStaticMeshesWithCookInBackgroundThread( InComponentsToCook, TaskProgress, NumComponentsToProcess, bInOnPreSaveWorld, InOnPreSaveWorld, SuccessfulComponents, FailedComponents, SkippedComponents); }); // We have to wait for cook(s) before completing refinement return EHoudiniProxyRefineRequestResult::PendingCooks; } else { RefineHoudiniProxyMeshesToStaticMeshesNotifyDone( NumComponentsToProcess, TaskProgress.Get(), bCancelled, bInOnPreSaveWorld, InOnPreSaveWorld, SuccessfulComponents, FailedComponents, SkippedComponents); // We didn't have to cook anything, so refinement is complete. return EHoudiniProxyRefineRequestResult::Refined; } } // Nothing to refine return EHoudiniProxyRefineRequestResult::None; } void FHoudiniEngineCommands::RefineHoudiniProxyMeshesToStaticMeshesWithCookInBackgroundThread(const TArray& InComponentsToCook, TSharedPtr InTaskProgress, const uint32 InNumComponentsToProcess, bool bInOnPreSaveWorld, UWorld *InOnPreSaveWorld, const TArray &InSuccessfulComponents, const TArray &InFailedComponents, const TArray &InSkippedComponents) { // Copy to a double linked list so that we can loop through // to check progress of each component and remove it easily // if it has completed/failed TDoubleLinkedList CookList; for (UHoudiniAssetComponent *HAC : InComponentsToCook) { CookList.AddTail(HAC); } // Add the successfully cooked components to the incoming successful components (previously refined) TArray SuccessfulComponents(InSuccessfulComponents); TArray FailedComponents(InFailedComponents); TArray SkippedComponents(InSkippedComponents); bool bCancelled = false; uint32 NumFailedToCook = 0; while (CookList.Num() > 0 && !bCancelled) { TDoubleLinkedList::TDoubleLinkedListNode *Node = CookList.GetHead(); while (Node && !bCancelled) { TDoubleLinkedList::TDoubleLinkedListNode *Next = Node->GetNextNode(); UHoudiniAssetComponent* HAC = Node->GetValue(); if (HAC && !HAC->IsPendingKill()) { const EHoudiniAssetState State = HAC->GetAssetState(); const EHoudiniAssetStateResult ResultState = HAC->GetAssetStateResult(); bool bUpdateProgress = false; if (State == EHoudiniAssetState::None) { // Cooked, count as success, remove node CookList.RemoveNode(Node); SuccessfulComponents.Add(HAC); bUpdateProgress = true; } else if (ResultState != EHoudiniAssetStateResult::None && ResultState != EHoudiniAssetStateResult::Working) { // Failed, remove node HOUDINI_LOG_ERROR(TEXT("Failed to cook %s to obtain static mesh."), *(HAC->GetPathName())); CookList.RemoveNode(Node); FailedComponents.Add(HAC); bUpdateProgress = true; NumFailedToCook++; } if (bUpdateProgress && InTaskProgress.IsValid()) { // Update progress only on the main thread, and check for cancellation request bCancelled = Async(EAsyncExecution::TaskGraphMainThread, [InTaskProgress]() { InTaskProgress->EnterProgressFrame(1.0f); return InTaskProgress->ShouldCancel(); }).Get(); } } else { SkippedComponents.Add(HAC); CookList.RemoveNode(Node); } Node = Next; } FPlatformProcess::Sleep(0.01f); } if (bCancelled) { HOUDINI_LOG_WARNING(TEXT("Mesh refinement cancelled while waiting for %d components to cook."), CookList.Num()); // Mark any remaining HACs in the cook list as skipped TDoubleLinkedList::TDoubleLinkedListNode* Node = CookList.GetHead(); while (Node) { TDoubleLinkedList::TDoubleLinkedListNode* const Next = Node->GetNextNode(); UHoudiniAssetComponent* HAC = Node->GetValue(); if (HAC) SkippedComponents.Add(HAC); CookList.RemoveNode(Node); Node = Next; } } // Cooking is done, or failed, display the notifications on the main thread Async(EAsyncExecution::TaskGraphMainThread, [InNumComponentsToProcess, InTaskProgress, bCancelled, bInOnPreSaveWorld, InOnPreSaveWorld, SuccessfulComponents, FailedComponents, SkippedComponents]() { RefineHoudiniProxyMeshesToStaticMeshesNotifyDone(InNumComponentsToProcess, InTaskProgress.Get(), bCancelled, bInOnPreSaveWorld, InOnPreSaveWorld, SuccessfulComponents, FailedComponents, SkippedComponents); }); } void FHoudiniEngineCommands::RefineHoudiniProxyMeshesToStaticMeshesNotifyDone(const uint32 InNumTotalComponents, FSlowTask* const InTaskProgress, const bool bCancelled, const bool bOnPreSaveWorld, UWorld* const InOnPreSaveWorld, const TArray &InSuccessfulComponents, const TArray &InFailedComponents, const TArray &InSkippedComponents) { FString Notification; const uint32 NumSkippedComponents = InSkippedComponents.Num(); const uint32 NumFailedToCook = InFailedComponents.Num(); if (NumSkippedComponents + NumFailedToCook > 0) { if (bCancelled) { Notification = FString::Printf(TEXT("Refinement cancelled after completing %d / %d components. The remaining components were skipped, in an invalid state, or could not be cooked. See the log for details."), NumSkippedComponents + NumFailedToCook, InNumTotalComponents); } else { Notification = FString::Printf(TEXT("Failed to refine %d / %d components, the components were in an invalid state, and were either not cooked or could not be cooked. See the log for details."), NumSkippedComponents + NumFailedToCook, InNumTotalComponents); } FHoudiniEngineUtils::CreateSlateNotification(Notification); HOUDINI_LOG_ERROR(TEXT("%s"), *Notification); } else if (InNumTotalComponents > 0) { Notification = TEXT("Done: Refining Houdini proxy meshes to static meshes."); // FHoudiniEngineUtils::CreateSlateNotification(Notification); HOUDINI_LOG_MESSAGE(TEXT("%s"), *Notification); } if (InTaskProgress) { InTaskProgress->Destroy(); } if (bOnPreSaveWorld && InSuccessfulComponents.Num() > 0) { FDelegateHandle& OnPostSaveWorldHandle = FHoudiniEngineCommands::GetOnPostSaveWorldRefineProxyMeshesHandle(); if (OnPostSaveWorldHandle.IsValid()) { if (FEditorDelegates::PostSaveWorld.Remove(OnPostSaveWorldHandle)) OnPostSaveWorldHandle.Reset(); } // Save the dirty static meshes in InSuccessfulComponents OnPostSaveWorld // TODO: Remove? This may not be necessary now as we save all dirty temporary cook data in PostSaveWorld() already (Static Meshes, Materials...) OnPostSaveWorldHandle = FEditorDelegates::PostSaveWorld.AddLambda([InSuccessfulComponents, bOnPreSaveWorld, InOnPreSaveWorld](uint32 InSaveFlags, UWorld* InWorld, bool bInSuccess) { if (bOnPreSaveWorld && InOnPreSaveWorld && InOnPreSaveWorld != InWorld) return; RefineProxyMeshesHandleOnPostSaveWorld(InSuccessfulComponents, InSaveFlags, InWorld, bInSuccess); FDelegateHandle& OnPostSaveWorldHandle = FHoudiniEngineCommands::GetOnPostSaveWorldRefineProxyMeshesHandle(); if (OnPostSaveWorldHandle.IsValid()) { if (FEditorDelegates::PostSaveWorld.Remove(OnPostSaveWorldHandle)) OnPostSaveWorldHandle.Reset(); } }); } // Broadcast refinement result per HAC for (UHoudiniAssetComponent* const HAC : InSuccessfulComponents) { if (OnHoudiniProxyMeshesRefinedDelegate.IsBound()) OnHoudiniProxyMeshesRefinedDelegate.Broadcast(HAC, EHoudiniProxyRefineResult::Success); } for (UHoudiniAssetComponent* const HAC : InFailedComponents) { if (OnHoudiniProxyMeshesRefinedDelegate.IsBound()) OnHoudiniProxyMeshesRefinedDelegate.Broadcast(HAC, EHoudiniProxyRefineResult::Failed); } for (UHoudiniAssetComponent* const HAC : InSkippedComponents) { if (OnHoudiniProxyMeshesRefinedDelegate.IsBound()) OnHoudiniProxyMeshesRefinedDelegate.Broadcast(HAC, EHoudiniProxyRefineResult::Skipped); } } void FHoudiniEngineCommands::RefineProxyMeshesHandleOnPostSaveWorld(const TArray &InSuccessfulComponents, uint32 InSaveFlags, UWorld* InWorld, bool bInSuccess) { TArray PackagesToSave; for (UHoudiniAssetComponent* HAC : InSuccessfulComponents) { if (!HAC || HAC->IsPendingKill()) continue; const int32 NumOutputs = HAC->GetNumOutputs(); for (int32 Index = 0; Index < NumOutputs; ++Index) { UHoudiniOutput *Output = HAC->GetOutputAt(Index); if (!Output || Output->IsPendingKill()) continue; if (Output->GetType() != EHoudiniOutputType::Mesh) continue; for (auto &OutputObjectPair : Output->GetOutputObjects()) { UObject *Obj = OutputObjectPair.Value.OutputObject; if (!Obj || Obj->IsPendingKill()) continue; UStaticMesh *SM = Cast(Obj); if (!SM) continue; UPackage *Package = SM->GetOutermost(); if (!Package || Package->IsPendingKill()) continue; if (Package->IsDirty() && Package->IsFullyLoaded() && Package != GetTransientPackage()) { PackagesToSave.Add(Package); } } } } UEditorLoadingAndSavingUtils::SavePackages(PackagesToSave, true); } #undef LOCTEXT_NAMESPACE