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.