Tutorials
May 8, 2026
c++csgbooleanstutorialvariadic

Fast Variadic Mesh Booleans in C++

Variadic mesh boolean operations in C++. Build one arrangement over N meshes, extract any boolean expression from it. 201 meshes (~22.5M input triangles) in 0.6 seconds. 125× faster than MeshLib on the Lévy benchmark.

Žiga Sajovic, Polydera

Pairwise booleans? See Fast Mesh Booleans in C++.

trueform extends mesh booleans to variadic CSG: boolean expressions over N meshes evaluated against a single implicit arrangement. Build the arrangement once, extract any union, intersection, or difference of any subset of operands. Adding another expression on the same N meshes pays only the extraction cost.

trueform GitHub Documentation

We follow the Lévy 2025 Fibonacci-bunny scene (cover figure of arXiv:2405.12949): a sphere with N Stanford Bunnies arranged on its surface via the Fibonacci lattice, oriented +Z radially outward. At N = 200 that's ~22.5M input triangles. From one CSG graph we extract both the variadic union sphere ∪ (⋃ bunnies) (11.7M triangles out) and the variadic difference sphere ∖ (⋃ bunnies) (7.9M triangles out).

613msBuild CSG graph201 meshes · 22.5M tris
69msExtract union11.7M output tris
682msTotalbuild + extract

One graph. Many extractions.

Apple M4 Max 16 threads Clang -O3 mimalloc

Loading the meshes

Include trueform:

#include <trueform/trueform.hpp>

Load the canonical sphere and bunny:

auto sphere = tf::make_sphere_mesh(2.5f, 32, 32);
auto bunny  = tf::cleaned(
    tf::read_stl("Stanford_Bunny.stl").polygons(),
    tf::epsilon<float>);

Each mesh is read once. The bunny is instanced N times via tagged transformations — no geometry is duplicated.

Tagging the canonical meshes

Pre-tag each canonical mesh with face membership, manifold edge link, and an AABB tree. Every instance shares the same tagged structures — the arrangement build reuses them across all N instances:

auto tag_all = [](auto &m) {
  auto fm  = tf::make_face_membership(m.polygons());
  auto mel = tf::make_manifold_edge_link(m.polygons() | tf::tag(fm));
  auto tr  = tf::aabb_tree<int, float, 3>(m.polygons(), tf::config_tree(4, 4));
  return std::tuple{std::move(fm), std::move(mel), std::move(tr)};
};

auto [b_fm, b_mel, b_tr] = tag_all(bunny);
auto [s_fm, s_mel, s_tr] = tag_all(sphere);
auto bunny_tagged  = bunny.polygons()  | tf::tag(b_fm) | tf::tag(b_mel) | tf::tag(b_tr);
auto sphere_tagged = sphere.polygons() | tf::tag(s_fm) | tf::tag(s_mel) | tf::tag(s_tr);

Distributing instances

Distribute N frames over the sphere using the Fibonacci lattice. Each instance is the canonical bunny with a tagged frame — same geometry, different pose:

constexpr int N = 200;
const float golden = (1.0f + std::sqrt(5.0f)) / 2.0f;

std::vector forms{
    sphere_tagged | tf::tag(tf::transformation<float, 3>{}),
};
for (int i = 0; i < N; ++i) {
  const float z  = 1.0f - (2.0f * i + 1.0f) / N;
  const float r  = std::sqrt(1.0f - z * z);
  const float th = 2.0f * tf::pi<float> * i / golden;
  tf::vector<float, 3> pos{2.5f * r * std::cos(th),
                           2.5f * r * std::sin(th),
                           2.5f * z};
  forms.push_back(bunny_tagged | tf::tag(
      tf::make_transformation_from_translation(pos)));
}

One canonical bunny + N frames: instancing without copying geometry.

Building the CSG graph

A CSG computation has three pieces:

  • A tf::csg_graph — the implicit arrangement of N forms, built once
  • A tf::csg::expr — a runtime boolean expression over operand ids
  • An extraction call: tf::make_csg_mesh(graph, expr)

