Three.js Raycaster — Mouse Picking and Intersection Detection
In this tutorial, you will learn about Three.js Raycaster. We cover key concepts, practical examples, and best practices to help you master this topic.
Three.js raycaster casts a ray from the camera through the mouse position and detects which meshes it intersects, enabling click and hover interactions in 3D.
What You'll Learn
By the end of this guide, you will cast rays from mouse coordinates, detect intersections with meshes, distinguish between click and hover events, highlight selected objects, and implement a 3D object picker.
Why Raycaster Matters
3D scenes need interaction. In Durga Antivirus Pro, the raycaster lets security analysts click on network nodes to inspect threat details. Without it, a 3D scene is just a passive display — with it, users can select, highlight, and manipulate objects.
flowchart LR
A[Mouse Click] --> B[Normalize Coordinates]
B --> C[Create Ray from Camera]
C --> D[Intersect With Scene Objects]
D --> E{Intersection?}
E -->|Yes| F[Get Intersected Object]
E -->|No| G[No Selection]
F --> H[Handle Click Event]
Basic Raycasting
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
function onMouseClick(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
var intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
var firstObject = intersects[0].object;
console.log('Clicked on:', firstObject.name);
}
}
window.addEventListener('click', onMouseClick);
Expected output: Clicking on any mesh in the scene logs its name to the console. Clicking empty space logs nothing.
Why normalized coordinates: Screen coordinates are 0 to width/height. Normalized device coordinates map to -1 to 1, which is what the projection matrix uses.
Highlighting Selected Objects
var selectedObject = null;
function onMouseClick(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
var intersects = raycaster.intersectObjects(scene.children);
if (selectedObject) {
selectedObject.material.emissive.setHex(0x000000);
}
if (intersects.length > 0) {
selectedObject = intersects[0].object;
selectedObject.material.emissive.setHex(0x444444);
} else {
selectedObject = null;
}
}
Expected output: Clicking a mesh highlights it with a glow. Clicking another mesh switches the highlight. Clicking empty space removes the highlight.
Hover Detection
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
var intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
document.body.style.cursor = 'pointer';
} else {
document.body.style.cursor = 'default';
}
}
window.addEventListener('mousemove', onMouseMove);
Expected output: Moving the mouse over any mesh changes the cursor to a pointer. Moving away restores the default cursor.
Intersection Data
The intersection object contains detailed information:
if (intersects.length > 0) {
var hit = intersects[0];
console.log('Object:', hit.object.name);
console.log('Point:', hit.point);
console.log('Distance:', hit.distance);
console.log('Face normal:', hit.face.normal);
console.log('UV:', hit.uv);
}
Expected output: Console shows the exact 3D position of the click, the distance from camera, the face normal at the hit point, and the UV coordinates.
Filtering Intersectable Objects
var clickableObjects = [mesh1, mesh2, mesh3];
raycaster.intersectObjects(clickableObjects);
// Or use recursive to include children
raycaster.intersectObjects(scene.children, true);
Why filtering: Checking all scene children is expensive. Maintain a separate array of interactive objects for better performance.
Common Mistakes
1. Not Converting Mouse Coordinates Correctly
The formula must normalize to -1 to 1 range. Using raw pixel coordinates produces incorrect ray directions.
2. Forgetting to Handle No Intersection
Always check intersects.length > 0 before accessing intersects[0]. An empty array means nothing was clicked.
3. Raycasting Against Invisible Objects
Objects with visible: false are still intersectable unless you filter them. Check object.visible in your handler.
4. Performance Issues With Many Objects
Raycasting against thousands of objects each frame is slow. Maintain a separate array of interactive objects and use spatial indexing for complex scenes.
5. Camera Position Changes Without Updating
Raycaster reads the camera on every call. If you move the camera between mouse events, the ray is already correct.
6. Double-Sided Raycasting
By default, raycaster checks the front face only. Pass true as the second argument to check both sides.
Practice Questions
Q1: What coordinate system does the raycaster use for mouse input? A: Normalized device coordinates (-1 to 1). X maps from left to right, Y from bottom to top.
Q2: What information does an intersection result contain? A: The object, hit point in world space, distance from camera, face normal, UV coordinates, and the face index.
Q3: How do you make only specific objects clickable?
A: Maintain a separate array of clickable objects and pass it to intersectObjects() instead of all scene children.
Q4: What does recursive: true do in intersectObjects?
A: It checks children of each object in the array, useful for Group hierarchies.
Q5: How do you detect right-click? A: Use event.button === 2 for right-click. The raycaster setup remains the same.
Challenge: Build a scene with draggable objects. When the user clicks and drags a mesh, it follows the mouse on a horizontal plane. On release, the object stays in its new position.
FAQ
Try It Yourself — 3D Object Picker
Build a complete page with multiple colored cubes and spheres. Click to select (highlight), hover to preview (change cursor), and display the clicked object's name and position.
<!DOCTYPE html>
<html>
<head>
<title>3D Object Picker</title>
<style>
body { margin: 0; overflow: hidden; font-family: sans-serif; }
#info { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.8); color: white; padding: 10px 20px; border-radius: 8px; font-size: 14px; }
</style>
</head>
<body>
<div id="info">Click any object to select it</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
var scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(5, 5, 8);
var renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
var controls = new OrbitControls(camera, renderer.domElement);
var ambient = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambient);
var dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(5, 10, 7);
scene.add(dirLight);
var clickableObjects = [];
var colors = [0xff6b35, 0x4ecdc4, 0x45b7d1, 0xf9ca24, 0xa29bfe, 0xfd79a8];
for (var i = 0; i < 8; i++) {
var isCube = i % 2 === 0;
var geo = isCube ? new THREE.BoxGeometry(1, 1, 1) : new THREE.SphereGeometry(0.6, 32, 32);
var mat = new THREE.MeshStandardMaterial({ color: colors[i % colors.length] });
var mesh = new THREE.Mesh(geo, mat);
mesh.position.set(
(Math.random() - 0.5) * 8,
(Math.random() - 0.5) * 4 + 2,
(Math.random() - 0.5) * 6
);
mesh.name = (isCube ? 'Cube' : 'Sphere') + ' ' + (i + 1);
scene.add(mesh);
clickableObjects.push(mesh);
}
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
var selected = null;
var info = document.getElementById('info');
window.addEventListener('click', function(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
var intersects = raycaster.intersectObjects(clickableObjects);
if (selected) {
selected.material.emissive.setHex(0x000000);
selected = null;
}
if (intersects.length > 0) {
selected = intersects[0].object;
selected.material.emissive.setHex(0x444444);
info.textContent = 'Selected: ' + selected.name +
' | Position: ' + selected.position.x.toFixed(2) +
', ' + selected.position.y.toFixed(2) +
', ' + selected.position.z.toFixed(2);
} else {
info.textContent = 'Click any object to select it';
}
});
window.addEventListener('mousemove', function(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
var intersects = raycaster.intersectObjects(clickableObjects);
document.body.style.cursor = intersects.length > 0 ? 'pointer' : 'default';
});
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', function() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
What's Next
Add physics simulation to your Three.js scene.
Physics — Physics engines for Three.js (Cannon-es, Ammo). Audio — Spatial audio in 3D environments.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro