Three.js + trueform: Real-time mesh booleans, registration, slicer, and more

We are building idiomatic TypeScript bindings for trueform, a production-tested C++ geometry processing library, to bring real-time CSG and mesh processing to Three.js users as a proper npm package.

As a proof of concept we prepared 8 interactive examples by hand-rolling Three.js to trueform interop via Emscripten. Every example runs on meshes up to 500K polygons, with frame-to-frame updates as you drag, scroll, or hover. The algorithms are robust to non-manifold geometry, inconsistent winding, and the kind of mesh artifacts you get from real-world pipelines.

You can try them directly in the browser, on desktop and mobile.

Try them here: trueform.polydera.com/live-examples/boolean

Each example lets you choose mesh complexity (50K, 125K, 250K, 500K polygons) so you can see how the algorithms scale. Meshes contain non-manifold flaps. Rendering is Three.js, geometry is trueform via Emscripten.

The examples

  • Mesh Booleans - Drag a mesh around. Union/difference/intersection recomputes every frame. Shows average time per operation.
  • Mesh Registration (ICP) - Move and rotate a mesh out of alignment, click Align, watch it snap back in milliseconds.
  • Slicing / Isobands - Scroll to move cutting planes through the geometry. Bands recompute as you scroll.
  • Cross-Section - Scroll to sweep a cutting plane. Contour extraction updates every frame.
  • Shape Index - Hover (or touch on mobile) over the surface. A histogram of local curvature updates based on a configurable radius.
  • Free-Form Smoothing - Click and drag on the mesh to smooth vertices. The spatial index updates incrementally so it stays interactive.
  • Collision Detection - 25 mesh instances in a grid. Drag one and contact detection runs every frame.
  • Closest Points - Drag either mesh. Closest point pair recomputes as you move.

The source code for all examples, the Dockerfile, and the GitHub Actions workflow that compiles WASM and builds the examples page are on GitHub if you want to see how it fits together.

What we are working on

The TypeScript package will expose these algorithms as typed functions. We are designing the API now and have some real questions for people who build with Three.js.

Variable N-gon meshes or triangles only? The C++ library supports dynamic polygon meshes where each face can have a different number of vertices (quads, pentagons, mixed). This involves an offset array, and an ids array. Is this something you would use, or are triangle meshes enough?

Async or sync by default? We will offer both. Should boolean_union(a, b) block and return the result, or return a Promise?

WASM module size. What size would you consider acceptable for a compiled WASM geometry module?

Happy to answer questions about the WASM build, the Three.js integration, or the algorithms.

Links

6 Likes

This looks really nice and fast.

Played with the live examples, but did not check the documentation - maybe it contains answers of some of the questions.

Here are a few questions:

  • Do you plan direct support for JavaScript? I have no idea how many people use Three.js via JavaScript and how many via TypeScript, but the JavaScript-only community is not small.
  • How do the CSG operations behave on nearly coincident planes? Many CSG libraries break and give bad results (Blender’s original CSG fails too)
  • Do you support different materials? A typical example would be to cut the dragon in half and color the cross-section into different color.
  • What do you do with additional vertex properties after boolean operations? For example, if a geometry of a mesh has UV coordinates, how do you recalculate them? Or maybe you extract them from the original objects?
  • How precise should be the collision demo? The way it is now sometimes a dragon could be moved completely over another dragon without any collision to be detected.
  • Does your library support operations on compound meshes - i.e. meshes made of other meshes? Many CSG libraries operate on geometries only, so users have to traverse and process the hierarchy of the mesh by themselves.
  • Have you thought of moving the calculations in the GPU instead of WASM? The GPU is supposed to proved much higher parallelism, but the porting the code might be painful and not that straightforward.
3 Likes

Great questions. Most of these are covered in the documentation, which is written as a tutorial, but happy to answer directly here.


JavaScript support

The npm package will be plain JavaScript with TypeScript type definitions (.d.ts) shipped alongside. Emscripten produces a standard ES6 module, so no TypeScript required. TypeScript users get full type safety and autocompletion, JavaScript users import the same module and it just works.


