ベジェに沿って2つの惑星間で船を移動し、加速のためのいくつかの方程式が欠落している


48

OK、私はすでにmath.stackechange.comでこれを投稿しましたが、答えは得られませんでした:(

最初にここに私の問題の写真があり、その後に説明が続きます。

代替テキスト

だから私はすべてのポイントと値を設定しました。

船は左の惑星を動き回るからスタートP1してS=0.27 Degrees、それが到達したときに、gametickごとにPoint Aそれが達するまで、ベジェ曲線を次開始しPoint D、それは右の惑星の周りを移動P2してS=0.42 Degreesゲームチックあたり。の違いはS、惑星の周りを同じ移動速度で移動することです。

これまでのところ、これで問題は解決しました。

ときS P1S P2あまりに異なり、それはそれの目的地に到達したとき、船はかなり悪い見えた、2つの速度の間でジャンプします。だから私は間の船を加速する必要があるPoint APoint DS P1S P2

私が不足しているものは紫色で、それらは次のとおりです。

  • ティックを計算する方法は、加速度を考慮して船がベジェに沿って移動するのに必要です。

  • また、Tに基づいてベジェ曲線上の位置を見つける方法で、再び加速を考慮します。

ATM N点間の距離を計算することにより、ベジェの長さを計算します。だから私は何だと思い、私が必要とする、拡張するための方法であるT加速度に応じて私のベジェ計算に入れるために私の必要性を。


2
それを理解する上で良い仕事。質問への回答として調査結果を投稿することをお勧めします。
-bummzack

回答:


83

OK、私はすべてが機能している、それは永遠にかかったので、ここで私の詳細なソリューションを投稿するつもりです。
注:すべてのコードサンプルはJavaScriptで記述されています。

それでは、問題を基本的な部分に分けましょう。

  1. 0..1ベジエ曲線の長さと、その間の点を計算する必要があります

  2. T船をある速度から別の速度に加速するために、スケーリングを調整する必要があります

ベジエを正しくする

ベジェ曲線を描くためのコードを見つけるのは簡単ですが、いくつかの異なるアプローチがありますが、そのうちの1つはDeCasteljauアルゴリズムですが、3次ベジェ曲線の方程式を使用することもできます。

// Part of a class, a, b, c, d are the four control points of the curve
x: function (t) {
    return ((1 - t) * (1 - t) * (1 - t)) * this.a.x
           + 3 * ((1 - t) * (1 - t)) * t * this.b.x
           + 3 * (1 - t) * (t * t) * this.c.x
           + (t * t * t) * this.d.x;
},

y: function (t) {
    return ((1 - t) * (1 - t) * (1 - t)) * this.a.y
           + 3 * ((1 - t) * (1 - t)) * t * this.b.y
           + 3 * (1 - t) * (t * t) * this.c.y
           + (t * t * t) * this.d.y;
}

これにより、1は現在呼び出すことによって、ベジェ曲線を描くことができるxyしてtの範囲これは0 to 1、のは、見てみましょう:

代替テキスト

ええと...それは実際にはポイントの均等な分布ではありませんか?
ベジエ曲線の性質により、上の点0...1は異なりますarc lenghts。そのため、開始点と終了点に近いセグメントは、曲線の中央に近いセグメントよりも長くなります。

Tを曲線上に均等にマッピングする、別名、アーク長パラメータ化

じゃあ何をすればいいの?まあ簡単な言葉で私たちはをマッピングする機能を必要とするT上にt、曲線のように、私たちT 0.25に結果tでのその25%曲線の長さの。

それをどうやってやるの?私たちはグーグル...しかし、用語はグーグルほどではないことが判明し、ある時点でこのPDFをヒットします。確かに素晴らしい読み物ですが、学校で学んだすべての数学のことを忘れてしまった場合(または数学記号が嫌いな場合)は、ほとんど役に立たないでしょう。

今何?さあ、グーグルでもう少し(読んで:6時間)、ついにそのトピックに関する素晴らしい記事(素敵な写真を含む!^ _ ^ ")を見つけることができます:http :
//www.planetclegg.com/projects/WarpingTextToSplines.html

実際のコードを実行する

ケースであなただけのPDFのことをダウンロードする抵抗することができませんでしたが、すでに長い、長い時間前にあなたの数学の知識を失ったと思います(とあなたはスキップするように管理さ偉大な「神は、これは取る:記事リンク)を、あなたが今考えるかもしれません数百行のコードと大量のCPU」

いいえ、それはしません。数学のことになると、すべてのプログラマーが行うことを行うから
です。

アーク長パラメーター化、怠laな方法

それに直面してみましょう、私たちはゲームで無限の精度を必要としませんよね?したがって、NASAで働いていて、人々に火星を送ることを計画しているのでなければ、0.000001 pixel完璧なソリューションは必要ありません。

それでは、どのようにマップTtますか?シンプルで、3つのステップのみで構成されています。

  1. 計算N曲線上の点使用t及び保存arc-length配列にその位置で(曲線の別名長さ)

  2. にマッピングTするにはt、最初Tに取得する曲線の全長を掛けてから、長さのu配列を検索して、以下の最大値のインデックスを探しますu

  3. 正確にヒットした場合、そのインデックスの配列値をで割った値を返します。N見つかったポイントと次のポイントの間を少し補間しない場合は、もう一度で割ってN戻ります。

それで全部です!それでは、完全なコードを見てみましょう。

function Bezier(a, b, c, d) {
    this.a = a;
    this.b = b;
    this.c = c;
    this.d = d;

    this.len = 100;
    this.arcLengths = new Array(this.len + 1);
    this.arcLengths[0] = 0;

    var ox = this.x(0), oy = this.y(0), clen = 0;
    for(var i = 1; i <= this.len; i += 1) {
        var x = this.x(i * 0.05), y = this.y(i * 0.05);
        var dx = ox - x, dy = oy - y;        
        clen += Math.sqrt(dx * dx + dy * dy);
        this.arcLengths[i] = clen;
        ox = x, oy = y;
    }
    this.length = clen;    
}

これにより、新しい曲線が初期化arg-lenghtsされ、total lengthが計算されthis.lenますN。また、曲線の最後の長さも保存されます。ここで重要なのは、です。上の図のサイズの曲線で100 points十分だと思われるため、マッピングがより正確になります。適切な長さの見積もりが必要な場合25は、例ではありますが、マッピングの精度は低くTなりtます。

Bezier.prototype = {
    map: function(u) {
        var targetLength = u * this.arcLengths[this.len];
        var low = 0, high = this.len, index = 0;
        while (low < high) {
            index = low + (((high - low) / 2) | 0);
            if (this.arcLengths[index] < targetLength) {
                low = index + 1;

            } else {
                high = index;
            }
        }
        if (this.arcLengths[index] > targetLength) {
            index--;
        }

        var lengthBefore = this.arcLengths[index];
        if (lengthBefore === targetLength) {
            return index / this.len;

        } else {
            return (index + (targetLength - lengthBefore) / (this.arcLengths[index + 1] - lengthBefore)) / this.len;
        }
    },

    mx: function (u) {
        return this.x(this.map(u));
    },

    my: function (u) {
        return this.y(this.map(u));
    },

実際のマッピングコードは、最初にbinary search格納された長さで単純な操作を行って、より小さい最大長を見つけtargetLength、それから単に戻るか、補間と戻りを行います。

    x: function (t) {
        return ((1 - t) * (1 - t) * (1 - t)) * this.a.x
               + 3 * ((1 - t) * (1 - t)) * t * this.b.x
               + 3 * (1 - t) * (t * t) * this.c.x
               + (t * t * t) * this.d.x;
    },

    y: function (t) {
        return ((1 - t) * (1 - t) * (1 - t)) * this.a.y
               + 3 * ((1 - t) * (1 - t)) * t * this.b.y
               + 3 * (1 - t) * (t * t) * this.c.y
               + (t * t * t) * this.d.y;
    }
};

繰り返しますが、これtは曲線上で計算されます。

結果の時間

代替テキスト

今すぐ使用することmxmy、あなたが均等に分布し得るT曲線に:)

そんなに難しくなかったでしょう?繰り返しになりますが、単純な(完璧なソリューションではありませんが)ゲームで十分であることがわかります。

完全なコードを見たい場合は、https://gist.github.com/670236で利用できるGistがあります

最後に、船を加速する

したがって、残されているのは、曲線上の位置Tを見つけるためtに使用する位置をマッピングすることにより、船をその経路に沿って加速することです。

まず、運動方程式の 2つ、つまりut + 1/2at²(v - u) / t

実際のコードでは、次のようになります。

startSpeed = getStartingSpeedInPixels() // Note: pixels
endSpeed = getFinalSpeedInPixels() // Note: pixels
acceleration = (endSpeed - startSpeed) // since we scale to 0...1 we can leave out the division by 1 here
position = 0.5 * acceleration * t * t + startSpeed * t;

次に、以下を実行してスケールダウンし0...1ます。

maxPosition = 0.5 * acceleration + startSpeed;
newT = 1 / maxPosition * position;

これで、船は進路に沿ってスムーズに移動しています。

うまくいかない場合...

これを読んでいるとき、すべてがうまく動作しますが、gamedevチャットルームの誰かに問題を説明するときに、加速部分にいくつかの問題が最初にありました。

元の質問の写真をまだ忘れていない場合は、sそこに言及していますが、それs度単位の速度ですが、船はピクセル単位の経路に沿って移動し、その事実を忘れていました。したがって、この場合に必要なことは、度単位の変位をピクセル単位の変位に変換することでした。これはかなり簡単なことがわかりました。

function rotationToMovement(planetSize, rotationSpeed) {
    var r = shipAngle * Math.PI / 180;
    var rr = (shipAngle + rotationSpeed) * Math.PI / 180;
    var orbit = planetSize + shipOrbit;
    var dx = Math.cos(r) * orbit - Math.cos(rr) * orbit;
    var dy = Math.sin(r) * orbit - Math.sin(rr) * orbit;
    return Math.sqrt(dx * dx + dy * dy);
};

それですべてです!読んでくれてありがとう ;)


