dt
TheMeshProject

Mesh Data Structures

Implementation Tutorial #2 — Designing a clean, extensible in memory representation for triangle meshes in the context of btm framework.

1. Why Mesh Data Structures Matter

In the previous tutorial, we established the rendering window and the platform layer. Now we move to the core of any geometry processing system: how a mesh is represented in memory. This choice determines:

  • how efficiently we can traverse neighborhoods
  • how robustly we can compute curvature
  • how easily we can detect feature lines
  • how cleanly we can segment or refine the mesh
  • how straightforward it is to upload data to the GPU
  • how extensible the system becomes as algorithms grow

In TheMeshProject, the mesh structure must be:

  • explicit about connectivity
  • friendly to numerical algorithms
  • compatible with btm framework’s rendering layer
  • simple enough to understand and extend
  • stable across tutorials and future modules
This tutorial introduces a minimal but extensible mesh representation that will serve as the foundation for curvature estimation, segmentation, and refinement.

2. Basic Entities: Vertices, Faces, and Connectivity

At the lowest level, a triangle mesh consists of:

  • a list of vertex positions
  • a list of triangles, each referencing three vertex indices
  • optional adjacency information

A minimal starting point looks like this:

template <typename T>
struct Vertex {
    basevec<T, 3> position;
};

struct Triangle {
    std::uint32_t v0, v1, v2;
};

struct VertexAdjacency {
    std::vector<std::uint32_t> incident_faces;
    std::vector<std::uint32_t> neighbor_vertices;
};

template <typename T>
struct MeshAttributes {
    std::vector<basevec<T, 3>> vertex_normals;
    std::vector<basevec<T, 3>> face_normals;

    // Curvature fields
    std::vector<T> k1;  // principal curvature 1
    std::vector<T> k2;  // principal curvature 2

    // Segmentation labels
    std::vector<int> segment_id;
};

template <typename T>
class MeshExplicit {
public:
    std::vector<Vertex<T>>       vertices;
    std::vector<Triangle>        triangles;
    std::vector<VertexAdjacency> adjacency;
    MeshAttributes<T>            attributes;
    // methods for loading, saving, and manipulating the mesh would go here
}; 

This representation is intentionally simple.

It is enough to:

  • load a mesh from file
  • iterate over faces and vertices
  • upload vertex/index buffers to the GPU
  • implement basic visualization

We will extend it gradually as algorithms require more structure.

3. Integrating with btm-framework

In btm-framework, the mesh structure typically lives in a geometry module, separate from the rendering code.

This separation is intentional:

  • geometry algorithms (curvature, segmentation, refinement) should not depend on OpenGL
  • rendering code should not need to know about adjacency or curvature
  • both modules should communicate through a clean, minimal interface

A common pattern is:

  • btm_framework::Mesh — owns vertices, triangles, adjacency, attributes
  • btm_framework::MeshRenderer — knows how to upload a Mesh to GPU buffers and draw it

These two should be decoupled enough that we can change the mesh structure (e.g., add half-edge connectivity) without affecting the rendering code, while still allowing efficient data transfer when needed.

Conceptually:

// in the geometry module
template <typename T>
class MeshExplicit {
public:
    std::vector<Vertex<T>>       vertices;
    std::vector<Triangle>        triangles;
    std::vector<VertexAdjacency> adjacency;
    MeshAttributes<T>            attributes;
};

// in the rendering module
template <typename T>
class MeshRenderer
{
public:
    MeshRenderer(const MeshExplicit<T>& mesh [, other optional parameters]) { 
        // here we would upload the mesh data to GPU buffers (vertex buffer, index buffer, etc.)
    }
    void render([drawing parameters]) {
        // actual OpenGL calls to draw the mesh using vertex/index buffers
    }
}; 

This separation keeps the geometry logic independent from the rendering backend, while still allowing tight integration where needed.

4. Adding Adjacency (Preparing for Curvature and Refinement)

Most algorithms in TheMeshProject rely on local neighborhoods:

  • curvature estimation
  • feature line detection
  • segmentation
  • metric based refinement
  • smoothing and filtering

To support these operations, we need adjacency information.

A simple and effective approach is to store, for each vertex:

  • the list of incident faces
  • the list of neighboring vertices

Example Adjacency structure

struct VertexAdjacency
{
    std::vector<std::uint32_t> incident_faces;   // indices into triangles[]
    std::vector<std::uint32_t> neighbor_vertices; // indices into vertices[]
};

class MeshExplicit
{
    std::vector<Vertex>         vertices;
    std::vector<Triangle>       triangles;
    std::vector<VertexAdjacency> adjacency;
};

The adjacency can be built once after loading the mesh:

void MeshExplicit::build_adjacency() {
    adjacency.clear();
    adjacency.resize(vertices.size());

    for (std::uint32_t f = 0; f < triangles.size(); ++f) {
        const auto& tri = triangles[f];

        std::uint32_t vs[3] = { tri.v0, tri.v1, tri.v2 };

        for (int i = 0; i < 3; ++i) {
            auto v = vs[i];
            adjacency[v].incident_faces.push_back(f);

            auto vn = vs[(i + 1) % 3];
            adjacency[v].neighbor_vertices.push_back(vn);
        }
    }
}

