1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DXライブラリへのSpine組み込み

Last updated at Posted at 2024-12-23

はじめに

Spineを公式サポート外のライブラリで描画したい場合、汎用ランタイムのspine-c若しくはspine-cppに独自部分を実装して使うことになります。
ここではDXライブラリへの組み込みを説明します。

用いる汎用ランタイム

Spineには後方互換性がありません。3.6以前にはspine-cppが存在しないこと、C言語版の方が見通しが効くことからspine-cを対象とします。

汎用ランタイムはソースコードの状態で利用します。3.6を基に説明しますが、4.1でも大きな変更はありません。公式ブランチspine-c/spine-c以下のファイルが目的のものです。

Visual Studioの場合のプロジェクト設定

  1. 追加のインクルードディレクトリspine-c/spine-c/include階層を追加。
    000.png
  2. spine-c/spine-c/src/spine以下のソースファイルをプロジェクトに追加。
    001.png

実装すべき項目

アニメーションに係る計算はSpine側が行うので、主に描画処理が独自部分になりますが、その他に汎用ランタイム側から呼び出される関数も実装する必要があります。これらはextension.hに宣言されています。

extension.h
/*
 * Functions that must be implemented:
 */

void _spAtlasPage_createTexture (spAtlasPage* self, const char* path);
void _spAtlasPage_disposeTexture (spAtlasPage* self);
char* _spUtil_readFile (const char* path, int* length);

メモリ割り当て・解放・再割り当て関数も変更可能ですが必須ではありません。
従って以下の項目が最低限実装すべき項目になります。

機能 関数名
ファイル入力 _spUtil_readFile()
テクスチャ作成 _spAtlasPage_createTexture()
テクスチャ破棄 _spAtlasPage_disposeTexture()
状態更新、描画 Update(), Draw()

前者3項目と最終項目とでは性格が異なるので分割してもよいのですが、ここではまとめてdxlib_spine_c.h, dxlib_spine_c.cppに実装することにします。また、この部分は依存を最小限にするために、インクルードは以下のものに留めます。

  • <spine/spine.h>
  • <spine/extension.h>
  • <DxLib.h>

ファイル入力

大抵extension.cにある_spReadFile()で事足ります。

dxlib_spine_c.cpp
char* _spUtil_readFile(const char* path, int* length)
{
	return _spReadFile(path, length);
}

中身はfopen()です。

extension.c
char* _spReadFile (const char* path, int* length) {
	char *data;
	FILE *file = fopen(path, "rb");
	if (!file) return 0;

	fseek(file, 0, SEEK_END);
	*length = (int)ftell(file);
	fseek(file, 0, SEEK_SET);

	data = MALLOC(char, *length);
	fread(data, 1, *length, file);
	fclose(file);

	return data;
}

これは新しめのWindowsでは問題になりませんが、古いWindowsも対象にしたい場合は文字列変換と_wfopen()を咬ますなど工夫が必要になります。

文字列変換

Spine側の文字列はcharですが、DXライブラリ側はTCHARですので変換が必要な場合があります。DXライブラリの文字列変換にはDxLib::ConvertStringCharCodeFormat()を使います。

dxlib_spine_c.cpp
wchar_t* WidenPath(const char* path)
{
	int iCharCode = DxLib::GetUseCharCodeFormat();
	int iWcharCode = DxLib::Get_wchar_t_CharCodeFormat();

	size_t nLen = strlen(path);
	wchar_t* pResult = static_cast<wchar_t*>(malloc((nLen + 1LL) * sizeof(wchar_t)));
	if (pResult == nullptr)return nullptr;
	wmemset(pResult, L'\0', nLen);

	int iLen = DxLib::ConvertStringCharCodeFormat
	(
		iCharCode,
		path,
		iWcharCode,
		pResult
	);
	if (iLen == -1)
	{
		free(pResult);
		return nullptr;
	}

	wchar_t* pTemp = static_cast<wchar_t*>(realloc(pResult, (iLen + 1LL) * sizeof(wchar_t)));
	if (pTemp != nullptr)
	{
		pResult = pTemp;
	}
	*(pResult + iLen) = L'\0';
	return pResult;
}

DxLib::GetUseCharCodeFormat()の取得値はシステム既定ではなく公式リファレンスにある通りWindows環境ではSHIFT-JISです。

テクスチャ作成 序

次に_spAtlasPage_createTexture()ですが、この関数はspAtlas_create()内で、spAtlasPage作成後に呼び出されます。

