如何在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
相关产品推荐
相关产品推荐

