iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Raycaster (picking)

A Raycaster casts a ray through the scene and reports which objects it hits. Use for picking with mouse / touch, hit-tests in VR, line-of-sight checks, drag-and-drop.

Mouse picking, intersection, performance

EXAMPLE
import * as THREE from 'three';

// 1) Setup
const raycaster = new THREE.Raycaster();
const pointer   = new THREE.Vector2();
const camera    = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 1000);
const scene     = new THREE.Scene();

// 2) Mouse picking
function onPointerMove(event) {
    // Convert pixel coords to normalised device coords [-1, 1]
    pointer.x = (event.clientX / window.innerWidth)  * 2 - 1;
    pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
window.addEventListener('pointermove', onPointerMove);

function onPointerDown(event) {
    onPointerMove(event);
    raycaster.setFromCamera(pointer, camera);

    const intersects = raycaster.intersectObjects(scene.children, /* recursive */ true);
    if (intersects.length > 0) {
        const hit = intersects[0];
        console.log('clicked:', hit.object.name);
        console.log('point:',   hit.point);                  // world position
        console.log('distance:', hit.distance);              // from camera
        console.log('face:',    hit.face);                   // hit triangle
        console.log('uv:',      hit.uv);                     // texture coord
        hit.object.material.color.setHex(0xff0000);
    }
}
window.addEventListener('pointerdown', onPointerDown);

// 3) Hover effects — highlight on mouseover
let hovered = null;

function tick() {
    raycaster.setFromCamera(pointer, camera);
    const hits = raycaster.intersectObjects(meshes, false);

    if (hits.length > 0) {
        if (hovered !== hits[0].object) {
            if (hovered) hovered.material.emissive.setHex(0);
            hovered = hits[0].object;
            hovered.material.emissive.setHex(0x444444);
        }
    } else if (hovered) {
        hovered.material.emissive.setHex(0);
        hovered = null;
    }

    renderer.render(scene, camera);
    requestAnimationFrame(tick);
}

// 4) Ray from arbitrary origin + direction (not from camera)
const origin    = new THREE.Vector3(0, 5, 0);
const direction = new THREE.Vector3(0, -1, 0).normalize();
raycaster.set(origin, direction);

const hits = raycaster.intersectObjects(scene.children, true);
// Use for: line-of-sight (player can see enemy?), bullet trajectory, AI vision

// 5) Limit raycast distance
raycaster.near = 0;
raycaster.far  = 100;                                  // skip anything farther

// 6) Layers — raycast against a specific layer
raycaster.layers.set(1);                              // only objects on layer 1

mesh.layers.set(1);                                    // put mesh on layer 1
// raycaster will skip meshes on other layers

// 7) Pick from a specific list (much faster than scene.children)
const pickables = [];
// During scene construction:
scene.traverse((obj) => {
    if (obj.isMesh && obj.userData.pickable) pickables.push(obj);
});

const hits = raycaster.intersectObjects(pickables, false);

// 8) intersectObject (singular) — check one object
const hits = raycaster.intersectObject(specificMesh, true);

// 9) Drag and drop
let selected = null;
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);   // XZ plane at y=0
const point = new THREE.Vector3();

function onDown(e) {
    setPointer(e);
    raycaster.setFromCamera(pointer, camera);
    const hits = raycaster.intersectObjects(pickables, false);
    if (hits.length > 0) selected = hits[0].object;
}

function onMove(e) {
    if (!selected) return;
    setPointer(e);
    raycaster.setFromCamera(pointer, camera);
    raycaster.ray.intersectPlane(plane, point);            // project ray onto plane
    selected.position.copy(point);
}

function onUp() { selected = null; }

// 10) Touch support
function onTouch(event) {
    if (event.touches.length === 0) return;
    const t = event.touches[0];
    pointer.x = (t.clientX / window.innerWidth)  * 2 - 1;
    pointer.y = -(t.clientY / window.innerHeight) * 2 + 1;
}
window.addEventListener('touchstart', onTouch);
window.addEventListener('touchmove', onTouch);

