Войти
ПрограммированиеСтатьиГрафика

Скелетная анимация на пальцах.

Автор:

Многие новички, которые только начинают постигать «скелетку», очень часто не понимают как она работает (даже при наличии многих статей на данном ресурсе) и что для её работы нужно, собственно им я и хотел бы объяснить на пальцах как работает скелетная анимация.

Ингредиенты.
Процесс.

Ингредиенты.

Любая скелетная анимация основана на шести неизменных составляющих.

1 — Индексы костей в вершине, обычно их бывает не больше четырёх.
2 — Веса костей в вершине, их число совпадает с количеством индексов и сумма всех весов должна быть равна единице.
3 — Список костей, это простой массив с названием костей, каждый индекс кости из вершины соответствует кости  из этого массива, поэтому этот массив ни в коем случае нельзя перестраивать.
4 — Bind pose — это поза в которой была заскинена модель, внутри файла это выглядит как массив обратных матриц на каждую кость и одна матрица для всей модели.
5 — Иерархия костей, это описание зависимостей костей друг от друга, у каждой кости может быть не больше одного предка, но детей может быть сколько угодно. Обычно вместе с иерархией в форматах пишется базовое положение кости.
6 — Сама анимация, обычно представляет из себя массив времени, где каждый элемент это время ключа, и массив матриц на каждую кость.

Animation | Скелетная анимация на пальцах.

Итак, при чтении формата мы должны достать четыре компонента.
- Модель, которая содержит в вершинах индексы и веса костей.
- Массив костей и их bind pose.
- Иерархия костей.
- Анимация.

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

struct Bone
{
   Bone *Parent; // Указатель на предка, эта информация бывает полезна.
   Bone **Childs; //  Массив указателей на детей.

   float4x4 Base; // Матрица базового положения.
   float4x4 Release; // Релизная матрица, будет использоваться в анимации.
};

Процесс.

Как должна происходить анимация. (Запомните, порядок перемножения матриц очень важен.)

1 — Мы проходим по ключам анимации и находим два ключа между которыми расположено искомое время. И для каждой кости в матрице Release записываем интерполированное значение. Псевдокод:

float DownTime = Time[ i ];
float UpTime = Time[ i + 1 ];
float LerpKoef = ( CurrentTime - DownTime ) / ( UpTime - DownTime );
Bone.Release = Lerp( Key[ i ], Key[ i + 1 ], LerpKoef ); 
//Если же кадр найти не удалось, используется базовое значение кости.
Bone.Release = Base; 

2 — После заполнения Release матрицы всех костей мы должны обновить иерархию.

Bone.Release = Release *  Parent-> Release; // при условии, что есть предок и он уже обновлён.
for( int i = 0; i < ChildsCount; i++ )
    Childs[i]->Update(); // Вызываем обновление у детей.

3 — Домножаем  Release на матрицу из bind pose и кладём её в релизный массив.

Final[ i ] =  ModelBindPose * Offset[ i ] * Bone[ i ];

Если представить весь этот процесс в голове, то он будет выглядеть так. Представим кость в виде реальной косточки. Расположим начало этой косточки в (0, 0, 0), в коде у нас за это отвечает

Final[ i ] =  ModelBindPose * Offset[ i ] * Bone[ i ];

Да-да, именно последний пункт, а всё дело в последовательности перемножения матриц. После того как кость была перенесена, она должна принять позу которая записана в анимации, эта процедура идентична первому пункту.

После выполнения этих процедур остаётся лишь перенести нашу кость в конец кости предка, который к этому моменту уже должен был пройти эту процедуру, эта стадия идентична второму пункту.

Теперь массив матриц костей текущего кадра готов, осталось всего лишь домножить каждую вершину на матрицу с учётом веса, выглядит это так.

 //Код из шейдера.

 float4 pos = mul( Input.Position, skinMat[ Input.Index.x ] ) * Input.Weight.x;
 pos += mul( Input.Position, skinMat[ Input.Index.y ] ) * Input.Weight.y;
 pos += mul( Input.Position, skinMat[ Input.Index.z ] ) * Input.Weight.z; 
 pos += mul( Input.Position, skinMat[ Input.Index.w ] ) * Input.Weight.w; 

 pos = mul( pos, world );
 Output.Position = mul( pos, matViewProjection ); 

Это только для позиции, таким же способом нужно обновить нормали, тангенты и бинормали.

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

AnimationPlayer Player =  AnimationPlayer( SomeCoolModel ); 
Player.Play( "Walk", LOOPED );
Player.ChangingTime( 100 ); // время для интерполяции между анимациями.
........
Player.Play( "Run", LOOPED ); 
Player.Update( DeltaTime );
........
SomeCoolModel.Shader.SetMatrixArray("Skin", Player.Frame(), Player.BonesCount());
SomeCoolModel.Draw();

Спасибо за внимание.

Demo

#графика, #анимация, #скелетная анимация

26 декабря 2012 (Обновление: 15 фев 2013)

Комментарии [59]