Come posso passare da un valore all'altro (ad esempio tonalità o rotazione)?


25

esempio di animazione congiunta

Visualizza la demo

Sto cercando di far ruotare il giunto attorno al centro della tela, verso l'angolo del puntatore del mouse. Quello che ho funziona, ma voglio che animi la distanza più breve possibile per arrivare all'angolo del mouse. Il problema si verifica quando il valore gira attorno alla linea orizzontale ( 3.14e -3.14). Passa il mouse su quell'area per vedere come cambia la direzione e riprende la lunga strada da percorrere.

Codice rilevante

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

Come posso farlo ruotare la distanza più breve, anche "attraverso lo spazio"?



1
@VaughanHilts Non sono sicuro di come userei la mia situazione. Puoi approfondire ulteriormente?
jackrugile,

Risposte:


11

Non è sempre il metodo migliore e può essere più costoso dal punto di vista computazionale (anche se ciò dipende in ultima analisi dal modo in cui memorizzi i tuoi dati), ma farò l'argomento secondo cui lerping valori 2D funziona abbastanza bene nella maggior parte dei casi. Invece di modificare l'angolazione desiderata, è possibile modificare il vettore di direzione normalizzato desiderato .

Un vantaggio di questo metodo rispetto al metodo "scegli la strada più breve per l'angolo" è che funziona quando è necessario interpolare tra più di due valori.

Quando si inseriscono valori di tonalità, è possibile sostituirli huecon un [cos(hue), sin(hue)]vettore.

Nel tuo caso, seguendo la direzione del giunto normalizzata:

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

Il codice può essere più breve se è possibile utilizzare una classe vettoriale 2D. Per esempio:

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

Grazie, qui funziona alla grande: codepen.io/jackrugile/pen/45c356f06f08ebea0e58daa4d06d204f Capisco la maggior parte di ciò che stai facendo, ma puoi elaborare un po 'di più sulla riga 5 del tuo codice durante la normalizzazione? Non sono sicuro di cosa stia succedendo esattamente lì dx /= len...
Jackrugile,

1
Dividere un vettore per la sua lunghezza è chiamato normalizzazione . Assicura che abbia lunghezza 1. La len ? len : 1.0parte evita semplicemente una divisione per zero, nel raro caso in cui il mouse sia posizionato esattamente nella posizione del giunto. E avrebbe potuto essere scritto: if (len != 0) dx /= len;.
Sam Hocevar,

-1. Questa risposta è tutt'altro che ottimale nella maggior parte dei casi. Cosa succede se si interpola tra e 180°? In forma vettoriale: [1, 0]e [-1, 0]. Interpolare i vettori ti darà o , 180°o una divisione per 0 errore, in caso di t=0.5.
Gustavo Maciel,

@GustavoMaciel che non è "la maggior parte dei casi", è un caso d'angolo molto specifico che in pratica non accade mai. Inoltre, non c'è divisione per zero, controlla il codice.
sam hocevar,

@GustavoMaciel dopo aver ricontrollato il codice, in realtà è estremamente sicuro e funziona esattamente come dovrebbe anche nel caso d'angolo che descrivi.
sam hocevar,

11

Il trucco è ricordare che gli angoli (almeno nello spazio euclideo) sono periodici di 2 * pi. Se la differenza tra l'angolo corrente e l'angolo target è troppo grande (ovvero il cursore ha oltrepassato il limite), basta regolare l'angolo corrente aggiungendo o sottraendo 2 * pi di conseguenza.

In questo caso, puoi provare quanto segue: (Non ho mai programmato in Javascript prima, quindi perdona il mio stile di codifica.)

  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 : in questa implementazione, spostare il cursore troppo rapidamente attorno al centro dell'articolazione provoca uno strappo. Questo è il comportamento previsto, poiché la velocità angolare dell'articolazione è sempre proporzionale dtheta. Se questo comportamento non è desiderato, il problema può essere facilmente risolto posizionando un cappuccio sull'accelerazione angolare dell'articolazione.

Per fare ciò, dovremo tenere traccia della velocità del giunto e imporre una massima accelerazione:

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

Quindi, per nostra comodità, introdurremo una funzione di ritaglio:

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

Ora, il nostro codice di movimento è simile al seguente. Innanzitutto, calcoliamo dthetacome prima, adattando joint.anglese necessario:

  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;

Quindi, invece di muovere immediatamente l'articolazione, calcoliamo una velocità target e la usiamo clipper forzarla all'interno del nostro intervallo accettabile.

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

Ciò produce un movimento fluido, anche quando si cambia direzione, mentre si eseguono calcoli in una sola dimensione. Inoltre, consente di regolare indipendentemente la velocità e l'accelerazione dell'articolazione. Guarda la demo qui: http://codepen.io/anon/pen/HGnDF/


Questo metodo è molto vicino, ma se il mio mouse è troppo veloce, inizia a saltare leggermente nella direzione sbagliata. Demo qui, fammi sapere se non l'ho implementato correttamente: codepen.io/jackrugile/pen/db40aee91e1c0b693346e6cec4546e98
jackrugile