atlas.c
spAtlas* spAtlas_create(const char* begin, int length, const char* dir, void* rendererObject) {
	spAtlas* self;
 	const char* end = begin + length;
	// 略
	spAtlasPage *page = 0;
 	spAtlasRegion *lastRegion = 0;
	// 略
	self = NEW(spAtlas);
	self->rendererObject = rendererObject;
	while (readLine(&begin, end, &str)) {
		if (str.end - str.begin == 0) {
			page = 0;
		}
		else if (!page) {
			// 略
			page = spAtlasPage_create(self, name);
			// 略
			_spAtlasPage_createTexture(page, path);
			FREE(path);
		}
		else {
   			spAtlasRegion *region = spAtlasRegion_create();
			if (lastRegion)
				lastRegion->next = region;
			else
				self->regions = region;
			lastRegion = region;

			region->page = page;
			// 略
}

spAtlas* spAtlas_createFromFile(const char* path, void* rendererObject) {
	const char* data;
	// 略
	data = _spUtil_readFile(path, &length);
	if (data) atlas = spAtlas_create(data, length, dir, rendererObject);
}

spAtlasRegionについては次節で追いますが、各構造は以下のようになっています。

atlas.h
struct spAtlas {
	spAtlasPage* pages;
	spAtlasRegion* regions;

	void* rendererObject;
};

struct spAtlasPage {
	const spAtlas *atlas;
    // 略
	void* rendererObject;
    // 略
};

struct spAtlasRegion {
	const char* name;
	// 略
	spAtlasPage* page;
	spAtlasRegion* next;
};

spAtlas_create()実行時に第三引数をspAtlas::rendererObjectとして、_spAtlasPage_createTexture()が呼び出された際に別途spAtlasPage::rendererObjectとしてユーザ定義データを格納できる作りになっていることが分かります。

実際何を格納すべきなのか、もう少しSpine内部について掘り下げていきます。

Spine内部のデータ参照

spAtlasRegionについてもう少し追います。これはspAtlasPage作成済みの場合にこれを参照できる形で作成されます。

更にspAtlasRegion_spAtlasAttachmentLoader_createAttachment()に於いて、spRegionAttachment等に紐づけられます。

AtlasAttachmentLoader.c
spAttachment* _spAtlasAttachmentLoader_createAttachment (spAttachmentLoader* loader, spSkin* skin, spAttachmentType type,
		const char* name, const char* path) {
	spAtlasAttachmentLoader* self = SUB_CAST(spAtlasAttachmentLoader, loader);
	switch (type) {
	case SP_ATTACHMENT_REGION: {
		spRegionAttachment* attachment;
		spAtlasRegion* region = spAtlas_findRegion(self->atlas, path);
		// 略
		attachment = spRegionAttachment_create(name);
		attachment->rendererObject = region;
		// 略
	case SP_ATTACHMENT_MESH:
	case SP_ATTACHMENT_LINKED_MESH: {
		spMeshAttachment* attachment;
		spAtlasRegion* region = spAtlas_findRegion(self->atlas, path);
		// 略
		attachment = spMeshAttachment_create(name);
		attachment->rendererObject = region;
		// 略
}
atlas.c
spAtlasRegion* spAtlas_findRegion (const spAtlas* self, const char* name) {
	spAtlasRegion* region = self->regions;
	while (region) {
		if (strcmp(region->name, name) == 0) return region;
		region = region->next;
	}
	return 0;
}

肝心のspAtlasですが、これは骨格ファイル読み取り時に渡すことになります。

SkeletonJson.c
spSkeletonJson* spSkeletonJson_create (spAtlas* atlas) {
	spAtlasAttachmentLoader* attachmentLoader = spAtlasAttachmentLoader_create(atlas);
	spSkeletonJson* self = spSkeletonJson_createWithLoader(SUPER(attachmentLoader));
	SUB_CAST(_spSkeletonJson, self)->ownsLoader = 1;
	return self;
}

spSkeletonData* spSkeletonJson_readSkeletonData (spSkeletonJson* self, const char* json) {
	int length;
	spSkeletonData* skeletonData;
	const char* json = _spUtil_readFile(path, &length);
     // 略
	skeletonData = spSkeletonJson_readSkeletonData(self, json);
	FREE(json);
	return skeletonData;
}

spSkeletonData作成時に渡すspSkeletonJsonspSkeletonBinaryはファイル解析失敗時のエラーコードを格納しておくもので、解析が終了したらすぐに破棄できますが、spSkeletonDataは抱えることになり、最終的にはspSkeletonに行き着きます。

skeleton.c
spSkeleton *spSkeleton_create(spSkeletonData *data) {
	_spSkeleton* internal = NEW(_spSkeleton);
	spSkeleton* self = SUPER(internal);
	CONST_CAST(spSkeletonData*, self->data) = data;
     // 略
	self->slotsCount = data->slotsCount;
	self->slots = MALLOC(spSlot*, self->slotsCount);
	for (i = 0; i < self->slotsCount; ++i) {
		spSlotData *slotData = data->slots[i];
		spBone* bone = self->bones[slotData->boneData->index];
		self->slots[i] = spSlot_create(slotData, bone);
	}
     // 略
	return self;
}

spSkeletonはファイルの解析結果であるspSkeletonDataの持つspSlotData配列を元にspSlot配列を作成します。

skeleton.h
typedef struct spSkeleton {
	spSkeletonData* const data;
    // 略
	int slotsCount;
	spSlot** slots;
	spSlot** drawOrder;
}
skeletonData.h
typedef struct spSkeletonData {
    // 略
	int slotsCount;
	spSlotData** slots;
slot.h
typedef struct spSlot {
	spSlotData* const data;
    // 略
	spAttachment* const attachment;
    // 略
} spSlot;

これによって最終的に

と辿れ、描画ライブラリ特有のデータをspAtlas作成時に渡しておくと、spSkeletonを通じて取り出せる構造になっているわけです。
DXライブラリの場合はspAtlasPage::rendererObjectにテクスチャを格納しておき、描画時に取り出すようにします。
spAtlas::rendererObjectは基本的にDXライブラリでは使いません。ライブラリによってはテクスチャを作成するのに必要なデータを渡すこともあります。

テクスチャ作成 実装

DxLib::LoadGraph()がDXライブラリの画像取り込み関数になります。

dxlib_spine_c.cpp
void _spAtlasPage_createTexture(spAtlasPage* pAtlasPage, const char* path)
{
#if	defined(_WIN32) && defined(_UNICODE)
	wchar_t* wcharPath = WidenPath(path);
	if (wcharPath == nullptr)return;
	int iDxLibTexture = DxLib::LoadGraph(wcharPath);
	free(wcharPath);
#else
	int iDxLibTexture = DxLib::LoadGraph(path);
#endif
	if (iDxLibTexture == -1)return;

	if (pAtlasPage->width == 0 && pAtlasPage->height == 0)
	{
		int iWidth = 0;
		int iHeight = 0;
		DxLib::GetGraphSize(iDxLibTexture, &iWidth, &iHeight);
		pAtlasPage->width = iWidth;
		pAtlasPage->height = iHeight;
	}

	void* p = reinterpret_cast<void*>(static_cast<unsigned long long>(iDxLibTexture));

	pAtlasPage->rendererObject = p;
}

ここで画像の実寸法を以ってspAtlasPageの寸法を上書きするのは避けましょう。版図に縮尺があるように、常に1:1の尺度になっているわけではないので、座標情報が崩壊しかねません。

Atlas.c
		else if (!page) {
			// 略
   			switch (readTuple(&begin, end, tuple)) {
			case 0:
				return abortAtlas(self);
			case 2:
				page->width = toInt(tuple);
				page->height = toInt(tuple + 1);
				if (!readTuple(&begin, end, tuple)) return abortAtlas(self);
			}
    		// 略
       else{
            // 略
			if (readTuple(&begin, end, tuple) != 2) return abortAtlas(self);
			region->width = toInt(tuple);
			region->height = toInt(tuple + 1);

			region->u = region->x / (float)page->width;
			region->v = region->y / (float)page->height;
			if (region->rotate) {
				region->u2 = (region->x + region->height) / (float)page->width;
				region->v2 = (region->y + region->width) / (float)page->height;
			}
			else {
				// 略
			}

DXライブラリでは補間法やアドレスモードをテクスチャ毎に指定することはできませんが、問題は生じないかと思います。

テクスチャ破棄

DxLib::DeleteGraph()を呼ぶだけです。

dxlib_spine_c.cpp
void _spAtlasPage_disposeTexture(spAtlasPage* pAtlasPage)
{
	DxLib::DeleteGraph(static_cast<int>(reinterpret_cast<unsigned long long>(pAtlasPage->rendererObject)));
}

ファイルデータ管理

さて、参照関係にあるためspSkeletonを作成し終わってもspAtlasspSkeletonDataは破棄できません。
ここでは次のように封装してしまうことにします。

spine_loader_c.h
#ifndef SPINE_LOADER_C_H_
#define SPINE_LOADER_C_H_

#include <memory>

#include <spine/spine.h>

namespace spine_loader_c
{
	std::shared_ptr<spAtlas> CreateAtlasFromFile(const char* filePath, void* rendererObject);

	std::shared_ptr<spSkeletonData> ReadTextSkeletonFromFile(const char* filePath, spAtlas* atlas, float scale = 1.f);
 	std::shared_ptr<spSkeletonData> ReadBinarySkeletonFromFile(const char* filePath, spAtlas* atlas, float scale = 1.f);
}
#endif // !SPINE_LOADER_C_H_
spine_loader_c.cpp

#include "spine_loader_c.h"

std::shared_ptr<spAtlas> spine_loader_c::CreateAtlasFromFile(const char* filePath, void* rendererObject)
{
	auto atlas = std::shared_ptr<spAtlas>
		(
			spAtlas_createFromFile(filePath, rendererObject),
			[](spAtlas *pAtlas)
			{
				if(pAtlas != nullptr)spAtlas_dispose(pAtlas);
			}
		);
	return atlas;
}
std::shared_ptr<spSkeletonData> spine_loader_c::ReadTextSkeletonFromFile(const char* filePath, spAtlas* atlas, float scale)
{
	spSkeletonJson* json = spSkeletonJson_create(atlas);
	json->scale = scale;
	auto skeletonData = std::shared_ptr<spSkeletonData>
		(
			spSkeletonJson_readSkeletonDataFile(json, filePath),
			[](spSkeletonData* pSkeletonData)
			{
				if(pSkeletonData != nullptr)spSkeletonData_dispose(pSkeletonData);
			}
		);
	spSkeletonJson_dispose(json);
	return skeletonData;
}
std::shared_ptr<spSkeletonData> spine_loader_c::ReadBinarySkeletonFromFile(const char* filePath, spAtlas* atlas, float scale)
{
	spSkeletonBinary* binary = spSkeletonBinary_create(atlas);
	binary->scale = scale;
	auto skeletonData = std::shared_ptr<spSkeletonData>
		(
			spSkeletonBinary_readSkeletonDataFile(binary, filePath),
			[](spSkeletonData* pSkeletonData)
			{
				if (pSkeletonData != nullptr)spSkeletonData_dispose(pSkeletonData);
			}
		);
	spSkeletonBinary_dispose(binary);
	return skeletonData;
}

実際にはファイル読み取りではなくメモリから作成することもありますが、ここでは割愛します。

可変配列マクロ

描画に際して、計算はSpine側で行うので描画ライブラリ側は配列番号付き頂点データを描くことが目標です。これはDXライブラリではDxLib::DrawPolygonIndexed2D()を使うことで実現できます。
頂点数は刻々と変動しますが、これを容易に扱うためにspine-cには可変配列マクロが用意されています。array.hに定義されている_SP_ARRAY_DECLARE_TYPE並び_SP_ARRAY_IMPLEMENT_TYPE_NO_CONTAINSがそうなのですが、後者は4.0以前には存在しないので4.1から移植する必要があります。

array.h
#define _SP_ARRAY_DECLARE_TYPE(name, itemType) \
	typedef struct name { int size; int capacity; itemType* items; } name; \
	SP_API name* name##_create(int initialCapacity); \
	SP_API void name##_dispose(name* self); \
	SP_API void name##_clear(name* self); \
	SP_API name* name##_setSize(name* self, int newSize); \
	SP_API void name##_ensureCapacity(name* self, int newCapacity); \
	SP_API void name##_add(name* self, itemType value); \
	SP_API void name##_addAll(name* self, name* other); \
	SP_API void name##_addAllValues(name* self, itemType* values, int offset, int count); \
	SP_API void name##_removeAt(name* self, int index); \
	SP_API int name##_contains(name* self, itemType value); \
	SP_API itemType name##_pop(name* self); \
	SP_API itemType name##_peek(name* self);


_SP_ARRAY_DECLARE_TYPE(spFloatArray, float)
_SP_ARRAY_DECLARE_TYPE(spIntArray, int)
_SP_ARRAY_DECLARE_TYPE(spShortArray, short)
_SP_ARRAY_DECLARE_TYPE(spUnsignedShortArray, unsigned short)
_SP_ARRAY_DECLARE_TYPE(spArrayFloatArray, spFloatArray*)
_SP_ARRAY_DECLARE_TYPE(spArrayShortArray, spShortArray*)

DxLib::DrawPolygonIndexed2D()DxLib::VERTEX2D*並びunsigned short*を引数としますが、この内unsigned shortのマクロは已にarray.cで定義されていますので、独自部分としてDxLib::VERTEX2Dの可変配列マクロを定義することになります。

描画クラス

時間と共に刻々と変動するデータはspSkeletonで、同データを更新する関数群はspAnimationStateで管理されます。
加えて、マスクと頂点分割を司るspSkeletonClippingがあります。従って描画クラスに最低限必要なデータは以下のようになります。

内容 変数名
spSkeleton skeleton
spAnimationState animationState
Spine側頂点データ格納配列 m_worldVertices
DXライブラリ側頂点データ格納配列 m_dxLibVertices
DXライブラリ側配列番号格納配列 m_dxLibIndices
spSkeletonClipping m_clipper

最低限必要な機能は毎ループ呼び出す状態更新関数と描画関数になりますが、実用上、乗算済みアルファの切り替え機能は加えることにします。

dxlib_spine_c.h
#ifndef DXLIB_SPINE_C_H_
#define DXLIB_SPINE_C_H_

#include <spine/spine.h>

#define DX_NON_USING_NAMESPACE_DXLIB
#include <DxLib.h>

/*Backport from spine-c 4.1*/
#ifndef _SP_ARRAY_IMPLEMENT_TYPE_NO_CONTAINS
#define _SP_ARRAY_IMPLEMENT_TYPE_NO_CONTAINS(name, itemType) \
	name* name##_create(int initialCapacity) { \
		name* array = CALLOC(name, 1); \
		array->size = 0; \
		array->capacity = initialCapacity; \
		array->items = CALLOC(itemType, initialCapacity); \
		return array; \
	} \
	void name##_dispose(name* self) { \
		FREE(self->items); \
		FREE(self); \
	} \
	void name##_clear(name* self) { \
		self->size = 0; \
	} \
	name* name##_setSize(name* self, int newSize) { \
		self->size = newSize; \
		if (self->capacity < newSize) { \
			self->capacity = MAX(8, (int)(self->size * 1.75f)); \
			self->items = REALLOC(self->items, itemType, self->capacity); \
		} \
		return self; \
	} \
	void name##_ensureCapacity(name* self, int newCapacity) { \
		if (self->capacity >= newCapacity) return; \
		self->capacity = newCapacity; \
		self->items = REALLOC(self->items, itemType, self->capacity); \
	} \
	void name##_add(name* self, itemType value) { \
		if (self->size == self->capacity) { \
			self->capacity = MAX(8, (int)(self->size * 1.75f)); \
			self->items = REALLOC(self->items, itemType, self->capacity); \
		} \
		self->items[self->size++] = value; \
	} \
	void name##_addAll(name* self, name* other) { \
		int i = 0; \
		for (; i < other->size; i++) { \
			name##_add(self, other->items[i]); \
		} \
	} \
	void name##_addAllValues(name* self, itemType* values, int offset, int count) { \
		int i = offset, n = offset + count; \
		for (; i < n; i++) { \
			name##_add(self, values[i]); \
		} \
	} \
	void name##_removeAt(name* self, int index) { \
		self->size--; \
		memmove(self->items + index, self->items + index + 1, sizeof(itemType) * (self->size - index)); \
	} \
	itemType name##_pop(name* self) { \
		itemType item = self->items[--self->size]; \
		return item; \
	} \
	itemType name##_peek(name* self) { \
		return self->items[self->size - 1]; \
	}
