Pular para o conteúdo principal

4 postagens marcadas com "inverse kinematics"

Ver todas os Marcadores

Thiago Lages de Alencar

Forward And Backward Reaching Inverse Kinematics (FABRIK) é a última kinematic que veremos. É utilizada para cadeias de ossos também mas apenas precisamos de uma iteração para definir o estado final (diferentemente da CCD).

Corrente de 4 ossos

Forward And Backward Reaching

A idéia é duas caminhadas na cadeia de ossos, a primeira irá mover os ossos em direção ao alvo (forward) e a segunda vai voltar o osso a base (backward).

A operação importante a se entender durante as duas etapas é ação de alcançar um outro ponto (reaching). Vamos utilizar ela durante as duas etapas então é bom entender isto primeiro.

Reaching

Alcançar um alvo é dividido em 2 etapas, olhar para o alvo e mover até o alvo.

Um osso e um alvo fora do alcance

Na primeira etapa podemos utilizar a mesma lógica do look at ou usar a função que sua game engine disponibilizar para rotacionar até um ponto.

Osso rotacionando até o alvo

Para mover até o ponto é preciso usar o tamanho do osso e calcular onde seria o novo ponto da base do osso. Visualizar onde seria é algo bem simples:

Osso transparente onde o osso precisa estar no final

Calcular isso envolve conseguir criar um vetor que represente o osso. Primeiro precisamos saber o vetor em que o osso se encontra, o vetor do ponto inicial dele até o alvo.

Vetor = Posição do alvo - Posição do osso

Vetor do osso até o alvo

Com isto podemos calcular a proporção desse vetor com o vetor do osso. Em outras palavras, qual o tamanho do vetor do osso em relação a esse vetor? É duas vezes o tamanho deste? É três vezes o tamanho? É metade desse vetor?

Tamanho do vetor = √(Vetor.x² + Vetor.y²)
Proporção = Tamanho do osso / Tamanho do vetor

Utilizando essa proporção podemos criar um vetor do tamanho do osso.

Vetor osso = Vetor * Proporção

Vetor osso

A última coisa é calcular o novo ponto onde o osso deve inciar. Basta pegar o ponto do alvo e reduzir pelo vetor do osso.

Posição do osso = Posição do alvo - Vetor osso

Vetor osso começando no ponto onde o osso tem que terminar

Pronto, sabemos onde botar o osso e podemos mover ele para lá (caso você já não tenha movido ele na última operação)

Osso na posição correta

Forward

A primeira caminhada pela cadeia de ossos envolve fazer cada osso andar em direção (forward) ao osso seguinte. No caso da ponta da cadeia, ela irá mover em direção ao alvo.

TODO

TODO

O ponto vermelho irá representar onde globalmente o início do osso atual está, nós usamos ele para decidir onde o osso seguinte vai alcançar.

TODO

TODO

TODO

Estou pausando aqui para lembrar que movimentar e rotacionar um osso afeta todos os filhos, por isto os ossos filhos são movimentados e rotacionados de forma a ficarem "piores" (mais longe do alvo).

TODO

O osso seguinte irá utilizar o osso anterior como referência, seguimos essa tática para cada um dos ossos.

TODO

TODO

TODO

TODO

TODO

TODO

O ponto azul representa a base da cadeia e é um ponto de referência que usaremos na próxima caminhada.

Backward

note

A última caminhada deixou tudo uma bagunça mas isso apenas porque eu escolhi trata-los da mesma forma que minha game engine (Godot) trata nodes nela.

Se tivessemos usado um array de ossos em vez de relações pai e filho, um não afetaria o outro!

Nós focaremos agora a mover os ossos em direção a base, ou seja, eles caminharam para tráz (backward). Dessa vez não precisamos nos preocupar em rotacionar, apenas mover para o final do osso anterior.

TODO

TODO

TODO

TODO

TODO

TODO

TODO

TODO

Iteration

Ao final de uma iteração podemos ter algo errado como visto acima, mas se repetirmos mais vezes vamos começar a caminhar para algo melhor.

O que acontece se começarmos outra iteração? Vamos começar novamente rotacionando o osso da ponta.

TODO

Se você der zoom na imagem, vai notar que o último osso passou do ponto alvo. Mas a etapa seguinte é mover de forma que o ponto final do osso bata com a posição do alvo.

TODO

Bem, agora o osso seguinte está incorreto... Mas se continuarmos repetindo o processo...