1
Non sono sicuro di cosa intendi saltando leggermente dalla parte sbagliata; per me, l'articolazione si muove sempre nella direzione corretta. Ovviamente, se muovi il mouse troppo rapidamente verso il centro, corri fuori dall'articolazione e si scatta mentre passa da una direzione all'altra. Questo è il comportamento previsto, poiché la velocità di rotazione è sempre proporzionale a dtheta. Intendevi che l'articolazione avesse un certo slancio?
David Zhang,

Vedi la mia ultima modifica per un modo per eliminare il movimento a scatti creato superando l'articolazione.
David Zhang,

Ehi, ho provato il tuo link demo, ma sembra che stia solo indicando il mio originale. Ne hai salvato uno nuovo? Altrimenti, dovrei essere in grado di implementarlo più tardi stasera e vedere come si comporta. Penso che ciò che hai descritto nella tua modifica sia più ciò che stavo cercando. Ti risponderò, grazie!
jackrugile,

1
Oh sì, hai ragione. Scusa, errore mio. Lo aggiusterò.
David Zhang,

3

Adoro le altre risposte fornite. Molto tecnico!

Se vuoi, ho un metodo molto semplice per raggiungere questo obiettivo. Assumeremo angoli per questi esempi. Il concetto può essere estrapolato ad altri tipi di valore, come i colori.

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

L'ho appena creato nel browser e non è mai stato testato. Spero di aver capito bene la logica al primo tentativo.

[Modifica] 2017/06/02 - Chiarita la logica un po '.

Inizia calcolando distanceForward e distanceBackwards e consenti ai risultati di estendersi oltre l'intervallo (0-360).

La normalizzazione degli angoli riporta quei valori nell'intervallo di (0-360). Per fare ciò, aggiungi 360 fino a quando il valore è superiore a zero e sottrai 360 mentre il valore è superiore a 360. Gli angoli di inizio / fine risultanti saranno equivalenti (-285 è uguale a 75).

Successivamente troverai il più piccolo angolo normalizzato di distanceForward o distanceBackward. distanceForward nell'esempio diventa 75, che è più piccolo del valore normalizzato di distanceBackward (300).

Se distanceForward è il più piccolo AND endAngle <startAngle, estendi endAngle oltre 360 ​​aggiungendo 360. (diventa 375 nell'esempio).

Se distanceBackward è il più piccolo AND endAngle> startAngle, estendi endAngle al di sotto di 0 sottraendo 360.

Ora passeresti da startAngle (300) al nuovo endAngle (375). Il motore dovrebbe regolare automaticamente i valori sopra 360 sottraendo 360 per te. Altrimenti dovresti passare da 300 a 360, POI da 0 a 15 se il motore non normalizza i valori per te.


Bene, sebbene l' atan2idea di base sia semplice, questa potrebbe risparmiare un po 'di spazio e un po' di prestazioni (non è necessario memorizzare xey, né calcolare troppo spesso sin, cos e atan2). Penso che valga la pena ampliare un po 'il motivo per cui questa soluzione è corretta (vale a dire scegliere il percorso più breve su un cerchio o una sfera, proprio come fa SLERP per i quaterion). Questa domanda e le risposte dovrebbero essere inserite nel wiki della comunità in quanto questo è un problema davvero comune che la maggior parte dei programmatori di gameplay deve affrontare.
Teodron,

Bella risposta. Mi piace anche l'altra risposta data da @ David Zhang. Tuttavia, quando eseguo una svolta rapida ottengo sempre uno strano jitter usando la sua soluzione modificata. Ma la tua risposta si adatta perfettamente al mio gioco. Sono interessato alla teoria matematica dietro la tua risposta. Anche se sembra semplice ma non è ovvio il motivo per cui dovremmo confrontare la distanza dell'angolo normalizzata di direzioni diverse.
Newguy,

Sono contento che il mio codice funzioni. Come accennato, questo è stato appena digitato nel browser, non testato in un progetto reale. Non ricordo nemmeno di aver risposto a questa domanda (tre anni fa)! Tuttavia, guardando oltre, sembra che stavo solo estendendo l'intervallo (0-360) per consentire ai valori di prova di andare oltre / prima di questo intervallo per confrontare la differenza di gradi totali e prendere la differenza più breve. La normalizzazione riporta nuovamente quei valori nell'intervallo di (0-360). Quindi distanceForward diventa 75 (-285 + 360), che è più piccolo di distanceBackward (285), quindi è la distanza più breve.
Doug.McFarlane,

Poiché distanceForward è la distanza più breve, viene utilizzata la prima parte della clausola IF. Poiché endAngle (15) è inferiore a startAngle (300), si aggiunge 360 ​​per ottenere 375. Quindi si passa da startAngle (300) al nuovo endAngle (375). Il motore dovrebbe regolare automaticamente i valori sopra 360 sottraendo 360 per te.
Doug.McFarlane,

Ho modificato la risposta per aggiungere più chiarezza.
Doug.McFarlane,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.