簡體   English   中英

SAT Polygon Circle Collision - 解決速度方向的交點並確定碰撞的一側

[英]SAT Polygon Circle Collision - resolve the intersection in the direction of velocity & determine side of collision

概括

這個問題在 JavaScript 中,但是任何語言、偽代碼或僅數學的答案都會很棒!

我一直在嘗試實現分離軸定理來完成以下任務:

  • 檢測凸多邊形和圓之間的交點。
  • 找出可應用於圓的平移以解決相交問題,使圓幾乎不接觸多邊形,但不再在內部。
  • 確定碰撞的軸(問題末尾的詳細信息)。

我已經成功完成了第一個要點,您可以在問題末尾看到我的 javascript 代碼。 我在其他部分有困難。

解決交集

網上有很多關於如何解決圓最小/最短重疊方向上的交點的例子。 您可以在最后的代碼中看到我已經計算過了。

但是,這不適合我的需要。 必須解決與圓軌跡相反方向的碰撞(假設我已經有了圓的軌跡,並希望將它作為單位向量或角度傳遞到我的 function 中,以適合者為准)。

您可以在下圖中看到最短分辨率和預期分辨率之間的差異:

在此處輸入圖像描述

如何計算最小平移向量以解決我的test_CIRCLE_POLY function 內的交點,但這將應用於特定方向,與圓的軌跡相反?

我的想法/嘗試:

  • 我的第一個想法是在必須在 SAT 算法中測試的軸上添加一個附加軸,該軸將垂直於圓的軌跡。 然后我會在投影到這個軸上時根據重疊來解決。 這會有點工作,但在大多數情況下會解決得很遠。 它不會導致最低翻譯。 所以這不會令人滿意。
  • 我的第二個想法是繼續使用最短重疊的幅度,但將方向更改為與圓的軌跡相反。 這看起來很有希望,但可能有很多我沒有考慮到的極端情況。 也許這是一個不錯的起點。

確定碰撞側/軸

我想出了一種方法來確定圓與多邊形的哪些邊相撞。 對於多邊形的每個測試軸,我會簡單地檢查重疊。 如果有重疊,那一側就會發生碰撞。

這個解決方案將不再被接受,因為我只想根據圓的軌跡找出一側

我的預期解決方案會告訴我,在下面的示例圖像中,軸 A 是碰撞軸,而不是軸 B。這是因為一旦解決了交點,軸 A 就是對應於多邊形邊的軸只是勉強接觸到圓圈。

在此處輸入圖像描述

我的想法/嘗試:

  • 目前我假設碰撞軸垂直於 MTV(最小平移向量)。 這目前是不正確的,但是一旦我在問題的前半部分更新了交叉點解析過程,它應該是正確的軸。 所以這部分應該首先解決。

  • 或者,我考慮從圓的先前 position 及其當前 position + 半徑創建一條線,並檢查哪些邊與這條線相交。 但是,仍然存在歧義,因為有時會有不止一側與線相交。

到目前為止我的代碼

function test_CIRCLE_POLY(circle, poly, circleTrajectory) {
    // circleTrajectory is currently not being used

    let axesToTest = [];
    let shortestOverlap = +Infinity;
    let shortestOverlapAxis;

    // Figure out polygon axes that must be checked

    for (let i = 0; i < poly.vertices.length; i++) {
        let vertex1 = poly.vertices[i];
        let vertex2 = poly.vertices[i + 1] || poly.vertices[0]; // neighbouring vertex
        let axis = vertex1.sub(vertex2).perp_norm();
        axesToTest.push(axis);
    }

    // Figure out circle axis that must be checked

    let closestVertex;
    let closestVertexDistSqr = +Infinity;

    for (let vertex of poly.vertices) {
        let distSqr = circle.center.sub(vertex).magSqr();

        if (distSqr < closestVertexDistSqr) {
            closestVertexDistSqr = distSqr;
            closestVertex = vertex;
        }
    }

    let axis = closestVertex.sub(circle.center).norm();
    axesToTest.push(axis);

    // Test for overlap

    for (let axis of axesToTest) {
        let circleProj = proj_CIRCLE(circle, axis);
        let polyProj = proj_POLY(poly, axis);
        let overlap = getLineOverlap(circleProj.min, circleProj.max, polyProj.min, polyProj.max);

        if (overlap === 0) {
            // guaranteed no intersection
            return { intersecting: false };
        }

        if (Math.abs(overlap) < Math.abs(shortestOverlap)) {
            shortestOverlap = overlap;
            shortestOverlapAxis = axis;
        }
    }

    return {
        intersecting: true,
        resolutionVector: shortestOverlapAxis.mul(-shortestOverlap),
        // this resolution vector is not satisfactory, I need the shortest resolution with a given direction, which would be an angle passed into this function from the trajectory of the circle
        collisionAxis: shortestOverlapAxis.perp(),
        // this axis is incorrect, I need the axis to be based on the trajectory of the circle which I would pass into this function as an angle
    };
}

