阅读 1130

图形学——平面中图形碰撞检测梳理

前言

以前做过两年多的canvas图形方向的程序员,但是已经很久没有使用过相关的知识忘记了不少,趁我还有印象特此做一下记录。
如果你有兴趣做个独立开发者,开发一些小游戏,只学习一些api和框架是没用的,图形类知识才是核心,未来我会不定期更新相关图形类内容。

在游戏领域中,我们经常会遇到需要进行碰撞检测的对象,而在一些情境下碰撞检测的对象不仅仅是简单的圆形、矩形等基础图形,本文整理了大部分常见图形及不规则图形团的碰撞检测方案。

基础图形

基础图形之间的碰撞非常简单,主要包括不旋转的矩形以及圆形。正常只有在一些需要比较基础判断的时候会用到(实际场景并不算多)

锚点

先来普及以下锚点的概念,对于一个基本的矩形一般都是有以下几个基本属性。 先来说一下锚点,通常是图形的几何中心,也是这个图形的核心点,比如我们描述rect1图形的位置时使用的rect1.xrect1.y,实际上指的就是这个锚点相对于它的坐标系的位置。如果我们对这个图形进行平移、旋转、缩放等操作也都是围绕这个锚点进行的。比如进行一次旋转:

锚点非常重要,如果你想进行图形开发,初始化锚点位置的时候一定要注意,通常我们使用一些框架画图形的时候会默认在左上角,你想默认将锚点放在中心位置也没关系,重要的是所有图形的锚点要遵循同一个规范。(也就是说要么都在左上角,要么都在中心位置)

矩形和矩形

只要判断两个矩形的任意一边是否无间距,从而判断是否碰撞。

(类似伪代码的形式帮助理解)


let rect1Left = rect1.x, rect1Right = rect1.x + rect1.widht; 
let rect1Top = rect1.y, rect1Bottom = rect1.y + rect1.height;
let rect2Left = rect2.x, rect2Right = rect2.x + rect2.widht;
let rect2Top = rect2.y, rect2Bottom = rect2.y + rect2.height;

((rect1Left > rect2Left && rect1Left < rect2Right) || 
(rect1Right > rect2Left && rect1Right < rect2Right)) &&  // x轴有重叠
(rect1Top > rect2Top && rect1Top < rect2Bottom) || 
(rect1Bottom > rect2Top && rect1Bottom < rect2Bottom) // y轴有重叠
复制代码

圆形和圆形

圆形之间:判断两个圆形圆心之间的距离是否小于两个圆形的半径和。

Math.sqrt(Math.pow(circleA.x - circleB.x, 2) +Math.pow(circleA.y - circleB.y, 2))< circleA.radius + circleB.radius

复制代码

矩形和圆形

先找到矩形距离圆心最近的点,然后判断该点与圆心的距离是否小于圆形半径。

// 矩形的锚点为矩形左上角
let targetPoint = {x: 0, y: 0}; // 矩形距离圆心最近的点
if(circle.x > react.x + react.width ) {
    // 如果圆形在矩形的右边
    targetPoint.x = rect.x + rect.width;
}else if(circle.x < react.x) {
    // 如果圆形在矩形的左边
    targetPoint.x = react.x;
}else {
    // 圆形中心的x在矩形中间
    targetPoint.x = circle.x
}
if(circle.y > react.y + react.height ) {
    // 如果圆形在矩形的下边
    targetPoint.x = rect.x + rect.width;
}else if(circle.y < react.y) {
    // 如果圆形在矩形的上边
    targetPoint.y = react.y;
}else {
    // 圆形中心的y在矩形中间
    targetPoint.y = circle.y
}

let result = Math.sqrt(Math.pow(targetPoint.x - circle.x, 2) + Math.pow(targetPoint.y - circle.y, 2)) < circle.radius;

复制代码

复杂图形

复杂图形的判断是最常用的,下面会介绍两种方式对图形碰撞进行判断,理论上如果你掌握了下面两种图形判断方式,可以判断所有图形是否碰撞,但是有些场景,性能方面就需要你做额外的优化。

不规则多边形

在游戏或者其他图形类应用中都会有很多复杂的图形,比如下图: 任意两个多边形之间的碰撞一定是两个多边形之间有线相交,我们只需要判断多边形1的每一条线段是否和多边形2的每一条线段是否相交了。

判断线段相交的函数

// p1-p2 为线段1 p3-p4为线段2
function crossProduct(p1, p2, p3, p4) {
    const x0 = p2.x - p1.x;
    const y0 = p2.y - p1.y;
    const x1 = p4.x - p3.x;
    const y1 = p4.y - p3.y;
    return x0 * y1 - y0 * x1;
}
// 判断平行
function isParallel(p1, p2, p3, p4) {
    // 正常结果 === 0 即为平行 但是实际计算中往往因为计算精确度导致一些误差
    return Math.abs(crossProduct(p1, p2, p3, p4)) <= 0.000001;
}

