ChatGPT Claude Codex CLI Cohere Cursor DeepSeek Gemini GitHub Copilot GLM Grok Kimi Llama MiniMax Mistral OpenAI opencode Skill

threejs-interaction

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.

LLM Mart · 0 points · 14 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download sickn33-agentic-awesome-skills-skills_threejs-interaction-286166a.zip · 4 KB
Part of sickn33/agentic-awesome-skills — 427 skills
This skill couldn't be refreshed from GitHub on the last check — you're seeing the last imported snapshot.

Install

skills CLI npx skills add https://github.com/sickn33/agentic-awesome-skills/tree/main/skills/threejs-interaction
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install sickn33-agentic-awesome-skills@llmmart
Git git clone https://github.com/sickn33/agentic-awesome-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole sickn33/agentic-awesome-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Three.js Interaction

When to Use

  • You need user interaction inside a Three.js scene.
  • The task involves raycasting, object picking, pointer handling, touch input, or camera controls.
  • You are building an interactive 3D experience rather than a passive render.

Quick Start

import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";

// Camera controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

// Raycasting for click detection
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();

// From camera (mouse picking)
raycaster.setFromCamera(mousePosition, camera);

// From any origin and direction
raycaster.set(origin, direction); // origin: Vector3, direction: normalized Vector3

// Get intersections
const 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

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 element
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();

// Near/far clipping (default: 0, Infinity)
raycaster.near = 0;
raycaster.far = 100;

// Line/Points precision
raycaster.params.Line.threshold = 0.1;
raycaster.params.Points.threshold = 0.1;

// Layers (only intersect objects on specific layers)
raycaster.layers.set(1);

Efficient Raycasting

// Only check specific objects
const clickables = [mesh1, mesh2, mesh3];
const intersects = raycaster.intersectObjects(clickables, false);

// Use layers for filtering
mesh1.layers.set(1); // Clickable layer
raycaster.layers.set(1);

// Throttle raycast for hover effects
let lastRaycast = 0;
function onMouseMove(event) {
  const now = Date.now();
  if (now - lastRaycast < 50) return; // 20fps max
  lastRaycast = now;

  // Raycast here
}

Camera Controls

OrbitControls

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 limits
controls.minPolarAngle = 0; // Top
controls.maxPolarAngle = Math.PI / 2; // Horizon
controls.minAzimuthAngle = -Math.PI / 4; // Left
controls.maxAzimuthAngle = Math.PI / 4; // Right

// Zoom limits
controls.minDistance = 2;
controls.maxDistance = 50;

// Enable/disable features
controls.enableRotate = true;
controls.enableZoom = true;
controls.enablePan = true;

// Auto-rotate
controls.autoRotate = true;
controls.autoRotateSpeed = 2.0;

// Target (orbit point)
controls.target.set(0, 1, 0);

// Update in animation loop
function animate() {
  controls.update(); // Required for damping and auto-rotate
  renderer.render(scene, camera);
}

OrbitControls Programmatic Methods (r183)

// Programmatic camera movement
controls.dolly(1.5); // Dolly in/out (zoom for perspective cameras)
controls.pan(deltaX, deltaY); // Pan the camera
controls.rotate(deltaAzimuth, deltaPolar); // Rotate around target

// Cursor style (r183)
controls.cursorStyle = { orbit: "grab", pan: "move", dolly: "zoom-in" };

FlyControls

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 delta
function animate() {
  controls.update(clock.getDelta());
  renderer.render(scene, camera);
}

FirstPersonControls

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

import { PointerLockControls } from "three/addons/controls/PointerLockControls.js";

const controls = new PointerLockControls(camera, document.body);

// Lock pointer on click
document.addEventListener("click", () => {
  controls.lock();
});

controls.addEventListener("lock", () => {
  console.log("Pointer locked");
});

controls.addEventListener("unlock", () => {
  console.log("Pointer unlocked");
});

// Movement
const 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

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

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.

import { TransformControls } from "three/addons/controls/TransformControls.js";

const transformControls = new TransformControls(camera, renderer.domElement);
scene.add(transformControls);

// Attach to object
transformControls.attach(selectedMesh);

// Switch modes
transformControls.setMode("translate"); // 'translate', 'rotate', 'scale'

// Change space
transformControls.setSpace("local"); // 'local', 'world'

// Size
transformControls.setSize(1);

// Events
transformControls.addEventListener("dragging-changed", (event) => {
  // Disable orbit controls while dragging
  orbitControls.enabled = !event.value;
});