7
これは消化するのに時間がかかります。しかし、すごい、あなた自身の質問に対する驚くべき答え。
AttackingHobo

7
この答えに賛成するためだけにアカウントを作成しました
誰も

私の友人にいくつかのポイントがあります。魅力のように働いた。質問と回答の両方が賛成です。
ジェイス

2
「i」は0.05で乗算され、「len」は100に設定されました。これは「0-1」ではなく「0-5」にマッピングされます。
悪の活動

1
@EvilActivityええ、私も見ました。彼の元の長さは20だったはずで、0.05から0.01に変更するのを忘れていました。そのため、動的な「len」(真のアーク長に適応、または場合によっては正確に等しい)を用意し、「ステップ」を1 /「len」で計算します。私はこれがとても奇妙だと思っています。他の誰もこの数年間これを引き起こしていません!
ビルコツィアス

4

問題は、船が自然にその軌道をとらないことです。そのため、完全に機能していても、正しく見えません。

惑星間の滑らかな移行をシミュレートする場合は、実際にモデリングすることをお勧めします。重力と推力の2つの重要な力しかないため、方程式は非常に単純です。

定数を設定するだけです。P1、P2、船の質量

各ゲームティック(時間:t)で3つのことをしています

  1. 船上のp1と船上のp2の重力を計算し、結果のベクトルを推力ベクトルに追加します。

  2. ステップ1の新しい加速度に基づいて新しい速度を計算します

  3. 新しい速度に応じて船を動かします

