| name | threejs-interaction |
| allowed-tools | Read, Glob |
| description | Three.js interaction - raycasting, controls, mouse/touch input, object selection. Use when handling user input, implementing click detection, adding camera controls, or creating interactive 3D experiences. |
Three.js Interaction
Iron Law
Always call raycaster.setFromCamera() with normalized device coordinates in range [-1, 1] — raw pixel coordinates produce silent miss-hits on all objects.
Quick Start
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onClick(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
console.log("Clicked:", intersects[0].object);
}
}
window.addEventListener("click", onClick);
Raycaster
Basic Raycasting
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mousePosition, camera);
raycaster.set(origin, direction);
const intersects = raycaster.intersectObjects(objects, recursive);
Mouse Position Conversion
const mouse = new THREE.Vector2();
function updateMouse(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
function updateMouseCanvas(event, canvas) {
const rect = canvas.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
}
Touch Support
function onTouchStart(event) {
event.preventDefault();
if (event.touches.length === 1) {
const touch = event.touches[0];
mouse.x = (touch.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(clickableObjects);
if (intersects.length > 0) {
handleSelection(intersects[0]);
}
}
}
renderer.domElement.addEventListener("touchstart", onTouchStart);
Raycaster Options
const raycaster = new THREE.Raycaster();
raycaster.near = 0;
raycaster.far = 100;
raycaster.params.Line.threshold = 0.1;
raycaster.params.Points.threshold = 0.1;
raycaster.layers.set(1);
Efficient Raycasting
const clickables = [mesh1, mesh2, mesh3];
const intersects = raycaster.intersectObjects(clickables, false);
mesh1.layers.set(1);
raycaster.layers.set(1);
let lastRaycast = 0;
function onMouseMove(event) {
const now = Date.now();
if (now - lastRaycast < 50) return;
lastRaycast = now;
}
Camera Controls
OrbitControls
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minPolarAngle = 0;
controls.maxPolarAngle = Math.PI / 2;
controls.minAzimuthAngle = -Math.PI / 4;
controls.maxAzimuthAngle = Math.PI / 4;
controls.minDistance = 2;
controls.maxDistance = 50;
controls.enableRotate = true;
controls.enableZoom = true;
controls.enablePan = true;
controls.autoRotate = true;
controls.autoRotateSpeed = 2.0;
controls.target.set(0, 1, 0);
function animate() {
controls.update();
renderer.render(scene, camera);
}
Other Control Types
| Control | Import | Best For |
|---|
FlyControls | controls/FlyControls.js | Free-flying, space/flight sims |
FirstPersonControls | controls/FirstPersonControls.js | FPS with look constraints |
PointerLockControls | controls/PointerLockControls.js | Immersive FPS (browser pointer lock) |
TrackballControls | controls/TrackballControls.js | Unconstrained rotation (rolls past poles) |
MapControls | controls/MapControls.js | Top-down map view |
Full code examples → references/advanced-controls.md
TransformControls
Gizmo for moving/rotating/scaling objects.
import { TransformControls } from "three/addons/controls/TransformControls.js";
const transformControls = new TransformControls(camera, renderer.domElement);
scene.add(transformControls);
transformControls.attach(selectedMesh);
transformControls.setMode("translate");
transformControls.setSpace("local");
transformControls.setSize(1);
transformControls.addEventListener("dragging-changed", (event) => {
orbitControls.enabled = !event.value;
});
transformControls.addEventListener("change", () => {
renderer.render(scene, camera);
});
window.addEventListener("keydown", (event) => {
switch (event.key) {
case "g":
transformControls.setMode("translate");
break;
case "r":
transformControls.setMode("rotate");
break;
case "s":
transformControls.setMode("scale");
break;
case "Escape":
transformControls.detach();
break;
}
});
DragControls
Drag objects directly.
import { DragControls } from "three/addons/controls/DragControls.js";
const draggableObjects = [mesh1, mesh2, mesh3];
const dragControls = new DragControls(
draggableObjects,
camera,
renderer.domElement,
);
dragControls.addEventListener("dragstart", (event) => {
orbitControls.enabled = false;
event.object.material.emissive.set(0xaaaaaa);
});
dragControls.addEventListener("drag", (event) => {
event.object.position.y = 0;
});
dragControls.addEventListener("dragend", (event) => {
orbitControls.enabled = true;
event.object.material.emissive.set(0x000000);
});
Selection System
Click to Select
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let selectedObject = null;
function onMouseDown(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(selectableObjects);
if (selectedObject) {
selectedObject.material.emissive.set(0x000000);
}
if (intersects.length > 0) {
selectedObject = intersects[0].object;
selectedObject.material.emissive.set(0x444444);
} else {
selectedObject = null;
}
}
Hover Effects
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let hoveredObject = null;
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(hoverableObjects);
if (hoveredObject) {
hoveredObject.material.color.set(hoveredObject.userData.originalColor);
document.body.style.cursor = "default";
}
if (intersects.length > 0) {
hoveredObject = intersects[0].object;
if (!hoveredObject.userData.originalColor) {
hoveredObject.userData.originalColor =
hoveredObject.material.color.getHex();
}
hoveredObject.material.color.set(0xff6600);
document.body.style.cursor = "pointer";
} else {
hoveredObject = null;
}
}
window.addEventListener("mousemove", onMouseMove);
Keyboard Input
const keys = {};
document.addEventListener("keydown", (event) => {
keys[event.code] = true;
});
document.addEventListener("keyup", (event) => {
keys[event.code] = false;
});
function update() {
const speed = 0.1;
if (keys["KeyW"]) player.position.z -= speed;
if (keys["KeyS"]) player.position.z += speed;
if (keys["KeyA"]) player.position.x -= speed;
if (keys["KeyD"]) player.position.x += speed;
if (keys["Space"]) player.position.y += speed;
if (keys["ShiftLeft"]) player.position.y -= speed;
}
References
references/advanced-controls.md — FlyControls, FirstPersonControls, PointerLockControls, TrackballControls, MapControls; box selection; world↔screen coordinate conversion; InteractionManager class; performance tips
See Also
threejs-fundamentals - Camera and scene setup
threejs-animation - Animating interactions
threejs-shaders - Visual feedback effects