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
| Property | Type | Description |
|---|---|---|
intersect.object | Object3D | The intersected object |
intersect.point | Vector3 | World-space hit position |
intersect.distance | number | Distance from ray origin |
intersect.face | Face | { a, b, c, normal, materialIndex } |
intersect.faceIndex | number | Index of the face in geometry |
intersect.uv | Vector2 | UV coordinates at hit point |
intersect.uv2 | Vector2 | Second UV at hit point |
intersect.instanceId | number | Index for InstancedMesh hits |
intersect.barycoord | Vector3 | Barycentric 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 intoGroupchildren. Passtruefor 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.nearandraycaster.farto match your camera's clipping planes to avoid hits outside the view frustum.
For
InstancedMesh,intersect.instanceIdidentifies which instance was hit — not theobject.id.
The
uvproperty in an intersection result requiresgeometry.attributes.uvto exist. It isundefinedotherwise.
On touchscreens, use
touchstart/touchmoveevents. Convertevent.touches[0].clientX/Yto 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); // ... });