вторник, 25 декабря 2012 г.

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

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

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

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

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

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 матрицу между последним используемым кадром из предыдущей анимации и значением кадра из текущей анимации. Сразу хочу предупредить, что проигрыватель анимации нужно отделить от модели, чтобы одну и туже модель можно было ставить в разные кадры и разные места. Ну и под конец небольшой концепт код.
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();

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

воскресенье, 23 декабря 2012 г.

Simple Style Format

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

 NodName = "Some Text" // Узел может иметь значение и вложенные узлы
 {
    Nod = 1; // Целочисленная запись
    Nod = 2.5; // Дробная
    Nod = "Text"; // Текст по определению является многострочным, сохраняет все переносы
    Nod = 0xFABC89D2F3 ;// Бинарные данные
    Nod = !SomeRaw; // Псевдоним для бинарных данных

    // Массив с смешанными значениями
    Nod = [ "Text", 1, 2.5,  0xFABC89D2F3 , !SomeRaw, ArrNode = "text" { nod = 1;} ]  
    {
       SubNod = "Text";
    }
 }
 // В документе может быть сколько угодно начальных узлов, нет ограничения как в XML
 NodName = !SomeRaw;

 // Псевдоним для бинарных данных всегда начинается с !, не может находится внутри узла
 !SomeRaw = 0xFABC89D2F3;