Modules | C++

Remesh

Simplification, isotropic remeshing, edge collapse, and custom handlers.

The Remesh module provides tools for modifying triangle mesh resolution: reducing complexity through simplification (decimate to a target face count, or simplify to an error budget) and edge collapse, or redistributing vertices through isotropic remeshing. All functions operate on triangle meshes and return a new mesh along with the resulting half-edge structure.

Include the module with:

#include <trueform/remesh.hpp>
All remesh functions require triangle meshes (static face size of 3). Polygonal meshes must be triangulated first using tf::triangulated from the Geometry module.
All remesh operations use parallel execution by default. Set config.parallel = false for sequential execution (e.g. when processing many meshes in parallel externally).
All edge length decisions (split thresholds, collapse thresholds, max edge length checks) respect tagged transformations. When the input polygons have a tagged frame, lengths are measured in the transformed coordinate space. This allows remeshing a scaled or rotated mesh without modifying vertex data.

Simplification

Both decimate and simplify reduce a triangle mesh by quadric-error edge collapse (Garland–Heckbert): edges are collapsed in priority order, the surviving vertex placed at the position that minimizes geometric error. They differ only in the stopping criterion:

  • decimate — stop at a target face count: the result keeps a fixed fraction of the input faces.
  • simplify — stop at a geometric error budget: edges collapse only while their quadric error stays within a chosen tolerance, so flat regions collapse to almost nothing while curved detail and feature edges survive.

To a Target Face Count

Reduce face count to a fixed fraction of the original using quadric error metrics. The algorithm collapses edges in priority order, placing the new vertex at the position that minimizes geometric error.

Basic Usage

decimated.cpp
auto mesh = tf::read_stl("model.stl");
auto polys = mesh.polygons();

// Decimate to 10% of original faces
auto [result, he] = tf::decimated(polys, 0.1f);

// result is a tf::polygons_buffer, he is a tf::half_edges
for (auto tri : result.polygons()) {
    auto [pt0, pt1, pt2] = tri;
}

With Configuration

decimated_config.cpp
tf::decimate_config<float> config;
config.preserve_boundary = true;
config.min_quality = 0.3f;
config.parallel = false; // sequential execution

auto [result, he] = tf::decimated(polys, 0.1f, config);

decimate_config parameters:

ParameterDefaultDescription
min_quality-1Worst triangle quality allowed after a collapse, in 0,1 (1 = equilateral). Negative disables, 0 = never worsen, >0 = quality floor
check_normalstrueReject collapses that would invert a triangle normal
preserve_boundarytrueIf true, boundary edges are never collapsed
stabilizer1e-3Tikhonov stabilizer for quadric solve
paralleltrueIf true, use parallel partitioned collapse
feature_angle-1 (disabled)Dihedral angle threshold for feature edge detection. Edges sharper than this are preserved
feature_weight100Penalty weight for feature edge quadrics. Higher = stronger preservation

Feature Edge Preservation

Preserve sharp creases and corners during decimation by setting feature_angle to the minimum dihedral angle (in degrees or radians) that defines a feature edge:

decimated_features.cpp
tf::decimate_config<float> config;
config.feature_angle = tf::deg(30.f); // preserve edges sharper than 30 degrees
config.feature_weight = 100.f;

auto [result, he] = tf::decimated(polys, 0.1f, config);

Feature edges receive penalty quadrics that keep the optimal collapse point on the feature line. Corner vertices (where 3+ feature edges meet) are automatically protected.

With Index Maps

Pass tf::return_index_map to also receive face and vertex index maps that track which elements survived the decimation:

decimated_index_map.cpp
auto [result, he, face_map, vertex_map] =
    tf::decimated(polys, 0.1f, tf::return_index_map);

// Or with config:
auto [result, he, face_map, vertex_map] =
    tf::decimated(polys, 0.1f, config, tf::return_index_map);

With Pre-Computed Half-Edges

When half-edges are already available (e.g. from a previous operation), tag them onto the polygons to skip rebuilding:

decimated_precompute.cpp
tf::half_edges<int> he(polys);

// Tag half-edges onto polygons - decimated reuses them
auto [result, he_out] = tf::decimated(polys | tf::tag(he), 0.1f);

To an Error Budget

