碰撞檢測 :Line
阿新 • • 發佈:2020-09-14
目錄
引子
在 Collision Detection :Rectangle 中主要介紹了矩形相關的碰撞檢測,接著來看看直線的情況。
以下示例未做相容性檢查,建議在最新的 Chrome 瀏覽器中檢視。
Line/Point
這是示例頁面。
線與點的碰撞檢測,觀察下面一張圖:
從圖中可以發現,當點在線上時,到兩個端點的距離之和與線的長度相同。兩點之間的距離,同樣使用之前用到過的勾股定理。考慮到計算的精度誤差,可以設定一個誤差允許範圍值,這樣會感覺更加自然一些。
/* * (x1,y1) 線的一個端點 * (x2,y2) 線的另一個端點 * (px,py) 檢測點的座標 */ function checkLinePoint({x1,y1,x2,y2,px,py}) { const d1 = getLen([px,py],[x2,y2]); const d2 = getLen([px,py],[x2,y2]); const lineLen = getLen([x1,y1],[x2,y2]); const buffer = 0.1; // 誤差允許範圍 if (d1+d2 >= lineLen-buffer && d1+d2 <= lineLen+buffer) { return true; // 發生碰撞 } else { return false; // 沒有碰撞 } } /* * 勾股定理計算兩點間直線距離 * point1 線的一個端點 * point2 線的另一個端點 */ function getLen(point1,point2) { const [x1,y2] = point1; const [x2,y2] = point1; const minusX = x2-x1; const minusY = y2-y1; const len = Math.sqrt(minusX*minusX + minusY*minusY); return len; }
Line/Circle
這是示例頁面。
直線和圓的碰撞檢測,首先需要考慮直線是否位於圓內,因為有可能出現直線的長度小於圓的直徑。為了檢測這個,可以使用之前 Point/Circle 的檢測方法,如果任意一端在內部,就直接返回 true
跳過剩下的檢測。
const isInside1 = checkPointCircle({px:x1,py:y1,cx,cy,radius}); const isInside2 = checkPointCircle({px:x2,py:y2,cx,cy,radius}); if (isInside1 || isInside2) { return true }
接下來需要找到直線上離圓心最近的一個點,這個時候使用向量的點積可以計算出最近點的座標。下面是一個簡單的數學推導過程。
/**
*
* a 代表線的向量
* t 係數
* p1 直線上任意一點
* p0 非直線上的一點
* pt 直線上離 p0 最近的一點
*
* pt = p1 + t*a // p1 和 pt 都在直線上,存在這樣成立的關係係數 t
*
* (a.x,a.y)*(pt.x-p0.x,pt.y-p0.y) = 0 // 垂直的向量,點積為 0
*
* (a.x,a.y)*( (p1+t*a).x-p0.x,(p1+t*a).y-p0.y) = 0 // 帶入 pt
*
* a.x *(p1.x + t*a.x - p0.x) + a.y *(p1.y + t*a.y - p0.y) = 0
* t*(a.x*a.x + a.y*a.y) = a.x*(p0.x-p1.x)+a.y*(p0.y-p1.y)
* t = (a.x*(p0.x-p1.x)+a.y*(p0.y-p1.y)) / ((a.x*a.x + a.y*a.y))
*
* 得出係數 t 的值後,代入到一開始的公式中,就可以得出 pt 的座標
*/
然而得出的這個點可能存在這條線延伸的方向上,所以需要判斷該點是否在所提供的線段上。這個時候可以使用前面介紹的關於 Line/Point 檢測的方法。
const isOnSegment = checkLinePoint({x1,y1,x2,y2, px:closestX,py:closestY});
if (!isOnSegment) return false;
最後計算圓心到直線上最近點的距離,與圓的半徑進行比較,判斷是否碰撞。下面是主要邏輯:
/*
* (x1,y1) 線的一個端點
* (x2,y2) 線的另一個端點
* (px,py) 圓心的座標
* radius 圓的半徑
*/
function checkLineCircle({x1,y1,x2,y2,cx,cy,radius}) {
const isInside1 = checkPointCircle({px:x1,py:y1,cx,cy,radius});
const isInside2 = checkPointCircle({px:x2,py:y2,cx,cy,radius});
if (isInside1 || isInside2) {
return true
}
const pointVectorX = x1 - x2;
const pointVectorY = y1 - y2;
const t = (pointVectorX*(cx - x1) + pointVectorY*(cy-y1))/(pointVectorX*pointVectorX+pointVectorY*pointVectorY);
const closestX = x1 + t*pointVectorX;
const closestY = y1 + t*pointVectorY;
const isOnSegment = checkLinePoint({x1,y1,x2,y2, px:closestX,py:closestY});
if (!isOnSegment) return false;
const distX = closestX - cx;
const distY = closestY - cy;
const distance = Math.sqrt( (distX*distX) + (distY*distY) );
if (distance <= radius) {
return true; // 發生碰撞
} else {
return false; // 沒有碰撞
}
}
Line/Line
這是示例頁面。
直線與直線的碰撞檢測,需要藉助數學的推導:
/**
*
* P1 P2 直線 1 上的兩個點
* A1 代表直線 1 的向量
* t1 直線 1 的係數
*
* P3 P4 直線 2 上的兩個點
* A2 代表直線 2 的向量
* t2 直線 2 的係數
*
* Pa = P1 + t1*A1
* Pb = P3 + t2*A2
*
* 相交時,Pa = Pb
* x1 + t1*(x2-x1) = x3 + t2*(x4-x3)
* y1 + t1*(y2-y1) =y3 + t2*(y4-y3)
*
* 剩下就是二元一次方程求解
* t1 = ((x4-x3)*(y1-y3) - (y4-y3)*(x1-x3))/((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1))
* t2 = ((x2-x1)*(y1-y3) - (y2-y1)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1))
*
*/
計算出兩條線的係數後,如果兩條線相交,就要符合條件:
if (t1 >= 0 && t1 <= 1 && t2 >= 0 && t2 <= 1) {
return true;
}
return false;
下面是完整的判斷方法:
/*
* (x1,y1) 線1的一個端點
* (x2,y2) 線1的另一個端點
* (x3,y3) 線2的一個端點
* (x4,y4) 線2的另一個端點
*/
function checkLineLine({x1,y1,x2,y2,x3,y3,x4,y4}) {
const t1 = ((x4-x3)*(y1-y3) - (y4-y3)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1));
const t2 = ((x2-x1)*(y1-y3) - (y2-y1)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1));
if (t1 >= 0 && t1 <= 1 && t2 >= 0 && t2 <= 1) {
return true; // 發生碰撞
} else {
return false; // 沒有碰撞
}
}
Back to top
Line/Rectangle
這是示例頁面。
直線與矩形的碰撞檢測,可以轉換為直線與矩形四條邊的碰撞檢測,使用前面介紹的關於 Line/Line 檢測的方法即可。
/*
* (x1,y1) 線的一個端點
* (x2,y2) 線的另一個端點
* (rx,ry) 矩形頂點座標
* rw 矩形寬度
* rh 矩形高度
*/
function checkLineRectangle({x1,y1,x2,y2,rx,ry,rw,rh}) {
const isLeftCollision = checkLineLine(x1,y1,x2,y2, x3:rx,y3:ry,x4:rx, y4:ry+rh);
const isRightCollision = checkLineLine(x1,y1,x2,y2, x3:rx+rw,y3:ry, x4:rx+rw,y4:ry+rh);
const isTopCollision = checkLineLine(x1,y1,x2,y2, x3:rx,y3:ry, x4:rx+rw,y4:ry);
const isBottomCollision = checkLineLine(x1,y1,x2,y2, x3:rx,y3:ry+rh, x4:rx+rw,y4:ry+rh);
if (isLeftCollision || isRightCollision || isTopCollision || isBottomCollision ) {
return true; // 發生碰撞
} else {
return false; // 沒有碰撞
}
}
Back to top