Comment lerp entre les valeurs qui bouclent (comme la teinte ou la rotation)?

25

exemple d'animation conjointe

Voir la démo

J'essaie de faire pivoter le joint en douceur autour du centre de la toile, vers l'angle du pointeur de la souris. Ce que j'ai fonctionne, mais je veux qu'il anime la distance la plus courte possible pour arriver à l'angle de la souris. Le problème se produit lorsque la valeur boucle sur la ligne horizontale ( 3.14et -3.14). Passez la souris sur cette zone pour voir comment la direction change et cela prend du temps.

Code pertinent

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

Comment puis-je le faire pivoter sur la distance la plus courte, même "à travers l'espace"?

jackrugile
la source
Utilisez modulo. : D en.wikipedia.org/wiki/Modular_arithmetic
Vaughan Hilts
1
@VaughanHilts Je ne sais pas comment je l'utiliserais dans ma situation. Pouvez-vous développer davantage?
jackrugile

Réponses:

11

Ce n'est pas toujours la meilleure méthode, et elle peut être plus coûteuse en termes de calcul (bien que cela dépende en fin de compte de la façon dont vous stockez vos données), mais je ferai valoir que le lerping des valeurs 2D fonctionne assez bien dans la majorité des cas. Au lieu de lerper un angle souhaité, vous pouvez lerp le vecteur de direction normalisé souhaité .

Un avantage de cette méthode par rapport à la méthode «choisissez l'itinéraire le plus court jusqu'à l'angle» est qu'elle fonctionne lorsque vous devez interpoler entre plus de deux valeurs.

Lorsque vous lerpez des valeurs de teinte, vous pouvez les remplacer huepar un [cos(hue), sin(hue)]vecteur.

Dans votre cas, lerpant la direction articulaire normalisée:

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

Le code peut être plus court si vous pouvez utiliser une classe vectorielle 2D. Par exemple:

// 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
la source
Merci, cela fonctionne très bien ici: codepen.io/jackrugile/pen/45c356f06f08ebea0e58daa4d06d204f Je comprends la plupart de ce que vous faites, mais pouvez-vous élaborer un peu plus sur la ligne 5 de votre code pendant la normalisation? Je ne sais pas exactement ce qui se passe là-bas dx /= len...
jackrugile
1
La division d'un vecteur par sa longueur est appelée normalisation . Il s'assure qu'il a la longueur 1. La len ? len : 1.0pièce évite juste une division par zéro, dans le cas rare où la souris est placée exactement à la position commune. Il aurait pu être écrit: if (len != 0) dx /= len;.
sam hocevar
-1. Cette réponse est loin d'être optimale dans la plupart des cas. Et si vous interpolez entre et 180°? Sous forme vectorielle: [1, 0]et [-1, 0]. Vecteurs interpoler vous donnera soit , 180°ou une division par 0 erreur, en cas de t=0.5.
Gustavo Maciel
@GustavoMaciel ce n'est pas «la plupart des cas», c'est un cas d'angle très spécifique qui ne se produit jamais dans la pratique. De plus, il n'y a pas de division par zéro, vérifiez le code.
sam hocevar
@GustavoMaciel ayant vérifié à nouveau le code, il est en fait extrêmement sûr et fonctionne exactement comme il devrait même dans le cas d'angle que vous décrivez.
sam hocevar
11

