62

我正在使用three.js。

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

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

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

4

7 回答 7

116

在 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)。)后一种方法可能会导致性能下降,因此我建议谨慎使用它。

我希望其他人能够通过他们对这个问题的解决方案来为这个问题做出贡献。在开发这里描述的解决方案之前,我自己挣扎了很长时间。

于 2012-07-14T03:13:26.573 回答
4

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

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

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

于 2012-07-13T17:54:13.383 回答
3

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...
    }
}
于 2022-01-10T11:38:11.470 回答
2

仅适用于 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/有一个使用这个的例子

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

于 2020-07-16T00:36:58.420 回答
1

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

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

/**
* 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,我从未使用过它,但它应该是一个更友好的库,不需要您创建辅助环境

于 2021-02-03T23:29:18.243 回答
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;
}

上面的 Function 与 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/

于 2020-08-15T01:53:51.793 回答
0

geometry.attributes.position.array在我的 threejs 版本中,我只有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;
        }
      }
    }
  }
};
于 2021-09-26T01:35:01.763 回答