js中常用的数学方法-用于测试形状与形状是否相交

4,688 阅读8分钟

以下代码均为 CocosCreator[www.cocos.com/docs/](游戏引擎)源码,一般都是效率极高且语意清晰的工具函数。我只是个代码的搬运工,并且写了一点点注释。

测试线段与线段是否相交

/**
 * !#en Test line and line
 * !#zh 测试线段与线段是否相交
 * @method lineLine
 * @param {Vec2} a1 - The start point of the first line
 * @param {Vec2} a2 - The end point of the first line
 * @param {Vec2} b1 - The start point of the second line
 * @param {Vec2} b2 - The end point of the second line
 * @return {boolean}
 */
function lineLine ( a1, a2, b1, b2 ) {
    // b1->b2向量 与 a1->b1向量的向量积
    var ua_t = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x);
    // a1->a2向量 与 a1->b1向量的向量积
    var ub_t = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x);
    // a1->a2向量 与 b1->b2向量的向量积
    var u_b  = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y);
    // u_b == 0时,角度为0或者180 平行或者共线不属于相交
    if ( u_b !== 0 ) {
        var ua = ua_t / u_b;
        var ub = ub_t / u_b;

        if ( 0 <= ua && ua <= 1 && 0 <= ub && ub <= 1 ) {
            return true;
        }
    }

    return false;
}

注:运用了向量的叉乘做的相交判断,向量积高中没学(特地问了高中老师),想明白原理的自行百度,关于向量积的篇幅很长就不放在这里讨论了。记住一个结论:向量积表示垂直于两向量的法向量,满足右手定则,向量积不满足交换率,满足 axb = -bxa;所以当都以a向量为第一个乘数时,如果两个向量的法向量符号相反时则有两个点在a直线的两边。

测试线段与矩形是否相交

/**
 * !#en Test line and rect
 * !#zh 测试线段与矩形是否相交
 * @method lineRect
 * @param {Vec2} a1 - The start point of the line
 * @param {Vec2} a2 - The end point of the line
 * @param {Rect} b - The rect
 * @return {boolean}
 */
function lineRect ( a1, a2, b ) {
    var r0 = new cc.Vec2( b.x, b.y );
    var r1 = new cc.Vec2( b.x, b.yMax );
    var r2 = new cc.Vec2( b.xMax, b.yMax );
    var r3 = new cc.Vec2( b.xMax, b.y );

    if ( lineLine( a1, a2, r0, r1 ) )
        return true;
    if ( lineLine( a1, a2, r1, r2 ) )
        return true;
    if ( lineLine( a1, a2, r2, r3 ) )
        return true;
    if ( lineLine( a1, a2, r3, r0 ) )
        return true;
    return false;
}

这里矩形是左上顶点加宽高的表示形式,具体思路就是判断线段与组成矩形的每条边是否相交

测试线段与多边形是否相交

/**
 * !#en Test line and polygon
 * !#zh 测试线段与多边形是否相交
 * @method linePolygon
 * @param {Vec2} a1 - The start point of the line
 * @param {Vec2} a2 - The end point of the line
 * @param {Vec2[]} b - The polygon, a set of points
 * @return {boolean}
 */
function linePolygon ( a1, a2, b ) {
    var length = b.length;

    for ( var i = 0; i < length; ++i ) {
        var b1 = b[i];
        var b2 = b[(i+1)%length];

        if ( lineLine( a1, a2, b1, b2 ) )
            return true;
    }

    return false;
}

这里多边形是按顺序的点的表示形式,具体思路就是判断线段与组成多边形形的每条边是否相交。其中var b2 = b[(i+1)%length];让我眼前一亮。哈哈,原来首尾循环可以这么写前一项与后一项。

测试矩形与矩形是否相交

/**
 * !#en Test rect and rect
 * !#zh 测试矩形与矩形是否相交
 * @method rectRect
 * @param {Rect} a - The first rect
 * @param {Rect} b - The second rect
 * @return {boolean}
 */
function rectRect ( a, b ) {
    // jshint camelcase:false

    var a_min_x = a.x;
    var a_min_y = a.y;
    var a_max_x = a.x + a.width;
    var a_max_y = a.y + a.height;

    var b_min_x = b.x;
    var b_min_y = b.y;
    var b_max_x = b.x + b.width;
    var b_max_y = b.y + b.height;

    return a_min_x <= b_max_x &&
           a_max_x >= b_min_x &&
           a_min_y <= b_max_y &&
           a_max_y >= b_min_y
           ;
}

