Navigate

Search topics across all sections

GitHub
Events & Interaction

Raycasting

Pointer events feel like magic -- you click a 3D cube and it responds. But how does the computer know which object you clicked? It cannot just check pixel colors like a 2D app. It needs to fire an invisible ray from your mouse into the 3D world and see what it hits. That process is called raycasting, and understanding it unlocks everything from click detection to shooting mechanics to AI line-of-sight.

You attach onClick to a detailed 3D character model with 100,000 triangles. Every time the user clicks anywhere on the canvas, R3F raycasts against all 100,000 triangles to check if the click hit the model. Your click response takes 50ms and the whole scene stutters.

terminal
Performance: intersectObject() took 47ms for mesh "Character"
  - Geometry faces: 100,000
  - Triangle intersection tests: 100,000
  - Consider using a simplified collider or meshBounds.

Real-world

Imagine you are in a dark room full of people, and you have a laser pointer. You aim it straight ahead and the beam travels in a straight line until it hits the first person. You now know exactly who is standing in that direction, how far away they are, and which part of them the beam touched.

That is raycasting. An invisible ray shoots from your camera through the mouse position into the 3D world. It tests every object along its path and reports what it hit first, where it hit, and how far away. R3F does this automatically for pointer events, but you can also cast rays manually for custom mechanics.

The Raycasting Pipeline

Camera + Mouse

Pointer position mapped to normalized coordinates

Cast a Ray

From camera origin through mouse point

Test Intersections

Ray vs. triangles of each mesh

Sort by Distance

Nearest hit comes first

Dispatch Event

onClick fires on the closest object

See It In Action

The demo below visualizes raycasting in real time. Watch the ray extend from the camera and see which objects it intersects as you move your mouse.

Building It Step by Step

Step 1 -- Understand automatic raycasting

When you add onClick or onPointerOver to a mesh, R3F automatically raycasts against that object. You do not need to set up a raycaster yourself. Only meshes with event handlers are tested -- everything else is invisible to the ray.

AutoRaycast.tsxTSX
// R3F raycasts against this mesh automatically
<mesh onClick={(e) => {
  console.log('Hit at:', e.point)
  console.log('Distance:', e.distance)
}}>
  <sphereGeometry args={[1, 32, 32]} />
  <meshStandardMaterial />
</mesh>

The event object gives you the exact 3D hit position, the distance from the camera, the face that was hit, and more. No manual setup required.

Step 2 -- Use meshBounds for fast approximate hits

For complex geometry where pixel-perfect accuracy is not critical, drei's meshBounds replaces per-triangle raycasting with a single bounding-box test. Thousands of times faster.

FastClick.tsxTSX
import { meshBounds } from '@react-three/drei'

<mesh
  raycast={meshBounds}
  onClick={() => console.log('Bounding box hit!')}
>
  <torusKnotGeometry args={[1, 0.3, 256, 64]} />
  <meshStandardMaterial color="purple" />
</mesh>

For a 16,000-triangle torus knot, this turns 16,000 intersection tests into just 1 box test. Clicks near the edges may register outside the actual geometry, but for buttons and UI that is fine.

Step 3 -- Cast rays manually for custom mechanics

For shooting, line-of-sight, or ground detection, create a Raycaster yourself and cast rays in any direction, not just from the mouse.

GroundSnap.tsxTSX
const raycaster = useRef(new THREE.Raycaster())
const down = useRef(new THREE.Vector3(0, -1, 0))

useFrame(() => {
  raycaster.current.set(player.position, down.current)
  const hits = raycaster.current.intersectObjects(ground)
  if (hits.length > 0) {
    player.position.y = hits[0].point.y + 1
  }
})

This casts a ray straight down from the player to find the ground, then snaps the player to the surface. The same idea powers shooters (ray from gun barrel), AI vision (ray toward target), and physics (collision probes).

What you just learned

Raycasting shoots an invisible ray and reports what it hits in the 3D scene

R3F raycasts automatically for pointer events -- only objects with handlers are tested

meshBounds from drei replaces per-triangle testing with a fast bounding-box check

You can cast rays manually for shooting, line-of-sight, and ground detection

Always store Raycaster and Vector3 objects in refs to avoid garbage collection stutter

Question

How does raycasting work with InstancedMesh? By default, R3F does not raycast individual instances -- it tests the base bounding box. For per-instance click detection, you need to implement a custom raycast function or use drei's <Instances> component which provides per-instance event support out of the box.

Think about it...

You have a 3D car model with 80,000 triangles. You want users to click it to open a detail panel. What is the fastest approach?

Try These Challenges

Put what you learned into practice. Try each challenge in the demo above using the Leva controls, then check the solution.

Try This!

Beginner

Toggle showRay off -- do events still work?

Try This!

Beginner

Set targetSize to 2 -- easier to hit!

Try This!

Beginner

Switch rayColor -- is it cosmetic only?

These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.

1
Raycasting against complex geometry
Testing 50,000 triangles per click when a simple box would do
Don't do this
Model.tsxTSX
// 50k polygon model -- 50k triangle tests per click
<mesh onClick={() => console.log('Clicked!')}>
  <primitive object={gltf.scene} />
</mesh>
Raycasting tests the ray against every triangle in the geometry. A 50,000 polygon model means 50,000 triangle intersection tests per pointer event. Instead, overlay a simple invisible collider (sphere, box, or capsule) that approximates the shape, and disable raycasting on the visual mesh with raycast={() => null}.
2
Not using layers for selective raycasting
Testing every object in the scene when you only need a few
Don't do this
Shooter.tsxTSX
// Tests EVERYTHING in the scene
const hits = raycaster.current.intersectObjects(
  scene.children, true
)
intersectObjects(scene.children, true) recursively tests every object in the scene. If you only care about a few targets, maintain an array of target refs or use Three.js layers to partition the scene. Set the raycaster to check only specific layers.
3
Creating new objects inside useFrame for raycasting
Allocating memory every frame causes stutter
Don't do this
FrameRaycast.tsxTSX
useFrame(() => {
  // NEW objects every frame = garbage collection stutter
  const origin = new THREE.Vector3(0, 5, 0)
  const direction = new THREE.Vector3(0, -1, 0)
  const raycaster = new THREE.Raycaster(origin, direction)
})
Creating new Vector3 and Raycaster objects inside useFrame allocates memory 60 times per second. The garbage collector eventually pauses your app to clean them up, causing visible stutter. Store these in useRef and reuse them with .set() or .copy().

Best Practices

Use simplified colliders

For complex models, overlay an invisible simple mesh for raycasting. Disable raycasting on the visual mesh with raycast={() => null}.

Minimize event handlers

Every mesh with a handler is a raycasting target. Fewer handlers means fewer intersection tests per pointer event.

Reuse objects in useFrame

Store Raycaster and Vector3 objects in refs. Never create new instances inside the frame loop.

meshBounds for UI elements

For buttons and clickable items, meshBounds from drei provides near-instant raycasting with minimal code.