CSG Engine
How MeshCraft integrates Manifold v3 for boolean mesh operations, caches results, and handles failure cases.
Library: Manifold v3
MeshCraft uses Manifold v3 for Constructive Solid Geometry evaluation. Manifold is a C++17 library that performs robust boolean mesh operations on watertight polygon meshes. It is included via CMake FetchContent at configure time.
FetchContent_Declare(manifold
GIT_REPOSITORY https://github.com/elalish/manifold.git
GIT_TAG v3.x.x
)
FetchContent_MakeAvailable(manifold)
CSG Evaluation Pipeline
SceneRenderer::resolveCsgMesh(Mc3Object* csgNode)
│
├─ 1. check csgCache_[csgNode]
│ hit → return cached RenderMesh
│
└─ 2. cache miss → evaluate:
│
├─ a. collect children:
│ base children → list of Manifold meshes
│ role="cutter" → list of cutter meshes
│
├─ b. tessellate each child primitive into Manifold::Mesh
│ (same geometry as rendering, converted to Manifold format)
│
├─ c. apply child transforms → build per-child Manifold object
│
├─ d. execute boolean:
│ Union → Manifold::BatchBoolean(meshes, OpType::Add)
│ Difference → base.Boolean(cutters, OpType::Subtract)
│ Intersection → Manifold::BatchBoolean(meshes, OpType::Intersect)
│
├─ e. extract result triangles → upload to GPU as RenderMesh
│
└─ f. store in csgCache_[csgNode]
Child Roles
Children of a CSG node can optionally carry a role attribute:
| role value | Behaviour in Difference | Behaviour in Union / Intersection |
|---|---|---|
| (absent) | Base solid (kept minus cutters) | Participates in operation |
cutter | Subtracted from all base solids | Ignored (no effect) |
Mesh Cache Lifecycle
The CSG cache is an std::unordered_map<Mc3Object*, manifold::Manifold> inside SceneRenderer. Cache entries are:
- Created lazily on first render of a CSG node.
- Invalidated globally when
MeshCraftApplication::pushUndo()is called. BecausepushUndodeep-copies theMc3Document, allMc3Object*pointers change — the old cache entries are stale and must be evicted. The renderer callsclearCaches()immediately after each undo push. - Not invalidated during animation playback — animation applies transform overrides that do not change geometry, so the cached CSG result remains correct.
CSG evaluation can be expensive for complex meshes (high segment counts). Every edit in a scene with many CSG nodes triggers clearCaches() + re-evaluation on the next frame. If the editor feels slow after edits, CSG complexity is the most likely cause.
Fallback Rendering
If Manifold returns an empty mesh (e.g., non-overlapping operands, degenerate input), resolveCsgMesh() falls back to rendering each child individually as if the parent were a plain group. The fallback is transparent to the user except that no boolean operation is visible.
Conditions that trigger the fallback:
- Manifold result has zero triangles (operands do not overlap for Intersection / Difference).
- Input mesh is non-watertight (open surface, e.g., a Plane with no thickness).
- Numerical precision issues with very thin or coplanar geometry.
Nested CSG
CSG nodes can be nested arbitrarily. The renderer resolves them bottom-up during the DFS traversal: leaf CSG nodes are evaluated first, their results are used as Manifold mesh inputs to parent CSG nodes.
CSG and mc3togltf
The mc3togltf exporter does not link against Manifold and does not resolve CSG geometry. It exports CSG child objects as individual glTF meshes. Implementing CSG resolution in the exporter is a planned future improvement.
Primitives → Manifold Conversion
Before passing geometry to Manifold, the renderer converts MeshCraft primitives to Manifold's mesh format. This involves tessellating the primitive into a triangle soup and applying the object's transform matrix to all vertices. Manifold requires:
- Consistent winding order (CCW, same as OpenGL convention).
- Watertight mesh (no boundary edges).
- No degenerate triangles (zero-area faces).
Planes are inherently not watertight (they have no volume) and cannot be used as CSG operands. The renderer skips plane children of CSG nodes with a warning.