ループする値(色相や回転など)を切り替えるにはどうすればよいですか?


25

共同アニメーションの例

デモを見る

ジョイントをキャンバスの中心の周りで、マウスポインターの角度に向かってスムーズに回転させようとしています。私が持っているものは動作しますが、マウスの角度に到達するために可能な限り最短距離をアニメーション化したいです。この問題は、値が水平線(3.14および-3.14)でループするときに発生します。その領域にカーソルを合わせて、方向がどのように切り替わるかを確認します。

関連コード

// ease the current angle to the target angle
joint.angle += ( joint.targetAngle - joint.angle ) * 0.1;

// get angle from joint to mouse
var dx = e.clientX - joint.x,
    dy = e.clientY - joint.y;  
joint.targetAngle = Math.atan2( dy, dx );

「ギャップを越えて」最短距離で回転させるにはどうすればよいですか?



1
@VaughanHilts自分の状況でどう使うかわからない。さらに詳しく説明していただけますか?
ジャッキージャイル2014年

回答:


11

これは常に最良の方法ではなく、計算コストが高くなる可能性があります(これは最終的にデータの保存方法に依存します)が、ほとんどの場合、2D値を正しく処理することは合理的であるという議論をします。目的の角度を変化させる代わりに、目的の正規化された方向ベクトルを変化させることができます。

この方法の「角度への最短ルートを選択する」方法に対する利点の1つは、3つ以上の値の間を補間する必要がある場合に機能することです。

色相値を変更する場合hue[cos(hue), sin(hue)]ベクトルに置き換えることができます。

あなたの場合、正規化されたジョイント方向を繰り返します:

// get normalised direction from joint to mouse
var dx = e.clientX - joint.x,
    dy = e.clientY - joint.y;
var len = Math.sqrt(dx * dx + dy * dy);
dx /= len ? len : 1.0; dy /= len ? len : 1.0;
// get current direction
var dirx = cos(joint.angle),
    diry = sin(joint.angle);
// ease the current direction to the target direction
dirx += (dx - dirx) * 0.1;
diry += (dy - diry) * 0.1;

joint.angle = Math.atan2(diry, dirx);

2Dベクトルクラスを使用できる場合は、コードを短くすることができます。例えば:

// get normalised direction from joint to mouse
var desired_dir = normalize(vec2(e.clientX, e.clientY) - joint);
// get current direction
var current_dir = vec2(cos(joint.angle), sin(joint.angle));
// ease the current direction to the target direction
current_dir += (desired_dir - current_dir) * 0.1;

joint.angle = Math.atan2(current_dir.y, current_dir.x);

ありがとうございます、これはここでうまく機能しています:codepen.io/jackrugile/pen/45c356f06f08ebea0e58daa4d06d204fあなたがやっていることのほとんどを理解していますが、正規化中にコードの5行目についてもう少し詳しく説明できますか?わからないまさにそこに何が起こっているのかdx /= len...
jackrugile

1
ベクトルをその長さで割ることを正規化と呼びます。これにより、長さ1が確保されます。len ? len : 1.0パーツは、ゼロによる除算を回避しますが、まれなケースでは、マウスがジョイント位置に正確に配置されます。次のように記述できますif (len != 0) dx /= len;
サムホセバー14年

-1。ほとんどの場合、この答えは最適とはほど遠いです。との間を補間する場合はどうなり180°ますか?ベクトル形式:[1, 0]および[-1, 0]。補間ベクトルはどちらかを与える180°またはの場合は0除算エラー、t=0.5
グスタボマシエル

「ほとんどのケース」ではない@GustavoMacielは、実際には決して発生しない非常に特殊なコーナーケースの1つです。また、ゼロによる除算はありません。コードを確認してください。
サムホセバー

@GustavoMacielはコードを再度チェックしましたが、実際には非常に安全であり、説明するような場合でも正確に動作します。
サムホセバー

11

トリックは、(少なくともユークリッド空間では)角度が2 * piで周期的であることを覚えておくことです。現在の角度とターゲット角度の差が大きすぎる場合(つまり、カーソルが境界を越えた場合)、それに応じて2 * piを加算または減算して現在の角度を調整します。

