Skip to content

Remeshing

A tessellator’s job is to be accurate: it puts samples where the surface curves and none where it does not, so a drilled plate comes back with 30 mm chords across the flats beside 0.5 mm chords around the bore rim. That is exactly right for rendering and exactly wrong for anything that solves on the mesh. An FEA solver wants elements of a known size with sensible aspect ratios; a smoothing, deformation or simulation pass wants a triangle whose one-ring means something.

Shape.Remeshed is the isotropic remesher (Remesher in EngrCAD.Mesh, Botsch and Kobbelt’s split/collapse/flip/smooth loop) reachable from the modelling vocabulary: rebuild the triangulation toward a uniform target edge length, keep the surface.

// A drilled plate, as tessellated and then remeshed to a 3 mm edge. Drawn as wireframe:
// the whole point is the triangles, and the default shaded view hides them.
var top = SketchPlane.At((0, 0, 4), Vector3d.UnitX, Vector3d.UnitY);
var plate = Shape.Box(60, 40, 8)
.Drill(StandardHoles.Clearance(6), [new(-18, 0), new(0, 0), new(18, 0)], depth: 10, top);
var scene = new Scene();
scene.Add(new Part("as tessellated", plate, Palette.Steel));
scene.Add(new Part("remeshed to 3 mm", plate.Remeshed(3.0, iterations: 14), Palette.Brass,
Matrix4d.CreateTranslation((0, 55, 0))));

A drilled plate's tessellation beside a remeshed copy with uniform triangles

The source (left) is a handful of long fan triangles across the caps and a dense ring at each bore; the remesh (right) is one uniform grade throughout — except at the bore rims, which stay fine because they are feature edges and a pinned chain may be split but never collapsed. That is the constraint doing its job, not the remesher failing.

The triangle count goes up — uniformity is not free — but the distribution is what changes: measured on a Ø20 × 20 cylinder remeshed to a 2 mm target, the share of edges inside the remesher’s [0.66 L, 1.33 L] band goes from 42% to 95%.

var cylinder = Shape.Cylinder(10, 20);
static double InBand(HalfEdgeMesh mesh, double target) =>
(double)mesh.Edges.Count(e => e.Vector.Length >= 0.66 * target && e.Vector.Length <= 1.33 * target)
/ mesh.Edges.Count();
double before = InBand(cylinder.ToMesh(), 2.0);
double after = InBand(cylinder.Remeshed(2.0, iterations: 14).ToMesh(), 2.0);
if (before > 0.5) throw new Exception($"the tessellation should be uneven, was {before:P0}");
if (after < 0.85) throw new Exception($"the remesh should be even, was {after:P0}");

Note what that measures and what it does not. The distribution is only half the story: an edge-length band says nothing about triangle shape, and the two can disagree sharply. RemeshResult.Quality is the other half — minimum angle and radius ratio, with the triangles the remesher was not free to change counted separately.

Bounding the longest edge

Left to itself the remesher converges its distribution fast and its maximum hardly at all, and spending more passes does not fix it, because the cause is not a shortage of passes. It is the flip stage: the flip rule is pure valence arithmetic that never looks at a length, so on an elongated quad it swaps the short diagonal for the long one and manufactures exactly the edge the split stage exists to remove. Switch flips off and the same run ends at exactly the 1.33 L threshold — the splits were doing their job all along.

PreventLongEdgeFlips refuses a flip that would leave an edge above the split threshold, unless it is shorter than the edge it replaces (a flip from 2.5 L to 1.5 L is progress, and refusing it strands slivers). It is on by default; switching it off is how you reproduce a pre-existing result, and it is what the comparison below does:

var cylinder = Shape.Cylinder(10, 20);
static double MaxEdge(HalfEdgeMesh mesh) => mesh.Edges.Max(e => e.Vector.Length);
var options = new RemeshOptions(2.0) { Iterations = 14 };
double bounded = MaxEdge(cylinder.Remeshed(options).ToMesh());
double plain = MaxEdge(cylinder.Remeshed(options with { PreventLongEdgeFlips = false }).ToMesh());
// Measured 4.02 mm (2.01 L) unguarded against 2.68 mm (1.34 L) — the threshold is 2.66 mm.
if (plain < 3.5) throw new Exception($"the unguarded maximum should stall high, was {plain:F2}");
if (bounded > 3.0) throw new Exception($"the default maximum should reach the band, was {bounded:F2}");