#endif //_SP_ARRAY_IMPLEMENT_TYPE_NO_CONTAINS

_SP_ARRAY_DECLARE_TYPE(spDxLibVertexArray, DxLib::VERTEX2D)

class CDxLibSpineDrawerC
{
public:
	CDxLibSpineDrawerC(spSkeletonData* pSkeletonData);
	~CDxLibSpineDrawerC();

	spSkeleton* skeleton = nullptr;
	spAnimationState* animationState = nullptr;
	float timeScale = 1.f;

	void Update(float fDelta);
	void Draw();

	void SwitchPma() { m_bAlphaPremultiplied ^= true; }
private:
	bool m_bAlphaPremultiplied = true;

	spFloatArray* m_worldVertices = nullptr;
	spDxLibVertexArray* m_dxLibVertices = nullptr;
	spUnsignedShortArray* m_dxLibIndices = nullptr;

	spSkeletonClipping* m_clipper = nullptr;
};
#endif // !DXLIB_SPINE_C_H_
dxlib_spine_c.cpp

#include <spine/extension.h>

#include "dxlib_spine_c.h"

_SP_ARRAY_IMPLEMENT_TYPE_NO_CONTAINS(spDxLibVertexArray, DxLib::VERTEX2D)

static wchar_t* WidenPath(const char* path)
{
// 中略
}

void _spAtlasPage_createTexture(spAtlasPage* pAtlasPage, const char* path)
{
// 中略
}

void _spAtlasPage_disposeTexture(spAtlasPage* pAtlasPage)
{
	DxLib::DeleteGraph(static_cast<int>(reinterpret_cast<unsigned long long>(pAtlasPage->rendererObject)));
}

char* _spUtil_readFile(const char* path, int* length)
{
	return _spReadFile(path, length);
}


CDxLibSpineDrawerC::CDxLibSpineDrawerC(spSkeletonData* pSkeletonData)
{
	spBone_setYDown(1);

	m_worldVertices = spFloatArray_create(128);
	m_dxLibVertices = spDxLibVertexArray_create(128);
	m_dxLibIndices = spUnsignedShortArray_create(128);

	skeleton = spSkeleton_create(pSkeletonData);
	spAnimationStateData* pAnimationStateData = spAnimationStateData_create(pSkeletonData);
	animationState = spAnimationState_create(pAnimationStateData);
	m_clipper = spSkeletonClipping_create();
}

CDxLibSpineDrawerC::~CDxLibSpineDrawerC()
{
	if (m_worldVertices != nullptr)
	{
		spFloatArray_dispose(m_worldVertices);
	}
	if (m_dxLibVertices != nullptr)
	{
		spDxLibVertexArray_dispose(m_dxLibVertices);
	}
	if (m_dxLibIndices != nullptr)
	{
		spUnsignedShortArray_dispose(m_dxLibIndices);
	}

	if (animationState != nullptr)
	{
		if (animationState->data != nullptr)
		{
			spAnimationStateData_dispose(animationState->data);
		}
		spAnimationState_dispose(animationState);
	}
	if (skeleton != nullptr)
	{
		spSkeleton_dispose(skeleton);
	}
	if (m_clipper != nullptr)
	{
		spSkeletonClipping_dispose(m_clipper);
	}
}

