From e5d8f76262bc29498527f26d13bd15a8aed427f6 Mon Sep 17 00:00:00 2001 From: jasonlmfong Date: Sat, 22 Nov 2025 23:30:17 -0500 Subject: [PATCH 1/3] fix bug in solver matrix for GH QEM --- .../src/scene/surface/Surface_GarlandHeckbert.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Model-Modifier/src/scene/surface/Surface_GarlandHeckbert.cpp b/Model-Modifier/src/scene/surface/Surface_GarlandHeckbert.cpp index 7dcf70e..0b65a3e 100644 --- a/Model-Modifier/src/scene/surface/Surface_GarlandHeckbert.cpp +++ b/Model-Modifier/src/scene/surface/Surface_GarlandHeckbert.cpp @@ -74,9 +74,9 @@ glm::mat4 Surface::ComputeQuadric(VertexRecord v0) 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][0] = Quad[0][0]; MatQ[1][0] = Quad[0][1]; MatQ[2][0] = Quad[0][2]; MatQ[3][0] = Quad[0][3]; + MatQ[0][1] = Quad[0][1]; MatQ[1][1] = Quad[1][1]; MatQ[2][1] = Quad[1][2]; MatQ[3][1] = Quad[1][3]; + MatQ[0][2] = Quad[0][2]; MatQ[1][2] = Quad[1][2]; MatQ[2][2] = Quad[2][2]; MatQ[3][2] = Quad[2][3]; MatQ[0][3] = 0; MatQ[1][3] = 0; MatQ[2][3] = 0; MatQ[3][3] = 1; return MatQ; } From d2f5a615604f4b0cebbcdec66719d90516290155 Mon Sep 17 00:00:00 2001 From: jasonlmfong Date: Sat, 22 Nov 2025 23:51:58 -0500 Subject: [PATCH 2/3] add gitignore for cmake --- .gitignore | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 292387a..3c889cb 100644 --- a/.gitignore +++ b/.gitignore @@ -208,4 +208,11 @@ MigrationBackup/ # Executables *.exe *.out -*.app \ No newline at end of file +*.app + +## CMake build +CMakeFiles/ +*.vcxproj* +cmake_install.cmake +CMakeCache.txt +*.sln From 30dd5006dff057034c2796ed0103576c3e4948dd Mon Sep 17 00:00:00 2001 From: jasonlmfong Date: Sun, 23 Nov 2025 00:08:22 -0500 Subject: [PATCH 3/3] Add LRZ QEM simplification --- Model-Modifier/CMakeLists.txt | 1 + .../gallery/Screenshot_2025-11-23_0750.png | Bin 0 -> 59037 bytes Model-Modifier/src/main.cpp | 14 + Model-Modifier/src/scene/surface/Surface.h | 13 +- .../scene/surface/Surface_GarlandHeckbert.cpp | 9 +- .../surface/Surface_LiuRahimzadehZordan.cpp | 494 ++++++++++++++++++ README.md | 1 + 7 files changed, 526 insertions(+), 6 deletions(-) create mode 100644 Model-Modifier/gallery/Screenshot_2025-11-23_0750.png create mode 100644 Model-Modifier/src/scene/surface/Surface_LiuRahimzadehZordan.cpp diff --git a/Model-Modifier/CMakeLists.txt b/Model-Modifier/CMakeLists.txt index c4124cb..8645ff6 100644 --- a/Model-Modifier/CMakeLists.txt +++ b/Model-Modifier/CMakeLists.txt @@ -485,6 +485,7 @@ set(Source_Files "src/scene/surface/Surface_CatmullClark.cpp" "src/scene/surface/Surface_DooSabin.cpp" "src/scene/surface/Surface_GarlandHeckbert.cpp" + "src/scene/surface/Surface_LiuRahimzadehZordan.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-23_0750.png b/Model-Modifier/gallery/Screenshot_2025-11-23_0750.png new file mode 100644 index 0000000000000000000000000000000000000000..11a339ce36502f2b02782a8af6cb5c64063fda96 GIT binary patch literal 59037 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 SwBsoFXXcD~(@(hu#QzVls9k>m literal 0 HcmV?d00001 diff --git a/Model-Modifier/src/main.cpp b/Model-Modifier/src/main.cpp index 3e2a8e1..64e97af 100644 --- a/Model-Modifier/src/main.cpp +++ b/Model-Modifier/src/main.cpp @@ -380,6 +380,20 @@ int main() ImGui::Indent(); ImGui::SliderInt("Desired count", &desiredTriCount, triCount/5, triCount); ImGui::Unindent(); + if (ImGui::Button("Liu Rahimzadeh Zordan Simplification Surface")) + { + obj.MakeTriangleMesh(); // Triangulate first + Surface LRZ(obj); + static float alpha = 0.5f; // default balanced weight + obj = LRZ.LineQEM(desiredTriCount, alpha); + ModifyModel = true; + } + ImGui::Indent(); + ImGui::SliderInt("Desired count", &desiredTriCount, triCount/5, triCount); + static float alpha = 0.5f; + ImGui::SliderFloat("Alpha (edge preservation)", &alpha, 0.0f, 1.0f); + ImGui::Text("0.0 = smooth, 0.5 = balanced, 1.0 = sharp edges"); + ImGui::Unindent(); ImGui::Unindent(); } diff --git a/Model-Modifier/src/scene/surface/Surface.h b/Model-Modifier/src/scene/surface/Surface.h index aab56e0..21ddd49 100644 --- a/Model-Modifier/src/scene/surface/Surface.h +++ b/Model-Modifier/src/scene/surface/Surface.h @@ -46,6 +46,9 @@ struct ValidPair bool edge; float error; glm::vec3 newVert; + // weight parameter balancing point vs line quadrics + // only for augmented version + float alpha; }; struct CompareValidPairs @@ -80,11 +83,16 @@ class Surface std::unordered_map> pointsPerEdge ); Object LoOutputOBJ(std::vector edgePoints); - glm::mat4 ComputeQuadric(VertexRecord v0); + // Shared QEM helpers + glm::mat4 ComputePlaneQuadric(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(); + Object QEMOutputOBJ(); // shared output builder for both QEM variants + // Line Quadric specific helpers + glm::vec3 ComputeVertexNormal(VertexRecord v0); + glm::mat4 ComputeLineQuadric(VertexRecord v0); + glm::mat4 ComputeWeightedQuadric(const glm::mat4& planeQuadric, const glm::mat4& lineQuadric, float alpha); // Modification algorithms Object Beehive(); @@ -93,6 +101,7 @@ class Surface Object DooSabin(); Object Loop(); Object QEM(unsigned int desiredCount); + Object LineQEM(unsigned int desiredCount, float alpha = 0.5f); public: std::vector m_Vertices; diff --git a/Model-Modifier/src/scene/surface/Surface_GarlandHeckbert.cpp b/Model-Modifier/src/scene/surface/Surface_GarlandHeckbert.cpp index 0b65a3e..6dea97b 100644 --- a/Model-Modifier/src/scene/surface/Surface_GarlandHeckbert.cpp +++ b/Model-Modifier/src/scene/surface/Surface_GarlandHeckbert.cpp @@ -3,7 +3,8 @@ ////////// helpers to build the Object ////////// -Object Surface::GHOutputOBJ() +// Shared output builder for both QEM and LineQEM +Object Surface::QEMOutputOBJ() { // build new Object class std::vector VertexPos; @@ -53,7 +54,7 @@ Object Surface::GHOutputOBJ() ////////// 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 Surface::ComputePlaneQuadric(VertexRecord v0) { glm::mat4 quadric{ 0.0f }; // for each neighbouring face, compute K_p @@ -179,7 +180,7 @@ Object Surface::QEM(unsigned int desiredCount) for (unsigned int i = 0; i < numVertices; i++) { - glm::mat4 quadric = ComputeQuadric(m_Vertices[i]); + glm::mat4 quadric = ComputePlaneQuadric(m_Vertices[i]); // add penalty quadric for boundary vertices bool isBoundaryVertex = false; @@ -517,5 +518,5 @@ Object Surface::QEM(unsigned int desiredCount) } } - return GHOutputOBJ(); + return QEMOutputOBJ(); } diff --git a/Model-Modifier/src/scene/surface/Surface_LiuRahimzadehZordan.cpp b/Model-Modifier/src/scene/surface/Surface_LiuRahimzadehZordan.cpp new file mode 100644 index 0000000..ad52beb --- /dev/null +++ b/Model-Modifier/src/scene/surface/Surface_LiuRahimzadehZordan.cpp @@ -0,0 +1,494 @@ +#include "Surface.h" + + +////////// helpers for the LRZ algorithm ////////// + +// compute line quadric by constructing quadric matrices from edges adjacent to vertex +// note this is not the same as the paper, we sum over adjacent edges instead of using vertex normal +// the proper implementation is below +glm::mat4 Surface::ComputeLineQuadric(VertexRecord v0) +{ + glm::mat4 lineQuadric{ 0.0f }; + glm::vec3 position = v0.position; + + // for each adjacent edge, create a line constraint + for (unsigned int edgeIdx : v0.adjEdgesIdx) + { + if (edgeIdx >= m_Edges.size()) continue; + + EdgeRecord edge = m_Edges[edgeIdx]; + + // get the other endpoint of the edge + glm::vec3 otherPos; + if (edge.endPoint1Idx < m_Vertices.size() && edge.endPoint2Idx < m_Vertices.size()) + { + if (m_Vertices[edge.endPoint1Idx].position == position) + { + otherPos = m_Vertices[edge.endPoint2Idx].position; + } + else + { + otherPos = m_Vertices[edge.endPoint1Idx].position; + } + } + else + { + continue; + } + + // compute line direction + glm::vec3 lineDir = glm::normalize(otherPos - position); + + // create perpendicular constraint planes for the line + // we need two orthogonal planes that contain the line + glm::vec3 perp1, perp2; + + // find first perpendicular vector + if (std::abs(lineDir.x) < 0.9f) + { + perp1 = glm::normalize(glm::cross(lineDir, glm::vec3(1.0f, 0.0f, 0.0f))); + } + else + { + perp1 = glm::normalize(glm::cross(lineDir, glm::vec3(0.0f, 1.0f, 0.0f))); + } + + // second perpendicular is orthogonal to both line and first perpendicular + perp2 = glm::normalize(glm::cross(lineDir, perp1)); + + // create plane equations: the point should lie on the line + // perpendicular constraint: n · (x - p) = 0 → n · x = n · p + glm::vec4 plane1{ perp1, -glm::dot(perp1, position) }; + glm::vec4 plane2{ perp2, -glm::dot(perp2, position) }; + + // add both perpendicular plane quadrics + lineQuadric += glm::outerProduct(plane1, plane1); + lineQuadric += glm::outerProduct(plane2, plane2); + } + + return lineQuadric; +} + +// proper implementation of line quadric +// glm::mat4 Surface::ComputeLineQuadric(VertexRecord v0) +// { +// glm::mat4 lineQuadric{ 0.0f }; +// glm::vec3 position = v0.position; + +// // Get (area-weighted) vertex normal +// glm::vec3 vertNormal = glm::normalize(ComputeVertexNormal(v0)); + +// // create perpendicular constraint planes for the line +// // we need two orthogonal planes that contain the line +// glm::vec3 perp1, perp2; + +// // find first perpendicular vector +// if (std::abs(vertNormal.x) < 0.9f) +// { +// perp1 = glm::normalize(glm::cross(vertNormal, glm::vec3(1.0f, 0.0f, 0.0f))); +// } +// else +// { +// perp1 = glm::normalize(glm::cross(vertNormal, glm::vec3(0.0f, 1.0f, 0.0f))); +// } + +// // second perpendicular is orthogonal to both line and first perpendicular +// perp2 = glm::normalize(glm::cross(vertNormal, perp1)); + +// // create plane equations: the point should lie on the line +// // perpendicular constraint: n · (x - p) = 0 → n · x = n · p +// glm::vec4 plane1{ perp1, -glm::dot(perp1, position) }; +// glm::vec4 plane2{ perp2, -glm::dot(perp2, position) }; + +// // add both perpendicular plane quadrics +// lineQuadric += glm::outerProduct(plane1, plane1); +// lineQuadric += glm::outerProduct(plane2, plane2); + +// return lineQuadric; +// } + +glm::mat4 Surface::ComputeWeightedQuadric(const glm::mat4& planeQuadric, const glm::mat4& lineQuadric, float alpha) +{ + return planeQuadric + alpha * lineQuadric; +} + + +////////// algorithms ////////// + +// Liu Rahimzadeh Zordan QEM simplification with line quadric constraints +Object Surface::LineQEM(unsigned int desiredCount, float alpha) +{ + 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 both point and line quadrics for each vertex + std::unordered_map planeQuadricLookup; + std::unordered_map lineQuadricLookup; + const float BOUNDARY_WEIGHT = 1000.0f; // large weight to preserve boundaries + + for (unsigned int i = 0; i < numVertices; i++) + { + glm::mat4 planeQuadric = ComputePlaneQuadric(m_Vertices[i]); + glm::mat4 lineQuadric = ComputeLineQuadric(m_Vertices[i]); + + // add penalty quadric for boundary vertices to point quadric + for (unsigned int edgeIdx : m_Vertices[i].adjEdgesIdx) + { + if (boundaryEdges.find(edgeIdx) != boundaryEdges.end()) + { + // 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) }; + planeQuadric += BOUNDARY_WEIGHT * glm::outerProduct(constraintPlane, constraintPlane); + } + } + } + + planeQuadricLookup.insert({ i, planeQuadric }); + lineQuadricLookup.insert({ i, lineQuadric }); + } + + 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; + newPair.alpha = alpha; + 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; + newPair.alpha = alpha; + validPairs.push_back(newPair); + } + // else, do nothing, not a valid pair + } + } + + // compute the new point and error associated for each valid pair + for (ValidPair& validPair : validPairs) + { + ComputeOptimalVertexAndError( + validPair, + ComputeWeightedQuadric(planeQuadricLookup[validPair.vertOne], lineQuadricLookup[validPair.vertOne], alpha), + ComputeWeightedQuadric(planeQuadricLookup[validPair.vertTwo], lineQuadricLookup[validPair.vertTwo], alpha) + ); + } + + // 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 + for (unsigned int faceIdx : facesToUpdate) + { + if (faceIdx < m_Faces.size()) + { + 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 + if (containsVertOne != containsVertTwo) + { + // 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 + 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 + for (VertexRecord& vertex : m_Vertices) + { + UpdateAdjacencyIndices(vertex.adjFacesIdx, removedFaceIndices); + } + + // update all edge adjacency lists + for (EdgeRecord& edge : m_Edges) + { + UpdateAdjacencyIndices(edge.adjFacesIdx, removedFaceIndices); + } + + numFaces = static_cast(m_Faces.size()); + + // update both point and line quadrics for the merged vertex + planeQuadricLookup[leastCost.vertOne] = planeQuadricLookup[leastCost.vertOne] + planeQuadricLookup[leastCost.vertTwo]; + lineQuadricLookup[leastCost.vertOne] = lineQuadricLookup[leastCost.vertOne] + lineQuadricLookup[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, + ComputeWeightedQuadric(planeQuadricLookup[leastCost.vertOne], lineQuadricLookup[leastCost.vertOne], alpha), + ComputeWeightedQuadric(planeQuadricLookup[otherVert], lineQuadricLookup[otherVert], alpha) + ); + + // 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, + ComputeWeightedQuadric(planeQuadricLookup[leastCost.vertOne], lineQuadricLookup[leastCost.vertOne], alpha), + ComputeWeightedQuadric(planeQuadricLookup[otherVert], lineQuadricLookup[otherVert], alpha) + ); + + // 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 QEMOutputOBJ(); +} diff --git a/README.md b/README.md index d73afb4..588c098 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Model-Modifier is my `C++17` and `OpenGL` implementation of an interactive mesh - [x] [Loop](https://en.wikipedia.org/wiki/Loop_subdivision_surface) - [x] Simplification surface - [x] [QEM](https://www.cs.cmu.edu/~./garland/Papers/quadrics.pdf) + - [x] [Line QEM](https://www.dgp.toronto.edu/~hsuehtil/pdf/lineQuadric.pdf) - [x] Shading options - [x] Flat shading (Per-face normals) - [x] Smooth shading (Per-vertex normals)