在Three.js中实现GLB飞机与地形的碰撞检测功能
问题描述
我跟着教程做一款飞机游戏,想给游戏加碰撞功能——飞机撞到地形后停止移动,而不是直接穿过去。这是我第一次接触游戏开发和Three.js,发现GLB模型的碰撞实现不太容易。
以下是地形和飞机的代码,参考自一个仓库,我只是练习加新功能:
地形代码
import React, { useRef } from "react"; import { useGLTF } from "@react-three/drei"; export function Landscape(props) { const { nodes, materials } = useGLTF("../assets/models/scene.glb"); return ( <group {...props} dispose={null} position={[0, 0, 0]}> <mesh geometry={nodes.landscape_gltf.geometry} material={materials["Material.009"]} /> <mesh geometry={nodes.landscape_borders.geometry} material={materials["Material.010"]} /> <mesh geometry={nodes.trees_light.geometry} material={materials["Material.008"]} /> <mesh geometry={nodes.water.geometry} material={materials.Water} /> <mesh geometry={nodes.water1.geometry} material={materials.Water} /> <mesh geometry={nodes.water2.geometry} material={materials.Water} /> <mesh geometry={nodes.lights.geometry} material={materials["Material.001"]} /> </group> ); } useGLTF.preload("../assets/models/scene.glb");
飞机代码
import React, { useRef } from "react"; import { useGLTF } from "@react-three/drei"; import { Matrix4, Vector3, Quaternion } from "three"; import { useFrame } from "@react-three/fiber"; import { updatePlaneAxis } from "./controls"; const x = new Vector3(1, 0, 0); const y = new Vector3(0, 1, 0); const z = new Vector3(0, 0, 1); export const planePosition = new Vector3(0, 3, 7); const delayedRotMatrix = new Matrix4(); const delayedQuaternion = new Quaternion(); export function Airplane(props) { const { nodes, materials } = useGLTF("../assets/models/airplane.glb"); const groupRef = useRef(); const helixMeshRef = useRef(); planePosition.add(new Vector3(0, 0, 0.01)); useFrame(({ camera }) => { updatePlaneAxis(x, y, z, planePosition, camera); const rotMatrix = new Matrix4().makeBasis(x, y, z); const matrix = new Matrix4() .multiply( new Matrix4().makeTranslation( planePosition.x, planePosition.y, planePosition.z ) ) .multiply(rotMatrix); groupRef.current.matrixAutoUpdate = false; groupRef.current.matrix.copy(matrix); groupRef.current.matrixWorldNeedsUpdate = true; var quaternionA = new Quaternion().copy(delayedQuaternion); var quaternionB = new Quaternion(); quaternionB.setFromRotationMatrix(rotMatrix); var interpolationFactor = 0.175; var interpolatedQuaternion = new Quaternion().copy(quaternionA); interpolatedQuaternion.slerp(quaternionB, interpolationFactor); delayedQuaternion.copy(interpolatedQuaternion); delayedRotMatrix.identity(); delayedRotMatrix.makeRotationFromQuaternion(delayedQuaternion); const cameraMatrix = new Matrix4() .multiply( new Matrix4().makeTranslation( planePosition.x, planePosition.y, planePosition.z ) ) .multiply(delayedRotMatrix) .multiply(new Matrix4().makeRotationX(-0.2)) .multiply(new Matrix4().makeTranslation(0, 0.015, 0.3)); camera.matrixAutoUpdate = false; camera.matrix.copy(cameraMatrix); camera.matrixWorldNeedsUpdate = true; helixMeshRef.current.rotation.z -= 1.0; }); return ( <group ref={groupRef} position={[0, 0, 0]}> <group {...props} dispose={null} scale={0.01} rotation-y={Math.PI}> <mesh geometry={nodes.supports.geometry} material={materials["Material.004"]} /> <mesh geometry={nodes.chassis.geometry} material={materials["Material.005"]} /> <mesh geometry={nodes.helix.geometry} material={materials["Material.005"]} /> <mesh geometry={nodes.helix.geometry} material={materials["Material.005"]} ref={helixMeshRef} /> </group> </group> ); } useGLTF.preload("../assets/models/airplane.glb");
我猜得给地形GLB模型创建碰撞边界?具体怎么实现?有没有适合我这个场景的库?
解决方案
对于React Three Fiber(R3F)项目,最适合新手的碰撞库是react-three-cannon——它是Cannon.js物理引擎的R3F封装,能快速给GLB模型添加碰撞体,处理物理交互。
步骤1:安装依赖
npm install react-three-cannon
步骤2:给地形添加静态碰撞体
地形是静态物体,用StaticBody包裹。因为GLB模型面数多,直接用原始几何做碰撞体性能差,建议用简化的Box碰撞体(如果地形是大致平整的),后续可根据地形形状调整:
修改Landscape组件:
import React, { useRef, useState } from "react"; import { useGLTF } from "@react-three/drei"; import { StaticBody, Box } from "react-three-cannon"; export function Landscape(props) { const { nodes, materials } = useGLTF("../assets/models/scene.glb"); const terrainRef = useRef(); const [boxSize, setBoxSize] = useState([1,1,1]); // 计算地形包围盒,自动匹配碰撞体大小 React.useEffect(() => { if (terrainRef.current) { terrainRef.current.geometry.computeBoundingBox(); const bbox = terrainRef.current.geometry.boundingBox; setBoxSize([ bbox.max.x - bbox.min.x, bbox.max.y - bbox.min.y, bbox.max.z - bbox.min.z ]); } }, []); return ( <group {...props} dispose={null} position={[0, 0, 0]}> <StaticBody position={[0, boxSize[1]/2, 0]} name="terrain"> {/* 隐藏碰撞盒,仅用于物理计算 */} <Box args={boxSize} visible={false} /> <group> <mesh ref={terrainRef} geometry={nodes.landscape_gltf.geometry} material={materials["Material.009"]} /> <mesh geometry={nodes.landscape_borders.geometry} material={materials["Material.010"]} /> <mesh geometry={nodes.trees_light.geometry} material={materials["Material.008"]} /> <mesh geometry={nodes.water.geometry} material={materials.Water} /> <mesh geometry={nodes.water1.geometry} material={materials.Water} /> <mesh geometry={nodes.water2.geometry} material={materials.Water} /> <mesh geometry={nodes.lights.geometry} material={materials["Material.001"]} /> </group> </StaticBody> </group> ); } useGLTF.preload("../assets/models/scene.glb");
步骤3:给飞机添加动态碰撞体并处理碰撞逻辑
飞机是动态物体,用Body包裹,监听碰撞事件,撞到地形时停止所有移动:
修改Airplane组件:
import React, { useRef, useState } from "react"; import { useGLTF } from "@react-three/drei"; import { Matrix4, Vector3, Quaternion } from "three"; import { useFrame } from "@react-three/fiber"; import { updatePlaneAxis } from "./controls"; import { Body, Box } from "react-three-cannon"; const x = new Vector3(1, 0, 0); const y = new Vector3(0, 1, 0); const z = new Vector3(0, 0, 1); export function Airplane(props) { const { nodes, materials } = useGLTF("../assets/models/airplane.glb"); const groupRef = useRef(); const helixMeshRef = useRef(); const bodyRef = useRef(); const [isCollided, setIsCollided] = useState(false); useFrame(({ camera }) => { if (isCollided) return; // 碰撞后停止所有移动逻辑 updatePlaneAxis(x, y, z, bodyRef.current.position, camera); const rotMatrix = new Matrix4().makeBasis(x, y, z); // 用物理引擎的位置更新模型矩阵 const matrix = new Matrix4() .multiply( new Matrix4().makeTranslation( bodyRef.current.position.x, bodyRef.current.position.y, bodyRef.current.position.z ) ) .multiply(rotMatrix); groupRef.current.matrixAutoUpdate = false; groupRef.current.matrix.copy(matrix); groupRef.current.matrixWorldNeedsUpdate = true; var quaternionA = new Quaternion().copy(bodyRef.current.quaternion); var quaternionB = new Quaternion(); quaternionB.setFromRotationMatrix(rotMatrix); var interpolationFactor = 0.175; var interpolatedQuaternion = new Quaternion().copy(quaternionA); interpolatedQuaternion.slerp(quaternionB, interpolationFactor); bodyRef.current.quaternion.copy(interpolatedQuaternion); const cameraMatrix = new Matrix4() .multiply( new Matrix4().makeTranslation( bodyRef.current.position.x, bodyRef.current.position.y, bodyRef.current.position.z ) ) .multiply(new Matrix4().makeRotationFromQuaternion(interpolatedQuaternion)) .multiply(new Matrix4().makeRotationX(-0.2)) .multiply(new Matrix4().makeTranslation(0, 0.015, 0.3)); camera.matrixAutoUpdate = false; camera.matrix.copy(cameraMatrix); camera.matrixWorldNeedsUpdate = true; helixMeshRef.current.rotation.z -= 1.0; }); // 碰撞事件处理:撞到地形则停止 const handleCollision = (event) => { if (event.body.name === "terrain") { setIsCollided(true); bodyRef.current.velocity.set(0,0,0); // 立即停止移动 bodyRef.current.angularVelocity.set(0,0,0); // 停止旋转 } }; return ( <Body ref={bodyRef} position={[0, 3, 7]} name="plane" onCollision={handleCollision} > {/* 飞机的碰撞盒,大小根据模型实际尺寸调整 */} <Box args={[0.5, 0.2, 1]} visible={false} /> <group ref={groupRef} position={[0, 0, 0]}> <group {...props} dispose={null} scale={0.01} rotation-y={Math.PI}> <mesh geometry={nodes.supports.geometry} material={materials["Material.004"]} /> <mesh geometry={nodes.chassis.geometry} material={materials["Material.005"]} /> <mesh geometry={nodes.helix.geometry} material={materials["Material.005"]} /> <mesh geometry={nodes.helix.geometry} material={materials["Material.005"]} ref={helixMeshRef} /> </group> </group> </Body> ); } useGLTF.preload("../assets/models/airplane.glb");
步骤4:添加物理引擎根容器
在根组件中用CannonProvider包裹整个场景,初始化物理引擎:
import { Canvas } from "@react-three/fiber"; import { CannonProvider } from "react-three-cannon"; import { Landscape } from "./Landscape"; import { Airplane } from "./Airplane"; function App() { return ( <Canvas> <CannonProvider> <Landscape /> <Airplane /> {/* 你的灯光、相机等其他组件 */} </CannonProvider> </Canvas> ); } export default App;
关键说明
- 碰撞体选择:如果地形是起伏的,Box碰撞体不够准确,可以改用
Heightfield碰撞体,需要提取地形的高度数据传给Cannon.js。 - 性能优化:绝对不要用GLB模型的原始几何做碰撞体——面数过多会导致物理计算卡顿,必须用简化的基本体(Box、Sphere、Capsule)或手动简化的碰撞几何。
- 碰撞判断:通过给
Body设置name属性,在碰撞事件中区分不同物体,确保只有撞到地形才触发停止逻辑。
内容的提问来源于stack exchange,提问作者Jeannie
相关产品推荐
相关产品推荐

