Spine runtime - 01.Setup
Spine runtime
Spine についてです。エディタでは無く、プログラムに組み込むための ランタイム について書こうと思います。Unityプラグインではなく、もっと下層の部分です。 数年前にも記事にしましたが、Spineのバージョンも進みました。大きく変わっていませんが、細かいところに変更も入っているので、version 3.8 にて書き直したいと思います。
Spine
こちらになります。詳しくは説明しません。もう持ってますよね?
ランタイム
プログラムからSpineを読み込んで表示させるために必要な物です。ただし、Spineのランタイムは マルチプラットフォーム 前提で組まれており、依存性が無く、標準ライブラリでコンパイルできるように作られています。
つまり、ポリゴン、点や線、サウンドが無い標準的な環境下でコンパイルさせるために、これらの機能が省かれています。私たちは、これらの機能を自分で作って、そしてランタイムと連係して、はじめて画面に表示することが出来るのです。
大変ですね。なので、普通は Unity とか UnrealEngine とか、提供されている便利なコンポーネントを使いますが、ここではそんな恵まれた環境ではない方向けのお話になります。
でも、まずは感覚を掴むために、Unityコンポーネント を使ってみるのをお勧めします。
ファイルのExport
Spineはエディタ用とランタイム用でファイル形式が異なります。まず、ランタイムで使用できるファイルを出力しましょう。公式、SpineBoyを出力すれば、以下3ファイルを得ることが出来ます。
spineboy-pro.json spineboy-pro.atlas.txt spineboy-pro.png
アニメーションデータ(json)、パーツ構造(atlas.txt)、画像(png)となります。-pro
はプロバージョンを表しますが、ランタイムでは関係ありません。エディタのみ、プロバージョンとエントリーバージョンが存在します。
バイナリ形式も出力できますが、json形式が推奨されています。実際の所、読み込み速度も米粒レベル未満の差なので、バイナリ形式の利点は薄いです。
ファイルのバージョン互換性
SpineでExportされたランタイム用のファイルは、残念ながら 後方互換性を持ちません。これはかなり厄介なので覚えておきましょう。追加機能が来たから最新にしてやるぜ!ということを簡単に行えないのが非常に残念な点です。
ですが、一応、バージョンがリリースになれば、そのバージョンに関しては互換性は維持されます。 現在、version 3.8 までがリリース、*version 3.9 がベータです。 エディタのバージョンが 3.8 であれば、ランタイムの 3.8 の 最新版 で安全に読み込めます。 リリースが、もうファイル形式弄らないよ。という宣言ですね。
サンプルプログラム
Spineのランタイムとサンプルプログラムは github からいつでもダウンロードすることが出来ます。
さて、いっぱいありますが、ランタイムを知る上でどのサンプルプログラムが良いでしょうか? 色々あるのですが、C#はUnityなので、ランタイムの構造を知るにはちょっと規模が大きいのと、Unity独自のセオリーが邪魔をします。
お勧めはC++でしょうか。実際にポリゴンで表示するところまで書かれている sfml を使ったサンプルがお勧めです。これをつかって説明していきます。
spine-runtimes/spine-sfml/cpp at 3.8 · EsotericSoftware/spine-runtimes · GitHub
ランタイムを直接扱うような人は、きっと C++ か C ぐらいなものですよね :)
ビルド
まずはビルドに成功してください。 上記のURLの Readme.md に書かれています。 具体的には CMake を使って、VSのプロジェクトファイルと、sfmlのソース一式をダウンロードして準備します。あとはVSで開いてコンパイルします。 なお、サンプルは32bitですが、64bitでもランタイムは問題なくコンパイルできます。構造を知るだけなら32bitで十分です。
まずはじめに
Spineのランタイムは依存性の無いライブラリです。これから依存性を与えていくことになります。ポリゴンなどのグラフィックもそうですが、メモリ確保とファイルIOもその1つです。これら依存を最初に定義する必要があります。それが Extention.h/cpp
です。
spine-runtimes/Extension.h at 3.8 · EsotericSoftware/spine-runtimes · GitHub
class SP_API SpineExtension { ... public: /// Implement this function to use your own memory allocator virtual void *_alloc(size_t size, const char *file, int line) = 0; virtual void *_calloc(size_t size, const char *file, int line) = 0; virtual void *_realloc(void *ptr, size_t size, const char *file, int line) = 0; /// If you provide a spineAllocFunc, you should also provide a spineFreeFunc virtual void _free(void *mem, const char *file, int line) = 0; virtual char *_readFile(const String &path, int *length) = 0; };
この SpineExtension を使って(派生させて)、中身をあなたの環境に書き換える必要があります。 と、それもなかなか大変なので、実は下の方に DefaultSpineExtension というのが用意されています。特に問題が無ければコレを使えばOKです。
_readFile
についてですが、ランタイムにはファイルデータをメモリから読み込む機能があります。実際のアプリケーションでは、パッケージングや暗号化を行う都合上、ファイルから読み込むことはありません。なので、_readFile
は使用されることが無いのであれば、中身の無い nullptr
を返すだけのコードでも構いません。
c の場合も触れておきます。c だとちょっと違います。
void _spAtlasPage_createTexture (spAtlasPage* self, const char* path); void _spAtlasPage_disposeTexture (spAtlasPage* self); char* _spUtil_readFile (const char* path, int* length);
c の場合はクラスはないので、ヘッダにグローバルな関数が定義されています。が、実装部分がありません。実装部分は直接書いてくださいという手法になっています。関数ポインタ指定ではないのでちょっと戸惑いますよね。また、mallocなどは定義済で、ファイルIOとTexture読み込みのみとなっています。
こんな感じで、cは直接実装部を書くスタイルになっています。
さて、c++ での SpineExtention の用意は以下のようになっています。
// extention
DebugExtension dbgExtension(SpineExtension::getInstance());
SpineExtension::setInstance(&dbgExtension);
SpineExtension *SpineExtension::getInstance() { if (!_instance) _instance = spine::getDefaultExtension(); assert(_instance); return _instance; }
DebugExtensionというログとメモリ解放ミスを検知するクラスで包んでいますが、SpineExtension::getInstance()
でデフォルトで用意された物取得して使用しています。
読み込み
Spineファイルを読み込みます。読み込みはまず atlasファイルを読み込んでから、アニメーション関係の jsonを読み込みます。
main.cpp の testcase() 関数に書かれています。
void testcase (...) { SFMLTextureLoader textureLoader; auto atlas = make_unique<Atlas>(atlasName, &textureLoader); auto skeletonData = readSkeletonJsonData(jsonName, atlas.get(), scale); func(skeletonData.get(), atlas.get()); // binary file format // skeletonData = readSkeletonBinaryData(binaryName, atlas.get(), scale); // func(skeletonData.get(), atlas.get()); }
なんですが、実はエラーが出ます。ランタイムの変更が main.cpp
にまだ反映されていないみたいです。そのうち直されると思いますが、一応ここで修正を上げておきます。
// auto atlas = make_unique<Atlas>(atlasName, &textureLoader); spine::Atlas *pAtlas = nullptr; { spine::String sAtlasName(atlasName); TextureLoader *pTextureLoader = &textureLoader; pAtlas = new spine::Atlas(sAtlasName, pTextureLoader, true); } unique_ptr<Atlas> atlas(pAtlas);
変更点は二点。make_unique
が正しく動かないことと、TextureLoader へ渡す文字列は spine::String に変更されたことです。make_unique
は先頭で再定義されてるのでそれが原因かと思いますが、重要ではないので触れません。
勿論、メモリ上から読み込む関数 readSkeletonData()
も用意されています。詳細については今回は省略します。
atlas(画像読み込み)
まず atlas ファイル、画像の読み込みからです。unique_ptrは無視して、spine::TextureLoader を派生したクラスを生成して渡しています。中身はなんてことない、load()とunload()しかないクラスです。c では先ほど _spAtlasPage_createTexture()
として出てきましたね。
namespace spine { class SP_API TextureLoader : public SpineObject { public: virtual void load(AtlasPage& page, const String& path) = 0; virtual void unload(void* texture) = 0; }; }
見慣れない=0
は他の言語で言う abstract です。実態が無いので派生クラスで実装します。ここでいう派生クラスが SFMLTextureLoader です。抜粋すると、
void SFMLTextureLoader::load(AtlasPage &page, const String &path) { Texture *texture = new Texture(); if (!texture->loadFromFile(path.buffer())) return; if (page.magFilter == TextureFilter_Linear) texture->setSmooth(true); if (page.uWrap == TextureWrap_Repeat && page.vWrap == TextureWrap_Repeat) texture->setRepeated(true); page.setRendererObject(texture); Vector2u size = texture->getSize(); page.width = size.x; page.height = size.y; } void SFMLTextureLoader::unload(void *texture) { delete (Texture *) texture; }
となっています。
path
には読み込むべき画像のファイルパスが設定されています。これは TextureLoader 時に指定した atlasName
に、atlasファイルパスに書かれている画像ファイル名 spineboy-pro.png
を足した物です。なので data/hogehoge/
に格納した場合は以下になります。
atlasName : data/hogehoge/sample.atlas.txt path : data/hogehoge/sample.png
page
はテクスチャ情報などを記憶しておく物で、テクスチャクラス、サイズ情報を書き加えて返還しています。unload()時にはこれらを元に解放してください。
このテクスチャ読み込みは、Spineによって1枚だったり、2枚だったりします。2枚なら二回呼ばれます。
特に難しい加代はありませんが、1つ注意点があるとすれば、ここの String は spine::String です。c++は他の言語と違い独自のstringを使用しているので注意してください。文字コードは UTF8 のcharです。2バイトUnicode(utf16)ではありません。
Json SkeletonData(データ読み込み)
アニメーションに関する情報を読み込みます。
shared_ptr<SkeletonData> readSkeletonJsonData (const String& filename, Atlas* atlas, float scale) { SkeletonJson json(atlas); json.setScale(scale); auto skeletonData = json.readSkeletonDataFile(filename); if (!skeletonData) { printf("%s\n", json.getError().buffer()); exit(0); } return shared_ptr<SkeletonData>(skeletonData); }
2段階に手順が分かれています。まず json ファイルの解析用に SkeletonJson を使用、読み込みます。次に、実際のアニメーション時に使用する SkeletonData へ変換します。SkeletonJson はもう使わないので解放してください。
json.setScale()
について補足しておきます。これはサイズを調整するものですが、データそのものに手を加えます。Spineエディタ上では x:100
であれば、スケールが0.01の場合は x:1
になります。普通の使い方では使用しない(1倍)方が賢明です。これが使用されているのはUnityの場合です。Unityは座標系は 1/100 が基準なので、それに合わせて縮小されています。Unityで値を拾うと小さくなっていて数値が合わない???ってハマるので覚えておいてください。
Skeleton AnimationStateData(状態クラス)
読み込みが完了しましたが、アニメーションさせるには状態を管理するステートクラスが必要になります。AnimationStateData , Skeleton の2つが必要になります。 この2つを生成していきましょう。
SkeletonDrawable drawable(skeletonData); drawable.timeScale = 1; drawable.setUsePremultipliedAlpha(true);
残念。実際のコードは SkeletonDrawable という、sfml で spine を表示する独自定義したクラスの中にあるようです。
skeleton = new(__FILE__, __LINE__) Skeleton(skeletonData); stateData = new(__FILE__, __LINE__) AnimationStateData(skeletonData);
デバッグ用のコードが入ってますが無視して、それぞれアニメーションデータである SkeletonData を元に生成するだけです。簡単ですね。
Skeleton
Skeleton はボーンの情報を保持するクラスです。Spineエディタで作ったボーンやアタッチメントがそのままこの中に入っています。これをプログラムから自由にいじることができます。いじったボーンはアニメーションがない限り、値はそのまま保持され続けます。
Spineのサンプルにあるような、プログラムから変更を行うものはほぼ9割以上この Skeleton を介して行われています。
AnimationStateData
アニメーション、およびトラック情報を保持します。アニメーションの詳細は次に説明しますが、Spineでは複数のアニメーションを何個も重ねることができます。これは動きを分けて管理することができ、アニメーション作成の負担を減らします。(なんですが、ランタイムの機能であり、エディタではこれができません。というわけで作ったのが SpineAnimationrig です。)これら複雑なアニメーションの管理を行います。
プログラムからはアニメーションの追加と削除、そして再生中のアニメーションを操作するトラック(track)の取得などに使用します。アニメーションそのものに変更を加えたりなど、高度な操作を行うことはまずないでしょう。
この AnimationStateData , Skeleton ですが、ここから SkeletonData クラスの内部データへアクセスする参照を持っています。なので、実際プログラムではこの2つを介して全ての操作を行います。SkeletonData は、解放してはいけませんが、private空間に押し込んでしまって構いません。
さて、これでアニメーションさせる手順は整いました。
- atlas + texture
- SkeletonData
- AnimationStateData
- Skeleton
の1+3クラスが動作に最低限必要な構成となります。
更新 アップデート
Spineの1フレームでの動作は以下の通りになります。
void SkeletonDrawable::update(float deltaTime) { skeleton->update(deltaTime); state->update(deltaTime); state->apply(*skeleton); skeleton->updateWorldTransform(); }
delttime
は1秒を 1.0f とした値です。60fpsならば 1/60 になります。
これを毎フレーム実行するだけでアニメーションします。勿論、表示は別ですよ。
ではそれぞれの役割を説明します。
skeleton->update()
昔は無かったと思います。実は、今のところ何もしていません。経過時間だけを記憶しているようです。今後使用される可能性もあるので、呼んでおきましょう。
state->update()
アニメーションの時間を進めます。もうちょっと詳しく言うと、アニメーショントラックの時間を進めます。
時間を進めるだけで、アニメーションの切り替えは次の state->apply で行われます。ここが曲者で、アニメーションの切り替え(setAnimation)の反映はこの時点では行われません。実行キューに追加されて、スタンバイに入ります。つまり、一度に大きく時間を進めた場合(スキップなど)、アニメーションの開始時間で誤差が出る可能性があります。これについてはまた書きたいと思いますが、今のところは毎フレームの頻度で呼ぶことを心がけましょう。
state->apply()
アニメーションの処理を行い、内容を skeleton に反映させます。skeleton はキャラクターのボーン情報を持った物で、アニメーションをもとにボーンの角度や位置を反映(上書き)していきます。上書き です。後々重要になるので覚えておいてください。
1つ注意して欲しいのが、ここでは個々のボーンの角度や位置しか反映されません。つまり、rootからの変形行列もskeletonは保持していますが、反映されるのは updateWorldTransform で行われます。
また、apply では IK制御や メッシュ変形 も行われます。他の更新命令と比べると 最も重い処理 を行うため、何回も呼ばないように気を付けてください。
skeleton->updateWorldTransform()
ボーンの位置や角度から、変形行列を生成します。なので、実はそんなに重くありません。頂点の計算もここでは行われません。頂点の計算は次の記事で説明します。
ボーン変更するならどのタイミング?
state->apply()の後で上書きすればOKです。
state->apply();
bone->setRotate(30);
skeleton->updateWorldTransform()
ただし、アニメーションの結果に値の変更を加えたい場合は一筋縄ではありません。例えば、現在の値に30度足す場合は以下のようになります。
bone->setRotate(bone->getRotate() + 30);
これは場合によって期待通りに動きません。それは、基本的にボーンの情報は保持され続けるためです。毎フレームごとに30度加算され続けます。(アニメーションが存在していれば state->apply() で上書きされます)
というわけで、どこかで値を初期化する必要があります。アニメーションで値が設定される前、つまり state->apply() の前が最適です。
skeleton->update(deltaTime); state->update(deltaTime); preparePhysics(); // 事前に物理計算ボーンを初期位置に上書き state->apply(*skeleton); // アニメーションの値が上書きされる skeleton->updateWorldTransform(); ApplyPhysics(); // skeletonの現在の値に加算する skeleton->updateWorldTransform();
上記は物理計算を反映させる場合の一例です。 物理計算はボーンの行列が必要になるため updateWorldTransform() が2回呼ばれています。物理計算は重い物なのです。
最後に
基本的な Spine の読み込み処理になります。
が、まだアニメーションも制御していなければ、表示すらできていません。 そのあたりを次の記事で説明します。