问题:判断两个旋转矩形是否重叠(相交和包含)
矩形的坐标是旋转前的坐标:
矩形A(left1,top1,width1,height1,angle1)
矩形B(left2,top2,width2,height2,angle2)
方法1:碰撞检测判断相交 + 点在多边形内部判断包含
遍历A的所有边,并与B的所有边做相交判断。该方法适用于所有的多边形相交碰撞检测问题
相交的定义为:重叠部分的面积>0,故相切情况不属于相交
不属于相交情况下,再判断包含的情况:
取矩阵A中心点P,判断P是否在矩阵B内部,同理判断矩阵B中心点P’是否在矩阵A内部,
两次判断只要有一次属于内部即可判断处于包含情况。
1.判断两线段相交
- 快速排斥
一个线段中 x 较大的端点是否小于另一个线段中 x 较小的段点,若是,则说明两个线段必然没有交点,同理判断下 y - 跨立实验
判断 A 点与 B 点是否在线段 DC 的两侧,即向量 DA 与向量 DB 分别在向量 DC 的两端,也就是其两个叉积结果是异号的,即
(DA 叉乘 DC) * (DB 叉乘 DC) < 0
同时还需要证明 C 点与 D 点在线段 AB 的两端,两个同时满足,则表示线段相交
注意: 共线情况可以通过跨立实验,不过在快速排斥阶段就被剔除了
// 判断线段AB 和线段CD 是否相交
function judgeSegmentsIntersect (A, B, C, D) {
//快速排斥, 不考虑相切情况 判断时要算上等于
if (Math.max(C.x, D.x) <= Math.min(A.x, B.x) || Math.max(C.y, D.y) <= Math.min(A.y, B.y) ||
Math.max(A.x, B.x) <= Math.min(C.x, D.x) || Math.max(A.y, B.y) <= Math.min(C.y, D.y)) {
return false
}
// 向量叉乘
const crossMul = (v1, v2) => {
return v1.x * v2.y - v1.y * v2.x
}
const vector = (start, end) => {
return {
x: end.x - start.x,
y: end.y - start.y
}
}
let AC = vector(A, C)
let AD = vector(A, D)
let BC = vector(B, C)
let BD = vector(B, D)
let CA = vector(C, A)
let DA = vector(D, A)
let CB = vector(C, B)
let DB = vector(D, B)
return (crossMul(AC, AD) * crossMul(BC, BD) <= 0)
&& (crossMul(CA, CB) * crossMul(DA, DB) <= 0)
}
2. 判断矩形相交
// 绕原点逆时针旋转后的点坐标
// 默认绕原点旋转
const rotate = ({ x, y }, deg, origin = { x: 0, y: 0 }) => ({
x: (x - origin.x) * Math.cos(deg) + (y - origin.y) * Math.sin(deg) + origin.x,
y: (origin.x - x) * Math.sin(deg) + (y - origin.y) * Math.cos(deg) + origin.y
})
const toDeg = (angle) => angle / 180 * Math.PI
const getCenterPoint = (box) => ({
x: box.left + box.width / 2,
y: box.top + box.height / 2
})
/**
* 转化为顶点坐标数组
* @param {Object} box
*/
function toRect (box) {
let deg = toDeg(box.angle)
let cp = getCenterPoint(box)
return [rotate({
x: box.left,
y: box.top
}, deg, cp), rotate({
x: box.left + box.width,
y: box.top,
}, deg, cp), rotate({
x: box.left + box.width,
y: box.top + box.height,
}, deg, cp), rotate({
x: box.left,
y: box.top + box.height
}, deg, cp)]
}
/**
* 判断矩形相交
*/
function judgeRectanglesIntersect (box1, box2) {
let rect1 = toRect(box1)
let rect2 = toRect(box2)
for (let i = 0; i < rect1.length; i++) {
let A = rect1[i]
let B = i === rect1.length - 1 ? rect1[0] : rect1[i + 1]
for (let j = 0; j < rect2.length; j++) {
let C = rect2[j]
let D = j === rect2.length - 1 ? rect2[0] : rect2[j + 1]
if (judgeSegmentsIntersect(A, B, C, D)) {
return true
}
}
}
return false
}
3. 判断矩阵包含
/**
* 已知两矩形不相交
* 判断矩形是否属于包含关系
* @param {*} rect1
* @param {*} rect2
*/
function judgeRectanglesContain (box1, box2) {
const getCenterPoint = (box) => ({
x: box.left + box.width / 2,
y: box.top + box.height / 2
})
let p1 = getCenterPoint(box1)
let p2 = getCenterPoint(box2)
// 点P需要绕另一个点P'逆时针旋转得到新的位置
let np1 = rotate(p1, toDeg(box2.angle), p2)
let np2 = rotate(p2, toDeg(box1.angle), p1)
// 判断点P是否在水平坐标系的矩形box中
const isInside = (p, { left, top, width, height }) => {
return p.x >= left && p.x <= left + width && p.y >= top && p.y <= top + height
}
return isInside(np1, box2) || isInside(np2, box1)
}
求出相交的点:
4.计算相交区域面积
计算所有顶点的横纵坐标均值,记作中心点,计算中心点到每个点的单位向量,以x轴正方向为起始边,按照顺时针方向扫描360度,对扫描到的点进行排序,先考虑从180度到360度,y>0,x从-1到1递增,对于从0到180度,y<0,x从1到-1递减。然后计算三角形面积(利用叉积) 最后将三角形面积求和。
方法2:分离轴定律/OBB方向包围盒算法
参考文章 矩形旋转碰撞,OBB方向包围盒算法实现
简单说 包围盒 就是用一个方便分离轴的规则形状去包围一个物体
而 分离轴定律 即是根据两个多边形在所有轴上的投影是否重叠判断是否碰撞
如何检测轴投影是否重叠?有两种方法。
- 计算每个矩形在检测轴的最大投影坐标区间,判断两个坐标区间是否重叠
- 计算每个矩形的半径投影(该矩形在检测轴的投影大小的一半),将这两个投影长度之和与两个矩形中心点连线在检测轴上投影的长度比较
这里我们采用第二种。
首先确定检测轴向量,为了计算方便,我们假设 检测轴向量 为单位向量,取值(cosθ,sinθ),其中 θ 为该向量与x轴的夹角,大小始终>0,对于另一坐标轴为90-θ
然后计算半径投影
检测轴为自身的矩形,半径投影即该检测轴长度的一半
检测轴在另一个矩形,半径投影即该矩形两边长在检测轴上投影大小之和的一半
假设目标轴为向量A,检测轴为向量B,则目标轴在检测轴上的投影为
上面提到 检测轴向量 为单位向量,所以目标轴向量(x,y)在检测轴上的投影为 x·cosθ+y·sinθ,即
/**
* 计算投影半径
* @param {Array(Number)} checkAxis 检测轴 [cosθ,sinθ]
* @param {Array} axis 目标轴 [x,y]
*/
function getProjectionRadius (checkAxis, axis) {
return Math.abs(axis[0] * checkAxis[0] + axis[1] * checkAxis[1])
}
全部代码如下:
// 绕原点逆时针旋转后的点坐标
// 默认绕原点旋转
const rotate = ({ x, y }, deg, origin = { x: 0, y: 0 }) => ({
x: (x - origin.x) * Math.cos(deg) + (y - origin.y) * Math.sin(deg) + origin.x,
y: (origin.x - x) * Math.sin(deg) + (y - origin.y) * Math.cos(deg) + origin.y
})
const toDeg = (angle) => angle / 180 * Math.PI
const getCenterPoint = (box) => ({
x: box.left + box.width / 2,
y: box.top + box.height / 2
})
/**
* 转化为顶点坐标数组
* @param {Object} box
*/
function toRect (box) {
let deg = toDeg(box.angle)
let cp = getCenterPoint(box)
return [rotate({
x: box.left,
y: box.top
}, deg, cp), rotate({
x: box.left + box.width,
y: box.top,
}, deg, cp), rotate({
x: box.left + box.width,
y: box.top + box.height,
}, deg, cp), rotate({
x: box.left,
y: box.top + box.height
}, deg, cp)]
}
/**
* 计算投影半径
* @param {Array(Number)} checkAxis 检测轴 [cosθ,sinθ]
* @param {Array} axis 目标轴 [x,y]
*/
function getProjectionRadius (checkAxis, axis) {
return Math.abs(axis[0] * checkAxis[0] + axis[1] * checkAxis[1])
}
/**
* 判断是否碰撞
* @param {Array} rect1 矩形顶点坐标数组 [Pa,Pb,Pc,Pd]
* @param {*} rect2
*/
function isCollision (box1, box2) {
let rect1 = toRect(box1)
let rect2 = toRect(box2)
const vector = (start, end) => {
return [end.x - start.x, end.y - start.y]
}
// 两个矩形的中心点
const p1 = getCenterPoint(box1)
const p2 = getCenterPoint(box2)
//向量 p1p2
const vp1p2 = vector(p1, p2)
//矩形1的两边向量
let AB = vector(rect1[0], rect1[1])
let BC = vector(rect1[1], rect1[2])
//矩形2的两边向量
let A1B1 = vector(rect2[0], rect2[1])
let B1C1 = vector(rect2[1], rect2[2])
// 矩形1 的两个弧度
let deg11 = toDeg(box1.angle)
let deg12 = toDeg(90 - box1.angle)
// 矩形2 的两个弧度
let deg21 = toDeg(box2.angle)
let deg22 = toDeg(90 - box2.angle)
// 投影重叠
const isCover = (checkAxisRadius, deg, targetAxis1, targetAxis2) => {
let checkAxis = [Math.cos(deg), Math.sin(deg)]
let targetAxisRadius = (getProjectionRadius(checkAxis, targetAxis1) + getProjectionRadius(checkAxis, targetAxis2)) / 2
let centerPointRadius = getProjectionRadius(checkAxis, vp1p2)
console.log(`checkAxis:${checkAxis},三个投影:${checkAxisRadius}, ${targetAxisRadius}, ${centerPointRadius}`)
return checkAxisRadius + targetAxisRadius > centerPointRadius
}
return isCover(box1.width / 2, deg11, A1B1, B1C1) &&
isCover(box1.height / 2, deg12, A1B1, B1C1) &&
isCover(box2.width / 2, deg21, AB, BC) &&
isCover(box2.height / 2, deg22, AB, BC)
}
(function main () {
let box1 = {
left: 0,
top: 0,
width: 100,
height: 100,
angle: 30
}
let box2 = {
left: 100,
top: 0,
width: 100,
height: 100,
angle: 0
}
return isCollision(box1, box2)
})()