web-dev-qa-db-ja.com

回転補間

注意:この質問は、単純さ、ラジアン、度、さまざまなゼロ方位のために度で示します。問題は基本的に同じです。

回転補間の背後にあるコードについて誰かが何かアイデアを持っていますか?線形補間関数が与えられた場合:Lerp(from、to、amount)、ここでamountは0 ... 1であり、fromとtoの間の値をamountで返します。この同じ関数を0度から360度の間の回転補間にどのように適用できますか?度を0と360の外に戻してはいけません。

度のこの単位円を考えると:

Unit Circle

ここで、from = 45からto = 315の場合、アルゴリズムは角度への最短経路をたどる必要があります。つまり、アルゴリズムはゼロを通過して360に到達し、次に315に到達する必要があります。90、180、270から315までは到達しません。

これを達成するための良い方法はありますか?それとも、if()ブロックの恐ろしい混乱になるのでしょうか?私はこれを行うためのいくつかのよく理解された標準的な方法を逃していますか?どんな助けでもいただければ幸いです。

29
Rob

私はこれが2歳であることを知っていますが、最近同じ問題を探していて、ここにifが投稿されていないとエレガントな解決策が見当たらないので、ここに行きます:

    shortest_angle=((((end - start) % 360) + 540) % 360) - 180;
    return start + (shortest_angle * amount) % 360;

それでおしまい

ps:もちろん、%はモジュロを意味し、shortest_angleは補間角度全体を保持する変数です。

33
user151496

申し訳ありませんが、それは少し複雑でした、ここにもっと簡潔なバージョンがあります:

    public static float LerpDegrees(float start, float end, float amount)
    {
        float difference = Math.Abs(end - start);
        if (difference > 180)
        {
            // We need to add on to one of the values.
            if (end > start)
            {
                // We'll add it on to start...
                start += 360;
            }
            else
            {
                // Add it on to end.
                end += 360;
            }
        }

        // Interpolate it.
        float value = (start + ((end - start) * amount));

        // Wrap it..
        float rangeZero = 360;

        if (value >= 0 && value <= 360)
            return value;

        return (value % rangeZero);
    }

より最適化されたバージョンを入手した人はいますか?

13
Rob

乗法で定義される形に悩まされないので、sinとcosを補間するのがより良いアプローチだと思います。 w = 0が角度A、w = 1が角度Bになるように、w = "amount"とします。

CS = (1-w)*cos(A) + w*cos(B);
SN = (1-w)*sin(A) + w*sin(B);
C = atan2(SN,CS);

必要に応じてラジアンと度に変換する必要があります。また、ブランチを調整する必要があります。 atan2の場合、Cは-piからpiの範囲に戻ります。 0から2piが必要な場合は、Cにpiを追加するだけです。

7
Paul Colby

注意:C#コードを使用する

私の脳の周りを狂ったようにうろついた後、これが私が思いついたものです。基本的に前提は、最後の最後に0-360ラッピングを実行することです。 0〜360以外の値を内部的に処理し、関数から値が要求された時点で0〜360内にラップします。

開始点と終了点を選択した時点で、次の操作を実行します。

float difference = Math.Abs(end - start);
if (difference > 180)
{
    // We need to add on to one of the values.
    if (end > start)
    {
        // We'll add it on to start...
        start += 360;
    }
    else
    {
        // Add it on to end.
        end += 360;
    }
}

これにより、実際の開始値と終了値が得られます。これは、0〜360の範囲外である可能性があります。

値が0から360の間であることを保証するためのラップ関数があります...

public static float Wrap(float value, float lower, float upper)
{
    float rangeZero = upper - lower;

    if (value >= lower && value <= upper)
        return value;

    return (value % rangeZero) + lower;
}

次に、関数に現在の値を要求した時点で、次のようにします。

return Wrap(Lerp(start, end, amount), 0, 360);

これはほぼ間違いなく問題の最適な解決策ではありませんが、一貫して機能しているように見えます。誰かがこれを行うためのより最適な方法を持っているなら、それは素晴らしいことです。

3
Rob

質問への回答をよりよく説明するために、回答を書き直したかったのです。数式にはExcelを使用し、単位には学位を使用しています。

