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

IT技术 javascript three.js webgl collision-detection
2021-03-01 02:37:55

我正在使用three.js。

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

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

我如何使用three.js 执行碰撞检测?如果three.js 没有碰撞检测功能,还有其他可以与three.js 结合使用的库吗?

6个回答

在 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 旋转它。当它与其中一个蓝色立方体相交时,如上所述,对于每个相交,屏幕顶部都会出现一次“命中”一词。代码的重要部分如下。

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

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

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

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

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

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

您链接的演示是光线碰撞
2021-04-20 02:37:55

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

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

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

CANNON.js 和 AMMO.js 都是建立在 THREE.js 之上的物理库。它们创建了一个二级物理环境,然后您将对象位置与该场景联系起来以模拟物理环境。文档很简单,可以遵循 CANNON 并且这是我使用的文档,但自 4 年前发布以来一直没有更新。该 repo 已经分叉,社区将其更新为 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)
}

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

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