L'astuce consiste à se rappeler que les angles (au moins dans l'espace euclidien) sont périodiques de 2 * pi. Si la différence entre l'angle actuel et l'angle cible est trop grande (c'est-à-dire que le curseur a franchi la limite), ajustez simplement l'angle actuel en ajoutant ou en soustrayant 2 * pi en conséquence.

Dans ce cas, vous pouvez essayer ce qui suit: (Je n'ai jamais programmé en Javascript auparavant, alors pardonnez mon style de codage.)

  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;

EDIT : Dans cette implémentation, déplacer le curseur trop rapidement autour du centre de l'articulation le fait bouger. C'est le comportement recherché, car la vitesse angulaire du joint est toujours proportionnelle à dtheta. Si ce comportement n'est pas souhaité, le problème peut être facilement résolu en plaçant un capuchon sur l'accélération angulaire du joint.

Pour ce faire, nous devons suivre la vitesse de l'articulation et imposer une accélération maximale:

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

Ensuite, pour notre commodité, nous allons introduire une fonction d'écrêtage:

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

Maintenant, notre code de mouvement ressemble à ceci. Tout d'abord, nous calculons dthetacomme précédemment, en ajustant joint.anglesi nécessaire:

  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;

Ensuite, au lieu de déplacer le joint immédiatement, nous calculons une vitesse cible et l'utilisons clippour la forcer dans notre plage acceptable.

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

Cela produit un mouvement fluide, même lors du changement de direction, tout en effectuant des calculs dans une seule dimension. De plus, il permet de régler indépendamment la vitesse et l'accélération du joint. Voir la démo ici: http://codepen.io/anon/pen/HGnDF/

David Zhang
la source
Cette méthode se rapproche vraiment, mais si ma souris est trop rapide, elle commence à sauter légèrement dans le mauvais sens. Démo ici, faites-moi savoir si je n'ai pas implémenté cela correctement: codepen.io/jackrugile/pen/db40aee91e1c0b693346e6cec4546e98
jackrugile
1
Je ne sais pas ce que vous voulez dire en sautant légèrement dans le mauvais sens; pour moi, l'articulation se déplace toujours dans la bonne direction. Bien sûr, si vous déplacez votre souris trop rapidement autour du centre, vous surpassez l'articulation, et elle saccade alors qu'elle passe d'un mouvement dans une direction à l'autre. C'est le comportement recherché, car la vitesse de rotation est toujours proportionnelle à dtheta. Souhaitiez-vous que l'articulation prenne un certain élan?
David Zhang
Voir mon dernier montage pour un moyen d'éliminer le mouvement saccadé créé en dépassant l'articulation.
David Zhang
Hé, vous avez essayé votre lien de démonstration, mais il semble qu'il pointe vers mon lien d'origine. En avez-vous enregistré un nouveau? Si ce n'est pas bien, je devrais pouvoir l'implémenter plus tard ce soir et voir comment cela fonctionne. Je pense que ce que vous avez décrit dans votre montage est plus ce que je cherchais. Je vous répondrai, merci!
jackrugile
1
Oh, ouais, tu as raison. Désolé, mon erreur. Je vais arranger ça.
David Zhang
3

J'adore les autres réponses données. Très technique!

Si vous le souhaitez, j'ai une méthode très simple pour y parvenir. Nous prendrons des angles pour ces exemples. Le concept peut être extrapolé à d'autres types de valeur, tels que les couleurs.

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

Je viens de le créer dans le navigateur et je n'ai jamais été testé. J'espère avoir compris la logique du premier coup.

[Modifier] 02/06/2017 - Clarifié un peu la logique.

Commencez par calculer distanceForward et distanceBackwards et laissez les résultats s'étendre au-delà de la plage (0-360).

La normalisation des angles ramène ces valeurs dans la plage (0-360). Pour ce faire, vous ajoutez 360 jusqu'à ce que la valeur soit supérieure à zéro et soustrayez 360 tandis que la valeur est supérieure à 360. Les angles de début / fin résultants seront équivalents (-285 est le même que 75).

Vous trouverez ensuite le plus petit angle normalisé de distanceForward ou distanceBackward. distanceForward dans l'exemple devient 75, ce qui est inférieur à la valeur normalisée de distanceBackward (300).

Si distanceForward est le plus petit ET endAngle <startAngle, étendez endAngle au-delà de 360 ​​en ajoutant 360. (il devient 375 dans l'exemple).

Si distanceBackward est le plus petit ET endAngle> startAngle, étendez endAngle en dessous de 0 en soustrayant 360.

Vous devez maintenant passer de startAngle (300) au nouveau endAngle (375). Le moteur devrait ajuster automatiquement les valeurs supérieures à 360 en soustrayant 360 pour vous. Sinon, vous devrez lerp de 300 à 360, PUIS lerp de 0 à 15 si le moteur ne normalise pas les valeurs pour vous.

Doug.McFarlane
la source
Eh bien, bien que l' atan2idée de base soit simple, celle-ci pourrait économiser de l'espace et un peu de performance (pas besoin de stocker x et y, ni de calculer trop souvent sin, cos et atan2). Je pense qu'il vaut la peine d'étendre un peu la raison pour laquelle cette solution est correcte (c'est-à-dire choisir le chemin le plus court sur un cercle ou une sphère, tout comme SLERP le fait pour les quaterions). Cette question et les réponses devraient être placées dans le wiki de la communauté car c'est un problème très courant auquel la plupart des programmeurs de gameplay sont confrontés.
teodron
Très bonne réponse. J'aime aussi l'autre réponse donnée par @David Zhang. Cependant, je reçois toujours une gigue étrange en utilisant sa solution modifiée lorsque je fais un virage rapide. Mais votre réponse s'intègre parfaitement dans mon jeu. Je m'intéresse à la théorie mathématique derrière votre réponse. Bien que cela semble simple, il n'est pas évident de savoir pourquoi nous devrions comparer la distance angulaire normalisée de différentes directions.
newguy
Je suis content que mon code fonctionne. Comme mentionné, cela vient d'être tapé dans le navigateur, non testé dans un projet réel. Je ne me souviens même pas d'avoir répondu à cette question (il y a trois ans)! Mais, en y regardant de plus près, il semble que j'allais juste étendre la plage (0-360) pour permettre aux valeurs de test d'aller au-delà / avant cette plage afin de comparer la différence totale en degrés et de prendre la différence la plus courte. La normalisation ramène simplement ces valeurs dans la plage (0-360). Ainsi, distanceForward devient 75 (-285 + 360), ce qui est plus petit que distanceBackward (285), c'est donc la distance la plus courte.
Doug.McFarlane
Étant donné que distanceForward est la distance la plus courte, la première partie de la clause IF est utilisée. Puisque endAngle (15) est inférieur à startAngle (300), nous ajoutons 360 pour obtenir 375. Vous devrez donc passer de startAngle (300) au nouveau endAngle (375). Le moteur devrait ajuster automatiquement les valeurs supérieures à 360 en soustrayant 360 pour vous.
Doug.McFarlane
J'ai édité la réponse pour plus de clarté.
Doug.McFarlane