[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill-6564cec3-e375-48d1-b3e4-c428434be5a6":3,"$f479H9bMZxVTxVB2DPXDEoP18-jMTSTMLjsg0RFjfU-8":43},{"id":4,"title":5,"description":6,"categoryId":7,"moduleId":8,"tags":9,"prompt":10,"icon":11,"source":12,"sourceUrl":13,"authorId":14,"authorName":15,"isPublic":16,"stars":17,"runs":18,"createdAt":19,"updatedAt":19,"module":20,"category":27,"packages":34},"6564cec3-e375-48d1-b3e4-c428434be5a6","threejs-interaction","Three.js交互 - 光线投射、控制、鼠标\u002F触摸输入、对象选择。用于处理用户输入、实现点击检测、添加相机控制或创建交互式3D体验。","cat_design_graphic","mod_design","sickn33,design","---\nname: threejs-interaction\ndescription: Three.js interaction - raycasting, controls, mouse\u002Ftouch input, object selection. Use when handling user input, implementing click detection, adding camera controls, or creating interactive 3D experiences.\nrisk: unknown\nsource: community\n---\n\n# Three.js Interaction\n\n## When to Use\n- You need user interaction inside a Three.js scene.\n- The task involves raycasting, object picking, pointer handling, touch input, or camera controls.\n- You are building an interactive 3D experience rather than a passive render.\n\n## Quick Start\n\n```javascript\nimport * as THREE from \"three\";\nimport { OrbitControls } from \"three\u002Faddons\u002Fcontrols\u002FOrbitControls.js\";\n\n\u002F\u002F Camera controls\nconst controls = new OrbitControls(camera, renderer.domElement);\ncontrols.enableDamping = true;\n\n\u002F\u002F Raycasting for click detection\nconst raycaster = new THREE.Raycaster();\nconst mouse = new THREE.Vector2();\n\nfunction onClick(event) {\n  mouse.x = (event.clientX \u002F window.innerWidth) * 2 - 1;\n  mouse.y = -(event.clientY \u002F window.innerHeight) * 2 + 1;\n\n  raycaster.setFromCamera(mouse, camera);\n  const intersects = raycaster.intersectObjects(scene.children);\n\n  if (intersects.length > 0) {\n    console.log(\"Clicked:\", intersects[0].object);\n  }\n}\n\nwindow.addEventListener(\"click\", onClick);\n```\n\n## Raycaster\n\n### Basic Raycasting\n\n```javascript\nconst raycaster = new THREE.Raycaster();\n\n\u002F\u002F From camera (mouse picking)\nraycaster.setFromCamera(mousePosition, camera);\n\n\u002F\u002F From any origin and direction\nraycaster.set(origin, direction); \u002F\u002F origin: Vector3, direction: normalized Vector3\n\n\u002F\u002F Get intersections\nconst intersects = raycaster.intersectObjects(objects, recursive);\n\n\u002F\u002F intersects array contains:\n\u002F\u002F {\n\u002F\u002F   distance: number,          \u002F\u002F Distance from ray origin\n\u002F\u002F   point: Vector3,            \u002F\u002F Intersection point in world coords\n\u002F\u002F   face: Face3,               \u002F\u002F Intersected face\n\u002F\u002F   faceIndex: number,         \u002F\u002F Face index\n\u002F\u002F   object: Object3D,          \u002F\u002F Intersected object\n\u002F\u002F   uv: Vector2,               \u002F\u002F UV coordinates at intersection\n\u002F\u002F   uv1: Vector2,              \u002F\u002F Second UV channel\n\u002F\u002F   normal: Vector3,           \u002F\u002F Interpolated face normal\n\u002F\u002F   instanceId: number         \u002F\u002F For InstancedMesh\n\u002F\u002F }\n```\n\n### Mouse Position Conversion\n\n```javascript\nconst mouse = new THREE.Vector2();\n\nfunction updateMouse(event) {\n  \u002F\u002F For full window\n  mouse.x = (event.clientX \u002F window.innerWidth) * 2 - 1;\n  mouse.y = -(event.clientY \u002F window.innerHeight) * 2 + 1;\n}\n\n\u002F\u002F For specific canvas element\nfunction updateMouseCanvas(event, canvas) {\n  const rect = canvas.getBoundingClientRect();\n  mouse.x = ((event.clientX - rect.left) \u002F rect.width) * 2 - 1;\n  mouse.y = -((event.clientY - rect.top) \u002F rect.height) * 2 + 1;\n}\n```\n\n### Touch Support\n\n```javascript\nfunction onTouchStart(event) {\n  event.preventDefault();\n\n  if (event.touches.length === 1) {\n    const touch = event.touches[0];\n    mouse.x = (touch.clientX \u002F window.innerWidth) * 2 - 1;\n    mouse.y = -(touch.clientY \u002F window.innerHeight) * 2 + 1;\n\n    raycaster.setFromCamera(mouse, camera);\n    const intersects = raycaster.intersectObjects(clickableObjects);\n\n    if (intersects.length > 0) {\n      handleSelection(intersects[0]);\n    }\n  }\n}\n\nrenderer.domElement.addEventListener(\"touchstart\", onTouchStart);\n```\n\n### Raycaster Options\n\n```javascript\nconst raycaster = new THREE.Raycaster();\n\n\u002F\u002F Near\u002Ffar clipping (default: 0, Infinity)\nraycaster.near = 0;\nraycaster.far = 100;\n\n\u002F\u002F Line\u002FPoints precision\nraycaster.params.Line.threshold = 0.1;\nraycaster.params.Points.threshold = 0.1;\n\n\u002F\u002F Layers (only intersect objects on specific layers)\nraycaster.layers.set(1);\n```\n\n### Efficient Raycasting\n\n```javascript\n\u002F\u002F Only check specific objects\nconst clickables = [mesh1, mesh2, mesh3];\nconst intersects = raycaster.intersectObjects(clickables, false);\n\n\u002F\u002F Use layers for filtering\nmesh1.layers.set(1); \u002F\u002F Clickable layer\nraycaster.layers.set(1);\n\n\u002F\u002F Throttle raycast for hover effects\nlet lastRaycast = 0;\nfunction onMouseMove(event) {\n  const now = Date.now();\n  if (now - lastRaycast \u003C 50) return; \u002F\u002F 20fps max\n  lastRaycast = now;\n\n  \u002F\u002F Raycast here\n}\n```\n\n## Camera Controls\n\n### OrbitControls\n\n```javascript\nimport { OrbitControls } from \"three\u002Faddons\u002Fcontrols\u002FOrbitControls.js\";\n\nconst controls = new OrbitControls(camera, renderer.domElement);\n\n\u002F\u002F Damping (smooth movement)\ncontrols.enableDamping = true;\ncontrols.dampingFactor = 0.05;\n\n\u002F\u002F Rotation limits\ncontrols.minPolarAngle = 0; \u002F\u002F Top\ncontrols.maxPolarAngle = Math.PI \u002F 2; \u002F\u002F Horizon\ncontrols.minAzimuthAngle = -Math.PI \u002F 4; \u002F\u002F Left\ncontrols.maxAzimuthAngle = Math.PI \u002F 4; \u002F\u002F Right\n\n\u002F\u002F Zoom limits\ncontrols.minDistance = 2;\ncontrols.maxDistance = 50;\n\n\u002F\u002F Enable\u002Fdisable features\ncontrols.enableRotate = true;\ncontrols.enableZoom = true;\ncontrols.enablePan = true;\n\n\u002F\u002F Auto-rotate\ncontrols.autoRotate = true;\ncontrols.autoRotateSpeed = 2.0;\n\n\u002F\u002F Target (orbit point)\ncontrols.target.set(0, 1, 0);\n\n\u002F\u002F Update in animation loop\nfunction animate() {\n  controls.update(); \u002F\u002F Required for damping and auto-rotate\n  renderer.render(scene, camera);\n}\n```\n\n#### OrbitControls Programmatic Methods (r183)\n\n```javascript\n\u002F\u002F Programmatic camera movement\ncontrols.dolly(1.5); \u002F\u002F Dolly in\u002Fout (zoom for perspective cameras)\ncontrols.pan(deltaX, deltaY); \u002F\u002F Pan the camera\ncontrols.rotate(deltaAzimuth, deltaPolar); \u002F\u002F Rotate around target\n\n\u002F\u002F Cursor style (r183)\ncontrols.cursorStyle = { orbit: \"grab\", pan: \"move\", dolly: \"zoom-in\" };\n```\n\n### FlyControls\n\n```javascript\nimport { FlyControls } from \"three\u002Faddons\u002Fcontrols\u002FFlyControls.js\";\n\nconst controls = new FlyControls(camera, renderer.domElement);\ncontrols.movementSpeed = 10;\ncontrols.rollSpeed = Math.PI \u002F 24;\ncontrols.dragToLook = true;\n\n\u002F\u002F Update with delta\nfunction animate() {\n  controls.update(clock.getDelta());\n  renderer.render(scene, camera);\n}\n```\n\n### FirstPersonControls\n\n```javascript\nimport { FirstPersonControls } from \"three\u002Faddons\u002Fcontrols\u002FFirstPersonControls.js\";\n\nconst controls = new FirstPersonControls(camera, renderer.domElement);\ncontrols.movementSpeed = 10;\ncontrols.lookSpeed = 0.1;\ncontrols.lookVertical = true;\ncontrols.constrainVertical = true;\ncontrols.verticalMin = Math.PI \u002F 4;\ncontrols.verticalMax = (Math.PI * 3) \u002F 4;\n\nfunction animate() {\n  controls.update(clock.getDelta());\n}\n```\n\n### PointerLockControls\n\n```javascript\nimport { PointerLockControls } from \"three\u002Faddons\u002Fcontrols\u002FPointerLockControls.js\";\n\nconst controls = new PointerLockControls(camera, document.body);\n\n\u002F\u002F Lock pointer on click\ndocument.addEventListener(\"click\", () => {\n  controls.lock();\n});\n\ncontrols.addEventListener(\"lock\", () => {\n  console.log(\"Pointer locked\");\n});\n\ncontrols.addEventListener(\"unlock\", () => {\n  console.log(\"Pointer unlocked\");\n});\n\n\u002F\u002F Movement\nconst velocity = new THREE.Vector3();\nconst direction = new THREE.Vector3();\nconst moveForward = false;\nconst moveBackward = false;\n\ndocument.addEventListener(\"keydown\", (event) => {\n  switch (event.code) {\n    case \"KeyW\":\n      moveForward = true;\n      break;\n    case \"KeyS\":\n      moveBackward = true;\n      break;\n  }\n});\n\nfunction animate() {\n  if (controls.isLocked) {\n    direction.z = Number(moveForward) - Number(moveBackward);\n    direction.normalize();\n\n    velocity.z -= direction.z * 0.1;\n    velocity.z *= 0.9; \u002F\u002F Friction\n\n    controls.moveForward(-velocity.z);\n  }\n}\n```\n\n### TrackballControls\n\n```javascript\nimport { TrackballControls } from \"three\u002Faddons\u002Fcontrols\u002FTrackballControls.js\";\n\nconst controls = new TrackballControls(camera, renderer.domElement);\ncontrols.rotateSpeed = 2.0;\ncontrols.zoomSpeed = 1.2;\ncontrols.panSpeed = 0.8;\ncontrols.staticMoving = true;\n\nfunction animate() {\n  controls.update();\n}\n```\n\n### MapControls\n\n```javascript\nimport { MapControls } from \"three\u002Faddons\u002Fcontrols\u002FMapControls.js\";\n\nconst controls = new MapControls(camera, renderer.domElement);\ncontrols.enableDamping = true;\ncontrols.dampingFactor = 0.05;\ncontrols.screenSpacePanning = false;\ncontrols.maxPolarAngle = Math.PI \u002F 2;\n```\n\n## TransformControls\n\nGizmo for moving\u002Frotating\u002Fscaling objects.\n\n```javascript\nimport { TransformControls } from \"three\u002Faddons\u002Fcontrols\u002FTransformControls.js\";\n\nconst transformControls = new TransformControls(camera, renderer.domElement);\nscene.add(transformControls);\n\n\u002F\u002F Attach to object\ntransformControls.attach(selectedMesh);\n\n\u002F\u002F Switch modes\ntransformControls.setMode(\"translate\"); \u002F\u002F 'translate', 'rotate', 'scale'\n\n\u002F\u002F Change space\ntransformControls.setSpace(\"local\"); \u002F\u002F 'local', 'world'\n\n\u002F\u002F Size\ntransformControls.setSize(1);\n\n\u002F\u002F Events\ntransformControls.addEventListener(\"dragging-changed\", (event) => {\n  \u002F\u002F Disable orbit controls while dragging\n  orbitControls.enabled = !event.value;\n});\n\ntransformControls.addEventListener(\"change\", () => {\n  renderer.render(scene, camera);\n});\n\n\u002F\u002F Keyboard shortcuts\nwindow.addEventListener(\"keydown\", (event) => {\n  switch (event.key) {\n    case \"g\":\n      transformControls.setMode(\"translate\");\n      break;\n    case \"r\":\n      transformControls.setMode(\"rotate\");\n      break;\n    case \"s\":\n      transformControls.setMode(\"scale\");\n      break;\n    case \"Escape\":\n      transformControls.detach();\n      break;\n  }\n});\n```\n\n## DragControls\n\nDrag objects directly.\n\n```javascript\nimport { DragControls } from \"three\u002Faddons\u002Fcontrols\u002FDragControls.js\";\n\nconst draggableObjects = [mesh1, mesh2, mesh3];\nconst dragControls = new DragControls(\n  draggableObjects,\n  camera,\n  renderer.domElement,\n);\n\ndragControls.addEventListener(\"dragstart\", (event) => {\n  orbitControls.enabled = false;\n  event.object.material.emissive.set(0xaaaaaa);\n});\n\ndragControls.addEventListener(\"drag\", (event) => {\n  \u002F\u002F Constrain to ground plane\n  event.object.position.y = 0;\n});\n\ndragControls.addEventListener(\"dragend\", (event) => {\n  orbitControls.enabled = true;\n  event.object.material.emissive.set(0x000000);\n});\n```\n\n## Selection System\n\n### Click to Select\n\n```javascript\nconst raycaster = new THREE.Raycaster();\nconst mouse = new THREE.Vector2();\nlet selectedObject = null;\n\nfunction onMouseDown(event) {\n  mouse.x = (event.clientX \u002F window.innerWidth) * 2 - 1;\n  mouse.y = -(event.clientY \u002F window.innerHeight) * 2 + 1;\n\n  raycaster.setFromCamera(mouse, camera);\n  const intersects = raycaster.intersectObjects(selectableObjects);\n\n  \u002F\u002F Deselect previous\n  if (selectedObject) {\n    selectedObject.material.emissive.set(0x000000);\n  }\n\n  \u002F\u002F Select new\n  if (intersects.length > 0) {\n    selectedObject = intersects[0].object;\n    selectedObject.material.emissive.set(0x444444);\n  } else {\n    selectedObject = null;\n  }\n}\n```\n\n### Box Selection\n\n```javascript\nimport { SelectionBox } from \"three\u002Faddons\u002Finteractive\u002FSelectionBox.js\";\nimport { SelectionHelper } from \"three\u002Faddons\u002Finteractive\u002FSelectionHelper.js\";\n\nconst selectionBox = new SelectionBox(camera, scene);\nconst selectionHelper = new SelectionHelper(renderer, \"selectBox\"); \u002F\u002F CSS class\n\ndocument.addEventListener(\"pointerdown\", (event) => {\n  selectionBox.startPoint.set(\n    (event.clientX \u002F window.innerWidth) * 2 - 1,\n    -(event.clientY \u002F window.innerHeight) * 2 + 1,\n    0.5,\n  );\n});\n\ndocument.addEventListener(\"pointermove\", (event) => {\n  if (selectionHelper.isDown) {\n    selectionBox.endPoint.set(\n      (event.clientX \u002F window.innerWidth) * 2 - 1,\n      -(event.clientY \u002F window.innerHeight) * 2 + 1,\n      0.5,\n    );\n  }\n});\n\ndocument.addEventListener(\"pointerup\", (event) => {\n  selectionBox.endPoint.set(\n    (event.clientX \u002F window.innerWidth) * 2 - 1,\n    -(event.clientY \u002F window.innerHeight) * 2 + 1,\n    0.5,\n  );\n\n  const selected = selectionBox.select();\n  console.log(\"Selected objects:\", selected);\n});\n```\n\n### Hover Effects\n\n```javascript\nconst raycaster = new THREE.Raycaster();\nconst mouse = new THREE.Vector2();\nlet hoveredObject = null;\n\nfunction onMouseMove(event) {\n  mouse.x = (event.clientX \u002F window.innerWidth) * 2 - 1;\n  mouse.y = -(event.clientY \u002F window.innerHeight) * 2 + 1;\n\n  raycaster.setFromCamera(mouse, camera);\n  const intersects = raycaster.intersectObjects(hoverableObjects);\n\n  \u002F\u002F Reset previous hover\n  if (hoveredObject) {\n    hoveredObject.material.color.set(hoveredObject.userData.originalColor);\n    document.body.style.cursor = \"default\";\n  }\n\n  \u002F\u002F Apply new hover\n  if (intersects.length > 0) {\n    hoveredObject = intersects[0].object;\n    if (!hoveredObject.userData.originalColor) {\n      hoveredObject.userData.originalColor =\n        hoveredObject.material.color.getHex();\n    }\n    hoveredObject.material.color.set(0xff6600);\n    document.body.style.cursor = \"pointer\";\n  } else {\n    hoveredObject = null;\n  }\n}\n\nwindow.addEventListener(\"mousemove\", onMouseMove);\n```\n\n## Keyboard Input\n\n```javascript\nconst keys = {};\n\ndocument.addEventListener(\"keydown\", (event) => {\n  keys[event.code] = true;\n});\n\ndocument.addEventListener(\"keyup\", (event) => {\n  keys[event.code] = false;\n});\n\nfunction update() {\n  const speed = 0.1;\n\n  if (keys[\"KeyW\"]) player.position.z -= speed;\n  if (keys[\"KeyS\"]) player.position.z += speed;\n  if (keys[\"KeyA\"]) player.position.x -= speed;\n  if (keys[\"KeyD\"]) player.position.x += speed;\n  if (keys[\"Space\"]) player.position.y += speed;\n  if (keys[\"ShiftLeft\"]) player.position.y -= speed;\n}\n```\n\n## World-Screen Coordinate Conversion\n\n### World to Screen\n\n```javascript\nfunction worldToScreen(position, camera) {\n  const vector = position.clone();\n  vector.project(camera);\n\n  return {\n    x: ((vector.x + 1) \u002F 2) * window.innerWidth,\n    y: (-(vector.y - 1) \u002F 2) * window.innerHeight,\n  };\n}\n\n\u002F\u002F Position HTML element over 3D object\nconst screenPos = worldToScreen(mesh.position, camera);\nelement.style.left = screenPos.x + \"px\";\nelement.style.top = screenPos.y + \"px\";\n```\n\n### Screen to World\n\n```javascript\nfunction screenToWorld(screenX, screenY, camera, targetZ = 0) {\n  const vector = new THREE.Vector3(\n    (screenX \u002F window.innerWidth) * 2 - 1,\n    -(screenY \u002F window.innerHeight) * 2 + 1,\n    0.5,\n  );\n\n  vector.unproject(camera);\n\n  const dir = vector.sub(camera.position).normalize();\n  const distance = (targetZ - camera.position.z) \u002F dir.z;\n\n  return camera.position.clone().add(dir.multiplyScalar(distance));\n}\n```\n\n### Ray-Plane Intersection\n\n```javascript\nfunction getRayPlaneIntersection(mouse, camera, plane) {\n  const raycaster = new THREE.Raycaster();\n  raycaster.setFromCamera(mouse, camera);\n\n  const intersection = new THREE.Vector3();\n  raycaster.ray.intersectPlane(plane, intersection);\n\n  return intersection;\n}\n\n\u002F\u002F Ground plane\nconst groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);\nconst worldPos = getRayPlaneIntersection(mouse, camera, groundPlane);\n```\n\n## Event Handling Best Practices\n\n```javascript\nclass InteractionManager {\n  constructor(camera, renderer, scene) {\n    this.camera = camera;\n    this.renderer = renderer;\n    this.scene = scene;\n    this.raycaster = new THREE.Raycaster();\n    this.mouse = new THREE.Vector2();\n    this.clickables = [];\n\n    this.bindEvents();\n  }\n\n  bindEvents() {\n    const canvas = this.renderer.domElement;\n\n    canvas.addEventListener(\"click\", (e) => this.onClick(e));\n    canvas.addEventListener(\"mousemove\", (e) => this.onMouseMove(e));\n    canvas.addEventListener(\"touchstart\", (e) => this.onTouchStart(e));\n  }\n\n  updateMouse(event) {\n    const rect = this.renderer.domElement.getBoundingClientRect();\n    this.mouse.x = ((event.clientX - rect.left) \u002F rect.width) * 2 - 1;\n    this.mouse.y = -((event.clientY - rect.top) \u002F rect.height) * 2 + 1;\n  }\n\n  getIntersects() {\n    this.raycaster.setFromCamera(this.mouse, this.camera);\n    return this.raycaster.intersectObjects(this.clickables, true);\n  }\n\n  onClick(event) {\n    this.updateMouse(event);\n    const intersects = this.getIntersects();\n\n    if (intersects.length > 0) {\n      const object = intersects[0].object;\n      if (object.userData.onClick) {\n        object.userData.onClick(intersects[0]);\n      }\n    }\n  }\n\n  addClickable(object, callback) {\n    this.clickables.push(object);\n    object.userData.onClick = callback;\n  }\n\n  dispose() {\n    \u002F\u002F Remove event listeners\n  }\n}\n\n\u002F\u002F Usage\nconst interaction = new InteractionManager(camera, renderer, scene);\ninteraction.addClickable(mesh, (intersect) => {\n  console.log(\"Clicked at:\", intersect.point);\n});\n```\n\n## Performance Tips\n\n1. **Limit raycasts**: Throttle mousemove handlers\n2. **Use layers**: Filter raycast targets\n3. **Simple collision meshes**: Use invisible simpler geometry for raycasting\n4. **Disable controls when not needed**: `controls.enabled = false`\n5. **Batch updates**: Group interaction checks\n\n```javascript\n\u002F\u002F Use simpler geometry for raycasting\nconst complexMesh = loadedModel;\nconst collisionMesh = new THREE.Mesh(\n  new THREE.BoxGeometry(1, 1, 1),\n  new THREE.MeshBasicMaterial({ visible: false }),\n);\ncollisionMesh.userData.target = complexMesh;\nclickables.push(collisionMesh);\n```\n\n## See Also\n\n- `threejs-fundamentals` - Camera and scene setup\n- `threejs-animation` - Animating interactions\n- `threejs-shaders` - Visual feedback effects\n\n## Limitations\n- Use this skill only when the task clearly matches the scope described above.\n- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.\n- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.\n","","imported","https:\u002F\u002Fgithub.com\u002Fsickn33\u002Fantigravity-awesome-skills","user_system_seed","SkillOPIC",true,92,1826,"2026-05-16 13:44:08",{"id":8,"name":21,"slug":22,"icon":23,"description":24,"sort":25,"createdAt":26},"设计创意","design","mdi-palette-outline","UI 设计、生成艺术、品牌视觉等创意 Skill",3,"2026-05-16 12:53:40",{"id":7,"name":28,"slug":29,"icon":30,"description":31,"moduleId":8,"sort":32,"skillCount":33,"createdAt":26},"视觉创意","graphic","mdi-brush","海报、Logo、插画等视觉创作",2,48,[35],{"id":36,"skillId":4,"version":37,"fileName":38,"fileSize":39,"filePath":40,"fileHash":41,"manifest":42,"createdAt":19},"e4f35d54-d79c-44fa-931e-aa207525228a","1.0.0","threejs-interaction.zip",4798,"uploads\u002Fskills\u002F6564cec3-e375-48d1-b3e4-c428434be5a6\u002Fthreejs-interaction.zip","50b630a74408de2563bbf27846f9b4a7d40a84555b2d5685f0ff5f3126cb0475","[{\"path\":\"SKILL.md\",\"isDirectory\":false,\"size\":17294}]",{"code":44,"message":45,"data":46},200,"success",{"items":47,"stats":48,"page":51},[],{"averageRating":49,"totalRatings":49,"ratingCounts":50},0,[49,49,49,49,49],{"limit":52,"offset":49,"hasMore":53,"nextOffset":52,"ratedOnly":16},15,false]