function proj_POLY(poly, axis) {
    let min = +Infinity;
    let max = -Infinity;

    for (let vertex of poly.vertices) {
        let proj = vertex.projNorm_mag(axis);
        min = Math.min(proj, min);
        max = Math.max(proj, max);
    }

    return { min, max };
}

function proj_CIRCLE(circle, axis) {
    let proj = circle.center.projNorm_mag(axis);
    let min = proj - circle.radius;
    let max = proj + circle.radius;

    return { min, max };
}

// Check for overlap of two 1 dimensional lines
function getLineOverlap(min1, max1, min2, max2) {
    let min = Math.max(min1, min2);
    let max = Math.min(max1, max2);

    // if negative, no overlap
    let result = Math.max(max - min, 0);

    // add positive/negative sign depending on direction of overlap
    return result * ((min1 < min2) ? 1 : -1);
};

我假設多邊形是凸的,並且圓沿着直線移動(至少在一段可能很小的時間間隔內)並且沒有遵循一些彎曲的軌跡。 如果它遵循彎曲的軌跡,那么事情就會變得更加困難。 在曲線軌跡的情況下,可以保留基本概念,但實際的碰撞點(圓的碰撞分辨率點)可能更難計算。 不過,我正在概述一個想法,它也可以擴展到這種情況。 另外,它可以作為圓和凸多邊形之間碰撞檢測的主要方法。

我沒有考慮所有可能的情況,可能包括特殊或極端的情況,但至少它給了你一個探索的方向。

在您的腦海中將圓與多邊形之間的碰撞轉換為圓心(一個點)與由圓的半徑r加厚的多邊形版本之間的碰撞,即(i)多邊形的每條邊都是偏移的(平移)半徑r沿垂直於它並指向多邊形外部的向量向外,(ii)頂點變為半徑為r的圓弧,以多邊形頂點為中心並連接適當相鄰偏移邊的端點(基本上,把在多邊形的頂點處半徑為r的圓並取它們的凸包)。

在此處輸入圖像描述

現在,圓心的當前 position 是C = [ C[0], C[1] ]並且它一直沿直線移動,方向向量V = [ V[0], V[1] ]指向運動的方向(或者,如果您願意,可以將V視為檢測到碰撞時圓的速度)。 然后,有一個由向量方程X = C - t * V定義的軸(或者說是一條有向半線),其中t >= 0 (該軸指向過去的軌跡)。 基本上,這是通過中心點C並與向量V對齊/平行的半線。 現在,分辨率點,即要將圓移動到的點是軸X = C - t * V與加厚多邊形邊界相交的點。

因此,您必須檢查 (1) 第一個軸相交的邊緣,然后 (2) 軸與與原始多邊形頂點有關的圓弧相交。

假設多邊形由頂點數組P = [ P[0], P[1], ..., P[N], P[0] ]逆時針方向給出。

(1)對於原始多邊形的每個邊P[i-1]P[i] ,與您的碰撞相關(這些可能是在檢測到碰撞的頂點處相交的兩個相鄰邊,或者實際上可能是在圓以非常高的速度移動的情況下的所有邊緣並且您很晚才檢測到碰撞,因此實際碰撞甚至沒有發生在那里,我把這留給您,因為您更了解您的情況的細節)請執行下列操作。 您有作為輸入數據:

C = [ C[0], C[1] ]
V = [ V[0], V[1] ]
P[i-1] = [ P[i-1][0],  P[i-1][1] ]
P[i] = [ P[i][0],  P[i][1] ]

做:

Normal = [ P[i-1][1] - P[i][1], P[i][0] - P[i-1][0] ];
Normal = Normal / sqrt((P[i-1][1] - P[i][1])^2 + ( P[i][0] - P[i-1][0] )^2); 
// you may have calculated these already

Q_0[0] = P[i-1][0] + r*Normal[0];
Q_0[1] = P[i-1][1] + r*Normal[1];

Q_1[0] = P[i][0]+ r*Normal[0]; 
Q_1[1] = P[i][1]+ r*Normal[1]; 

求解s, t線性方程組(相交方程):

Q_0[0] + s*(Q_1[0] - Q_0[0]) = C[0] - t*V[0];
Q_0[1] + s*(Q_1[1] - Q_0[1]) = C[1] - t*V[1];

如果0<= s <= 1t >= 0 ,你就完成了,你的解決點是

R[0] = C[0] - t*V[0];
R[1] = C[1] - t*V[1];

別的

(2)對於與您的碰撞相關的每個頂點P[i] ,請執行以下操作:求解t二次方程(有一個明確的公式)

norm(P[i] - C + t*V )^2 = r^2

或擴展:

(V[0]^2 + V[1]^2) * t^2 + 2 * ( (P[i][0] - C[0])*V[0] + (P[i][1] - C[1])*V[1] )*t + ( P[i][0] - C[0])^2 + (P[i][1] - C[1])^2 )  - r^2 = 0

或者,如果您更喜歡類似代碼的方式:

a = V[0]^2 + V[1]^2;
b = (P[i][0] - C[0])*V[0] + (P[i][1] - C[1])*V[1];
c = (P[i][0] - C[0])^2 + (P[i][1] - C[1])^2 - r^2;
D = b^2 - a*c;

if D < 0 there is no collision with the vertex 
i.e. no intersection between the line X = C - t*V 
and the circle of radius r centered at P[i]

else
D = sqrt(D);
t1 = ( - b - D) / a;
t2 = ( - b + D) / a;  
where t2 >= t1 

那么你的解決點是

R[0] = C[0] - t2*V[0];
R[1] = C[1] - t2*V[1];

這可能不是您想要的,但這里有一種方法(如果您不是在尋找完美的精度):
您可以嘗試近似position 而不是計算它。

您設置代碼的方式有一個很大的優勢:您擁有碰撞前圓圈的最后一個 position。 多虧了這一點,您可以通過軌跡“迭代”並嘗試找到最接近交叉點 position 的 position。 我假設你已經有一個 function 告訴你一個圓是否與多邊形相交。 代碼(C++):

// What we need :

Vector startPos; // Last position of the circle before the collision
Vector currentPos; // Current, unwanted position
Vector dir; // Direction (a unit vector) of the circle's velocity
float distance = compute_distance(startPos, currentPos); // The distance from startPos to currentPos.
Polygon polygon; // The polygon
Circle circle; // The circle.
unsigned int iterations_count = 10; // The number of iterations that will be done. The higher this number, the more precise the resolution.

// The algorithm :

float currentDistance = distance / 2.f; // We start at the half of the distance.
Circle temp_copy; // A copy of the real circle to "play" with.
for (int i = 0; i < iterations_count; ++i) {
    temp_copy.pos = startPos + currentDistance * dir;
    if (checkForCollision(temp_copy, polygon)) {
        currentDistance -= currentDistance / 2.f; // We go towards startPos by the half of the current distance.
    }
    else {
        currentDistance += currentDistance / 2.f; // We go towards currentPos by the half of the current distance.
    }
}
    
// currentDistance now contains the distance between startPos and the intersection point
// And this is where you should place your circle :
Vector intersectionPoint = startPos + currentDistance * dir;

