Threejs Interaction

CloudAI-X/threejs-skills/skills/threejs-interaction

by CloudAI-Xb1c623076c661fc9b03dac19292e825a5d106823No license3.4K starsListed Oct 9, 2026Updated Oct 9, 2026Repository updated 3 months ago

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.

Instructions onlySoftware Development
AI-generated overview

Reference guide for Three.js user interaction: raycasting, camera controls, selection, and input handling.

What it does
Provides code snippets and patterns for handling user input in Three.js scenes, including raycasting for click and hover detection, mouse and touch coordinate conversion, and object selection. It documents camera control classes such as OrbitControls, FlyControls, FirstPersonControls, PointerLockControls, TrackballControls, MapControls, TransformControls, and DragControls. It also covers keyboard input, world-to-screen and screen-to-world conversion, ray-plane intersection, an interaction manager class, and performance tips.
When to use it
Use when implementing interactive 3D experiences in Three.js, such as click detection, object picking, hover effects, camera navigation, or draggable and transformable objects. Also useful when adding touch support or converting between screen and world coordinates.
Requirements
Requires a Three.js project environment; the skill is instructions and code examples only and ships no scripts. No credentials or network access are needed beyond installing Three.js.

Three.js Interaction

Quick Start

javascript
import * as THREE from "three";import { OrbitControls } from "three/addons/controls/OrbitControls.js";
// Camera controlsconst controls = new OrbitControls(camera, renderer.domElement);controls.enableDamping = true;
// Raycasting for click detectionconst 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

javascript
const raycaster = new THREE.Raycaster();
// From camera (mouse picking)raycaster.setFromCamera(mousePosition, camera);
// From any origin and directionraycaster.set(origin, direction); // origin: Vector3, direction: normalized Vector3
// Get intersectionsconst intersects = raycaster.intersectObjects(objects, recursive);
// intersects array contains:// {//   distance: number,          // Distance from ray origin//   point: Vector3,            // Intersection point in world coords//   face: Face3,               // Intersected face//   faceIndex: number,         // Face index//   object: Object3D,          // Intersected object//   uv: Vector2,               // UV coordinates at intersection//   uv1: Vector2,              // Second UV channel//   normal: Vector3,           // Interpolated face normal//   instanceId: number         // For InstancedMesh// }

Mouse Position Conversion

