source/render/MeshSimplifier.cpp (410 lines of code) (raw):

/** * Copyright 2004-present Facebook. All Rights Reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ #pragma GCC diagnostic push #if !defined(__has_warning) #pragma GCC diagnostic ignored "-Wmaybe-uninitialized" #elif __has_warning("-Wmaybe-uninitialized") #pragma GCC diagnostic ignored "-Wmaybe-uninitialized" #endif #include "source/render/MeshSimplifier.h" #pragma GCC diagnostic pop #include <map> #include <set> #include <thread> #include <folly/Format.h> using namespace fb360_dep; using namespace fb360_dep::render; void MeshSimplifier::loadVertexes( const Eigen::MatrixXd& vertexesIn, const int begin, const int end) { for (int i = begin; i < end; ++i) { Vertex vertex; vertex.coord = vertexesIn.row(i); vertexes[i] = vertex; } } void MeshSimplifier::loadFaces(const Eigen::MatrixXi& facesIn, const int begin, const int end) { for (int i = begin; i < end; ++i) { Face face; for (int j = 0; j < NUM_VERTEXES_FACE; ++j) { face.vertexesIdx[j] = facesIn(i, j); } faces[i] = face; } } MeshSimplifier::MeshSimplifier( const Eigen::MatrixXd& vertexesIn, const Eigen::MatrixXi& facesIn, const bool equiError, const int nThreads) { numThreads = nThreads; isEquiError = equiError; LOG(INFO) << folly::sformat("Getting {} vertexes...", vertexesIn.rows()); vertexes.resize(vertexesIn.rows()); std::vector<std::thread> threads; for (int i = 0; i < numThreads; ++i) { const int begin = i * vertexesIn.rows() / numThreads; const int end = (i + 1) * vertexesIn.rows() / numThreads; threads.emplace_back(&MeshSimplifier::loadVertexes, this, vertexesIn, begin, end); } for (std::thread& thread : threads) { thread.join(); } LOG(INFO) << folly::sformat("Getting {} faces...", facesIn.rows()); faces.resize(facesIn.rows()); threads.clear(); for (int i = 0; i < numThreads; ++i) { const int begin = i * facesIn.rows() / numThreads; const int end = (i + 1) * facesIn.rows() / numThreads; threads.emplace_back(&MeshSimplifier::loadFaces, this, facesIn, begin, end); } for (std::thread& thread : threads) { thread.join(); } } Eigen::MatrixXd MeshSimplifier::getVertexes() { Eigen::MatrixXd vertexesOut(vertexes.size(), NUM_VERTEXES_FACE); for (int i = 0; i < int(vertexes.size()); ++i) { vertexesOut.row(i) = vertexes[i].coord; } return vertexesOut; } Eigen::MatrixXi MeshSimplifier::getFaces() { Eigen::MatrixXi facesOut(faces.size(), NUM_VERTEXES_FACE); for (int i = 0; i < int(faces.size()); ++i) { Face& face = faces[i]; facesOut.row(i) = Eigen::Vector3i(face.vertexesIdx[0], face.vertexesIdx[1], face.vertexesIdx[2]); } return facesOut; } double computeFastError(Eigen::Matrix4d q, Eigen::Vector3d& v) { return q(0, 0) * v.x() * v.x() + 2 * q(0, 1) * v.x() * v.y() + 2 * q(0, 2) * v.x() * v.z() + 2 * q(0, 3) * v.x() + q(1, 1) * v.y() * v.y() + 2 * q(1, 2) * v.y() * v.z() + 2 * q(1, 3) * v.y() + q(2, 2) * v.z() * v.z() + 2 * q(2, 3) * v.z() + q(3, 3); } // error = vT * Q * v, where Q = (Q1 + Q2) // We find the target vertex by setting the derivative of error to 0 and solving // for v. This is equivalent to solving // v' = Q * [0 0 0 1] // | q11 q12 q13 q14 |^-1 | 0 | // = | q12 q22 q23 q24 | * | 0 | // | q13 q23 q33 q34 | | 0 | // | 0 0 0 1 | | 1 | // Note that we set the last row of Q to [0 0 0 1] because v is an homogeneous // vector // If the modified Q is not invertible, or if we are at the mesh boundary, we // use the optimal (lowest cost) vector from v1, v2, and (v1 + v2) / 2 // // Since Q is symmetric and the last row is homogeneous we can simplify the // error computation: // 1) The determinant of Q is just the determinant of the top-left 3x3 // 2) Multiplying Q^-1 by a homogeneous vector means we only want the first 3 // elements of the last column of Q^-1. This is, we only need to compute 3 // 3x3 minors, so that v = 1/det * [-M234; M134; -M124], where // MXYZ = det(colX|colY|colZ) // 3) The error becomes // vT * Q * v = q11x^2 + 2q12xy + 2q13xz + 2q14x + q22y^2 + 2q23yz + 2q24y + // q33z^2 + 2q34z + q44 double MeshSimplifier::computeError( const Vertex& vertex0, const Vertex& vertex1, Eigen::Vector3d& vTarget) { Eigen::Matrix4d q = vertex0.q + vertex1.q; const double det = q.block<3, 3>(0, 0).determinant(); // Do not do quadric approach on boundary edges const bool isBoundary = vertex0.isBoundary && vertex1.isBoundary; double error; if (det != 0 && !isBoundary) { Eigen::Matrix3d mX; mX << q(0, 1), q(0, 2), q(0, 3), q(1, 1), q(1, 2), q(1, 3), q(2, 1), q(2, 2), q(2, 3); Eigen::Matrix3d mY; mY << q(0, 0), q(0, 2), q(0, 3), q(1, 0), q(1, 2), q(1, 3), q(2, 0), q(2, 2), q(2, 3); Eigen::Matrix3d mZ; mZ << q(0, 0), q(0, 1), q(0, 3), q(1, 0), q(1, 1), q(1, 3), q(2, 0), q(2, 1), q(2, 3); vTarget = 1 / det * Eigen::Vector3d(-mX.determinant(), mY.determinant(), -mZ.determinant()); error = computeFastError(q, vTarget); } else { std::vector<Eigen::Vector3d> vCandidates = { vertex0.coord, vertex1.coord, (vertex0.coord + vertex1.coord) / 2}; std::vector<double> errors; for (auto& v : vCandidates) { errors.push_back(computeFastError(q, v)); } auto minIter = std::min_element(errors.begin(), errors.end()); vTarget = vCandidates[minIter - errors.begin()]; error = *minIter; } // If mesh is not distributed to have equierror, we need to penalize costs for // changes further away (e.g. a change of 1m far away is less noticeable than // a change of 1m up close). Dividing by the square distance of the target // vertex is a good penalization return isEquiError ? error : error / vTarget.squaredNorm(); } // Q = q * qT // q = [a, b, c, d] // n = (p1 - p0) x (p2 - p0) = [a, b, c] // d = -n.dot(p0) // n: normal to plane going though [p0, p1, p2] // | aa ab ac ad | // Q = | ab bb bc bd | // | ac bc cc cd | // | ad bd cd dd | // Note how Q is symmetric void MeshSimplifier::computeSubQuadrics(const int begin, const int end) { for (int i = begin; i < end; ++i) { Face& face = faces[i]; face.isDeleted = false; Eigen::Vector3d p[NUM_VERTEXES_FACE]; for (int j = 0; j < NUM_VERTEXES_FACE; ++j) { p[j] = vertexes[face.vertexesIdx[j]].coord; } Eigen::Vector3d normal = (p[1] - p[0]).cross(p[2] - p[0]).normalized(); face.normal = normal; Eigen::Vector4d q(normal.x(), normal.y(), normal.z(), -normal.dot(p[0])); face.q = q * q.transpose(); } } void MeshSimplifier::computeSubError(const int begin, const int end) { for (int i = begin; i < end; ++i) { Face& face = faces[i]; for (int j = 0; j < NUM_VERTEXES_FACE; ++j) { const int i0 = face.vertexesIdx[j]; const int i1 = face.vertexesIdx[(j + 1) % NUM_VERTEXES_FACE]; Eigen::Vector3d p; face.cost[j] = computeError(vertexes[i0], vertexes[i1], p); } } } void MeshSimplifier::computeInitialQuadrics() { std::vector<std::thread> threads; LOG(INFO) << "Computing quadrics..."; for (int i = 0; i < numThreads; ++i) { const int begin = i * faces.size() / numThreads; const int end = (i + 1) * faces.size() / numThreads; threads.emplace_back(&MeshSimplifier::computeSubQuadrics, this, begin, end); } for (std::thread& thread : threads) { thread.join(); } LOG(INFO) << "Accumulating quadrics..."; for (auto& face : faces) { for (int j = 0; j < NUM_VERTEXES_FACE; ++j) { vertexes[face.vertexesIdx[j]].q += face.q; } } LOG(INFO) << "Updating faces costs..."; threads.clear(); for (int i = 0; i < numThreads; ++i) { const int begin = i * faces.size() / numThreads; const int end = (i + 1) * faces.size() / numThreads; threads.emplace_back(&MeshSimplifier::computeSubError, this, begin, end); } for (std::thread& thread : threads) { thread.join(); } } // Remove from the list all faces that have been marked as deleted void MeshSimplifier::removeDeletedFaces() { int idx = 0; for (auto& face : faces) { face.isTouched = false; if (!face.isDeleted) { faces[idx++] = face; } } faces.resize(idx); } void MeshSimplifier::assignFaceVertexes() { for (auto& vertex : vertexes) { vertex.facesIdx.clear(); } for (int i = 0; i < int(faces.size()); ++i) { const Face& face = faces[i]; for (int j = 0; j < NUM_VERTEXES_FACE; ++j) { vertexes[face.vertexesIdx[j]].facesIdx.push_back(i); } } } std::vector<int> MeshSimplifier::commonFaces(const int vIdx0, const int vIdx1) { std::vector<int> commonFaces; for (int i1 : vertexes[vIdx0].facesIdx) { for (int i2 : vertexes[vIdx1].facesIdx) { if (i1 == i2) { commonFaces.push_back(i1); } } } return commonFaces; } // A vertex is is considered a to be on the boundary if it only shares one face // with any adjacent vertex void MeshSimplifier::identifySubBoundaries(const int begin, const int end) { for (int i = begin; i < end; ++i) { vertexes[i].isBoundary = false; } for (int i = begin; i < end; ++i) { // Ignore if it has already been marked as boundary if (vertexes[i].isBoundary) { continue; } // If it only has one face, it is a boundary if (vertexes[i].facesIdx.size() == 1) { vertexes[i].isBoundary = true; continue; } bool isBorder = false; std::set<int> vertexesVisited; for (auto& faceIdx : vertexes[i].facesIdx) { for (int j = 0; j < NUM_VERTEXES_FACE; ++j) { const int vIdx = faces[faceIdx].vertexesIdx[j]; if (vIdx != i) { // Check if we already visited this vertex on a previous face if (vertexesVisited.count(vIdx) == 0) { vertexesVisited.insert(vIdx); if (vertexes[vIdx].facesIdx.size() == 1 || commonFaces(i, vIdx).size() == 1) { vertexes[vIdx].isBoundary = true; isBorder = true; continue; } } } } } if (isBorder) { vertexes[i].isBoundary = true; } } } void MeshSimplifier::identifyBoundaries() { std::vector<std::thread> threads; for (int i = 0; i < numThreads; ++i) { const int begin = i * vertexes.size() / numThreads; const int end = (i + 1) * vertexes.size() / numThreads; threads.emplace_back(&MeshSimplifier::identifySubBoundaries, this, begin, end); } for (std::thread& thread : threads) { thread.join(); } } double MeshSimplifier::getThreshold(const float strictness) { std::vector<double> errors(faces.size() * NUM_VERTEXES_FACE); for (int i = 0; i < int(faces.size()); ++i) { Face& face = faces[i]; for (int j = 0; j < NUM_VERTEXES_FACE; ++j) { errors[i * NUM_VERTEXES_FACE + j] = face.cost[j]; } } const int idxPerc = strictness * (errors.size() - 1); std::nth_element(errors.begin(), errors.begin() + idxPerc, errors.end()); return errors[idxPerc]; } // Compare the normal of each neighboring face before and after the contraction. // If the normal flips, that contraction will be disallowed bool MeshSimplifier::haveNormalsFlipped(Eigen::Vector3d p, int vIdx0, int vIdx1) { for (int i = 0; i < int(vertexes[vIdx0].facesIdx.size()); ++i) { Face& face = faces[vertexes[vIdx0].facesIdx[i]]; // Ignore faces marked as deleted if (face.isDeleted) { continue; } // Find vertex index in the face, clockwise int order = 0; for (int j = 0; j < NUM_VERTEXES_FACE; ++j) { if (face.vertexesIdx[j] == vIdx0) { order = j; break; } } int i0 = face.vertexesIdx[(order + 1) % 3]; int i1 = face.vertexesIdx[(order + 2) % 3]; // Ignore edge formed by vertex0 and vertex1 (= deleted face) if (i0 == vIdx1 || i1 == vIdx1) { continue; } // Opposite directions begin at negative dot product Eigen::Vector3d v0 = (vertexes[i0].coord - p).normalized(); Eigen::Vector3d v1 = (vertexes[i1].coord - p).normalized(); Eigen::Vector3d normal = v0.cross(v1).normalized(); if (normal.dot(face.normal) < 0) { return true; } } return false; } void MeshSimplifier::updateCosts(const int vIdx0, const int vIdx1, const Eigen::Vector3d& pTarget) { // Both vertexes collapse into new one, defined by pTarget and Q0 + Q1 // Vertex 0 will act as new vertex vertexes[vIdx0].coord = pTarget; vertexes[vIdx0].q += vertexes[vIdx1].q; // Gather all faces touched by both vertexes // No need to check for duplicates, since they are faces marked for deletion std::vector<int> allFaces(vertexes[vIdx0].facesIdx); std::vector<int> facesVertex1 = vertexes[vIdx1].facesIdx; allFaces.insert(allFaces.end(), facesVertex1.begin(), facesVertex1.end()); for (int faceIdx : allFaces) { Face& face = faces[faceIdx]; if (face.isDeleted) { continue; } // Find vertex index in the face for (int i = 0; i < NUM_VERTEXES_FACE; ++i) { if (face.vertexesIdx[i] == vIdx0 || face.vertexesIdx[i] == vIdx1) { face.vertexesIdx[i] = vIdx0; face.isTouched = true; break; } } for (int i = 0; i < NUM_VERTEXES_FACE; ++i) { const int i0 = face.vertexesIdx[i]; const int i1 = face.vertexesIdx[(i + 1) % NUM_VERTEXES_FACE]; Eigen::Vector3d p; face.cost[i] = computeError(vertexes[i0], vertexes[i1], p); } } } // Reassign all indeces of all vertexes and faces to construct final mesh void MeshSimplifier::createFinalMesh() { for (auto& vertex : vertexes) { vertex.isDeleted = true; vertex.facesIdx.clear(); } // Remove faces marked for deletion, and mark all valid vertexes removeDeletedFaces(); for (auto& face : faces) { for (int i = 0; i < NUM_VERTEXES_FACE; ++i) { vertexes[face.vertexesIdx[i]].isDeleted = false; } } // Reassign vertexes coordinates std::map<int, int> mapVertexes; int currIdx = 0; for (int i = 0; i < int(vertexes.size()); ++i) { if (!vertexes[i].isDeleted) { mapVertexes.insert(std::make_pair(i, currIdx)); vertexes[currIdx++].coord = vertexes[i].coord; } } vertexes.resize(currIdx); // Reassign faces' vertexes for (auto& face : faces) { for (int i = 0; i < NUM_VERTEXES_FACE; ++i) { face.vertexesIdx[i] = mapVertexes.at(face.vertexesIdx[i]); } } } void MeshSimplifier::simplify( const int numFacesOut, const float strictness, const bool removeBoundaryEdges) { // Compute Q matrices and errors for all vertexes LOG(INFO) << "Computing initial costs..."; computeInitialQuadrics(); const int numFacesIn = faces.size(); int numFacesDeleted = 0; int numFacesDeletedPrev = 0; double threshold = 0; int countNumFacesSame = 0; int iteration = 0; while (int(faces.size()) > numFacesOut) { removeDeletedFaces(); if (iteration == 0) { LOG(INFO) << "Assigning faces and vertexes..."; } assignFaceVertexes(); if (iteration == 0) { LOG(INFO) << "Identifying boundaries..."; identifyBoundaries(); } if (iteration == 0 || numFacesDeletedPrev != numFacesDeleted) { threshold = getThreshold(strictness); countNumFacesSame = 0; } else { // Scale threshold up to avoid getting stuck threshold *= 2 * ++countNumFacesSame; if (std::isinf(threshold)) { // Sometimes there are no qualifying faces left, so the threshold would // increase without limit break; } } numFacesDeletedPrev = numFacesDeleted; if (iteration % 1 == 0) { LOG(INFO) << folly::sformat( "Iter: {}, faces: {}, threshold: {}", iteration, faces.size(), threshold); } for (auto& face : faces) { if (face.isDeleted || face.isTouched) { continue; } // Select all valid vertex pairs for (int i = 0; i < NUM_VERTEXES_FACE; ++i) { // Ignore if error (cost) is higher than threshold if (face.cost[i] > threshold) { continue; } int vIdx0 = face.vertexesIdx[i]; int vIdx1 = face.vertexesIdx[(i + 1) % NUM_VERTEXES_FACE]; // Ignore non-boundary edges with one boundary vertex if (vertexes[vIdx0].isBoundary != vertexes[vIdx1].isBoundary) { continue; } // Optionally ignore boundary edges entirely if (!removeBoundaryEdges && (vertexes[vIdx0].isBoundary || vertexes[vIdx1].isBoundary)) { continue; } // Compute optimal target point Eigen::Vector3d pTarget; computeError(vertexes[vIdx0], vertexes[vIdx1], pTarget); // Prevent mesh inversion if (haveNormalsFlipped(pTarget, vIdx0, vIdx1) || haveNormalsFlipped(pTarget, vIdx1, vIdx0)) { continue; } // Mark faces for deletion. These are the faces common to both vertexes const std::vector<int> commonFacesIdxs = commonFaces(vIdx0, vIdx1); for (int faceIdx : commonFacesIdxs) { faces[faceIdx].isDeleted = true; } numFacesDeleted += commonFacesIdxs.size(); // Update costs of all valid pairs involving new vertex updateCosts(vIdx0, vIdx1, pTarget); // Nothing else to do on the remaining vertexes of current face break; } // Check remaining faces after processing every face if (numFacesIn - numFacesDeleted <= numFacesOut) { break; } } ++iteration; } // Assign final values to vertexes and faces LOG(INFO) << "Creating final mesh..."; createFinalMesh(); }