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.
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.
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).

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 (float → int32, double → int64). 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
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.
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
@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}
}