Three.js & Rendering · Rendering · core
Raycasting
shoot an invisible line and ask what it touches first
A ray is an origin plus a direction. Raycasting returns the nearest surface it crosses, with hit point, distance and surface normal.
A ray sweeps from the white origin. The cyan segment ends at the first surface it hits; the orange dot is the hit point and the green arrow is that surface's normal. Turn off auto sweep to aim it yourself, and shorten max distance to see misses.
What it is
A ray is the half-line P(t) = O + t·D for t ≥ 0. A raycast tests that line against colliders or triangles and returns the closest hit: the point, the distance t, the object, and the surface normal at that point.
Games use it everywhere: hitscan weapons, mouse picking, line-of-sight checks, ground detection under a character, camera collision, and placing objects on terrain. In Three.js, THREE.Raycaster does this against meshes. Physics engines offer faster versions against their own colliders.
Raycasts are cheap individually but add up. Limit the distance, filter by layer or mask, and test against simple proxy shapes, or an acceleration structure such as a BVH, rather than every triangle of a detailed mesh.
Key formulas
- Ray
- Ray–plane hit
No hit when the denominator is ~0 (parallel ray) or t < 0 (plane behind the origin).
- Ray–sphere hit
A negative discriminant (b² − c < 0) means a miss.
Rules of thumb
- Always normalize the direction; most APIs assume a unit vector, so
tis in world units. - Set
far(max distance) to the gameplay range, not infinity. - Ground checks: start the ray slightly above the feet and cast a little past them (e.g. 0.1–0.2 units) to tolerate small steps.
- Use layers/masks so the player's own collider doesn't block their shots.
Implementation
THREE.Raycaster:const raycaster = new THREE.Raycaster()
const pointer = new THREE.Vector2()
window.addEventListener("pointerdown", (e) => {
// Pixel → normalized device coordinates (-1..1)
pointer.x = (e.clientX / innerWidth) * 2 - 1
pointer.y = -(e.clientY / innerHeight) * 2 + 1
raycaster.setFromCamera(pointer, camera)
raycaster.far = 100
const [hit] = raycaster.intersectObjects(pickables, false)
if (hit) {
console.log(hit.object.name, hit.point, hit.distance, hit.face?.normal)
}
})origin = feet + up * 0.1
hit = raycast(origin, -up, maxDistance = 0.3, mask = WORLD)
grounded = hit != null and dot(hit.normal, up) > 0.7 // walkable slopeCommon mistakes
- Forgetting the Y flip when converting mouse coordinates to NDC.
- Using
intersectObjects(scene.children, true)every frame on detailed meshes. Raycast against simple proxies or a BVH instead. - Starting the ray inside a collider. Many engines ignore or report odd hits for backfaces and inside starts.
- Expecting
face.normalin world space. In Three.js it is in object space, so transform it with the object's normal matrix.
Prompt
Add raycast-based interaction to my Three.js scene in TypeScript. On pointerdown, convert the pointer to NDC (flip Y), call raycaster.setFromCamera, limit raycaster.far to 100, and intersect only an explicit `pickables` array (non-recursive). For the closest hit, log object name, world-space hit point, distance, and the world-space normal (transform face.normal by the object's normal matrix). Also write a reusable `groundCheck(origin, maxDistance)` that casts straight down and treats the hit as walkable if dot(normal, up) > 0.7. No per-frame allocations.
Related
Further reading
Updated 2026-11-01