// Copyright Epic Games, Inc. All Rights Reserved. #include "StaticMeshEditorSubsystem.h" #include "ActorEditorUtils.h" #include "AssetRegistry/AssetRegistryModule.h" #include "Components/MeshComponent.h" #include "ContentBrowserModule.h" #include "Editor.h" #include "Editor/UnrealEdEngine.h" #include "EditorFramework/AssetImportData.h" #include "EngineUtils.h" #include "Engine/Brush.h" #include "Engine/Selection.h" #include "Engine/StaticMesh.h" #include "Engine/StaticMeshActor.h" #include "FbxMeshUtils.h" #include "FileHelpers.h" #include "GameFramework/Actor.h" #include "IContentBrowserSingleton.h" #include "IMeshMergeUtilities.h" #include "Kismet/GameplayStatics.h" #include "Kismet2/ComponentEditorUtils.h" #include "LevelEditorViewport.h" #include "Engine/MapBuildDataRegistry.h" #include "StaticMeshAttributes.h" #include "StaticMeshComponentLODInfo.h" #include "StaticMeshOperations.h" #include "MeshMergeModule.h" #include "PhysicsEngine/BodySetup.h" #include "ScopedTransaction.h" #include "Async/ParallelFor.h" #include "Algo/AllOf.h" #include "Algo/AnyOf.h" #include "Async/Async.h" #include "Misc/ScopedSlowTask.h" #include "Misc/FeedbackContext.h" #include "StaticMeshResources.h" #include "UnrealEdGlobals.h" #include "GeomFitUtils.h" #include "ConvexDecompTool.h" #include "Subsystems/AssetEditorSubsystem.h" #include "Subsystems/UnrealEditorSubsystem.h" #include "Layers/LayersSubsystem.h" #include "EditorScriptingHelpers.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(StaticMeshEditorSubsystem) #define LOCTEXT_NAMESPACE "StaticMeshEditorSubsystem" DEFINE_LOG_CATEGORY(LogStaticMeshEditorSubsystem); namespace InternalEditorMeshLibrary { /** Note: This method is a replicate of FStaticMeshEditor::DoDecomp */ bool GenerateConvexCollision(UStaticMesh* StaticMesh, uint32 HullCount, int32 MaxHullVerts, uint32 HullPrecision) { // Check we have a valid StaticMesh if (!StaticMesh || !StaticMesh->IsMeshDescriptionValid(0)) { return false; } TRACE_CPUPROFILER_EVENT_SCOPE(GenerateConvexCollision) // If RenderData has not been computed yet, do it if (!StaticMesh->GetRenderData()) { StaticMesh->CacheDerivedData(); } const FStaticMeshLODResources& LODModel = StaticMesh->GetRenderData()->LODResources[0]; // Make vertex buffer int32 NumVerts = LODModel.VertexBuffers.StaticMeshVertexBuffer.GetNumVertices(); TArray Verts; Verts.Reserve(NumVerts); for (int32 i = 0; i < NumVerts; i++) { Verts.Add(LODModel.VertexBuffers.PositionVertexBuffer.VertexPosition(i)); } // Grab all indices TArray AllIndices; LODModel.IndexBuffer.GetCopy(AllIndices); // Only copy indices that have collision enabled TArray CollidingIndices; for (const FStaticMeshSection& Section : LODModel.Sections) { if (Section.bEnableCollision) { for (uint32 IndexIdx = Section.FirstIndex; IndexIdx < Section.FirstIndex + (Section.NumTriangles * 3); IndexIdx++) { CollidingIndices.Add(AllIndices[IndexIdx]); } } } // Do not perform any action if we have invalid input if (Verts.Num() < 3 || CollidingIndices.Num() < 3) { return false; } // Get the BodySetup we are going to put the collision into UBodySetup* BodySetup = StaticMesh->GetBodySetup(); if (BodySetup) { BodySetup->RemoveSimpleCollision(); } else { // Otherwise, create one here. StaticMesh->CreateBodySetup(); BodySetup = StaticMesh->GetBodySetup(); } // Run actual util to do the work (if we have some valid input) DecomposeMeshToHulls(BodySetup, Verts, CollidingIndices, HullCount, MaxHullVerts, HullPrecision); StaticMesh->bCustomizedCollision = true; //mark the static mesh for collision customization return true; } bool IsUVChannelValid(UStaticMesh* StaticMesh, int32 LODIndex, int32 UVChannelIndex) { if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("The StaticMesh is null.")); return false; } if (LODIndex >= StaticMesh->GetNumLODs() || LODIndex < 0) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("The StaticMesh doesn't have LOD %d."), LODIndex); return false; } if (!StaticMesh->IsMeshDescriptionValid(LODIndex)) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("No mesh description for LOD %d."), LODIndex); return false; } int32 NumUVChannels = StaticMesh->GetNumUVChannels(LODIndex); if (UVChannelIndex < 0 || UVChannelIndex >= NumUVChannels) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("The given UV channel index %d is out of bounds."), UVChannelIndex); return false; } return true; } template int32 ReplaceMeshes(const ArrayType& Array, UStaticMesh* MeshToBeReplaced, UStaticMesh* NewMesh) { //Would use FObjectEditorUtils::SetPropertyValue, but meshes are a special case. They need a lock and we need to use the SetMesh function FProperty* StaticMeshProperty = FindFieldChecked(UStaticMeshComponent::StaticClass(), "StaticMesh"); TArray> ObjectsThatChanged; int32 NumberOfChanges = 0; for (UStaticMeshComponent* Component : Array) { const bool bIsClassDefaultObject = Component->HasAnyFlags(RF_ClassDefaultObject); if (!bIsClassDefaultObject) { if (Component->GetStaticMesh() == MeshToBeReplaced) { FEditPropertyChain PropertyChain; PropertyChain.AddHead(StaticMeshProperty); static_cast(Component)->PreEditChange(PropertyChain); // Set the mesh Component->SetStaticMesh(NewMesh); ++NumberOfChanges; ObjectsThatChanged.Add(Component); } } } // Route post edit change after all components have had their values changed. This is to avoid // construction scripts from re-running in the middle of setting values and wiping out components we need to modify for (UObject* ObjectData : ObjectsThatChanged) { FPropertyChangedEvent PropertyEvent(StaticMeshProperty); ObjectData->PostEditChangeProperty(PropertyEvent); } return NumberOfChanges; } template int32 ReplaceMaterials(ArrayType& Array, UMaterialInterface* MaterialToBeReplaced, UMaterialInterface* NewMaterial) { //Would use FObjectEditorUtils::SetPropertyValue, but Material are a special case. They need a lock and we need to use the SetMaterial function FProperty* MaterialProperty = FindFieldChecked(UMeshComponent::StaticClass(), GET_MEMBER_NAME_CHECKED(UMeshComponent, OverrideMaterials)); TArray> ObjectsThatChanged; int32 NumberOfChanges = 0; for (UMeshComponent* Component : Array) { const bool bIsClassDefaultObject = Component->HasAnyFlags(RF_ClassDefaultObject); if (!bIsClassDefaultObject) { const int32 NumberOfMaterial = Component->GetNumMaterials(); for (int32 Index = 0; Index < NumberOfMaterial; ++Index) { if (Component->GetMaterial(Index) == MaterialToBeReplaced) { FEditPropertyChain PropertyChain; PropertyChain.AddHead(MaterialProperty); static_cast(Component)->PreEditChange(PropertyChain); // Set the material Component->SetMaterial(Index, NewMaterial); ++NumberOfChanges; ObjectsThatChanged.Add(Component); } } } } // Route post edit change after all components have had their values changed. This is to avoid // construction scripts from re-running in the middle of setting values and wiping out components we need to modify for (UObject* ObjectData : ObjectsThatChanged) { FPropertyChangedEvent PropertyEvent(MaterialProperty); ObjectData->PostEditChangeProperty(PropertyEvent); } return NumberOfChanges; } template bool FindValidActorAndComponents(TArray ActorsToTest, TArray& OutValidActor, TArray& OutPrimitiveComponent, FVector& OutAverageLocation, FString& OutFailureReason) { for (int32 Index = ActorsToTest.Num() - 1; Index >= 0; --Index) { if (!IsValid(ActorsToTest[Index])) { ActorsToTest.RemoveAtSwap(Index); } } if (ActorsToTest.Num() == 0) { return false; } // All actors need to come from the same World UWorld* CurrentWorld = ActorsToTest[0]->GetWorld(); if (CurrentWorld == nullptr) { OutFailureReason = TEXT("The actors were not in a valid world."); return false; } if (CurrentWorld->WorldType != EWorldType::Editor && CurrentWorld->WorldType != EWorldType::EditorPreview) { OutFailureReason = TEXT("The actors were not in an editor world."); return false; } ULevel* CurrentLevel = ActorsToTest[0]->GetLevel(); if (CurrentLevel == nullptr) { OutFailureReason = TEXT("The actors were not in a valid level."); return false; } FVector PivotLocation = FVector::ZeroVector; OutPrimitiveComponent.Reset(ActorsToTest.Num()); OutValidActor.Reset(ActorsToTest.Num()); { bool bShowedDifferentLevelMessage = false; for (AStaticMeshActor* MeshActor : ActorsToTest) { if (MeshActor->GetWorld() != CurrentWorld) { OutFailureReason = TEXT("Some actors were not from the same world."); return false; } if (!bShowedDifferentLevelMessage && MeshActor->GetLevel() != CurrentLevel) { UE_LOG(LogStaticMeshEditorSubsystem, Log, TEXT("Not all actors are from the same level. The Actor will be created in the first level found.")); bShowedDifferentLevelMessage = true; } PivotLocation += MeshActor->GetActorLocation(); TInlineComponentArray ComponentArray; MeshActor->GetComponents(ComponentArray); bool bActorIsValid = false; for (UStaticMeshComponent* MeshCmp : ComponentArray) { if (MeshCmp->GetStaticMesh() && MeshCmp->GetStaticMesh()->GetRenderData()) { bActorIsValid = true; OutPrimitiveComponent.Add(MeshCmp); } } //Actor needs at least one StaticMeshComponent to be considered valid if (bActorIsValid) { OutValidActor.Add(MeshActor); } } } OutAverageLocation = PivotLocation / OutValidActor.Num(); return true; } FName GenerateValidOwnerBasedComponentNameForNewOwner(UStaticMeshComponent* OriginalComponent, AActor* NewOwner) { check(OriginalComponent); check(OriginalComponent->GetOwner()); check(NewOwner); //Find first valid name on new owner by incrementing internal index FName NewName = OriginalComponent->GetOwner()->GetFName(); const int32 InitialNumber = NewName.GetNumber(); while (FindObjectFast(NewOwner, NewName) != nullptr) { uint32 NextNumber = NewName.GetNumber(); if (NextNumber >= 0xfffffe) { NewName = NAME_None; break; } ++NextNumber; NewName.SetNumber(NextNumber); } return NewName; } } UStaticMeshEditorSubsystem::UStaticMeshEditorSubsystem() : UEditorSubsystem() { } int32 UStaticMeshEditorSubsystem::SetLodsWithNotification(UStaticMesh* StaticMesh, const FStaticMeshReductionOptions& ReductionOptions, bool bApplyChanges) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return -1; } if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetLODs: The StaticMesh is null.")); return -1; } // If LOD 0 does not exist, warn and return if (StaticMesh->GetNumSourceModels() == 0) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetLODs: This StaticMesh does not have LOD 0.")); return -1; } if (ReductionOptions.ReductionSettings.Num() == 0) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetLODs: Nothing done as no LOD settings were provided.")); return -1; } // Close the mesh editor to prevent crashing. If changes are applied, reopen it after the mesh has been built. bool bStaticMeshIsEdited = false; UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); if (AssetEditorSubsystem->FindEditorForAsset(StaticMesh, false)) { AssetEditorSubsystem->CloseAllEditorsForAsset(StaticMesh); bStaticMeshIsEdited = true; } if (bApplyChanges) { StaticMesh->Modify(); } // Resize array of LODs to only keep LOD 0 StaticMesh->SetNumSourceModels(1); // Set up LOD 0 StaticMesh->GetSourceModel(0).ReductionSettings.PercentTriangles = ReductionOptions.ReductionSettings[0].PercentTriangles; StaticMesh->GetSourceModel(0).ScreenSize = ReductionOptions.ReductionSettings[0].ScreenSize; int32 LODIndex = 1; for (; LODIndex < ReductionOptions.ReductionSettings.Num(); ++LODIndex) { // Create new SourceModel for new LOD FStaticMeshSourceModel& SrcModel = StaticMesh->AddSourceModel(); // Copy settings from previous LOD SrcModel.BuildSettings = StaticMesh->GetSourceModel(LODIndex - 1).BuildSettings; SrcModel.ReductionSettings = StaticMesh->GetSourceModel(LODIndex - 1).ReductionSettings; // Modify reduction settings based on user's requirements SrcModel.ReductionSettings.PercentTriangles = ReductionOptions.ReductionSettings[LODIndex].PercentTriangles; SrcModel.ScreenSize = ReductionOptions.ReductionSettings[LODIndex].ScreenSize; // Stop when reaching maximum of supported LODs if (StaticMesh->GetNumSourceModels() == MAX_STATIC_MESH_LODS) { break; } } StaticMesh->bAutoComputeLODScreenSize = ReductionOptions.bAutoComputeLODScreenSize ? 1 : 0; if (bApplyChanges) { // Request re-building of mesh with new LODs StaticMesh->PostEditChange(); // Reopen MeshEditor on this mesh if the MeshEditor was previously opened in it if (bStaticMeshIsEdited) { AssetEditorSubsystem->OpenEditorForAsset(StaticMesh); } } return LODIndex; } void UStaticMeshEditorSubsystem::GetLodReductionSettings(const UStaticMesh* StaticMesh, const int32 LodIndex, FMeshReductionSettings& OutReductionOptions) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return; } if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("GetLodReductionSettings: The StaticMesh is null.")); return; } // If LOD 0 does not exist, warn and return if (LodIndex < 0 || StaticMesh->GetNumSourceModels() <= LodIndex) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("GetLodReductionSettings: Invalid LOD index.")); return; } const FStaticMeshSourceModel& LODModel = StaticMesh->GetSourceModel(LodIndex); // Copy over the reduction settings OutReductionOptions = LODModel.ReductionSettings; } void UStaticMeshEditorSubsystem::SetLodReductionSettings(UStaticMesh* StaticMesh, const int32 LodIndex, const FMeshReductionSettings& ReductionOptions) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return; } if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetLodReductionSettings: The StaticMesh is null.")); return; } // If LOD 0 does not exist, warn and return if (LodIndex < 0 || StaticMesh->GetNumSourceModels() <= LodIndex) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetLodReductionSettings: Invalid LOD index.")); return; } // Close the mesh editor to prevent crashing. If changes are applied, reopen it after the mesh has been built. bool bStaticMeshIsEdited = false; UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); if (AssetEditorSubsystem->FindEditorForAsset(StaticMesh, false)) { AssetEditorSubsystem->CloseAllEditorsForAsset(StaticMesh); bStaticMeshIsEdited = true; } StaticMesh->Modify(); FStaticMeshSourceModel& LODModel = StaticMesh->GetSourceModel(LodIndex); // Copy over the reduction settings LODModel.ReductionSettings = ReductionOptions; // Request re-building of mesh with new LODs StaticMesh->PostEditChange(); // Reopen MeshEditor on this mesh if the MeshEditor was previously opened in it if (bStaticMeshIsEdited) { AssetEditorSubsystem->OpenEditorForAsset(StaticMesh); } } void UStaticMeshEditorSubsystem::GetLodBuildSettings(const UStaticMesh* StaticMesh, const int32 LodIndex, FMeshBuildSettings& OutBuildOptions) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return; } if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("GetLodBuildSettings: The StaticMesh is null.")); return; } // If LOD 0 does not exist, warn and return if (LodIndex < 0 || StaticMesh->GetNumSourceModels() <= LodIndex) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("GetLodBuildSettings: Invalid LOD index.")); return; } const FStaticMeshSourceModel& LODModel = StaticMesh->GetSourceModel(LodIndex); // Copy over the reduction settings OutBuildOptions = LODModel.BuildSettings; } void UStaticMeshEditorSubsystem::SetLodBuildSettings(UStaticMesh* StaticMesh, const int32 LodIndex, const FMeshBuildSettings& BuildOptions) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return; } if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetLodBuildSettings: The StaticMesh is null.")); return; } // If LOD 0 does not exist, warn and return if (LodIndex < 0 || StaticMesh->GetNumSourceModels() <= LodIndex) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetLodBuildSettings: Invalid LOD index.")); return; } // Close the mesh editor to prevent crashing. If changes are applied, reopen it after the mesh has been built. bool bStaticMeshIsEdited = false; UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); if (AssetEditorSubsystem->FindEditorForAsset(StaticMesh, false)) { AssetEditorSubsystem->CloseAllEditorsForAsset(StaticMesh); bStaticMeshIsEdited = true; } StaticMesh->Modify(); FStaticMeshSourceModel& LODModel = StaticMesh->GetSourceModel(LodIndex); // Copy over the build settings LODModel.BuildSettings = BuildOptions; // Request re-building of mesh with new LODs StaticMesh->PostEditChange(); // Reopen MeshEditor on this mesh if the MeshEditor was previously opened in it if (bStaticMeshIsEdited) { AssetEditorSubsystem->OpenEditorForAsset(StaticMesh); } } FName UStaticMeshEditorSubsystem::GetLODGroup(const UStaticMesh* StaticMesh) { if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("StaticMesh GetLODGroup: The StaticMesh is null.")); return NAME_None; } return StaticMesh->LODGroup; } bool UStaticMeshEditorSubsystem::SetLODGroup(UStaticMesh* StaticMesh, FName LODGroup, bool bRebuildImmediately) { if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("StaticMesh SetLODGroup: The StaticMesh is null.")); return false; } TArray LODGroups; UStaticMesh::GetLODGroups(LODGroups); if(LODGroups.Contains(LODGroup)) { GWarn->BeginSlowTask(FText::Format(FText::FromString("SetLODGroup: Applying changes to %s"), FText::FromString(StaticMesh->GetName())), true, false); // Close the mesh editor to prevent crashing. If changes are applied, reopen it after the mesh has been built. bool bStaticMeshIsEdited = false; UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); if (AssetEditorSubsystem->FindEditorForAsset(StaticMesh, false)) { AssetEditorSubsystem->CloseAllEditorsForAsset(StaticMesh); bStaticMeshIsEdited = true; } StaticMesh->SetLODGroup(LODGroup, bRebuildImmediately); GWarn->EndSlowTask(); return true; } UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetLODGroup: %s is not a valid LODGroup"), *LODGroup.ToString()); return false; } int32 UStaticMeshEditorSubsystem::ImportLOD(UStaticMesh* BaseStaticMesh, const int32 LODIndex, const FString& SourceFilename) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("StaticMesh ImportLOD: Cannot import or re-import when editor PIE is active.")); return INDEX_NONE; } if (BaseStaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("StaticMesh ImportLOD: The StaticMesh is null.")); return INDEX_NONE; } // Make sure the LODIndex we want to add the LOD is valid if (BaseStaticMesh->GetNumSourceModels() < LODIndex) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("StaticMesh ImportLOD: Invalid LODIndex, the LOD index cannot be greater the the number of LOD, static mesh cannot have hole in the LOD array.")); return INDEX_NONE; } FString ResolveFilename = SourceFilename; const bool bSourceFileExists = FPaths::FileExists(ResolveFilename); if (!bSourceFileExists) { if (BaseStaticMesh->IsSourceModelValid(LODIndex)) { const FStaticMeshSourceModel& SourceModel = BaseStaticMesh->GetSourceModel(LODIndex); ResolveFilename = SourceModel.SourceImportFilename.IsEmpty() ? SourceModel.SourceImportFilename : UAssetImportData::ResolveImportFilename(SourceModel.SourceImportFilename, nullptr); } } if (!FPaths::FileExists(ResolveFilename)) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("StaticMesh ImportLOD: Invalid source filename.")); return INDEX_NONE; } constexpr bool bAsyncFalse = false; if (!FbxMeshUtils::ImportStaticMeshLOD(BaseStaticMesh, ResolveFilename, LODIndex, bAsyncFalse)) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("StaticMesh ImportLOD: Cannot import mesh LOD.")); return INDEX_NONE; } GEditor->GetEditorSubsystem()->BroadcastAssetPostLODImport(BaseStaticMesh, LODIndex); return LODIndex; } bool UStaticMeshEditorSubsystem::ReimportAllCustomLODs(UStaticMesh* StaticMesh) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("StaticMesh ReimportAllCustomLODs: Cannot import or re-import when editor PIE is active.")); return false; } if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("StaticMesh ReimportAllCustomLODs: The StaticMesh is null.")); return false; } bool bResult = true; int32 LODNumber = StaticMesh->GetNumLODs(); //Iterate the static mesh LODs, start at index 1 for (int32 LODIndex = 1; LODIndex < LODNumber; ++LODIndex) { const FStaticMeshSourceModel& SourceModel = StaticMesh->GetSourceModel(LODIndex); //Skip LOD import in the same file as the base mesh, they are already re-import if (SourceModel.bImportWithBaseMesh) { continue; } bool bHasBeenSimplified = !StaticMesh->IsMeshDescriptionValid(LODIndex) || StaticMesh->IsReductionActive(LODIndex); if (bHasBeenSimplified) { continue; } if (ImportLOD(StaticMesh, LODIndex, SourceModel.SourceImportFilename) != LODIndex) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("StaticMesh ReimportAllCustomLODs: Cannot re-import LOD %d."), LODIndex); bResult = false; } } return bResult; } int32 UStaticMeshEditorSubsystem::SetLodFromStaticMesh(UStaticMesh* DestinationStaticMesh, int32 DestinationLodIndex, UStaticMesh* SourceStaticMesh, int32 SourceLodIndex, bool bReuseExistingMaterialSlots) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return -1; } if (DestinationStaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetLodFromStaticMesh: The DestinationStaticMesh is null.")); return -1; } if (SourceStaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetLodFromStaticMesh: The SourceStaticMesh is null.")); return -1; } if (!SourceStaticMesh->IsSourceModelValid(SourceLodIndex)) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetLodFromStaticMesh: SourceLodIndex is invalid.")); return -1; } // Close the mesh editor to prevent crashing. Reopen it after the mesh has been built. UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); bool bStaticMeshIsEdited = false; if (AssetEditorSubsystem->FindEditorForAsset(DestinationStaticMesh, false)) { AssetEditorSubsystem->CloseAllEditorsForAsset(DestinationStaticMesh); bStaticMeshIsEdited = true; } DestinationStaticMesh->Modify(); if (DestinationStaticMesh->GetNumSourceModels() < DestinationLodIndex + 1) { // Add one LOD DestinationStaticMesh->AddSourceModel(); DestinationLodIndex = DestinationStaticMesh->GetNumSourceModels() - 1; // The newly added SourceModel won't have a MeshDescription so create it explicitly DestinationStaticMesh->CreateMeshDescription(DestinationLodIndex); } // Transfers the build settings and the reduction settings. const FStaticMeshSourceModel& SourceMeshSourceModel = SourceStaticMesh->GetSourceModel(SourceLodIndex); FStaticMeshSourceModel& DestinationMeshSourceModel = DestinationStaticMesh->GetSourceModel(DestinationLodIndex); DestinationMeshSourceModel.BuildSettings = SourceMeshSourceModel.BuildSettings; DestinationMeshSourceModel.ReductionSettings = SourceMeshSourceModel.ReductionSettings; // Base the reduction on the new lod DestinationMeshSourceModel.ReductionSettings.BaseLODModel = DestinationLodIndex; bool bDoesSourceLodUseReduction = SourceStaticMesh->IsReductionActive(SourceLodIndex); int32 BaseSourceLodIndex = bDoesSourceLodUseReduction ? SourceMeshSourceModel.ReductionSettings.BaseLODModel : SourceLodIndex; bool bIsReductionSettingAproximated = false; // Find the original mesh description for this LOD while (!SourceStaticMesh->IsMeshDescriptionValid(BaseSourceLodIndex)) { if (!SourceStaticMesh->IsSourceModelValid(BaseSourceLodIndex)) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetLodFromStaticMesh: The SourceStaticMesh is in a invalid state.")); return -1; } const FMeshReductionSettings& PossibleSourceMeshReductionSetting = SourceStaticMesh->GetSourceModel(BaseSourceLodIndex).ReductionSettings; DestinationMeshSourceModel.ReductionSettings.PercentTriangles *= PossibleSourceMeshReductionSetting.PercentTriangles; DestinationMeshSourceModel.ReductionSettings.PercentVertices *= PossibleSourceMeshReductionSetting.PercentVertices; BaseSourceLodIndex = SourceStaticMesh->GetSourceModel(BaseSourceLodIndex).ReductionSettings.BaseLODModel; bIsReductionSettingAproximated = true; } if (bIsReductionSettingAproximated) { TArray InOrderedArguments; InOrderedArguments.Reserve(4); InOrderedArguments.Add(SourceStaticMesh->GetName()); InOrderedArguments.Add(SourceLodIndex); InOrderedArguments.Add(DestinationLodIndex); InOrderedArguments.Add(DestinationStaticMesh->GetName()); UE_LOG(LogStaticMeshEditorSubsystem, Warning, TEXT("%s"), *FString::Format(TEXT("SetLodFromStaticMesh: The reduction settings from the SourceStaticMesh {0} LOD {1} were approximated." " The LOD {2} from {3} might not be identical."), InOrderedArguments)); } // Copy the source import file. DestinationMeshSourceModel.SourceImportFilename = SourceStaticMesh->GetSourceModel(BaseSourceLodIndex).SourceImportFilename; // Copy the mesh description const FMeshDescription& SourceMeshDescription = *SourceStaticMesh->GetMeshDescription(BaseSourceLodIndex); FMeshDescription& DestinationMeshDescription = *DestinationStaticMesh->GetMeshDescription(DestinationLodIndex); DestinationMeshDescription = SourceMeshDescription; DestinationStaticMesh->CommitMeshDescription(DestinationLodIndex); // Assign materials for the destination LOD { auto FindMaterialIndex = [](UStaticMesh* StaticMesh, const UMaterialInterface* Material) -> int32 { for (int32 MaterialIndex = 0; MaterialIndex < StaticMesh->GetStaticMaterials().Num(); ++MaterialIndex) { if (StaticMesh->GetMaterial(MaterialIndex) == Material) { return MaterialIndex; } } return INDEX_NONE; }; TMap< int32, int32 > LodSectionMaterialMapping; // LOD section index -> destination material index int32 NumDestinationMaterial = DestinationStaticMesh->GetStaticMaterials().Num(); const int32 SourceLodNumSections = SourceStaticMesh->GetSectionInfoMap().GetSectionNumber(SourceLodIndex); for (int32 SourceLodSectionIndex = 0; SourceLodSectionIndex < SourceLodNumSections; ++SourceLodSectionIndex) { const FMeshSectionInfo& SourceMeshSectionInfo = SourceStaticMesh->GetSectionInfoMap().Get(SourceLodIndex, SourceLodSectionIndex); const UMaterialInterface* SourceMaterial = SourceStaticMesh->GetMaterial(SourceMeshSectionInfo.MaterialIndex); int32 DestinationMaterialIndex = INDEX_NONE; if (bReuseExistingMaterialSlots) { DestinationMaterialIndex = FindMaterialIndex(DestinationStaticMesh, SourceMaterial); } if (DestinationMaterialIndex == INDEX_NONE) { DestinationMaterialIndex = NumDestinationMaterial++; } LodSectionMaterialMapping.Add(SourceLodSectionIndex, DestinationMaterialIndex); } for (TMap< int32, int32 >::TConstIterator It = LodSectionMaterialMapping.CreateConstIterator(); It; ++It) { const int32 SectionIndex = It->Key; const FMeshSectionInfo& SourceSectionInfo = SourceStaticMesh->GetSectionInfoMap().Get(SourceLodIndex, SectionIndex); UMaterialInterface* SourceMaterial = SourceStaticMesh->GetMaterial(SourceSectionInfo.MaterialIndex); const int32 SourceMaterialIndex = SourceSectionInfo.MaterialIndex; const int32 DestinationMaterialIndex = It->Value; if (!DestinationStaticMesh->GetStaticMaterials().IsValidIndex(DestinationMaterialIndex)) { DestinationStaticMesh->GetStaticMaterials().Add(SourceStaticMesh->GetStaticMaterials()[SourceSectionInfo.MaterialIndex]); ensure(DestinationStaticMesh->GetStaticMaterials().Num() == DestinationMaterialIndex + 1); // We assume that we are not creating holes in StaticMaterials } FMeshSectionInfo DestinationSectionInfo = SourceSectionInfo; DestinationSectionInfo.MaterialIndex = DestinationMaterialIndex; DestinationStaticMesh->GetSectionInfoMap().Set(DestinationLodIndex, SectionIndex, MoveTemp(DestinationSectionInfo)); } } DestinationStaticMesh->PostEditChange(); // Reopen MeshEditor on this mesh if the MeshEditor was previously opened in it if (bStaticMeshIsEdited) { AssetEditorSubsystem->OpenEditorForAsset(DestinationStaticMesh); } return DestinationLodIndex; } int32 UStaticMeshEditorSubsystem::GetLodCount(const UStaticMesh* StaticMesh) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("GetLODCount: The StaticMesh is null.")); return -1; } if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return -1; } return StaticMesh->GetNumSourceModels(); } bool UStaticMeshEditorSubsystem::RemoveLods(UStaticMesh* StaticMesh) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("RemoveLODs: The StaticMesh is null.")); return false; } if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return false; } // No main LOD, skip if (StaticMesh->GetNumSourceModels() == 0) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("RemoveLODs: This StaticMesh does not have LOD 0.")); return false; } // Close the mesh editor to prevent crashing. Reopen it after the mesh has been built. UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); bool bStaticMeshIsEdited = false; if (AssetEditorSubsystem->FindEditorForAsset(StaticMesh, false)) { AssetEditorSubsystem->CloseAllEditorsForAsset(StaticMesh); bStaticMeshIsEdited = true; } // Reduce array of source models to 1 StaticMesh->Modify(); StaticMesh->SetNumSourceModels(1); // Request re-building of mesh with new LODs StaticMesh->PostEditChange(); // Reopen MeshEditor on this mesh if the MeshEditor was previously opened in it if (bStaticMeshIsEdited) { AssetEditorSubsystem->OpenEditorForAsset(StaticMesh); } return true; } TArray UStaticMeshEditorSubsystem::GetLodScreenSizes(const UStaticMesh* StaticMesh) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); TArray ScreenSizes; if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return ScreenSizes; } if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("GetLodScreenSizes: The StaticMesh is null.")); return ScreenSizes; } for (int i = 0; i < StaticMesh->GetNumLODs(); i++) { if (StaticMesh->GetRenderData()) { float CurScreenSize = StaticMesh->GetRenderData()->ScreenSize[i].GetValue(); ScreenSizes.Add(CurScreenSize); } else { UE_LOG(LogStaticMeshEditorSubsystem, Warning, TEXT("GetLodScreenSizes: The RenderData is invalid for LOD %d."), i); } } return ScreenSizes; } bool UStaticMeshEditorSubsystem::SetLodScreenSizes(UStaticMesh* StaticMesh, const TArray& ScreenSizes) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return false; } if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetLodScreenSizes: Input StaticMesh is null.")); return false; } FStaticMeshRenderData* RenderData = StaticMesh->GetRenderData(); if (RenderData == nullptr || (StaticMesh->GetNumLODs() == 0)) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetLodScreenSizes: Input StaticMesh is invalid (missing RenderData or meshes).")); return false; } if (ScreenSizes.Num() == 0) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetLodScreenSizes: Input ScreenSizes array is empty.")); return false; } // If not enough screen sizes, we set remainder to arbitrary monotonically decreasing defaults, and also ensure consecutive // values are monotonically decreasing, similar to what the user interface does when editing the values manually. if (ScreenSizes.Num() < StaticMesh->GetNumLODs()) { UE_LOG(LogStaticMeshEditorSubsystem, Warning, TEXT("SetLodScreenSizes: Only %d of %d ScreenSizes provided, remainder will be set to arbitrary defaults."), ScreenSizes.Num(), StaticMesh->GetNumLODs()); } const float MonotonicDifference = 0.0001f; // This difference value matches the user interface bool bSanitizationRequired = false; // Disable automatic screen size calculation, since we're providing manually overriden values StaticMesh->bAutoComputeLODScreenSize = 0; // Arbitrarily set this to a value that won't affect the monotonic clamping on the first iteration of the loop float LastScreenSize = ScreenSizes[0] + 2.0f * MonotonicDifference; for (int i = 0; i < StaticMesh->GetNumLODs(); i++) { float ScreenSizeForLOD; if (i < ScreenSizes.Num()) { ScreenSizeForLOD = FMath::Min(ScreenSizes[i], LastScreenSize - MonotonicDifference); } else { ScreenSizeForLOD = LastScreenSize - MonotonicDifference; } ScreenSizeForLOD = FMath::Max(ScreenSizeForLOD, 0.0f); // Track if the input values needed to be sanitized in any way, so we can warn the user this happened. if (i < ScreenSizes.Num() && ScreenSizeForLOD != ScreenSizes[i]) { bSanitizationRequired = true; } RenderData->ScreenSize[i].Default = ScreenSizeForLOD; StaticMesh->GetSourceModel(i).ScreenSize = ScreenSizeForLOD; LastScreenSize = ScreenSizeForLOD; } if (bSanitizationRequired) { UE_LOG(LogStaticMeshEditorSubsystem, Warning, TEXT("SetLodScreenSizes: Some input values were sanitized to be monotonic.")); } return true; } bool UStaticMeshEditorSubsystem::ImportNaniteHiResMesh(UStaticMesh* StaticMesh, const FString& SourceFilename, bool bShowDialogWhenFileMissing) { if (!StaticMesh) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("ImportNaniteHiResMesh: The StaticMesh is null.")); return false; } FStaticMeshSourceModel& HiResModel = StaticMesh->GetHiResSourceModel(); FString ResolveFilename = SourceFilename; if (ResolveFilename.IsEmpty()) { //Grab the existing source ResolveFilename = HiResModel.SourceImportFilename; } bool bSourceFileExists = FPaths::FileExists(ResolveFilename); if (!bSourceFileExists) { ResolveFilename = ResolveFilename.IsEmpty() ? ResolveFilename : UAssetImportData::ResolveImportFilename(ResolveFilename, nullptr); } bSourceFileExists = FPaths::FileExists(ResolveFilename); // Close the mesh editor to prevent crashing. Reopen it after the mesh has been built. UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); bool bStaticMeshIsEdited = false; if (AssetEditorSubsystem->FindEditorForAsset(StaticMesh, false)) { AssetEditorSubsystem->CloseAllEditorsForAsset(StaticMesh); bStaticMeshIsEdited = true; } const bool OriginalEnableNaniteValue = StaticMesh->NaniteSettings.bEnabled; StaticMesh->NaniteSettings.bEnabled = true; bool bResult = true; constexpr bool bAsyncFalse = false; if(bSourceFileExists) { if (!FbxMeshUtils::ImportStaticMeshHiResSourceModel(StaticMesh, ResolveFilename, bAsyncFalse)) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("ImportNaniteHiResMesh: Cannot import hi res mesh.")); bResult = false; } } else if (bShowDialogWhenFileMissing) { if (!FbxMeshUtils::ImportStaticMeshHiResSourceModelDialog(StaticMesh)) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("ImportNaniteHiResMesh: Cannot import hi res mesh with dialog.")); bResult = false; } } else { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("ImportNaniteHiResMesh: Cannot import hi res mesh, the provided file is invalid.")); bResult = false; } if (!bResult) { StaticMesh->NaniteSettings.bEnabled = OriginalEnableNaniteValue; } if (bStaticMeshIsEdited) { AssetEditorSubsystem->OpenEditorForAsset(StaticMesh); } return bResult; } FString UStaticMeshEditorSubsystem::GetNaniteSourceFilename(const UStaticMesh* StaticMesh) { FString ResolveFilename; if (!StaticMesh) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("GetNaniteSourceFilename: The StaticMesh is null.")); return ResolveFilename; } const FStaticMeshSourceModel& HiResModel = StaticMesh->GetHiResSourceModel(); ResolveFilename = HiResModel.SourceImportFilename; bool bSourceFileExists = FPaths::FileExists(ResolveFilename); if (!bSourceFileExists) { ResolveFilename = ResolveFilename.IsEmpty() ? ResolveFilename : UAssetImportData::ResolveImportFilename(ResolveFilename, nullptr); } return ResolveFilename; } bool UStaticMeshEditorSubsystem::UpdateNaniteSourceFilename(UStaticMesh* StaticMesh, const FString& NewSourceFilename) { bool bResult = false; FString ResolveFilename = NewSourceFilename; if (!StaticMesh) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("UpdateNaniteSourceFilename: The StaticMesh is null.")); return bResult; } if (!FPaths::FileExists(ResolveFilename)) { ResolveFilename = ResolveFilename.IsEmpty() ? ResolveFilename : UAssetImportData::ResolveImportFilename(ResolveFilename, nullptr); } if (!StaticMesh->GetHiResSourceModel().SourceImportFilename.Equals(ResolveFilename)) { if (ResolveFilename.IsEmpty()) { FbxMeshUtils::RemoveStaticMeshHiRes(StaticMesh); bResult = true; } else if(FPaths::FileExists(ResolveFilename)) { //We have a valid path, update it and re-import the nanite mesh StaticMesh->GetHiResSourceModel().SourceImportFilename = ResolveFilename; ImportNaniteHiResMesh(StaticMesh, ResolveFilename, false); bResult = true; } else { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("UpdateNaniteSourceFilename: The new source filename is invalid (it can be empty or must point to an existing filename).")); } } return bResult; } FMeshNaniteSettings UStaticMeshEditorSubsystem::GetNaniteSettings(const UStaticMesh* StaticMesh) { if (StaticMesh) { return StaticMesh->NaniteSettings; } else { FFrame::KismetExecutionMessage(TEXT("Cannot call GetNaniteSettings without a static mesh"), ELogVerbosity::Error); return FMeshNaniteSettings(); } } void UStaticMeshEditorSubsystem::SetNaniteSettings(UStaticMesh* StaticMesh, FMeshNaniteSettings NaniteSettings, bool bApplyChanges) { if (!StaticMesh) { FFrame::KismetExecutionMessage(TEXT("Cannot call SetNaniteSettings without a static mesh"), ELogVerbosity::Error); return; } // Close the mesh editor to prevent crashing. Reopen it after the mesh has been built. UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); bool bStaticMeshIsEdited = false; if (AssetEditorSubsystem->FindEditorForAsset(StaticMesh, false)) { AssetEditorSubsystem->CloseAllEditorsForAsset(StaticMesh); bStaticMeshIsEdited = true; } StaticMesh->Modify(); StaticMesh->NaniteSettings = NaniteSettings; if (bApplyChanges) { // Request re-building of mesh with new collision shapes StaticMesh->PostEditChange(); // Reopen MeshEditor on this mesh if the MeshEditor was previously opened in it if (bStaticMeshIsEdited) { AssetEditorSubsystem->OpenEditorForAsset(StaticMesh); } } } int32 UStaticMeshEditorSubsystem::AddSimpleCollisionsWithNotification(UStaticMesh* StaticMesh, const EScriptCollisionShapeType ShapeType, bool bApplyChanges) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("AddSimpleCollisions: The StaticMesh is null.")); return INDEX_NONE; } if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return INDEX_NONE; } // Close the mesh editor to prevent crashing. Reopen it after the mesh has been built. UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); bool bStaticMeshIsEdited = false; if (AssetEditorSubsystem->FindEditorForAsset(StaticMesh, false)) { AssetEditorSubsystem->CloseAllEditorsForAsset(StaticMesh); bStaticMeshIsEdited = true; } int32 PrimIndex = INDEX_NONE; switch (ShapeType) { case EScriptCollisionShapeType::Box: { PrimIndex = GenerateBoxAsSimpleCollision(StaticMesh); if(PrimIndex != INDEX_NONE) { StaticMesh->GetBodySetup()->AggGeom.BoxElems[PrimIndex].bIsGenerated = true; } break; } case EScriptCollisionShapeType::Sphere: { PrimIndex = GenerateSphereAsSimpleCollision(StaticMesh); if(PrimIndex != INDEX_NONE) { StaticMesh->GetBodySetup()->AggGeom.SphereElems[PrimIndex].bIsGenerated = true; } break; } case EScriptCollisionShapeType::Capsule: { PrimIndex = GenerateSphylAsSimpleCollision(StaticMesh); if(PrimIndex != INDEX_NONE) { StaticMesh->GetBodySetup()->AggGeom.SphylElems[PrimIndex].bIsGenerated = true; } break; } case EScriptCollisionShapeType::NDOP10_X: { TArray DirArray(KDopDir10X, 10); PrimIndex = GenerateKDopAsSimpleCollision(StaticMesh, DirArray); if(PrimIndex != INDEX_NONE) { StaticMesh->GetBodySetup()->AggGeom.ConvexElems[PrimIndex].bIsGenerated = true; } break; } case EScriptCollisionShapeType::NDOP10_Y: { TArray DirArray(KDopDir10Y, 10); PrimIndex = GenerateKDopAsSimpleCollision(StaticMesh, DirArray); if(PrimIndex != INDEX_NONE) { StaticMesh->GetBodySetup()->AggGeom.ConvexElems[PrimIndex].bIsGenerated = true; } break; } case EScriptCollisionShapeType::NDOP10_Z: { TArray DirArray(KDopDir10Z, 10); PrimIndex = GenerateKDopAsSimpleCollision(StaticMesh, DirArray); if(PrimIndex != INDEX_NONE) { StaticMesh->GetBodySetup()->AggGeom.ConvexElems[PrimIndex].bIsGenerated = true; } break; } case EScriptCollisionShapeType::NDOP18: { TArray DirArray(KDopDir18, 18); PrimIndex = GenerateKDopAsSimpleCollision(StaticMesh, DirArray); if(PrimIndex != INDEX_NONE) { StaticMesh->GetBodySetup()->AggGeom.ConvexElems[PrimIndex].bIsGenerated = true; } break; } case EScriptCollisionShapeType::NDOP26: { TArray DirArray(KDopDir26, 26); PrimIndex = GenerateKDopAsSimpleCollision(StaticMesh, DirArray); if(PrimIndex != INDEX_NONE) { StaticMesh->GetBodySetup()->AggGeom.ConvexElems[PrimIndex].bIsGenerated = true; } break; } } if (bApplyChanges) { // Request re-building of mesh with new collision shapes StaticMesh->PostEditChange(); // Reopen MeshEditor on this mesh if the MeshEditor was previously opened in it if (bStaticMeshIsEdited) { AssetEditorSubsystem->OpenEditorForAsset(StaticMesh); } } return PrimIndex; } int32 UStaticMeshEditorSubsystem::GetSimpleCollisionCount(const UStaticMesh* StaticMesh) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("GetSimpleCollisionCount: The StaticMesh is null.")); return -1; } if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return -1; } UBodySetup* BodySetup = StaticMesh->GetBodySetup(); if (BodySetup == nullptr) { return 0; } int32 Count = BodySetup->AggGeom.BoxElems.Num(); Count += BodySetup->AggGeom.SphereElems.Num(); Count += BodySetup->AggGeom.SphylElems.Num(); return Count; } TEnumAsByte UStaticMeshEditorSubsystem::GetCollisionComplexity(const UStaticMesh* StaticMesh) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("GetCollisionComplexity: The StaticMesh is null.")); return ECollisionTraceFlag::CTF_UseDefault; } if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return ECollisionTraceFlag::CTF_UseDefault; } if (StaticMesh->GetBodySetup()) { return StaticMesh->GetBodySetup()->CollisionTraceFlag; } return ECollisionTraceFlag::CTF_UseDefault; } int32 UStaticMeshEditorSubsystem::GetConvexCollisionCount(const UStaticMesh* StaticMesh) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("GetConvexCollisionCount: The StaticMesh is null.")); return -1; } if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return -1; } UBodySetup* BodySetup = StaticMesh->GetBodySetup(); if (BodySetup == nullptr) { return 0; } return BodySetup->AggGeom.ConvexElems.Num(); } bool UStaticMeshEditorSubsystem::BulkSetConvexDecompositionCollisionsWithNotification(const TArray& InStaticMeshes, int32 HullCount, int32 MaxHullVerts, int32 HullPrecision, bool bApplyChanges) { TRACE_CPUPROFILER_EVENT_SCOPE(UStaticMeshEditorSubsystem::SetConvexDecompositionCollisionsWithNotification) TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return false; } TArray StaticMeshes(InStaticMeshes); StaticMeshes.RemoveAll([](const UStaticMesh* StaticMesh) { return StaticMesh == nullptr || !StaticMesh->IsMeshDescriptionValid(0); }); if (StaticMeshes.Num() == 0) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetConvexDecompositionCollisions: The StaticMesh is null.")); return false; } if (HullCount < 0 || HullPrecision < 0) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetConvexDecompositionCollisions: Parameters HullCount and HullPrecision must be positive.")); return false; } if (Algo::AnyOf(StaticMeshes, [](const UStaticMesh* StaticMesh) { return StaticMesh->GetRenderData() == nullptr; })) { UStaticMesh::BatchBuild(StaticMeshes); } Algo::SortBy( StaticMeshes, [](const UStaticMesh* StaticMesh) { return StaticMesh->GetRenderData()->LODResources[0].VertexBuffers.StaticMeshVertexBuffer.GetNumVertices(); }, TGreater<>() ); // Close the mesh editor to prevent crashing. Reopen it after the mesh has been built. UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); TSet EditedStaticMeshes; for (UStaticMesh* StaticMesh : StaticMeshes) { if (AssetEditorSubsystem->FindEditorForAsset(StaticMesh, false)) { AssetEditorSubsystem->CloseAllEditorsForAsset(StaticMesh); EditedStaticMeshes.Add(StaticMesh); } if (StaticMesh->GetBodySetup()) { if (bApplyChanges) { StaticMesh->GetBodySetup()->Modify(); } // Remove simple collisions StaticMesh->GetBodySetup()->RemoveSimpleCollision(); } } TArray bResults; bResults.SetNumZeroed(StaticMeshes.Num()); TAtomic Processed(0); TFuture Result = Async( EAsyncExecution::ThreadPool, [&Processed, &bResults, &StaticMeshes, HullCount, MaxHullVerts, HullPrecision]() { ParallelFor( StaticMeshes.Num(), [&Processed, &bResults, &StaticMeshes, HullCount, MaxHullVerts, HullPrecision](int32 Index) { bResults[Index] = InternalEditorMeshLibrary::GenerateConvexCollision(StaticMeshes[Index], HullCount, MaxHullVerts, HullPrecision); Processed++; }, EParallelForFlags::Unbalanced ); } ); uint32 LastProcessed = 0; const FText ProgressText = LOCTEXT("ComputingConvexCollision", "Computing convex collision for static mesh {0}/{1} ..."); FScopedSlowTask Progress(static_cast(StaticMeshes.Num()), FText::Format(ProgressText, LastProcessed, StaticMeshes.Num())); Progress.MakeDialog(); while (!Result.WaitFor(FTimespan::FromMilliseconds(33.0))) { uint32 LocalProcessed = Processed.Load(EMemoryOrder::Relaxed); Progress.EnterProgressFrame(static_cast(LocalProcessed - LastProcessed), FText::Format(ProgressText, LocalProcessed, StaticMeshes.Num())); LastProcessed = LocalProcessed; } // refresh collision change back to static mesh components RefreshCollisionChanges(StaticMeshes); if (bApplyChanges) { for (UStaticMesh* StaticMesh : StaticMeshes) { // Mark mesh as dirty StaticMesh->MarkPackageDirty(); // Request re-building of mesh following collision changes StaticMesh->PostEditChange(); } } // Reopen MeshEditor on this mesh if the MeshEditor was previously opened in it for (UStaticMesh* StaticMesh : EditedStaticMeshes) { AssetEditorSubsystem->OpenEditorForAsset(StaticMesh); } return Algo::AllOf(bResults); } bool UStaticMeshEditorSubsystem::SetConvexDecompositionCollisionsWithNotification(UStaticMesh* StaticMesh, int32 HullCount, int32 MaxHullVerts, int32 HullPrecision, bool bApplyChanges) { return BulkSetConvexDecompositionCollisionsWithNotification({ StaticMesh }, HullCount, MaxHullVerts, HullPrecision, bApplyChanges); } bool UStaticMeshEditorSubsystem::RemoveCollisionsWithNotification(UStaticMesh* StaticMesh, bool bApplyChanges) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return false; } if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("RemoveCollisions: The StaticMesh is null.")); return false; } if (StaticMesh->GetBodySetup() == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Log, TEXT("RemoveCollisions: No collision set up. Nothing to do.")); return true; } // Close the mesh editor to prevent crashing. Reopen it after the mesh has been built. UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); bool bStaticMeshIsEdited = false; if (AssetEditorSubsystem->FindEditorForAsset(StaticMesh, false)) { AssetEditorSubsystem->CloseAllEditorsForAsset(StaticMesh); bStaticMeshIsEdited = true; } if (bApplyChanges) { StaticMesh->GetBodySetup()->Modify(); } // Remove simple collisions StaticMesh->GetBodySetup()->RemoveSimpleCollision(); // refresh collision change back to static mesh components RefreshCollisionChange(*StaticMesh); if (bApplyChanges) { // Request re-building of mesh with new collision shapes StaticMesh->PostEditChange(); // Reopen MeshEditor on this mesh if the MeshEditor was previously opened in it if (bStaticMeshIsEdited) { AssetEditorSubsystem->OpenEditorForAsset(StaticMesh); } } return true; } bool UStaticMeshEditorSubsystem::CanModifyStaticMeshSection(const UStaticMesh* StaticMesh, int32 LODIndex, int32 SectionIndex, const FString& LogFunctionName) { if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return false; } if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("%s: The StaticMesh is null."), *LogFunctionName); return false; } if (LODIndex >= StaticMesh->GetNumLODs()) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("%s: Invalid LOD index %d (of %d)."), *LogFunctionName, LODIndex, StaticMesh->GetNumLODs()); return false; } if (SectionIndex >= StaticMesh->GetNumSections(LODIndex)) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("%s: Invalid section index %d (of %d)."), *LogFunctionName, SectionIndex, StaticMesh->GetNumSections(LODIndex)); return false; } return true; } void UStaticMeshEditorSubsystem::EnableSectionCastShadow(UStaticMesh* StaticMesh, bool bCastShadow, int32 LODIndex, int32 SectionIndex) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (CanModifyStaticMeshSection(StaticMesh, LODIndex, SectionIndex, "EnableSectionCastShadow")) { StaticMesh->Modify(); FMeshSectionInfo SectionInfo = StaticMesh->GetSectionInfoMap().Get(LODIndex, SectionIndex); SectionInfo.bCastShadow = bCastShadow; StaticMesh->GetSectionInfoMap().Set(LODIndex, SectionIndex, SectionInfo); StaticMesh->PostEditChange(); } } bool UStaticMeshEditorSubsystem::IsSectionCastShadowEnabled(const UStaticMesh* StaticMesh, int32 LODIndex, int32 SectionIndex) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (CanModifyStaticMeshSection(StaticMesh, LODIndex, SectionIndex, "IsSectionCastShadowEnabled")) { FMeshSectionInfo SectionInfo = StaticMesh->GetSectionInfoMap().Get(LODIndex, SectionIndex); return SectionInfo.bCastShadow; } return false; } void UStaticMeshEditorSubsystem::EnableSectionCollision(UStaticMesh* StaticMesh, bool bCollisionEnabled, int32 LODIndex, int32 SectionIndex) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (CanModifyStaticMeshSection(StaticMesh, LODIndex, SectionIndex, "EnableSectionCollision")) { StaticMesh->Modify(); FMeshSectionInfo SectionInfo = StaticMesh->GetSectionInfoMap().Get(LODIndex, SectionIndex); SectionInfo.bEnableCollision = bCollisionEnabled; StaticMesh->GetSectionInfoMap().Set(LODIndex, SectionIndex, SectionInfo); StaticMesh->PostEditChange(); } } bool UStaticMeshEditorSubsystem::IsSectionCollisionEnabled(const UStaticMesh* StaticMesh, int32 LODIndex, int32 SectionIndex) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (CanModifyStaticMeshSection(StaticMesh, LODIndex, SectionIndex, "IsSectionCollisionEnabled")) { FMeshSectionInfo SectionInfo = StaticMesh->GetSectionInfoMap().Get(LODIndex, SectionIndex); return SectionInfo.bEnableCollision; } return false; } void UStaticMeshEditorSubsystem::EnableSectionVisibleInRayTracing(UStaticMesh* StaticMesh, bool bVisibleInRayTracing, int32 LODIndex, int32 SectionIndex) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (CanModifyStaticMeshSection(StaticMesh, LODIndex, SectionIndex, "EnableSectionVisibleInRayTracing")) { StaticMesh->Modify(); FMeshSectionInfo SectionInfo = StaticMesh->GetSectionInfoMap().Get(LODIndex, SectionIndex); SectionInfo.bVisibleInRayTracing = bVisibleInRayTracing; StaticMesh->GetSectionInfoMap().Set(LODIndex, SectionIndex, SectionInfo); StaticMesh->PostEditChange(); } } bool UStaticMeshEditorSubsystem::IsSectionVisibleInRayTracingEnabled(const UStaticMesh* StaticMesh, int32 LODIndex, int32 SectionIndex) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (CanModifyStaticMeshSection(StaticMesh, LODIndex, SectionIndex, "IsSectionVisibleInRayTracingEnabled")) { FMeshSectionInfo SectionInfo = StaticMesh->GetSectionInfoMap().Get(LODIndex, SectionIndex); return SectionInfo.bVisibleInRayTracing; } return false; } void UStaticMeshEditorSubsystem::EnableSectionAffectDistanceFieldLighting(UStaticMesh* StaticMesh, bool bAffectDistanceFieldLighting, int32 LODIndex, int32 SectionIndex) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (CanModifyStaticMeshSection(StaticMesh, LODIndex, SectionIndex, "EnableSectionAffectDistanceFieldLighting")) { StaticMesh->Modify(); FMeshSectionInfo SectionInfo = StaticMesh->GetSectionInfoMap().Get(LODIndex, SectionIndex); SectionInfo.bAffectDistanceFieldLighting = bAffectDistanceFieldLighting; StaticMesh->GetSectionInfoMap().Set(LODIndex, SectionIndex, SectionInfo); StaticMesh->PostEditChange(); } } bool UStaticMeshEditorSubsystem::IsSectionAffectDistanceFieldLightingEnabled(const UStaticMesh* StaticMesh, int32 LODIndex, int32 SectionIndex) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (CanModifyStaticMeshSection(StaticMesh, LODIndex, SectionIndex, "IsSectionAffectDistanceFieldLightingEnabled")) { FMeshSectionInfo SectionInfo = StaticMesh->GetSectionInfoMap().Get(LODIndex, SectionIndex); return SectionInfo.bAffectDistanceFieldLighting; } return false; } void UStaticMeshEditorSubsystem::EnableSectionForceOpaque(UStaticMesh* StaticMesh, bool bForceOpaque, int32 LODIndex, int32 SectionIndex) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (CanModifyStaticMeshSection(StaticMesh, LODIndex, SectionIndex, "EnableSectionForceOpaque")) { StaticMesh->Modify(); FMeshSectionInfo SectionInfo = StaticMesh->GetSectionInfoMap().Get(LODIndex, SectionIndex); SectionInfo.bForceOpaque = bForceOpaque; StaticMesh->GetSectionInfoMap().Set(LODIndex, SectionIndex, SectionInfo); StaticMesh->PostEditChange(); } } bool UStaticMeshEditorSubsystem::IsSectionForceOpaqueEnabled(const UStaticMesh* StaticMesh, int32 LODIndex, int32 SectionIndex) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (CanModifyStaticMeshSection(StaticMesh, LODIndex, SectionIndex, "IsSectionForceOpaqueEnabled")) { FMeshSectionInfo SectionInfo = StaticMesh->GetSectionInfoMap().Get(LODIndex, SectionIndex); return SectionInfo.bForceOpaque; } return false; } void UStaticMeshEditorSubsystem::SetLODMaterialSlot(UStaticMesh* StaticMesh, int32 MaterialSlotIndex, int32 LODIndex, int32 SectionIndex) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return; } if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetLODMaterialSlot: The StaticMesh is null.")); return; } if (LODIndex >= StaticMesh->GetNumLODs()) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetLODMaterialSlot: Invalid LOD index %d (of %d)."), LODIndex, StaticMesh->GetNumLODs()); return; } if (SectionIndex >= StaticMesh->GetNumSections(LODIndex)) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetLODMaterialSlot: Invalid section index %d (of %d)."), SectionIndex, StaticMesh->GetNumSections(LODIndex)); return; } if (MaterialSlotIndex >= StaticMesh->GetStaticMaterials().Num()) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetLODMaterialSlot: Invalid slot index %d (of %d)."), MaterialSlotIndex, StaticMesh->GetStaticMaterials().Num()); return; } StaticMesh->Modify(); FMeshSectionInfo SectionInfo = StaticMesh->GetSectionInfoMap().Get(LODIndex, SectionIndex); SectionInfo.MaterialIndex = MaterialSlotIndex; StaticMesh->GetSectionInfoMap().Set(LODIndex, SectionIndex, SectionInfo); StaticMesh->PostEditChange(); } int32 UStaticMeshEditorSubsystem::GetLODMaterialSlot(const UStaticMesh* StaticMesh, int32 LODIndex, int32 SectionIndex ) { TGuardValue UnattendedScriptGuard( GIsRunningUnattendedScript, true ); if ( !EditorScriptingHelpers::CheckIfInEditorAndPIE() ) { return INDEX_NONE; } if ( StaticMesh == nullptr ) { UE_LOG( LogStaticMeshEditorSubsystem, Error, TEXT( "GetLODMaterialSlot: The StaticMesh is null." ) ); return INDEX_NONE; } if ( LODIndex >= StaticMesh->GetNumLODs() ) { UE_LOG( LogStaticMeshEditorSubsystem, Error, TEXT( "GetLODMaterialSlot: Invalid LOD index %d (of %d)." ), LODIndex, StaticMesh->GetNumLODs() ); return INDEX_NONE; } if ( SectionIndex >= StaticMesh->GetNumSections( LODIndex ) ) { UE_LOG( LogStaticMeshEditorSubsystem, Error, TEXT( "GetLODMaterialSlot: Invalid section index %d (of %d)." ), SectionIndex, StaticMesh->GetNumSections( LODIndex ) ); return INDEX_NONE; } return StaticMesh->GetSectionInfoMap().Get( LODIndex, SectionIndex ).MaterialIndex; } bool UStaticMeshEditorSubsystem::HasVertexColors(const UStaticMesh* StaticMesh) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return false; } if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("HasVertexColors: The StaticMesh is null.")); return false; } for (int32 LodIndex = 0; LodIndex < StaticMesh->GetNumSourceModels(); ++LodIndex) { const FMeshDescription* MeshDescription = StaticMesh->GetMeshDescription(LodIndex); FStaticMeshConstAttributes Attributes(*MeshDescription); TVertexInstanceAttributesConstRef VertexInstanceColors = Attributes.GetVertexInstanceColors(); if (!VertexInstanceColors.IsValid()) { continue; } for (const FVertexInstanceID VertexInstanceID : MeshDescription->VertexInstances().GetElementIDs()) { FLinearColor VertexInstanceColor(VertexInstanceColors[VertexInstanceID]); if (VertexInstanceColor != FLinearColor::White) { return true; } } } return false; } bool UStaticMeshEditorSubsystem::HasInstanceVertexColors(const UStaticMeshComponent* StaticMeshComponent) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return false; } if (StaticMeshComponent == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("HasInstanceVertexColors: The StaticMeshComponent is null.")); return false; } for (const FStaticMeshComponentLODInfo& CurrentLODInfo : StaticMeshComponent->LODData) { if (CurrentLODInfo.OverrideVertexColors != nullptr || CurrentLODInfo.PaintedVertices.Num() > 0) { return true; } } return false; } bool UStaticMeshEditorSubsystem::SetGenerateLightmapUVs(UStaticMesh* StaticMesh, bool bGenerateLightmapUVs) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return false; } if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetGenerateLightmapUVs: The StaticMesh is null.")); return false; } bool AnySettingsToChange = false; for (int32 LodIndex = 0; LodIndex < StaticMesh->GetNumSourceModels(); ++LodIndex) { FStaticMeshSourceModel& SourceModel = StaticMesh->GetSourceModel(LodIndex); //Make sure LOD is not a reduction before considering its BuildSettings if (StaticMesh->IsMeshDescriptionValid(LodIndex)) { AnySettingsToChange = (SourceModel.BuildSettings.bGenerateLightmapUVs != bGenerateLightmapUVs); if (AnySettingsToChange) { break; } } } if (AnySettingsToChange) { StaticMesh->Modify(); for (int32 LodIndex = 0; LodIndex < StaticMesh->GetNumSourceModels(); LodIndex++) { StaticMesh->GetSourceModel(LodIndex).BuildSettings.bGenerateLightmapUVs = bGenerateLightmapUVs; } StaticMesh->Build(); StaticMesh->PostEditChange(); return true; } return false; } int32 UStaticMeshEditorSubsystem::GetNumberVerts(const UStaticMesh* StaticMesh, int32 LODIndex) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return 0; } if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("GetNumberVerts: The StaticMesh is null.")); return 0; } return StaticMesh->GetNumVertices(LODIndex); } int32 UStaticMeshEditorSubsystem::GetNumberMaterials(const UStaticMesh* StaticMesh) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return 0; } if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("GetNumberMaterials: The StaticMesh is null.")); return 0; } return StaticMesh->GetStaticMaterials().Num(); } void UStaticMeshEditorSubsystem::SetAllowCPUAccess(UStaticMesh* StaticMesh, bool bAllowCPUAccess) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return; } if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("SetAllowCPUAccess: The StaticMesh is null.")); return; } StaticMesh->Modify(); StaticMesh->bAllowCPUAccess = bAllowCPUAccess; StaticMesh->PostEditChange(); } int32 UStaticMeshEditorSubsystem::GetNumUVChannels(const UStaticMesh* StaticMesh, int32 LODIndex) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return 0; } if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("GetNumUVChannels: The StaticMesh is null.")); return 0; } if (LODIndex >= StaticMesh->GetNumLODs() || LODIndex < 0) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("GetNumUVChannels: The StaticMesh doesn't have LOD %d."), LODIndex); return 0; } return StaticMesh->GetNumUVChannels(LODIndex); } bool UStaticMeshEditorSubsystem::AddUVChannel(UStaticMesh* StaticMesh, int32 LODIndex) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return false; } if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("AddUVChannel: The StaticMesh is null.")); return false; } if (LODIndex >= StaticMesh->GetNumLODs() || LODIndex < 0) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("AddUVChannel: The StaticMesh doesn't have LOD %d."), LODIndex); return false; } if (StaticMesh->GetNumUVChannels(LODIndex) >= MAX_MESH_TEXTURE_COORDS_MD) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("AddUVChannel: Cannot add UV channel. Maximum number of UV channels reached (%d)."), MAX_MESH_TEXTURE_COORDS_MD); return false; } return StaticMesh->AddUVChannel(LODIndex); } bool UStaticMeshEditorSubsystem::InsertUVChannel(UStaticMesh* StaticMesh, int32 LODIndex, int32 UVChannelIndex) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return false; } if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("InsertUVChannel: The StaticMesh is null.")); return false; } if (LODIndex >= StaticMesh->GetNumLODs() || LODIndex < 0) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("InsertUVChannel: The StaticMesh doesn't have LOD %d."), LODIndex); return false; } int32 NumUVChannels = StaticMesh->GetNumUVChannels(LODIndex); if (UVChannelIndex < 0 || UVChannelIndex > NumUVChannels) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("InsertUVChannel: Cannot insert UV channel. Given UV channel index %d is out of bounds."), UVChannelIndex); return false; } if (NumUVChannels >= MAX_MESH_TEXTURE_COORDS_MD) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("InsertUVChannel: Cannot add UV channel. Maximum number of UV channels reached (%d)."), MAX_MESH_TEXTURE_COORDS_MD); return false; } return StaticMesh->InsertUVChannel(LODIndex, UVChannelIndex); } bool UStaticMeshEditorSubsystem::RemoveUVChannel(UStaticMesh* StaticMesh, int32 LODIndex, int32 UVChannelIndex) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return false; } if (StaticMesh == nullptr) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("RemoveUVChannel: The StaticMesh is null.")); return false; } if (LODIndex >= StaticMesh->GetNumLODs() || LODIndex < 0) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("RemoveUVChannel: The StaticMesh doesn't have LOD %d."), LODIndex); return false; } int32 NumUVChannels = StaticMesh->GetNumUVChannels(LODIndex); if (NumUVChannels == 1) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("RemoveUVChannel: Cannot remove UV channel. There must be at least one channel.")); return false; } if (UVChannelIndex < 0 || UVChannelIndex >= NumUVChannels) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("RemoveUVChannel: Cannot remove UV channel. Given UV channel index %d is out of bounds."), UVChannelIndex); return false; } return StaticMesh->RemoveUVChannel(LODIndex, UVChannelIndex); } bool UStaticMeshEditorSubsystem::GeneratePlanarUVChannel(UStaticMesh* StaticMesh, int32 LODIndex, int32 UVChannelIndex, const FVector& Position, const FRotator& Orientation, const FVector2D& Tiling) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return false; } if (!InternalEditorMeshLibrary::IsUVChannelValid(StaticMesh, LODIndex, UVChannelIndex)) { return false; } FMeshDescription* MeshDescription = StaticMesh->GetMeshDescription(LODIndex); FUVMapParameters UVParameters(Position, Orientation.Quaternion(), StaticMesh->GetBoundingBox().GetSize(), FVector::OneVector, Tiling); TMap TexCoords; FStaticMeshOperations::GeneratePlanarUV(*MeshDescription, UVParameters, TexCoords); return StaticMesh->SetUVChannel(LODIndex, UVChannelIndex, TexCoords); } bool UStaticMeshEditorSubsystem::GenerateCylindricalUVChannel(UStaticMesh* StaticMesh, int32 LODIndex, int32 UVChannelIndex, const FVector& Position, const FRotator& Orientation, const FVector2D& Tiling) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return false; } if (!InternalEditorMeshLibrary::IsUVChannelValid(StaticMesh, LODIndex, UVChannelIndex)) { return false; } FMeshDescription* MeshDescription = StaticMesh->GetMeshDescription(LODIndex); FUVMapParameters UVParameters(Position, Orientation.Quaternion(), StaticMesh->GetBoundingBox().GetSize(), FVector::OneVector, Tiling); TMap TexCoords; FStaticMeshOperations::GenerateCylindricalUV(*MeshDescription, UVParameters, TexCoords); return StaticMesh->SetUVChannel(LODIndex, UVChannelIndex, TexCoords); } bool UStaticMeshEditorSubsystem::GenerateBoxUVChannel(UStaticMesh* StaticMesh, int32 LODIndex, int32 UVChannelIndex, const FVector& Position, const FRotator& Orientation, const FVector& Size) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return false; } if (!InternalEditorMeshLibrary::IsUVChannelValid(StaticMesh, LODIndex, UVChannelIndex)) { return false; } FMeshDescription* MeshDescription = StaticMesh->GetMeshDescription(LODIndex); FUVMapParameters UVParameters(Position, Orientation.Quaternion(), Size, FVector::OneVector, FVector2D::UnitVector); TMap TexCoords; FStaticMeshOperations::GenerateBoxUV(*MeshDescription, UVParameters, TexCoords); return StaticMesh->SetUVChannel(LODIndex, UVChannelIndex, TexCoords); } void UStaticMeshEditorSubsystem::ReplaceMeshComponentsMaterials(const TArray& MeshComponents, UMaterialInterface* MaterialToBeReplaced, UMaterialInterface* NewMaterial) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return; } FScopedTransaction ScopedTransaction(LOCTEXT("ReplaceMeshComponentsMaterials", "Replace components materials")); int32 ChangeCounter = InternalEditorMeshLibrary::ReplaceMaterials(MeshComponents, MaterialToBeReplaced, NewMaterial); if (ChangeCounter > 0) { // Redraw viewports to reflect the material changes GEditor->RedrawLevelEditingViewports(); } UE_LOG(LogStaticMeshEditorSubsystem, Log, TEXT("ReplaceMeshComponentsMaterials. %d material(s) changed occurred."), ChangeCounter); } void UStaticMeshEditorSubsystem::ReplaceMeshComponentsMaterialsOnActors(const TArray& Actors, UMaterialInterface* MaterialToBeReplaced, UMaterialInterface* NewMaterial) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return; } FScopedTransaction ScopedTransaction(LOCTEXT("ReplaceComponentUsedMaterial", "Replace components materials")); int32 ChangeCounter = 0; TInlineComponentArray ComponentArray; for (AActor* Actor : Actors) { if (Actor && IsValidChecked(Actor)) { Actor->GetComponents(ComponentArray); ChangeCounter += InternalEditorMeshLibrary::ReplaceMaterials(ComponentArray, MaterialToBeReplaced, NewMaterial); } } if (ChangeCounter > 0) { // Redraw viewports to reflect the material changes GEditor->RedrawLevelEditingViewports(); } UE_LOG(LogStaticMeshEditorSubsystem, Log, TEXT("ReplaceMeshComponentsMaterialsOnActors. %d material(s) changed occurred."), ChangeCounter); } void UStaticMeshEditorSubsystem::ReplaceMeshComponentsMeshes(const TArray& MeshComponents, UStaticMesh* MeshToBeReplaced, UStaticMesh* NewMesh) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return; } FScopedTransaction ScopedTransaction(LOCTEXT("ReplaceMeshComponentsMeshes", "Replace components meshes")); int32 ChangeCounter = InternalEditorMeshLibrary::ReplaceMeshes(MeshComponents, MeshToBeReplaced, NewMesh); if (ChangeCounter > 0) { // Redraw viewports to reflect the material changes GEditor->RedrawLevelEditingViewports(); } UE_LOG(LogStaticMeshEditorSubsystem, Log, TEXT("ReplaceMeshComponentsMeshes. %d mesh(es) changed occurred."), ChangeCounter); } void UStaticMeshEditorSubsystem::ReplaceMeshComponentsMeshesOnActors(const TArray& Actors, UStaticMesh* MeshToBeReplaced, UStaticMesh* NewMesh) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return; } FScopedTransaction ScopedTransaction(LOCTEXT("ReplaceMeshComponentsMeshes", "Replace components meshes")); int32 ChangeCounter = 0; TInlineComponentArray ComponentArray; for (AActor* Actor : Actors) { if (Actor && IsValidChecked(Actor)) { Actor->GetComponents(ComponentArray); ChangeCounter += InternalEditorMeshLibrary::ReplaceMeshes(ComponentArray, MeshToBeReplaced, NewMesh); } } if (ChangeCounter > 0) { // Redraw viewports to reflect the material changes GEditor->RedrawLevelEditingViewports(); } UE_LOG(LogStaticMeshEditorSubsystem, Log, TEXT("ReplaceMeshComponentsMeshesOnActors. %d mesh(es) changed occurred."), ChangeCounter); } AActor* UStaticMeshEditorSubsystem::JoinStaticMeshActors(const TArray& ActorsToMerge, const FJoinStaticMeshActorsOptions& JoinOptions) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return nullptr; } TArray AllActors; TArray AllComponents; FVector PivotLocation; FString FailureReason; if (!InternalEditorMeshLibrary::FindValidActorAndComponents(ActorsToMerge, AllActors, AllComponents, PivotLocation, FailureReason)) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("JoinStaticMeshActors failed. %s"), *FailureReason); return nullptr; } if (AllActors.Num() < 2) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("JoinStaticMeshActors failed. A merge operation requires at least 2 valid Actors.")); return nullptr; } // Create the new Actor FActorSpawnParameters Params; Params.OverrideLevel = AllActors[0]->GetLevel(); AActor* NewActor = AllActors[0]->GetWorld()->SpawnActor(PivotLocation, FRotator::ZeroRotator, Params); if (!NewActor) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("JoinStaticMeshActors failed. Internal error while creating the join actor.")); return nullptr; } if (!JoinOptions.NewActorLabel.IsEmpty()) { NewActor->SetActorLabel(JoinOptions.NewActorLabel); } // Duplicate and attach all components to the new actors USceneComponent* NewRootComponent = NewObject(NewActor, TEXT("Root")); NewActor->SetRootComponent(NewRootComponent); NewRootComponent->SetMobility(EComponentMobility::Static); for (UStaticMeshComponent* ActorCmp : AllComponents) { FName NewName = NAME_None; if (JoinOptions.bRenameComponentsFromSource) { NewName = InternalEditorMeshLibrary::GenerateValidOwnerBasedComponentNameForNewOwner(ActorCmp, NewActor); } UStaticMeshComponent* NewComponent = DuplicateObject(ActorCmp, NewActor, NewName); NewActor->AddInstanceComponent(NewComponent); FTransform CmpTransform = ActorCmp->GetComponentToWorld(); NewComponent->SetComponentToWorld(CmpTransform); NewComponent->AttachToComponent(NewRootComponent, FAttachmentTransformRules::KeepWorldTransform); NewComponent->RegisterComponent(); } if (JoinOptions.bDestroySourceActors) { ULayersSubsystem* Layers = GEditor->GetEditorSubsystem(); UWorld* World = AllActors[0]->GetWorld(); for (AActor* Actor : AllActors) { Layers->DisassociateActorFromLayers(Actor); World->EditorDestroyActor(Actor, true); } } //Select newly created actor GEditor->SelectNone(false, true, false); GEditor->SelectActor(NewActor, true, false); GEditor->NoteSelectionChange(); UE_LOG(LogStaticMeshEditorSubsystem, Log, TEXT("JoinStaticMeshActors joined %d actors toghether in actor '%s'."), AllComponents.Num(), *NewActor->GetActorLabel()); return NewActor; } bool UStaticMeshEditorSubsystem::MergeStaticMeshActors(const TArray& ActorsToMerge, const FMergeStaticMeshActorsOptions& MergeOptions, AStaticMeshActor*& OutMergedActor) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); OutMergedActor = nullptr; if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return false; } UUnrealEditorSubsystem* UnrealEditorSubsystem = GEditor->GetEditorSubsystem(); if (!UnrealEditorSubsystem) { return false; } FString FailureReason; FString PackageName = EditorScriptingHelpers::ConvertAnyPathToLongPackagePath(MergeOptions.BasePackageName, FailureReason); if (PackageName.IsEmpty()) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("MergeStaticMeshActors. Failed to convert the BasePackageName. %s"), *FailureReason); return false; } TArray AllActors; TArray AllComponents; FVector PivotLocation; if (!InternalEditorMeshLibrary::FindValidActorAndComponents(ActorsToMerge, AllActors, AllComponents, PivotLocation, FailureReason)) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("MergeStaticMeshActors failed. %s"), *FailureReason); return false; } // // See MeshMergingTool.cpp // const IMeshMergeUtilities& MeshUtilities = FModuleManager::Get().LoadModuleChecked("MeshMergeUtilities").GetUtilities(); FVector MergedActorLocation; TArray CreatedAssets; const float ScreenAreaSize = TNumericLimits::Max(); MeshUtilities.MergeComponentsToStaticMesh(AllComponents, AllActors[0]->GetWorld(), MergeOptions.MeshMergingSettings, nullptr, nullptr, PackageName, CreatedAssets, MergedActorLocation, ScreenAreaSize, true); UStaticMesh* MergedMesh = nullptr; if (!CreatedAssets.FindItemByClass(&MergedMesh)) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("MergeStaticMeshActors failed. No mesh was created.")); return false; } FAssetRegistryModule& AssetRegistry = FModuleManager::Get().LoadModuleChecked("AssetRegistry"); for (UObject* Obj : CreatedAssets) { AssetRegistry.AssetCreated(Obj); } //Also notify the content browser that the new assets exists if (!IsRunningCommandlet()) { FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked("ContentBrowser"); ContentBrowserModule.Get().SyncBrowserToAssets(CreatedAssets, true); } // Place new mesh in the world if (MergeOptions.bSpawnMergedActor) { FActorSpawnParameters Params; Params.OverrideLevel = AllActors[0]->GetLevel(); OutMergedActor = AllActors[0]->GetWorld()->SpawnActor(MergedActorLocation, FRotator::ZeroRotator, Params); if (!OutMergedActor) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("MergeStaticMeshActors failed. Internal error while creating the merged actor.")); return false; } OutMergedActor->GetStaticMeshComponent()->SetStaticMesh(MergedMesh); OutMergedActor->SetActorLabel(MergeOptions.NewActorLabel); AllActors[0]->GetWorld()->UpdateCullDistanceVolumes(OutMergedActor, OutMergedActor->GetStaticMeshComponent()); } // Remove source actors if (MergeOptions.bDestroySourceActors) { ULayersSubsystem* Layers = GEditor->GetEditorSubsystem(); UWorld* World = AllActors[0]->GetWorld(); for (AActor* Actor : AllActors) { Layers->DisassociateActorFromLayers(Actor); World->EditorDestroyActor(Actor, true); } } //Select newly created actor GEditor->SelectNone(false, true, false); GEditor->SelectActor(OutMergedActor, true, false); GEditor->NoteSelectionChange(); return true; } bool UStaticMeshEditorSubsystem::CreateProxyMeshActor(const TArray& ActorsToMerge, const FCreateProxyMeshActorOptions& MergeOptions, class AStaticMeshActor*& OutMergedActor) { // See FMeshProxyTool::RunMerge (Engine\Source\Editor\MergeActors\Private\MeshProxyTool\MeshProxyTool.cpp) TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); OutMergedActor = nullptr; if (!EditorScriptingHelpers::CheckIfInEditorAndPIE()) { return false; } UUnrealEditorSubsystem* UnrealEditorSubsystem = GEditor->GetEditorSubsystem(); if (!UnrealEditorSubsystem) { return false; } FString FailureReason; FString PackageName = EditorScriptingHelpers::ConvertAnyPathToLongPackagePath(MergeOptions.BasePackageName, FailureReason); if (PackageName.IsEmpty()) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("CreateProxyMeshActor. Failed to convert the BasePackageName. %s"), *FailureReason); return false; } // Cleanup actors TArray StaticMeshActors; TArray AllComponents_UNUSED; FVector PivotLocation; if (!InternalEditorMeshLibrary::FindValidActorAndComponents(ActorsToMerge, StaticMeshActors, AllComponents_UNUSED, PivotLocation, FailureReason)) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("CreateProxyMeshActor failed. %s"), *FailureReason); return false; } TArray AllActors(StaticMeshActors); const IMeshMergeUtilities& MeshUtilities = FModuleManager::Get().LoadModuleChecked("MeshMergeUtilities").GetUtilities(); FCreateProxyDelegate ProxyDelegate; TArray CreatedAssets; ProxyDelegate.BindLambda([&CreatedAssets](const FGuid Guid, TArray& InAssetsToSync) {CreatedAssets.Append(InAssetsToSync); }); MeshUtilities.CreateProxyMesh( AllActors, // List of Actors to merge MergeOptions.MeshProxySettings, // Merge settings nullptr, // Base Material used for final proxy material. Note: nullptr for default impl: GEngine->DefaultFlattenMaterial nullptr, // Package for generated assets. Note: if nullptr, BasePackageName is used PackageName, // Will be used for naming generated assets, in case InOuter is not specified ProxyBasePackageName will be used as long package name for creating new packages FGuid::NewGuid(), // Identify a job, First argument of the ProxyDelegate ProxyDelegate // Called back on asset creation ); UStaticMesh* MergedMesh = nullptr; if (!CreatedAssets.FindItemByClass(&MergedMesh)) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("CreateProxyMeshActor failed. No mesh created.")); return false; } // Update the asset registry that a new static mesh and material has been created FAssetRegistryModule& AssetRegistry = FModuleManager::Get().LoadModuleChecked("AssetRegistry"); for (UObject* Asset : CreatedAssets) { AssetRegistry.AssetCreated(Asset); GEditor->BroadcastObjectReimported(Asset); } // Also notify the content browser that the new assets exists if (!IsRunningCommandlet()) { FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked("ContentBrowser"); ContentBrowserModule.Get().SyncBrowserToAssets(CreatedAssets, true); } // Place new mesh in the world UWorld* ActorWorld = AllActors[0]->GetWorld(); ULevel* ActorLevel = AllActors[0]->GetLevel(); if (MergeOptions.bSpawnMergedActor) { FActorSpawnParameters Params; Params.OverrideLevel = ActorLevel; OutMergedActor = ActorWorld->SpawnActor(FVector::ZeroVector, FRotator::ZeroRotator, Params); if (!OutMergedActor) { UE_LOG(LogStaticMeshEditorSubsystem, Error, TEXT("CreateProxyMeshActor failed. Internal error while creating the merged actor.")); return false; } OutMergedActor->GetStaticMeshComponent()->SetStaticMesh(MergedMesh); OutMergedActor->SetActorLabel(MergeOptions.NewActorLabel); ActorWorld->UpdateCullDistanceVolumes(OutMergedActor, OutMergedActor->GetStaticMeshComponent()); } // Remove source actors if (MergeOptions.bDestroySourceActors) { ULayersSubsystem* Layers = GEditor->GetEditorSubsystem(); for (AActor* Actor : AllActors) { Layers->DisassociateActorFromLayers(Actor); ActorWorld->EditorDestroyActor(Actor, true); } } //Select newly created actor if (OutMergedActor) { GEditor->SelectNone(false, true, false); GEditor->SelectActor(OutMergedActor, true, false); // don't notify but manually call NoteSelectionChange ? GEditor->NoteSelectionChange(); } return true; } #undef LOCTEXT_NAMESPACE