A simple mesh class

Overview

Our goal in this article will be to define what a mesh is an ecapsulate it into a C++ class.

Mesh

From now on whenever I refer to mesh I mean a triangle mesh. That is, a polygonal surface generated by connected triangles like so

There are many different data structures one can use to represent a mesh. It’s not a simple matter to select the right one. It all depends on what kind of queries you want to make inexpensive, and what you are willing to let suffer in return. One of the most common ways of representing a triangle mesh is the vertex-face representation. In this representation a mesh is stored as a list of vertices V and a list of faces F. A face in this context refers to the outward facing “flat” parts of the mesh, in our case individual triangles.

To help explain it’s best to have an example in mind. We’ll use the simplist mesh possible, a single triangle

V = [[-1,0,0],
    [1,0,0],
    [0,1,0]]

F = [[1,2,0]]

This mesh corresponds to a triangle pictured below.

The 3D cartesian coordinates of it’s vertices are specified by the V matrix in (x,y,z) notation. However it alone just tells us where the points are in space. It’s the role of the F matrix to let us know that these 3 points are actually the vertices of a triangle. The values in the F matrix are indices into the V matrix. So F=[[1,2,0]] tells us that there is 1 triangle in the mesh and it’s 3 vertices are the 1st, 0th, and 2nd rows of V.

In practice there are many triangles in a mesh so instead of using nested primative arrays we’ll be using Eigen matricies.

A note on ordering

You’ll notice that I drew ‘<’ characters on the triangle edges in the picture above. That is because when we send a triangle to the GPU for rendering OpenGL expects the edges to be ordered counter-clockwise. This requirement is embedded into the ordering we choose for the indices in the F matrix. Although the vertices are listed as leftmost, rightmost, topmost in V, the F matrix indexes them as rightmost, leftmost, topmost. That was just to drive home the point this very point.

Mesh class

So with all that in mind lets create our Mesh class

#include <vector>
#include <GL/glew.h>
#include <Eigen/Core>
#ifndef MESH
#define MESH
/**
    \brief Defines a mesh
    
     A mesh is represented by a list of vertices and edges. A vertex is a set of 3 floats representing a (x,y,z) pair in R^3. 
     An Edge is represented by a pair of indices into the vertex list. Vertices in edges are listed in clockwise order as they appear on the mesh.
     Once a mesh is built it is loaded onto the GPU so it can be rendered
**/

typedef Eigen::Matrix<GLfloat, Eigen::Dynamic, 3, Eigen::RowMajor> List3df;
typedef Eigen::Matrix<GLint, Eigen::Dynamic, 3, Eigen::RowMajor> List3di;

class Mesh
{
    private:        
        List3df m_vertices; // list of vertices in mesh
        List3di m_faces; // list of pairs of indices into vertex list in clockwise order.
        GLuint m_VAO;
        GLuint m_VBO;
        GLuint m_EBO;

    void GenerateVAO();

    public:
        /**
            Builds a mesh with no vertices or edges
        **/
        Mesh();

        /**
            Builds a mesh with the provided vertices and edges
            @param vertices nx3 matrix of vertices for the mesh. 
            @param faces mx3 list of face indices for the mesh. Listed in clockwise order
        **/
        Mesh(List3df vertices, List3di faces);

        /**
            @return A copy of the vertices in the mesh
        **/
        List3df GetVertices();

        /**
            @return A copy of the faces in the mesh
        **/
        List3di GetFaces();

        /**
            @return The number of edges in the mesh
        **/
        GLuint GetNumEdges();

        /**
            @return The vertex array buffer index for this mesh
        **/
        GLuint GetVAO();

        /**
            Removes the mesh from the GPU
        **/
        void CleanUp();
    
};
#endif

I hope most of that is self-evident at this point. The only interesting bits are the use of a typedef to avoid having to write those log Eigen matrix strings everywhere, and the GenerateVAO function. A Vertex Array Object is how OpenGL keeps track of meshes on the GPU. It involves moving the raw data represented by the V and F matrices to the GPU and telling OpenGL how to read them.  The CleanUp method is used to remove the mesh from the GPU when it no longer needs to be rendered.

#include <mesh.h>
#include <iostream>

Mesh::Mesh(){};

Mesh::Mesh(List3df vertices, List3di faces)
{
    m_vertices = vertices;
    m_faces = faces;
    GenerateVAO();
}

List3df Mesh::GetVertices()
{
    return m_vertices;
}

List3di Mesh::GetFaces()
{
    return m_faces;
}

GLuint Mesh::GetNumEdges()
{
    return 3*m_faces.rows();
};

GLuint Mesh::GetVAO()
{
    return m_VAO;
}

void Mesh::GenerateVAO()
{
    glGenVertexArrays(1,&m_VAO);
 
    glGenBuffers(1,&m_VBO);

    glGenBuffers(1,&m_EBO);

    glBindVertexArray(m_VAO);

    GLfloat vertices[3*m_vertices.rows()];
    
    // Copy underlying data from m_vertices into primative array
    Eigen::Map<List3df>( vertices, m_vertices.rows(), m_vertices.cols() ) = m_vertices;
    
    glBindBuffer(GL_ARRAY_BUFFER,m_VBO);
    glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);
    
    GLuint n_F = m_faces.rows();
    GLint indices[3*n_F];
    Eigen::Map<List3di>( indices, m_faces.rows(), m_faces.cols() ) = m_faces;

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, 0);

    glBindVertexArray(0); // unbind vertex array
};

void Mesh::CleanUp()
{
    glDeleteVertexArrays(1,&m_VAO);
    glDeleteBuffers(1,&m_VBO);
    glDeleteBuffers(1,&m_EBO);
}

I won’t go into detail here on how this code loads and removes data from the GPU since I’m assuming you know the basics of OpenGL already. Since our face list is already in counter-clockwise order we can just copy it into a primative array as is.