9

概要

この質問は JavaScript で書かれていますが、任意の言語、疑似コード、または数学だけで答えていただければ幸いです。

次のことを達成するために、分離軸定理を実装しようとしています。

  • 凸多角形と円の交点を検出します。
  • 交点を解決するために円に適用できる平行移動を見つけて、円が多角形にかろうじて接触し、内側にならないようにします。
  • 衝突の軸を決定します(質問の最後に詳細があります)。

最初の箇条書きを正常に完了しました。質問の最後に私の JavaScript コードが表示されます。他の部分で困っています。

交差点の解決

円の重なりが最小/最短の方向で交差点を解決する方法については、オンラインで多くの例があります。最後のコードで、これが既に計算されていることがわかります。

しかし、これは私のニーズには合いません。円の軌道の反対方向で衝突を解決する必要があります (円の軌道が既にあり、それを単位ベクトルまたは角度のいずれか適切な方として関数に渡したいとします)。

下の画像で、最短解像度と意図した解像度の違いを確認できます。

ここに画像の説明を入力

関数内の交点を解決するための最小平行移動ベクトルを計算するにはどうすればよいtest_CIRCLE_POLYですか?ただし、円の軌道とは反対の特定の方向に適用する必要があります。

私のアイデア/試み:

  • 私の最初のアイデアは、SAT アルゴリズムでテストする必要がある軸に追加の軸を追加することでした。この軸は、円の軌跡に対して垂直になります。次に、この軸に投影するときにオーバーラップに基づいて解決します。これはある程度機能しますが、ほとんどの状況ではるかに解決されます。最小限の翻訳にはなりません。したがって、これは満足のいくものではありません。
  • 私の 2 番目のアイデアは、最短のオーバーラップの大きさを引き続き使用することでしたが、円の軌道の反対になるように方向を変更しました。これは有望に見えますが、おそらく私が説明していない多くのエッジケースがあります. たぶん、これは始めるのに良い場所です。

衝突の側面/軸の決定

円が衝突しているポリゴンの側面を特定する方法を見つけました。多角形のテストされた軸ごとに、オーバーラップをチェックするだけです。重なりがある場合は、その面が衝突しています。

円の軌道に応じて片側だけを把握したいので、この解決策は再び受け入れられません。

私の意図した解決策は、下の例の画像で、軸 A が衝突の軸であり、軸 B ではないことを教えてくれます。これは、交差が解決されると、軸 A がポリゴンの側面に対応する軸になるためです。円にかろうじて触れるだけです。

ここに画像の説明を入力

私のアイデア/試み:

  • 現在、衝突の軸は MTV (最小平行移動ベクトル) に垂直であると想定しています。これは現在正しくありませんが、質問の前半で交差解決プロセスを更新すると、正しい軸になるはずです。そのため、その部分を最初に取り組む必要があります。

  • または、円の前の位置と現在の位置 + 半径から線を作成し、この線と交差する辺を確認することを検討しました。ただし、場合によっては複数の辺が線と交差することがあるため、あいまいさが残ります。

これまでの私のコード

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);
};
4

4 に答える 4

1

円ポリゴン切片

ボールが動いていて、ボールが常にポリゴンの外側から始まるようにすることができれば、解決策はかなり単純です。

ボールとその動きをボールラインと呼びます。ボールの現在の位置から開始し、次のフレームでボールが配置される位置で終了します。

解決するには、ボール ラインの始点に最も近いインターセプトを見つけます。

インターセプトには 2 つのタイプがあります。

  • 線分(ボールライン)と線分(ポリゴンエッジ)
  • 円付きの線分 (ボール ライン) (各 (凸のみ) ポリゴン コーナーの円)

サンプル コードには、Lines2関連する 2 つのインターセプト関数を含むオブジェクトがあります。切片は、Vec22 つの単位距離を含む として返されます。インターセプト関数は、ライン セグメントではなくライン (無限長) 用です。切片がない場合、戻り値は未定義です。

ライン インターセプトLine2.unitInterceptsLine(line, result = new Vec2())の単位値 ( result) は、始点からの各ラインに沿った単位距離です。負の値は開始より遅れています。

ボールの半径を考慮するために、各ポリゴン エッジはその法線に沿ってボールの半径をオフセットします。ポリゴン エッジの方向が一定であることが重要です。この例では、法線は線の右側にあり、ポリゴン ポイントは時計回りの方向にあります。

線分/円の切片Line2.unitInterceptsCircle(center, radius, result = new Vec2())の場合、単位値 ( result) は円と交わる線に沿った単位距離です。result.x常に最も近い切片が含まれます (円の外で開始すると仮定します)。インターセプトがある場合、同じポイントにある場合でも、ウェイは常に 2 つあります。

例には必要なものがすべて含まれています

関心のあるオブジェクトはballpoly

  • ballボールとその動きを定義します。例のためにそれを描画するコードもあります

  • polyポリゴンのポイントを保持します。ボールの半径に応じて、ポイントをオフセット ラインに変換します。ボールの半径が変化した場合にのみラインを計算するように最適化されています。

関数poly.movingBallInterceptは、すべての作業を行う関数です。これは、ボール オブジェクトとオプションの結果ベクトルを取ります。

Vec2ポリゴンに接触した場合、その位置をボールの として返します。

これは、オフセット ラインとポイント (円として) までの最小単位距離を見つけ、その単位距離を使用して結果を配置することによって行われます。

ボールがポリゴンの内側にある場合、コーナーとの交点が逆になることに注意してください。この関数Line2.unitInterceptsCircleは、線が円に出入りする 2 単位の距離を提供します。ただし、どちらを使用するかを知るには、屋内か屋外かを知る必要があります。この例では、ポリゴンの外側にいると想定しています。

指示

  • マウスを動かしてボールのパスを変更します。
  • クリックしてボールの開始位置を設定します。

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 - v.x) === 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 * v.x + this.y * v.y },
    uDot(v, div) { return (this.x * v.x + this.y * v.y) / div },
    cross(v) { return this.x * v.y - this.y * v.x },
    uCross(v, div) { return (this.x * v.y - 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.x, l.p1.y);
                ctx.lineTo(l.p2.x, l.p2.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.x, p.y) }
        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

簡単にするには、ベクター ライブラリが役立ちます。この例では、クイックVec2アンドLine2オブジェクトを作成しました (この例で使用されている関数のみがテストされていることに注意してください。注: オブジェクトはパフォーマンスを考慮して設計されています。経験の浅いコーダーは、これらのオブジェクトの使用を避け、より標準的なベクターおよびライン ライブラリを選択する必要があります)。

于 2020-06-20T21:48:16.983 に答える