Learning record of source code analysis - Seamless mesh stitching

Keywords: C++ 3d

2021SC@SDUSC


An important feature of Dust3D is "seamless modeling". When the model spacing is less than a certain value, the automatic mesh stitching between models is realized. This process involves the relevant knowledge of half edge structure.


half-edge structure

Half edge data structure is a data structure based on grid edges. In this data structure, we believe that each edge of the grid is divided into two opposite sides.

convertHalfEdgesToEdgeLoops()

convertHalfEdgesToEdgeLoops() in the MeshRecombiner class converts a half edge structure into a list of circular edges. First, store each half edge in the vertexLinkMap, and then traverse backward from the first vertex. When the number of elements does not exceed the sequence capacity limit, add vertices to edgeLoop until a closed loop is formed.

bool MeshRecombiner::convertHalfEdgesToEdgeLoops(const std::vector<std::pair<size_t, size_t>> &halfEdges,
    std::vector<std::vector<size_t>> *edgeLoops)
{
    std::map<size_t, size_t> vertexLinkMap;
    for (const auto &halfEdge: halfEdges) {
        auto inserResult = vertexLinkMap.insert(halfEdge);
        if (!inserResult.second) {
            return false;
        }
    }
    while (!vertexLinkMap.empty()) {
        std::vector<size_t> edgeLoop;
        //Find the first value of the first node in the map
        size_t vertex = vertexLinkMap.begin()->first;
        size_t head = vertex;
        bool loopBack = false;
        size_t limitLoop = MAX_EDGE_LOOP_LENGTH;
        //When the number of elements does not exceed the sequence capacity limit, a new edge is added to edgeLoop
        while ((limitLoop--) > 0) {
            edgeLoop.push_back(vertex);  //Adds the current vertex to the edge loop
            auto findNext = vertexLinkMap.find(vertex);
            //If the current node is the last node of the map, a new loop is started
            if (findNext == vertexLinkMap.end())
                break;
            //If the second value of the current node is the head node of the map, the closed-loop edge sequence is successfully generated
            vertex = findNext->second;
            if (vertex == head) {
                loopBack = true;
                break;
            }
        }
        //Failed to generate closed loop
        if (!loopBack) {
            return false;
        }
        //The number of edges in edgeLoop is less than 3 and cannot form a closed loop
        if (edgeLoop.size() < 3) {
            return false;
        }
        for (const auto &vertex: edgeLoop) {
            vertexLinkMap.erase(vertex);
        }
        edgeLoops->push_back(edgeLoop);
    }
    return true;
}

recombine()

First, create a face map for the half edge, and then find the position where there is a gap:
Traverse each face, and read the vertex index sequence of the face with index. If the first node value of the current node is empty, check the next adjacent face. If the first node value is also empty, there is a gap in the current position.

    std::map<size_t, std::vector<size_t>> seamLink;
    for (const auto &face: *m_faces) {
        for (size_t i = 0; i < face.size(); ++i) {
            const auto &index = face[i];
            auto source = (*m_verticesSourceIndices)[index];  //Face vertex index sequence
            if (MeshCombiner::Source::None == source.first) {
                //If the value of the first node is None, the next adjacent face is checked
                auto next = face[(i + 1) % face.size()];
                auto nextSource = (*m_verticesSourceIndices)[next];
                if (MeshCombiner::Source::None == nextSource.first) {
                    //There is a gap between index and next
                    seamLink[index].push_back(next);
                }
            }
        }
    }

The vertices at the gap are separated one by one as independent individuals_ For each face of faces: read its vertex index sequence. If the first node of the sequence vertex is empty, find the corresponding vertex in islandmap. If it is not the tail node of map, it indicates that the islandmap contains the vertex at the gap, in M_ The area where faces is located increases the connection between the face and the island, and regenerates the edges.

    std::map<size_t, size_t> seamVertexToIslandMap;
    //Separate the vertices at the gap as independent individuals
    size_t islands = splitSeamVerticesToIslands(seamLink, &seamVertexToIslandMap);  
    std::map<std::pair<size_t, size_t>, std::pair<size_t, bool>> edgesInSeamArea;
    for (size_t faceIndex = 0; faceIndex < (*m_faces).size(); ++faceIndex) {
        const auto &face = (*m_faces)[faceIndex];
        bool containsSeamVertex = false;
        bool inFirstGroup = false;
        size_t island = 0;
        for (size_t i = 0; i < face.size(); ++i) {
            const auto &index = face[i];
            auto source = (*m_verticesSourceIndices)[index];
            if (MeshCombiner::Source::None == source.first) {
                //If the first value of sequence vertex pair is null, find the corresponding vertex in seamVertexToIslandMap
                const auto &findIsland = seamVertexToIslandMap.find(index);
                //If the vertex is not the tail node of seamVertexToIslandMap, the island map contains the vertex at the gap
                if (findIsland != seamVertexToIslandMap.end()) {
                    containsSeamVertex = true;
                    island = findIsland->second;
                }
            } 
            else if (MeshCombiner::Source::First == source.first) {
                inFirstGroup = true;
            }
        }
        if (containsSeamVertex) {
            //In M_ The area where faces is located adds the face combined with the island
            m_facesInSeamArea.insert({faceIndex, island});
            //Regenerate edges for the combination
            for (size_t i = 0; i < face.size(); ++i) {
                const auto &index = face[i];
                const auto &next = face[(i + 1) % face.size()];
                std::pair<size_t, size_t> edge = {index, next};
                edgesInSeamArea.insert({edge, {island, inFirstGroup}});
            }
        }
    }

For islandsMap, you need to adjust its triangular mesh

    for (auto &it: islandsMap) {
        for (size_t side = 0; side < 2; ++side) {
            for (size_t i = 0; i < it.second.edgeLoops[side].size(); ++i) {
                auto &edgeLoop = it.second.edgeLoops[side][i];
                size_t totalAdjustedTriangles = 0;
                size_t adjustedTriangles = 0;
                while ((adjustedTriangles=adjustTrianglesFromSeam(edgeLoop, it.first)) > 0) {
                    totalAdjustedTriangles += adjustedTriangles;
                }
            }
        }
    }

adjustTrianglesFromSeam()

After mesh stitching, relevant duplicate information needs to be deleted and adjusted.

size_t MeshRecombiner::adjustTrianglesFromSeam(std::vector<size_t> &edgeLoop, size_t seamIndex)
{
    if (edgeLoop.size() <= 3)
        return 0;

    std::vector<size_t> halfEdgeToFaces;
    for (size_t i = 0; i < edgeLoop.size(); ++i) {
        size_t j = (i + 1) % edgeLoop.size();
        //Find the face it represents for half (j, i)
        auto findFace = m_halfEdgeToFaceMap.find({edgeLoop[j], edgeLoop[i]});
        if (findFace == m_halfEdgeToFaceMap.end()) {
            qDebug() << "Find face for half edge failed:" << edgeLoop[j] << edgeLoop[i];
            return 0;
        }
        //Stores the second vertex of the current half in the sequence
        halfEdgeToFaces.push_back(findFace->second);
    }
    
    std::vector<size_t> removedFaceIndices;
    std::set<size_t> ignored;
    for (size_t i = 0; i < edgeLoop.size(); ++i) {
        size_t j = (i + 1) % edgeLoop.size();
        //When an adjacent half represents the same face, one edge is deleted in the sequence index and the other half is recorded
        if (halfEdgeToFaces[i] == halfEdgeToFaces[j]) {
            removedFaceIndices.push_back(halfEdgeToFaces[i]);
            ignored.insert(edgeLoop[j]);
            ++i;
            continue;
        }
    }
    //When there is a deletion operation, a new cyclic edge sequence is generated
    if (!ignored.empty()) {
        std::vector<size_t> newEdgeLoop;
        for (const auto &v: edgeLoop) {
            if (ignored.find(v) != ignored.end())
                continue;
            newEdgeLoop.push_back(v);
        }
        if (newEdgeLoop.size() < 3)
            return 0;
        edgeLoop = newEdgeLoop;
        for (const auto &faceIndex: removedFaceIndices)
            m_facesInSeamArea.insert({faceIndex, seamIndex});
    }
    
    return ignored.size();
}

If there is any mistake, please correct it.

Posted by surion on Thu, 14 Oct 2021 10:46:29 -0700