1026 lines
41 KiB
C++
1026 lines
41 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "MuCOE/CustomizableObjectInstanceBakingUtils.h"
|
|
|
|
#include "MuCOE/CustomizableObjectEditor.h"
|
|
#include "MuCOE/CustomizableObjectEditorLogger.h"
|
|
|
|
#include "Misc/MessageDialog.h"
|
|
#include "FileHelpers.h"
|
|
#include "UnrealBakeHelpers.h"
|
|
#include "Animation/Skeleton.h"
|
|
#include "Engine/SkeletalMesh.h"
|
|
#include "Engine/SkinnedAssetCommon.h"
|
|
#include "Materials/MaterialInstanceConstant.h"
|
|
#include "Materials/MaterialInstanceDynamic.h"
|
|
#include "Materials/Material.h"
|
|
#include "PhysicsEngine/PhysicsAsset.h"
|
|
|
|
#include "MuCO/CustomizableObject.h"
|
|
#include "MuCO/CustomizableObjectInstanceAssetUserData.h"
|
|
#include "MuCO/CustomizableObjectInstancePrivate.h"
|
|
#include "MuCO/CustomizableObjectMipDataProvider.h"
|
|
#include "MuT/UnrealPixelFormatOverride.h"
|
|
#include "Rendering/SkeletalMeshModel.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "CustomizableObjectEditor"
|
|
|
|
|
|
/**
|
|
* Remove the prefix from the provided string.
|
|
* @param InOutString The string to update
|
|
* @param ToCheckPrefixes The list of possible prefixes that, if present, will get removed from the target string
|
|
*/
|
|
void RemovePrefix(FString& InOutString, const TArray<FString>& ToCheckPrefixes)
|
|
{
|
|
for (const FString& PrefixToRemove : ToCheckPrefixes)
|
|
{
|
|
if (InOutString.Find(PrefixToRemove, ESearchCase::CaseSensitive) == 0)
|
|
{
|
|
InOutString = InOutString.RightChop(PrefixToRemove.Len());
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Compose the resource name to be used by a resource.
|
|
* @param InPrefix The prefix we want to use for a given resource.
|
|
* @param InObjectName The name of the object this resource is part of.
|
|
* @param InOutResourceName The name of the resource itself. This string will be the target of the concatenations.
|
|
*/
|
|
void ComposeResourceName( const FString& InPrefix, const FString& InObjectName, FString& InOutResourceName)
|
|
{
|
|
InOutResourceName = InPrefix + InObjectName + InOutResourceName;
|
|
}
|
|
|
|
|
|
/**
|
|
* Simple wrapper to be able to invoke the generation of a popup or log message depending on the execution context in which this code is being ran
|
|
* @param InMessage The message to display
|
|
* @param InTitle The title to be used for the popup or the log generated
|
|
*/
|
|
void ShowErrorNotification(const FText& InMessage, const FText& InTitle = LOCTEXT("CustomizableObjectInstanceBakingUtils_GenericBakingError","Baking Error") )
|
|
{
|
|
if (!FApp::IsUnattended())
|
|
{
|
|
FMessageDialog::Open(EAppMsgType::Ok, InMessage, InTitle);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogMutable, Error, TEXT("%s - %s"), *InTitle.ToString(), *InMessage.ToString());
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Utility functions for the baking operation.
|
|
*/
|
|
|
|
|
|
/**
|
|
* Validates the filename chosen for the baking data
|
|
* @param InFileName The filename chosen by the user
|
|
* @return True if validation was successful, false otherwise
|
|
*/
|
|
bool ValidateProvidedFileName(const FString& InFileName)
|
|
{
|
|
// Check for invalid characters in the name of the object to be serialized
|
|
TCHAR InvalidCharacter = '0';
|
|
{
|
|
FString InvalidCharacters = FPaths::GetInvalidFileSystemChars();
|
|
for (int32 InvalidCharIndex = 0; InvalidCharIndex < InvalidCharacters.Len(); ++InvalidCharIndex)
|
|
{
|
|
TCHAR Char = InvalidCharacters[InvalidCharIndex];
|
|
FString SearchedChar = FString::Chr(Char);
|
|
if (InFileName.Contains(SearchedChar))
|
|
{
|
|
InvalidCharacter = InvalidCharacters[InvalidCharIndex];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (InvalidCharacter != '0')
|
|
{
|
|
const FText InvalidCharacterText = FText::FromString(FString::Chr(InvalidCharacter));
|
|
const FText ErrorText = FText::Format(LOCTEXT("CustomizableObjectInstanceBakingUtils_InvalidCharacter", "The selected contains an invalid character ({0})."), InvalidCharacterText);
|
|
|
|
ShowErrorNotification(ErrorText);
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Validates the AssetPath chosen for the baking data
|
|
* @param FileName The filename chosen by the user
|
|
* @param AssetPath The AssetPath chosen by the user
|
|
* @param InstanceCO The CustomizableObject from the provided COI
|
|
* @return True if validation was successful, false otherwise
|
|
*/
|
|
bool ValidateProvidedAssetPath(const FString& FileName, const FString& AssetPath, const UCustomizableObject* InstanceCO)
|
|
{
|
|
if (AssetPath.IsEmpty())
|
|
{
|
|
UE_LOG(LogMutable, Error, TEXT("The AssetPath can not be empty!"));
|
|
return false;
|
|
}
|
|
|
|
// Ensure we are not overriding the parent CO
|
|
const FString FullAssetPath = AssetPath + FString("/") + FileName + FString(".") + FileName; // Full asset path to the new asset we want to create
|
|
if (const bool bWouldOverrideParentCO = InstanceCO->GetPathName() == FullAssetPath)
|
|
{
|
|
const FText ErrorText = LOCTEXT("CustomizableObjectInstanceBakingUtils_OverwriteCO", "The selected path would overwrite the instance's parent Customizable Object.");
|
|
|
|
ShowErrorNotification(ErrorText);
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Given a collection of UObjects return the index of the one that matches the name also provided
|
|
* @param InName The name of the object we are looking for
|
|
* @param Objects The collection of objects we want to check for an element with the given name
|
|
* @return The index of the element whose name matches the one provided or INDEX_NONE if the element could not be found
|
|
*/
|
|
int32 GetObjectWithNameIndex(const FString& InName, const TArray<UObject*>& Objects )
|
|
{
|
|
int32 Index = 0;
|
|
for (UObject* CachedResource : Objects)
|
|
{
|
|
if (CachedResource && CachedResource->GetName() == InName)
|
|
{
|
|
return Index;
|
|
}
|
|
Index++;
|
|
}
|
|
|
|
return INDEX_NONE;
|
|
}
|
|
|
|
|
|
/**
|
|
* Outputs a string that we know it is unique.
|
|
* @param ResourceName The name of the resource we have provided. This should have the name of the current resource and will have the unique name for the resource once the method exits
|
|
* @param CachedResources Collection with all the already processed resources name's
|
|
*/
|
|
void MakeResourceNameUnique(FString& ResourceName, const TArray<UObject*>& CachedResources)
|
|
{
|
|
// Look for the resource name provided to see if we have already worked with it.
|
|
int32 FindResult = GetObjectWithNameIndex(ResourceName, CachedResources);
|
|
if (FindResult != INDEX_NONE)
|
|
{
|
|
// Add an integer suffix to create the unique name
|
|
uint32 Count = 0;
|
|
while (FindResult != INDEX_NONE)
|
|
{
|
|
FindResult = GetObjectWithNameIndex(ResourceName + "_" + FString::FromInt(Count), CachedResources);
|
|
Count++;
|
|
}
|
|
|
|
ResourceName += "_" + FString::FromInt(--Count);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the package at a given path and with a given name
|
|
* @param InPackageSavePath The path were we plan to save the package
|
|
* @param InObjName The name used by the package
|
|
* @return True if the asset is already at path
|
|
*/
|
|
UPackage* GetPackage(const FString& InPackageSavePath, const FString& InObjName)
|
|
{
|
|
// Check if the package already exists
|
|
const FString PackagePath = InPackageSavePath + "/" + InObjName;
|
|
UPackage* ExistingPackage = FindPackage(nullptr, *PackagePath);
|
|
if (!ExistingPackage)
|
|
{
|
|
const FString PackageFilePath = PackagePath + "." + InObjName;
|
|
|
|
FString PackageFileName;
|
|
if (FPackageName::DoesPackageExist(PackageFilePath, &PackageFileName))
|
|
{
|
|
ExistingPackage = LoadPackage(nullptr, *PackageFileName, LOAD_EditorOnly);
|
|
}
|
|
}
|
|
|
|
return ExistingPackage;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the save resolution type for a given package. Will return or New or Reuse type depending if the package already exists on disk or not
|
|
* @param InPackageSavePath The path where to save the package
|
|
* @param InPackageName The name to be given to the package that we want to save or reuse
|
|
* @return
|
|
*/
|
|
EPackageSaveResolutionType GetPackageSaveResolution(const FString& InPackageSavePath, const FString& InPackageName)
|
|
{
|
|
EPackageSaveResolutionType SaveResolutionType = EPackageSaveResolutionType::None;
|
|
if (GetPackage(InPackageSavePath, InPackageName))
|
|
{
|
|
// File found, instead of creating a new package use this one and update the contents it has
|
|
SaveResolutionType = EPackageSaveResolutionType::ReusedFile;
|
|
}
|
|
else
|
|
{
|
|
// The package could not be found. It is safe to create a package at this path and with the given name
|
|
SaveResolutionType = EPackageSaveResolutionType::NewFile;
|
|
}
|
|
|
|
// Just to be sure this case never happens even with future changes
|
|
check(SaveResolutionType == EPackageSaveResolutionType::ReusedFile || SaveResolutionType == EPackageSaveResolutionType::NewFile);
|
|
|
|
const UEnum* OptimizationLevelEnum = StaticEnum<EPackageSaveResolutionType>();
|
|
check(OptimizationLevelEnum);
|
|
UE_LOG(LogMutable, Verbose, TEXT("SaveResolution for the \"%s\" package is \"%s\""), *InPackageSavePath, *OptimizationLevelEnum->GetValueAsName(SaveResolutionType).ToString())
|
|
|
|
return SaveResolutionType;
|
|
}
|
|
|
|
|
|
static bool bIsBakeOperationAlreadyScheduled = false;
|
|
|
|
|
|
void OnInstanceUpdateFinish(const FUpdateContext& Result)
|
|
{
|
|
FCustomizableObjectEditorLogger::CreateLog(
|
|
LOCTEXT("CustomizableObjectInstanceBakingUtils_UpdateFinished", "The COInstance Update operation for baking has finished."))
|
|
.Category(ELoggerCategory::COInstanceBaking)
|
|
.CustomNotification()
|
|
.Notification(true)
|
|
.Log();
|
|
|
|
// Allow the baking of more instances once the bake of this one has completed (or at least until the callbacks have been broadcast)
|
|
bIsBakeOperationAlreadyScheduled = false;
|
|
}
|
|
|
|
|
|
void ScheduleInstanceUpdateForBaking(UCustomizableObjectInstance& InInstance, FInstanceUpdateNativeDelegate& InInstanceUpdateDelegate)
|
|
{
|
|
// This prevents the queue of updates to have more than one instance for baking.
|
|
if (bIsBakeOperationAlreadyScheduled)
|
|
{
|
|
UE_LOG(LogMutable, Error, TEXT("The COInstance update for baking could not be scheduled. Another instance is being updated for baking."));
|
|
InInstanceUpdateDelegate.Broadcast({EUpdateResult::Error});
|
|
return;
|
|
}
|
|
|
|
// TODO UE-262123: Compile Instance with correct settings instead
|
|
if (!InInstance.GetCustomizableObject() || !InInstance.GetCustomizableObject()->IsCompiled())
|
|
{
|
|
UE_LOG(LogMutable, Error, TEXT("The COInstance update for baking could not be scheduled. Instance not compiled."));
|
|
InInstanceUpdateDelegate.Broadcast({ EUpdateResult::Error });
|
|
return;
|
|
}
|
|
|
|
InInstanceUpdateDelegate.AddStatic(&OnInstanceUpdateFinish);
|
|
|
|
// Desired System settings for the update part of a baking operation
|
|
TSharedPtr<FMutableSystemSettingsOverrides> SystemSettingsOverride =
|
|
MakeShared<FMutableSystemSettingsOverrides>(
|
|
false,
|
|
false,
|
|
UnrealPixelFormatFunc);
|
|
|
|
// Schedule the update
|
|
check (InInstance.GetPrivate());
|
|
InInstance.GetPrivate()->UpdateSkeletalMeshAsyncResult(InInstanceUpdateDelegate,true,true, SystemSettingsOverride);
|
|
|
|
// Tell the baking system that an instance for baking will be processed
|
|
bIsBakeOperationAlreadyScheduled = true;
|
|
|
|
FCustomizableObjectEditorLogger::CreateLog(
|
|
LOCTEXT("CustomizableObjectInstanceBakingUtils_UpdateScheduled", "The COInstance Update operation for baking has been scheduled. Please hold."))
|
|
.Category(ELoggerCategory::COInstanceBaking)
|
|
.CustomNotification()
|
|
.Notification(true)
|
|
.Log();
|
|
}
|
|
|
|
|
|
/**
|
|
* Checks if the texture was generated by mutable or if it was not
|
|
* @param InTexture The texture we want to check
|
|
* @return true if the texture was generated by mutable, false otherwise
|
|
*/
|
|
bool IsAMutableTexture (const UTexture2D* InTexture)
|
|
{
|
|
bool bIsMutableTexture = false;
|
|
for (UAssetUserData* UserData : *InTexture->GetAssetUserDataArray())
|
|
{
|
|
if (Cast<UMutableTextureMipDataProviderFactory>(UserData))
|
|
{
|
|
bIsMutableTexture = true;
|
|
break;
|
|
}
|
|
}
|
|
return bIsMutableTexture;
|
|
}
|
|
|
|
|
|
/**
|
|
* Add the package and save resolution only once to prevent the overriding of packages marked as New with Reuse ones if processed multiple times
|
|
* @param NewObjectPair The Pair of data we want to add to the map. Only if the package is not present in the map the action will get performed.
|
|
* @param OutSavedPackages The map you want to add data into.
|
|
*/
|
|
void AddUniqueToSavePackage(TPair<UPackage*, EPackageSaveResolutionType> NewObjectPair, TMap<UPackage*,EPackageSaveResolutionType>& OutSavedPackages)
|
|
{
|
|
if (!OutSavedPackages.Find(NewObjectPair.Key))
|
|
{
|
|
OutSavedPackages.Add(NewObjectPair);
|
|
}
|
|
}
|
|
|
|
|
|
bool BakeCustomizableObjectInstance(UCustomizableObjectInstance& InInstance, const FString& FileName, const FString& AssetPath,
|
|
const bool bExportAllResources, const bool bGenerateConstantMaterialInstances, bool bHasPermissionToOverride, bool bIsUnattendedExecution,
|
|
TArray<TPair<EPackageSaveResolutionType, UPackage*>>& OutSavedPackages)
|
|
{
|
|
FBakingConfiguration BakingConfiguration;
|
|
BakingConfiguration.OutputPath = AssetPath;
|
|
BakingConfiguration.OutputFilesBaseName = FileName;
|
|
BakingConfiguration.bExportAllResourcesOnBake = bExportAllResources;
|
|
BakingConfiguration.bGenerateConstantMaterialInstancesOnBake = bGenerateConstantMaterialInstances;
|
|
|
|
TMap<UPackage*, EPackageSaveResolutionType> SavedPackages;
|
|
bool bWasOperationSuccessful = BakeCustomizableObjectInstance(InInstance, BakingConfiguration, bIsUnattendedExecution, SavedPackages);
|
|
|
|
// Generate the array this method expects as an output based on the "SavedPackages" map
|
|
OutSavedPackages.Reset(SavedPackages.Num());
|
|
for (TPair<UPackage*,EPackageSaveResolutionType> SavedPackagesEntry : SavedPackages)
|
|
{
|
|
TPair<EPackageSaveResolutionType, UPackage*> ArrayEntry = {SavedPackagesEntry.Value, SavedPackagesEntry.Key};
|
|
OutSavedPackages.Add(ArrayEntry);
|
|
}
|
|
|
|
return bWasOperationSuccessful;
|
|
}
|
|
|
|
|
|
bool BakeCustomizableObjectInstance(
|
|
UCustomizableObjectInstance& InInstance,
|
|
const FBakingConfiguration& Configuration,
|
|
bool bIsUnattendedExecution,
|
|
TMap<UPackage*,EPackageSaveResolutionType>& OutSavedPackages)
|
|
{
|
|
OutSavedPackages.Reset();
|
|
|
|
// Extract some baking settings from the configuration file provided
|
|
const FString& FileName = Configuration.OutputFilesBaseName;
|
|
const FString& AssetPath = Configuration.OutputPath;
|
|
|
|
// Resource prefixes to be used. If an invalid prefix has been provided by the configuration object use the UE default one instead
|
|
const FString SkeletalMeshAssetPrefix = Configuration.SkeletalMeshAssetPrefix.IsEmpty() ? TEXT("SK_") : Configuration.SkeletalMeshAssetPrefix;
|
|
const FString SkeletonAssetPrefix = Configuration.SkeletonAssetPrefix.IsEmpty() ? TEXT("SKEL_") : Configuration.SkeletonAssetPrefix;
|
|
const FString PhysicsAssetPrefix = Configuration.PhysicsAssetPrefix.IsEmpty() ? TEXT("PHYS_") : Configuration.PhysicsAssetPrefix;
|
|
const FString MaterialAssetPrefix = Configuration.MaterialAssetPrefix.IsEmpty() ? TEXT("M_") : Configuration.MaterialAssetPrefix;
|
|
const FString TextureAssetPrefix = Configuration.TextureAssetPrefix.IsEmpty() ? TEXT("T_") : Configuration.TextureAssetPrefix;
|
|
const FString MaterialInstanceAssetPrefix = Configuration.MaterialInstanceAssetPrefix.IsEmpty() ? TEXT("MI_") : Configuration.MaterialInstanceAssetPrefix;
|
|
const FString MaterialDynamicInstanceAssetPrefix = Configuration.MaterialDynamicInstanceAssetPrefix.IsEmpty() ? TEXT("MID_") : Configuration.MaterialDynamicInstanceAssetPrefix;
|
|
const FString MaterialConstantInstanceAssetPrefix = Configuration.MaterialConstantInstanceAssetPrefix.IsEmpty() ? TEXT("MIC_") : Configuration.MaterialConstantInstanceAssetPrefix;
|
|
|
|
|
|
// Ensure that the state of the COI provided is valid --------------------------------------------------------------------------------------------
|
|
UCustomizableObject* InstanceCO = InInstance.GetCustomizableObject();
|
|
|
|
// Ensure the CO of the COI is accessible
|
|
if (!InstanceCO || InstanceCO->GetPrivate()->IsLocked())
|
|
{
|
|
FCustomizableObjectEditorLogger::CreateLog(
|
|
LOCTEXT("CustomizableObjectInstanceBakingUtils_LockedObject", "Please wait until the Customizable Object is compiled"))
|
|
.Category(ELoggerCategory::COInstanceBaking)
|
|
.CustomNotification()
|
|
.Notification(true)
|
|
.Log();
|
|
|
|
return false;
|
|
}
|
|
|
|
if (InstanceCO->GetPrivate()->Status.Get() == FCustomizableObjectStatus::EState::Loading)
|
|
{
|
|
FCustomizableObjectEditorLogger::CreateLog(
|
|
LOCTEXT("CustomizableObjectInstanceBakingUtils_LoadingObject","Please wait until the Customizable Object is loaded"))
|
|
.Category(ELoggerCategory::COInstanceBaking)
|
|
.CustomNotification()
|
|
.Notification(true)
|
|
.Log();
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!ValidateProvidedFileName(FileName))
|
|
{
|
|
UE_LOG(LogMutable, Error, TEXT("The FileName for the instance baking is not valid."));
|
|
return false;
|
|
}
|
|
|
|
if (!ValidateProvidedAssetPath(FileName,AssetPath,InstanceCO))
|
|
{
|
|
UE_LOG(LogMutable, Error, TEXT("The AssetPath for the instance baking is not valid."));
|
|
return false;
|
|
}
|
|
|
|
// Exit early if the provided instance does not have a skeletal mesh
|
|
if (!InInstance.HasAnySkeletalMesh())
|
|
{
|
|
UE_LOG(LogMutable, Error, TEXT("The provided instance does not have an skeletal mesh."));
|
|
return false;
|
|
}
|
|
|
|
// Early out if the Instance ModelResources is not valid.
|
|
const UModelResources* ModelResources = InstanceCO->GetPrivate()->GetModelResources();
|
|
if (!ModelResources)
|
|
{
|
|
UE_LOG(LogMutable, Error, TEXT("The ModelResources from the Customizable Object is not valid."));
|
|
return false;
|
|
}
|
|
|
|
// COI Validation completed : Proceed with the baking operation ----------------------------------------------------------------------------------
|
|
|
|
// Notify of better configuration -> Continue operation normally
|
|
if (ModelResources->bCompiledWithHDTextureCompression == false)
|
|
{
|
|
FCustomizableObjectEditorLogger::CreateLog(
|
|
LOCTEXT("CustomizableObjectInstanceBakingUtils_LowQualityTextures", "The Customizable Object wasn't compiled with high quality textures. For the best baking results, change the Texture Compression setting and recompile it."))
|
|
.Category(ELoggerCategory::COInstanceBaking)
|
|
.CustomNotification()
|
|
.Notification(true)
|
|
.Log();
|
|
}
|
|
|
|
// Prefixes used for UMaterial assets.
|
|
const TArray<FString> MaterialResourcePrefixes =
|
|
{
|
|
MaterialAssetPrefix,
|
|
MaterialInstanceAssetPrefix,
|
|
MaterialDynamicInstanceAssetPrefix,
|
|
MaterialConstantInstanceAssetPrefix
|
|
};
|
|
|
|
// Array with the already processed resource names and resources (UObjects)
|
|
TArray<UObject*> DuplicatedObjects;
|
|
TArray<UObject*> HandledSourceObjects;
|
|
|
|
|
|
for (int32 ObjectComponentIndex = 0; ObjectComponentIndex < InstanceCO->GetComponentCount(); ++ObjectComponentIndex)
|
|
{
|
|
check(DuplicatedObjects.Num() == HandledSourceObjects.Num())
|
|
|
|
const FName ComponentName = InstanceCO->GetPrivate()->GetComponentName(FCustomizableObjectComponentIndex(ObjectComponentIndex));
|
|
USkeletalMesh* Mesh = InInstance.GetComponentMeshSkeletalMesh(ComponentName);
|
|
|
|
if (!Mesh)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Append the component name to the ObjectFileName
|
|
FString ObjectBaseName = FileName;
|
|
if (FileName.IsEmpty())
|
|
{
|
|
ObjectBaseName = ComponentName.ToString();
|
|
}
|
|
else
|
|
{
|
|
ObjectBaseName = FileName + TEXT("_") + ComponentName.ToString();
|
|
}
|
|
ObjectBaseName += TEXT("_");
|
|
|
|
TMap<UObject*, UObject*> ReplacementMap;
|
|
|
|
if (Configuration.bExportAllResourcesOnBake)
|
|
{
|
|
UMaterial* Material;
|
|
FString PackageName;
|
|
|
|
// Each element of the array corresponds to a material of the mesh
|
|
// The key is the parameter index and the value the texture to be used there
|
|
TArray<TMap<int, UTexture*>> TextureReplacementMaps;
|
|
|
|
// Duplicate Textures found in the Material Instances of the SkeletalMesh so we can later assign them to the
|
|
// duplicates of those material instances. At the end of the baking we will have a series of materials with the
|
|
// parameters set as the material instances they are based of.
|
|
for (int32 MaterialIndex = 0; MaterialIndex < Mesh->GetMaterials().Num(); ++MaterialIndex)
|
|
{
|
|
UMaterialInterface* Interface = Mesh->GetMaterials()[MaterialIndex].MaterialInterface;
|
|
Material = Interface->GetMaterial();
|
|
UMaterialInstance* MaterialInstance = Cast<UMaterialInstance>(Interface);
|
|
|
|
TextureReplacementMaps.AddDefaulted();
|
|
|
|
if (Material != nullptr && MaterialInstance != nullptr)
|
|
{
|
|
TArray<FGuid> ParameterIds;
|
|
TArray<FMaterialParameterInfo> ParameterInfoObjects;
|
|
Material->GetAllTextureParameterInfo(ParameterInfoObjects, ParameterIds);
|
|
|
|
for (int32 ParameterInfoIndex = 0; ParameterInfoIndex < ParameterInfoObjects.Num(); ParameterInfoIndex++)
|
|
{
|
|
UTexture* Texture;
|
|
const FName& ParameterName = ParameterInfoObjects[ParameterInfoIndex].Name;
|
|
if (MaterialInstance->GetTextureParameterValue(ParameterName, Texture))
|
|
{
|
|
UTexture2D* SourceTexture = Cast<UTexture2D>(Texture);
|
|
if (!SourceTexture)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Update the name adding to it the name of the component alongside other data
|
|
FString TextureName = SourceTexture->GetName();
|
|
RemovePrefix(TextureName, {TextureAssetPrefix});
|
|
ComposeResourceName(TextureAssetPrefix, ObjectBaseName, TextureName);
|
|
|
|
// Experimental
|
|
// If the texture is not from mutable and we know it has already been processed...
|
|
if (!IsAMutableTexture(SourceTexture) && HandledSourceObjects.Contains(SourceTexture))
|
|
{
|
|
// Instead of just creating a new name for the texture to be stored in disk, a better aproach could be using
|
|
// the already duplicated file and assigning it to the material we are working with.
|
|
// The issue this has is that the texture may be named as from one component and now will be used for multiple ones
|
|
for (UObject* DuplicatedObject : DuplicatedObjects)
|
|
{
|
|
if (DuplicatedObject &&
|
|
DuplicatedObject->IsA<UTexture>() &&
|
|
DuplicatedObject->GetName() == TextureName)
|
|
{
|
|
// TODO: Allow reusing a texture of one component in another one (DuplicatedObject->GetName() == TextureName)
|
|
// to be able to do this I will need to stop using the name of the texture as the name has the component embedded on it
|
|
// or, instead, remove the component name from the full name and then check
|
|
|
|
UTexture* DupTexture = Cast<UTexture>(DuplicatedObject);
|
|
TextureReplacementMaps[MaterialIndex].Add(ParameterInfoIndex, DupTexture);
|
|
|
|
UE_LOG(LogMutable, Verbose, TEXT("Matching texture found with name %s in component %s"), *TextureName, *ComponentName.ToString())
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Here we will know if the asset already exists (ReusedFile) or if it is new (NewFile)
|
|
const EPackageSaveResolutionType SaveResolution = GetPackageSaveResolution(AssetPath, TextureName);
|
|
|
|
// Duplicating mutable generated textures
|
|
if (IsAMutableTexture(SourceTexture))
|
|
{
|
|
if (SourceTexture->GetPlatformData() && SourceTexture->GetPlatformData()->Mips.Num() > 0)
|
|
{
|
|
// Recover original name of the texture parameter value, now substituted by the generated Mutable texture
|
|
UTexture* OriginalTexture = nullptr;
|
|
MaterialInstance->Parent->GetTextureParameterValue(FName(*ParameterName.GetPlainNameString()), OriginalTexture);
|
|
|
|
PackageName = AssetPath + FString("/") + TextureName;
|
|
UTexture2D* DuplicatedTexture = FUnrealBakeHelpers::BakeHelper_CreateAssetTexture(SourceTexture, TextureName, PackageName, OriginalTexture, nullptr, SaveResolution);
|
|
|
|
HandledSourceObjects.Add(SourceTexture);
|
|
DuplicatedObjects.Add(DuplicatedTexture);
|
|
|
|
AddUniqueToSavePackage({DuplicatedTexture->GetPackage(), SaveResolution},OutSavedPackages);
|
|
|
|
if (OriginalTexture != nullptr)
|
|
{
|
|
TextureReplacementMaps[MaterialIndex].Add(ParameterInfoIndex, DuplicatedTexture);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Duplicate the non-mutable textures of the Material instance (pass-through textures)
|
|
|
|
PackageName = AssetPath + FString("/") + TextureName;
|
|
UObject* DuplicatedTexture = FUnrealBakeHelpers::BakeHelper_DuplicateAsset(Texture, TextureName, PackageName, nullptr, false, SaveResolution);
|
|
|
|
HandledSourceObjects.Add(Texture);
|
|
DuplicatedObjects.Add(DuplicatedTexture);
|
|
|
|
AddUniqueToSavePackage({DuplicatedTexture->GetPackage(), SaveResolution},OutSavedPackages);
|
|
|
|
UTexture* DupTexture = Cast<UTexture>(DuplicatedTexture);
|
|
TextureReplacementMaps[MaterialIndex].Add(ParameterInfoIndex, DupTexture);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// At this point we have an array where each element represents one material index.
|
|
// Each element is formed by a map where the key is the parameter index and the value the texture to be used in that index so
|
|
// Later we will be able to update all materials of the component with the duplicated textures we have just created
|
|
|
|
// Duplicate the materials used by each material instance so that the replacement map has proper information
|
|
// when duplicating the material instances
|
|
// Each material will get filled with the data of the interface it is related to.
|
|
for (int32 MaterialIndex = 0; MaterialIndex < Mesh->GetMaterials().Num(); ++MaterialIndex)
|
|
{
|
|
UMaterialInterface* Interface = Mesh->GetMaterials()[MaterialIndex].MaterialInterface;
|
|
Material = Interface ? Interface->GetMaterial() : nullptr;
|
|
|
|
if (Material)
|
|
{
|
|
// If the material has already been processed (so it is already part of the ReplacementMap) just skip it as we only need
|
|
// to update the Material Interface once (Material Interface <- Material)
|
|
if (HandledSourceObjects.Contains(Interface))
|
|
{
|
|
// should be the case if the interface has already been processed
|
|
check(ReplacementMap.Contains(Interface));
|
|
continue;
|
|
}
|
|
|
|
FString MaterialName = Interface->GetName();
|
|
RemovePrefix(MaterialName, MaterialResourcePrefixes);
|
|
ComposeResourceName(MaterialAssetPrefix, ObjectBaseName, MaterialName);
|
|
|
|
// Give it an unique name or may happen that another material already stored on disk during this bake will get updated
|
|
// with data form this material. We want to avoid that from happening as we do not want to require the name to always be a hash of
|
|
// the contents (they are not always produced by mutable in this case)
|
|
MakeResourceNameUnique(MaterialName, DuplicatedObjects);
|
|
|
|
const EPackageSaveResolutionType SaveResolution = GetPackageSaveResolution(AssetPath, MaterialName);
|
|
|
|
PackageName = AssetPath + FString("/") + MaterialName;
|
|
UObject* DuplicatedObject = FUnrealBakeHelpers::BakeHelper_DuplicateAsset(Material, MaterialName, PackageName,
|
|
nullptr, Configuration.bGenerateConstantMaterialInstancesOnBake, SaveResolution);
|
|
|
|
HandledSourceObjects.Add(Material);
|
|
DuplicatedObjects.Add(DuplicatedObject);
|
|
|
|
// Trying to add the same interface twice would mean something is not being done correctly in this method
|
|
check(!ReplacementMap.Contains(Interface));
|
|
// Tell the system that the Interface object will be replaced by the Duplicated one
|
|
ReplacementMap.Add(Interface, DuplicatedObject);
|
|
|
|
AddUniqueToSavePackage({DuplicatedObject->GetPackage(), SaveResolution},OutSavedPackages);
|
|
|
|
// Copy the texture parameters from the interface to the material
|
|
if (UMaterial* DuplicatedMaterial = Cast<UMaterial>(DuplicatedObject))
|
|
{
|
|
FUnrealBakeHelpers::CopyAllMaterialParameters<UMaterial>(*DuplicatedMaterial, *Interface, TextureReplacementMaps[MaterialIndex]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Export only the mutable generated resources
|
|
|
|
// Duplicate the material instances
|
|
for (int32 MaterialIndex = 0; MaterialIndex < Mesh->GetMaterials().Num(); ++MaterialIndex)
|
|
{
|
|
UMaterialInterface* Interface = Mesh->GetMaterials()[MaterialIndex].MaterialInterface;
|
|
|
|
// Check that the resource has not yet been processed, and if so, skip it as we only want to duplicate each interface once
|
|
if (HandledSourceObjects.Contains(Interface))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FString MaterialName;
|
|
{
|
|
MaterialName = Interface->GetName(); // Use the name of the interface as it is unique
|
|
RemovePrefix(MaterialName, MaterialResourcePrefixes);
|
|
|
|
if (Configuration.bGenerateConstantMaterialInstancesOnBake && Interface->IsA<UMaterialInstance>())
|
|
{
|
|
// Change the prefix to the Material Constant Instance since in this situation the new asset based on the Interface
|
|
// will be of the Material Constant Instance type
|
|
ComposeResourceName(MaterialConstantInstanceAssetPrefix, ObjectBaseName, MaterialName);
|
|
}
|
|
else
|
|
{
|
|
if (Interface->IsA<UMaterial>())
|
|
{
|
|
ComposeResourceName(MaterialAssetPrefix, ObjectBaseName, MaterialName);
|
|
}
|
|
else if (Interface->IsA<UMaterialInstanceConstant>())
|
|
{
|
|
ComposeResourceName(MaterialConstantInstanceAssetPrefix, ObjectBaseName, MaterialName);
|
|
}
|
|
else if (Interface->IsA<UMaterialInstanceDynamic>())
|
|
{
|
|
ComposeResourceName(MaterialDynamicInstanceAssetPrefix, ObjectBaseName, MaterialName);
|
|
}
|
|
else
|
|
{
|
|
checkNoEntry(); // Invalid material type.
|
|
}
|
|
}
|
|
}
|
|
|
|
// One material could be used by multiple material instances so, to be sure, make sure the name is unique so we do not reuse it during the bake
|
|
// We could be more specific in which cases we do this but I left it this way to keep the complexity as low as possible
|
|
MakeResourceNameUnique(MaterialName, DuplicatedObjects);
|
|
|
|
const EPackageSaveResolutionType SaveResolution = GetPackageSaveResolution(AssetPath, MaterialName);
|
|
|
|
FString MatPkgName = AssetPath + FString("/") + MaterialName;
|
|
UObject* DuplicatedMaterial = FUnrealBakeHelpers::BakeHelper_DuplicateAsset(Interface, MaterialName,
|
|
MatPkgName, &ReplacementMap, Configuration.bGenerateConstantMaterialInstancesOnBake, SaveResolution);
|
|
HandledSourceObjects.Add(Interface);
|
|
DuplicatedObjects.Add(DuplicatedMaterial);
|
|
|
|
AddUniqueToSavePackage({DuplicatedMaterial->GetPackage(), SaveResolution},OutSavedPackages);
|
|
|
|
// Only need to duplicate the generate textures if the original material is a dynamic instance
|
|
// If the material has Mutable textures, then it will be a dynamic material instance for sure
|
|
if (UMaterialInstance* MaterialInstance = Cast<UMaterialInstance>(Interface))
|
|
{
|
|
// Duplicate generated textures
|
|
UMaterialInstanceDynamic* InstDynamic = Cast<UMaterialInstanceDynamic>(DuplicatedMaterial);
|
|
UMaterialInstanceConstant* InstConstant = Cast<UMaterialInstanceConstant>(DuplicatedMaterial);
|
|
|
|
if (InstDynamic || InstConstant)
|
|
{
|
|
for (int32 TextureIndex = 0; TextureIndex < MaterialInstance->TextureParameterValues.Num(); ++TextureIndex)
|
|
{
|
|
if (MaterialInstance->TextureParameterValues[TextureIndex].ParameterValue)
|
|
{
|
|
if (MaterialInstance->TextureParameterValues[TextureIndex].ParameterValue->HasAnyFlags(RF_Transient))
|
|
{
|
|
if (UTexture2D* SourceTexture = Cast<UTexture2D>(MaterialInstance->TextureParameterValues[TextureIndex].ParameterValue))
|
|
{
|
|
// If this source texture has already been processed during this bake operation, instead of creating a duplicate,
|
|
// just use the resource cached in the DuplicatedObjects array
|
|
if (HandledSourceObjects.Contains(SourceTexture))
|
|
{
|
|
UTexture* PrevTexture = Cast<UTexture>(DuplicatedObjects[HandledSourceObjects.Find(SourceTexture)]);
|
|
check(PrevTexture);
|
|
|
|
if (InstDynamic)
|
|
{
|
|
InstDynamic->SetTextureParameterValue(MaterialInstance->TextureParameterValues[TextureIndex].ParameterInfo.Name, PrevTexture);
|
|
}
|
|
else if (InstConstant)
|
|
{
|
|
InstConstant->SetTextureParameterValueEditorOnly(MaterialInstance->TextureParameterValues[TextureIndex].ParameterInfo.Name, PrevTexture);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// This should never happen as we handle this in the previous conditional block where we early out
|
|
check(!HandledSourceObjects.Contains(SourceTexture))
|
|
|
|
// The source texture has not yet been processed so duplicate it
|
|
|
|
// Generate a new name for the duplicated texture so it does not collide with the original one
|
|
FString TextureName = SourceTexture->GetName();
|
|
RemovePrefix(TextureName, {TextureAssetPrefix});
|
|
ComposeResourceName(TextureAssetPrefix, ObjectBaseName, TextureName);
|
|
|
|
// Check if the file is already in disk, so we can later decide what we do with that file
|
|
const EPackageSaveResolutionType TextureSaveResolution = GetPackageSaveResolution(AssetPath, TextureName);
|
|
|
|
FString TexPkgName = AssetPath + FString("/") + TextureName;
|
|
UTexture2D* DuplicatedTexture = FUnrealBakeHelpers::BakeHelper_CreateAssetTexture(SourceTexture, TextureName, TexPkgName, nullptr, nullptr, SaveResolution);
|
|
|
|
// Cache the file we just have handled and it's duplicate
|
|
HandledSourceObjects.Add(SourceTexture);
|
|
DuplicatedObjects.Add(DuplicatedTexture);
|
|
|
|
AddUniqueToSavePackage({DuplicatedTexture->GetPackage(), TextureSaveResolution},OutSavedPackages);
|
|
|
|
if (InstDynamic)
|
|
{
|
|
InstDynamic->SetTextureParameterValue(MaterialInstance->TextureParameterValues[TextureIndex].ParameterInfo.Name, DuplicatedTexture);
|
|
}
|
|
else if(InstConstant)
|
|
{
|
|
InstConstant->SetTextureParameterValueEditorOnly(MaterialInstance->TextureParameterValues[TextureIndex].ParameterInfo.Name, DuplicatedTexture);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogMutable, Error, TEXT("A Mutable texture that is not a Texture2D has been found while baking a CustomizableObjectInstance."));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If it's not transient it's not a mutable texture, it's a pass-through texture
|
|
// Just set the original texture
|
|
if (InstDynamic)
|
|
{
|
|
InstDynamic->SetTextureParameterValue(MaterialInstance->TextureParameterValues[TextureIndex].ParameterInfo.Name, MaterialInstance->TextureParameterValues[TextureIndex].ParameterValue);
|
|
}
|
|
else if (InstConstant)
|
|
{
|
|
InstConstant->SetTextureParameterValueEditorOnly(MaterialInstance->TextureParameterValues[TextureIndex].ParameterInfo.Name, MaterialInstance->TextureParameterValues[TextureIndex].ParameterValue);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: As it happens with the textures we may be able to re-use the other resources generated for one component into the baked data of the other
|
|
// for now we will keep them separated for the assets being processed bellow
|
|
|
|
// Get the clean name of the Skeletal mesh (without the embedded CO name)
|
|
FString SkeletalMeshName = Mesh->GetName();
|
|
if (Mesh->HasAnyFlags(RF_Transient))
|
|
{
|
|
const FString CustomizableObjectName = InInstance.GetCustomizableObject()->GetName() + TEXT("_");
|
|
SkeletalMeshName = SkeletalMeshName.Replace(*CustomizableObjectName,TEXT(""));
|
|
}
|
|
RemovePrefix(SkeletalMeshName, {SkeletalMeshAssetPrefix});
|
|
ComposeResourceName(SkeletalMeshAssetPrefix, ObjectBaseName, SkeletalMeshName);
|
|
MakeResourceNameUnique(SkeletalMeshName, DuplicatedObjects);
|
|
|
|
// Skeletal Mesh's Skeleton
|
|
if (USkeleton* Skeleton = Mesh->GetSkeleton())
|
|
{
|
|
const bool bTransient = Skeleton->GetPackage() == GetTransientPackage();
|
|
|
|
// Duplicate only if transient or export all assets.
|
|
if (bTransient || Configuration.bExportAllResourcesOnBake)
|
|
{
|
|
FString SkeletonName = Mesh->GetName();
|
|
RemovePrefix(SkeletonName, {SkeletalMeshAssetPrefix});
|
|
ComposeResourceName(SkeletonAssetPrefix, ObjectBaseName, SkeletonName);
|
|
MakeResourceNameUnique(SkeletonName, DuplicatedObjects);
|
|
|
|
const EPackageSaveResolutionType SkeletonSaveResolution = GetPackageSaveResolution(AssetPath, SkeletonName);
|
|
FString SkeletonPkgName = AssetPath + FString("/") + SkeletonName;
|
|
UObject* DuplicatedSkeleton = FUnrealBakeHelpers::BakeHelper_DuplicateAsset(Skeleton, SkeletonName,
|
|
SkeletonPkgName, &ReplacementMap, false, SkeletonSaveResolution);
|
|
|
|
HandledSourceObjects.Add(Skeleton);
|
|
DuplicatedObjects.Add(DuplicatedSkeleton);
|
|
|
|
AddUniqueToSavePackage({DuplicatedSkeleton->GetPackage(), SkeletonSaveResolution},OutSavedPackages);
|
|
|
|
ReplacementMap.Add(Skeleton, DuplicatedSkeleton);
|
|
}
|
|
}
|
|
|
|
// Skeletal Mesh's Physics Asset
|
|
bool bNewPhysicsAssetCreated = false;
|
|
if (UPhysicsAsset* PhysicsAsset = Mesh->GetPhysicsAsset())
|
|
{
|
|
const bool bTransient = PhysicsAsset->GetPackage() == GetTransientPackage();
|
|
|
|
// Duplicate only if transient or export all assets.
|
|
if (bTransient || Configuration.bExportAllResourcesOnBake)
|
|
{
|
|
FString PhysicsAssetName = Mesh->GetName();
|
|
RemovePrefix(PhysicsAssetName, {SkeletalMeshAssetPrefix});
|
|
ComposeResourceName(PhysicsAssetPrefix, ObjectBaseName, PhysicsAssetName);
|
|
MakeResourceNameUnique(PhysicsAssetName, DuplicatedObjects);
|
|
|
|
const EPackageSaveResolutionType PhysicsAssetSaveResolution = GetPackageSaveResolution(AssetPath, PhysicsAssetName);
|
|
FString PhysicsAssetPkgName = AssetPath + FString("/") + PhysicsAssetName;
|
|
UObject* DuplicatedPhysicsAsset = FUnrealBakeHelpers::BakeHelper_DuplicateAsset(PhysicsAsset, PhysicsAssetName,
|
|
PhysicsAssetPkgName, &ReplacementMap, false, PhysicsAssetSaveResolution);
|
|
|
|
HandledSourceObjects.Add(PhysicsAsset);
|
|
DuplicatedObjects.Add(DuplicatedPhysicsAsset);
|
|
|
|
AddUniqueToSavePackage({DuplicatedPhysicsAsset->GetPackage(), PhysicsAssetSaveResolution},OutSavedPackages);
|
|
|
|
ReplacementMap.Add(PhysicsAsset, DuplicatedPhysicsAsset);
|
|
|
|
bNewPhysicsAssetCreated = true;
|
|
}
|
|
}
|
|
|
|
|
|
// Skeletal Mesh
|
|
const EPackageSaveResolutionType MeshSaveResolution = GetPackageSaveResolution(AssetPath, SkeletalMeshName);
|
|
FString PkgName = AssetPath + FString("/") + SkeletalMeshName;
|
|
UObject* DuplicatedMesh = FUnrealBakeHelpers::BakeHelper_DuplicateAsset(Mesh, SkeletalMeshName, PkgName,
|
|
&ReplacementMap, false, MeshSaveResolution);
|
|
HandledSourceObjects.Add(Mesh);
|
|
DuplicatedObjects.Add(DuplicatedMesh);
|
|
|
|
AddUniqueToSavePackage({DuplicatedMesh->GetPackage(), MeshSaveResolution},OutSavedPackages);
|
|
|
|
Mesh->Build();
|
|
|
|
if (USkeletalMesh* SkeletalMesh = Cast<USkeletalMesh>(DuplicatedMesh))
|
|
{
|
|
SkeletalMesh->ResetLODInfo();
|
|
for (int32 LODIndex = 0; LODIndex < Mesh->GetLODNum(); ++LODIndex)
|
|
{
|
|
SkeletalMesh->AddLODInfo(*Mesh->GetLODInfo(LODIndex));
|
|
}
|
|
|
|
SkeletalMesh->GetImportedModel()->SkeletalMeshModelGUID = FGuid::NewGuid();
|
|
|
|
// Duplicate AssetUserData
|
|
{
|
|
const TArray<UAssetUserData*>* AssetUserDataArray = Mesh->GetAssetUserDataArray();
|
|
for (const UAssetUserData* AssetUserData : *AssetUserDataArray)
|
|
{
|
|
if (AssetUserData)
|
|
{
|
|
// Duplicate to change ownership
|
|
UAssetUserData* NewAssetUserData = Cast<UAssetUserData>(StaticDuplicateObject(AssetUserData, SkeletalMesh));
|
|
SkeletalMesh->AddAssetUserData(NewAssetUserData);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add Instance Info in a custom AssetUserData
|
|
{
|
|
const FCustomizableInstanceComponentData* ComponentData = InInstance.GetPrivate()->GetComponentData(ComponentName);
|
|
check(ComponentData);
|
|
|
|
if (InInstance.GetAnimationGameplayTags().Num() ||
|
|
ComponentData->AnimSlotToBP.Num())
|
|
{
|
|
UCustomizableObjectInstanceUserData* InstanceData = NewObject<UCustomizableObjectInstanceUserData>(SkeletalMesh, NAME_None, RF_Public | RF_Transactional);
|
|
InstanceData->AnimationGameplayTag = InInstance.GetAnimationGameplayTags();
|
|
|
|
for (const TTuple<FName, TSoftClassPtr<UAnimInstance>>& AnimSlot : ComponentData->AnimSlotToBP)
|
|
{
|
|
FCustomizableObjectAnimationSlot AnimationSlot;
|
|
AnimationSlot.Name = AnimSlot.Key;
|
|
AnimationSlot.AnimInstance = AnimSlot.Value;
|
|
|
|
InstanceData->AnimationSlots.Add(AnimationSlot);
|
|
}
|
|
|
|
SkeletalMesh->AddAssetUserData(InstanceData);
|
|
}
|
|
}
|
|
|
|
// Copy LODSettings from the Reference Skeletal Mesh
|
|
{
|
|
if (ModelResources->ReferenceSkeletalMeshesData.IsValidIndex(ObjectComponentIndex))
|
|
{
|
|
USkeletalMeshLODSettings* LODSettings = ModelResources->ReferenceSkeletalMeshesData[ObjectComponentIndex].SkeletalMeshLODSettings;
|
|
SkeletalMesh->SetLODSettings(LODSettings);
|
|
}
|
|
}
|
|
|
|
// Set the physics asset preview mesh if the SkeletalMesh physics assets has been generated as part of the bake.
|
|
if (SkeletalMesh->GetPhysicsAsset() && bNewPhysicsAssetCreated)
|
|
{
|
|
SkeletalMesh->GetPhysicsAsset()->SetPreviewMesh(SkeletalMesh);
|
|
}
|
|
|
|
// Generate render data
|
|
SkeletalMesh->Build();
|
|
}
|
|
|
|
check(DuplicatedObjects.Num() == HandledSourceObjects.Num());
|
|
|
|
// Remove duplicated UObjects from Root (previously added to avoid objects from being GC in the middle of the bake process)
|
|
for (UObject* Obj : DuplicatedObjects)
|
|
{
|
|
Obj->RemoveFromRoot();
|
|
}
|
|
}
|
|
|
|
// Save the packages generated during the baking operation --------------------------------------------------------------------------------------
|
|
|
|
// Complete the baking by saving the packages we have cached during the baking operation
|
|
if (OutSavedPackages.Num())
|
|
{
|
|
// Prepare the list of assets we want to provide to "PromptForCheckoutAndSave" for saving
|
|
TArray<UPackage*> PackagesToSaveProxy;
|
|
PackagesToSaveProxy.Reserve(OutSavedPackages.Num());
|
|
for (TPair<UPackage*, EPackageSaveResolutionType> DataToSave : OutSavedPackages)
|
|
{
|
|
// Ensure we have no duplicates here
|
|
check(!PackagesToSaveProxy.Contains(DataToSave.Key));
|
|
|
|
PackagesToSaveProxy.Push(DataToSave.Key);
|
|
}
|
|
|
|
// List of packages that could not be saved
|
|
TArray<UPackage*> FailedToSavePackages;
|
|
const bool bWasSavingSuccessful = FEditorFileUtils::PromptForCheckoutAndSave(PackagesToSaveProxy, false, !bIsUnattendedExecution, &FailedToSavePackages, false, false) == FEditorFileUtils::EPromptReturnCode::PR_Success;
|
|
|
|
// Remove all packages that were going to be saved but failed to do so
|
|
uint32 RemovedPackagesCount = 0;
|
|
for (UPackage* Package : FailedToSavePackages)
|
|
{
|
|
if (OutSavedPackages.Remove(Package))
|
|
{
|
|
RemovedPackagesCount++;
|
|
}
|
|
}
|
|
OutSavedPackages.Shrink();
|
|
|
|
return RemovedPackagesCount > 0 ? false : bWasSavingSuccessful;
|
|
}
|
|
|
|
// The operation will fail if no packages are there to save
|
|
return false;
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|