状態更新関数

基本的に独自部分は書くことのない、汎用処理です1

dxlib_spine_c.cpp
void CDxLibSpineDrawerC::Update(float fDelta)
{
	if (skeleton == nullptr || animationState == nullptr)return;

	spSkeleton_update(skeleton, fDelta);
	spAnimationState_update(animationState, fDelta * timeScale);
	spAnimationState_apply(animationState, skeleton);
	spSkeleton_updateWorldTransform(skeleton);
}

spAnimationState_update()の単位は秒ですので60fpsの場合1/60.fとします。

描画関数 汎用部

spSkeleton::spSlot::spAttachmentを基に頂点データ・色情報を算出する処理になります。先に出てきたspRegionAttachment等は図形の基底クラスとなるspAttachmentから派生したものです。

Attachment.h
typedef enum {
	SP_ATTACHMENT_REGION,
	SP_ATTACHMENT_BOUNDING_BOX,
	SP_ATTACHMENT_MESH,
	SP_ATTACHMENT_LINKED_MESH,
	SP_ATTACHMENT_PATH,
	SP_ATTACHMENT_POINT,
	SP_ATTACHMENT_CLIPPING
} spAttachmentType;

typedef struct spAttachment {
	const char* const name;
	const spAttachmentType type;
	const void* const vtable;
	struct spAttachmentLoader* attachmentLoader;
} spAttachment;

typedef struct spRegionAttachment {
	spAttachment super;
	// 略
} spRegionAttachment;

typedef struct spMeshAttachment {
	spAttachment super;
	// 略
} spMeshAttachment;

これらのspAttachment派生クラスはspSlotに収められているのですが、最初はspAttachmentがあっても時間を進めると空になることがあるのでNULLチェックは必ず行います。
SP_ATTACHMENT_REGION並びSP_ATTACHMENT_MESHが描画対象で、computeWorldVertices系の関数で演算結果を得られます2が、事前にspFloatArray_setSize()を実行して十分なメモリを確保しておく必要があります。

dxlib_spine_c.cpp
void CDxLibSpineDrawerC::Draw()
{
	if (m_worldVertices == nullptr || m_clipper == nullptr || skeleton == nullptr || animationState == nullptr)return;

	if (skeleton->color.a == 0) return;

	static unsigned short quadIndices[] = { 0, 1, 2, 2, 3, 0 };

	for (int i = 0; i < skeleton->slotsCount; ++i)
	{
		spSlot* pSlot = skeleton->drawOrder[i];
		spAttachment* pAttachment = pSlot->attachment;

		if (pAttachment == nullptr || pSlot->color.a == 0)
		{
			spSkeletonClipping_clipEnd(m_clipper, pSlot);
			continue;
		}

		spFloatArray* pVertices = m_worldVertices;
		float* pAttachmentUvs = nullptr;
		unsigned short* pIndices = nullptr;
		int indicesCount = 0;

		spColor* pAttachmentColor = nullptr;

		int iDxLibTexture = -1;

		if (pAttachment->type == SP_ATTACHMENT_REGION)
		{
			spRegionAttachment* pRegionAttachment = (spRegionAttachment*)pAttachment;
			pAttachmentColor = &pRegionAttachment->color;

			if (pAttachmentColor->a == 0)
			{
				spSkeletonClipping_clipEnd(m_clipper, pSlot);
				continue;
			}

			spFloatArray_setSize(pVertices, 8);
			spRegionAttachment_computeWorldVertices(pRegionAttachment, pSlot->bone, pVertices->items, 0, 2);

			pAttachmentUvs = pRegionAttachment->uvs;
			pIndices = quadIndices;
			indicesCount = sizeof(quadIndices)/sizeof(unsigned short);

			iDxLibTexture = (static_cast<int>(reinterpret_cast<unsigned long long>(static_cast<spAtlasRegion*>(pRegionAttachment->rendererObject)->page->rendererObject)));
		}
		else if (pAttachment->type == SP_ATTACHMENT_MESH)
		{
			spMeshAttachment* pMeshAttachment = (spMeshAttachment*)pAttachment;
			pAttachmentColor = &pMeshAttachment->color;

			if (pAttachmentColor->a == 0)
			{
				spSkeletonClipping_clipEnd(m_clipper, pSlot);
				continue;
			}

			spFloatArray_setSize(pVertices, pMeshAttachment->super.worldVerticesLength);
			spVertexAttachment_computeWorldVertices(SUPER(pMeshAttachment), pSlot, 0, pMeshAttachment->super.worldVerticesLength, pVertices->items, 0, 2);

			pAttachmentUvs = pMeshAttachment->uvs;
			pIndices = pMeshAttachment->triangles;
			indicesCount = pMeshAttachment->trianglesCount;

			iDxLibTexture = (static_cast<int>(reinterpret_cast<unsigned long long>(static_cast<spAtlasRegion*>(pMeshAttachment->rendererObject)->page->rendererObject)));

		}
		else if (pAttachment->type == SP_ATTACHMENT_CLIPPING)
		{
			spClippingAttachment* clip = (spClippingAttachment*)pSlot->attachment;
			spSkeletonClipping_clipStart(m_clipper, pSlot, clip);
			continue;
		}
		else
		{
			spSkeletonClipping_clipEnd(m_clipper, pSlot);
			continue;
		}

		if (spSkeletonClipping_isClipping(m_clipper))
		{
			spSkeletonClipping_clipTriangles(m_clipper, pVertices->items, pVertices->size, pIndices, indicesCount, pAttachmentUvs, 2);
			if (m_clipper->clippedTriangles->size == 0)
			{
				spSkeletonClipping_clipEnd(m_clipper, pSlot);
				continue;
			}
			pVertices = m_clipper->clippedVertices;
			pAttachmentUvs = m_clipper->clippedUVs->items;
			pIndices = m_clipper->clippedTriangles->items;
			indicesCount = m_clipper->clippedTriangles->size;
		}

		spColor tint;
		tint.r = skeleton->color.r * pSlot->color.r * pAttachmentColor->r;
		tint.g = skeleton->color.g * pSlot->color.g * pAttachmentColor->g;
		tint.b = skeleton->color.b * pSlot->color.b * pAttachmentColor->b;
		tint.a = skeleton->color.a * pSlot->color.a * pAttachmentColor->a;

		/* 描画ライブラリ独自部分 */

		spSkeletonClipping_clipEnd(m_clipper, pSlot);
	}
	spSkeletonClipping_clipEnd2(m_clipper);
}

この部分はテクスチャの型変換を除き汎用処理で、描画ライブラリに依存しません。
実際4.2からspine-cppの方はspine::SkeletonRendererに処理が纏められました。
コメントにした箇所が次節で述べる独自部分になります。

描画関数 独自部

頂点データと色情報を手に入れたので、これを描画ライブラリに合わせる工程です。

