目录
2、分离轴定理(separating axis theorem, SAT)
其次,圆心在多边形外,计算每条边距离圆心的距离,方法同胶囊体
一、引言
图形相交测试是碰撞检测的基础逻辑
二、基础概念
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)。
- 设 A 点旋转前的角度为 δ,则旋转(逆时针)到 C 点后的角度为(δ+β)
- 由于 |AB| 与 |CB| 相等(即长度),且
- |AB| = y/sin(δ) = x / cos(δ)
- |CB| = d/sin(δ + β) = c / cos(δ + β)
- 半径 r = x / cos(δ) = y / sin(δ) = d / sin(δ + β) = c / cos(δ + β)
- 由以下三角函数两角和差公式:
- sin(δ + β) = sin(δ)cos(β) + cos(δ)sin(β)
- cos(δ + β) = cos(δ)cos(β) - sin(δ)sin(β)
- 可得出旋转后的坐标:
- 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