// 11) Custom intersect handling — BufferGeometry hit testing
raycaster.params.Points.threshold = 0.1;               // for points
raycaster.params.Line.threshold   = 0.05;              // for lines
// Points and lines need a threshold (a ray rarely hits them exactly)

// 12) Hit position — show indicator at hit point
function tick() {
    raycaster.setFromCamera(pointer, camera);
    const hits = raycaster.intersectObjects(scene.children, true);
    if (hits.length > 0) {
        indicator.position.copy(hits[0].point);
        indicator.visible = true;
    } else {
        indicator.visible = false;
    }
    renderer.render(scene, camera);
    requestAnimationFrame(tick);
}

// 13) Visualise the ray for debugging
const arrow = new THREE.ArrowHelper(
    raycaster.ray.direction,
    raycaster.ray.origin,
    100,                                                // length
    0xff0000,                                            // color
);
scene.add(arrow);

// 14) Picking via screen position vs camera frustum
// setFromCamera uses unprojection — accurate for perspective + orthographic
raycaster.setFromCamera(pointer, camera);

// 15) Performance tips
//   • Limit candidates — pickables array instead of scene.children
//   • Skip recursion when possible (intersectObjects(arr, false))
//   • Use layers to filter
//   • Limit raycaster.far
//   • For thousands of objects: spatial partition (BVH, octree)

// BVH for large scenes — three-mesh-bvh package
// npm i three-mesh-bvh
import { computeBoundsTree, disposeBoundsTree, acceleratedRaycast } from 'three-mesh-bvh';
THREE.BufferGeometry.prototype.computeBoundsTree   = computeBoundsTree;
THREE.BufferGeometry.prototype.disposeBoundsTree   = disposeBoundsTree;
THREE.Mesh.prototype.raycast                       = acceleratedRaycast;

mesh.geometry.computeBoundsTree();                     // pre-compute on geometry
// Now raycasts against this mesh use BVH — orders of magnitude faster on big meshes

// 16) Common patterns

// Click to add an object
function onClick(e) {
    setPointer(e);
    raycaster.setFromCamera(pointer, camera);
    const hits = raycaster.intersectObject(floor);
    if (hits.length > 0) {
        const cube = new THREE.Mesh(
            new THREE.BoxGeometry(0.5, 0.5, 0.5),
            new THREE.MeshStandardMaterial({ color: 0x0ea5e9 }),
        );
        cube.position.copy(hits[0].point);
        cube.position.y += 0.25;                            // sit on the floor
        scene.add(cube);
    }
}

// Click to delete
function onClick(e) {
    setPointer(e);
    raycaster.setFromCamera(pointer, camera);
    const hits = raycaster.intersectObjects(scene.children, true);
    if (hits.length > 0 && hits[0].object.userData.deletable) {
        scene.remove(hits[0].object);
        hits[0].object.geometry.dispose();
        hits[0].object.material.dispose();
    }
}

// 17) Common bugs
//   • Forgetting to convert mouse to NDC [-1, 1]
//   • Y flipped (Y goes UP in NDC, DOWN in screen) → multiply by -1
//   • Setting raycaster.layers without setting mesh.layers → no hits
//   • Recursive vs non-recursive — Group children won't be tested if recursive=false
//   • Hitting transparent meshes you didn't intend to — set renderOrder + set raycast to undefined to skip

// 18) Best practices
//   ✅ Maintain a 'pickables' array; don't traverse the whole scene per frame
//   ✅ Use layers for fast filtering
//   ✅ BVH for large meshes / high-poly scenes
//   ✅ Set far/near to limit ray range
//   ✅ Handle pointermove (not mousemove) — works on touch + pen
//   ✅ Apply NDC conversion correctly (X * 2 - 1, Y inverted)

Why it matters

Raycaster + a curated pickables array covers all interactive 3D needs — click to select, hover effects, drag-and-drop, line-of-sight. For huge scenes, switch to three-mesh-bvh; BVH lookup is orders of magnitude faster than naive triangle iteration.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
const ray = new THREE.Raycaster();
ray.setFromCamera(mouseNDC, camera);
const hits = ray.intersectObjects(scene.children);
Try it Yourself »

Exercise

Picking is done with…

const r = new THREE. ();

Discussion

Loading…