求助:React与Three.js中实现物体X/Z轴拖拽的简单方案
实现React + Three.js XZ平面物体拖拽的轻量方案
嘿,我完全懂你现在的处境——React结合Three.js做拖拽的示例要么复杂到看不懂,要么就得加一堆额外依赖。既然你不想用额外的Node模块,我给你一个基于你现有代码的纯原生实现方案,核心就是用Three.js自带的Raycaster和鼠标事件来搞定XZ平面的拖拽,逻辑清晰,代码量也不大。
核心思路
- 用
Raycaster检测鼠标点击到的3D物体 - 鼠标按下时记录选中的物体和鼠标在XZ平面的初始交点,计算物体与交点的偏移量
- 鼠标移动时,实时计算当前鼠标在XZ平面的投影点,结合偏移量更新物体位置
- 处理OrbitControls和拖拽的冲突,拖拽时禁用相机控制,松开后恢复
修改后的完整代码
我基于你的现有代码做了扩展,添加了拖拽逻辑,关键部分都加了注释:
import React, { Component } from 'react'; import * as THREE from 'three'; import NavBar from "./navbar.js"; import { Button, Row, Col, Container } from "react-bootstrap"; const OrbitControls = require("three-orbit-controls")(THREE); var raycaster = new THREE.Raycaster(); var mouse = new THREE.Vector2(); var OBJLoader = require('three-obj-loader'); OBJLoader(THREE); export default class ThreeEntryPoint extends Component { constructor() { super(); this.camera = ""; this.scene = ""; this.renderer = ""; this.world = ""; this.ground = ""; this.controls = ""; this.container = ""; this.state = { toggle: false, currentSandBoxState: "" }; this.itemList = []; this.lastAddedXPosition = 0; this.lastAddedZPosition = 0; this.selectedObject = ""; this.offset = new THREE.Vector3(); } setSandboxState = (e) => { console.log(e); this.setState({ currentSandBoxState: e }); } addTree = (x, z) => { this.THREE = THREE; var objLoader = new this.THREE.OBJLoader() this.setState({ toggle: !this.state.toggle }); var this_ = this; objLoader.load('https://swamp-back.s3.eu-central-1.amazonaws.com/objects/lowpolytree.obj', function (object) { object.position.y = 4; object.position.z = z + this_.lastAddedZPosition; object.position.x = x + this_.lastAddedXPosition; this_.lastAddedXPosition++; this_.lastAddedZPosition++; object.scale.set(2, 2, 2); this_.scene.add(object); }, function (xhr) { }, function (error) { console.log(error); }); } setCamera = (camera) => { camera.position.z = 100; camera.position.y = 30; camera.lookAt(new THREE.Vector3(0, 0, 0)); camera.add(new THREE.PointLight(0xffffff, 0.7)) } // 鼠标按下:检测选中物体并记录初始状态 onMouseDown = (event) => { // 将鼠标坐标转换为Three.js的标准化设备坐标 mouse.x = (event.clientX / this.renderer.domElement.clientWidth) * 2 - 1; mouse.y = -(event.clientY / this.renderer.domElement.clientHeight) * 2 + 1; // 更新射线投射器的射线方向 raycaster.setFromCamera(mouse, this.camera); // 检测与场景中所有物体的交点(true表示检测子物体) const intersects = raycaster.intersectObjects(this.scene.children, true); if (intersects.length > 0) { this.selectedObject = intersects[0].object; // 创建一个XZ平面(y=0,和你的地面y值一致) const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); // 计算鼠标射线与平面的交点 raycaster.ray.intersectPlane(plane, this.offset); // 记录物体初始位置与交点的偏移量 this.offset.sub(this.selectedObject.position); // 禁用OrbitControls,避免拖拽时旋转相机 this.controls.enabled = false; } } // 鼠标移动:更新选中物体的位置 onMouseMove = (event) => { if (!this.selectedObject) return; // 更新鼠标的标准化坐标 mouse.x = (event.clientX / this.renderer.domElement.clientWidth) * 2 - 1; mouse.y = -(event.clientY / this.renderer.domElement.clientHeight) * 2 + 1; raycaster.setFromCamera(mouse, this.camera); const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); const intersectionPoint = new THREE.Vector3(); // 计算当前鼠标在XZ平面的投影点 if (raycaster.ray.intersectPlane(plane, intersectionPoint)) { // 更新物体位置:当前交点 + 初始偏移量 this.selectedObject.position.copy(intersectionPoint.add(this.offset)); // 锁定Y轴,确保物体始终在XZ平面移动(和你添加树时的y值保持一致) this.selectedObject.position.y = 4; } } // 鼠标松开:取消选中并恢复相机控制 onMouseUp = () => { this.selectedObject = null; this.controls.enabled = true; } componentDidMount() { this.Three = THREE; var container_ = document.getElementById("container-canvas"); this.contaier = container_; const width = window.innerWidth - 300; const height = window.innerHeight - 200; this.scene = new THREE.Scene(); this.camera = new THREE.PerspectiveCamera(45, width / height, 10, 1000); this.renderer = new THREE.WebGLRenderer(); this.renderer.setClearColor(0xffffff); this.controls = new OrbitControls(this.camera, this.renderer.domElement); // 修正:绑定到渲染画布元素 this.controls.target.set(0, 5, 0); this.controls.rotateSpeed = 1.0; this.controls.zoomSpeed = 1.2; this.controls.panSpeed = 0.8; this.renderer.setSize(width, height); container_.appendChild(this.renderer.domElement); var ambientLight = new THREE.AmbientLight(0x0f0f0f); this.world = new THREE.Object3D(); this.ground = new THREE.Mesh( new THREE.BoxGeometry(100, 1, 40), new THREE.MeshLambertMaterial({ color: "green" }) ); this.ground.position.y = 0; this.scene.add(this.world); let this_ = this; var animate = function () { requestAnimationFrame(animate); this_.controls.update(); this_.renderer.render(this_.scene, this_.camera); }; this.addTree(10, 11); this.scene.add(this.ground); this.scene.add(ambientLight); this.scene.add(new THREE.DirectionalLight(0xffffff, 0.5)); this.setCamera(this.camera); // 绑定鼠标事件到渲染画布 this.renderer.domElement.addEventListener('mousedown', this.onMouseDown); this.renderer.domElement.addEventListener('mousemove', this.onMouseMove); this.renderer.domElement.addEventListener('mouseup', this.onMouseUp); animate(); } // 组件卸载时移除事件监听,避免内存泄漏 componentWillUnmount() { this.renderer.domElement.removeEventListener('mousedown', this.onMouseDown); this.renderer.domElement.removeEventListener('mousemove', this.onMouseMove); this.renderer.domElement.removeEventListener('mouseup', this.onMouseUp); } render() { return ( <Container> <NavBar passState={this.setSandboxState} /> <Row id="container-canvas" style={{ display: "flex", alignItems: "center", justifyContent: "center" }}> </Row> </Container> ); } };
关键细节说明
- 修正OrbitControls绑定目标:之前代码里
new OrbitControls(this.camera, this.canvas)是错误的,应该绑定到this.renderer.domElement,否则相机控制会失效。 - 锁定Y轴:拖拽时强制设置物体的
y=4,确保它始终在XZ平面移动,不会上下飘。 - 子物体检测:
intersectObjects的第二个参数设为true,可以检测OBJ模型的子mesh(因为OBJ加载后通常是一个包含多个mesh的组)。 - 事件清理:在
componentWillUnmount里移除鼠标事件监听,避免组件卸载后内存泄漏。
可选优化
- 如果场景里有很多不可拖拽的物体,可以把所有可拖拽物体放到一个单独的
THREE.Group里,然后只检测这个group的子物体,提升性能:raycaster.intersectObjects(this.draggableGroup.children, true) - 可以添加拖拽时的视觉反馈,比如给选中的物体加一个高亮材质。
内容的提问来源于stack exchange,提问作者prat
相关产品推荐
相关产品推荐

