Wie kann ich zwischen Werten wechseln, die sich in einer Schleife befinden (z. B. Farbton oder Drehung)?

25

gemeinsames Animationsbeispiel

Demo ansehen

Ich versuche, das Gelenk gleichmäßig um die Mitte der Leinwand in Richtung des Mauszeigers zu drehen. Was ich habe, funktioniert, aber ich möchte, dass es die kürzest mögliche Entfernung animiert, um zum Mauswinkel zu gelangen. Das Problem tritt auf, wenn der Wert an der horizontalen Linie ( 3.14und -3.14) eine Schleife durchläuft . Fahren Sie mit der Maus über diesen Bereich, um zu sehen, wie sich die Richtung ändert, und es dauert den langen Weg zurück.

Relevanter Code

// 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 );

Wie kann ich ihn auf die kürzeste Distanz drehen lassen, sogar "über die Lücke"?

Jackrugile
quelle
Verwenden Sie Modulo. : D en.wikipedia.org/wiki/Modular_arithmetic
Vaughan Hilts
1
@VaughanHilts Ich bin mir nicht sicher, wie ich es in meiner Situation nutzen würde. Können Sie näher darauf eingehen?
Jackrugile

Antworten:

11

Es ist nicht immer die beste Methode und kann rechenintensiver sein (obwohl dies letztendlich davon abhängt, wie Sie Ihre Daten speichern), aber ich werde argumentieren, dass das Lerpen von 2D-Werten in den meisten Fällen recht gut funktioniert. Anstatt einen gewünschten Winkel zu verändern, können Sie den gewünschten normalisierten Richtungsvektor verändern.

Ein Vorteil dieser Methode gegenüber der Methode "Wählen Sie die kürzeste Route zum Winkel" ist, dass sie funktioniert, wenn Sie zwischen mehr als zwei Werten interpolieren müssen.

Wenn Sie Farbtonwerte einlesen, können Sie diese durch hueeinen [cos(hue), sin(hue)]Vektor ersetzen .

In Ihrem Fall unterbrechen Sie die normalisierte Gelenkrichtung:

// 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);

Der Code kann kürzer sein, wenn Sie eine 2D-Vektorklasse verwenden können. Zum Beispiel:

// 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);
sam hocevar
quelle
Vielen Dank, dies funktioniert hier hervorragend : codepen.io/jackrugile/pen/45c356f06f08ebea0e58daa4d06d204f Ich verstehe das meiste, was Sie tun, aber können Sie in Zeile 5 Ihres Codes während der Normalisierung etwas mehr ausführen? Nicht sicher, was genau dort passiert dx /= len...
Jackrugile
1
Das Teilen eines Vektors durch seine Länge wird als Normalisierung bezeichnet . Es stellt sicher, dass es Länge 1 hat. Das len ? len : 1.0Teil vermeidet nur eine Division durch Null, in dem seltenen Fall, dass die Maus genau an der Gelenkposition platziert ist. Es hätte geschrieben: if (len != 0) dx /= len;.
Sam Hocevar
-1. Diese Antwort ist in den meisten Fällen alles andere als optimal. Was ist, wenn Sie zwischen und interpolieren 180°? In vektorieller Form: [1, 0]und [-1, 0]. Interpolations - Vektoren werden Sie geben entweder , 180°oder eine Division durch 0 Fehler, bei t=0.5.
Gustavo Maciel
@GustavoMaciel das ist nicht "die meisten Fälle", das ist ein sehr spezieller Eckfall, der in der Praxis einfach nie vorkommt. Es gibt auch keine Division durch Null. Überprüfen Sie den Code.
Sam Hocevar
@GustavoMaciel Nachdem der Code noch einmal überprüft wurde, ist er tatsächlich extrem sicher und funktioniert genau so, wie er sollte, auch in dem von Ihnen beschriebenen Eckfall.
Sam Hocevar
11