大変な作業のように思えるかもしれませんが、数十行のコードで実行でき、非常に自然に見えます。

物理学に関する支援が必要な場合はお知らせください。


あなたが取る関数内でこれを行う方法を提供することができます場合、私はそれをテストし検討するかもしれないt:)
イヴォウェッツェル

-しかし、ゲームプログラミングでは、tを変数として使用しません。すでに船の新しいdxとdyを計算しているだけなので、基本的にはすでにパラメトリックな状況にいます。これは、2つの惑星(Flash)aharrisbooks.net/flash/fg2r12/twoPlanets.htmlを周回する例です 。Pythonでも同じことが言え
Two pi

2

javascriptで記述されたコード例を使用して、この問題の可能な解決策を説明する優れた記事を見つけました。これは、「tを調整する」値を正しい方向に向けることによって機能します。

代わりに、ドット分布の平均脚長d_avgは、等間隔のドットが生成する脚長とほぼ同じであるという事実を使用できます(この類似性は、nが増加するにつれて増加します)。実際の脚の長さdと平均の脚の長さd_avgの差d_errを計算する場合、各ポイントに対応する時間パラメーターtを調整して、この差を小さくすることができます。

この質問にはすでに多くのクールな答えがありますが、このソリューションは注目に値するものであることがわかりました。


1

この問題をどのように解決したかを説明した素晴らしいページをありがとう。私は深くメモリに制約されていたので、私はあなたとは多少異なることをしました:配列を構築しないか、バイナリ検索で正しい「セグメント」を検索する必要があります。これは、ベジエ曲線の一方の端から別の端に移動していることを常に知っているためです。したがって、単に「現在の」セグメントを覚えており、そのセグメントの境界を超えて次の計算を行う場合位置、次の(または前の)セグメント(移動方向に基づいて)を計算します。これは私のアプリケーションに非常によく機能します。私が解決しなければならなかった唯一の不具合は、いくつかの曲線では、セグメントのサイズが非常に小さく、ポイントする次のプロットが-まれに-現在のセグメントの前に複数のセグメントであったことでした「

これが意味をなすかどうかはわかりませんが、これは確かに助けになりました。


0

そのようなモデリングは奇妙であり、奇妙な非論理的な結果を生み出す可能性があります。特に、開始惑星の速度が本当に遅い場合。

推力で船をモデリングします。

船が出発惑星の最後の軌道に乗ったら、全力で加速します。

船が特定の距離内に到達したら、逆推力を使用して船を目標の惑星の軌道速度まで減速させます。

編集:ノードが軌道から出ようとしているときに、シミュレーション全体を一度に実行します。すべてのデータを送信するか、いくつかの動きベクトルを間隔を置いて送信し、それらの間を補間します。


問題は、これはすべてティックベースであり、中間位置がないことです。これはネットワーキングマルチプレイヤーゲームであり、フルゲームで600以上の船のすべてのポジションを送信すると、すべてのネットワーキングが停止します。tickOffsetを送信するイベントのみがあり、残りは現在のワールドティックとオフセットに基づいて計算されます。
イヴォウェッツェル

返信を編集しました。
AttackingHobo

0

私がそれを正しく理解していれば、あなたの問題は過剰に制約されています。

宇宙船をある時間tで軌道間の指定された経路に沿って移動させ、同時に同じ時間tで速度s1から速度s2に加速させたいと思います。残念ながら、これらの制約の両方を同時に満たす加速を(一般に)見つけることはできません。

問題を解決可能にするために、少しリラックスする必要があります。


2
それからリラックスする方法は?私が想像できるのは、ベジェパスのものにプラグインするTを変更することです。最初に0.5に遅くなり、その後1に速くなるように、何らかの方法でスケーリングする必要があります。そのため、船は元の速度から曲線の中央の固定速度まで減速し、この速度から最後の速度まで再び加速します。曲線の?
イヴォヴェ

1
宇宙船が元の速度から移動の中間点付近まで加速し、その後新しい軌道に減速すると、より現実的に見えると思います。
ガレス・リース