CSG on nearly coincident planes and Robustness

We fully support coplanar polygons in our queries. For example, boolean operations and intersection operations handle coplanar faces correctly. We have tests where we subtract the same mesh 10 times from a mesh and check for correctness each time: R_i = boolean_difference(R_{i-1}, Mesh)

More broadly, the library is made to handle real-world data. Not only coplanar faces, but also meshes with non-manifold flaps, inconsistent winding, and accumulated pipeline artifacts. If you are interested in the theoretical side, check our Research page.


Different materials / vertex properties (UVs)

All cut operations (booleans, isobands, embedded isocurves, embedded intersection curves) return a labels buffer — one integer per face indicating its classification; the mesh it originated from.

For vertex attributes like UVs: we support tf::index_map, which tell you which indices map where. All operations that permute or subsample the mesh in any way (booleans, clean, slicing, etc.) support returning these index maps. New vertices created along intersection curves sit on known original edges, so you interpolate from the two endpoints. This will be supported in the TypeScript API.

For example, the examples for our python bindings already use these. You may try those, following these instructions.


Collision detection precision

The collision demo uses tf::intersects, the same spatial query that the boolean operations and all other algorithms use internally. These are exact narrow-phase triangle-to-triangle tests, accelerated by an AABB tree. The library is production-tested with an extensive test suite (around 6K tests in total on GitHub). Collisions should be narrow-phase correct.

In our testing of examples, what you describe does not happen and should not happen. Two possibilities: either it appears that the dragon is penetrating from the viewing angle but geometrically it is not, or there is a bug on the rendering side of our demo. The collision code itself is the same we use in production.


Compound meshes

What you probably mean is meshes composed of multiple connected components: for example a single mesh for a spine containing multiple vertebrae. This is fully supported in all algorithms. The topology of a mesh if defined by a flat array of faces. It can contain multiple connected components.


GPU (WebGPU) instead of WASM

Currently we are a CPU library. The core algorithms (intersection graph reduction, topology classification, face splitting) have data-dependent branching and irregular memory access patterns that do not map well to GPU parallelism. We may offer GPU support in the future.

1 Like

Thanks for the detailed answers.

By ‘compound meshes’ I mean containers of meshes, possibly nested, like THREE.Group that contain other meshes as children, and they may further contain subchildren. And each child may have own position, scale and rotation in the local space of its parent. Another ‘compound meshes’ are THREE.InstancedMesh and THREE.BatchMesh.

Collision works fine and is precise. Most likely, as you say, it was just a matter of viewing the dragons from a specific view point. I should have rotated the scene and looked at it from different view points. My bad.

I’m specifically interested in collisions and self-collisions, because I want to improve Disfigure to allow a natural posture when a figure carries, holds or touches an object (including the same figure). For example, a hand holding an apple, or a hand placed on head - I want to make the fingers wrap naturally around the touch surface. I see you support self-collisions, so your library is one of the options to consider for Disfigure.

2 Likes

I see, I misunderstood the ‘compound mesh’ phrase. Instances that only differ in the transformation can share the spatial structure, since it is built in local coordinates and the transformation is applied during queries. The spatial structures work over arbitrary primitives, and a mesh is also a primitive, so you can build a tree over a collection of meshes and query it the same way. Checking whether a hand intersects a forearm or a head is a regular intersection query over such a hierarchy.

For self-intersections within a single mesh (e.g. a deformed hand intersecting itself), we support that too. Not only detection, but also things like tf::make_self_intersection_curves, and tf::embedded_self_intersection_curves which embeds those curves into the mesh topology.

Another thing that might interest you for your use case: tf::mod_tree supports incremental updates, so when only part of the geometry changes (e.g. fingers moving), you do not have to rebuild the whole spatial tree. This is what our free-form smoothing example uses: we do point picking with our own ray casting on the deforming geometry, and the tree updates each frame. See the incremental update benchmarks.

Similarly to how our C++ library has a dedicated module for direct integration with VTK, the TypeScript bindings will have one for Three.js. Questions like yours help shape what this module should look like.

1 Like