矩形与矩形相交没用到lineline的算法,思路是: 如果a矩形的最小x坐标小于等于b矩形的最大x坐标,且a的最大x坐标大于等于b的最小x坐标-能判断出b矩形的左顶点在a矩形的左右顶点之间。 同样的方法能判断出y。 这样也不会有a矩形包含b矩形的问题,顶多两个矩形位置和宽高是一模一样的。

测试矩形与多边形是否相交

/**
 * !#en Test rect and polygon
 * !#zh 测试矩形与多边形是否相交
 * @method rectPolygon
 * @param {Rect} a - The rect
 * @param {Vec2[]} b - The polygon, a set of points
 * @return {boolean}
 */
function rectPolygon ( a, b ) {
    var i, l;
    var r0 = new cc.Vec2( a.x, a.y );
    var r1 = new cc.Vec2( a.x, a.yMax );
    var r2 = new cc.Vec2( a.xMax, a.yMax );
    var r3 = new cc.Vec2( a.xMax, a.y );

    // 矩形的每条边与多边形是否相交
    if ( linePolygon( r0, r1, b ) )
        return true;

    if ( linePolygon( r1, r2, b ) )
        return true;

    if ( linePolygon( r2, r3, b ) )
        return true;

    if ( linePolygon( r3, r0, b ) )
        return true;
        
    // 走到这可以检测出两个图形无交点
    // 检测是否矩形包含多边形,如果多边形上存在一个点在矩形内,则相交
    for ( i = 0, l = b.length; i < l; ++i ) {
        if ( pointInPolygon(b[i], a) )
            return true;
    }

    // 检测是否多边形包含矩形,如果矩形上存在一个点在多边形内,则相交
    if ( pointInPolygon(r0, b) )
        return true;

    if ( pointInPolygon(r1, b) )
        return true;

    if ( pointInPolygon(r2, b) )
        return true;

    if ( pointInPolygon(r3, b) )
        return true;

    return false;
}

思路:矩形与多边形相交,首先需要判断矩形每一条边是否和多边形有交点(找到为止),如果没有,则可能矩形包含多边形,或者多边形包含矩形。判断方式是:判断是否点在多边形或者矩形内,如果没有交点且没有点的包含关系,那么则不相交。

为什么要遍历和循环,而不是随便找一个点就行?恰好一个图形有一个或者多个顶点在另一个图形上

测试多边形与多边形是否相交

/**
 * !#en Test polygon and polygon
 * !#zh 测试多边形与多边形是否相交
 * @method polygonPolygon
 * @param {Vec2[]} a - The first polygon, a set of points
 * @param {Vec2[]} b - The second polygon, a set of points
 * @return {boolean}
 */
function polygonPolygon ( a, b ) {
    var i, l;

    // a的每条边与b的每条边做相交检测
    for ( i = 0, l = a.length; i < l; ++i ) {
        var a1 = a[i];
        var a2 = a[(i+1)%l];

        if ( linePolygon( a1, a2, b ) )
            return true;
    }

    // 判断两个多边形的包含关系
    for ( i = 0, l = b.length; i < l; ++i ) {
        if ( pointInPolygon(b[i], a) )
            return true;
    }

    // 判断两个多边形的包含关系
    for ( i = 0, l = a.length; i < l; ++i ) {
        if ( pointInPolygon( a[i], b ) )
            return true;
    }

    return false;
}

思想和矩形是一样的,只不过矩形有其特殊之处而已(一般游戏中矩形爱用左上顶点+宽高表示,多边形用顺序点)。

测试圆形与圆形是否相交

/**
 * !#en Test circle and circle
 * !#zh 测试圆形与圆形是否相交
 * @method circleCircle
 * @param {Object} a - Object contains position and radius
 * @param {Object} b - Object contains position and radius
 * @return {boolean}
 * @typescript circleCircle(a: {position: Vec2, radius: number}, b: {position: Vec2, radius: number}): boolean
 */
function circleCircle (a, b) {
    var distance = a.position.sub(b.position).mag(); // 这里用到了内部方法,写在下面了,就是在求a与b之间的距离
    // let sub = function (vector, out) {
    // 向量减法,并返回新结果。因为引擎是3d的所以是Vec3,大家可以直接用Vec2
    // out = out || new Vec3();
    // out.x = this.x - vector.x;
    // out.y = this.y - vector.y;
    // out.z = this.z - vector.z;
    // return out;
    //};
    // mag() {
    // 返回一个距离
    //  return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
    //}
     // function Vec2(x, y) {
    //    this.x = x;
    //    this.y = y;
    // }

    return distance < (a.radius + b.radius);
}