この場合、次のことを試すことができます:(以前JavaScriptでプログラミングしたことがないので、コーディングスタイルを許してください。)

  var dtheta = joint.targetAngle - joint.angle;
  if (dtheta > Math.PI) joint.angle += 2*Math.PI;
  else if (dtheta < -Math.PI) joint.angle -= 2*Math.PI;
  joint.angle += ( joint.targetAngle - joint.angle ) * joint.easing;

編集:この実装では、カーソルをジョイントの中心の周りであまりにも速く動かすと、ジャークが発生します。ジョイントの角速度は常にに比例するため、これは意図した動作dthetaです。この動作が望ましくない場合は、ジョイントの角加速度にキャップを付けることで問題を簡単に修正できます。

これを行うには、ジョイントの速度を追跡し、最大加速度を課す必要があります。

  joint = {
    // snip
    velocity: 0,
    maxAccel: 0.01
  },

次に、便宜上、クリッピング関数を導入します。

function clip(x, min, max) {
  return x < min ? min : x > max ? max : x
}

さて、移動コードは次のようになります。まず、必要に応じてdtheta調整joint.angleし、以前と同様に計算します。

  var dtheta = joint.targetAngle - joint.angle;
  if (dtheta > Math.PI) joint.angle += 2*Math.PI;
  else if (dtheta < -Math.PI) joint.angle -= 2*Math.PI;

次に、ジョイントをすぐに移動する代わりに、ターゲット速度を計算し、clipそれを使用して許容範囲内に強制します。

  var targetVel = ( joint.targetAngle - joint.angle ) * joint.easing;
  joint.velocity = clip(targetVel,
                        joint.velocity - joint.maxAccel,
                        joint.velocity + joint.maxAccel);
  joint.angle += joint.velocity;

これにより、1次元のみで計算を実行しながら、方向を切り替えてもスムーズなモーションが生成されます。さらに、ジョイントの速度と加速度を個別に調整できます。こちらのデモをご覧ください:http : //codepen.io/anon/pen/HGnDF/


この方法は非常に近いものですが、マウスの速度が速すぎると、少し間違った方向にジャンプし始めます。ここでデモを行い、正しく実装しなかった場合は教えてください:codepen.io/jackrugile/pen/db40aee91e1c0b693346e6cec4546e98
jackrugile

1
少し間違った方法でジャンプした場合の意味がわかりません。私にとって、関節は常に正しい方向に動きます。もちろん、マウスを中心の周りであまりにも速く動かすと、ジョイントを使い果たしてしまい、一方向から他の方向への移動に切り替わるときに、動きがぎくしゃくします。回転速度は常にに比例するため、これは意図した動作dthetaです。ジョイントに何らかの勢いを持たせるつもりでしたか?
デビッドチャン14年

ジョイントのアウトランによって生じるぎくしゃくした動きをなくす方法については、最後の編集を参照してください。
デビッドチャン14年

デモリンクを試してみましたが、元のリンクを指しているようです。新しいものを保存しましたか?そうでない場合は、今夜遅くに実装して、どのように機能するかを確認する必要があります。編集であなたが説明したことは、私が探していたものよりも多いと思います。返信します、ありがとう!
ジャッキージャイル2014年

1
ああ、そう、あなたは正しい。すみません、私の間違いです。それを修正します。
デビッドチャン14年

3

与えられた他の答えが大好きです。非常に技術的です!

必要に応じて、これを実現する非常に簡単な方法があります。これらの例では角度を想定します。この概念は、色などの他の値タイプに外挿できます。

double MAX_ANGLE = 360.0;
double startAngle = 300.0;
double endAngle = 15.0;
double distanceForward = 0.0;  // Clockwise
double distanceBackward = 0.0; // Counter-Clockwise

// Calculate both distances, forward and backward:
distanceForward = endAngle - startAngle;    // -285.00
distanceBackward = startAngle - endAngle;   // +285.00

