简易碰撞检测原理--图形相交测试

目录

一、引言

二、基础概念

1、常用2D几何图形

2、凸集(convex set)

2、分离轴定理(separating axis theorem, SAT)

三、多边形与多边形相交测试

1、AABB与AABB 

2、OBB与OBB

3、凸多边形与凸多边形

四、圆形与其他凸集相交测试

1、圆形与圆形

2、圆形与AABB

3、圆形与OBB

4、圆形与胶囊体

5、圆形与扇形

6、圆形与凸多边形

      首先判断圆心是否在多边形内:

      其次,圆心在多边形外,计算每条边距离圆心的距离,方法同胶囊体

四、参考资料


一、引言

图形相交测试是碰撞检测的基础逻辑

二、基础概念

1、常用2D几何图形

圆形

矩形

      AABB(axis-aligned bounding box, 轴对称包围盒):各面与主轴对齐的、不能任意旋转

      OBB(oriented bounding box, 定向包围盒):不与轴对齐,可以任意旋转

胶囊体

扇形

凸多边形

2、凸集(convex set)

凸集(convex set)是点的集合,集合中任意两点间的线段都在该集合中。

2、分离轴定理(separating axis theorem, SAT)

两个不相交的凸集必然存在一个分离轴,使两个凸集在该轴上的投影是分离的,见下图。

那么,要判断两个形状是否相交,我们只需要找出可能的分离轴,然后判断那些轴是否能把两个形状分离。若所有可能的分离轴都不能分离它们,就可以确认它们是相交的。

三、多边形与多边形相交测试

在程序中,遍历所有角度是不现实的。那如何确定 投影轴 呢?其实投影轴的数量与多边形的边数相等即可。

如果图形A有x条边,图形B有y条边,那么需要检测的投影轴最多有(x+y)条

 

1、AABB与AABB 

int c2AABBtoAABB( c2AABB A, c2AABB B )
{
	int d0 = B.max.x < A.min.x; -- A在B右方
	int d1 = A.max.x < B.min.x; -- A在B左方
	int d2 = B.max.y < A.min.y; -- A在B上方
	int d3 = A.max.y < B.min.y; -- A在B下方
	return !(d0 | d1 | d2 | d3);
}

2、OBB与OBB

推断公式:BE>(BC+DE)=(1/2AC+1/2DF)=1/2(AC+DF)      ---》 BE>1/2(AC+DF) 可推出两个矩形是分离了;

    var OBB = function (centerPoint, width, height, rotation) {

        this.centerPoint = centerPoint;
        this.extents = [width / 2, height / 2];
        this.axes = [new Vector2(Math.cos(rotation), Math.sin(rotation)), new Vector2(-1 * Math.sin(rotation), Math.cos(rotation))];

        this._width = width;
        this._height = height;
        this._rotation = rotation;
    }

    OBB.prototype = {
        getProjectionRadius: function (axis) {
        return this.extents[0] * Math.abs(axis.dot(this.axes[0])) + this.extents[1] * Math.abs(axis.dot(this.axes[1]));
        }
    }

    var Vector2 = function (x, y) {
        this.x = x || 0;
        this.y = y || 0;
    };

    Vector2.prototype = {
        sub: function (v) {
            return new Vector2(this.x - v.x, this.y - v.y)
        },
        dot: function (v) {
            return this.x * v.x + this.y * v.y;
        }
    };

    var CollisionDetector = {

        detectorOBBvsOBB: function (OBB1, OBB2) {
            var nv = OBB1.centerPoint.sub(OBB2.centerPoint);
            var axisA1 = OBB1.axes[0];
            if (OBB1.getProjectionRadius(axisA1) + OBB2.getProjectionRadius(axisA1) <= Math.abs(nv.dot(axisA1))) return false;
            var axisA2 = OBB1.axes[1];
            if (OBB1.getProjectionRadius(axisA2) + OBB2.getProjectionRadius(axisA2) <= Math.abs(nv.dot(axisA2))) return false;
            var axisB1 = OBB2.axes[0];
            if (OBB1.getProjectionRadius(axisB1) + OBB2.getProjectionRadius(axisB1) <= Math.abs(nv.dot(axisB1))) return false;
            var axisB2 = OBB2.axes[1];
            if (OBB1.getProjectionRadius(axisB2) + OBB2.getProjectionRadius(axisB2) <= Math.abs(nv.dot(axisB2))) return false;
            return true;

        }
    }

3、凸多边形与凸多边形

步骤一:取多边形a的一边,得出该边的法线(即分离轴)。

步骤二:算出两个多边形在该法线上的投影

步骤三:计算两个投影是否重叠,如果不重叠,说明不相交。

