three-bvh-csg does boolean geometry with a BVH instead of a BSP tree
A flexible, memory compact, fast and dynamic CSG implementation on top of three-mesh-bvh
At a glance
- What is it?
- three-bvh-csg is an MIT licensed, still experimental Constructive Solid Geometry implementation for three.js that builds on three-mesh-bvh rather than BSP trees. It suits web and game developers who need runtime boolean operations on brushes, and it is not a substitute for a CAD kernel when the result has to be watertight.
- Who is it for?
- Adopt three-bvh-csg if you are building three.js tools, level editors or 3D printing previews where live boolean operations on brushes matter more than manifold guarantees, and keep a CAD kernel for anything that must slice or print.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 26 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Boolean geometry without the BSP tree
Constructive Solid Geometry means taking two solid meshes and producing a third defined by a set operation: union, difference, intersection. The classic JavaScript approach in three.js was to build a BSP tree, which works but slows down badly on complex input and consumes memory for the tree itself.
three-bvh-csg takes a different route. It sits on top of three-mesh-bvh, the bounding volume hierarchy library it is built on, and uses that acceleration structure to find the parts of each brush that actually overlap. The repository description calls it a flexible, memory compact, fast and dynamic CSG implementation on top of three-mesh-bvh, and the README states it is more than 100 times faster than other BSP based three.js CSG libraries in complex cases. That figure is the project's own claim in its README, not an independent measurement, so treat it as a direction of travel rather than a number to plan around.
The target audience is visible in the topic list: threejs, webgl, game-development, 3d-printing, geometry and graphics. In practice that means browser tools, level editors, procedural modelling and printing previews, anywhere a solid is assembled from a handful of pieces at runtime.
The honest framing comes from the README's own first line, which calls the project experimental and in progress. The version number backs that up: the published package version is 0.0.18.
The two-manifold requirement is the whole contract
The README puts a Note block before anything else, and it is the single most important thing to understand before writing a line of code. All brush geometry must be two-manifold, or water tight with no triangle interpenetration. A mesh with holes, self intersections or inconsistent winding will produce garbage output, and the library will not warn you.
That is a hard constraint on the input side, and it is not unusual for CSG libraries, but it changes who can use this. A CAD system, or an artist who has run a repair pass, hands you clean solids. A mesh downloaded from the internet or produced by a marching cubes pass may not be. The topics list includes 3d-printing, and printing is exactly the case where a non-manifold result is worthless.
A second Warning block says that due to numerical precision and corner cases the resulting geometry may not be correctly completely two-manifold. So even with perfect input, the output is not guaranteed to be watertight. The README then points readers at Manifold CAD for CAD operations with more robust numerical solutions. That is the library's own statement of where it should not be used.
The output quality problem is not hypothetical either. The roadmap lists open work on fixing triangle splitting and missing triangle issues, referenced as issues 73 and 68, plus polygon splitting and triangulation as issue 51.
Brushes, operations and the evaluator API
The core types are Brush, Operation, OperationGroup and Evaluator. Brush extends THREE.Mesh and keeps the same interface, so anything you can do with a mesh you can do with a brush, but its geometry must not be modified once created because the BVH is built from it. The README recommends removing groups from a geometry before creating a brush if multi material support is not required.
Operation extends Brush and carries an .operation property, an ADDITION style CSGOperation constant, which defines which operation is computed on the mesh when the operation is evaluated in a chain. The OperationGroup extends THREE.Group, so a list of operations can be grouped with the familiar .add method, and Operation provides .insertBefore and .insertAfter to slot brushes into a parent's child list around an operation.
Evaluator is the engine. Its .evaluate method takes two brushes and an operation, and returns a Brush or Mesh. If no target is passed, a new Brush is created with the new geometry. If a target is provided, the provided Brush is modified in place and geometry is disposed or marked for update as needed. There is also an array form of evaluate that takes arrays of operations and targets and returns an array of results, which is how you run a chain rather than a single operation.
The operation constants are worth reading closely because one of them breaks the solid model. In addition to ADDITION, SUBTRACTION, REVERSE_SUBTRACTION, DIFFERENCE and INTERSECTION, there are hollow operations, HOLLOW_SUBTRACTION and HOLLOW_INTERSECTION. The documentation says hollow operations are non-solid and result in simply removing the geometry within Brush B from brush A, and for these operations Brush A can be non-manifold while Brush B must still be a water tight, two-manifold mesh. That asymmetry is a useful escape hatch, since it lets you cut a hole in a messy mesh.
A first subtraction in ten lines
The README's use example is the shortest complete path through the library. You import the operation constants, Brush and Evaluator from the package, build two brushes from standard three.js geometries, update their world matrices, and evaluate:
import { SUBTRACTION, Brush, Evaluator } from 'three-bvh-csg';
import { MeshStandardMaterial, Mesh, SphereGeometry, BoxGeometry } from 'three';
const brush1 = new Brush( new SphereGeometry() );
brush1.updateMatrixWorld();
const brush2 = new Brush( new BoxGeometry() );
brush2.position.y = 0.5;
brush2.updateMatrixWorld();
const evaluator = new Evaluator();
const result = evaluator.evaluate( brush1, brush2, SUBTRACTION );The two updateMatrixWorld calls are not decoration. A Brush is a Mesh, and the evaluator works on world space, so a brush whose matrix has not been updated contributes stale transforms. Moving a brush after evaluation has the same problem.
The returned result is a Brush, and the example comment says to render the result. Since Brush is a Mesh subclass, you can add it to a scene like any other mesh. Note that the example imports MeshStandardMaterial and Mesh without using them, which is a small reminder that this snippet is about evaluation, not about material setup.
For a project with real assets, the practical difference is that your source geometry must be prepared before it becomes a Brush. Run it through whatever makes it watertight, drop the groups if you do not need multiple materials, and only then wrap it.
Evaluator flags: groups, materials, CDT clipping and debug output
The Evaluator exposes a handful of switches, and they are where the design trade-offs live.
useGroups defaults to true, meaning geometry groups are used while processing. If groups are used, a material array and groups are assigned to the target Brush after processing; if they are disabled, a single coherent piece of geometry with no groups is produced. That is the single most consequential flag for a web app, because groups turn one draw call into many.
Two flags soften that. consolidateGroups defaults to true and merges any group in the final geometry that shares a common material with another group, to reduce the number of draw calls required by the resulting mesh. removeUnusedMaterials defaults to true and drops materials not used in the final result from the joined material arrays. So the default path is already trying to keep the group explosion in check, but if your source brushes each carry their own material identity you can end up with many groups anyway.
The attributes property is the set of geometry attributes to transfer to the result geometry, and the default shown in the API section is position, uv and normal. Anything else you attach to the source geometry will not survive evaluation unless you add it there.
useCDTClipping defaults to false and is described as an experimental triangle clipping implementation that uses Constrained Delaunay Triangulation to triangulate clipped triangles. The documentation says it can provide a more robust, simple triangulation at the cost of performance in some cases. The build script shows where that comes from: the package bundles cdt2d as an ESM file into src/libs/cdt2d.js with esbuild before the rollup step, so the triangulation library is vendored rather than depended on at runtime.
There is also a debug property. Set debug.enabled = true before calling evaluate to collect intersection data, which is how the debug example and the coplanar example in the examples directory are meant to be used.
Running the tests, the examples and the benchmark
The package manifest defines the developer workflow. Tests run under vitest, the examples are served by vite, and there is a separate benchmark script that points at a file in the benchmark directory:
"test": "vitest run", "benchmark": "node ./benchmark/benchmark.js", "start": "vite --config ./vite.config.js",The build is two stages, which matters if you want to reproduce the shipped artefact rather than just consume the source. The build script runs the cdt2d bundling step first and then rollup, and prepublishOnly runs that same build before publishing. The published files list includes src/* and build/*, and the manifest is an ES module package whose module entry points at src/index.js while the main entry points at build/index.umd.cjs, so consumers get both a bundler friendly source form and a CommonJS fallback.
The examples directory is the best documentation in the repository beyond the README. It includes simple.js, geometry.js for complex models, hollow.js, multimaterial.js, multiop.js, hierarchy.js, coplanar.js, debug.js and triangleSplit.js, each with a matching HTML file. The names map directly onto the API surface described above, so they double as usage documentation for hollow operations, multi material output, operation chains and the hierarchy case. The hierarchy example is the one the README points to for understanding how Operation and OperationGroup fit together.
The peer dependencies are three >=0.179.0 and three-mesh-bvh >=0.9.7, which is the constraint to check before an upgrade, since this package is built directly on that BVH library's query API.
What the project is not ready for
Worker support is on the roadmap as issue 14, which means heavy evaluation currently blocks the main thread. For a browser tool that runs CSG on every edit, that is a real constraint, and it pushes you toward doing the work in a worker you write yourself, or accepting a laggy UI.
Polygon splitting and triangulation is still open as issue 51, alongside the triangle splitting and missing triangle fixes in issues 73 and 68. When the boolean result comes back with holes or missing triangles, that is not your input being wrong, it is the known limitation list.
The version scheme tells the rest of the story. The releases are v0.0.16 in January 2024, v0.0.17 in April 2025 and v0.0.18 in February 2026, which is a slow cadence for a 0.0.x package, and a pre-1.0 version carries no compatibility promise. The repository is not archived and the last push was on 2026-09-27, so work continues, but the CHANGELOG and the MIT licence in the manifest are the terms you actually get, and the MIT licence is permissive enough for commercial use without notice obligations beyond retaining the licence text.
Editorial conclusion
Adopt three-bvh-csg if you are building three.js tools, level editors or 3D printing previews where live boolean operations on brushes matter more than manifold guarantees, and keep a CAD kernel for anything that must slice or print. Before you commit, test your own source geometry against the two-manifold requirement, decide whether you need the experimental useCDTClipping path or the default triangulation, and check that your three and three-mesh-bvh versions satisfy the peer ranges. The project is labelled experimental and in progress, its version is still 0.0.18, and the last push was on 2026-09-27.
Frequently asked questions
What is three-bvh-csg and what is it built on?
It is an experimental Constructive Solid Geometry implementation for three.js built on top of the three-mesh-bvh bounding volume hierarchy library. The package version is 0.0.18 and the licence is MIT.
What geometry requirements does three-bvh-csg have for input brushes?
All brush geometry must be two-manifold, or water tight with no triangle interpenetration. The README also warns that due to numerical precision and corner cases the resulting geometry may not be correctly completely two-manifold.
How do I run a subtraction with three-bvh-csg?
Create two Brush objects from your geometries, call updateMatrixWorld on each, then call evaluator.evaluate( brush1, brush2, SUBTRACTION ). If you pass a target brush, it is modified in place, otherwise a new Brush is returned.
What is the difference between SUBTRACTION and HOLLOW_SUBTRACTION?
Hollow operations are non-solid and simply remove the geometry within Brush B from brush A. For hollow operations Brush A may be non-manifold, while Brush B must still be a water tight, two-manifold mesh.
Does three-bvh-csg support multiple materials in the result?
Yes, through geometry groups. useGroups defaults to true and assigns a material array and groups to the target Brush, and consolidateGroups and removeUnusedMaterials are enabled by default to limit the resulting draw call count.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/gkjohnson-three-bvh-csg)Was this useful?