It shipped opt-in and became the default on measurement. Over a box, a cylinder, two UV spheres and an open height-field grid at 10 / 14 / 40 passes, every measure improves: maximum 1.37–2.65 → 1.27–1.67 L, shortest edge 0.03–0.26 → 0.10–0.67 L, out of band 2.5–11.8% → 0.0–0.9%, worst triangle angle 0.24–17.1° → 22.8–36.3°, worst radius ratio 0.0001–0.30 → 0.38–0.81 — and the run is faster every time (a box at ten passes, 79 ms → 22 ms), because a mesh full of slivers is a mesh full of work.

One measure does trade on a heavily pinned model, and it is worth knowing because it is the same lesson as “judge by the share, not the extremes”, one level up. The drilled plate at the top of this page has bore rims far finer than the 3 mm target which, being pinned creases, cannot be coarsened: it keeps a single 0.01° triangle against a bore where the unguarded run reads 1.40°, both slivers. Every population figure on that fixture goes the other way and by a lot — slivers 419 → 157, out of band 14.1% → 7.5%, longest edge 2.56 → 1.49 L. Compare sliver counts; use the worst angle to find a defect, not to rank two meshes.

The deciding argument is downstream, though: tet meshing’s boundary recovery needs a surface that is already the boundary of the Delaunay tetrahedralization of its own vertices, and a valence flip can replace a Delaunay diagonal with a longer one. This flag is the difference between a remeshed sphere that meshes in zero recovery rounds and one that is refused — and a default that produces surfaces the kernel’s own tet mesher will not accept is the wrong default.

Measuring the shape, not just the lengths

var plate = Shape.Box(60, 40, 8);
var remesh = Remesher.Remesh(plate.ToMesh(), new RemeshOptions(3.0) { Iterations = 14 });
var quality = remesh.Quality;
// Minimum angle and radius ratio (1 = equilateral), over the triangles the remesher was
// actually free to change. Triangles every corner of which is pinned — a crease chain
// finer than the target cannot be coarsened, because a collapse needs a free end — are
// counted separately rather than rated against the same bar.
if (quality.MinAngleDegrees < 20)
throw new Exception($"worst free angle {quality.MinAngleDegrees:F1} degrees");
if (quality.MinRadiusRatio < 0.3)
throw new Exception($"worst radius ratio {quality.MinRadiusRatio:F3}");

What that partition can and cannot tell you is worth stating: a constrained sliver and a free one are the same triangle, and the only difference is which vertices you pinned, which is intent rather than shape. A partially pinned triangle counts as free — the conservative side, since the alternative hides real defects behind constraints that were not binding.

What it keeps, and what it does not

Two settings do most of the work.

The shape is held by projection. Smoothing is curvature flow: left alone it shrinks the model a little every pass, so each pass ends by projecting every free vertex back onto a target surface. Shape.Remeshed supplies one automatically — the child’s mesh as lowered — so what is preserved is the child’s tessellation at the requested quality. Remesh a sphere from a coarse lowering and the result is faithful to that coarse mesh, not to the sphere. To project onto the exact geometry instead, pass SdfProjectionTarget over the shape’s own field:

var sphere = Shape.Sphere(10);
var refined = sphere.Remeshed(new RemeshOptions(2.0)
{
Iterations = 14,
FeatureAngleDegrees = 0, // a sphere has no creases
ProjectionTarget = new SdfProjectionTarget(sphere.ToImplicit()),
}).ToMesh(new MeshQuality { SegmentsPerCircle = 16 }); // a deliberately coarse lowering
double worst = refined.Vertices.Max(v => Math.Abs(v.Position.Length - 10));
if (worst > 0.05) throw new Exception($"vertices should sit on the exact sphere, worst {worst}");

Creases are protected by feature detection. FeatureAngleDegrees (30° by default) pins the ends of every edge whose dihedral is sharper than that, so a box stays a box rather than melting into a rounded lump. The trap is that feature detection reads the mesh it is given, not the surface you meant: a coarse tessellation of a smooth surface has large dihedrals too, so a 12-segment sphere’s facets meet at ~30° and much of it gets pinned. Pass 0 (or a larger angle) when remeshing tessellated curvature — as the sphere example above does.

Remeshing part of a model