dxlib_spine_c.cpp
		spDxLibVertexArray_clear(m_dxLibVertices);
		for (int ii = 0; ii < pVertices->size; ii += 2)
		{
			DxLib::VERTEX2D dxLibVertex{};
			dxLibVertex.pos.x = pVertices->items[ii];
			dxLibVertex.pos.y = pVertices->items[ii + 1LL];
			dxLibVertex.pos.z = 0.f;
			dxLibVertex.rhw = 1.f;
			dxLibVertex.dif.r = (BYTE)(tint.r * 255.f);
			dxLibVertex.dif.g = (BYTE)(tint.g * 255.f);
			dxLibVertex.dif.b = (BYTE)(tint.b * 255.f);
			dxLibVertex.dif.a = (BYTE)(tint.a * 255.f);
			dxLibVertex.u = pAttachmentUvs[ii];
			dxLibVertex.v = pAttachmentUvs[ii + 1LL];
			spDxLibVertexArray_add(m_dxLibVertices, dxLibVertex);
		}

		spUnsignedShortArray_clear(m_dxLibIndices);
		for (int ii = 0; ii < indicesCount; ++ii)
		{
			spUnsignedShortArray_add(m_dxLibIndices, pIndices[ii]);
		}

		int iDxLibBlendMode;
		switch (pSlot->data->blendMode)
		{
		case spBlendMode::SP_BLEND_MODE_ADDITIVE:
			iDxLibBlendMode = m_bAlphaPremultiplied ? DX_BLENDMODE_PMA_ADD : DX_BLENDMODE_SPINE_ADDITIVE;
			break;
		case spBlendMode::SP_BLEND_MODE_MULTIPLY:
			iDxLibBlendMode = DX_BLENDMODE_SPINE_MULTIPLY;
			break;
		case spBlendMode::SP_BLEND_MODE_SCREEN:
			iDxLibBlendMode = DX_BLENDMODE_SPINE_SCREEN;
			break;
		default:
			iDxLibBlendMode = m_bAlphaPremultiplied ? DX_BLENDMODE_PMA_ALPHA : DX_BLENDMODE_SPINE_NORMAL;
			break;
		}
		DxLib::SetDrawBlendMode(iDxLibBlendMode, 255);
		DxLib::DrawPolygonIndexed2D
		(
			m_dxLibVertices->items,
			m_dxLibVertices->size,
			m_dxLibIndices->items,
			m_dxLibIndices->size / 3,
			iDxLibTexture, TRUE
		);

どういう機構なのか分かりませんが、DXライブラリではアルファ値をRGB値に乗算せずに済みます。描画ライブラリによっては次のように書きます。

(BYTE)(tint.r * 255.f * (m_bAlphaPremultiplied ? tint.a : 1.f));
(BYTE)(tint.g * 255.f * (m_bAlphaPremultiplied ? tint.a : 1.f));
(BYTE)(tint.b * 255.f * (m_bAlphaPremultiplied ? tint.a : 1.f));

spUnsignedShortArray_addに関してSpine側もDXライブラリ側もunsigned shortなのでこの場合は無駄なのですが、これも描画ライブラリに依ります3

後は毎ループUpdate()Draw()を呼び出すだけです。

実行例

公式のSpineboyを動かしてみます。
DXライブラリの設定は以下のようにしています。

dxlib_init.h
#ifndef DXLIB_INIT_H_
#define DXLIB_INIT_H_

struct SDxLibInit
{
	SDxLibInit();
	~SDxLibInit();

	int iDxLibInitialised = -1;
};

#endif // !DXLIB_INIT_H_
dxlib_init.cpp

#define DX_NON_USING_NAMESPACE_DXLIB
#include <DxLib.h>

#include "dxlib_init.h"

SDxLibInit::SDxLibInit()
{
	int iRet = -1;
	iRet = DxLib::SetOutApplicationLogValidFlag(FALSE);
	if (iRet == -1)return;

	iRet = DxLib::SetChangeScreenModeGraphicsSystemResetFlag(FALSE);
	if (iRet == -1)return;
#ifdef _WIN32
	iRet = DxLib::ChangeWindowMode(TRUE);
	if (iRet == -1)return;
#endif
	iRet = DxLib::SetUseCharCodeFormat(DX_CHARCODEFORMAT_UTF8);
	if (iRet == -1)return;

	iDxLibInitialised = DxLib::DxLib_Init();

	DxLib::SetDrawScreen(DX_SCREEN_BACK);
	DxLib::SetDrawMode(DX_DRAWMODE_BILINEAR);
	DxLib::SetTextureAddressMode(DX_TEXADDRESS_WRAP);
}

SDxLibInit::~SDxLibInit()
{
	if (iDxLibInitialised != -1)
	{
		DxLib::DxLib_End();
	}
}

main関数はこんな感じです。
マウス入力に応じたアニメーション切り替え・座標移動機能を加えています。

main.cpp
#include <locale.h>

#include <string>
#include <vector>
#include <memory>

