Check if Point is Inside ConvexGeometry in Three.js

This article explains how to efficiently determine if a specific 3D point lies strictly inside a complex ConvexGeometry in Three.js. We will explore the mathematical approach of using face planes and provide a clean, high-performance JavaScript function to perform this check in local or world space.

The Mathematical Approach: Face Planes

Because a ConvexGeometry is convex, we can leverage a key geometric property: a point is strictly inside a convex polyhedron if and only if it lies on the interior side of every single face plane.

In Three.js, the vertices of the outer shell are wound counter-clockwise. This means we can construct a plane for each triangular face where the normal vector points outward. If the signed distance from our target 3D point to every face plane is negative, the point is strictly inside the geometry.

Accounting for Object Transformations

Before running the check, you must ensure the point and the geometry are in the same coordinate space. If your mesh has been scaled, rotated, or translated, it is highly inefficient to transform the entire geometry. Instead, transform the query point from world space to the mesh’s local space using the inverse of the mesh’s world matrix.

Code Implementation

The following function performs the check. It handles both indexed and non-indexed BufferGeometry structures and accounts for mesh transformations.

import * as THREE from 'three';

/**
 * Determines if a world-space point is strictly inside a ConvexGeometry mesh.
 * @param {THREE.Vector3} worldPoint - The point to test in world coordinates.
 * @param {THREE.Mesh} mesh - The Mesh containing the ConvexGeometry.
 * @returns {boolean} True if the point is strictly inside, false otherwise.
 */
function isPointInsideConvexMesh(worldPoint, mesh) {
    const geometry = mesh.geometry;
    
    // 1. Convert the world point to the mesh's local coordinate space
    const localPoint = worldPoint.clone().applyMatrix4(mesh.matrixWorld.invert());

    const positionAttribute = geometry.attributes.position;
    const index = geometry.index;
    
    const vertexA = new THREE.Vector3();
    const vertexB = new THREE.Vector3();
    const vertexC = new THREE.Vector3();
    const plane = new THREE.Plane();

    const faceCount = index ? index.count / 3 : positionAttribute.count / 3;

    // 2. Iterate through every triangular face of the geometry
    for (let i = 0; i < faceCount; i++) {
        if (index) {
            vertexA.fromBufferAttribute(positionAttribute, index.getX(i * 3));
            vertexB.fromBufferAttribute(positionAttribute, index.getY(i * 3));
            vertexC.fromBufferAttribute(positionAttribute, index.getZ(i * 3));
        } else {
            vertexA.fromBufferAttribute(positionAttribute, i * 3);
            vertexB.fromBufferAttribute(positionAttribute, i * 3 + 1);
            vertexC.fromBufferAttribute(positionAttribute, i * 3 + 2);
        }

        // Create a plane from the face vertices (normal points outward)
        plane.setFromCoplanarPoints(vertexA, vertexB, vertexC);

        // Calculate the signed distance from the point to the plane
        const distance = plane.distanceToPoint(localPoint);

        // If distance is >= 0, the point is on or outside this face.
        // For strictly inside, we use a small epsilon offset to avoid floating-point errors.
        const epsilon = 0.000001;
        if (distance >= -epsilon) {
            return false; 
        }
    }

    // The point is on the negative (inner) side of all planes
    return true;
}

Performance and Complexity