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.
 
 
 
 
 
 

517 lines
20 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.22
#pragma once
#include <Mathematics/MinHeap.h>
#include <Mathematics/Polygon2.h>
#include <Mathematics/TriangulateEC.h>
#include <Mathematics/Vector3.h>
#include <Mathematics/VETManifoldMesh.h>
#include <set>
namespace gte
{
template <typename Real>
class VertexCollapseMesh
{
public:
// Construction.
VertexCollapseMesh(int numPositions, Vector3<Real> const* positions,
int numIndices, int const* indices)
:
mNumPositions(numPositions),
mPositions(positions),
mMesh(VCVertex::Create)
{
if (numPositions <= 0 || !positions || numIndices < 3 || !indices)
{
mNumPositions = 0;
mPositions = nullptr;
return;
}
// Build the manifold mesh from the inputs.
int numTriangles = numIndices / 3;
int const* current = indices;
for (int t = 0; t < numTriangles; ++t)
{
int v0 = *current++;
int v1 = *current++;
int v2 = *current++;
mMesh.Insert(v0, v1, v2);
}
// Locate the vertices (if any) on the mesh boundary.
auto const& vmap = mMesh.GetVertices();
for (auto const& eelement : mMesh.GetEdges())
{
auto edge = eelement.second.get();
if (!edge->T[1])
{
for (int i = 0; i < 2; ++i)
{
auto velement = vmap.find(edge->V[i]);
auto vertex = static_cast<VCVertex*>(velement->second.get());
vertex->isBoundary = true;
}
}
}
// Build the priority queue of weights for the interior vertices.
mMinHeap.Reset((int)vmap.size());
for (auto const& velement : vmap)
{
auto vertex = static_cast<VCVertex*>(velement.second.get());
Real weight;
if (vertex->isBoundary)
{
weight = std::numeric_limits<Real>::max();
}
else
{
weight = vertex->ComputeWeight(mPositions);
}
auto record = mMinHeap.Insert(velement.first, weight);
mHeapRecords.insert(std::make_pair(velement.first, record));
}
}
// Decimate the mesh using vertex collapses
struct Record
{
// The index of the interior vertex that is removed from the mesh.
// The triangles adjacent to the vertex are 'removed' from the
// mesh. The polygon boundary of the adjacent triangles is
// triangulated and the new triangles are 'inserted' into the
// mesh.
int vertex;
std::vector<TriangleKey<true>> removed;
std::vector<TriangleKey<true>> inserted;
};
// Return 'true' when a vertex collapse occurs. Once the function
// returns 'false', no more vertex collapses are allowed so you may
// then stop calling the function. The implementation has several
// consistency tests that should not fail with a theoretically correct
// implementation. If a test fails, the function returns 'false' and
// the record.vertex is set to the invalid integer 0x80000000. When
// the Logger system is enabled, the failed tests are reported to any
// Logger listeners.
bool DoCollapse(Record& record)
{
record.vertex = 0x80000000;
record.removed.clear();
record.inserted.clear();
if (mNumPositions == 0)
{
// The constructor failed, so there is nothing to collapse.
return false;
}
while (mMinHeap.GetNumElements() > 0)
{
int v = -1;
Real weight = std::numeric_limits<Real>::max();
mMinHeap.GetMinimum(v, weight);
if (weight == std::numeric_limits<Real>::max())
{
// There are no more interior vertices to collapse.
return false;
}
auto const& vmap = mMesh.GetVertices();
auto velement = vmap.find(v);
if (velement == vmap.end())
{
// Unexpected condition.
return false;
}
auto vertex = static_cast<VCVertex*>(velement->second.get());
std::vector<TriangleKey<true>> removed, inserted;
std::vector<int> linkVertices;
int result = TriangulateLink(vertex, removed, inserted, linkVertices);
if (result == VCM_UNEXPECTED_ERROR)
{
return false;
}
if (result == VCM_ALLOWED)
{
result = Collapsed(removed, inserted, linkVertices);
if (result == VCM_UNEXPECTED_ERROR)
{
return false;
}
if (result == VCM_ALLOWED)
{
// Remove the vertex and associated weight.
mMinHeap.Remove(v, weight);
mHeapRecords.erase(v);
// Update the weights of the link vertices.
for (auto vlink : linkVertices)
{
velement = vmap.find(vlink);
if (velement == vmap.end())
{
// Unexpected condition.
return false;
}
vertex = static_cast<VCVertex*>(velement->second.get());
if (!vertex->isBoundary)
{
auto iter = mHeapRecords.find(vlink);
if (iter == mHeapRecords.end())
{
// Unexpected condition.
return false;
}
weight = vertex->ComputeWeight(mPositions);
mMinHeap.Update(iter->second, weight);
}
}
record.vertex = v;
record.removed = std::move(removed);
record.inserted = std::move(inserted);
return true;
}
// else: result == VCM_DEFERRED
}
// To get here, result must be VCM_DEFERRED. The vertex
// collapse would cause mesh fold-over. Temporarily set the
// edge weight to infinity. After removal of other triangles,
// the vertex weight will be updated to a finite value and the
// vertex possibly can be removed at that time.
auto iter = mHeapRecords.find(v);
if (iter == mHeapRecords.end())
{
// Unexpected condition.
return false;
}
mMinHeap.Update(iter->second, std::numeric_limits<Real>::max());
}
// We do not expect to reach this line of code, even for a closed
// mesh. However, the compiler does not know this, yet requires
// a return value.
return false;
}
// Access the current state of the mesh, whether the original built
// in the constructor or a decimated mesh during DoCollapse calls.
inline ETManifoldMesh const& GetMesh() const
{
return mMesh;
}
private:
struct VCVertex : public VETManifoldMesh::Vertex
{
VCVertex(int v)
:
VETManifoldMesh::Vertex(v),
normal(Vector3<Real>::Zero()),
isBoundary(false)
{
}
static std::unique_ptr<Vertex> Create(int v)
{
return std::make_unique<VCVertex>(v);
}
// The weight depends on the area of the triangles sharing the
// vertex and the lengths of the projections of the adjacent
// vertices onto the vertex normal line. A side effect of the
// call is that the vertex normal is computed and stored.
Real ComputeWeight(Vector3<Real> const* positions)
{
Real weight = (Real)0;
normal = { (Real)0, (Real)0, (Real)0 };
for (auto const& tri : TAdjacent)
{
Vector3<Real> E0 = positions[tri->V[1]] - positions[tri->V[0]];
Vector3<Real> E1 = positions[tri->V[2]] - positions[tri->V[0]];
Vector3<Real> N = Cross(E0, E1);
normal += N;
weight += Length(N);
}
Normalize(normal);
for (int index : VAdjacent)
{
Vector3<Real> diff = positions[index] - positions[V];
weight += std::fabs(Dot(normal, diff));
}
return weight;
}
Vector3<Real> normal;
bool isBoundary;
};
// The functions TriangulateLink and Collapsed return one of the
// enumerates described next.
//
// VCM_NO_MORE_ALLOWED:
// Either the mesh has no more interior vertices or a collapse
// will lead to a mesh fold-over or to a nonmanifold mesh. The
// returned value 'v' is invalid (0x80000000) and 'removed' and
// 'inserted' are empty.
//
// VCM_ALLOWED:
// An interior vertex v has been removed. This is allowed using
// the following algorithm. The vertex normal is the weighted
// average of non-unit-length normals of triangles sharing v. The
// weights are the triangle areas. The adjacent vertices are
// projected onto a plane containing v and having normal equal to
// the vertex normal. If the projection is a simple polygon in
// the plane, the collapse is allowed. The triangles sharing v
// are 'removed', the polygon is triangulated, and the new
// triangles are 'inserted' into the mesh.
//
// VCM_DEFERRED:
// If the projection polygon described in the previous case is not
// simple (at least one pair of edges overlaps at some
// edge-interior point), the collapse would produce a fold-over in
// the mesh. We do not collapse in this case. It is possible
// that such a vertex occurs in a later collapse as its neighbors
// are adjusted by collapses. When this case occurs, v is valid
// (even though the collapse was not allowed) but 'removed' and
// 'inserted' are empty.
//
// VCM_UNEXPECTED_ERROR:
// The code has several tests for conditions that are not expected
// to occur for a theoretically correct implementation. If you
// receive this error, file a bug report and provide a data set
// that caused the error.
enum
{
VCM_NO_MORE_ALLOWED,
VCM_ALLOWED,
VCM_DEFERRED,
VCM_UNEXPECTED_ERROR
};
int TriangulateLink(VCVertex* vertex, std::vector<TriangleKey<true>>& removed,
std::vector<TriangleKey<true>>& inserted, std::vector<int>& linkVertices) const
{
// Create the (CCW) polygon boundary of the link of the vertex.
// The incoming vertex is interior, so the number of triangles
// sharing the vertex is equal to the number of vertices of the
// polygon. A precondition of the function call is that the
// vertex normal has already been computed.
// Get the edges of the link that are opposite the incoming
// vertex.
int const numVertices = static_cast<int>(vertex->TAdjacent.size());
removed.resize(numVertices);
int j = 0;
std::map<int, int> edgeMap;
for (auto tri : vertex->TAdjacent)
{
for (int i = 0; i < 3; ++i)
{
if (tri->V[i] == vertex->V)
{
edgeMap.insert(std::make_pair(tri->V[(i + 1) % 3], tri->V[(i + 2) % 3]));
break;
}
}
removed[j++] = TriangleKey<true>(tri->V[0], tri->V[1], tri->V[2]);
}
if (edgeMap.size() != vertex->TAdjacent.size())
{
return VCM_UNEXPECTED_ERROR;
}
// Connect the edges into a polygon.
linkVertices.resize(numVertices);
auto iter = edgeMap.begin();
for (int i = 0; i < numVertices; ++i)
{
linkVertices[i] = iter->first;
iter = edgeMap.find(iter->second);
if (iter == edgeMap.end())
{
return VCM_UNEXPECTED_ERROR;
}
}
if (iter->first != linkVertices[0])
{
return VCM_UNEXPECTED_ERROR;
}
// Project the polygon onto the plane containing the incoming
// vertex and having the vertex normal. The projected polygon
// is computed so that the incoming vertex is projected to (0,0).
Vector3<Real> center = mPositions[vertex->V];
Vector3<Real> basis[3];
basis[0] = vertex->normal;
ComputeOrthogonalComplement(1, basis);
std::vector<Vector2<Real>> projected(numVertices);
std::vector<int> indices(numVertices);
for (int i = 0; i < numVertices; ++i)
{
Vector3<Real> diff = mPositions[linkVertices[i]] - center;
projected[i][0] = Dot(basis[1], diff);
projected[i][1] = Dot(basis[2], diff);
indices[i] = i;
}
// The polygon must be simple in order to triangulate it.
Polygon2<Real> polygon(projected.data(), numVertices, indices.data(), true);
if (polygon.IsSimple())
{
TriangulateEC<Real, Real> triangulator(numVertices, projected.data());
triangulator();
auto const& triangles = triangulator.GetTriangles();
if (triangles.size() == 0)
{
return VCM_UNEXPECTED_ERROR;
}
int const numTriangles = static_cast<int>(triangles.size());
inserted.resize(numTriangles);
for (int t = 0; t < numTriangles; ++t)
{
inserted[t] = TriangleKey<true>(
linkVertices[triangles[t][0]],
linkVertices[triangles[t][1]],
linkVertices[triangles[t][2]]);
}
return VCM_ALLOWED;
}
else
{
return VCM_DEFERRED;
}
}
int Collapsed(std::vector<TriangleKey<true>> const& removed,
std::vector<TriangleKey<true>> const& inserted, std::vector<int> const& linkVertices)
{
// The triangles that were disconnected from the link edges are
// guaranteed to allow manifold reconnection to 'inserted'
// triangles. On the insertion, each diagonal of the link becomes
// a mesh edge and shares two (link) triangles. It is possible
// that the mesh already contains the (diagonal) edge, which will
// lead to a nonmanifold connection, which we cannot allow. The
// following code traps this condition and restores the mesh to
// its state before the 'Remove(...)' call.
bool isCollapsible = true;
auto const& emap = mMesh.GetEdges();
std::set<EdgeKey<false>> edges;
for (auto const& tri : inserted)
{
for (int k0 = 2, k1 = 0; k1 < 3; k0 = k1++)
{
EdgeKey<false> edge(tri.V[k0], tri.V[k1]);
if (edges.find(edge) == edges.end())
{
edges.insert(edge);
}
else
{
// The edge has been visited twice, so it is a
// diagonal of the link.
auto eelement = emap.find(edge);
if (eelement != emap.end())
{
if (eelement->second->T[1])
{
// The edge will not allow a manifold
// connection.
isCollapsible = false;
break;
}
}
edges.erase(edge);
}
};
if (!isCollapsible)
{
return VCM_DEFERRED;
}
}
// Remove the old triangle neighborhood, which will lead to the
// vertex itself being removed from the mesh.
for (auto tri : removed)
{
mMesh.Remove(tri.V[0], tri.V[1], tri.V[2]);
}
// Insert the new triangulation.
for (auto const& tri : inserted)
{
mMesh.Insert(tri.V[0], tri.V[1], tri.V[2]);
}
// If the Remove(...) calls remove a boundary vertex that is in
// the link vertices, the Insert(...) calls will insert the
// boundary vertex again. We must re-tag those boundary
// vertices.
auto const& vmap = mMesh.GetVertices();
size_t const numVertices = linkVertices.size();
for (size_t i0 = numVertices - 1, i1 = 0; i1 < numVertices; i0 = i1++)
{
EdgeKey<false> ekey(linkVertices[i0], linkVertices[i1]);
auto eelement = emap.find(ekey);
if (eelement == emap.end())
{
return VCM_UNEXPECTED_ERROR;
}
auto edge = eelement->second.get();
if (!edge)
{
return VCM_UNEXPECTED_ERROR;
}
if (edge->T[0] && !edge->T[1])
{
for (int k = 0; k < 2; ++k)
{
auto velement = vmap.find(edge->V[k]);
if (velement == vmap.end())
{
return VCM_UNEXPECTED_ERROR;
}
auto vertex = static_cast<VCVertex*>(velement->second.get());
vertex->isBoundary = true;
}
}
}
return VCM_ALLOWED;
}
int mNumPositions;
Vector3<Real> const* mPositions;
VETManifoldMesh mMesh;
MinHeap<int, Real> mMinHeap;
std::map<int, typename MinHeap<int, Real>::Record*> mHeapRecords;
};
}