TODO

Aos poucos os ossos vão indo para uma posição melhor, mas eu não pretendo mostra-lo uma segunda iteração pois eu fiz estes desenhos a mão. 🤣

Conclusion

O código simplificado em GDScript (linguagem do Godot):

for i in iterations:
_apply_forwards()
_apply_backwards(base_global_position)


func _apply_forwards() -> void:
# O osso da ponta vai morar no alvo, os seguintes vão tratar o anterior como alvo.
var target_global_position: Vector2 = target.global_position

# Esse array leva da ponta até a base.
for bone in chain:
# Rotaciona em direção ao alvo.
bone.look_at(target_global_position)

# Evita calcular ratio como infinito.
if target_global_position == bone.global_position:
continue

# Calcula a nova posição do osso.
var stretch: Vector2 = target_global_position - bone.global_position
var ratio: float = bone.get_bone_length() / stretch.length()
bone.global_position = target_global_position - stretch * ratio

# Define o alvo do osso seguinte.
target_global_position = bone.global_position


func _apply_backwards(base_global_position: Vector2) -> void:
# Esse array leva da ponta até a base, então agora precisamos caminhar ao contrário.
for i in range(chain.size() - 1, -1, -1):
var bone: Bone = chain[i]

bone.global_position = base_global_position

# Calcula a posição do osso seguinte.
var direction := Vector2(cos(bone.global_rotation), sin(bone.global_rotation))
base_global_position = bone.global_position + direction * bone.get_bone_length()

References

Thiago Lages de Alencar

Cyclic Coordinate Descent Inverse Kinematic (CCDIK) é diferente das lógicas anteriores, pois nós não sabemos qual o estado final que desejamos para os ossos. A ideia é fazer diversas iterações até que chegue em um resultado aceitável.

Em outras palavras, CCDIK se trata da jornada e não do resultado final.

Corrente de 4 ossos

Cyclic Coordinate Descent

Para cada osso temos que calcular a rotação para a ponta chegue mais perto do ponto desejado.

Dependendo da direção que você caminhar pela cadeia de ossos o movimento pode ser diferente. Nesse exemplo iremos fazer de tráz para frente (osso mais perto da ponta até osso mais longe da ponta).

Rotacionando osso 4

Rotacionnado osso 3

Rotacionnado osso 2

Rotacionnado osso 1

Após primeira iteração

E com isto fizemos a primeira iteração. Vamos começar a segunda iteração.

Rotacionando osso 4

Rotacionnado osso 3

Rotacionnado osso 2

Rotacionnado osso 1

Após segunda iteração

Cada iteração se aproximando mais do ponto desejado.

Após N iterações

O único cálculo que precisamos fazer toda iteração é o ângulo da ponta até o alvo.

Negative Scale

Recomendo ler no Two Bone sobre escala negativa. O que importa é que o mesmo se aplica neste caso, se uma das escalas for negativa, precisamos rotacionar na direção oposta.

Conclusion

Em GDScript o código seria algo como:

for bone in chain:
var angle_to_target: float = bone.global_position.angle_to_point(target.global_position)
var angle_to_tip: float = bone.global_position.angle_to_point(tip.global_position)
var angle_diff: float = angle_to_target - angle_to_tip

# Escala negativa ou não.
if bone.global_scale.sign().x == bone.global_scale.sign().y:
bone.rotate(angle_diff)
else:
bone.rotate(-angle_diff)
note

Normalmente toda iteração você verificaria se chegou em um resultado aceitável.

No meu caso (game engine), estou fazendo uma iteração por frame e sem pensar se chegou ou não em um resultado aceitável.

References

Thiago Lages de Alencar

Faz um mês desde que escrevi sobre inverse kinematic look at. Talvez eu esteja enrolando para falar desta pois foi por ela que eu comecei a ver inverse kinematics... e sofri muito.

Two bone inverse kinematic! Dado que queremos a mão em uma devida posição, como os dois ossos responsáveis pelo braço devem se encontrar?

Note que não vamos ditar onde a mão vai estar, porém onde desejamos que ela estivesse. Isso é importante pois o calculo muda dependendo se a mão alcança ou não a posição desejada.

Um braço dobrado e com a mão aberta

Two Bone

Braço estendido

O que você faz quando tenta alcançar algo longe de você?
Estica o máximo possível.

O que você faz quando tenta alcançar algo perto de você?
Curva o braço de forma que sua mão acabe na posição desejada.

Primeira coisa a se fazer é descobrir se está fora ou dentro do alcance 🤣.
Em outras palavras, a base do braço até o ponto desejado é maior ou menor que o braço todo?

Braço estendido com vetor para um ponto fora do alcance

Podemos descobrir a distância entre dois pontos se calcularmos o vetor entre eles e depois usarmos a clássica formúla para distância. Resumidamente:

  • P2-P1
  • √(x²+y²)

Sabendo disso podemos calcular as seguintes distâncias:

  • A -> T
    • Distância até posição desejada
  • A -> B
    • Tamanho do osso 1
  • B -> C
    • Tamanho do osso 2

Agora podemos verificar justamente se está dentro ou fora do alcance!

Distância até posição desejada > (Tamanho do osso 1 + Tamanho do osso 2)

Out of Range

Acontece que estender o braço em uma direção é apenas tornar o ângulo global dos ossos equivalentes ao da direção.

Mostrando o ângulo global do vetor

Mostrando o ângulo global do braço quando está na mesma direção do vetor

Já vimos em IK Look at como fazer um osso/vetor apontar para uma direção e isso é tudo que precisamos fazer aqui também.

  • Apontar osso 1 para posição desejada
  • Apontar osso 2 para posição desejada

Fim.

In range - Triangle

Espero que este desenho já deixe claro como utilizaremos trigonometria com braços curvados.

Mostrando que braços curvados podem ser vistos como triângulos

Neste caso o ponto onde desejamos posicionar a mão está dentro do alcance dela, então irá acabar sendo exatamente a posição da mão (utilizaremos C mas poderia ser T).

Mostrando um braço curvado e que utilizaremos as letras A,B,C para representar pontos e a,b,c para representar tamanho do lado do triângulo

Já calculamos os lados do triângulo, então agora vamos focar no seus ângulos internos (utilizaremos α β γ).

Mostrando um braço curvado e que utilizaremos as letras A,B,C para representar pontos e a,b,c para representar tamanho do lado do triângulo

Sabendo todos os lados do triângulo podemos utilizar leis do cossenos para descobrir cada ângulo interno:

a² = b² + c² - 2bc*cos(α)
b² = a² + c² - 2ac*cos(β)
c² = a² + b² - 2ab*cos(γ)

Sabendo os lados e sabendo os ângulos internos nós conseguimos dizer como o braço precisa estar dobrado. O problema é que ele ainda pode estar dessa forma de diversas maneiras 🤣:

Mostrando diferentes maneiras que o braço pode estar rotacionado

In range - Two Angles

Existem dois ângulos que estamos buscando descobrir, rotacionando eles conseguiremos os ossos exatamente onde queremos:

Mostrando rotação por rotação a se fazer em um braço que está inicialmente apontando para o eixo X

Nessa imagem o braço estava esticado em direção ao eixo X, rotacionamos osso 1 por θ1 e osso 2 por θ2 para obter o braço no formato que queriamos.

note

Eu sei que os desenhos tem ficado cada vez piores, eu deveria estar usando uma ferramenta apropriada ou organizando melhor os desenhos...

Mas a preguiça ganhou 🙂

Como podemos obter θ1?

Se você estava pensando "é só calcular o ângulo do eixo X até o osso 2 que você consegue o θ1", deixe-me lembra-lo que o braço vai começar de forma desconhecida.

Mesmo se estivesse esticado no eixo X, o osso 2 não vai estar na posição desejada ainda!

Mostrando o braço no eixo X e o ponto desejado acima dele

Mas sabe o que podemos fazer? Calcular o ângulo do eixo X até o ponto desejado (T).

Mostrando o ângulo do eixo X até o vetor feito do osso 1 até o ponto desejado

Sabe o porque eu chamei ele de α'? Porque ele está relacionado com α!

Acontece que para obter o ângulo desejado, podemos rotacionar até a direção de T e depois remover a rotação interna do triângulo (α).

Mostrando os ângulos α' e α

Não precisamos literalmente rotacionar, podemos calcular o ângulo e depois rotacionar: α' - α

Mostrando que se reduzirmos α' pelo ângulo interno α conseguimos o osso 1 apontando na direção certa

Como podemos obter θ2?

Felizmente o osso 2 não rotacionado faz um ângulo de 180º com o osso 1.

Mostrando que o osso 2 quando tem rotação 0º, faz um ângulo de 180º com osso 1

Se rotacionarmos por 180º e diminuirmos pelo ângulo interno (β), obtemos justamente o ângulo que queriamos.

Mostrando o ângulo de 180º e β para melhor ver que é possível conseguir o ângulo do osso 2

Novamente não precisamos literalmente rotacionar, podemos calcular o ângulo e depois rotacionar: 180º - β

Mostrando que se reduzirmos β do 180º conseguimos o osso 2 apontando corretamente

No final chegamos aos ângulos graças aos ângulos internos do triângulo:

θ1 = α' - α
θ2 = 180º - β

In range - Bend Direction

Mas se nós quisermos que o braço fique curvado para o outro lado?

Acontece que mesmo curvando para o outro lado, os valores internos do triângulo não se alteram.

Mostrando que mudar a direção que o braço curva não afeta o triângulo interno

Então todo o calculo se mantém até a última etapa, onde precisamos mudar o sinal da rotação interna.

θ1 = α' + α
θ2 = 180º + β

In range - Negative Scale

Quando você escala qualquer um dos eixos por negativo, você também está dizendo que a direção para qual ele está rotacionando trocou:

Vetor (1,1) antes e após escalar X por -1

Se agora escalarmos o eixo Y negativamente, a rotação irá voltar a ser igual o início.
Cada vez que você escala um eixo negativamente, você troca a direção das rotações.

Como isso afeta nossos calculos?

Apenas o ângulo que utiliza o eixo X como referência é afetado (pois o eixo X nunca é escalado negativamente)

Mesma imagem anterior porém mostrando o segundo ângulo do ponto de vista do eixo X

Agora não queremos reduzir do ângulo α', mas sim acrescentar:

θ1 = α' + α

Mas se quisermos o osso curvado para a outra direção? É, então queremos novamente reduzir...

θ1 = α' - α

Err... basicamente estamos bricando de jogo do troca, dependendo da situação queremos rotacionar para diferentes direções.

Conclusion

Este é o meu código escrito em GDScript (linguagem do Godot):

var flip_bend: bool = false
var target_distance: float = bone_one.global_position.distance_to(target.global_position)
var bone_one_length: float = bone_one.get_bone_length()
var bone_two_length: float = bone_two.get_bone_length()
var angle_to_x_axis: float = (target.global_position - bone_one.global_position).angle()

# Fora do alcance.
if target_distance > bone_one_length + bone_two_length:
bone_one.global_rotation = angle_to_x_axis
return

# Lei dos cossenos.
var angle_0: float = acos(
(target_distance ** 2 + bone_one_length ** 2 - bone_two_length ** 2) / (2 * target_distance * bone_one_length)
)

var angle_1: float = acos(
(bone_two_length ** 2 + bone_one_length ** 2 - target_distance ** 2) / (2 * bone_two_length * bone_one_length)
)

# Direção da curva do braço.
if flip_bend:
angle_0 = -angle_0
angle_1 = -angle_1

# Escala negativa ou não.
if bone_one.global_scale.sign().x == bone_one.global_scale.sign().y:
bone_one.global_rotation = angle_to_x_axis - angle_0
else:
bone_one.global_rotation = angle_to_x_axis + angle_0

bone_two.rotation = PI + angle_1

Extra - Negative Scale in Godot

Este é extra pois depende muito da ferramenta que está utilizando, no meu caso Godot em 2D.

Godot representa translação, rotação e escala utilizando matriz. Entenda mais sobre transforms na documentação do Godot, aqui iremos direto ao assunto.

Matriz identidade representa um transform sem alteração nenhuma (translação, rotação e escala)

Matriz identidade

A desvantagem de utilizar uma matriz para armazenar todas essas informações é que algumas são impossíveis de extrarir corretamente. Olhe a matriz após escalar X por -1:

Matriz com X escalado por -1

Agora olhe a matriz após rotacionar por 180º e escalar Y por -1:

Mesma matriz apresentada anteriormente

Exatamente a mesma matriz... Se você der essa matriz para Godot, ele vai assumir que você fez a segunda opção (rotacionou e escalou Y por -1).

Como isso afeta nossa Inverse Kinematic?

Não afeta se você utilizou funções que já levam esse problema em conta, porém se vc operou diretamente sobre os transforms... Você talvez note alguns problemas.

References

Thiago Lages de Alencar

Dado que já refleti sobre forward kinematics, está na hora de falar sobre inverse kinematics (por mais que eu esteja com preguiça de fazer isso).

Vamos começar por algo que eu até me questiono se seria inverse kinematic: Look at.

Ser capaz de fazer uma mão apontar para uma posição ou a cabeça olhar para uma direção.

Jogador 2D movendo a cabeça para cima

Look At

Lembrando que articulações sempre possuem as suas informações locais e que as informações globais são facilmente calculáveis, nossa tarefa é descobrir como queremos alterar qualquer uma delas para alcançar nosso objetivo.

No caso, nosso objetivo é fazer com que o vetor da articulação aponte para X.

Uma imagem com o vector apontando para um X na direita dele e outra imagem igual porém com o X movido para cima do vetor

Na primeira parte da imagem, temos o vetor já apontando em direção do X.
Na segunda parte da imagem, o X se encontra a 90º graus do vetor.

Olhando a imagem nós conseguimos saber que para continuar apontando para X temos que rotacionar por 90º, mas como conseguir este ângulo matemáticamente?

Talvez você já tenha notado mas vamos fazer isto usando trigonometria (se prepare que IK é triângulo para tudo que é lado).

0º ~ 90º

Um bom começo é sabermos calcular o ângulo para uma posição, sem se preocupar com detalhes como global e local.

Vetor com ângulo desconhecido entre 0º e 90º

Funções trigonométricas são o segredo para trabalhar com triângulos (seno, cosseno e tangente) e neste caso tangente é justamente o que procuramos.

tan θ = cateto oposto / cateto adjacente

Agora nós temos o valor da tangente para qualquer posição (x,y).

  • (1,1)
    • tan(θ) = 1
  • (3,2)
    • tan(θ) = 2/3
  • (4,7)
    • tan(θ) = 7/4

Se você bem se lembra, existem funções trigonométricas inversas que são justamente quem vão nos dar o ângulo que chega ao valor que temos.

  • (1,1)
    • tan(θ) = 1
      • atan(1) = θ
        • 45
  • (3,2)
    • tan(θ) = 2/3
      • atan(2/3) = θ
        • 33.690067526
  • (4,7)
    • tan(θ) = 7/4
      • atan(7/4) = θ
        • 60.255118703

0º ~ 360º

Um problema que cedo ou tarde iriamos notar é que dos valores da tangete não é possível definir qual quadrante se trata.

Imagem mostrando que não da pra decompor o ângulo a partir do valor da tangente

atan(1) pode ser 45º ou 225º
atan(-1) pode ser 135º ou 315º

Única maneira de saber exatamente o quadrante é sabendo o sinal dos eixos.

  • (positivo,positivo)
    • atan(θ) + 0º
  • (negativo,positivo)
    • atan(θ) + 90º
  • (negativo,negativo)
    • atan(θ) + 180º
  • (positivo,negativo)
    • atan(θ) + 270º

É por isso que em muitas bibliotecas matemáticas existe a função atan(v) e a função atan2(x, y).
A segunda utiliza os eixos para saber o real ângulo.

Rotating

Agora que sabemos como obter o ângulo do ponto (0,0) até uma posição qualquer, podemos finalmente rotacionar a articulação.

O próximo problema é que não estamos falando de rotacionar a partir do ponto (0,0) mas sim da posição da articulação.

Mostrando diferença entre o ângulo local e global

θ1: Rotação global, usando ponto (0,0) como referência.
θ2: Rotação local, usando a posição da articulação como referência.

Para solucionar isto podemos calcular a posição do ponto em relação a articulação:

posição do ponto relativa à articulação =
posição global do ponto - posição global da articulação

Exemplo:

Posição global do ponto: (35, 10)
Posição global da articulação: (25, 10)
Posição do ponto relativa à articulação: (10, 0)
Ângulo: 0º

Se movermos o ponto:

Posição global do ponto: (50, 30)
Posição global da articulação: (25, 10)
Posição do ponto relativa à articulação: (25, 20)
Ângulo: 38º

Pronto, agora sabemos qual deveria ser a rotação daquela articulação!

Conclusion

Você provavelmente não terá que pensar em nada disso pois muitas game engines já possuem métodos para lidar com isto, por exemplo em Godot podemos encontrar algo como:

Node2D.get_angle_to(global_position)

Essa função retorna o ângulo global que falta para estar em direção ao ponto global.

References