簡単にするために、Bは2つの値のうち大きい方であり、Aは2つの値のうち小さい方です。後でソリューションでそれぞれMAX()MIN()を使用できます。

パート1-どちらに行くのですか?

最初に実行したいのは、時計回りまたは反時計回りのどちらの方向で計算を実行するかを決定することです。そのためにIF()ステートメントを使用します。

_IF( (B-A)<=180, (Clockwise_Formula), (AntiClockwise_Formula) )
_

上記の式は、反時計回りにBからAに移動する(これは、時計回りにAからBに移動するのと同じです)が以下であるかどうかを確認します。 180度。そうでない場合は、反対方向に進む方が短くなります。

これが機能することを確認するには:90-45 = 45(180以下)はIFステートメントをTRUEにするため、時計回りの方向は短くなりますが、315-45 = 270(180より大きい)はifステートメントになります。 FALSEなので、反時計回りの式は短くなります。

パート2-時計回りの式

ここで、時計回りまたは反時計回りに、NAの間でB回を補間します。時計回りの式は比較的単純です。

_Clockwise_Formula: ((B-A)/N*S)+A
_

ここで、Sは、1から始まりN-1で終わる補間の数のカウントです(_S = N_の場合、答えはBになります)

例:A = 90、B = 270、N = 4

_S=1:     ((270-90)/4*1)+90 = 135
S=2:     ((270-90)/4*2)+90 = 180
S=3:     ((270-90)/4*3)+90 = 225
_

パート3-ANITCLOCKWISE式

360度の角度で反時計回りに交差する必要があるため、反時計回りの式はもう少し複雑になります。私が考えることができる最も簡単な方法は、360をAに追加してから、MOD(FORMULA,VALUE)関数を使用して答えを360で変調することです。

また、Aが最小数になっているため、数式内でBBを入れ替える必要があります。 (少し紛らわしいように聞こえるかもしれませんが、機能します!)

_(Unmodulated) AntiClockwise_Formula: (((A+360)-B)/N*S)+B
_

例:A = 60、B = 300、N = 4

_S=1:     (((60+360)-300)/4*1)+300 = 330
S=2:     (((60+360)-300)/4*2)+300 = 360
S=3:     (((60+360)-300)/4*3)+300 = 390
_

パート4-0と360の間の回答を制限する

答えが360を超えることがある(常にではない)ことを確認してください。ここで、Anticlockwise_formulaをMOD()関数でラップします。

_AntiClockwise_Formula: MOD((((A+360)-B)/N*S)+B,360)
_

パート3で使用した例を変調すると、次のようになります。

_S=1:     330
S=2:     0
S=3:     30
_

パート5-すべてをまとめる

パート1〜4のすべての要素を組み合わせると、答えは次のようになります。

_IF((B-A)<=180,((B-A)/N*S)+A,MOD((((A+360)-B)/N*S)+B,360))
_

どこ:

A = 2つの値のうち小さい方(AをMIN()に置き換えることができます)

B = 2つの値の大きい方(BをMAX()に置き換えることができます)

N =実行する補間の数(たとえば、2は半分、3は3分の1など)

S =最大N-1までの増分カウント(説明についてはパート2を参照)

2
Adam

角度を処理するための私の好ましい方法は、1回転あたり2の累乗である単位を使用することです。たとえば、16ビットの符号付き整数を使用して-180〜 + 180度を表す場合、(from-to)/ num_stepsを使用して補間を行うことができます。 360から0に移動した時点でバイナリ値がオーバーフローするため、角度の加算と減算は常に機能します。

あなたの場合におそらくやりたいことは、360を法とする数学です。したがって、角度の差は(from-to)%360として計算されます。他のSOの質問で対処されたものには、まだいくつかの兆候の問題があります。

1
phkahler

学位を取得するための私の解決策。私のVarTrackerクラスで

    @classmethod
