// Copyright Epic Games, Inc. All Rights Reserved. #include "ChaosClothAsset/ClothDataflowTools.h" #include "ChaosClothAsset/ClothGeometryTools.h" #include "ChaosClothAsset/CollectionClothFacade.h" #include "Dataflow/DataflowNode.h" #include "DynamicMesh/NonManifoldMappingSupport.h" #include "Framework/Notifications/NotificationManager.h" #include "Interfaces/ITargetPlatformManagerModule.h" #include "Modules/ModuleManager.h" #include "Rendering/SkeletalMeshLODModel.h" #include "Widgets/Notifications/SNotificationList.h" #include "MeshDescriptionToDynamicMesh.h" #include "MeshUtilities.h" #include "PropertyHandle.h" #include "ReferenceSkeleton.h" #include "SkeletalMeshAttributes.h" #include "ToDynamicMesh.h" DEFINE_LOG_CATEGORY(LogChaosClothAssetDataflowNodes); namespace UE::Chaos::ClothAsset { namespace Private { // // Wrapper for accessing a SkelMeshSection. Implements the interface expected by TToDynamicMesh<>. // This will weld all vertices which are the same. // template struct FSkelMeshSectionWrapper { typedef int32 TriIDType; typedef int32 VertIDType; typedef int32 WedgeIDType; typedef int32 UVIDType; typedef int32 NormalIDType; typedef int32 ColorIDType; FSkelMeshSectionWrapper(const FSkeletalMeshLODModel& SkeletalMeshModel, const int32 SectionIndex, bool bInHasNormals) : bHasNormals(bInHasNormals) , SourceSection(SkeletalMeshModel.Sections[SectionIndex]) , IndexBuffer(SkeletalMeshModel.IndexBuffer.GetData() + SourceSection.BaseIndex, SourceSection.NumTriangles * 3) { const int32 NumVerts = SourceSection.SoftVertices.Num(); const int32 NumTriangles = SourceSection.NumTriangles; // We need to weld the mesh verts to get rid of duplicates (happens for smoothing groups) TArray UniqueVerts; OriginalToMerged.AddDefaulted(NumVerts); constexpr float ThreshSq = UE_THRESH_POINTS_ARE_SAME * UE_THRESH_POINTS_ARE_SAME; for (int32 VertIndex = 0; VertIndex < NumVerts; ++VertIndex) { const FSoftSkinVertex& SourceVert = SourceSection.SoftVertices[VertIndex]; bool bUnique = true; int32 RemapIndex = INDEX_NONE; const int32 NumUniqueVerts = UniqueVerts.Num(); for (int32 UniqueVertIndex = 0; UniqueVertIndex < NumUniqueVerts; ++UniqueVertIndex) { FVector& UniqueVert = UniqueVerts[UniqueVertIndex]; if ((UniqueVert - (FVector)SourceVert.Position).SizeSquared() <= ThreshSq) { // Not unique bUnique = false; RemapIndex = UniqueVertIndex; break; } } if (bUnique) { // Unique UniqueVerts.Add((FVector)SourceVert.Position); OriginalIndexes.Add(VertIndex); OriginalToMerged[VertIndex] = VertIndex; } else { OriginalToMerged[VertIndex] = OriginalIndexes[RemapIndex]; } } TriIDs.SetNum(NumTriangles); for (int32 Tri = 0; Tri < NumTriangles; ++Tri) { TriIDs[Tri] = Tri; } } int32 NumTris() const { return TriIDs.Num(); } int32 NumVerts() const { return OriginalIndexes.Num(); } int32 NumUVLayers() const { return MAX_TEXCOORDS; } // --"Vertex Buffer" info const TArray& GetVertIDs() const { return OriginalIndexes; } const FVector3d GetPosition(const VertIDType VtxID) const { return (FVector3d)SourceSection.SoftVertices[VtxID].Position; } // --"Index Buffer" info const TArray& GetTriIDs() const { return TriIDs; } // return false if this TriID is not contained in mesh. bool GetTri(const TriIDType TriID, VertIDType& VID0, VertIDType& VID1, VertIDType& VID2) const { if (TriID >= 0 && TriID < (int32)SourceSection.NumTriangles) { VID0 = OriginalToMerged[IndexBuffer[3 * TriID + 0] - SourceSection.BaseVertexIndex]; VID1 = OriginalToMerged[IndexBuffer[3 * TriID + 1] - SourceSection.BaseVertexIndex]; VID2 = OriginalToMerged[IndexBuffer[3 * TriID + 2] - SourceSection.BaseVertexIndex]; return true; } return false; } bool HasNormals() const { return bHasNormals; } bool HasTangents() const { return bHasTangents; } bool HasBiTangents() const { return bHasBiTangents; } bool HasColors() const { return bHasColors; } // Each triangle corner is a wedge. This will lookup into original unwelded soft verts void GetWedgeIDs(const TriIDType& TriID, WedgeIDType& WID0, WedgeIDType& WID1, WedgeIDType& WID2) const { WID0 = IndexBuffer[3 * TriID + 0] - SourceSection.BaseVertexIndex; WID1 = IndexBuffer[3 * TriID + 1] - SourceSection.BaseVertexIndex; WID2 = IndexBuffer[3 * TriID + 2] - SourceSection.BaseVertexIndex; } // attribute access per-wedge // NB: ToDynamicMesh will attempt to weld identical attributes that are associated with the same vertex FVector2f GetWedgeUV(int32 UVLayerIndex, WedgeIDType WID) const { check(UVLayerIndex < MAX_TEXCOORDS); return SourceSection.SoftVertices[WID].UVs[UVLayerIndex]; } FVector3f GetWedgeNormal(WedgeIDType WID) const { return SourceSection.SoftVertices[WID].TangentZ; } FVector3f GetWedgeTangent(WedgeIDType WID) const { return SourceSection.SoftVertices[WID].TangentX; } FVector3f GetWedgeBiTangent(WedgeIDType WID) const { return SourceSection.SoftVertices[WID].TangentY; } FVector4f GetWedgeColor(WedgeIDType WID) const { return FLinearColor(SourceSection.SoftVertices[WID].Color); } // attribute access that exploits shared attributes. // each group of shared attributes presents itself as a mesh with its own attribute vertex buffer. // NB: If the mesh has no shared Attr attributes, then Get{Attr}IDs() should return an empty array. // NB: Get{Attr}Tri() functions should return false if the triangle is not set in the attribute mesh. const TArray& GetUVIDs(int32 LayerID) const { return EmptyArray; } FVector2f GetUV(int32 LayerID, UVIDType UVID) const { check(false); return FVector2f(); } bool GetUVTri(int32 LayerID, const TriIDType& TID, UVIDType& ID0, UVIDType& ID1, UVIDType& ID2) const { return false; } const TArray& GetNormalIDs() const { if (bHasNormals) { return OriginalIndexes; } else { return EmptyArray; } } FVector3f GetNormal(NormalIDType ID) const { check(bHasNormals); return SourceSection.SoftVertices[ID].TangentZ; } bool GetNormalTri(const TriIDType& TriID, NormalIDType& NID0, NormalIDType& NID1, NormalIDType& NID2) const { if (bHasNormals) { return GetTri(TriID, NID0, NID1, NID2); } return false; } const TArray& GetTangentIDs() const { return EmptyArray; } FVector3f GetTangent(NormalIDType ID) const { check(false); return FVector3f(); } bool GetTangentTri(const TriIDType& TID, NormalIDType& NID0, NormalIDType& NID1, NormalIDType& NID2) const { return false; } const TArray& GetBiTangentIDs() const { return EmptyArray; } FVector3f GetBiTangent(NormalIDType ID) const { check(false); return FVector3f(); } bool GetBiTangentTri(const TriIDType& TID, NormalIDType& NID0, NormalIDType& NID1, NormalIDType& NID2) const { return false; } const TArray& GetColorIDs() const { return EmptyArray; } FVector4f GetColor(ColorIDType ID) const { check(false); return FVector4f(); } bool GetColorTri(const TriIDType& TID, ColorIDType& NID0, ColorIDType& NID1, ColorIDType& NID2) const { return false; } // weight maps information int32 NumWeightMapLayers() const { return 0; } float GetVertexWeight(int32 WeightMapIndex, int32 SrcVertID) const { check(false); return 0.f; } FName GetWeightMapName(int32 WeightMapIndex) const { check(false); return FName(); } // skin weight attributes information int32 NumSkinWeightAttributes() const { return 1; } UE::AnimationCore::FBoneWeights GetVertexSkinWeight(int32 SkinWeightAttributeIndex, VertIDType VtxID) const { using namespace UE::AnimationCore; check(SkinWeightAttributeIndex == 0); const int32 NumInfluences = SourceSection.MaxBoneInfluences; const FSoftSkinVertex& SoftVertex = SourceSection.SoftVertices[VtxID]; TArray BoneWeightArray; BoneWeightArray.SetNumUninitialized(NumInfluences); for (int32 Idx = 0; Idx < NumInfluences; ++Idx) { BoneWeightArray[Idx] = FBoneWeight(SourceSection.BoneMap[SoftVertex.InfluenceBones[Idx]], (float)SoftVertex.InfluenceWeights[Idx] * UE::AnimationCore::InvMaxRawBoneWeightFloat); } return FBoneWeights::Create(BoneWeightArray, FBoneWeightsSettings()); } FName GetSkinWeightAttributeName(int32 SkinWeightAttributeIndex) const { checkfSlow(SkinWeightAttributeIndex == 0, TEXT("Cloth assets should only have one skin weight profile")); return FSkeletalMeshAttributes::DefaultSkinWeightProfileName; } // bone attributes information int32 GetNumBones() const { return 0; } FName GetBoneName(int32 BoneIdx) const { check(false); return FName(); } int32 GetBoneParentIndex(int32 BoneIdx) const { check(false); return INDEX_NONE; } FTransform GetBonePose(int32 BoneIdx) const { check(false); return FTransform(); } FVector4f GetBoneColor(int32 BoneIdx) const { check(false); return FLinearColor::White; } const bool bHasNormals; const FSkelMeshSection& SourceSection; const TConstArrayView IndexBuffer; TArray OriginalIndexes; // UniqueIndex -> OrigIndex TArray OriginalToMerged; // OriginalIndex -> OriginalIndexes[UniqueVertIndex] TArray TriIDs; TArray EmptyArray; }; } // Private void FClothDataflowTools::AddRenderPatternFromSkeletalMeshSection(const TSharedRef& ClothCollection, const FSkeletalMeshLODModel& SkeletalMeshModel, const int32 SectionIndex, const FString& RenderMaterialPathName) { check(SectionIndex < SkeletalMeshModel.Sections.Num()); FCollectionClothFacade Cloth(ClothCollection); check(Cloth.IsValid()); FCollectionClothRenderPatternFacade ClothPatternFacade = Cloth.AddGetRenderPattern(); const FSkelMeshSection& Section = SkeletalMeshModel.Sections[SectionIndex]; ClothPatternFacade.SetNumRenderVertices(Section.NumVertices); ClothPatternFacade.SetNumRenderFaces(Section.NumTriangles); TArrayView RenderPosition = ClothPatternFacade.GetRenderPosition(); TArrayView RenderNormal = ClothPatternFacade.GetRenderNormal(); TArrayView RenderTangentU = ClothPatternFacade.GetRenderTangentU(); TArrayView RenderTangentV = ClothPatternFacade.GetRenderTangentV(); TArrayView> RenderUVs = ClothPatternFacade.GetRenderUVs(); TArrayView RenderColor = ClothPatternFacade.GetRenderColor(); TArrayView> RenderBoneIndices = ClothPatternFacade.GetRenderBoneIndices(); TArrayView> RenderBoneWeights = ClothPatternFacade.GetRenderBoneWeights(); const uint32 NumTexCoords = FMath::Min((uint32)MAX_TEXCOORDS, SkeletalMeshModel.NumTexCoords); for (int32 VertexIndex = 0; VertexIndex < Section.NumVertices; ++VertexIndex) { const FSoftSkinVertex& SoftVertex = Section.SoftVertices[VertexIndex]; RenderPosition[VertexIndex] = SoftVertex.Position; RenderNormal[VertexIndex] = SoftVertex.TangentZ; RenderTangentU[VertexIndex] = SoftVertex.TangentX; RenderTangentV[VertexIndex] = SoftVertex.TangentY; RenderUVs[VertexIndex].SetNum(NumTexCoords); for (uint32 TexCoordIndex = 0; TexCoordIndex < NumTexCoords; ++TexCoordIndex) { RenderUVs[VertexIndex][TexCoordIndex] = SoftVertex.UVs[TexCoordIndex]; } RenderColor[VertexIndex] = FLinearColor(SoftVertex.Color); const int32 NumBones = Section.MaxBoneInfluences; RenderBoneIndices[VertexIndex].SetNum(NumBones); RenderBoneWeights[VertexIndex].SetNum(NumBones); for (int32 BoneIndex = 0; BoneIndex < NumBones; ++BoneIndex) { RenderBoneIndices[VertexIndex][BoneIndex] = (int32)Section.BoneMap[(int32)SoftVertex.InfluenceBones[BoneIndex]]; RenderBoneWeights[VertexIndex][BoneIndex] = (float)SoftVertex.InfluenceWeights[BoneIndex] * UE::AnimationCore::InvMaxRawBoneWeightFloat; } } const int32 VertexOffset = ClothPatternFacade.GetRenderVerticesOffset(); TArrayView RenderIndices = ClothPatternFacade.GetRenderIndices(); for (uint32 FaceIndex = 0; FaceIndex < Section.NumTriangles; ++FaceIndex) { const uint32 IndexOffset = Section.BaseIndex + FaceIndex * 3; RenderIndices[FaceIndex] = FIntVector3( SkeletalMeshModel.IndexBuffer[IndexOffset + 0] - Section.BaseVertexIndex + VertexOffset, SkeletalMeshModel.IndexBuffer[IndexOffset + 1] - Section.BaseVertexIndex + VertexOffset, SkeletalMeshModel.IndexBuffer[IndexOffset + 2] - Section.BaseVertexIndex + VertexOffset ); } ClothPatternFacade.SetRenderMaterialPathName(RenderMaterialPathName); } void FClothDataflowTools::AddSimPatternsFromSkeletalMeshSection(const TSharedRef& ClothCollection, const FSkeletalMeshLODModel& SkeletalMeshModel, const int32 SectionIndex, const int32 UVChannelIndex, const FVector2f& UVScale, bool bImportNormals, TArray* OutSim2DToSourceVertex) { check(SectionIndex < SkeletalMeshModel.Sections.Num()); // Convert to DynamicMesh and then use that to create patterns. UE::Geometry::TToDynamicMesh> SkelMeshSectionToDynamicMesh; Private::FSkelMeshSectionWrapper<> SectionWrapper(SkeletalMeshModel, SectionIndex, bImportNormals); UE::Geometry::FDynamicMesh3 DynamicMesh; DynamicMesh.EnableAttributes(); constexpr bool bCopyTangents = false; SkelMeshSectionToDynamicMesh.Convert(DynamicMesh, SectionWrapper, [](int32) { return 0; }, [](int32) { return INDEX_NONE; }, bCopyTangents); // Set ToSrcVertIDMap as an overlay that the build sim mesh code expects. UE::Geometry::FNonManifoldMappingSupport::AttachNonManifoldVertexMappingData(SkelMeshSectionToDynamicMesh.ToSrcVertIDMap, DynamicMesh); constexpr bool bAppend = true; const int32 OrigNumSimVertices2D = FCollectionClothConstFacade(ClothCollection).GetNumSimVertices2D(); check(!OutSim2DToSourceVertex || OutSim2DToSourceVertex->Num() == OrigNumSimVertices2D); FClothGeometryTools::BuildSimMeshFromDynamicMesh(ClothCollection, DynamicMesh, UVChannelIndex, UVScale, bAppend, bImportNormals, OutSim2DToSourceVertex); if (OutSim2DToSourceVertex) { // SrcVertID doesn't include SourceSection.BaseVertexIndex. for (int32 Index = OrigNumSimVertices2D; Index < OutSim2DToSourceVertex->Num(); ++Index) { (*OutSim2DToSourceVertex)[Index] += SkeletalMeshModel.Sections[SectionIndex].BaseVertexIndex; } } } void FClothDataflowTools::LogAndToastWarning(const FDataflowNode& DataflowNode, const FText& Headline, const FText& Details) { static const FTextFormat TextFormat = FTextFormat::FromString(TEXT("{0}: {1}\n{2}")); const FText NodeName = FText::FromName(DataflowNode.GetName()); const FText Text = FText::Format(TextFormat, NodeName, Headline, Details); FNotificationInfo NotificationInfo(Text); NotificationInfo.ExpireDuration = 5.0f; FSlateNotificationManager::Get().AddNotification(NotificationInfo); UE_LOG(LogChaosClothAssetDataflowNodes, Warning, TEXT("%s"), *Text.ToString()); } bool FClothDataflowTools::MakeCollectionName(FString& InOutString) { const FString SourceString = InOutString; InOutString = SlugStringForValidName(InOutString, TEXT("_")).Replace(TEXT("\\"), TEXT("_")); bool bCharsWereRemoved; do { InOutString.TrimCharInline(TEXT('_'), &bCharsWereRemoved); } while (bCharsWereRemoved); return InOutString.Equals(SourceString); } static void CopyBuildSettings(const FMeshBuildSettings& InStaticMeshBuildSettings, FSkeletalMeshBuildSettings& OutSkeletalMeshBuildSettings) { OutSkeletalMeshBuildSettings.bRecomputeNormals = InStaticMeshBuildSettings.bRecomputeNormals; OutSkeletalMeshBuildSettings.bRecomputeTangents = InStaticMeshBuildSettings.bRecomputeTangents; OutSkeletalMeshBuildSettings.bUseMikkTSpace = InStaticMeshBuildSettings.bUseMikkTSpace; OutSkeletalMeshBuildSettings.bComputeWeightedNormals = InStaticMeshBuildSettings.bComputeWeightedNormals; OutSkeletalMeshBuildSettings.bRemoveDegenerates = InStaticMeshBuildSettings.bRemoveDegenerates; OutSkeletalMeshBuildSettings.bUseHighPrecisionTangentBasis = InStaticMeshBuildSettings.bUseHighPrecisionTangentBasis; OutSkeletalMeshBuildSettings.bUseFullPrecisionUVs = InStaticMeshBuildSettings.bUseFullPrecisionUVs; OutSkeletalMeshBuildSettings.bUseBackwardsCompatibleF16TruncUVs = InStaticMeshBuildSettings.bUseBackwardsCompatibleF16TruncUVs; // The rest we leave at defaults. } bool FClothDataflowTools::BuildSkeletalMeshModelFromMeshDescription(const FMeshDescription* const InMeshDescription, const FMeshBuildSettings& InBuildSettings, FSkeletalMeshLODModel& SkeletalMeshModel) { // This is following StaticToSkeletalMeshConverter.cpp::AddLODFromStaticMeshSourceModel FSkeletalMeshBuildSettings BuildSettings; CopyBuildSettings(InBuildSettings, BuildSettings); FMeshDescription SkeletalMeshGeometry = *InMeshDescription; FSkeletalMeshAttributes SkeletalMeshAttributes(SkeletalMeshGeometry); SkeletalMeshAttributes.Register(); // Full binding to the root bone. constexpr int32 RootBoneIndex = 0; FSkinWeightsVertexAttributesRef SkinWeights = SkeletalMeshAttributes.GetVertexSkinWeights(); UE::AnimationCore::FBoneWeight RootInfluence(RootBoneIndex, 1.0f); UE::AnimationCore::FBoneWeights RootBinding = UE::AnimationCore::FBoneWeights::Create({ RootInfluence }); for (const FVertexID VertexID : SkeletalMeshGeometry.Vertices().GetElementIDs()) { SkinWeights.Set(VertexID, RootBinding); } FSkeletalMeshImportData SkeletalMeshImportGeometry = FSkeletalMeshImportData::CreateFromMeshDescription(SkeletalMeshGeometry); // Data needed by BuildSkeletalMesh TArray LODPoints; TArray LODWedges; TArray LODFaces; TArray LODInfluences; TArray LODPointToRawMap; SkeletalMeshImportGeometry.CopyLODImportData(LODPoints, LODWedges, LODFaces, LODInfluences, LODPointToRawMap); IMeshUtilities::MeshBuildOptions BuildOptions; BuildOptions.TargetPlatform = GetTargetPlatformManagerRef().GetRunningTargetPlatform(); BuildOptions.FillOptions(BuildSettings); SkeletalMeshModel.NumTexCoords = SkeletalMeshImportGeometry.NumTexCoords; static const FString SkeletalMeshName("ClothAssetStaticMeshImportConvert"); // This is only used by warning messages in the mesh builder. // Build a RefSkeleton with just a root bone. The BuildSkeletalMesh code expects you have a reference skeleton with at least one bone to work. FReferenceSkeleton RootBoneRefSkeleton; FReferenceSkeletonModifier SkeletonModifier(RootBoneRefSkeleton, nullptr); FMeshBoneInfo RootBoneInfo; RootBoneInfo.Name = FName("Root"); SkeletonModifier.Add(RootBoneInfo, FTransform()); RootBoneRefSkeleton.RebuildRefSkeleton(nullptr, true); IMeshUtilities& MeshUtilities = FModuleManager::Get().LoadModuleChecked("MeshUtilities"); TArray WarningMessages; if (!MeshUtilities.BuildSkeletalMesh(SkeletalMeshModel, SkeletalMeshName, RootBoneRefSkeleton, LODInfluences, LODWedges, LODFaces, LODPoints, LODPointToRawMap, BuildOptions, &WarningMessages)) { for (const FText& Message : WarningMessages) { UE_LOG(LogChaosClothAssetDataflowNodes, Warning, TEXT("%s"), *Message.ToString()); } return false; } return true; } FDataflowNode* FClothDataflowTools::GetPropertyOwnerDataflowNode(const TSharedPtr& PropertyHandle, const UStruct* DataflowNodeStruct) { for (TSharedPtr OwnerHandle = PropertyHandle->GetParentHandle(); OwnerHandle; OwnerHandle = OwnerHandle->GetParentHandle()) { if (const TSharedPtr OwnerHandleStruct = OwnerHandle->AsStruct()) { if (TSharedPtr StructOnScope = OwnerHandleStruct->GetStructData()) { if (StructOnScope->GetStruct()->IsChildOf(DataflowNodeStruct)) { return reinterpret_cast(StructOnScope->GetStructMemory()); } } } } return nullptr; } FClothDataflowTools::FSimMeshCleanup::FSimMeshCleanup(const TArray& InTriangleToVertexIndex, const TArray& InRestPositions2D, const TArray& InDrapedPositions3D) : TriangleToVertexIndex(InTriangleToVertexIndex) , RestPositions2D(InRestPositions2D) , DrapedPositions3D(InDrapedPositions3D) { check(RestPositions2D.Num() == DrapedPositions3D.Num()); OriginalTriangles.SetNum(TriangleToVertexIndex.Num()); for (int32 Index = 0; Index < OriginalTriangles.Num(); ++Index) { OriginalTriangles[Index].Add(Index); } OriginalVertices.SetNum(DrapedPositions3D.Num()); for (int32 Index = 0; Index < OriginalVertices.Num(); ++Index) { OriginalVertices[Index].Add(Index); } } bool FClothDataflowTools::FSimMeshCleanup::RemoveDegenerateTriangles() { check(RestPositions2D.Num() == DrapedPositions3D.Num()); bool bHasDegenerateTriangles = false; const int32 VertexCount = RestPositions2D.Num(); // Remap[Index] is the index of the first vertex in a group of degenerated triangles to be callapsed. // When two groups of collapsed vertices are merged, the group with the greatest Remap[index] value must adopt the one from the other group. // For Example: // 1. For all i, Remap[i] = i // 2. Finds one degenerated triangle (7, 9, 4) with collapsed edges (7, 9), (9, 4), and (7, 4) -> Remap[4] = 4, Remap[7] = 4, and Remap[9] = 4 // 3. Finds another degenerated triangle (2, 3, 4) with collapsed edges (2, 4) -> Remap[2] = 2, Remap[4] = 2, Remap[7] = 2, and Remap[9] = 2 TArray Remap; Remap.SetNumUninitialized(VertexCount); for (int32 Index = 0; Index < VertexCount; ++Index) { Remap[Index] = Index; } int32 OutVertexCount = VertexCount; auto RemapAndPropagateIndex = [&Remap, &OutVertexCount](int32 Index0, int32 Index1) { if (Remap[Index0] != Remap[Index1]) { if (Remap[Index0] > Remap[Index1]) // Always remap from the lowest index to ensure the earlier index is always kept { Swap(Index0, Index1); } // Merge groups with this new first index Remap[Index0] const int32 PrevRemapIndex = Remap[Index1]; for (int32 Index = PrevRemapIndex; Index < Remap.Num(); ++Index) // Only need to start from the first index of the group to merge { if (Remap[Index] == PrevRemapIndex) { Remap[Index] = Remap[Index0]; } } --OutVertexCount; } }; const int32 TriangleCount = TriangleToVertexIndex.Num(); TArray OutTriangleToVertexIndex; OutTriangleToVertexIndex.Reserve(TriangleCount); TArray> OutOriginalTriangles; OutOriginalTriangles.Reserve(TriangleCount); for (int32 TriangleIndex = 0; TriangleIndex < TriangleCount; ++TriangleIndex) { const int32 Index0 = TriangleToVertexIndex[TriangleIndex][0]; const int32 Index1 = TriangleToVertexIndex[TriangleIndex][1]; const int32 Index2 = TriangleToVertexIndex[TriangleIndex][2]; const FVector3f& P0 = DrapedPositions3D[Index0]; const FVector3f& P1 = DrapedPositions3D[Index1]; const FVector3f& P2 = DrapedPositions3D[Index2]; const FVector3f P0P1 = P1 - P0; const FVector3f P0P2 = P2 - P0; const float TriNormSizeSquared = (P0P1 ^ P0P2).SizeSquared(); if (TriNormSizeSquared <= UE_SMALL_NUMBER) { const FVector3f P1P2 = P2 - P1; if (P0P1.SquaredLength() <= UE_SMALL_NUMBER) { RemapAndPropagateIndex(Index0, Index1); } if (P0P2.SquaredLength() <= UE_SMALL_NUMBER) { RemapAndPropagateIndex(Index0, Index2); } if (P1P2.SquaredLength() <= UE_SMALL_NUMBER) { RemapAndPropagateIndex(Index1, Index2); } } else { OutTriangleToVertexIndex.Emplace(TriangleToVertexIndex[TriangleIndex]); OutOriginalTriangles.Emplace(OriginalTriangles[TriangleIndex]); } } TriangleToVertexIndex = MoveTemp(OutTriangleToVertexIndex); OriginalTriangles = MoveTemp(OutOriginalTriangles); const int32 OutTriangleCount = TriangleToVertexIndex.Num(); bHasDegenerateTriangles = (TriangleCount != OutTriangleCount); UE_CLOG(bHasDegenerateTriangles, LogChaosClothAssetDataflowNodes, Display, TEXT("USD import found and removed %d degenerated triangles out of %d source triangles."), TriangleCount - OutTriangleCount, TriangleCount); // Reconstruct vertices TArray OutRestPositions2D; OutRestPositions2D.Reserve(OutVertexCount); TArray OutDrapedPositions3D; OutDrapedPositions3D.Reserve(OutVertexCount); TArray> OutOriginalVertices; OutOriginalVertices.Reserve(OutVertexCount); TArray OutIndices; OutIndices.Reserve(VertexCount); int32 OutIndex = -1; for (int32 VertexIndex = 0; VertexIndex < VertexCount; ++VertexIndex) { if (Remap[VertexIndex] == VertexIndex) { OutRestPositions2D.Emplace(RestPositions2D[VertexIndex]); OutDrapedPositions3D.Emplace(DrapedPositions3D[VertexIndex]); OutOriginalVertices.Emplace(OriginalVertices[VertexIndex]); OutIndices.Emplace(++OutIndex); } else { const int32 OutRemappedIndex = OutIndices[Remap[VertexIndex]]; OutOriginalVertices[OutRemappedIndex].Append(OriginalVertices[VertexIndex]); OutIndices.Emplace(OutRemappedIndex); } } ensure(OutIndex + 1 == OutVertexCount); RestPositions2D = MoveTemp(OutRestPositions2D); DrapedPositions3D = MoveTemp(OutDrapedPositions3D); OriginalVertices = MoveTemp(OutOriginalVertices); // Remap final triangles for (int32 TriangleIndex = 0; TriangleIndex < OutTriangleCount; ++TriangleIndex) { int32& Index0 = TriangleToVertexIndex[TriangleIndex][0]; int32& Index1 = TriangleToVertexIndex[TriangleIndex][1]; int32& Index2 = TriangleToVertexIndex[TriangleIndex][2]; Index0 = OutIndices[Index0]; Index1 = OutIndices[Index1]; Index2 = OutIndices[Index2]; checkSlow(Index0 != Index1); checkSlow(Index0 != Index2); checkSlow(Index1 != Index2); checkSlow((OutDrapedPositions3D[Index0] - OutDrapedPositions3D[Index1]).SquaredLength() > UE_SMALL_NUMBER); checkSlow((OutDrapedPositions3D[Index0] - OutDrapedPositions3D[Index2]).SquaredLength() > UE_SMALL_NUMBER); checkSlow((OutDrapedPositions3D[Index1] - OutDrapedPositions3D[Index2]).SquaredLength() > UE_SMALL_NUMBER); } return bHasDegenerateTriangles; } bool FClothDataflowTools::FSimMeshCleanup::RemoveDuplicateTriangles() { bool bHasDuplicatedTriangles = false; const int32 TriangleCount = TriangleToVertexIndex.Num(); TMap Triangles; Triangles.Reserve(TriangleCount); TArray OutTriangleToVertexIndex; OutTriangleToVertexIndex.Reserve(TriangleCount); TArray> OutOriginalTriangles; OutOriginalTriangles.Reserve(TriangleCount); auto GetSortedIndices = [](const FIntVector3& TriangleIndices)->FIntVector3 { const int32 Index0 = TriangleIndices[0]; const int32 Index1 = TriangleIndices[1]; const int32 Index2 = TriangleIndices[2]; return (Index0 < Index1) ? (Index1 < Index2) ? FIntVector3(Index0, Index1, Index2) : (Index0 < Index2) ? FIntVector3(Index0, Index2, Index1) : FIntVector3(Index2, Index0, Index1) : (Index0 < Index2) ? FIntVector3(Index1, Index0, Index2) : (Index1 < Index2) ? FIntVector3(Index1, Index2, Index0) : FIntVector3(Index2, Index1, Index0); }; for (int32 Index = 0; Index < TriangleCount; ++Index) { const FIntVector3& TriangleIndices = TriangleToVertexIndex[Index]; const FIntVector3 TriangleSortedIndices = GetSortedIndices(TriangleIndices); if (int32* NewTriangle = Triangles.Find(TriangleSortedIndices)) { bHasDuplicatedTriangles = true; OutOriginalTriangles[*NewTriangle].Append(OriginalTriangles[Index]); } else { NewTriangle = &Triangles.Emplace(TriangleSortedIndices); *NewTriangle = OutTriangleToVertexIndex.Emplace(TriangleIndices); OutOriginalTriangles.Emplace(OriginalTriangles[Index]); } } TriangleToVertexIndex = MoveTemp(OutTriangleToVertexIndex); OriginalTriangles = MoveTemp(OutOriginalTriangles); UE_CLOG(bHasDuplicatedTriangles, LogChaosClothAssetDataflowNodes, Display, TEXT("USD import found and removed %d duplicated triangles out of %d source triangles."), TriangleCount - TriangleToVertexIndex.Num(), TriangleCount); return bHasDuplicatedTriangles; } template> || std::is_same_v>)> TArray FClothDataflowTools::GetOriginalToNewIndices(const TConstArrayView& NewToOriginals, int32 NumOriginalIndices) { TArray OriginalToNewIndices; OriginalToNewIndices.Init(INDEX_NONE, NumOriginalIndices); for (int32 NewIndex = 0; NewIndex < NewToOriginals.Num(); ++NewIndex) { for (const int32 OriginalIndex : NewToOriginals[NewIndex]) { check(OriginalToNewIndices.IsValidIndex(OriginalIndex)); check(OriginalToNewIndices[OriginalIndex] == INDEX_NONE); OriginalToNewIndices[OriginalIndex] = NewIndex; } } return OriginalToNewIndices; } template TArray FClothDataflowTools::GetOriginalToNewIndices>(const TConstArrayView>& NewToOriginals, int32 NumOriginalIndices); template TArray FClothDataflowTools::GetOriginalToNewIndices>(const TConstArrayView>& NewToOriginals, int32 NumOriginalIndices); bool FClothDataflowTools::RemoveDegenerateTriangles( const TArray& TriangleToVertexIndex, const TArray& RestPositions2D, const TArray& DrapedPositions3D, TArray& OutTriangleToVertexIndex, TArray& OutRestPositions2D, TArray& OutDrapedPositions3D, TArray& OutIndices) { FSimMeshCleanup SimMeshCleanup(TriangleToVertexIndex, RestPositions2D, DrapedPositions3D); const bool bHasDegenerateTriangles = SimMeshCleanup.RemoveDegenerateTriangles(); OutIndices = GetOriginalToNewIndices>(SimMeshCleanup.OriginalVertices, DrapedPositions3D.Num()); OutTriangleToVertexIndex = MoveTemp(SimMeshCleanup.TriangleToVertexIndex); OutRestPositions2D = MoveTemp(SimMeshCleanup.RestPositions2D); OutDrapedPositions3D = MoveTemp(SimMeshCleanup.DrapedPositions3D); return bHasDegenerateTriangles; } bool FClothDataflowTools::RemoveDuplicateStitches(TArray>& SeamStitches) { bool bHasDuplicateStitches = false; const int32 NumSeamStitches = SeamStitches.Num(); // Calculate the total number of stitches int32 NumStitches = 0; for (const TArray& Stitches : SeamStitches) { NumStitches += Stitches.Num(); } TSet StichSet; StichSet.Reserve(NumStitches); int32 OutNumStitches = 0; TArray> OutSeamStitches; OutSeamStitches.Reserve(NumSeamStitches); for (const TArray& Stitches : SeamStitches) { TArray OutStitches; OutStitches.Reserve(Stitches.Num()); for (const FIntVector2& Stitch : Stitches) { const FIntVector2 SortedStitch = Stitch[0] < Stitch[1] ? FIntVector2(Stitch[0], Stitch[1]) : FIntVector2(Stitch[1], Stitch[0]); bool bIsAlreadyInSet; StichSet.FindOrAdd(SortedStitch, &bIsAlreadyInSet); if (bIsAlreadyInSet) { bHasDuplicateStitches = true; } else { OutStitches.Emplace(Stitch); } } if (OutStitches.Num()) { OutSeamStitches.Emplace(OutStitches); OutNumStitches += OutStitches.Num(); } } UE_CLOG(bHasDuplicateStitches, LogChaosClothAssetDataflowNodes, Display, TEXT("USD import found and removed %d duplicated stitches out of %d source stitches."), NumStitches - OutNumStitches, NumStitches); SeamStitches = MoveTemp(OutSeamStitches); return bHasDuplicateStitches; } } // End namespace UE::Chaos::ClothAsset