Tutorials
October 31, 2026
javascripttypescriptcollisiontutorialwebassembly

Fast and Robust Mesh Collision Detection in JavaScript

Test whether two triangle meshes collide in JavaScript and TypeScript, measure their clearance, find the closest pair, and move meshes without rebuilding anything. WebAssembly, runs in Node.js and the browser.

Žiga Sajovic, Polydera

Also available in Python and C++.

Collision detection asks three questions about two meshes. Do they touch? If not, how far apart are they? Where is the contact? trueform answers all three on triangle meshes, with each mesh in its own pose. One npm install. It runs as WebAssembly in Node.js and the browser, with TypeScript types. This tutorial covers the test, the clearance, the closest pair, the contact curves, and moving a mesh without rebuilding anything.

trueform GitHub Documentation Try it live

Install

npm install @polydera/trueform

The WebAssembly binary loads on first use. In the browser, multithreading needs SharedArrayBuffer headers. See the installation guide.

Loading the meshes

import * as tf from "@polydera/trueform";
import { readFileSync } from "node:fs";

const fixture = tf.readStl(readFileSync("fixture.stl"));
const part = tf.readObj(readFileSync("part.obj"));

In the browser, pass the bytes from fetch instead. To follow along without files, use a subdivided box as the fixed mesh and a small sphere as the moving one:

const fixed = tf.boxMesh(2.0, 2.0, 2.0, 10, 10, 10);
const moving = tf.sphereMesh(0.5, 30, 30);

The box spans −1 to 1 on each axis. The sphere has radius 0.5.

Moving a mesh

Each Mesh can carry a 4×4 transformation. Every query applies it on the fly. The points are not rewritten and the spatial tree stays valid.

moving.transformation = tf.makeTranslation(3.0, 0, 0);
moving.transformation = tf.makeRotation(45, "z");

Matrices are row-major. A Three.js Matrix4 is column-major, so transpose it:

moving.transformation = tf.ndarray(object.matrixWorld.elements, [4, 4]).T;

Do the meshes collide?

tf.intersects returns a boolean:

moving.transformation = tf.makeTranslation(3.0, 0, 0);
tf.intersects(fixed, moving);  // false

moving.transformation = tf.makeTranslation(1.3, 0, 0);
tf.intersects(fixed, moving);  // true

Touching counts. With the sphere at x = 1.5, its surface just meets the box face at x = 1, and the answer is true.

The test is on surfaces. A sphere entirely inside the box does not cross any box face, so tf.intersects returns false, and tf.distance returns the gap between the two surfaces, 0.5 here.

How far apart are they?

For clearance, use tf.distance. It returns 0 when the meshes touch.

moving.transformation = tf.makeTranslation(1.55, 0, 0);
tf.distance(fixed, moving);   // 0.05
tf.distance2(fixed, moving);  // 0.0025, squared

To get the closest pair itself, call tf.neighborSearch with two meshes:

const { elementId0, elementId1, point0, point1, distance2 } =
  tf.neighborSearch(fixed, moving);
// elementId0, elementId1: closest face on each mesh
// point0, point1:         closest points, in world coordinates
// distance2:              squared distance between them

The same calls take primitives such as points and segments. A common use is a capsule, a segment with a radius, as a stand-in for a tool or a robot link:

const axis = tf.segment(tf.point(1.3, -0.5, 0), tf.point(1.3, 0.5, 0));
const radius = 0.2;

const clearance = tf.distance(fixed, axis) - radius;  // 0.1
const { elementId, point } = tf.neighborSearch(fixed, axis);  // closest box face and point

Where is the contact?

When the meshes overlap, the contact is the curve where their surfaces cross. Compute it with intersection curves:

moving.transformation = tf.makeTranslation(1.3, 0, 0);
const contact = tf.intersectionCurves(fixed, moving);
contact.length;  // 1, a closed loop on the box face

The curve is exact. Use it to draw the contact, to measure it, or to decide whether a boolean is needed.

Working with results

In a motion loop, build the trees once and only change the transformation:

fixed.buildTree();
moving.buildTree();

for (let x = 3.0; x >= 0; x -= 0.5) {
  moving.transformation = tf.makeTranslation(x, 0, 0);
  const hit = tf.intersects(fixed, moving);
  const gap = tf.distance(fixed, moving);
}

To test many copies of one part in different poses, use shallowCopy(). Each copy shares the points and faces, and carries its own transformation:

const copies = [1.2, 2.2, 3.2].map((x) => {
  const view = moving.shallowCopy();
  view.transformation = tf.makeTranslation(x, 0, 0);
  return view;
});

copies.map((view) => tf.intersects(fixed, view));  // [true, false, false]

In the browser, the async variants run off the main thread and keep the frame loop free:

const hit = await tf.async.intersects(fixed, moving);
const gap = await tf.async.distance(fixed, moving);

Performance

We measured mesh–mesh queries between the dragon and a transformed copy of it, at five sizes, against three-mesh-bvh's closestPointToGeometry and intersectsGeometry. The closest pair is taken across a gap; the intersection test on a pose where the surfaces cross.

0.19msMesh–mesh closest pair1,025,766 faces each, vs 80.2 ms three-mesh-bvh
413×Faster than three-mesh-bvhClosest pair, trees built
26×Faster than three-mesh-bvhClosest pair, trees built inside the timer

Apple M4 Max 16 threads Node.js 25 · WebAssembly

Faces eachClosest pair: trueformthree-mesh-bvhIntersects: trueformthree-mesh-bvh58,2980.045 ms7.46 ms0.107 ms0.386 ms141,9660.098 ms12.8 ms0.065 ms0.504 ms244,0260.088 ms24.1 ms0.091 ms0.747 ms480,8680.184 ms61.3 ms0.046 ms0.802 ms1,025,7660.194 ms80.2 ms0.043 ms2.68 ms1,025,766, both trees built inside the timer22.2 ms573 ms23.4 ms466 ms

Both trees are built before the timer, except in the last row. Both libraries return the same answer on every input. Median of 9 runs after 2 warm-ups, each library in its own Node.js process. three-mesh-bvh runs on one JavaScript thread.

Measured 2026-10-31 with trueform 0.10.8; harness and raw results: benchmarks/tutorials/javascript-collision-detection.

Every collision query runs on a spatial tree. On a 1,025,766-polygon mesh, trueform builds it in 15.8 ms in Node.js, against 259.9 ms for three-mesh-bvh. The tree is built once. Moving a mesh by its transformation does not rebuild it.

Full benchmarks · Documentation

Cite as
@article{polydera:fast-mesh-collision-detection-in-javascript,
  title={Fast and Robust Mesh Collision Detection in JavaScript},
  author={Sajovic, {\v{Z}}iga, Polydera},
  year={2026},
  url={https://polydera.com/tutorials/fast-mesh-collision-detection-in-javascript},
  organization={Polydera}
}