вторник, 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; 

вторник, 24 апреля 2012 г.

Google Protocol Buffer

Не знаю может я такой криворукий, или может не умею искать информацию в интернете, но у меня так и не получилось с экспортировать классы генерируемые GPB в dll, постоянное падение линковки на двух функциях. Это печали сразу по нескольким причинам:
1- На GPB основывалась сериализация моего DataCloud.
2- GPB планировалось использовать для написания формата моделей.
3- GPB мог сохранятся сразу в двух видах, в текстовом и в бинарном, а значит придётся придумывать сразу два формата.
На данный момент решил для бинарного хранения использовать свой формат, а для текстового XML (слава компилятору PugiXML спокойно экспортируется в dll). Казалось бы зачем два формата бери один, но у того и у того есть свои минусы и плюсы. Минусы XML к примеру: он очень долго грузится, он текстовый а значит легко вскрывается, и весит очень много, но для дебага и SVN он подходит идеально, то есть он хорошо подходит на стадии разработки. Бинарный же формат лишён всех минусов XML: он быстро грузится, тяжело вскрывается, и весит много меньше XML, но для этапа разработки он совсем не подходит, с SVN по любому будут проблемы, для отладки данные не посмотришь. Именно поэтому я решил взять оба формата.
up
И так так парой днями раньше я всё таки запилил сериализацию в два этих формата. и само собой формат XML оказался крайне медленным, но зато с свном проблем не будет. Бинарный формат ко всему прочему также поддерживает проверку на целостность блока данных ^_^.

суббота, 14 апреля 2012 г.

Candy engine мнение

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

И так первая цель, за месяц привести движок и редактор к более или менее пригодному для использования виду. Коротко о двиге. Изначально архитектура двига была рассчитана на удобный и открытый доступ к данным, но реализация не понравилась: слишком много ограничений, плохая расширяемость, сильная связность, неустойчивость, слишком много параметров. На данном этапе отвечающий за данные DataManager был сильно изменён и переделан в DataCloude название было выбрано так из-за похожести на облачные сервисы. Менеджер стал лёгок в использовании, лёгок в расширяемости, менее связан, устойчив, и количество параметров сильно уменьшилось, в общем новая система мне так понравилась, что я практически избавился от менеджера ресурсов, всё что он теперь делает так отслеживает сброс девайса и уникальность ресурса. Также новый менеджер стал обладать лёгким механизмом сериализации построенным на GoogleProtocolBuffer. Теперь можно лишь раз настроить ресурсы, данные, связи и сохранив их использовать в любом другом приложении и делается это всего двумя строчками.
c_DataCloud::GetInstance()->SaveToFile("SomeData.dpt");
и
c_DataCloud::GetInstance()->LoadFromFile("SomeData.dpt");

И так на данный момент мы имеем редактор который выглядит так
Но к сожаления, он пока не несёт никакой функциональности и построен на предыдущей версии движка, задача на этот месяц, добавится к движку функциональны ObjectManager и реализовать в редакторе функционал по управлению объектами, то есть Gizmo, Property page, project page, resource page. Хотя бы для базовых объектов: Static mesh, Animated mesh. Сюда соответственно собираюсь выкладывать информацию по продвижению. Надеюсь мне это поможет.

пятница, 13 апреля 2012 г.

AngelScript

Введение.

В процессе рассмотрения LUA и Python я выделил для себя, что LUA является достаточно быстрым, но с немного непривычным синтаксисом. Python же обладает очень простым синтаксисом и массой полезных библиотек, но, к сожалению, он оказался довольно медленным, и его довольно тяжело привязывать к С++. И тут на работе мне подсказали использовать AngelScript, мол, он удобный для связки, быстрее LUA и имеет С-подобный синтаксис. Как только я начал его изучать, я понял, что это тот самый скриптовый язык моей мечты.

aslogo | AngelScript

Превью.

Вот что можно прочитать про этот язык на Википедии:

AngelScript представляет собой движок, в котором приложение может регистрировать функции, свойства и типы, которые могут использоваться в скриптах. Скрипты компилируются в модули. Количество используемых модулей варьрируется в зависимости от нужд. Приложение может также использовать различные интерфейсы для каждого модуля с помощью групп конфигурации. Это особенно полезно, когда приложение работает с несколькими типами скриптов, например, GUI, AI и т.д.

Программа «Hello, world» в простейшем случае выглядит так:

void main()
{
print("Hello world\n");
}

Да, синтаксис языка радует с самого начала. Язык поддерживает как методы функционального программирования, так и ООП. С самого начала он подкупает своей простотой регистрации функций, переменных, типов.

Например, регистрация глобальной переменной:

g_Engine->RegisterGlobalProperty("int SomeVal",&SomeVal);  
где SomeVal — это переменная типа int.

Регистрация глобальной функции:

g_Engine->RegisterGlobalFunction("void Print(string val)", 
asFUNCTION(Print), asCALL_CDECL);
void Print(string val)
{
cout<<val.data();
}

Да, для AngelScript не нужно писать функции биндинга, что является огроменным плюсом в сравнении с другими языками. Для регистрации своих типов придётся написать парочку функций. Фабрику для создания экземпляров и счётчик ссылок, для типа Тип-ссылка, и вызовы конструктора и деструктора, для объекта типа Тип-значение.

Например, у нас есть класс float3, который мы хотели бы зарегистрировать.

// класс счётчик ссылок
class RefC
{
private:
int refC;
public:
RefC(){refC=1;}
void AddRef(){refC++;}
void Release()
{
if(!--refC)
delete this;
}
};
// Класс, который мы хотим зарегистрировать
class float3:public RefC
{
public:
float x;
float y;
float z;

float3(){x=y=z=0;}
void Normalize()
{
float Len=sqrt(x*x+y*y+z*z);
Len=Len?Len:1;
x/=Len;
y/=Len;
z/=Len;
}
}
// Фабрика
float3* Float3FactoryE()
{
return new float3();
}
// Функция вывода на экран
void PrintF3(float3* val)
{
cout<<"x="<<val->x<<",y="<<val->y<<",z="<<val->z;
}

Для этого мы регистрируем объект как Тип-ссылка и указываем ему фабрику, счётчик ссылок, метод и функцию вывода данных на экран, и вот как это выглядит.
g_Engine->RegisterObjectType("float3",0,asOBJ_REF);
g_Engine->RegisterObjectMethod("float3"," void Normalize()",asMETHOD(float3, Normalize),asCALL_THISCALL);
g_Engine->RegisterObjectBehaviour("float3",asBEHAVE_FACTORY,"float3@ new_float3()",asFUNCTION(Float3FactoryE),asCALL_CDECL);
g_Engine->RegisterObjectBehaviour("float3",asBEHAVE_ADDREF,"void AddRef()",asMETHOD(float3,AddRef),asCALL_THISCALL);
g_Engine->RegisterObjectBehaviour("float3",asBEHAVE_RELEASE,"void Release()",asMETHOD(float3,Release),asCALL_THISCALL);
g_Engine->RegisterGlobalFunction("void Print(float3@ val)",asFUNCTION(PrintF3),asCALL_CDECL);
Мы на этом конечно же не остановимся, так как нам нужен доступ к значениям xyz, поэтому их мы тоже должны зарегистрировать, что мы и делаем написав.
g_Engine->RegisterObjectProperty("float3","float x",offsetof(float3,x));
g_Engine->RegisterObjectProperty("float3","float y",offsetof(float3,y));
g_Engine->RegisterObjectProperty("float3","float z",offsetof(float3,z));

Всё предельно просто и понятно. Теперь в скрипте можно написать

float3@ ObjPos;
ObjPos.x=1;
ObjPos.y=2;
ObjPos.z=3;
ObjPos.Normalize();
Print( ObjPos );

Выполнив этот скрипт, мы увидим на экране значение нормализованного вектора.

Особенности.

Меня очень порадовала возможность перегрузки операторов в AngelScript. В С++ для это существует ключевое слово operator и символ оператора. AngelScript для этого использует определённые функции. Вот полный список соответствий.

♦ – opNeg
♦ ~ opCom
♦ ++ opPreInc
♦ -- opPreDec
♦ ++ opPostInc
♦ -- opPostDec
♦ == opEquals
♦ != opEquals
♦ < opCmp
♦ <= opCmp
♦ > opCmp
♦ >= opCmp
♦ = opAssign
♦ += opAddAssign
♦ -= opSubAssign
♦ *= opMulAssign
♦ /= opDivAssign
♦ &= opAndAssign
♦ |= opOrAssign
♦ ^= opXorAssign
♦ %= opModAssign
♦ <<= opShlAssign
♦ >>= opShrAssign
♦ >>>= opUShrAssign
♦ + opAdd opAdd_r
♦ - opSub opSub_r
♦ * opMul opMul_r
♦ / opDiv opDiv_r
♦ % opMod opMod_r
♦ & opAnd opAnd_r
♦ | opOr opOr_r
♦ ^ opXor opXor_r
♦ << opShl opShl_r
♦ >> opShr opShr_r
♦ >>> opUShr opUShr_r
♦ [] opIndex

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

class float3:public RefC
{
public:
float x;
float y;
float z;

float3(){x=y=z=0;}
void Normalize()
{
float Len=sqrt(x*x+y*y+z*z);
Len=Len?Len:1;
x/=Len;
y/=Len;
z/=Len;
}
float3* operator+=(float3* _rval)
{
x+=_rval->x;
y+=_rval->y;
z+=_rval->z;
this->AddRef();
return this;
}
};

Осталось только зарегистрированный новый метод.

g_Engine->RegisterObjectMethod("float3", "float3@ opAddAssign(float3@ _rval)",
asMETHOD(float3, operator+=), asCALL_THISCALL);

И теперь можно спокойно писать так:

float3@ ObjPos;
ObjPos.x=1;
ObjPos.y=2;
ObjPos.z=3;

float3@ ObjOffset;
ObjOffset .x=3;
ObjOffset .y=1;
ObjOffset .z=5;

ObjPos+=ObjOffset ;
Print( ObjPos );
и мы увидим на экране x=4, y=3, z=8.

AngelScript поддерживает свойства. Выглядит это так:

class  MyObj
{
type get_ValueName();
type set_ValueName(type Val);
}
MyObj a;
type tmp=a.ValueName;// вызовется get_ValueName
a.ValueName = tmp; // вызовется set_ValueName

Также свойства поддерживаются для оператора индекса:

class MyObj
{
float get_opIndex(int idx) ;
void set_opIndex(int idx, float value);
}
MyObj a;
float val=a[1];// вызовется get_opIndex
a[2]=val;// вызовется set_opIndex

Проблемы.

Во время изучения языка AngelScript, появилось несколько проблем, решение которых отняло у меня довольно много времени. Вот список проблем, о которых я хотел бы рассказать:

♦ Регистрация перегруженных функций.
♦ Регистрация перегруженных методов.
♦ Получения адреса на переменную объявленную в классе.

Регистрация перегруженных функций.

Очень часто возникает необходимость объявить перегруженную функцию для удобства чтения и понимания кода.

Например:

void Print(string val)
{
cout<<val.data();
}

void Print(int val)
{
cout<<val;
}

void Print(float val)
{
cout<<val;
}

void Print(float3* val)
{
cout<<"x="<<val->x<<",y="<<val->y<<",z="<<val->z;
}

Обычная регистрация вызовет ошибку.

g_Engine->RegisterGlobalFunction("void Print(string val)",
asFUNCTION(Print), asCALL_CDECL); // Ошибка

Так как компилятор не понимает адрес какой из четырёх функций нужно передать, то на ум приходит два решения:

♦ Создать typedef нужной функции и осуществить приведение типов.
♦ Создать переменную указатель на нужную функцию и передать его.

вот как выглядит второе решение:

void (*PrintS)(string val)=&Print;
void (*PrintI)(int val)=&Print;
void (*PritnF)(float val)=&Print;
void (*PrintF3)(float3* val)=&Print;

g_Engine->RegisterGlobalFunction("void Print(string val)",asFUNCTION(PrintS),asCALL_CDECL);
g_Engine->RegisterGlobalFunction("void Print(int val)",asFUNCTION(PrintI),asCALL_CDECL);
g_Engine->RegisterGlobalFunction("void Print(float val)",asFUNCTION(PritnF),asCALL_CDECL);
g_Engine->RegisterGlobalFunction("void Print(float3@ val)",asFUNCTION(PrintF3),asCALL_CDECL);

Регистрация перегруженных методов.

С данной проблемой вы столкнётесь, если захотите зарегистрировать оператор присваивания. Давайте для примера модифицируем наш класс.

class float3:public RefC
{
public:
float x;
float y;
float z;
float3(){x=y=z=0;}
void Normalize()
{
float Len=sqrt(x*x+y*y+z*z);
Len=Len?Len:1;
x/=Len;
y/=Len;
z/=Len;
}

float3* operator+=(float3* _rval)
{
x+=_rval->x;
y+=_rval->y;
z+=_rval->z;
this->AddRef();
return this;
}
float3* operator=(float3* _rval)
{
x=_rval->x;
y=_rval->y;
z=_rval->z;
this->AddRef();
return this;
}
};

Обычная регистрация вызовет ошибку.

g_Engine->RegisterObjectMethod("float3", "float3@ opAssign(float3@ _rval)",asMETHOD(float3, operator=), asCALL_THISCALL); // Ошибка

Решения, которые подходят для функций, тут не подойдут. Для решения проблемы надо внимательно взглянуть на макрос asMETHOD он выглядит так:
#define asMETHOD(c,m) asSMethodPtr::Convert((void (c::*)())(&c::m)) 

Соответственно, чтобы нам зарегистрировать метод, нужно конвертировать его. Тогда наша регистрация будет выглядеть так:
g_Engine->RegisterObjectMethod("float3", "float3@ opAssign(float3@ _rval)",
asSMethodPtr::Convert((float3* (float3::*)(float3*))(&float3::operator=)),
asCALL_THISCALL);

Получения адреса на переменную объявленную в классе.

Если с глобальными переменными дела обстоят предельно просто, то с переменными в пределах класса всё немного сложнее. Стандартные средства не позволяют получить ID переменной по её имени или объявлению, поэтому для этого необходимо пройтись по всем переменным в классе самому и сравнить имена. Для поиска адреса по имени переменной нам потребуется само имя и указатель на экземпляр класса скрипта.

Функция будет выглядеть так:

void* GetPropAddress(const char* Name,asIScriptObject* ScriptObject)
{
for(int i=0;iGetPropertyCount();i++)
if(!strcmp(ScriptObject->GetPropertyName(i),Name))
return ScriptObject->GetAddressOfProperty(i);
return 0;
}

Вот и всё, что мне хотелось бы рассказать, да я не осветил моменты касающиеся JIT и Шаблонов, но это только потому, что ещё не разбирался с этим. По мере изучения буду обновлять статью.

Angel Script Example

Полезные ссылки.

Сайт разработчиков http://www.angelcode.com/
SVN Репозиторий на WIP https://angelscript.svn.sourceforge.net/svnroot/angelscript/trunk
Русский мануал http://13d-labs.com/angelscript_manual/main.html
Мануал на английском http://www.angelcode.com/angelscript/sdk/docs/manual/index.html
JIT Компилятор https://github.com/BlindMindStudios/AngelScript-JIT-Compiler