transformControls.addEventListener("change", () => {
  renderer.render(scene, camera);
});

// Keyboard shortcuts
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) => {
  // 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

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

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

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

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

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 object
const screenPos = worldToScreen(mesh.position, camera);
element.style.left = screenPos.x + "px";
element.style.top = screenPos.y + "px";

Screen to World

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

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 plane
const groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
const worldPos = getRayPlaneIntersection(mouse, camera, groundPlane);

Event Handling Best Practices

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

// Usage
const 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
// Use simpler geometry for raycasting
const 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

Limitations

  • Use this skill only when the task clearly matches the scope described above.
  • Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
  • Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
Files (agentic-awesome-skills)
  • SKILL.md 16.9 KB
    ---
    name: threejs-interaction
    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.
    risk: critical
    source: community
    ---
    
    # Three.js Interaction
    
    ## When to Use
    - You need user interaction inside a Three.js scene.
    - The task involves raycasting, object picking, pointer handling, touch input, or camera controls.
    - You are building an interactive 3D experience rather than a passive render.
    
    ## Quick Start
    
    ```javascript
    import * as THREE from "three";
    import { OrbitControls } from "three/addons/controls/OrbitControls.js";
    
    // Camera controls
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    
    // Raycasting for click detection
    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
    
    ```javascript
    const raycaster = new THREE.Raycaster();
    
    // From camera (mouse picking)
    raycaster.setFromCamera(mousePosition, camera);
    
    // From any origin and direction
    raycaster.set(origin, direction); // origin: Vector3, direction: normalized Vector3
    
    // Get intersections
    const 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 element
    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
    
    ```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 precision
    raycaster.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 objects
    const clickables = [mesh1, mesh2, mesh3];
    const intersects = raycaster.intersectObjects(clickables, false);
    
    // Use layers for filtering
    mesh1.layers.set(1); // Clickable layer
    raycaster.layers.set(1);
    
    // Throttle raycast for hover effects
    let 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 limits
    controls.minPolarAngle = 0; // Top
    controls.maxPolarAngle = Math.PI / 2; // Horizon
    controls.minAzimuthAngle = -Math.PI / 4; // Left
    controls.maxAzimuthAngle = Math.PI / 4; // Right
    
    // Zoom limits
    controls.minDistance = 2;
    controls.maxDistance = 50;
    
    // Enable/disable features
    controls.enableRotate = true;
    controls.enableZoom = true;
    controls.enablePan = true;
    
    // Auto-rotate
    controls.autoRotate = true;
    controls.autoRotateSpeed = 2.0;
    
    // Target (orbit point)
    controls.target.set(0, 1, 0);
    
    // Update in animation loop
    function animate() {
      controls.update(); // Required for damping and auto-rotate
      renderer.render(scene, camera);
    }
    ```
    
    #### OrbitControls Programmatic Methods (r183)
    
    ```javascript
    // Programmatic camera movement
    controls.dolly(1.5); // Dolly in/out (zoom for perspective cameras)
    controls.pan(deltaX, deltaY); // Pan the camera
    controls.rotate(deltaAzimuth, deltaPolar); // Rotate around target
    
    // Cursor style (r183)
    controls.cursorStyle = { orbit: "grab", pan: "move", dolly: "zoom-in" };
    ```
    
    ### 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 delta
    function 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 click
    document.addEventListener("click", () => {
      controls.lock();
    });
    
    controls.addEventListener("lock", () => {
      console.log("Pointer locked");
    });
    
    controls.addEventListener("unlock", () => {
      console.log("Pointer unlocked");
    });
    
    // Movement
    const 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 object
    transformControls.attach(selectedMesh);
    
    // Switch modes
    transformControls.setMode("translate"); // 'translate', 'rotate', 'scale'
    
    // Change space
    transformControls.setSpace("local"); // 'local', 'world'
    
    // Size
    transformControls.setSize(1);
    
    // Events
    transformControls.addEventListener("dragging-changed", (event) => {
      // Disable orbit controls while dragging
      orbitControls.enabled = !event.value;
    });
    
    transformControls.addEventListener("change", () => {
      renderer.render(scene, camera);
    });
    
    // Keyboard shortcuts
    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.
    
    ```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 object
    const 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 plane
    const 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
      }
    }
    
    // Usage
    const 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 raycasting
    const 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
    
    ## Limitations
    - Use this skill only when the task clearly matches the scope described above.
    - Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
    - Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related