simplify collapses every edge whose quadric error stays within error_rel × bounding-box diagonal, cheapest first, to exhaustion. There is no target face count: flat regions collapse to almost nothing while curved detail and feature edges are kept. This is the right tool when you care about fidelity rather than a specific size — e.g. cleaning up the over-sampled output of a boolean or arrangement.

Basic Usage

simplified.cpp
auto mesh = tf::read_stl("model.stl");
auto polys = mesh.polygons();

// Simplify within the default budget (0.2% of the bbox diagonal)
auto [result, he] = tf::simplified(polys);

// result is a tf::polygons_buffer, he is a tf::half_edges

With Configuration

simplified_config.cpp
tf::simplify_config<float> config;
config.error_rel = 0.005f;        // allow more deviation -> fewer faces
config.feature_angle = tf::deg(30.f);
config.preserve_boundary = false;

auto [result, he] = tf::simplified(polys, config);

simplify_config parameters:

ParameterDefaultDescription
error_rel0.002Error allowed per collapse pass, as a fraction of the bounding-box diagonal: an edge collapses when its quadric error is ≤ error_rel × diagonal. With iterations > 1 it is re-applied each pass (against the current surface), so it caps per-pass error, not total deviation from the original
optimize_iterations3Rounds of quality cleanup (min-angle flip + tangential relaxation) run after each collapse. 0 = pure error-budget collapse
iterations1Outer collapse rounds. 1 = single collapse + cleanup; >1 re-collapses after each cleanup (iterated remesh), removing more at the cost of more deviation from the original
relaxation_iters3Tangential relaxation passes per cleanup round
lambda0.5Damping factor for tangential relaxation in (0, 1]
min_quality0.3Worst triangle quality allowed after a collapse, in 0,1 (1 = equilateral). Negative disables, 0 = never worsen, >0 = quality floor
check_normalstrueReject collapses that would invert a triangle normal
preserve_boundarytrueIf true, boundary edges are never collapsed
feature_angle-1 (disabled)Dihedral angle threshold for feature edge detection. Edges sharper than this are preserved
feature_weight100Penalty weight for feature edge quadrics. Higher = stronger preservation
stabilizer1e-3Tikhonov stabilizer for quadric solve
paralleltrueIf true, use parallel partitioned collapse

Quality Cleanup

A pure error-budget collapse leaves whatever triangulation the collapses produce, which can include thin slivers in flattened regions. Set optimize_iterations to run that many rounds of min-angle edge flip plus tangential relaxation after the collapse — feature, region and boundary aware — to even out the result:

simplified_optimize.cpp
tf::simplify_config<float> config;
config.optimize_iterations = 5; // collapse, then 5 cleanup rounds

auto [result, he] = tf::simplified(polys, config);

Iterated Remesh

A single collapse stops at the edges the quality guard blocked. Set iterations > 1 to re-collapse after each cleanup round: the flip pass repairs the slivers a collapse leaves behind, which unblocks collapses the previous round refused, so more is removed. This turns simplify into an iterated error-budget remesh — the error sibling of isotropic_remesh. It trades fidelity for coarseness, since each round measures error against the already-collapsed surface rather than the original.

simplified_iterated.cpp
tf::simplify_config<float> config;
config.iterations = 3; // collapse + cleanup, three times

auto [result, he] = tf::simplified(polys, config);

In Place on a Points Buffer

When you already own the half-edges and a points buffer (e.g. mid-pipeline), tf::simplify mutates them in place — collapsing, compacting, and reindexing the points — without copying into a new mesh:

simplify_inplace.cpp
tf::half_edges<int> he(polys);
tf::points_buffer<double, 3> points;
points.allocate(polys.points().size());
tf::parallel_copy(polys.points(), points.points());

tf::simplify_config<double> config;
tf::simplify(he, points, config); // he and points are now simplified

auto faces = tf::make_faces_buffer(he);

Region Preservation

To keep the boundaries between labelled regions (e.g. geological domains, material groups) intact, pass tf::preserve_regions with a per-face label range. Edges between differently-labelled faces are treated as features and never crossed; the call returns the output mesh's per-face labels alongside the mesh. This works the same way across tf::decimated, tf::simplified, tf::isotropic_remeshed, and tf::collapsed_short_edges — each has an overload taking tf::preserve_regions and returning the per-face labels. The returned labels keep the value type of the range you pass in.

simplified_regions.cpp
auto [result, he, face_labels] =
    tf::simplified(polys, config, tf::preserve_regions(region_of_face));

// same shape for the other operations, e.g.:
auto [dmesh, dhe, dlabels] =
    tf::decimated(polys, 0.1f, tf::preserve_regions(region_of_face));

Isotropic Remeshing

Redistribute vertices to achieve uniform edge lengths. Each iteration splits long edges, collapses short edges, flips edges to improve valence, and relaxes vertex positions tangentially.

Basic Usage

isotropic_remeshed.cpp
auto mesh = tf::read_stl("model.stl");
auto polys = mesh.polygons();

// Remesh to target edge length
float mel = tf::mean_edge_length(polys);
auto [result, he] = tf::isotropic_remeshed(polys, 2.0f * mel);

With Configuration

isotropic_remeshed_config.cpp
tf::isotropic_remesh_config<float> config{2.0f * mel};
config.iterations = 5;
config.relaxation_iters = 5;
config.preserve_boundary = true;

auto [result, he] = tf::isotropic_remeshed(polys, config);

isotropic_remesh_config parameters:

ParameterDefaultDescription
target_length(required)Target edge length. Edges longer are split, shorter are collapsed
iterations3Number of outer iterations (split + collapse + flip + relax)
relaxation_iters3Number of tangential relaxation iterations per outer iteration
min_quality0.3Worst triangle quality allowed after a collapse, in 0,1 (1 = equilateral). Negative disables, 0 = never worsen, >0 = quality floor
check_normalsfalseReject collapses, and edge flips, that would invert a triangle normal
lambda0.5Damping factor for tangential relaxation in (0, 1]
preserve_boundarytrueIf true, boundary edges are never split or collapsed
use_quadricfalseIf true, use quadric error metric for collapse vertex placement
paralleltrueIf true, use parallel collapse during the collapse step
feature_angle-1 (disabled)Dihedral angle threshold for feature edge detection. Edges sharper than this are preserved
feature_weight100Penalty weight for feature edge quadrics. Higher = stronger preservation

With Pre-Computed Half-Edges

isotropic_remeshed_precompute.cpp
tf::half_edges<int> he(polys);
auto [result, he_out] = tf::isotropic_remeshed(polys | tf::tag(he), 2.0f * mel);

Edge Collapse

Collapse edges shorter than a threshold. Unlike decimation which uses quadric error ordering, this collapses by edge length — shortest first.

Basic Usage

collapsed_short_edges.cpp
auto mesh = tf::read_stl("model.stl");
auto polys = mesh.polygons();

float mel = tf::mean_edge_length(polys);

// Collapse edges shorter than 3/4 mel
auto [result, he] = tf::collapsed_short_edges(polys, 0.75f * mel);

With Configuration

collapsed_short_edges_config.cpp
tf::length_collapse_config<float> config;
config.max_length = 1.25f * mel;
config.preserve_boundary = true;
config.use_quadric = true;

auto [result, he] = tf::collapsed_short_edges(polys, 0.75f * mel, config);

length_collapse_config parameters:

ParameterDefaultDescription
max_lengthmaxMaximum edge length allowed after a collapse
min_quality-1Worst triangle quality allowed after a collapse, in 0,1 (1 = equilateral). Negative disables, 0 = never worsen, >0 = quality floor
check_normalsfalseReject collapses that would invert a triangle normal
preserve_boundarytrueIf true, boundary edges are never collapsed
use_quadrictrueIf true, use quadric error metric for vertex placement
paralleltrueIf true, use parallel partitioned collapse
feature_angle-1 (disabled)Dihedral angle threshold for feature edge detection
feature_weight100Penalty weight for feature edge quadrics
stabilizer1e-6Tikhonov stabilizer for quadric solve

With Index Maps

Pass tf::return_index_map to also receive face and vertex index maps:

collapsed_short_edges_index_map.cpp
auto [result, he, face_map, vertex_map] =
    tf::collapsed_short_edges(polys, 0.75f * mel, tf::return_index_map);

// Or with config:
auto [result, he, face_map, vertex_map] =
    tf::collapsed_short_edges(polys, 0.75f * mel, config, tf::return_index_map);