步骤四:遍历多边形a所有的边和多边形b的所有边,重复步骤一到三,如果投影都有重叠,则说明两多边形相交。

function vec(x, y)
    return {x, y}
end
 
v = vec -- shortcut
 
-- 点积
function dot(v1, v2)
    return v1[1]*v2[1] + v1[2]*v2[2]
end
 
-- 归一化单位向量
function normalize(v)
    local mag = math.sqrt(v[1]^2 + v[2]^2)
    return vec(v[1]/mag, v[2]/mag)
end
 
-- 求法线
function perp(v)
    return {v[2],-v[1]}
end
 
-- 创建线段
function segment(a, b)
    local obj = {a=a, b=b, dir={b[1] - a[1], b[2] - a[2]}}
    obj[1] = obj.dir[1]; obj[2] = obj.dir[2]
    return obj
end

-- 创建多边形
function polygon(vertices)
    local obj = {}
    obj.vertices = vertices
    obj.edges = {}
    for i=1,#vertices do
        table.insert(obj.edges, segment(vertices[i], vertices[1+i%(#vertices)]))
    end
    return obj
end

-- We keep a running range (min and max) values of the projection, and then use that as our shadow
 
-- 求投影范围
function project(a, axis)
    axis = normalize(axis)
    local min = dot(a.vertices[1],axis)
    local max = min
    for i,v in ipairs(a.vertices) do
        local proj =  dot(v, axis) -- projection
        if proj < min then min = proj end
        if proj > max then max = proj end
    end
 
    return {min, max}
end
 
function contains(n, range)
    local a, b = range[1], range[2]
    if b < a then a = b; b = range[1] end
    return n >= a and n <= b
end
 
-- 计算是否重叠
function overlap(a_, b_)
    if contains(a_[1], b_) then return true
    elseif contains(a_[2], b_) then return true
    elseif contains(b_[1], a_) then return true
    elseif contains(b_[2], a_) then return true
    end
    return false
end

-- 分离轴算法
function sat(a, b)
    for i,v in ipairs(a.edges) do
        -- 法线为分离轴
        local axis = perp(v)
        -- 分布求投影
        local a_, b_ = project(a, axis), project(b, axis)
        -- 计算投影是否重叠
        if not overlap(a_, b_) then return false end
    end
    for i,v in ipairs(b.edges) do
        local axis = perp(v)
        local a_, b_ = project(a, axis), project(b, axis)
        if not overlap(a_, b_) then return false end
    end
 
    return true
end


四、圆形与其他凸集相交测试

存在 圆形A 与 凸集B

凸集B中距离圆形A圆心最短距离为L, 如果L小于等于圆形半径,那么相交。

而寻找最短距离通常需要找到 凸集B中距离圆形A圆心最近的点。

1、圆形与圆形

int c2CircletoCircle( c2Circle A, c2Circle B )
{
	c2v c = c2Sub( B.p, A.p );
	float d2 = c2Dot( c, c );
	float r2 = A.r + B.r;
	r2 = r2 * r2;
	return d2 < r2;
}

2、圆形与AABB

int c2CircletoAABB( c2Circle A, c2AABB B )
{
	c2v L = c2Clampv( A.p, B.min, B.max );
	c2v ab = c2Sub( A.p, L );
	float d2 = c2Dot( ab, ab );
	float r2 = A.r * A.r;
	return d2 < r2;
}

3、圆形与OBB

先给出可直接套用的公式,从而得出旋转后的圆心坐标:

x’ = cos(β) * (cx – centerX) – sin(β) * (cy – centerY) + centerX
y’ = sin(β) * (cx – centerX) + cos(β) * (cy – centerY) + centerY

下面给出该公式的推导过程:

根据下图,计算某个点绕另外一个点旋转一定角度后的坐标。我们设 A(x,y) 绕 B(a,b) 旋转 β 度后的位置为 C(c,d)。

  1. 设 A 点旋转前的角度为 δ,则旋转(逆时针)到 C 点后的角度为(δ+β)
  2. 由于 |AB| 与 |CB| 相等(即长度),且
    1. |AB| = y/sin(δ) = x / cos(δ)
    2. |CB| = d/sin(δ + β) = c / cos(δ + β)
  3. 半径 r = x / cos(δ) = y / sin(δ) = d / sin(δ + β) = c / cos(δ + β)
  4. 由以下三角函数两角和差公式:
    • sin(δ + β) = sin(δ)cos(β) + cos(δ)sin(β)
    • cos(δ + β) = cos(δ)cos(β) - sin(δ)sin(β)
  5. 可得出旋转后的坐标:
    • c = r * cos(δ + β) = r * cos(δ)cos(β) - r * sin(δ)sin(β) = x * cos(β) - y * sin(β)
    • d = r * sin(δ + β) = r * sin(δ)cos(β) + r * cos(δ)sin(β) = y * cos(β) + x * sin(β)

由上述公式推导后可得:旋转后的坐标 (c,d) 只与旋转前的坐标 (x,y) 及旋转的角度 β 有关。

当然,(c,d) 是旋转一定角度后『相对于旋转点(轴)的坐标』。因此,前面提到的『可直接套用的公式』中加上了矩形的中心点的坐标值。

从图中也可以得出以下结论:A 点旋转后的 C 点总是在圆周(半径为 |AB|)上运动,利用这点可让物体绕旋转点(轴)做圆周运动。

得到旋转后的圆心坐标值后,即可使用『圆形与矩形(无旋转)』方法进行碰撞检测了。

4、圆形与胶囊体

因此,这个相交问题可以转化为点与线段的求最短距离问题(下图中的 ),然后再比较该距离是否少于半径之和。

 

    1、求出点 x 在线段 u 所在直线上的投影点 P;

    2、将投影点 P 限制在线段的范围内(如右图中投影点不在线段内,则限定到线段内);

    3、x 与 P 的距离即为所求;

如同圆盘与圆盘的相交测试,我们可以只比较距离的平方,从而除去开方运算。

    /// <summary>
    /// 线段与点的最短距离。
    /// </summary>
    /// <param name="x0">线段起点</param>
    /// <param name="u">线段向量</param>
    /// <param name="x">求解点</param>
    /// <returns></returns>
    public static float SqrDistanceBetweenSegmentAndPoint(Vector2 x0, Vector2 u, Vector2 x)
    {
        float t = Vector2.Dot(x - x0, u) / u.sqrMagnitude;
        return (x - (x0 + Mathf.Clamp01(t) * u)).sqrMagnitude;
    }
// see: http://www.randygaul.net/2014/07/23/distance-point-to-line-segment/
int c2CircletoCapsule( c2Circle A, c2Capsule B )
{
	c2v n = c2Sub( B.b, B.a );
	c2v ap = c2Sub( A.p, B.a );
	float da = c2Dot( ap, n );
	float d2;

	if ( da < 0 ) d2 = c2Dot( ap, ap );
	else
	{
		float db = c2Dot( c2Sub( A.p, B.b ), n );
		if ( db < 0 )
		{
			c2v e = c2Sub( ap, c2Mulvs( n, (da / c2Dot( n, n )) ) );
			d2 = c2Dot( e, e );
		}
		else
		{
			c2v bp = c2Sub( A.p, B.b );
			d2 = c2Dot( bp, bp );
		}
	}

	float r = A.r + B.r;
	return d2 < r * r;
}

5、圆形与扇形

距离圆心最近点可能情况

情况一:在扇形内部

情况二:在圆弧上

情况三:在两天直线边上

// 扇形与圆盘相交测试
// a 扇形圆心
// u 扇形方向(单位矢量)
// theta 扇形扫掠半角 
// l 扇形边长
// c 圆盘圆心
// r 圆盘半径
static bool IsSectorDiskIntersect(
    Vector2 a, Vector2 u, float theta, float l,
    Vector2 c, float r)
{
    // 1. 如果扇形圆心和圆盘圆心的方向能分离,两形状不相交
    Vector2 d = c - a;
    float rsum = l + r;
    if (d.sqrMagnitude > rsum * rsum)
        return false;

    // 2. 计算出扇形局部空间的 p
    float px = Vector2.Dot(d, u);
    float py = Mathf.Abs(Vector2.Dot(d, new Vector2(-u.y, u.x)));

    // 3. 如果 p_x > ||p|| cos theta,两形状相交
    if (px > d.magnitude * Mathf.Cos(theta))
        return true;
    
    // 4. 求左边线段与圆盘是否相交
    Vector2 q = l * new Vector2(Mathf.Cos(theta), Mathf.Sin(theta));
    Vector2 p = new Vector2(px, py);
    return SegmentPointSqrDistance(Vector2.zero, q, p) <= r * r;
}

6、圆形与凸多边形

      首先判断圆心是否在多边形内:

          方法一:夹角和判别法(与每个相邻顶点的夹角和为360度)

          方法二:面积和判别法(判断目标点与多边形的每条边组成的三角形面积和是否等于该多边形,相等则在多边形内部)

    海伦公式
    //圆心和每条边的两个顶点构成三角形,a,b,c分别为三条边的长度
    double p = (a + b + c) / 2;// 半周长
    double s = Math.sqrt(p * (p - a) * (p - b) * (p - c));// 海伦公式求面积

          方法三:叉乘判别法(略)

   方法四:引射线法(略)

      其次,圆心在多边形外,计算每条边距离圆心的距离,方法同胶囊体

        /// <summary>
        /// 判断多边形与圆形相交
        /// </summary>
        /// <param name="polygonArea"></param>
        /// <param name="target"></param>
        /// <returns></returns>
        public static bool PolygonS(PolygonArea polygonArea, CircleArea target)
        {
            if (polygonArea.vertexes.Length < 3)
            {
                Debug.Log("多边形边数小于3.");
                return false;
            }
            #region 定义临时变量
            //圆心
            Vector2 circleCenter = target.o;
            //半径的平方
            float sqrR = target.r * target.r;
            //多边形顶点
            Vector2[] polygonVertexes = polygonArea.vertexes;
            //圆心指向顶点的向量数组
            Vector2[] directionBetweenCenterAndVertexes = new Vector2[polygonArea.vertexes.Length];
            //多边形的边
            Vector2[] polygonEdges = new Vector2[polygonArea.vertexes.Length];
            for (int i = 0; i < polygonArea.vertexes.Length; i++)
            {
                directionBetweenCenterAndVertexes[i] = polygonVertexes[i] - circleCenter;
                polygonEdges[i] = polygonVertexes[i] - polygonVertexes[(i + 1)% polygonArea.vertexes.Length];
            }
            #endregion
 
            #region 以下为圆心处于多边形内的判断。
            //总夹角
            float totalAngle = Vector2.SignedAngle(directionBetweenCenterAndVertexes[polygonVertexes.Length - 1], directionBetweenCenterAndVertexes[0]);
            for (int i = 0; i < polygonVertexes.Length - 1; i++)
                totalAngle += Vector2.SignedAngle(directionBetweenCenterAndVertexes[i], directionBetweenCenterAndVertexes[i + 1]);
            if (Mathf.Abs(Mathf.Abs(totalAngle) - 360f) < 0.1f)
                return true;
            #endregion
            #region 以下为多边形的边与圆形相交的判断。
            for (int i = 0; i < polygonEdges.Length; i++)
                if (SegmentPointSqrDistance(polygonVertexes[i], polygonEdges[i], circleCenter) < sqrR)
                    return true;
            #endregion
            return false;
        }

四、参考资料

1、扇形与圆盘相交测试浅析 https://zhuanlan.zhihu.com/p/23903445

2、多边形碰撞检测 https://blog.csdn.net/yuliying/article/details/56849489

3、tinyc2 一个2D图形相交检测算法库 https://github.com/sro5h/collision2d/blob/master/tinyc2.h

4、OBB碰撞检测 https://www.cnblogs.com/iamzhanglei/archive/2012/06/07/2539751.html

  • 3
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
OBB碰撞检测算法的原理是基于包围盒的相交检测,通过判断两个旋转的包围盒是否相交来确定碰撞情况。 以下是OBB碰撞检测算法的基本原理: 1. 包围盒定义:每个物体(例如车辆)都被定义为一个旋转的包围盒,通常是一个矩形或长方体。包围盒由中心点、尺寸(长度、宽度和高度)和旋转角度(通常表示为欧拉角或四元数)来确定。 2. 包围盒的表示:包围盒可以使用矩阵变换来表示。通过将包围盒的中心和尺寸与旋转矩阵相乘,可以将包围盒从局部坐标系转换到世界坐标系。 3. 碰撞检测:通过检测两个包围盒是否相交来判断碰撞情况。两个包围盒相交的条件是:它们在三个坐标轴上的投影相交。即,两个包围盒在X轴、Y轴和Z轴上的投影都有重叠部分。 4. 投影检测:为了检测投影是否相交,可以使用包围盒的顶点或边在投影轴上的最小和最大值。通过计算两个包围盒在每个轴上的投影,并比较它们的最小和最大值,可以确定是否相交。 使用OBB碰撞检测算法时,需要注意以下几点: - 确定包围盒的正确性和准确性,以确保它们能够正确地表示物体的形状和方向。 - 选择合适的碰撞检测策略,可以采用分离轴定理(Separating Axis Theorem)或其他相应的算法来进行检测。 - 在实际应用中,可能需要考虑包围盒的更新和优化策略,以提高碰撞检测的效率和准确性。 总之,OBB碰撞检测算法通过比较旋转的包围盒在各个轴上的投影,来判断物体之间是否发生碰撞。这种算法在自动驾驶等领域中被广泛应用于车辆碰撞检测和避免碰撞的实现。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值