测试多边形与圆形是否相交

/**
 * !#en Test polygon and circle
 * !#zh 多边形与圆形是否相交
 * @method polygonCircle
 * @param {Vec2[]} polygon - The Polygon, a set of points
 * @param {Object} circle - Object contains position and radius
 * @return {boolean}
 * @typescript polygonCircle(polygon: Vec2[], circle: {position: Vec2, radius: number}): boolean
 */
function polygonCircle (polygon, circle) {
    //先判断圆心有没有在多边形内,如果在,一定相交
    var position = circle.position;
    if (pointInPolygon(position, polygon)) {
        return true;
    }
    // 否则遍历多边形的每一条边,如果圆形到边的距离小于圆的半径,则相交
    // 为什么不用点到圆心的距离?我也不清楚。。。望大佬解答
    for (var i = 0, l = polygon.length; i < l; i++) {
        var start = i === 0 ? polygon[polygon.length - 1] : polygon[i- 1];
        var end = polygon[i];

        if (pointLineDistance(position, start, end, true) < circle.radius) {
            return true;
        }
    }

    return false;
}

测试一个点是否在一个多边形中


/**
 * !#en Test whether the point is in the polygon
 * !#zh 测试一个点是否在一个多边形中
 * @method pointInPolygon
 * @param {Vec2} point - The point
 * @param {Vec2[]} polygon - The polygon, a set of points
 * @return {boolean}
 */
function pointInPolygon (point, polygon) {
    //* 射线法判断点是否在多边形内
    //* 点射线(向右水平)与多边形相交点的个数为奇数则认为该点在多边形内
    //* 点射线(向右水平)与多边形相交点的个数为偶数则认为该点不在多边形内
    var inside = false;
    var x = point.x;
    var y = point.y;

    // use some raycasting to test hits
    // https://github.com/substack/point-in-polygon/blob/master/index.js
    var length = polygon.length;

    for ( var i = 0, j = length-1; i < length; j = i++ ) {
        var xi = polygon[i].x, yi = polygon[i].y,
            xj = polygon[j].x, yj = polygon[j].y,
            intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
            // (yi > y) !== (yj > y)表示此条边的两个端点的y值一个大于这个点的y一个小于这个点的y
            //  (x < (xj - xi) * (y - yi) / (yj - yi) + xi) 这个看起来像是求投影呢,还没搞明白
        if ( intersect ) inside = !inside;
    }

    return inside;
}

计算点到直线的距离。如果这是一条线段并且垂足不在线段内,则会计算点到线段端点的距离。

/**
 * !#en Calculate the distance of point to line.
 * !#zh 计算点到直线的距离。如果这是一条线段并且垂足不在线段内,则会计算点到线段端点的距离。
 * @method pointLineDistance
 * @param {Vec2} point - The point
 * @param {Vec2} start - The start point of line
 * @param {Vec2} end - The end point of line
 * @param {boolean} isSegment - whether this line is a segment
 * @return {number}
 */
function pointLineDistance(point, start, end, isSegment) {
    var dx = end.x - start.x;
    var dy = end.y - start.y;
    var d = dx*dx + dy*dy;
    var t = ((point.x - start.x) * dx + (point.y - start.y) * dy) / d;
    var p;

    if (!isSegment) {
        p = cc.v2(start.x + t * dx, start.y + t * dy);
    }
    else {
        if (d) {
            if (t < 0) p = start;
            else if (t > 1) p = end;
            else p = cc.v2(start.x + t * dx, start.y + t * dy);
        }
        else {
            p = start;
        }
    }
        
    dx = point.x - p.x;
    dy = point.y - p.y;
    return Math.sqrt(dx*dx + dy*dy);
}

未彻底明白的点:

pointInPolygon中(x < (xj - xi) * (y - yi) / (yj - yi) + xi);

lineLine中

        var ua = ua_t / u_b;
        var ub = ub_t / u_b;

        if ( 0 <= ua && ua <= 1 && 0 <= ub && ub <= 1 ) {
            return true;
        }

polygonCircle中 为什么不用点到圆心的距离,而用线段到圆心的距离

后面会继续深究一下未明白的点,但也望大佬可以指点一下。