Многие новички, которые только начинают постигать скелетку, очень часто не понимают как она работает (даже при наличии многих статей на данном ресурсе) и что для её работы нужно, собственно им я и хотел бы объяснить на пальцах как работает скелетная анимация.
Ингредиенты.
Любая скелетная анимация основана на шести неизменных составляющих.
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 ];
После выполнения этих процедур остаётся лишь перенести нашу кость в конец кости предка, который к этому моменту уже должен был пройти эту процедуру, эта стадия идентична второму пункту.
Теперь массив матриц костей текущего кадра готов, осталось всего лишь домножить каждую вершину на матрицу с учётом веса, выглядит это так.
//Код из шейдера. 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 матрицу между последним используемым кадром из предыдущей анимации и значением кадра из текущей анимации. Сразу хочу предупредить, что проигрыватель анимации нужно отделить от модели, чтобы одну и туже модель можно было ставить в разные кадры и разные места. Ну и под конец небольшой концепт код.
Спасибо за внимание.