// Copyright Epic Games, Inc. All Rights Reserved. #include "MeshMergeUtilities.h" #include "Engine/MapBuildDataRegistry.h" #include "MeshMerge/MeshInstancingSettings.h" #include "MeshMerge/MeshMergingSettings.h" #include "MeshMerge/MeshProxySettings.h" #include "Engine/StaticMeshSocket.h" #include "MaterialOptions.h" #include "IMaterialBakingModule.h" #include "Misc/PackageName.h" #include "MaterialUtilities.h" #include "Materials/MaterialInstanceConstant.h" #include "Components/SkeletalMeshComponent.h" #include "Components/SplineMeshComponent.h" #include "Components/SkinnedMeshComponent.h" #include "Components/ShapeComponent.h" #include "SkeletalMeshTypes.h" #include "SkeletalRenderPublic.h" #include "UObject/UObjectBaseUtility.h" #include "UObject/Package.h" #include "Materials/Material.h" #include "Materials/MaterialInstanceDynamic.h" #include "Misc/ScopedSlowTask.h" #include "Modules/ModuleManager.h" #include "HierarchicalLODUtilitiesModule.h" #include "MeshMergeData.h" #include "IHierarchicalLODUtilities.h" #include "Engine/MeshMergeCullingVolume.h" #include "Landscape.h" #include "LandscapeProxy.h" #include "Editor.h" #include "ProxyGenerationProcessor.h" #include "Editor/EditorPerProjectUserSettings.h" #include "Engine/StaticMesh.h" #include "PhysicsEngine/ConvexElem.h" #include "PhysicsEngine/BodySetup.h" #include "MeshUtilities.h" #include "ImageUtils.h" #include "LandscapeHeightfieldCollisionComponent.h" #include "IMeshReductionManagerModule.h" #include "IMeshReductionInterfaces.h" #include "ProxyGenerationProcessor.h" #include "IMaterialBakingAdapter.h" #include "StaticMeshComponentLODInfo.h" #include "SkeletalMeshAdapter.h" #include "StaticMeshAdapter.h" #include "MeshMergeDataTracker.h" #include "Misc/FileHelper.h" #include "MeshMergeHelpers.h" #include "Settings/EditorExperimentalSettings.h" #include "MaterialBakingStructures.h" #include "Async/ParallelFor.h" #include "ScopedTransaction.h" #include "Components/InstancedStaticMeshComponent.h" #include "Engine/LODActor.h" #include "HierarchicalLODVolume.h" #include "Engine/Selection.h" #include "MaterialBakingHelpers.h" #include "IMeshMergeExtension.h" #include "RawMesh.h" #include "StaticMeshAttributes.h" #include "StaticMeshOperations.h" #include "TriangleTypes.h" #include "MaterialUtilities.h" #include "Async/Future.h" #include "Async/Async.h" #include "TextureCompiler.h" #include "Widgets/Notifications/SNotificationList.h" #include "Framework/Notifications/NotificationManager.h" #include "ISMPartition/ISMComponentBatcher.h" #include "ISMPartition/ISMComponentDescriptor.h" #include "UObject/GCObjectScopeGuard.h" #include "Algo/RemoveIf.h" #define LOCTEXT_NAMESPACE "MeshMergeUtils" DEFINE_LOG_CATEGORY(LogMeshMerging); static TAutoConsoleVariable CVarMeshMergeUtilitiesUVGenerationMethod( TEXT("MeshMergeUtilities.UVGenerationMethod"), 0, TEXT("UV generation method when creating merged or proxy meshes\n" "0 - Engine default - (currently Patch Builder)\n" "1 - Legacy\n" "2 - UVAtlas\n" "3 - XAtlas\n" "4 - Patch Builder\n")); static FStaticMeshOperations::EGenerateUVMethod GetUVGenerationMethodToUse() { switch (CVarMeshMergeUtilitiesUVGenerationMethod.GetValueOnAnyThread()) { case 1: return FStaticMeshOperations::EGenerateUVMethod::Legacy; case 2: return FStaticMeshOperations::EGenerateUVMethod::UVAtlas; case 3: return FStaticMeshOperations::EGenerateUVMethod::XAtlas; case 4: return FStaticMeshOperations::EGenerateUVMethod::PatchBuilder; default: return FStaticMeshOperations::EGenerateUVMethod::Default; } } FMeshMergeUtilities::FMeshMergeUtilities() { Processor = new FProxyGenerationProcessor(this); } FMeshMergeUtilities::~FMeshMergeUtilities() { FModuleManager::Get().OnModulesChanged().Remove(ModuleLoadedDelegateHandle); } void FMeshMergeUtilities::BakeMaterialsForComponent(TArray>& OptionObjects, IMaterialBakingAdapter* Adapter) const { // Try and find material (merge) options from provided set of objects TWeakObjectPtr* MaterialOptionsObject = OptionObjects.FindByPredicate([](TWeakObjectPtr Object) { return Cast(Object.Get()) != nullptr; }); TWeakObjectPtr* MaterialMergeOptionsObject = OptionObjects.FindByPredicate([](TWeakObjectPtr Object) { return Cast(Object.Get()) != nullptr; }); UMaterialOptions* MaterialOptions = MaterialOptionsObject ? Cast(MaterialOptionsObject->Get()) : nullptr; checkf(MaterialOptions, TEXT("No valid material options found")); UMaterialMergeOptions* MaterialMergeOptions = MaterialMergeOptionsObject ? Cast(MaterialMergeOptionsObject->Get()) : nullptr; // Mesh / LOD index TMap RawMeshLODs; // Unique set of sections in mesh TArray UniqueSections; TArray Sections; int32 NumLODs = Adapter->GetNumberOfLODs(); // LOD index, TArray> UniqueSectionIndexPerLOD; UniqueSectionIndexPerLOD.AddDefaulted(NumLODs); // Retrieve raw mesh data and unique sections for (int32 LODIndex = 0; LODIndex < NumLODs; ++LODIndex) { // Reset section for reuse Sections.SetNum(0, EAllowShrinking::No); // Extract raw mesh data const bool bProcessedLOD = MaterialOptions->LODIndices.Contains(LODIndex); if (bProcessedLOD) { FMeshDescription& RawMesh = RawMeshLODs.Add(LODIndex); FStaticMeshAttributes(RawMesh).Register(); Adapter->RetrieveRawMeshData(LODIndex, RawMesh, MaterialOptions->bUseMeshData); } // Extract sections for given LOD index from the mesh Adapter->RetrieveMeshSections(LODIndex, Sections); for (int32 SectionIndex = 0; SectionIndex < Sections.Num(); ++SectionIndex) { FSectionInfo& Section = Sections[SectionIndex]; Section.bProcessed = bProcessedLOD; const int32 UniqueIndex = UniqueSections.AddUnique(Section); UniqueSectionIndexPerLOD[LODIndex].Emplace(SectionIndex, UniqueIndex); } } TArray UniqueMaterials; TMultiMap UniqueMaterialToUniqueSectionMap; // Populate list of unique materials and store section mappings for (int32 SectionIndex = 0; SectionIndex < UniqueSections.Num(); ++SectionIndex) { FSectionInfo& Section = UniqueSections[SectionIndex]; const int32 UniqueIndex = UniqueMaterials.AddUnique(Section.Material); UniqueMaterialToUniqueSectionMap.Add(UniqueIndex, SectionIndex); } TArray GlobalMeshSettings; TArray GlobalMaterialSettings; TArray> OutputMaterialsMap; OutputMaterialsMap.AddDefaulted(NumLODs); for (int32 MaterialIndex = 0; MaterialIndex < UniqueMaterials.Num(); ++MaterialIndex) { UMaterialInterface* Material = UniqueMaterials[MaterialIndex]; // Retrieve all sections using this material TArray SectionIndices; UniqueMaterialToUniqueSectionMap.MultiFind(MaterialIndex, SectionIndices); if (MaterialOptions->bUseMeshData) { for (const int32 LODIndex : MaterialOptions->LODIndices) { FMeshData MeshSettings; MeshSettings.MeshDescription = nullptr; // Add material indices used for rendering out material for (const auto& Pair : UniqueSectionIndexPerLOD[LODIndex]) { if (SectionIndices.Contains(Pair.Value)) { MeshSettings.MaterialIndices.Add(Pair.Key); } } if (MeshSettings.MaterialIndices.Num()) { // Retrieve raw mesh MeshSettings.MeshDescription = RawMeshLODs.Find(LODIndex); //Should not be using mesh data if there is no mesh check(MeshSettings.MeshDescription); MeshSettings.TextureCoordinateBox = FBox2D(FVector2D(0.0f, 0.0f), FVector2D(1.0f, 1.0f)); const bool bUseVertexColor = FStaticMeshOperations::HasVertexColor(*(MeshSettings.MeshDescription)); if (MaterialOptions->bUseSpecificUVIndex) { MeshSettings.TextureCoordinateIndex = MaterialOptions->TextureCoordinateIndex; } // if you use vertex color, we can't rely on overlapping UV channel, so use light map UV to unwrap UVs else if (bUseVertexColor) { MeshSettings.TextureCoordinateIndex = Adapter->LightmapUVIndex(); } else { MeshSettings.TextureCoordinateIndex = 0; } Adapter->ApplySettings(LODIndex, MeshSettings); // In case part of the UVs is not within the 0-1 range try to use the lightmap UVs const bool bNeedsUniqueUVs = FMeshMergeHelpers::CheckWrappingUVs(*(MeshSettings.MeshDescription), MeshSettings.TextureCoordinateIndex); const int32 LightMapUVIndex = Adapter->LightmapUVIndex(); TVertexInstanceAttributesConstRef VertexInstanceUVs = FStaticMeshConstAttributes(*MeshSettings.MeshDescription).GetVertexInstanceUVs(); if (bNeedsUniqueUVs && MeshSettings.TextureCoordinateIndex != LightMapUVIndex && VertexInstanceUVs.GetNumElements() > 0 && VertexInstanceUVs.GetNumChannels() > LightMapUVIndex) { MeshSettings.TextureCoordinateIndex = LightMapUVIndex; } FMaterialData MaterialSettings; MaterialSettings.Material = Material; // Add all user defined properties for baking out for (const FPropertyEntry& Entry : MaterialOptions->Properties) { if (!Entry.bUseConstantValue && Entry.Property != MP_MAX) { int32 NumTextureCoordinates; bool bUsesVertexData; Material->AnalyzeMaterialProperty(Entry.Property, NumTextureCoordinates, bUsesVertexData); MaterialSettings.PropertySizes.Add(Entry.Property, Entry.bUseCustomSize ? Entry.CustomSize : MaterialOptions->TextureSize); } } // For each original material index add an entry to the corresponding LOD and bake output index for (int32 Index : MeshSettings.MaterialIndices) { OutputMaterialsMap[LODIndex].Emplace(Index, GlobalMeshSettings.Num()); } GlobalMeshSettings.Add(MeshSettings); GlobalMaterialSettings.Add(MaterialSettings); } } } else { // If we are not using the mesh data we aren't doing anything special, just bake out uv range FMeshData MeshSettings; for (int32 LODIndex : MaterialOptions->LODIndices) { for (const auto& Pair : UniqueSectionIndexPerLOD[LODIndex]) { if (SectionIndices.Contains(Pair.Value)) { MeshSettings.MaterialIndices.Add(Pair.Key); } } } if (MeshSettings.MaterialIndices.Num()) { MeshSettings.MeshDescription = nullptr; MeshSettings.TextureCoordinateBox = FBox2D(FVector2D(0.0f, 0.0f), FVector2D(1.0f, 1.0f)); MeshSettings.TextureCoordinateIndex = 0; FMaterialData MaterialSettings; MaterialSettings.Material = Material; // Add all user defined properties for baking out for (const FPropertyEntry& Entry : MaterialOptions->Properties) { // With substrate, we cannot use connection on the root node to infer Active or Constant properties. So we activate all material input for now. // SUBSTRATE_TODO: gather information about the material used input, maybe using Material->GetSubstrateCompilationConfig(). const float bIsSubstrateEnabled = Substrate::IsSubstrateEnabled(); if ((bIsSubstrateEnabled || (!Entry.bUseConstantValue && Material->IsPropertyActive(Entry.Property))) && Entry.Property != MP_MAX) { MaterialSettings.PropertySizes.Add(Entry.Property, Entry.bUseCustomSize ? Entry.CustomSize : MaterialOptions->TextureSize); } } for (int32 LODIndex : MaterialOptions->LODIndices) { for (const auto& Pair : UniqueSectionIndexPerLOD[LODIndex]) { if (SectionIndices.Contains(Pair.Value)) { /// For each original material index add an entry to the corresponding LOD and bake output index OutputMaterialsMap[LODIndex].Emplace(Pair.Key, GlobalMeshSettings.Num()); } } } GlobalMeshSettings.Add(MeshSettings); GlobalMaterialSettings.Add(MaterialSettings); } } } TArray MeshSettingPtrs; for (int32 SettingsIndex = 0; SettingsIndex < GlobalMeshSettings.Num(); ++SettingsIndex) { MeshSettingPtrs.Add(&GlobalMeshSettings[SettingsIndex]); } TArray MaterialSettingPtrs; for (int32 SettingsIndex = 0; SettingsIndex < GlobalMaterialSettings.Num(); ++SettingsIndex) { MaterialSettingPtrs.Add(&GlobalMaterialSettings[SettingsIndex]); } TArray BakeOutputs; IMaterialBakingModule& Module = FModuleManager::Get().LoadModuleChecked("MaterialBaking"); Module.BakeMaterials(MaterialSettingPtrs, MeshSettingPtrs, BakeOutputs); // Append constant properties which did not require baking out TArray ConstantData; FIntPoint ConstantSize(1, 1); for (const FPropertyEntry& Entry : MaterialOptions->Properties) { if (Entry.bUseConstantValue && Entry.Property != MP_MAX) { ConstantData.SetNum(1, EAllowShrinking::No); ConstantData[0] = FColor(Entry.ConstantValue * 255.0f, Entry.ConstantValue * 255.0f, Entry.ConstantValue * 255.0f); for (FBakeOutput& Ouput : BakeOutputs) { Ouput.PropertyData.Add(Entry.Property, ConstantData); Ouput.PropertySizes.Add(Entry.Property, ConstantSize); } } } TArray NewMaterials; FString PackageName = Adapter->GetBaseName(); const FGuid NameGuid = FGuid::NewGuid(); for (int32 OutputIndex = 0; OutputIndex < BakeOutputs.Num(); ++OutputIndex) { // Create merged material asset FString MaterialAssetName = TEXT("M_") + FPackageName::GetShortName(PackageName) + TEXT("_") + MaterialSettingPtrs[OutputIndex]->Material->GetName() + TEXT("_") + NameGuid.ToString(); FString MaterialPackageName = FPackageName::GetLongPackagePath(PackageName) + TEXT("/") + MaterialAssetName; FBakeOutput& Output = BakeOutputs[OutputIndex]; // Optimize output for (auto DataPair : Output.PropertyData) { FMaterialUtilities::OptimizeSampleArray(DataPair.Value, Output.PropertySizes[DataPair.Key]); } UMaterialInterface* Material = nullptr; if (Adapter->GetOuter()) { Material = FMaterialUtilities::CreateProxyMaterialAndTextures(Adapter->GetOuter(), MaterialAssetName, Output, *MeshSettingPtrs[OutputIndex], *MaterialSettingPtrs[OutputIndex], MaterialOptions); } else { Material = FMaterialUtilities::CreateProxyMaterialAndTextures(MaterialPackageName, MaterialAssetName, Output, *MeshSettingPtrs[OutputIndex], *MaterialSettingPtrs[OutputIndex], MaterialOptions); } NewMaterials.Add(Material); } // Retrieve material indices which were not baked out and should still be part of the final asset TArray NonReplaceMaterialIndices; for (int32 LODIndex = 0; LODIndex < NumLODs; ++LODIndex) { const bool bProcessedLOD = MaterialOptions->LODIndices.Contains(LODIndex); if (!bProcessedLOD) { for (const auto& Pair : UniqueSectionIndexPerLOD[LODIndex]) { NonReplaceMaterialIndices.AddUnique(Adapter->GetMaterialIndex(LODIndex, Pair.Key)); } } } // Remap all baked out materials to their new material indices TMap NewMaterialRemap; for (int32 LODIndex : MaterialOptions->LODIndices) { // Key == original section index, Value == unique material index for (const auto& Pair : OutputMaterialsMap[LODIndex]) { int32 SetIndex = Adapter->GetMaterialIndex(LODIndex, Pair.Key); if (!NonReplaceMaterialIndices.Contains(SetIndex)) { Adapter->SetMaterial(SetIndex, NewMaterials[Pair.Value]); } else { // Check if this material was processed and a new entry already exists if (uint32* ExistingIndex = NewMaterialRemap.Find(Pair.Value)) { Adapter->RemapMaterialIndex(LODIndex, Pair.Key, *ExistingIndex); } else { // Add new material int32 NewMaterialIndex = INDEX_NONE; if (Adapter->GetMaterialSlotName(Pair.Key).IsNone() || Adapter->GetImportedMaterialSlotName(Pair.Key).IsNone()) { NewMaterialIndex = Adapter->AddMaterial(NewMaterials[Pair.Value]); } else { NewMaterialIndex = Adapter->AddMaterial(NewMaterials[Pair.Value], Adapter->GetMaterialSlotName(Pair.Key), Adapter->GetImportedMaterialSlotName(Pair.Key)); } NewMaterialRemap.Add(Pair.Value, NewMaterialIndex); Adapter->RemapMaterialIndex(LODIndex, Pair.Key, NewMaterialIndex); } } } } Adapter->UpdateUVChannelData(); GlobalMeshSettings.Empty(); } void FMeshMergeUtilities::BakeMaterialsForComponent(USkeletalMeshComponent* SkeletalMeshComponent) const { if (!SkeletalMeshComponent || !SkeletalMeshComponent->GetSkeletalMeshAsset()) { return; } // Retrieve settings object UMaterialOptions* MaterialOptions = DuplicateObject(GetMutableDefault(), GetTransientPackage()); UAssetBakeOptions* AssetOptions = GetMutableDefault(); UMaterialMergeOptions* MergeOptions = GetMutableDefault(); TArray> Objects{ MergeOptions, AssetOptions, MaterialOptions }; const int32 NumLODs = SkeletalMeshComponent->GetSkeletalMeshAsset()->GetLODNum(); IMaterialBakingModule& Module = FModuleManager::Get().LoadModuleChecked("MaterialBaking"); if (!Module.SetupMaterialBakeSettings(Objects, NumLODs)) { return; } // Bake out materials for skeletal mesh SkeletalMeshComponent->GetSkeletalMeshAsset()->Modify(); FSkeletalMeshComponentAdapter Adapter(SkeletalMeshComponent); BakeMaterialsForComponent(Objects, &Adapter); SkeletalMeshComponent->MarkRenderStateDirty(); } void FMeshMergeUtilities::BakeMaterialsForComponent(UStaticMeshComponent* StaticMeshComponent) const { if (!StaticMeshComponent || !StaticMeshComponent->GetStaticMesh()) { return; } // Retrieve settings object UMaterialOptions* MaterialOptions = DuplicateObject(GetMutableDefault(), GetTransientPackage()); UAssetBakeOptions* AssetOptions = GetMutableDefault(); UMaterialMergeOptions* MergeOptions = GetMutableDefault(); TArray> Objects{ MergeOptions, AssetOptions, MaterialOptions }; const int32 NumLODs = StaticMeshComponent->GetStaticMesh()->GetNumLODs(); IMaterialBakingModule& Module = FModuleManager::Get().LoadModuleChecked("MaterialBaking"); if (!Module.SetupMaterialBakeSettings(Objects, NumLODs)) { return; } // Bake out materials for static mesh component FStaticMeshComponentAdapter Adapter(StaticMeshComponent); BakeMaterialsForComponent(Objects, &Adapter); StaticMeshComponent->MarkRenderStateDirty(); } void FMeshMergeUtilities::BakeMaterialsForMesh(UStaticMesh* StaticMesh) const { if (!StaticMesh) { return; } // Retrieve settings object UMaterialOptions* MaterialOptions = DuplicateObject(GetMutableDefault(), GetTransientPackage()); UAssetBakeOptions* AssetOptions = GetMutableDefault(); UMaterialMergeOptions* MergeOptions = GetMutableDefault(); TArray> Objects{ MergeOptions, AssetOptions, MaterialOptions }; const int32 NumLODs = StaticMesh->GetNumLODs(); IMaterialBakingModule& Module = FModuleManager::Get().LoadModuleChecked("MaterialBaking"); if (!Module.SetupMaterialBakeSettings(Objects, NumLODs)) { return; } // Bake out materials for static mesh asset StaticMesh->Modify(); FStaticMeshAdapter Adapter(StaticMesh); BakeMaterialsForComponent(Objects, &Adapter); } static bool DetermineMaterialVertexDataUsage(UMaterialInterface* Material, const UMaterialOptions* MaterialOptions) { TRACE_CPUPROFILER_EVENT_SCOPE(DetermineMaterialVertexDataUsage); for (const FPropertyEntry& Entry : MaterialOptions->Properties) { // Don't have to check a property if the result is going to be constant anyway if (!Entry.bUseConstantValue && Entry.Property != MP_MAX) { int32 NumTextureCoordinates; bool bUsesVertexData; Material->AnalyzeMaterialProperty(Entry.Property, NumTextureCoordinates, bUsesVertexData); if (bUsesVertexData || NumTextureCoordinates > 1) { return true; } } } return false; } void FMeshMergeUtilities::ConvertOutputToFlatMaterials(const TArray& BakeOutputs, const TArray& MaterialData, TArray &FlattenedMaterials) const { for (int32 OutputIndex = 0; OutputIndex < BakeOutputs.Num(); ++OutputIndex) { const FBakeOutput& Output = BakeOutputs[OutputIndex]; const FMaterialData& MaterialInfo = MaterialData[OutputIndex]; FFlattenMaterial Material; for (TPair SizePair : Output.PropertySizes) { EFlattenMaterialProperties OldProperty = ToFlattenProperty(SizePair.Key); if (ensure(OldProperty != EFlattenMaterialProperties::NumFlattenMaterialProperties)) { Material.SetPropertySize(OldProperty, SizePair.Value); Material.GetPropertySamples(OldProperty).Append(Output.PropertyData[SizePair.Key]); } } Material.bDitheredLODTransition = MaterialInfo.Material->IsDitheredLODTransition(); Material.BlendMode = BLEND_Opaque; Material.bTwoSided = MaterialInfo.Material->IsTwoSided(); Material.EmissiveScale = Output.EmissiveScale; FlattenedMaterials.Add(Material); } } void FMeshMergeUtilities::TransferOutputToFlatMaterials(const TArray& InMaterialData, TArray& InOutBakeOutputs, TArray &OutFlattenedMaterials) const { TRACE_CPUPROFILER_EVENT_SCOPE(FMeshMergeUtilities::TransferOutputToFlatMaterials) OutFlattenedMaterials.SetNum(InOutBakeOutputs.Num()); for (int32 OutputIndex = 0; OutputIndex < InOutBakeOutputs.Num(); ++OutputIndex) { FBakeOutput& Output = InOutBakeOutputs[OutputIndex]; const FMaterialData& MaterialInfo = InMaterialData[OutputIndex]; FFlattenMaterial& Material = OutFlattenedMaterials[OutputIndex]; for (TPair SizePair : Output.PropertySizes) { EFlattenMaterialProperties OldProperty = ToFlattenProperty(SizePair.Key); if (ensure(OldProperty != EFlattenMaterialProperties::NumFlattenMaterialProperties)) { Material.SetPropertySize(OldProperty, SizePair.Value); Material.GetPropertySamples(OldProperty) = MoveTemp(Output.PropertyData[SizePair.Key]); } } Material.bDitheredLODTransition = MaterialInfo.Material->IsDitheredLODTransition(); Material.BlendMode = BLEND_Opaque; Material.bTwoSided = MaterialInfo.Material->IsTwoSided(); Material.EmissiveScale = Output.EmissiveScale; } } EFlattenMaterialProperties FMeshMergeUtilities::ToFlattenProperty(EMaterialProperty MaterialProperty) const { switch (MaterialProperty) { case EMaterialProperty::MP_BaseColor: return EFlattenMaterialProperties::Diffuse; case EMaterialProperty::MP_Metallic: return EFlattenMaterialProperties::Metallic; case EMaterialProperty::MP_Specular: return EFlattenMaterialProperties::Specular; case EMaterialProperty::MP_Roughness: return EFlattenMaterialProperties::Roughness; case EMaterialProperty::MP_Anisotropy: return EFlattenMaterialProperties::Anisotropy; case EMaterialProperty::MP_Normal: return EFlattenMaterialProperties::Normal; case EMaterialProperty::MP_Tangent: return EFlattenMaterialProperties::Tangent; case EMaterialProperty::MP_Opacity: return EFlattenMaterialProperties::Opacity; case EMaterialProperty::MP_EmissiveColor: return EFlattenMaterialProperties::Emissive; case EMaterialProperty::MP_OpacityMask: return EFlattenMaterialProperties::OpacityMask; case EMaterialProperty::MP_AmbientOcclusion: return EFlattenMaterialProperties::AmbientOcclusion; default: return EFlattenMaterialProperties::NumFlattenMaterialProperties; } } UMaterialOptions* FMeshMergeUtilities::PopulateMaterialOptions(const FMaterialProxySettings& MaterialSettings) const { UMaterialOptions* MaterialOptions = DuplicateObject(GetMutableDefault(), GetTransientPackage()); MaterialOptions->Properties.Empty(); MaterialOptions->TextureSize = MaterialSettings.TextureSize; FPropertyEntry Property; PopulatePropertyEntry(MaterialSettings, MP_BaseColor, Property); MaterialOptions->Properties.Add(Property); PopulatePropertyEntry(MaterialSettings, MP_Specular, Property); if (MaterialSettings.bSpecularMap) MaterialOptions->Properties.Add(Property); PopulatePropertyEntry(MaterialSettings, MP_Roughness, Property); if (MaterialSettings.bRoughnessMap) MaterialOptions->Properties.Add(Property); PopulatePropertyEntry(MaterialSettings, MP_Anisotropy, Property); if (MaterialSettings.bAnisotropyMap) { MaterialOptions->Properties.Add(Property); } PopulatePropertyEntry(MaterialSettings, MP_Metallic, Property); if (MaterialSettings.bMetallicMap) MaterialOptions->Properties.Add(Property); PopulatePropertyEntry(MaterialSettings, MP_Normal, Property); if (MaterialSettings.bNormalMap) MaterialOptions->Properties.Add(Property); PopulatePropertyEntry(MaterialSettings, MP_Tangent, Property); if (MaterialSettings.bTangentMap) { MaterialOptions->Properties.Add(Property); } PopulatePropertyEntry(MaterialSettings, MP_Opacity, Property); if (MaterialSettings.bOpacityMap) MaterialOptions->Properties.Add(Property); PopulatePropertyEntry(MaterialSettings, MP_OpacityMask, Property); if (MaterialSettings.bOpacityMaskMap) MaterialOptions->Properties.Add(Property); PopulatePropertyEntry(MaterialSettings, MP_EmissiveColor, Property); if (MaterialSettings.bEmissiveMap) MaterialOptions->Properties.Add(Property); PopulatePropertyEntry(MaterialSettings, MP_AmbientOcclusion, Property); if (MaterialSettings.bAmbientOcclusionMap) MaterialOptions->Properties.Add(Property); return MaterialOptions; } void FMeshMergeUtilities::PopulatePropertyEntry(const FMaterialProxySettings& MaterialSettings, EMaterialProperty MaterialProperty, FPropertyEntry& InOutPropertyEntry) const { InOutPropertyEntry.Property = MaterialProperty; switch (MaterialSettings.TextureSizingType) { /** Set property output size to unique per-property user set sizes */ case TextureSizingType_UseManualOverrideTextureSize: { InOutPropertyEntry.bUseCustomSize = true; InOutPropertyEntry.CustomSize = [MaterialSettings, MaterialProperty]() -> FIntPoint { switch (MaterialProperty) { case MP_BaseColor: return MaterialSettings.DiffuseTextureSize; case MP_Specular: return MaterialSettings.SpecularTextureSize; case MP_Roughness: return MaterialSettings.RoughnessTextureSize; case MP_Anisotropy: return MaterialSettings.AnisotropyTextureSize; case MP_Metallic: return MaterialSettings.MetallicTextureSize; case MP_Normal: return MaterialSettings.NormalTextureSize; case MP_Tangent: return MaterialSettings.TangentTextureSize; case MP_Opacity: return MaterialSettings.OpacityTextureSize; case MP_OpacityMask: return MaterialSettings.OpacityMaskTextureSize; case MP_EmissiveColor: return MaterialSettings.EmissiveTextureSize; case MP_AmbientOcclusion: return MaterialSettings.AmbientOcclusionTextureSize; default: { checkf(false, TEXT("Invalid Material Property")); return FIntPoint(); } } }(); break; } /** Set property output size to biased values off the TextureSize value (Normal at fullres, Diffuse at halfres, and anything else at quarter res */ case TextureSizingType_UseAutomaticBiasedSizes: { const FIntPoint FullRes = MaterialSettings.TextureSize; const FIntPoint HalfRes = FIntPoint(FMath::Max(8, FullRes.X >> 1), FMath::Max(8, FullRes.Y >> 1)); const FIntPoint QuarterRes = FIntPoint(FMath::Max(4, FullRes.X >> 2), FMath::Max(4, FullRes.Y >> 2)); InOutPropertyEntry.bUseCustomSize = true; InOutPropertyEntry.CustomSize = [FullRes, HalfRes, QuarterRes, MaterialSettings, MaterialProperty]() -> FIntPoint { switch (MaterialProperty) { case MP_Normal: return FullRes; case MP_Tangent: return HalfRes; case MP_BaseColor: return HalfRes; case MP_Specular: return QuarterRes; case MP_Roughness: return QuarterRes; case MP_Anisotropy: return QuarterRes; case MP_Metallic: return QuarterRes; case MP_Opacity: return QuarterRes; case MP_OpacityMask: return QuarterRes; case MP_EmissiveColor: return QuarterRes; case MP_AmbientOcclusion: return QuarterRes; default: { checkf(false, TEXT("Invalid Material Property")); return FIntPoint(); } } }(); break; } /** Set all sizes to TextureSize */ case TextureSizingType_UseSingleTextureSize: case TextureSizingType_UseSimplygonAutomaticSizing: { InOutPropertyEntry.bUseCustomSize = false; InOutPropertyEntry.CustomSize = MaterialSettings.TextureSize; break; } default: UE_LOG(LogMeshMerging, Error, TEXT("Unsupported TextureSizingType value. You should resolve the material texture size first with ResolveTextureSize()")); } /** Check whether or not a constant value should be used for this property */ InOutPropertyEntry.bUseConstantValue = [MaterialSettings, MaterialProperty]() -> bool { switch (MaterialProperty) { case MP_BaseColor: return false; case MP_Normal: return !MaterialSettings.bNormalMap; case MP_Tangent: return !MaterialSettings.bTangentMap; case MP_Specular: return !MaterialSettings.bSpecularMap; case MP_Roughness: return !MaterialSettings.bRoughnessMap; case MP_Anisotropy: return !MaterialSettings.bAnisotropyMap; case MP_Metallic: return !MaterialSettings.bMetallicMap; case MP_Opacity: return !MaterialSettings.bOpacityMap; case MP_OpacityMask: return !MaterialSettings.bOpacityMaskMap; case MP_EmissiveColor: return !MaterialSettings.bEmissiveMap; case MP_AmbientOcclusion: return !MaterialSettings.bAmbientOcclusionMap; default: { checkf(false, TEXT("Invalid Material Property")); return false; } } }(); /** Set the value if a constant value should be used for this property */ InOutPropertyEntry.ConstantValue = [MaterialSettings, MaterialProperty]() -> float { switch (MaterialProperty) { case MP_BaseColor: return 1.0f; case MP_Normal: return 1.0f; case MP_Tangent: return 1.0f; case MP_Specular: return MaterialSettings.SpecularConstant; case MP_Roughness: return MaterialSettings.RoughnessConstant; case MP_Anisotropy: return MaterialSettings.AnisotropyConstant; case MP_Metallic: return MaterialSettings.MetallicConstant; case MP_Opacity: return MaterialSettings.OpacityConstant; case MP_OpacityMask: return MaterialSettings.OpacityMaskConstant; case MP_EmissiveColor: return 0.0f; case MP_AmbientOcclusion: return MaterialSettings.AmbientOcclusionConstant; default: { checkf(false, TEXT("Invalid Material Property")); return 1.0f; } } }(); } void FMeshMergeUtilities::CopyTextureRect(const FColor* Src, const FIntPoint& SrcSize, FColor* Dst, const FIntPoint& DstSize, const FIntPoint& DstPos, bool bCopyOnlyMaskedPixels) const { const int32 RowLength = SrcSize.X * sizeof(FColor); FColor* RowDst = Dst + DstSize.X*DstPos.Y; const FColor* RowSrc = Src; if(bCopyOnlyMaskedPixels) { for (int32 RowIdx = 0; RowIdx < SrcSize.Y; ++RowIdx) { for (int32 ColIdx = 0; ColIdx < SrcSize.X; ++ColIdx) { if(RowSrc[ColIdx] != FColor::Magenta) { RowDst[DstPos.X + ColIdx] = RowSrc[ColIdx]; } } RowDst += DstSize.X; RowSrc += SrcSize.X; } } else { for (int32 RowIdx = 0; RowIdx < SrcSize.Y; ++RowIdx) { FMemory::Memcpy(RowDst + DstPos.X, RowSrc, RowLength); RowDst += DstSize.X; RowSrc += SrcSize.X; } } } void FMeshMergeUtilities::SetTextureRect(const FColor& ColorValue, const FIntPoint& SrcSize, FColor* Dst, const FIntPoint& DstSize, const FIntPoint& DstPos) const { FColor* RowDst = Dst + DstSize.X*DstPos.Y; for (int32 RowIdx = 0; RowIdx < SrcSize.Y; ++RowIdx) { for (int32 ColIdx = 0; ColIdx < SrcSize.X; ++ColIdx) { RowDst[DstPos.X + ColIdx] = ColorValue; } RowDst += DstSize.X; } } FIntPoint FMeshMergeUtilities::ConditionalImageResize(const FIntPoint& SrcSize, const FIntPoint& DesiredSize, TArray& InOutImage, bool bLinearSpace) const { const int32 NumDesiredSamples = DesiredSize.X*DesiredSize.Y; if (InOutImage.Num() && InOutImage.Num() != NumDesiredSamples) { check(InOutImage.Num() == SrcSize.X*SrcSize.Y); TArray OutImage; if (NumDesiredSamples > 0) { const bool bOpaqueAlpha = true; // ?? FImageUtils::ImageResize(SrcSize.X, SrcSize.Y, InOutImage, DesiredSize.X, DesiredSize.Y, OutImage, bLinearSpace, bOpaqueAlpha); } Exchange(InOutImage, OutImage); return DesiredSize; } return SrcSize; } void FMeshMergeUtilities::MergeFlattenedMaterials(TArray& InMaterialList, int32 InGutter, FFlattenMaterial& OutMergedMaterial, TArray& OutUVTransforms) const { // Fill output UV transforms with invalid values OutUVTransforms.SetNumZeroed(InMaterialList.Num()); const int32 AtlasGridSize = FMath::CeilToInt(FMath::Sqrt(static_cast(InMaterialList.Num()))); OutMergedMaterial.EmissiveScale = FlattenEmissivescale(InMaterialList); for (int32 PropertyIndex = 0; PropertyIndex < (int32)EFlattenMaterialProperties::NumFlattenMaterialProperties; ++PropertyIndex) { const EFlattenMaterialProperties Property = (EFlattenMaterialProperties)PropertyIndex; if (OutMergedMaterial.ShouldGenerateDataForProperty(Property)) { const FIntPoint AtlasTextureSize = OutMergedMaterial.GetPropertySize(Property); const FIntPoint ExportTextureSize = AtlasTextureSize / AtlasGridSize; const int32 AtlasNumSamples = AtlasTextureSize.X*AtlasTextureSize.Y; check(OutMergedMaterial.GetPropertySize(Property) == AtlasTextureSize); TArray& Samples = OutMergedMaterial.GetPropertySamples(Property); Samples.SetNumUninitialized(AtlasNumSamples); // Fill with magenta (as we will be box blurring this later) for(FColor& SampleColor : Samples) { SampleColor = FColor(255, 0, 255); } } } int32 AtlasRowIdx = 0; int32 AtlasColIdx = 0; FIntPoint Gutter(InGutter, InGutter); FIntPoint DoubleGutter(InGutter * 2, InGutter * 2); FIntPoint GlobalAtlasTargetPos = Gutter; bool bSamplesWritten[(uint32)EFlattenMaterialProperties::NumFlattenMaterialProperties]; FMemory::Memset(bSamplesWritten, 0); // Used to calculate UV transforms const FIntPoint GlobalAtlasTextureSize = OutMergedMaterial.GetPropertySize(EFlattenMaterialProperties::Diffuse); const FIntPoint GlobalExportTextureSize = (GlobalAtlasTextureSize / AtlasGridSize) - DoubleGutter; const FIntPoint GlobalExportEntrySize = (GlobalAtlasTextureSize / AtlasGridSize); // Flatten all materials and merge them into one material using texture atlases for (int32 MatIdx = 0; MatIdx < InMaterialList.Num(); ++MatIdx) { FFlattenMaterial& FlatMaterial = InMaterialList[MatIdx]; OutMergedMaterial.bTwoSided |= FlatMaterial.bTwoSided; OutMergedMaterial.bDitheredLODTransition = FlatMaterial.bDitheredLODTransition; for (int32 PropertyIndex = 0; PropertyIndex < (int32)EFlattenMaterialProperties::NumFlattenMaterialProperties; ++PropertyIndex) { const EFlattenMaterialProperties Property = (EFlattenMaterialProperties)PropertyIndex; const FIntPoint PropertyTextureSize = OutMergedMaterial.GetPropertySize(Property); const int32 NumPropertySamples = PropertyTextureSize.X*PropertyTextureSize.Y; const FIntPoint PropertyAtlasTextureSize = (PropertyTextureSize / AtlasGridSize) - DoubleGutter; const FIntPoint PropertyAtlasEntrySize = (PropertyTextureSize / AtlasGridSize); const FIntPoint AtlasTargetPos((AtlasColIdx * PropertyAtlasEntrySize.X) + InGutter, (AtlasRowIdx * PropertyAtlasEntrySize.Y) + InGutter); if (OutMergedMaterial.ShouldGenerateDataForProperty(Property) && FlatMaterial.DoesPropertyContainData(Property)) { TArray& SourceSamples = FlatMaterial.GetPropertySamples(Property); TArray& TargetSamples = OutMergedMaterial.GetPropertySamples(Property); if (FlatMaterial.IsPropertyConstant(Property)) { SetTextureRect(SourceSamples[0], PropertyAtlasTextureSize, TargetSamples.GetData(), PropertyTextureSize, AtlasTargetPos); } else { FIntPoint PropertySize = FlatMaterial.GetPropertySize(Property); PropertySize = ConditionalImageResize(PropertySize, PropertyAtlasTextureSize, SourceSamples, false); CopyTextureRect(SourceSamples.GetData(), PropertyAtlasTextureSize, TargetSamples.GetData(), PropertyTextureSize, AtlasTargetPos); FlatMaterial.SetPropertySize(Property, PropertySize); } bSamplesWritten[PropertyIndex] |= true; } } check(OutUVTransforms.IsValidIndex(MatIdx)); // Offset OutUVTransforms[MatIdx].Key = FVector2D( (float)GlobalAtlasTargetPos.X / GlobalAtlasTextureSize.X, (float)GlobalAtlasTargetPos.Y / GlobalAtlasTextureSize.Y); // Scale OutUVTransforms[MatIdx].Value = FVector2D( (float)GlobalExportTextureSize.X / GlobalAtlasTextureSize.X, (float)GlobalExportTextureSize.Y / GlobalAtlasTextureSize.Y); AtlasColIdx++; if (AtlasColIdx >= AtlasGridSize) { AtlasColIdx = 0; AtlasRowIdx++; } GlobalAtlasTargetPos = FIntPoint((AtlasColIdx * GlobalExportEntrySize.X) + InGutter, (AtlasRowIdx * GlobalExportEntrySize.Y) + InGutter); } // Check if some properties weren't populated with data (which means we can empty them out) for (int32 PropertyIndex = 0; PropertyIndex < (int32)EFlattenMaterialProperties::NumFlattenMaterialProperties; ++PropertyIndex) { EFlattenMaterialProperties Property = (EFlattenMaterialProperties)PropertyIndex; if (!bSamplesWritten[PropertyIndex]) { OutMergedMaterial.GetPropertySamples(Property).Empty(); OutMergedMaterial.SetPropertySize(Property, FIntPoint(0, 0)); } else { // Smear borders const FIntPoint PropertySize = OutMergedMaterial.GetPropertySize(Property); FMaterialBakingHelpers::PerformUVBorderSmear(OutMergedMaterial.GetPropertySamples(Property), PropertySize.X, PropertySize.Y); } } } void FMeshMergeUtilities::FlattenBinnedMaterials(TArray& InMaterialList, const TArray& InMaterialBoxes, int32 InGutter, bool bCopyOnlyMaskedPixels, FFlattenMaterial& OutMergedMaterial, TArray& OutUVTransforms) const { // Fill output UV transforms with invalid values OutUVTransforms.SetNumZeroed(InMaterialList.Num()); // Flatten emissive scale across all incoming materials OutMergedMaterial.EmissiveScale = FlattenEmissivescale(InMaterialList); // Merge all material properties for (int32 Index = 0; Index < (int32)EFlattenMaterialProperties::NumFlattenMaterialProperties; ++Index) { const EFlattenMaterialProperties Property = (EFlattenMaterialProperties)Index; const FIntPoint& OutTextureSize = OutMergedMaterial.GetPropertySize(Property); if (OutTextureSize != FIntPoint::ZeroValue) { TArray& OutSamples = OutMergedMaterial.GetPropertySamples(Property); OutSamples.Reserve(OutTextureSize.X * OutTextureSize.Y); OutSamples.SetNumUninitialized(OutTextureSize.X * OutTextureSize.Y); // Fill with magenta (as we will be box blurring this later) for(FColor& SampleColor : OutSamples) { SampleColor = FColor(255, 0, 255); } FVector2D Gutter2D((float)InGutter, (float)InGutter); bool bMaterialsWritten = false; for (int32 MaterialIndex = 0; MaterialIndex < InMaterialList.Num(); ++MaterialIndex) { // Determine output size and offset FFlattenMaterial& FlatMaterial = InMaterialList[MaterialIndex]; OutMergedMaterial.bDitheredLODTransition |= FlatMaterial.bDitheredLODTransition; OutMergedMaterial.bTwoSided |= FlatMaterial.bTwoSided; if (FlatMaterial.DoesPropertyContainData(Property)) { const FBox2D MaterialBox = InMaterialBoxes[MaterialIndex]; const FIntPoint& InputSize = FlatMaterial.GetPropertySize(Property); TArray& InputSamples = FlatMaterial.GetPropertySamples(Property); // Resize material to match output (area) size FIntPoint OutputSize = FIntPoint((OutTextureSize.X * MaterialBox.GetSize().X) - (InGutter * 2), (OutTextureSize.Y * MaterialBox.GetSize().Y) - (InGutter * 2)); ConditionalImageResize(InputSize, OutputSize, InputSamples, false); // Copy material data to the merged 'atlas' texture FIntPoint OutputPosition = FIntPoint((OutTextureSize.X * MaterialBox.Min.X) + InGutter, (OutTextureSize.Y * MaterialBox.Min.Y) + InGutter); CopyTextureRect(InputSamples.GetData(), OutputSize, OutSamples.GetData(), OutTextureSize, OutputPosition, bCopyOnlyMaskedPixels); // Set the UV tranforms only once if (Index == 0) { FUVOffsetScalePair& UVTransform = OutUVTransforms[MaterialIndex]; UVTransform.Key = MaterialBox.Min + (Gutter2D / FVector2D(OutTextureSize)); UVTransform.Value = MaterialBox.GetSize() - ((Gutter2D * 2.0f) / FVector2D(OutTextureSize)); } bMaterialsWritten = true; } } if (!bMaterialsWritten) { OutSamples.Empty(); OutMergedMaterial.SetPropertySize(Property, FIntPoint(0, 0)); } else { // Smear borders const FIntPoint PropertySize = OutMergedMaterial.GetPropertySize(Property); FMaterialBakingHelpers::PerformUVBorderSmear(OutSamples, PropertySize.X, PropertySize.Y); } } } } float FMeshMergeUtilities::FlattenEmissivescale(TArray& InMaterialList) const { // Find maximum emissive scaling value across materials float MaxScale = 0.0f; for (const FFlattenMaterial& Material : InMaterialList) { MaxScale = FMath::Max(MaxScale, Material.EmissiveScale); } // Renormalize samples const float Multiplier = 1.0f / MaxScale; const int32 NumThreads = [&]() { return FPlatformProcess::SupportsMultithreading() ? FPlatformMisc::NumberOfCores() : 1; }(); const int32 MaterialsPerThread = FMath::CeilToInt((float)InMaterialList.Num() / (float)NumThreads); ParallelFor(NumThreads, [&InMaterialList, MaterialsPerThread, Multiplier, MaxScale] (int32 Index) { int32 StartIndex = FMath::CeilToInt((float)Index * (float)MaterialsPerThread); const int32 EndIndex = FMath::Min(FMath::CeilToInt((float)(Index + 1) * (float)MaterialsPerThread), InMaterialList.Num()); for (; StartIndex < EndIndex; ++StartIndex) { FFlattenMaterial& Material = InMaterialList[StartIndex]; if (Material.EmissiveScale != MaxScale) { for (FColor& Sample : Material.GetPropertySamples(EFlattenMaterialProperties::Emissive)) { if (Sample != FColor::Magenta) { Sample.R = Sample.R * Multiplier; Sample.G = Sample.G * Multiplier; Sample.B = Sample.B * Multiplier; Sample.A = Sample.A * Multiplier; } } } } }, NumThreads == 1); return MaxScale; } static TArray GetCustomTextureCoordinates(const FMeshDescription& InMeshDescription, const UStaticMesh* InStaticMesh, const FMeshProxySettings& InMeshProxySettings) { TArray CustomTextureCoordinates; TVertexInstanceAttributesConstRef VertexInstanceUVs = FStaticMeshConstAttributes(InMeshDescription).GetVertexInstanceUVs(); // If we already have lightmap uvs generated and they are valid, we can reuse those instead of having to generate new ones const int32 LightMapCoordinateIndex = InStaticMesh->GetLightMapCoordinateIndex(); if (InMeshProxySettings.bReuseMeshLightmapUVs && LightMapCoordinateIndex > 0 && VertexInstanceUVs.GetNumElements() > 0 && VertexInstanceUVs.GetNumChannels() > LightMapCoordinateIndex) { for (const FVertexInstanceID VertexInstanceID : InMeshDescription.VertexInstances().GetElementIDs()) { CustomTextureCoordinates.Add(FVector2D(VertexInstanceUVs.Get(VertexInstanceID, LightMapCoordinateIndex))); } } else { FStaticMeshOperations::FGenerateUVOptions GenerateUVOptions; GenerateUVOptions.TextureResolution = InMeshProxySettings.MaterialSettings.TextureSize.GetMax(); GenerateUVOptions.bMergeTrianglesWithIdenticalAttributes = false; GenerateUVOptions.UVMethod = GetUVGenerationMethodToUse(); bool bSuccess = FStaticMeshOperations::GenerateUV(InMeshDescription, GenerateUVOptions, CustomTextureCoordinates); if (!bSuccess) { UE_LOG(LogMeshMerging, Warning, TEXT("GenerateUV: Failed to pack UVs for static mesh \"%s\" (num triangles = %d, texture resolution = %d)."), *InStaticMesh->GetName(), InMeshDescription.Triangles().Num(), InMeshProxySettings.MaterialSettings.TextureSize.GetMax()); CustomTextureCoordinates.Empty(); } } return CustomTextureCoordinates; } class FProxyMeshDescriptor { public: FProxyMeshDescriptor(const UStaticMeshComponent* StaticMeshComponent, int32 LODIndex) : LODIndex(LODIndex) , LightMapIndex(INDEX_NONE) { ISMDescriptor.InitFrom(StaticMeshComponent, false); // Retrieve lightmap for usage of lightmap data if (StaticMeshComponent->LODData.IsValidIndex(0)) { const FStaticMeshComponentLODInfo& ComponentLODInfo = StaticMeshComponent->LODData[0]; const FMeshMapBuildData* MeshMapBuildData = StaticMeshComponent->GetMeshMapBuildData(ComponentLODInfo); if (MeshMapBuildData) { LightMap = MeshMapBuildData->LightMap; LightMapIndex = StaticMeshComponent->GetStaticMesh()->GetLightMapCoordinateIndex(); } } Hash = ISMDescriptor.ComputeHash(); Hash = FCrc::TypeCrc32(LODIndex, Hash); if (LightMapIndex != INDEX_NONE) { Hash = FCrc::TypeCrc32(LightMap.GetReference(), Hash); Hash = FCrc::TypeCrc32(LightMapIndex, Hash); } } bool operator==(const FProxyMeshDescriptor& InOther) const { return Hash == InOther.Hash && LODIndex == InOther.LODIndex && LightMap == InOther.LightMap && LightMapIndex == InOther.LightMapIndex && ISMDescriptor == InOther.ISMDescriptor; } int32 GetLODIndex() const { return LODIndex; } FLightMapRef GetLightMap() const { return LightMap; } int32 GetLightMapIndex() const { return LightMapIndex; } UStaticMesh* GetStaticMesh() const { return ISMDescriptor.StaticMesh; } bool IsValid() const { return !MeshDescription.IsEmpty(); } const FMeshDescription& GetMeshDescription() const { return MeshDescription; } const TArray& GetCustomTextureCoordinates() const { return CustomTextureCoordinates; } void PrepareMeshDescription(const FMeshProxySettings& InMeshProxySettings) { TRACE_CPUPROFILER_EVENT_SCOPE(PrepareMeshDescription); // Retrieve mesh data in FMeshDescription form FStaticMeshAttributes(MeshDescription).Register(); FMeshMergeHelpers::RetrieveMesh(ISMDescriptor.StaticMesh, LODIndex, MeshDescription); CustomTextureCoordinates = ::GetCustomTextureCoordinates(MeshDescription, ISMDescriptor.StaticMesh, InMeshProxySettings); if (CustomTextureCoordinates.IsEmpty()) { // Failure, clear mesh description MeshDescription.Empty(); } } private: int32 Hash; int32 LODIndex; FLightMapRef LightMap; int32 LightMapIndex; FISMComponentDescriptor ISMDescriptor; FMeshDescription MeshDescription; TArray CustomTextureCoordinates; }; static void ScaleTextureCoordinatesToBox(const FBox2D& Box, TArray& InOutTextureCoordinates) { const FBox2D CoordinateBox(InOutTextureCoordinates); const FVector2D CoordinateRange = CoordinateBox.GetSize(); const FVector2D Offset = CoordinateBox.Min + Box.Min; const FVector2D Scale = Box.GetSize() / CoordinateRange; for (FVector2D& Coordinate : InOutTextureCoordinates) { Coordinate = (Coordinate - Offset) * Scale; } } typedef TFunctionRef FGetMeshLODFunc; struct FInstancedMeshDescriptionData { FMeshDescription* MeshDescription; TArray InstancesTransforms; }; static TArray GatherGeometry(const TArray& InStaticMeshComponents, const FMeshProxySettings& InMeshProxySettings, TArray& InDescriptors, const TArray>& InMeshesToMergePerDescriptor, const TArray& InMeshesToMeshDescriptor, FGetMeshLODFunc InGetMeshLODFunc, int32& OutSummedLightmapPixels) { TRACE_CPUPROFILER_EVENT_SCOPE(FMeshMergeUtilities::GatherGeometry); TArray TempDescriptionData; TempDescriptionData.SetNum(InStaticMeshComponents.Num()); TAtomic SummedLightmapPixels(0); TArray MeshesDescriptions; MeshesDescriptions.SetNum(InDescriptors.Num()); // If grouping by identical meshes, prepare each mesh description along with their flattened UVs // These meshes descriptions will serve for material baking, but also as the basis for creating a // single merged mesh out of all instances. if (InMeshProxySettings.bGroupIdenticalMeshesForBaking) { TRACE_CPUPROFILER_EVENT_SCOPE(UniqueUVs); ParallelFor(InDescriptors.Num(), [&InDescriptors, &InMeshProxySettings](uint32 Index) { InDescriptors[Index].PrepareMeshDescription(InMeshProxySettings); }); } // Gather geometry from each component, expand ISMC geometry { TRACE_CPUPROFILER_EVENT_SCOPE(GatherGeometryFromComponents); ParallelFor(InStaticMeshComponents.Num(), [InStaticMeshComponents, &InGetMeshLODFunc, InDescriptors, InMeshesToMeshDescriptor, InMeshProxySettings, &TempDescriptionData, &SummedLightmapPixels](uint32 Index) { TRACE_CPUPROFILER_EVENT_SCOPE(GatherGeometryFromComponent); const UStaticMeshComponent* StaticMeshComponent = InStaticMeshComponents[Index]; FMeshDescription& MeshDescription = TempDescriptionData[Index]; // Retrieve meshes if (!InMeshProxySettings.bGroupIdenticalMeshesForBaking || !InDescriptors[InMeshesToMeshDescriptor[Index]].IsValid()) { const int32 LODIndex = InGetMeshLODFunc(StaticMeshComponent); static const bool bPropagateVertexColours = true; // Retrieve mesh data in FMeshDescription form FStaticMeshAttributes(MeshDescription).Register(); FMeshMergeHelpers::RetrieveMesh(StaticMeshComponent, LODIndex, MeshDescription, bPropagateVertexColours); } else { MeshDescription = InDescriptors[InMeshesToMeshDescriptor[Index]].GetMeshDescription(); if (!InMeshProxySettings.bGroupIdenticalMeshesForBaking) { FStaticMeshOperations::ApplyTransform(MeshDescription, StaticMeshComponent->GetComponentTransform()); } } // If the component is an ISMC then we need to duplicate the vertex data int32 NumInstances = 1; if (const UInstancedStaticMeshComponent* InstancedStaticMeshComponent = Cast(StaticMeshComponent)) { if (!InMeshProxySettings.bGroupIdenticalMeshesForBaking) { FMeshMergeHelpers::ExpandInstances(InstancedStaticMeshComponent, MeshDescription); } NumInstances = InstancedStaticMeshComponent->PerInstanceSMData.Num(); } int32 LightMapWidth, LightMapHeight; StaticMeshComponent->GetLightMapResolution(LightMapWidth, LightMapHeight); // Make sure we at least have some lightmap space allocated in case the static mesh is set up with invalid input SummedLightmapPixels += FMath::Max(16, LightMapHeight * LightMapWidth * NumInstances); }, EParallelForFlags::Unbalanced); } // For each mesh, append each component geometry { TRACE_CPUPROFILER_EVENT_SCOPE(AppendMeshes); FStaticMeshOperations::FAppendSettings AppendSettings; for (int32 ChannelIdx = 0; ChannelIdx < FStaticMeshOperations::FAppendSettings::MAX_NUM_UV_CHANNELS; ++ChannelIdx) { AppendSettings.bMergeUVChannels[ChannelIdx] = true; } ParallelFor(InDescriptors.Num(), [&MeshesDescriptions, &InMeshesToMergePerDescriptor, &InStaticMeshComponents, &TempDescriptionData, &AppendSettings, &InMeshProxySettings](uint32 Index) { TRACE_CPUPROFILER_EVENT_SCOPE(AppendMeshes); FMeshDescription* TargetMeshDescription = new FMeshDescription(); FStaticMeshAttributes(*TargetMeshDescription).Register(); // When using this option, do not expand the instances, but rather send their transforms to the ProxyLOD tool if (InMeshProxySettings.bGroupIdenticalMeshesForBaking) { TArray InstancesTransforms; for (int32 Idx : InMeshesToMergePerDescriptor[Index]) { UStaticMeshComponent* StaticMeshComponent = InStaticMeshComponents[Idx]; if (InMeshProxySettings.bGroupIdenticalMeshesForBaking) { FTransform ComponentTransform = StaticMeshComponent->GetComponentTransform(); if (const UInstancedStaticMeshComponent* InstancedStaticMeshComponent = Cast(StaticMeshComponent)) { for (const FInstancedStaticMeshInstanceData& InstanceData : InstancedStaticMeshComponent->PerInstanceSMData) { InstancesTransforms.Add(FTransform(InstanceData.Transform) * ComponentTransform); } } else { InstancesTransforms.Add(ComponentTransform); } } } if (!InMeshesToMergePerDescriptor[Index].IsEmpty()) { *TargetMeshDescription = TempDescriptionData[InMeshesToMergePerDescriptor[Index][0]]; } MeshesDescriptions[Index].InstancesTransforms = MoveTemp(InstancesTransforms); } else { TArray SourceMeshDescriptions; SourceMeshDescriptions.Reserve(InMeshesToMergePerDescriptor[Index].Num()); for (int32 TempIdx : InMeshesToMergePerDescriptor[Index]) { SourceMeshDescriptions.Add(&TempDescriptionData[TempIdx]); } FStaticMeshOperations::AppendMeshDescriptions(SourceMeshDescriptions, *TargetMeshDescription, AppendSettings); } MeshesDescriptions[Index].MeshDescription = TargetMeshDescription; }, EParallelForFlags::Unbalanced); } OutSummedLightmapPixels = SummedLightmapPixels; return MeshesDescriptions; } TArray PrepareBakingMeshes(const struct FMeshProxySettings& InMeshProxySettings, const TArray& InDescriptors, TArray InMeshDescriptionData) { TRACE_CPUPROFILER_EVENT_SCOPE(PrepareBakingMeshes) check(InDescriptors.Num() == InMeshDescriptionData.Num()); TArray MeshData; MeshData.SetNum(InDescriptors.Num()); // GetLightMap() must be called from the game thread check(IsInGameThread()); for (int32 MeshIndex = 0; MeshIndex < InDescriptors.Num(); ++MeshIndex) { if (InDescriptors[MeshIndex].GetLightMapIndex() != INDEX_NONE) { MeshData[MeshIndex].LightMap = InDescriptors[MeshIndex].GetLightMap(); MeshData[MeshIndex].LightMapIndex = InDescriptors[MeshIndex].GetLightMapIndex(); } } // Parallel step - fetching custom (unwrapped) texture coordinates is the slowest part here ParallelFor(InDescriptors.Num(), [&MeshData, &InDescriptors, &InMeshDescriptionData, &InMeshProxySettings](uint32 MeshIndex) { const FProxyMeshDescriptor& MeshDescriptor = InDescriptors[MeshIndex]; FMeshData& MeshSettings = MeshData[MeshIndex]; MeshSettings.TextureCoordinateBox = FBox2D(FVector2D(0.0f, 0.0f), FVector2D(1.0f, 1.0f)); if (InMeshProxySettings.bGroupIdenticalMeshesForBaking) { // Grouping by identical meshes, the UVs should have already been setup MeshSettings.MeshDescription = &MeshDescriptor.GetMeshDescription(); MeshSettings.CustomTextureCoordinates = MeshDescriptor.GetCustomTextureCoordinates(); } else { FMeshDescription& MeshDescription = *InMeshDescriptionData[MeshIndex].MeshDescription; MeshSettings.MeshDescription = &MeshDescription; MeshSettings.CustomTextureCoordinates = GetCustomTextureCoordinates(MeshDescription, MeshDescriptor.GetStaticMesh(), InMeshProxySettings); } if (MeshSettings.CustomTextureCoordinates.IsEmpty()) { MeshSettings.MeshDescription = nullptr; MeshSettings.TextureCoordinateIndex = 0; } }); return MeshData; } void FMeshMergeUtilities::CreateProxyMesh(const TArray& InActors, const struct FMeshProxySettings& InMeshProxySettings, UPackage* InOuter, const FString& InProxyBasePackageName, const FGuid InGuid, const FCreateProxyDelegate& InProxyCreatedDelegate, const bool bAllowAsync, const float ScreenSize) const { CreateProxyMesh(InActors, InMeshProxySettings, GEngine->DefaultFlattenMaterial, InOuter, InProxyBasePackageName, InGuid, InProxyCreatedDelegate, bAllowAsync, ScreenSize); } void FMeshMergeUtilities::CreateProxyMesh(const TArray& InStaticMeshComps, const struct FMeshProxySettings& InMeshProxySettings, UPackage* InOuter, const FString& InProxyBasePackageName, const FGuid InGuid, const FCreateProxyDelegate& InProxyCreatedDelegate, const bool bAllowAsync, const float ScreenSize) const { CreateProxyMesh(InStaticMeshComps, InMeshProxySettings, GEngine->DefaultFlattenMaterial, InOuter, InProxyBasePackageName, InGuid, InProxyCreatedDelegate, bAllowAsync, ScreenSize); } void FMeshMergeUtilities::CreateProxyMesh(const TArray& InActors, const struct FMeshProxySettings& InMeshProxySettings, UMaterialInterface* InBaseMaterial, UPackage* InOuter, const FString& InProxyBasePackageName, const FGuid InGuid, const FCreateProxyDelegate& InProxyCreatedDelegate, const bool bAllowAsync /*= false*/, const float ScreenSize /*= 1.0f*/) const { // No actors given as input if (InActors.Num() == 0) { UE_LOG(LogMeshMerging, Log, TEXT("No actors specified to generate a proxy mesh for")); return; } // Collect components to merge TArray ComponentsToMerge; for (AActor* Actor : InActors) { TInlineComponentArray Components; Actor->GetComponents(Components); ComponentsToMerge.Append(Components); } CreateProxyMesh(ComponentsToMerge, InMeshProxySettings, InBaseMaterial, InOuter, InProxyBasePackageName, InGuid, InProxyCreatedDelegate, bAllowAsync, ScreenSize); } void FMeshMergeUtilities::CreateProxyMesh(const TArray& InComponentsToMerge, const struct FMeshProxySettings& InMeshProxySettings, UMaterialInterface* InBaseMaterial, UPackage* InOuter, const FString& InProxyBasePackageName, const FGuid InGuid, const FCreateProxyDelegate& InProxyCreatedDelegate, const bool bAllowAsync, const float ScreenSize) const { TRACE_CPUPROFILER_EVENT_SCOPE(FMeshMergeUtilities::CreateProxyMesh) // The MeshReductionInterface manages the choice mesh reduction plugins, Unreal native vs third party (e.g. Simplygon) IMeshReductionModule& ReductionModule = FModuleManager::Get().LoadModuleChecked("MeshReductionInterface"); // Error/warning checking for input if (ReductionModule.GetMeshMergingInterface() == nullptr) { UE_LOG(LogMeshMerging, Error, TEXT("No mesh reduction module available. You must enable a plugin that provides that functionality (ex: ProxyLODPlugin)")); return; } // Check that the delegate has a func-ptr bound to it if (!InProxyCreatedDelegate.IsBound()) { UE_LOG(LogMeshMerging, Warning, TEXT("Invalid (unbound) delegate for returning generated proxy mesh")); return; } TArray ComponentsToMerge = InComponentsToMerge; // Remove invalid components ComponentsToMerge.RemoveAll([](UStaticMeshComponent* Val) { return Val->GetStaticMesh() == nullptr; }); // No actors given as input if (ComponentsToMerge.Num() == 0) { UE_LOG(LogMeshMerging, Log, TEXT("No static mesh specified to generate a proxy mesh for")); TArray OutAssetsToSync; InProxyCreatedDelegate.ExecuteIfBound(InGuid, OutAssetsToSync); return; } // Base asset name for a new assets // In case outer is null ProxyBasePackageName has to be long package name if (InOuter == nullptr && FPackageName::IsShortPackageName(InProxyBasePackageName)) { UE_LOG(LogMeshMerging, Warning, TEXT("Invalid long package name: '%s'."), *InProxyBasePackageName); return; } FScopedSlowTask SlowTask(100.f, (LOCTEXT("CreateProxyMesh_CreateMesh", "Creating Mesh Proxy"))); SlowTask.MakeDialog(); TArray SourceMeshes; TMap> GlobalMaterialMap; FBoxSphereBounds::Builder EstimatedBoundsBuilder; for (const UStaticMeshComponent* StaticMeshComponent : ComponentsToMerge) { EstimatedBoundsBuilder += StaticMeshComponent->Bounds; } FBoxSphereBounds EstimatedBounds = EstimatedBoundsBuilder; static const float FOVRad = FMath::DegreesToRadians(45.0f); static const FMatrix ProjectionMatrix = FPerspectiveMatrix(FOVRad, 1920, 1080, 0.01f); FHierarchicalLODUtilitiesModule& HLODModule = FModuleManager::LoadModuleChecked("HierarchicalLODUtilities"); IHierarchicalLODUtilities* Utilities = HLODModule.GetUtilities(); float EstimatedDistance = Utilities->CalculateDrawDistanceFromScreenSize(EstimatedBounds.SphereRadius, ScreenSize, ProjectionMatrix); auto SelectLODFunc = [&InMeshProxySettings, &Utilities, EstimatedDistance] (const UStaticMeshComponent* Component) { int32 LODIndex = 0; if (InMeshProxySettings.bCalculateCorrectLODModel) { LODIndex = Utilities->GetLODLevelForScreenSize(Component, Utilities->CalculateScreenSizeFromDrawDistance(Component->Bounds.SphereRadius, ProjectionMatrix, EstimatedDistance)); } return LODIndex; }; SlowTask.EnterProgressFrame(5.0f, LOCTEXT("CreateProxyMesh_CollectingMeshes", "Collecting Input Static Meshes")); // Mesh / LOD index TMap RawMeshLODs; // Mesh index, TMultiMap> MeshSectionToUniqueSection; // Unique set of sections in mesh TArray UniqueSections; TMultiMap SectionToMesh; TArray ImposterMeshComponents; TArray StaticMeshComponents; for (UStaticMeshComponent* StaticMeshComponent : ComponentsToMerge) { if (StaticMeshComponent->HLODBatchingPolicy != EHLODBatchingPolicy::None) { ImposterMeshComponents.Add(StaticMeshComponent); } else { StaticMeshComponents.Add(StaticMeshComponent); } } TArray MeshDescriptors; TArray> MeshesToMergePerDescriptor; TArray MeshToMeshDescriptor; TArray> GlobalSections; MeshToMeshDescriptor.Reserve(StaticMeshComponents.Num()); for (int32 ComponentIndex = 0; ComponentIndex < StaticMeshComponents.Num(); ++ComponentIndex) { const UStaticMeshComponent* StaticMeshComponent = StaticMeshComponents[ComponentIndex]; FProxyMeshDescriptor MeshDescriptor(StaticMeshComponent, SelectLODFunc(StaticMeshComponent)); int32 Index; if (!InMeshProxySettings.bGroupIdenticalMeshesForBaking || !MeshDescriptors.Find(MeshDescriptor, Index)) { Index = MeshDescriptors.Num(); MeshDescriptors.Add(MeshDescriptor); MeshesToMergePerDescriptor.AddDefaulted(); TArray& Sections = GlobalSections.AddDefaulted_GetRef(); // Extract sections for given LOD index from the mesh FMeshMergeHelpers::ExtractSections(StaticMeshComponent, MeshDescriptor.GetLODIndex(), Sections); } MeshesToMergePerDescriptor[Index].Add(ComponentIndex); MeshToMeshDescriptor.Add(Index); } for (int32 MeshIndex = 0; MeshIndex < GlobalSections.Num(); ++MeshIndex) { TArray& Sections = GlobalSections[MeshIndex]; for (int32 SectionIndex = 0; SectionIndex < Sections.Num(); ++SectionIndex) { FSectionInfo& Section = Sections[SectionIndex]; const int32 UniqueIndex = UniqueSections.AddUnique(Section); MeshSectionToUniqueSection.Add(MeshIndex, TPair(SectionIndex, UniqueIndex)); SectionToMesh.Add(UniqueIndex, MeshIndex); } } int32 SummedLightmapPixels; TArray MeshDescriptionData = GatherGeometry(StaticMeshComponents, InMeshProxySettings, MeshDescriptors, MeshesToMergePerDescriptor, MeshToMeshDescriptor, SelectLODFunc, SummedLightmapPixels); TArray MeshBakingData = PrepareBakingMeshes(InMeshProxySettings, MeshDescriptors, MeshDescriptionData); TArray UniqueMaterials; //Unique material index to unique section index TMultiMap MaterialToSectionMap; for (int32 SectionIndex = 0; SectionIndex < UniqueSections.Num(); ++SectionIndex) { FSectionInfo& Section = UniqueSections[SectionIndex]; const int32 UniqueIndex = UniqueMaterials.AddUnique(Section.Material); MaterialToSectionMap.Add(UniqueIndex, SectionIndex); } TArray GlobalMeshSettings; TArray GlobalMaterialSettings; FMaterialProxySettings MaterialProxySettings = InMeshProxySettings.MaterialSettings; TArray PrimitiveComponents; Algo::Transform(StaticMeshComponents, PrimitiveComponents, [](UStaticMeshComponent* SMComponent) { return SMComponent; }); if (MaterialProxySettings.ResolveTexelDensity(PrimitiveComponents)) { double Total3DArea = 0; for (const FInstancedMeshDescriptionData& InstancedMeshDescriptionData : MeshDescriptionData) { double Mesh3DArea = 0; const FMeshDescription& MeshDescription = *InstancedMeshDescriptionData.MeshDescription; FStaticMeshConstAttributes Attributes(MeshDescription); TVertexAttributesConstRef Positions = Attributes.GetVertexPositions(); for (const FTriangleID TriangleID : MeshDescription.Triangles().GetElementIDs()) { // World space area TArrayView TriVertices = MeshDescription.GetTriangleVertices(TriangleID); Mesh3DArea += UE::Geometry::VectorUtil::Area(Positions[TriVertices[0]], Positions[TriVertices[1]], Positions[TriVertices[2]]); } // Account for multiple instances (no transforms means a single instance) uint32 NumInstances = FMath::Max(1, InstancedMeshDescriptionData.InstancesTransforms.Num()); Total3DArea += Mesh3DArea * NumInstances; } MaterialProxySettings.TextureSize = FMaterialUtilities::GetTextureSizeFromTargetTexelDensity(Total3DArea, 1.0f, MaterialProxySettings.TargetTexelDensityPerMeter); MaterialProxySettings.TextureSizingType = ETextureSizingType::TextureSizingType_UseSingleTextureSize; } UMaterialOptions* Options = PopulateMaterialOptions(MaterialProxySettings); TGCObjectScopeGuard MaterialOptionsGCScopeGuard(Options); TArray MaterialProperties; for (const FPropertyEntry& Entry : Options->Properties) { if (Entry.Property != MP_MAX) { MaterialProperties.Add(Entry.Property); } } // Mesh index / ( Mesh relative section index / output index ) TMultiMap> OutputMaterialsMap; { TRACE_CPUPROFILER_EVENT_SCOPE(FMeshMergeUtilities::MaterialAnalysisAndUVGathering); for (int32 MaterialIndex = 0; MaterialIndex < UniqueMaterials.Num(); ++MaterialIndex) { UMaterialInterface* Material = UniqueMaterials[MaterialIndex]; //Unique section indices TArray SectionIndices; MaterialToSectionMap.MultiFind(MaterialIndex, SectionIndices); // Check whether or not this material requires mesh data int32 NumTexCoords = 0; bool bUseVertexData = false; FMaterialUtilities::AnalyzeMaterial(Material, MaterialProperties, NumTexCoords, bUseVertexData); FMaterialData MaterialSettings; MaterialSettings.Material = Material; for (const FPropertyEntry& Entry : Options->Properties) { if (!Entry.bUseConstantValue && Material->IsPropertyActive(Entry.Property) && Entry.Property != MP_MAX) { MaterialSettings.PropertySizes.Add(Entry.Property, Entry.bUseCustomSize ? Entry.CustomSize : Options->TextureSize); } } if (bUseVertexData || NumTexCoords != 0) { for (uint32 SectionIndex : SectionIndices) { TArray MeshIndices; SectionToMesh.MultiFind(SectionIndex, MeshIndices); for (const uint32 MeshIndex : MeshIndices) { FMeshData MeshSettings = MeshBakingData[MeshIndex]; // Section index is a unique one so we need to map it to the mesh's equivalent(s) TArray> SectionToUniqueSectionIndices; MeshSectionToUniqueSection.MultiFind(MeshIndex, SectionToUniqueSectionIndices); for (const TPair& IndexPair : SectionToUniqueSectionIndices) { if (IndexPair.Value == SectionIndex) { MeshSettings.MaterialIndices.Add(IndexPair.Key); OutputMaterialsMap.Add(MeshIndex, TPair(IndexPair.Key, GlobalMeshSettings.Num())); } } GlobalMeshSettings.Add(MoveTemp(MeshSettings)); GlobalMaterialSettings.Add(MaterialSettings); } } } else { // Add simple bake entry FMeshData MeshSettings; MeshSettings.MeshDescription = nullptr; MeshSettings.TextureCoordinateBox = FBox2D(FVector2D(0.0f, 0.0f), FVector2D(1.0f, 1.0f)); MeshSettings.TextureCoordinateIndex = 0; // For each original material index add an entry to the corresponding LOD and bake output index for (uint32 SectionIndex : SectionIndices) { TArray MeshIndices; SectionToMesh.MultiFind(SectionIndex, MeshIndices); for (uint32 MeshIndex : MeshIndices) { TArray> SectionToUniqueSectionIndices; MeshSectionToUniqueSection.MultiFind(MeshIndex, SectionToUniqueSectionIndices); for (const TPair& IndexPair : SectionToUniqueSectionIndices) { if (IndexPair.Value == SectionIndex) { OutputMaterialsMap.Add(MeshIndex, TPair(IndexPair.Key, GlobalMeshSettings.Num())); } } } } GlobalMeshSettings.Add(MeshSettings); GlobalMaterialSettings.Add(MaterialSettings); } } } TArray FlattenedMaterials; IMaterialBakingModule& MaterialBakingModule = FModuleManager::Get().LoadModuleChecked("MaterialBaking"); auto MaterialFlattenLambda = [this, &Options, &GlobalMeshSettings, &GlobalMaterialSettings, &MeshDescriptionData, &OutputMaterialsMap, &MaterialBakingModule](TArray& FlattenedMaterialArray) { TRACE_CPUPROFILER_EVENT_SCOPE(MaterialFlatten) TArray MeshSettingPtrs; for (int32 SettingsIndex = 0; SettingsIndex < GlobalMeshSettings.Num(); ++SettingsIndex) { MeshSettingPtrs.Add(&GlobalMeshSettings[SettingsIndex]); } TArray MaterialSettingPtrs; for (int32 SettingsIndex = 0; SettingsIndex < GlobalMaterialSettings.Num(); ++SettingsIndex) { MaterialSettingPtrs.Add(&GlobalMaterialSettings[SettingsIndex]); } // This scope ensures BakeOutputs is never used after TransferOutputToFlatMaterials { TArray BakeOutputs; MaterialBakingModule.BakeMaterials(MaterialSettingPtrs, MeshSettingPtrs, BakeOutputs); // Append constant properties ? TArray ConstantData; FIntPoint ConstantSize(1, 1); for (const FPropertyEntry& Entry : Options->Properties) { if (Entry.bUseConstantValue && Entry.Property != MP_MAX) { ConstantData.SetNum(1, EAllowShrinking::No); ConstantData[0] = FColor(Entry.ConstantValue * 255.0f, Entry.ConstantValue * 255.0f, Entry.ConstantValue * 255.0f); for (FBakeOutput& Output : BakeOutputs) { Output.PropertyData.Add(Entry.Property, ConstantData); Output.PropertySizes.Add(Entry.Property, ConstantSize); } } } TransferOutputToFlatMaterials(GlobalMaterialSettings, BakeOutputs, FlattenedMaterialArray); } { TRACE_CPUPROFILER_EVENT_SCOPE(RemapBakedMaterials) // Now have the baked out material data, need to have a map or actually remap the raw mesh data to baked material indices for (int32 MeshIndex = 0; MeshIndex < MeshDescriptionData.Num(); ++MeshIndex) { FMeshDescription& MeshDescription = *MeshDescriptionData[MeshIndex].MeshDescription; TArray> SectionAndOutputIndices; OutputMaterialsMap.MultiFind(MeshIndex, SectionAndOutputIndices); TArray Remap; // Reorder loops for (const TPair& IndexPair : SectionAndOutputIndices) { const int32 SectionIndex = IndexPair.Key; const int32 NewIndex = IndexPair.Value; if (Remap.Num() < (SectionIndex + 1)) { Remap.SetNum(SectionIndex + 1); } Remap[SectionIndex] = NewIndex; } TMap RemapPolygonGroup; for (const FPolygonGroupID PolygonGroupID : MeshDescription.PolygonGroups().GetElementIDs()) { checkf(Remap.IsValidIndex(PolygonGroupID.GetValue()), TEXT("Missing material bake output index entry for mesh(section)")); int32 RemapID = Remap[PolygonGroupID.GetValue()]; RemapPolygonGroup.Add(PolygonGroupID, FPolygonGroupID(RemapID)); } MeshDescription.RemapPolygonGroups(RemapPolygonGroup); } } }; // Landscape culling. NB these are temporary copies of the culling data and should be deleted after use. TArray CullingRawMeshes; if (InMeshProxySettings.bUseLandscapeCulling) { SlowTask.EnterProgressFrame(5.0f, LOCTEXT("CreateProxyMesh_LandscapeCulling", "Applying Landscape Culling")); UWorld* InWorld = ComponentsToMerge[0]->GetWorld(); FMeshMergeHelpers::RetrieveCullingLandscapeAndVolumes(InWorld, EstimatedBounds, InMeshProxySettings.LandscapeCullingPrecision, CullingRawMeshes); } // Allocate merge complete data FMergeCompleteData* Data = new FMergeCompleteData(); Data->InOuter = InOuter; Data->InProxySettings = InMeshProxySettings; Data->ProxyBasePackageName = InProxyBasePackageName; Data->CallbackDelegate = InProxyCreatedDelegate; Data->ImposterComponents = ImposterMeshComponents; Data->StaticMeshComponents = StaticMeshComponents; Data->BaseMaterial = InBaseMaterial; // Lightmap resolution if (InMeshProxySettings.bComputeLightMapResolution) { Data->InProxySettings.LightMapResolution = FMath::CeilToInt(FMath::Sqrt(static_cast(SummedLightmapPixels))); } // Add this proxy job to map Processor->AddProxyJob(InGuid, Data); TArray MergeDataEntries; { TRACE_CPUPROFILER_EVENT_SCOPE(MergeDataPreparation) for (int32 Index = 0; Index < MeshDescriptionData.Num(); ++Index) { FInstancedMeshMergeData MergeData; MergeData.SourceStaticMesh = MeshDescriptors[Index].GetStaticMesh(); MergeData.RawMesh = MeshDescriptionData[Index].MeshDescription; MergeData.NewUVs = MeshDescriptors[Index].GetCustomTextureCoordinates(); MergeData.bIsClippingMesh = false; MergeData.InstanceTransforms = MeshDescriptionData[Index].InstancesTransforms; FMeshMergeHelpers::CalculateTextureCoordinateBoundsForMesh(*MergeData.RawMesh, MergeData.TexCoordBounds); if (MergeData.NewUVs.IsEmpty()) { FMeshData* MeshData = GlobalMeshSettings.FindByPredicate([&](const FMeshData& Entry) { return Entry.MeshDescription == MergeData.RawMesh && (Entry.CustomTextureCoordinates.Num() || Entry.TextureCoordinateIndex != 0); }); if (MeshData) { if (MeshData->CustomTextureCoordinates.Num()) { MergeData.NewUVs = MeshData->CustomTextureCoordinates; } else { TVertexInstanceAttributesConstRef VertexInstanceUVs = FStaticMeshConstAttributes(*MeshData->MeshDescription).GetVertexInstanceUVs(); MergeData.NewUVs.Reset(MeshData->MeshDescription->VertexInstances().Num()); for (const FVertexInstanceID VertexInstanceID : MeshData->MeshDescription->VertexInstances().GetElementIDs()) { MergeData.NewUVs.Add(FVector2D(VertexInstanceUVs.Get(VertexInstanceID, MeshData->TextureCoordinateIndex))); } } MergeData.TexCoordBounds[0] = FBox2D(FVector2D(0.0f, 0.0f), FVector2D(1.0f, 1.0f)); } } MergeDataEntries.Add(MergeData); } } if (MergeDataEntries.Num() != 0) { // Populate landscape clipping geometry for (FMeshDescription* RawMesh : CullingRawMeshes) { FInstancedMeshMergeData ClipData; ClipData.bIsClippingMesh = true; ClipData.RawMesh = RawMesh; MergeDataEntries.Add(ClipData); } SlowTask.EnterProgressFrame(50.0f, LOCTEXT("CreateProxyMesh_GenerateProxy", "Generating Proxy Mesh")); { TRACE_CPUPROFILER_EVENT_SCOPE(ProxyGeneration) // Choose Simplygon Swarm (if available) or local proxy lod method if (ReductionModule.GetDistributedMeshMergingInterface() != nullptr && GetDefault()->bUseSimplygonSwarm && bAllowAsync) { MaterialFlattenLambda(FlattenedMaterials); ReductionModule.GetDistributedMeshMergingInterface()->ProxyLOD(MergeDataEntries, Data->InProxySettings, FlattenedMaterials, InGuid); } else { IMeshMerging* MeshMerging = ReductionModule.GetMeshMergingInterface(); // Register the Material Flattening code if parallel execution is supported, otherwise directly run it. if (MeshMerging->bSupportsParallelMaterialBake()) { MeshMerging->BakeMaterialsDelegate.BindLambda(MaterialFlattenLambda); } else { MaterialFlattenLambda(FlattenedMaterials); } MeshMerging->ProxyLOD(MergeDataEntries, Data->InProxySettings, FlattenedMaterials, InGuid); Processor->Tick(0); // make sure caller gets merging results } } } else { FMeshDescription MeshDescription; FStaticMeshAttributes(MeshDescription).Register(); FFlattenMaterial FlattenMaterial; Processor->ProxyGenerationComplete(MeshDescription, FlattenMaterial, InGuid); } TRACE_CPUPROFILER_EVENT_SCOPE(Cleanup) // Clean up the CullingRawMeshes ParallelFor(CullingRawMeshes.Num(), [&CullingRawMeshes](int32 Index) { delete CullingRawMeshes[Index]; } ); // Clean up the MeshDescriptionData ParallelFor( MeshDescriptionData.Num(), [&MeshDescriptionData](int32 Index) { delete MeshDescriptionData[Index].MeshDescription; } ); } void FMeshMergeUtilities::RetrieveMeshDescription(const UStaticMeshComponent* InStaticMeshComponent, int32 LODIndex, FMeshDescription& InOutMeshDescription, bool bPropagateMeshData) const { FMeshMergeHelpers::RetrieveMesh(InStaticMeshComponent, LODIndex, InOutMeshDescription, bPropagateMeshData); } void FMeshMergeUtilities::RetrieveMeshDescription(const USkeletalMeshComponent* InSkeletalMeshComponent, int32 LODIndex, FMeshDescription& InOutMeshDescription, bool bPropagateMeshData) const { FMeshMergeHelpers::RetrieveMesh(InSkeletalMeshComponent, LODIndex, InOutMeshDescription, bPropagateMeshData); } void FMeshMergeUtilities::RetrieveMeshDescription(const UStaticMesh* InStaticMesh, int32 LODIndex, FMeshDescription& InOutMeshDescription) const { FMeshMergeHelpers::RetrieveMesh(InStaticMesh, LODIndex, InOutMeshDescription); } void FMeshMergeUtilities::RegisterExtension(IMeshMergeExtension* InExtension) { MeshMergeExtensions.Add(InExtension); } void FMeshMergeUtilities::UnregisterExtension(IMeshMergeExtension* InExtension) { MeshMergeExtensions.Remove(InExtension); } bool RetrieveRawMeshData(FMeshMergeDataTracker& DataTracker , const int32 ComponentIndex , const int32 LODIndex , UStaticMeshComponent* Component , const bool bPropagateMeshData , TArray& Sections , FStaticMeshComponentAdapter& Adapter , const bool bMergeMaterialData , const FMeshMergingSettings& InSettings) { // Retrieve raw mesh data FMeshDescription& RawMesh = DataTracker.AddAndRetrieveRawMesh(ComponentIndex, LODIndex, Component->GetStaticMesh()); Adapter.RetrieveRawMeshData(LODIndex, RawMesh, bPropagateMeshData); // Reset section for reuse Sections.SetNum(0, EAllowShrinking::No); // Extract sections for given LOD index from the mesh Adapter.RetrieveMeshSections(LODIndex, Sections); for (int32 SectionIndex = 0; SectionIndex < Sections.Num(); ++SectionIndex) { const FSectionInfo& Section = Sections[SectionIndex]; // Unique section index for remapping const int32 UniqueIndex = DataTracker.AddSection(Section); // Store of original to unique section index entry for this component + LOD index DataTracker.AddSectionRemapping(ComponentIndex, LODIndex, SectionIndex, UniqueIndex); DataTracker.AddMaterialSlotName(Section.Material, Section.MaterialSlotName); if (!bMergeMaterialData) { FStaticMeshOperations::SwapPolygonPolygonGroup(RawMesh, UniqueIndex, Section.StartIndex, Section.EndIndex, false); } } //Compact the PolygonGroupID to make sure it follow the section index FElementIDRemappings RemapInformation; RawMesh.Compact(RemapInformation); // If the component is an ISMC then we need to duplicate the vertex data if (Component->IsA()) { const UInstancedStaticMeshComponent* InstancedStaticMeshComponent = Cast(Component); FMeshMergeHelpers::ExpandInstances(InstancedStaticMeshComponent, RawMesh); } if (InSettings.bUseLandscapeCulling) { FMeshMergeHelpers::CullTrianglesFromVolumesAndUnderLandscapes(Component->GetWorld(), Adapter.GetBounds(), RawMesh); } // If the valid became invalid during retrieval remove it again const bool bValidMesh = RawMesh.VertexInstances().Num() > 0; if (!bValidMesh) { DataTracker.RemoveRawMesh(ComponentIndex, LODIndex); } else if (Component->GetStaticMesh() != nullptr) { // If the mesh is valid at this point, record the lightmap UV so we have a record for use later DataTracker.AddLightmapChannelRecord(ComponentIndex, LODIndex, Component->GetStaticMesh()->GetLightMapCoordinateIndex()); } return bValidMesh; } void FMeshMergeUtilities::MergeComponentsToStaticMesh(const TArray& ComponentsToMerge, UWorld* World, const FMeshMergingSettings& InSettings, UMaterialInterface* InBaseMaterial, UPackage* InOuter, const FString& InBasePackageName, TArray& OutAssetsToSync, FVector& OutMergedActorLocation, const float ScreenSize, bool bSilent /*= false*/) const { TRACE_CPUPROFILER_EVENT_SCOPE(FMeshMergeUtilities::MergeComponentsToStaticMesh); // Use first mesh for naming and pivot bool bFirstMesh = true; FString MergedAssetPackageName; FVector MergedAssetPivot; TArray StaticMeshComponentsToMerge; TArray ImposterComponents; for (int32 MeshId = 0; MeshId < ComponentsToMerge.Num(); ++MeshId) { UStaticMeshComponent* MeshComponent = Cast(ComponentsToMerge[MeshId]); if (MeshComponent && MeshComponent->GetStaticMesh()) { // Make sure referenced lightmaps and shadowmaps are compiled if (MeshComponent->LODData.IsValidIndex(0)) { const FStaticMeshComponentLODInfo& ComponentLODInfo = MeshComponent->LODData[0]; const FMeshMapBuildData* MeshMapBuildData = MeshComponent->GetMeshMapBuildData(ComponentLODInfo); if (MeshMapBuildData) { TArray ReferencedTextures; FLightMap2D* Lightmap = MeshMapBuildData && MeshMapBuildData->LightMap ? MeshMapBuildData->LightMap->GetLightMap2D() : nullptr; if (Lightmap) { Lightmap->GetReferencedTextures(ReferencedTextures); } FShadowMap2D* Shadowmap = MeshMapBuildData && MeshMapBuildData->ShadowMap ? MeshMapBuildData->ShadowMap->GetShadowMap2D() : nullptr; if (Shadowmap && Shadowmap->IsValid()) { ReferencedTextures.Add(Shadowmap->GetTexture()); } FTextureCompilingManager::Get().FinishCompilation(TArray(MoveTemp(ReferencedTextures))); } } if((MeshComponent->HLODBatchingPolicy != EHLODBatchingPolicy::None) && InSettings.bIncludeImposters) { ImposterComponents.Add(MeshComponent); } else { StaticMeshComponentsToMerge.Add(MeshComponent); } // Save the pivot and asset package name of the first mesh, will later be used for creating merged mesh asset if (bFirstMesh) { // Mesh component pivot point MergedAssetPivot = InSettings.bPivotPointAtZero ? FVector::ZeroVector : MeshComponent->GetComponentTransform().GetLocation(); // Source mesh asset package name MergedAssetPackageName = MeshComponent->GetStaticMesh()->GetOutermost()->GetName(); bFirstMesh = false; } } } // Nothing to do if no StaticMeshComponents if (StaticMeshComponentsToMerge.Num() == 0 && ImposterComponents.Num() == 0) { return; } FMeshMergeDataTracker DataTracker; const bool bMergeAllLODs = InSettings.LODSelectionType == EMeshLODSelectionType::AllLODs; const bool bMergeMaterialData = InSettings.bMergeMaterials && InSettings.LODSelectionType != EMeshLODSelectionType::AllLODs; const bool bPropagateMeshData = InSettings.bBakeVertexDataToMesh || (bMergeMaterialData && InSettings.bUseVertexDataForBakingMaterial); TArray Adapters; TArray Sections; if (bMergeAllLODs) { TRACE_CPUPROFILER_EVENT_SCOPE(RetrieveRawMeshData); for (int32 ComponentIndex = 0; ComponentIndex < StaticMeshComponentsToMerge.Num(); ++ComponentIndex) { UStaticMeshComponent* Component = StaticMeshComponentsToMerge[ComponentIndex]; Adapters.Add(FStaticMeshComponentAdapter(Component)); FStaticMeshComponentAdapter& Adapter = Adapters.Last(); if (InSettings.bComputedLightMapResolution) { int32 LightMapHeight, LightMapWidth; if (Component->GetLightMapResolution(LightMapWidth, LightMapHeight)) { DataTracker.AddLightMapPixels(LightMapWidth * LightMapHeight); } } const int32 NumLODs = [&]() { const int32 NumberOfLODsAvailable = Adapter.GetNumberOfLODs(); if (Component->HLODBatchingPolicy != EHLODBatchingPolicy::None) { return InSettings.bIncludeImposters ? NumberOfLODsAvailable : NumberOfLODsAvailable - 1; } return NumberOfLODsAvailable; }(); for (int32 LODIndex = 0; LODIndex < NumLODs; ++LODIndex) { if (!RetrieveRawMeshData(DataTracker , ComponentIndex , LODIndex , Component , bPropagateMeshData , Sections , Adapter , false , InSettings)) { //If the rawmesh was not retrieve properly break the loop break; } DataTracker.AddLODIndex(LODIndex); } } } else { TRACE_CPUPROFILER_EVENT_SCOPE(RetrieveRawMeshData); // Retrieve HLOD module for calculating LOD index from screen size FHierarchicalLODUtilitiesModule& Module = FModuleManager::LoadModuleChecked("HierarchicalLODUtilities"); IHierarchicalLODUtilities* Utilities = Module.GetUtilities(); // Adding LOD 0 for merged mesh output DataTracker.AddLODIndex(0); // Retrieve mesh and section data for each component for (int32 ComponentIndex = 0; ComponentIndex < StaticMeshComponentsToMerge.Num(); ++ComponentIndex) { // Create material merge adapter for this component UStaticMeshComponent* Component = StaticMeshComponentsToMerge[ComponentIndex]; Adapters.Add(FStaticMeshComponentAdapter(Component)); FStaticMeshComponentAdapter& Adapter = Adapters.Last(); // Determine LOD to use for merging, either user specified or calculated index and ensure we clamp to the maximum LOD index for this adapter const int32 LODIndex = [&]() { int32 LowestDetailLOD = Adapter.GetNumberOfLODs() - 1; if (Component->HLODBatchingPolicy != EHLODBatchingPolicy::None && !InSettings.bIncludeImposters) { LowestDetailLOD = FMath::Max(0, LowestDetailLOD - 1); } switch (InSettings.LODSelectionType) { case EMeshLODSelectionType::SpecificLOD: return FMath::Min(LowestDetailLOD, InSettings.SpecificLOD); case EMeshLODSelectionType::CalculateLOD: return FMath::Min(LowestDetailLOD, Utilities->GetLODLevelForScreenSize(Component, FMath::Clamp(ScreenSize, 0.0f, 1.0f))); case EMeshLODSelectionType::LowestDetailLOD: default: return LowestDetailLOD; } }(); RetrieveRawMeshData(DataTracker , ComponentIndex , LODIndex , Component , bPropagateMeshData , Sections , Adapter , bMergeMaterialData , InSettings); } } { TRACE_CPUPROFILER_EVENT_SCOPE(ProcessRawMeshes); DataTracker.ProcessRawMeshes(); } // Merge sockets TMap MergedSockets; if (InSettings.bMergeMeshSockets) { const FTransform PivotTransform = FTransform(MergedAssetPivot); for (UPrimitiveComponent* PrimitiveComponent : ComponentsToMerge) { if (UStaticMeshComponent* StaticMeshComponent = Cast(PrimitiveComponent)) { if (UStaticMesh* StaticMesh = StaticMeshComponent->GetStaticMesh()) { for (UStaticMeshSocket* Socket : StaticMesh->Sockets) { if (Socket) { UStaticMeshSocket* SocketCopy = DuplicateObject(Socket, nullptr); // Fix name - rename if duplicates are found FString PlainName = SocketCopy->SocketName.GetPlainNameString(); int32 CurrentNumber = SocketCopy->SocketName.GetNumber(); while (MergedSockets.Contains(SocketCopy->SocketName)) { SocketCopy->SocketName = FName(PlainName, CurrentNumber++); } // Fix transform - make relative to pivot FTransform SocketTransformWorldSpace = StaticMeshComponent->GetSocketTransform(Socket->SocketName, RTS_World); FTransform SocketTransformPivotSpace = SocketTransformWorldSpace.GetRelativeTransform(PivotTransform); SocketCopy->RelativeLocation = SocketTransformPivotSpace.GetLocation(); SocketCopy->RelativeRotation = FRotator(SocketTransformPivotSpace.GetRotation()); SocketCopy->RelativeScale = SocketTransformPivotSpace.GetScale3D(); MergedSockets.Add(SocketCopy->SocketName, SocketCopy); } } } } } } // Find all unique materials and remap section to unique materials TArray UniqueMaterials; TMap CollapsedMaterialMap; for (int32 SectionIndex = 0; SectionIndex < DataTracker.NumberOfUniqueSections(); ++SectionIndex) { // Unique index for material UMaterialInterface* MaterialInterface = DataTracker.GetMaterialForSectionIndex(SectionIndex); int32 UniqueIndex = UniqueMaterials.IndexOfByPredicate([&InSettings, MaterialInterface](const UMaterialInterface* InMaterialInterface) { // Perform an optional custom comparison if we are trying to collapse material instances if (InSettings.bMergeEquivalentMaterials) { return FMaterialKey(MaterialInterface) == FMaterialKey(InMaterialInterface); } return MaterialInterface == InMaterialInterface; }); if (UniqueIndex == INDEX_NONE) { UniqueIndex = UniqueMaterials.Add(MaterialInterface); } // Update map to 'collapsed' materials CollapsedMaterialMap.Add(MaterialInterface, UniqueMaterials[UniqueIndex]); } // Retrieve physics data UBodySetup* BodySetupSource = nullptr; TArray PhysicsGeometry; if (InSettings.bMergePhysicsData) { RetrievePhysicsData(ComponentsToMerge, PhysicsGeometry, BodySetupSource); } TMultiMap< FMeshLODKey, MaterialRemapPair > OutputMaterialsMap; // If the user wants to merge materials into a single one if (bMergeMaterialData && UniqueMaterials.Num() != 0) { // Create the merged material FFlattenMaterial FlattenMaterial; CreateMergedMaterial(DataTracker, InSettings, StaticMeshComponentsToMerge, Adapters, UniqueMaterials, CollapsedMaterialMap, OutputMaterialsMap, bMergeAllLODs, bMergeMaterialData, MergedAssetPivot, FlattenMaterial); if (FlattenMaterial.HasData()) { // Don't recreate render states with the material update context as we will manually do it through // the FStaticMeshComponentRecreateRenderStateContext used below at the creation of the static mesh. FMaterialUpdateContext MaterialUpdateContext(FMaterialUpdateContext::EOptions::Default & ~FMaterialUpdateContext::EOptions::RecreateRenderStates); UMaterialInterface* MergedMaterial = CreateProxyMaterial(InBasePackageName, MergedAssetPackageName, InBaseMaterial, InOuter, InSettings, FlattenMaterial, OutAssetsToSync, &MaterialUpdateContext); UniqueMaterials.Empty(1); UniqueMaterials.Add(MergedMaterial); FSectionInfo NewSection; NewSection.Material = MergedMaterial; NewSection.EnabledProperties.Add(GET_MEMBER_NAME_CHECKED(FStaticMeshSection, bCastShadow)); DataTracker.AddBakedMaterialSection(NewSection); for (IMeshMergeExtension* Extension : MeshMergeExtensions) { Extension->OnCreatedProxyMaterial(StaticMeshComponentsToMerge, MergedMaterial); } } } // Create the merged mesh TArray MergedRawMeshes; CreateMergedRawMeshes(DataTracker, InSettings, StaticMeshComponentsToMerge, UniqueMaterials, CollapsedMaterialMap, OutputMaterialsMap, bMergeAllLODs, bMergeMaterialData, MergedAssetPivot, MergedRawMeshes); // Notify listeners that our merged mesh was created for (IMeshMergeExtension* Extension : MeshMergeExtensions) { Extension->OnCreatedMergedRawMeshes(StaticMeshComponentsToMerge, DataTracker, MergedRawMeshes); } // Populate mesh section map FMeshSectionInfoMap SectionInfoMap; for (TConstLODIndexIterator Iterator = DataTracker.GetLODIndexIterator(); Iterator; ++Iterator) { const int32 LODIndex = *Iterator; TArray UniqueMaterialIndices; const FMeshDescription& TargetRawMesh = MergedRawMeshes[LODIndex]; uint32 MaterialIndex = 0; for (FPolygonGroupID PolygonGroupID : TargetRawMesh.PolygonGroups().GetElementIDs()) { //Skip empty group if (TargetRawMesh.GetPolygonGroupPolygonIDs(PolygonGroupID).Num() > 0) { if (PolygonGroupID.GetValue() < DataTracker.NumberOfUniqueSections()) { UniqueMaterialIndices.AddUnique(PolygonGroupID.GetValue()); } else { UniqueMaterialIndices.AddUnique(MaterialIndex); } MaterialIndex++; } } UniqueMaterialIndices.Sort(); for (int32 Index = 0; Index < UniqueMaterialIndices.Num(); ++Index) { const int32 SectionIndex = UniqueMaterialIndices[Index]; // unclear when this would not be the case, but it seems to be able to occur if (SectionIndex < DataTracker.NumberOfUniqueSections()) { const FSectionInfo& StoredSectionInfo = DataTracker.GetSection(SectionIndex); FMeshSectionInfo SectionInfo; SectionInfo.bCastShadow = StoredSectionInfo.EnabledProperties.Contains(GET_MEMBER_NAME_CHECKED(FMeshSectionInfo, bCastShadow)); SectionInfo.bEnableCollision = StoredSectionInfo.EnabledProperties.Contains(GET_MEMBER_NAME_CHECKED(FMeshSectionInfo, bEnableCollision)); SectionInfo.MaterialIndex = UniqueMaterials.Num() == 1 ? 0 : UniqueMaterials.IndexOfByKey(CollapsedMaterialMap[StoredSectionInfo.Material]); SectionInfoMap.Set(LODIndex, Index, SectionInfo); } } } // Transform physics primitives to merged mesh pivot if (InSettings.bMergePhysicsData && !MergedAssetPivot.IsZero()) { FTransform PivotTM(-MergedAssetPivot); for (FKAggregateGeom& Geometry : PhysicsGeometry) { FMeshMergeHelpers::TransformPhysicsGeometry(PivotTM, false, Geometry); } } // Compute target lightmap channel for each LOD, by looking at the first empty UV channel const int32 LightMapUVChannel = [&]() { if (InSettings.bGenerateLightMapUV) { const int32 TempChannel = DataTracker.GetAvailableLightMapUVChannel(); if (TempChannel == INDEX_NONE) { // Output warning message UE_LOG(LogMeshMerging, Warning, TEXT("Failed to find an available channel for Lightmap UVs. Lightmap UVs will not be generated.")); } return TempChannel; } return (int32)INDEX_NONE; }(); // //Create merged mesh asset // MergedRawMeshes.SetNum(Algo::RemoveIf(MergedRawMeshes, [](const FMeshDescription& MeshDescription) { return MeshDescription.IsEmpty(); })); const bool bContainsImposters = !ImposterComponents.IsEmpty(); const bool bContainsMergedMeshes = !MergedRawMeshes.IsEmpty(); if (bContainsMergedMeshes || bContainsImposters) { FString AssetName; FString PackageName; if (InBasePackageName.IsEmpty()) { AssetName = TEXT("SM_MERGED_") + FPackageName::GetShortName(MergedAssetPackageName); PackageName = FPackageName::GetLongPackagePath(MergedAssetPackageName) / AssetName; } else { AssetName = TEXT("SM_") + FPackageName::GetShortName(InBasePackageName); PackageName = FPackageName::GetLongPackagePath(InBasePackageName) / AssetName; } UPackage* Package = InOuter; if (Package == nullptr) { Package = CreatePackage( *PackageName); check(Package); Package->FullyLoad(); Package->Modify(); } // Check that an asset of a different class does not already exist { UObject* ExistingObject = StaticFindObject( nullptr, Package, *AssetName); if(ExistingObject && !ExistingObject->GetClass()->IsChildOf(UStaticMesh::StaticClass())) { // Change name of merged static mesh to avoid name collision UPackage* ParentPackage = CreatePackage( *FPaths::GetPath(Package->GetPathName())); ParentPackage->FullyLoad(); AssetName = MakeUniqueObjectName( ParentPackage, UStaticMesh::StaticClass(), *AssetName).ToString(); Package = CreatePackage( *(ParentPackage->GetPathName() / AssetName )); check(Package); Package->FullyLoad(); Package->Modify(); // Let user know name of merged static mesh has changed UE_LOG(LogMeshMerging, Warning, TEXT("Cannot create %s %s.%s\n") TEXT("An object with the same fully qualified name but a different class already exists.\n") TEXT("\tExisting Object: %s\n") TEXT("The merged mesh will be named %s.%s"), *UStaticMesh::StaticClass()->GetName(), *ExistingObject->GetOutermost()->GetPathName(), *ExistingObject->GetName(), *ExistingObject->GetFullName(), *Package->GetPathName(), *AssetName); } } FStaticMeshComponentRecreateRenderStateContext RecreateRenderStateContext(FindObject(Package, *AssetName)); UStaticMesh* StaticMesh = NewObject(Package, *AssetName, RF_Public | RF_Standalone); StaticMesh->InitResources(); FString OutputPath = StaticMesh->GetPathName(); // make sure it has a new lighting guid StaticMesh->SetLightingGuid(); if (LightMapUVChannel != INDEX_NONE) { StaticMesh->SetLightMapResolution(InSettings.TargetLightMapResolution); StaticMesh->SetLightMapCoordinateIndex(LightMapUVChannel); } // Ray tracing support StaticMesh->bSupportRayTracing = InSettings.bSupportRayTracing; TArray ImposterMaterials; FBox ImposterBounds(EForceInit::ForceInit); for (int32 LODIndex = 0; LODIndex < MergedRawMeshes.Num(); ++LODIndex) { FMeshDescription& MergedMeshLOD = MergedRawMeshes[LODIndex]; if (MergedMeshLOD.Vertices().Num() > 0 || bContainsImposters) { FStaticMeshSourceModel& SrcModel = StaticMesh->AddSourceModel(); // Don't allow the engine to recalculate normals SrcModel.BuildSettings.bRecomputeNormals = false; SrcModel.BuildSettings.bRecomputeTangents = false; SrcModel.BuildSettings.bRemoveDegenerates = false; SrcModel.BuildSettings.bUseHighPrecisionTangentBasis = false; SrcModel.BuildSettings.bUseFullPrecisionUVs = false; SrcModel.BuildSettings.bBuildReversedIndexBuffer = false; SrcModel.BuildSettings.bGenerateLightmapUVs = LightMapUVChannel != INDEX_NONE; SrcModel.BuildSettings.MinLightmapResolution = InSettings.bComputedLightMapResolution ? DataTracker.GetLightMapDimension() : InSettings.TargetLightMapResolution; SrcModel.BuildSettings.SrcLightmapIndex = 0; SrcModel.BuildSettings.DstLightmapIndex = LightMapUVChannel != INDEX_NONE ? LightMapUVChannel : 0; if(!InSettings.bAllowDistanceField) { SrcModel.BuildSettings.DistanceFieldResolutionScale = 0.0f; } if (bContainsImposters) { // Merge imposter meshes to rawmesh FMeshMergeHelpers::MergeImpostersToMesh(ImposterComponents, MergedMeshLOD, MergedAssetPivot, UniqueMaterials.Num(), ImposterMaterials); const FTransform PivotTransform = FTransform(MergedAssetPivot); for (const UStaticMeshComponent* Component : ImposterComponents) { if (Component->GetStaticMesh()) { ImposterBounds += Component->GetStaticMesh()->GetBoundingBox().TransformBy(Component->GetComponentToWorld().GetRelativeTransform(PivotTransform)); } } } FMeshDescription* MeshDescription = StaticMesh->CreateMeshDescription(LODIndex, MergedMeshLOD); UStaticMesh::FCommitMeshDescriptionParams CommitParams; CommitParams.bUseHashAsGuid = true; StaticMesh->CommitMeshDescription(LODIndex, CommitParams); } } auto IsMaterialImportedNameUnique = [&StaticMesh](FName ImportedMaterialSlotName) { for (const FStaticMaterial& StaticMaterial : StaticMesh->GetStaticMaterials()) { #if WITH_EDITOR if (StaticMaterial.ImportedMaterialSlotName == ImportedMaterialSlotName) #else if (StaticMaterial.MaterialSlotName == ImportedMaterialSlotName) #endif { return false; } } return true; }; for (UMaterialInterface* Material : UniqueMaterials) { if (Material && (!Material->IsAsset() && InOuter != GetTransientPackage())) { // MIDs are not assets, duplicate them and outer them to the static mesh. if (UMaterialInstanceDynamic* MID = Cast(Material)) { Material = DuplicateObject(MID, StaticMesh); } else { UE_LOG(LogMeshMerging, Error, TEXT("Merged mesh is referencing a non-asset material (\"%s\"). This material can't be referenced outside it's package as it is not public. Clearing out material entry in the final mesh."), *Material->GetPathName()); Material = nullptr; // do not save non-asset materials } } //Make sure we have unique slot name here FName MaterialSlotName = DataTracker.GetMaterialSlotName(Material); int32 Counter = 1; while (!IsMaterialImportedNameUnique(MaterialSlotName)) { MaterialSlotName = *(DataTracker.GetMaterialSlotName(Material).ToString() + TEXT("_") + FString::FromInt(Counter++)); } StaticMesh->GetStaticMaterials().Add(FStaticMaterial(Material, MaterialSlotName)); } for(UMaterialInterface* ImposterMaterial : ImposterMaterials) { //Make sure we have unique slot name here FName MaterialSlotName = ImposterMaterial->GetFName(); int32 Counter = 1; while (!IsMaterialImportedNameUnique(MaterialSlotName)) { MaterialSlotName = *(ImposterMaterial->GetName() + TEXT("_") + FString::FromInt(Counter++)); } StaticMesh->GetStaticMaterials().Add(FStaticMaterial(ImposterMaterial, MaterialSlotName)); } if (InSettings.bMergePhysicsData) { StaticMesh->CreateBodySetup(); if (BodySetupSource) { StaticMesh->GetBodySetup()->CopyBodyPropertiesFrom(BodySetupSource); } StaticMesh->GetBodySetup()->AggGeom = FKAggregateGeom(); // Copy collision from the source meshes for (const FKAggregateGeom& Geom : PhysicsGeometry) { StaticMesh->GetBodySetup()->AddCollisionFrom(Geom); } // Bake rotation into verts of convex hulls, so they scale correctly after rotation for (FKConvexElem& ConvexElem : StaticMesh->GetBodySetup()->AggGeom.ConvexElems) { ConvexElem.BakeTransformToVerts(); } } // Add merged sockets if (InSettings.bMergeMeshSockets) { for (auto& [SocketName, Socket] : MergedSockets) { Socket->Rename(nullptr, StaticMesh); StaticMesh->AddSocket(Socket); } } StaticMesh->GetSectionInfoMap().CopyFrom(SectionInfoMap); StaticMesh->GetOriginalSectionInfoMap().CopyFrom(SectionInfoMap); //Set the Imported version before calling the build StaticMesh->ImportVersion = EImportStaticMeshVersion::LastVersion; StaticMesh->SetLightMapResolution(InSettings.bComputedLightMapResolution ? DataTracker.GetLightMapDimension() : InSettings.TargetLightMapResolution); // Nanite settings StaticMesh->NaniteSettings = InSettings.NaniteSettings; #if WITH_EDITOR //If we are running the automation test if (GIsAutomationTesting) { StaticMesh->BuildCacheAutomationTestGuid = FGuid::NewGuid(); } #endif // Ensure the new mesh is not referencing non standalone materials FMeshMergeHelpers::FixupNonStandaloneMaterialReferences(StaticMesh); if (ImposterBounds.IsValid) { const FBox StaticMeshBox = StaticMesh->GetBoundingBox(); const FBox CombinedBox = StaticMeshBox + ImposterBounds; StaticMesh->SetPositiveBoundsExtension((CombinedBox.Max - StaticMeshBox.Max)); StaticMesh->SetNegativeBoundsExtension((StaticMeshBox.Min - CombinedBox.Min)); StaticMesh->CalculateExtendedBounds(); } UStaticMesh::FBuildParameters BuildParameters; BuildParameters.bInSilent = true; BuildParameters.bInRebuildUVChannelData = true; BuildParameters.bInEnforceLightmapRestrictions = true; // Nanite meshes can sometime contain smaller triangles that could be discarded in the proxy mesh. // If those are found on the outskirt of a merged mesh, the static mesh build will complain about // the bounds delta being too high. BuildParameters.bIgnoreBoundsDiff = StaticMesh->NaniteSettings.bEnabled; StaticMesh->Build(BuildParameters); OutAssetsToSync.Add(StaticMesh); OutMergedActorLocation = MergedAssetPivot; } else { UE_LOG(LogMeshMerging, Display, TEXT("MergeComponentsToStaticMesh: Skipped creation of a static mesh asset as no input meshes were provided")); } } void FMeshMergeUtilities::CreateMergedMaterial(FMeshMergeDataTracker& InDataTracker, const FMeshMergingSettings& InSettings, const TArray& InStaticMeshComponentsToMerge, TArray& InAdapters, const TArray& InUniqueMaterials, const TMap& InCollapsedMaterialMap, TMultiMap& InOutputMaterialsMap, bool bInMergeAllLODs, bool bInMergeMaterialData, const FVector& InMergedAssetPivot, FFlattenMaterial& OutFlattenMaterial) const { OutFlattenMaterial.ReleaseData(); TArray PrimitiveComponents; Algo::Transform(InStaticMeshComponentsToMerge, PrimitiveComponents, [](UStaticMeshComponent* SMComponent) { return SMComponent; }); FMaterialProxySettings MaterialProxySettings = InSettings.MaterialSettings; if (MaterialProxySettings.ResolveTexelDensity(PrimitiveComponents)) { MaterialProxySettings.TextureSize = InDataTracker.GetTextureSizeFromTargetTexelDensity(MaterialProxySettings.TargetTexelDensityPerMeter); MaterialProxySettings.TextureSizingType = ETextureSizingType::TextureSizingType_UseSingleTextureSize; } UMaterialOptions* MaterialOptions = PopulateMaterialOptions(MaterialProxySettings); TGCObjectScopeGuard MaterialOptionsGCScopeGuard(MaterialOptions); // Check each material to see if the shader actually uses vertex data and collect flags TArray> bMaterialUsesVertexData; bMaterialUsesVertexData.SetNum(InUniqueMaterials.Num()); // Deferred call, as this may not be required by all code paths and is pretty costly to compute auto DoesMaterialUsesVertexData = [&](const int32 InMaterialIndex) { if (!bMaterialUsesVertexData[InMaterialIndex].IsSet()) { bMaterialUsesVertexData[InMaterialIndex] = DetermineMaterialVertexDataUsage(InUniqueMaterials[InMaterialIndex], MaterialOptions); } return bMaterialUsesVertexData[InMaterialIndex].GetValue(); }; // For each unique material calculate how 'important' they are TArray MaterialImportanceValues; FMaterialUtilities::DetermineMaterialImportance(InUniqueMaterials, MaterialImportanceValues); TArray GlobalMeshSettings; TArray GlobalMaterialSettings; TArray SectionMaterialImportanceValues; TMap PropertySizes; for (const FPropertyEntry& Entry : MaterialOptions->Properties) { if (!Entry.bUseConstantValue && Entry.Property != MP_MAX) { PropertySizes.Add(Entry.Property, Entry.bUseCustomSize ? Entry.CustomSize : MaterialOptions->TextureSize); } } // If we are generating a single LOD and want to merge materials we can utilize texture space better by generating unique UVs // for the merged mesh and baking out materials using those UVs const bool bGloballyRemapUVs = !bInMergeAllLODs && !InSettings.bReuseMeshLightmapUVs; typedef TTuple FMeshLODTuple; typedef TFuture> FUVComputeFuture; TMap MeshLODsTextureCoordinates; TMap MeshDataAwaitingResults; for (TConstRawMeshIterator RawMeshIterator = InDataTracker.GetConstRawMeshIterator(); RawMeshIterator; ++RawMeshIterator) { const FMeshLODKey& Key = RawMeshIterator.Key(); const FMeshDescription& RawMesh = RawMeshIterator.Value(); const bool bRequiresUniqueUVs = InDataTracker.DoesMeshLODRequireUniqueUVs(Key); const FMeshDescription* MeshDescription = InDataTracker.GetRawMeshPtr(Key); UStaticMeshComponent* Component = InStaticMeshComponentsToMerge[Key.GetMeshIndex()]; UStaticMesh* StaticMesh = Component->GetStaticMesh(); // Retrieve all sections and materials for key TArray SectionRemapPairs; InDataTracker.GetMappingsForMeshLOD(Key, SectionRemapPairs); // Contains unique materials used for this key, and the accompanying section index which point to the material TMap> MaterialAndSectionIndices; for (const SectionRemapPair& RemapPair : SectionRemapPairs) { const int32 UniqueIndex = RemapPair.Value; const int32 SectionIndex = RemapPair.Key; TArray& SectionIndices = MaterialAndSectionIndices.FindOrAdd(InCollapsedMaterialMap.FindChecked(InDataTracker.GetMaterialForSectionIndex(UniqueIndex))); SectionIndices.Add(SectionIndex); } for (TPair>& MaterialSectionIndexPair : MaterialAndSectionIndices) { UMaterialInterface* Material = MaterialSectionIndexPair.Key; const int32 MaterialIndex = InUniqueMaterials.IndexOfByKey(Material); const TArray& SectionIndices = MaterialSectionIndexPair.Value; FMaterialData MaterialData; MaterialData.Material = InCollapsedMaterialMap.FindChecked(Material); MaterialData.PropertySizes = PropertySizes; FMeshData NewMeshData; const bool bUseMeshData = bGloballyRemapUVs || (InSettings.bUseVertexDataForBakingMaterial && (bRequiresUniqueUVs || DoesMaterialUsesVertexData(MaterialIndex))); if (bUseMeshData) { NewMeshData.Mesh = Key.GetMesh(); NewMeshData.MeshDescription = MeshDescription; NewMeshData.VertexColorHash = Key.GetVertexColorHash(); NewMeshData.bMirrored = Component->GetComponentTransform().GetDeterminant() < 0.0f; NewMeshData.MaterialIndices = SectionIndices; if (!Component->GetCustomPrimitiveData().Data.IsEmpty()) { NewMeshData.PrimitiveData = FPrimitiveData(); NewMeshData.PrimitiveData->CustomPrimitiveData = &Component->GetCustomPrimitiveData(); } } auto CompareMaterialData = [&InSettings](const FMaterialData& LHS, const FMaterialData& RHS) { return InSettings.bMergeEquivalentMaterials ? FMaterialKey(LHS.Material) == FMaterialKey(RHS.Material) : LHS.Material == RHS.Material; }; auto CompareCustomPrimitiveData = [](const FCustomPrimitiveData* LHS, const FCustomPrimitiveData* RHS) { // Return true if both are null, false if one of them is null - otherwise, compare content return (!LHS && !RHS) ? true : (!LHS || !RHS) ? false : (*LHS == *RHS); }; auto ComparePrimitiveData = [&CompareCustomPrimitiveData](const TOptional& LHS, const TOptional& RHS) { // Return true if both are null, false if one of them is null - otherwise, compare content return (!LHS && !RHS) ? true : (!LHS || !RHS) ? false : CompareCustomPrimitiveData(LHS->CustomPrimitiveData, RHS->CustomPrimitiveData); }; auto CompareMeshData = [&ComparePrimitiveData](const FMeshData& LHS, const FMeshData& RHS) { return (LHS.Mesh == RHS.Mesh) && (LHS.MaterialIndices == RHS.MaterialIndices) && (LHS.bMirrored == RHS.bMirrored) && (LHS.VertexColorHash == RHS.VertexColorHash) && ComparePrimitiveData(LHS.PrimitiveData, RHS.PrimitiveData); }; // Find material & mesh pair int32 MeshDataIndex = INDEX_NONE; for (int32 GlobalMaterialSettingsIndex = 0; GlobalMaterialSettingsIndex < GlobalMaterialSettings.Num(); ++GlobalMaterialSettingsIndex) { if (CompareMaterialData(GlobalMaterialSettings[GlobalMaterialSettingsIndex], MaterialData) && CompareMeshData(GlobalMeshSettings[GlobalMaterialSettingsIndex], NewMeshData)) { MeshDataIndex = GlobalMaterialSettingsIndex; break; } } // We've found a match, no need to process this mesh/material pair if (MeshDataIndex == INDEX_NONE) { // We're processing a new pair MeshDataIndex = GlobalMeshSettings.Num(); FMeshData& MeshData = GlobalMeshSettings.Emplace_GetRef(NewMeshData); GlobalMaterialSettings.Add(MaterialData); SectionMaterialImportanceValues.Add(MaterialImportanceValues[MaterialIndex]); if (bUseMeshData) { // if it has vertex color/*WedgetColors.Num()*/, it should also use light map UV index // we can't do this for all meshes, but only for the mesh that has vertex color. if (bRequiresUniqueUVs || MeshData.MeshDescription->VertexInstances().Num() > 0) { // Check if there are lightmap uvs available? const int32 LightMapUVIndex = StaticMesh->GetLightMapCoordinateIndex(); TVertexInstanceAttributesConstRef VertexInstanceUVs = FStaticMeshConstAttributes(*MeshData.MeshDescription).GetVertexInstanceUVs(); if (InSettings.bReuseMeshLightmapUVs && VertexInstanceUVs.GetNumElements() > 0 && VertexInstanceUVs.GetNumChannels() > LightMapUVIndex) { MeshData.TextureCoordinateIndex = LightMapUVIndex; } else { // Verify if we started an async task to generate UVs for this static mesh & LOD FMeshLODTuple Tuple(Key.GetMesh(), Key.GetLODIndex()); if (!MeshLODsTextureCoordinates.Find(Tuple)) { // No job found yet, fire an async task MeshLODsTextureCoordinates.Add(Tuple, Async(EAsyncExecution::Thread, [MeshDescription, MaterialOptions, this]() { FStaticMeshOperations::FGenerateUVOptions GenerateUVOptions; GenerateUVOptions.TextureResolution = MaterialOptions->TextureSize.GetMax(); GenerateUVOptions.bMergeTrianglesWithIdenticalAttributes = false; GenerateUVOptions.UVMethod = GetUVGenerationMethodToUse(); TArray UniqueTextureCoordinates; FStaticMeshOperations::GenerateUV(*MeshDescription, GenerateUVOptions, UniqueTextureCoordinates); if (GenerateUVOptions.UVMethod == FStaticMeshOperations::EGenerateUVMethod::Legacy) { ScaleTextureCoordinatesToBox(FBox2D(FVector2D::ZeroVector, FVector2D(1, 1)), UniqueTextureCoordinates); } return UniqueTextureCoordinates; })); } // Keep track of the fact that this mesh is waiting for the UV computation to finish MeshDataAwaitingResults.Add(MeshDataIndex, Tuple); } } InAdapters[Key.GetMeshIndex()].ApplySettings(Key.GetLODIndex(), MeshData); } } for (const auto& OriginalSectionIndex : SectionIndices) { InOutputMaterialsMap.Add(Key, MaterialRemapPair(OriginalSectionIndex, MeshDataIndex)); } } } // Fetch results from the async UV computation tasks for (auto MeshData : MeshDataAwaitingResults) { GlobalMeshSettings[MeshData.Key].CustomTextureCoordinates = MeshLODsTextureCoordinates[MeshData.Value].Get(); } TArray MeshSettingPtrs; for (int32 SettingsIndex = 0; SettingsIndex < GlobalMeshSettings.Num(); ++SettingsIndex) { MeshSettingPtrs.Add(&GlobalMeshSettings[SettingsIndex]); } TArray MaterialSettingPtrs; for (int32 SettingsIndex = 0; SettingsIndex < GlobalMaterialSettings.Num(); ++SettingsIndex) { MaterialSettingPtrs.Add(&GlobalMaterialSettings[SettingsIndex]); } if (bGloballyRemapUVs) { // Adjust merge settings when merging for the unique UV/material baking pass // The final merged mesh will use the original settings FMeshMergingSettings RemapUVMergeSettings = InSettings; // Keep vertex data in order to properly generate unique UVs RemapUVMergeSettings.bBakeVertexDataToMesh = true; // Keep all UVs as some channels might be needed to properly bake the material for (EUVOutput& OutputUV : RemapUVMergeSettings.OutputUVs) { OutputUV = EUVOutput::OutputChannel; } TArray MergedRawMeshes; CreateMergedRawMeshes(InDataTracker, RemapUVMergeSettings, InStaticMeshComponentsToMerge, InUniqueMaterials, InCollapsedMaterialMap, InOutputMaterialsMap, false, false, InMergedAssetPivot, MergedRawMeshes); // Create texture coords for the merged mesh FStaticMeshOperations::FGenerateUVOptions GenerateUVOptions; GenerateUVOptions.TextureResolution = MaterialOptions->TextureSize.GetMax(); GenerateUVOptions.bMergeTrianglesWithIdenticalAttributes = true; GenerateUVOptions.UVMethod = GetUVGenerationMethodToUse(); TArray GlobalTextureCoordinates; bool bSuccess = FStaticMeshOperations::GenerateUV(MergedRawMeshes[0], GenerateUVOptions, GlobalTextureCoordinates); if (bSuccess) { if (GenerateUVOptions.UVMethod == FStaticMeshOperations::EGenerateUVMethod::Legacy) { ScaleTextureCoordinatesToBox(FBox2D(FVector2D::ZeroVector, FVector2D(1, 1)), GlobalTextureCoordinates); } // copy UVs back to the un-merged mesh's custom texture coords // iterate the raw meshes in the same way as when we combined the mesh above in CreateMergedRawMeshes() int32 GlobalUVIndex = 0; for (TConstRawMeshIterator RawMeshIterator = InDataTracker.GetConstRawMeshIterator(); RawMeshIterator; ++RawMeshIterator) { const FMeshLODKey& Key = RawMeshIterator.Key(); const FMeshDescription& RawMesh = RawMeshIterator.Value(); // Build a local array for this raw mesh TArray UniqueTextureCoordinates; UniqueTextureCoordinates.SetNumUninitialized(RawMesh.VertexInstances().Num()); for (FVector2D& UniqueTextureCoordinate : UniqueTextureCoordinates) { UniqueTextureCoordinate = GlobalTextureCoordinates[GlobalUVIndex++]; } // copy to mesh data for (FMeshData& MeshData : GlobalMeshSettings) { if (MeshData.MeshDescription == &RawMesh) { MeshData.CustomTextureCoordinates = UniqueTextureCoordinates; } } } // Dont smear borders as we will copy back non-pink pixels for (FMaterialData& MaterialData : GlobalMaterialSettings) { MaterialData.bPerformBorderSmear = false; } } else { UE_LOG(LogMeshMerging, Warning, TEXT("GenerateUV: Failed to pack UVs for static mesh")); } } TArray FlattenedMaterials; // This scope ensures BakeOutputs is never used after TransferOutputToFlatMaterials { TArray BakeOutputs; IMaterialBakingModule& Module = FModuleManager::Get().LoadModuleChecked("MaterialBaking"); // If we're working with a new set of UVs, we can bake all materials directly to the same bake output // as our remapped UVs for each mesh don't overlap. if (bGloballyRemapUVs) { FBakeOutput& BakeOutput = BakeOutputs.Emplace_GetRef(); Module.BakeMaterials(MaterialSettingPtrs, MeshSettingPtrs, BakeOutput); } else { Module.BakeMaterials(MaterialSettingPtrs, MeshSettingPtrs, BakeOutputs); } // Append constant properties ? TArray ConstantData; FIntPoint ConstantSize(1, 1); for (const FPropertyEntry& Entry : MaterialOptions->Properties) { if (Entry.bUseConstantValue && Entry.Property != MP_MAX) { ConstantData.SetNum(1, EAllowShrinking::No); ConstantData[0] = FLinearColor(Entry.ConstantValue, Entry.ConstantValue, Entry.ConstantValue).ToFColor(true); for (FBakeOutput& Output : BakeOutputs) { Output.PropertyData.Add(Entry.Property, ConstantData); Output.PropertySizes.Add(Entry.Property, ConstantSize); } } } TransferOutputToFlatMaterials(GlobalMaterialSettings, BakeOutputs, FlattenedMaterials); } if (!bGloballyRemapUVs) { // Try to optimize materials where possible for (FFlattenMaterial& InMaterial : FlattenedMaterials) { FMaterialUtilities::OptimizeFlattenMaterial(InMaterial); } } for (const FPropertyEntry& Entry : MaterialOptions->Properties) { if (Entry.Property != MP_MAX) { EFlattenMaterialProperties OldProperty = ToFlattenProperty(Entry.Property); if (ensure(OldProperty != EFlattenMaterialProperties::NumFlattenMaterialProperties)) { OutFlattenMaterial.SetPropertySize(OldProperty, Entry.bUseCustomSize ? Entry.CustomSize : MaterialOptions->TextureSize); } } } TArray UVTransforms; if (bGloballyRemapUVs) { // If we have globally remapped UVs we copy non-pink pixels over the dest texture rather than // copying sub-charts TArray MaterialBoxes; MaterialBoxes.SetNumUninitialized(GlobalMaterialSettings.Num()); for (FBox2D& Box2D : MaterialBoxes) { Box2D = FBox2D(FVector2D(0.0f, 0.0f), FVector2D(1.0f, 1.0f)); } FlattenBinnedMaterials(FlattenedMaterials, MaterialBoxes, 0, true, OutFlattenMaterial, UVTransforms); static const FUVOffsetScalePair NoUVTransform = { FVector2D::Zero(), FVector2D::One() }; UVTransforms.Init(NoUVTransform, GlobalMaterialSettings.Num()); } else { /** Reweighting */ float TotalValue = 0.0f; for (const float& Value : SectionMaterialImportanceValues) { TotalValue += Value; } float Multiplier = 1.0f / TotalValue; for (float& Value : SectionMaterialImportanceValues) { Value *= Multiplier; } /** End reweighting */ if (InSettings.bUseTextureBinning) { TArray MaterialBoxes; FMaterialUtilities::GeneratedBinnedTextureSquares(FVector2D(1.0f, 1.0f), SectionMaterialImportanceValues, MaterialBoxes); FlattenBinnedMaterials(FlattenedMaterials, MaterialBoxes, InSettings.GutterSize, false, OutFlattenMaterial, UVTransforms); } else { MergeFlattenedMaterials(FlattenedMaterials, InSettings.GutterSize, OutFlattenMaterial, UVTransforms); } } // Adjust UVs for (int32 ComponentIndex = 0; ComponentIndex < InStaticMeshComponentsToMerge.Num(); ++ComponentIndex) { TArray ProcessedMaterials; for (TPair& MappingPair : InOutputMaterialsMap) { if (MappingPair.Key.GetMeshIndex() == ComponentIndex && !ProcessedMaterials.Contains(MappingPair.Value.Key)) { // Retrieve raw mesh data for this component and lod pair FMeshDescription* RawMesh = InDataTracker.GetRawMeshPtr(MappingPair.Key); FMeshData& MeshData = GlobalMeshSettings[MappingPair.Value.Value]; const FUVOffsetScalePair& UVTransform = UVTransforms[MappingPair.Value.Value]; const uint32 MaterialIndex = MappingPair.Value.Key; ProcessedMaterials.Add(MaterialIndex); if (RawMesh->Vertices().Num()) { TVertexInstanceAttributesRef VertexInstanceUVs = FStaticMeshAttributes(*RawMesh).GetVertexInstanceUVs(); int32 NumUVChannel = FMath::Min(VertexInstanceUVs.GetNumChannels(), (int32)MAX_MESH_TEXTURE_COORDS); for (int32 UVChannelIdx = 0; UVChannelIdx < NumUVChannel; ++UVChannelIdx) { int32 VertexIndex = 0; for (FVertexInstanceID VertexInstanceID : RawMesh->VertexInstances().GetElementIDs()) { FVector2D UV = FVector2D(VertexInstanceUVs.Get(VertexInstanceID, UVChannelIdx)); if (UVChannelIdx == 0) { if (MeshData.CustomTextureCoordinates.Num()) { UV = MeshData.CustomTextureCoordinates[VertexIndex]; } else if (MeshData.TextureCoordinateIndex != 0) { check(MeshData.TextureCoordinateIndex < NumUVChannel); UV = FVector2D(VertexInstanceUVs.Get(VertexInstanceID, MeshData.TextureCoordinateIndex)); } } const TArray& Polygons = RawMesh->GetVertexInstanceConnectedPolygons(VertexInstanceID); for (FPolygonID PolygonID : Polygons) { FPolygonGroupID PolygonGroupID = RawMesh->GetPolygonPolygonGroup(PolygonID); if (PolygonGroupID.GetValue() == MaterialIndex) { if (UVTransform.Value != FVector2D::ZeroVector) { VertexInstanceUVs.Set(VertexInstanceID, UVChannelIdx, FVector2f(UV * UVTransform.Value + UVTransform.Key)); // LWC_TODO: Precision loss break; } } } VertexIndex++; } } } } } } for (TRawMeshIterator Iterator = InDataTracker.GetRawMeshIterator(); Iterator; ++Iterator) { FMeshDescription& RawMesh = Iterator.Value(); // Reset material indexes TMap RemapPolygonGroups; for (FPolygonGroupID PolygonGroupID : RawMesh.PolygonGroups().GetElementIDs()) { RemapPolygonGroups.Add(PolygonGroupID, FPolygonGroupID(0)); } RawMesh.RemapPolygonGroups(RemapPolygonGroups); } OutFlattenMaterial.UVChannel = INDEX_NONE; } void FMeshMergeUtilities::CreateMergedRawMeshes(FMeshMergeDataTracker& InDataTracker, const FMeshMergingSettings& InSettings, const TArray& InStaticMeshComponentsToMerge, const TArray& InUniqueMaterials, const TMap& InCollapsedMaterialMap, const TMultiMap& InOutputMaterialsMap, bool bInMergeAllLODs, bool bInMergeMaterialData, const FVector& InMergedAssetPivot, TArray& OutMergedRawMeshes) const { TRACE_CPUPROFILER_EVENT_SCOPE(FMeshMergeUtilities::CreateMergedRawMeshes) if (bInMergeAllLODs) { OutMergedRawMeshes.AddDefaulted(InDataTracker.GetNumLODsForMergedMesh()); for (TConstLODIndexIterator Iterator = InDataTracker.GetLODIndexIterator(); Iterator; ++Iterator) { // Find meshes for each lod const int32 LODIndex = *Iterator; FMeshDescription& MergedMesh = OutMergedRawMeshes[LODIndex]; FStaticMeshAttributes(MergedMesh).Register(); for (int32 ComponentIndex = 0; ComponentIndex < InStaticMeshComponentsToMerge.Num(); ++ComponentIndex) { int32 RetrievedLODIndex = LODIndex; FMeshDescription* RawMeshPtr = InDataTracker.TryFindRawMeshForLOD(ComponentIndex, RetrievedLODIndex); if (RawMeshPtr != nullptr) { InDataTracker.AddComponentToWedgeMapping(ComponentIndex, LODIndex, MergedMesh.VertexInstances().Num()); FStaticMeshOperations::FAppendSettings AppendSettings; AppendSettings.PolygonGroupsDelegate = FAppendPolygonGroupsDelegate::CreateLambda([&bInMergeMaterialData, &InDataTracker, &InOutputMaterialsMap, &ComponentIndex, &LODIndex](const FMeshDescription& SourceMesh, FMeshDescription& TargetMesh, PolygonGroupMap& RemapPolygonGroups) { TPolygonGroupAttributesConstRef SourceImportedMaterialSlotNames = SourceMesh.PolygonGroupAttributes().GetAttributesRef(MeshAttribute::PolygonGroup::ImportedMaterialSlotName); TPolygonGroupAttributesRef TargetImportedMaterialSlotNames = TargetMesh.PolygonGroupAttributes().GetAttributesRef(MeshAttribute::PolygonGroup::ImportedMaterialSlotName); //Copy the polygon group if (bInMergeMaterialData) { FPolygonGroupID PolygonGroupID(0); if (!TargetMesh.PolygonGroups().IsValid(PolygonGroupID)) { TargetMesh.CreatePolygonGroupWithID(PolygonGroupID); TargetImportedMaterialSlotNames[PolygonGroupID] = SourceMesh.PolygonGroups().IsValid(PolygonGroupID) ? SourceImportedMaterialSlotNames[PolygonGroupID] : FName(TEXT("DefaultMaterialName")); } for (FPolygonGroupID SourcePolygonGroupID : SourceMesh.PolygonGroups().GetElementIDs()) { RemapPolygonGroups.Add(SourcePolygonGroupID, PolygonGroupID); } } else { TArray SectionMappings; InDataTracker.GetMappingsForMeshLOD(FMeshLODKey(ComponentIndex, LODIndex), SectionMappings); for (FPolygonGroupID SourcePolygonGroupID : SourceMesh.PolygonGroups().GetElementIDs()) { // First map from original section index to unique material index int32 UniqueIndex = INDEX_NONE; // then map to the output material map, if any if (InOutputMaterialsMap.Num() > 0) { TArray MaterialMappings; InOutputMaterialsMap.MultiFind(FMeshLODKey(ComponentIndex, LODIndex), MaterialMappings); for (MaterialRemapPair& Pair : MaterialMappings) { if (Pair.Key == SourcePolygonGroupID.GetValue()) { UniqueIndex = Pair.Value; break; } } // Note that at this point UniqueIndex is NOT a material index, but a unique section index! } if(UniqueIndex == INDEX_NONE) { UniqueIndex = SourcePolygonGroupID.GetValue(); } FPolygonGroupID TargetPolygonGroupID(UniqueIndex); if (!TargetMesh.PolygonGroups().IsValid(TargetPolygonGroupID)) { while (TargetMesh.PolygonGroups().Num() <= UniqueIndex) { TargetPolygonGroupID = TargetMesh.CreatePolygonGroup(); } check(TargetPolygonGroupID.GetValue() == UniqueIndex); TargetImportedMaterialSlotNames[TargetPolygonGroupID] = SourceImportedMaterialSlotNames[SourcePolygonGroupID]; } RemapPolygonGroups.Add(SourcePolygonGroupID, TargetPolygonGroupID); } } }); AppendSettings.bMergeVertexColor = InSettings.bBakeVertexDataToMesh; AppendSettings.MergedAssetPivot = InMergedAssetPivot; for (int32 ChannelIdx = 0; ChannelIdx < FStaticMeshOperations::FAppendSettings::MAX_NUM_UV_CHANNELS; ++ChannelIdx) { AppendSettings.bMergeUVChannels[ChannelIdx] = InDataTracker.DoesUVChannelContainData(ChannelIdx, LODIndex) && InSettings.OutputUVs[ChannelIdx] == EUVOutput::OutputChannel; } FStaticMeshOperations::AppendMeshDescription(*RawMeshPtr, MergedMesh, AppendSettings); } } //Cleanup the empty material to avoid empty section later TArray PolygonGroupToRemove; for (FPolygonGroupID PolygonGroupID : MergedMesh.PolygonGroups().GetElementIDs()) { if (MergedMesh.GetPolygonGroupPolygonIDs(PolygonGroupID).Num() < 1) { PolygonGroupToRemove.Add(PolygonGroupID); } } for (FPolygonGroupID PolygonGroupID : PolygonGroupToRemove) { MergedMesh.DeletePolygonGroup(PolygonGroupID); } } } else { FMeshDescription& MergedMesh = OutMergedRawMeshes.AddDefaulted_GetRef(); FStaticMeshAttributes(MergedMesh).Register(); for (int32 ComponentIndex = 0; ComponentIndex < InStaticMeshComponentsToMerge.Num(); ++ComponentIndex) { int32 LODIndex = 0; FMeshDescription* RawMeshPtr = InDataTracker.FindRawMeshAndLODIndex(ComponentIndex, LODIndex); if (RawMeshPtr != nullptr) { FMeshDescription& RawMesh = *RawMeshPtr; const int32 TargetLODIndex = 0; InDataTracker.AddComponentToWedgeMapping(ComponentIndex, TargetLODIndex, MergedMesh.VertexInstances().Num()); FStaticMeshOperations::FAppendSettings AppendSettings; AppendSettings.PolygonGroupsDelegate = FAppendPolygonGroupsDelegate::CreateLambda([&bInMergeMaterialData, &InDataTracker, &InOutputMaterialsMap, &ComponentIndex, &LODIndex](const FMeshDescription& SourceMesh, FMeshDescription& TargetMesh, PolygonGroupMap& RemapPolygonGroups) { TPolygonGroupAttributesConstRef SourceImportedMaterialSlotNames = SourceMesh.PolygonGroupAttributes().GetAttributesRef(MeshAttribute::PolygonGroup::ImportedMaterialSlotName); TPolygonGroupAttributesRef TargetImportedMaterialSlotNames = TargetMesh.PolygonGroupAttributes().GetAttributesRef(MeshAttribute::PolygonGroup::ImportedMaterialSlotName); //Copy the polygon group if (bInMergeMaterialData) { FPolygonGroupID PolygonGroupID(0); if (!TargetMesh.PolygonGroups().IsValid(PolygonGroupID)) { TargetMesh.CreatePolygonGroupWithID(PolygonGroupID); TargetImportedMaterialSlotNames[PolygonGroupID] = SourceMesh.PolygonGroups().IsValid(PolygonGroupID) ? SourceImportedMaterialSlotNames[PolygonGroupID] : FName(TEXT("DefaultMaterialName")); } for (FPolygonGroupID SourcePolygonGroupID : SourceMesh.PolygonGroups().GetElementIDs()) { RemapPolygonGroups.Add(SourcePolygonGroupID, PolygonGroupID); } } else { TArray SectionMappings; InDataTracker.GetMappingsForMeshLOD(FMeshLODKey(ComponentIndex, LODIndex), SectionMappings); for (FPolygonGroupID SourcePolygonGroupID : SourceMesh.PolygonGroups().GetElementIDs()) { // First map from original section index to unique material index int32 UniqueIndex = INDEX_NONE; // then map to the output material map, if any if (InOutputMaterialsMap.Num() > 0) { TArray MaterialMappings; InOutputMaterialsMap.MultiFind(FMeshLODKey(ComponentIndex, LODIndex), MaterialMappings); for (MaterialRemapPair& Pair : MaterialMappings) { if (Pair.Key == SourcePolygonGroupID.GetValue()) { UniqueIndex = Pair.Value; break; } } // Note that at this point UniqueIndex is NOT a material index, but a unique section index! } //Fallback if(UniqueIndex == INDEX_NONE) { UniqueIndex = SourcePolygonGroupID.GetValue(); } FPolygonGroupID TargetPolygonGroupID(UniqueIndex); if (!TargetMesh.PolygonGroups().IsValid(TargetPolygonGroupID)) { while (TargetMesh.PolygonGroups().Num() <= UniqueIndex) { TargetPolygonGroupID = TargetMesh.CreatePolygonGroup(); } check(TargetPolygonGroupID.GetValue() == UniqueIndex); TargetImportedMaterialSlotNames[TargetPolygonGroupID] = SourceImportedMaterialSlotNames[SourcePolygonGroupID]; } RemapPolygonGroups.Add(SourcePolygonGroupID, TargetPolygonGroupID); } } }); AppendSettings.bMergeVertexColor = InSettings.bBakeVertexDataToMesh; AppendSettings.MergedAssetPivot = InMergedAssetPivot; for (int32 ChannelIdx = 0; ChannelIdx < FStaticMeshOperations::FAppendSettings::MAX_NUM_UV_CHANNELS; ++ChannelIdx) { AppendSettings.bMergeUVChannels[ChannelIdx] = InDataTracker.DoesUVChannelContainData(ChannelIdx, LODIndex) && InSettings.OutputUVs[ChannelIdx] == EUVOutput::OutputChannel; } FStaticMeshOperations::AppendMeshDescription(*RawMeshPtr, MergedMesh, AppendSettings); } } } } void FMeshMergeUtilities::MergeComponentsToInstances(const TArray& ComponentsToMerge, UWorld* World, ULevel* Level, const FMeshInstancingSettings& InSettings, bool bActuallyMerge /*= true*/, bool bReplaceSourceActors /* = false */, FText* OutResultsText /*= nullptr*/) const { auto HasInstanceVertexColors = [](UStaticMeshComponent* StaticMeshComponent) { for (const FStaticMeshComponentLODInfo& CurrentLODInfo : StaticMeshComponent->LODData) { if(CurrentLODInfo.OverrideVertexColors != nullptr || CurrentLODInfo.PaintedVertices.Num() > 0) { return true; } } return false; }; // Gather valid components TArray ValidComponents; for(UPrimitiveComponent* ComponentToMerge : ComponentsToMerge) { if(UStaticMeshComponent* StaticMeshComponent = Cast(ComponentToMerge)) { // Dont harvest from 'destination' actors if(StaticMeshComponent->GetOwner()->GetClass() != InSettings.ActorClassToUse.Get()) { if( !InSettings.bSkipMeshesWithVertexColors || !HasInstanceVertexColors(StaticMeshComponent)) { ValidComponents.Add(StaticMeshComponent); } } } } if(OutResultsText != nullptr) { *OutResultsText = LOCTEXT("InstanceMergePredictedResultsNone", "The current settings will not result in any instanced meshes being created"); } if(ValidComponents.Num() > 0) { /** Helper struct representing a spawned ISMC */ struct FComponentEntry { FComponentEntry(UStaticMeshComponent* InComponent) { StaticMesh = InComponent->GetStaticMesh(); InComponent->GetUsedMaterials(Materials); bReverseCulling = InComponent->GetComponentTransform().ToMatrixWithScale().Determinant() < 0.0f; CollisionProfileName = InComponent->GetCollisionProfileName(); CollisionEnabled = InComponent->GetCollisionEnabled(); OriginalComponents.Add(InComponent); } bool operator==(const FComponentEntry& InOther) const { return StaticMesh == InOther.StaticMesh && Materials == InOther.Materials && bReverseCulling == InOther.bReverseCulling && CollisionProfileName == InOther.CollisionProfileName && CollisionEnabled == InOther.CollisionEnabled; } UStaticMesh* StaticMesh; TArray Materials; TArray OriginalComponents; FName CollisionProfileName; bool bReverseCulling; ECollisionEnabled::Type CollisionEnabled; }; /** Helper struct representing a spawned ISMC-containing actor */ struct FActorEntry { FActorEntry(UStaticMeshComponent* InComponent, ULevel* InLevel) : MergedActor(nullptr) { // intersect with HLOD volumes if we have a level if(InLevel) { for (AActor* Actor : InLevel->Actors) { if (AHierarchicalLODVolume* HierarchicalLODVolume = Cast(Actor)) { FBox BoundingBox = InComponent->Bounds.GetBox(); FBox VolumeBox = HierarchicalLODVolume->GetComponentsBoundingBox(true); if (VolumeBox.IsInside(BoundingBox) || (HierarchicalLODVolume->bIncludeOverlappingActors && VolumeBox.Intersect(BoundingBox))) { HLODVolume = HierarchicalLODVolume; break; } } } } } bool operator==(const FActorEntry& InOther) const { return HLODVolume == InOther.HLODVolume; } AActor* MergedActor; AHierarchicalLODVolume* HLODVolume; TArray ComponentEntries; }; // Gather a list of components to merge TArray ActorEntries; for(UStaticMeshComponent* StaticMeshComponent : ValidComponents) { int32 ActorEntryIndex = ActorEntries.AddUnique(FActorEntry(StaticMeshComponent, InSettings.bUseHLODVolumes ? Level : nullptr)); FActorEntry& ActorEntry = ActorEntries[ActorEntryIndex]; FComponentEntry ComponentEntry(StaticMeshComponent); if(FComponentEntry* ExistingComponentEntry = ActorEntry.ComponentEntries.FindByKey(ComponentEntry)) { ExistingComponentEntry->OriginalComponents.Add(StaticMeshComponent); } else { ActorEntry.ComponentEntries.Add(ComponentEntry); } } // Filter by component count for(FActorEntry& ActorEntry : ActorEntries) { ActorEntry.ComponentEntries = ActorEntry.ComponentEntries.FilterByPredicate([&InSettings](const FComponentEntry& InEntry) { return InEntry.OriginalComponents.Num() >= InSettings.InstanceReplacementThreshold; }); } // Remove any empty actor entries ActorEntries.RemoveAll([](const FActorEntry& ActorEntry){ return ActorEntry.ComponentEntries.Num() == 0; }); int32 TotalComponentCount = 0; TArray ActorsToCleanUp; for(FActorEntry& ActorEntry : ActorEntries) { for(const FComponentEntry& ComponentEntry : ActorEntry.ComponentEntries) { TotalComponentCount++; for(UStaticMeshComponent* OriginalComponent : ComponentEntry.OriginalComponents) { if(AActor* OriginalActor = OriginalComponent->GetOwner()) { ActorsToCleanUp.AddUnique(OriginalActor); } } } } if(ActorEntries.Num() > 0) { if(OutResultsText != nullptr) { *OutResultsText = FText::Format(LOCTEXT("InstanceMergePredictedResults", "The current settings will result in {0} instanced static mesh components ({1} actors will be replaced)"), FText::AsNumber(TotalComponentCount), FText::AsNumber(ActorsToCleanUp.Num())); } if(bActuallyMerge) { // Create our actors const FScopedTransaction Transaction(LOCTEXT("PlaceInstancedActors", "Place Instanced Actor(s)")); Level->Modify(false); FActorSpawnParameters Params; Params.OverrideLevel = Level; // We now have the set of component data we want to apply for(FActorEntry& ActorEntry : ActorEntries) { ActorEntry.MergedActor = World->SpawnActor(InSettings.ActorClassToUse.Get(), Params); for(const FComponentEntry& ComponentEntry : ActorEntry.ComponentEntries) { UInstancedStaticMeshComponent* NewComponent = nullptr; NewComponent = (UInstancedStaticMeshComponent*)ActorEntry.MergedActor->FindComponentByClass(InSettings.ISMComponentToUse.Get()); if (NewComponent && NewComponent->PerInstanceSMData.Num() > 0) { NewComponent = nullptr; } if (NewComponent == nullptr) { NewComponent = NewObject(ActorEntry.MergedActor, InSettings.ISMComponentToUse.Get(), NAME_None, RF_Transactional); NewComponent->bHasPerInstanceHitProxies = true; if (ActorEntry.MergedActor->GetRootComponent()) { // Attach to root if we already have one NewComponent->AttachToComponent(ActorEntry.MergedActor->GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform); } else { // Make a new root if we dont have a root already ActorEntry.MergedActor->SetRootComponent(NewComponent); } // Take 'instanced' ownership so it persists with this actor ActorEntry.MergedActor->RemoveOwnedComponent(NewComponent); NewComponent->CreationMethod = EComponentCreationMethod::Instance; ActorEntry.MergedActor->AddOwnedComponent(NewComponent); } NewComponent->SetStaticMesh(ComponentEntry.StaticMesh); for(int32 MaterialIndex = 0; MaterialIndex < ComponentEntry.Materials.Num(); ++MaterialIndex) { NewComponent->SetMaterial(MaterialIndex, ComponentEntry.Materials[MaterialIndex]); } NewComponent->SetReverseCulling(ComponentEntry.bReverseCulling); NewComponent->SetCollisionProfileName(ComponentEntry.CollisionProfileName); NewComponent->SetCollisionEnabled(ComponentEntry.CollisionEnabled); NewComponent->SetMobility(EComponentMobility::Static); FISMComponentBatcher ISMComponentBatcher; ISMComponentBatcher.Append(ComponentEntry.OriginalComponents); ISMComponentBatcher.InitComponent(NewComponent); NewComponent->RegisterComponent(); } World->UpdateCullDistanceVolumes(ActorEntry.MergedActor); } // Now clean up our original actors for(AActor* ActorToCleanUp : ActorsToCleanUp) { if (bReplaceSourceActors) { ActorToCleanUp->Destroy(); } else { ActorToCleanUp->Modify(); ActorToCleanUp->bIsEditorOnlyActor = true; ActorToCleanUp->SetHidden(true); ActorToCleanUp->bHiddenEd = true; ActorToCleanUp->SetIsTemporarilyHiddenInEditor(true); } } // pop a toast allowing selection auto SelectActorsLambda = [ActorEntries]() { GEditor->GetSelectedActors()->Modify(); GEditor->GetSelectedActors()->BeginBatchSelectOperation(); GEditor->SelectNone(false, true, false); for(const FActorEntry& ActorEntry : ActorEntries) { GEditor->SelectActor(ActorEntry.MergedActor, true, false, true); } GEditor->GetSelectedActors()->EndBatchSelectOperation(); }; // Always change selection if we removed the source actors, // Otherwise, allow selection change through notification if (bReplaceSourceActors) { SelectActorsLambda(); } else { FNotificationInfo NotificationInfo(FText::Format(LOCTEXT("CreatedInstancedActorsMessage", "Created {0} Instanced Actor(s)"), FText::AsNumber(ActorEntries.Num()))); NotificationInfo.Hyperlink = FSimpleDelegate::CreateLambda(SelectActorsLambda); NotificationInfo.HyperlinkText = LOCTEXT("SelectActorsHyperlink", "Select Actors"); NotificationInfo.ExpireDuration = 5.0f; FSlateNotificationManager::Get().AddNotification(NotificationInfo); } } } } } UMaterialInterface* FMeshMergeUtilities::CreateProxyMaterial(const FString &InBasePackageName, FString MergedAssetPackageName, UMaterialInterface* InBaseMaterial, UPackage* InOuter, const FMeshMergingSettings &InSettings, const FFlattenMaterial& OutMaterial, TArray& OutAssetsToSync, FMaterialUpdateContext* InMaterialUpdateContext) const { // Create merged material asset FString MaterialAssetName; FString MaterialPackageName; if (InBasePackageName.IsEmpty()) { MaterialAssetName = FPackageName::GetShortName(MergedAssetPackageName); MaterialPackageName = FPackageName::GetLongPackagePath(MergedAssetPackageName) + TEXT("/"); } else { MaterialAssetName = FPackageName::GetShortName(InBasePackageName); MaterialPackageName = FPackageName::GetLongPackagePath(InBasePackageName) + TEXT("/"); } UPackage* MaterialPackage = InOuter; if (MaterialPackage == nullptr) { MaterialPackage = CreatePackage( *(MaterialPackageName + MaterialAssetName)); check(MaterialPackage); MaterialPackage->FullyLoad(); MaterialPackage->Modify(); } UMaterialInstanceConstant* MergedMaterial = FMaterialUtilities::CreateFlattenMaterialInstance(MaterialPackage, InSettings.MaterialSettings, InBaseMaterial, OutMaterial, MaterialPackageName, MaterialAssetName, OutAssetsToSync, InMaterialUpdateContext); // Set material static lighting usage flag if project has static lighting enabled if (IsStaticLightingAllowed()) { MergedMaterial->CheckMaterialUsage(MATUSAGE_StaticLighting); } return MergedMaterial; } void FMeshMergeUtilities::RetrievePhysicsData(const TArray& ComponentsToMerge, TArray& InOutPhysicsGeometry, UBodySetup*& OutBodySetupSource) const { InOutPhysicsGeometry.AddDefaulted(ComponentsToMerge.Num()); for (int32 ComponentIndex = 0, PhysicsGeometryIndex = 0; ComponentIndex < ComponentsToMerge.Num(); ++ComponentIndex) { UPrimitiveComponent* PrimComp = ComponentsToMerge[ComponentIndex]; UBodySetup* BodySetup = nullptr; FTransform ComponentToWorld = FTransform::Identity; auto ExtractPhysicGeometry = [&BodySetup, &ComponentToWorld, &OutBodySetupSource, &InOutPhysicsGeometry, PrimComp](int32 PhysicsIndex) { USplineMeshComponent* SplineMeshComponent = Cast(PrimComp); FMeshMergeHelpers::ExtractPhysicsGeometry(BodySetup, ComponentToWorld, SplineMeshComponent != nullptr, InOutPhysicsGeometry[PhysicsIndex]); if (SplineMeshComponent) { FMeshMergeHelpers::PropagateSplineDeformationToPhysicsGeometry(SplineMeshComponent, InOutPhysicsGeometry[PhysicsIndex]); } // We will use first valid BodySetup as a source of physics settings if (OutBodySetupSource == nullptr) { OutBodySetupSource = BodySetup; } }; if (UInstancedStaticMeshComponent* ISMComp = Cast(PrimComp)) { const int32 NumberOfInstances = ISMComp->PerInstanceSMData.Num(); const UStaticMesh* SrcMesh = ISMComp->GetStaticMesh(); if (NumberOfInstances > 1) { InOutPhysicsGeometry.AddDefaulted(NumberOfInstances - 1); } if (SrcMesh) { BodySetup = SrcMesh->GetBodySetup(); } for (const FInstancedStaticMeshInstanceData& InstanceData : ISMComp->PerInstanceSMData) { ComponentToWorld = FTransform(InstanceData.Transform) * ISMComp->GetComponentToWorld(); ExtractPhysicGeometry(PhysicsGeometryIndex++); } } else if (UStaticMeshComponent* StaticMeshComp = Cast(PrimComp)) { UStaticMesh* SrcMesh = StaticMeshComp->GetStaticMesh(); if (SrcMesh) { BodySetup = SrcMesh->GetBodySetup(); } ComponentToWorld = StaticMeshComp->GetComponentToWorld(); ExtractPhysicGeometry(PhysicsGeometryIndex++); } else if (UShapeComponent* ShapeComp = Cast(PrimComp)) { BodySetup = ShapeComp->GetBodySetup(); ComponentToWorld = ShapeComp->GetComponentToWorld(); ExtractPhysicGeometry(PhysicsGeometryIndex++); } } } #undef LOCTEXT_NAMESPACE // "MeshMergeUtils"