libmesh

Unified mesh library: templated mesh representation, multi-format I/O, and processing pipeline

Overview

libmesh is a unified, templated mesh library that provides consistent mesh representation, multi-format I/O, halfedge topology operations, and a composable processing pipeline.

Features

  • Templated Precision: Mesh<float> for GPU/STL, Mesh<double> for FEM analysis
  • Multi-Format I/O: STL (binary/ASCII), OBJ, PLY (binary/ASCII), VTK export
  • Processing Pipeline: Normal generation, vertex merging, triangulation, transforms
  • Halfedge Topology: Efficient topology queries and modification operations
  • Advanced Processors: Loop subdivision, quadric simplification, hole filling
  • FEM Integration: Eigen conversion utilities, boundary condition support

Quick Start

Basic Mesh Operations

#include "mesh/core/Mesh.hpp"

// Create a mesh
mesh::Mesh<double> m;
m.addVertex(0, 0, 0);
m.addVertex(1, 0, 0);
m.addVertex(0.5, 1, 0);
m.addElement(mesh::ElementType::Tri3, {0, 1, 2});

// Compute normals
m.recalculateNormals();

// Get bounding box
mesh::Vector3<double> min, max;
m.computeBounds(min, max);

Loading and Saving Files

#include "mesh/io/IORegistry.hpp"

// Load from file (format auto-detected)
auto result = mesh::io::load<double>("model.stl");
if (result.isValid()) {
    auto& mesh = result.value();
    // Use mesh...
}

// Save to file
mesh::io::save(mesh, "output.obj");

Processing Pipeline

#include "mesh/process/Pipeline.hpp"

mesh::Mesh<double> mesh = loadMesh();

// Apply processing operations
mesh::process::process(mesh,
    mesh::process::GenerateNormals |
    mesh::process::MergeVertices |
    mesh::process::CenterOrigin);

Halfedge Topology

#include "mesh/topology/HalfedgeMesh.hpp"

// Build halfedge structure
auto he = mesh::topology::HalfedgeMesh<double>::fromMesh(mesh);

// Query topology
bool closed = he.isClosed();
int euler = he.eulerCharacteristic();
auto boundaries = he.boundaryLoops();

// Iterate vertex neighbors
for (auto vit = he.vertexVertices(v); vit; ++vit) {
    auto neighbor = *vit;
    // Process neighbor...
}

Subdivision and Simplification

#include "mesh/process/LoopSubdivision.hpp"
#include "mesh/process/QuadricSimplification.hpp"

// Subdivide (4x face count)
mesh::process::LoopSubdivision<double> subdiv;
subdiv.process(mesh, {});

// Simplify to 50% faces
mesh::process::ProcessOptions opts;
opts.extra["targetRatio"] = 0.5;
mesh::process::QuadricSimplification<double> simplify;
simplify.process(mesh, opts);

QuadricSimplification is feature-aware (construct-mesh-quality §AR-5.3): dihedral-angle analysis tags crease and corner vertices, crease constraint planes keep decimation sliding along sharp edges instead of eroding them, corners are pinned, and only link-condition-safe collapses run — the closed/manifold/orientable predicates are re-validated afterwards and the input restored if they regressed. Tune with setFeatureAngleDeg() / setPreserveFeatures() / setFeatureWeight().

SDF-referenced post-processing

Two processors consume a signed-distance field alongside the mesh (construct-mesh-quality §AR-5); both preserve watertight-manifoldness:

#include "mesh/process/SdfProjection.hpp"
#include "mesh/process/CoplanarMerge.hpp"

// Snap vertices onto the field's zero level set (Newton, ÷‖∇Φ‖²),
// with tangential relaxation. Crease vertices move only along their
// feature line; corners stay pinned.
mesh::process::SdfProjection<float> project;
project.setField([&](const mesh::Vector3<float> &p) {
    mesh::process::SdfSample<float> s;
    s.value = phi(p);        // Φ(p)
    s.gradient = gradPhi(p); // raw ∇Φ (need not be unit length)
    return s;
});
project.process(mesh, {});

// Merge near-coplanar facets and retriangulate each patch from its
// boundary loop (flat caps become fans).
mesh::process::CoplanarMerge<float> merge;
merge.setAngleToleranceDeg(1.0f);
merge.process(mesh, {});
#include "mesh/process/IsotropicRemesh.hpp"