def shortest_angle(cls, start: float, end: float, amount: float):
    """ Find shortest angle change around circle from start to end, the return
        fractional part by amount.
    VarTracker.shortest_angle(10, 30, 0.1) --> 2.0
    VarTracker.shortest_angle(30, 10, 0.1) --> -2.0
    VarTracker.shortest_angle(350, 30, 0.1) --> 4.0
    VarTracker.shortest_angle(350, 30, 0.8) --> 32.0
    VarTracker.shortest_angle(30, 350, 0.5) --> -20.0
    VarTracker.shortest_angle(170, 190, 0.1) --> 2.0
    VarTracker.shortest_angle(10, 310, 0.5) --> -30.0
    """
    sa = ((((end - start) % 360) + 540) % 360) - 180;
    return sa * amount;

@classmethod
def slerp(cls, current: float, target: float, amount: float):
    """ Return the new value if spherical linear interpolation from current toward target, by amount, all in degrees.
    This method uses abs(amount) so sign of amount is ignored.
    current and target determine the direction of the lerp.
    Wraps around 360 to 0 correctly.

    Lerp from 10 degrees toward 30 degrees by 3 degrees
    VarTracker.slerp(10, 30, 3.0) --> 13.0
    Ignores sign of amount
    VarTracker.slerp(10, 30, -3.0) --> 13.0
    VarTracker.slerp(30, 10, 3.0) --> 27.0
    Wraps around 360 correctly
    VarTracker.slerp(350, 30, 6) --> 356.0
    VarTracker.slerp(350, 30, 12) --> 2.0
    VarTracker.slerp(30, 350, -35) --> 355.0
    a = VarTracker.slerp(30, 3140, -35) --> 355.0
    VarTracker.slerp(170, 190, 2) --> 172.0
    VarTracker.slerp(10, 310, 12) --> 358.0
    Wraps over 0 degrees correctly
    VarTracker.slerp(-10, 10, 3) --> 353.0
    VarTracker.slerp(10, -10, 12) --> 358
    """
    a = VarTracker.shortest_angle(current, target, 1.0)
    diff = target - current
    if np.abs(amount) > np.abs(diff):
        amount = diff
    if a < 0:
        amount = -np.abs(amount)
    else:
        amount = np.abs(amount)
    ret = current + amount
    while ret < 0:
        ret = ret + 360
    ret = ret % 360
    return ret
0
Richard Keene

User151496の回答の変更(元の回答は度数であり、間違った出力も表示されていました):

 def interp_angle(theta_1, theta_2, ratio):
    shortest_angle = ((((theta_2 - theta_1) % (np.pi*2)) + np.pi) % (np.pi*2)) - np.pi
    return (theta_1 + shortest_angle * ratio) % (np.pi*2)

テスト:で実行

theta1, theta2 = 0, 0.5
print('Average of {:.4g}pi rad and {:.4g}pi rad = {:.4g}pi rad'.format(theta1, theta2, interp_angle(theta1*np.pi, theta2*np.pi, 0.5)/np.pi))
theta1, theta2 = 0, 0.99
print('Average of {:.4g}pi rad and {:.4g}pi rad = {:.4g}pi rad'.format(theta1, theta2, interp_angle(theta1*np.pi, theta2*np.pi, 0.5)/np.pi))
theta1, theta2 = 0, 1.01
print('Average of {:.4g}pi rad and {:.4g}pi rad = {:.4g}pi rad'.format(theta1, theta2, interp_angle(theta1*np.pi, theta2*np.pi, 0.5)/np.pi))
theta1, theta2 = 0.1, -0.1
print('Average of {:.4g}pi rad and {:.4g}pi rad = {:.4g}pi rad'.format(theta1, theta2, interp_angle(theta1*np.pi, theta2*np.pi, 0.5)/np.pi))
theta1, theta2 = 0.1, 2-0.1
print('Average of {:.4g}pi rad and {:.4g}pi rad = {:.4g}pi rad'.format(theta1, theta2, interp_angle(theta1*np.pi, theta2*np.pi, 0.5)/np.pi))

私に与える:

Average of 0pi rad and 0.5pi rad = 0.25pi rad
Average of 0pi rad and 0.99pi rad = 0.495pi rad
Average of 0pi rad and 1.01pi rad = 1.505pi rad
Average of 0.1pi rad and -0.1pi rad = 0pi rad
Average of 0.1pi rad and 1.9pi rad = 0pi rad
0
Peter