其他
Three.js 3D 交互开发:用射线拾取…
在 Three.js 项目中需要处理鼠标、触摸、键盘输入,并实现点击拾取、悬停、选择、相机控制和对象拖拽时使用。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 35/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 39/100 ×0.15
适用场景
Three.js 3D 交互开发:用射线特定
解决思路
- 初始化时引入 three 和 OrbitControls,创建 `new OrbitControls(camera, renderer.domElement)`,设置 `controls.enableDamping = true`。
- 创建 `new THREE.Raycaster()` 和 `new THREE.Vector2()` 保存鼠标坐标。
- 点击检测:把 `event.clientX/clientY` 转成 NDC:`mouse.x = (clientX / innerWidth) * 2 - 1`,`mouse.y = -(clientY / innerHeight) * 2 + 1`。
- 调用 `raycaster.setFromCamera(mouse, camera)`,再 `raycaster.intersectObjects(scene.children)`;若 `intersects.length > 0`,处理 `intersects[0].object`。
- 给 `window` 或 canvas 添加 `click` 监听,在回调中执行上述射线检测。
- 基础 Raycaster 也可用 `raycaster.set(origin, direction)`,其中 `origin` 是 `Vector3`,`direction` 是归一化 `Vector3`。
- 用 `raycaster.intersectObjects(objects, recursive)` 获取交点数组;交点包含 `distance`、`point`、`face`、`faceIndex`、`object`、`uv`、`uv1`、`normal`、`instanceId` 等字段。
- 鼠标坐标转换分两种:全窗口用 `innerWidth/innerHeight`;指定 canvas 用 `canvas.getBoundingClientRect()`,按 `rect.left/rect.top/rect.width/rect.height` 计算。
- 触摸支持:在 `renderer.domElement` 监听 `touchstart`,先 `event.preventDefault()`;若 `event.touches.length === 1`,用 `touch.clientX/clientY` 转 NDC,再 `setFromCamera` 和 `intersectObjects(clickableObjects)`,命中则处理 `intersects[0]`。
- Raycaster 可设置 `near` / `far`(默认 0 到 Infinity)做距离裁剪。
- 对线和点设置精度:`raycaster.params.Line.threshold = 0.1`,`raycaster.params.Points.threshold = 0.1`。
- 用 layers 过滤:给对象或 raycaster 设置 `layers.set(1)`,只检测特定层对象。
- 高效 raycasting:只把需要检测的对象放进 `clickables` 数组,调用 `raycaster.intersectObjects(clickables, false)`,避免遍历整个场景。
- 悬停等 mousemove 射线检测要节流:记录 `lastRaycast = Date.now()`,若 `now - lastRaycast < 50` 直接返回,限制约 20fps。
- OrbitControls 常用配置:`enableDamping = true`、`dampingFactor = 0.05`、设置 `minPolarAngle/maxPolarAngle`、`minAzimuthAngle/maxAzimuthAngle`、`minDistance/maxDistance`。
- OrbitControls 可开关 `enableRotate`、`enableZoom`、`enablePan`,设置 `autoRotate` / `autoRotateSpeed`,用 `controls.target.set(0, 1, 0)` 指定环绕点。
- OrbitControls 必须在动画循环中调用 `controls.update()`,否则 damping 和 autoRotate 不生效。
- FlyControls:`new FlyControls(camera, renderer.domElement)`,设置 `movementSpeed`、`rollSpeed`、`dragToLook`,在 animate 中用 delta 调 `controls.update(clock.getDelta())`。
- FirstPersonControls:设置 `movementSpeed`、`lookSpeed`、`lookVertical`、`constrainVertical`、`verticalMin/verticalMax`,在 animate 中调 `controls.update(clock.getDelta())`。
- PointerLockControls:`new PointerLockControls(camera, document.body)`,点击时 `controls.lock()`;监听 `lock` / `unlock`。
- PointerLockControls 移动:维护 `velocity`、`direction` 和前后移动布尔值;`keydown` 设置 `KeyW/KeyS`,在 `controls.isLocked` 时计算 `direction.z`、归一化、更新 `velocity.z`(含摩擦 `velocity.z *= 0.9`),调 `controls.moveForward(-velocity.z)`。
- TrackballControls:设置 `rotateSpeed`、`zoomSpeed`、`panSpeed`、`staticMoving`,在 animate 中调 `controls.update()`。
- MapControls:设置 `enableDamping`、`dampingFactor`、`screenSpacePanning = false`、`maxPolarAngle = Math.PI / 2`。
- TransformControls:创建后 `scene.add(transformControls)`,用 `attach(selectedMesh)` 绑定对象,用 `setMode('translate'|'rotate'|'scale')` 切换模式。
- TransformControls 可 `setSpace('local'|'world')`、`setSize(1)`;监听 `dragging-changed`,拖拽时把 orbitControls 禁用:`orbitControls.enabled = !event.value`。
- TransformControls 监听 `change` 触发重渲染;可加键盘快捷键:`g` 平移、`r` 旋转、`s` 缩放、`Escape` 调 `detach()`。
- DragControls:`new DragControls(draggableObjects, camera, renderer.domElement)`;`dragstart` 禁用 orbitControls 并改 `material.emissive`,`drag` 中约束位置(如 `event.object.position.y = 0`),`dragend` 恢复 controls 和材质。
- 点击选择:在 `mousedown` 中射线检测 `selectableObjects`;先取消旧对象选中(`material.emissive.set(0x000000)`),再选中新对象(`material.emissive.set(0x444444)`),无命中则清空 `selectedObject`。
- 框选:引入 `SelectionBox` 和 `SelectionHelper`;`new SelectionBox(camera, scene)`、`new SelectionHelper(renderer, 'selectBox')`。
- 框选交互:`pointerdown` 设置 `selectionBox.startPoint`,`pointermove` 在 `selectionHelper.isDown` 时设置 `endPoint`,`pointerup` 设置 `endPoint` 后调用 `selectionBox.select()` 获取选中对象。
- 悬停效果:在 `mousemove` 中射线检测 `hoverableObjects`;先恢复旧对象颜色和 `cursor = 'default'`;命中新对象时保存 `userData.originalColor`(若未保存),把颜色设为 `0xff6600` 并把 `cursor = 'pointer'`。
- 键盘输入:用 `keys` 对象记录按键,`keydown` 置 true、`keyup` 置 false;在 update 中根据 `keys['KeyW']`、`KeyS`、`KeyA`、`KeyD`、`Space`、`ShiftLeft` 修改玩家位置。
- 世界坐标转屏幕坐标:克隆位置并 `vector.project(camera)`,返回 `x = ((vector.x + 1) / 2) * innerWidth`,`y = (-(vector.y - 1) / 2) * innerHeight`;可用来把 HTML 元素定位到 3D 对象上方。
- 屏幕坐标转世界坐标:构造 NDC `Vector3`(z=0.5),调用 `unproject(camera)`,计算 `dir = vector.sub(camera.position).normalize()`,再用 `distance = (targetZ - camera.position.z) / dir.z` 得到交点。
- 射线与平面相交:`raycaster.setFromCamera(mouse, camera)` 后调 `raycaster.ray.intersectPlane(plane, intersection)`;例如地面平面 `new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)`。
- 事件管理最佳实践:封装 `InteractionManager`,构造时保存 camera、renderer、scene,创建 raycaster、mouse、clickables,并绑定 canvas 的 click、mousemove、touchstart。
- InteractionManager 中 `updateMouse` 用 canvas 的 `getBoundingClientRect()` 计算 NDC;`getIntersects` 调 `setFromCamera` 后 `intersectObjects(clickables, true)`。
- InteractionManager 的 `onClick` 先 updateMouse,再 getIntersects;命中对象且 `object.userData.onClick` 存在时,调用 `object.userData.onClick(intersects[0])`。
- InteractionManager 提供 `addClickable(object, callback)`,把对象加入 `clickables` 并设置 `object.userData.onClick = callback`;提供 `dispose()` 移除事件监听。
- 性能优化:限制 raycast 频率(节流 mousemove)、用 layers 过滤、用不可见简单几何体做碰撞网格、不需要时 `controls.enabled = false`、批量处理交互检查。
- 简单碰撞网格做法:用 `BoxGeometry` + `MeshBasicMaterial({ visible: false })` 创建 `collisionMesh`,设置 `collisionMesh.userData.target = complexMesh`,把 `collisionMesh` 加入 `clickables`。
- 需要相机/场景基础、动画或着色器视觉反馈时,参考 `threejs-fundamentals`、`threejs-animation`、`threejs-shaders`。
适用边界 · 注意事项
- 未使用 Three.js 或不需要 3D 交互时,不适用。
- 需要真实物理碰撞、刚体、重力、复杂拖拽约束时,单靠 Raycaster 和官方 Controls 不够,应配合物理引擎。
- 场景对象极多或几何极复杂且未使用简化碰撞体、layers、节流时,直接每帧 raycast 会导致性能问题。