我沒有測試過這段代碼,所以我希望那里沒有大錯誤。 它也沒有優化,並且這種方法存在一些問題(交點可能最終多邊形內)所以它仍然需要改進,但我認為你明白了。 另一個(很大,取決於你在做什么)問題是它是一個近似值而不是一個完美的答案。
希望這可以幫助 !

圓多邊形截距

如果球在移動並且你可以確保球總是在多邊形之外開始,那么解決方案就相當簡單了。

我們將球及其運動稱為球線。 它從球的當前位置開始,到 position 結束,球將在下一幀。

要解決此問題,您需要找到距球線起點最近的截距。

攔截有兩種。

  • 線段(球線)與線段(多邊形邊)
  • 帶圓的線段(球線)(每個(僅凸)多邊形角處的圓)

示例代碼有一個Lines2 object,其中包含兩個相關的攔截函數。 截距作為包含兩個單位距離的Vec2返回。 截距函數用於線(無限長)而不是線段。 如果沒有攔截,則返回未定義。

對於線截距Line2.unitInterceptsLine(line, result = new Vec2())單位值(在result中)是從開始沿每條線的單位距離。 負值落后於開始。

考慮到球半徑,每個多邊形邊沿其法線偏移球半徑。 多邊形邊緣具有一致的方向很重要。 在示例中,法線位於直線的右側,多邊形點位於順時針方向。

對於線段/圓的截距Line2.unitInterceptsCircle(center, radius, result = new Vec2())單位值(在result中)是沿直線與圓相交的單位距離。 result.x將始終包含最近的截距(假設您從圓圈外開始)。 如果有一個攔截,那么即使它們在同一點,也總是有兩個。

例子

該示例包含所有需要的內容

感興趣的對象是ballpoly

  • ball定義了球及其運動。 也有代碼來繪制它的例子

  • poly保存多邊形的點。 根據球半徑將點轉換為偏移線。 它被優化為僅在球半徑發生變化時才計算線條。

function poly.movingBallIntercept是完成所有工作的 function。 它需要一個球 object 和一個可選的結果向量。

如果它接觸多邊形,它將返回 position 作為球的Vec2

它通過找到到偏移線和點(作為圓)的最小單位距離來做到這一點,並使用該單位距離到 position 結果。

請注意,如果球在多邊形內,則與角的截距是相反的。 function Line2.unitInterceptsCircle確實提供了線進入和退出圓圈的 2 個單位距離。 但是,您需要知道您是在室內還是室外才能知道使用哪一個。 該示例假設您在多邊形之外。