javascript
const mouse = new THREE.Vector2();
function updateMouse(event) {  // For full window  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;}
// For specific canvas elementfunction 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

javascript
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

javascript
const raycaster = new THREE.Raycaster();
// Near/far clipping (default: 0, Infinity)raycaster.near = 0;raycaster.far = 100;
// Line/Points precisionraycaster.params.Line.threshold = 0.1;raycaster.params.Points.threshold = 0.1;
// Layers (only intersect objects on specific layers)raycaster.layers.set(1);

Efficient Raycasting

javascript
// Only check specific objectsconst clickables = [mesh1, mesh2, mesh3];const intersects = raycaster.intersectObjects(clickables, false);
// Use layers for filteringmesh1.layers.set(1); // Clickable layerraycaster.layers.set(1);
// Throttle raycast for hover effectslet lastRaycast = 0;function onMouseMove(event) {  const now = Date.now();  if (now - lastRaycast < 50) return; // 20fps max  lastRaycast = now;
  // Raycast here}

Camera Controls

OrbitControls

javascript
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
const controls = new OrbitControls(camera, renderer.domElement);
// Damping (smooth movement)controls.enableDamping = true;controls.dampingFactor = 0.05;
// Rotation limitscontrols.minPolarAngle = 0; // Topcontrols.maxPolarAngle = Math.PI / 2; // Horizoncontrols.minAzimuthAngle = -Math.PI / 4; // Leftcontrols.maxAzimuthAngle = Math.PI / 4; // Right
// Zoom limitscontrols.minDistance = 2;controls.maxDistance = 50;
// Enable/disable featurescontrols.enableRotate = true;controls.enableZoom = true;controls.enablePan = true;
// Auto-rotatecontrols.autoRotate = true;controls.autoRotateSpeed = 2.0;
// Target (orbit point)controls.target.set(0, 1, 0);
// Update in animation loopfunction animate() {  controls.update(); // Required for damping and auto-rotate  renderer.render(scene, camera);}

FlyControls

javascript
import { FlyControls } from "three/addons/controls/FlyControls.js";
const controls = new FlyControls(camera, renderer.domElement);controls.movementSpeed = 10;controls.rollSpeed = Math.PI / 24;controls.dragToLook = true;
// Update with deltafunction animate() {  controls.update(clock.getDelta());  renderer.render(scene, camera);}

FirstPersonControls

javascript
import { FirstPersonControls } from "three/addons/controls/FirstPersonControls.js";
const controls = new FirstPersonControls(camera, renderer.domElement);controls.movementSpeed = 10;controls.lookSpeed = 0.1;controls.lookVertical = true;controls.constrainVertical = true;controls.verticalMin = Math.PI / 4;controls.verticalMax = (Math.PI * 3) / 4;
function animate() {  controls.update(clock.getDelta());}

PointerLockControls

javascript
import { PointerLockControls } from "three/addons/controls/PointerLockControls.js";
const controls = new PointerLockControls(camera, document.body);
// Lock pointer on clickdocument.addEventListener("click", () => {  controls.lock();});
controls.addEventListener("lock", () => {  console.log("Pointer locked");});
controls.addEventListener("unlock", () => {  console.log("Pointer unlocked");});
// Movementconst velocity = new THREE.Vector3();const direction = new THREE.Vector3();const moveForward = false;const moveBackward = false;
document.addEventListener("keydown", (event) => {  switch (event.code) {    case "KeyW":      moveForward = true;      break;    case "KeyS":      moveBackward = true;      break;  }});
function animate() {  if (controls.isLocked) {    direction.z = Number(moveForward) - Number(moveBackward);    direction.normalize();
    velocity.z -= direction.z * 0.1;    velocity.z *= 0.9; // Friction
    controls.moveForward(-velocity.z);  }}

TrackballControls

javascript
import { TrackballControls } from "three/addons/controls/TrackballControls.js";
const controls = new TrackballControls(camera, renderer.domElement);controls.rotateSpeed = 2.0;controls.zoomSpeed = 1.2;controls.panSpeed = 0.8;controls.staticMoving = true;
function animate() {  controls.update();}

MapControls

javascript
import { MapControls } from "three/addons/controls/MapControls.js";
const controls = new MapControls(camera, renderer.domElement);controls.enableDamping = true;controls.dampingFactor = 0.05;controls.screenSpacePanning = false;controls.maxPolarAngle = Math.PI / 2;

TransformControls

Gizmo for moving/rotating/scaling objects.

javascript
import { TransformControls } from "three/addons/controls/TransformControls.js";
const transformControls = new TransformControls(camera, renderer.domElement);scene.add(transformControls);
// Attach to objecttransformControls.attach(selectedMesh);
// Switch modestransformControls.setMode("translate"); // 'translate', 'rotate', 'scale'
// Change spacetransformControls.setSpace("local"); // 'local', 'world'
// SizetransformControls.setSize(1);
// EventstransformControls.addEventListener("dragging-changed", (event) => {  // Disable orbit controls while dragging  orbitControls.enabled = !event.value;});
transformControls.addEventListener("change", () => {  renderer.render(scene, camera);});
// Keyboard shortcutswindow.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.

javascript
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) => {  // Constrain to ground plane  event.object.position.y = 0;});
dragControls.addEventListener("dragend", (event) => {  orbitControls.enabled = true;  event.object.material.emissive.set(0x000000);});

Selection System

Click to Select

javascript
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);
  // Deselect previous  if (selectedObject) {    selectedObject.material.emissive.set(0x000000);  }
  // Select new  if (intersects.length > 0) {    selectedObject = intersects[0].object;    selectedObject.material.emissive.set(0x444444);  } else {    selectedObject = null;  }}

Box Selection

javascript
import { SelectionBox } from "three/addons/interactive/SelectionBox.js";import { SelectionHelper } from "three/addons/interactive/SelectionHelper.js";
const selectionBox = new SelectionBox(camera, scene);const selectionHelper = new SelectionHelper(renderer, "selectBox"); // CSS class
document.addEventListener("pointerdown", (event) => {  selectionBox.startPoint.set(    (event.clientX / window.innerWidth) * 2 - 1,    -(event.clientY / window.innerHeight) * 2 + 1,    0.5,  );});
document.addEventListener("pointermove", (event) => {  if (selectionHelper.isDown) {    selectionBox.endPoint.set(      (event.clientX / window.innerWidth) * 2 - 1,      -(event.clientY / window.innerHeight) * 2 + 1,      0.5,    );  }});
document.addEventListener("pointerup", (event) => {  selectionBox.endPoint.set(    (event.clientX / window.innerWidth) * 2 - 1,    -(event.clientY / window.innerHeight) * 2 + 1,    0.5,  );
  const selected = selectionBox.select();  console.log("Selected objects:", selected);});

