Скелетная анимация в первый раз

Введение


Доброго времени суток. Я пишу статью тут впервые, и цель моя очень проста, я хочу поделиться своим видением скелетной анимации и имею огромное желание получить критику и помошь. Все создание этой системы было путём проб и ошибок, я не нашёл никаких книг о том как это делать, что это вообще из себя представляет и как пользоваться. Но сейчас я зашёл в тупик, выход из которого хотелось бы найти именно тут.

Что я использовал?

  • MSVS 2013 Professional — это моя основная IDE
  • Qt Creator — при помощи этого я писал программу для редактирования анимации
  • Qt 5.1 — как GUI для редактора анимации
  • SDL 2 — ввод/вывод команд и отображения графики в конечной программе(где и должна происходить анимация)
  • OpenGL 2.1 — для отображения графики
  • GLEW — для подключения расширений OpenGL
  • Boost — для использования таймера
  • Protobuf Google — Для сохранения в файлы анимации
  • С++ — ЯП на котором я все и делал


С чего начать?


Я решил что для начала мне нужно определить структуры, и мне будет так легче понять как они должны затем работать.
Для начала покажу мои #define'ы:
#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


и вот к таким структурам я в итоге пришёл:
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 — Это значения по умолчанию. Углы я решил хранить в радианах, от 0 до 6.28. И с этим у меня потом возникло куча проблем. Но об этом немного позже.
aX, aY, aAngle — Это параметры анимации. Они обозначают наколько кадр уже интерполировался. Тоесть прошедшая анимация записывается именно в них. Например анимация длится 900 ms(TIME), сейчас происходит 450 ms(NOW), но анимация не может оновляться каждую милисекунду, поэтому если раньше было допустим 400 ms, а щас 450, и интерполируем X и представим, что есть 2 ключа в первом X = 50(KEY1), во втором 100(KEY2), то конечное значение X считаю так:
_Joint.x += (KEY2 - KEY1) / TIME * NOW - _Joint.aX;
_Joint.aX = (KEY2 - KEY1) / TIME * NOW;
22,3 += (100-50) / 900 * 450 - 22,3
22,3 — Это X кости с учетом того что когда время (NOW) было равно 0, то X, тоже был равен нулю

KeyData и Keyframe — это структуры именно для анимации


	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;


Я долго думал, как можно организовать поиск костей для интерполяции. Поиск в дереве довольно затранный процесс для анимации на мой взгляд. У меня в голове было 2 решения. Первое решение, это использовать идексы для обозначения ветвей и это немного бы ускорило поиск, т.к. это была бы обычная адресация. Но выбрал я второе решение. На мой взгляд обновить дерево целиком куда проще и быстрее чем какие-то яего отдельные части. Очень просто, представим у нас есть главная кость, которая имеет по 10 детей, которые в свою очередь имеют ещё по 5. В итоге у нас всего 51 кость(кстати рутовый сустав у меня не видим вообще, изменение его x и y привдит к изменению положения всего обьекта).
Нам понадобится 10 циклов по 5 итераций, тоесть всего 50 итераций. И в итоге получим 51 интерполяцию при обновлении всего дерева. В противном случае, если мы используем поиск кости, то для поиска последней добавленной кости потребуется 50 итераий, для предпоследней 49 и так далее, в сумме куда больше чем обновление всего дерева. _Keyframe имеющий индекс 0, хранит время этой анмации, все его дети имеют параметр времени 0.

Animation — хранит список ключей

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

keyNumber — это индекс проигрываемой анимации, по умолчанию равен нулю. Все ключи анимации хранятся в массиве key, в хронологическом порядке. Все способы реализации, создания удаления структур и добавления в них новых обьектов я скрою чтоб бы не нагромождать тут много кода. Но покажу функции для самой анимации.
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;
}


Когда функция возвращает true, то таймер сбрасывется на ноль.(boost::timer)

И заключительная функция:
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);
	}
}


Вот такая скелетная анимация и работает она довольно не плохо. Туча проблем возникла на этапе создания программы в Qt, и самая первая это движения костей мышью, да и вообще движение костей. Длина вращения кости по кругу 6.28, тоесть 2*Pi или 360 градусов. Но получается движения происходит лишь в одну сторону. Если мы получаем допустим при расчётах 4.47, но хотим движение в другую сторону, в принципи можем сделать так:
4,47-6.28=-1,81
Вроде движение в другую сторону. Но это очень не удобно. так же я пробовал искать оптимальный путь до цели вращения. Если мы представим допустим наша позиция 1,57(A), цель наша 5.57(B), то:
negAngle = B-A;
posAngle = (B-6.28)-A;
if(fabs(posAngle) > fabs(negAngle))
{ /*Выбираем меньшее*/ }


