Como lerp entre valores desse loop (como matiz ou rotação)?

25

exemplo de animação conjunta

Ver demonstração

Estou tentando fazer a junta girar suavemente em torno do centro da tela, em direção ao ângulo do ponteiro do mouse. O que tenho funciona, mas quero que ele anime a menor distância possível para chegar ao ângulo do mouse. O problema ocorre quando o valor circula na linha horizontal ( 3.14e -3.14). Passe o mouse nessa área para ver como a direção muda e leva o longo caminho de volta.

Código relevante

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

Como posso fazê-lo girar a menor distância, mesmo "atravessando a lacuna"?

jackrugile
fonte
Use módulo. : D en.wikipedia.org/wiki/Modular_arithmetic
Vaughan Hilts
11
@VaughanHilts Não tenho certeza de como eu usaria minha situação. Você pode elaborar mais?
jackrugile

Respostas:

11

Nem sempre é o melhor método, e pode ser mais caro computacionalmente (embora isso dependa de como você armazena seus dados), mas argumentarei que a leitura de valores 2D funciona razoavelmente bem na maioria dos casos. Em vez de ler um ângulo desejado, você pode ler o vetor de direção normalizado desejado .

Uma vantagem desse método em relação ao método “escolher a rota mais curta para o ângulo” é que ele funciona quando você precisa interpolar entre mais de dois valores.

Ao ler valores de matiz, você pode substituir huepor um [cos(hue), sin(hue)]vetor.

No seu caso, lendo a direção da junta normalizada:

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

O código pode ser mais curto se você puder usar uma classe de vetores 2D. Por exemplo:

// 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
fonte
Obrigado, isso está funcionando muito bem aqui: codepen.io/jackrugile/pen/45c356f06f08ebea0e58daa4d06d204f Entendo a maior parte do que você está fazendo, mas você pode elaborar um pouco mais da linha 5 do seu código durante a normalização? Não sei o que está acontecendo lá exatamente dx /= len...
jackrugile
11
Dividir um vetor por seu comprimento é chamado de normalização . Ele garante o comprimento 1. A len ? len : 1.0peça evita uma divisão por zero, no raro caso em que o mouse é colocado exatamente na posição da articulação. Ele poderia ter sido escrito: if (len != 0) dx /= len;.
Sam Hocevar
-1. Essa resposta está longe de ser ótima na maioria dos casos. E se você estiver interpolando entre e 180°? Em forma de vetor: [1, 0]e [-1, 0]. Vetores de interpolação vai lhe dar qualquer , 180°ou uma divisão por 0 erro, no caso de t=0.5.
Gustavo Maciel
@GustavoMaciel que não é a "maioria dos casos", é um caso de canto muito específico que nunca acontece na prática. Além disso, não há divisão por zero, verifique o código.
23615 sam samococar
@GustavoMaciel, depois de verificar o código novamente, é realmente extremamente seguro e funciona exatamente como deveria, mesmo no canto que você descreve.
23615 sam samococar
11

O truque é lembrar que os ângulos (pelo menos no espaço euclidiano) são periódicos em 2 * pi. Se a diferença entre o ângulo atual e o ângulo alvo for muito grande (ou seja, o cursor cruzou o limite), basta ajustar o ângulo atual adicionando ou subtraindo 2 * pi de acordo.

Nesse caso, você pode tentar o seguinte: (nunca havia programado em Javascript antes, perdoe meu estilo de codificação.)

  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 : Nesta implementação, mover o cursor muito rapidamente ao redor do centro da articulação faz com que ele se solte. Este é o comportamento pretendido, uma vez que a velocidade angular da articulação é sempre proporcional a dtheta. Se esse comportamento for indesejável, o problema pode ser facilmente resolvido colocando uma tampa na aceleração angular da articulação.

Para fazer isso, precisamos acompanhar a velocidade da articulação e impor uma aceleração máxima:

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

Em seguida, para nossa conveniência, apresentaremos uma função de recorte:

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

Agora, nosso código de movimento se parece com isso. Primeiro, calculamos dthetacomo antes, ajustando joint.angleconforme necessário:

  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;

Em seguida, em vez de mover a articulação imediatamente, calculamos uma velocidade alvo e a usamos clippara forçá-la dentro de nossa faixa aceitável.

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

Isso produz movimento suave, mesmo ao mudar de direção, enquanto realiza cálculos em apenas uma dimensão. Além disso, permite que a velocidade e a aceleração da junta sejam ajustadas independentemente. Veja a demonstração aqui: http://codepen.io/anon/pen/HGnDF/

David Zhang
fonte
Esse método chega muito perto, mas se eu colocar o mouse muito rápido, ele começa a pular levemente na direção errada. Demonstração aqui, deixe-me saber se eu não implementar isso corretamente: codepen.io/jackrugile/pen/db40aee91e1c0b693346e6cec4546e98
jackrugile
11
Não tenho certeza do que você quer dizer pulando levemente na direção errada; para mim, a articulação sempre se move na direção correta. Obviamente, se você mover o mouse pelo centro com muita rapidez, ultrapassa a articulação e ela estremece quando muda de se mover de uma direção para a outra. Esse é o comportamento pretendido, pois a velocidade de rotação é sempre proporcional a dtheta. Você pretendia que a junta tivesse algum impulso?
David Zhang
Veja minha última edição para uma maneira de eliminar o movimento brusco criado por ultrapassar a articulação.
David Zhang
Ei, tentei o seu link de demonstração, mas parece que ele está apenas apontando para o meu original. Você salvou um novo? Caso contrário, tudo bem, eu deveria ser capaz de implementar isso mais tarde esta noite e ver como ele funciona. Acho que o que você descreveu na sua edição é mais o que eu estava procurando. Vai voltar para você, obrigado!
jackrugile
11
Ah, sim, você está certo. Desculpe meu erro. Eu vou consertar isso.
David Zhang
3

Eu amo as outras respostas dadas. Muito técnico!

Se você quiser, eu tenho um método muito simples para fazer isso. Assumiremos ângulos para esses exemplos. O conceito pode ser extrapolado para outros tipos de valor, como cores.

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

Acabei de criar isso no navegador e nunca foi testado. Espero ter acertado a lógica na primeira tentativa.

02/06/2017 - Esclareceu um pouco a lógica.

Comece calculando distanceForward e distanceBackwards e permita que os resultados ultrapassem o intervalo (0-360).

A normalização de ângulos traz esses valores de volta ao intervalo de (0-360). Para fazer isso, você adiciona 360 até que o valor esteja acima de zero e subtrai 360 enquanto o valor está acima de 360. Os ângulos iniciais / finais resultantes serão equivalentes (-285 é igual a 75).

Em seguida, você encontrará o menor ângulo Normalizado de distanceForward ou distanceBackward. distanceForward no exemplo se torna 75, que é menor que o valor normalizado de distanceBackward (300).

Se distanceForward for o menor AND endAngle <startAngle, estenda endAngle além de 360 ​​adicionando 360. (torna-se 375 no exemplo).

Se distanceBackward for o menor AND endAngle> startAngle, estenda endAngle para abaixo de 0 subtraindo 360.

Você agora leria de startAngle (300) para o novo endAngle (375). O mecanismo deve ajustar automaticamente os valores acima de 360 ​​subtraindo 360 para você. Caso contrário, você teria que lerp de 300 a 360, ENTÃO lerp de 0 a 15 se o mecanismo não normalizar os valores para você.

Doug.McFarlane
fonte
Bem, embora a atan2ideia baseada seja direta, essa poderia economizar um pouco de espaço e um pouco de desempenho (não é necessário armazenar xey, nem calcular o pecado, cos e atan2 com muita frequência). Eu acho que vale a pena expandir um pouco o porquê dessa solução estar correta (ou seja, escolher o caminho mais curto em um círculo ou esfera, assim como o SLERP faz para perguntas). Esta pergunta e respostas devem ser colocadas no wiki da comunidade, pois esse é um problema muito comum que a maioria dos programadores de jogos enfrenta.
Teodron 26/03
Ótima resposta. Também gosto da outra resposta dada por @David Zhang. No entanto, eu sempre fico com um tremor estranho usando sua solução editada quando faço uma curva rápida. Mas sua resposta se encaixa perfeitamente no meu jogo. Estou interessado na teoria matemática por trás da sua resposta. Embora pareça simples, mas não é óbvio por que devemos comparar a distância angular normalizada de diferentes direções.
newguy
Fico feliz que meu código funcione. Como mencionado, isso foi digitado no navegador, não testado em um projeto real. Nem me lembro de responder a essa pergunta (três anos atrás)! Mas, olhando para cima, parece que eu estava apenas estendendo o intervalo (0-360) para permitir que os valores do teste ultrapassem / antes desse intervalo, a fim de comparar a diferença total de graus e fazer a menor diferença. A normalização apenas coloca esses valores no intervalo de (0-360) novamente. Assim, distanceForward se torna 75 (-285 + 360), que é menor que distanceBackward (285), portanto é a distância mais curta.
precisa saber é o seguinte
Como distanceForward é a distância mais curta, a primeira parte da cláusula IF é usada. Como endAngle (15) é menor que startAngle (300), adicionamos 360 para obter 375. Portanto, você deve ler de startAngle (300) para o novo endAngle (375). O mecanismo deve ajustar automaticamente os valores acima de 360 ​​subtraindo 360 para você.
precisa saber é o seguinte
Editei a resposta para adicionar mais clareza.
precisa saber é o seguinte