Hover Effects

javascript
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);
  // Reset previous hover  if (hoveredObject) {    hoveredObject.material.color.set(hoveredObject.userData.originalColor);    document.body.style.cursor = "default";  }
  // Apply new hover  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

javascript
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;}

World-Screen Coordinate Conversion

World to Screen

javascript
function worldToScreen(position, camera) {  const vector = position.clone();  vector.project(camera);
  return {    x: ((vector.x + 1) / 2) * window.innerWidth,    y: (-(vector.y - 1) / 2) * window.innerHeight,  };}
// Position HTML element over 3D objectconst screenPos = worldToScreen(mesh.position, camera);element.style.left = screenPos.x + "px";element.style.top = screenPos.y + "px";

Screen to World

javascript
function screenToWorld(screenX, screenY, camera, targetZ = 0) {  const vector = new THREE.Vector3(    (screenX / window.innerWidth) * 2 - 1,    -(screenY / window.innerHeight) * 2 + 1,    0.5,  );
  vector.unproject(camera);
  const dir = vector.sub(camera.position).normalize();  const distance = (targetZ - camera.position.z) / dir.z;
  return camera.position.clone().add(dir.multiplyScalar(distance));}

Ray-Plane Intersection

javascript
function getRayPlaneIntersection(mouse, camera, plane) {  const raycaster = new THREE.Raycaster();  raycaster.setFromCamera(mouse, camera);
  const intersection = new THREE.Vector3();  raycaster.ray.intersectPlane(plane, intersection);
  return intersection;}
// Ground planeconst groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);const worldPos = getRayPlaneIntersection(mouse, camera, groundPlane);

Event Handling Best Practices

javascript
class InteractionManager {  constructor(camera, renderer, scene) {    this.camera = camera;    this.renderer = renderer;    this.scene = scene;    this.raycaster = new THREE.Raycaster();    this.mouse = new THREE.Vector2();    this.clickables = [];
    this.bindEvents();  }
  bindEvents() {    const canvas = this.renderer.domElement;
    canvas.addEventListener("click", (e) => this.onClick(e));    canvas.addEventListener("mousemove", (e) => this.onMouseMove(e));    canvas.addEventListener("touchstart", (e) => this.onTouchStart(e));  }
  updateMouse(event) {    const rect = this.renderer.domElement.getBoundingClientRect();    this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;    this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;  }
  getIntersects() {    this.raycaster.setFromCamera(this.mouse, this.camera);    return this.raycaster.intersectObjects(this.clickables, true);  }
  onClick(event) {    this.updateMouse(event);    const intersects = this.getIntersects();
    if (intersects.length > 0) {      const object = intersects[0].object;      if (object.userData.onClick) {        object.userData.onClick(intersects[0]);      }    }  }
  addClickable(object, callback) {    this.clickables.push(object);    object.userData.onClick = callback;  }
  dispose() {    // Remove event listeners  }}
// Usageconst interaction = new InteractionManager(camera, renderer, scene);interaction.addClickable(mesh, (intersect) => {  console.log("Clicked at:", intersect.point);});

Performance Tips

  1. Limit raycasts: Throttle mousemove handlers
  2. Use layers: Filter raycast targets
  3. Simple collision meshes: Use invisible simpler geometry for raycasting
  4. Disable controls when not needed: controls.enabled = false
  5. Batch updates: Group interaction checks
javascript
// Use simpler geometry for raycastingconst complexMesh = loadedModel;const collisionMesh = new THREE.Mesh(  new THREE.BoxGeometry(1, 1, 1),  new THREE.MeshBasicMaterial({ visible: false }),);collisionMesh.userData.target = complexMesh;clickables.push(collisionMesh);

See Also

  • threejs-fundamentals - Camera and scene setup
  • threejs-animation - Animating interactions
  • threejs-shaders - Visual feedback effects

Source and attribution

Source:CloudAI-X/threejs-skillsinskills/threejs-interactionat commitb1c6230

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal