AFrame & Three.JS detecting collision between moving point and box which happens between frames

Viewed 127

I'm trying to implement "bullet and target collision" problem and create an explosion when collision occurs. I managed to do it using aframe-physics-system which was working good: the explosion was rendering at the exact point of the collision and in the exact time. Now I decided to get rid of the physics system as I don't need such overhead - my only goal is to render an explosion.

I tried to use box.containsPoint as well as Raycaster:

tick(time: number, delta: number): void {
  
  // bullet-component
  // ...

  // Update speed based on acceleration
  this.speed = this.currentAcceleration * .01 * delta;
  if (this.speed > this.data.maxSpeed) {
    this.speed = this.data.maxSpeed;
  }

  // there is an initial position and direction set in data property.
  const newBulletPosition = this.position.add(this.direction.multiplyScalar(this.speed));

  // targets is an array of boxes
  const found = this._detectCollision(newBulletPosition, this.targets);
    if (found) {
      console.log("found!");
      this.resetBullet();
      this.el.emit("collide", {
        coordinates: newBulletPosition//found
      });
      return;
    }

  this.el.object3D.position.set(newBulletPosition.x, newBulletPosition.y, newBulletPosition.z);
},
_detectCollision(point: THREE.Vector3, obj: THREE.Object3D[]): THREE.Vector3 | null {
  const ray = new THREE.Raycaster(point,
    this.temps.direction.clone().multiplyScalar(-1).normalize());
  const intersects = ray.intersectObjects(obj, true);

  return intersects.length % 2 === 1 ? intersects[0].point : null;
},
_box: new THREE.Box3(),
_inverseWorldMatrix: new THREE.Matrix4(),
_detectCollision2(point: THREE.Vector3, obj: THREE.Object3D): THREE.Vector3 | null {
  obj.updateMatrixWorld(true);
  this._inverseWorldMatrix.copy(obj.matrix).invert();

  this._box.setFromObject(obj);

  this._inverseBulletPosition.set(point.x, point.y, point.z);
  this._inverseBulletPosition.applyMatrix4(this._inverseWorldMatrix);

  return this._box.containsPoint(this._inverseBulletPosition);
}

But both approaches have the following flaw: On frame X the bullet is just in front of a box, but in frame X+1 it is already behind this box. For some reason in this case there might be desirable intersections, but the last bullet position is different than the intersection. Which causes the explosion to be rendered in a wrong position. So, the second approach works only if bullet during it's "jumps" appears inside of a box which is far from being frequent.

The question is how in this case I can repeat the behaviour I had with physics system:

  1. Bullet is moving relatively fast
  2. The intersection is being detected instantly once a bullet crosses any face of a box, so there is no "jump" in bullet's movement.

Thanks in advance.

2 Answers

This is a common problem when trying to recreate the calculations of a physics engine. Since your bullet is too small and sometimes travels beyond the wall in between frames, I see two options:

  1. On frame x+1 you could calculate how much distance has been traveled since frame x, and use that as the size of the bullet. If the plane is crossed in the distance travelled between x -> x1, then you know you've had a collision.
  2. If collision points don't move, you could use a THREE.Raycaster and calculate the point of collision pre-emptively, so you'll know where the bullet will hit before that point is reached:
const raycaster = new THREE.Raycaster();

shoot() {
  raycaster.set(origin, direction);
  const intersects = raycaster.intersectObjects(arrayOfWalls);

  // No intersection took place
  if (intersects[0] == undefined) return;

  // How far away from origin the collision takes place.
  intersects[0].distance;

  // The Vector3 where the bullet crosses the wall
  intersects[0].point;
}

You can read more about Raycasters in the docs.

Thanks to @Marquizzo, I ended up with the following solution:

I'm casting a ray from the bullet position to the position of the gun. If there is 1 intersection, then the bullet is inside of the box, so I can render an explosion at the intersection position. But if there are two intersections I will take the second one as it will be more far from the ray origin point and hence closer to the gun. But also I had to calculate the distance between the bullet position and the intersection which as was advised should be less than the distance bullet passed between the frames:

tick(time: number, delta: number): void {
  const el = this.el;
  if (!el) {
    console.warn("AFRAME entity is undefined.");
    return;
  }

  this.el.object3D.lookAt(this.direction.clone().multiplyScalar(1000));
  // Update acceleration based on the friction
  this.temps.position.copy(this.el.object3D.position);

  // Update speed based on acceleration
  this.speed = this.currentAcceleration * 0.05 * delta;
  if (this.speed > this.data.maxSpeed) {
    this.speed = this.data.maxSpeed;
  }

  // Set new position
  this.temps.direction.copy(this.direction);
  const newBulletPosition = this.temps.position.add(this.temps.direction.multiplyScalar(this.speed));

  if (newBulletPosition.length() >= FADE_DISTANCE) {
    this.resetBullet();
    return;
  }

  const found = this._detectCollision(newBulletPosition, this.targetCollisionShapes);
  if (found) {
    const jumpDistance = newBulletPosition.clone().sub(this.el.object3D.position).length();
    const collisionDistance = newBulletPosition.clone().sub(found).length();
    if (collisionDistance < jumpDistance) {
      console.log("found!");
      this.resetBullet();
      this.el.emit("collide", {
        target: this.target,
        coordinates: found
      } as CollisionEvent);
      return;
    }

  this.el.object3D.position.set(newBulletPosition.x, newBulletPosition.y, newBulletPosition.z);
},
_detectCollision(point: THREE.Vector3, obj: THREE.Object3D[]): THREE.Vector3 | null {
  const ray = new THREE.Raycaster(point, this.direction.clone().multiplyScalar(-1).normalize());
  const intersects = ray.intersectObjects(obj, true);

  return intersects.length % 2 === 1
    ? intersects[0].point
    : intersects.length > 1 ? intersects[1].point : null;
}
Related