// Copyright Epic Games, Inc. All Rights Reserved. #include "InterchangeGenericMeshPipeline.h" #include "AssetRegistry/AssetRegistryModule.h" #include "Async/Async.h" #include "CoreMinimal.h" #include "InterchangeMaterialFactoryNode.h" #include "InterchangeMeshLODContainerNode.h" #include "InterchangeMeshNode.h" #include "InterchangePipelineLog.h" #include "InterchangePipelineMeshesUtilities.h" #include "InterchangeSceneComponentNodes.h" #include "InterchangeSceneNode.h" #include "InterchangeStaticMeshFactoryNode.h" #include "InterchangeStaticMeshLodDataNode.h" #include "Mesh/InterchangeMeshHelper.h" #include "Nodes/InterchangeBaseNode.h" #include "Nodes/InterchangeBaseNodeContainer.h" #include "Nodes/InterchangeSourceNode.h" #include "Nodes/InterchangeUserDefinedAttribute.h" #include "Async/Async.h" #include "CoreMinimal.h" #include "Tasks/Task.h" #include "UObject/Object.h" #include "UObject/ObjectMacros.h" namespace UE::Interchange::Private { FString GetNodeName(TObjectPtr PipelineMeshesUtilities, const UInterchangeBaseNodeContainer& NodeContainer, const FString& NodeUid) { const UInterchangeBaseNode* BaseNode = NodeContainer.GetNode(NodeUid); if (BaseNode) { FString NodeName = BaseNode->GetDisplayLabel(); if (const UInterchangeSceneNode* SceneNode = Cast(BaseNode)) { if (SceneNode->IsSpecializedTypeContains(UE::Interchange::FSceneNodeStaticData::GetLodGroupSpecializeTypeString())) { TArray LodGroupChildrens = NodeContainer.GetNodeChildrenUids(SceneNode->GetUniqueID()); if (LodGroupChildrens.Num() > 0) { return GetNodeName(PipelineMeshesUtilities, NodeContainer, LodGroupChildrens[0]); } } } else if (const UInterchangeMeshNode* MeshNode = Cast(BaseNode)) { //If this mesh is reference by only one scene node that do not have any children, use the scene node display label const FInterchangeMeshGeometry& MeshGeometry = PipelineMeshesUtilities->GetMeshGeometryByUid(NodeUid); if (MeshGeometry.ReferencingMeshInstanceUids.Num() == 1 && NodeContainer.GetNodeChildrenCount(MeshGeometry.ReferencingMeshInstanceUids[0]) == 0) { if(const UInterchangeBaseNode* InstanceMeshNode = NodeContainer.GetNode(MeshGeometry.ReferencingMeshInstanceUids[0])) { return InstanceMeshNode->GetDisplayLabel(); } } } return NodeName; } else { return FString(); } } } static TTuple GetCollisionMeshType( TObjectPtr PipelineMeshesUtilities, const UInterchangeBaseNodeContainer& NodeContainer, const FString& NodeUid, const TArray& AllNodeUids, bool bImportCollisionAccordingToMeshName ) { EInterchangeMeshCollision CollisionType = EInterchangeMeshCollision::None; // Try to find the mesh node we're actually talking about const UInterchangeBaseNode* BaseNode = NodeContainer.GetNode(NodeUid); const UInterchangeMeshNode* MeshNode = Cast(BaseNode); if (!MeshNode) { if (const UInterchangeSceneNode* SceneNode = Cast(BaseNode)) { FString MeshDependency; if (SceneNode->GetCustomAssetInstanceUid(MeshDependency)) { MeshNode = Cast(NodeContainer.GetNode(MeshDependency)); } } } // If we have an explicit collision type on the mesh, we know the mesh node is a collision mesh itself if (MeshNode) { if (MeshNode->GetCustomCollisionType(CollisionType) && CollisionType != EInterchangeMeshCollision::None) { return {CollisionType, NodeUid}; } } if (bImportCollisionAccordingToMeshName) { FString MeshName = UE::Interchange::Private::GetNodeName(PipelineMeshesUtilities, NodeContainer, NodeUid); // Determine if the mesh name is a potential collision mesh if (MeshName.StartsWith(TEXT("UBX_"))) { CollisionType = EInterchangeMeshCollision::Box; } else if (MeshName.StartsWith(TEXT("UCX_")) || MeshName.StartsWith(TEXT("MCDCX_"))) { CollisionType = EInterchangeMeshCollision::Convex18DOP; } else if (MeshName.StartsWith(TEXT("USP_"))) { CollisionType = EInterchangeMeshCollision::Sphere; } else if (MeshName.StartsWith(TEXT("UCP_"))) { CollisionType = EInterchangeMeshCollision::Capsule; } else { return { EInterchangeMeshCollision::None, FString() }; } // We have a mesh name with a collision type suffix. // However it should only be treated as a collision mesh if its body name corresponds to one of the other meshes. // If we get here, we know there is at least one underscore, so we never expect either of these character searches to fail. int32 FirstUnderscore = INDEX_NONE; verify(MeshName.FindChar(TEXT('_'), FirstUnderscore)); int32 LastUnderscore = INDEX_NONE; verify(MeshName.FindLastChar(TEXT('_'), LastUnderscore)); auto MatchPredicate = [&PipelineMeshesUtilities , &NodeContainer](FString Body) { // Generate a predicate to be used by the below Finds. return [Body, &PipelineMeshesUtilities, &NodeContainer](const FString& ToCompare) { return Body == UE::Interchange::Private::GetNodeName(PipelineMeshesUtilities, NodeContainer, ToCompare); }; }; // If we find a mesh named the same as the collision mesh (following the collision prefix), we have a match // e.g. this will match 'UBX_House' with a mesh called 'House' { const FString RenderMeshName = MeshName.RightChop(FirstUnderscore + 1); const FString* CorrespondingMeshUid = AllNodeUids.FindByPredicate(MatchPredicate(RenderMeshName)); if (CorrespondingMeshUid) { return { CollisionType, *CorrespondingMeshUid }; } } // Otherwise strip the final underscore suffix from the collision mesh name and look again // e.g. this will match 'UBX_House_01' with a mesh called 'House' if (FirstUnderscore != LastUnderscore) { const FString RenderMeshName = MeshName.Mid(FirstUnderscore + 1, LastUnderscore - FirstUnderscore - 1); const FString* CorrespondingMeshUid = AllNodeUids.FindByPredicate(MatchPredicate(RenderMeshName)); if (CorrespondingMeshUid) { return { CollisionType, *CorrespondingMeshUid }; } } // It is possible our target mesh has been renamed away by a name sanitizing process (see UE-248981 and // FFbxParser::EnsureNodeNameAreValid). Since it is unusual to have a prefixed collision mesh and no target, let's // assume we actually have a render mesh somewhere and try stripping any nameclash-looking suffixes from mesh names // to see if we have a match { // Like above start by just stripping the prefix (e.g. 'UBX_House_01' --> 'House_01') FString RenderMeshName = MeshName.RightChop(FirstUnderscore + 1); auto NameCollisionSearchPredicate = [&RenderMeshName, &PipelineMeshesUtilities, &NodeContainer](const FString& Other) { FString MeshName = UE::Interchange::Private::GetNodeName(PipelineMeshesUtilities, NodeContainer, Other); if (!MeshName.StartsWith(RenderMeshName)) { return false; } FString MeshNameSuffix = MeshName.RightChop(RenderMeshName.Len()); // Check if everything after the render mesh name is just some combination of "ncl" (nameclash), numbers and/or underscores MeshNameSuffix = MeshNameSuffix.Replace(TEXT("ncl"), TEXT("")); FString LastChar = MeshNameSuffix.Right(1); while ((LastChar.IsNumeric() || LastChar == TEXT("_")) && MeshNameSuffix.Len() > 0) { MeshNameSuffix.LeftChopInline(1, EAllowShrinking::No); LastChar = MeshNameSuffix.Right(1); } // If there's nothing left, everything after the RenderMeshName was nameclash stuff, so this is likely our mesh return MeshNameSuffix.Len() == 0; }; const FString* CorrespondingMeshUid = AllNodeUids.FindByPredicate(NameCollisionSearchPredicate); if (CorrespondingMeshUid) { return {CollisionType, *CorrespondingMeshUid}; } // If we still have nothing, try also stripping numbered suffixes from the render mesh name and repeating the search // (e.g. 'UBX_House_01' --> 'House') RenderMeshName = MeshName.Mid(FirstUnderscore + 1, LastUnderscore - FirstUnderscore - 1); CorrespondingMeshUid = AllNodeUids.FindByPredicate(NameCollisionSearchPredicate); if (CorrespondingMeshUid) { return {CollisionType, *CorrespondingMeshUid}; } } } return { EInterchangeMeshCollision::None, FString() }; } // Returns true if MeshUid describes a Mesh node that is purely the collision mesh of some other mesh static bool IsJustCollisionMesh(TObjectPtr PipelineMeshesUtilities, const UInterchangeBaseNodeContainer& NodeContainer, const FString& MeshUid, const TArray& MeshUids, bool bImportCollisionAccordingToMeshName) { TTuple Tuple = GetCollisionMeshType(PipelineMeshesUtilities, NodeContainer, MeshUid, MeshUids, bImportCollisionAccordingToMeshName); // If MeshUid is a collision mesh but *of itself*, then it is both a collision and a render mesh, and we should return false return Tuple.Key != EInterchangeMeshCollision::None && Tuple.Value != MeshUid; } static void BuildMeshToCollisionMeshMap(TObjectPtr PipelineMeshesUtilities, const UInterchangeBaseNodeContainer& NodeContainer, const TArray& MeshUids, TMap>& MeshToCollisionMeshMap, bool bImportCollisionAccordingToMeshName) { for (const FString& MeshUid : MeshUids) { TTuple CollisionType = GetCollisionMeshType(PipelineMeshesUtilities, NodeContainer, MeshUid, MeshUids, bImportCollisionAccordingToMeshName); if (CollisionType.Get<0>() != EInterchangeMeshCollision::None) { MeshToCollisionMeshMap.FindOrAdd(FString(CollisionType.Get<1>())).Emplace(MeshUid); } } } void UInterchangeGenericMeshPipeline::ExecutePreImportPipelineStaticMesh() { check(CommonMeshesProperties.IsValid()); #if WITH_EDITOR //Make sure the generic pipeline will cover all staticmesh build settings when we import Async(EAsyncExecution::TaskGraphMainThread, []() { static bool bVerifyBuildProperties = false; if (!bVerifyBuildProperties) { bVerifyBuildProperties = true; TArray Classes; Classes.Add(UInterchangeGenericCommonMeshesProperties::StaticClass()); Classes.Add(UInterchangeGenericMeshPipeline::StaticClass()); if (!DoClassesIncludeAllEditableStructProperties(Classes, FMeshBuildSettings::StaticStruct())) { UE_LOG(LogInterchangePipeline, Log, TEXT("UInterchangeGenericMeshPipeline: The generic pipeline does not cover all static mesh build options.")); } } }); #endif if (UInterchangeSourceNode* SourceNode = UInterchangeSourceNode::FindOrCreateUniqueInstance(BaseNodeContainer)) { // Keep a single triangle threshold for the entire scene SourceNode->SetCustomNaniteTriangleThreshold(NaniteTriangleThreshold); } if (bImportStaticMeshes && (CommonMeshesProperties->ForceAllMeshAsType == EInterchangeForceMeshType::IFMT_None || CommonMeshesProperties->ForceAllMeshAsType == EInterchangeForceMeshType::IFMT_StaticMesh)) { if (bCombineStaticMeshes) { // Combine all the static meshes bool bFoundMeshes = false; { // If baking transforms, get all the static mesh instance nodes, and group them by LOD TArray MeshUids; PipelineMeshesUtilities->GetAllStaticMeshInstance(MeshUids); TMap> MeshUidsPerLodIndex; for (const FString& MeshUid : MeshUids) { const FInterchangeMeshInstance& MeshInstance = PipelineMeshesUtilities->GetMeshInstanceByUid(MeshUid); for (const auto& LodIndexAndSceneNodeContainer : MeshInstance.SceneNodePerLodIndex) { const int32 LodIndex = LodIndexAndSceneNodeContainer.Key; const FInterchangeLodSceneNodeContainer& SceneNodeContainer = LodIndexAndSceneNodeContainer.Value; TArray& TranslatedNodes = MeshUidsPerLodIndex.FindOrAdd(LodIndex); for (const UInterchangeBaseNode* BaseNode : SceneNodeContainer.BaseNodes) { TranslatedNodes.Add(BaseNode->GetUniqueID()); } } } //We must add all nodes that are not part of any lod to all lods upper then 0 if (MeshUidsPerLodIndex.Num() > 1) { constexpr int32 LodIndexZero = 0; for (const FString& MeshUid : MeshUids) { const FInterchangeMeshInstance& MeshInstance = PipelineMeshesUtilities->GetMeshInstanceByUid(MeshUid); if (MeshInstance.LodGroupNode == nullptr && MeshInstance.SceneNodePerLodIndex.Num() == 1 && MeshInstance.SceneNodePerLodIndex.Contains(LodIndexZero) && MeshInstance.SceneNodePerLodIndex[LodIndexZero].BaseNodes.Num() == 1) { const UInterchangeBaseNode* BaseNode = MeshInstance.SceneNodePerLodIndex[LodIndexZero].BaseNodes[0]; //This mesh must be added to all existing LODs over 0 for (TPair>& MeshUidsPerLodIndexPair : MeshUidsPerLodIndex) { if (MeshUidsPerLodIndexPair.Key > LodIndexZero) { TArray& TranslatedNodes = MeshUidsPerLodIndexPair.Value; TranslatedNodes.AddUnique(BaseNode->GetUniqueID()); } } } } } // If we got some instances, create a static mesh factory node if (MeshUidsPerLodIndex.Num() > 0) { UInterchangeStaticMeshFactoryNode* StaticMeshFactoryNode = CreateStaticMeshFactoryNode(MeshUidsPerLodIndex); StaticMeshFactoryNodes.Add(StaticMeshFactoryNode); bFoundMeshes = true; } } if (!bFoundMeshes) { // If we haven't yet managed to build a factory node, look at static mesh geometry directly. TArray MeshUids; PipelineMeshesUtilities->GetAllStaticMeshGeometry(MeshUids); TMap> MeshUidsPerLodIndex; for (const FString& MeshUid : MeshUids) { // MeshGeometry cannot have Lod since LODs are defined in the scene node const FInterchangeMeshGeometry& MeshGeometry = PipelineMeshesUtilities->GetMeshGeometryByUid(MeshUid); const int32 LodIndex = 0; TArray& TranslatedNodes = MeshUidsPerLodIndex.FindOrAdd(LodIndex); TranslatedNodes.Add(MeshGeometry.MeshUid); } if (MeshUidsPerLodIndex.Num() > 0) { UInterchangeStaticMeshFactoryNode* StaticMeshFactoryNode = CreateStaticMeshFactoryNode(MeshUidsPerLodIndex); StaticMeshFactoryNodes.Add(StaticMeshFactoryNode); } } } else { // Do not combine static meshes auto CreateMeshFactoryNodeUnCombined = [this](const TArray& MeshUids, const bool bInstancedMesh)->bool { constexpr int32 LodIndexZero = 0; bool bFoundMeshes = false; // Work out which meshes are collision meshes which correspond to another mesh TMap> MeshToCollisionMeshMap; BuildMeshToCollisionMeshMap(PipelineMeshesUtilities, *BaseNodeContainer, MeshUids, MeshToCollisionMeshMap, bImportCollisionAccordingToMeshName); // Now iterate through each mesh UID, creating a new factory for each one for (const FString& MeshUid : MeshUids) { // If this is just a collision mesh, don't add a factory node; it will be added as part of another factory node if (IsJustCollisionMesh(PipelineMeshesUtilities, *BaseNodeContainer, MeshUid, MeshUids, bImportCollisionAccordingToMeshName)) { continue; } TMap> MeshUidsPerLodIndex; if (bInstancedMesh) { //Instanced geometry can have lods const FInterchangeMeshInstance& MeshInstance = PipelineMeshesUtilities->GetMeshInstanceByUid(MeshUid); for (const auto& LodIndexAndSceneNodeContainer : MeshInstance.SceneNodePerLodIndex) { const int32 LodIndex = LodIndexAndSceneNodeContainer.Key; const FInterchangeLodSceneNodeContainer& SceneNodeContainer = LodIndexAndSceneNodeContainer.Value; TArray& TranslatedNodes = MeshUidsPerLodIndex.FindOrAdd(LodIndex); for (const UInterchangeBaseNode* BaseNode : SceneNodeContainer.BaseNodes) { TranslatedNodes.Add(BaseNode->GetUniqueID()); } } } else { // #interchange_lod_rework //Original presumption was: 'Non instanced geometry cannot have lods', which is not quite correct, // will have to fix when refactoring LOD handling. const FInterchangeMeshGeometry& MeshGeometry = PipelineMeshesUtilities->GetMeshGeometryByUid(MeshUid); TArray& TranslatedNodes = MeshUidsPerLodIndex.FindOrAdd(LodIndexZero); TranslatedNodes.Add(MeshGeometry.MeshUid); } if (MeshUidsPerLodIndex.Num() > 0) { if (bCollision) { if (const TArray* CorrespondingCollisionMeshes = MeshToCollisionMeshMap.Find(MeshUid)) { TArray& TranslatedNodes = MeshUidsPerLodIndex.FindOrAdd(LodIndexZero); for (const FString& CollisionMesh : *CorrespondingCollisionMeshes) { // AddUnique here because now we support meshes being both collision AND render meshes at the // same time we may already have these entries in MeshUidsPerLodIndex TranslatedNodes.AddUnique(CollisionMesh); } } } if (UInterchangeStaticMeshFactoryNode* StaticMeshFactoryNode = CreateStaticMeshFactoryNode(MeshUidsPerLodIndex, MeshUid)) { StaticMeshFactoryNodes.Add(StaticMeshFactoryNode); UpdateAssemblyPartDependencyTable(StaticMeshFactoryNode, MeshUidsPerLodIndex); bFoundMeshes = true; } } } return bFoundMeshes; }; TArray MeshUids; PipelineMeshesUtilities->GetAllStaticMeshInstance(MeshUids); if (!CreateMeshFactoryNodeUnCombined(MeshUids, true/*bInstancedMesh*/)) { MeshUids.Reset(); PipelineMeshesUtilities->GetAllStaticMeshGeometry(MeshUids); CreateMeshFactoryNodeUnCombined(MeshUids, false/*bInstancedMesh*/); } } } CreateAssemblyPartDependencies(); } namespace UE::InterchangeStaticMeshFactory::Private { void ApplyNaniteTriangleThreshold( const UInterchangeStaticMeshFactoryNode* FactoryNode, UStaticMesh* StaticMesh, const UInterchangeBaseNodeContainer* NodeContainer ) { #if WITH_EDITORONLY_DATA if (!FactoryNode || !NodeContainer || !StaticMesh || !StaticMesh->GetNaniteSettings().bEnabled) { return; } int64 TriangleThreshold = 0; if (const UInterchangeSourceNode* SourceNode = UInterchangeSourceNode::GetUniqueInstance(NodeContainer)) { SourceNode->GetCustomNaniteTriangleThreshold(TriangleThreshold); } if (TriangleThreshold <= 0) { return; } const int32 LodIndex = 0; FMeshDescription* MeshDescription = StaticMesh->GetMeshDescription(LodIndex); if (!MeshDescription) { return; } if (MeshDescription->Triangles().Num() < TriangleThreshold) { StaticMesh->GetNaniteSettings().bEnabled = false; } #endif // #if WITH_EDITORONLY_DATA } } // namespace UE::InterchangeStaticMeshFactory::Private void UInterchangeGenericMeshPipeline::ExecutePostFactoryPipelineStaticMesh( const UInterchangeBaseNodeContainer* InBaseNodeContainer, const FString& NodeKey, UObject* CreatedAsset, bool bIsAReimport ) { UStaticMesh* StaticMesh = Cast(CreatedAsset); if (!StaticMesh) { return; } UInterchangeStaticMeshFactoryNode* StaticMeshFactoryNode = Cast(InBaseNodeContainer->GetFactoryNode(NodeKey)); if (!StaticMeshFactoryNode) { return; } // If we're reimporting and being told to leave the properties alone, just early out instead if (bIsAReimport) { EReimportStrategyFlags Strategy = StaticMeshFactoryNode->GetReimportStrategyFlags(); if (Strategy == EReimportStrategyFlags::ApplyNoProperties) { return; } } UE::InterchangeStaticMeshFactory::Private::ApplyNaniteTriangleThreshold(StaticMeshFactoryNode, StaticMesh, InBaseNodeContainer); } bool UInterchangeGenericMeshPipeline::MakeMeshFactoryNodeUidAndDisplayLabel(const TMap>& MeshUidsPerLodIndex, int32 LodIndex, FString& NewNodeUid, FString& DisplayLabel, const FString& BaseMeshUid) { int32 SceneNodeCount = 0; if (!ensure(LodIndex >= 0 && MeshUidsPerLodIndex.Num() > LodIndex)) { return false; } auto SetUidLabel = [&NewNodeUid, &DisplayLabel](const UInterchangeBaseNode* BaseNode) { NewNodeUid = BaseNode->GetUniqueID(); DisplayLabel = BaseNode->GetDisplayLabel(); }; auto SetUidLabelFromSceneNode = [&NewNodeUid, &DisplayLabel](const UInterchangeSceneNode* SceneNode, bool& bIsLodGroup) { bIsLodGroup = SceneNode->IsSpecializedTypeContains(UE::Interchange::FSceneNodeStaticData::GetLodGroupSpecializeTypeString()); //We can't have the ScenNode's id become a FactoryNodeUID as is, so we are adding a \\Mesh\\ prefix. NewNodeUid = (bIsLodGroup ? TEXT("\\MeshLODGroupInstance\\") : TEXT("\\MeshInstance\\")) + SceneNode->GetUniqueID(); DisplayLabel = SceneNode->GetDisplayLabel(); }; if (!BaseMeshUid.IsEmpty()) { //BaseMeshUid represents either: // - UInterchangeSceneNode's UID that is a LOD Group. // - UInterchangeSceneNode's UID that is instantiating a UInterchangeMeshNode. (via CustomAssetInstanceUid). // - UInterchangeMeshNode's UID. // - UInterchangeMeshLODContainerNode's UID. // - in case of bCombineStaticMeshes == true this ID should be empty for now. const UInterchangeBaseNode* BaseNode = BaseNodeContainer->GetNode(BaseMeshUid); if (BaseNode) { if (const UInterchangeSceneNode* SceneNode = Cast(BaseNode)) { //Regardless of the SceneNode being a LOD Group, or just instantiating a Mesh, // if it was marked as the baseMeshUid we should use it's UID and Name: bool bIsLodGroup = false; SetUidLabelFromSceneNode(SceneNode, bIsLodGroup); if (!CommonMeshesProperties->bBakeMeshes && !bIsLodGroup) { //We recursively check the AssetInstanceUid's chain to make sure we find LOD Group or MeshUID. FString AssetInstanceUid; while (SceneNode && SceneNode->GetCustomAssetInstanceUid(AssetInstanceUid)) { const UInterchangeBaseNode* AssetInstanceNode = BaseNodeContainer->GetNode(AssetInstanceUid); SceneNode = Cast(AssetInstanceNode); //if its a sceneNode without a LOD group specialization we continue the search if (SceneNode && SceneNode->IsSpecializedTypeContains(UE::Interchange::FSceneNodeStaticData::GetLodGroupSpecializeTypeString())) { //if it is a LOD Group specialization then we use the original NewNodeUid. (as then we want the 'instantiation of the LOD Group) NewNodeUid = TEXT("\\MeshLODGroupInstance\\") + BaseMeshUid; return true; } if (const UInterchangeMeshNode* MeshNode = Cast(AssetInstanceNode)) { //if it is a Mesh Node then we confirmed that it is an instanced Mesh // but since we are not baking meshes we want the MeshNode //It cannot be part of a LOD group as that would have broken out sooner due to the LodGroup check. //For this case we are using MeshNode's UID and DisplayName. SetUidLabel(AssetInstanceNode); return true; } } } else { return true; } } else if (const UInterchangeMeshLODContainerNode* LODContainerNode = Cast(BaseNode)) { SetUidLabel(BaseNode); return true; } else if (const UInterchangeMeshNode* MeshNode = Cast(BaseNode)) { SetUidLabel(BaseNode); return true; } } } //If BaseMeshUID is not provided and or BaseMeshUID is not a LOD Group, fallback onto LOD=0 UID[0] to generate UID and Name. for (const TPair>& LodIndexAndMeshUids : MeshUidsPerLodIndex) { if (LodIndex == LodIndexAndMeshUids.Key) { const TArray& Uids = LodIndexAndMeshUids.Value; if (Uids.Num() > 0) { const FString& Uid = Uids[0]; const UInterchangeBaseNode* BaseNode = BaseNodeContainer->GetNode(Uid); if (const UInterchangeMeshNode* MeshNode = Cast(BaseNode)) { SetUidLabel(BaseNode); return true; } if (const UInterchangeSceneNode* SceneNode = Cast(BaseNode)) { bool bIsLodGroup = false; SetUidLabelFromSceneNode(SceneNode, bIsLodGroup); if (!CommonMeshesProperties->bBakeMeshes && !bIsLodGroup) { //We recursively check the AssetInstanceUid's chain to make sure we find LOD Group or MeshUID. FString AssetInstanceUid; while (SceneNode && SceneNode->GetCustomAssetInstanceUid(AssetInstanceUid)) { const UInterchangeBaseNode* AssetInstanceNode = BaseNodeContainer->GetNode(AssetInstanceUid); SceneNode = Cast(AssetInstanceNode); //if its a sceneNode without a LOD group specialization we continue the search if (SceneNode && SceneNode->IsSpecializedTypeContains(UE::Interchange::FSceneNodeStaticData::GetLodGroupSpecializeTypeString())) { //if it is a LOD Group specialization then we use the original NewNodeUid. (as then we want the 'instantiation' of the LOD Group) NewNodeUid = TEXT("\\MeshLODGroupInstance\\") + Uid; return true; } if (const UInterchangeMeshNode* MeshNode = Cast(AssetInstanceNode)) { //if it is a Mesh Node then we confirmed that it is an instanced Mesh // but since we are not baking meshes we want the MeshNode //It cannot be part of a LOD group as that would have broken out sooner due to the LodGroup check. //For this case we are using MeshNode's UID and DisplayName. SetUidLabel(AssetInstanceNode); return true; } } } else { return true; } } if (const UInterchangeInstancedStaticMeshComponentNode* ISMComponentNode = Cast(BaseNode)) { FString RefMeshUid; if (ISMComponentNode->GetCustomInstancedAssetUid(RefMeshUid)) { const UInterchangeBaseNode* MeshNode = BaseNodeContainer->GetNode(RefMeshUid); if (MeshNode) { SetUidLabel(MeshNode); return true; } } } } // We found the lod but there is no valid Mesh node to return the Uid break; } } return false; } UInterchangeStaticMeshFactoryNode* UInterchangeGenericMeshPipeline::CreateStaticMeshFactoryNode(const TMap>& MeshUidsPerLodIndex, const FString& BaseMeshUid) { check(CommonMeshesProperties.IsValid()); if (MeshUidsPerLodIndex.Num() == 0) { return nullptr; } // Create the static mesh factory node, name it according to the first mesh node compositing the meshes FString StaticMeshUid_MeshNamePart; FString DisplayLabel; const int32 BaseLodIndex = 0; if (!MakeMeshFactoryNodeUidAndDisplayLabel(MeshUidsPerLodIndex, BaseLodIndex, StaticMeshUid_MeshNamePart, DisplayLabel, BaseMeshUid)) { // Log an error return nullptr; } // #interchange_lod_rework: if rework will be calling out LOD Groups directly this won't be needed. auto CheckAndStoreBaseMeshUidForLODGroup = [this, BaseMeshUid](UInterchangeStaticMeshFactoryNode* StaticMeshFactoryNode) { if (const UInterchangeSceneNode* BaseMeshSceneNode = Cast(BaseNodeContainer->GetNode(BaseMeshUid))) { if (BaseMeshSceneNode->IsSpecializedTypeContains(UE::Interchange::FSceneNodeStaticData::GetLodGroupSpecializeTypeString())) { StaticMeshFactoryNode->RegisterAttribute(UE::Interchange::FAttributeKey(FString(TEXT("LODGroupSceneNodeUid"))), BaseMeshUid); } else { bool bLODGroup_WithoutLODSpecialization = false; if (BaseMeshSceneNode->GetAttribute(TEXT("LODGroup_WithoutLODSpecialization"), bLODGroup_WithoutLODSpecialization)) { if (bLODGroup_WithoutLODSpecialization) { StaticMeshFactoryNode->RegisterAttribute(UE::Interchange::FAttributeKey(FString(TEXT("LODGroupSceneNodeUid"))), BaseMeshUid); } } } } }; const FString StaticMeshUid = UInterchangeFactoryBaseNode::BuildFactoryNodeUid(StaticMeshUid_MeshNamePart); UInterchangeFactoryBaseNode* FactoryBaseNode = BaseNodeContainer->GetFactoryNode(StaticMeshUid); UInterchangeStaticMeshFactoryNode* StaticMeshFactoryNode = Cast(FactoryBaseNode); if (StaticMeshFactoryNode) { CheckAndStoreBaseMeshUidForLODGroup(StaticMeshFactoryNode); return StaticMeshFactoryNode; } StaticMeshFactoryNode = NewObject(BaseNodeContainer, NAME_None); if (!ensure(StaticMeshFactoryNode)) { return nullptr; } StaticMeshFactoryNode->InitializeStaticMeshNode(StaticMeshUid, DisplayLabel, UStaticMesh::StaticClass()->GetName(), BaseNodeContainer); CheckAndStoreBaseMeshUidForLODGroup(StaticMeshFactoryNode); //Set the pipeline import sockets property StaticMeshFactoryNode->SetCustomImportSockets(CommonMeshesProperties->bImportSockets); if (CommonMeshesProperties->bKeepSectionsSeparate) { StaticMeshFactoryNode->SetCustomKeepSectionsSeparate(CommonMeshesProperties->bKeepSectionsSeparate); } AddLodDataToStaticMesh(StaticMeshFactoryNode, MeshUidsPerLodIndex); switch (CommonMeshesProperties->VertexColorImportOption) { case EInterchangeVertexColorImportOption::IVCIO_Replace: { StaticMeshFactoryNode->SetCustomVertexColorReplace(true); } break; case EInterchangeVertexColorImportOption::IVCIO_Ignore: { StaticMeshFactoryNode->SetCustomVertexColorIgnore(true); } break; case EInterchangeVertexColorImportOption::IVCIO_Override: { StaticMeshFactoryNode->SetCustomVertexColorOverride(CommonMeshesProperties->VertexOverrideColor); } break; } StaticMeshFactoryNode->SetCustomLODGroup(LodGroup); //Common meshes build options StaticMeshFactoryNode->SetCustomRecomputeNormals(CommonMeshesProperties->bRecomputeNormals); StaticMeshFactoryNode->SetCustomRecomputeTangents(CommonMeshesProperties->bRecomputeTangents); StaticMeshFactoryNode->SetCustomUseMikkTSpace(CommonMeshesProperties->bUseMikkTSpace); StaticMeshFactoryNode->SetCustomComputeWeightedNormals(CommonMeshesProperties->bComputeWeightedNormals); StaticMeshFactoryNode->SetCustomUseHighPrecisionTangentBasis(CommonMeshesProperties->bUseHighPrecisionTangentBasis); StaticMeshFactoryNode->SetCustomUseFullPrecisionUVs(CommonMeshesProperties->bUseFullPrecisionUVs); StaticMeshFactoryNode->SetCustomUseBackwardsCompatibleF16TruncUVs(CommonMeshesProperties->bUseBackwardsCompatibleF16TruncUVs); StaticMeshFactoryNode->SetCustomRemoveDegenerates(CommonMeshesProperties->bRemoveDegenerates); //Static meshes build options StaticMeshFactoryNode->SetCustomBuildReversedIndexBuffer(bBuildReversedIndexBuffer); StaticMeshFactoryNode->SetCustomGenerateLightmapUVs(bGenerateLightmapUVs); StaticMeshFactoryNode->SetCustomGenerateDistanceFieldAsIfTwoSided(bGenerateDistanceFieldAsIfTwoSided); StaticMeshFactoryNode->SetCustomSupportFaceRemap(bSupportFaceRemap); StaticMeshFactoryNode->SetCustomMinLightmapResolution(MinLightmapResolution); StaticMeshFactoryNode->SetCustomSrcLightmapIndex(SrcLightmapIndex); StaticMeshFactoryNode->SetCustomDstLightmapIndex(DstLightmapIndex); StaticMeshFactoryNode->SetCustomBuildScale3D(BuildScale3D); StaticMeshFactoryNode->SetCustomDistanceFieldResolutionScale(DistanceFieldResolutionScale); StaticMeshFactoryNode->SetCustomDistanceFieldReplacementMesh(DistanceFieldReplacementMesh.Get()); StaticMeshFactoryNode->SetCustomMaxLumenMeshCards(MaxLumenMeshCards); StaticMeshFactoryNode->SetCustomBuildNanite(bBuildNanite); StaticMeshFactoryNode->SetCustomAutoComputeLODScreenSizes(bAutoComputeLODScreenSizes); StaticMeshFactoryNode->SetLODScreenSizes(LODScreenSizes); return StaticMeshFactoryNode; } UInterchangeStaticMeshLodDataNode* UInterchangeGenericMeshPipeline::CreateStaticMeshLodDataNode(const FString& NodeName, const FString& NodeUniqueID) { FString DisplayLabel(NodeName); FString NodeUID(NodeUniqueID); UInterchangeStaticMeshLodDataNode* StaticMeshLodDataNode = NewObject(BaseNodeContainer, NAME_None); if (!ensure(StaticMeshLodDataNode)) { // @TODO: log error return nullptr; } BaseNodeContainer->SetupNode(StaticMeshLodDataNode, NodeUID, DisplayLabel, EInterchangeNodeContainerType::FactoryData); StaticMeshLodDataNode->SetOneConvexHullPerUCX(bOneConvexHullPerUCX); StaticMeshLodDataNode->SetImportCollision(bCollision); StaticMeshLodDataNode->SetImportCollisionType(Collision); StaticMeshLodDataNode->SetForceCollisionPrimitiveGeneration(bForceCollisionPrimitiveGeneration); return StaticMeshLodDataNode; } void UInterchangeGenericMeshPipeline::AddLodDataToStaticMesh(UInterchangeStaticMeshFactoryNode* StaticMeshFactoryNode, const TMap>& NodeUidsPerLodIndex) { check(CommonMeshesProperties.IsValid()); const FString StaticMeshFactoryUid = StaticMeshFactoryNode->GetUniqueID(); int32 MaxLodIndex = 0; for (const TPair>& LodIndexAndNodeUids : NodeUidsPerLodIndex) { MaxLodIndex = FMath::Max(MaxLodIndex, LodIndexAndNodeUids.Key); } TArray EmptyLodData; const int32 LodCount = MaxLodIndex+1; for(int32 LodIndex = 0; LodIndex < LodCount; ++LodIndex) { if (!CommonMeshesProperties->bImportLods && LodIndex > 0) { // If the pipeline should not import lods, skip any lod over base lod continue; } const TArray& NodeUids = NodeUidsPerLodIndex.Contains(LodIndex) ? NodeUidsPerLodIndex.FindChecked(LodIndex) : EmptyLodData; // Create a lod data node with all the meshes for this LOD const FString StaticMeshLodDataName = TEXT("LodData") + FString::FromInt(LodIndex); const FString LODDataPrefix = TEXT("\\LodData") + (LodIndex > 0 ? FString::FromInt(LodIndex) : TEXT("")); const FString StaticMeshLodDataUniqueID = LODDataPrefix + StaticMeshFactoryUid; // Create LodData node if it doesn't already exist UInterchangeStaticMeshLodDataNode* LodDataNode = Cast(BaseNodeContainer->GetFactoryNode(StaticMeshLodDataUniqueID)); if (!LodDataNode) { // Add the data for the LOD (all the mesh node fbx path, so we can find them when we will create the payload data) LodDataNode = CreateStaticMeshLodDataNode(StaticMeshLodDataName, StaticMeshLodDataUniqueID); BaseNodeContainer->SetNodeParentUid(StaticMeshLodDataUniqueID, StaticMeshFactoryUid); StaticMeshFactoryNode->AddLodDataUniqueId(StaticMeshLodDataUniqueID); } TMap ExistingLodSlotMaterialDependencies; constexpr bool bAddSourceNodeName = true; for (const FString& NodeUid : NodeUids) { TMap SlotMaterialDependencies; if (const UInterchangeSceneNode* SceneNode = Cast(BaseNodeContainer->GetNode(NodeUid))) { FString MeshDependency; SceneNode->GetCustomAssetInstanceUid(MeshDependency); if (BaseNodeContainer->IsNodeUidValid(MeshDependency)) { const UInterchangeMeshNode* MeshDependencyNode = Cast(BaseNodeContainer->GetNode(MeshDependency)); UInterchangeUserDefinedAttributesAPI::DuplicateAllUserDefinedAttribute(MeshDependencyNode, StaticMeshFactoryNode, bAddSourceNodeName); StaticMeshFactoryNode->AddTargetNodeUid(MeshDependency); StaticMeshFactoryNode->AddSocketUids(PipelineMeshesUtilities->GetMeshGeometryByUid(MeshDependency).AttachedSocketUids); MeshDependencyNode->AddTargetNodeUid(StaticMeshFactoryNode->GetUniqueID()); MeshDependencyNode->GetSlotMaterialDependencies(SlotMaterialDependencies); } else { SceneNode->GetSlotMaterialDependencies(SlotMaterialDependencies); } UInterchangeUserDefinedAttributesAPI::DuplicateAllUserDefinedAttribute(SceneNode, StaticMeshFactoryNode, bAddSourceNodeName); } else if (const UInterchangeMeshNode* MeshNode = Cast(BaseNodeContainer->GetNode(NodeUid))) { UInterchangeUserDefinedAttributesAPI::DuplicateAllUserDefinedAttribute(MeshNode, StaticMeshFactoryNode, bAddSourceNodeName); StaticMeshFactoryNode->AddTargetNodeUid(NodeUid); StaticMeshFactoryNode->AddSocketUids(PipelineMeshesUtilities->GetMeshGeometryByUid(NodeUid).AttachedSocketUids); MeshNode->AddTargetNodeUid(StaticMeshFactoryNode->GetUniqueID()); MeshNode->GetSlotMaterialDependencies(SlotMaterialDependencies); } else if (const UInterchangeInstancedStaticMeshComponentNode* ISMComponentNode = Cast(BaseNodeContainer->GetNode(NodeUid))) { FString MeshDependency; ISMComponentNode->GetCustomInstancedAssetUid(MeshDependency); if (BaseNodeContainer->IsNodeUidValid(MeshDependency)) { const UInterchangeMeshNode* MeshDependencyNode = Cast(BaseNodeContainer->GetNode(MeshDependency)); UInterchangeUserDefinedAttributesAPI::DuplicateAllUserDefinedAttribute(MeshDependencyNode, StaticMeshFactoryNode, bAddSourceNodeName); StaticMeshFactoryNode->AddTargetNodeUid(MeshDependency); StaticMeshFactoryNode->AddSocketUids(PipelineMeshesUtilities->GetMeshGeometryByUid(MeshDependency).AttachedSocketUids); MeshDependencyNode->AddTargetNodeUid(StaticMeshFactoryNode->GetUniqueID()); MeshDependencyNode->GetSlotMaterialDependencies(SlotMaterialDependencies); } UInterchangeUserDefinedAttributesAPI::DuplicateAllUserDefinedAttribute(ISMComponentNode, StaticMeshFactoryNode, bAddSourceNodeName); } UE::Interchange::MeshesUtilities::ApplySlotMaterialDependencies(*StaticMeshFactoryNode, SlotMaterialDependencies, *BaseNodeContainer, &ExistingLodSlotMaterialDependencies); const FString MeshName = UE::Interchange::Private::GetNodeName(PipelineMeshesUtilities, *BaseNodeContainer, NodeUid); // Ignore the "_" so we can detect potential typos done by the user. const bool bHasCollisionPrefix = MeshName.StartsWith(TEXT("UBX")) || MeshName.StartsWith(TEXT("UCX")) || MeshName.StartsWith(TEXT("USP")) || MeshName.StartsWith(TEXT("UCP")) || MeshName.StartsWith(TEXT("MCDCX")); auto LogWarning = [this, &StaticMeshFactoryNode](const FText& Text) { UInterchangeResultWarning_Generic* Message = AddMessage(); Message->SourceAssetName = SourceDatas[0]->GetFilename(); Message->AssetFriendlyName = StaticMeshFactoryNode->GetAssetName(); Message->AssetType = StaticMeshFactoryNode->GetObjectClass(); Message->Text = Text; }; if (bImportCollisionAccordingToMeshName) { TTuple MeshType = GetCollisionMeshType(PipelineMeshesUtilities, *BaseNodeContainer, NodeUid, NodeUids, bImportCollisionAccordingToMeshName); EInterchangeMeshCollision CollisionType = MeshType.Key; const FString& RenderMeshUid = MeshType.Value; const FString& ColliderMeshUid = NodeUid; switch (CollisionType) { case EInterchangeMeshCollision::None: LodDataNode->AddMeshUid(NodeUid); // If the user is attempting to import a collision mesh using a naming convention but the mesh doesn't match, warn them that it won't be imported as collision. if (bHasCollisionPrefix) { LogWarning(FText::Format(NSLOCTEXT("UInterchangeGenericStaticMeshPipeline", "NoMatchingRenderMeshForCustomCollision", "No render mesh found for custom collision '{0}'. The name must follow the pattern 'UCX_[ExactRenderMeshName]', and a matching render mesh with that name must exist in the source file."), FText::FromString(MeshName))); } break; case EInterchangeMeshCollision::Box: LodDataNode->AddBoxCollisionMeshUids(ColliderMeshUid, RenderMeshUid); break; case EInterchangeMeshCollision::Sphere: LodDataNode->AddSphereCollisionMeshUids(ColliderMeshUid, RenderMeshUid); break; case EInterchangeMeshCollision::Capsule: LodDataNode->AddCapsuleCollisionMeshUids(ColliderMeshUid, RenderMeshUid); break; case EInterchangeMeshCollision::Convex10DOP_X: case EInterchangeMeshCollision::Convex10DOP_Y: case EInterchangeMeshCollision::Convex10DOP_Z: case EInterchangeMeshCollision::Convex18DOP: case EInterchangeMeshCollision::Convex26DOP: LodDataNode->AddConvexCollisionMeshUids(ColliderMeshUid, RenderMeshUid); break; } // If the Mesh is both render AND collision mesh let's also add it as a render mesh, // as the above switch will have only added the collision mesh to the LodDataNode if (RenderMeshUid == NodeUid && CollisionType != EInterchangeMeshCollision::None) { LodDataNode->AddMeshUid(NodeUid); } } else { LodDataNode->AddMeshUid(NodeUid); // Warn the user that this mesh appears to be a custom collision mesh but that the pipeline is not set to import it as such if (bHasCollisionPrefix) { LogWarning(FText::Format(NSLOCTEXT("UInterchangeGenericStaticMeshPipeline", "ImportCollisionAccordingToMeshNameDisabled", "The mesh '{0}' appears to use custom collision naming pattern, but the setting 'Import Collisions According to Mesh Name' is disabled. It will be treated as a render mesh."), FText::FromString(MeshName))); } } } UE::Interchange::MeshesUtilities::ReorderSlotMaterialDependencies(*StaticMeshFactoryNode, *BaseNodeContainer); } } // #interchange_lod_rework void UInterchangeGenericMeshPipeline::FindAndProcessIdenticalStaticMeshLODGroups(const UInterchangeBaseNodeContainer* InBaseNodeContainer) { if (!CommonMeshesProperties->bBakeMeshes) { auto GetGlobalTransform = [&InBaseNodeContainer]( const FString& InMeshOrSceneNodeUid, FTransform& OutGlobalTransform, bool& bMeshNode, FString& LODMeshAssetUid ) { bMeshNode = true; const UInterchangeBaseNode* Node = InBaseNodeContainer->GetNode(InMeshOrSceneNodeUid); const UInterchangeMeshNode* MeshNode = Cast(Node); if (MeshNode == nullptr) { bMeshNode = false; // MeshUid must refer to a scene node const UInterchangeSceneNode* SceneNode = Cast(Node); if (!ensure(SceneNode)) { return; } // Get the transform from the scene node FTransform SceneNodeGlobalTransform; if (SceneNode->GetCustomGlobalTransform(InBaseNodeContainer, FTransform::Identity, SceneNodeGlobalTransform)) { OutGlobalTransform = SceneNodeGlobalTransform; } UE::Interchange::Private::MeshHelper::AddSceneNodeGeometricAndPivotToGlobalTransform(OutGlobalTransform, SceneNode, true /*bakeMeshes*/, true /*bakePivotMeshes*/); // And get the mesh node which it references FString MeshDependencyUid; SceneNode->GetCustomAssetInstanceUid(MeshDependencyUid); LODMeshAssetUid = MeshDependencyUid; } else { LODMeshAssetUid = InMeshOrSceneNodeUid; } }; //Iterate through all UInterchangeStaticMeshFactoryNode and build a helper struct that will help us identify identical LOD Group Static Meshes. struct FLODGroupIIHelper //Identical Identifier { TMap Meshes; UInterchangeStaticMeshFactoryNode* LODGroupStaticMeshFactoryNode; TArray DisabledIdenticals; bool DataEquals(const FLODGroupIIHelper& LODGroupIIHelper) { if (Meshes.Num() != LODGroupIIHelper.Meshes.Num()) { return false; } for (const TPair& Entry : Meshes) { if (!LODGroupIIHelper.Meshes.Contains(Entry.Key)) { return false; } if (!LODGroupIIHelper.Meshes[Entry.Key].Equals(Entry.Value)) { return false; } } return true; } }; TArray LODGroupIdenticalIdentifier; auto ProcessEntryCandidate = [&LODGroupIdenticalIdentifier](FLODGroupIIHelper& ToCompare) { for (FLODGroupIIHelper& LODGroupIIHelper : LODGroupIdenticalIdentifier) { //TODO/Note: we should compare all attributes of the UInterchangeStaticMeshFactoryNode to make sure that regardless of Mesh and Transforms they are identical and can be subsituted. //For now however we are only checking Mesh and Transform. //For now this is fine as the rest of the UInterchangeStaticMeshFactoryNode attributes are set from the Pipeline and Utilities properties which are shared across. if (LODGroupIIHelper.DataEquals(ToCompare)) { LODGroupIIHelper.DisabledIdenticals.Add(ToCompare.LODGroupStaticMeshFactoryNode); ToCompare.LODGroupStaticMeshFactoryNode->SetEnabled(false); ToCompare.LODGroupStaticMeshFactoryNode->SetCustomSubstituteUID(LODGroupIIHelper.LODGroupStaticMeshFactoryNode->GetUniqueID()); return; } } LODGroupIdenticalIdentifier.Add(ToCompare); }; InBaseNodeContainer->IterateNodesOfType( [&InBaseNodeContainer, &GetGlobalTransform, &LODGroupIdenticalIdentifier, &ProcessEntryCandidate](const FString& NodeUid, UInterchangeStaticMeshFactoryNode* StaticMeshFactoryNode) { FString LODGroupSceneUid; if (StaticMeshFactoryNode->GetAttribute(TEXT("LODGroupSceneNodeUid"), LODGroupSceneUid)) { const UInterchangeSceneNode* LODGroupSceneNode = Cast(InBaseNodeContainer->GetNode(LODGroupSceneUid)); if (!LODGroupSceneNode) { return; } FLODGroupIIHelper LODGroupIIHelper; LODGroupIIHelper.LODGroupStaticMeshFactoryNode = StaticMeshFactoryNode; //Get LOD Group's Global Transform: FTransform LODGroupGlobalTransform; bool bLODGroupMeshNode; //Dummy FString LODMeshAssetUid; //Dummy GetGlobalTransform(LODGroupSceneUid, LODGroupGlobalTransform, bLODGroupMeshNode, LODMeshAssetUid); FTransform LODGroupGlobalTransformInverse = LODGroupGlobalTransform.Inverse(); TArray LodDataUniqueIds; StaticMeshFactoryNode->GetLodDataUniqueIds(LodDataUniqueIds); for (size_t LODIndex = 0; LODIndex < LodDataUniqueIds.Num(); LODIndex++) { const FString& LodUniqueId = LodDataUniqueIds[LODIndex]; const FString LODIndexString = FString::FromInt(LODIndex); const UInterchangeStaticMeshLodDataNode* LodDataNode = Cast(InBaseNodeContainer->GetNode(LodUniqueId)); if (!ensure(LodDataNode)) { continue; } TArray MeshUids; LodDataNode->GetMeshUids(MeshUids); for (const FString& MeshUid : MeshUids) { FTransform MeshGlobalTransform; bool bMeshNode; FString MeshAssetUid; GetGlobalTransform(MeshUid, MeshGlobalTransform, bMeshNode, MeshAssetUid); if (!MeshAssetUid.IsEmpty()) { if (bMeshNode) { LODGroupIIHelper.Meshes.Add(LODIndexString + TEXT("_") + MeshAssetUid, FTransform::Identity); } else { //Note: Need the relative transform from the LODGroup not from the root for equal check purposes. LODGroupIIHelper.Meshes.Add(LODIndexString + TEXT("_") + MeshAssetUid, LODGroupGlobalTransformInverse * MeshGlobalTransform); } } } } ProcessEntryCandidate(LODGroupIIHelper); } }); } }