// Copyright Epic Games, Inc. All Rights Reserved. #include "Operations/TransferBoneWeights.h" #include "DynamicMesh/DynamicMesh3.h" #include "DynamicMesh/DynamicVertexSkinWeightsAttribute.h" #include "DynamicMesh/DynamicBoneAttribute.h" #include "DynamicMesh/DynamicMeshAttributeSet.h" #include "DynamicMesh/DynamicMeshAABBTree3.h" #include "DynamicMesh/MeshNormals.h" #include "Async/ParallelFor.h" #include "Util/ProgressCancel.h" #include "BoneIndices.h" #include "IndexTypes.h" #include "TransformTypes.h" #include "Algo/Count.h" #include "Solvers/Internal/QuadraticProgramming.h" #include "Solvers/LaplacianMatrixAssembly.h" #include "Operations/SmoothBoneWeights.h" using namespace UE::AnimationCore; using namespace UE::Geometry; namespace TransferBoneWeightsLocals { /** * Given a triangle and point on a triangle (via barycentric coordinates), compute the bone weights for the point. * * @param OutWeights Interpolated weights for a vertex with Bary barycentric coordinates * @param TriVertices The vertices of a triangle containing the point we are interpolating the weights for * @param Bary Barycentric coordinates of the point * @param Attribute Attribute containing bone weights of the mesh that TriVertices belong to * @param MaxNumInfluences The maximum allowed number of influences per vertex stored in OutWeights * @param SourceIndexToBone Optional map from bone index to bone name for the source mesh * @param TargetBoneToIndex OPtional map from bone name to bone index for the target mesh * @param bNormalizeToOne If true, OutWeights will be normalized to sum to 1. */ void InterpolateBoneWeights(FBoneWeights& OutWeights, const FIndex3i& TriVertices, const FVector3f& Bary, const FDynamicMeshVertexSkinWeightsAttribute* Attribute, const int32 MaxNumInfluences, const TArray* SourceIndexToBone = nullptr, const TMap* TargetBoneToIndex = nullptr, bool bNormalizeToOne = true) { FBoneWeights Weight1, Weight2, Weight3; Attribute->GetValue(TriVertices[0], Weight1); Attribute->GetValue(TriVertices[1], Weight2); Attribute->GetValue(TriVertices[2], Weight3); FBoneWeightsSettings BlendSettings; BlendSettings.SetNormalizeType(bNormalizeToOne ? EBoneWeightNormalizeType::Always : EBoneWeightNormalizeType::None); BlendSettings.SetBlendZeroInfluence(true); BlendSettings.SetMaxWeightCount(MaxNumInfluences); OutWeights = FBoneWeights::Blend(Weight1, Weight2, Weight3, Bary[0], Bary[1], Bary[2], BlendSettings); // TODO: Blend method can potentially skip applying renormalization and prunning weights to match MaxNumInfluences // using the BlendSettings so force renormalization. Remove this once FBoneWeights::Blend is fixed. OutWeights.Renormalize(BlendSettings); // Check if we need to remap the indices if (SourceIndexToBone && TargetBoneToIndex) { FBoneWeightsSettings BoneSettings; BoneSettings.SetNormalizeType(EBoneWeightNormalizeType::None); FBoneWeights MappedWeights; for (int32 WeightIdx = 0; WeightIdx < OutWeights.Num(); ++WeightIdx) { const FBoneWeight& BoneWeight = OutWeights[WeightIdx]; FBoneIndexType FromIdx = BoneWeight.GetBoneIndex(); uint16 FromWeight = BoneWeight.GetRawWeight(); checkSlow(FromIdx < SourceIndexToBone->Num()); if (FromIdx < SourceIndexToBone->Num()) { FName BoneName = (*SourceIndexToBone)[FromIdx]; if (TargetBoneToIndex->Contains(BoneName)) { FBoneIndexType ToIdx = (*TargetBoneToIndex)[BoneName]; FBoneWeight MappedBoneWeight(ToIdx, FromWeight); MappedWeights.SetBoneWeight(MappedBoneWeight, BoneSettings); } else { UE_LOG(LogGeometry, Error, TEXT("FTransferBoneWeights: Bone name %s does not exist in the target mesh."), *BoneName.ToString()); } } } if (MappedWeights.Num() == 0) { // If no bone mappings were found, add a single entry for the root bone MappedWeights.SetBoneWeight(FBoneWeight(0, 1.0f), FBoneWeightsSettings()); } else if (OutWeights.Num() != MappedWeights.Num() && bNormalizeToOne) { // In case some of the bones were not mapped we need to renormalize MappedWeights.Renormalize(FBoneWeightsSettings()); } OutWeights = MappedWeights; } } FDynamicMeshVertexSkinWeightsAttribute* GetOrCreateSkinWeightsAttribute(FDynamicMesh3& InMesh, const FName& InProfileName) { checkSlow(InMesh.HasAttributes()); FDynamicMeshVertexSkinWeightsAttribute* Attribute = InMesh.Attributes()->GetSkinWeightsAttribute(InProfileName); if (Attribute == nullptr) { Attribute = new FDynamicMeshVertexSkinWeightsAttribute(&InMesh); InMesh.Attributes()->AttachSkinWeightsAttribute(InProfileName, Attribute); } return Attribute; } static FVector3f ToUENormal(const FVector3d& Normal) { return FVector3f((float)Normal.X, (float)Normal.Y, (float)Normal.Z); } } FTransferBoneWeights::FTransferBoneWeights(const FDynamicMesh3* InSourceMesh, const FName& InSourceProfileName, const FDynamicMeshAABBTree3* InSourceBVH) : SourceMesh(InSourceMesh), SourceProfileName(InSourceProfileName), SourceBVH(InSourceBVH) { // If the BVH for the source mesh was not specified then create one if (SourceBVH == nullptr) { InternalSourceBVH = MakeUnique(SourceMesh); } } FTransferBoneWeights::~FTransferBoneWeights() { } bool FTransferBoneWeights::Cancelled() { return (Progress == nullptr) ? false : Progress->Cancelled(); } EOperationValidationResult FTransferBoneWeights::Validate() { if (SourceMesh == nullptr) { return EOperationValidationResult::Failed_UnknownReason; } // Either BVH was passed by the caller or was created internally in the constructor if (SourceBVH == nullptr && InternalSourceBVH.IsValid() == false) { return EOperationValidationResult::Failed_UnknownReason; } if (SourceMesh->HasAttributes() == false) { return EOperationValidationResult::Failed_UnknownReason; } if (SourceMesh->Attributes()->GetSkinWeightsAttribute(SourceProfileName) == nullptr) { return EOperationValidationResult::Failed_UnknownReason; } if (bIgnoreBoneAttributes == false && SourceMesh->Attributes()->HasBones() == false) { return EOperationValidationResult::Failed_UnknownReason; } return EOperationValidationResult::Ok; } bool FTransferBoneWeights::TransferWeightsToMesh(FDynamicMesh3& InOutTargetMesh, const FName& InTargetProfileName) { using TransferBoneWeightsLocals::ToUENormal; if (Validate() != EOperationValidationResult::Ok) { return false; } if (!InOutTargetMesh.HasAttributes()) { InOutTargetMesh.EnableAttributes(); } if (!bIgnoreBoneAttributes && !InOutTargetMesh.Attributes()->HasBones()) { return false; // the target mesh must have bone attributes } // If we need to compare normals, make sure both the target and the source meshes have per-vertex normals data TUniquePtr InternalTargetMeshNormals; if (NormalThreshold >= 0) { if (!SourceMesh->HasVertexNormals() && !InternalSourceMeshNormals) { // only do this once for the source mesh in case of subsequent calls to the method InternalSourceMeshNormals = MakeUnique(SourceMesh); InternalSourceMeshNormals->ComputeVertexNormals(); } if (!InOutTargetMesh.HasVertexNormals()) { InternalTargetMeshNormals = MakeUnique(&InOutTargetMesh); InternalTargetMeshNormals->ComputeVertexNormals(); } } FDynamicMeshVertexSkinWeightsAttribute* TargetSkinWeights = TransferBoneWeightsLocals::GetOrCreateSkinWeightsAttribute(InOutTargetMesh, InTargetProfileName); checkSlow(TargetSkinWeights); // Map the bone name to its index for the target mesh. // Will be null if either the target and the source skeletons are the same or the caller forced the attributes to be ignored TUniquePtr> TargetBoneToIndex; if (!bIgnoreBoneAttributes) { const TArray& SourceBoneNames = SourceMesh->Attributes()->GetBoneNames()->GetAttribValues(); const TArray& TargetBoneNames = InOutTargetMesh.Attributes()->GetBoneNames()->GetAttribValues(); if (SourceBoneNames != TargetBoneNames) { TargetBoneToIndex = MakeUnique>(); TargetBoneToIndex->Reserve(TargetBoneNames.Num()); for (int32 BoneID = 0; BoneID < TargetBoneNames.Num(); ++BoneID) { const FName& BoneName = TargetBoneNames[BoneID]; if (TargetBoneToIndex->Contains(BoneName)) { checkSlow(false); return false; // there should be no duplicates } TargetBoneToIndex->Add(BoneName, static_cast(BoneID)); } } } bool bFailed = false; // compute the transfer only for the subset of vertices if necessary const bool bUseSubset = !TargetVerticesSubset.IsEmpty(); const int32 NumVerticesToTransfer = bUseSubset ? TargetVerticesSubset.Num() : InOutTargetMesh.MaxVertexID(); if (TransferMethod == ETransferBoneWeightsMethod::ClosestPointOnSurface) { MatchedVertices.Init(false, NumVerticesToTransfer); ParallelFor(NumVerticesToTransfer, [this, &InOutTargetMesh, &TargetBoneToIndex, &TargetSkinWeights, &InternalTargetMeshNormals, bUseSubset](int32 InVertexID) { if (Cancelled()) { return; } const int32 VertexID = bUseSubset ? TargetVerticesSubset[InVertexID] : InVertexID; if (InOutTargetMesh.IsVertex(VertexID)) { const FVector3d Point = InOutTargetMesh.GetVertex(VertexID); FVector3f Normal = FVector3f::UnitY(); if (NormalThreshold >= 0) { const bool bHasNormals = InOutTargetMesh.HasVertexNormals(); if (ensure(bHasNormals || InternalTargetMeshNormals)) { Normal = bHasNormals ? InOutTargetMesh.GetVertexNormal(VertexID) : ToUENormal(InternalTargetMeshNormals->GetNormals()[VertexID]); } } FBoneWeights Weights; if (TransferWeightsToPoint(Weights, Point, TargetBoneToIndex.Get(), Normal)) { TargetSkinWeights->SetValue(VertexID, Weights); MatchedVertices[VertexID] = true; } } }, bUseParallel ? EParallelForFlags::None : EParallelForFlags::ForceSingleThread); // If the caller requested to simply find the closest point for all vertices then the number of matched vertices // must be equal to the target mesh vertex count if (SearchRadius < 0 && NormalThreshold < 0) { int32 NumMatched = 0; for (bool Flag : MatchedVertices) { if (Flag) { NumMatched++; } } bFailed = NumMatched != NumVerticesToTransfer; } } else if (TransferMethod == ETransferBoneWeightsMethod::InpaintWeights) { /** * Given two meshes, Mesh1 without weights and Mesh2 with weights, assume they are aligned in 3d space. * For every vertex on Mesh1 find the closest point on the surface of Mesh2 within a radius R. If the difference * between the normals of the two points is below the threshold, then it's a match. Otherwise no match. * So now we have two sets of vertices on Mesh1. One with a match on the source mesh and one without a match. * For all the vertices with a match, copy weights over. For all the vertices without the match, do nothing. * Now, for all the vertices without a match, try to approximate the weights by smoothly interpolating between * the weights at the known vertices via solving a quadratic problem. * * The solver minimizes an energy * trace(W^t Q W) * W \in R^(nxm) is a matrix where n is the number of vertices and m is the number of bones. So (i,j) entry is * the influence (weight) of a vertex i by bone j * Q \in R^(nxn) is a matrix that combines both Dirichlet and Laplacian energies, Q = -L + L*M^(-1)*L * where L is a cotangent Laplacian and M is a mass matrix * * subject to constraints * All weights at a single vertex sum to 1: sum(W(i,:)) = 1 * All weights must be non-negative: W(i,j) >=0 for any i, j * Any vertex for which we found a match must have fixed weights that can't be changed, * i.e. W(i,j) = KnownWeights(i,j) where i is a vertex for which we found a match on the body. */ MatchedVertices.Init(false, InOutTargetMesh.MaxVertexID()); // Check if the target mesh contains the user specifed force inpaint weight map const FDynamicMeshWeightAttribute* ForceInpaintLayer = nullptr; if (!ForceInpaintWeightMapName.IsNone() && (ForceInpaint.IsEmpty() || ForceInpaint.Num() != InOutTargetMesh.MaxVertexID())) // ForceInpaint array takes priority if valid { for (int32 Idx = 0; Idx < InOutTargetMesh.Attributes()->NumWeightLayers(); ++Idx) { const FDynamicMeshWeightAttribute* WeightLayer = InOutTargetMesh.Attributes()->GetWeightLayer(Idx); if (WeightLayer && WeightLayer->GetName() == ForceInpaintWeightMapName) { ForceInpaintLayer = WeightLayer; break; } } } // because the inpaint algorithm can extract data from regions outside the target vertex subset, a temporary attribute profile is used to modify the weights. // NOTE: make sure to copy the weights of the vertex subset into the complete TargetSkinWeights attribute before exciting the function. (see CopySubsetWeightsIfNeeded) FDynamicMeshVertexSkinWeightsAttribute SubsetTargetSkinWeights; if (bUseSubset) { SubsetTargetSkinWeights.Copy(*TargetSkinWeights); } FDynamicMeshVertexSkinWeightsAttribute* EditedSkinWeights = bUseSubset ? &SubsetTargetSkinWeights : TargetSkinWeights; // For every vertex on the target mesh try to find the match on the source mesh using the distance and normal checks ParallelFor(InOutTargetMesh.MaxVertexID(), [this, &InOutTargetMesh, &ForceInpaintLayer, &TargetBoneToIndex, &EditedSkinWeights, &InternalTargetMeshNormals](int32 VertexID) { if (Cancelled()) { return; } if (InOutTargetMesh.IsVertex(VertexID)) { // check if we need to force the vertex to not have a match if (ForceInpaint.Num() == InOutTargetMesh.MaxVertexID() && ForceInpaint[VertexID] != 0) { return; } else if (ForceInpaintLayer != nullptr) { float Value; ForceInpaintLayer->GetValue(VertexID, &Value); if (Value != 0) { return; } } const FVector3d Point = InOutTargetMesh.GetVertex(VertexID); FVector3f Normal = FVector3f::UnitY(); if (NormalThreshold >= 0) { const bool bHasNormals = InOutTargetMesh.HasVertexNormals(); if (ensure(bHasNormals || InternalTargetMeshNormals.IsValid())) { Normal = bHasNormals ? InOutTargetMesh.GetVertexNormal(VertexID) : ToUENormal(InternalTargetMeshNormals->GetNormals()[VertexID]); } } FBoneWeights Weights; if (TransferWeightsToPoint(Weights, Point, TargetBoneToIndex.Get(), Normal)) { EditedSkinWeights->SetValue(VertexID, Weights); MatchedVertices[VertexID] = true; } } }, bUseParallel ? EParallelForFlags::None : EParallelForFlags::ForceSingleThread); if (Cancelled()) { return false; } int32 NumMatched = 0; if (!bUseSubset) { for (bool Flag : MatchedVertices) { if (Flag) { NumMatched++; } } } else { NumMatched = (int32)Algo::CountIf(TargetVerticesSubset, [this](int32 VertexID) { return MatchedVertices.IsValidIndex(VertexID) && MatchedVertices[VertexID]; }); } // If no vertices matched, we have nothing to inpaint. if (NumMatched == 0) { return false; } auto CopySubsetWeightsIfNeeded = [Subset = TargetVerticesSubset, &EditedSkinWeights, &TargetSkinWeights, &InOutTargetMesh]() { if (EditedSkinWeights && EditedSkinWeights != TargetSkinWeights) { for (const int32 VertexID: Subset) { if (InOutTargetMesh.IsVertex(VertexID)) { FBoneWeights Weights; EditedSkinWeights->GetValue(VertexID, Weights); TargetSkinWeights->SetValue(VertexID, Weights); } } } }; // If all vertices were matched then nothing else to do if (NumMatched == NumVerticesToTransfer) { // copy weights from the subset skin weights if using subset CopySubsetWeightsIfNeeded(); return true; } // Compute linearization so we can store constraints at linearized indices FVertexLinearization VtxLinearization(InOutTargetMesh, false); const TArray& ToMeshV = VtxLinearization.ToId(); const TArray& ToIndex = VtxLinearization.ToIndex(); // Setup the sparse matrix FixedValues of known (matched) weight values and the array (FixedIndices) of the matched vertex IDs const int32 TargetNumBones = InOutTargetMesh.Attributes()->GetBoneNames()->GetAttribValues().Num(); FSparseMatrixD FixedValues; FixedValues.resize(NumMatched, TargetNumBones); std::vector> FixedValuesTriplets; FixedValuesTriplets.reserve(NumMatched); TArray FixedIndices; FixedIndices.Reserve(NumMatched); for (int32 VertexID = 0; VertexID < InOutTargetMesh.MaxVertexID(); ++VertexID) { if (InOutTargetMesh.IsVertex(VertexID) && MatchedVertices[VertexID]) { FBoneWeights Data; EditedSkinWeights->GetValue(VertexID, Data); const int32 NumBones = Data.Num(); checkSlow(NumBones > 0); const int32 CurIdx = FixedIndices.Num(); for (int32 BoneID = 0; BoneID < NumBones; ++BoneID) { const int BoneIdx = Data[BoneID].GetBoneIndex(); const double BoneWeight = Data[BoneID].GetWeight(); FixedValuesTriplets.emplace_back(CurIdx, BoneIdx, BoneWeight); } checkSlow(VertexID < ToIndex.Num()); FixedIndices.Add(ToIndex[VertexID]); } } FixedValues.setFromTriplets(FixedValuesTriplets.begin(), FixedValuesTriplets.end()); const int32 NumVerts = VtxLinearization.NumVerts(); FEigenSparseMatrixAssembler CotangentAssembler(NumVerts, NumVerts); FEigenSparseMatrixAssembler LaplacianAssembler(NumVerts, NumVerts); UE::Tasks::FTask CotangentTask, LaplacianTask; if (bUseIntrinsicLaplacian) { CotangentTask = UE::Tasks::Launch(UE_SOURCE_LOCATION, [&InOutTargetMesh, &VtxLinearization, &CotangentAssembler]() { // Construct the Cotangent weights matrix UE::MeshDeformation::ConstructFullIDTCotangentLaplacian(InOutTargetMesh, VtxLinearization, CotangentAssembler, UE::MeshDeformation::ECotangentWeightMode::Default, UE::MeshDeformation::ECotangentAreaMode::NoArea); }); LaplacianTask = UE::Tasks::Launch(UE_SOURCE_LOCATION, [&InOutTargetMesh, &VtxLinearization, &LaplacianAssembler]() { // Construct the Laplacian with cotangent weights scaled by the voronoi area (i.e. M^(-1)*L matrix where M is the mass/stiffness matrix) UE::MeshDeformation::ConstructFullIDTCotangentLaplacian(InOutTargetMesh, VtxLinearization, LaplacianAssembler, UE::MeshDeformation::ECotangentWeightMode::Default, UE::MeshDeformation::ECotangentAreaMode::VoronoiArea); }); } else { CotangentTask = UE::Tasks::Launch(UE_SOURCE_LOCATION, [&InOutTargetMesh, &VtxLinearization, &CotangentAssembler]() { UE::MeshDeformation::ConstructFullCotangentLaplacian(InOutTargetMesh, VtxLinearization, CotangentAssembler, UE::MeshDeformation::ECotangentWeightMode::Default, UE::MeshDeformation::ECotangentAreaMode::NoArea); }); LaplacianTask = UE::Tasks::Launch(UE_SOURCE_LOCATION, [&InOutTargetMesh, &VtxLinearization, &LaplacianAssembler]() { UE::MeshDeformation::ConstructFullCotangentLaplacian(InOutTargetMesh, VtxLinearization, LaplacianAssembler, UE::MeshDeformation::ECotangentWeightMode::Default, UE::MeshDeformation::ECotangentAreaMode::VoronoiArea); }); } FSparseMatrixD CotangentMatrix, MassCotangentMatrix; UE::Tasks::FTask CotangentExtractTask = UE::Tasks::Launch(UE_SOURCE_LOCATION, [&CotangentMatrix, &CotangentAssembler]() { CotangentAssembler.ExtractResult(CotangentMatrix); }, CotangentTask); UE::Tasks::FTask LaplacianExtractTask = UE::Tasks::Launch(UE_SOURCE_LOCATION, [&MassCotangentMatrix, &LaplacianAssembler]() { LaplacianAssembler.ExtractResult(MassCotangentMatrix); }, LaplacianTask); TArray PendingTasks = { CotangentExtractTask, LaplacianExtractTask }; UE::Tasks::Wait(PendingTasks); // -L * L* M^(-1)*L energy FSparseMatrixD Energy = -1*CotangentMatrix + CotangentMatrix*MassCotangentMatrix; // Solve the QP problem with fixed constraints FSparseMatrixD TargetWeights; TArray VaribleRows; // We want the solution TargetWeights matrix to only contain the rows representing the variable (non-fixed) rows constexpr bool bVariablesOnly = true; bFailed = !FQuadraticProgramming::SolveWithFixedConstraints(Energy, nullptr, FixedIndices, FixedValues, TargetWeights, bVariablesOnly, KINDA_SMALL_NUMBER, &VaribleRows); checkSlow((VaribleRows.Num() + FixedIndices.Num()) == Energy.rows()); if (!bFailed) { // Transpose so we can efficiently iterate over the col-major matrix. Each column now contains per-vertex weights. // Otherwise, we are iterating over rows of a col-major matrix which is slow. FSparseMatrixD TargetWeightsTransposed = TargetWeights.transpose(); FBoneWeightsSettings BoneSettings; BoneSettings.SetNormalizeType(EBoneWeightNormalizeType::None); FBoneWeightsSettings RenormalizeBoneSettings; RenormalizeBoneSettings.SetMaxWeightCount(MaxNumInfluences); // Iterate over every column containing all bone weights for the vertex for (int32 ColIdx = 0; ColIdx < TargetWeightsTransposed.outerSize(); ++ColIdx) { FBoneWeights WeightArray; // Iterate over only non-zero rows (i.e. bone indices with non-zero weights) for (FSparseMatrixD::InnerIterator Itr(TargetWeightsTransposed, ColIdx); Itr; ++Itr) { const FBoneIndexType BoneIdx = static_cast(Itr.row()); const float Weight = static_cast(Itr.value()); FBoneWeight Bweight(BoneIdx, Weight); WeightArray.SetBoneWeight(Bweight, BoneSettings); } WeightArray.Renormalize(RenormalizeBoneSettings); const int32 VertexIDLinearalized = bVariablesOnly ? static_cast(VaribleRows[ColIdx]) : ColIdx; // linearized vertex ID (matrix row) of the variable in the Energy matrix const int32 VertexID = static_cast(ToMeshV[VertexIDLinearalized]); EditedSkinWeights->SetValue(VertexID, WeightArray); } // copy weights from the subset skin weights if using subset CopySubsetWeightsIfNeeded(); // Optional post-processing smoothing of the weights at the vertices without a match if (NumSmoothingIterations > 0 && SmoothingStrength > 0) { TArray VerticesToSmooth; const int32 NumNotMatched = InOutTargetMesh.VertexCount() - NumMatched; VerticesToSmooth.Reserve(NumNotMatched); for (int32 VertexID = 0; VertexID < InOutTargetMesh.MaxVertexID(); ++VertexID) { if (InOutTargetMesh.IsVertex(VertexID) && !MatchedVertices[VertexID]) { VerticesToSmooth.Add(VertexID); } } FSmoothDynamicMeshVertexSkinWeights SmoothWeights(&InOutTargetMesh, InTargetProfileName); SmoothWeights.MaxNumInfluences = MaxNumInfluences; if (ensure(SmoothWeights.Validate() == EOperationValidationResult::Ok)) { ensure(SmoothWeights.SmoothWeightsAtVerticesWithinDistance(VerticesToSmooth, SmoothingStrength, SearchRadius, NumSmoothingIterations)); } } } } else { checkNoEntry(); // unsupported method } if (Cancelled() || bFailed) { return false; } return true; } bool FTransferBoneWeights::TransferWeightsToPoint(UE::AnimationCore::FBoneWeights& OutWeights, const FVector3d& InPoint, const TMap* TargetBoneToIndex, const FVector3f& InNormal) { using TransferBoneWeightsLocals::ToUENormal; // Find the containing triangle and the barycentric coordinates of the closest point int32 TriID; FVector3d Bary; if (!FindClosestPointOnSourceSurface(InPoint, TargetToWorld, TriID, Bary)) { return false; } FVector3f BaryF = FVector3f((float)Bary[0], (float)Bary[1], (float)Bary[2]); const FIndex3i TriVertex = SourceMesh->GetTriangle(TriID); const FDynamicMeshVertexSkinWeightsAttribute* SourceSkinWeights = SourceMesh->Attributes()->GetSkinWeightsAttribute(SourceProfileName); const TArray* SourceBoneNames = nullptr; if (!bIgnoreBoneAttributes) { SourceBoneNames = &SourceMesh->Attributes()->GetBoneNames()->GetAttribValues(); } if (SearchRadius < 0 && NormalThreshold < 0) { // If the radius and normals are ignored, simply interpolate the weights and return the result TransferBoneWeightsLocals::InterpolateBoneWeights(OutWeights, TriVertex, BaryF, SourceSkinWeights, MaxNumInfluences, SourceBoneNames, TargetBoneToIndex); } else { bool bPassedRadiusCheck = true; if (SearchRadius >= 0) { const FVector3d MatchedPoint = SourceMesh->GetTriBaryPoint(TriID, Bary[0], Bary[1], Bary[2]); bPassedRadiusCheck = (InPoint - MatchedPoint).Length() <= SearchRadius; } bool bPassedNormalsCheck = true; if (NormalThreshold >= 0) { FVector3f Normal0 = FVector3f::UnitY(); FVector3f Normal1 = FVector3f::UnitY(); FVector3f Normal2 = FVector3f::UnitY(); const bool bHasSourceNormals = SourceMesh->HasVertexNormals(); if (ensure(bHasSourceNormals || InternalSourceMeshNormals.IsValid())) { Normal0 = bHasSourceNormals ? SourceMesh->GetVertexNormal(TriVertex[0]) : ToUENormal(InternalSourceMeshNormals->GetNormals()[TriVertex[0]]); Normal1 = bHasSourceNormals ? SourceMesh->GetVertexNormal(TriVertex[1]) : ToUENormal(InternalSourceMeshNormals->GetNormals()[TriVertex[1]]); Normal2 = bHasSourceNormals ? SourceMesh->GetVertexNormal(TriVertex[2]) : ToUENormal(InternalSourceMeshNormals->GetNormals()[TriVertex[2]]); } const FVector3f MatchedNormal = Normalized(BaryF[0]*Normal0 + BaryF[1]*Normal1 + BaryF[2]*Normal2); const FVector3f InNormalNormalized = Normalized(InNormal); const float NormalAngle = FMathf::ACos(InNormalNormalized.Dot(MatchedNormal)); bPassedNormalsCheck = (double)NormalAngle <= NormalThreshold; if (!bPassedNormalsCheck && LayeredMeshSupport) { // try again with a flipped normal bPassedNormalsCheck = (double)(TMathUtil::Pi - NormalAngle) <= NormalThreshold; } } if (bPassedRadiusCheck && bPassedNormalsCheck) { TransferBoneWeightsLocals::InterpolateBoneWeights(OutWeights, TriVertex, BaryF, SourceSkinWeights, MaxNumInfluences, SourceBoneNames, TargetBoneToIndex); } else { return false; } } return true; } template bool FTransferBoneWeights::TransferWeightsToPoint(TArray& OutBones, TArray& OutWeights, const UE::Math::TVector& InPoint, const TMap* TargetBoneToIndex, const UE::Math::TVector& InNormal) { FBoneWeights BoneWeights; if (!this->TransferWeightsToPoint(BoneWeights, FVector3d(InPoint.X, InPoint.Y, InPoint.Z), TargetBoneToIndex, FVector3f((float)InNormal.X, (float)InNormal.Y, (float)InNormal.Z))) { return false; } const int32 NumEntries = BoneWeights.Num(); OutBones.SetNum(NumEntries); OutWeights.SetNum(NumEntries); for (int32 BoneIdx = 0; BoneIdx < NumEntries; ++BoneIdx) { OutBones[BoneIdx] = static_cast(BoneWeights[BoneIdx].GetBoneIndex()); OutWeights[BoneIdx] = static_cast(BoneWeights[BoneIdx].GetWeight()); } return true; } bool FTransferBoneWeights::FindClosestPointOnSourceSurface(const FVector3d& InPoint, const FTransformSRT3d& InToWorld, int32& NearTriID, FVector3d& Bary) { IMeshSpatial::FQueryOptions Options; double NearestDistSqr; const FVector3d WorldPoint = InToWorld.TransformPosition(InPoint); if (SourceBVH != nullptr) { NearTriID = SourceBVH->FindNearestTriangle(WorldPoint, NearestDistSqr, Options); } else { NearTriID = InternalSourceBVH->FindNearestTriangle(WorldPoint, NearestDistSqr, Options); } if (!ensure(NearTriID != IndexConstants::InvalidID)) { return false; } const FDistPoint3Triangle3d Query = TMeshQueries::TriangleDistance(*SourceMesh, NearTriID, WorldPoint); const FVector3d NearestPnt = Query.ClosestTrianglePoint; const FIndex3i TriVertex = SourceMesh->GetTriangle(NearTriID); Bary = VectorUtil::BarycentricCoords(NearestPnt, SourceMesh->GetVertexRef(TriVertex.A), SourceMesh->GetVertexRef(TriVertex.B), SourceMesh->GetVertexRef(TriVertex.C)); return true; } // template instantiation template DYNAMICMESH_API bool UE::Geometry::FTransferBoneWeights::TransferWeightsToPoint(class TArray > &,class TArray > &,struct UE::Math::TVector const &,class TMap > const *,struct UE::Math::TVector const &);