function lineIntersect(p1, p2, p3, p4) {
    // 先判断是否平行
    if(isParallel(p1, p2, p3, p4)) {
        return false;
    }
    // 设定三个向量 vec1 = p3->p1, vec2 = p3->p2, vec3 = p3->p4
    // 如果vec1和vec2的向量积 与 vec2和vec3的向量积,同号则说明 p1,p2 在 p3-p4的一侧,异号则说明p1,p2在 p3-p4的两侧
    // 设定三个向量 vec4 = p1->p3, vec5 = p1->p4, vec6 = p1->p2
    // 如果vec4和vec5的向量积 与 vec5和vec6的向量积,同号则说明 p3,p4 在 p1-p2的一侧,异号则说明p3,p4在 p1-p2的两侧
    // p1,p2 在p3-p4的两侧且 p3,p4在p1-p2的两侧 即可以确定两条线段相交
    if(crossProduct(p3, p1, p3, p2) * crossProduct(p3, p2, p3, p4) <= 0) {
        return false;
    }
    if(crossProduct(p1, p3, p1, p4) * crossProduct(p1, p4, p1, p2) <= 0) {
        return false;
    }
    return true;
}

复制代码

这里面涉及到向量、叉积的几何意义包括证明等,需要一点数学基础(主要怕把您劝退),网络上也有很多好的文章进行描述,我不多赘述。
之后对图形上面的每一条线进行遍历即可了

// polygon1,polygon2 假设为记录多边形每一个点的数组
for(let i = 0; i < polygon1.length; i ++) {
    const p1 = polygon1[i];
    const p2 = polygon1[(i + 1) % polygon1.length];
    for(let j = 0; j < polygon2.length; j ++) {
        const p3 = polygon2[j];
        const p4 = polygon2[(j + 1) % polygon2.length];
        if(lineIntersect(p1, p2, p3, p4)) {
            return true;
        }
    }
    return false;
}

复制代码

这种方式适用于任意的多边形之间的判断包括三角形、旋转的矩形等等。 当然这属于比较粗暴的通过线之间的碰撞判断图形的碰撞,但是这个是多边形判断碰撞的最核心点,一般在实际应用时可以根据需求特点增加一些前置的判断逻辑,尽量争取在执行到这一步之前就判断出是否发生了碰撞。

完全不规则图像

还有一些场景的图像使用完全不规则的图像,这种图形的检测一般使用像素级的检测,简单说就是判断两个图像是否有非透明的像素点有重合如果有则说明两个图形有碰撞,比如判断下面两只小兔子是否碰撞

先获取两个图像的矩形,计算两个矩形重叠的部分(也是一个矩形)

// 默认坐标系在左上角,x轴向右延伸,y轴向下延伸,图形的锚点在自身的左上角

// 代表左边兔子的矩形信息
let imageBounds1 = {
  x: 0,
  y: 0,
  width: 26,
  height: 37
};

// 代表右边兔子的矩形信息
let imageBounds2 = {
  x: 10,
  y: 20,
  width: 26,
  height: 37
}


// 左兔子的右边
let bounds1Right = imageBounds1.x + imageBounds1.width; 
// 右兔子的右边
let bounds2Right = imageBounds2.x + imageBounds2.width;
// 左兔子的下边
let bounds1Bottom = imageBounds1.y + imageBounds1.height;
// 右兔子的下边
let bounds2Bottom = imageBounds2.y + imageBounds2.height;

// 构建重叠区域形成的矩形
let intersectionBounds = {x: 0, y: 0, width: 0, height: 0}; 
// 重叠区域的x
intersectionBounds.x = Math.max(imageBounds1.x , imageBounds2.x);
// 重叠区域的y
intersectionBounds.y = Math.max(imageBounds1.y , imageBounds2.y); 
// 重叠区域的宽度
intersectionBounds.width = Math.min(bounds1Right , bounds2Right) - intersectionBounds.x; 
// 重叠区域的高度
intersectionBounds.height = Math.min(bounds1Bottom , bounds2Bottom) - intersectionBounds.y;

复制代码

之后只要判断重叠矩形中的像素点,有一个像素点有颜色即为碰撞

let imageData = ctx.getImageData(intersectionBounds.x, intersectionBounds.y, intersectionBounds.width, intersectionBounds.height);

let isIntersection = false;

for (let i = 0; i < imageData.data.length / 4; i ++) {
  if(imageData.data[(i + 1) * 4 - 1] > 0) {
    console.log(imageData.data);
    isIntersection = true;
    break;
  } 
}


复制代码

总结

本文重点描述了复杂多边形和不规则图形碰撞检测的方法,可以根据实际的项目需求选择合适的检测方法,当然也有一些配合碰撞检测的优化方案减少判断的次数,比如四分法等。