Estou girando um objeto em dois eixos, então por que ele continua girando em torno do terceiro eixo?

38

Vejo muitas vezes perguntas surgindo que têm esse problema subjacente, mas todas são capturadas nos detalhes de um determinado recurso ou ferramenta. Aqui está uma tentativa de criar uma resposta canônica à qual podemos referir os usuários quando isso surgir - com muitos exemplos animados! :)


Digamos que estamos fazendo uma câmera de primeira pessoa. A idéia básica é que você deve olhar para a esquerda e para a direita e inclinar para cima e para baixo. Então, escrevemos um pouco de código como este (usando o Unity como exemplo):

void Update() {
    float speed = lookSpeed * Time.deltaTime;

    // Yaw around the y axis using the player's horizontal input.        
    transform.Rotate(0f, Input.GetAxis("Horizontal") * speed, 0f);

    // Pitch around the x axis using the player's vertical input.
    transform.Rotate(-Input.GetAxis("Vertical") * speed,  0f, 0f);
}

ou talvez

// Construct a quaternion or a matrix representing incremental camera rotation.
Quaternion rotation = Quaternion.Euler(
                        -Input.GetAxis("Vertical") * speed,
                         Input.GetAxis("Horizontal") * speed,
                         0);

// Fold this change into the camera's current rotation.
transform.rotation *= rotation;

E geralmente funciona, mas com o tempo a visualização começa a ficar torta. A câmera parece estar girando em seu eixo de rotação (z), embora tenhamos dito apenas para girar nos x e y!

Exemplo animado de câmera em primeira pessoa inclinada para o lado

Isso também pode acontecer se estivermos tentando manipular um objeto na frente da câmera - digamos que é um globo que queremos girar para olhar ao redor:

Exemplo animado de um globo inclinado para o lado

O mesmo problema - depois de um tempo, o Pólo Norte começa a se afastar para a esquerda ou direita. Estamos dando entrada em dois eixos, mas estamos obtendo essa rotação confusa em um terceiro. E acontece se aplicamos todas as nossas rotações em torno dos eixos locais do objeto ou dos eixos globais do mundo.

Em muitos mecanismos, você também verá isso no inspetor - gire o objeto no mundo e, de repente, os números mudam em um eixo que nem tocamos!

Exemplo animado mostrando a manipulação de um objeto ao lado de uma leitura de seus ângulos de rotação.  O ângulo z muda mesmo que o usuário não tenha manipulado esse eixo.

Então, isso é um bug do motor?Como podemos dizer ao programa que não queremos que ele adicione rotação extra?

Tem algo a ver com ângulos de Euler? Em vez disso, devo usar quaterniões, matrizes de rotação ou vetores de base?

DMGregory
fonte
11
Achei a resposta para a seguinte pergunta muito útil também. gamedev.stackexchange.com/questions/123535/…
Travis Pettry 19/07

Respostas:

51

Não, isso não é um bug do mecanismo ou um artefato de uma representação de rotação específica (isso também pode acontecer, mas esse efeito se aplica a todos os sistemas que representam rotações, inclusive quaterniões).

Você descobriu um fato real sobre como a rotação funciona no espaço tridimensional e parte de nossa intuição sobre outras transformações, como a tradução:

Exemplo animado mostrando que a aplicação de rotações em uma ordem diferente fornece resultados diferentes

Quando compomos rotações em mais de um eixo, o resultado obtido não é apenas o valor total / líquido que aplicamos a cada eixo (como seria de esperar para a tradução). A ordem em que aplicamos as rotações altera o resultado, à medida que cada rotação move os eixos nos quais as próximas rotações são aplicadas (se estiver girando sobre os eixos locais do objeto) ou a relação entre o objeto e o eixo (se estiver girando sobre o mundo) eixos).

A mudança das relações dos eixos ao longo do tempo pode confundir nossa intuição sobre o que cada eixo deve "fazer". Em particular, certas combinações de rotação de guinada e inclinação produzem o mesmo resultado que uma rotação de rolagem!

Exemplo animado que mostra uma sequência de pitch-yaw-pitch local fornece a mesma saída que um único rolo local

Você pode verificar se cada etapa está girando corretamente em torno do eixo que solicitamos - não há falhas ou artefatos no motor em nossa notação que interfiram ou adivinhem nossa entrada - a natureza esférica (ou hiperesférica / quaternária) da rotação significa apenas as transformações " ao redor "um sobre o outro. Eles podem ser ortogonais localmente, para pequenas rotações, mas à medida que se acumulam, descobrimos que não são ortogonais globalmente.

Isso é mais dramático e claro em curvas de 90 graus, como as anteriores, mas os eixos errantes também se arrastam por muitas pequenas rotações, como demonstrado na pergunta.

Então, o que fazemos sobre isso?

Se você já possui um sistema de rotação de inclinação da guinada, uma das maneiras mais rápidas de eliminar o rolo indesejado é alterar uma das rotações para operar nos eixos de transformação global ou pai, em vez dos eixos locais do objeto. Dessa forma, você não pode obter contaminação cruzada entre os dois - um eixo permanece absolutamente controlado.

Aqui está a mesma sequência de pitch-yaw-pitch que se tornou um exemplo no exemplo acima, mas agora aplicamos nossa guinada ao redor do eixo Y global em vez da do objeto

Exemplo animado de caneca rodando com pitch local e guinada global, sem problemas de rolagemExemplo animado de câmera em primeira pessoa usando guinada global

Assim, podemos consertar a câmera em primeira pessoa com o mantra "Pitch Locally, Yaw Globally":

void Update() {
    float speed = lookSpeed * Time.deltaTime;

    transform.Rotate(0f, Input.GetAxis("Horizontal") * speed, 0f, Space.World);
    transform.Rotate(-Input.GetAxis("Vertical") * speed,  0f, 0f, Space.Self);
}

Se você estiver compondo suas rotações usando a multiplicação, inverta a ordem esquerda / direita de uma das multiplicações para obter o mesmo efeito:

// Yaw happens "over" the current rotation, in global coordinates.
Quaternion yaw = Quaternion.Euler(0f, Input.GetAxis("Horizontal") * speed, 0f);
transform.rotation =  yaw * transform.rotation; // yaw on the left.

// Pitch happens "under" the current rotation, in local coordinates.
Quaternion pitch = Quaternion.Euler(-Input.GetAxis("Vertical") * speed, 0f, 0f);
transform.rotation = transform.rotation * pitch; // pitch on the right.

(A ordem específica dependerá das convenções de multiplicação em seu ambiente, mas esquerda = mais global / direita = mais local é uma escolha comum)

Isso equivale a armazenar a guinada total líquida e a inclinação total que você deseja como variáveis ​​de flutuação, aplicando sempre o resultado líquido de uma só vez, construindo uma única nova orientação ou matriz de orientação a partir desses ângulos (desde que você mantenha-se totalPitchpreso):

// Construct a new orientation quaternion or matrix from Euler/Tait-Bryan angles.
var newRotation = Quaternion.Euler(totalPitch, totalYaw, 0f);
// Apply it to our object.
transform.rotation = newRotation;

ou equivalente...

// Form a view vector using total pitch & yaw as spherical coordinates.
Vector3 forward = new Vector3(
                    Mathf.cos(totalPitch) * Mathf.sin(totalYaw),
                    Mathf.sin(totalPitch),
                    Mathf.cos(totalPitch) * Mathf.cos(totalYaw));

// Construct an orientation or view matrix pointing in that direction.
var newRotation = Quaternion.LookRotation(forward, new Vector3(0, 1, 0));
// Apply it to our object.
transform.rotation = newRotation;

Usando essa divisão global / local, as rotações não têm chance de se compor e influenciar uma à outra, porque são aplicadas a conjuntos independentes de eixos.

A mesma idéia pode ajudar se for um objeto no mundo que queremos rotacionar. Para um exemplo como o globo, muitas vezes queremos invertê-lo e aplicar nossa guinada localmente (para que ela sempre gire em torno de seus polos) e incline-se globalmente (para que se incline em direção à nossa visão, em vez de na Austrália. , onde quer que esteja apontando ...)

Exemplo animado de um globo mostrando rotação com melhor comportamento

Limitações

Essa estratégia híbrida global / local nem sempre é a solução correta. Por exemplo, em um jogo com vôo / natação em 3D, convém apontar para cima / para baixo e ainda ter controle total. Mas com esta configuração, você apertará o bloqueio do cardan - seu eixo de guinada (ascendente global) se torna paralelo ao seu eixo de rolagem (local para a frente) e você não tem como olhar para a esquerda ou direita sem torcer.

O que você pode fazer em casos como esse é usar rotações locais puras, como começamos na pergunta acima (para que seus controles pareçam o mesmo, não importa para onde você esteja olhando), o que inicialmente permitirá que alguns movimentos entrem em ação - mas depois nós corrigimos isso.

Por exemplo, podemos usar rotações locais para atualizar nosso vetor "forward" e, em seguida, usar esse vetor forward junto com um vetor "up" de referência para construir nossa orientação final. (Usando, por exemplo, o Quaternion da Unity. LookRotation método , ou construindo manualmente uma matriz ortonormal a partir desses vetores). Controlando o vetor up, controlamos o roll ou twist.

Para o exemplo de voo / natação, convém aplicar essas correções gradualmente ao longo do tempo. Se for muito abrupto, a vista pode mudar de uma maneira perturbadora. Em vez disso, você pode usar o vetor atual do player e apontá-lo para a vertical, quadro a quadro, até que a visualização seja nivelada. Aplicar isso durante um turno às vezes pode ser menos nauseante do que girar a câmera enquanto os controles do player estão ociosos.

DMGregory
fonte
11
Posso perguntar como você fez para gifs? Eles parecem legais.
Bálint
11
@ Bálint Eu uso um programa gratuito chamado LICEcap - ele permite gravar uma parte da sua tela em um gif. Depois, aparei a animação / adicionei texto de legenda adicional / comprima o gif usando o Photoshop.
DMGregory
Esta resposta é apenas para o Unity? Existe uma resposta mais genérica na Web?
posfan12
2
@ posfan12 O código de exemplo usa a sintaxe do Unity para concretização, mas as situações e a matemática envolvidas se aplicam igualmente em todos os aspectos. Você está tendo alguma dificuldade em aplicar os exemplos ao seu ambiente?
DMGregory
2
A matemática já está presente acima. A julgar pela sua edição, você está apenas esmagando as partes erradas. Parece que você está usando o tipo de vetor definido aqui . Isso inclui um método para construir uma matriz de rotação a partir de um conjunto de ângulos, como descrito acima. Comece com uma matriz de identidade e chame TCVector::calcRotationMatrix(totalPitch, totalYaw, identity)- isso é equivalente ao exemplo "de uma só vez Euler" acima.
DMGregory
1

APÓS 16 horas de funções parentais e rotação lol.

Eu só queria que o código tivesse um vazio, basicamente, girando em círculos e também virando a frente.

Ou, em outras palavras, o objetivo é girar a guinada e o arremesso sem nenhum efeito no rolamento.

Aqui estão os poucos que realmente funcionaram no meu aplicativo

Na unidade:

  1. Crie um vazio. Chame isso de ForYaw.
  2. Dê a um filho vazio, chamado ForPitch.
  3. Por fim, faça um cubo e mova-o para z = 5 (avançar). Agora podemos ver o que estamos tentando evitar, o que girará o cubo ("rolagem" acidental enquanto guinamos e giramos) .

Agora, crie scripts no Visual Studio, faça sua instalação e termine com isso na atualização:

objectForYaw.Rotate(objectForYaw.up, 1f, Space.World);
    objectForPitch.Rotate(objectForPitch.right, 3f, Space.World);

    //this will work on the pitch object as well
    //objectForPitch.RotateAround
    //    (objectForPitch.position, objectForPitch.right, 3f);

Observe que tudo quebrou para mim quando tentei variar a ordem dos pais. Ou, se eu mudar o Space.World para o objeto de pitch filho (o Yaw pai não se importa de ser girado pelo Space.Self). Confuso. Definitivamente, eu poderia usar alguns esclarecimentos sobre o motivo de ter que usar um eixo local, mas aplicá-lo no espaço do mundo - por que o Space.Self estraga tudo?

Jeh
fonte
Não está claro para mim se isso pretende responder à pergunta ou se você está pedindo ajuda para entender por que o código que você escreveu funciona dessa maneira. Se for o último, você deve postar uma nova pergunta, com link para esta página para o contexto, se quiser. Como dica, me.Rotate(me.right, angle, Space.World)é o mesmo que me.Rotate(Vector3.right, angle, Space.Self, e suas transformações aninhadas são equivalentes aos exemplos "Inclinar localmente, guiar globalmente" na resposta aceita.
DMGregory
Um pouco dos dois. Parcialmente para compartilhar a resposta (por exemplo, aqui está exatamente como funcionou para mim). E também para introduzir a pergunta. Essa dica me fez pensar. Muito obrigado!
Jeh
Surpreendente, isso não foi votado. Depois de horas lendo respostas abrangentes que, embora educacionais ainda não funcionassem, a resposta simples de usar o eixo de rotação de objetos em vez de um eixo Vector3.right genérico é o suficiente para manter a rotação fixa no objeto. Incrível o que eu estava procurando foi enterrado aqui com 0 votos e na 2ª página do Google de muitas e muitas perguntas semelhantes.
user4779 21/09