// Copyright Epic Games, Inc. All Rights Reserved. #include "ClothingAsset.h" #include "ClothingAssetCustomVersion.h" #include "ClothPhysicalMeshData.h" #include "ClothConfig.h" #include "Utils/ClothingMeshUtils.h" #include "Features/IModularFeatures.h" #include "Engine/SkeletalMesh.h" #include "PhysicsEngine/PhysicsAsset.h" #include "Widgets/Notifications/SNotificationList.h" #include "Framework/Notifications/NotificationManager.h" #include "Rendering/SkeletalMeshModel.h" #include "Rendering/SkeletalMeshRenderData.h" #include "Components/SkeletalMeshComponent.h" #include "ClothingSimulationInteractor.h" #include "ComponentReregisterContext.h" #include "UObject/UObjectIterator.h" #include "UObject/PhysicsObjectVersion.h" #include "UObject/FortniteMainBranchObjectVersion.h" #include "UObject/FortniteReleaseBranchCustomObjectVersion.h" #include "UObject/UE5ReleaseStreamObjectVersion.h" #include "UObject/UE5SpecialProjectStreamObjectVersion.h" #include "GPUSkinPublicDefs.h" #include "GPUSkinVertexFactory.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(ClothingAsset) DEFINE_LOG_CATEGORY(LogClothingAsset) #define LOCTEXT_NAMESPACE "ClothingAsset" //============================================================================== // ClothingAssetUtils //============================================================================== #if WITH_EDITOR void ClothingAssetUtils::GetAllMeshClothingAssetBindings(const USkeletalMesh* SkeletalMesh, TArray& OutBindings) { OutBindings.Empty(); if (!SkeletalMesh) { return; } if (const FSkeletalMeshModel* MeshModel = SkeletalMesh->GetImportedModel()) { int32 LODNum = MeshModel->LODModels.Num(); for (int32 LODIndex = 0; LODIndex < LODNum; ++LODIndex) { const FSkeletalMeshLODModel& MeshLODModel = MeshModel->LODModels[LODIndex]; if (MeshLODModel.HasClothData()) { TArray LodBindings; GetAllLodMeshClothingAssetBindings(SkeletalMesh, LodBindings, LODIndex); OutBindings.Append(LodBindings); } } } } void ClothingAssetUtils::GetAllLodMeshClothingAssetBindings(const USkeletalMesh* SkeletalMesh, TArray& OutBindings, int32 InLodIndex) { OutBindings.Empty(); if (!SkeletalMesh || !SkeletalMesh->GetImportedModel()) { return; } const FSkeletalMeshModel* MeshModel = SkeletalMesh->GetImportedModel(); if (!MeshModel || !MeshModel->LODModels.IsValidIndex(InLodIndex)) { return; } const FSkeletalMeshLODModel& MeshLODModel = MeshModel->LODModels[InLodIndex]; if (MeshLODModel.HasClothData()) { TArray LodBindings; int32 SectionNum = MeshLODModel.Sections.Num(); for (int32 SectionIndex = 0; SectionIndex < SectionNum; ++SectionIndex) { const FSkelMeshSection& Section = MeshLODModel.Sections[SectionIndex]; if (Section.HasClothingData()) { UClothingAssetBase* ClothingAsset = SkeletalMesh->GetClothingAsset(Section.ClothingData.AssetGuid); FClothingAssetMeshBinding ClothBinding; ClothBinding.Asset = Cast(ClothingAsset); ClothBinding.AssetInternalLodIndex = Section.ClothingData.AssetLodIndex;// InSkelMesh->GetClothingAssetIndex(Section.ClothingData.AssetGuid); check(ClothBinding.AssetInternalLodIndex == Section.ClothingData.AssetLodIndex); ClothBinding.LODIndex = InLodIndex; ClothBinding.SectionIndex = SectionIndex; OutBindings.Add(ClothBinding); } } } } void ClothingAssetUtils::ClearSectionClothingData(FSkelMeshSection& InSection) { InSection.ClothingData.AssetGuid = FGuid(); InSection.ClothingData.AssetLodIndex = INDEX_NONE; InSection.CorrespondClothAssetIndex = INDEX_NONE; InSection.ClothMappingDataLODs.Empty(); } #endif //============================================================================== // UClothingAssetCommon //============================================================================== UClothingAssetCommon::UClothingAssetCommon(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) , PhysicsAsset(nullptr) #if WITH_EDITORONLY_DATA , ClothSimConfig_DEPRECATED(nullptr) , ChaosClothSimConfig_DEPRECATED(nullptr) #endif , ReferenceBoneIndex(0) { } #if WITH_EDITOR void Warn(const FText& Error) { FNotificationInfo* const NotificationInfo = new FNotificationInfo(Error); NotificationInfo->ExpireDuration = 5.0f; FSlateNotificationManager::Get().QueueNotification(NotificationInfo); UE_LOG(LogClothingAsset, Warning, TEXT("%s"), *Error.ToString()); } bool UClothingAssetCommon::BindToSkeletalMesh( USkeletalMesh* InSkelMesh, const int32 InMeshLodIndex, const int32 InSectionIndex, const int32 InAssetLodIndex) { // Make sure the legacy LOD are upgraded (BindToSkeletalMesh could be called before the Cloth Asset's PostLoad is completed) for (UClothLODDataCommon_Legacy* LodDeprecated : ClothLodData_DEPRECATED) { if (LodDeprecated) { LodDeprecated->ConditionalPostLoad(); const int32 Idx = AddNewLod(); LodDeprecated->MigrateTo(LodData[Idx]); } } ClothLodData_DEPRECATED.Empty(); // If we've been added to the wrong mesh if(InSkelMesh != GetOuter()) { FText Error = FText::Format( LOCTEXT("Error_WrongMesh", "Failed to bind clothing asset {0} as the provided mesh is not the owner of this asset."), FText::FromString(GetName())); Warn(Error); return false; } // If we don't have clothing data if(!LodData.IsValidIndex(InAssetLodIndex)) { FText Error = FText::Format( LOCTEXT("Error_NoClothingLod", "Failed to bind clothing asset {0} LOD{1} as LOD{2} does not exist."), FText::FromString(GetName()), InAssetLodIndex, InAssetLodIndex); Warn(Error); return false; } // If we don't have a mesh if(!InSkelMesh) { FText Error = FText::Format( LOCTEXT("Error_NoMesh", "Failed to bind clothing asset {0} as provided skel mesh does not exist."), FText::FromString(GetName())); Warn(Error); return false; } // If the mesh LOD index is invalid if(!InSkelMesh->GetImportedModel()->LODModels.IsValidIndex(InMeshLodIndex)) { FText Error = FText::Format( LOCTEXT("Error_InvalidMeshLOD", "Failed to bind clothing asset {0} as mesh LOD{1} does not exist."), FText::FromString(GetName()), InMeshLodIndex); Warn(Error); return false; } const int32 NumMapEntries = LodMap.Num(); for(int MapIndex = 0; MapIndex < NumMapEntries; ++MapIndex) { const int32& MappedLod = LodMap[MapIndex]; if(MappedLod == InAssetLodIndex) { FText Error = FText::Format( LOCTEXT("Error_LodMapped", "Failed to bind clothing asset {0} LOD{1} as LOD{2} is already mapped to mesh LOD{3}."), FText::FromString(GetName()), InAssetLodIndex, InAssetLodIndex, MapIndex); Warn(Error); return false; } } if(LodMap.IsValidIndex(InMeshLodIndex) && LodMap[InMeshLodIndex] != INDEX_NONE) { // Already mapped return false; } CalculateReferenceBoneIndex(); // Grab the clothing and skel lod data FClothLODDataCommon& ClothLodData = LodData[InAssetLodIndex]; FSkeletalMeshLODModel& SkelLod = InSkelMesh->GetImportedModel()->LODModels[InMeshLodIndex]; FSkelMeshSection& OriginalSection = SkelLod.Sections[InSectionIndex]; // Data for mesh to mesh binding TArray MeshToMeshData; TArray RenderPositions; TArray RenderNormals; TArray RenderTangents; RenderPositions.Reserve(OriginalSection.SoftVertices.Num()); RenderNormals.Reserve(OriginalSection.SoftVertices.Num()); RenderTangents.Reserve(OriginalSection.SoftVertices.Num()); // Original data to weight to the clothing simulation mesh for(FSoftSkinVertex& UnrealVert : OriginalSection.SoftVertices) { RenderPositions.Add(UnrealVert.Position); RenderNormals.Add(UnrealVert.TangentZ); RenderTangents.Add(UnrealVert.TangentX); } TArrayView IndexView(SkelLod.IndexBuffer); IndexView = IndexView.Slice(OriginalSection.BaseIndex, OriginalSection.NumTriangles * 3); TArray RenderIndices; RenderIndices.Reserve(OriginalSection.NumTriangles * 3); for (uint32 OriginalIndex : IndexView) { const int32 TempIndex = (int32)OriginalIndex - (int32)OriginalSection.BaseVertexIndex; if (ensure(RenderPositions.IsValidIndex(TempIndex))) { RenderIndices.Add(TempIndex); } } const ClothingMeshUtils::ClothMeshDesc TargetMesh(RenderPositions, RenderNormals, RenderTangents, RenderIndices); const ClothingMeshUtils::ClothMeshDesc SourceMesh(ClothLodData.PhysicalMeshData.Vertices, ClothLodData.PhysicalMeshData.Indices); // Calculate averaged normals const FPointWeightMap* const MaxDistances = ClothLodData.PhysicalMeshData.FindWeightMap(EWeightMapTargetCommon::MaxDistance); ClothingMeshUtils::GenerateMeshToMeshVertData(MeshToMeshData, TargetMesh, SourceMesh, MaxDistances, ClothLodData.bSmoothTransition, ClothLodData.bUseMultipleInfluences, ClothLodData.SkinningKernelRadius); // We have to copy the bone map to verify we don't exceed the maximum while adding the clothing bones TArray TempBoneMap = OriginalSection.BoneMap; for(FName& BoneName : UsedBoneNames) { const int32 BoneIndex = InSkelMesh->GetRefSkeleton().FindBoneIndex(BoneName); if(BoneIndex != INDEX_NONE) { TempBoneMap.AddUnique(BoneIndex); } } // Verify number of bones against current capabilities if(TempBoneMap.Num() > FGPUBaseSkinVertexFactory::GetMaxGPUSkinBones()) { // Failed to apply as we've exceeded the number of bones we can skin FText Error = FText::Format( LOCTEXT("Error_TooManyBones", "Failed to bind clothing asset {0} LOD{1} as this causes the section to require {2} bones. The maximum per section is currently {3}."), FText::FromString(GetName()), InAssetLodIndex, TempBoneMap.Num(), FGPUBaseSkinVertexFactory::GetMaxGPUSkinBones()); Warn(Error); return false; } // After verifying copy the new bone map to the section OriginalSection.BoneMap = TempBoneMap; //Register the scope post edit change FScopedSkeletalMeshPostEditChange SkeletalMeshPostEditChange(InSkelMesh); // calculate LOD verts before adding our new section uint32 NumLodVertices = 0; for(const FSkelMeshSection& CurSection : SkelLod.Sections) { NumLodVertices += CurSection.GetNumVertices(); } // Set the asset index, used during rendering to pick the correct sim mesh buffer int32 AssetIndex = INDEX_NONE; const bool bMeshClothingAssetsWasFound = InSkelMesh->GetMeshClothingAssets().Find(this, AssetIndex); check(bMeshClothingAssetsWasFound); OriginalSection.CorrespondClothAssetIndex = AssetIndex; // sim properties OriginalSection.ClothMappingDataLODs.SetNum(1); OriginalSection.ClothMappingDataLODs[0] = MeshToMeshData; OriginalSection.ClothingData.AssetGuid = AssetGuid; OriginalSection.ClothingData.AssetLodIndex = InAssetLodIndex; bool bRequireBoneChange = false; for(FBoneIndexType& BoneIndex : OriginalSection.BoneMap) { if(!SkelLod.RequiredBones.Contains(BoneIndex)) { bRequireBoneChange = true; if(InSkelMesh->GetRefSkeleton().IsValidIndex(BoneIndex)) { SkelLod.RequiredBones.Add(BoneIndex); SkelLod.ActiveBoneIndices.AddUnique(BoneIndex); } } } if(bRequireBoneChange) { SkelLod.RequiredBones.Sort(); InSkelMesh->GetRefSkeleton().EnsureParentsExistAndSort(SkelLod.ActiveBoneIndices); } // Make sure the LOD map is always big enough for the asset to use. // This shouldn't grow to an unwieldy size but maybe consider compacting later. while(LodMap.Num() - 1 < InMeshLodIndex) { LodMap.Add(INDEX_NONE); } LodMap[InMeshLodIndex] = InAssetLodIndex; // Update the extra cloth deformer mapping LOD bias using this cloth entry if (InSkelMesh->GetSupportRayTracing()) { check(LodMap[InMeshLodIndex] == InAssetLodIndex); // UpdateLODBiasMappings relies on the LodMap being up to date UpdateLODBiasMappings(InSkelMesh, InMeshLodIndex, InSectionIndex); } return true; // FScopedSkeletalMeshPostEditChange goes out of scope, causing postedit change and components to be re-registered } void UClothingAssetCommon::UpdateAllLODBiasMappings(USkeletalMesh* SkeletalMesh) { check(SkeletalMesh); FSkeletalMeshModel* const SkeletalMeshModel = SkeletalMesh->GetImportedModel(); if (!SkeletalMeshModel) { return; } // Iterate through all source LODs with cloth that could lead to upper (raytraced) sections needing some additional mapping TIndirectArray& LODModels = SkeletalMesh->GetImportedModel()->LODModels; for (int32 LODIndex = LODModels.Num() - 1; LODIndex >= 0; --LODIndex) // Iterate in reverse order to allow shrinking the mapping array first { // Go through all sections to find the one(s?) that uses this cloth asset and clear the existing bias mappings TArray& Sections = LODModels[LODIndex].Sections; for (int32 SectionIndex = 0; SectionIndex < Sections.Num(); ++SectionIndex) { if (Sections[SectionIndex].ClothingData.AssetGuid == GetAssetGuid()) { Sections[SectionIndex].ClothMappingDataLODs.SetNum(1); // Only keep ClothMappingDataLODs[0] that is the same LOD mapping to remove LOD bias mappings if (SkeletalMesh->GetSupportRayTracing()) { UpdateLODBiasMappings(SkeletalMesh, LODIndex, SectionIndex); // Updates the upper LODs mappings of the specified section from this LODIndex } } } } } void UClothingAssetCommon::UpdateLODBiasMappings(const USkeletalMesh* SkeletalMesh, int32 UpdatedLODIndex, int32 SectionIndex) { check(SkeletalMesh); check(UpdatedLODIndex >= 0); FSkeletalMeshModel* const SkeletalMeshModel = SkeletalMesh->GetImportedModel(); check(SkeletalMeshModel); const bool bAddMappingsToAnyLOD = (SkeletalMesh->GetClothLODBiasMode() == EClothLODBiasMode::MappingsToAnyLOD); const bool bAddMappingsToMinLOD = (SkeletalMesh->GetClothLODBiasMode() == EClothLODBiasMode::MappingsToMinLOD && SkeletalMesh->GetRayTracingMinLOD() > 0); TIndirectArray& LODModels = SkeletalMeshModel->LODModels; TArray RenderPositions; TArray RenderNormals; TArray RenderTangents; TArray RenderIndices; auto PrepareDeformerTargetDescriptor = [&LODModels, &RenderPositions, &RenderNormals, &RenderTangents, &RenderIndices](int32 LODIndex, int32 SectionIndex) { FSkeletalMeshLODModel& LODModel = LODModels[LODIndex]; FSkelMeshSection& Section = LODModel.Sections[SectionIndex]; RenderPositions.Reset(Section.SoftVertices.Num()); RenderNormals.Reset(Section.SoftVertices.Num()); RenderTangents.Reset(Section.SoftVertices.Num()); for (const FSoftSkinVertex& SoftSkinVertex : Section.SoftVertices) { RenderPositions.Add(SoftSkinVertex.Position); RenderNormals.Add(SoftSkinVertex.TangentZ); RenderTangents.Add(SoftSkinVertex.TangentX); } const TConstArrayView Indices = TConstArrayView(LODModel.IndexBuffer).Slice(Section.BaseIndex, Section.NumTriangles * 3); RenderIndices.Reset(Section.NumTriangles * 3); for (uint32 VertexIndex : Indices) { RenderIndices.Add(VertexIndex - Section.BaseVertexIndex); } return ClothingMeshUtils::ClothMeshDesc(RenderPositions, RenderNormals, RenderTangents, RenderIndices); }; // Get the min and max LODs that might be affected by LOD bias and update other sections' bias with this LOD section int32 MinLOD = 0; int32 MaxLOD = 0; if (bAddMappingsToAnyLOD) { MinLOD = UpdatedLODIndex + 1; MaxLOD = LODModels.Num() - 1; } else if (bAddMappingsToMinLOD) { MinLOD = MaxLOD = SkeletalMesh->GetRayTracingMinLOD(); } if (UpdatedLODIndex < MinLOD && MinLOD <= MaxLOD) { // Prepare the deformer source (simulation) mesh descriptor const FClothLODDataCommon& ClothLodData = LodData[LodMap[UpdatedLODIndex]]; const ClothingMeshUtils::ClothMeshDesc SourceMesh(ClothLodData.PhysicalMeshData.Vertices, ClothLodData.PhysicalMeshData.Indices); // Calculate averaged normals // Retrieve max distance mask const FPointWeightMap* const MaxDistances = ClothLodData.PhysicalMeshData.FindWeightMap(EWeightMapTargetCommon::MaxDistance); // Iterate through all deformer target (render) mesh sections for (int32 LODIndex = MinLOD; LODIndex <= MaxLOD; ++LODIndex) { FSkeletalMeshLODModel& LODModel = LODModels[LODIndex]; // Note that the section LOD being updated here is the biased (raytraced) section LOD, not the UpdatedLODIndex which is done in the second loop below if (!LODModel.Sections.IsValidIndex(SectionIndex)) { continue; } FSkelMeshSection& Section = LODModel.Sections[SectionIndex]; if (!Section.HasClothingData()) { continue; // This LOD section won't render clothing and therefore can be skipped as it won't ever need mappings } // Update bias mapping to the newly updated LOD index const int32 LODBias = LODIndex - UpdatedLODIndex; check(LODBias > 0); Section.ClothMappingDataLODs.SetNum(FMath::Max(LODBias + 1, Section.ClothMappingDataLODs.Num())); TArray& MeshToMeshData = Section.ClothMappingDataLODs[LODBias]; MeshToMeshData.Reset(); // Prepare the deformer target (render) mesh descriptor const ClothingMeshUtils::ClothMeshDesc TargetMesh = PrepareDeformerTargetDescriptor(LODIndex, SectionIndex); // Generate the missing bias mapping data ClothingMeshUtils::GenerateMeshToMeshVertData(MeshToMeshData, TargetMesh, SourceMesh, MaxDistances, ClothLodData.bSmoothTransition, ClothLodData.bUseMultipleInfluences, ClothLodData.SkinningKernelRadius); UE_LOG(LogClothingAsset, Verbose, TEXT("Added deformer data for [%s/%s], section %d, LODBias = %d, render mesh LOD = %d, sim mesh LOD = %d"), *SkeletalMesh->GetName(), *GetName(), SectionIndex, LODBias, LODIndex, UpdatedLODIndex); } } // Update this LOD section bias mappings that didn't get setup since there weren't no cloth attached to it yet if (bAddMappingsToAnyLOD || (bAddMappingsToMinLOD && UpdatedLODIndex == SkeletalMesh->GetRayTracingMinLOD())) { FSkeletalMeshLODModel& LODModel = LODModels[UpdatedLODIndex]; // Note that the section LOD being updated here is the UpdatedLODIndex, not the biased (raytraced) section LOD which is done in the first loop above FSkelMeshSection& Section = LODModel.Sections[SectionIndex]; check(Section.HasClothingData()); // Prepare the deformer target (render) mesh descriptor const ClothingMeshUtils::ClothMeshDesc TargetMesh = PrepareDeformerTargetDescriptor(UpdatedLODIndex, SectionIndex); // Iterate through all the LOD that could be rendered and need this source LOD to be raytraced for (int32 LODIndex = 0; LODIndex < UpdatedLODIndex; ++LODIndex) { FSkeletalMeshLODModel& SourceLODModel = LODModels[LODIndex]; if (!SourceLODModel.Sections.IsValidIndex(SectionIndex)) { continue; } FSkelMeshSection& SourceSection = SourceLODModel.Sections[SectionIndex]; // Locate the deformer's source (simulation) cloth asset, since it could be a different cloth asset. const UClothingAssetCommon* const ClothingAsset = Cast(SkeletalMesh->GetClothingAsset(SourceSection.ClothingData.AssetGuid)); if (!ClothingAsset) { const FText Error = FText::Format( LOCTEXT("Error_DifferentAsset", "Incomplete raytracing cloth LOD bias mappings generation on [{0}/{1}], section {2}, LOD {3} due to missing cloth asset at LOD {4}."), FText::FromString(SkeletalMesh->GetName()), FText::FromString(GetName()), SectionIndex, UpdatedLODIndex, LODIndex); Warn(Error); continue; } // Prepare the deformer source (simulation) mesh descriptor const FClothLODDataCommon& ClothLodData = ClothingAsset->LodData[LodMap[LODIndex]]; const ClothingMeshUtils::ClothMeshDesc SourceMesh(ClothLodData.PhysicalMeshData.Vertices, ClothLodData.PhysicalMeshData.Indices); // Calculate averaged normals // Retrieve max distance mask const FPointWeightMap* const MaxDistances = ClothLodData.PhysicalMeshData.FindWeightMap(EWeightMapTargetCommon::MaxDistance); // Update bias mapping for the updated LOD section const int32 LODBias = UpdatedLODIndex - LODIndex; check(LODBias > 0); Section.ClothMappingDataLODs.SetNum(FMath::Max(LODBias + 1, Section.ClothMappingDataLODs.Num())); TArray& MeshToMeshData = Section.ClothMappingDataLODs[LODBias]; MeshToMeshData.Reset(); ClothingMeshUtils::GenerateMeshToMeshVertData(MeshToMeshData, TargetMesh, SourceMesh, MaxDistances, ClothLodData.bSmoothTransition, ClothLodData.bUseMultipleInfluences, ClothLodData.SkinningKernelRadius); UE_LOG(LogClothingAsset, Verbose, TEXT("Added deformer data for [%s/%s], section %d, LODBias = %d, render mesh LOD = %d, sim mesh LOD = %d"), *SkeletalMesh->GetName(), *GetName(), SectionIndex, LODBias, UpdatedLODIndex, LODIndex); } } } void UClothingAssetCommon::ClearLODBiasMappings(const USkeletalMesh* SkeletalMesh, int32 UpdatedLODIndex, int32 SectionIndex) { check(SkeletalMesh); check(UpdatedLODIndex >= 0); FSkeletalMeshModel* const SkeletalMeshModel = SkeletalMesh->GetImportedModel(); check(SkeletalMeshModel); for (int32 LODIndex = UpdatedLODIndex + 1; LODIndex < SkeletalMeshModel->LODModels.Num(); ++LODIndex) { FSkeletalMeshLODModel& BiasLodModel = SkeletalMeshModel->LODModels[LODIndex]; if (BiasLodModel.Sections.IsValidIndex(SectionIndex)) { FSkelMeshSection& BiasSection = BiasLodModel.Sections[SectionIndex]; const int32 LODBias = LODIndex - UpdatedLODIndex; if (BiasSection.ClothMappingDataLODs.IsValidIndex(LODBias)) { BiasSection.ClothMappingDataLODs[LODBias].Empty(); UE_LOG(LogClothingAsset, Verbose, TEXT("Removed deformer data for [%s/%s], section %d LODBias = %d, render mesh LOD = %d, sim mesh LOD = %d"), *SkeletalMesh->GetName(), *GetName(), SectionIndex, LODBias, LODIndex, UpdatedLODIndex); } } } } void UClothingAssetCommon::UnbindFromSkeletalMesh(USkeletalMesh* InSkelMesh) { if(FSkeletalMeshModel* Mesh = InSkelMesh->GetImportedModel()) { const int32 NumLods = Mesh->LODModels.Num(); for(int32 LodIndex = 0; LodIndex < NumLods; ++LodIndex) { UnbindFromSkeletalMesh(InSkelMesh, LodIndex); } } } void UClothingAssetCommon::UnbindFromSkeletalMesh( USkeletalMesh* InSkelMesh, const int32 InMeshLodIndex) { bool bChangedMesh = false; // Find the chunk(s) we created if(FSkeletalMeshModel* Mesh = InSkelMesh->GetImportedModel()) { if(!Mesh->LODModels.IsValidIndex(InMeshLodIndex)) { FText Error = FText::Format( LOCTEXT("Error_UnbindNoMeshLod", "Failed to remove clothing asset {0} from mesh LOD{1} as that LOD doesn't exist."), FText::FromString(GetName()), InMeshLodIndex); Warn(Error); return; } FSkeletalMeshLODModel& LodModel = Mesh->LODModels[InMeshLodIndex]; for(int32 SectionIdx = LodModel.Sections.Num() - 1; SectionIdx >= 0; --SectionIdx) { FSkelMeshSection& Section = LodModel.Sections[SectionIdx]; if(Section.HasClothingData() && Section.ClothingData.AssetGuid == AssetGuid) { // Don't do this when in async task, this should have been done before initiating the build if (IsInGameThread()) { InSkelMesh->PreEditChange(nullptr); } // Log the LOD bias mapping data that will be removed through the call to ClearSectionClothingData for (int32 LodIndex = 0; LodIndex < InMeshLodIndex; ++LodIndex) { const int32 LODBias = InMeshLodIndex - LodIndex; UE_CLOG(Section.ClothMappingDataLODs.IsValidIndex(LODBias) && Section.ClothMappingDataLODs[LODBias].Num(), LogClothingAsset, Verbose, TEXT("Removed deformer data for [%s/%s], section %d LODBias = %d, render mesh LOD = %d, sim mesh LOD = %d"), *InSkelMesh->GetName(), *GetName(), SectionIdx, LODBias, InMeshLodIndex, LodIndex); } ClothingAssetUtils::ClearSectionClothingData(Section); if (FSkelMeshSourceSectionUserData* UserSectionData = LodModel.UserSectionsData.Find(Section.OriginalDataSectionIndex)) { UserSectionData->CorrespondClothAssetIndex = INDEX_NONE; UserSectionData->ClothingData.AssetLodIndex = INDEX_NONE; UserSectionData->ClothingData.AssetGuid = FGuid(); } // Clear all remaining LOD bias mapping data from other LODs that relied on this section ClearLODBiasMappings(InSkelMesh, InMeshLodIndex, SectionIdx); bChangedMesh = true; } } // Clear the LOD map entry for this asset LOD, after a unbind we must be able to bind any asset if (LodMap.IsValidIndex(InMeshLodIndex)) { LodMap[InMeshLodIndex] = INDEX_NONE; bChangedMesh = true; } } // If the mesh changed we need to re-register any components that use it to reflect the changes if(bChangedMesh) { //Register the scope post edit change FScopedSkeletalMeshPostEditChange SkeletalMeshPostEditChange(InSkelMesh); } } void UClothingAssetCommon::ReregisterComponentsUsingClothing() { if(USkeletalMesh* OwnerMesh = Cast(GetOuter())) { for(TObjectIterator It; It; ++It) { if(USkeletalMeshComponent* Component = *It) { if(Component->GetSkeletalMeshAsset() == OwnerMesh) { FComponentReregisterContext Context(Component); // Context goes out of scope, causing Component to be re-registered } } } } } void UClothingAssetCommon::ForEachInteractorUsingClothing(TFunction Func) { if(USkeletalMesh* OwnerMesh = Cast(GetOuter())) { for(TObjectIterator It; It; ++It) { if(USkeletalMeshComponent* Component = *It) { if(Component->GetSkeletalMeshAsset() == OwnerMesh) { if(UClothingSimulationInteractor* CurInteractor = Component->GetClothingSimulationInteractor()) { Func(CurInteractor); } } } } } } void UClothingAssetCommon::ApplyParameterMasks(bool bUpdateFixedVertData, bool bInvalidateDerivedDataCache) { for(FClothLODDataCommon& Lod : LodData) { Lod.PushWeightsToMesh(); } // Invalidate all cached data that depend on masks InvalidateFlaggedCachedData(EClothingCachedDataFlagsCommon::InverseMasses | EClothingCachedDataFlagsCommon::Tethers); // Recompute weights if needed USkeletalMesh* const SkeletalMesh = Cast(GetOuter()); if (bUpdateFixedVertData && SkeletalMesh) { FSkeletalMeshModel* Resource = SkeletalMesh->GetImportedModel(); FScopedSkeletalMeshPostEditChange ScopedSkeletalMeshPostEditChange(SkeletalMesh); SkeletalMesh->PreEditChange(nullptr); for (FSkeletalMeshLODModel& LodModel : Resource->LODModels) { for (FSkelMeshSection& Section : LodModel.Sections) { if (!Section.HasClothingData() || Cast(SkeletalMesh->GetClothingAsset(Section.ClothingData.AssetGuid)) != this) { continue; } const FClothLODDataCommon& LodDatum = LodData[Section.ClothingData.AssetLodIndex]; const FPointWeightMap* const MaxDistances = LodDatum.PhysicalMeshData.FindWeightMap(EWeightMapTargetCommon::MaxDistance); for (TArray& ClothMappingData : Section.ClothMappingDataLODs) { ClothingMeshUtils::ComputeVertexContributions(ClothMappingData, MaxDistances, LodDatum.bSmoothTransition, LodDatum.bUseMultipleInfluences); } } } if (bInvalidateDerivedDataCache) // We must always dirty the DDC key unless previewing { SkeletalMesh->InvalidateDeriveDataCacheGUID(); } } } void UClothingAssetCommon::BuildLodTransitionData() { const int32 NumLods = GetNumLods(); for(int32 LodIndex = 0; LodIndex < NumLods; ++LodIndex) { const bool bHasPrevLod = LodIndex > 0; const bool bHasNextLod = LodIndex < NumLods - 1; FClothLODDataCommon& CurrentLod = LodData[LodIndex]; const FClothPhysicalMeshData& CurrentPhysMesh = CurrentLod.PhysicalMeshData; FClothLODDataCommon* const PrevLod = bHasPrevLod ? &LodData[LodIndex - 1] : nullptr; FClothLODDataCommon* const NextLod = bHasNextLod ? &LodData[LodIndex + 1] : nullptr; const int32 CurrentLodNumVerts = CurrentPhysMesh.Vertices.Num(); ClothingMeshUtils::ClothMeshDesc CurrentMeshDesc(CurrentPhysMesh.Vertices, CurrentPhysMesh.Normals, CurrentPhysMesh.Indices); const FPointWeightMap* MaxDistances = nullptr; // No need to update the vertex contribution on the transition maps constexpr bool bUseSmoothTransitions = false; // Smooth transitions are only used at rendering for now and not during LOD transitions constexpr bool bUseMultipleInfluences = false; // Multiple influences must not be used for LOD transitions constexpr float SkinningKernelRadius = 0.f; // KernelRadius is only required when using multiple influences if(PrevLod) { FClothPhysicalMeshData& PrevPhysMesh = PrevLod->PhysicalMeshData; CurrentLod.TransitionUpSkinData.Empty(CurrentLodNumVerts); ClothingMeshUtils::ClothMeshDesc PrevMeshDesc(PrevPhysMesh.Vertices, PrevPhysMesh.Indices); // Will calculate averaged normals ClothingMeshUtils::GenerateMeshToMeshVertData(CurrentLod.TransitionUpSkinData, CurrentMeshDesc, PrevMeshDesc, MaxDistances, bUseSmoothTransitions, bUseMultipleInfluences, SkinningKernelRadius); } if(NextLod) { FClothPhysicalMeshData& NextPhysMesh = NextLod->PhysicalMeshData; CurrentLod.TransitionDownSkinData.Empty(CurrentLodNumVerts); ClothingMeshUtils::ClothMeshDesc NextMeshDesc(NextPhysMesh.Vertices, NextPhysMesh.Indices); // Will calculate averaged normals ClothingMeshUtils::GenerateMeshToMeshVertData(CurrentLod.TransitionDownSkinData, CurrentMeshDesc, NextMeshDesc, MaxDistances, bUseSmoothTransitions, bUseMultipleInfluences, SkinningKernelRadius); } } } void UClothingAssetCommon::PreEditUndo() { Super::PreEditUndo(); // Stop the simulation if (const USkeletalMesh* const OwnerMesh = Cast(GetOuter())) { for (TObjectIterator It; It; ++It) { if (USkeletalMeshComponent* const Component = *It) { if (Component->GetSkeletalMeshAsset() == OwnerMesh) { Component->ReleaseAllClothingResources(); } } } } } void UClothingAssetCommon::PostEditUndo() { Super::PostEditUndo(); // Resume the simulation if (const USkeletalMesh* const OwnerMesh = Cast(GetOuter())) { for (TObjectIterator It; It; ++It) { if (USkeletalMeshComponent* const Component = *It) { if (Component->GetSkeletalMeshAsset() == OwnerMesh) { Component->RecreateClothingActors(); } } } } } #endif // WITH_EDITOR void UClothingAssetCommon::RefreshBoneMapping(USkeletalMesh* InSkelMesh) { // No mesh, can't remap if(!InSkelMesh) { return; } if(UsedBoneNames.Num() != UsedBoneIndices.Num()) { UsedBoneIndices.Reset(); UsedBoneIndices.AddDefaulted(UsedBoneNames.Num()); } // Repopulate the used indices. for(int32 BoneNameIndex = 0; BoneNameIndex < UsedBoneNames.Num(); ++BoneNameIndex) { const int32 BoneIndex = InSkelMesh->GetRefSkeleton().FindBoneIndex(UsedBoneNames[BoneNameIndex]); UsedBoneIndices[BoneNameIndex] = (BoneIndex != INDEX_NONE) ? BoneIndex : 0; // Remap to the root bone if the two skeletons differ } } void UClothingAssetCommon::CalculateReferenceBoneIndex() { // Starts at root ReferenceBoneIndex = 0; // Find the root bone for this clothing asset (common bone for all used bones) typedef TArray BoneIndexArray; // List of valid paths to the root bone from each weighted bone TArray PathsToRoot; const USkeletalMesh* OwnerMesh = Cast(GetOuter()); if(OwnerMesh) { // First build a list per used bone for it's path to root TArray WeightedBones; // List of actually weighted (not just used) bones for(FClothLODDataCommon& CurLod : LodData) { const FClothPhysicalMeshData& MeshData = CurLod.PhysicalMeshData; for(const FClothVertBoneData& VertBoneData : MeshData.BoneData) { for(int32 InfluenceIndex = 0; InfluenceIndex < MAX_TOTAL_INFLUENCES; ++InfluenceIndex) { if(VertBoneData.BoneWeights[InfluenceIndex] > SMALL_NUMBER) { const int32 UnmappedBoneIndex = VertBoneData.BoneIndices[InfluenceIndex]; check(UsedBoneIndices.IsValidIndex(UnmappedBoneIndex)); WeightedBones.AddUnique(UsedBoneIndices[UnmappedBoneIndex]); } else { // Hit the last weight (they're sorted) break; } } } } const int32 NumWeightedBones = WeightedBones.Num(); PathsToRoot.Reserve(NumWeightedBones); // Compute paths to the root bone const FReferenceSkeleton& RefSkel = OwnerMesh->GetRefSkeleton(); for(int32 WeightedBoneIndex = 0; WeightedBoneIndex < NumWeightedBones; ++WeightedBoneIndex) { PathsToRoot.AddDefaulted(); BoneIndexArray& Path = PathsToRoot.Last(); int32 CurrentBone = WeightedBones[WeightedBoneIndex]; Path.Add(CurrentBone); while(CurrentBone != 0 && CurrentBone != INDEX_NONE) { CurrentBone = RefSkel.GetParentIndex(CurrentBone); Path.Add(CurrentBone); } } // Paths are from leaf->root, we want the other way for(BoneIndexArray& Path : PathsToRoot) { Algo::Reverse(Path); } // Verify the last common bone in all paths as the root of the sim space const int32 NumPaths = PathsToRoot.Num(); if(NumPaths > 0) { BoneIndexArray& FirstPath = PathsToRoot[0]; const int32 FirstPathSize = FirstPath.Num(); for(int32 PathEntryIndex = 0; PathEntryIndex < FirstPathSize; ++PathEntryIndex) { const int32 CurrentQueryIndex = FirstPath[PathEntryIndex]; bool bValidRoot = true; for(int32 PathIndex = 1; PathIndex < NumPaths; ++PathIndex) { if(!PathsToRoot[PathIndex].Contains(CurrentQueryIndex)) { bValidRoot = false; break; } } if(bValidRoot) { ReferenceBoneIndex = CurrentQueryIndex; } else { // Once we fail to find a valid root we're done. break; } } } else { // Just use root ReferenceBoneIndex = 0; } } } bool UClothingAssetCommon::IsValidLod(int32 InLodIndex) const { return LodData.IsValidIndex(InLodIndex); } int32 UClothingAssetCommon::GetNumLods() const { return LodData.Num(); } void UClothingAssetCommon::PostLoad() { Super::PostLoad(); #if WITH_EDITORONLY_DATA // Migrate the deprecated UObject based lod class to the non-UObject lod structure to prevent PostLoad dependency issues // TODO: Remove all UObject PostLoad dependencies. // Even with these ConditionalPostLoad calls, the UObject PostLoads' order of execution cannot be guaranteed. for (UClothLODDataCommon_Legacy* LodDeprecated : ClothLodData_DEPRECATED) { if (LodDeprecated) { LodDeprecated->ConditionalPostLoad(); const int32 Idx = AddNewLod(); LodDeprecated->MigrateTo(LodData[Idx]); } } ClothLodData_DEPRECATED.Empty(); const int32 AnimPhysCustomVersion = GetLinkerCustomVersion(FAnimPhysObjectVersion::GUID); if (AnimPhysCustomVersion < FAnimPhysObjectVersion::AddedClothingMaskWorkflow) { // Convert current parameters to masks for (FClothLODDataCommon& Lod : LodData) { const FClothPhysicalMeshData& PhysMesh = Lod.PhysicalMeshData; // Didn't do anything previously - clear out in case there's something in there // so we can use it correctly now. Lod.PointWeightMaps.Reset(3); // Max distances const FPointWeightMap* const MaxDistances = PhysMesh.FindWeightMap(EWeightMapTargetCommon::MaxDistance); if (MaxDistances) { Lod.PointWeightMaps.AddDefaulted(); FPointWeightMap& MaxDistanceMask = Lod.PointWeightMaps.Last(); MaxDistanceMask.Initialize(*MaxDistances, EWeightMapTargetCommon::MaxDistance); } // Following params are only added if necessary, if we don't have any backstop // radii then there's no backstops. const FPointWeightMap* const BackstopRadiuses = PhysMesh.FindWeightMap(EWeightMapTargetCommon::BackstopRadius); if (BackstopRadiuses && !BackstopRadiuses->IsZeroed()) { // Backstop radii Lod.PointWeightMaps.AddDefaulted(); FPointWeightMap& BackstopRadiusMask = Lod.PointWeightMaps.Last(); BackstopRadiusMask.Initialize(*BackstopRadiuses, EWeightMapTargetCommon::BackstopRadius); // Backstop distances Lod.PointWeightMaps.AddDefaulted(); FPointWeightMap& BackstopDistanceMask = Lod.PointWeightMaps.Last(); const FPointWeightMap& BackstopDistances = PhysMesh.GetWeightMap(EWeightMapTargetCommon::BackstopDistance); BackstopDistanceMask.Initialize(BackstopDistances, EWeightMapTargetCommon::BackstopDistance); } } // Make sure we're transactional SetFlags(RF_Transactional); } const int32 ClothingCustomVersion = GetLinkerCustomVersion(FClothingAssetCustomVersion::GUID); // Fix content imported before we kept vertex colors if (ClothingCustomVersion < FClothingAssetCustomVersion::AddVertexColorsToPhysicalMesh) { for (FClothLODDataCommon& Lod : LodData) { const int32 NumVerts = Lod.PhysicalMeshData.Vertices.Num(); // number of verts Lod.PhysicalMeshData.VertexColors.Reset(); Lod.PhysicalMeshData.VertexColors.AddUninitialized(NumVerts); for (int32 VertIdx = 0; VertIdx < NumVerts; VertIdx++) { Lod.PhysicalMeshData.VertexColors[VertIdx] = FColor::White; } } } EClothingCachedDataFlagsCommon ClothingCachedDataFlags = EClothingCachedDataFlagsCommon::None; if (AnimPhysCustomVersion < FAnimPhysObjectVersion::CacheClothMeshInfluences) { ClothingCachedDataFlags |= EClothingCachedDataFlagsCommon::NumInfluences; } // Add any missing configs for the available cloth factories, and try to migrate them from any existing one // TODO: Remove all UObject PostLoad dependencies. // Even with these ConditionalPostLoad calls, the UObject PostLoads' order of execution cannot be guaranteed. for (auto& ClothConfig : ClothConfigs) { if (ClothConfig.Value) { ClothConfig.Value->ConditionalPostLoad(); // PostLoad configs before adding new ones } } if (ClothSimConfig_DEPRECATED) { ClothSimConfig_DEPRECATED->ConditionalPostLoad(); // PostLoad old configs before replacing them ClothSimConfig_DEPRECATED->Rename(nullptr, nullptr, REN_DoNotDirty | REN_DontCreateRedirectors | REN_NonTransactional); // Rename the config so that the name doesn't collide with the new config map name } if (ChaosClothSimConfig_DEPRECATED) { ChaosClothSimConfig_DEPRECATED->ConditionalPostLoad(); // PostLoad old configs before replacing them ChaosClothSimConfig_DEPRECATED->Rename(nullptr, nullptr, REN_DoNotDirty | REN_DontCreateRedirectors | REN_NonTransactional); // Rename the config so that the name doesn't collide with the new config map name } if (ClothSharedSimConfig_DEPRECATED) { ClothSharedSimConfig_DEPRECATED->ConditionalPostLoad(); // PostLoad old configs before replacing them ClothSharedSimConfig_DEPRECATED->Rename(nullptr, nullptr, REN_DoNotDirty | REN_DontCreateRedirectors | REN_NonTransactional); // Rename the config so that the name doesn't collide with the new config map name } if (AddClothConfigs()) { // With a new config added best to recache everything ClothingCachedDataFlags |= EClothingCachedDataFlagsCommon::All; } // Migrate configs bool bMigrateSharedConfigToConfig = true; // Shared config to config migration can be disabled to avoid overriding the newly migrated values if (ClothingCustomVersion < FClothingAssetCustomVersion::MovePropertiesToCommonBaseClasses) { // Remap legacy struct FClothConfig to new config objects for (auto& ClothConfig : ClothConfigs) { if (UClothConfigCommon* const ClothConfigCommon = Cast(ClothConfig.Value)) { ClothConfigCommon->ConditionalPostLoad(); ClothConfigCommon->MigrateFrom(ClothConfig_DEPRECATED); } } bMigrateSharedConfigToConfig = false; } else { // Migrate simulation dependent config parameters to the new config map if (ClothSimConfig_DEPRECATED) { // Try a remap to the new config objects through the legacy structure if (const UClothConfigCommon* const ClothSimConfigCommon = Cast(ClothSimConfig_DEPRECATED)) { FClothConfig_Legacy ClothConfigLegacy; if (ClothSimConfigCommon->MigrateTo(ClothConfigLegacy)) { for (auto& ClothConfig : ClothConfigs) { if (UClothConfigCommon* const ClothConfigCommon = Cast(ClothConfig.Value)) { ClothConfigCommon->ConditionalPostLoad(); ClothConfigCommon->MigrateFrom(ClothConfigLegacy); } } } } // And keep the old config too SetClothConfig(ToRawPtr(ClothSimConfig_DEPRECATED)); ClothSimConfig_DEPRECATED = nullptr; bMigrateSharedConfigToConfig = false; } if (ChaosClothSimConfig_DEPRECATED) { SetClothConfig(ToRawPtr(ChaosClothSimConfig_DEPRECATED)); ChaosClothSimConfig_DEPRECATED = nullptr; bMigrateSharedConfigToConfig = false; } if (ClothSharedSimConfig_DEPRECATED) { SetClothConfig(ToRawPtr(ClothSharedSimConfig_DEPRECATED)); ClothSharedSimConfig_DEPRECATED = nullptr; bMigrateSharedConfigToConfig = false; } } // Propagate shared configs between cloth assets PropagateSharedConfigs(bMigrateSharedConfigToConfig); // Cache tethers and reference bone index should already have been calculated const int32 FortniteReleaseBranchCustomObjectVersion = GetLinkerCustomVersion(FFortniteReleaseBranchCustomObjectVersion::GUID); const int32 SpecialProjectBranchObjectVersion = GetLinkerCustomVersion(FUE5SpecialProjectStreamObjectVersion::GUID); if (SpecialProjectBranchObjectVersion < FUE5SpecialProjectStreamObjectVersion::ChaosClothRemoveKinematicTethers) { ClothingCachedDataFlags |= EClothingCachedDataFlagsCommon::Tethers; } if (FortniteReleaseBranchCustomObjectVersion < FFortniteReleaseBranchCustomObjectVersion::ChaosClothAddTethersToCachedData && SpecialProjectBranchObjectVersion < FUE5SpecialProjectStreamObjectVersion::ChaosClothAddTethersToCachedData) { ClothingCachedDataFlags |= EClothingCachedDataFlagsCommon::Tethers; // ReferenceBoneIndex is only required when rebinding the cloth. USkeletalMesh* const OwnerMesh = CastChecked(GetOuter()); RefreshBoneMapping(OwnerMesh); CalculateReferenceBoneIndex(); } // After fixing the content, we are ready to call functions that rely on it if (ClothingCachedDataFlags != EClothingCachedDataFlagsCommon::None) { // Rebuild data cache InvalidateFlaggedCachedData(ClothingCachedDataFlags); } const int32 PhysicsObjectVersion = GetLinkerCustomVersion(FPhysicsObjectVersion::GUID); const int32 FortniteMainBranchObjectVersion = GetLinkerCustomVersion(FFortniteMainBranchObjectVersion::GUID); if (PhysicsObjectVersion < FPhysicsObjectVersion::ChaosClothFixLODTransitionMaps || FortniteMainBranchObjectVersion < FFortniteMainBranchObjectVersion::ChaosClothFixLODTransitionMaps) { BuildLodTransitionData(); } #endif // #if WITH_EDITORONLY_DATA } #if WITH_EDITORONLY_DATA void UClothingAssetCommon::DeclareConstructClasses(TArray& OutConstructClasses, const UClass* SpecificSubclass) { Super::DeclareConstructClasses(OutConstructClasses, SpecificSubclass); const TArray ClassProviders = IModularFeatures::Get().GetModularFeatureImplementations(IClothingSimulationFactoryClassProvider::FeatureName); for (IClothingSimulationFactoryClassProvider* Provider : ClassProviders) { check(Provider); if (UClass* const ClothingSimulationFactoryClass = *TSubclassOf(Provider->GetClothingSimulationFactoryClass())) { const UClothingSimulationFactory* const ClothingSimulationFactory = ClothingSimulationFactoryClass->GetDefaultObject(); for (TSubclassOf ClothConfigClass : ClothingSimulationFactory->GetClothConfigClasses()) { OutConstructClasses.Add(FTopLevelAssetPath(ClothConfigClass)); } } } // OutConstructClasses.Add(FTopLevelAssetPath(UChaosClothSharedSimConfig::StaticClass())); //OutConstructClasses.Add(FTopLevelAssetPath(UChaosClothConfig::StaticClass())); } #endif void UClothingAssetCommon::Serialize(FArchive& Ar) { Super::Serialize(Ar); Ar.UsingCustomVersion(FAnimPhysObjectVersion::GUID); Ar.UsingCustomVersion(FClothingAssetCustomVersion::GUID); Ar.UsingCustomVersion(FFortniteMainBranchObjectVersion::GUID); Ar.UsingCustomVersion(FPhysicsObjectVersion::GUID); Ar.UsingCustomVersion(FUE5SpecialProjectStreamObjectVersion::GUID); } bool UClothingAssetCommon::AddClothConfigs() { bool bNewConfigAdded = false; const TArray ClassProviders = IModularFeatures::Get().GetModularFeatureImplementations(IClothingSimulationFactoryClassProvider::FeatureName); for (IClothingSimulationFactoryClassProvider* Provider : ClassProviders) { check(Provider); if (UClass* const ClothingSimulationFactoryClass = *TSubclassOf(Provider->GetClothingSimulationFactoryClass())) { const UClothingSimulationFactory* const ClothingSimulationFactory = ClothingSimulationFactoryClass->GetDefaultObject(); for (TSubclassOf ClothConfigClass : ClothingSimulationFactory->GetClothConfigClasses()) { const FName ClothConfigName = ClothConfigClass->GetFName(); TObjectPtr* const ClothConfigPtr = ClothConfigs.Find(ClothConfigName); if (!ClothConfigPtr || !*ClothConfigPtr) { // Create new config object checkf(!StaticFindObject(ClothConfigClass, this, *ClothConfigClass->GetName(), true), TEXT("Found an existing instance of ClothConfigClass in %s. Class:%s"), *GetPathNameSafe(this), *ClothConfigClass->GetName()); UClothConfigBase* const ClothConfig = NewObject(this, ClothConfigClass, ClothConfigClass->GetFName(), RF_Transactional); // Use the legacy config struct to try find a common config as an acceptable migration source // This code could be removed once the legacy code is removed, although this will then prevent // migration from compatible config sources if (UClothConfigCommon* const ClothConfigCommon = Cast(ClothConfig)) { for (auto ClothConfigPair : ClothConfigs) { if (const UClothConfigCommon* SourceConfig = Cast(ClothConfigPair.Value)) { FClothConfig_Legacy LegacyConfig; if (SourceConfig->MigrateTo(LegacyConfig)) { ClothConfigCommon->MigrateFrom(LegacyConfig); break; } } } } // Set the new config check(ClothConfig); if (ClothConfigPtr) { *ClothConfigPtr = ClothConfig; } else { ClothConfigs.Emplace(ClothConfigName, ClothConfig); } bNewConfigAdded = true; } } } } return bNewConfigAdded; } void UClothingAssetCommon::PropagateSharedConfigs(bool bMigrateSharedConfigToConfig) { // Update this asset's shared config when the asset belongs to a skeletal mesh if (USkeletalMesh* const SkeletalMesh = Cast(GetOuter())) { const TArray& ClothingAssets = SkeletalMesh->GetMeshClothingAssets(); // Collect all shared configs found in the other assets decltype(ClothConfigs) ClothSharedConfigs; for (const UClothingAssetBase* ClothingAssetBase : ClothingAssets) { if (ClothingAssetBase == static_cast(this)) { continue; } // Only common assets have shared configs if (const UClothingAssetCommon* const ClothingAsset = Cast(ClothingAssetBase)) { // Reserve space in the map, use the total number of configs in case they're unlikely all shared configs const int32 Max = ClothSharedConfigs.Num() + ClothingAsset->ClothConfigs.Num(); ClothSharedConfigs.Reserve(Max); // Iterate through all configs, and find the shared ones for (const auto& ClothSharedConfigItem : ClothingAsset->ClothConfigs) { if (Cast(ClothSharedConfigItem.Value) && // Only needs shared configs !ClothSharedConfigs.Find(ClothSharedConfigItem.Key)) // Only needs a single shared config per type { ClothSharedConfigs.Add(ClothSharedConfigItem); } } } } // Propagate the found shared configs to this asset for (const auto& ClothSharedConfigItem : ClothSharedConfigs) { // Set share config if (TObjectPtr* const ClothConfigBase = ClothConfigs.Find(ClothSharedConfigItem.Key)) { // Reset this shared config *ClothConfigBase = ClothSharedConfigItem.Value; } else { // Add new map entry ClothConfigs.Add(ClothSharedConfigItem); } } // Migrate the common shared configs' deprecated parameters to all per cloth configs, and fix the shared config ownership for (TPair>& ClothSharedConfigItem : ClothConfigs) { if (UClothSharedConfigCommon* const ClothSharedConfig = Cast(ClothSharedConfigItem.Value)) { // Migrate from this shared config to non shared configs if needed if (bMigrateSharedConfigToConfig) { for (const TPair>& ClothConfigItem : ClothConfigs) { if (Cast(ClothConfigItem.Value)) { continue; // Don't migrate shared configs to another shared configs (or itself) } if (UClothConfigCommon* const ClothConfig = Cast(ClothConfigItem.Value)) { ClothConfig->MigrateFrom(ClothSharedConfig); } } } // Fix the shared config outer if it is still a UClothingAssetCommon (the config must belong to the skeletal mesh, as it is shared between assets) if (Cast(ClothSharedConfig->GetOuter())) { ClothSharedConfig->Rename(nullptr, SkeletalMesh, REN_DontCreateRedirectors | REN_NonTransactional); } // Fix the shared config ownership, asset might have been copied and the shared config could still point to a different skeletal mesh if (ClothSharedConfig->GetOuter() != SkeletalMesh) { ClothSharedConfigItem.Value = DuplicateObject(ClothSharedConfig, SkeletalMesh, ClothSharedConfig->GetFName()); } } } } } void UClothingAssetCommon::PostUpdateAllAssets() { // Add any missing configs for the available cloth factories, and try to migrate them from any existing one const bool bInvalidateCachedData = AddClothConfigs(); // Propagate shared configs PropagateSharedConfigs(); #if WITH_EDITORONLY_DATA // Invalidate cached data if the configs have changed if (bInvalidateCachedData) { InvalidateAllCachedData(); } #endif } #if WITH_EDITORONLY_DATA void UClothingAssetCommon::InvalidateFlaggedCachedData(EClothingCachedDataFlagsCommon Flags) { const bool bNeedsInverseMasses = EnumHasAllFlags((EClothingCachedDataFlagsCommon)Flags, EClothingCachedDataFlagsCommon::InverseMasses) && AnyOfClothConfigs([](UClothConfigBase& Config) { return Config.NeedsInverseMasses(); }); const bool bNeedsNumInfluences = EnumHasAllFlags((EClothingCachedDataFlagsCommon)Flags, EClothingCachedDataFlagsCommon::NumInfluences) && AnyOfClothConfigs([](UClothConfigBase& Config) { return Config.NeedsNumInfluences(); }); const bool bNeedsSelfCollisionData = EnumHasAllFlags((EClothingCachedDataFlagsCommon)Flags, EClothingCachedDataFlagsCommon::SelfCollisionData) && AnyOfClothConfigs([](UClothConfigBase& Config) { return Config.NeedsSelfCollisionData(); }); const bool bNeedsTethers = EnumHasAllFlags((EClothingCachedDataFlagsCommon)Flags, EClothingCachedDataFlagsCommon::Tethers) && AnyOfClothConfigs([](UClothConfigBase& Config) { return Config.NeedsTethers(); }); float SelfCollisionRadius = 0.f; if (bNeedsSelfCollisionData) { // Note: Only PhysX based NvCloth needs to build the SelfCollisionIndices at the moment for (const TPair>& ClothConfig : ClothConfigs) { SelfCollisionRadius = FMath::Max(SelfCollisionRadius, ClothConfig.Value->GetSelfCollisionRadius()); } } bool bThethersUseEuclideanDistance = false; bool bThethersUseGeodesicDistance = false; if (bNeedsTethers) { for (const TPair>& ClothConfig : ClothConfigs) { if (ClothConfig.Value->NeedsTethers()) { if (ClothConfig.Value->TethersUseGeodesicDistance()) { bThethersUseGeodesicDistance = true; } else { bThethersUseEuclideanDistance = true; } } } } // Recalculate cached data bool bHasClothChanged = false; for (FClothLODDataCommon& CurrentLodData : LodData) { FClothPhysicalMeshData& PhysMesh = CurrentLodData.PhysicalMeshData; if (bNeedsInverseMasses) { PhysMesh.CalculateInverseMasses(); bHasClothChanged = true; } if (bNeedsNumInfluences) { PhysMesh.CalculateNumInfluences(); bHasClothChanged = true; } if (bNeedsSelfCollisionData) { PhysMesh.BuildSelfCollisionData(SelfCollisionRadius); bHasClothChanged = true; } if (bNeedsTethers) { PhysMesh.CalculateTethers(bThethersUseEuclideanDistance, bThethersUseGeodesicDistance); bHasClothChanged = true; } } // Inform the simulations that the cloth has changed if (bHasClothChanged) { ForEachInteractorUsingClothing([](UClothingSimulationInteractor* InInteractor) { if (InInteractor) { InInteractor->ClothConfigUpdated(); } }); } } #endif // #if WITH_EDITORONLY_DATA #if WITH_EDITOR int32 UClothingAssetCommon::AddNewLod() { return LodData.AddDefaulted(); } void UClothingAssetCommon::PostEditChangeChainProperty(FPropertyChangedChainEvent& ChainEvent) { Super::PostEditChangeChainProperty(ChainEvent); bool bReregisterComponents = false; if (ChainEvent.ChangeType != EPropertyChangeType::Interactive) { const FName& PropertyName = ChainEvent.PropertyChain.GetActiveMemberNode() && ChainEvent.PropertyChain.GetActiveMemberNode()->GetNextNode() ? ChainEvent.PropertyChain.GetActiveMemberNode()->GetNextNode()->GetValue()->GetFName() : NAME_None; if (PropertyName == FName("bUseSelfCollisionSpheres") || PropertyName == FName("SelfCollisionSphereRadius") || PropertyName == FName("SelfCollisionSphereRadiusCullMultiplier")) { InvalidateFlaggedCachedData(EClothingCachedDataFlagsCommon::SelfCollisionData); bReregisterComponents = true; } else if (PropertyName == FName("bUseGeodesicDistance")) { InvalidateFlaggedCachedData(EClothingCachedDataFlagsCommon::Tethers); bReregisterComponents = true; } else if(ChainEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(UClothingAssetCommon, PhysicsAsset)) { bReregisterComponents = true; } else { // Other properties just require a config refresh ForEachInteractorUsingClothing([](UClothingSimulationInteractor* InInteractor) { if (InInteractor) { InInteractor->ClothConfigUpdated(); } }); } } if (bReregisterComponents) { ReregisterComponentsUsingClothing(); } } #endif // WITH_EDITOR #undef LOCTEXT_NAMESPACE