Build the graph over the forms range:

auto graph = tf::make_csg_graph(tf::make_range(forms));

Operand ids are positions in the forms range — 0 is the sphere, 1..N are the bunnies.

Boolean expressions

Expressions are built from tf::csg::merge, tf::csg::intersection, tf::csg::difference, tf::csg::complement, and tf::csg::any_of / tf::csg::all_of for ranges. Integers auto-promote to leaves, so the algebra reads like algebra.

For N operands, csg::any_of over a sequence range encodes ⋃; csg::all_of encodes ⋂.

Union: sphere ∪ (⋃ bunnies)

auto bunnies = tf::csg::any_of(tf::make_sequence_range(1, N + 1));

auto union_mesh = tf::make_csg_mesh(graph, tf::csg::merge(0, bunnies));

Difference: sphere ∖ (⋃ bunnies)

auto diff_mesh = tf::make_csg_mesh(graph, tf::csg::difference(0, bunnies));

Intersection: sphere ∩ (⋃ bunnies)

auto inter_mesh = tf::make_csg_mesh(graph, tf::csg::intersection(0, bunnies));

Many expressions, one graph

The arrangement is the cost. Once graph is built, every additional expression runs without re-arranging:

auto union_mesh = tf::make_csg_mesh(graph, tf::csg::merge(0, bunnies));
auto diff_mesh  = tf::make_csg_mesh(graph, tf::csg::difference(0, bunnies));
auto inter_mesh = tf::make_csg_mesh(graph, tf::csg::intersection(0, bunnies));

N-ary boolean trees that would otherwise chain pairwise extractions evaluate in one pass.

Higher precision

The lattice resolution resolves automatically from input scalar (floatint32, doubleint64). Override for meshes spanning very large coordinate ranges:

auto graph = tf::make_csg_graph<tf::exact::int64>(tf::make_range(forms));

Output coordinate type is independent:

auto out = tf::make_csg_mesh<double>(graph, expr);

The arrangement carries exact intersection points regardless of the output type. Materialisation to IEEE float crosses the precision boundary exactly once.

Performance

Lévy Fibonacci-bunny benchmark. Stanford bunnies arranged Fibonacci-style on a sphere (R = 2.5), oriented +Z radially outward; sphere ∖ (⋃ bunnies) (difference) and sphere ∪ (⋃ bunnies) (union).

Comparison

LibraryOpN = 50N = 100N = 200trueformdiff194 ms (1×)347 ms (1×)673 ms (1×)trueformunion194 ms (1×)351 ms (1×)682 ms (1×)Solideandiff3,956 ms (20.4×)8,024 ms (23.1×)16,395 ms (24.4×)Solideanunion3,755 ms (19.4×)7,896 ms (22.5×)16,524 ms (24.2×)MeshLibdiff4,486 ms (23.1×)20,318 ms (58.6×)84,639 ms (125.8×)MeshLibunion6,957 ms (35.9×)26,517 ms (75.5×)105,102 ms (154.1×)

Output meshes are closed and 2-manifold on every cell; volumes agree to within ~0.05% across libraries.

Breakdown

The arrangement is the cost. Each extraction is ~10% of the build.

NArrangement+ Difference+ UnionTotal (both)50176 ms18 ms18 ms212 ms100316 ms31 ms35 ms382 ms200613 ms60 ms69 ms742 ms

From one graph, both booleans cost the arrangement plus small extraction overhead. Additional expressions on the same N meshes pay only the extraction cost. Three expressions on 200 meshes run in ~810 ms; rebuilding the arrangement three times would cost over 2,000 ms.

Full benchmarks and methodology · How the algorithm works · The STL for Geometry

Cite as
@article{polydera:fast-variadic-mesh-booleans-in-cpp,
  title={Fast Variadic Mesh Booleans in C++},
  author={Sajovic, {\v{Z}}iga, Polydera},
  year={2026},
  url={https://polydera.com/tutorials/fast-variadic-mesh-booleans-in-cpp},
  organization={Polydera}
}