How to detect collision in three.js?

Viewed 74466

I am using three.js.

I have two mesh geometries in my scene.

If these geometries are intersected (or would intersect if translated) I want to detect this as a collision.

How do I go about performing collision detection with three.js? If three.js does not have collision detection facilities, are there other libraries I might use in conjuction with three.js?

7 Answers

An updated version of Lee's answer that works with latest version of 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...
    }
}

only works on BoxGeometry and BoxBufferGeometry

create the following function:

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

use it in conditional statements like this:

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

I have an example using this at https://3d-collion-test.glitch.me/

Note: if you rotate(or scale) one (or both) of the cubes/prisims, it will detect as though they haven't been turned(or scaled)

since my other answer is limited I made something else that is more accurate and only returns true when there is a collision and false when there isn't (but sometimes when There still is) anyway, First make The Following Function:

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

that above Function is the same as an answer in this question by Lee Stemkoski (who I am giving credit for by typing that) but I made changes so it runs faster and you don't need to create an array of meshes. Ok step 2: create this function:

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

it returns true if the center of mesh A isn't in mesh B AND the center of mesh B isn't in A OR There positions are equal AND they are actually touching. This DOES still work if you scale one (or both) of the meshes. I have an example at: https://3d-collsion-test-r.glitch.me/

It seems like this has already been solved but I have an easier solution if you are not to comfortable using ray casting and creating your own physics environment.

CANNON.js and AMMO.js are both physics libraries built on top of THREE.js. They create a secondary physics environment and you tie your object positions to that scene to emulate a physics environment. the documentation is simple enough to follow for CANNON and it is what I use but it hasnt been updated since it was released 4 years ago. The repo has since been forked and a community keeps it updated as cannon-es. I will leave a code snippet here so you can see how it works

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

This makes a floor and a ball but also creates the same thing in the CANNON.js enironment.

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

After this you just update the position of your THREE.js scene in the animate function based on the position of your physics scene.

Please check out the documentation as it might seem more complicated than it really is. Using a physics library is going to be the easiest way to simulate collisions. Also check out Physi.js, I have never used it but it is supposed to be a more friendly library that doesn't require you to make a secondary environment

In my threejs version, I only have geometry.attributes.position.array and not geometry.vertices. To convert it to vertices, I use the following TS function:

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

I pass in the object's position for each dimension because the bufferVertices by default are relative to the object's center, and for my purposes I wanted them to be global.

I also wrote up a little function to detect collisions based on vertices. It optionally samples vertices for very involved objects, or checks for proximity of all vertices to the vertices of the other object:

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