// Full feature-aware Botsch-Kobbelt loop toward a uniform edge length:
// split long / collapse short / flip to valence 6 / smooth + project.
mesh::process::IsotropicRemesh<float> remesh;
remesh.setField(sdf);            // same callback shape as SdfProjection
remesh.setTargetEdgeLength(0.3f);
remesh.process(mesh, {});

Shared feature tagging lives in mesh/process/FeatureDetection.hpp (analyzeFeatures): dihedral-angle feature edges plus the smooth/crease/corner vertex classification used by the processors above. confirmFeaturesAgainstField verifies those tags against the SDF's gradient (a real crease turns the gradient sharply across a small probe; coarse-mesh curvature does not), so false features never pin vertices off the surface.

Supported File Formats

Format Extension Import Export Notes
STL Binary .stl Yes Yes Default format
STL ASCII .stl Yes Yes Detected automatically
Wavefront OBJ .obj Yes Yes Normals, tex coords, groups
Stanford PLY .ply Yes Yes ASCII and binary
VTK Legacy .vtk No Yes Visualization export

Element Types

enum class ElementType : uint8_t {
    Point1 = 0,     // 0D
    Bar2 = 10, Bar3 = 11,  // 1D
    Tri3 = 20, Tri6 = 21,  // 2D triangles
    Quad4 = 22, Quad8 = 23, Quad9 = 24,  // 2D quads
    Tet4 = 30, Tet10 = 31,  // 3D tetrahedra
    Hex8 = 32, Hex20 = 33, Hex27 = 34,  // 3D hexahedra
    Wedge6 = 35, Pyramid5 = 36  // 3D other
};

Processing Flags

enum ProcessFlags : uint32_t {
    Triangulate       = 1 << 0,   // Convert polygons to triangles
    GenerateNormals   = 1 << 1,   // Compute vertex normals
    MergeVertices     = 1 << 2,   // Deduplicate within tolerance
    FlipNormals       = 1 << 3,   // Reverse winding order
    RemoveDegenerates = 1 << 4,   // Remove zero-area elements
    CenterOrigin      = 1 << 6,   // Move centroid to origin
    NormalizeScale    = 1 << 7,   // Scale to unit bounding box
    Subdivide         = 1 << 11,  // Loop subdivision
    Simplify          = 1 << 12,  // Quadric decimation
    FillHoles         = 1 << 14,  // Fill boundary holes
    SdfProject        = 1 << 20,  // Newton-project onto an SDF zero set
    MergeCoplanar     = 1 << 21,  // Merge near-coplanar facets
};

FEM Integration

For finite element analysis, use the conversion utilities:

#include "mesh/convert/ToEigen.hpp"
#include "mesh/convert/FEMBridge.hpp"

// Convert to Eigen vectors
Eigen::Vector3d ev = mesh::convert::toEigen(meshVec);

// Convert mesh vertices to matrix
Eigen::MatrixXd verts = mesh::convert::toEigenMatrix(mesh.vertices);

// Apply FEM displacements back to mesh
mesh::convert::applyDisplacements(mesh, displacements, dofsPerNode);

Boundary Conditions

#include "mesh/core/BoundaryCondition.hpp"

mesh::BoundaryConditionSet<double> bcs;

// Fix a node
bcs.fixNode(nodeIndex, {true, true, true});

// Prescribe displacement
bcs.prescribeDisplacement(nodeIndex, 0, 0.001);  // x-direction

// Apply force
bcs.applyForce(nodeIndex, 1, 100.0);  // y-direction force

Architecture

libmesh/
├── mesh/core/       - Vector3, Matrix3, Mesh<T>, ElementType
├── mesh/topology/   - HalfedgeMesh, handles, circulators
├── mesh/io/         - STL, OBJ, PLY, VTK importers/exporters
├── mesh/process/    - Pipeline, processors
└── mesh/convert/    - ToEigen, FEMBridge

Dependencies

  • Qt6 Core: File I/O, containers, strings
  • No Eigen: Eigen is contained to libfem only

Test Coverage

286 unit tests covering:

  • Core primitives (Vector3, Matrix3)
  • Element types and mesh operations
  • All I/O formats with round-trip verification
  • Halfedge topology construction and queries
  • Processing pipeline and all processors
  • FEM integration utilities
In this section
Topics
reference libmesh mesh 3d