Files

573 lines
22 KiB
C++

/*
* Copyright (c) <2017> Side Effects Software Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#include "HoudiniEngineCommandlet.h"
#include "HoudiniApi.h"
#include "HoudiniEngine.h"
#include "HoudiniEngineUtils.h"
#include "HoudiniEngineBakeUtils.h"
#include "HoudiniAssetComponent.h"
#include "HoudiniEngineRuntimePrivatePCH.h"
#include "CoreMinimal.h"
#include "Engine/StaticMesh.h"
#include "HAL/PlatformFilemanager.h"
#include "HAL/FileManagerGeneric.h"
const FString LocalAutoBakeFolder = TEXT("/HoudiniEngine/AutoBake/");
UHoudiniEngineConvertBgeoCommandlet::UHoudiniEngineConvertBgeoCommandlet()
{
IsClient = false;
IsEditor = true;
IsServer = false;
LogToConsole = true;
}
int32 UHoudiniEngineConvertBgeoCommandlet::Main( const FString& Params )
{
// Run me via UE4editor.exe my.uproject -run=HoudiniEngineConvertBgeo BGEO_IN (UASSET_OUT)
HOUDINI_LOG_MESSAGE( TEXT( "Houdini Engine Convert BGEO Commandlet" ) );
// Parse the params to a string arrays
TArray<FString> ArgumentsArray;
Params.ParseIntoArray(ArgumentsArray, TEXT(" "), true);
// We're expecting at least one param (the bgeo in) and a maximum of 2 params (bgeo in, uasset out)
// The first param is the Commandlet name, so ignore that
if ( ( ArgumentsArray.Num() < 2 ) || ( ArgumentsArray.Num() > 3 ) )
{
// Invalid number of arguments, Print usage and error out
HOUDINI_LOG_MESSAGE( TEXT( "HoudiniEngineConvertBgeoCommandlet" ) );
HOUDINI_LOG_MESSAGE( TEXT( "Converts a .bgeo file to Static Meshes .uasset files." ) );
HOUDINI_LOG_MESSAGE( TEXT( "Usage: -run=HoudiniEngineConvertBgeo BGEO_IN UASSET_OUT" ) );
HOUDINI_LOG_MESSAGE( TEXT( "BGEO_IN" ) );
HOUDINI_LOG_MESSAGE( TEXT( "\tPath to the the source .bgeo file to convert." ) );
HOUDINI_LOG_MESSAGE( TEXT( "UASSET_OUT (optional)" ) );
HOUDINI_LOG_MESSAGE( TEXT( "\tPath for the converted uasset file. If not present, the directory/name of the bgeo file will be used" ) );
return 1;
}
FString BGEOFilePath = ArgumentsArray[ 1 ];
FString UASSETFilePath = ArgumentsArray.Num() > 2 ? ArgumentsArray[ 2 ] : FString();
if ( !FHoudiniCommandletUtils::ConvertBGEOFileToUAsset( BGEOFilePath, UASSETFilePath ) )
return 1;
return 0;
}
UHoudiniEngineConvertBgeoDirCommandlet::UHoudiniEngineConvertBgeoDirCommandlet()
{
IsClient = false;
IsEditor = true;
IsServer = false;
LogToConsole = true;
}
int32 UHoudiniEngineConvertBgeoDirCommandlet::Main(const FString& Params)
{
// Run me via UE4editor.exe my.uproject -run=HoudiniEngineConvertBgeoDir BGEO_DIR_IN (UASSET_DIR_OUT)
HOUDINI_LOG_MESSAGE(TEXT("Houdini Engine Convert BGEO directory"));
// Parse the params to a string arrays
TArray<FString> ArgumentsArray;
Params.ParseIntoArray(ArgumentsArray, TEXT(" "), true);
// We're expecting at least one param (the bgeo dir in) and a maximum of 3 params (bgeo dir in, uasset dir out, timeout)
// The first param is the Commandlet name, so ignore that
if ( (ArgumentsArray.Num() < 1 ) || ( ArgumentsArray.Num() > 3 ) )
{
// Invalid number of arguments, Print usage and error out
HOUDINI_LOG_MESSAGE(TEXT("HoudiniEngineTestCommandlet:"));
HOUDINI_LOG_MESSAGE(TEXT("Converts .bgeo files in directory to Static Meshes .uasset files in a Out directory."));
HOUDINI_LOG_MESSAGE(TEXT("Usage: -run=HoudiniEngineConvertBgeoDir BGEO_DIR_IN UASSET_DIR_OUT TIMEOUT"));
HOUDINI_LOG_MESSAGE(TEXT("BGEO_DIR_IN"));
HOUDINI_LOG_MESSAGE(TEXT("\tPath to a directory containing the .bgeo files to convert."));
HOUDINI_LOG_MESSAGE(TEXT("UASSET_DIR_OUT (optional)"));
HOUDINI_LOG_MESSAGE(TEXT("\tPath for the converted uasset files."));
HOUDINI_LOG_MESSAGE(TEXT("TIMEOUT (optional)"));
HOUDINI_LOG_MESSAGE(TEXT("\tAfter this amount of time of inactivity, the commandlet will exit."));
return 1;
}
FString BGEODirPath = ArgumentsArray[ 0 ];
FString UASSETDirPath = ArgumentsArray.Num() > 1 ? ArgumentsArray[ 1 ] : BGEODirPath;
FString InactivityTimeOutStr = ArgumentsArray.Num() > 3 ? ArgumentsArray[ 2 ] : FString();
float InactivityTimeOut = 1000.0f;
if ( InactivityTimeOutStr.IsNumeric() )
InactivityTimeOut = FCString::Atof( *InactivityTimeOutStr );
// First check the source directory is valid
if ( !FPaths::DirectoryExists( BGEODirPath ) )
{
// Cant find input BGEO dir
HOUDINI_LOG_ERROR( TEXT( "The source BGEO directory does not exist: %s" ), *BGEODirPath );
return false;
}
// Then the output directory
if ( !FPaths::DirectoryExists( UASSETDirPath ) )
{
// Cant find Output dir
HOUDINI_LOG_ERROR( TEXT( "The output UASSET directory does not exist: %s" ), *UASSETDirPath );
return false;
}
HOUDINI_LOG_MESSAGE( TEXT( "Looking for .bgeo files in %s ." ), *BGEODirPath );
// Sleep time in seconds before listing the files again
const float SleepTime = 1.0f;
// Maximum number of conversion for a file
const int32 NumConvertAttempts = 2;
// If true, will literally try to erase all files before overwriting them
// Will also empty the "local" directory
const bool CrushFiles = true;
// If true, source bgeo files will be deleted upon conversion
const bool DeleteFileAfterConversion = false;
if ( CrushFiles )
{
// Nuke everything in our temporary bake folder
FFileManagerGeneric::Get().DeleteDirectory( *LocalAutoBakeFolder, false, true );
}
// Map tracking the number of failures for a given file
TMap< FString, int32 > FailingFileMap;
// M<ap listing the files that couldn't/weren't removed
TArray< FString > UndeletedFileMap;
bool KeepLookingForFile = true;
float currentInactivity = 0.0f;
while ( KeepLookingForFile )
{
// List all the .bgeo files in the directory
TArray< FString > CurrentFileList;
FFileManagerGeneric::Get().FindFiles( CurrentFileList, *BGEODirPath, TEXT( ".bgeo" ) );
// Convert the files we found
int32 ConversionCount = 0;
for ( int32 n = CurrentFileList.Num() - 1; n >= 0; n-- )
{
FString CurrentFile = CurrentFileList[ n ];
// Skip undeleted files
if ( UndeletedFileMap.Contains( CurrentFile ) )
continue;
// Skip failing files
if ( FailingFileMap.Contains(CurrentFile) && FailingFileMap[ CurrentFile ] >= NumConvertAttempts )
continue;
// Build the in / out file names
FString BGEOFile = BGEODirPath + TEXT("/") + CurrentFileList[ n ];
FString UASSETFile = UASSETDirPath + TEXT("/") + CurrentFileList[ n ].LeftChop(5) + TEXT(".uasset");
if ( CrushFiles )
{
if ( FFileManagerGeneric::Get().FileExists( *UASSETFile ) )
{
// Erase the file if it already exists!
FFileManagerGeneric::Get().Delete( *UASSETFile, false, true, true );
}
}
// Attempting to convert the file
ConversionCount++;
if ( !FHoudiniCommandletUtils::ConvertBGEOFileToUAsset( BGEOFile, UASSETFile ) )
{
if ( FailingFileMap.Contains( CurrentFile ) )
FailingFileMap[ CurrentFile ]++;
else
FailingFileMap.Add( CurrentFile, 1 );
continue;
}
HOUDINI_LOG_MESSAGE(TEXT("Successfully converted BGEO file: %s to %s"), *BGEOFile, *UASSETFile );
// Delete the source BGEO
if ( !DeleteFileAfterConversion || !FFileManagerGeneric::Get().Delete( *BGEOFile, false, true, true ) )
UndeletedFileMap.Add( CurrentFile );
}
// Update the inactivity counter
if ( ConversionCount == 0 )
{
currentInactivity += SleepTime;
if ( (InactivityTimeOut > 0.0f ) && ( currentInactivity > InactivityTimeOut ) )
KeepLookingForFile = false;
}
else
{
// reset the inactivity counter
currentInactivity = 0.0f;
}
if ( CrushFiles )
{
// Nuke everything in our temporary bake folder
FFileManagerGeneric::Get().DeleteDirectory(*LocalAutoBakeFolder, false, true);
}
// Go to bed for a while...
FPlatformProcess::Sleep( SleepTime );
}
return 0;
}
bool FHoudiniCommandletUtils::ConvertBGEOFileToUAsset( const FString& InBGEOFilePath, const FString& OutUAssetFilePath )
{
#if WITH_EDITOR
//---------------------------------------------------------------------------------------------
// 1. Handle the BGEO param
//---------------------------------------------------------------------------------------------
FString BGEOFilePath = InBGEOFilePath;
if ( !FPaths::FileExists( BGEOFilePath ) )
{
// Cant find BGEO file
HOUDINI_LOG_ERROR( TEXT( "BGEO file %s could not be found!" ), *BGEOFilePath );
return false;
}
// Make sure we're using absolute path!
BGEOFilePath = FPaths::ConvertRelativePathToFull( BGEOFilePath );
// Split the file path
FString BGEOPath, BGEOFileName, BGEOExtension;
FPaths::Split( BGEOFilePath, BGEOPath, BGEOFileName, BGEOExtension );
if ( BGEOExtension.IsEmpty() )
BGEOExtension = TEXT("bgeo");
if ( BGEOExtension.Compare( TEXT("bgeo"), ESearchCase::IgnoreCase ) != 0 )
{
// Not a bgeo file!
HOUDINI_LOG_ERROR( TEXT( "First argument %s is not a .bgeo FILE!"), *BGEOFilePath );
return false;
}
BGEOFilePath = BGEOPath + TEXT("/") + BGEOFileName + TEXT(".") + BGEOExtension;
//---------------------------------------------------------------------------------------------
// 2. Handle the UASSET param
//---------------------------------------------------------------------------------------------
FString UASSETFilePath = OutUAssetFilePath;
if ( UASSETFilePath.IsEmpty() )
{
// No OUT parameter, build it from the bgeo
UASSETFilePath = BGEOPath + TEXT("/") + BGEOFileName + TEXT(".uasset");
HOUDINI_LOG_MESSAGE( TEXT( "UASSET_OUT argument missing! Will use %s.") , *UASSETFilePath);
}
// Make sure we're using absolute path!
UASSETFilePath = FPaths::ConvertRelativePathToFull( UASSETFilePath );
if ( FPaths::FileExists( UASSETFilePath ) )
{
// UAsset already exists, overwrite?
HOUDINI_LOG_MESSAGE( TEXT( "UASSET file : %s already exists, overwriting!"), *UASSETFilePath );
}
// Split the file path
FString UASSETPath, UASSETFileName, UASSETExtension;
FPaths::Split( UASSETFilePath, UASSETPath, UASSETFileName, UASSETExtension );
UASSETFilePath = UASSETPath + TEXT("/") + UASSETFileName + TEXT(".") + UASSETExtension;
HOUDINI_LOG_MESSAGE( TEXT( "Converting BGEO file: %s to UASSET file : %s" ), *BGEOFilePath, *UASSETFilePath );
// ERROR Lambda
// Saves a debug HIP file through HEngine and returns an error (in case of HAPI/HEngine issue).
auto SaveDebugHipFileAndReturnError = [&]()
{
FString HipFileName = BGEOPath + BGEOFileName + TEXT(".hip");
std::string HipFileNameStr = TCHAR_TO_ANSI(*HipFileName);
FHoudiniApi::SaveHIPFile(FHoudiniEngine::Get().GetSession(), HipFileNameStr.c_str(), false);
return false;
};
//---------------------------------------------------------------------------------------------
// 3. Load the bgeo file in HAPI
//---------------------------------------------------------------------------------------------
HAPI_NodeId NodeId = -1;
if ( !FHoudiniCommandletUtils::LoadBGEOFileInHAPI( BGEOFilePath, NodeId) )
return SaveDebugHipFileAndReturnError();
//---------------------------------------------------------------------------------------------
// 4. Create a package for the result
//---------------------------------------------------------------------------------------------
FString PackagePath = LocalAutoBakeFolder + UASSETFileName;
FString PackageFilePath = UPackageTools::SanitizePackageName( PackagePath );
UPackage * Package = FindPackage( nullptr, *PackageFilePath );
if ( !Package )
{
// Create actual package.
Package = CreatePackage( nullptr, *PackageFilePath );
if ( !Package )
{
// Couldn't create the package
HOUDINI_LOG_ERROR( TEXT( "Could not create local Package for the UASSET !!!" ) );
return false;
}
}
else
{
// Package already exists, overwrite?
HOUDINI_LOG_MESSAGE( TEXT( "Package %s already exists, overwriting!" ), *PackageFilePath );
}
// ERROR Lambda
// Tries to delete the local package and creates an error ( in case of UE4/Package error)
auto DeleteLocalPackageAndReturnError = [&]()
{
FFileManagerGeneric::Get().Delete( *PackageFilePath, false, true, true );
return SaveDebugHipFileAndReturnError();
};
//---------------------------------------------------------------------------------------------
// 5. Create the Static Meshes from the result of the bgeo cooking
//---------------------------------------------------------------------------------------------
TMap< FHoudiniGeoPartObject, UStaticMesh * > StaticMeshesOut;
if ( !FHoudiniCommandletUtils::CreateStaticMeshes(
BGEOFileName, NodeId, Package, StaticMeshesOut ) )
{
// There was some cook errors
HOUDINI_LOG_ERROR(TEXT("Could not create Static Meshes from the bgeo!!!"));
return SaveDebugHipFileAndReturnError();
}
//---------------------------------------------------------------------------------------------
// 6. Bake the resulting Static Meshes to the package
//---------------------------------------------------------------------------------------------
if ( !FHoudiniCommandletUtils::BakeStaticMeshesToPackage( BGEOFileName, StaticMeshesOut, Package ) )
return SaveDebugHipFileAndReturnError();
//---------------------------------------------------------------------------------------------
// 7. Delete the node in Houdini
//---------------------------------------------------------------------------------------------
if ( HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( FHoudiniEngine::Get().GetSession(), NodeId ) )
{
// Could not delete the bgeo's file sop !
HOUDINI_LOG_WARNING( TEXT( "Could not delete the bgeo file sop for %s"), * BGEOFileName );
}
//---------------------------------------------------------------------------------------------
// 8. Save the package
//---------------------------------------------------------------------------------------------
Package->SetDirtyFlag( true );
Package->FullyLoad();
FString LocalPackageFileName = FPackageName::LongPackageNameToFilename( PackageFilePath, FPackageName::GetAssetPackageExtension() );
if (!UPackage::SavePackage(Package, NULL, RF_Standalone, *LocalPackageFileName, GError, nullptr, false, true, SAVE_NoError))
{
// There was some cook errors
HOUDINI_LOG_ERROR( TEXT( "Could not save the local package %s"), *PackageFilePath );
return DeleteLocalPackageAndReturnError();
}
//---------------------------------------------------------------------------------------------
// 9. Move the local package to its final destination
//---------------------------------------------------------------------------------------------
LocalPackageFileName = FPaths::ConvertRelativePathToFull( LocalPackageFileName );
if (!FFileManagerGeneric::Get().Move(*UASSETFilePath, *LocalPackageFileName, true, true, false, false ) )
{
HOUDINI_LOG_ERROR( TEXT("Could not move local package %s to %s"), *LocalPackageFileName, *UASSETFilePath);
return DeleteLocalPackageAndReturnError();
}
#endif
return true;
}
bool FHoudiniCommandletUtils::LoadBGEOFileInHAPI( const FString& InputFilePath, HAPI_NodeId& NodeId )
{
NodeId = -1;
// Check HoudiniEngine / HAPI init?
if ( !FHoudiniEngine::IsInitialized() )
{
HOUDINI_LOG_ERROR( TEXT( "Couldn't initialize HoudiniEngine!") );
return false;
}
// Create a file SOP
HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::CreateNode(
FHoudiniEngine::Get().GetSession(), -1,
"SOP/file", "bgeo", true, &NodeId ), false );
// Set the file path parameter
HAPI_ParmId ParmId = -1;
HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetParmIdFromName(
FHoudiniEngine::Get().GetSession(),
NodeId, "file", &ParmId), false );
std::string ConvertedString = TCHAR_TO_UTF8( *InputFilePath );
HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::SetParmStringValue(
FHoudiniEngine::Get().GetSession(), NodeId, ConvertedString.c_str(), ParmId, 0 ), false );
// Cook the node
HAPI_CookOptions CookOptions;
FHoudiniApi::CookOptions_Init(&CookOptions);
//FMemory::Memzero< HAPI_CookOptions >( CookOptions );
CookOptions.curveRefineLOD = 8.0f;
CookOptions.clearErrorsAndWarnings = false;
CookOptions.maxVerticesPerPrimitive = 3;
CookOptions.splitGeosByGroup = false;
CookOptions.splitGeosByAttribute = false;
CookOptions.splitAttrSH = 0;
CookOptions.refineCurveToLinear = true;
CookOptions.handleBoxPartTypes = false;
CookOptions.handleSpherePartTypes = false;
CookOptions.splitPointsByVertexAttributes = false;
CookOptions.packedPrimInstancingMode = HAPI_PACKEDPRIM_INSTANCING_MODE_FLAT;
HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::CookNode(
FHoudiniEngine::Get().GetSession(), NodeId, &CookOptions ), false );
// Wait for the cook to finish
int status = HAPI_STATE_MAX_READY_STATE + 1;
while ( status > HAPI_STATE_MAX_READY_STATE )
{
// Retrieve the status
HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetStatus(
FHoudiniEngine::Get().GetSession(),
HAPI_STATUS_COOK_STATE, &status ), false );
FString StatusString = FHoudiniEngineUtils::GetStatusString( HAPI_STATUS_COOK_STATE, HAPI_STATUSVERBOSITY_ERRORS );
HOUDINI_LOG_MESSAGE( TEXT( "Still Cooking, current status: %s." ), *StatusString );
// Go to bed..
if ( status > HAPI_STATE_MAX_READY_STATE )
FPlatformProcess::Sleep( 0.5f );
}
if ( status != HAPI_STATE_READY )
{
// There was some cook errors
HOUDINI_LOG_ERROR( TEXT("Finished Cooking with errors!") );
return false;
}
HOUDINI_LOG_MESSAGE( TEXT( "Finished Cooking!" ) );
return true;
}
bool FHoudiniCommandletUtils::CreateStaticMeshes(
const FString& InputName, HAPI_NodeId& NodeId, UPackage* OuterPackage,
TMap<FHoudiniGeoPartObject, UStaticMesh *>& StaticMeshesOut )
{
if (!OuterPackage || OuterPackage->IsPendingKill())
return false;
// Create a "fake" HoudiniAssetComponent
FName HACName( *InputName );
UHoudiniAssetComponent* HoudiniAssetComponent = NewObject< UHoudiniAssetComponent >( OuterPackage, HACName );
// Create the CookParams
FHoudiniCookParams HoudiniCookParams( HoudiniAssetComponent );
HoudiniCookParams.StaticMeshBakeMode = EBakeMode::CookToTemp; // EBakeMode::CreateNewAssets?
HoudiniCookParams.MaterialAndTextureBakeMode = EBakeMode::CookToTemp; // FHoudiniCookParams::GetDefaultMaterialAndTextureCookMode()?
FTransform ComponentTransform;
TMap< FHoudiniGeoPartObject, UStaticMesh * > StaticMeshesIn;
//TMap< FHoudiniGeoPartObject, UStaticMesh * > StaticMeshesOut;
// Create the Static Meshes from the cook result
bool ProcessResult = FHoudiniEngineUtils::CreateStaticMeshesFromHoudiniAsset(
NodeId, HoudiniCookParams, true, true,
StaticMeshesIn, StaticMeshesOut, ComponentTransform );
if ( !ProcessResult || StaticMeshesOut.Num() <= 0 )
{
// There was some cook errors
HOUDINI_LOG_ERROR( TEXT( "Could not create Static Meshes from the bgeo!!!" ) );
return false;
}
return true;
}
bool FHoudiniCommandletUtils::BakeStaticMeshesToPackage(
const FString& InputName, TMap<FHoudiniGeoPartObject, UStaticMesh *>& StaticMeshes, UPackage* OutPackage )
{
bool BakedSomething = false;
for ( TMap< FHoudiniGeoPartObject, UStaticMesh * >::TIterator Iter( StaticMeshes ); Iter; ++Iter )
{
// Get the Static Mesh
const FHoudiniGeoPartObject HoudiniGeoPartObject = Iter.Key();
UStaticMesh* CurrentSM = Iter.Value();
if ( !CurrentSM )
continue;
// Build a name for this Static Mesh
FName StaticMeshName = FName( *( InputName + CurrentSM->GetName() ) );
// Duplicate the Static MEsh to copy it to the package.
UStaticMesh* DuplicatedStaticMesh = DuplicateObject< UStaticMesh >( CurrentSM, OutPackage, StaticMeshName );
if ( !DuplicatedStaticMesh )
continue;
// Set the proper flags on the duplicated mesh
DuplicatedStaticMesh->SetFlags( RF_Public | RF_Standalone );
// Notify the registry that we have created a new duplicate mesh.
FAssetRegistryModule::AssetCreated( DuplicatedStaticMesh );
// Dirty the static mesh package.
DuplicatedStaticMesh->MarkPackageDirty();
BakedSomething = true;
}
if ( !BakedSomething )
{
// There was some cook errors
HOUDINI_LOG_ERROR( TEXT( "Could not create Static Meshes from the bgeo!!!" ) );
return false;
}
return true;
}