【问题标题】:How to detect collision in three.js?如何检测three.js中的碰撞?
【发布时间】:2025-12-09 09:20:04
【问题描述】:

我正在使用three.js。

我的场景中有两个网格几何体。

如果这些几何图形相交(或者如果翻译会相交),我想将其检测为碰撞。

如何使用 three.js 进行碰撞检测?如果three.js 没有碰撞检测功能,我是否可以将其他库与three.js 结合使用?

【问题讨论】:

  • 我在谷歌上搜索过,但我只发现了射线碰撞。
  • 我认为光线碰撞是要走的路...... Adnan(大概)引用的 CollisionUtils.js 和 Collisions.js 文件已过时,不是最新的(v49在撰写本文时)three.js 版本。
  • 对我来说似乎是个好问题。所以可以这么愚蠢。
  • @Adi 我用谷歌搜索了它。这是第一个结果。
  • 我必须创建一堵墙并在其上添加一个窗口,用户用户并在他们决定将其放在墙上的哪个位置之前将其拖过墙。我必须将窗口的拖动限制在墙壁的范围内。我想我必须检测碰撞并获取顶点或其他东西。我不确定,请提出一些建议。我是 three.js 或任何类型的 3D 应用程序的新手。

标签: javascript three.js webgl collision-detection


【解决方案1】:

在 Three.js 中,实用程序 CollisionUtils.js 和 Collisions.js 似乎不再受支持,并且 mrdoob(three.js 的创建者)本人建议更新到最新版本的 three.js 并使用 Ray 类而是为此目的。下面是一种解决方法。

这个想法是这样的:假设我们要检查一个给定的网格,称为“玩家”,是否与包含在一个名为“collidableMeshList”的数组中的任何网格相交。我们可以做的是创建一组射线,这些射线从 Player 网格 (Player.position) 的坐标开始,并延伸到 Player 网格几何中的每个顶点。每条射线都有一个称为“intersectObjects”的方法,它返回射线相交的对象数组,以及到每个对象的距离(从射线的原点测量)。如果到交点的距离小于玩家位置与几何体顶点之间的距离,则碰撞发生在玩家网格的内部——我们可能称之为“实际”碰撞。

我在以下位置发布了一个工作示例:

http://stemkoski.github.io/Three.js/Collision-Detection.html

您可以使用箭头键移动红色线框立方体并使用 W/A/S/D 旋转它。当它与其中一个蓝色立方体相交时,如上所述,对于每个交叉点,“Hit”一词将出现在屏幕顶部一次。代码的重要部分如下。

for (var vertexIndex = 0; vertexIndex < Player.geometry.vertices.length; vertexIndex++)
{       
    var localVertex = Player.geometry.vertices[vertexIndex].clone();
    var globalVertex = Player.matrix.multiplyVector3(localVertex);
    var directionVector = globalVertex.subSelf( Player.position );

    var ray = new THREE.Ray( Player.position, directionVector.clone().normalize() );
    var collisionResults = ray.intersectObjects( collidableMeshList );
    if ( collisionResults.length > 0 && collisionResults[0].distance < directionVector.length() ) 
    {
        // a collision occurred... do something...
    }
}

这种特殊方法存在两个潜在问题。

(1)当射线的原点在网格M内时,不会返回射线与M的碰撞结果。

(2) 小物体(相对于 Player 网格)可能会在各种光线之间“滑动”,因此不会记录碰撞。降低此问题发生几率的两种可能方法是编写代码,以便小对象创建光线并从它们的角度进行碰撞检测,或者在网格上包含更多顶点(例如使用 CubeGeometry(100, 100, 100, 20, 20, 20) 而不是 CubeGeometry(100, 100, 100, 1, 1, 1)。)后一种方法可能会导致性能下降,因此我建议谨慎使用。

我希望其他人能够通过他们对这个问题的解决方案为这个问题做出贡献。在开发此处描述的解决方案之前,我自己已经为此苦苦挣扎了一段时间。

【讨论】:

  • 感谢您的详细解释!我还在努力为我的游戏找到一个体面的解决方案,上面有地形和 3D 对象,你的回答给了我一些新的见解!
  • 虽然此方法似乎测试是否有任何顶点与对象的中心相交,但测试所有边(连接的顶点)会慢一倍但 100%(?)准确。因此,要详细说明,您需要遍历每个面,并采用 vertex[n] 和 vertex[(n + 1)%len] 来获取所有边缘。如果我拥抱某人,他们会与我的位置中心和我的手相交,但不会与我的皮肤相交,因为进行边缘检查会测试。
  • 这是个好主意!对于 100%(?)的准确度,我认为您需要测试两个网格中的每一个上的边缘,并且您需要在两个方向上测试它们,因为碰撞仅在一个方向上检测到,当光线从网格的外部到内部。当然它可能会慢一点,但你可以通过初步的边界球半径检查来加快它。但最重要的是,我认为您的准确率可能是 100%……
  • 我建议你避免在渲染循环中实例化一个新的Raycaster。实例化一个并重用它。
  • 如果网格几何中没有geometry.vertices 数据怎么办。在 obj 模型中有 geometry.attributes.position.array 但没有 geometry.vertices