Whole-model remeshing is rarely what a real part wants: one bore wall may need 0.2 mm triangles while the plate around it should not be touched at all. RegionRemesher takes a face selection, remeshes it in place and stitches it back, leaving the rest of the model exactly as it was.

var mesh = Shape.Box(40, 40, 10).ToMesh();
var top = MeshFaceSelection.FromIndices(
mesh, mesh.Faces.Where(f => f.Vertices().All(v => v.Position.Z == 5)).Select(f => f.Index));
var result = RegionRemesher.Remesh(mesh, top, new RemeshOptions(3.0)
{
Iterations = 10,
FeatureAngleDegrees = 0,
});
result.Mesh.Validate();
if (!result.Mesh.IsClosed) throw new Exception("a region remesh must not open the model");
if (Math.Abs(result.Mesh.Volume() - 16000) > 1e-6) throw new Exception("the solid must not change");
if (result.Region.Count <= top.Count) throw new Exception("the region should have refined");

The region’s rim is the contract: it may gain vertices but never move, and splits along it are carried into the neighbouring faces so no T-junction is left behind. That is what lets a finely remeshed patch meet a coarse model at all.

The honest support story

Remeshing is defined on a triangulation, and Explain says so rather than pretending otherwise:

var shape = Shape.Box(20, 20, 20).Remeshed(3.0);
// B-Rep: impossible. A remesh produces a tessellation, not a surface, and there is no
// mesh-to-B-Rep import.
if (shape.Explain(TargetRep.Brep).IsConvertible) throw new Exception("should be impossible");
// Mesh: native ground, reached by the mesh route rather than through the field.
var mesh = shape.Explain(TargetRep.Mesh);
if (!mesh.IsConvertible) throw new Exception("mesh should work");
// Implicit: bridged through a mesh SDF of the remeshed triangles, so the field carries
// the tessellation's chord error rather than the box's exact one.
var field = shape.Explain(TargetRep.Implicit);
if (!field.IsConvertible) throw new Exception("implicit should work");
Console.WriteLine(field.Entries.Single(e => e.Node.StartsWith("Remeshed(")).Detail);

So Remeshed belongs at the end of a model, after the exact work is done — put it in the middle and everything downstream inherits a tessellation. A uniform scale above it scales the target edge length with it, so the node means the same thing wherever the graph places it.

Fairing (Laplacian smoothing)

Shape.Smoothed is the sibling operation: where remeshing rebuilds the connectivity and holds the shape, fairing moves the vertices and changes it. Each pass solves the implicit fairing system (M + λL)·x′ = M·x (LaplacianMeshSmoother in EngrCAD.Mesh), which rounds sharp features and reduces noise. It acts on the tessellation, so a coarse cube — eight vertices — barely moves; remesh first for a dense surface, then fair it.

// A finely tessellated cube, faired into a rounded solid. A closed solid has no boundary
// to pin, so the whole surface fairs: curvature flow that rounds corners and shrinks.
var cube = Shape.Box(20, 20, 20).Remeshed(1.5, iterations: 12);
var scene = new Scene();
scene.Add(new Part("remeshed cube", cube, Palette.Steel));
scene.Add(new Part("faired", cube.Smoothed(1.0, iterations: 3), Palette.Brass,
Matrix4d.CreateTranslation((0, 30, 0))));

A finely tessellated cube beside a Laplacian-faired copy with rounded corners

The timeStep is dimensionless and scale-free (λ = timeStep · h̄² for the mean edge length), so the same value fairs the same amount at any model scale; iterations rebuilds the operator from the current geometry each pass, so honest curvature flow. And because it is defined on a triangulation, its support story is the remesh’s: B-Rep is impossible (a tessellation, not a surface), implicit is bridged through a mesh SDF of the faired triangles.

var box = Shape.Box(20, 20, 20);
var faired = box.Smoothed(1.0, iterations: 2);
// A closed solid has no boundary to pin, so the whole surface fairs: the corners pull in
// and the volume drops. That is the operation, not a defect.
double before = box.ToMesh().Volume();
double after = faired.ToMesh().Volume();
if (after >= before) throw new Exception($"fairing should shrink the body: {before} -> {after}");
if (faired.Explain(TargetRep.Brep).IsConvertible) throw new Exception("B-Rep should be impossible");
if (!faired.Explain(TargetRep.Implicit).IsConvertible) throw new Exception("implicit should work");