
Insert a Spine Generic Runtime into a C ++ project
Hello!
Recently, we faced the task of adding skeletal animation to the project. On the advice of colleagues, we turned our attention to Spine .
After it became clear that the capabilities of the editor satisfy our needs ( there is a review of the animation editor here ), we began to embed Spine in our C ++ engine.
The "general" source codes for C in Rtime are here . Compilation sources give 3 errors - 3 functions must be implemented during integration. They will be described below.
The exported data ( here you can download the graphical editor and several exported animations for etst) consist of an animation json-file, texture (atlas) and atlas description file.
Let's start by loading the texture atlas. To do this, we will write a small wrapper class that will load and unload texture atlases in Spine format.
The spAtlas_create function from the SpineAtlas constructor calls the _spAtlasPage_createTexture function , which must be overridden when Spine is integrated into the engine. Here we also define the _spAtlasPage_disposeTexture function paired with it .
The function textures :: LoadTexture loads the texture from the file at the specified path. render :: ReleaseTexture - platform- dependent unloading of texture from memory.
The simplest wrapper for Spine animation is as follows.
The spSkeletonJson_readSkeletonDataFile function from the SpineAnimaion constructor calls the _spUtil_readFile function . This is the last of the three functions that must be implemented in the code for integrating Spine. She uses malloc in Spine style.
When loading an animation file, you can specify a global scale ( SpineAnimation :: SpineAnimation ).
Skinning is implemented as follows ( SpineAnimation :: Play ):
When playing, you can set the speed of the animation, as well as mirror it horizontally and / or vertically ( SpineAnimaiton :: Update ):
The selected animation can be launched along the desired path. The animation position is taken into account when filling in vertexes during rendering ( SpineAnimaiton :: Render )
The sources described in this article can be downloaded here . For ease of reading, Set / Get functions are missing.
PS: When writing this article, I found 2 small errors in the code. Often write articles on Habr!
Recently, we faced the task of adding skeletal animation to the project. On the advice of colleagues, we turned our attention to Spine .
After it became clear that the capabilities of the editor satisfy our needs ( there is a review of the animation editor here ), we began to embed Spine in our C ++ engine.
Source code
The "general" source codes for C in Rtime are here . Compilation sources give 3 errors - 3 functions must be implemented during integration. They will be described below.
Data format
The exported data ( here you can download the graphical editor and several exported animations for etst) consist of an animation json-file, texture (atlas) and atlas description file.
Integration Atlas
Let's start by loading the texture atlas. To do this, we will write a small wrapper class that will load and unload texture atlases in Spine format.
// объявление класса
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);
}
The spAtlas_create function from the SpineAtlas constructor calls the _spAtlasPage_createTexture function , which must be overridden when Spine is integrated into the engine. Here we also define the _spAtlasPage_disposeTexture function paired with it .
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);
}
The function textures :: LoadTexture loads the texture from the file at the specified path. render :: ReleaseTexture - platform- dependent unloading of texture from memory.
Integration, animation
The simplest wrapper for Spine animation is as follows.
// объявление класса
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);
}
}
// отрисовка, render::BindTexture и render::DrawVertexArray - платформозависимая отрисовка
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;
render::BindTexture(texture);
render::DrawVertexArray(vertices, 6);
}
}
// заполнение одной вершины в формате 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();
}
The spSkeletonJson_readSkeletonDataFile function from the SpineAnimaion constructor calls the _spUtil_readFile function . This is the last of the three functions that must be implemented in the code for integrating Spine. She uses malloc in Spine style.
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;
}
Additional features
When loading an animation file, you can specify a global scale ( SpineAnimation :: SpineAnimation ).
spSkeletonJson* skeletonJson = spSkeletonJson_create(mAtlas->GetAtlas());
skeletonJson->scale = scale;
Skinning is implemented as follows ( SpineAnimation :: Play ):
if (!skinName.empty())
{
spSkeleton_setSkinByName(mSkeleton, skinName.c_str());
spSkeleton_setSlotsToSetupPose(mSkeleton);
}
When playing, you can set the speed of the animation, as well as mirror it horizontally and / or vertically ( SpineAnimaiton :: Update ):
if (IsPlaying())
{
mSkeleton->flipX = mFlipX;
mSkeleton->flipY = mFlipY;
spSkeleton_update(mSkeleton, timeElapsed * mSpeed / 1000);
...
}
The selected animation can be launched along the desired path. The animation position is taken into account when filling in vertexes during rendering ( SpineAnimaiton :: Render )
FillSlotVertices(vertices], mPosition.x, mPosition.y, slot, regionAttachment);
Source code
The sources described in this article can be downloaded here . For ease of reading, Set / Get functions are missing.
PS: When writing this article, I found 2 small errors in the code. Often write articles on Habr!