關於簡單的碰撞檢測


【前言】

這篇博文旨在給自己做個記錄和備忘,同時希望也能給有這方面簡易碰撞模型需求的同學一點點參考價值。

【關於像素級別檢測】

前一陣有同學問我說能否做到像素級別的碰撞檢測,做過類似碰撞檢測的同學應該清楚,按照我們最常規的想法,假如要檢測一個運動的物體和一條線之間是否有碰撞,最簡單的判斷條件,就是看當前幀,這個物體的位置,是否超過的我們的界定范圍...

但這樣簡單的判定確實是有問題的,我們舉一個實際一點的例子。

假如 小球 從 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點前到公司,坑爹,要早起了。
今天早點睡吧,各位晚安 : ) 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM