Skip to content

Browse a mesh

The nicest way to traverse a mesh with ultimaille is to use iterators. These allow you to traverse mesh primitives at different levels: at the mesh level or at the primitives level.

While you can iterate over the primitives of a mesh at mesh level without any prerequisites, iterating over primitives at the level of other primitives requires something called connectivity.

The simplest way to traverse a mesh is to connect it. Connecting a mesh allows different primitives to know how they relate to each other.

However, a connected mesh is not easy to update or modify, and although Ultimaille takes care of updating connectivity when the mesh is modified, these operations can be slow and tedious. Furthermore, it's not always necessary to connect a mesh to work on it.

A mesh is not connected by default, so operations such as iterating over the vertices of a mesh face will not be accessible. Specifically, iterating on elements that require connectivity will raise an error if the mesh has not been previously connected.

In what follows, we present the two levels of iterating over mesh primitives.

Without connectivity

Example 1 - Iterate primtives on mesh

For example, if you need to iterate over primitives of a mesh, you can do this directly as following:

/**
 * This example shows how to iterate primitives over a mesh without connectivity
*/
#include "helpers.h"
#include <ultimaille/all.h>


using namespace UM;

int main(int argc, char** argv) {

    // --- LOAD ---

    // Get path of current executable
    std::string path = getAssetPath();

    // Declare a mesh with triangle surface
    Triangles m;
    // Loading catorus.geogram into m
    read_by_extension(path + "catorus.geogram", m);

    // --- ITERATE ---

    // Iterate over vertices
    std::cout << "iter over vertices: " << std::endl;
    for (auto v : m.iter_vertices()) {
        std::cout << "( " << v.pos() << ")" << std::endl;
    }

    // Iterate over facets
    std::cout << "iter over facets: " << std::endl;
    for (auto f : m.iter_facets()) {
        std::cout << f << std::endl;
    }

    // Iterate over hald-edges
    std::cout << "iter over half-edges: " << std::endl;
    for (auto he : m.iter_halfedges()) {
        std::cout << he << std::endl;
    }

    // --- END ---

    return 0;
}

However, if you desire to do much more complicated things on theses primitives, you need to traverse the mesh at lower level.

Example 2 - Get primitives by id

You can also get a primitive by its id:

// Get a primitive by index

// Get facet 5 on mesh m
Surface::Facet f(m, 5);
std::cout << "Pos of local vertex 0 of facet 5: " << f.vertex(0).pos() << std::endl;

// Get vertex 8 on mesh m
Surface::Vertex v(m, 8);
std::cout << "Pos of vertex 8: " << v.pos() << std::endl;

// Get halfedge 0 on mesh m
Surface::Halfedge h(m, 0);
std::cout << "Pos of start vertex of half-edge 0: " << h.from().pos() << std::endl;

With connectivity

Example 1 - Iterate on primitives

Just load a mesh as previously:

// Get path of current executable
std::string path = getAssetPath();

// Declare a mesh with triangle surface
Triangles m;
// Loading catorus.geogram into m
read_by_extension(path + "catorus.geogram", m);

Connect the mesh:

// Connect the mesh 
// (result to connect the components - primitives - of the mesh)
// If you comment this instruction you will get the following error: 
// Surface::Halfedge::facet(): Assertion `m.connected()' failed. Aborted (core dumped)
m.connect();

Now, you can traverse mesh using the relations between primitives. For example, we want to display for all half-edges of all faces of the mesh, the positions of their vertices:

    // Iterate over facets
    for (auto f : m.iter_facets()) {

        // Print info about facet
        std::cout 
            << std::endl
            << "facet: " << f 
            << " has " << f.size() << " sides. " 
            << std::endl;

        // Iterate over half-edge of the facet
        // This only be possible on connected mesh !
        for (auto he : f.iter_halfedges()) {
            // Print info about edge 
            std::cout 
                << "hald-edge " << he << ":" 
                << he.from().pos() << "," 
                << he.to().pos() 
                << std::endl;
        }
    }

     END ---

    return 0;
}

As you may have noticed, it wasn't necessary to traverse the primitives in this way, as we could have directly traversed the mesh's half-edges. However, without connectivity, we wouldn't have been able to retrieve easily the vertices of the half-edges using the from() and to() functions.

Example 2 - Move around

Connectivity also allows us to "move around" the mesh. For example, for a given face, we can find all its opposite faces (if it exists) by iterating over its half-edges and looking at their opposites. If you remember how half-edges work, you'll probably recall that it is possible to recover the opposite of a given half-edge. Below is an example of how to move from face to face on a surface mesh using half-edges and their opposites:

// Create a facet attribute
FacetAttribute<int> fa(m, 0);

// Get a facet
Surface::Facet f = m.iter_facets().begin().f;

// Set 1 to this facet attribute
fa[f] = 1;

// Repeat 5 times
for (int i = 0; i < 5; i++) {

    // Get opposite halfedge of the main halfedge of the face
    Surface::Halfedge opposite_he = f.halfedge().opposite();
    // Check if opposite halfedge is active
    // It can be unactive if halfedge is on the border for example
    // Or if there is many opposite, in this case, the mesh is said to be non-manifold
    if (!opposite_he.active())
        break;

    // Get facet of the opposite halfedge (opposite facet)
    Surface::Facet opposite_f = opposite_he.facet();

    // Mark attribute of opposite facet
    fa[opposite_f] = 1;
    f = opposite_f;

    // Write the result in a file 
    std::string filename = "catorus_opp_" + std::to_string(i) + ".geogram";
    write_by_extension(filename.c_str(), m, {{}, {{"fa", fa.ptr}}, {}});
}

You can open the produced files into graphite by using the following command graphite * (open all files in the directory), and you have to get a result like this:

Graphite screenshot

Example 3 - Random walk

Just for fun, we can adapt the last example to implement the "random walk" algorithm on the surface of a mesh:

// Create a facet attribute
FacetAttribute<int> fa(m, 0);

// Get a facet
Surface::Facet f = m.iter_facets().begin().f;

// Set 1 to this facet attribute
fa[f] = 1;

// Repeat 50 times
for (int i = 0; i < 50; i++) {

    std::optional<Surface::Facet> opposite_f_opt;

    do {

        // Get random halfedge in face
        int lh = rand() % 3;
        Surface::Halfedge random_he = f.halfedge(lh);
        // Get opposite halfedge of the selected halfedge of the face
        Surface::Halfedge opposite_he = random_he.opposite();

        // Check if opposite halfedge is active
        // It can be unactive if halfedge is on the border for example
        // Or if there is many opposite, in this case, the mesh is said to be non-manifold
        if (!opposite_he.active())
            break;

        // Get facet of the opposite halfedge (opposite facet)
        opposite_f_opt = std::optional<Surface::Facet>(opposite_he.facet());

    // Does not return to previously traversed faces
    } while (fa[opposite_f_opt.value()] == 1);

    // Mark attribute of opposite facet
    fa[opposite_f_opt.value()] = 1;
    f = opposite_f_opt.value();

    // Write the result in a file 
    std::string filename = "catorus_rw_" + std::to_string(i) + ".geogram";
    write_by_extension(filename.c_str(), m, {{}, {{"fa", fa.ptr}}, {}});
}

Open files in Graphite by using the following command graphite *, and you should see something like that:

Graphite random walk