#include "dxlib_init.h"
#include "dxlib_spine_c.h"
#include "spine_loader_c.h"

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPWSTR    lpCmdLine,
    _In_ int       nCmdShow)
{
    setlocale(LC_ALL, ".utf8");

    SDxLibInit sDxLibInit;
    if (sDxLibInit.iDxLibInitialised == -1)
    {
        return 0;
    }

    std::string strAtlasPath = R"(data\spineboy\export\spineboy.atlas)";
    std::string strSkelPath =  R"(data\spineboy\export\spineboy-ess.json)";

    auto atlas = spine_loader_c::CreateAtlasFromFile(strAtlasPath.c_str(), nullptr);
    if (atlas.get() == nullptr)return 0;

    auto skeletonData = spine_loader_c::ReadTextSkeletonFromFile(strSkelPath.c_str(), atlas.get());
    if (skeletonData.get() == nullptr)return 0;

    auto spineDrawable = std::make_shared<CDxLibSpineDrawerC>(skeletonData.get());

    std::vector<std::string> animationNames;
    size_t animationIndex = 0;
    for (size_t i = 0; i < skeletonData->animationsCount; ++i)
    {
        const std::string& strAnimationName = skeletonData->animations[i]->name;
        auto iter = std::find(animationNames.begin(), animationNames.end(), strAnimationName);
        if (iter == animationNames.cend())animationNames.push_back(strAnimationName);
    }

    if (animationNames.empty())return 0;

    spAnimationState_setAnimationByName(spineDrawable->animationState, 0, animationNames[0].c_str(), 1);

    const auto GetDrawingInterval = []()
        -> float
        {
            DEVMODE sDevMode{};
            ::EnumDisplaySettings(nullptr, ENUM_CURRENT_SETTINGS, &sDevMode);
            return 1 / static_cast<float>(sDevMode.dmDisplayFrequency);
        };

    float fDelta = GetDrawingInterval();

    int iWindowWidth = skeletonData->width > 0.f ? static_cast<int>(skeletonData->width) : 1080;
    int iWindowHeight = skeletonData->height > 0.f ? static_cast<int>(skeletonData->height) : 720;
    int iRet = DxLib::SetGraphMode(iWindowWidth, iWindowHeight, 32);
    if (iRet == DX_CHANGESCREEN_RETURN || iRet == DX_CHANGESCREEN_DEFAULT)return 0;

    DxLib::FLOAT2 fSpinePosOffset{};

    const auto UpdateSpinePosition = [&spineDrawable, &iWindowWidth, &iWindowHeight, &fSpinePosOffset]()
        -> void
        {
            spineDrawable->skeleton->x = iWindowWidth / 2.f - fSpinePosOffset.u;
            spineDrawable->skeleton->y = iWindowHeight / 2.f - fSpinePosOffset.v;
        };

    UpdateSpinePosition();

    int keyboardState[256]{};
    std::pair<int, int> iMouseStartPos;
    int iForeMouseState = 0;
    while (DxLib::ProcessMessage() != -1)
    {
        DxLib::GetHitKeyStateAllEx(keyboardState);
        if (keyboardState[KEY_INPUT_ESCAPE] == -1)
        {
            break;
        }
        else if (keyboardState[KEY_INPUT_A] == -1)
        {
            spineDrawable->SwitchPma();
        }

        int iCurrentMouseState = DxLib::GetMouseInput();
        if ((iCurrentMouseState & MOUSE_INPUT_LEFT) && !(iForeMouseState & MOUSE_INPUT_LEFT))
        {
            DxLib::GetMousePoint(&iMouseStartPos.first, &iMouseStartPos.second);
        }
        else if (!(iCurrentMouseState & MOUSE_INPUT_LEFT) && (iForeMouseState & MOUSE_INPUT_LEFT))
        {
            std::pair<int, int> iMouseEndPos;
            DxLib::GetMousePoint(&iMouseEndPos.first, &iMouseEndPos.second);

            int iX = iMouseStartPos.first - iMouseEndPos.first;
            int iY = iMouseStartPos.second - iMouseEndPos.second;
            if (iX == 0 && iY == 0)
            {
                if (++animationIndex >= animationNames.size())animationIndex = 0;
                spAnimationState_setAnimationByName(spineDrawable->animationState, 0, animationNames[animationIndex].c_str(), 1);
            }
            else
            {
                fSpinePosOffset.u += iX;
                fSpinePosOffset.v += iY;
                UpdateSpinePosition();
            }

        }
        else if (!(iCurrentMouseState & MOUSE_INPUT_MIDDLE) && (iForeMouseState & MOUSE_INPUT_MIDDLE))
        {
            fSpinePosOffset = DxLib::FLOAT2{};
            UpdateSpinePosition();
        }
        iForeMouseState = iCurrentMouseState;

        DxLib::ClearDrawScreen();

        spineDrawable->Update(fDelta);
        spineDrawable->Draw();

        DxLib::ScreenFlip();
    }

    return 0;
}

こういうものが出来上がります。
sample.gif

おわりに

実用にはもっと肉付けが必要ですが、DXライブラリとはあまり関係のない話になってしまうので、これにて締めます。公式でも述べられていますが、色の薄さがDXライブラリの特徴なので、別のエンジンなり制作ツールなりにSpineを組み込む際の助けにもなれば幸いです。

説明の都合上コードがぶつ切りになってしまったので纏めたものを暫くはこちらに置いておきます。 

  1. 一応説明を加えると、まずspSkeleton_update()ですが、spSlotspAttachmentが装着されてからの経過時間を算出するためのものです。とはいえ使い道はなく、4.1で一旦消えました。そして4.2からは用途を変えて物理演算のために復活しました。
    spAnimationState_apply()からは_spScaleTimeline_apply()などが呼ばれます。3.7以前では時間軸を適切に設定しておかないとここで等倍に戻されてしまいますので、拡縮に関しては他の手段に頼ることになります。
    spSkeleton_updateWorldTransform()には4.2から物理演算の有無を指定する第二引数が増えています。

  2. 4.1からはspRegionAttachment_computeWorldVertices()の第二引数がspBoneではなくspSlotに変わっています。

  3. 2.1ではSpine側はintでした。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?