Но, оптимальный путь нужен не всегда. Поэтому это тоже не совсем подходит. Я вот подумаю над двумя вариантами решения этих проблем. Первый вариант — это расчитвать строгую интерполяцию. Тоесть ключём будет являться именно кость в анимации. Второй способ это задавать ключём только движение, ничего более. Но по правде говоря я попал в тупик и не знаю, что делать дальше. Программа Qt работает с относительным успехом, но сама анимация вроде не плоха, но пока я писал GUI редактор я понимал её неэффективность и громоздкость. Как вообще пределать скелетную анимацию к физике(я горю желанием присоединения к Box2d), но как сделать это не имею ни малейшего представления. Я решил порлностью все переделать, но хочу услышать советы, критику, что и как я сделал неправильно. Ещё раз повторю, это моя система, сделана не по примеру, это мой взгляд на скелетную анимаию, не судите строго. Спасибо за внимание!

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 9

    +5
    Совет №0: прогоните текст через word или любой другую доступную систему проверки орфографии. Он будет вызывать меньше раздражения — и как следствие, больше желания помочь.
      0
      Спасибо за статью, довольно интересный подход. Привязка скелетной анимации к физике всегда довольно непростая задача, и всегда очень специфичная. Обычно включает себя упрощения якобианов и магию. Игры это иллюзия, так что магии много :)
      Можно для начала реализовать ragdoll для своей скелетной модели. По этому топику можно найти множество информации, попроще что бы, советую доклад о Diablo 3 Ragdolls с GDC2012.
      Сама Ваша модель довольно наивна, но, будет работать. Современные анимации стараются считать больше (если не полностью) на GPU, например.
      Что бы посмотреть другие примеры, есть множество источников. Для простоты — есть Spine, редактор 2д скелетных анимаций. Он с открытым форматом и множеством реализаций клиентской стороны с открытым кодом. Оттуда можно почерпнуть знания. Если хочется чего-то серьезного, всегда есть серезные проекты, например Unreal 4, за 20$ можно получить полный исходный код.
        0
        Прикольно, это 2D скелетка? Давно хотел попробовать наряду с имитацией освещения в 2D. Только вот в статье нет ни строчки на C++ :-)
          0
          Ты прав, по большей части это C, но все эти структуры завернуты в класс. Я просто не стал сдесь выкладывать весь код, для экономии места.
          0
          Брр. Как насчет кватернионов?
            0
            В 2D анимации смысла в кватернионах нет.
              0
              Очень аргументировано. Почему в 3D смысл есть, а в 2D почему-то нет? Нет никакой разницы, а если вы хотите сэкономить — в двухмерном случае кватернион все равно превратится в обычное комплексное число.
            +2
            Слишком сложно.
            На мой взгляд было бы логичнее сделать следующее:
            1) Перевести все трансформации на квартернионы (либо с позицией, либо двойные), как писал ZimM. В случае с 2D вся их математика значительно упрощается (для ориентации достаточно zw координат).
            2) Joint должен хранить:
            — индекс родительской кости, и -1 в случае если это root
            — имя или хеш имени, для реиндексации (если анимация должна подходить для другого меша, всяких обвесов или оружия)
            — инвертированную базовую трансформацию в которой он был прикреплён к мешу или объекту
            3) Анимация должна хранить:
            — информацию о скорости проигрывания (FPS), количестве кадров в анимации, количестве задействованных Joint'ов
            — индекс Joint'а и все его KeyFrame (в виде кватернион+позиция, или двойных кватернионов, что в случае с 2D звучит страшнее чем на самом деле)
            4) Для модели хранить стек анимаций, каждая анимация которого имеет динамически изменяемый «вес». Это позволит делать так называемый Merging & Blending анимаций друг с другом (бег + махание руками = бежим и машем)
            5) Каждая вершина геометрии (меш) должна хранить индексы влияющих Joint'ов (2 вполне достаточно) и их веса. В случае если достаточно одного влияния, вес будет не нужен (всегда = 1) и можно будет избавиться от инвертированной трансформации из п. 2 применив её к самой геометрии перед сохранением (оптимизация runtime расчётов).
            6) Геометрия должна хранить индексы и имена влияющих костей для реиндексации, выработка стандарта имён в скелете позволит применять одну и ту же анимацию для различных мешей, разных пропорций.
            7) В итоге, пробежавшись по всем Joint'ам скелета, собрав итоговые интерполированнные трансформации для текущего кадра в стеке, получишь массив итоговых трансформаций, который необходимо умножить на соответствующие инвертированные трансформации из п. 2 (если не используется оптимизация п. 5), и передать всё это добро в шейдер. Далее всё весьма тривиально.
            Может что-то забыл, но как-то так…
              0
              В своё время пришлось перечитывать несколько источников чтобы собрать это всё воедино. Нигде не встречал материалов, чтобы рассказали сразу про всё и понятно, тем более на русском.
              Может этот пост поможет победить лень и поделиться своим опытом с webgl по данной теме…

            Only users with full accounts can post comments. Log in, please.