Der Trick besteht darin, sich daran zu erinnern, dass die Winkel (zumindest im euklidischen Raum) um 2 * pi periodisch sind. Wenn der Unterschied zwischen dem aktuellen Winkel und dem Zielwinkel zu groß ist (dh der Cursor hat die Grenze überschritten), passen Sie den aktuellen Winkel einfach an, indem Sie 2 * pi entsprechend addieren oder subtrahieren.

In diesem Fall können Sie Folgendes versuchen: (Ich habe noch nie in Javascript programmiert. Verzeihen Sie also meinen Codierungsstil.)

  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;

BEARBEITEN : In dieser Implementierung ruckelt der Cursor, wenn er zu schnell um die Gelenkmitte bewegt wird. Dies ist das beabsichtigte Verhalten, da die Winkelgeschwindigkeit des Gelenks immer proportional ist dtheta. Wenn dieses Verhalten unerwünscht ist, kann das Problem leicht behoben werden, indem eine Kappe auf die Winkelbeschleunigung des Gelenks aufgesetzt wird.

Dazu müssen wir die Geschwindigkeit des Gelenks verfolgen und eine maximale Beschleunigung auferlegen:

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

Aus praktischen Gründen führen wir eine Clipping-Funktion ein:

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

Unser Bewegungscode sieht nun so aus. Zuerst berechnen wir dthetawie zuvor und passen joint.anglenach Bedarf an:

  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;

Anstatt das Gelenk sofort zu bewegen, berechnen wir eine Zielgeschwindigkeit und cliperzwingen sie innerhalb unseres akzeptablen Bereichs.

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

Dies erzeugt eine gleichmäßige Bewegung, selbst wenn die Richtung geändert wird, während Berechnungen in nur einer Dimension ausgeführt werden. Darüber hinaus können Geschwindigkeit und Beschleunigung des Gelenks unabhängig voneinander eingestellt werden. Sehen Sie die Demo hier: http://codepen.io/anon/pen/HGnDF/

David Zhang
quelle
Diese Methode kommt mir sehr nahe, aber wenn ich meine Maus zu schnell bewege, beginnt sie leicht in die falsche Richtung zu springen. Demo hier, lassen Sie mich wissen, wenn ich das nicht richtig implementiert habe: codepen.io/jackrugile/pen/db40aee91e1c0b693346e6cec4546e98
jackrugile
1
Ich bin mir nicht sicher, was du meinst, wenn du leicht in die falsche Richtung springst. bei mir bewegt sich das gelenk immer in die richtige richtung. Wenn Sie die Maus zu schnell in der Mitte bewegen, läuft das Gelenk aus und ruckelt, wenn es von einer Richtung in die andere wechselt. Dies ist das beabsichtigte Verhalten, da die Drehzahl immer proportional zu ist dtheta. Wollten Sie, dass das Joint etwas Schwung bekommt?
David Zhang
Sehen Sie sich meine letzte Bearbeitung an, um die ruckartige Bewegung zu beseitigen, die durch das Auslaufen des Gelenks entsteht.
David Zhang
Hey, habe deinen Demo-Link ausprobiert, aber er scheint nur auf meinen Original-Link zu verweisen. Hast du einen neuen gespeichert? Wenn das nicht in Ordnung ist, sollte ich dies später in der Lage sein, zu sehen, wie es funktioniert. Ich denke, was Sie in Ihrer Bearbeitung beschrieben haben, ist eher das, wonach ich gesucht habe. Wir melden uns bei Ihnen, danke!
Jackrugile
1
Oh ja, du hast recht. Entschuldigung, mein Fehler. Ich werde das reparieren.
David Zhang
3

Ich liebe die anderen Antworten. Sehr technisch!

Wenn Sie möchten, habe ich eine sehr einfache Methode, um dies zu erreichen. Für diese Beispiele nehmen wir Winkel an. Das Konzept kann auf andere Werttypen, z. B. Farben, extrapoliert werden.

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

Ich habe dies gerade im Browser erstellt und wurde noch nie getestet. Ich hoffe, ich habe die Logik beim ersten Versuch richtig verstanden.

[Bearbeiten] 02.06.2017 - Die Logik wurde ein wenig geklärt.

