Tutorials
October 31, 2026
pythoncollisiontutorial

Fast and Robust Mesh Collision Detection in Python

Test whether two triangle meshes collide, measure the clearance between them, and find which faces touch, in Python. Move meshes with a 4×4 transformation, no rebuilds. One pip install, NumPy in and out.

Žiga Sajovic, Polydera

Also available in C++ and JavaScript.

Collision detection asks three questions about two meshes. Do they touch? If not, how far apart are they? If they do, which faces touch? trueform answers all three on triangle meshes, with each mesh in its own pose. One pip install. NumPy arrays in, NumPy arrays out. This tutorial covers the test, the clearance, the contact faces, and moving a mesh without rebuilding anything.

trueform GitHub Documentation Try it live

Install

pip install trueform

See the installation guide for all options.

Loading the meshes

import trueform as tf
import numpy as np

fixture = tf.Mesh(*tf.read_stl("fixture.stl"))
part = tf.Mesh(*tf.read_obj("part.obj"))

To follow along without files, use a subdivided box as the static mesh and a small sphere as the moving one:

static = tf.Mesh(*tf.make_box_mesh(2.0, 2.0, 2.0, 10, 10, 10))
moving = tf.Mesh(*tf.make_sphere_mesh(0.5, stacks=30, segments=30))

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

Moving a mesh

Each tf.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.

def translation(x, y, z):
    T = np.eye(4, dtype=np.float32)
    T[:3, 3] = [x, y, z]
    return T

moving.transformation = translation(3.0, 0.0, 0.0)

The matrix dtype matches the mesh: float32 here. Rotations go in T[:3, :3].

Do the meshes collide?

tf.intersects returns a bool:

moving.transformation = translation(3.0, 0.0, 0.0)
tf.intersects(static, moving)  # False

moving.transformation = translation(1.3, 0.0, 0.0)
tf.intersects(static, 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. To catch containment, test one vertex of the moving mesh with a point-in-mesh test.

How far apart are they?

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

moving.transformation = translation(1.55, 0.0, 0.0)
tf.distance(static, moving)   # 0.05
tf.distance2(static, moving)  # 0.0025, squared

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

(face0, face1), (dist2, pt0, pt1) = tf.neighbor_search(static, moving)
# face0, face1: closest face on each mesh
# pt0, pt1:     closest points, in world coordinates
# dist2:        squared distance between them

For a primitive against a primitive, use tf.closest_metric_point_pair. It takes points, segments, triangles, polygons, rays, lines, planes and boxes, not meshes. A common use is a capsule: a segment with a radius, as a stand-in for a tool or a robot link.

axis = tf.Segment(np.array([[1.3, -0.5, 0.0], [1.3, 0.5, 0.0]], dtype=np.float32))
radius = 0.2

# closest box face to the capsule axis
face_id, d2, pt = tf.neighbor_search(static, axis)
clearance = np.sqrt(d2) - radius   # 0.1

# closest points between the axis and that face
tri = tf.Triangle(static.points[static.faces[face_id]])
d2, on_axis, on_face = tf.closest_metric_point_pair(axis, tri)

Both operands must share a dtype. A float64 segment against a float32 mesh raises a TypeError.

Which faces touch?

tf.gather_intersecting_ids returns every pair of intersecting faces, as an (M, 2) array:

moving.transformation = translation(1.3, 0.0, 0.0)

pairs = tf.gather_intersecting_ids(static, moving)
pairs.shape              # (120, 2)
np.unique(pairs[:, 0])   # faces of the box in contact
np.unique(pairs[:, 1])   # faces of the sphere in contact

Column 0 indexes faces of the first mesh, column 1 faces of the second. Use them to color the contact, to transfer attributes, or to restrict a finer check.

For near misses, gather the pairs within a distance:

moving.transformation = translation(1.55, 0.0, 0.0)

near = tf.gather_ids_within_distance(static, moving, distance=0.1)
near.shape  # (430, 2)

Working with results

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

static.build_tree()
moving.build_tree()

for x in np.linspace(3.0, 0.0, 7):
    moving.transformation = translation(x, 0.0, 0.0)
    hit = tf.intersects(static, moving)
    gap = tf.distance(static, moving)

To test many copies of one part in different poses, use part.shared_view(). Each view shares the data and the tree, and carries its own transformation.

When you need the contact as geometry, not just face pairs, compute the intersection curves:

paths, curve_points = tf.intersection_curves(static, moving)

Performance

< 1 msMesh–mesh closest point1,025,766 polygons, vs 370 ms FCL
28.5 msSpatial tree build1,025,766 polygons, vs 302.7 ms MeshLib
3.5 msIntersection curves2×500K polygons

Apple M4 Max 16 threads

On a 1,025,766-polygon mesh, the closest point between two meshes takes under 1 ms in trueform, against 370 ms in FCL. The spatial tree on the same mesh builds in 28.5 ms from Python, against 302.7 ms in MeshLib. 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-python,
  title={Fast and Robust Mesh Collision Detection in Python},
  author={Sajovic, {\v{Z}}iga, Polydera},
  year={2026},
  url={https://polydera.com/tutorials/fast-mesh-collision-detection-in-python},
  organization={Polydera}
}