下面详细介绍 Three.js 中实现拖放交互的几种方法。
javascript
import * as THREE from 'three';
import { DragControls } from 'three/examples/jsm/controls/DragControls';
// 创建场景、相机、渲染器...
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
// 创建可拖拽的物体
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 初始化拖拽控制
const objects = [cube]; // 需要拖拽的对象数组
const dragControls = new DragControls(objects, camera, renderer.domElement);
// 添加事件监听
dragControls.addEventListener('dragstart', function(event) {
event.object.material.color.set(0xff0000); // 拖拽开始变红色
controls.enabled = false; // 禁用相机控制
});
dragControls.addEventListener('drag', function(event) {
console.log('拖拽中:', event.object.position);
});
dragControls.addEventListener('dragend', function(event) {
event.object.material.color.set(0x00ff00); // 拖拽结束恢复颜色
controls.enabled = true; // 启用相机控制
});
// 限制拖拽平面
dragControls.transformGroup = true; // 以组的形式拖拽
javascript
class DragAndDrop {
constructor(scene, camera, renderer) {
this.scene = scene;
this.camera = camera;
this.renderer = renderer;
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
this.selectedObject = null;
this.offset = new THREE.Vector3();
this.plane = new THREE.Plane();
this.intersection = new THREE.Vector3();
this.init();
}
init() {
const domElement = this.renderer.domElement;
domElement.addEventListener('mousedown', this.onMouseDown.bind(this));
domElement.addEventListener('mousemove', this.onMouseMove.bind(this));
domElement.addEventListener('mouseup', this.onMouseUp.bind(this));
// 支持触摸屏
domElement.addEventListener('touchstart', this.onTouchStart.bind(this));
domElement.addEventListener('touchmove', this.onTouchMove.bind(this));
domElement.addEventListener('touchend', this.onTouchEnd.bind(this));
}
onMouseDown(event) {
event.preventDefault();
// 计算鼠标位置归一化坐标
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
this.handlePointerDown();
}
onTouchStart(event) {
event.preventDefault();
if (event.touches.length === 1) {
this.mouse.x = (event.touches[0].pageX / window.innerWidth) * 2 - 1;
this.mouse.y = -(event.touches[0].pageY / window.innerHeight) * 2 + 1;
this.handlePointerDown();
}
}
handlePointerDown() {
// 更新射线
this.raycaster.setFromCamera(this.mouse, this.camera);
// 检测与哪个物体相交
const intersects = this.raycaster.intersectObjects(this.scene.children, true);
if (intersects.length > 0) {
this.selectedObject = intersects[0].object;
// 计算拖拽平面(垂直于相机视线)
const cameraDirection = new THREE.Vector3();
this.camera.getWorldDirection(cameraDirection);
this.plane.setFromNormalAndCoplanarPoint(
cameraDirection,
this.selectedObject.position
);
// 计算偏移量
if (this.raycaster.ray.intersectPlane(this.plane, this.intersection)) {
this.offset.copy(this.intersection).sub(this.selectedObject.position);
}
}
}
onMouseMove(event) {
event.preventDefault();
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
this.handlePointerMove();
}
onTouchMove(event) {
event.preventDefault();
if (event.touches.length === 1) {
this.mouse.x = (event.touches[0].pageX / window.innerWidth) * 2 - 1;
this.mouse.y = -(event.touches[0].pageY / window.innerHeight) * 2 + 1;
this.handlePointerMove();
}
}
handlePointerMove() {
if (this.selectedObject) {
this.raycaster.setFromCamera(this.mouse, this.camera);
if (this.raycaster.ray.intersectPlane(this.plane, this.intersection)) {
this.selectedObject.position.copy(
this.intersection.sub(this.offset)
);
}
}
}
onMouseUp(event) {
event.preventDefault();
this.selectedObject = null;
}
onTouchEnd(event) {
event.preventDefault();
this.selectedObject = null;
}
}
javascript
class PlaneDragControls {
constructor(object, planeNormal = new THREE.Vector3(0, 1, 0)) {
this.object = object;
this.planeNormal = planeNormal.normalize();
this.plane = new THREE.Plane();
this.isDragging = false;
this.offset = new THREE.Vector3();
this.intersection = new THREE.Vector3();
this.worldPosition = new THREE.Vector3();
this.inverseMatrix = new THREE.Matrix4();
}
startDrag(mouse, camera) {
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
// 创建拖拽平面
this.object.updateMatrixWorld();
this.object.getWorldPosition(this.worldPosition);
this.plane.setFromNormalAndCoplanarPoint(
this.planeNormal,
this.worldPosition
);
if (raycaster.ray.intersectPlane(this.plane, this.intersection)) {
this.offset.copy(this.intersection).sub(this.worldPosition);
this.isDragging = true;
return true;
}
return false;
}
drag(mouse, camera) {
if (!this.isDragging) return;
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
if (raycaster.ray.intersectPlane(this.plane, this.intersection)) {
const newPosition = this.intersection.sub(this.offset);
// 如果需要保持物体在局部坐标系
this.object.position.copy(newPosition);
// 或者转换到世界坐标
// this.object.worldToLocal(newPosition);
// this.object.position.copy(newPosition);
}
}
endDrag() {
this.isDragging = false;
}
}
javascript
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
class AdvancedDragAndDrop {
constructor() {
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000);
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.initScene();
this.setupLights();
this.createGrid();
this.createDraggableObjects();
this.setupControls();
this.setupEventListeners();
this.selectedObject = null;
this.snapToGrid = true;
this.gridSize = 1;
this.animate();
}
initScene() {
this.renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(this.renderer.domElement);
this.camera.position.set(10, 10, 10);
this.camera.lookAt(0, 0, 0);
this.scene.background = new THREE.Color(0xf0f0f0);
}
setupLights() {
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 20, 0);
this.scene.add(directionalLight);
}
createGrid() {
const gridHelper = new THREE.GridHelper(20, 20, 0x000000, 0x000000);
gridHelper.material.opacity = 0.2;
gridHelper.material.transparent = true;
this.scene.add(gridHelper);
}
createDraggableObjects() {
// 创建多个可拖拽物体
const geometries = [
new THREE.BoxGeometry(1, 1, 1),
new THREE.SphereGeometry(0.5, 32, 32),
new THREE.ConeGeometry(0.5, 1, 32)
];
const colors = [0xff0000, 0x00ff00, 0x0000ff];
geometries.forEach((geometry, index) => {
const material = new THREE.MeshPhongMaterial({
color: colors[index],
transparent: true,
opacity: 0.8
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(
(index - 1) * 2,
0.5,
0
);
// 添加边框高亮
const edges = new THREE.EdgesGeometry(geometry);
const line = new THREE.LineSegments(
edges,
new THREE.LineBasicMaterial({ color: 0x000000 })
);
mesh.add(line);
mesh.userData.draggable = true;
mesh.userData.originalY = mesh.position.y;
this.scene.add(mesh);
});
}
setupControls() {
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
}
setupEventListeners() {
this.renderer.domElement.addEventListener('mousedown', this.onMouseDown.bind(this));
this.renderer.domElement.addEventListener('mousemove', this.onMouseMove.bind(this));
this.renderer.domElement.addEventListener('mouseup', this.onMouseUp.bind(this));
window.addEventListener('resize', this.onWindowResize.bind(this));
// 键盘控制
window.addEventListener('keydown', (event) => {
if (event.key === 'g') {
this.snapToGrid = !this.snapToGrid;
console.log('网格吸附:', this.snapToGrid ? '开启' : '关闭');
}
});
}
onMouseDown(event) {
event.preventDefault();
const mouse = new THREE.Vector2();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, this.camera);
const intersects = raycaster.intersectObjects(
this.scene.children.filter(obj => obj.userData.draggable)
);
if (intersects.length > 0) {
this.selectedObject = intersects[0].object;
this.controls.enabled = false;
// 创建拖拽平面
const planeNormal = new THREE.Vector3(0, 1, 0);
const worldPosition = new THREE.Vector3();
this.selectedObject.getWorldPosition(worldPosition);
this.dragPlane = new THREE.Plane();
this.dragPlane.setFromNormalAndCoplanarPoint(
planeNormal,
worldPosition
);
// 计算偏移
this.offset = new THREE.Vector3();
const intersection = new THREE.Vector3();
if (raycaster.ray.intersectPlane(this.dragPlane, intersection)) {
this.offset.copy(intersection).sub(worldPosition);
}
// 高亮选中物体
this.highlightObject(this.selectedObject, true);
}
}
onMouseMove(event) {
if (!this.selectedObject) return;
const mouse = new THREE.Vector2();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, this.camera);
const intersection = new THREE.Vector3();
if (raycaster.ray.intersectPlane(this.dragPlane, intersection)) {
let newPosition = intersection.sub(this.offset);
// 网格吸附
if (this.snapToGrid) {
newPosition.x = Math.round(newPosition.x / this.gridSize) * this.gridSize;
newPosition.z = Math.round(newPosition.z / this.gridSize) * this.gridSize;
newPosition.y = this.selectedObject.userData.originalY;
}
this.selectedObject.position.copy(newPosition);
}
}
onMouseUp() {
if (this.selectedObject) {
this.highlightObject(this.selectedObject, false);
this.selectedObject = null;
this.controls.enabled = true;
}
}
highlightObject(object, highlight) {
if (object.children[0]) {
object.children[0].material.color.set(highlight ? 0xffff00 : 0x000000);
}
}
onWindowResize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
}
animate() {
requestAnimationFrame(this.animate.bind(this));
this.controls.update();
this.renderer.render(this.scene, this.camera);
}
}
// 初始化
new AdvancedDragAndDrop();
性能优化
javascript
// 使用节流防止频繁更新
let dragTimeout;
function onDrag(event) {
clearTimeout(dragTimeout);
dragTimeout = setTimeout(() => {
// 更新逻辑
}, 16); // 约60fps
}
多平台支持
javascript
// 统一处理鼠标和触摸事件
function getNormalizedCoordinates(event) {
let clientX, clientY;
if (event.touches) {
clientX = event.touches[0].clientX;
clientY = event.touches[0].clientY;
} else {
clientX = event.clientX;
clientY = event.clientY;
}
return {
x: (clientX / window.innerWidth) * 2 - 1,
y: -(clientY / window.innerHeight) * 2 + 1
};
}
拖拽约束
javascript
// 限制拖拽范围
const bounds = {
minX: -10, maxX: 10,
minY: 0, maxY: 10,
minZ: -10, maxZ: 10
};
function constrainPosition(position) {
position.x = Math.max(bounds.minX, Math.min(bounds.maxX, position.x));
position.y = Math.max(bounds.minY, Math.min(bounds.maxY, position.y));
position.z = Math.max(bounds.minZ, Math.min(bounds.maxZ, position.z));
}
这些方法覆盖了从简单到复杂的拖拽实现,你可以根据具体需求选择合适的方案。