Beginnen Sie mit der Berechnung von distanceForward und distanceBackwards und lassen Sie die Ergebnisse über den Bereich (0-360) hinausgehen.

Durch das Normalisieren der Winkel werden diese Werte wieder in den Bereich von (0-360) gebracht. Dazu addieren Sie 360, bis der Wert über Null liegt, und subtrahieren 360, während der Wert über 360 liegt. Die resultierenden Start- / Endwinkel entsprechen dem Äquivalent (-285 entspricht 75).

Als nächstes finden Sie den kleinsten normalisierten Winkel von distanceForward oder distanceBackward. distanceForward im Beispiel wird zu 75, was kleiner als der normalisierte Wert von distanceBackward (300) ist.

Wenn distanceForward das kleinste AND endAngle <startAngle ist, erweitern Sie endAngle über 360 hinaus, indem Sie 360 ​​hinzufügen (im Beispiel wird es 375).

Wenn distanceBackward das kleinste AND endAngle> startAngle ist, erweitern Sie endAngle auf unter 0, indem Sie 360 ​​subtrahieren.

Sie würden jetzt von startAngle (300) zu dem neuen endAngle (375) springen. Der Motor sollte automatisch Werte über 360 anpassen, indem er 360 für Sie subtrahiert. Andernfalls müssten Sie von 300 bis 360 lerpen, DANN von 0 bis 15 lerpen, wenn die Engine die Werte für Sie nicht normalisiert.

Doug.McFarlane
quelle
Nun, obwohl die zugrunde liegende atan2Idee einfach ist, könnte diese etwas Platz und ein bisschen Leistung sparen (keine Notwendigkeit, x und y zu speichern oder sin, cos und atan2 zu oft zu berechnen). Ich denke, es lohnt sich, ein wenig darüber nachzudenken, warum diese Lösung richtig ist (dh den kürzesten Weg auf einem Kreis oder einer Kugel zu wählen, genau wie es SLERP für Quaterions tut). Diese Frage und die Antworten sollten in das Community-Wiki gestellt werden, da dies ein sehr häufiges Problem ist, mit dem die meisten Gameplay-Programmierer konfrontiert sind.
Teodron
Gute Antwort. Mir gefällt auch die andere Antwort von @David Zhang. Allerdings bekomme ich immer einen seltsamen Jitter, wenn ich seine bearbeitete Lösung benutze, wenn ich eine schnelle Kurve mache. Aber deine Antwort passt perfekt zu meinem Spiel. Ich interessiere mich für die mathematische Theorie hinter Ihrer Antwort. Es sieht zwar einfach aus, aber es ist nicht klar, warum wir den normalisierten Winkelabstand verschiedener Richtungen vergleichen sollten.
Neuzugang
Ich bin froh, dass mein Code funktioniert. Wie bereits erwähnt, wurde dies nur in den Browser eingegeben und nicht in einem tatsächlichen Projekt getestet. Ich kann mich nicht einmal erinnern, diese Frage beantwortet zu haben (vor drei Jahren)! Aber wenn ich es mir anschaue, sieht es so aus, als würde ich nur den Bereich (0-360) erweitern, damit die Testwerte über / vor diesen Bereich hinausgehen können, um die Gesamtgraddifferenz zu vergleichen und die kürzeste Differenz zu ermitteln. Durch das Normalisieren werden diese Werte wieder in den Bereich von (0-360) gebracht. DistanceForward wird also zu 75 (-285 + 360), was kleiner ist als distanceBackward (285), es ist also die kürzeste Entfernung.
Doug.McFarlane
Da distanceForward die kürzeste Entfernung ist, wird der erste Teil der IF-Klausel verwendet. Da endAngle (15) kleiner als startAngle (300) ist, addieren wir 360, um 375 zu erhalten. Sie würden also von startAngle (300) zu dem neuen endAngle (375) springen. Der Motor sollte automatisch Werte über 360 anpassen, indem er 360 für Sie subtrahiert.
Doug.McFarlane
Ich habe die Antwort bearbeitet, um mehr Klarheit zu schaffen.
Doug.McFarlane