Agent Skills
Discover and share powerful Agent Skills for AI assistants
threejs-interaction - Agent Skill - Agent Skills
Home/ Skills / 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.
Use the skills CLI to install this skill with one command. Auto-detects all installed AI assistants.
Method 1 - skills CLI
npx skills i CloudAI-X/threejs-skills/skills/threejs-interaction CopyMethod 2 - openskills (supports sync & update)
npx openskills install CloudAI-X/threejs-skills CopyAuto-detects Claude Code, Cursor, Codex CLI, Gemini CLI, and more. One install, works everywhere.
Installation Path
Download and extract to one of the following locations:
Claude Code Cursor OpenCode Gemini CLI Codex CLI
~/.claude/skills/threejs-interaction/ Back No setup needed. Let our cloud agents run this skill for you.
Select Model
Claude Haiku 4.5 $0.10 Claude Sonnet 4.5 $0.20 Claude Opus 4.5 $0.50 Claude Sonnet 4.5 $0.20 /task
Best for coding tasks
Try NowNo setup required
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.
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
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
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 (
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 =
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
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.
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.
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 ();
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.
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 ;
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
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 ) =>
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 /
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,
Hover Effects
const raycaster = new THREE . Raycaster ();
const mouse = new THREE . Vector2 ();
let hoveredObject = null ;
function onMouseMove ( event ) {
const keys = {};
document. addEventListener ( "keydown" , ( event ) => {
keys[event.code] = true ;
});
document. addEventListener ( "keyup" , ( event ) => {
keys[event.code] = false ;
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 )
Screen to World
function screenToWorld ( screenX , screenY , camera , targetZ = 0 ) {
const vector = new THREE . Vector3 (
(screenX / window.innerWidth) * 2 - 1 ,
- (screenY / window.innerHeight) *
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);
Event Handling Best Practices
class InteractionManager {
constructor ( camera , renderer , scene ) {
this .camera = camera;
this .renderer
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 =
See Also
threejs-fundamentals - Camera and scene setup
threejs-animation - Animating interactions
threejs-shaders - Visual feedback effects
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
// }
+
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.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);
1
);
0
;
function onMouseMove ( event ) {
const now = Date. now ();
if (now - lastRaycast < 50 ) return ; // 20fps max
lastRaycast = now;
// Raycast here
}
=
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);
}
update
(clock.
getDelta
());
renderer. render (scene, camera);
}
PI
/
4
;
controls.verticalMax = (Math. PI * 3 ) / 4 ;
function animate () {
controls. update (clock. getDelta ());
}
});
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);
}
}
update
();
}
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 ;
}
});
{
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 );
});
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 ;
}
}
"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);
});
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);
});
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;
}
/
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" ;
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));
}
return
intersection;
}
// Ground plane
const groundPlane = new THREE . Plane ( new THREE . Vector3 ( 0 , 1 , 0 ), 0 );
const worldPos = getRayPlaneIntersection (mouse, camera, groundPlane);
=
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);
});
complexMesh;
clickables. push (collisionMesh);