With Pre-Computed Half-Edges

collapsed_short_edges_precompute.cpp
tf::half_edges<int> he(polys);
auto [result, he_out] = tf::collapsed_short_edges(polys | tf::tag(he), 0.75f * mel);

Custom Handlers

For advanced use cases — adaptive remeshing, per-region target lengths, narrow triangle removal — the module exposes the underlying handler types. These give full control over edge scoring and acceptance while reusing the collapse machinery (quadrics, feature masks, parallel partitioning).

Split Handler

A tf::split_handler wraps a scoring lambda. Edges with score > 1 are split.

split_handler.cpp
float max2 = max_length * max_length;

auto handler = tf::make_split_handler<float>(
    [max2](const auto &he, const auto &points, auto heh) -> float {
        auto v0 = he.start_vertex_handle(tf::unsafe, heh).id();
        auto v1 = he.end_vertex_handle(tf::unsafe, heh).id();
        auto len2 = (points[v1] - points[v0]).length2();
        return len2 / max2; // > 1 triggers split
    },
    true,  // preserve_boundary
    3);    // max_iterations

tf::split_edges(he, points, handler);

Collapse Handler

A tf::collapse_handler wraps a scoring lambda and an acceptance checker. Edges with score < 1 are eligible for collapse, ordered by score (0 = highest priority). The score lambda receives a reference to the handler, providing access to quadrics and config.

collapse_handler.cpp
float min2 = min_length * min_length;

auto checker = tf::make_collapse_checker<float>(
    0.3f,    // min_quality (scalar, [0,1]; 1 = equilateral)
    max2);   // max_edge_length² (scalar)

tf::collapse_config<float> config;
config.use_quadric = true;
config.feature_angle = tf::deg(30.f);

auto handler = tf::make_collapse_handler<float>(
    [min2](const auto &he, const auto &points, auto heh,
           const auto &) -> float {
        auto v0 = he.start_vertex_handle(tf::unsafe, heh).id();
        auto v1 = he.end_vertex_handle(tf::unsafe, heh).id();
        auto len2 = (points[v1] - points[v0]).length2();
        return len2 / min2; // < 1 triggers collapse
    },
    checker, config);

tf::collapse_edges(he, points, handler);

Collapse Checker

The tf::collapse_checker wraps the dihedral geometric check. Each parameter can be a scalar, a callable (half_edges, points, edge_handle) → Real, or tf::none to disable:

collapse_checker.cpp
// Scalar thresholds
auto checker = tf::make_collapse_checker<float>(0.3f, max_len_sq);

// Per-edge max length via callable, no quality check
auto checker = tf::make_collapse_checker<float>(
    tf::none,
    [&](const auto &he, const auto &points, auto eh) -> float {
        // look up per-face target from adjacent faces
        auto heh = he.half_edge_handle(tf::unsafe, eh, false);
        auto f = he.face_handle(tf::unsafe, heh).id();
        return face_targets[f] * face_targets[f];
    });

// No geometric checks at all
auto checker = tf::make_collapse_checker<float>();

Quadric Scoring

For decimation-style scoring using quadric error metrics, the score lambda can access the handler's quadrics (built during init):

quadric_scoring.cpp
auto score = [](const auto &he, const auto &points, auto heh,
                const auto &handler) -> float {
    return tf::remesh::collapse_error_quadric<float>(
        handler._quadrics, points, he, heh, handler._config.stabilizer);
};

auto checker = tf::make_collapse_checker<float>(40.0f);
auto handler = tf::make_collapse_handler<float>(score, checker, config);

// Collapse to target face count
tf::collapse_edges(he, points, handler, target_faces);

Configuration Hierarchy

All configs share a common base:

ConfigInheritsAdds
collapse_configpreserve_boundary, use_quadric, parallel, feature_angle, feature_weight, stabilizer
collapse_guard_configcollapse_configmin_quality, check_normals
decimate_configcollapse_guard_config— (sets check_normals true)
simplify_configcollapse_guard_configerror_rel, iterations, optimize_iterations, relaxation_iters, lambda
length_collapse_configcollapse_guard_configmax_length
isotropic_remesh_configcollapse_guard_configtarget_length, iterations, relaxation_iters, lambda