// David Eberly, Geometric Tools, Redmond WA 98052 // Copyright (c) 1998-2020 // 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.2019.08.13 #pragma once #include #include #include #include #include #include namespace WwiseGTE { template class VertexCollapseMesh { public: // Construction. VertexCollapseMesh(int numPositions, Vector3 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; if (!edge->T[1].lock()) { for (int i = 0; i < 2; ++i) { auto velement = vmap.find(edge->V[i]); auto vertex = std::static_pointer_cast(velement->second); 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 = std::static_pointer_cast(velement.second); Real weight; if (vertex->isBoundary) { weight = std::numeric_limits::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> removed; std::vector> 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::max(); mMinHeap.GetMinimum(v, weight); if (weight == std::numeric_limits::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 = std::static_pointer_cast(velement->second); std::vector> removed, inserted; std::vector 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 = std::static_pointer_cast(velement->second); 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::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::Zero()), isBoundary(false) { } static std::shared_ptr Create(int v) { return std::make_shared(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 const* positions) { Real weight = (Real)0; normal = { (Real)0, (Real)0, (Real)0 }; for (auto const& tri : TAdjacent) { Vector3 E0 = positions[tri->V[1]] - positions[tri->V[0]]; Vector3 E1 = positions[tri->V[2]] - positions[tri->V[0]]; Vector3 N = Cross(E0, E1); normal += N; weight += Length(N); } Normalize(normal); for (int index : VAdjacent) { Vector3 diff = positions[index] - positions[V]; weight += std::fabs(Dot(normal, diff)); } return weight; } Vector3 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(std::shared_ptr const& vertex, std::vector>& removed, std::vector>& inserted, std::vector& 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(vertex->TAdjacent.size()); removed.resize(numVertices); int j = 0; std::map 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(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 center = mPositions[vertex->V]; Vector3 basis[3]; basis[0] = vertex->normal; ComputeOrthogonalComplement(1, basis); std::vector> projected(numVertices); std::vector indices(numVertices); for (int i = 0; i < numVertices; ++i) { Vector3 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 polygon(projected.data(), numVertices, indices.data(), true); if (polygon.IsSimple()) { TriangulateEC triangulator(numVertices, projected.data()); triangulator(); auto const& triangles = triangulator.GetTriangles(); if (triangles.size() == 0) { return VCM_UNEXPECTED_ERROR; } int const numTriangles = static_cast(triangles.size()); inserted.resize(numTriangles); for (int t = 0; t < numTriangles; ++t) { inserted[t] = TriangleKey( linkVertices[triangles[t][0]], linkVertices[triangles[t][1]], linkVertices[triangles[t][2]]); } return VCM_ALLOWED; } else { return VCM_DEFERRED; } } int Collapsed(std::vector> const& removed, std::vector> const& inserted, std::vector 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> edges; for (auto const& tri : inserted) { for (int k0 = 2, k1 = 0; k1 < 3; k0 = k1++) { EdgeKey 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].lock()) { // 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 ekey(linkVertices[i0], linkVertices[i1]); auto eelement = emap.find(ekey); if (eelement == emap.end()) { return VCM_UNEXPECTED_ERROR; } auto edge = eelement->second; if (!edge) { return VCM_UNEXPECTED_ERROR; } if (edge->T[0].lock() && !edge->T[1].lock()) { for (int k = 0; k < 2; ++k) { auto velement = vmap.find(edge->V[k]); if (velement == vmap.end()) { return VCM_UNEXPECTED_ERROR; } auto vertex = std::static_pointer_cast(velement->second); vertex->isBoundary = true; } } } return VCM_ALLOWED; } int mNumPositions; Vector3 const* mPositions; VETManifoldMesh mMesh; MinHeap mMinHeap; std::map::Record*> mHeapRecords; }; }