Optimize Three.js Raycasting with Bounding Spheres

Raycasting in Three.js is a powerful tool for user interaction and collision detection, but it can quickly degrade application performance when testing against complex 3D meshes with thousands of polygons. This article explains how to optimize this process by ensuring Three.js performs a fast mathematical check against a simple bounding sphere before executing expensive face-by-face geometry intersection tests.

The Cost of Detailed Raycasting

By default, when you use Raycaster.intersectObject() on a mesh, Three.js needs to determine exactly which triangle face the ray intersects. For high-poly models, this requires iterating through thousands of individual faces, transforming their vertices, and performing ray-triangle intersection math on each one. Doing this on every mouse movement frame rate will quickly drop your application’s FPS.

The Bounding Sphere Broad-Phase Check

A bounding sphere is the smallest possible sphere that completely encloses a 3D object. Testing whether a ray intersects a sphere is computationally trivial compared to testing thousands of triangles.

By utilizing a bounding sphere as a “broad-phase” check, Three.js can instantly discard any objects that the ray doesn’t even point toward. If the ray misses the bounding sphere, Three.js immediately skips the face-level (“narrow-phase”) check for that object, saving massive amounts of CPU cycles.

Implementing the Optimization in Three.js

Three.js has built-in support for bounding sphere checks during raycasting, but it relies on the bounding sphere being pre-calculated. If the bounding sphere of a geometry is null, Three.js may either skip this optimization or compute it on the fly, which causes a performance hiccup.

Step 1: Pre-compute the Bounding Sphere

You should compute the bounding sphere once during the initialization of your geometry, right after loading or creating the mesh:

// Compute the bounding sphere for your geometry
geometry.computeBoundingSphere();

Once computed, Three.js stores this sphere in geometry.boundingSphere.

Step 2: Standard Raycaster Usage

When you call the raycaster, Three.js automatically checks this bounding sphere first behind the scenes:

const raycaster = new THREE.Raycaster();
const intersects = raycaster.intersectObjects(myObjectsArray, true);

During this call, the internal Mesh.prototype.raycast method performs the following logic: 1. It checks if geometry.boundingSphere exists. If not, it computes it. 2. It tests the ray against this sphere. 3. If the ray misses the sphere, the function returns early, completely bypassing the face-by-face intersection loop.

Manual Broad-Phase Filtering for Ultra-High Performance

If you are dealing with thousands of separate meshes, even passing them all into raycaster.intersectObjects() can cause overhead. You can manually perform a bounding sphere check to filter your object array before passing it to the raycaster.

const ray = raycaster.ray;
const candidates = [];

for (let i = 0; i < myObjectsArray.length; i++) {
    const mesh = myObjectsArray[i];
    const geometry = mesh.geometry;

    if (!geometry.boundingSphere) {
        geometry.computeBoundingSphere();
    }

    // Transform the bounding sphere to world space
    const sphere = geometry.boundingSphere.clone().applyMatrix4(mesh.matrixWorld);

    // Check if the ray intersects the world-space bounding sphere
    if (ray.intersectsSphere(sphere)) {
        candidates.push(mesh);
    }
}

// Only raycast against meshes that passed the bounding sphere test
const intersects = raycaster.intersectObjects(candidates, true);

By filtering your objects manually, you ensure that the complex raycasting pipeline is only ever initiated for meshes that are guaranteed to be in the direct path of the ray.