⬆️ ⬇️

Skeletal animation for the first time

Introduction



Good day. I am writing an article here for the first time, and my goal is very simple, I want to share my vision of skeletal animation and have a great desire to receive criticism and help. All the creation of this system was a trial and error, I did not find any books about how to do it, what it is all about and how to use it. But now I am at a dead end, I would like to find a way out of it here.



What did I use?






Where to begin?



I decided that for a start I need to define structures, and it will be easier for me to understand how they should then work.

First, I’ll show my define definitions:

#define M_PI 3.1415926535 #define M_PI_2 M_PI/2 #define M_PI_3 M_PI/3 #define M_PI_4 M_PI/4 #define RAD1 M_PI/180.0f #define DEG1 180.0f/M_PI #define RAD2DEG(rad) rad*DEG1 #define DEG2RAD(deg) deg*RAD1 #define MAX_CHILDREN_COUNT 10 #define MAX_KEYFRAME_COUNT 20 




and here I finally came to such structures:

Joint is a joint


  typedef struct _Joint { //inheritanse _Joint* root; /* Pointer to root element. All elements have it. */ _Joint* parent; /* Pointer to parent */ //simple joints variables float x, y; /* Position */ float angle; /* Angle of rotate. In radians!!! */ uint8_t level; /* Level of hierarchy. Root have 0, next level +1 */ float dX, dY; /* Default Position */ float dAngle; /* Default Angle */ float aX, aY; /* Animation Position */ float aAngle; /* Animation Angle */ //children uint8_t childCount; /* Number of children */ _Joint* child[MAX_CHILDREN_COUNT]; /* Array of children */ //index uint16_t indexCount; /* Last number for index. Only Root have the var. */ uint16_t index; /* Unique index of joint */ } S_Joint; 


')

dX, dY, dAngle - These are the default values. I decided to store the angles in radians, from 0 to 6.28. And with that, I later had a lot of problems. But more about that later.

aX, aY, aAngle - These are the animation parameters. They indicate that the frame has already been interpolated. Ie the last animation is recorded in them. For example, the animation lasts 900 ms ( TIME ), 450 ms ( NOW ) now occurs, but the animation cannot be updated every millisecond, so if it used to be 400 ms, but 450, we interpolate X and imagine that there are 2 keys in the first X = 50 ( KEY1 ), in the second 100 ( KEY2 ), then I consider the final value of X as follows:

_Joint.x += (KEY2 - KEY1) / TIME * NOW - _Joint.aX;

_Joint.aX = (KEY2 - KEY1) / TIME * NOW;

22,3 += (100-50) / 900 * 450 - 22,3

22.3 - This is X bone given that when time ( NOW ) was 0, then X was also zero.



KeyData and Keyframe are the structures for animation.


 typedef struct _KeyData { float x, y, angle; } S_KeyData; typedef struct _Keyframe { S_KeyData data; /* data of Joint */ uint16_t time; /* ~32 sec.(32768 ms) is maximum time for animation. Only Root have it */ uint16_t index; /* Index of joint, which we want interpolate */ _Keyframe* parent; uint8_t childCount; /* Number of children */ _Keyframe* child[MAX_CHILDREN_COUNT]; } S_Keyframe; 




I have been thinking for a long time how to organize a bone search for interpolation. Searching the tree is a rather obscure process for animation in my opinion. I had 2 solutions in my head. The first solution is to use idedes to designate branches, and this would speed up the search a bit, since this would be normal addressing. But I chose the second solution. In my opinion, updating a whole tree is much easier and faster than some separate parts of it. Very simply, imagine we have a main bone, which has 10 children each, which in turn have 5 more. As a result, we have only 51 bones (by the way, I don’t see the root joint at all, changing it x and y will lead to a change the position of the whole object).

We will need 10 cycles of 5 iterations, that is, only 50 iterations. And in the end we get 51 interpolation when updating the entire tree. Otherwise, if we use a bone search, then searching for the last added bone will require 50 iterations, for the penultimate 49, and so on, in total, much more than updating the entire tree. _Keyframe having an index of 0 , stores the time of this input, all its children have the time parameter 0.



Animation - stores a list of keys


  typedef struct _Animation { uint8_t keyNumber; uint8_t keyCount; S_Keyframe* key[MAX_KEYFRAME_COUNT]; } S_Animation; 


keyNumber is the index of the animation being played, default to zero. All animation keys are stored in the key array, in chronological order. I will hide all ways of implementing, creating deleting structures and adding new objects to them so as not to clutter up a lot of code here. But I will show the functions for the animation itself.

 bool doAnimation(S_Joint *root, S_Animation *anim, uint16_t time) { if (!root) return false; if (!anim) return false; bool timeOut = true; for (int i = 0; i < anim->keyCount; i++) { if (time < anim->key[i]->time) //search keyframes for interpolation { //  1200,  2400, 2400-1200=1200 uint16_t mtime = anim->key[i]->time - anim->key[i - 1]->time; //nowTime is 1560, mtime = 1200(ERROR), 1560 - 1200(last key)=360(realtime) uint16_t nowTime = time - anim->key[i - 1]->time; if (i != anim->keyNumber) //    keyNumber,        { setDefaultAnimTree(root); //set to 0.0 animation changes(aX,yX,aAngle) } anim->keyNumber = i; //   doInterpolate(root, anim->key[i - 1], anim->key[i], mtime, nowTime); timeOut = false; break; } } if (timeOut == true) { setDefaultTree(root); //          } return timeOut; } 




When the function returns true, the timer is reset to zero. (Boost :: timer)



And the final function:

 void doInterpolate(S_Joint* root, S_Keyframe* key1, S_Keyframe* key2, uint16_t time, uint16_t nowTime) { if (root->index != key2->index) return; float x = (key2->data.x - key1->data.x) / time * nowTime; //        float y = (key2->data.y - key1->data.y) / time * nowTime; float angle = (key2->data.angle - key1->data.angle) / time * nowTime; root->x += x - root->aX; //root->aX -        ,        root->y += y - root->aY; root->angle += angle - root->aAngle; root->aX = x; //     ,     x,y,angle root->aY = y; root->aAngle = angle; //     for (int i = 0; i < root->childCount; i++) { doInterpolate(root->child[i], key1->child[i], key2->child[i], time, nowTime); } } 




This is the skeleton animation and it works quite well. A cloud of problems arose at the stage of creating a program in Qt, and the very first is the movement of bones with a mouse, and indeed the movement of bones. The length of rotation of the bone in a circle is 6.28, ie 2 * Pi or 360 degrees. But it turns out the movement happens only in one direction. If we get let's say at calculations 4.47, but we want to move in the other direction, in principle we can do this:

4,47-6.28=-1,81

Like a move to the other side. But it is not very convenient. I also tried to find the best way to the goal of rotation. If we imagine our position is admitted 1.57 (A), our goal is 5.57 (B), then:

 negAngle = BA; posAngle = (B-6.28)-A; if(fabs(posAngle) > fabs(negAngle)) { /* */ } 




But the optimal path is not always needed. Therefore, it is also not quite suitable. I'll think about two options for solving these problems. The first option is to calculate strict interpolation. Ie, the key in the animation will be the key. The second way is to set the key only movement, nothing more. But in truth, I was stumped and I do not know what to do next. The Qt program works with relative success, but the animation itself seems not bad, but as long as I wrote the GUI editor I understood its inefficiency and cumbersomeness. How do I limit skeletal animation to physics (I’m eager to join Box2d), but I don’t have any idea how to do this. I decided to redo everything, but I want to hear advice, criticism, what and how I did wrong. Once again, this is my system, it was not done by example, this is my view on skeletal animation, do not judge strictly. Thanks for attention!

Source: https://habr.com/ru/post/219509/



All Articles