Polygon Mesh Processing Library
Loading...
Searching...
No Matches
Tutorial

Welcome to the PMP library tutorial!

This tutorial will walk you through your first steps using PMP. After completion, you should be able to build your own mesh processing applications and use PMP for basic mesh processing tasks.

Note that this tutorial is not a comprehensive introduction to mesh processing. If that's what you are looking for, we highly recommend the textbook of Botsch et al. [3].

By the way: This is a living document that's built from the latest version of our sources. If you spot any errors, inaccuracies, or typos, please don't hesitate to report them using our issue tracker. Your feedback and suggestions are welcome!

Getting Started

Let's begin by getting the PMP library source code and building the library and its example applications. In the following, I'm assuming you are using a Unix-like operating system such as Linux or macOS. If you are on Windows the steps are the same but the exact commands might be slightly different.

First, clone the repository using git:

git clone https://github.com/pmp-library/pmp-library.git

The next step is to configure the build system using CMake. Create a separate build directory for all the files you'll generate:

cd pmp-library && mkdir build && cd build

Now run CMake to generate the build files:

cmake ..

This should work out-of-the-box as long as you have recent compiler and standard build tools installed. In case of doubt, see the detailed Installation instructions.

By default, CMake will generate Makefiles on Linux and macOS. You can now build the library using

make -j

After watching a few pages of compiler output passing by, you should see something like this:

[ 98%] Built target subdivision
[100%] Linking CXX executable ../mpview
[100%] Built target mpview

Congratulations! You successfully built PMP!

We include a number of example applications that you can to try out. The mpview application provides a simple graphical interface for many of our algorithms. You can give it a try and see what you can do with the famous Stanford bunny:

./mpview ../data/off/bunny.off

You should see a window like this:

MeshProcessingViewer showing the Stanford bunny.

If you're having trouble with one of the steps, please read the detailed Installation instructions carefully. You can also head over to our discussions forum and ask for help.

Basic Concepts

Polygon meshes are a fundamental data structure for representing 3D surfaces in computer graphics and geometry processing. A mesh is composed of vertices, edges, and faces:

  • Vertices are the individual points defining the shape of the object. Each vertex typically has 3D coordinates \((x,y,z)\) associated with it. In many applications additional properties such as colors or texture coordinates are stored for each vertex.
  • Edges are the line segments connecting two vertices. They define the boundary of each face. In a manifold surface mesh, all edges are connected to exactly two faces. In PMP, all meshes are manifold.
  • Faces are the polygonal shapes that make up the visible surface of the object. Each face has at least three vertices. Triangles and quads (4 vertices) are the most frequently used faces. PMP supports meshes with general polygonal faces.

We also call those mesh elements.

Mesh elements.

In order to access and manipulate the mesh, you also need incidence relationships between those elements. The central class for storing and manipulating all that data is pmp::SurfaceMesh. It stores connectivity information based on halfedges, i.e., pairs of directed edges with opposing direction. To be more precise:

  • Each vertex stores an outgoing halfedge.
  • Each face stores an incident halfedge.
  • Each halfedge stores its incident face, its target vertex, and its previous and next halfedges within the face.

The halfedge connectivity is illustrated in the figure below:

Halfedge connectivity.

In the following, we show the basic usage of pmp::SurfaceMesh by example.

Adding Elements

The basic usage of pmp::SurfaceMesh is demonstrated in the example below. The program first instantiates a pmp::SurfaceMesh object as well as four vertex handles. These handles, as well as the handles for the other mesh elements Halfedge, Edge and Face basically indices. Four vertices are added to the mesh, as well as four triangular faces composing a tetrahedron. Finally, the number of vertices, edges, and faces is printed to standard output.

// instantiate a SurfaceMesh object
SurfaceMesh mesh;
// add 4 vertices
const auto v0 = mesh.add_vertex(Point(0, 0, 0));
const auto v1 = mesh.add_vertex(Point(1, 0, 0));
const auto v2 = mesh.add_vertex(Point(0, 1, 0));
const auto v3 = mesh.add_vertex(Point(0, 0, 1));
// add 4 triangular faces
mesh.add_triangle(v0, v1, v3);
mesh.add_triangle(v1, v2, v3);
mesh.add_triangle(v2, v0, v3);
mesh.add_triangle(v0, v2, v1);
std::cout << "vertices: " << mesh.n_vertices() << std::endl;
std::cout << "edges: " << mesh.n_edges() << std::endl;
std::cout << "faces: " << mesh.n_faces() << std::endl;

Iterators and Circulators

In order to sequentially access mesh elements pmp::SurfaceMesh provides iterators for each element type:

  1. pmp::SurfaceMesh::VertexIterator
  2. pmp::SurfaceMesh::HalfedgeIterator
  3. pmp::SurfaceMesh::EdgeIterator
  4. pmp::SurfaceMesh::FaceIterator

Similar to iterators, pmp::SurfaceMesh also provides circulators for the ordered enumeration of all incident vertices, halfedges, or faces around a given face or vertex. The example below demonstrates the use of iterators and circulators for computing the mean valence of a mesh.

float mean_valence = 0.0f;
// loop over all vertices
for (auto v : mesh.vertices())
{
// sum up vertex valences
mean_valence += mesh.valence(v);
}
mean_valence /= mesh.n_vertices();
std::cout << "mean valence: " << mean_valence << std::endl;

Dynamic Properties

Attaching additional attributes to mesh elements is important for many applications. pmp::SurfaceMesh supports properties by means of synchronized arrays that can be (de-)allocated dynamically at run-time. Property arrays are also used internally, e.g., to store vertex coordinates. The example program below shows how to access vertex coordinates through the pre-defined point property.

// get pre-defined property storing vertex positions
auto points = mesh.get_vertex_property<Point>("v:point");
Point p(0, 0, 0);
for (auto v : mesh.vertices())
{
// access point property like an array
p += points[v];
}
p /= mesh.n_vertices();
std::cout << "barycenter: " << p << std::endl;

The dynamic (de-)allocation of properties at run-time is managed by a set of four different functions.

  • Add a new property of a specific type for a given element type. Example:

    auto vertex_weights = mesh.add_vertex_property<Scalar>("v:weight");
  • Get a handle to an existing property. Example:

    auto points = mesh.get_vertex_property<Point>("v:point");
  • Get or add: Return a handle to an existing property if a property of the same type and name exists. If there is no such property, a new one is allocated and its handle is returned. Example:

    auto edge_weights = mesh.edge_property<Scalar>("e:weight");
  • Remove a property given its handle:

    auto face_colors = mesh.face_property<Color>("f:color");
    mesh.remove_face_property(face_colors);

Functions that allocate a new property take an optional default value for the property as a second argument. Example:

mesh.face_property<Color>("f:color", Color(1.0, 0.0, 0.0));

The code excerpt below demonstrates how to allocate, use and remove a custom edge property.

// get pre-defined point property
auto points = mesh.get_vertex_property<Point>("v:point");
// add new property storing a point per edge
auto edge_points = mesh.add_edge_property<Point>("e:point");
// loop over all edges
for (auto e : mesh.edges())
{
// compute midpoint
const auto p0 = points[mesh.vertex(e, 0)];
const auto p1 = points[mesh.vertex(e, 1)];
const auto midpoint = (p0 + p1 ) * Scalar(0.5);
// access edge property like an array
edge_points[e] = midpoint;
}
// remove property to free memory
mesh.remove_edge_property(edge_points);

Connectivity Queries

Commonly used connectivity queries such as retrieving the next halfedge or the target vertex of an halfedge are illustrated below.

Halfedge h;
auto h0 = mesh.next_halfedge(h);
auto h1 = mesh.prev_halfedge(h);
auto h2 = mesh.opposite_halfedge(h);
auto f = mesh.face(h);
auto v0 = mesh.from_vertex(h);
auto v1 = mesh.to_vertex(h);
Connectivity queries

Topological Operations

pmp::SurfaceMesh also offers higher-level topological operations, such as performing edge flips, edge splits, face splits, or halfedge collapses. The figure below illustrates some of these operations.

High-level operations changing the topology.

The corresponding member functions and their syntax is demonstrated in the pseudo-code below.

Vertex v;
Edge e;
Halfedge h;
Face f;
mesh.split(f, v);
mesh.split(e, v);
mesh.flip(e);
mesh.collapse(h);

When elements are removed from the mesh due to topological changes, the member function pmp::SurfaceMesh::garbage_collection() has to be called in order to ensure the consistency of the data structure.

File I/O

All I/O operations are handled by the pmp::read() and pmp::write() functions. They take a mesh, a file path, and optional pmp::IOFlags as an argument.

We currently support reading and writing several standard file formats: OFF, OBJ, STL. See the reference documentation for the pmp::read() and pmp::write() functions for details on which format supports reading / writing which type of data.

A simple example reading and writing a mesh is shown below.

// instantiate a SurfaceMesh object
SurfaceMesh mesh;
// read a mesh specified as the first command line argument
if (argc > 1)
read(mesh, argv[1]);
// ...
// do something with the mesh
// ...
// write the mesh to the file specified as second argument
if (argc > 2)
write(mesh, argv[2]);