Показаны сообщения с ярлыком Статьи. Показать все сообщения
Показаны сообщения с ярлыком Статьи. Показать все сообщения

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

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

пятница, 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