// Copyright Epic Games, Inc. All Rights Reserved. #include "ChaosClothAsset/TransferSkinWeightsNode.h" #include "ChaosClothAsset/ClothPatternToDynamicMesh.h" #include "ChaosClothAsset/ClothDataflowTools.h" #include "ChaosClothAsset/ClothGeometryTools.h" #include "ChaosClothAsset/CollectionClothFacade.h" #include "Animation/Skeleton.h" #include "Dataflow/DataflowInputOutput.h" #include "DynamicMesh/DynamicBoneAttribute.h" #include "DynamicMesh/DynamicMesh3.h" #include "DynamicMesh/DynamicMeshAABBTree3.h" #include "DynamicMesh/DynamicMeshAttributeSet.h" #include "DynamicMesh/DynamicVertexSkinWeightsAttribute.h" #include "DynamicMesh/MeshNormals.h" #include "DynamicMesh/MeshTransforms.h" #include "DynamicMesh/NonManifoldMappingSupport.h" #include "Engine/SkeletalMesh.h" #include "MeshDescriptionToDynamicMesh.h" #include "Operations/TransferBoneWeights.h" #include "Rendering/SkeletalMeshLODImporterData.h" #include "Rendering/SkeletalMeshModel.h" #include "SkeletalMeshAttributes.h" #include "Selections/MeshConnectedComponents.h" #include "BoneWeights.h" #include "ChaosClothAsset/ClothCollectionGroup.h" #include "Rendering/SkeletalMeshRenderData.h" #include "SkeletalMeshLODRenderDataToDynamicMesh.h" #include "Tasks/Task.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(TransferSkinWeightsNode) #define LOCTEXT_NAMESPACE "ChaosClothAssetTransferSkinWeightsNode" static const FName InpaintWeightMaskName(TEXT("_InpaintWeightMask")); namespace UE::Geometry { class FDynamicMesh3; template class TMeshAABBTree3; typedef TMeshAABBTree3 FDynamicMeshAABBTree3; } namespace UE::Chaos::ClothAsset::Private { /** Helper struct to pass the transfer settings around */ struct TransferBoneWeightsSettings { /** * Settings for controlling which meshes to transfer to and from. */ bool bTransferToSim = true; // if true, transfer to sim mesh, otherwise skip sim mesh bool bTransferToRender = true; // if true, transfer to render mesh, otherwise skip render mesh bool bTransferToRenderFromSim = true; // if true, for render mesh only, transfer from the sim mesh, otherwise transfer from the source skeletal mesh /** * Shared Transfer Operator Settings */ bool bUseParallel = false; int32 MaxNumInfluences = 8; UE::Geometry::FTransferBoneWeights::ETransferBoneWeightsMethod TransferMethod = UE::Geometry::FTransferBoneWeights::ETransferBoneWeightsMethod::ClosestPointOnSurface; /** * Settings for the ETransferBoneWeightsMethod::InpaintWeights transfer method */ double NormalThreshold = 0; double RadiusPercentage = 0; bool LayeredMeshSupport = false; int32 NumSmoothingIterations = 0; double SmoothingStrength = 0; FString InpaintMaskWeightMapName; }; UE::Geometry::FDynamicMeshVertexSkinWeightsAttribute* GetOrCreateSkinWeightsAttribute(FDynamicMesh3& InMesh, const FName& InProfileName="Default") { checkSlow(InMesh.HasAttributes()); UE::Geometry::FDynamicMeshVertexSkinWeightsAttribute* Attribute = InMesh.Attributes()->GetSkinWeightsAttribute(InProfileName); if (Attribute == nullptr) { Attribute = new UE::Geometry::FDynamicMeshVertexSkinWeightsAttribute(&InMesh); InMesh.Attributes()->AttachSkinWeightsAttribute(InProfileName, Attribute); } return Attribute; } /** * Convert the USkeletalMesh to DynamicMesh. If requested LOD was auto-generated, will convert render data instead of the mesh description. */ static bool SkeletalMeshToDynamicMesh(const USkeletalMesh* SkeletalMesh, const int32 LodIndex, FDynamicMesh3& ToDynamicMesh) { if (SkeletalMesh->HasMeshDescription(LodIndex)) { const FMeshDescription* SourceMesh = SkeletalMesh->GetMeshDescription(LodIndex); if (!SourceMesh) { return false; } FMeshDescriptionToDynamicMesh Converter; Converter.Convert(SourceMesh, ToDynamicMesh); } else { const FSkeletalMeshRenderData* RenderData = SkeletalMesh->GetResourceForRendering(); if (!RenderData) { return false; } if (!RenderData->LODRenderData.IsValidIndex(LodIndex)) { return false; } const FSkeletalMeshLODRenderData* SkeletalMeshLODRenderData = &(RenderData->LODRenderData[LodIndex]); UE::Geometry::FSkeletalMeshLODRenderDataToDynamicMesh::ConversionOptions ConversionOptions; ConversionOptions.bWantUVs = false; ConversionOptions.bWantVertexColors = false; ConversionOptions.bWantMaterialIDs = false; ConversionOptions.bWantSkinWeights = true; UE::Geometry::FSkeletalMeshLODRenderDataToDynamicMesh::Convert(SkeletalMeshLODRenderData, SkeletalMesh->GetRefSkeleton(), ConversionOptions, ToDynamicMesh); } return true; } /** Convert the ClothCollection to DynamicMesh. */ static bool ClothToDynamicMesh( const TSharedRef& ClothCollection, const FReferenceSkeleton& TargetRefSkeleton, // the reference skeleton to add to the dynamic mesh const bool bIsSim, // if true, Mesh will contain the sim mesh, otherwise the render mesh UE::Geometry::FDynamicMesh3& Mesh) // the resulting sim or render mesh { using namespace UE::Geometry; using namespace UE::Chaos::ClothAsset; // Check if ClothCollection is empty FCollectionClothConstFacade ClothFacade(ClothCollection); const int32 NumVertices = bIsSim ? ClothFacade.GetNumSimVertices3D() : ClothFacade.GetNumRenderVertices(); const int32 NumFaces = bIsSim ? ClothFacade.GetNumSimFaces() : ClothFacade.GetNumRenderFaces(); if (NumVertices <= 0 || NumFaces <= 0) { UE_LOG(LogChaosClothAssetDataflowNodes, Warning, TEXT("TransferSkinWeightsNode: Failed to convert the Cloth Collection to Dynamic Mesh. Cloth Collection is empty.")); return false; } // Convert the sim mesh to DynamicMesh. FClothPatternToDynamicMesh PatternToDynamicMesh; constexpr bool bDisableAttributes = false; const EClothPatternVertexType PatternType = bIsSim ? EClothPatternVertexType::Sim3D : EClothPatternVertexType::Render; PatternToDynamicMesh.Convert(ClothCollection, INDEX_NONE, PatternType, Mesh, bDisableAttributes); // Setup the skeleton. // @note we can't simply copy the bone attributes from the source USkeletalMesh because the cloth // asset reference skeleton comes from the USkeleton not the USkeletalMesh Mesh.Attributes()->EnableBones(TargetRefSkeleton.GetRawBoneNum()); for (int32 BoneIdx = 0; BoneIdx < TargetRefSkeleton.GetRawBoneNum(); ++BoneIdx) { Mesh.Attributes()->GetBoneNames()->SetValue(BoneIdx, TargetRefSkeleton.GetRawRefBoneInfo()[BoneIdx].Name); } return true; } /** Copy the skin weights from DynamicMesh to Render Collection. */ static void CopySkinWeightsFromDynamicMeshToRenderCloth(const FDynamicMesh3& RenderMesh, const bool bUseParallel, const TSharedRef& ClothCollection) { using namespace UE::Geometry; UE::Chaos::ClothAsset::FCollectionClothFacade ClothFacade(ClothCollection); ParallelFor(RenderMesh.MaxVertexID(), [&ClothFacade, &RenderMesh](int32 VertexID) { const FDynamicMeshVertexSkinWeightsAttribute* OutAttribute = RenderMesh.Attributes()->GetSkinWeightsAttribute(FSkeletalMeshAttributes::DefaultSkinWeightProfileName); checkSlow(OutAttribute); checkSlow(RenderMesh.IsVertex(VertexID)); checkSlow(VertexID < ClothFacade.GetNumRenderVertices()); OutAttribute->GetValue(VertexID, ClothFacade.GetRenderBoneIndices()[VertexID], ClothFacade.GetRenderBoneWeights()[VertexID]); }, bUseParallel ? EParallelForFlags::None : EParallelForFlags::ForceSingleThread); } /** Copy the skin weights from DynamicMesh to Sim Collection, handling split vertices. */ static void CopySkinWeightsFromDynamicMeshToSimCloth(const FDynamicMesh3& WeldedSimMesh, const bool bUseParallel, const TSharedRef& ClothCollection, const int32 MaxNumInfluences) { using namespace UE::Geometry; UE::Chaos::ClothAsset::FCollectionClothFacade ClothFacade(ClothCollection); // // Copy the new bone weight data from the welded sim mesh back to the cloth patterns. // const FNonManifoldMappingSupport NonManifoldMapping(WeldedSimMesh); TArray> SimMeshToDynamicMesh; if (NonManifoldMapping.IsNonManifoldVertexInSource()) { // WeldedSimMesh indices don't match cloth collection. SimMeshToDynamicMesh.SetNum(ClothFacade.GetNumSimVertices3D()); for (int32 DynamicMeshVert = 0; DynamicMeshVert < WeldedSimMesh.VertexCount(); ++DynamicMeshVert) { SimMeshToDynamicMesh[NonManifoldMapping.GetOriginalNonManifoldVertexID(DynamicMeshVert)].Add(DynamicMeshVert); } ParallelFor(ClothFacade.GetNumSimVertices3D(), [&ClothFacade, &WeldedSimMesh, &SimMeshToDynamicMesh, MaxNumInfluences](int32 SimVertexID) { const FDynamicMeshVertexSkinWeightsAttribute* const OutAttribute = WeldedSimMesh.Attributes()->GetSkinWeightsAttribute(FSkeletalMeshAttributes::DefaultSkinWeightProfileName); checkSlow(OutAttribute); if (!ensure(SimMeshToDynamicMesh[SimVertexID].Num() > 0)) { ClothFacade.GetSimBoneIndices()[SimVertexID].Reset(); ClothFacade.GetSimBoneWeights()[SimVertexID].Reset(); return; } if (SimMeshToDynamicMesh[SimVertexID].Num() == 1) { // Simple most common case, one-to-one correspondence. Just copy over. const int32 WeldedID = SimMeshToDynamicMesh[SimVertexID][0]; checkSlow(WeldedSimMesh.IsVertex(WeldedID)); OutAttribute->GetValue(WeldedID, ClothFacade.GetSimBoneIndices()[SimVertexID], ClothFacade.GetSimBoneWeights()[SimVertexID]); } else { // Need to merge data because dynamic mesh split the original vertex TMap> CombinedData; for (const int32 WeldedID : SimMeshToDynamicMesh[SimVertexID]) { TArray Indices; TArray Weights; checkSlow(WeldedSimMesh.IsVertex(WeldedID)); OutAttribute->GetValue(WeldedID, Indices, Weights); check(Indices.Num() == Weights.Num()); for (int32 Idx = 0; Idx < Indices.Num(); ++Idx) { TPair& WeightedFloat = CombinedData.FindOrAdd(Indices[Idx]); WeightedFloat.Get<0>() += Weights[Idx]; WeightedFloat.Get<1>() += 1; } } TArray& BoneIndices = ClothFacade.GetSimBoneIndices()[SimVertexID]; TArray& BoneWeights = ClothFacade.GetSimBoneWeights()[SimVertexID]; BoneIndices.Reset(CombinedData.Num()); BoneWeights.Reset(CombinedData.Num()); float WeightsSum = 0.f; for (TMap>::TConstIterator CombinedDataIter = CombinedData.CreateConstIterator(); CombinedDataIter; ++CombinedDataIter) { check(CombinedDataIter.Value().Get<1>() > 0); BoneIndices.Add(CombinedDataIter.Key()); const float FloatVal = CombinedDataIter.Value().Get<0>() / (float)CombinedDataIter.Value().Get<1>(); BoneWeights.Add(FloatVal); WeightsSum += FloatVal; } if (BoneIndices.Num() > MaxNumInfluences) { // Choose MaxNumInfluences highest weighted bones. TArray> SortableData; SortableData.Reserve(BoneIndices.Num()); for (int32 Idx = 0; Idx < BoneIndices.Num(); ++Idx) { SortableData.Emplace(BoneWeights[Idx], BoneIndices[Idx]); } SortableData.Sort([](const TPair& A, const TPair& B) { return A > B; }); BoneIndices.SetNum(MaxNumInfluences); BoneWeights.SetNum(MaxNumInfluences); WeightsSum = 0.f; for (int32 Idx = 0; Idx < MaxNumInfluences; ++Idx) { BoneIndices[Idx] = SortableData[Idx].Get<1>(); BoneWeights[Idx] = SortableData[Idx].Get<0>(); WeightsSum += SortableData[Idx].Get<0>(); } } // Normalize weights const float WeightsSumRecip = WeightsSum > UE_SMALL_NUMBER ? 1.f / WeightsSum : 0.f; for (float& Weight : BoneWeights) { Weight *= WeightsSumRecip; } } }, bUseParallel ? EParallelForFlags::None : EParallelForFlags::ForceSingleThread); } else { ParallelFor(WeldedSimMesh.MaxVertexID(), [&ClothFacade, &WeldedSimMesh, &NonManifoldMapping](int32 WeldedID) { const FDynamicMeshVertexSkinWeightsAttribute* OutAttribute = WeldedSimMesh.Attributes()->GetSkinWeightsAttribute(FSkeletalMeshAttributes::DefaultSkinWeightProfileName); checkSlow(OutAttribute); checkSlow(WeldedSimMesh.IsVertex(WeldedID)); checkSlow(WeldedID < ClothFacade.GetNumSimVertices3D()); OutAttribute->GetValue(WeldedID, ClothFacade.GetSimBoneIndices()[WeldedID], ClothFacade.GetSimBoneWeights()[WeldedID]); }, bUseParallel ? EParallelForFlags::None : EParallelForFlags::ForceSingleThread); } } /** Copy the inpaint mask array to Collection, handling split vertices. */ static void CopyInpaintMapFromDynamicMeshToSimCloth(const FDynamicMesh3& WeldedSimMesh, const FName WeightMapName, const bool bUseParallel, const TArray& MatchedVertices, const TSharedRef& ClothCollection) { using namespace UE::Geometry; UE::Chaos::ClothAsset::FCollectionClothFacade ClothFacade(ClothCollection); TArrayView InpaintWeightMask = ClothFacade.GetWeightMap(WeightMapName); const FNonManifoldMappingSupport NonManifoldMapping(WeldedSimMesh); TArray> SimMeshToDynamicMesh; if (NonManifoldMapping.IsNonManifoldVertexInSource()) { // WeldedSimMesh indices don't match cloth collection. SimMeshToDynamicMesh.SetNum(ClothFacade.GetNumSimVertices3D()); for (int32 DynamicMeshVert = 0; DynamicMeshVert < WeldedSimMesh.VertexCount(); ++DynamicMeshVert) { SimMeshToDynamicMesh[NonManifoldMapping.GetOriginalNonManifoldVertexID(DynamicMeshVert)].Add(DynamicMeshVert); } ParallelFor(ClothFacade.GetNumSimVertices3D(), [&ClothFacade, &WeldedSimMesh, &SimMeshToDynamicMesh, &InpaintWeightMask, &MatchedVertices](int32 SimVertexID) { if (!ensure(SimMeshToDynamicMesh[SimVertexID].Num() > 0)) { return; } if (SimMeshToDynamicMesh[SimVertexID].Num() == 1) { const int32 WeldedID = SimMeshToDynamicMesh[SimVertexID][0]; checkSlow(WeldedSimMesh.IsVertex(WeldedID)); InpaintWeightMask[SimVertexID] = MatchedVertices[WeldedID] ? 1.0f : 0.0f; } else { const int32 WeldedID = SimMeshToDynamicMesh[SimVertexID][0]; // Any welded id can be used here InpaintWeightMask[SimVertexID] = MatchedVertices[WeldedID] ? 1.0f : 0.0f; } }, bUseParallel ? EParallelForFlags::None : EParallelForFlags::ForceSingleThread); } else { ParallelFor(WeldedSimMesh.MaxVertexID(), [&ClothFacade, &WeldedSimMesh, &NonManifoldMapping, &InpaintWeightMask, &MatchedVertices](int32 WeldedID) { checkSlow(WeldedSimMesh.IsVertex(WeldedID)); checkSlow(WeldedID < ClothFacade.GetNumSimVertices3D()); InpaintWeightMask[WeldedID] = MatchedVertices[WeldedID] ? 1.0f : 0.0f; }, bUseParallel ? EParallelForFlags::None : EParallelForFlags::ForceSingleThread); } } /** * Transfer skin weights from the source to the target dynamic mesh. * The target mesh is split into connected components and the transfer is run on each component separately. */ static bool TransferInpaintWeightsToMeshComponents(const FReferenceSkeleton& TargetRefSkeleton, const UE::Geometry::FDynamicMesh3& SourceDynamicMesh, UE::Geometry::FDynamicMesh3& TargetDynamicMesh, const TransferBoneWeightsSettings& TransferSettings) { using namespace UE::Geometry; // Find connected-components FMeshConnectedComponents ConnectedComponents(&TargetDynamicMesh); ConnectedComponents.FindConnectedTriangles(); // Pointer to the weight layer containing force inpaint mask (if one exists) const FDynamicMeshWeightAttribute* ForceInpaintWeightLayer = nullptr; if (!TransferSettings.InpaintMaskWeightMapName.IsEmpty()) { for (int32 Idx = 0; Idx < TargetDynamicMesh.Attributes()->NumWeightLayers(); ++Idx) { const FDynamicMeshWeightAttribute* WeightLayer = TargetDynamicMesh.Attributes()->GetWeightLayer(Idx); if (WeightLayer && WeightLayer->GetName() == FName(TransferSettings.InpaintMaskWeightMapName)) { ForceInpaintWeightLayer = WeightLayer; break; } } } // Iterate over each component and perform per-component skin weight transfer const int32 NumComponents = ConnectedComponents.Num(); const int32 NumTasks = FMath::Max(FMath::Min(FTaskGraphInterface::Get().GetNumWorkerThreads(), NumComponents), 1); constexpr int32 MinComponentByTask = 1; const int32 ComnponentsByTask = FMath::Max(FMath::Max(FMath::DivideAndRoundUp(NumComponents, NumTasks), MinComponentByTask), 1); const int32 NumBatches = FMath::DivideAndRoundUp(NumComponents, ComnponentsByTask); TArray PendingTasks; PendingTasks.Reserve(NumBatches); TArray Submeshes; Submeshes.SetNum(NumComponents); TArray> SubmeshToBaseVs; SubmeshToBaseVs.SetNum(NumComponents); std::atomic bTransferParametersInvalid(false); // Creating the BVH here in single threaded which is expensive allow to avoid every thread to pay the expensive cost FDynamicMeshAABBTree3 SourceBVH(&SourceDynamicMesh); for (int32 BatchIndex = 0; BatchIndex < NumBatches; BatchIndex++) { const int32 StartIndex = BatchIndex * ComnponentsByTask; int32 EndIndex = (BatchIndex + 1) * ComnponentsByTask; EndIndex = BatchIndex == NumBatches - 1 ? FMath::Min(NumComponents, EndIndex) : EndIndex; UE::Tasks::FTask PendingTask = UE::Tasks::Launch(UE_SOURCE_LOCATION, [StartIndex, EndIndex, &TargetRefSkeleton, &ConnectedComponents, &TargetDynamicMesh, &ForceInpaintWeightLayer, &TransferSettings, &SourceDynamicMesh, &SourceBVH, &Submeshes, &SubmeshToBaseVs, &bTransferParametersInvalid]() { QUICK_SCOPE_CYCLE_COUNTER(STAT_TransferInpaintWeightsToMeshComponentsTask); if (bTransferParametersInvalid.load(std::memory_order_relaxed)) { return; } // Setup transfer weights operator FTransferBoneWeights TransferBoneWeights(&SourceDynamicMesh, FSkeletalMeshAttributes::DefaultSkinWeightProfileName, &SourceBVH); TransferBoneWeights.bUseParallel = TransferSettings.bUseParallel; TransferBoneWeights.MaxNumInfluences = TransferSettings.MaxNumInfluences; TransferBoneWeights.TransferMethod = TransferSettings.TransferMethod; TransferBoneWeights.NormalThreshold = FMathd::DegToRad * TransferSettings.NormalThreshold; TransferBoneWeights.SearchRadius = TransferSettings.RadiusPercentage * TargetDynamicMesh.GetBounds().DiagonalLength(); TransferBoneWeights.NumSmoothingIterations = TransferSettings.NumSmoothingIterations; TransferBoneWeights.SmoothingStrength = TransferSettings.SmoothingStrength; TransferBoneWeights.LayeredMeshSupport = TransferSettings.LayeredMeshSupport; // multilayerd clothing if (TransferBoneWeights.Validate() != EOperationValidationResult::Ok) { bTransferParametersInvalid.store(true, std::memory_order_relaxed); return; } for (int32 ComponentIdx = StartIndex; ComponentIdx < EndIndex; ++ComponentIdx) { // Mesh containing the geometry of the current component FDynamicMesh3& Submesh = Submeshes[ComponentIdx]; Submesh.EnableAttributes(); Submesh.Attributes()->EnableBones(TargetRefSkeleton.GetRawBoneNum()); for (int32 BoneIdx = 0; BoneIdx < TargetRefSkeleton.GetRawBoneNum(); ++BoneIdx) { Submesh.Attributes()->GetBoneNames()->SetValue(BoneIdx, TargetRefSkeleton.GetRawRefBoneInfo()[BoneIdx].Name); } // Keep track of the maps between Mesh and the component submesh vertex/triangle indices TMap BaseToSubmeshV; TArray& SubmeshToBaseV = SubmeshToBaseVs[ComponentIdx]; TArray SubmeshToBaseT; const TArray& ComponentTris = ConnectedComponents[ComponentIdx].Indices; for (const int32 TID : ComponentTris) { const FIndex3i Triangle = TargetDynamicMesh.GetTriangle(TID); FIndex3i NewTriangle; for (int32 TriCornerIdx = 0; TriCornerIdx < 3; ++TriCornerIdx) { const int32* FoundIdx = BaseToSubmeshV.Find(Triangle[TriCornerIdx]); if (FoundIdx) { NewTriangle[TriCornerIdx] = *FoundIdx; } else { const FVector3d Position = TargetDynamicMesh.GetVertex(Triangle[TriCornerIdx]); const int32 NewVtxID = Submesh.AppendVertex(Position); check(NewVtxID == SubmeshToBaseV.Num()); SubmeshToBaseV.Add(Triangle[TriCornerIdx]); BaseToSubmeshV.Add(Triangle[TriCornerIdx], NewVtxID); NewTriangle[TriCornerIdx] = NewVtxID; } } const int32 NewTriID = Submesh.AppendTriangle(NewTriangle); check(NewTriID == SubmeshToBaseT.Num()); SubmeshToBaseT.Add(TID); } // Copy over the force inpaint values if (ForceInpaintWeightLayer != nullptr) { TArray ForceInpaint; ForceInpaint.SetNum(Submesh.MaxVertexID()); for (int32 VID = 0; VID < Submesh.MaxVertexID(); ++VID) { float Value = 0; ForceInpaintWeightLayer->GetValue(SubmeshToBaseV[VID], &Value); ForceInpaint[VID] = Value; } // Set the mask in the transfer operator TransferBoneWeights.ForceInpaint = MoveTemp(ForceInpaint); } // Transfer weights to the current submesh only. If transfer using the inpaint method fails, fall back to // the closest point method which should always succeed. Common reason for the failure is if we didn't find // any matches on the source at all with the current transfer settings. if (!TransferBoneWeights.TransferWeightsToMesh(Submesh, FSkeletalMeshAttributes::DefaultSkinWeightProfileName)) { UE_LOG(LogChaosClothAssetDataflowNodes, Warning, TEXT("TransferSkinWeightsNode: Failed to transfer skin weights to some of the vertices of the render mesh using inpaint method, falling back to closest point method.")); // We can reuse the same operator but change the method type TransferBoneWeights.TransferMethod = FTransferBoneWeights::ETransferBoneWeightsMethod::ClosestPointOnSurface; // Ignore radius and normal settings, so all points on the source are considered const double OldSearchRadius = TransferBoneWeights.SearchRadius; const double OldNormalThreshold = TransferBoneWeights.NormalThreshold; TransferBoneWeights.SearchRadius = -1; TransferBoneWeights.NormalThreshold = -1; // This should always succeed if (!ensure(TransferBoneWeights.TransferWeightsToMesh(Submesh, FSkeletalMeshAttributes::DefaultSkinWeightProfileName))) { UE_LOG(LogChaosClothAssetDataflowNodes, Warning, TEXT("TransferSkinWeightsNode: Failed to transfer skin weights to some of the vertices of the render mesh.")); } // Revert back the settings TransferBoneWeights.TransferMethod = TransferSettings.TransferMethod; TransferBoneWeights.SearchRadius = OldSearchRadius; TransferBoneWeights.NormalThreshold = OldNormalThreshold; } } }); PendingTasks.Add(PendingTask); } UE::Tasks::Wait(PendingTasks); if (bTransferParametersInvalid) { UE_LOG(LogChaosClothAssetDataflowNodes, Warning, TEXT("TransferSkinWeightsNode: Transfer method parameters are invalid.")); return false; } for (int32 ComponentIdx = 0; ComponentIdx < NumComponents; ++ComponentIdx) { TArray& SubmeshToBaseV = SubmeshToBaseVs[ComponentIdx]; // Copy over the data from the submesh to the base mesh FDynamicMeshVertexSkinWeightsAttribute* SubMeshSkinWeights = UE::Chaos::ClothAsset::Private::GetOrCreateSkinWeightsAttribute(Submeshes[ComponentIdx]); FDynamicMeshVertexSkinWeightsAttribute* BaseMeshSkinWeights = UE::Chaos::ClothAsset::Private::GetOrCreateSkinWeightsAttribute(TargetDynamicMesh); for (int32 SubMeshVID = 0; SubMeshVID < SubmeshToBaseV.Num(); ++SubMeshVID) { const int32 BaseMeshVID = SubmeshToBaseV[SubMeshVID]; UE::AnimationCore::FBoneWeights Weights; SubMeshSkinWeights->GetValue(SubMeshVID, Weights); BaseMeshSkinWeights->SetValue(BaseMeshVID, Weights); } } return true; } /** Transfer skin weights to sim cloth. */ static bool TransferInpaintWeightsToSim( const FReferenceSkeleton& TargetRefSkeleton, const UE::Geometry::FDynamicMesh3& SourceDynamicMesh, const TSharedRef& ClothCollection, const TransferBoneWeightsSettings& TransferSettings, UE::Geometry::FDynamicMesh3& WeldedSimMesh) { using namespace UE::Geometry; UE::Chaos::ClothAsset::FCollectionClothFacade ClothFacade(ClothCollection); // Convert cloth sim mesh LOD to the welded dynamic sim mesh. if (!ClothToDynamicMesh(ClothCollection, TargetRefSkeleton, true, WeldedSimMesh)) { UE_LOG(LogChaosClothAssetDataflowNodes, Warning, TEXT("TransferSkinWeightsNode: Failed to weld the simulation mesh for LOD.")); return false; } // Transfer the weights from the body to the welded sim mesh. // TODO: run the transfer on components instead FTransferBoneWeights TransferBoneWeights(&SourceDynamicMesh, FSkeletalMeshAttributes::DefaultSkinWeightProfileName); TransferBoneWeights.bUseParallel = TransferSettings.bUseParallel; TransferBoneWeights.MaxNumInfluences = TransferSettings.MaxNumInfluences; TransferBoneWeights.TransferMethod = TransferSettings.TransferMethod; TransferBoneWeights.NormalThreshold = FMathd::DegToRad * TransferSettings.NormalThreshold; TransferBoneWeights.SearchRadius = TransferSettings.RadiusPercentage * WeldedSimMesh.GetBounds().DiagonalLength(); TransferBoneWeights.NumSmoothingIterations = TransferSettings.NumSmoothingIterations; TransferBoneWeights.SmoothingStrength = TransferSettings.SmoothingStrength; TransferBoneWeights.LayeredMeshSupport = TransferSettings.LayeredMeshSupport; // multilayerd clothing TransferBoneWeights.ForceInpaintWeightMapName = FName(TransferSettings.InpaintMaskWeightMapName); if (TransferBoneWeights.Validate() != EOperationValidationResult::Ok) { UE_LOG(LogChaosClothAssetDataflowNodes, Warning, TEXT("TransferSkinWeightsNode: Transfer method parameters are invalid.")); return false; } if (!TransferBoneWeights.TransferWeightsToMesh(WeldedSimMesh, FSkeletalMeshAttributes::DefaultSkinWeightProfileName)) { UE_LOG(LogChaosClothAssetDataflowNodes, Warning, TEXT("TransferSkinWeightsNode: Transferring skin weights failed.")); return false; } ClothFacade.AddWeightMap(InpaintWeightMaskName); // Copy the new bone weight data and inpaint mask from the welded sim mesh back to the sim cloth patterns. CopySkinWeightsFromDynamicMeshToSimCloth(WeldedSimMesh, true, ClothCollection, TransferSettings.MaxNumInfluences); CopyInpaintMapFromDynamicMeshToSimCloth(WeldedSimMesh, InpaintWeightMaskName, true, TransferBoneWeights.MatchedVertices, ClothCollection); return true; } static bool TransferClosestPointOnSurface( const FReferenceSkeleton& TargetRefSkeleton, const FDynamicMesh3& SkeletalDynamicMesh, const TSharedRef& ClothCollection, const TransferBoneWeightsSettings& TransferSettings, const TSharedPtr& SimClothCollection); /** Transfer skin weights to render cloth. */ static bool TransferInpaintWeightsToRender( const FReferenceSkeleton& TargetRefSkeleton, const UE::Geometry::FDynamicMesh3& SourceDynamicMesh, const TSharedRef& ClothCollection, const TransferBoneWeightsSettings& TransferSettings) { using namespace UE::Geometry; // Convert cloth render mesh LOD to the dynamic render mesh. FDynamicMesh3 RenderDynamicMesh; if (!ClothToDynamicMesh(ClothCollection, TargetRefSkeleton, false, RenderDynamicMesh)) { UE_LOG(LogChaosClothAssetDataflowNodes, Warning, TEXT("TransferSkinWeightsNode: Failed to create the render dynamic mesh for LOD.")); return false; } const FNonManifoldMappingSupport NonManifoldMapping(RenderDynamicMesh); if (NonManifoldMapping.IsNonManifoldVertexInSource()) { UE_LOG(LogChaosClothAssetDataflowNodes, Warning, TEXT("TransferSkinWeightsNode: Render mesh inpaint method failed due to non manifold geometry, falling back to closest point method instead.")); checkf(!TransferSettings.bTransferToRenderFromSim, TEXT("Logic error, TransferClosestPointOnSurface should have been called instead transferring from Sim mesh.")); TransferBoneWeightsSettings NewTransferSettings = TransferSettings; NewTransferSettings.bTransferToSim = false; NewTransferSettings.TransferMethod = FTransferBoneWeights::ETransferBoneWeightsMethod::ClosestPointOnSurface; TSharedPtr EmptySimClothCollection; return TransferClosestPointOnSurface(TargetRefSkeleton, SourceDynamicMesh, ClothCollection, NewTransferSettings, EmptySimClothCollection); } else { // Transfer weights to render mesh if (!TransferInpaintWeightsToMeshComponents(TargetRefSkeleton, SourceDynamicMesh, RenderDynamicMesh, TransferSettings)) { UE_LOG(LogChaosClothAssetDataflowNodes, Warning, TEXT("TransferSkinWeightsNode: Failed to transfer skin weights to render mesh.")); return false; } } CopySkinWeightsFromDynamicMeshToRenderCloth(RenderDynamicMesh, TransferSettings.bUseParallel, ClothCollection); return true; } /** Transfer skin weights to sim and render cloth. */ static bool TransferInpaintWeights( const FReferenceSkeleton& TargetRefSkeleton, const UE::Geometry::FDynamicMesh3& SourceDynamicMesh, const TSharedRef& ClothCollection, const TransferBoneWeightsSettings& TransferSettings, const TSharedPtr& SimClothCollection) { using namespace UE::Geometry; FDynamicMesh3 WeldedSimMesh; if (TransferSettings.bTransferToSim) { // // Convert cloth sim mesh LOD to the welded dynamic sim mesh and transfer weights. // if (!TransferInpaintWeightsToSim(TargetRefSkeleton, SourceDynamicMesh, ClothCollection, TransferSettings, WeldedSimMesh)) { return false; } } if (TransferSettings.bTransferToRender) { // // Compute the bone weights for the render mesh by transferring weights from the sim mesh // if (TransferSettings.bTransferToRenderFromSim) { ensureMsgf(TransferSettings.bTransferToSim, TEXT("The UI shouldn't allow a sim mesh InpaintWeights transfer without the sim mesh set to transfer, check the EditConditions.")); // Transfers from the sim mesh most likely mean that a SkeletalMesh InpaintWeights transfer is not working for this render mesh, // therefore it is best to assume that a closest point transfer will always provide a better result in this particular case. TransferBoneWeightsSettings NewTransferSettings = TransferSettings; NewTransferSettings.bTransferToSim = false; NewTransferSettings.TransferMethod = FTransferBoneWeights::ETransferBoneWeightsMethod::ClosestPointOnSurface; return TransferClosestPointOnSurface(TargetRefSkeleton, SourceDynamicMesh, ClothCollection, NewTransferSettings, SimClothCollection); } else { // Transfer skin weights to render cloth from the skeletal asset mesh if (!TransferInpaintWeightsToRender(TargetRefSkeleton, SourceDynamicMesh, ClothCollection, TransferSettings)) { return false; } } } return true; } static bool TransferClosestPointOnSurface( const FReferenceSkeleton& TargetRefSkeleton, const FDynamicMesh3& SkeletalDynamicMesh, const TSharedRef& ClothCollection, const TransferBoneWeightsSettings& TransferSettings, const TSharedPtr& SimClothCollection) { using namespace UE::Geometry; FCollectionClothFacade ClothFacade(ClothCollection); // // Compute the bone index mappings. This allows the transfer operator to retarget weights to the correct skeleton. // TMap TargetBoneToIndex; TargetBoneToIndex.Reserve(TargetRefSkeleton.GetRawBoneNum()); for (int32 BoneIdx = 0; BoneIdx < TargetRefSkeleton.GetRawBoneNum(); ++BoneIdx) { TargetBoneToIndex.Add(TargetRefSkeleton.GetRawRefBoneInfo()[BoneIdx].Name, BoneIdx); } if (TransferSettings.bTransferToSim) { // // Transfer weights to the sim mesh. // FTransferBoneWeights TransferBoneWeights(&SkeletalDynamicMesh, FSkeletalMeshAttributes::DefaultSkinWeightProfileName); TransferBoneWeights.bUseParallel = TransferSettings.bUseParallel; TransferBoneWeights.MaxNumInfluences = TransferSettings.MaxNumInfluences; TransferBoneWeights.TransferMethod = TransferSettings.TransferMethod; if (TransferBoneWeights.Validate() != EOperationValidationResult::Ok) { UE_LOG(LogChaosClothAssetDataflowNodes, Warning, TEXT("TransferSkinWeightsNode: Transfer method parameters are invalid.")); return false; } ParallelFor(ClothFacade.GetNumSimVertices3D(), [&TransferBoneWeights, &TargetBoneToIndex, &ClothFacade](int32 VertexID) { TransferBoneWeights.TransferWeightsToPoint(ClothFacade.GetSimBoneIndices()[VertexID], ClothFacade.GetSimBoneWeights()[VertexID], ClothFacade.GetSimPosition3D()[VertexID], &TargetBoneToIndex); }, TransferSettings.bUseParallel ? EParallelForFlags::None : EParallelForFlags::ForceSingleThread); } if (TransferSettings.bTransferToRender) { // // Transfer weights to the render mesh. // const FDynamicMesh3* SourceMeshToTransferFrom = &SkeletalDynamicMesh; // transfer from body FDynamicMesh3 WeldedSimMesh; if (TransferSettings.bTransferToRenderFromSim) { // Convert sim cloth to dynamic mesh, use a different sim cloth collection if needed constexpr bool bIsSimMesh = true; if (!ClothToDynamicMesh(SimClothCollection.IsValid() ? SimClothCollection.ToSharedRef() : ClothCollection, TargetRefSkeleton, bIsSimMesh, WeldedSimMesh)) { return false; } SourceMeshToTransferFrom = &WeldedSimMesh; // transfer from sim mesh instead } FTransferBoneWeights TransferBoneWeights(SourceMeshToTransferFrom, FSkeletalMeshAttributes::DefaultSkinWeightProfileName); TransferBoneWeights.bUseParallel = TransferSettings.bUseParallel; TransferBoneWeights.TransferMethod = TransferSettings.TransferMethod; TransferBoneWeights.MaxNumInfluences = TransferSettings.MaxNumInfluences; if (TransferBoneWeights.Validate() != EOperationValidationResult::Ok) { UE_LOG(LogChaosClothAssetDataflowNodes, Warning, TEXT("TransferSkinWeightsNode: Transfer method parameters are invalid.")); return false; } ParallelFor(ClothFacade.GetNumRenderVertices(), [&TransferBoneWeights, &TargetBoneToIndex, &ClothFacade](int32 VertexID) { TransferBoneWeights.TransferWeightsToPoint(ClothFacade.GetRenderBoneIndices()[VertexID], ClothFacade.GetRenderBoneWeights()[VertexID], ClothFacade.GetRenderPosition()[VertexID], &TargetBoneToIndex); }, TransferSettings.bUseParallel ? EParallelForFlags::None : EParallelForFlags::ForceSingleThread); } return true; } } FChaosClothAssetTransferSkinWeightsNode::FChaosClothAssetTransferSkinWeightsNode(const UE::Dataflow::FNodeParameters& InParam, FGuid InGuid) : FDataflowNode(InParam, InGuid) { RegisterInputConnection(&Collection); RegisterInputConnection(&SkeletalMesh); RegisterInputConnection(&LodIndex); RegisterInputConnection(&SimCollection) .SetCanHidePin(true) .SetPinIsHidden(true); RegisterInputConnection(&InpaintMask.WeightMap, GET_MEMBER_NAME_CHECKED(FChaosClothAssetWeightedValueNonAnimatableNoLowHighRange, WeightMap)) .SetCanHidePin(true) .SetPinIsHidden(true); RegisterOutputConnection(&Collection, &Collection); } void FChaosClothAssetTransferSkinWeightsNode::Evaluate(UE::Dataflow::FContext& Context, const FDataflowOutput* Out) const { QUICK_SCOPE_CYCLE_COUNTER(STAT_FChaosClothAssetTransferSkinWeightsNode_Evaluate); using namespace UE::Chaos::ClothAsset; using namespace UE::Chaos::ClothAsset::Private; using namespace UE::Geometry; if (Out->IsA(&Collection)) { // Evaluate inputs FManagedArrayCollection InputCollection = GetValue(Context, &Collection); const TSharedRef ClothCollection = MakeShared(MoveTemp(InputCollection)); const TSharedPtr SimClothCollection = IsConnected(&SimCollection) ? MakeShared(GetValue(Context, &SimCollection)) : TSharedPtr(); FCollectionClothFacade ClothFacade(ClothCollection); if (ClothFacade.IsValid()) // Can only act on the collection if it is a valid cloth collection { TStrongObjectPtr InSkeletalMesh; const FReferenceSkeleton* TargetRefSkeleton = nullptr; FDynamicMesh3 SourceDynamicMesh; const bool bNeedsSkeletalMesh = TargetMeshType != EChaosClothAssetTransferTargetMeshType::Render || RenderMeshSourceType == EChaosClothAssetTransferRenderMeshSource::SkeletalMesh; if (bNeedsSkeletalMesh) { InSkeletalMesh = TStrongObjectPtr(GetValue(Context, &SkeletalMesh)); if (!InSkeletalMesh) { FClothDataflowTools::LogAndToastWarning(*this, LOCTEXT("InvalidSkeletalMeshHeadline", "Invalid Skeletal Mesh."), FText::Format( LOCTEXT("InvalidSkeletalMeshDetails", "No skeletal mesh has been specified and one is required for this type of transfer."), LodIndex)); SetValue(Context, MoveTemp(*ClothCollection), &Collection); return; } int32 InLodIndex = GetValue(Context, &LodIndex); if (!InSkeletalMesh->IsValidLODIndex(InLodIndex)) { const int32 LastLODIndex = InSkeletalMesh->GetLODNum() - 1; if (InSkeletalMesh->IsValidLODIndex(LastLODIndex)) { FClothDataflowTools::LogAndToastWarning(*this, LOCTEXT("InvalidLodIndexHeadline", "Invalid LOD Index."), FText::Format( LOCTEXT("InvalidLodIndexSubstituteDetails", "LOD index {0} is not a valid LOD for skeletal mesh {1}. Using LOD index {2} instead."), InLodIndex, FText::FromString(InSkeletalMesh->GetName()), LastLODIndex)); InLodIndex = LastLODIndex; } else { FClothDataflowTools::LogAndToastWarning(*this, LOCTEXT("InvalidLodIndexHeadline", "Invalid LOD Index."), FText::Format( LOCTEXT("InvalidLodIndexDetails", "LOD index {0} is not a valid LOD for skeletal mesh {1}."), InLodIndex, FText::FromString(InSkeletalMesh->GetName()))); SetValue(Context, MoveTemp(*ClothCollection), &Collection); return; } } if (!SkeletalMeshToDynamicMesh(InSkeletalMesh.Get(), InLodIndex, SourceDynamicMesh)) { FClothDataflowTools::LogAndToastWarning(*this, LOCTEXT("InvalidLodHeadline", "Could not convert LOD to Dynamic Mesh."), FText::Format( LOCTEXT("InvalidLodDetails", "Could not convert LOD index {0} of the skeletal mesh {1} to dyanamic mesh."), InLodIndex, FText::FromString(InSkeletalMesh->GetName()))); SetValue(Context, MoveTemp(*ClothCollection), &Collection); return; } MeshTransforms::ApplyTransform(SourceDynamicMesh, Transform, true); TargetRefSkeleton = &InSkeletalMesh->GetRefSkeleton(); ClothFacade.SetSkeletalMeshSoftObjectPathName(InSkeletalMesh->GetPathName()); } else { // Reuse the input sim mesh skeleton auto GetCollectionRefSkeleton = [&InSkeletalMesh](const FCollectionClothConstFacade& ClothFacade) -> const FReferenceSkeleton* { const FSoftObjectPath& SkeletalMeshPathName = ClothFacade.GetSkeletalMeshSoftObjectPathName(); InSkeletalMesh = TStrongObjectPtr(Cast(SkeletalMeshPathName.TryLoad())); return InSkeletalMesh ? &InSkeletalMesh->GetRefSkeleton() : nullptr; }; if (SimClothCollection.IsValid()) { FCollectionClothConstFacade SimClothFacade(SimClothCollection.ToSharedRef()); if (SimClothFacade.IsValid()) { TargetRefSkeleton = GetCollectionRefSkeleton(SimClothFacade); } if (!TargetRefSkeleton) { FClothDataflowTools::LogAndToastWarning(*this, LOCTEXT("InvalidSimRefSkeletonHeadline", "Invalid Reference Skeleton."), LOCTEXT("InvalidSimRefSkeletonDetails", "Couldn't find a valid reference skeleton from the input sim collection.")); SetValue(Context, MoveTemp(*ClothCollection), &Collection); return; } } else { TargetRefSkeleton = GetCollectionRefSkeleton(ClothFacade); if (!TargetRefSkeleton) { FClothDataflowTools::LogAndToastWarning(*this, LOCTEXT("InvalidRefSkeletonHeadline", "Invalid Reference Skeleton."), LOCTEXT("InvalidRefSkeletonDetails", "Couldn't find a valid reference skeleton from the input collection.")); SetValue(Context, MoveTemp(*ClothCollection), &Collection); return; } } } // Clean up orphaned vertices UE::Chaos::ClothAsset::FClothGeometryTools::CleanupAndCompactMesh(ClothCollection); if (SimClothCollection.IsValid()) { UE::Chaos::ClothAsset::FClothGeometryTools::CleanupAndCompactMesh(SimClothCollection.ToSharedRef()); } // // Setup the bone weight transfer settings. // TransferBoneWeightsSettings TransferSettings; TransferSettings.bTransferToSim = TargetMeshType != EChaosClothAssetTransferTargetMeshType::Render; TransferSettings.bTransferToRender = TargetMeshType != EChaosClothAssetTransferTargetMeshType::Simulation; TransferSettings.bTransferToRenderFromSim = RenderMeshSourceType == EChaosClothAssetTransferRenderMeshSource::SimulationMesh; TransferSettings.bUseParallel = true; TransferSettings.MaxNumInfluences = static_cast(FChaosClothAssetTransferSkinWeightsNode::MaxNumInfluences); TransferSettings.TransferMethod = static_cast(TransferMethod); TransferSettings.NormalThreshold = NormalThreshold; TransferSettings.RadiusPercentage = RadiusPercentage; TransferSettings.LayeredMeshSupport = LayeredMeshSupport; TransferSettings.NumSmoothingIterations = NumSmoothingIterations; TransferSettings.SmoothingStrength = SmoothingStrength; TransferSettings.InpaintMaskWeightMapName = GetValue(Context, &InpaintMask.WeightMap); bool bTransferResult = false; switch (TransferMethod) { case EChaosClothAssetTransferSkinWeightsMethod::InpaintWeights: bTransferResult = TransferInpaintWeights(*TargetRefSkeleton, SourceDynamicMesh, ClothCollection, TransferSettings, SimClothCollection); break; case EChaosClothAssetTransferSkinWeightsMethod::ClosestPointOnSurface: bTransferResult = TransferClosestPointOnSurface(*TargetRefSkeleton, SourceDynamicMesh, ClothCollection, TransferSettings, SimClothCollection); break; default: checkNoEntry(); break; } if (!bTransferResult) { FClothDataflowTools::LogAndToastWarning(*this, LOCTEXT("TransferWeightsFailedHeadline", "Transfer Weights Failed."), LOCTEXT("TransferWeightsDetails", "Failed to transfer skinning weights from the source.")); SetValue(Context, MoveTemp(*ClothCollection), &Collection); return; } // Optional check to make sure all vertices adhere to the bone influence limit constexpr bool bCheckMaxInfluenceComplience = false; if (bCheckMaxInfluenceComplience) { for (int32 VID = 0; VID < ClothFacade.GetNumSimVertices3D(); ++VID) { const int32 NumInfluences = ClothFacade.GetSimBoneIndices()[VID].Num(); if (!ensure(NumInfluences <= static_cast(FChaosClothAssetTransferSkinWeightsNode::MaxNumInfluences))) { UE_LOG(LogChaosClothAssetDataflowNodes, Warning, TEXT("TransferSkinWeightsNode: Maximum number of influences is exceeded for sim vertex %i."), VID); } } for (int32 VID = 0; VID < ClothFacade.GetNumRenderVertices(); ++VID) { const int32 NumInfluences = ClothFacade.GetRenderBoneIndices()[VID].Num(); if (!ensure(NumInfluences <= static_cast(FChaosClothAssetTransferSkinWeightsNode::MaxNumInfluences))) { UE_LOG(LogChaosClothAssetDataflowNodes, Warning, TEXT("TransferSkinWeightsNode: Maximum number of influences is exceeded for render vertex %i."), VID); } } } } SetValue(Context, MoveTemp(*ClothCollection), &Collection); } } #undef LOCTEXT_NAMESPACE