// Which direction is shortest?
// Forward? (normalized to 75)
if (NormalizeAngle(distanceForward) < NormalizeAngle(distanceBackward)) {
    // Adjust for 360/0 degree wrap
    if (endAngle < startAngle) endAngle += MAX_ANGLE; // Will be above 360
}

// Backward? (normalized to 285)
else {
    // Adjust for 360/0 degree wrap
    if (endAngle > startAngle) endAngle -= MAX_ANGLE; // Will be below 0
}

// Now, Lerp between startAngle and endAngle. 

// EndAngle can be above 360 if wrapping clockwise past 0, or
// EndAngle can be below 0 if wrapping counter-clockwise before 0.
// Normalize each returned Lerp value to bring angle in range of 0 to 360 if required.  Most engines will automatically do this for you.


double NormalizeAngle(double angle) {
    while (angle < 0) 
        angle += MAX_ANGLE;
    while (angle >= MAX_ANGLE) 
        angle -= MAX_ANGLE;
    return angle;
}

ブラウザでこれを作成したばかりで、テストされたことはありません。最初の試行でロジックが正しくなったことを願っています。

[編集] 2017/06/02-ロジックを少し明確にしました。

distanceForwardとdistanceBackwardsを計算することから始めて、結果が範囲(0〜360)を超えるようにします。

角度を正規化すると、これらの値は(0〜360)の範囲に戻ります。これを行うには、値がゼロを超えるまで360を追加し、値が360を超える間360を減算します。結果の開始/終了角度は同等になります(-285は75と同じです)。

次に、distanceForwardまたはdistanceBackwardの最小の正規化角度を見つけます。この例のdistanceForwardは75になり、distanceBackwardの正規化された値(300)よりも小さくなります。

distanceForwardが最小のAND endAngle <startAngleである場合、360を追加してendAngleを360を超えて拡張します(例では375になります)。

distanceBackwardが最小AND endAngle> startAngleの場合、360を減算してendAngleを0未満に拡張します。

startAngle(300)から新しいendAngle(375)に移行します。エンジンは、360を差し引くことで360を超える値を自動的に調整する必要があります。そうしないと、エンジンが値を正規化しない場合、300から360に変更し、0から15に変更する必要があります。


けれどもまあ、atan2ベースの考え方は簡単です、これはいくつかのスペースと性能のビット(あまりにも頻繁に店のxとyに、また計算罪、COSおよびATAN2する必要はありません)を救うことができます。この解決策が正しい理由について少し説明する価値があると思います(つまり、SLERPが四元数に対して行うように、円または球上の最短経路を選択する)。これはほとんどのゲームプレイプログラマが直面する非常に一般的な問題であるため、この質問と回答はコミュニティwikiに入れてください。
テオドロン14年

素晴らしい答え。また、@ David Zhangによる他の回答も気に入っています。しかし、私が彼の編集したソリューションを使用すると、私が迅速なターンをするときにいつも奇妙な不安を感じます。しかし、あなたの答えは私のゲームにぴったりです。あなたの答えの背後にある数学理論に興味があります。単純に見えますが、異なる方向の正規化された角度距離を比較する必要がある理由は明らかではありません。
ニューガイ

私のコードが機能することを嬉しく思います。前述のとおり、これはブラウザに入力されただけで、実際のプロジェクトではテストされていません。(3年前)この質問に答えたことさえ覚えていません!しかし、それを見ると、範囲(0-360)を拡張して、合計値の差を比較し、最短の差を取るために、テスト値がこの範囲を超えて/この範囲の前になるように見えます。正規化すると、それらの値は再び(0-360)の範囲になります。そのため、distanceForwardは75(-285 + 360)になり、これはdistanceBackward(285)よりも小さいため、最短距離になります。
ダグ。マクファーレン

distanceForwardは最短距離であるため、IF句の最初の部分が使用されます。endAngle(15)はstartAngle(300)よりも小さいので、360を追加して375を取得します。したがって、startAngle(300)から新しいendAngle(375)に移行します。エンジンは、360を差し引くことで360を超える値を自動的に調整する必要があります。
ダグ。マクファーレン

答えを編集して、より明確にしました。
ダグ。マクファーレン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.