You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

661 lines
27 KiB

// David Eberly, Geometric Tools, Redmond WA 98052
// Copyright (c) 1998-2021
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt
// https://www.geometrictools.com/License/Boost/LICENSE_1_0.txt
// Version: 4.0.2021.04.25
#pragma once
#include <Mathematics/Vector2.h>
#include <Mathematics/Vector3.h>
#include <Mathematics/ETManifoldMesh.h>
#include <cstdint>
#include <cstring>
#include <functional>
#include <thread>
// This class is an implementation of the barycentric mapping algorithm
// described in Section 5.3 of the book
// Polygon Mesh Processing
// Mario Botsch, Leif Kobbelt, Mark Pauly, Pierre Alliez, Bruno Levy
// AK Peters, Ltd., Natick MA, 2010
// It uses the mean value weights described in Section 5.3.1 to allow the mesh
// geometry to influence the texture coordinate generation, and it uses
// Gauss-Seidel iteration to solve the sparse linear system. The authors'
// advice is that the Gauss-Seidel approach works well for at most about 5000
// vertices, presumably the convergence rate degrading as the number of
// vertices increases.
//
// The algorithm implemented here has an additional preprocessing step that
// computes a topological distance transform of the vertices. The boundary
// texture coordinates are propagated inward by updating the vertices in
// topological distance order, leading to fast convergence for large numbers
// of vertices.
namespace gte
{
template <typename Real>
class GenerateMeshUV
{
public:
// Construction and destruction. Set the number of threads to 0 when
// you want the code to run in the main thread of the applications.
// Set the number of threads to a positive number when you want the
// code to run multithreaded on the CPU. Derived classes that use the
// GPU ignore the number of threads, setting the constructor input to
// std::numeric_limits<uint32_t>::max(). Provide a callback when you
// want to monitor each iteration of the uv-solver. The input to the
// progress callback is the current iteration; it starts at 1 and
// increases to the numIterations input to the operator() member
// function.
GenerateMeshUV(uint32_t numThreads,
std::function<void(uint32_t)> const* progress = nullptr)
:
mNumThreads(numThreads),
mProgress(progress),
mNumVertices(0),
mVertices(nullptr),
mTCoords(nullptr),
mNumBoundaryEdges(0),
mBoundaryStart(0)
{
}
virtual ~GenerateMeshUV() = default;
// The incoming mesh must be edge-triangle manifold and have rectangle
// topology (simply connected, closed polyline boundary). The arrays
// 'vertices' and 'tcoords' must both have 'numVertices' elements.
// Set 'useSquareTopology' to true for the generated coordinates to
// live in the uv-square [0,1]^2. Set it to false for the generated
// coordinates to live in a convex polygon that inscribes the uv-disk
// of center (1/2,1/2) and radius 1/2.
void operator()(uint32_t numIterations, bool useSquareTopology,
int numVertices, Vector3<Real> const* vertices, int numIndices,
int const* indices, Vector2<Real>* tcoords)
{
// Ensure that numIterations is even, which avoids having a memory
// copy from the temporary ping-pong buffer to 'tcoords'.
if (numIterations & 1)
{
++numIterations;
}
mNumVertices = numVertices;
mVertices = vertices;
mTCoords = tcoords;
// The linear system solver has a first pass to initialize the
// texture coordinates to ensure the Gauss-Seidel iteration
// converges rapidly. This requires the texture coordinates all
// start as (-1,-1).
for (int i = 0; i < numVertices; ++i)
{
mTCoords[i][0] = (Real)-1;
mTCoords[i][1] = (Real)-1;
}
// Create the manifold mesh data structure.
mGraph.Clear();
int const numTriangles = numIndices / 3;
for (int t = 0; t < numTriangles; ++t)
{
int v0 = *indices++;
int v1 = *indices++;
int v2 = *indices++;
mGraph.Insert(v0, v1, v2);
}
TopologicalVertexDistanceTransform();
if (useSquareTopology)
{
AssignBoundaryTextureCoordinatesSquare();
}
else
{
AssignBoundaryTextureCoordinatesDisk();
}
ComputeMeanValueWeights();
SolveSystem(numIterations);
}
protected:
// A CPU-based implementation is provided by this class. The derived
// classes using the GPU override this function.
virtual void SolveSystemInternal(uint32_t numIterations)
{
if (mNumThreads > 1)
{
SolveSystemCPUMultiple(numIterations);
}
else
{
SolveSystemCPUSingle(numIterations);
}
}
// Constructor inputs.
uint32_t mNumThreads;
std::function<void(uint32_t)> const* mProgress;
// Convenience members that store the input parameters to operator().
int mNumVertices;
Vector3<Real> const* mVertices;
Vector2<Real>* mTCoords;
// The edge-triangle manifold graph, where each edge is shared by at
// most two triangles.
ETManifoldMesh mGraph;
// The mVertexInfo array stores -1 for the interior vertices. For a
// boundary edge <v0,v1> that is counterclockwise,
// mVertexInfo[v0] = v1, which gives us an orded boundary polyline.
enum { INTERIOR_VERTEX = -1 };
std::vector<int> mVertexInfo;
int mNumBoundaryEdges, mBoundaryStart;
typedef ETManifoldMesh::Edge Edge;
std::set<Edge*> mInteriorEdges;
// The vertex graph required to set up a sparse linear system of
// equations to determine the texture coordinates.
struct Vertex
{
// The topological distance from the boundary of the mesh.
int distance;
// The value range0 is the index into mVertexGraphData for the
// first adjacent vertex. The value range1 is the number of
// adjacent vertices.
int range0, range1;
// Unused on the CPU. The padding is necessary for the HLSL and
// GLSL programs in GPUGenerateMeshUV.h.
int padding;
};
std::vector<Vertex> mVertexGraph;
std::vector<std::pair<int, Real>> mVertexGraphData;
// The vertices are listed in the order determined by a topological
// distance transform. Boundary vertices have 'distance' 0. Any
// vertices that are not boundary vertices but are edge-adjacent to
// boundary vertices have 'distance' 1. Neighbors of those have
// distance '2', and so on. The mOrderedVertices array stores
// distance-0 vertices first, distance-1 vertices second, and so on.
std::vector<int> mOrderedVertices;
private:
void TopologicalVertexDistanceTransform()
{
// Initialize the graph information.
mVertexInfo.resize(mNumVertices);
std::fill(mVertexInfo.begin(), mVertexInfo.end(), INTERIOR_VERTEX);
mVertexGraph.resize(mNumVertices);
mVertexGraphData.resize(2 * mGraph.GetEdges().size());
std::pair<int, Real> initialData = std::make_pair(-1, (Real)-1);
std::fill(mVertexGraphData.begin(), mVertexGraphData.end(), initialData);
mOrderedVertices.resize(mNumVertices);
mInteriorEdges.clear();
mNumBoundaryEdges = 0;
mBoundaryStart = std::numeric_limits<int>::max();
// Count the number of adjacent vertices for each vertex. For
// data sets with a large number of vertices, this is a
// preprocessing step to avoid a dynamic data structure that has
// a large number of std:map objects that take a very long time
// to destroy when a debugger is attached to the executable.
// Instead, we allocate a single array that stores all the
// adjacency information. It is also necessary to bundle the
// data this way for a GPU version of the algorithm.
std::vector<int> numAdjacencies(mNumVertices);
std::fill(numAdjacencies.begin(), numAdjacencies.end(), 0);
for (auto const& element : mGraph.GetEdges())
{
++numAdjacencies[element.first.V[0]];
++numAdjacencies[element.first.V[1]];
if (element.second->T[1])
{
// This is an interior edge.
mInteriorEdges.insert(element.second.get());
}
else
{
// This is a boundary edge. Determine the ordering of the
// vertex indices to make the edge counterclockwise.
++mNumBoundaryEdges;
int v0 = element.second->V[0], v1 = element.second->V[1];
auto tri = element.second->T[0];
int i;
for (i = 0; i < 3; ++i)
{
int v2 = tri->V[i];
if (v2 != v0 && v2 != v1)
{
// The vertex is opposite the boundary edge.
v0 = tri->V[(i + 1) % 3];
v1 = tri->V[(i + 2) % 3];
mVertexInfo[v0] = v1;
mBoundaryStart = std::min(mBoundaryStart, v0);
break;
}
}
}
}
// Set the range data for each vertex.
for (int vIndex = 0, aIndex = 0; vIndex < mNumVertices; ++vIndex)
{
int numAdjacent = numAdjacencies[vIndex];
mVertexGraph[vIndex].range0 = aIndex;
mVertexGraph[vIndex].range1 = numAdjacent;
aIndex += numAdjacent;
mVertexGraph[vIndex].padding = 0;
}
// Compute a topological distance transform of the vertices.
std::set<int> currFront;
for (auto const& element : mGraph.GetEdges())
{
int v0 = element.second->V[0], v1 = element.second->V[1];
for (int i = 0; i < 2; ++i)
{
if (mVertexInfo[v0] == INTERIOR_VERTEX)
{
mVertexGraph[v0].distance = -1;
}
else
{
mVertexGraph[v0].distance = 0;
currFront.insert(v0);
}
// Insert v1 into the first available slot of the
// adjacency array.
int range0 = mVertexGraph[v0].range0;
int range1 = mVertexGraph[v0].range1;
for (int j = 0; j < range1; ++j)
{
std::pair<int, Real>& data = mVertexGraphData[range0 + j];
if (data.second == (Real)-1)
{
data.first = v1;
data.second = (Real)0;
break;
}
}
std::swap(v0, v1);
}
}
// Use a breadth-first search to propagate the distance
// information.
int nextDistance = 1;
size_t numFrontVertices = currFront.size();
std::copy(currFront.begin(), currFront.end(), mOrderedVertices.begin());
while (currFront.size() > 0)
{
std::set<int> nextFront;
for (auto v : currFront)
{
int range0 = mVertexGraph[v].range0;
int range1 = mVertexGraph[v].range1;
auto* current = &mVertexGraphData[range0];
for (int j = 0; j < range1; ++j, ++current)
{
int a = current->first;
if (mVertexGraph[a].distance == -1)
{
mVertexGraph[a].distance = nextDistance;
nextFront.insert(a);
}
}
}
std::copy(nextFront.begin(), nextFront.end(), mOrderedVertices.begin() + numFrontVertices);
numFrontVertices += nextFront.size();
currFront = std::move(nextFront);
++nextDistance;
}
}
void AssignBoundaryTextureCoordinatesSquare()
{
// Map the boundary of the mesh to the unit square [0,1]^2. The
// selection of square vertices is such that the relative
// distances between boundary vertices and the relative distances
// between polygon vertices is preserved, except that the four
// corners of the square are required to have boundary points
// mapped to them. The first boundary point has an implied
// distance of zero. The value distance[i] is the length of the
// boundary polyline from vertex 0 to vertex i+1.
std::vector<Real> distance(mNumBoundaryEdges);
Real total = (Real)0;
int v0 = mBoundaryStart, v1, i;
for (i = 0; i < mNumBoundaryEdges; ++i)
{
v1 = mVertexInfo[v0];
total += Length(mVertices[v1] - mVertices[v0]);
distance[i] = total;
v0 = v1;
}
Real invTotal = (Real)1 / total;
for (auto& d : distance)
{
d *= invTotal;
}
auto begin = distance.begin(), end = distance.end();
int endYMin = (int)(std::lower_bound(begin, end, (Real)0.25) - begin);
int endXMax = (int)(std::lower_bound(begin, end, (Real)0.50) - begin);
int endYMax = (int)(std::lower_bound(begin, end, (Real)0.75) - begin);
int endXMin = (int)distance.size() - 1;
// The first polygon vertex is (0,0). The remaining vertices are
// chosen counterclockwise around the square.
v0 = mBoundaryStart;
mTCoords[v0][0] = (Real)0;
mTCoords[v0][1] = (Real)0;
for (i = 0; i < endYMin; ++i)
{
v1 = mVertexInfo[v0];
mTCoords[v1][0] = distance[i] * (Real)4;
mTCoords[v1][1] = (Real)0;
v0 = v1;
}
v1 = mVertexInfo[v0];
mTCoords[v1][0] = (Real)1;
mTCoords[v1][1] = (Real)0;
v0 = v1;
for (++i; i < endXMax; ++i)
{
v1 = mVertexInfo[v0];
mTCoords[v1][0] = (Real)1;
mTCoords[v1][1] = distance[i] * (Real)4 - (Real)1;
v0 = v1;
}
v1 = mVertexInfo[v0];
mTCoords[v1][0] = (Real)1;
mTCoords[v1][1] = (Real)1;
v0 = v1;
for (++i; i < endYMax; ++i)
{
v1 = mVertexInfo[v0];
mTCoords[v1][0] = (Real)3 - distance[i] * (Real)4;
mTCoords[v1][1] = (Real)1;
v0 = v1;
}
v1 = mVertexInfo[v0];
mTCoords[v1][0] = (Real)0;
mTCoords[v1][1] = (Real)1;
v0 = v1;
for (++i; i < endXMin; ++i)
{
v1 = mVertexInfo[v0];
mTCoords[v1][0] = (Real)0;
mTCoords[v1][1] = (Real)4 - distance[i] * (Real)4;
v0 = v1;
}
}
void AssignBoundaryTextureCoordinatesDisk()
{
// Map the boundary of the mesh to a convex polygon. The selection
// of convex polygon vertices is such that the relative distances
// between boundary vertices and the relative distances between
// polygon vertices is preserved. The first boundary point has an
// implied distance of zero. The value distance[i] is the length
// of the boundary polyline from vertex 0 to vertex i+1.
std::vector<Real> distance(mNumBoundaryEdges);
Real total = (Real)0;
int v0 = mBoundaryStart;
for (int i = 0; i < mNumBoundaryEdges; ++i)
{
int v1 = mVertexInfo[v0];
total += Length(mVertices[v1] - mVertices[v0]);
distance[i] = total;
v0 = v1;
}
// The convex polygon lives in [0,1]^2 and inscribes a circle with
// center (1/2,1/2) and radius 1/2. The polygon center is not
// necessarily the circle center! This is the case when a
// boundary edge has length larger than half the total length of
// the boundary polyline; we do not expect such data for our
// meshes. The first polygon vertex is (1/2,0). The remaining
// vertices are chosen counterclockwise around the polygon.
Real multiplier = (Real)GTE_C_TWO_PI / total;
v0 = mBoundaryStart;
mTCoords[v0][0] = (Real)1;
mTCoords[v0][1] = (Real)0.5;
for (int i = 1; i < mNumBoundaryEdges; ++i)
{
int v1 = mVertexInfo[v0];
Real angle = multiplier * distance[i - 1];
mTCoords[v1][0] = (std::cos(angle) + (Real)1) * (Real)0.5;
mTCoords[v1][1] = (std::sin(angle) + (Real)1) * (Real)0.5;
v0 = v1;
}
}
void ComputeMeanValueWeights()
{
for (auto const& edge : mInteriorEdges)
{
int v0 = edge->V[0], v1 = edge->V[1];
for (int i = 0; i < 2; ++i)
{
// Compute the direction from X0 to X1 and compute the
// length of the edge (X0,X1).
Vector3<Real> X0 = mVertices[v0];
Vector3<Real> X1 = mVertices[v1];
Vector3<Real> X1mX0 = X1 - X0;
Real x1mx0length = Normalize(X1mX0);
Real weight;
if (x1mx0length > (Real)0)
{
// Compute the weight for X0 associated with X1.
weight = (Real)0;
for (int j = 0; j < 2; ++j)
{
// Find the vertex of triangle T[j] opposite edge
// <X0,X1>.
auto tri = edge->T[j];
int k;
for (k = 0; k < 3; ++k)
{
int v2 = tri->V[k];
if (v2 != v0 && v2 != v1)
{
Vector3<Real> X2 = mVertices[v2];
Vector3<Real> X2mX0 = X2 - X0;
Real x2mx0Length = Normalize(X2mX0);
if (x2mx0Length > (Real)0)
{
Real dot = Dot(X2mX0, X1mX0);
Real cs = std::min(std::max(dot, (Real)-1), (Real)1);
Real angle = std::acos(cs);
weight += std::tan(angle * (Real)0.5);
}
else
{
weight += (Real)1;
}
break;
}
}
}
weight /= x1mx0length;
}
else
{
weight = (Real)1;
}
int range0 = mVertexGraph[v0].range0;
int range1 = mVertexGraph[v0].range1;
for (int j = 0; j < range1; ++j)
{
std::pair<int, Real>& data = mVertexGraphData[range0 + j];
if (data.first == v1)
{
data.second = weight;
}
}
std::swap(v0, v1);
}
}
}
void SolveSystem(uint32_t numIterations)
{
// On the first pass, average only neighbors whose texture
// coordinates have been computed. This is a good initial guess
// for the linear system and leads to relatively fast convergence
// of the Gauss-Seidel iterates.
Real zero = (Real)0;
for (int i = mNumBoundaryEdges; i < mNumVertices; ++i)
{
int v0 = mOrderedVertices[i];
int range0 = mVertexGraph[v0].range0;
int range1 = mVertexGraph[v0].range1;
auto const* current = &mVertexGraphData[range0];
Vector2<Real> tcoord{ zero, zero };
Real weight, weightSum = zero;
for (int j = 0; j < range1; ++j, ++current)
{
int v1 = current->first;
if (mTCoords[v1][0] != -1.0f)
{
weight = current->second;
weightSum += weight;
tcoord += weight * mTCoords[v1];
}
}
tcoord /= weightSum;
mTCoords[v0] = tcoord;
}
SolveSystemInternal(numIterations);
}
void SolveSystemCPUSingle(uint32_t numIterations)
{
// Use ping-pong buffers for the texture coordinates.
std::vector<Vector2<Real>> tcoords(mNumVertices);
size_t numBytes = mNumVertices * sizeof(Vector2<Real>);
std::memcpy(&tcoords[0], mTCoords, numBytes);
Vector2<Real>* inTCoords = mTCoords;
Vector2<Real>* outTCoords = &tcoords[0];
// The value numIterations is even, so we always swap an even
// number of times. This ensures that on exit from the loop,
// outTCoords is tcoords.
for (uint32_t i = 1; i <= numIterations; ++i)
{
if (mProgress)
{
(*mProgress)(i);
}
for (int j = mNumBoundaryEdges; j < mNumVertices; ++j)
{
int v0 = mOrderedVertices[j];
int range0 = mVertexGraph[v0].range0;
int range1 = mVertexGraph[v0].range1;
auto const* current = &mVertexGraphData[range0];
Vector2<Real> tcoord{ (Real)0, (Real)0 };
Real weight, weightSum = (Real)0;
for (int k = 0; k < range1; ++k, ++current)
{
int v1 = current->first;
weight = current->second;
weightSum += weight;
tcoord += weight * inTCoords[v1];
}
tcoord /= weightSum;
outTCoords[v0] = tcoord;
}
std::swap(inTCoords, outTCoords);
}
}
void SolveSystemCPUMultiple(uint32_t numIterations)
{
// Use ping-pong buffers for the texture coordinates.
std::vector<Vector2<Real>> tcoords(mNumVertices);
size_t numBytes = mNumVertices * sizeof(Vector2<Real>);
std::memcpy(&tcoords[0], mTCoords, numBytes);
Vector2<Real>* inTCoords = mTCoords;
Vector2<Real>* outTCoords = &tcoords[0];
// Partition the data for multiple threads.
int numV = mNumVertices - mNumBoundaryEdges;
int numVPerThread = numV / mNumThreads;
std::vector<int> vmin(mNumThreads), vmax(mNumThreads);
for (uint32_t t = 0; t < mNumThreads; ++t)
{
vmin[t] = mNumBoundaryEdges + t * numVPerThread;
vmax[t] = vmin[t] + numVPerThread - 1;
}
vmax[mNumThreads - 1] = mNumVertices - 1;
// The value numIterations is even, so we always swap an even
// number of times. This ensures that on exit from the loop,
// outTCoords is tcoords.
for (uint32_t i = 1; i <= numIterations; ++i)
{
if (mProgress)
{
(*mProgress)(i);
}
// Execute Gauss-Seidel iterations in multiple threads.
std::vector<std::thread> process(mNumThreads);
for (uint32_t t = 0; t < mNumThreads; ++t)
{
process[t] = std::thread([this, t, &vmin, &vmax, inTCoords,
outTCoords]()
{
for (int j = vmin[t]; j <= vmax[t]; ++j)
{
int v0 = mOrderedVertices[j];
int range0 = mVertexGraph[v0].range0;
int range1 = mVertexGraph[v0].range1;
auto const* current = &mVertexGraphData[range0];
Vector2<Real> tcoord{ (Real)0, (Real)0 };
Real weight, weightSum = (Real)0;
for (int k = 0; k < range1; ++k, ++current)
{
int v1 = current->first;
weight = current->second;
weightSum += weight;
tcoord += weight * inTCoords[v1];
}
tcoord /= weightSum;
outTCoords[v0] = tcoord;
}
});
}
// Wait for all threads to finish.
for (uint32_t t = 0; t < mNumThreads; ++t)
{
process[t].join();
}
std::swap(inTCoords, outTCoords);
}
}
};
}