Three.js Raycaster Navmesh Pathfinding

This article explains how to use the Three.js Raycaster to detect intersections against an invisible navigation mesh (navmesh) for character pathfinding and movement. You will learn how to set up the invisible navmesh, configure a downward-pointing raycaster from the character’s position, and use the intersection data to constrain movement within walkable boundaries and adjust the character’s height dynamically.

Setting Up the Invisible Navmesh

A navigation mesh is a simplified 3D geometry that defines the walkable surfaces of your 3D environment. To use it for collision and pathfinding without rendering it to the user, you must make it invisible while keeping it active in the physics and raycasting calculations.

In Three.js, setting mesh.visible = false will prevent the Raycaster from detecting it. To bypass this, keep the mesh’s visibility active but make its material invisible by setting the material’s visible property to false, or by disabling color writing.

// Load or create your navmesh geometry
const navMeshGeometry = new THREE.BufferGeometry(); // Replace with your loaded geometry
const navMeshMaterial = new THREE.MeshBasicMaterial({
    color: 0x00ff00,
    visible: false // Invisible to the camera, but still detectable by Raycaster
});

const navMesh = new THREE.Mesh(navMeshGeometry, navMeshMaterial);
scene.add(navMesh);

Alternatively, you can use colorWrite: false on the material so that the mesh is rendered to the depth buffer but does not write any colors to the screen.

Configuring the Raycaster for Movement

To keep your character glued to the walkable surface of the navmesh, you need to project a vertical ray downward from the character’s position. This ray will detect the exact height (Y-coordinate) of the navmesh at any given X and Z coordinate.

Initialize a Raycaster and a Vector3 to manage the ray’s direction and origin:

const raycaster = new THREE.Raycaster();
const downDirection = new THREE.Vector3(0, -1, 0);
const rayLength = 10; // Adjust based on your scene's scale and character height

Detecting Intersections and Updating Position

During your game loop or physics update step, calculate the character’s potential next position. Cast the ray slightly above the character’s feet pointing straight down to find the intersection point on the navmesh.

function updateCharacterPosition(character, movementVector) {
    // Calculate the proposed new horizontal position
    const nextX = character.position.x + movementVector.x;
    const nextZ = character.position.z + movementVector.z;

    // Set the raycast origin slightly above the proposed position to handle slopes
    const raycastOffset = 2; 
    const rayOrigin = new THREE.Vector3(nextX, character.position.y + raycastOffset, nextZ);

    raycaster.set(rayOrigin, downDirection);
    raycaster.far = rayLength;

    // Intersect against the invisible navmesh
    const intersections = raycaster.intersectObject(navMesh);

    if (intersections.length > 0) {
        const intersectPoint = intersections[0].point;

        // The proposed position is on the navmesh. Update character position.
        character.position.x = nextX;
        character.position.z = nextZ;
        character.position.y = intersectPoint.y; // Snap character feet to the navmesh height
    } else {
        // No intersection means the proposed move goes off the walkable navmesh.
        // Block movement or slide along the navmesh boundary.
    }
}

Performance Optimization

Raycasting on every frame can become CPU-intensive if your navmesh has a high polygon count. To maintain performance: