はじめに
Spineを公式サポート外のライブラリで描画したい場合、汎用ランタイムのspine-c
若しくはspine-cpp
に独自部分を実装して使うことになります。
ここではDXライブラリへの組み込みを説明します。
用いる汎用ランタイム
Spineには後方互換性がありません。3.6
以前にはspine-cpp
が存在しないこと、C言語版の方が見通しが効くことからspine-c
を対象とします。
汎用ランタイムはソースコードの状態で利用します。3.6
を基に説明しますが、4.1
でも大きな変更はありません。公式ブランチのspine-c/spine-c
以下のファイルが目的のものです。
Visual Studioの場合のプロジェクト設定
実装すべき項目
アニメーションに係る計算はSpine側が行うので、主に描画処理が独自部分になりますが、その他に汎用ランタイム側から呼び出される関数も実装する必要があります。これらは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()
で事足ります。
char* _spUtil_readFile(const char* path, int* length)
{
return _spReadFile(path, length);
}
中身はfopen()
です。
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()
を使います。
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
作成後に呼び出されます。
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
については次節で追いますが、各構造は以下のようになっています。
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
等に紐づけられます。
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;
// 略
}
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
ですが、これは骨格ファイル読み取り時に渡すことになります。
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
作成時に渡すspSkeletonJson
やspSkeletonBinary
はファイル解析失敗時のエラーコードを格納しておくもので、解析が終了したらすぐに破棄できますが、spSkeletonData
は抱えることになり、最終的にはspSkeleton
に行き着きます。
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
配列を作成します。
typedef struct spSkeleton {
spSkeletonData* const data;
// 略
int slotsCount;
spSlot** slots;
spSlot** drawOrder;
}
typedef struct spSkeletonData {
// 略
int slotsCount;
spSlotData** slots;
typedef struct spSlot {
spSlotData* const data;
// 略
spAttachment* const attachment;
// 略
} spSlot;
これによって最終的に
と辿れ、描画ライブラリ特有のデータをspAtlas
作成時に渡しておくと、spSkeleton
を通じて取り出せる構造になっているわけです。
DXライブラリの場合はspAtlasPage::rendererObject
にテクスチャを格納しておき、描画時に取り出すようにします。
spAtlas::rendererObject
は基本的にDXライブラリでは使いません。ライブラリによってはテクスチャを作成するのに必要なデータを渡すこともあります。
テクスチャ作成 実装
DxLib::LoadGraph()
がDXライブラリの画像取り込み関数になります。
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の尺度になっているわけではないので、座標情報が崩壊しかねません。
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()
を呼ぶだけです。
void _spAtlasPage_disposeTexture(spAtlasPage* pAtlasPage)
{
DxLib::DeleteGraph(static_cast<int>(reinterpret_cast<unsigned long long>(pAtlasPage->rendererObject)));
}
ファイルデータ管理
さて、参照関係にあるためspSkeleton
を作成し終わってもspAtlas
とspSkeletonData
は破棄できません。
ここでは次のように封装してしまうことにします。
#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_
#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
から移植する必要があります。
#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 |
最低限必要な機能は毎ループ呼び出す状態更新関数と描画関数になりますが、実用上、乗算済みアルファの切り替え機能は加えることにします。
#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_
#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。
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
から派生したものです。
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()
を実行して十分なメモリを確保しておく必要があります。
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
に処理が纏められました。
コメントにした箇所が次節で述べる独自部分になります。
描画関数 独自部
頂点データと色情報を手に入れたので、これを描画ライブラリに合わせる工程です。
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ライブラリの設定は以下のようにしています。
#ifndef DXLIB_INIT_H_
#define DXLIB_INIT_H_
struct SDxLibInit
{
SDxLibInit();
~SDxLibInit();
int iDxLibInitialised = -1;
};
#endif // !DXLIB_INIT_H_
#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関数はこんな感じです。
マウス入力に応じたアニメーション切り替え・座標移動機能を加えています。
#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;
}
おわりに
実用にはもっと肉付けが必要ですが、DXライブラリとはあまり関係のない話になってしまうので、これにて締めます。公式でも述べられていますが、色の薄さがDXライブラリの特徴なので、別のエンジンなり制作ツールなりにSpineを組み込む際の助けにもなれば幸いです。
説明の都合上コードがぶつ切りになってしまったので纏めたものを暫くはこちらに置いておきます。
-
一応説明を加えると、まず
spSkeleton_update()
ですが、spSlot
にspAttachment
が装着されてからの経過時間を算出するためのものです。とはいえ使い道はなく、4.1
で一旦消えました。そして4.2
からは用途を変えて物理演算のために復活しました。
spAnimationState_apply()
からは_spScaleTimeline_apply()
などが呼ばれます。3.7
以前では時間軸を適切に設定しておかないとここで等倍に戻されてしまいますので、拡縮に関しては他の手段に頼ることになります。
spSkeleton_updateWorldTransform()
には4.2
から物理演算の有無を指定する第二引数が増えています。 ↩ -
4.1
からはspRegionAttachment_computeWorldVertices()
の第二引数がspBone
ではなくspSlot
に変わっています。 ↩ -
2.1
ではSpine側はint
でした。 ↩