Three.js Cheatsheet

Raycasting

Use this Three.js reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Raycaster Basics

const raycaster = new THREE.Raycaster();
const pointer   = new THREE.Vector2(); // NDC coordinates (-1 to +1)

// Convert mouse position to NDC
window.addEventListener('mousemove', (event) => {
  pointer.x = (event.clientX / window.innerWidth)  * 2 - 1;
  pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
});

// Cast ray from camera through pointer
raycaster.setFromCamera(pointer, camera);

// Test against a list of objects
const intersects = raycaster.intersectObjects(scene.children, true); // true = recursive

if (intersects.length > 0) {
  const hit = intersects[0]; // closest intersection
  console.log(hit.object);   // the intersected Mesh
  console.log(hit.point);    // THREE.Vector3 — world position of hit
  console.log(hit.distance); // distance from ray origin
  console.log(hit.face);     // face data (normal, indices)
  console.log(hit.faceIndex);
  console.log(hit.uv);       // UV at intersection point
}

Intersection Object Properties

PropertyTypeDescription
intersect.objectObject3DThe intersected object
intersect.pointVector3World-space hit position
intersect.distancenumberDistance from ray origin
intersect.faceFace{ a, b, c, normal, materialIndex }
intersect.faceIndexnumberIndex of the face in geometry
intersect.uvVector2UV coordinates at hit point
intersect.uv2Vector2Second UV at hit point
intersect.instanceIdnumberIndex for InstancedMesh hits
intersect.barycoordVector3Barycentric coordinates on the face

Click / Pick Events Pattern

const raycaster = new THREE.Raycaster();
const pointer   = new THREE.Vector2();
let hoveredObj  = null;

function onMouseMove(event) {
  pointer.x = (event.clientX / window.innerWidth)  * 2 - 1;
  pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;

  raycaster.setFromCamera(pointer, camera);
  const hits = raycaster.intersectObjects(clickables, false);

  const newHovered = hits.length > 0 ? hits[0].object : null;

  if (newHovered !== hoveredObj) {
    if (hoveredObj) hoveredObj.material.color.set(0xffffff); // unhover
    if (newHovered) newHovered.material.color.set(0xffaa00); // hover
    hoveredObj = newHovered;
    document.body.style.cursor = newHovered ? 'pointer' : 'default';
  }
}

function onClick(event) {
  pointer.x = (event.clientX / window.innerWidth)  * 2 - 1;
  pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;

  raycaster.setFromCamera(pointer, camera);
  const hits = raycaster.intersectObjects(clickables, false);

  if (hits.length > 0) {
    const obj = hits[0].object;
    console.log('Clicked:', obj.name);
  }
}

window.addEventListener('mousemove', onMouseMove);
window.addEventListener('click',     onClick);

Raycast Against Specific Objects

// Only check specific meshes (faster, avoid full scene traversal)
const clickables = [meshA, meshB, meshC];
const hits = raycaster.intersectObjects(clickables, false);

// Recurse into Groups / GLTF models
const hits = raycaster.intersectObjects([group], true);

// Single object
const hits = raycaster.intersectObject(mesh, true);

Set Ray Manually (non-camera ray)

// Ray from an arbitrary origin in a direction
raycaster.set(
  new THREE.Vector3(0, 10, 0),   // origin
  new THREE.Vector3(0, -1, 0)    // direction (must be normalized)
);

const hits = raycaster.intersectObjects(scene.children, true);

Raycaster Properties

raycaster.near          = 0;       // ignore hits closer than this
raycaster.far           = Infinity; // ignore hits farther than this
raycaster.ray.origin;               // THREE.Ray — the origin Vector3
raycaster.ray.direction;            // normalized direction Vector3

// Line/Points precision
raycaster.params.Line.threshold   = 0.1; // pick distance for Lines
raycaster.params.Points.threshold = 0.1; // pick distance for Points

Raycasting with InstancedMesh

const iMesh = new THREE.InstancedMesh(geo, mat, 1000);
// ... set instance matrices

const hits = raycaster.intersectObject(iMesh);
if (hits.length > 0) {
  const instanceId = hits[0].instanceId;
  console.log('Hit instance:', instanceId);

  // Change color of hit instance
  iMesh.setColorAt(instanceId, new THREE.Color(0xff0000));
  iMesh.instanceColor.needsUpdate = true;

  // Get world matrix of that instance
  const matrix = new THREE.Matrix4();
  iMesh.getMatrixAt(instanceId, matrix);
}

Ground Plane Intersection (Common Pattern)

// Cast ray onto an invisible ground plane
const groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); // normal up, d=0
const hitPoint    = new THREE.Vector3();

raycaster.setFromCamera(pointer, camera);
raycaster.ray.intersectPlane(groundPlane, hitPoint);

if (hitPoint) {
  console.log('Ground hit at', hitPoint);
  marker.position.copy(hitPoint);
}

Ray / Geometry Queries (without a scene)

const ray = new THREE.Ray(origin, direction);

// Intersect primitives
const sphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), 1);
const boxObj = new THREE.Box3(new THREE.Vector3(-1,-1,-1), new THREE.Vector3(1,1,1));
const plane  = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);

const sphereHit = new THREE.Vector3();
ray.intersectSphere(sphere, sphereHit);

const boxHit = new THREE.Vector3();
ray.intersectBox(boxObj, boxHit);

const planeHit = new THREE.Vector3();
ray.intersectPlane(plane, planeHit);

// Signed distance from point to plane
plane.distanceToPoint(point);

BVH Acceleration (three-mesh-bvh)

For large, complex meshes, raycasting can be slow. Use three-mesh-bvh for orders-of-magnitude speedup.

import * as THREE from 'three';
import { MeshBVH, acceleratedRaycast } from 'three-mesh-bvh';

// Patch Three.js prototype once
THREE.Mesh.prototype.raycast = acceleratedRaycast;

// Build the BVH (once, after geometry is set)
geometry.boundsTree = new MeshBVH(geometry);

// Now raycaster uses the BVH automatically
const hits = raycaster.intersectObject(mesh);

Gotchas

raycaster.intersectObjects(arr, false) does not recurse into Group children. Pass true for recursion into groups and GLTF models.

Return value is always an array sorted by ascending distance — intersects[0] is the closest hit.

Raycasting is done against geometry, not visible pixels — transparent materials and occluded faces still count unless you filter by object.visible.

Set raycaster.near and raycaster.far to match your camera's clipping planes to avoid hits outside the view frustum.

For InstancedMesh, intersect.instanceId identifies which instance was hit — not the object.id.

The uv property in an intersection result requires geometry.attributes.uv to exist. It is undefined otherwise.

On touchscreens, use touchstart/touchmove events. Convert event.touches[0].clientX/Y to NDC the same way as mouse.

// Touch NDC conversion
canvas.addEventListener('touchstart', (e) => {
  e.preventDefault();
  const touch = e.touches[0];
  pointer.x = (touch.clientX / window.innerWidth)  * 2 - 1;
  pointer.y = -(touch.clientY / window.innerHeight) * 2 + 1;
  raycaster.setFromCamera(pointer, camera);
  // ...
});