From fdde829137f14cc7c5e1451b69dce2386964301d Mon Sep 17 00:00:00 2001 From: jasonlmfong Date: Sat, 22 Nov 2025 17:35:10 -0500 Subject: [PATCH] ad QEM simplification --- Model-Modifier/CMakeLists.txt | 1 + .../gallery/Screenshot_2025-11-22_1549.png | Bin 0 -> 61383 bytes Model-Modifier/src/main.cpp | 28 +- Model-Modifier/src/scene/surface/Surface.cpp | 18 - Model-Modifier/src/scene/surface/Surface.h | 27 +- .../scene/surface/Surface_GarlandHeckbert.cpp | 521 ++++++++++++++++++ README.md | 4 +- 7 files changed, 567 insertions(+), 32 deletions(-) create mode 100644 Model-Modifier/gallery/Screenshot_2025-11-22_1549.png create mode 100644 Model-Modifier/src/scene/surface/Surface_GarlandHeckbert.cpp diff --git a/Model-Modifier/CMakeLists.txt b/Model-Modifier/CMakeLists.txt index e4ecb2d..c4124cb 100644 --- a/Model-Modifier/CMakeLists.txt +++ b/Model-Modifier/CMakeLists.txt @@ -484,6 +484,7 @@ set(Source_Files "src/scene/surface/Surface.cpp" "src/scene/surface/Surface_CatmullClark.cpp" "src/scene/surface/Surface_DooSabin.cpp" + "src/scene/surface/Surface_GarlandHeckbert.cpp" "src/scene/surface/Surface_Loop.cpp" "src/scene/util/OrderVertices.cpp" "src/scene/util/PlaneProjection.cpp" diff --git a/Model-Modifier/gallery/Screenshot_2025-11-22_1549.png b/Model-Modifier/gallery/Screenshot_2025-11-22_1549.png new file mode 100644 index 0000000000000000000000000000000000000000..624df19428a8a50e3d2566864016279267882ec5 GIT binary patch literal 61383 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ͳ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 C&fX6I literal 0 HcmV?d00001 diff --git a/Model-Modifier/src/main.cpp b/Model-Modifier/src/main.cpp index 36e9778..3e2a8e1 100644 --- a/Model-Modifier/src/main.cpp +++ b/Model-Modifier/src/main.cpp @@ -135,6 +135,10 @@ int main() Object obj = objects.findObj(currObject); Material meshMat; + // object triangle count (QEM) + int triCount = static_cast(obj.m_TriFaceIndices.size()); + int desiredTriCount = triCount; + VertexBufferLayout layout; layout.Push(3); // 3d coordinates layout.Push(3); // normals @@ -366,17 +370,17 @@ int main() obj = Lo.Loop(); ModifyModel = true; } - //if (ImGui::Button("Garland Heckbert Simplication Surface")) - //{ - // Surface GH(obj); - // obj = GH.QEM(); - // mesh.Rebuild(obj); // rebuild mesh based on object info - // numFaces = static_cast(mesh.m_Object.m_FaceIndices.size()); // update number of faces - - // objectVA.Bind(); - // objectVB.AssignData(mesh.m_OutVertices, mesh.m_OutNumVert * sizeof(float), DRAW_MODE::STATIC); - // objectIB.AssignData(mesh.m_OutIndices, mesh.m_OutNumIdx, DRAW_MODE::STATIC); - //} + if (ImGui::Button("Garland Heckbert Simplification Surface")) + { + obj.MakeTriangleMesh(); // Triangulate first + Surface GH(obj); + obj = GH.QEM(desiredTriCount); + ModifyModel = true; + } + ImGui::Indent(); + ImGui::SliderInt("Desired count", &desiredTriCount, triCount/5, triCount); + ImGui::Unindent(); + ImGui::Unindent(); } @@ -626,6 +630,7 @@ int main() currObject = nextObject; obj = objects.findObj(currObject); // search for the object requested + triCount = static_cast(obj.m_TriFaceIndices.size()); desiredTriCount = triCount; ModifyModel = true; } @@ -635,6 +640,7 @@ int main() { mesh.Rebuild(obj); // rebuild mesh based on object info numFaces = static_cast(mesh.m_Object.m_FaceIndices.size()); // update number of faces + triCount = static_cast(obj.m_TriFaceIndices.size()); desiredTriCount = triCount; objectVA.Bind(); objectVB.AssignData(mesh.m_OutVertices, mesh.m_OutNumVert * sizeof(float), DRAW_MODE::STATIC); diff --git a/Model-Modifier/src/scene/surface/Surface.cpp b/Model-Modifier/src/scene/surface/Surface.cpp index b9dec95..22cd2e8 100644 --- a/Model-Modifier/src/scene/surface/Surface.cpp +++ b/Model-Modifier/src/scene/surface/Surface.cpp @@ -137,21 +137,3 @@ glm::vec3 Surface::ComputeFaceNormal(glm::vec3 pos0, glm::vec3 pos1, glm::vec3 p { return glm::normalize(glm::cross(pos1 - pos0, pos2 - pos0)); } - -// computer quadric matrix by summing all K_p matrices of a vertice v0 -glm::mat4 Surface::ComputeQuadric(VertexRecord v0) -{ - glm::mat4 quadric{ 0.0f }; - // for each neighbouring face, compute K_p - glm::vec3 position = v0.position; - for (unsigned int faceIdx : v0.adjFacesIdx) - { - FaceRecord face = m_Faces[faceIdx]; - glm::vec3 faceNormal = ComputeFaceNormal(face); - glm::vec4 plane{ faceNormal, -glm::dot(faceNormal, position) }; // plane equation ax+by+cz+d = 0 - - quadric += glm::outerProduct(plane, plane); // K_p - } - - return quadric; -} diff --git a/Model-Modifier/src/scene/surface/Surface.h b/Model-Modifier/src/scene/surface/Surface.h index 7f4baff..aab56e0 100644 --- a/Model-Modifier/src/scene/surface/Surface.h +++ b/Model-Modifier/src/scene/surface/Surface.h @@ -3,6 +3,7 @@ #include #include #include +#include #include "../../external/glm/ext/vector_float3.hpp" #include "../../external/glm/ext/vector_uint2.hpp" @@ -37,6 +38,24 @@ struct FaceRecord std::vector edgesIdx; // each face can have n edges }; +// QEM +struct ValidPair +{ + unsigned int vertOne; + unsigned int vertTwo; + bool edge; + float error; + glm::vec3 newVert; +}; + +struct CompareValidPairs +{ + bool operator()(const ValidPair& a, const ValidPair& b) const + { + return a.error > b.error; // min heap + } +}; + class Surface { public: @@ -62,6 +81,10 @@ class Surface ); Object LoOutputOBJ(std::vector edgePoints); glm::mat4 ComputeQuadric(VertexRecord v0); + glm::mat4 BuildQuadricSolverMatrix(const glm::mat4& Quad); + void ComputeOptimalVertexAndError(ValidPair& validPair, const glm::mat4& quadric1, const glm::mat4& quadric2); + void UpdateAdjacencyIndices(std::vector& adjFaces, const std::vector& removedFaceIndices); + Object GHOutputOBJ(); // Modification algorithms Object Beehive(); @@ -69,7 +92,7 @@ class Surface Object CatmullClark(); Object DooSabin(); Object Loop(); - Object QEM(); + Object QEM(unsigned int desiredCount); public: std::vector m_Vertices; @@ -80,4 +103,6 @@ class Surface glm::vec3 m_Max; std::unordered_map> m_EdgeIdxLookup; +private: + std::priority_queue, CompareValidPairs> m_QuadricErrorHeap; }; diff --git a/Model-Modifier/src/scene/surface/Surface_GarlandHeckbert.cpp b/Model-Modifier/src/scene/surface/Surface_GarlandHeckbert.cpp new file mode 100644 index 0000000..7dcf70e --- /dev/null +++ b/Model-Modifier/src/scene/surface/Surface_GarlandHeckbert.cpp @@ -0,0 +1,521 @@ +#include "Surface.h" + + +////////// helpers to build the Object ////////// + +Object Surface::GHOutputOBJ() +{ + // build new Object class + std::vector VertexPos; + std::unordered_map>> VertLookup; + std::vector> FaceIndices; + std::unordered_map NumberPolygons; + + for (VertexRecord vert : m_Vertices) + { + glm::vec3 vertPos = vert.position; + getVertIndex(vertPos, VertexPos, VertLookup); + } + + for (FaceRecord face : m_Faces) + { + std::vector vertsIdx; + + for (unsigned int i = 0; i < face.verticesIdx.size(); i++) + { + if (face.verticesIdx[i] < m_Vertices.size()) + { + glm::vec3 vert = m_Vertices[face.verticesIdx[i]].position; + vertsIdx.push_back(getVertIndex(vert, VertexPos, VertLookup)); + } + } + + // Only add face if it has at least 3 unique vertices + std::set uniqueVerts(vertsIdx.begin(), vertsIdx.end()); + if (uniqueVerts.size() >= 3) + { + FaceIndices.push_back(vertsIdx); + NumberPolygons[static_cast(vertsIdx.size())] += 1; + } + } + + // build object + Object Obj; + Obj.m_Min = m_Min; Obj.m_Max = m_Max; + Obj.m_VertexPos = VertexPos; Obj.m_FaceIndices = FaceIndices; + Obj.m_NumPolygons = NumberPolygons; + Obj.TriangulateFaces(); + + return Obj; +} + + +////////// helpers for the GH algorithm ////////// + +// computer quadric matrix by summing all K_p matrices of a vertice v0 +glm::mat4 Surface::ComputeQuadric(VertexRecord v0) +{ + glm::mat4 quadric{ 0.0f }; + // for each neighbouring face, compute K_p + glm::vec3 position = v0.position; + for (unsigned int faceIdx : v0.adjFacesIdx) + { + FaceRecord face = m_Faces[faceIdx]; + glm::vec3 faceNormal = ComputeFaceNormal(face); + glm::vec4 plane{ faceNormal, -glm::dot(faceNormal, position) }; // plane equation ax+by+cz+d = 0 + + quadric += glm::outerProduct(plane, plane); // K_p + } + + return quadric; +} + +// build the matrix for solving optimal vertex position +glm::mat4 Surface::BuildQuadricSolverMatrix(const glm::mat4& Quad) +{ + glm::mat4 MatQ; + MatQ[0][0] = Quad[0][0]; MatQ[1][0] = Quad[1][0]; MatQ[2][0] = Quad[2][0]; MatQ[3][0] = Quad[3][0]; + MatQ[0][1] = Quad[0][1]; MatQ[1][1] = Quad[1][1]; MatQ[2][1] = Quad[2][1]; MatQ[3][1] = Quad[3][1]; + MatQ[0][2] = Quad[0][2]; MatQ[1][2] = Quad[1][2]; MatQ[2][2] = Quad[2][2]; MatQ[3][2] = Quad[3][2]; + MatQ[0][3] = 0; MatQ[1][3] = 0; MatQ[2][3] = 0; MatQ[3][3] = 1; + return MatQ; +} + +// compute the optimal vertex position and error for a valid pair contraction +void Surface::ComputeOptimalVertexAndError(ValidPair& validPair, const glm::mat4& quadric1, const glm::mat4& quadric2) +{ + glm::mat4 Quad = quadric1 + quadric2; + glm::mat4 MatQ = BuildQuadricSolverMatrix(Quad); + + if (glm::determinant(MatQ) != 0) + { + glm::vec4 newVPos = glm::inverse(MatQ) * glm::vec4{ 0, 0, 0, 1 }; + + // calculate error: v^T * Q * v + validPair.error = glm::dot(newVPos, Quad * newVPos); + + // change back to 3d coords from homogeneous coordinates + validPair.newVert = glm::vec3(newVPos) / newVPos.w; + } + else + { + glm::vec4 end1 = { m_Vertices[validPair.vertOne].position, 1.0f }; + float end1Error = glm::dot(end1, Quad * end1); + + glm::vec4 end2 = { m_Vertices[validPair.vertTwo].position, 1.0f }; + float end2Error = glm::dot(end2, Quad * end2); + + glm::vec4 mid = (end1 + end2) / 2.0f; + float midError = glm::dot(mid, Quad * mid); + + float minError = std::min({ end1Error, end2Error, midError }); + validPair.error = minError; + if (minError == end1Error) + { + validPair.newVert = glm::vec3(end1) / end1.w; + } + else if (minError == end2Error) + { + validPair.newVert = glm::vec3(end2) / end2.w; + } + else if (minError == midError) + { + validPair.newVert = glm::vec3(mid) / mid.w; + } + } +} + +// update adjacency indices after face removal, removing deleted indices and shifting remaining ones +void Surface::UpdateAdjacencyIndices(std::vector& adjFaces, const std::vector& removedFaceIndices) +{ + std::vector newAdjFaces; + for (unsigned int faceIdx : adjFaces) + { + // count how many removed faces have indices less than this one + unsigned int offset = 0; + for (unsigned int removedIdx : removedFaceIndices) + { + if (removedIdx < faceIdx) + { + offset++; + } + else if (removedIdx == faceIdx) + { + // this face was removed, skip it + offset = UINT_MAX; + break; + } + } + + if (offset != UINT_MAX) + { + newAdjFaces.push_back(faceIdx - offset); + } + } + adjFaces = newAdjFaces; +} + + +////////// algorithms ////////// + +// Garland Heckbert simplification surface algorithm +Object Surface::QEM(unsigned int desiredCount) +{ + unsigned int numVertices = static_cast(m_Vertices.size()); + + // identify boundary edges (edges with only one adjacent face) + std::set boundaryEdges; + for (unsigned int i = 0; i < m_Edges.size(); i++) + { + if (m_Edges[i].adjFacesIdx.size() == 1) + { + boundaryEdges.insert(i); + } + } + + // calculate quadric error for each vertex + std::unordered_map quadricLookup; + const float BOUNDARY_WEIGHT = 1000.0f; // large weight to preserve boundaries + + for (unsigned int i = 0; i < numVertices; i++) + { + glm::mat4 quadric = ComputeQuadric(m_Vertices[i]); + + // add penalty quadric for boundary vertices + bool isBoundaryVertex = false; + for (unsigned int edgeIdx : m_Vertices[i].adjEdgesIdx) + { + if (boundaryEdges.find(edgeIdx) != boundaryEdges.end()) + { + isBoundaryVertex = true; + + // Create constraint plane perpendicular to the boundary edge + EdgeRecord& edge = m_Edges[edgeIdx]; + glm::vec3 v1 = m_Vertices[edge.endPoint1Idx].position; + glm::vec3 v2 = m_Vertices[edge.endPoint2Idx].position; + glm::vec3 edgeDir = glm::normalize(v2 - v1); + + // for a boundary edge, create a perpendicular constraint with face normal of the adjacent face + if (!edge.adjFacesIdx.empty() && edge.adjFacesIdx[0] < m_Faces.size()) + { + glm::vec3 faceNormal = ComputeFaceNormal(m_Faces[edge.adjFacesIdx[0]]); + glm::vec3 perpendicular = glm::normalize(glm::cross(edgeDir, faceNormal)); + glm::vec4 constraintPlane{ perpendicular, -glm::dot(perpendicular, v1) }; + quadric += BOUNDARY_WEIGHT * glm::outerProduct(constraintPlane, constraintPlane); + } + } + } + + quadricLookup.insert({ i, quadric }); + } + + const float THRESHOLD = 0.05f; + + // select all valid pairs + std::vector> vertexPairLookup; // maintain vertex pairs + vertexPairLookup.resize(numVertices); + + std::vector validPairs; + for (unsigned int firstV = 0; firstV < numVertices; firstV++) + { + for (unsigned int secondV = firstV + 1; secondV < numVertices; secondV++) + { + auto searchx = m_EdgeIdxLookup.find(firstV); + if (searchx != m_EdgeIdxLookup.end()) + { + // search if ending vertex in our lookup + auto searchy = searchx->second.find(secondV); + if (searchy != searchx->second.end()) + { + // add the pair idx to the vertices + vertexPairLookup[firstV].insert(static_cast(validPairs.size())); + vertexPairLookup[secondV].insert(static_cast(validPairs.size())); + // found edge, add to valid pairs + ValidPair newPair{}; newPair.vertOne = firstV; newPair.vertTwo = secondV; newPair.edge = true; + validPairs.push_back(newPair); + } + } + // not found, check if the edges are close (distance smaller than threshold) + glm::vec3 firstPos = m_Vertices[firstV].position; + glm::vec3 secondPos = m_Vertices[secondV].position; + if (glm::distance(firstPos, secondPos) < THRESHOLD) + { + // add the pair idx to the vertices + vertexPairLookup[firstV].insert(static_cast(validPairs.size())); + vertexPairLookup[secondV].insert(static_cast(validPairs.size())); + // add to valid pairs + ValidPair newPair{}; newPair.vertOne = firstV; newPair.vertTwo = secondV; newPair.edge = false; + validPairs.push_back(newPair); + } + // else, do nothing, not a valid pair + } + } + + // compute the new point and error associated for each valid pair + + // each pair should contain a few pieces of information + // vertex 1, vertex 2, the new vertex position, the error after contraction, the quadric matrices for both 1 and 2, and the new vertex after contraction + for (ValidPair& validPair : validPairs) + { + ComputeOptimalVertexAndError(validPair, quadricLookup[validPair.vertOne], quadricLookup[validPair.vertTwo]); + } + + // create a min-heap to store all the valid pairs, ordered by error cost + m_QuadricErrorHeap = std::priority_queue, CompareValidPairs>(validPairs.begin(), validPairs.end()); + + // track which pairs have been contracted to avoid processing stale duplicates + std::set> contractedPairs; + + // track which vertices have been merged into others + std::set deletedVertices; + + // iteratively remove the validpair with the lowest cost, until numFaces == desiredCount + unsigned int numFaces = static_cast(m_Faces.size()); + while (numFaces > desiredCount && !m_QuadricErrorHeap.empty()) + { + ValidPair leastCost = m_QuadricErrorHeap.top(); + m_QuadricErrorHeap.pop(); + + // skip if either vertex has been deleted/merged + if (deletedVertices.find(leastCost.vertOne) != deletedVertices.end() || + deletedVertices.find(leastCost.vertTwo) != deletedVertices.end()) + { + continue; + } + + // skip if this pair has already been contracted + auto pairKey = std::make_pair( + std::min(leastCost.vertOne, leastCost.vertTwo), + std::max(leastCost.vertOne, leastCost.vertTwo) + ); + if (contractedPairs.find(pairKey) != contractedPairs.end()) + { + continue; + } + + // mark this pair as contracted + contractedPairs.insert(pairKey); + + // mark vertTwo as deleted + deletedVertices.insert(leastCost.vertTwo); + + // store original face normals BEFORE any modifications, used to compare later + std::set facesToUpdate; + facesToUpdate.insert(m_Vertices[leastCost.vertTwo].adjFacesIdx.begin(), m_Vertices[leastCost.vertTwo].adjFacesIdx.end()); + facesToUpdate.insert(m_Vertices[leastCost.vertOne].adjFacesIdx.begin(), m_Vertices[leastCost.vertOne].adjFacesIdx.end()); + + std::unordered_map originalNormals; + for (unsigned int faceIdx : facesToUpdate) + { + if (faceIdx < m_Faces.size()) + { + originalNormals[faceIdx] = ComputeFaceNormal(m_Faces[faceIdx]); + } + } + + // contract the current pair + // move vertOne to the new position, and merge all references to vertTwo into vertOne + m_Vertices[leastCost.vertOne].position = leastCost.newVert; + + // merge all faces, edges from vertTwo into vertOne + for (unsigned int faceIdx : m_Vertices[leastCost.vertTwo].adjFacesIdx) + { + if (std::find(m_Vertices[leastCost.vertOne].adjFacesIdx.begin(), + m_Vertices[leastCost.vertOne].adjFacesIdx.end(), faceIdx) + == m_Vertices[leastCost.vertOne].adjFacesIdx.end()) + { + m_Vertices[leastCost.vertOne].adjFacesIdx.push_back(faceIdx); + } + } + + for (unsigned int edgeIdx : m_Vertices[leastCost.vertTwo].adjEdgesIdx) + { + if (std::find(m_Vertices[leastCost.vertOne].adjEdgesIdx.begin(), + m_Vertices[leastCost.vertOne].adjEdgesIdx.end(), edgeIdx) + == m_Vertices[leastCost.vertOne].adjEdgesIdx.end()) + { + m_Vertices[leastCost.vertOne].adjEdgesIdx.push_back(edgeIdx); + } + } + + // update all faces that reference vertTwo to reference vertOne instead + // (facesToUpdate and originalNormals were already computed before vertex position change) + for (unsigned int faceIdx : facesToUpdate) + { + if (faceIdx < m_Faces.size()) // Bounds check + { + FaceRecord& face = m_Faces[faceIdx]; + + // check if this face contains BOTH vertices - if so, it will become degenerate + bool containsVertOne = std::find(face.verticesIdx.begin(), face.verticesIdx.end(), leastCost.vertOne) != face.verticesIdx.end(); + bool containsVertTwo = std::find(face.verticesIdx.begin(), face.verticesIdx.end(), leastCost.vertTwo) != face.verticesIdx.end(); + + // store original normal before any changes + glm::vec3 originalNormal = originalNormals[faceIdx]; + + // update vertex references + for (unsigned int& vertIdx : face.verticesIdx) + { + if (vertIdx == leastCost.vertTwo) + { + vertIdx = leastCost.vertOne; + } + } + + // only check orientation if face originally contained only ONE of the two vertices + // (faces with both vertices will become degenerate and be removed later) + if (containsVertOne != containsVertTwo) // basically an XOR here + { + // calculate new normal after vertex update + glm::vec3 newNormal = ComputeFaceNormal(face); + + // validate that both normals are non-zero before comparing + float originalLength = glm::length(originalNormal); + float newLength = glm::length(newNormal); + + if (originalLength > 1e-6f && newLength > 1e-6f) + { + // normalize for accurate dot product comparison + glm::vec3 origNormalized = originalNormal / originalLength; + glm::vec3 newNormalized = newNormal / newLength; + + // if new normal is opposite to original, flip the face to preserve orientation + if (glm::dot(newNormalized, origNormalized) < 0.0f) + { + std::reverse(face.verticesIdx.begin(), face.verticesIdx.end()); + } + } + } + } + } + + // update all edges that reference vertTwo to reference vertOne instead + for (unsigned int edgeIdx : m_Vertices[leastCost.vertTwo].adjEdgesIdx) + { + EdgeRecord& edge = m_Edges[edgeIdx]; + if (edge.endPoint1Idx == leastCost.vertTwo) + { + edge.endPoint1Idx = leastCost.vertOne; + } + if (edge.endPoint2Idx == leastCost.vertTwo) + { + edge.endPoint2Idx = leastCost.vertOne; + } + } + + // remove degenerate faces (after vertex updates are complete) + std::vector removedFaceIndices; + + for (unsigned int i = 0; i < m_Faces.size(); i++) + { + // check if face has duplicate vertices (degenerate after vertex merge) + std::set uniqueVerts(m_Faces[i].verticesIdx.begin(), m_Faces[i].verticesIdx.end()); + if (uniqueVerts.size() < m_Faces[i].verticesIdx.size()) + { + removedFaceIndices.push_back(i); + } + } + + // remove faces in reverse order to maintain indices + for (auto it = removedFaceIndices.rbegin(); it != removedFaceIndices.rend(); ++it) + { + m_Faces.erase(m_Faces.begin() + *it); + } + + // update all vertex adjacency lists: remove deleted indices and shift remaining ones + for (VertexRecord& vertex : m_Vertices) + { + UpdateAdjacencyIndices(vertex.adjFacesIdx, removedFaceIndices); + } + + // update all edge adjacency lists similarly + for (EdgeRecord& edge : m_Edges) + { + UpdateAdjacencyIndices(edge.adjFacesIdx, removedFaceIndices); + } + + numFaces = static_cast(m_Faces.size()); + + // update the quadric for the merged vertex + quadricLookup[leastCost.vertOne] = quadricLookup[leastCost.vertOne] + quadricLookup[leastCost.vertTwo]; + + // update the cost of all valid pairs involving the current pair + for (unsigned int pairIdx : vertexPairLookup[leastCost.vertOne]) + { + ValidPair& validPair = validPairs[pairIdx]; + + // skip if this pair was already contracted + auto checkPair = std::make_pair( + std::min(validPair.vertOne, validPair.vertTwo), + std::max(validPair.vertOne, validPair.vertTwo) + ); + if (contractedPairs.find(checkPair) != contractedPairs.end()) + { + continue; + } + + // determine which vertex in the pair is the one we need to update + unsigned int otherVert = (validPair.vertOne == leastCost.vertOne) ? validPair.vertTwo : validPair.vertOne; + + // recalculate error for this pair + ComputeOptimalVertexAndError(validPair, quadricLookup[leastCost.vertOne], quadricLookup[otherVert]); + + // push back to heap if neither vertex has been deleted + if (deletedVertices.find(validPair.vertOne) == deletedVertices.end() && + deletedVertices.find(validPair.vertTwo) == deletedVertices.end()) + { + m_QuadricErrorHeap.push(validPair); + } + } + + for (unsigned int pairIdx : vertexPairLookup[leastCost.vertTwo]) + { + ValidPair& validPair = validPairs[pairIdx]; + + // skip the pair we just contracted + if ((validPair.vertOne == leastCost.vertOne && validPair.vertTwo == leastCost.vertTwo) || + (validPair.vertOne == leastCost.vertTwo && validPair.vertTwo == leastCost.vertOne)) + { + continue; + } + + // skip if this pair was already contracted + auto checkPair = std::make_pair( + std::min(validPair.vertOne, validPair.vertTwo), + std::max(validPair.vertOne, validPair.vertTwo) + ); + if (contractedPairs.find(checkPair) != contractedPairs.end()) + { + continue; + } + + // determine which vertex in the pair is the one we need to update + unsigned int otherVert = (validPair.vertOne == leastCost.vertTwo) ? validPair.vertTwo : validPair.vertOne; + + // update the pair to reference vertOne instead of vertTwo + if (validPair.vertOne == leastCost.vertTwo) + { + validPair.vertOne = leastCost.vertOne; + } + if (validPair.vertTwo == leastCost.vertTwo) + { + validPair.vertTwo = leastCost.vertOne; + } + + // add this pair to vertOne's lookup + vertexPairLookup[leastCost.vertOne].insert(pairIdx); + + // recalculate error for this pair + ComputeOptimalVertexAndError(validPair, quadricLookup[leastCost.vertOne], quadricLookup[otherVert]); + + // push back to heap if neither vertex has been deleted + if (deletedVertices.find(validPair.vertOne) == deletedVertices.end() && + deletedVertices.find(validPair.vertTwo) == deletedVertices.end()) + { + m_QuadricErrorHeap.push(validPair); + } + } + } + + return GHOutputOBJ(); +} diff --git a/README.md b/README.md index 285680e..d73afb4 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ Model-Modifier is my `C++17` and `OpenGL` implementation of an interactive mesh - [x] [Catmull-Clark](https://en.wikipedia.org/wiki/Catmull%E2%80%93Clark_subdivision_surface) - [x] [Doo-Sabin](https://en.wikipedia.org/wiki/Doo%E2%80%93Sabin_subdivision_surface) - [x] [Loop](https://en.wikipedia.org/wiki/Loop_subdivision_surface) - - [ ] Simplification surface - - [ ] [QEM](https://www.cs.cmu.edu/~./garland/Papers/quadrics.pdf) + - [x] Simplification surface + - [x] [QEM](https://www.cs.cmu.edu/~./garland/Papers/quadrics.pdf) - [x] Shading options - [x] Flat shading (Per-face normals) - [x] Smooth shading (Per-vertex normals)