Многие новички, которые только начинают постигать «скелетку», очень часто не понимают как она работает (даже при наличии многих статей на данном ресурсе) и что для её работы нужно, собственно им я и хотел бы объяснить на пальцах как работает скелетная анимация.
Любая скелетная анимация основана на шести неизменных составляющих.
1 — Индексы костей в вершине, обычно их бывает не больше четырёх.
2 — Веса костей в вершине, их число совпадает с количеством индексов и сумма всех весов должна быть равна единице.
3 — Список костей, это простой массив с названием костей, каждый индекс кости из вершины соответствует кости из этого массива, поэтому этот массив ни в коем случае нельзя перестраивать.
4 — Bind pose — это поза в которой была заскинена модель, внутри файла это выглядит как массив обратных матриц на каждую кость и одна матрица для всей модели.
5 — Иерархия костей, это описание зависимостей костей друг от друга, у каждой кости может быть не больше одного предка, но детей может быть сколько угодно. Обычно вместе с иерархией в форматах пишется базовое положение кости.
6 — Сама анимация, обычно представляет из себя массив времени, где каждый элемент это время ключа, и массив матриц на каждую кость.
Итак, при чтении формата мы должны достать четыре компонента.
- Модель, которая содержит в вершинах индексы и веса костей.
- Массив костей и их 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 ];
Да-да, именно последний пункт, а всё дело в последовательности перемножения матриц. После того как кость была перенесена, она должна принять позу которая записана в анимации, эта процедура идентична первому пункту.
После выполнения этих процедур остаётся лишь перенести нашу кость в конец кости предка, который к этому моменту уже должен был пройти эту процедуру, эта стадия идентична второму пункту.
Теперь массив матриц костей текущего кадра готов, осталось всего лишь домножить каждую вершину на матрицу с учётом веса, выглядит это так.
Это только для позиции, таким же способом нужно обновить нормали, тангенты и бинормали.
Собственно говоря это всё, из простейших плюшек можно также добавить интерполяцию между различными анимациями, этот процесс вклинивается между пунктами 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();