【解决方案2】:

这确实是一个太宽泛的主题,无法在 SO 问题中涵盖,但是为了稍微润滑网站的 SEO,这里有几个简单的起点:

如果您想要真正简单的碰撞检测而不是完整的物理引擎,请查看(由于没有更多现有网站,链接已删除)

另一方面,如果您确实想要一些碰撞响应,而不仅仅是“A 和 B 碰撞了吗?”,请查看 Physijs,这是一个超级易于使用的 Ammo.js 包装器,围绕 3 构建。 js

【讨论】:

  • 你链接的演示是射线碰撞
【解决方案3】:

仅适用于 BoxGeometry 和 BoxBufferGeometry

创建以下函数:

function checkTouching(a, d) {
  let b1 = a.position.y - a.geometry.parameters.height / 2;
  let t1 = a.position.y + a.geometry.parameters.height / 2;
  let r1 = a.position.x + a.geometry.parameters.width / 2;
  let l1 = a.position.x - a.geometry.parameters.width / 2;
  let f1 = a.position.z - a.geometry.parameters.depth / 2;
  let B1 = a.position.z + a.geometry.parameters.depth / 2;
  let b2 = d.position.y - d.geometry.parameters.height / 2;
  let t2 = d.position.y + d.geometry.parameters.height / 2;
  let r2 = d.position.x + d.geometry.parameters.width / 2;
  let l2 = d.position.x - d.geometry.parameters.width / 2;
  let f2 = d.position.z - d.geometry.parameters.depth / 2;
  let B2 = d.position.z + d.geometry.parameters.depth / 2;
  if (t1 < b2 || r1 < l2 || b1 > t2 || l1 > r2 || f1 > B2 || B1 < f2) {
    return false;
  }
  return true;
}

在这样的条件语句中使用它:

if (checkTouching(cube1,cube2)) {
alert("collision!")
}

我在https://3d-collion-test.glitch.me/有一个使用这个的例子

注意:如果您旋转(或缩放)其中一个(或两个)立方体/棱镜,它会检测到好像它们没有被旋转(或缩放)

