如何在 three.js 中检测碰撞?

74

我正在使用three.js。

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

如果这些几何体相交(或者在平移时将相交),我想将其检测为碰撞。

如何使用three.js执行碰撞检测? 如果three.js没有碰撞检测功能,那么是否有其他库可以与three.js一起使用?


6
我在谷歌上搜索过,但只找到了有关光线碰撞的信息。 - eqiproo
4
我认为使用光线碰撞是可行的…Adnan(据推测)参考的CollisionUtils.js和Collisions.js文件已经过时,不包括最新的(写作时的v49版本)three.js版本。 - Stemkoski
40
我认为这是一个好问题。有时候 SO 的回答很蠢。 - Lee Goddard
21
我会尽力为您翻译:@Adi,我已经谷歌搜索过了,而这确实是第一个结果。 - user3024235
我需要创建一堵墙并在其上添加一个窗户,用户可以拖动窗户到他们决定放置在墙上的位置之前。我必须限制窗户在墙的范围内拖动。我认为我需要检测碰撞并获取顶点或其他内容。我不确定,请给出建议。我是three.js或任何3D应用程序的新手。 - Deeps
10个回答

122
在Three.js中,实用程序CollisionUtils.js和Collisions.js似乎不再受支持,而mrdoob(three.js的创建者)本人建议更新到最新版本的three.js,并使用Ray类来代替。以下是一种方法。
思路是这样的:假设我们想检查一个名为“Player”的给定网格是否与包含在名为“collidableMeshList”的数组中的任何网格相交。我们可以创建一组射线,这些射线从Player网格的坐标(Player.position)开始,并向几何体中每个顶点延伸。每个Ray都有一个名为“intersectObjects”的方法,该方法返回Ray与之相交的对象数组以及到每个对象的距离(从Ray的原点测量)。如果交点到交点的距离小于Player位置和几何体顶点之间的距离,则发生了碰撞-我们可能会称之为“实际”碰撞。
我已经发布了一个工作示例:

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) 对于一个相对于玩家网格来说很小的物体,它可能会“滑”过各种射线,从而没有发生任何碰撞。减少这个问题的可能方法有两种:一种是编写代码,使小对象创建射线并从它们的角度进行碰撞检测;另一种是在网格上包含更多的顶点(例如使用CubeGeometry(100, 100, 100, 20, 20, 20)而不是CubeGeometry(100, 100, 100, 1, 1, 1))。后一种方法可能会导致性能下降,因此建议谨慎使用。
希望其他人也能为这个问题提供他们的解决方案。在开发本文所述的解决方案之前,我自己也苦苦挣扎了一段时间。

4
非常感谢你提供了这么详细的解释!我仍在苦苦寻找适合我的地形和3D物体游戏的解决方案,你的回答为我提供了一些新的思路! - Nick
3
虽然这种方法似乎是从物体中心测试任何顶点是否相交,但测试所有边缘(连接的顶点)会慢两倍,但精度为100%。因此,要详细说明,您需要循环遍历每个面,并取顶点[n]和顶点[(n + 1)%len]以获取所有边。如果我拥抱某人,他们会与我的位置和手的中心相交,但是它们不会与我的皮肤相交,因为进行边缘检查会测试这一点。 - dansch
8
建议避免在渲染循环内实例化新的 "Raycaster"。可以先实例化一个,然后重复使用它。 - WestLangley
很好的答案。只是为了澄清:collisionResults [0] .distance是从射线起点到射线与物体相交的位置的距离(对于复杂形状的物体,它不一定是从射线起点到物体的最短距离)。 - Jan Wrobel
5
如果网格几何体中没有geometry.vertices数据,那该怎么办?在OBJ模型中,有geometry.attributes.position.array,但没有geometry.vertices - XIMRX
显示剩余8条评论

6
一份更新的 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...
    }
}

1
第三行是index还是vertexIndex? - Hesamoy
是的,已经修复了。 - Kartheyan

4
这个主题太广泛了,无法在SO问题中涵盖,但是为了使网站的SEO更好,这里提供几个简单的起点:
如果您只想要非常简单的碰撞检测而不是全功能物理引擎,请查看(由于网站不存在,已删除链接)。
另一方面,如果您确实想要一些碰撞响应,而不仅仅是“A和B相撞了吗?”,请查看(由于网站不存在,已删除链接),这是一个围绕Three.js构建的超级易于使用的Ammo.js包装器。

4
你提供的演示是光线碰撞。 - eqiproo

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/ 上查看。

注意:如果您旋转(或缩放)一个(或两个)立方体/棱柱,它将被检测为未经转动(或缩放)。


1

