Skip to content

Load & navigate a mesh

This tutorial walks through some basics with geometry-central, showing how to load a mesh from file and iterate through its elements.

View full, runnable source code in the tutorial repository.

Basic setup

To begin, we include the relevant headers, including some for visualization using Polyscope.

#include "geometrycentral/surface/manifold_surface_mesh.h"
#include "geometrycentral/surface/meshio.h"
#include "geometrycentral/surface/surface_mesh.h"

#include "polyscope/polyscope.h"
#include "polyscope/surface_mesh.h"

All functionality of geometry-central is contained within the geometrycentral namespace; surface meshes live in geometrycentral::surface. We will bring both in to scope so we can just type SurfaceMesh instead of geometrycentral::surface::SurfaceMesh, etc.

using namespace geometrycentral;
using namespace geometrycentral::surface;

Now, we use the mesh loaders to construct a surface mesh’s connectivity and geometry from file. Many common file formats like .obj, .ply, and .stl are supported. By default, this mesh class represents very general polygonal meshes, including nonmanifold meshes.

  std::unique_ptr<SurfaceMesh> mesh;
  std::unique_ptr<VertexPositionGeometry> geometry;
  std::tie(mesh, geometry) = readSurfaceMesh("spot.obj");

Alternately, you could construct a mesh which is required to be manifold.

  std::tie(mesh, geometry) = readManifoldSurfaceMesh("spot.obj");

If you’r not already familiar with std::unique_ptr<>, the following gives a bit more context (click to expand).

Why use std::unique_ptr<>?

The mesh loader, like many functions in geometry-central, returns constructed objects via a unique_ptr. Unique pointers are an important tool for memory management in modern C++; if you haven’t used them before, we suggest you give them a try!

In most ways, a unique_ptr acts just like a normal C++ pointer. You can dereference it with *uPtr, and access its members and function like uPtr->function(). However, the unique_ptr helps prevent common memory-management mistakes, and communicates the programmer’s intent about object lifetime. This is accomplished with two properties:

  • You don’t need to call delete on a unique_ptr, it happens automatically when the pointer is destructed, e.g. when it goes out of scope at the end of a function, or when the object it is a member of gets deleted. This helps prevent memory leaks where you forget to deallocate the object.

  • You cannot copy the unique_ptr; hence it is “unique”! You can still pass around references, or std::move() the pointer, which are sufficient for most reasonable uses. This helps prevent you from creating a copy, and then accidentally deleting the pointer twice.


The general paradigm in geometry-central (and a recommended style in modern C++) is to return long-lived, allocated objects with a unique_ptr, and pass these objects in to functions and dependent classes by reference.

For instance, we might write a function which takes a mesh as an argument like

void processMesh(SurfaceMesh& inputMesh) { /* do stuff */}

and call it by dereferencing the unique pointer to pass a reference

std::unique_ptr<SurfaceMesh> mesh;
std::unique_ptr<VertexPositionGeometry> geometry;
std::tie(mesh, geometry) = readSurfaceMesh("spot.obj"); 

processMesh(*mesh);

For more details about unique pointers, see the language documentation, or many tutorials around the web.


If you really don’t want to use unique pointers, you can simply release the unique pointer to an ordinary pointer:

std::unique_ptr<SurfaceMesh> mesh /* populated as above */;
SurfaceMesh* meshPtr = mesh.release();

The meshPtr now points the mesh object, and you are responsible for eventually deleting this pointer. After calling release(), the unique pointer points to nothing and will no longer deallocate the object.

Traverse the mesh

As a simple demonstration of the mesh data structure, lets iterate through the vertices of the mesh, and for each vertex print the adjacent faces.

  for (Vertex v : mesh->vertices()) {
    std::cout << "Vertex " << v << " has degree " << v.degree() << "\n";
    for (Face fn : v.adjacentFaces()) {
      std::cout << "  incident on face " << fn << "\n";
    }
  }

This prints something like:

...
Vertex v_2907 has degree 6
  incident on face f_5815
  incident on face f_2885
  incident on face f_2887
  incident on face f_5812
  incident on face f_5813
  incident on face f_5814
Vertex v_2908 has degree 6
  incident on face f_5822
  incident on face f_2888
...

Visualize the mesh with Polyscope

Finally, we can easily visualize the mesh we loaded via Polyscope.

  polyscope::init(); // initialize the gui

  // add the mesh to the gui
  polyscope::registerSurfaceMesh("my mesh", 
      geometry->vertexPositions, mesh->getFaceVertexList());

  polyscope::show(); // pass control to the gui until the user exits