This tutorial is a follow-up of Working with 3D skeletons.
Previously, we were able to control the rotations of bones in order to manipulatewhere our arm was (forward kinematics). But what if we wanted to solve this problemin reverse? Inverse kinematics (IK) tells us how to rotate our bones in order to reacha desired position.
A simple example of IK is the human arm: While we intuitively know the targetposition of an object we want to reach for, our brains need to figure out how much tomove each joint in our arm to get to that target.
Initial problem¶
Talking in Godot terminology, the task we want to solve here is to positionthe 2 angles on the joints of our upperarm and lowerarm so that the tip of thelowerarm bone is as close to the target point (which is set by the target Vector3)as possible using only rotations. This task is calculation-intensive and neverresolved by analytical equation solving, as it is an under-constrainedproblem which means that there is more than one solution to anIK problem.
For easy calculation in this chapter, we consider the target being achild of Skeleton. If this is not the case for your setup you can alwaysreparent it in your script, as you will save on calculations if youdo so.
In the picture, you see the angles alpha and beta. In this case, we don’tuse poles and constraints, so we need to add our own. On the picturethe angles are 2D angles living in a plane which is defined by bonebase, bone tip, and target.
The rotation axis is easily calculated using the cross-product of the bonevector and the target vector. The rotation in this case will be always inpositive direction. If t
is the Transform which we get from theget_bone_global_pose() function, the bone vector is
t.basis[2]
So we have all the information we need to execute our algorithm.
In game dev it is common to resolve this problem by iteratively closingto the desired location, adding/subtracting small numbers to the anglesuntil the distance change achieved is less than some small error value.Sounds easy enough, but there are still Godot problems we need to resolveto achieve our goal.
- How to find coordinates of the tip of the bone?
- How to find the vector from the bone base to the target?
For our goal (tip of the bone moved within area of target), we need to knowwhere the tip of our IK bone is. As we don’t use a leaf bone as IK bone, weknow the coordinate of the bone base is the tip of the parent bone. All thesecalculations are quite dependent on the skeleton’s structure. You could usepre-calculated constants, or you could add an extra bone at the tip of theIK bone and calculate using that.
Implementation¶
We will use an exported variable for the bone length to make it easy.
export var ik_bone = "lowerarm"export var ik_bone_length = 1.0export var ik_error = 0.1
Now, we need to apply our transformations from the IK bone to the base ofthe chain, so we apply a rotation to the IK bone, then move from our IK bone up toits parent, apply rotation again, then move to the parent of thecurrent bone again, etc. So we need to limit our chain somewhat.
export var ik_limit = 2
For the _ready()
function:
var skelfunc _ready(): skel = get_node("arm/Armature/Skeleton") set_process(true)
Now we can write our chain-passing function:
func pass_chain(): var b = skel.find_bone(ik_bone) var l = ik_limit while b >= 0 and l > 0: print( "name":", skel.get_bone_name(b)) print( "local transform":", skel.get_bone_pose(b)) print( "global transform":", skel.get_bone_global_pose(b)) b = skel.get_bone_parent(b) l = l - 1
And for the _process()
function:
func _process(delta): pass_chain(delta)
Executing this script will pass through the bone chain, printing bonetransforms.
extends Spatialexport var ik_bone = "lowerarm"export var ik_bone_length = 1.0export var ik_error = 0.1export var ik_limit = 2var skelfunc _ready(): skel = get_node("arm/Armature/Skeleton") set_process(true)func pass_chain(delta): var b = skel.find_bone(ik_bone) var l = ik_limit while b >= 0 and l > 0: print("name: ", skel.get_bone_name(b)) print("local transform: ", skel.get_bone_pose(b)) print( "global transform:", skel.get_bone_global_pose(b)) b = skel.get_bone_parent(b) l = l - 1func _process(delta): pass_chain(delta)
Now we need to actually work with the target. The target should be placedsomewhere accessible. Since “arm” is an imported scene, we better placethe target node within our top level scene. But for us to work with targeteasily its Transform should be on the same level as the Skeleton.
To cope with this problem, we create a “target” node under our scene rootnode and at runtime we will reparent it, copying the global transformwhich will achieve the desired effect.
Create a new Spatial node under the root node and rename it to “target”.Then modify the _ready()
function to look like this:
var skelvar targetfunc _ready(): skel = get_node("arm/Armature/Skeleton") target = get_node("target") var ttrans = target.get_global_transform() remove_child(target) skel.add_child(target) target.set_global_transform(ttrans) set_process(true)