それでも私は全部に加速をプラグインする方法についてこだわっている、私は何とかTを変更する必要があります/
イヴォウェッツェル

0

この答えに出くわしたのは、ベジェ曲線を使用するsvgパスに沿ってポイントを均等に分散しようとしているからです。

MDNは非推奨であると言っていますが、を使用しpath.getPointAtLengthて正しい結果を得ることができます。https://developer.mozilla.org/en-US/docs/Web/API/SVGPathElement/getPointAtLength

現在、Chrome / Safari / Firefoxで動作し、IE / Edgeでも動作するはずですが、私はそれらを確認しませんでした2。


-1

受け入れられた解決策の問題

ベジエは指数関数であるため、曲線の異なる領域で異なる前進速度が予想されます。

Ivoのソリューションは、これらの初期指数サンプル間を線形補間するため、不正確さは、それらのデルタが最大になる(通常は3次)曲線の両端/中央に大きく偏ります。そのため、彼が示唆するようにサンプルレートが大幅に増加しない限り、エラーは明らかであり、特定のズームレベルでは常に明らかです。つまり、バイアスはそのアルゴリズムに固有のものです。ズームが無制限の場合があるベクターベースのグラフィックなどには適していません。NN

ガイド付きサンプリングによるバイアスの相殺

別の解決策は、ベジェ関数が生成する自然バイアスに対抗distanceしたt後、線形に再マッピングすることです。

これが理想的なものだと仮定すると:

curve length = 10

t      distance
0.2    2
0.4    4
0.6    6
0.8    8
1.0    10

しかし、これはベジェ位置関数から得られるものです。

t      distance
0.2    0.12
0.4    1.22
0.6    2.45
0.8    5.81
1.0    10.00

N採取されたサンプルを見ると、距離のデルタが最大である場所を確認し、隣接する2つの距離の中間で1ずつリサンプリング(「分割」)できNます。たとえば、t=0.9(最大のデルタの中間である)取得する:

0.8    5.81
0.9    7.39
1.0    10.00

セット全体の任意の2つの距離の最大デルタがminDistanceDelta、より具体的には、より具体的には、epsilonステップにマップする特定の距離から離れるまで、次の最大距離間隔でこのプロセスを繰り返しますt。その後、目的のtステップを対応するに線形にマッピングできますdistance。これにより、安価にアクセスできるハッシュテーブル/マップが生成され、その値を実行時にバイアスなしで急ぐことができます。

セットが大きくなるとN、これを繰り返すためのコストが増加するため、理想的には前処理としてこれを行います。N増加するたびに、2つの新しい結果の間隔をintervalsコレクションに追加し、置き換えられた古い単一の間隔を削除します。これは、2つに分割する次の最大間隔を見つけるために作業する構造です。intervals距離でソートしておくと、次の作業項目を最後からポップしたり、分割したりすることができるため、物事が簡単になります。

最終的には、私たちが理想的に望んでいたようなものになります。

epsilon: 0.01

t            distance
0.200417     2.00417
0.3998132    3.9998132
0.600703     6.00703
0.800001     8.00001
0.9995309    9.995309

我々はすべてのステップで推測を取っているので、私たちは、正確な正確な距離を取得することはできません24など私たちは望んでいたが、あなたがあなたのにマッピングできるように、これらの反復を繰り返して所望の距離値に近い十分に得るtバイアスによるものを排除し、公正な精度での手順ほぼ等距離サンプリングに。

その後、たとえばt=0.5、Ivoが答えで行っているように、つまり、上記の2つの最も近い値(3.9998132および6.00703)の間を補間することにより、取得できます。

結論

ほとんどの場合、Ivoのソリューションは適切に機能しますが、バイアスをすべてのコストで回避する必要がある場合は、distancesができるだけ均等に分散されていることを確認してから、に線形マッピングされtます。

毎回真ん中を分割するのではなく、確率的に分割を行うことができることに注意してください。たとえば、その最初の例の間隔をでt=0.827なくで分割する場合がありt=0.9ます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.