- PVSM.RU - https://www.pvsm.ru -

Вставляем Spine Generic Runtime в проект на С++

Всем привет!

Недавно перед нами встала задача добавления в проект скелетной анимации. По совету коллег мы обратили внимание на 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/