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

# はじめに

[Spine](http://esotericsoftware.com/)を公式サポート外のライブラリで描画したい場合、汎用ランタイムの`spine-c`若しくは`spine-cpp`に独自部分を実装して使うことになります。
ここでは[DXライブラリ](https://dxlib.xsrv.jp/index.html)への組み込みを説明します。

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

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

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

1. `追加のインクルードディレクトリ`に`spine-c/spine-c/include`階層を追加。  
![000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3941954/886130b3-5f6d-7bf3-7c94-c04da7799972.png)
2. `spine-c/spine-c/src/spine`以下のソースファイルをプロジェクトに追加。
![001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3941954/9e2c5959-1a8a-db4a-d287-67983bead538.png)

## 実装すべき項目

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

```c: 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()`で事足ります。
```c: dxlib_spine_c.cpp
char* _spUtil_readFile(const char* path, int* length)
{
	return _spReadFile(path, length);
}
```
中身は`fopen()`です。
```c: 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](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setlocale-wsetlocale?view=msvc-170#utf-8-support)では問題になりませんが、古いWindowsも対象にしたい場合は文字列変換と`_wfopen()`を咬ますなど工夫が必要になります。

## 文字列変換

Spine側の文字列は`char`ですが、DXライブラリ側は`TCHAR`ですので変換が必要な場合があります。DXライブラリの文字列変換には`DxLib::ConvertStringCharCodeFormat()`を使います。
```c: 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()`の取得値はシステム既定ではなく[公式リファレンス](https://dxlib.xsrv.jp/function/dxfunc_other.html#R22N1)にある通りWindows環境では`SHIFT-JIS`です。

## テクスチャ作成 序

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

```c: 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`については次節で追いますが、各構造は以下のようになっています。
```c: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`としてユーザ定義データを格納できる作りになっていることが分かります。
```mermaid
flowchart LR
    spAtlas <--> spAtlasPage
    spAtlas --> rendererObject1[rendererObject]
    spAtlasPage --> rendererObject2[rendererObject]
```
実際何を格納すべきなのか、もう少しSpine内部について掘り下げていきます。

## Spine内部のデータ参照

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

```mermaid
flowchart LR
    spAtlas <--> spAtlasPage
    spAtlas --> spAtlasRegion
    spAtlasRegion --> spAtlasPage
```
更に`spAtlasRegion`は`_spAtlasAttachmentLoader_createAttachment()`に於いて、`spRegionAttachment`等に紐づけられます。

```c: 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;
		// 略
}
```
```c: 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;
}
```
```mermaid
flowchart LR
subgraph spAttachment
    spRegionAttachment
    spMeshAttachment
end
    spAttachment --> spAtlasRegion
```
肝心の`spAtlas`ですが、これは骨格ファイル読み取り時に渡すことになります。

```c: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;
}
```

```mermaid
flowchart LR
subgraph spAttachmentLoader
    spAtlasAttachmentLoader
end
    spAtlasAttachmentLoader --> spAtlas
    spSkeletonData -.-> spSkeletonJson
    spSkeletonData -.-> spSkeletonBinary
    spSkeletonJson --> spAttachmentLoader
    spSkeletonBinary --> spAttachmentLoader
```

`spSkeletonData`作成時に渡す`spSkeletonJson`や`spSkeletonBinary`はファイル解析失敗時のエラーコードを格納しておくもので、解析が終了したらすぐに破棄できますが、`spSkeletonData`は抱えることになり、最終的には`spSkeleton`に行き着きます。
```c: 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`配列を作成します。
```mermaid
flowchart LR
    spSkeleton --> spSkeletonData
    spSkeletonData --> spSlotData
    spSkeleton --> spSlot
    spSlot --> spSlotData
    spSlot --> spAttachment
```
```c:skeleton.h
typedef struct spSkeleton {
	spSkeletonData* const data;
    // 略
	int slotsCount;
	spSlot** slots;
	spSlot** drawOrder;
}
```

```c:skeletonData.h
typedef struct spSkeletonData {
    // 略
	int slotsCount;
	spSlotData** slots;
```

```c:slot.h
typedef struct spSlot {
	spSlotData* const data;
    // 略
	spAttachment* const attachment;
    // 略
} spSlot;
```

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

## テクスチャ作成 実装

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

```c: 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の尺度になっているわけではないので、座標情報が崩壊しかねません。
```c: 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()`を呼ぶだけです。

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

## ファイルデータ管理

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

```cpp: 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_
```

```cpp: 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`から移植する必要があります。

```c: 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 |

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

```cpp: 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_
```

```cpp: 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]。

[^1]: 一応説明を加えると、まず`spSkeleton_update()`ですが、`spSlot`に`spAttachment`が装着されてからの経過時間を算出するためのものです。とはいえ使い道はなく、`4.1`で一旦[消えました](https://github.com/EsotericSoftware/spine-runtimes/blob/4.1/CHANGELOG.md?plain=1#L14)。そして`4.2`からは用途を変えて物理演算のために[復活](https://github.com/EsotericSoftware/spine-runtimes/blob/4.2/CHANGELOG.md?plain=1#L30)しました。
`spAnimationState_apply()`からは`_spScaleTimeline_apply()`などが呼ばれます。`3.7`以前では時間軸を適切に設定しておかないとここで等倍に戻されてしまいますので、拡縮に関しては他の手段に頼ることになります。
`spSkeleton_updateWorldTransform()`には`4.2`から物理演算の有無を指定する第二引数が増えています。

```cpp: 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`から派生したものです。

```c: 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()`を実行して十分なメモリを確保しておく必要があります。

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

```cpp: 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`に処理が纏められました。
コメントにした箇所が次節で述べる独自部分になります。

## 描画関数 独自部

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

```cpp: 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値に乗算せずに済みます。描画ライブラリによっては次のように書きます。
```c
(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]。

[^3]: `2.1`ではSpine側は`int`でした。

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

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

```cpp: dxlib_init.h
#ifndef DXLIB_INIT_H_
#define DXLIB_INIT_H_

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

	int iDxLibInitialised = -1;
};

#endif // !DXLIB_INIT_H_
```

```cpp: 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関数はこんな感じです。  
マウス入力に応じたアニメーション切り替え・座標移動機能を加えています。
```cpp: 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3941954/f433f19f-7e62-b1ba-d30b-ebf27fabe742.gif)

## おわりに

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


説明の都合上コードがぶつ切りになってしまったので纏めたものを暫くは[こちら](https://cloud.mail.ru/public/yJz4/bJT4vXXSc)に置いておきます。