- PVSM.RU - https://www.pvsm.ru -
Всем привет!
Недавно перед нами встала задача добавления в проект скелетной анимации. По совету коллег мы обратили внимание на Spine [1].
После того, как стало понятно, что возможности редактора удовлетворяют нашим нуждам (здесь [2] есть обзор редактора анимаций), мы стали вставлять Spine в наш С++ движок.
«Общие» исходники редактора на С лежат здесь [3]. Исходники при компиляции выдают 3 ошибки – при интеграции надо реализовать 3 функции. Они будут описаны ниже.
Сэкспортированые данные (здесь [4]можно скачать графический редактор и несколько сэкспортированных анмиаций для етста) состоят из json-файла анимации, текстуры (атласа) и файла описания атласа.
Начнем с загрузки текстурного атласа. Для этого напишем небольшой класс- wrapper, который будет загружать и выгружать текстурные атласы в формате Spine.
// объявление класса
class SpineAtlas
{
public:
SpineAtlas(const std::string& name);
~SpineAtlas();
private:
std::string mName;
spAtlas* mAtlas;
};
// загрузка атласа, store::Load и store::Free – функции движка, загружающие файл в память и освобождающие память соответственно.
SpineAtlas::SpineAtlas(const std::string& name) :
mName(name), mAtlas(0)
{
int length = 0;
const char* data = (const char*)store::Load(name + ".atlas", length);
if (data)
{
mAtlas = spAtlas_create(data, length, "", 0);
store::Free(name + ".atlas");
}
}
// выгрузка атласа
SpineAtlas::~SpineAtlas()
{
spAtlas_dispose(mAtlas);
}
Функция spAtlas_create из конструктора SpineAtlas вызывает функцию _spAtlasPage_createTexture, которая должна быть переопределена при интеграции Spine в движок. Здесь же определим и парную ей функцию _spAtlasPage_disposeTexture.
extern "C" void _spAtlasPage_createTexture(spAtlasPage* self, const char* path)
{
Texture* texture = textures::LoadTexture(path);
self->width = texture->width;
self->height = texture->height;
self->rendererObject = texture;
}
extern "C" void _spAtlasPage_disposeTexture(spAtlasPage* self)
{
Texture* texture = (Texture*)self->rendererObject;
render::ReleaseTexture(texture);
}
Функция textures::LoadTexture загружает текстуру из файла по указанному пути. render::ReleaseTexture – платформозависимая выгрузка текстуры из памяти.
Простейший wrapper для Spine анимации выглядит следующим образом.
// объявление класса
class SpineAnimation
{
public:
SpineAnimation(const std::string& name);
~SpineAnimation();
void Update(float timeElapsed);
void Render();
void Play(const std::string& skin, const std::string& animation, bool looped);
void Stop();
void OnAnimationEvent(SpineAnimationState* state, int trackIndex, int type, spEvent* event, int loopCount);
private:
spAnimation* GetAnimation(const std::string& name) const;
void FillSlotVertices(Vertex* points, float x, float y, spSlot* slot, spRegionAttachment* attachment);
std::string mName;
std::string mCurrentAnimation;
SpineAtlas* mAtlas;
spAnimationState* mState;
spAnimationStateData* mStateData;
spSkeleton* mSkeleton;
bool mPlaying;
};
// загрузка анимации
SpineAnimation::SpineAnimation(const std::string& name) :
mName(name), mAtlas(0), mState(0), mStateData(0), mSkeleton(0), mSpeed(1), mPlaying(false), mFlipX(false)
{
mAtlas = gAnimationHost.GetAtlas(mName);
spSkeletonJson* skeletonJson = spSkeletonJson_create(mAtlas->GetAtlas());
spSkeletonData* skeletonData = spSkeletonJson_readSkeletonDataFile(skeletonJson, (name + ".json").c_str());
assert(skeletonData);
spSkeletonJson_dispose(skeletonJson);
mSkeleton = spSkeleton_create(skeletonData);
mStateData = spAnimationStateData_create(skeletonData);
mState = spAnimationState_create(mStateData);
mState->rendererObject = this;
spSkeleton_update(mSkeleton, 0);
spAnimationState_update(mState, 0);
spAnimationState_apply(mState, mSkeleton);
spSkeleton_updateWorldTransform(mSkeleton);
}
// выгрузка анимации
SpineAnimation::~SpineAnimation()
{
spAnimationState_dispose(mState);
spAnimationStateData_dispose(mStateData);
spSkeleton_dispose(mSkeleton);
}
// update анимации
void SpineAnimation::Update(float timeElapsed)
{
if (IsPlaying())
{
spSkeleton_update(mSkeleton, timeElapsed / 1000); // timeElapsed - ms, Spine использует время в секундах
spAnimationState_update(mState, timeElapsed / 1000);
spAnimationState_apply(mState, mSkeleton);
spSkeleton_updateWorldTransform(mSkeleton);
}
}
// отрисовка
void SpineAnimation::Render()
{
int slotCount = mSkeleton->slotCount;
Vertex vertices[6];
for (int i = 0; i < slotCount; ++i)
{
spSlot* slot = mSkeleton->slots[i];
spAttachment* attachment = slot->attachment;
if (!attachment || attachment->type != SP_ATTACHMENT_REGION)
continue;
spRegionAttachment* regionAttachment = (spRegionAttachment*)attachment;
FillSlotVertices(vertices], 0, 0, slot, regionAttachment);
texture = (Texture*)((spAtlasRegion*)regionAttachment->rendererObject)->page->rendererObject;
}
}
// заполнение одной вершины в формате triangle list
// формат структуры Vertex: xyz – координаты, uv – текстурные координаты, с - цвет
void SpineAnimation::FillSlotVertices(Vertex* points, float x, float y, spSlot* slot, spRegionAttachment* attachment)
{
Color color(mSkeleton->r * slot->r, mSkeleton->g * slot->g, mSkeleton->b * slot->b, mSkeleton->a * slot->a);
points[0].c = points[1].c = points[2].c = points[3].c = points[4].c = points[5].c = color;
points[0].uv.x = points[5].uv.x = attachment->uvs[SP_VERTEX_X1];
points[0].uv.y = points[5].uv.y = attachment->uvs[SP_VERTEX_Y1];
points[1].uv.x = attachment->uvs[SP_VERTEX_X2];
points[1].uv.y = attachment->uvs[SP_VERTEX_Y2];
points[2].uv.x = points[3].uv.x = attachment->uvs[SP_VERTEX_X3];
points[2].uv.y = points[3].uv.y = attachment->uvs[SP_VERTEX_Y3];
points[4].uv.x = attachment->uvs[SP_VERTEX_X4];
points[4].uv.y = attachment->uvs[SP_VERTEX_Y4];
float* offset = attachment->offset;
float xx = slot->skeleton->x + slot->bone->worldX;
float yy = slot->skeleton->y + slot->bone->worldY;
points[0].xyz.x = points[5].xyz.x = x + xx + offset[SP_VERTEX_X1] * slot->bone->m00 + offset[SP_VERTEX_Y1] * slot->bone->m01;
points[0].xyz.y = points[5].xyz.y = y - yy - (offset[SP_VERTEX_X1] * slot->bone->m10 + offset[SP_VERTEX_Y1] * slot->bone->m11);
points[1].xyz.x = x + xx + offset[SP_VERTEX_X2] * slot->bone->m00 + offset[SP_VERTEX_Y2] * slot->bone->m01;
points[1].xyz.y = y - yy - (offset[SP_VERTEX_X2] * slot->bone->m10 + offset[SP_VERTEX_Y2] * slot->bone->m11);
points[2].xyz.x = points[3].xyz.x = x + xx + offset[SP_VERTEX_X3] * slot->bone->m00 + offset[SP_VERTEX_Y3] * slot->bone->m01;
points[2].xyz.y = points[3].xyz.y = y - yy - (offset[SP_VERTEX_X3] * slot->bone->m10 + offset[SP_VERTEX_Y3] * slot->bone->m11);
points[4].xyz.x = x + xx + offset[SP_VERTEX_X4] * slot->bone->m00 + offset[SP_VERTEX_Y4] * slot->bone->m01;
points[4].xyz.y = y - yy - (offset[SP_VERTEX_X4] * slot->bone->m10 + offset[SP_VERTEX_Y4] * slot->bone->m11);
}
// Глобальный listener для обработки событий анимации
void SpineAnimationStateListener(spAnimationState* state, int trackIndex, spEventType type, spEvent* event, int loopCount)
{
SpineAnimation* sa = (SpineAnimation*)state->rendererObject;
if (sa)
sa->OnAnimationEvent((SpineAnimationState*)state, trackIndex, type, event, loopCount);
}
// проигрывание анимации
void SpineAnimation::Play(const std::string& animationName, bool looped)
{
if (mCurrentAnimation == animationName) // не запускаем анмиацию повторно
return;
spAnimation* animation = GetAnimation(animationName);
if (animation)
{
mCurrentAnimation = animationName;
spTrackEntry* entry = spAnimationState_setAnimation(mState, 0, animation, looped);
if (entry)
entry->listener = SpineAnimationStateListener;
mPlaying = true;
}
else
Stop();
}
// остановка анимации
void SpineAnimation::Stop()
{
mCurrentAnimation.clear();
mPlaying = false;
}
// получение анимации по имени
spAnimation* SpineAnimation::GetAnimation(const std::string& name) const
{
return spSkeletonData_findAnimation(mSkeleton->data, name.c_str());
}
// остановка анимации по завершению
void SpineAnimation::OnAnimationEvent(SpineAnimationState* state, int trackIndex, int type, spEvent* event, int loopCount)
{
spTrackEntry* entry = spAnimationState_getCurrent(state, trackIndex);
if (entry && !entry->loop && type == SP_ANIMATION_COMPLETE)
Stop();
}
Функция spSkeletonJson_readSkeletonDataFile из конструктора SpineAnimaion вызывает функцию _spUtil_readFile. Это последняя из трех функций, которые должны быть реализованы в коде, для интеграции Spine. Она использует malloc в стиле Spine.
extern "C" char* _spUtil_readFile(const char* path, int* length)
{
char* result = 0;
const void* buffer = store::Load(path, *length);
if (buffer)
{
result = (char*)_malloc(*length, __FILE__, __LINE__); // Spine malloc
memcpy(result, buffer, *length);
store::Free(path);
}
return result;
}
При загрузке файла анимации можно указать глобальный scale (SpineAnimation::SpineAnimation).
spSkeletonJson* skeletonJson = spSkeletonJson_create(mAtlas->GetAtlas());
skeletonJson->scale = scale;
Skinning реализуется следующим образом (SpineAnimation::Play):
if (!skinName.empty())
{
spSkeleton_setSkinByName(mSkeleton, skinName.c_str());
spSkeleton_setSlotsToSetupPose(mSkeleton);
}
При проигрывании можно задавать скорость анимации, а также зеркалить ее по горизонтали и/или вертикали (SpineAnimaiton::Update):
if (IsPlaying())
{
mSkeleton->flipX = mFlipX;
mSkeleton->flipY = mFlipY;
spSkeleton_update(mSkeleton, timeElapsed * mSpeed / 1000);
...
}
Выбранную анимацию можно запустить по желаемой траектории. Позиция анимации учитывается при заполнении вертекстов при отрисовке (SpineAnimaiton::Render)
FillSlotVertices(vertices], mPosition.x, mPosition.y, slot, regionAttachment);
Исходники, описанные в этой статье, можно скачать здесь [5]. Для простоты чтения Set/Get функции в них отсутствуют.
PS: При написании этой статьи я нашел 2 небольших ошибки в коде. Почаще пишите статьи на Хабр!
Автор: DenKon
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-3/67289
Ссылки в тексте:
[1] Spine: http://esotericsoftware.com/
[2] здесь: http://habrahabr.ru/post/207904/
[3] здесь: https://github.com/EsotericSoftware/spine-runtimes/tree/master/spine-c
[4] здесь : http://esotericsoftware.com/spine-download
[5] здесь: https://www.dropbox.com/s/lfk06fptt6ru7ad/SpineAnimaiton4Habr.zip
[6] Источник: http://habrahabr.ru/post/233027/
Нажмите здесь для печати.