【讨论】:

    【解决方案4】:

    Lee 答案的更新版本,适用于最新版本的 three.js

    for (var vertexIndex = 0; vertexIndex < Player.geometry.attributes.position.array.length; vertexIndex++)
    {       
        var localVertex = new THREE.Vector3().fromBufferAttribute(Player.geometry.attributes.position, vertexIndex).clone();
        var globalVertex = localVertex.applyMatrix4(Player.matrix);
        var directionVector = globalVertex.sub( Player.position );
    
        var ray = new THREE.Raycaster( Player.position, directionVector.clone().normalize() );
        var collisionResults = ray.intersectObjects( collidableMeshList );
        if ( collisionResults.length > 0 && collisionResults[0].distance < directionVector.length() ) 
        {
            // a collision occurred... do something...
        }
    }
    

    【讨论】:

    • 是第三行的索引还是vertexIndex?
    • 是的。修好了。
    【解决方案5】:

    因为我的其他答案是有限的,所以我做了一些更准确的东西,只在发生碰撞时返回 true,在没有碰撞时返回 false(但有时当仍然存在时) 无论如何,首先制作以下功能:

    function rt(a,b) {
      let d = [b]; 
      let e = a.position.clone();
      let f = a.geometry.vertices.length;
      let g = a.position;
      let h = a.matrix;
      let i = a.geometry.vertices;
        for (var vertexIndex = f-1; vertexIndex >= 0; vertexIndex--) {      
            let localVertex = i[vertexIndex].clone();
            let globalVertex = localVertex.applyMatrix4(h);
            let directionVector = globalVertex.sub(g);
            
            let ray = new THREE.Raycaster(e,directionVector.clone().normalize());
            let collisionResults = ray.intersectObjects(d);
            if ( collisionResults.length > 0 && collisionResults[0].distance < directionVector.length() ) { 
                return true;
        }
        }
     return false;
    }
    

    上面的函数与这个问题的答案相同 Lee Stemkoski(我通过键入来感谢他)但我进行了更改,因此它运行得更快,并且您不需要创建网格数组。好的第2步:创建这个函数:

    function ft(a,b) {
      return rt(a,b)||rt(b,a)||(a.position.z==b.position.z&&a.position.x==b.position.x&&a.position.y==b.position.y)
    }
    

    如果网格 A 的中心不在网格 B 中并且网格 B 的中心不在 A 中或位置相等并且它们实际上是接触的,则返回 true。如果您缩放一个(或两个)网格,这仍然有效。 我有一个例子:https://3d-collsion-test-r.glitch.me/

    【讨论】:

      【解决方案6】:

      这似乎已经解决了,但如果您不习惯使用光线投射和创建自己的物理环境,我有一个更简单的解决方案。

      CANNON.js 和 AMMO.js 都是建立在 THREE.js 之上的物理库。它们创建了一个辅助物理环境,您将对象位置与该场景联系起来以模拟物理环境。 CANNON 的文档很简单,我使用的是它,但它自 4 年前发布以来一直没有更新。该回购已被分叉,社区将其更新为大炮。我将在这里留下一个代码 sn-p 以便您查看它是如何工作的

      /**
      * Floor
      */
      const floorShape = new CANNON.Plane()
      const floorBody = new CANNON.Body()
      floorBody.mass = 0
      floorBody.addShape(floorShape)
      floorBody.quaternion.setFromAxisAngle(
          new CANNON.Vec3(-1,0,0),
          Math.PI / 2
      )
      world.addBody(floorBody)
      
      const floor = new THREE.Mesh(
      new THREE.PlaneGeometry(10, 10),
      new THREE.MeshStandardMaterial({
          color: '#777777',
          metalness: 0.3,
          roughness: 0.4,
          envMap: environmentMapTexture
      })
      )
      floor.receiveShadow = true
      floor.rotation.x = - Math.PI * 0.5
      scene.add(floor)
      
      // THREE mesh
      const mesh = new THREE.Mesh(
          sphereGeometry,
          sphereMaterial
      )
      mesh.scale.set(1,1,1)
      mesh.castShadow = true
      mesh.position.copy({x: 0, y: 3, z: 0})
      scene.add(mesh)
      
      // Cannon
      const shape = new CANNON.Sphere(1)
      const body = new CANNON.Body({
          mass: 1,
          shape,
          material: concretePlasticMaterial
      })
      body.position.copy({x: 0, y: 3, z: 0})
      world.addBody(body)
      

      这会产生一个地板和一个球,但也会在 CANNON.js 环境中创建相同的东西。

      const tick = () =>
      {
          const elapsedTime = clock.getElapsedTime() 
          const deltaTime = elapsedTime - oldElapsedTime
          oldElapsedTime = elapsedTime
      
          // Update Physics World
          mesh.position.copy(body.position)
      
          world.step(1/60,deltaTime,3)
      
      
          // Render
          renderer.render(scene, camera)
      
          // Call tick again on the next frame
          window.requestAnimationFrame(tick)
      }
      

      在此之后,您只需根据物理场景的位置在动画函数中更新 THREE.js 场景的位置。

      请查看文档,因为它可能看起来比实际更复杂。使用物理库将是模拟碰撞的最简单方法。另请查看 Physi.js,我从未使用过它,但它应该是一个更友好的库,不需要您创建辅助环境

      【讨论】:

        【解决方案7】:

        在我的threejs 版本中,我只有geometry.attributes.position.array 而没有geometry.vertices。要将其转换为顶点,我使用以下 TS 函数:

        export const getVerticesForObject = (obj: THREE.Mesh): THREE.Vector3[] => {
          const bufferVertices = obj.geometry.attributes.position.array;
          const vertices: THREE.Vector3[] = [];
        
          for (let i = 0; i < bufferVertices.length; i += 3) {
            vertices.push(
              new THREE.Vector3(
                bufferVertices[i] + obj.position.x,
                bufferVertices[i + 1] + obj.position.y,
                bufferVertices[i + 2] + obj.position.z
              )
            );
          }
          return vertices;
        };
        
        

        我为每个维度传递对象的位置,因为默认情况下 bufferVertices 是相对于对象的中心的,出于我的目的,我希望它们是全局的。

        我还写了一个小函数来检测基于顶点的碰撞。它可以选择对非常复杂的对象的顶点进行采样,或者检查所有顶点与另一个对象的顶点的接近程度:

        const COLLISION_DISTANCE = 0.025;
        const SAMPLE_SIZE = 50;
        export const detectCollision = ({
          collider,
          collidables,
          method,
        }: DetectCollisionParams): GameObject | undefined => {
          const { geometry, position } = collider.obj;
          if (!geometry.boundingSphere) return;
        
          const colliderCenter = new THREE.Vector3(position.x, position.y, position.z);
          const colliderSampleVertices =
            method === "sample"
              ? _.sampleSize(getVerticesForObject(collider.obj), SAMPLE_SIZE)
              : getVerticesForObject(collider.obj);
        
          for (const collidable of collidables) {
            // First, detect if it's within the bounding box
            const { geometry: colGeometry, position: colPosition } = collidable.obj;
            if (!colGeometry.boundingSphere) continue;
            const colCenter = new THREE.Vector3(
              colPosition.x,
              colPosition.y,
              colPosition.z
            );
            const bothRadiuses =
              geometry.boundingSphere.radius + colGeometry.boundingSphere.radius;
            const distance = colliderCenter.distanceTo(colCenter);
            if (distance > bothRadiuses) continue;
        
            // Then, detect if there are overlapping vectors
            const colSampleVertices =
              method === "sample"
                ? _.sampleSize(getVerticesForObject(collidable.obj), SAMPLE_SIZE)
                : getVerticesForObject(collidable.obj);
            for (const v1 of colliderSampleVertices) {
              for (const v2 of colSampleVertices) {
                if (v1.distanceTo(v2) < COLLISION_DISTANCE) {
                  return collidable;
                }
              }
            }
          }
        };
        

        【讨论】: