Docs/Internals/CSG Engine

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 valueBehaviour in DifferenceBehaviour in Union / Intersection
(absent)Base solid (kept minus cutters)Participates in operation
cutterSubtracted from all base solidsIgnored (no effect)

Mesh Cache Lifecycle

The CSG cache is an std::unordered_map<Mc3Object*, manifold::Manifold> inside SceneRenderer. Cache entries are:

⚠️

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:

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:

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.