
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.
- Installs
- 1.1K
- Last 24h
- +13
- Trending rank
- #7503
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/calesthio/openmontage --skill threejs-interaction- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads threejs-interaction automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
Three.js Interaction
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);
}
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
- Limit raycasts: Throttle mousemove handlers
- Use layers: Filter raycast targets
- Simple collision meshes: Use invisible simpler geometry for raycasting
- Disable controls when not needed:
controls.enabled = false - 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 setupthreejs-animation- Animating interactionsthreejs-shaders- Visual feedback effects
FAQ
What does the threejs-interaction skill do?
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.
How do I install threejs-interaction?
Run npx skills add https://github.com/calesthio/openmontage --skill threejs-interaction in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/threejs-interaction/ (personal) or .claude/skills/threejs-interaction/ (project).
Which AI agents can use threejs-interaction?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is threejs-interaction safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains threejs-interaction?
It is published by calesthio in calesthio/openmontage. It has 1,053 recorded installs.
More from calesthio/openmontage
video-edit
calesthio/openmontage
Edit videos locally using ffmpeg. Trim, concat, resize, speed, overlay, extract audio, compress, and convert. Use when: (1) Trimming or cutting video segments, (2) Concatenating multiple clips, (3) Resizing video for social platforms, (4) Extracting or replacing audio, (5) Compressing video, (6) Converting video formats, (7) Getting video info.
video-understand
calesthio/openmontage
Understand video content locally using ffmpeg frame extraction and Whisper transcription. No API keys needed. Use when: (1) Understanding what a video contains, (2) Transcribing video audio locally, (3) Extracting key frames for visual analysis, (4) Getting video content without API keys.
svg-character-animation
calesthio/openmontage
Animate SVG character rigs with GSAP, CSS transforms, Remotion frame control, and HyperFrames-compatible browser previews.
ai-video-gen
calesthio/openmontage
Generate AI videos from text prompts using multiple provider gateways. Use when: (1) Generating videos from text descriptions, (2) Creating AI-generated video clips for content production, (3) Image-to-video generation with a reference image, (4) Choosing between video generation providers (VEO, Kling, Sora, Runway, Seedance, MiniMax, Gemini Omni). Supports gateways: HeyGen API, fal.ai API, Kling official direct API, and the Gemini API (Gemini Omni Flash).
video-download
calesthio/openmontage
Download video and audio from YouTube and 1000+ sites using yt-dlp. No API keys needed. Use when: (1) Downloading a video from YouTube or other sites, (2) Extracting audio from a video URL, (3) Downloading subtitles/captions from a video, (4) Getting video metadata without downloading.
remotion
calesthio/openmontage
Toolkit-specific Remotion patterns — custom transitions, shared components, and project conventions. For core Remotion framework knowledge (hooks, animations, rendering, etc.), see the remotion-official skill.
Popular media & creative skills
hyperframes
heygen-com/hyperframes
Read first to make, edit, animate or render any video with HyperFrames (HTML rendered to video), including launch films, explainers, music videos, charts, maps, vertical clips, title cards, captions and overlays. Holds the render contract, the work loop and what each kind of video needs.
media-use
heygen-com/hyperframes
Get or make any media for a HyperFrames video - music, sound effects, images, icons, logos, voiceover, avatar and generated video, transcripts, cutouts, colour grades and looks - with HeyGen or free local tools, and mix placed audio. Also for footage that looks dark, flat or dull, or should be stylized or hidden.
hyperframes-registry
heygen-com/hyperframes
About 450 ready-made HyperFrames blocks and components installed as editable source - transitions, overlays, titles, kinetic type, captions, charts, map overlays, 3D, camera moves. Use it before hand-building any named visual (CRT scanlines, glitch, film grain, a chart); also for hyperframes add and catalog.
hyperframes-creative
heygen-com/hyperframes
Non-animation creative direction for HyperFrames videos. Use for design spec (frame.md / design.md) handling, palettes, typography, narration, beat planning, audio-reactive visuals, composition patterns, and brand / style decisions. For atomic motion patterns and scene blueprints, use hyperframes-animation.
ai-video-generation
101-skills/superpowers
Generate AI videos with Google Veo, Seedance 2.0, HappyHorse, Wan, Grok and 40+ models via inference.sh CLI. Models: Veo 3.1, Veo 3, Seedance 2.0, HappyHorse 1.0, Wan 2.5, Grok Imagine Video, OmniHuman, Fabric, HunyuanVideo. Capabilities: text-to-video, image-to-video, reference-to-video, video editing, lipsync, avatar animation, video upscaling, foley sound. Use for: social media videos, marketing content, explainer videos, product demos, AI avatars. Triggers: video generation, ai video, text to video, image to video, veo, animate image, video from image, ai animation, video generator, generate video, t2v, i2v, ai video maker, create video with ai, runway alternative, pika alternative, sora alternative, kling alternative, seedance, happyhorse
ai-image-generation
101-skills/superpowers
Generate AI images with GPT-Image-2, FLUX, Gemini, Grok, Seedream, Reve and 50+ models via inference.sh CLI. Models: GPT-Image-2, FLUX Dev LoRA, FLUX.2 Klein LoRA, Gemini 3 Pro Image, Grok Imagine, Seedream 4.5, Reve, ImagineArt. Capabilities: text-to-image, image-to-image, inpainting, LoRA, image editing, upscaling, text rendering. Use for: AI art, product mockups, concept art, social media graphics, marketing visuals, illustrations. Triggers: flux, image generation, ai image, text to image, stable diffusion, generate image, ai art, midjourney alternative, dall-e alternative, text2img, t2i, image generator, ai picture, create image with ai, generative ai, ai illustration, grok image, gemini image, gpt image, openai image, chatgpt image