This is not the only possible representation, but it is:

  • easy to understand
  • good enough for curvature estimation and many local operators
  • compatible with later refinements (half edge, edge maps, etc.)

5. Attributes and Extensibility

As we progress through the implementation series, we will need to attach additional data to the mesh:

  • per vertex normals
  • principal curvatures \(k_1, k_2\)
  • mean and Gaussian curvature
  • feature intensities
  • segmentation labels
  • refinement metrics

A clean approach is to store these in parallel arrays, grouped under a MeshAttributes structure.

template <typename T> struct MeshAttributes {
    std::vector vertex_normals;
    std::vector face_normals;

    // Curvature fields
    std::vector<T> k1; // principal curvature 1
    std::vector<T> k2; // principal curvature 2

    // Segmentation labels
    std::vector<int> segment_id;
};

template <typename T> 
class MeshExplicit {
public:
    std::vector<Vertex<T>> vertices;
    std::vector<Triangle> triangles;
    std::vector<VertexAdjacency> adjacency;
    MeshAttributes<T> attributes;
}; 

This keeps the core connectivity structure stable while allowing attributes to evolve as algorithms become more sophisticated.

6. Connecting to the Rendering Window

From the perspective of the rendering window created in Tutorial #1, the mesh structure provides:

  • a vertex buffer (positions, normals, etc.)
  • an index buffer (triangles)

A typical flow in btm framework might look like:

// in our main function, after setting up the platform and window
// here we create/load our mesh
MeshExplicit mesh;
create_mesh(mesh);
// The MeshRenderer will generate the vertex buffer and other necessary data for rendering 
// based on the mesh topology and attributes.
std::unique_ptr<MeshRenderer<float>> mesh_renderer(new MeshRenderer<float>(mesh));
while (pollEvents()) {
    // in this example, resources contains the camera and the shader programs, 
    // which the renderer can use to set up the rendering state.
    render(resources, mesh_renderer.get());
}

The renderer would be responsible for uploading the vertex and index buffers to the GPU, while the main loop handles events and issues draw calls.

static bool create_mesh(MeshExplicit& r_mesh) {
    // vertices of a cube
    static std::vector> cube_vertices = {
        {0.5f, 0.5f, -0.5f},   {0.5f, -0.5f, -0.5f},  {0.5f, 0.5f, 0.5f},
        {0.5f, -0.5f, 0.5f},   {-0.5f, 0.5f, -0.5f}, {-0.5f, -0.5f, -0.5f},
        {-0.5f, 0.5f, 0.5f},  {-0.5f, -0.5f, 0.5f},
    };
    // triangles of the cube (12 triangles, 2 per face)
    static std::vector cube_triangles = {
        {4, 2, 0}, {2, 7, 3}, {6, 5, 7}, {1, 7, 5}, {0, 3, 1}, {4, 1, 5},
        {4, 6, 2}, {2, 6, 7}, {6, 4, 5}, {1, 3, 7}, {0, 2, 3}, {4, 0, 1}
    };

    // Add vertices and triangles to the mesh. The MeshExplicit class will store them in its internal data structures.
    for (const auto &v : cube_vertices) {
        r_mesh.add_vertex(v);
    }
    for (const auto &tri : cube_triangles) {
        r_mesh.add_triangle(tri);
    }

    // build mesh adjacency and attributes. The adjacency will be used to compute the vertex normals and other attributes for rendering.
    r_mesh.build_adjacency();
    r_mesh.build_attributes();

    return true;
}

void render(render_resources& resources, MeshRenderer<float>* mesh_renderer) {
    // first we check OpenGL state
    btm::GLContext* context = btm::get_current_gl_context();
    if (!context)
        return;

    begin_render();

    glClearColor(0.2f, 0.4f, 0.6f, 1.f);

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // gets the window dimensions from the GL context and sets the camera aspect ratio and viewport accordingly. 
    // This ensures that the rendered scene is displayed correctly within the window.
    int width = context->width();
    int height = context->height();
    resources.g_cam->set_aspect(width, height);
    resources.g_cam->set_viewport();
    // We then compute the camera matrix (combination of view and projection) and pass it to the shader. 
    fmat4 cam_matrix = resources.g_cam->perspective();
    // bring the shader into use and set the camera matrix uniform. 
    // The shader will use this matrix to transform the vertex positions from world space to clip space for rendering.
    resources.g_shader->use();
    resources.g_shader->set_mat4("camera", cam_matrix);

    // now call the renderer to draw the mesh. The MeshRenderer will use the shader and the mesh data to issue 
    // OpenGL draw calls to render the triangles of the mesh on the screen.
    mesh_renderer->render(resources.g_shader.get());

    resources.g_shader->end();

    end_render();
}

In later tutorials, we will enhance the renderer to upload data to the GPU via buffers, as well as implement more advanced visualization techniques such as displaying: curvature fields, feature lines, segmentation overlays, and refinement diagnostics.

7. Summary and Next Steps

In this tutorial, we:

  • defined a minimal mesh structure (vertices + triangles)
  • introduced adjacency as a foundation for local operators
  • outlined how attributes can be attached and extended
  • sketched how this structure connects to the rendering window

This data structure is the backbone for many algorithms in TheMeshProject. In the next tutorial, we will use it to implement:

  • Curvature Estimation in C++ — computing curvature on triangle meshes and preparing the results for visualization.