如何在three.js中检测冲突

How to detect collision in three.js?

本文关键字:检测 冲突 js three      更新时间:2023-09-26

我使用的是three.js.

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

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

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

在Three.js中,似乎不再支持实用程序CollisionUtils.js和Collisions.js,mrdoob(Three.js的创建者)自己建议更新到Three.js的最新版本,并为此使用Ray类。以下是一种方法。

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

我在上发布了一个工作示例

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

可以使用箭头键移动红色线框立方体,并使用W/A/S/D旋转它。当它与其中一个蓝色立方体相交时,如上所述,每个相交处的屏幕顶部都会出现一次"命中"一词。代码的重要部分如下。

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、20、20)而不是CubeGeometrics(100、10、100、1、1)。)后一种方法可能会影响性能,所以我建议谨慎使用它。

我希望其他人将为这个问题作出贡献,提出解决这个问题的办法。在开发这里描述的解决方案之前,我自己也为此奋斗了很长一段时间。

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

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

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

另一方面,如果你确实想要一些碰撞响应,而不仅仅是";A和B撞了吗&";,看看(由于没有现有网站,链接被删除),这是一个超级容易使用的Ammo.js包装器,它是围绕Three.js 构建的

仅适用于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/

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

因为我的另一个答案是有限的,我做了其他更准确的东西,只在发生碰撞时返回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/

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

CANNON.js和AMMO.js都是建立在THRE.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,我从未使用过它,但它应该是一个更友好的库,不需要您制作辅助环境

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

您可以尝试canon.js。它使碰撞和我最喜欢的碰撞检测库变得容易。还有ammon.js。

试试这个:

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()
          }
        }
      })