如何检测旋转矩形之间的碰撞。

3
在多次看到这个问题并回答了旧的(不可用的)代码之后,我决定重新做一切并发表有关此事的帖子。
矩形由以下内容定义:
- `center`:x 和 y 表示其位置(请记住 0;0 是左上角,因此 Y 轴向下) - `size`:x 和 y 表示其大小 - `angle` 表示其旋转角度(以度为单位,0 度表示沿着 X 轴,并顺时针旋转)
目标是判断两个矩形是否发生碰撞。
1个回答

11
我将使用JavaScript来演示这个过程(并提供代码),但是可以用任何语言来完成。
链接:
- [Codepen上的最终演示](link1) - [GitHub存储库](link2)
概念:
为了实现这一点,我们将在另一个矩形的两个轴(X和Y)上使用角投影。只有当一个矩形的四个投影与另一个矩形相交时,这两个矩形才会发生碰撞:
- 蓝色矩形的角在橙色矩形的X轴上 - 蓝色矩形的角在橙色矩形的Y轴上 - 橙色矩形的角在蓝色矩形的X轴上 - 橙色矩形的角在蓝色矩形的Y轴上

3 concept previews

过程

1- 找到矩形轴

首先创建两个向量,代表从矩形中心到X轴(OX)和Y轴(OY),然后将它们旋转以与矩形轴对齐。

维基百科关于旋转二维向量的文章

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}),
  ];
}

Vector是一个简单的x,y对象。
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),
    });
  }
}

线段由两个向量表示:

  • 起点:表示起始位置的向量
  • 方向:表示单位方向的向量
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});
  }
}

步骤结果

enter image description here

2- 使用矩形轴获取角落
首先,我们希望扩展我们的轴(我们以1像素/单位大小为基准),以便获得宽度的一半(对于X轴)和高度的一半(对于Y轴),通过添加它们(或它们的相反数),我们可以获得所有的角落。
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),
  ]
}

使用这两种新的方法来处理向量:
  // 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,
    })
  }

步骤结果

enter image description here

3- 获取角点投影
对于矩形的每个角点,获取另一个矩形在两个轴上的投影坐标。
只需将此函数添加到向量类中即可:
  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,
    })
  }

(特别感谢Mbo提供了解决方案来获取投影。)

步骤结果

enter image description here

4- 选择投影上的外角
为了按矩形轴排序所有投影点并获取外部投影点,我们可以采取以下步骤:
  • 创建一个向量来表示:从矩形中心到投影角的向量
  • 使用向量大小函数来获取距离。
  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);
}

然后,通过使用一个简单的循环和最小/最大值的测试,我们可以找到两个外角。它们之间的线段是一个矩形在另一个轴上的投影。
步骤结果

enter image description here

5- 最终:所有的投影都命中矩形吗?
通过沿轴线进行简单的一维测试,我们可以知道它们是否命中。
const isProjectionHit = (minSignedDistance < 0 && maxSignedDistance > 0
        || Math.abs(minSignedDistance) < rectHalfSize
        || Math.abs(maxSignedDistance) < rectHalfSize);

enter image description here

完成

测试所有4个投影将为您提供最终结果。=] !!

3 concept previews

希望这个回答能够帮助尽可能多的人。欢迎任何评论。


干得不错。你的方法适用于“希腊十字”矩形交叉和完全包含吗? - MBo
1
好的。你看过“分离轴算法”吗?也许你会发现一些相似之处和想法。文章链接 - MBo
在我回答那个问题的时候,我看到了它。但是它看起来比我的要复杂得多(而且也更完整)^^ - Arthur
这太酷了,但是我该如何编写一个函数来接收两个可能旋转的DOM元素,并返回一个布尔值来判断它们是否重叠? - Mike Wagz
@MikeWagz 一样的方式,你只需要计算你的两个DOM元素的位置、宽度和高度,然后运行这段代码来判断它们是否重叠。 - Arthur

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接