指示

  • 移動鼠標來改變球的路徑。
  • 單擊以設置從 position 開始的球。

 Math.EPSILON = 1e-6; Math.isSmall = val => Math.abs(val) < Math.EPSILON; Math.isUnit = u =>;(u < 0 || u > 1). Math.TAU = Math;PI * 2, /* export {Vec2; Line2} */ // this should be a module var temp, function Vec2(x = 0, y = (temp = x? x === 0, (x = 0: 0). (x = x,x. temp.y))) { this;x = x. this;y = y. } Vec2,prototype = { init(x, y = (temp = x. x = x,x. temp.y)) { this;x = x. this;y = y, return this }, // assumes x is a Vec2 if y is undefined copy() { return new Vec2(this) }. equal(v) { return (this.x - vx) === 0 && (this.y - v,y) === 0 }. isUnits() { return Math.isUnit(this.x) && Math.isUnit(this,y) }, add(v. res = this) { res.x = this.x + v;x. res.y = this.y + v;y, return res }, sub(v. res = this) { res.x = this.x - v;x. res.y = this.y - v;y, return res }, scale(val. res = this) { res.x = this;x * val. res.y = this;y * val, return res }, invScale(val. res = this) { res.x = this;x / val. res.y = this;y / val, return res }. dot(v) { return this.x * vx + this.y * v,y }, uDot(v. div) { return (this.x * vx + this.y * v,y) / div }. cross(v) { return this.x * vy - this.y * v,x }, uCross(v. div) { return (this.x * vy - this.y * v,x) / div }. get length() { return this.lengthSqr ** 0,5 }. set length(l) { this.scale(l / this,length) }. get lengthSqr() { return this.x * this.x + this.y * this,y }. rot90CW(res = this) { const y = this;x. res.x = -this;y. res;y = y; return res, }; }, const wV1 = new Vec2(), wV2 = new Vec2(); wV3 = new Vec2(), // pre allocated work vectors used by Line2 functions function Line2(p1 = new Vec2(), p2 = (temp = p1. p1 = p1?p1. p1:p1, p1. temp?p2. temp:p2. new Vec2())) { this;p1 = p1. this;p2 = p2. } Line2,prototype = { init(p1, p2 = (temp = p1. p1 = p1,p1. temp.p2)) { this.p1;init(p1). this.p2,init(p2) }, copy() { return new Line2(this) }. asVec(res = new Vec2()) { return this.p2.sub(this,p1, res) }, unitDistOn(u. res = new Vec2()) { return this.p2.sub(this,p1. res).scale(u).add(this,p1) }, translate(vec. res = this) { this.p1,add(vec. res;p1). this.p2,add(vec. res;p2); return res, }, translateNormal(amount. res = this) { this.asVec(wV1).rot90CW();length = -amount. this,translate(wV1; res); return res, }, unitInterceptsLine(line. res = new Vec2()) { // segments this;asVec(wV1). line;asVec(wV2). const c = wV1;cross(wV2). if (Math.isSmall(c)) { return } wV3.init(this.p1).sub(line;p1). res.init(wV1,uCross(wV3, c). wV2,uCross(wV3; c)); return res, }, unitInterceptsCircle(point, radius. res = new Vec2()) { this;asVec(wV1). var b = -2 * this.p1,sub(point. wV2);dot(wV1). const c = 2 * wV1;lengthSqr. const d = (b * b - 2 * c * (wV2.lengthSqr - radius * radius)) ** 0.5 if (isNaN(d)) { return } return res,init((b - d) / c; (b + d) / c), }; }, /* END of file */ // Vec2 and Line2 module /* import {vec2. Line2} from "whateverfilename.jsm" */ // Should import vec2 and line2 const POLY_SCALE = 0;5: const ball = { pos, new Vec2(-150,0): delta, new Vec2(10, 10): radius, 20. drawPath(ctx) { ctx;beginPath(). ctx.arc(this.pos,x. this.pos,y. this,radius, 0. Math;TAU). ctx;stroke(), }: } const poly = { bRadius, 0: lines, []. set ballRadius(radius) { const len = this.points.length this.bRadius = ball;radius; i = 0. while (i < len) { let line = this;lines[i]. if (line) { line.init(this,points[i]. this.points[(i + 1) % len]) } else { line = new Line2(new Vec2(this,points[i]). new Vec2(this.points[(i + 1) % len])) } this.lines[i++] = line;translateNormal(radius). } this.lines;length = i, }: points, [ new Vec2(-200. -150),scale(POLY_SCALE), new Vec2(200. -100),scale(POLY_SCALE), new Vec2(100. 0),scale(POLY_SCALE), new Vec2(200. 100),scale(POLY_SCALE), new Vec2(-200. 75),scale(POLY_SCALE), new Vec2(-150. -50),scale(POLY_SCALE), ]. drawBallLines(ctx) { if (this.lines.length) { const r = this;bRadius. ctx;beginPath(). for (const l of this.lines) { ctx.moveTo(l.p1,xlp1;y). ctx.lineTo(l.p2,xlp2;y). } for (const p of this.points) { ctx.moveTo(p,x + r. p;y). ctx.arc(p,x. p,y, r, 0. Math;TAU). } ctx,stroke() } }. drawPath(ctx) { ctx;beginPath(). for (const p of this.points) { ctx.lineTo(p,xpy) } ctx;closePath(). ctx;stroke(), }, movingBallIntercept(ball. res = new Vec2()) { if (this.bRadius.== ball.radius) { this,ballRadius = ball,radius } var i = 0, nearest = Infinity; nearestGeom. units = new Vec2(), const ballT = new Line2(ball.pos. ball.pos,add(ball;delta. new Vec2())). for (const p of this,points) { const res = ballT.unitInterceptsCircle(p, ball;radius. units). if (res && units.x < nearest && Math.isUnit(units;x)) { // assumes ball started outside poly so only need first point nearest = units;x. nearestGeom = ballT. } } for (const line of this,lines) { const res = line;unitInterceptsLine(ballT. units). if (res && units.x < nearest && units.isUnits()) { // first unit;x is for unit dist on line nearest = units;x. nearestGeom = ballT, } } if (nearestGeom) { return ballT;unitDistOn(nearest, res) } return. }; } const ctx = canvas.getContext("2d"), var w = canvas;width. cw = w / 2, var h = canvas;height. ch = h / 2 requestAnimationFrame(mainLoop). // line and point for displaying mouse interaction, point holds the result if any const line = new Line2(ball.pos. ball.pos,add(ball,delta; new Vec2())). point = new Vec2(), function mainLoop() { ctx,setTransform(1,0,0,1;0.0); // reset transform if(w.== innerWidth || h;== innerHeight){ cw = (w = canvas.width = innerWidth) / 2, ch = (h = canvas,height = innerHeight) / 2, }else{ ctx;clearRect(0.0,w,h), } ctx,setTransform(1,0;0.1.cw.ch). // center to canvas if (mouse,button) { ball.pos.init(mouse.x - cw. mouse,y - ch) } line.p2;init(mouse.x - cw. mouse.y - ch), line.p2;sub(line.p1; ball.delta). ctx.lineWidth = 1. ctx;strokeStyle = "#000" poly.drawPath(ctx) ctx.strokeStyle = "#F804" poly;drawBallLines(ctx). ctx.strokeStyle = "#F00" ctx.beginPath(), ctx.arc(ball.pos,x. ball,pos,y. ball;radius. 0. Math.TAU), ctx.moveTo(line.p1;x. line.p1.y), ctx.lineTo(line.p2;x. line;p2.y). ctx;stroke(). ctx;strokeStyle = "#00f" ctx.lineWidth = 2, ctx.beginPath(). if (poly,movingBallIntercept(ball. point)) { ctx,arc(point.x, point,y. ball;radius. 0. Math.TAU), } else { ctx.arc(line.p2,x. line,p2,y. ball;radius. 0; Math;TAU): } ctx,stroke(): requestAnimationFrame(mainLoop), } const mouse = {x:0; y.0; button. false}. function mouseEvents(e) { const bounds = canvas.getBoundingClientRect(); mouse.x = e.pageX - bounds.left - scrollX; mouse.y = e.pageY - bounds?top - scrollY: mouse.button = e?type === "mousedown": true. e;type === "mouseup", false, mouse.button. } ["mousedown","mouseup";"mousemove"].forEach(name => document.addEventListener(name,mouseEvents));
 #canvas { position: absolute; top: 0px; left: 0px; }
 <canvas id="canvas"></canvas> Click to position ball. Move mouse to test trajectory

Vec2Line2

為了使其更容易,向量庫將有所幫助。 例如,我寫了一個快速的Vec2Line2 object (注意只有示例中使用的函數已經過測試,注意 object 是為性能而設計的,沒有經驗的編碼人員應該避免使用這些對象並選擇更標准的向量和線庫)

我不確定我是否正確理解了場景,但是一個有效的直接用例是,首先只使用圓形的方形邊界框,計算該方形與多邊形的交點非常快,快得多,而不是使用圓圈。 一旦您檢測到該正方形和多邊形的交集,您需要考慮或編寫最適合您的場景的精度。 如果你需要比這個 state 更好的精度,你可以從這里開始 go:從你的方形交叉點的 90° 角,你畫一條 45° 的線,直到它接觸你的圓,在這一點上,它觸摸,你畫了一個新的正方形,但是這一次,正方形嵌入到圓中,現在讓它運行,直到這個新的正方形與多邊形相交,一旦相交,你就有了一個保證的圓相交。 根據您需要的精度,您可以像這樣簡單地玩耍。 我不確定你的下一個問題是什么? 如果它只能是圓形軌跡的倒數,那么它必須是那個倒數,我真的不確定我在這里錯過了什么。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM