Skip to content

Rewrite from_points as a multi-level grid mesher - #31

Merged
fmrico merged 1 commit into
rollingfrom
multilevel_mesher
Oct 15, 2026
Merged

fmrico merged 1 commit into
rollingfrom
multilevel_mesher

Conversation

@fmrico

@fmrico fmrico commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #30

from_points (and from_pointcloud2) grew triangles from seed fans and border edges and produced overlapping, non-manifold meshes. This replaces it with a deterministic multi-level grid triangulation that is manifold by construction.

How it works

  1. Levels. Points are binned in XY cells of resolution. In each cell, points closer than max(min_clearance, max_dz) in z form one level: stacked floors give one vertex per floor.

  2. Height of a level. A robust plane fit (two passes, trimming outliers) of its upper face, at the cell centre.

    • A slab seen from above and below keeps its upper face.
    • A level that is not a plane (wall, furniture, bush) keeps a high top, so it does not join the floor.
  3. Sparse faces. Two neighbour-consensus passes:

    • a sparse upper face is adopted when a neighbour stands on it;
    • a cell that only saw the lower face of a slab takes its neighbours' upper face, but only if there is a level below it in its column.

    Spikes, potholes and boxes are never lifted.

  4. Holes. Enclosed empty regions up to max_hole_area (1 m² by default) are filled from the levels around most of their border.

  5. Links and triangles.

    • Each level links to the closest level of each of the 8 neighbouring cells, within the step and slope limits, only if the choice is mutual.
    • Each quad gets at most the two triangles of one diagonal per level: no overlap, at most two triangles per edge.
    • No triangle is steeper than max_slope_deg.
  6. Output.

    • Only used vertices are kept.
    • Surfaces are the connected components; max_surfaces and the new min_surface_area drop the triangles and vertices of the removed surfaces.
    • Layers: elevation, plus the new support (points behind each triangle, 0 if interpolated).

New BuildParams: min_clearance, min_surface_area, max_hole_area. max_edge_len, neighbor_radius, k_neighbors, use_radius and min_angle_deg are kept for API compatibility and are now ignored. Voxelized clouds need a resolution of at least twice the voxel size.

build_adjacency now sorts the edge uses instead of hashing, resets the neighbors before rebuilding, leaves non-manifold edges open (no asymmetric links) and counts them (nonmanifold_edges()).

The .navmap file format and the message are unchanged.

Results

Max triangles per edge in brackets.

Cloud Before After
Flat 20x20 m at 1 m 2193 triangles, 1592 m² (6) 800 triangles, 400 m² (2)
Excavation, 0.3 m 75 s, 7.2 M triangles, 4.5 M m² (46) 0.13 s, 113 k triangles (2)
House, 0.3 m 1.3 s, 166 000 m² for a ~300 m² house (47) 14 ms (2)
Simulated VLP-16 over the excavation, 5 cm noise + 1% spikes, 0.5 m height error p50/p90 2/11 cm 1/3 cm, +18% coverage
12.7 M points — 1.7–2.2 s, 0.6–0.7 GB

Tests

  • test_from_points (30 tests):
    • flat, noisy, irregular and voxelized clouds;
    • stacked floors and a ramp joining them;
    • steps, slope limit, wall, box, table vs min_clearance;
    • slab faces, sparse upper faces, pothole;
    • hole filling and its limits, multi-floor holes;
    • outliers, spikes, non-finite points, point order;
    • max_surfaces / min_surface_area, support layer, degenerate inputs.
  • test_adjacency (6 tests): non-manifold edges, stale links after a rebuild, copy.
  • navmap_core and navmap_ros pass under ASan/UBSan.
  • The tests of navmap_tools, navmap_examples and the EasyNav NavMap plugins pass unchanged.

from_points grew triangles from seed fans and border edges and produced
overlapping, non-manifold meshes. It now triangulates a grid of XY cells:

- Points of a cell are split into levels separated by more than the
  clearance (min_clearance, default 1 m), so stacked floors are separate
  vertices. A level's height is a robust plane fit of its upper face at the
  cell centre; a slab seen from above and below keeps its upper face, and a
  level that is not a plane (wall, furniture) keeps a high top, as an
  obstacle.
- Sparse upper faces and cells that only saw the lower face of a slab are
  recovered from the neighbouring cells, without lifting spikes, potholes
  or boxes.
- Small enclosed holes (max_hole_area) are filled from their border.
- Neighbouring levels are linked by mutual closest height within the step
  and slope limits, and every quad gets at most the two triangles of one
  diagonal per level: no overlap, at most two triangles per edge.
- Only used vertices are kept; max_surfaces and min_surface_area drop the
  triangles and vertices of the removed surfaces. A "support" layer counts
  the points behind each triangle (0 if interpolated).

build_adjacency resets the neighbors, leaves non-manifold edges open and
counts them (nonmanifold_edges()).

Fixes #30

Signed-off-by: Francisco Martín Rico <fmrico@gmail.com>
@fmrico
fmrico merged commit 7befd36 into rolling Oct 15, 2026
1 check passed
@fmrico
fmrico deleted the multilevel_mesher branch October 15, 2026 04:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

from_points builds overlapping, non-manifold meshes

1 participant