Reorganize: Move all skills to skills/ folder
- Created skills/ directory - Moved 272 skills to skills/ subfolder - Kept agents/ at root level - Kept installation scripts and docs at root level Repository structure: - skills/ - All 272 skills from skills.sh - agents/ - Agent definitions - *.sh, *.ps1 - Installation scripts - README.md, etc. - Documentation Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
660
skills/threejs-interaction/skill.md
Normal file
660
skills/threejs-interaction/skill.md
Normal file
@@ -0,0 +1,660 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
# Three.js Interaction
|
||||
|
||||
## 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);
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
Reference in New Issue
Block a user