Skills Agentes

Threejs Interaction

Interacción en Three.js: raycasting, controles, entrada de ratón y táctil, y selección de objetos. Úsalo al gestionar entrada del usuario, detectar clics o añadir controles de cámara.

Estrellas
49.5k

en todo el repo

Actividad
36

0–100, la ruta de este skill

Actualizado
hace 4 meses

último commit aquí

Commits
0

últimos 90 días

Contexto
4.1k tok

51 tok en reposo

Paquete
1 archivo

16 KB

Instalar

Funciona con cualquier agente que lea SKILL.md

npx -y skills add calesthio/OpenMontage --skill threejs-interaction --agent claude-code

Se instala solo en este repositorio.

Qué hace

  • Cubre `Raycaster` para detección de clics: conversión de posición del ratón, soporte táctil, opciones y raycasting eficiente
  • Documenta los controles de cámara: `OrbitControls`, `FlyControls`, `FirstPersonControls`, `PointerLockControls`, `TrackballControls` y `MapControls`
  • Trata `TransformControls` y `DragControls`
  • Monta un sistema de selección: clic, selección por caja y efectos de hover
  • Explica conversión entre coordenadas de mundo y pantalla, e intersección rayo-plano

Úsalo cuando

  • Gestionar entrada del usuario en una escena 3D
  • Implementar detección de clics sobre objetos
  • Añadir controles de cámara
  • Crear experiencias 3D interactivas

No lo uses cuando

    Qué lo activa

    Di cualquiera de estas frases y el agente debería cargar este skill.

    • Haz que se pueda hacer clic en estos objetos 3D
    • Añade OrbitControls a la escena
    • Permite arrastrar este objeto con el ratón
    • Convierte esta posición 3D a coordenadas de pantalla

    SKILL.md

    En inglés

    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

    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

    Reproducido de calesthio/OpenMontage bajo licencia AGPL-3.0. Leer esta página en markdown.

    Archivos

    1 archivo en el paquete. Solo se lee SKILL.md al activarse — las referencias se cargan si el skill decide que las necesita.

    Detalles

    Creador
    calesthio
    Categoría
    Diseño y UI
    Licencia
    AGPL-3.0
    Recursos incluidos
    Solo SKILL.md
    Código fuente
    Ver SKILL.md

    Etiquetas

    Más de calesthio/OpenMontage

    Este repo incluye 89 skills. Si instalas uno, normalmente ya tienes los demás.

    Comfyui

    49.5k

    Úsalo al trabajar con workflows de ComfyUI en OpenMontage: comfyui_image/video/music, workflows propios, selección de output_node, modelos que faltan, LoRAs, poca VRAM e importación de workflows de la comunidad.

    Costo de contexto al activarse
    1.9k tok
    Tamaño del paquete
    1 archivo
    Última actualización
    hace 9 días
    diseno ui

    Genera vídeo cinematográfico de 4-30 s con ByteDance Seedance 2.5 por fal.ai, Volcengine Ark, Runway o ComfyUI. Cubre el contrato de prompt 2.5, cortes duros, locks de continuidad y voz.

    Costo de contexto al activarse
    3.2k tok
    Tamaño del paquete
    2 archivos
    Última actualización
    hace 4 días
    diseno ui

    Genera narración expresiva y multilingüe con fish.audio (modelos S1 / S2) y reutiliza voces clonadas mediante reference_id.

    Costo de contexto al activarse
    1.4k tok
    Tamaño del paquete
    1 archivo
    Última actualización
    hace 9 días
    diseno ui

    Genera y edita conversacionalmente vídeos cortos con Google Gemini Omni Flash: itera con ediciones en lenguaje natural, clips de 3-10s a 720p con audio y texto en pantalla, e imágenes de referencia por etiquetas.

    Costo de contexto al activarse
    2.1k tok
    Tamaño del paquete
    1 archivo
    Última actualización
    hace 9 días
    Permisos
    diseno ui

    Genera vídeo con MiniMax H3 (Hailuo 3.0) por la API oficial v2, fal.ai, Runway, nodos partner de ComfyUI o pesos abiertos locales. Clips de 4-15s a 2K con animación de primer/último fotograma.

    Costo de contexto al activarse
    582 tok
    Tamaño del paquete
    1 archivo
    Última actualización
    hace 9 días
    diseno ui

    Genera, reconstruye, inspecciona y enruta activos 3D de producción para mundos de OpenMontage con Atlas Cloud, fal.ai, catálogos con licencia y Blender.

    Costo de contexto al activarse
    1.2k tok
    Tamaño del paquete
    2 archivos
    Última actualización
    hace 9 días
    diseno ui

    Skills relacionados

    Genera, reconstruye, inspecciona y enruta activos 3D de producción para mundos de OpenMontage con Atlas Cloud, fal.ai, catálogos con licencia y Blender.

    Costo de contexto al activarse
    1.2k tok
    Tamaño del paquete
    2 archivos
    Última actualización
    hace 9 días
    diseno ui

    Acestep

    49.5k

    Generación musical con ACE-Step 1.5: música de fondo, pistas con voz, versiones y extracción de stems para producción de vídeo.

    Costo de contexto al activarse
    2.3k tok
    Tamaño del paquete
    1 archivo
    Última actualización
    hace 4 meses
    diseno ui

    Genera vídeos con IA desde texto usando varias pasarelas — HeyGen, fal.ai, Kling y Gemini — con soporte de imagen a vídeo y comparación entre VEO, Kling, Sora, Runway, Seedance y MiniMax.

    Costo de contexto al activarse
    3k tok
    Tamaño del paquete
    1 archivo
    Última actualización
    el mes pasado
    diseno ui