You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在THREE.js中实现点击拖拽交互

THREE.js 最简点击拖拽实现(无需RayCasting)

如果你想避开复杂的RayCasting,实现类似旧版THREE.interaction的简单点击拖拽,核心思路是利用DOM鼠标事件+坐标映射,直接通过鼠标位置计算对象的世界坐标偏移,无需射线检测。以下是具体实现:

核心逻辑

  1. 记录拖拽状态、目标对象、鼠标与对象的初始位置
  2. 将鼠标屏幕坐标转换为THREE.js的标准化设备坐标,再映射为世界坐标
  3. 通过初始与当前鼠标的世界坐标差,计算对象的拖拽偏移量

代码示例(正交相机场景)

正交相机的坐标映射更直接,适合2D或平面拖拽场景:

// 基础场景初始化
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(
  window.innerWidth / -2,
  window.innerWidth / 2,
  window.innerHeight / 2,
  window.innerHeight / -2,
  1,
  1000
);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 创建可拖拽立方体
const cube = new THREE.Mesh(
  new THREE.BoxGeometry(50, 50, 50),
  new THREE.MeshBasicMaterial({ color: 0x00ff00 })
);
scene.add(cube);
camera.position.z = 5;

// 拖拽状态变量
let isDragging = false;
let dragTarget = null;
const mouseStart = new THREE.Vector2();
const objectStart = new THREE.Vector3();

// 鼠标按下:启动拖拽
renderer.domElement.addEventListener('mousedown', (e) => {
  // 这里可以直接指定拖拽目标,或用 bounding box 简单判断点击范围
  dragTarget = cube;
  isDragging = true;

  // 记录鼠标初始位置(转标准化设备坐标)
  mouseStart.x = (e.clientX / window.innerWidth) * 2 - 1;
  mouseStart.y = -(e.clientY / window.innerHeight) * 2 + 1;

  // 记录对象初始位置
  objectStart.copy(dragTarget.position);
});

// 鼠标移动:执行拖拽
renderer.domElement.addEventListener('mousemove', (e) => {
  if (!isDragging || !dragTarget) return;

  // 当前鼠标标准化坐标
  const mouseCurrent = new THREE.Vector2(
    (e.clientX / window.innerWidth) * 2 - 1,
    -(e.clientY / window.innerHeight) * 2 + 1
  );

  // 转换为世界坐标
  const startWorld = new THREE.Vector3(mouseStart.x, mouseStart.y, 0).unproject(camera);
  const currentWorld = new THREE.Vector3(mouseCurrent.x, mouseCurrent.y, 0).unproject(camera);

  // 计算偏移并更新对象位置
  const offset = currentWorld.sub(startWorld);
  dragTarget.position.copy(objectStart.clone().add(offset));
});

// 鼠标松开:结束拖拽
renderer.domElement.addEventListener('mouseup', () => {
  isDragging = false;
  dragTarget = null;
});

// 渲染循环
function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
animate();

扩展:简单拾取判断(无需RayCasting)

如果需要判断点击到对象才启动拖拽,可以用对象的boundingBox做范围检测,比射线检测轻量:

// 在mousedown事件中替换拖拽目标赋值逻辑
renderer.domElement.addEventListener('mousedown', (e) => {
  // 鼠标转世界坐标
  mouseStart.x = (e.clientX / window.innerWidth) * 2 - 1;
  mouseStart.y = -(e.clientY / window.innerHeight) * 2 + 1;
  const mouseWorld = new THREE.Vector3(mouseStart.x, mouseStart.y, 0).unproject(camera);

  // 计算对象的世界空间包围盒
  cube.geometry.computeBoundingBox();
  const worldBox = cube.geometry.boundingBox.clone();
  worldBox.applyMatrix4(cube.matrixWorld);

  // 判断鼠标是否在包围盒内
  if (worldBox.containsPoint(mouseWorld)) {
    dragTarget = cube;
    isDragging = true;
    objectStart.copy(dragTarget.position);
  }
});

透视相机适配

如果用透视相机,只需将坐标转换时的z值设置为对象的z坐标(确保鼠标坐标映射到对象所在的深度平面):

// 转换世界坐标时指定对象的z值
const startWorld = new THREE.Vector3(mouseStart.x, mouseStart.y, cube.position.z).unproject(camera);
const currentWorld = new THREE.Vector3(mouseCurrent.x, mouseCurrent.y, cube.position.z).unproject(camera);

这种方式无需依赖复杂的射线检测,代码简洁,适合简单拖拽场景,完全可以替代旧版THREE.interaction的基础功能。

内容的提问来源于stack exchange,提问作者Om Solari

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 14:20:30