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

如何在R3F中限制3D模型于动态地板范围内拖拽?

实现方案

1. 先装齐依赖

确保项目里安装了这些必要包:

npm install @react-three/drei @react-three/fiber three

2. 改造FinalRoom组件,输出地板边界

现在的FinalRoom只能渲染场景,我们需要让它把地板的实际可拖拽范围传递给外部。给组件加个回调,每次尺寸变化时自动计算地板的世界坐标边界:

改完的FinalRoom.tsx:

import * as THREE from "three";
import React, { useRef, useEffect } from "react";
import { useGLTF } from "@react-three/drei";
import { GLTF } from "three-stdlib";

type GLTFResult = GLTF & {
  nodes: {
    Cube_1: THREE.Mesh;
    Cube_2: THREE.Mesh;
  };
  materials: {
    ["Material.005"]: THREE.MeshStandardMaterial;
    ["Material.006"]: THREE.MeshStandardMaterial;
  };
};

type FinalRoomProps = JSX.IntrinsicElements["group"] & {
  width: number;
  height: number;
  length: number;
  shiny: boolean;
  setShiny: (value: boolean) => void;
  onFloorBoundsUpdate?: (bounds: { minX: number; maxX: number; minZ: number; maxZ: number; minY: number }) => void;
};

export function FinalRoom(props: FinalRoomProps) {
  const { nodes, materials } = useGLTF("/models/finalRoom.glb") as GLTFResult;
  const floorRef = useRef<THREE.Mesh>(null);
  const groupRef = useRef<THREE.Group>(null);

  useEffect(() => {
    if (floorRef.current && groupRef.current && props.onFloorBoundsUpdate) {
      // 计算地板本地边界,再转换为世界坐标
      const box = new THREE.Box3().setFromObject(floorRef.current);
      box.applyMatrix4(groupRef.current.matrixWorld);
      props.onFloorBoundsUpdate({
        minX: box.min.x,
        maxX: box.max.x,
        minZ: box.min.z,
        maxZ: box.max.z,
        minY: box.min.y,
      });
    }
  }, [props.width, props.length, props.onFloorBoundsUpdate]);

  return (
    <group
      ref={groupRef}
      {...props}
      dispose={null}
      castShadow
      receiveShadow
      position={[0, -3, 0]}
    >
      <group
        position={[0, 0, 0]}
        scale={[
          (props.width * 1.68) / 4000,
          (props.height * 1.1) / 4000,
          (props.length * 1.68) / 4000,
        ]}
      >
        {/* FLOOR */}
        <mesh
          ref={floorRef}
          geometry={nodes.Cube_1.geometry}
          material={materials["Material.005"]}
          material-side={THREE.FrontSide}
          receiveShadow
          material-color={props.shiny ? "beige" : "#ffffff"}
          onPointerEnter={() => props.setShiny(true)}
          onPointerLeave={() => props.setShiny(false)}
        />
        {/* WALLS */}
        <mesh
          geometry={nodes.Cube_2.geometry}
          material={materials["Material.006"]}
          material-side={THREE.FrontSide}
          castShadow
          material-color="#818181"
        />
      </group>
    </group>
  );
}

useGLTF.preload("/models/finalRoom.glb");

3. 实现带边界限制的可拖拽模型组件

用@react-three/drei的useDrag钩子处理拖拽逻辑,同时把模型位置锁死在地板范围内:

import * as THREE from "three";
import React, { useRef } from "react";
import { useDrag, useGLTF } from "@react-three/drei";
import { GLTF } from "three-stdlib";

type DraggableModelProps = {
  floorBounds: { minX: number; maxX: number; minZ: number; maxZ: number; minY: number };
  modelPath: string;
};

type GLTFModelResult = GLTF & {
  nodes: Record<string, THREE.Mesh>;
  materials: Record<string, THREE.MeshStandardMaterial>;
};

export function DraggableModel({ floorBounds, modelPath }: DraggableModelProps) {
  const { scene } = useGLTF(modelPath) as GLTFModelResult;
  const modelRef = useRef<THREE.Object3D>(null);

  useDrag((event) => {
    if (!modelRef.current) return;

    // 用射线检测鼠标与地板平面的交点
    const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), floorBounds.minY);
    const intersectionPoint = new THREE.Vector3();
    event.ray.intersectPlane(plane, intersectionPoint);

    // 限制X、Z坐标在地板范围内
    const clampedX = THREE.MathUtils.clamp(intersectionPoint.x, floorBounds.minX, floorBounds.maxX);
    const clampedZ = THREE.MathUtils.clamp(intersectionPoint.z, floorBounds.minZ, floorBounds.maxZ);

    // 固定Y坐标,让模型贴在地板上(如果模型原点不在底部,需加上模型自身半高)
    modelRef.current.position.set(clampedX, floorBounds.minY + 0.5, clampedZ);
  }, { modelRef });

  return <primitive ref={modelRef} object={scene} castShadow receiveShadow />;
}

// 预加载你的模型,替换为实际路径
useGLTF.preload("/models/your-model.glb");

4. 主组件整合所有功能

在App组件里把FinalRoom、DraggableModel和尺寸调整面板拼在一起:

import React, { useState } from "react";
import { Canvas } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";
import { FinalRoom } from "./FinalRoom";
import { DraggableModel } from "./DraggableModel";

export default function App() {
  const [roomDimensions, setRoomDimensions] = useState({
    width: 4000,
    height: 3000,
    length: 4000,
  });
  const [shiny, setShiny] = useState(false);
  const [floorBounds, setFloorBounds] = useState({
    minX: -2,
    maxX: 2,
    minZ: -2,
    maxZ: 2,
    minY: -3,
  });

  // 调整地板尺寸的函数
  const handleResizeRoom = (direction: "width" | "length", amount: number) => {
    setRoomDimensions(prev => ({
      ...prev,
      [direction]: prev[direction] + amount,
    }));
  };

  return (
    <div style={{ width: "100vw", height: "100vh" }}>
      {/* 顶部控制面板,用于调整地板大小 */}
      <div style={{ position: "absolute", top: 20, left: 20, zIndex: 10, background: "white", padding: 16, borderRadius: 8 }}>
        <div style={{ marginBottom: 8 }}>
          <button onClick={() => handleResizeRoom("width", 500)}>加宽</button>
          <button onClick={() => handleResizeRoom("width", -500)} style={{ marginLeft: 8 }}>缩窄</button>
          <span style={{ marginLeft: 12 }}>宽度: {roomDimensions.width}</span>
        </div>
        <div>
          <button onClick={() => handleResizeRoom("length", 500)}>加长</button>
          <button onClick={() => handleResizeRoom("length", -500)} style={{ marginLeft: 8 }}>缩短</button>
          <span style={{ marginLeft: 12 }}>长度: {roomDimensions.length}</span>
        </div>
      </div>

      <Canvas shadows camera={{ position: [10, 10, 10], fov: 50 }}>
        <ambientLight intensity={0.5} />
        <directionalLight position={[5, 10, 5]} castShadow />
        <FinalRoom
          width={roomDimensions.width}
          height={roomDimensions.height}
          length={roomDimensions.length}
          shiny={shiny}
          setShiny={setShiny}
          onFloorBoundsUpdate={(bounds) => setFloorBounds(bounds)}
        />
        <DraggableModel
          floorBounds={floorBounds}
          modelPath="/models/your-model.glb"
        />
        <OrbitControls />
      </Canvas>
    </div>
  );
}

关键细节提示

  • Y坐标适配:如果你的模型原点不在底部,需要先计算模型的包围盒,把Y坐标调整为floorBounds.minY + 模型半高,避免模型陷入地板。
  • 边界准确性:通过Box3和matrixWorld转换为世界坐标,不管地板怎么缩放、移动,都能拿到精准的可拖拽范围。
  • 动态响应:每次调整地板尺寸,FinalRoom会自动重新计算边界并同步给DraggableModel,拖拽限制会实时更新。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 20:42:32