看起来这个问题已经解决了,但如果你不太喜欢使用光线投射和创建自己的物理环境,我有一个更简单的解决方案。

CANNON.js 和 AMMO.js 都是基于 THREE.js 构建的物理库。它们创建了一个次要的物理环境,并将你的对象位置绑定到该场景以模拟物理环境。CANNON 的文档足够简单,我使用它,但自从四年前发布以来就没有更新过。这个仓库已经被 fork 并且社区一直在更新它作为 cannon-es。我将在这里留下一个代码片段,让你看看它是如何工作的。

/**
* 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)
}

在此之后,您只需要根据物理场景的位置,在animate函数中更新您的THREE.js场景的位置即可。
请查看文档,因为它可能比实际上更复杂。使用物理库是模拟碰撞最简单的方法。还要查看Physi.js,虽然我从未使用过它,但它应该是一个更友好的库,不需要您创建第二个环境。

1

由于我的其他答案有限,我制作了另一种更准确的东西,只有在发生碰撞时才返回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在这个问题中给出的答案相同(我通过打字给他信用),但我进行了更改,使其运行更快,并且您不需要创建网格数组。好的,第二步:创建此函数:

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/有一个示例。


0
在我的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;
};

我传递每个维度的对象位置,因为缓冲区顶点默认相对于对象中心,而出于我的目的,我希望它们是全局的。

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

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

0
我只是遍历了所有的墙壁。我检查了x和z坐标。如果它们匹配,我就把相机往后移动一点。这就是我做的方式。 这可以在7中看到。
假设this.segments是一个Wall对象的数组。
this.segments.forEach(elem => {
  var pos = elem.position;
  if ((Math.round(this.camera.position.x) === (pos.x | pos.z)) || (Math.round(this.camera.position.z) === (pos.x | pos.z))) {
  console.log(Math.round(this.camera.position.x) + "x" + pos.x + " " + pos.z);
  console.log(Math.round(this.camera.position.z) + "z" + pos.x + " " + pos.z);
  if ((Math.round(this.camera.position.z) === (pos.z)) && this.hid.moveForward) {
    this.hid.controls.moveForward(-moveSpeed);
  } else if ((Math.round(this.camera.position.z) !== (pos.z)) && this.hid.moveForward) {
    this.hid.controls.moveForward(moveSpeed);
  }
  else if ((Math.round(this.camera.position.z) === (pos.z)) && this.hid.moveBackward) {
    this.hid.controls.moveForward(-moveSpeed);
  }
  else if ((Math.round(this.camera.position.z) !== (pos.z)) && this.hid.moveBackward) {
    this.hid.controls.moveForward(moveSpeed);
  }
  if (Math.round(this.camera.position.x) === (pos.x) && this.hid.moveLeft) {
    this.hid.controls.moveRight(moveSpeed);
  }
  else if (Math.round(this.camera.position.x) !== (pos.x) && this.hid.moveLeft) {
    this.hid.controls.moveRight(-moveSpeed);
  }
  else if (Math.round(this.camera.position.x) === (pos.x) && this.hid.moveRight) {
    this.hid.controls.moveRight(-moveSpeed);
  }
  else if (Math.round(this.camera.position.x) !== (pos.x) && this.hid.moveRight) {
    this.hid.controls.moveRight(moveSpeed);
  }
  if (Math.round(this.camera.position.x) === (pos.z) && this.hid.moveLeft) {
    this.hid.controls.moveRight(moveSpeed);
  }
  else if (Math.round(this.camera.position.x) !== (pos.z) && this.hid.moveLeft) {
    this.hid.controls.moveRight(-moveSpeed);
  }
  else if (Math.round(this.camera.position.x) === (pos.z) && this.hid.moveRight) {
    this.hid.controls.moveRight(-moveSpeed);
  }
  else if (Math.round(this.camera.position.x) !== (pos.z) && this.hid.moveRight) {
    this.hid.controls.moveRight(moveSpeed);
  }
  }
});

0
你可以尝试使用 cannon.js。它使得碰撞检测变得容易,是我最喜欢的碰撞检测库之一。还有 ammo.js 也不错。

虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供参考链接。如果链接页面更改,仅有链接的答案可能会失效。- 来自审查 - Lord-JulianXLII

-1
试试这个:
let hasCollided = false
      // listener for collision
      thing1.addEventListener('collide', (e) => {
        // make sure it only fires once
        if (!hasCollided) {
          // make sure its colliding with the right target
          if (e.detail.body.el.id === 'thing2') {
            hasCollided = true
            doThing()
          }
        }
      }) 

你的回答目前写得不清楚。请编辑以添加更多详细信息,帮助其他人理解如何回答问题。你可以在帮助中心找到关于如何撰写好回答的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接