-
Notifications
You must be signed in to change notification settings - Fork 71
Tutorial 2.0
The general idea to using this library can be summarized into three steps:
-
Problem definition: Create a tree, assign algorithms/effectors/etc. to the tree, and call
ik_solver_build()
. -
Solving: Copy positions/rotations into the tree and call
ik_solver_solve()
. - Retrieve: Copy the new positions/rotations back out of the tree.
Step 1. usually only has to be performed once. Steps 2. and 3. can be repeated over and over for every frame. If the structure of the tree changes, however, you will have to repeat parts of Step 1.
Before you can use any part of the library you must initialize it:
if (ik_init() != 0)
error("Failed to initialize IK");
You can call ik_init()
multiple times. The library keeps track of how many times it was initialized. Notice however that if ik_init()
fails, you should skip the corresponding call to ik_deinit()
.
int memory_leaks = ik_deinit();
If you called ik_init()
more than once, then the "last" call to ik_deinit()
is the one that will deinit the library. It returns the number of memory leaks if IK_MEMORY_DEBUGGING=ON
. This may or may not be interesting to you and it is safe to ignore the return value. If IK_MEMORY_DEBUGGING=OFF
then 0 is returned.
The library will generate various log messages (warnings and statistics), which are silenced by default. If you are interested in listening to what the library has to say, you can initialize the log with:
if (ik_log_init() != 0)
error("Failed to initialize IK logging");
Make sure to deinit the log when you're done with it (ik_deinit()
does not deinit the log):
/* Done with logging */
ik_log_deinit();
Also note that you can call ik_log_init()
multiple times and it will keep track of the number of times you called it, just like with ik_init()
.
The library will make an effort to inform you if something is configured wrong, or if you use something in the wrong way, so it is recommended to enable logging at least in debug mode. The messages are written to stderr by default. It is possible to redirect the log messages by registering a callback:
void my_log_callback(ik_log_severity severity, const char* msg, void* args) {
/* process message here */
}
ik_log_set_callback(my_log_callback, my_args);
Here, my_args
is stored and passed to your callback function whenever it gets called. You can use this to store the this
pointer to a C++ object, for example.
You can also configure the severity of messages. For debug builds, the severity is set to IK_DEBUG
by default. For release builds, it is set to IK_INFO
. Generated messages below the configured severity level are discarded.
ik_log_set_severity(IK_WARNING); /* Only interested in warnings, errors, and fatal messages */
Now to the fun part! I believe in learning by example, so let's look at a 2-bone example:
#include "ik/ik.h"
void main() {
ik_init();
ik_log_init();
/* create a 2-bone structure */
struct ik_node* base = ik_node_create(ik_uid(0));
IK_INCREF(base); /* Always have to take ownership of root node */
struct ik_node* mid = ik_node_create_child(base, ik_uid(1));
struct ik_node* tip = ik_node_create_child(mid, ik_uid(2));
/* attach an end-effector to the tip node */
struct ik_effector* e1 = ik_node_create_effector(tip);
/* attach an algorithm to the base node */
struct ik_algorithm* a1 = ik_node_create_algorithm(base, IK_TWO_BONE);
/* Now, create the solver */
struct ik_solver* s1 = ik_solver_build(tree);
IK_INCREF(s1); /* again, we have to take ownership of the solver */
/*
* Define the positions/rotations of the nodes. We'll keep things simple.
* The library always assumes everything is in local space, so this will
* end up looking something like this:
*
* tip <- e1
* |
* mid
* |
* base <- a1
*/
base->position = ik_vec3(0, 0, 0);
mid->position = ik_vec3(0, 2, 0);
tip->position = ik_vec3(0, 2, 0);
/*
* Set the target position. This is where the tip node should try and reach to.
* Also note that this is in local space, relative to where the tip node's
* position is. In world space, the coordinates are 2, 0, 0, so the tree should
* end up looking something like this when the solver is done:
*
* mid
* / \
* base tip
*/
e1->target_position = ik_vec3(2, -2, 0);
/*
* Because we changed node positions *after* we built the solver, it is necessary
* to update translations. If we had set the node positions first and then called
* ik_solver_build(), this step would not be necessary.
*
* Notice: This only has to be done when the *distances* between nodes change. If
* you're only updating node rotations, or updating positions in a way where the
* distance to parent nodes doesn't change, this is not required.
*/
ik_solver_update_translations(s1);
/* Great, now solve. This returns the number of effectors that actually reached
* their target position. In our case, it should be 1. */
int converged = ik_solver_solve(s1);
/* Print the positions and see if they are what we expected them to be */
print_pos("base: ", base->position);
print_pos("mid: ", mid->position);
print_pos("tip: ", tip->position);
print_pos("target: ", e1->target_position);
/* We are done with the tree and the solver */
IK_DECREF(base);
IK_DECREF(s1);
/* We are done with the library */
ik_log_deinit();
ik_deinit();
return 0;
}
There are some things to talk about here.
ik_node* base = ik_node_create(ik_uid(0));
Every node within the tree must be uniquely identifiable. This is achieved by either providing a UID type, which is just an unsigned integer, or you can provide a pointer to user data, which is used as the UID instead. You cannot set both at the same time, because internally, the node stores this data as:
union ik_node_user_data {
void* ptr;
uintptr_t uid;
};
The data can later be accessed again through the node pointer:
printf("Node ID: %d\n", base->user.uid);
/* or */
printf("Node user pointer: 0x%p\n", base->user.ptr);
You may want to store a pointer to some context (e.g. `` this``` pointer to a C++ object) instead of an ID, so the call would look like this instead:
void* ctx = MyContext();
ik_node* base = ik_node_create(ik_ptr(ctx));
Keep in mind that you can't store the same pointer into different nodes, though, otherwise those nodes are no longer uniquely identifiable. If this is something that you actually need to be able to do, then please contact me (make an issue on github) and I can make the change.
IK_INCREF(base); /* Always have to take ownership of root node */
All functions in this library return what we call a "borrowed" reference. Even functions that create new objects return borrowed references. This might seem counter-intuitive (why would you not own the object you just allocated?), but you'll soon see that it makes the whole API more consistent and thus, less error-prone.
Additionally, there are no "reference stealing" functions in this library, that is, functions that transfer ownership from the caller. If you incref an object, you can rely on the fact that you are also responsible for decref'ing that object again.
We're cheating a little bit in the example code above. We only really own base
and s1
, because those are the only objects we called IK_INCREF()
on. All of the other objects (mid
, tip
, e1
, and a1
) are borrowed references and could be deleted at any time. So strictly speaking, the example code is semantically incorrect.
The reason why it works anyway is because we know that the base node owns a reference to its child node mid
. As long as we hold a reference to base
, mid
will continue to live. This is also true for tip
and all of the other objects part of the tree.
The solver object s1
is not part of the tree, though, so we must take ownership of it before using it.
I intentionally wrote the example the hard way to show you how it works on the lowest level. If you are using C++ then you can automate this reference counting process by including "ik/cpputils.hpp"
and using the ik::Ref
class:
#include "ik/cpputils.hpp"
int main() {
ik::Ref<ik_node> base = ik_node_create(ik_guid(0));
ik::Ref<ik_node> mid = ik_node_create_child(base, ik_guid(1));
ik::Ref<ik_node> tip = ik_node_create_child(mid, ik_guid(2));
ik::Ref<ik_effector> e1 = ik_node_create_effector(tip);
ik::Ref<ik_algorithm> a1 = ik_node_create_algorithm(base, IK_TWO_BONE);
ik::Ref<ik_solver> s1 = ik_solver_build(base);
e1->target_position = ...;
base->position = ...;
mid->position = ...;
/* etc */
}
Here it becomes clear why ik_node_create()
must return a borrowed reference. If the API included functions that returned new references mixed with functions that returned borrowed references, then the caller would have to somehow specify how they wish to acquire a reference (either by stealing it or by incref'ing it).
Version 1.1 of the library used a hierarchical ownership approach. This sounded good on paper but it became an immense pain to write wrappers or script bindings for the library, because you had to deal with nodes being deleted outside of your control. Reference counting solves the issue of script objects trying to own internal IK objects.
struct ik_node* mid = ik_node_create_child(base, ik_uid(1));
struct ik_node* tip = ik_node_create_child(mid, ik_uid(2));
This is self explanatory. Again, the returned objects are borrowed references, but the parent node is holding a reference to them so as long as we own the root node then all child nodes are guaranteed to exist.
ik_node_create_child()
is short-hand for:
struct ik_node* mid = ik_node_create(ik_uid(1));
ik_node_link(base, mid); /* base incref's mid */
All node positions and rotations are set in local space. Node positions are represented as a 3 dimensional vector type and rotations as a quaternion.
Effectors are used to set the target position and rotation of a node.
In order to mark a node as an end-effector, you must create and attach an effector object:
/* attach an end-effector to the tip node */
struct ik_effector* e1 = ik_node_create_effector(tip);
This is short-hand for:
ik_effector* e1 = ik_effector_create();
ik_node_attach_effector(tip, e1); /* tip incref's e1 */
The most important fields that can be set on the effector are:
e1->chain_length = N;
e1->target_position = SomeTargetPos();
e1->target_rotation = SomeTargetRot();
e1->weight = SplineFunction();
A chain length of 1 would mean a single segment or "bone" is affected. Arms and legs typically have two bones, so you would set e1->chain_length = 2
. The default value is 0 which means all segments right down to the root node are affected.
The target position and rotation are set in local space, where the target position is a 3-dimensional vector type and the rotation is a quaternion.
The e1->weight
parameter ranges from 0.0 to 1.0 and indicates how much influence it has on the tree to be solved. You can make use of this to smoothly transition in and out of IK solutions. For example, you might only want to use IK when your character's foot is touching the ground, but when they are taking a step forwards (lifting their foot off of the ground) you will want to transition from IK to FK until the foot touches the ground again.
Algorithms have two purposes.
- For each effector, segments down to the next algorithm are affected. This means that if you attach an algorithm somewhere in the middle of the tree, then the chain will terminate at the algorithm node rather than the root node, even if
e->chain_length
is longer. This can be used as a tool to help partition your tree into different isolated parts, as a complex character rig will always have more than one IK chain in it. - Algorithms control solver parameters such as the solver type, tolerance, and maximum number of iterations to use.
/* attach an algorithm to the base node */
struct ik_algorithm* a1 = ik_node_create_algorithm(base, IK_TWO_BONE);
In the example we attach an algorithm to the base node, which causes a chain to be created between the effector on the tip node and the algorithm on the base node. The algorithm type is set to IK_TWO_BONE
in the example, but you could also change it to a more general solver such as IK_FABRIK
or IK_MSS
. You can use ik_algorithm_set_type()
to change the algorithm later, however, this requires you to build a new solver before the changes take effect.
You can change the following settings without having to build a new solver at any time:
a1->max_iterations = 20;
a1->tolerance = 1e-3;
a1->features = ...;
Depending on the solver type you may want to tweak these differently. ik_node_create_algorithm()
will initialize the algorithm with sane default values for you.
FABRIK converges extremely quickly. You can sometimes get away with just 5 iterations. More realistically, you need around 20-50 iterations.
MSS takes longer to converge, somewhere around 100-1000 iterations depending on the distribution of mass.
Depending on your world scale, the tolerance should be set to something that is approximately a 10th to a 100th the size of the chain being solved. It is a trade-off between accuracy and solver speed, since the less accurate the result has to be, the earlier the solver can bail.
You can enable and disable feature flags on the algorithm. These are:
- IK_ALGORITHM_CONSTRAINTS: Due to performance reasons, constraint support is disabled by default. If you plan on using constraints you will want to enable this.
-
IK_ALGORITHM_TARGET_ROTATIONS: When enabled, the effectors will try to match the target rotation (specified by
e->target_rotation
) as well as the target position. When disabled, the solver will only try to reach the target's position. -
IK_ALGORITHM_JOINT_ROTATIONS: (Enabled by default) Required for skinned models. When enabled, solvers that work entirely with positions (FABRIK, MSS) will calculate the correct joint rotations as a post processing step. When disabled, these solvers will only calculate positions and
node->rotation
will be left untouched. Solvers that require joint rotations will calculate them anyway (ONE_BONE, TWO_BONE, CCD).
You can enable or disable features at any point by setting or clearing one of the appropriate flags listed above. Here is an example:
a1->flags |= IK_ALGORITHM_CONSTRAINTS; /* We want constraint support */
a1->flags &= ~IK_ALGORITHM_TARGET_ROTATIONS; /* We don't want to match the effector's target rotation */
Once the tree is created and you've attached all of the different bits and pieces, you can build a solver:
/* Now, create the solver */
struct ik_solver* s1 = ik_solver_build(tree);
IK_INCREF(s1); /* again, we have to take ownership of the solver */
This parses the tree, isolates the different chains, allocates specific solvers for each algorithm, and bundles them all up into a single object which you can use to solve all chains at once.