// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "DynamicMesh/DynamicMesh3.h" #include "Util/IndexUtil.h" #include "Solvers/MatrixInterfaces.h" #include "Solvers/MeshLinearization.h" #include "Solvers/MeshLaplacian.h" #include "Solvers/PrecomputedMeshWeightData.h" #include "Operations/IntrinsicTriangulationMesh.h" namespace UE { namespace MeshDeformation { using namespace UE::Geometry; /** * Construct a sparse matrix representation of a uniform weighted Laplacian. * The uniform weighted Laplacian is defined solely in terms of the connectivity * of the mesh. Note, by construction this should be a symmetric matrix. * * The mesh itself is assumed to have N interior vertices, and M boundary vertices. * * Row i represents the Laplacian at vert_i, the non-zero entries correspond to * the incident one-ring vertices vert_j. * * L_{ij} = 1 if vert_j is in the one-ring of vert_i * L_{ii} = -Sum{ L_{ij}, j != i} * * * @param DynamicMesh The triangle mesh * @param VertexMap On return, Additional arrays used to map between vertexID and offset in a linear array (i.e. the row). * The vertices are ordered so that last M ( = VertexMap.NumBoundaryVerts() ) correspond to those on the boundary. * @param LaplacianInterior On return, the laplacian operator that acts on the interior vertices: sparse N x N matrix * @param LaplacianBoundary On return, the portion of the operator that acts on the boundary vertices: sparse N x M matrix * * LaplacianInterior * Vector_InteriorVerts + LaplacianBoundary * Vector_BoundaryVerts = Full Laplacian applied to interior vertices. */ template void ConstructUniformLaplacian(const MeshType& Mesh, const FVertexLinearization& VertexMap, UE::Solvers::TSparseMatrixAssembler& LaplacianInterior, UE::Solvers::TSparseMatrixAssembler& LaplacianBoundary); /** * Construct a sparse matrix representation of an umbrella weighted Laplacian. * This Laplacian is defined solely in terms of the connectivity * of the mesh. Note, there is no expectation that the resulting matrix will be symmetric. * * The mesh itself is assumed to have N interior vertices, and M boundary vertices. * * Row i represents the Laplacian at vert_i, the non-zero entries correspond to * the incident one-ring vertices vert_j. * * L_{ij} = 1 / valence(of i) if vert_j is in the one-ring of vert_i * L_{ii} = -Sum{ L_{ij}, j != i} = -1 * * * @param DynamicMesh The triangle mesh * @param VertexMap Additional arrays used to map between vertexID and offset in a linear array (i.e. the row). * The vertices are ordered so that last M ( = VertexMap.NumBoundaryVerts() ) correspond to those on the boundary. * @param LaplacianInterior On return, the laplacian operator that acts on the interior vertices: sparse N x N matrix * @param LaplacianBoundary On return, the portion of the operator that acts on the boundary vertices: sparse N x M matrix * * LaplacianInterior * Vector_InteriorVerts + LaplacianBoundary * Vector_BoundaryVerts = Full Laplacian applied to interior vertices. */ template void ConstructUmbrellaLaplacian(const FDynamicMesh3& DynamicMesh, const FVertexLinearization& VertexMap, UE::Solvers::TSparseMatrixAssembler& LaplacianInterior, UE::Solvers::TSparseMatrixAssembler& LaplacianBoundary); /** * Construct a sparse matrix representation of a valence-weighted Laplacian. * The valence weighted Laplacian is defined solely in terms of the connectivity * of the mesh. Note, by construction this should be a symmetric matrix. * * The mesh itself is assumed to have N interior vertices, and M boundary vertices. * * Row i represents the Laplacian at vert_i, the non-zero entries correspond to * the incident one-ring vertices vert_j. * * L_{ij} = 1/\sqrt(valence(i) + valence(j)) if vert_j is in the one-ring of vert_i * L_{ii} = -Sum{ L_{ij}, j != i} * * @param DynamicMesh The triangle mesh * @param VertexMap Additional arrays used to map between vertexID and offset in a linear array (i.e. the row). * The vertices are ordered so that last M ( = VertexMap.NumBoundaryVerts() ) correspond to those on the boundary. * @param LaplacianInterior On return, the laplacian operator that acts on the interior vertices: sparse N x N matrix * @param LaplacianBoundary On return, the portion of the operator that acts on the boundary vertices: sparse N x M matrix * * LaplacianInterior * Vector_InteriorVerts + LaplacianBoundary * Vector_BoundaryVerts = Full Laplacian applied to interior vertices. */ template void ConstructValenceWeightedLaplacian(const FDynamicMesh3& DynamicMesh, const FVertexLinearization& VertexMap, UE::Solvers::TSparseMatrixAssembler& LaplacianInterior, UE::Solvers::TSparseMatrixAssembler& LaplacianBoundary); /** * Construct a sparse matrix representation using a meanvalue-weighted Laplacian. * NB: there is no reason to expect this to be a symmetric matrix. * * The mesh itself is assumed to have N interior vertices, and M boundary vertices. * * @param DynamicMesh The triangle mesh * @param VertexMap Additional arrays used to map between vertexID and offset in a linear array (i.e. the row). * The vertices are ordered so that last M ( = VertexMap.NumBoundaryVerts() ) correspond to those on the boundary. * @param LaplacianInterior On return, the laplacian operator that acts on the interior vertices: sparse N x N matrix * @param LaplacianBoundary On return, the portion of the operator that acts on the boundary vertices: sparse N x M matrix * * LaplacianInterior * Vector_InteriorVerts + LaplacianBoundary * Vector_BoundaryVerts = Full Laplacian applied to interior vertices. */ template void ConstructMeanValueWeightLaplacian(const FDynamicMesh3& DynamicMesh, const FVertexLinearization& VertexMap, UE::Solvers::TSparseMatrixAssembler& LaplacianInterior, UE::Solvers::TSparseMatrixAssembler& LaplacianBoundary); /** * Construct a sparse matrix representation using a cotangent-weighted Laplacian. * but returns the result in two symmetric parts. * * The mesh itself is assumed to have N interior vertices, and M boundary vertices. * * (AreaMatrix^{-1}) * L_hat = Cotangent weighted Laplacian. * * * @param DynamicMesh The triangle mesh * @param VertexMap Additional arrays used to map between vertexID and offset in a linear array (i.e. the row). * The vertices are ordered so that last M ( = VertexMap.NumBoundaryVerts() ) correspond to those on the boundary. * @param AreaMatrix On return, the mass matrix for the internal vertices. sparse N x N matrix * @param LaplacianInterior On return, the laplacian operator that acts on the interior vertices: sparse N x N matrix - symmetric * @param LaplacianBoundary On return, the portion of the operator that acts on the boundary vertices: sparse N x M matrix * * AreaMatrix^{-1} * ( LaplacianInterior * Vector_InteriorVerts + LaplacianBoundary * Vector_BoundaryVerts) = Full Laplacian applied to interior vertices. */ template void ConstructCotangentLaplacian(const FDynamicMesh3& DynamicMesh, const FVertexLinearization& VertexMap, UE::Solvers::TSparseMatrixAssembler& AreaMatrix, UE::Solvers::TSparseMatrixAssembler& LaplacianInterior, UE::Solvers::TSparseMatrixAssembler& LaplacianBoundary); /** * Construct a sparse matrix representation using a pre-multiplied cotangent-weighted Laplacian. * NB: there is no reason to expect this to be a symmetric matrix. * * This computes the laplacian scaled by the average area A_ave: ie. LScaled = A_ave/(2A_i) ( Cot alpha_ij + Cot beta_ij ) * * The mesh itself is assumed to have N interior vertices, and M boundary vertices. * * @param DynamicMesh The triangle mesh * @param VertexMap Additional arrays used to map between vertexID and offset in a linear array (i.e. the row). * The vertices are ordered so that last M ( = VertexMap.NumBoundaryVerts() ) correspond to those on the boundary. * @param LaplacianInterior On return, scaled laplacian operator that acts on the interior vertices: sparse N x N matrix * @param LaplacianBoundary On return, scaled portion of the operator that acts on the boundary vertices: sparse N x M matrix * @param bClampAreas Indicates if (A_ave / A_i) should be clamped to (0.5, 5) range. * in practice this is desirable when creating the biharmonic operator, but not the mean curvature flow operator * * LaplacianInterior * Vector_InteriorVerts + LaplacianBoundary * Vector_BoundaryVerts = Full Laplacian applied to interior vertices. */ template void ConstructCotangentLaplacian(const FDynamicMesh3& DynamicMesh, const FVertexLinearization& VertexMap, UE::Solvers::TSparseMatrixAssembler& LaplacianInterior, UE::Solvers::TSparseMatrixAssembler& LaplacianBoundary, const bool bClampWeights); /** * Construct a sparse matrix representation using a pre-multiplied cotangent-weighted Laplacian, using an intrinsic Delaunay mesh internally * NB: there is no reason to expect this to be a symmetric matrix. * * This computes the laplacian scaled by the average area A_ave: ie. LScaled = A_ave/(2A_i) ( Cot alpha_ij + Cot beta_ij ) * * The mesh itself is assumed to have N interior vertices, and M boundary vertices. * * @param DynamicMesh The triangle mesh * @param VertexMap Additional arrays used to map between vertexID and offset in a linear array (i.e. the row). * The vertices are ordered so that last M ( = VertexMap.NumBoundaryVerts() ) correspond to those on the boundary. * @param LaplacianInterior On return, scaled laplacian operator that acts on the interior vertices: sparse N x N matrix * @param LaplacianBoundary On return, scaled portion of the operator that acts on the boundary vertices: sparse N x M matrix * @param bClampAreas Indicates if (A_ave / A_i) should be clamped to (0.5, 5) range. * in practice this is desirable when creating the biharmonic operator, but not the mean curvature flow operator * * LaplacianInterior * Vector_InteriorVerts + LaplacianBoundary * Vector_BoundaryVerts = Full Laplacian applied to interior vertices. */ template void ConstructIDTCotangentLaplacian(const FDynamicMesh3& DynamicMesh, const FVertexLinearization& VertexMap, UE::Solvers::TSparseMatrixAssembler& LaplacianInterior, UE::Solvers::TSparseMatrixAssembler& LaplacianBoundary, const bool bClampWeights); enum class ECotangentWeightMode { /** Standard cotangent weights */ Default = 0, /** Magnitude of matrix entries clamped to [-1e5,1e5], scaled by area weight */ ClampedMagnitude = 1, /** Divide cotangent weights by the area of the triangle */ TriangleArea = 2 }; enum class ECotangentAreaMode { /** uniform-weighted cotangents */ NoArea = 0, /** weight each vertex/row by 1/voronoi_area */ VoronoiArea = 1 }; /** * Construct sparse Cotangent Laplacian matrix. * This variant combines the N interior and M boundary vertices into a single (N+M) matrix and does not do any special treatment of boundaries, * they just get standard Cotan weights * * If you don't want to move the boundary vertices to the end, then initialize the VertexMap with bRemapBoundary * set to false. * * * Example usage: * * If you want to get the standard cotangent weight matrix for a mesh with no boundary reordering: * * FDynamicMesh3 DynamicMesh; * const bool bRemapBoundary = false; * FVertexLinearization VertexMap(DynamicMesh, bRemapBoundary); * ConstructFullCotangentLaplacian(DynamicMesh, VertexMap, ECotangentWeightMode::Default, ECotangentAreaMode::NoArea); */ template void ConstructFullCotangentLaplacian(const FDynamicMesh3& DynamicMesh, const FVertexLinearization& VertexMap, UE::Solvers::TSparseMatrixAssembler& LaplacianMatrix, ECotangentWeightMode WeightMode = ECotangentWeightMode::ClampedMagnitude, ECotangentAreaMode AreaMode = ECotangentAreaMode::NoArea ); /** * Use intrinsic Delaunay mesh to construct sparse Cotangent Laplacian matrix. * This variant combines the N interior and M boundary vertices into a single (N+M) matrix and does not do any special treatment of boundaries, * they just get standard Cotan weights. */ template void ConstructFullIDTCotangentLaplacian(const FDynamicMesh3& Mesh, const FVertexLinearization& VertexMap, UE::Solvers::TSparseMatrixAssembler& LaplacianMatrix, ECotangentWeightMode WeightMode = ECotangentWeightMode::ClampedMagnitude, ECotangentAreaMode AreaMode = ECotangentAreaMode::NoArea); } } // // // Implementations // // template void UE::MeshDeformation::ConstructUniformLaplacian(const MeshType& Mesh, const FVertexLinearization& VertexMap, UE::Solvers::TSparseMatrixAssembler& LaplacianInterior, UE::Solvers::TSparseMatrixAssembler& LaplacianBoundary) { //check(VertexMap_is_good) const TArray& ToMeshV = VertexMap.ToId(); const TArray& ToIndex = VertexMap.ToIndex(); const int32 NumVerts = VertexMap.NumVerts(); const int32 NumBoundaryVerts = VertexMap.NumBoundaryVerts(); const int32 NumInteriorVerts = NumVerts - NumBoundaryVerts; // pre-allocate space when possible int32 NumMatrixEntries = ComputeNumMatrixElements(Mesh, ToMeshV); LaplacianInterior.ReserveEntriesFunc(NumMatrixEntries); // Construct Laplacian Matrix: loop over verts constructing the corresponding matrix row. // NB: the vertices are ordered with the interior verts first. for (int32 i = 0; i < NumInteriorVerts; ++i) { const int32 VertId = ToMeshV[i]; RealType CenterWeight = RealType(0); // equal and opposite the sum of the neighbor weights checkSlow(!Mesh.IsBoundaryVertex(VertId)); // we should only be looping over the internal verts for (int32 NeighborVertId : Mesh.VtxVerticesItr(VertId)) { const int32 j = ToIndex[NeighborVertId]; RealType NeighborWeight = RealType(1); CenterWeight += NeighborWeight; if (j < NumInteriorVerts) { // add the neighbor LaplacianInterior.AddEntryFunc(i, j, NeighborWeight); } else { int32 jBoundary = j - NumInteriorVerts; LaplacianBoundary.AddEntryFunc(i, jBoundary, NeighborWeight); } } // add the center LaplacianInterior.AddEntryFunc(i, i, -CenterWeight); } } template void UE::MeshDeformation::ConstructUmbrellaLaplacian(const FDynamicMesh3& DynamicMesh, const FVertexLinearization& VertexMap, UE::Solvers::TSparseMatrixAssembler& LaplacianInterior, UE::Solvers::TSparseMatrixAssembler& LaplacianBoundary) { //check(VertexMap_is_good) const TArray& ToMeshV = VertexMap.ToId(); const TArray& ToIndex = VertexMap.ToIndex(); const int32 NumVerts = VertexMap.NumVerts(); const int32 NumBoundaryVerts = VertexMap.NumBoundaryVerts(); const int32 NumInteriorVerts = NumVerts - NumBoundaryVerts; // pre-allocate space when possible int32 NumMatrixEntries = ComputeNumMatrixElements(DynamicMesh, ToMeshV); LaplacianInterior.ReserveEntriesFunc(NumMatrixEntries); // Cache valency of each vertex. // Number of non-zero elements in the i'th row = 1 + OneRingSize(i) TArray OneRingSize; { OneRingSize.SetNumUninitialized(NumVerts); for (int32 i = 0; i < NumVerts; ++i) { const int32 VertId = ToMeshV[i]; OneRingSize[i] = DynamicMesh.GetVtxEdgeCount(VertId); } } // Construct Laplacian Matrix: loop over verts constructing the corresponding matrix row. for (int32 i = 0; i < NumInteriorVerts; ++i) { const int32 VertId = ToMeshV[i]; const int32 Valence = OneRingSize[i]; RealType InvValence = (Valence != 0) ? (RealType)1.0 / RealType(Valence) : 0.; checkSlow(!DynamicMesh.IsBoundaryVertex(VertId)); for (int32 NeighborVertId : DynamicMesh.VtxVerticesItr(VertId)) { const int32 j = ToIndex[NeighborVertId]; // add the neighbor if (j < NumInteriorVerts) { LaplacianInterior.AddEntryFunc(i, j, InvValence); } else { int32 jBoundary = j - NumInteriorVerts; LaplacianBoundary.AddEntryFunc(i, jBoundary, InvValence); } } // add the center LaplacianInterior.AddEntryFunc(i, i, -RealType(1)); } } template void UE::MeshDeformation::ConstructValenceWeightedLaplacian(const FDynamicMesh3& DynamicMesh, const FVertexLinearization& VertexMap, UE::Solvers::TSparseMatrixAssembler& LaplacianInterior, UE::Solvers::TSparseMatrixAssembler& LaplacianBoundary) { const TArray& ToMeshV = VertexMap.ToId(); const TArray& ToIndex = VertexMap.ToIndex(); const int32 NumVerts = VertexMap.NumVerts(); const int32 NumBoundaryVerts = VertexMap.NumBoundaryVerts(); const int32 NumInteriorVerts = NumVerts - NumBoundaryVerts; // pre-allocate space when possible int32 NumMatrixEntries = ComputeNumMatrixElements(DynamicMesh, ToMeshV); LaplacianInterior.ReserveEntriesFunc(NumMatrixEntries); // Cache valency of each vertex. // Number of non-zero elements in the i'th row = 1 + OneRingSize(i) TArray OneRingSize; { OneRingSize.SetNumUninitialized(NumVerts); for (int32 i = 0; i < NumVerts; ++i) { const int32 VertId = ToMeshV[i]; OneRingSize[i] = DynamicMesh.GetVtxEdgeCount(VertId); } } // Construct Laplacian Matrix: loop over verts constructing the corresponding matrix row. for (int32 i = 0; i < NumInteriorVerts; ++i) { const int32 VertId = ToMeshV[i]; const int32 IOneRingSize = OneRingSize[i]; RealType CenterWeight = RealType(0); // equal and opposite the sum of the neighbor weights for (int32 NeighborVertId : DynamicMesh.VtxVerticesItr(VertId)) { const int32 j = ToIndex[NeighborVertId]; const int32 JOneRingSize = OneRingSize[j]; RealType NeighborWeight = RealType(1) / TMathUtil::Sqrt(IOneRingSize + JOneRingSize); CenterWeight += NeighborWeight; if (j < NumInteriorVerts) { // add the neighbor LaplacianInterior.AddEntryFunc(i, j, NeighborWeight); } else { int32 jBoundary = j - NumInteriorVerts; LaplacianBoundary.AddEntryFunc(i, jBoundary, NeighborWeight); } } // add the center LaplacianInterior.AddEntryFunc(i, i, -CenterWeight); } } template void UE::MeshDeformation::ConstructMeanValueWeightLaplacian(const FDynamicMesh3& DynamicMesh, const FVertexLinearization& VertexMap, UE::Solvers::TSparseMatrixAssembler& LaplacianInterior, UE::Solvers::TSparseMatrixAssembler& LaplacianBoundary) { const TArray& ToMeshV = VertexMap.ToId(); const TArray& ToIndex = VertexMap.ToIndex(); const int32 NumVerts = VertexMap.NumVerts(); const int32 NumBoundaryVerts = VertexMap.NumBoundaryVerts(); const int32 NumInteriorVerts = NumVerts - NumBoundaryVerts; // pre-allocate space when possible int32 NumMatrixEntries = ComputeNumMatrixElements(DynamicMesh, ToMeshV); LaplacianInterior.ReserveEntriesFunc(NumMatrixEntries); // Map the triangles. FTriangleLinearization TriangleMap(DynamicMesh); const TArray& ToMeshTri = TriangleMap.ToId(); const TArray& ToTriIdx = TriangleMap.ToIndex(); const int32 NumTris = TriangleMap.NumTris(); // Create an array that holds all the geometric information we need for each triangle. TArray TriangleDataArray; ConstructTriangleDataArray(DynamicMesh, TriangleMap, TriangleDataArray); // Construct Laplacian Matrix: loop over verts constructing the corresponding matrix row. // skipping the boundary verts for later use. for (int32 i = 0; i < NumInteriorVerts; ++i) { const int32 IVertId = ToMeshV[i]; // I - the row double WeightII = 0.; // accumulate to equal and opposite the sum of the neighbor weights // for each connecting edge for (int32 EdgeId : DynamicMesh.VtxEdgesItr(IVertId)) { // [v0, v1, t0, t1]: NB: both t0 & t1 exist since IVert isn't a boundary vert. const FDynamicMesh3::FEdge Edge = DynamicMesh.GetEdge(EdgeId); // the other vert in the edge - identifies the matrix column const int32 JVertId = (Edge.Vert[0] == IVertId) ? Edge.Vert[1] : Edge.Vert[0]; // J - the column // Get the cotangents for this edge. const int32 Tri0Idx = ToTriIdx[Edge.Tri[0]]; const auto& Tri0Data = TriangleDataArray[Tri0Idx]; double TanHalfAngleSum = Tri0Data.GetTanHalfAngle(IVertId); double EdgeLength = FMathd::Max(1.e-5, Tri0Data.GetEdgeLength(EdgeId)); // Clamp the length // The second triangle will be invalid if this is an edge! TanHalfAngleSum += (Edge.Tri[1] != FDynamicMesh3::InvalidID) ? TriangleDataArray[ToTriIdx[Edge.Tri[1]]].GetTanHalfAngle(IVertId) : 0.; double WeightIJ = TanHalfAngleSum / EdgeLength; WeightII += WeightIJ; const int32 j = ToIndex[JVertId]; if (j < NumInteriorVerts) { LaplacianInterior.AddEntryFunc(i, j, (RealType)WeightIJ); } else { int32 jBoundary = j - NumInteriorVerts; LaplacianBoundary.AddEntryFunc(i, jBoundary, (RealType)WeightIJ); } } LaplacianInterior.AddEntryFunc(i, i, (RealType)-WeightII); } } template void UE::MeshDeformation::ConstructCotangentLaplacian(const FDynamicMesh3& DynamicMesh, const FVertexLinearization& VertexMap, UE::Solvers::TSparseMatrixAssembler& AreaMatrix, UE::Solvers::TSparseMatrixAssembler& LaplacianInterior, UE::Solvers::TSparseMatrixAssembler& LaplacianBoundary) { const TArray& ToMeshV = VertexMap.ToId(); const TArray& ToIndex = VertexMap.ToIndex(); const int32 NumVerts = VertexMap.NumVerts(); const int32 NumBoundaryVerts = VertexMap.NumBoundaryVerts(); const int32 NumInteriorVerts = NumVerts - NumBoundaryVerts; // pre-allocate space when possible int32 NumMatrixEntries = ComputeNumMatrixElements(DynamicMesh, ToMeshV); LaplacianInterior.ReserveEntriesFunc(NumMatrixEntries); // Create the mapping of triangles FTriangleLinearization TriangleMap(DynamicMesh); const TArray& ToMeshTri = TriangleMap.ToId(); const TArray& ToTriIdx = TriangleMap.ToIndex(); const int32 NumTris = TriangleMap.NumTris(); // Clear space for the areas AreaMatrix.ReserveEntriesFunc(NumVerts); // Create an array that holds all the geometric information we need for each triangle. TArray CotangentTriangleDataArray; ConstructTriangleDataArray(DynamicMesh, TriangleMap, CotangentTriangleDataArray); // Construct Laplacian Matrix: loop over verts constructing the corresponding matrix row. // store the id of the boundary verts for later use. for (int32 i = 0; i < NumInteriorVerts; ++i) { const int32 IVertId = ToMeshV[i]; // I - the row // Compute the Voronoi area for this vertex. double WeightArea = 0.; for (int32 TriId : DynamicMesh.VtxTrianglesItr(IVertId)) { const int32 TriIdx = ToTriIdx[TriId]; const CotanTriangleData& TriData = CotangentTriangleDataArray[TriIdx]; // The three VertIds for this triangle. const FIndex3i TriVertIds = DynamicMesh.GetTriangle(TriId); // Which of the corners is IVertId? int32 Offset = 0; while (TriVertIds[Offset] != IVertId) { Offset++; checkSlow(Offset < 3); } WeightArea += TriData.VoronoiArea[Offset]; } double WeightII = 0.; // accumulate to equal and opposite the sum of the neighbor weights // for each connecting edge for (int32 EdgeId : DynamicMesh.VtxEdgesItr(IVertId)) { // [v0, v1, t0, t1]: NB: both t0 & t1 exist since IVert isn't a boundary vert. const FDynamicMesh3::FEdge Edge = DynamicMesh.GetEdge(EdgeId); // the other vert in the edge - identifies the matrix column const int32 JVertId = (Edge.Vert[0] == IVertId) ? Edge.Vert[1] : Edge.Vert[0]; // J - the column checkSlow(JVertId != IVertId); // Get the cotangents for this edge. const int32 Tri0Idx = ToTriIdx[Edge.Tri[0]]; const CotanTriangleData& Tri0Data = CotangentTriangleDataArray[Tri0Idx]; const double CotanAlpha = Tri0Data.GetOpposingCotangent(EdgeId); // The second triangle will be invalid if this is an edge! const double CotanBeta = (Edge.Tri[1] != FDynamicMesh3::InvalidID) ? CotangentTriangleDataArray[ToTriIdx[Edge.Tri[1]]].GetOpposingCotangent(EdgeId) : 0.; double WeightIJ = 0.5 * (CotanAlpha + CotanBeta); WeightII += WeightIJ; const int32 j = ToIndex[JVertId]; if (j < NumInteriorVerts) { LaplacianInterior.AddEntryFunc(i, j, (RealType)WeightIJ); } else { int32 jBoundary = j - NumInteriorVerts; LaplacianBoundary.AddEntryFunc(i, jBoundary, (RealType)WeightIJ); } } LaplacianInterior.AddEntryFunc(i, i, (RealType)-WeightII); AreaMatrix.AddEntryFunc(i, i, (RealType)WeightArea); } } template void UE::MeshDeformation::ConstructCotangentLaplacian(const FDynamicMesh3& DynamicMesh, const FVertexLinearization& VertexMap, UE::Solvers::TSparseMatrixAssembler& LaplacianInterior, UE::Solvers::TSparseMatrixAssembler& LaplacianBoundary, const bool bClampWeights) { const TArray& ToMeshV = VertexMap.ToId(); const TArray& ToIndex = VertexMap.ToIndex(); const int32 NumVerts = VertexMap.NumVerts(); const int32 NumBoundaryVerts = VertexMap.NumBoundaryVerts(); const int32 NumInteriorVerts = NumVerts - NumBoundaryVerts; // pre-allocate space when possible int32 NumMatrixEntries = ComputeNumMatrixElements(DynamicMesh, ToMeshV); LaplacianInterior.ReserveEntriesFunc(NumMatrixEntries); // Map the triangles. FTriangleLinearization TriangleMap(DynamicMesh); const TArray& ToMeshTri = TriangleMap.ToId(); const TArray& ToTriIdx = TriangleMap.ToIndex(); const int32 NumTris = TriangleMap.NumTris(); // Create an array that holds all the geometric information we need for each triangle. TArray CotangentTriangleDataArray; ConstructTriangleDataArray(DynamicMesh, TriangleMap, CotangentTriangleDataArray); // Construct Laplacian Matrix: loop over verts constructing the corresponding matrix row. // skipping the boundary verts for later use. for (int32 i = 0; i < NumInteriorVerts; ++i) { const int32 IVertId = ToMeshV[i]; // I - the row // Compute the Voronoi area for this vertex. double WeightArea = 0.; for (int32 TriId : DynamicMesh.VtxTrianglesItr(IVertId)) { const int32 TriIdx = ToTriIdx[TriId]; const CotanTriangleData& TriData = CotangentTriangleDataArray[TriIdx]; // The three VertIds for this triangle. const FIndex3i TriVertIds = DynamicMesh.GetTriangle(TriId); // Which of the corners is IVertId? int32 Offset = 0; while (TriVertIds[Offset] != IVertId) { Offset++; checkSlow(Offset < 3); } WeightArea += TriData.VoronoiArea[Offset]; } double WeightII = 0.; // accumulate to equal and opposite the sum of the neighbor weights // for each connecting edge for (int32 EdgeId : DynamicMesh.VtxEdgesItr(IVertId)) { // [v0, v1, t0, t1]: NB: both t0 & t1 exist since IVert isn't a boundary vert. const FDynamicMesh3::FEdge Edge = DynamicMesh.GetEdge(EdgeId); // the other vert in the edge - identifies the matrix column const int32 JVertId = (Edge.Vert[0] == IVertId) ? Edge.Vert[1] : Edge.Vert[0]; // J - the column checkSlow(JVertId != IVertId); // Get the cotangents for this edge. const int32 Tri0Idx = ToTriIdx[Edge.Tri[0]]; const CotanTriangleData& Tri0Data = CotangentTriangleDataArray[Tri0Idx]; const double CotanAlpha = Tri0Data.GetOpposingCotangent(EdgeId); // The second triangle will be invalid if this is an edge! const double CotanBeta = (Edge.Tri[1] != FDynamicMesh3::InvalidID) ? CotangentTriangleDataArray[ToTriIdx[Edge.Tri[1]]].GetOpposingCotangent(EdgeId) : 0.; double WeightIJ = 0.5 * (CotanAlpha + CotanBeta); if (bClampWeights) { WeightIJ = FMathd::Clamp(WeightIJ, -1.e5 * WeightArea, 1.e5 * WeightArea); } WeightII += WeightIJ; const int32 j = ToIndex[JVertId]; if (j < NumInteriorVerts) { LaplacianInterior.AddEntryFunc(i, j, (RealType)(WeightIJ / WeightArea)); } else { int32 jBoundary = j - NumInteriorVerts; LaplacianBoundary.AddEntryFunc(i, jBoundary, (RealType)(WeightIJ / WeightArea)); } } LaplacianInterior.AddEntryFunc(i, i, (RealType)(-WeightII / WeightArea)); } } template void UE::MeshDeformation::ConstructIDTCotangentLaplacian(const FDynamicMesh3& DynamicMesh, const FVertexLinearization& VertexMap, UE::Solvers::TSparseMatrixAssembler& LaplacianInterior, UE::Solvers::TSparseMatrixAssembler& LaplacianBoundary, const bool bClampWeights) { const TArray& ToMeshV = VertexMap.ToId(); const TArray& ToIndex = VertexMap.ToIndex(); const int32 NumVerts = VertexMap.NumVerts(); const int32 NumBoundaryVerts = VertexMap.NumBoundaryVerts(); const int32 NumInteriorVerts = NumVerts - NumBoundaryVerts; // create intrinsic mesh with delaunay triangulation UE::Geometry::FSimpleIntrinsicEdgeFlipMesh IntrinsicMesh(DynamicMesh); TSet Uncorrected; // some edges can't be flipped. const int32 NumFlips = UE::Geometry::FlipToDelaunay(IntrinsicMesh, Uncorrected); // pre-allocate space when possible int32 NumMatrixEntries = ComputeNumMatrixElements(IntrinsicMesh, ToMeshV); LaplacianInterior.ReserveEntriesFunc(NumMatrixEntries); // cache the cotan weight for each edge (i.e. 1/2 ( cot(a_ij) + cot(b_ij) for edge(i,j) ) TArray EdgeCotanWeights; { const int32 MaxEID = IntrinsicMesh.MaxEdgeID(); EdgeCotanWeights.AddZeroed(MaxEID); for (int32 EdgeId = 0; EdgeId < MaxEID; ++EdgeId) { if (!IntrinsicMesh.IsEdge(EdgeId)) { continue; } const double CTWeight = IntrinsicMesh.EdgeCotanWeight(EdgeId); EdgeCotanWeights[EdgeId] = FMathd::Max(0., CTWeight); // roundoff errors can produce very, very small negative values when the angles opposite the edge are each 90-degrees. } } // Construct Laplacian Matrix: loop over verts constructing the corresponding matrix row. // skipping the boundary verts for later use. for (int32 i = 0; i < NumInteriorVerts; ++i) { const int32 IVertId = ToMeshV[i]; // I - the row // Compute the Voronoi area for this vertex. // 1/8 Sum ( cot(a) + cot(b) )*length^2 // see for example http://www.geometry.caltech.edu/pubs/DMSB_III.pdf double WeightArea = 0.0; bool bUseUniform = false; { for (int32 EdgeId : IntrinsicMesh.VtxEdgesItr(IVertId)) { const double EdgeLength = IntrinsicMesh.GetEdgeLength(EdgeId); const double CTWeight = EdgeCotanWeights[EdgeId]; const double CTWeightLSqr = CTWeight * EdgeLength * EdgeLength; if (Uncorrected.Contains(EdgeId)) // this part of the intrinsic mesh is not Delaunay. { bUseUniform = true; } WeightArea += CTWeightLSqr; } WeightArea *= 0.25; WeightArea = FMathd::Max(WeightArea, 1.e-5); } double WeightII = 0.; // accumulate to equal and opposite the sum of the neighbor weights if (bUseUniform) { // use uniform stencil for this vertex for (int32 EdgeId : IntrinsicMesh.VtxEdgesItr(IVertId)) { // note: both sides of the edge exist since this isn't a boundary vert const FIndex2i EdgeV = IntrinsicMesh.GetEdgeV(EdgeId); // the other vert in the edge - identifies the matrix column const int32 JVertId = (EdgeV[0] == IVertId) ? EdgeV[1] : EdgeV[0]; // J - the column const int32 j = ToIndex[JVertId]; double WeightIJ = 1.; WeightII += WeightIJ; if (j < NumInteriorVerts) { LaplacianInterior.AddEntryFunc(i, j, (RealType)(WeightIJ)); } else { int32 jBoundary = j - NumInteriorVerts; LaplacianBoundary.AddEntryFunc(i, jBoundary, (RealType)(WeightIJ)); } } LaplacianInterior.AddEntryFunc(i, i, (RealType)(-WeightII)); } else { // for each connecting edge for (int32 EdgeId : IntrinsicMesh.VtxEdgesItr(IVertId)) { // note: both sides of the edge exist since this isn't a boundary vert const FIndex2i EdgeV = IntrinsicMesh.GetEdgeV(EdgeId); if (EdgeV.A == EdgeV.B) { continue; // the weights cancle. } double WeightIJ = EdgeCotanWeights[EdgeId]; if (bClampWeights) { WeightIJ = FMathd::Clamp(WeightIJ, -1.e5 * WeightArea, 1.e5 * WeightArea); } double Weight = (WeightIJ / WeightArea); WeightII += Weight; // the other vert in the edge - identifies the matrix column const int32 JVertId = (EdgeV[0] == IVertId) ? EdgeV[1] : EdgeV[0]; // J - the column const int32 j = ToIndex[JVertId]; if (j < NumInteriorVerts) { LaplacianInterior.AddEntryFunc(i, j, (RealType)(Weight)); } else { int32 jBoundary = j - NumInteriorVerts; LaplacianBoundary.AddEntryFunc(i, jBoundary, (RealType)(Weight)); } } LaplacianInterior.AddEntryFunc(i, i, (RealType)(-WeightII)); } } } template void UE::MeshDeformation::ConstructFullCotangentLaplacian(const FDynamicMesh3& Mesh, const FVertexLinearization& VertexMap, UE::Solvers::TSparseMatrixAssembler& LaplacianMatrix, ECotangentWeightMode WeightMode, ECotangentAreaMode AreaMode) { const TArray& ToMeshV = VertexMap.ToId(); const TArray& ToIndex = VertexMap.ToIndex(); const int32 NumVerts = VertexMap.NumVerts(); // Map the triangles. FTriangleLinearization TriangleMap(Mesh); const TArray& ToTriIdx = TriangleMap.ToIndex(); const int32 NumTris = TriangleMap.NumTris(); const int32 NumTasks = FMath::Clamp(FTaskGraphInterface::Get().GetNumWorkerThreads()/2, 1, NumVerts); constexpr int32 MinVerticesByTask = 60; const int32 VerticesByTask = FMath::Max(FMath::DivideAndRoundUp(NumVerts, NumTasks), MinVerticesByTask); const int32 NumBatches = FMath::DivideAndRoundUp(NumVerts, VerticesByTask); TArray PendingTasks; PendingTasks.Reserve(NumBatches+1); UE::Tasks::FTask FirstPendingTask = UE::Tasks::Launch(UE_SOURCE_LOCATION, [&Mesh, &ToMeshV, &LaplacianMatrix]() { // pre-allocate space when possible const int32 NumMatrixEntries = ComputeNumMatrixElements(Mesh, ToMeshV); LaplacianMatrix.ReserveEntriesFunc(NumMatrixEntries); }); PendingTasks.Add(FirstPendingTask); // Create an array that holds all the geometric information we need for each triangle. TArray CotangentTriangleDataArray; ConstructTriangleDataArray(Mesh, TriangleMap, CotangentTriangleDataArray); using MatrixEntry = typename UE::Solvers::TSparseMatrixAssembler::FTupleData; TArray> TriplesBatches; TriplesBatches.SetNum(NumBatches); for (int32 BatchIndex = 0; BatchIndex < NumBatches; BatchIndex++) { const int32 StartIndex = BatchIndex * VerticesByTask; int32 EndIndex = (BatchIndex + 1) * VerticesByTask; EndIndex = BatchIndex == NumBatches - 1 ? FMath::Min(NumVerts, EndIndex) : EndIndex; TArray& TriplesBatch = TriplesBatches[BatchIndex]; UE::Tasks::FTask PendingTask = UE::Tasks::Launch(UE_SOURCE_LOCATION, [StartIndex, EndIndex, &ToMeshV, &ToIndex, &ToTriIdx, &CotangentTriangleDataArray, &Mesh, AreaMode, WeightMode, &TriplesBatch, &LaplacianMatrix]() { // Construct Laplacian Matrix: loop over verts constructing the corresponding matrix row. for (int32 i = StartIndex; i < EndIndex; ++i) { const int32 IVertId = ToMeshV[i]; // I - the row double WeightArea = 1.0; if (AreaMode == ECotangentAreaMode::VoronoiArea) { WeightArea = 0.0; Mesh.EnumerateVertexTriangles(IVertId, [&](int32 TriId) { const int32 TriIdx = ToTriIdx[TriId]; const CotanTriangleData& TriData = CotangentTriangleDataArray[TriIdx]; const FIndex3i TriVertIds = Mesh.GetTriangle(TriId); int32 TriVertIndex = IndexUtil::FindTriIndex(IVertId, TriVertIds); WeightArea += TriData.VoronoiArea[TriVertIndex]; }); } double WeightII = 0.; // accumulate to equal and opposite the sum of the neighbor weights for (int32 EdgeId : Mesh.VtxEdgesItr(IVertId)) { const FDynamicMesh3::FEdge Edge = Mesh.GetEdge(EdgeId); // the other vert in the edge - identifies the matrix column const int32 JVertId = (Edge.Vert[0] == IVertId) ? Edge.Vert[1] : Edge.Vert[0]; // J - the column // Get the cotangents for this edge. const int32 Tri0Idx = ToTriIdx[Edge.Tri[0]]; const CotanTriangleData& Tri0Data = CotangentTriangleDataArray[Tri0Idx]; double CotanAlpha = Tri0Data.GetOpposingCotangent(EdgeId); // The second triangle will be invalid if this is an edge! double CotanBeta = (Edge.Tri[1] != FDynamicMesh3::InvalidID) ? CotangentTriangleDataArray[ToTriIdx[Edge.Tri[1]]].GetOpposingCotangent(EdgeId) : 0.0; if (WeightMode == ECotangentWeightMode::TriangleArea) { CotanAlpha /= Tri0Data.Area; if (Edge.Tri[1] != FDynamicMesh3::InvalidID) { const CotanTriangleData& Tri1Data = CotangentTriangleDataArray[ToTriIdx[Edge.Tri[1]]]; CotanBeta /= Tri1Data.Area; } } // do not need to multiply by 0.5 here... //double WeightIJ = 0.5 * (CotanAlpha + CotanBeta); double WeightIJ = (CotanAlpha + CotanBeta); if (WeightMode == ECotangentWeightMode::ClampedMagnitude) { WeightIJ = FMathd::Clamp(WeightIJ, -1.e5 * WeightArea, 1.e5 * WeightArea); } WeightII += WeightIJ; const int32 j = ToIndex[JVertId]; TriplesBatch.Emplace(i, j, (RealType)(WeightIJ / WeightArea)); } TriplesBatch.Emplace(i, i, (RealType)(-WeightII / WeightArea)); } }); PendingTasks.Add(PendingTask); } UE::Tasks::Wait(PendingTasks); for (int32 BatchIndex = 0; BatchIndex < NumBatches; BatchIndex++) { TArray & Triplets = TriplesBatches[BatchIndex]; LaplacianMatrix.AddEntriesFunc(Triplets.GetData(), Triplets.Num()); } } template void UE::MeshDeformation::ConstructFullIDTCotangentLaplacian(const FDynamicMesh3& Mesh, const FVertexLinearization& VertexMap, UE::Solvers::TSparseMatrixAssembler& LaplacianMatrix, ECotangentWeightMode WeightMode, ECotangentAreaMode AreaMode) { const TArray& ToMeshV = VertexMap.ToId(); const TArray& ToIndex = VertexMap.ToIndex(); const int32 NumVerts = VertexMap.NumVerts(); // create intrinsic mesh with delaunay triangulation UE::Geometry::FSimpleIntrinsicEdgeFlipMesh IntrinsicMesh(Mesh); TSet Uncorrected; const int32 NumFlips = UE::Geometry::FlipToDelaunay(IntrinsicMesh, Uncorrected); // pre-allocate space when possible int32 NumMatrixEntries = ComputeNumMatrixElements(IntrinsicMesh, ToMeshV); LaplacianMatrix.ReserveEntriesFunc(NumMatrixEntries); // cache the cotan weight for each edge (i.e. 1/2 ( cot(a_ij) + cot(b_ij) for edge(i,j) ) TArray EdgeCotanWeights; { const int32 MaxEID = IntrinsicMesh.MaxEdgeID(); EdgeCotanWeights.AddUninitialized(MaxEID); for (int32 e = 0; e < MaxEID; ++e) { if (!IntrinsicMesh.IsEdge(e)) { continue; } EdgeCotanWeights[e] = IntrinsicMesh.EdgeCotanWeight(e); } } // Construct Laplacian Matrix: loop over verts constructing the corresponding matrix row. for (int32 i = 0; i < NumVerts; ++i) { const int32 IVertId = ToMeshV[i]; // I - the row bool bUseUniform = false; double WeightArea = 1.0; if (AreaMode == ECotangentAreaMode::VoronoiArea) { // 1/8 Sum ( cot(a) + cot(b) )*length^2 // see for example http://www.geometry.caltech.edu/pubs/DMSB_III.pdf WeightArea = 0.0; for (int32 EdgeId : IntrinsicMesh.VtxEdgesItr(IVertId)) { const double EdgeLength = IntrinsicMesh.GetEdgeLength(EdgeId); const double CTWeight = EdgeCotanWeights[EdgeId]; const double CTWeightLSqr = CTWeight * EdgeLength * EdgeLength; if (Uncorrected.Contains(EdgeId) || CTWeightLSqr < 1.e-1) { bUseUniform = true; } WeightArea += CTWeightLSqr; } WeightArea *= 0.25; } if (bUseUniform) { WeightArea = 1.0; } double WeightII = 0.; // accumulate to equal and opposite the sum of the neighbor weights for (int32 EdgeId : IntrinsicMesh.VtxEdgesItr(IVertId)) { const FIndex2i EdgeV = IntrinsicMesh.GetEdgeV(EdgeId); if (EdgeV.A == EdgeV.B) { continue; // weights will cancle. } // the other vert in the edge - identifies the matrix column const int32 JVertId = (EdgeV[0] == IVertId) ? EdgeV[1] : EdgeV[0]; // J - the column double WeightIJ = (bUseUniform) ? 1. : EdgeCotanWeights[EdgeId]; if (WeightMode == ECotangentWeightMode::ClampedMagnitude) { WeightIJ = FMathd::Clamp(WeightIJ, -1.e5 * WeightArea, 1.e5 * WeightArea); } double Weight = WeightIJ / WeightArea; WeightII += Weight; const int32 j = ToIndex[JVertId]; LaplacianMatrix.AddEntryFunc(i, j, (RealType)(Weight)); } LaplacianMatrix.AddEntryFunc(i, i, (RealType)(-WeightII)); } }