关于简单的碰撞检测
【前言】
这篇博文旨在给自己做个记录和备忘,同时希望也能给有这方面简易碰撞模型需求的同学一点点参考价值。
【关于像素级别检测】
前一阵有同学问我说能否做到像素级别的碰撞检测,做过类似碰撞检测的同学应该清楚,按照我们最常规的想法,假如要检测一个运动的物体和一条线之间是否有碰撞,最简单的判断条件,就是看当前帧,这个物体的位置,是否超过的我们的界定范围...
但这样简单的判定确实是有问题的,我们举一个实际一点的例子。
假如 小球 从 a 点向 c 点的方向 落下。外面的黑框为我们所示的边界,那么,我们想要小球在碰到边界的时候反弹... 那么我们该怎么做呢?
可能有同学会迫不及待说了,这还不简单,一个 条件语句搞定: 假设 小球的 纵向 速度 为 vy,那么在循环帧里判断 小球当前的 位置, 当发现 位置 低于 边界 的时候 vy = -vy 就可以了。
没错,这的确是最简单的判别 方法。 但是也是漏洞百出的 办法。 在复杂度不高的情况下 勉强可以胜任需求。
如果我们把要求升级一点。小球每反弹一次,它的纵向 速度值 降为原来的 0.5 ,那么会出现什么情况下。我们假设 每个循环帧的间隔为dt,我下面画出 小球 在临界碰撞 前后3帧 位置的示意图, 大家看看就会发现问题了。
假设a,b,c 三点为小球 1,2,3帧的位置,那么我们按上面说的 检测方法, 小球在b点的时候检测 发现 小球位置 已经超出 界定范围了,那么 vy 反向,同时 ,vy的数值 减半,那么可想而知 ,再下一帧的位置 就会大约是在 c点的位置。
那么按照上面说的检测方法,问题就来了,在 c 点,检测发现,小球位置依然在 界定范围外, 又被判定为 “碰撞” ,速度反向, 那么可想而知,以此类推, 这个小球 就永远也弹不起来了... 坑爹。
好吧,为了绕过这种问题,肯定有同学都想到了,我们的碰撞检测 再 加上 对于速度方向的判断 不就可以了吗?
没错,对于这个模型,再加上一个速度方向的判断,也可以勉强的解决问题。 不过话说回来,这样不觉得有些别扭吗? 而且其实反弹的点也不对,正确的应该是在红色交点 o 处反弹的,结果按照上面的思路变成 在 b 点反弹了.... 还是有点坑爹。
那么怎么把 反弹点从b点移到o点呢?
可能又有同学会说了,你这不就回到最开始的问题了吗,要精确的得到 反弹点 o 不就需要所谓的 像素级判断了吗?理论上是不可能的....
确实,如果按照上面的思路是不可能的。但是换个思路,一切就有戏了。
【关于线段相交】
回到正题,上面的例子已经表明 那种 简单的 利用当前位置来判断 碰撞的模型 是不太靠谱的。 那么换个思路,按小球的运行轨迹 与 边界 的相交性 来判断,是否可行呢?
什么意思呢?就是说 我们 把小球 当前帧 和 下一帧 的位置 连接起来, 变成一条线段, 然后跟 需要进行检测的 边界 这条线段 放到一起,碰撞问题 就变成 数学中 简单的 【二维平面中两条线段相交问题】了。
同样,我们以 a, b, c 三点来表示小球 前后 3 帧的位置,中间那条黑色的实线表示边界N, 那么 在第一帧 到第二帧, 也就是 a -> b 的运行过程中,线段ab 和边界N 明显是没有交点的,那么 自然可以认为 是没有碰撞的。
而由b->c 的过程中,可以发现,线段bc 和 N 就有 交点 o 了,只要我们有办法 证明 bc 和N 相交,并且求出 这个交点 o 的位置,那么 就可以证明 小球在 b -> c 的运行过程中,必然和边界N 碰撞。同时 交点 o 即为反弹点。
通过这种方式来判断碰撞关系的话,就不会说因为小球的速度 过大 或者 FPS 过小 而造成 碰撞检测失效的情况了,而且还能精确的得到反弹点。甚至反弹角度。
好吧,接下来,咋们有了数学模型,就是所谓的 关于两条线段 的相交问题。这个问题怎么处理呢。
判断已知的两条线段是否相交的办法,我这里提供两个。
1. 分别求出 两条线段 的 二维 表示公式 如:线段A: y= a1*x + b1; 线段B: y=a2*x+b2; 然后把线段A 的两个端点 分别 带到 线段B 的公式里。 我们知道,把一个点坐标 带到 一条直线 公式中, a2*x + b2 -y ; 如果等于0 ,那么表示点在线上,如果小于0表示点在线的一方, 大于0 表示在另一方。 那么 当 两个端点 带到 公式里面的结果 ,一正一负 就可以表示 这条线段 的两个端点 分别在这条直线的两边。
如:
把 a, b点分别 带到 线段N 所在 的直线公式中, 如果 结果 一正一负 就可以 表示 a, b 分布在N 的两边,
但是光把 a, b 戴到N中 去算 是不够的,因为 还有这种情况,
因为是线段嘛,所以 还要把另一条线段 的 端点 再带到 自己所在直线 的公式 上,按同样的方式 进行计算, 得到 另一条线段 端点 也分布在 相对应的 线段两边 的时候, 才能保证线段 是相交的。
另外一种判断 两条 线段 是否相交的思路,可以用 向量外积来判断。关于外积,如果已经还给数学老师的同学,可以百度或者google一下。简单的理解,其实 外积本身 也就是带方向的向量, 数值上等于 两条向量组成的三角形的外接矩形面积。
我们可以简单的理解为,我们先求 线段ab 的两个端点 相对于 cd 的“外积” 。 比如, 由 a,c,d 三点组成的三角形的外积,得到的结果是一个带方向的数值,我们假定为 S_acd, 同样的方式 得到 b点 和 线段cd 组成的三角形 外积 S_bcd 。
数学上可以证明,如果a, b 两点分布在 cd 的两侧的话,那么 S_acd 和S_bcd 一定是反向的。即 S_acd*S_bcd < 0;
同理,为了避免类似:
这种, 虽然 S_abc * S_abd < 0 , 但是 S_acd*S_bcd > 0;
同样不能判定相交。
所以必须是:
S_abc * S_abd < 0 S_acd * S_bcd < 0
同时满足时,才能证明 ab 和 cd 两条线段相交。
关于二维坐标系三角形外积的求法,我这里给出简单的代码:
/* == Helper == */ // 用于帮助检测 碰撞 /** * Helper area calculation function. Returns 2 X the area. * 三角形外积 * * @param {Vec2} pointA * @param {Vec2} pointB * @param {Vec2} pointC * @return {number} */ function signed2DTriArea(pointA, pointB, pointC) { return ((pointA.x - pointC.x) * (pointB.y - pointC.y) - (pointA.y - pointC.y) * (pointB.x - pointC.x)); }
有了上面的知识,就可以得出两条线段相交 的判定 方法 和交点了:
/** * Helper intersection function. Checks if two lines intersect. * * @param {LineSegment2} a Line A * @param {LineSegment2} b Line B * @return {Object} An object is returned with intersection point and time if they intersect, otherwise null. * 判断两条线段是否相交,如果是,返回交点,和相交比例, 否则返回null */ function intersectLineSegments(a, b) { var a1 = signed2DTriArea(a.a, a.b, b.b); var a2 = signed2DTriArea(a.a, a.b, b.a); if (a1 * a2 < 0) { var a3 = signed2DTriArea(b.a, b.b, a.a); var a4 = a3 + a2 - a1; if (a3 * a4 < 0) { var intersectionTime = a3 / (a3 - a4); // intersectionPoint = a.a + intersectionTime * (a.b - a.a); var intersectionPoint = new Vec2(a.b.x, a.b.y); intersectionPoint.sub(a.a); intersectionPoint.mul(intersectionTime); intersectionPoint.add(a.a); return {intersectionPoint: intersectionPoint,intersectionTime : intersectionTime}; } } return null; }
【关于线段类】
当然,我们要用到线段的概念,那么最好抽象出一个 线段的 类, 我这里也提供在线段处理中常用的几个方法,比如
- 获取线段上 到指定点p 最短距离的点
- 获取距离p点最短距离的点 的 比例
- 得到 p 点到线段的最短距离
类似的,可以看看 这个 demo 就知道 是怎么回事了。http://hongru.github.com/proj/laro/test/lineSegment.test.html (需canvas支持,里面几个端点都是可以拖动的)。
【关于 由 多条线段组成的不规则凸起物的碰撞】
有了针对运动物体相对于 一条 线段 碰撞的检测后, 那么 由多条线段组成的 凸起物 也就顺着解决就行,无非就是 将 这种 由多条线段组成的 凸起物 按每条边分解成 单一的 向量,分别检测 就行。
以下是 一个圆 相对于 不规则突起物 的碰撞检测判断代码:
/** * Test a sweep-circle against convex shape. * * @param {Circle} circle Circle used in test * @param {Vec2} movement Vector describing movement * @param {ConvexShape} shape Shape to be tested against * @return {Collisions} Collision objects that holds both contact points and intersection time. * 判断一个移动的圆 和 不规则凸起形状的碰撞相交关系,如果相交,返回关联的点和 相交比例 * convex shape 的 points 顶点顺序 需要是逆时针排列的,这样可以避免在内部碰撞的检测,同时movement反向的也可以直接跳出 */ function getCollisionShape(circle, movement, shape) { var contactPoints = [], SWEEP_EPSILON = pkg.SWEEP_EPSILON; var foundOne = false; var minIntersectionTime = Number.MAX_VALUE; var localIntersectionTime = Number.MAX_VALUE; var i = 0; // Used for iteration var pt; // Handle during loops var contact; // Placeholder for eventual contact point if (shape.numOfPoints() > 1) { pt = shape.points; for (i = 0; i < pt.length; i++) { var last = pt[i]; var curr = i+1 === pt.length ? pt[0] : pt[i+1]; // last & curr is now start and end points of the line. // 远离 var normal = new Vec2(-(curr.y - last.y), curr.x - last.x); if (0 < normal.dot(movement)) { continue; } normal.normalize(); // last = last + normal * (circle.r + SWEEP_EPSILON) var _last = new Vec2(normal.x, normal.y); _last.mul(circle.r + SWEEP_EPSILON); _last.add(last); // curr = curr + normal * (circle.r + SWEEP_EPSILON) var _curr = new Vec2(normal.x, normal.y); _curr.mul(circle.r + SWEEP_EPSILON); _curr.add(curr); var endPos = new Vec2(circle.c.x, circle.c.y); endPos.add(movement); var localContact = new Vec2(); var res = intersectLineSegments(new LineSegment2(circle.c, endPos), new LineSegment2(_last, _curr)); if (res) { if (res.intersectionTime < minIntersectionTime) { foundOne = true; minIntersectionTime = res.intersectionTime; // localContact - normal * circle.r var _lc = new Vec2(normal.x, normal.y); _lc.mul(circle.r); res.intersectionPoint.sub(_lc); contact = new CollisionContact(res.intersectionPoint, normal, 0, shape.user, shape.material); } } } } if (!foundOne) { pt = shape.points; for (i = 0; i < pt.length; i++) { localIntersectionTime = Number.MAX_VALUE; // Multiply the length of the ray to make sure that the spheres won't go // through each other at extremely low speeds var _mov = new Vec2(movement.x, movement.y); _mov.mul(1.1); if (intersectRaySphere(new Ray2(circle.c, _mov), new Circle(pt[i], circle.r))) { localIntersectionTime = closure(); if (localIntersectionTime < minIntersectionTime) { // Got collision. foundOne = true; minIntersectionTime = localIntersectionTime; // circle.c + localIntersectionTime * movement - pt[i] var _normal = new Vec2(movement.x, movement.y); _normal.mul(localIntersectionTime); _normal.sub(pt[i]); _normal.add(circle.c); _normal.normalize(); // Contact point is the vertex. // Normal is vector from corner to position of sphere at collision time. contact = new CollisionContact(pt[i], _normal, 0, shape.user, shape.material); } } } } if (foundOne) { contactPoints.push(contact); return new Collisions(contactPoints, minIntersectionTime); } else { return null; } };
其他的代码我就不多贴了,相信明白了原理之后,大家都能自己写出类似功能的代码。
我也就不献丑了。
最后把另外两个 用上面的方式 进行碰撞检测 的demo 贴出来:(都需要canvas支持)
http://hongru.github.com/proj/laro/test/laro.collision.test2.html
http://hongru.github.com/proj/laro/test/laro.collision.test3.html
【注:test3 demo 引入的弹性碰撞 和 非弹性碰撞的概念, 这个后面再说】
【后记】
代码功能越多,复杂度越高,必然导致计算量增加。 更重要的是找一个权衡吧。
PS,吐槽下,所谓的弹性工作时间没了,明天要9点前到公司,坑爹,要早起了。
今天早点睡吧,各位晚安 : )