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).
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.
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.
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:
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
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
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
Pronto, sabemos onde botar o osso e podemos mover ele para lá (caso você já não tenha movido ele na última operação)
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.
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.
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).
O osso seguinte irá utilizar o osso anterior como referência, seguimos essa tática para cada um dos ossos.
O ponto azul representa a base da cadeia e é um ponto de referência que usaremos na próxima caminhada.
Backward
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.
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.
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.
Bem, agora o osso seguinte está incorreto... Mas se continuarmos repetindo o processo...
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()