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

求助: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>
    );
  }
};

关键细节说明

  1. 修正OrbitControls绑定目标:之前代码里new OrbitControls(this.camera, this.canvas)是错误的,应该绑定到this.renderer.domElement,否则相机控制会失效。
  2. 锁定Y轴:拖拽时强制设置物体的y=4,确保它始终在XZ平面移动,不会上下飘。
  3. 子物体检测:intersectObjects的第二个参数设为true,可以检测OBJ模型的子mesh(因为OBJ加载后通常是一个包含多个mesh的组)。
  4. 事件清理:在componentWillUnmount里移除鼠标事件监听,避免组件卸载后内存泄漏。

可选优化

  • 如果场景里有很多不可拖拽的物体,可以把所有可拖拽物体放到一个单独的THREE.Group里,然后只检测这个group的子物体,提升性能:raycaster.intersectObjects(this.draggableGroup.children, true)
  • 可以添加拖拽时的视觉反馈,比如给选中的物体加一个高亮材质。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 08:51:36