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

在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;

关键说明

  1. 碰撞体选择:如果地形是起伏的,Box碰撞体不够准确,可以改用Heightfield碰撞体,需要提取地形的高度数据传给Cannon.js。
  2. 性能优化:绝对不要用GLB模型的原始几何做碰撞体——面数过多会导致物理计算卡顿,必须用简化的基本体(Box、Sphere、Capsule)或手动简化的碰撞几何。
  3. 碰撞判断:通过给Body设置name属性,在碰撞事件中区分不同物体,确保只有撞到地形才触发停止逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 11:29:52