javascriptmathgeometrycollision-detectionrectangles

How to detect when rotated rectangles are colliding each other


After seeing this question many times and replying with old (and not usable) code I decided to redo everything and post about it.

Rectangles are defined by:

The goal is to know if 2 rectangles are colliding or not.


Solution

  • I will use Javascript in order to demo this (and also provide code) but I can be done on every language following the process.

    Links

    Concept

    In order to achieve this we'll use corners projections on the other rectangle 2 axis (X and Y). The 2 rectangles are only colliding when the 4 projections on one rectangles hit the others:

    3 concept previews

    Process

    1- Find the rects axis

    Start by creating 2 vectors for axis 0;0 (center of rect) to X (OX) and Y (OY) then rotate both of them in order to get aligned to rectangles axis.

    Wikipedia about rotate a 2D vector

    const getAxis = (rect) => {
      const OX = new Vector({x:1, y:0});
      const OY = new Vector({x:0, y:1});
      // Do not forget to transform degree to radian
      const RX = OX.Rotate(rect.angle * Math.PI / 180);
      const RY = OY.Rotate(rect.angle * Math.PI / 180);
    
      return [
         new Line({...rect.center, dx: RX.x, dy: RX.y}),
         new Line({...rect.center, dx: RY.x, dy: RY.y}),
      ];
    }
    

    Where Vector is a simple x,y object

    class Vector {
      constructor({x=0,y=0}={}) {
        this.x = x;
        this.y = y;
      }
      Rotate(theta) {
        return new Vector({
          x: this.x * Math.cos(theta) - this.y * Math.sin(theta),
          y: this.x * Math.sin(theta) + this.y * Math.cos(theta),
        });
      }
    }
    

    And Line represent a slop using 2 vectors:

    class Line {
      constructor({x=0,y=0, dx=0, dy=0}) {
        this.origin = new Vector({x,y});
        this.direction = new Vector({x:dx,y:dy});
      }
    }
    

    Step Result

    enter image description here

    2- Use Rect Axis to get corners

    First we want extend our axis (we are 1px/unit size) in order to get the half of width (for X) and height (for Y) so by adding then (or inverse of them) we can get the all corners.

    const getCorners = (rect) => {
      const axis = getAxis(rect);
      const RX = axis[0].direction.Multiply(rect.w/2);
      const RY = axis[1].direction.Multiply(rect.h/2);
      return [
        rect.center.Add(RX).Add(RY),
        rect.center.Add(RX).Add(RY.Multiply(-1)),
        rect.center.Add(RX.Multiply(-1)).Add(RY.Multiply(-1)),
        rect.center.Add(RX.Multiply(-1)).Add(RY),
      ]
    }
    

    Using this 2 news methods for Vector:

      // Add(5)
      // Add(Vector)
      // Add({x, y})
      Add(factor) {
        const f = typeof factor === 'object'
          ? { x:0, y:0, ...factor}
          : {x:factor, y:factor}
        return new Vector({
          x: this.x + f.x,
          y: this.y + f.y,
        })
      }
      // Multiply(5)
      // Multiply(Vector)
      // Multiply({x, y})
      Multiply(factor) {
        const f = typeof factor === 'object'
          ? { x:0, y:0, ...factor}
          : {x:factor, y:factor}
        return new Vector({
          x: this.x * f.x,
          y: this.y * f.y,
        })
      }
    

    Step Result

    enter image description here

    3- Get corners projections

    For every corners of a rectangle, get the projection coord on both axis of the other rectangle.

    Simply by adding this function to Vector class:

      Project(line) {
        let dotvalue = line.direction.x * (this.x - line.origin.x)
          + line.direction.y * (this.y - line.origin.y);
        return new Vector({
          x: line.origin.x + line.direction.x * dotvalue,
          y: line.origin.y + line.direction.y * dotvalue,
        })
      }
    

    (Special thank to Mbo for the solution to get projection.)

    Step Result

    enter image description here

    4- Select externals corners on projections

    In order to sort (along the rect axis) all the projected point and take the externals projected points we can:

      get magnitude() {
        return Math.sqrt(this.x * this.x + this.y * this.y);
      }
    
    getSignedDistance = (rect, line, corner) => {
      const projected = corner.Project(line);
      const CP = projected.Minus(rect.center);
      // Sign: Same directon of axis : true.
      const sign = (CP.x * line.direction.x) + (CP.y * line.direction.y) > 0;
      const signedDistance = CP.magnitude * (sign ? 1 : -1);
    }
    

    Then using a simple loop and test of min/max we can find the 2 externals corners. The segment between them is the projection of one Rect on the other one axis.

    Step result

    enter image description here

    5- Final: Do all projections hit rect ?

    Using simple 1D test along the axis we can know if they hit or not:

    const isProjectionHit = (minSignedDistance < 0 && maxSignedDistance > 0
            || Math.abs(minSignedDistance) < rectHalfSize
            || Math.abs(maxSignedDistance) < rectHalfSize);
    

    enter image description here

    Done

    Testing all 4 projections will give you the final result. =] !!

    3 concept previews

    Hope this answer will help as many people as possible. Any comments are appreciated.