spine-cランタイム ドキュメント

ライセンスについて

Spineランタイムをアプリケーションに組み込む前に、必ずSpine Runtimes Licenseを確認してください。

はじめに

spine-cとは、C、C++、Swift、Rust、Objective-Cなど、C言語とネイティブに連携できる言語で書かれたゲームエンジンやフレームワークにSpineのアニメーションを組み込むための汎用ランタイムです。

spine-cは、以下の機能を提供します。

spine-cランタイムはエンジンに依存しない汎用ランタイムであるため、ユーザー自身が、エンジン固有のファイル入出力や画像のロード、エンジンのレンダリングシステムを介したspine-cランタイムで生成されたデータのレンダリング、さらにラグドールなどの高度なユースケースが要求される場合にはエンジンの物理システムとの統合を行う一連の関数を実装する必要があります。

spine-cランタイムは ANSI C89 で記述されており、さまざまなプラットフォームやコンパイラとの互換性が保証されています。

また、他の公式Spineランタイムはspine-cをベースにしているため、お好みのエンジンとの統合のためのサンプルとして利用できます:

  • spine-cocos2d-objc: Cocos2D-Obj用のランタイム。
  • spine-sfml: SFML用のランタイムであり、最もシンプルなspine-cベースのランタイム。

以下の各セクションでは、spine-cランタイムの概要とその使用方法について、エンジンに依存しない形で説明します。spine-cをベースにした公式Spineランタイムの多くは、spine-c APIの一部を独自の使いやすいAPIにカプセル化しています。しかし、基礎となるspine-cランタイムの基本を理解しておくことは有益です。

注意: 当ガイドでは、Spineで使用される基本的なランタイムのアーキテクチャと用語を理解されていることを前提としています。ランタイムのより高度な機能については、APIリファレンスも参照してください。

Spineアセットのspine-c向けエクスポート

以下の実行方法については、Spineユーザーガイド内で紹介されています :

  1. JSONまたはバイナリ形式でのスケルトン&アニメーションデータのエクスポート
  2. スケルトンの画像を含むテクスチャアトラスのエクスポート

スケルトンデータとテクスチャアトラスをエクスポートすると、以下のファイルが得られます :

  1. skeleton-name.jsonまたはskeleton-name.skel: これはスケルトンとアニメーションのデータを含んでいます。
  2. skeleton-name.atlas: これはテクスチャアトラスの情報を含んでいます。
  3. 1つまたは複数の.pngファイル: これはテクスチャアトラスの各ページで、スケルトンが使用するイメージを含んでいます。

注意: スケルトンごとに1つのテクスチャアトラスを作成するのではなく、複数のスケルトンの画像を1つのテクスチャアトラスにパックすることもできます。詳しくはテクスチャパッキングのガイドをご覧ください。

Spineアセットのロード

spine-cは、テクスチャアトラス、Spineスケルトンデータ(ボーン、スロット、アタッチメント、スキン、アニメーション)をロードし、AnimationStateDataによってアニメーション間のミックスタイムを定義するためのAPIを提供します。この3種類のデータはセットアップポーズデータとも呼ばれ、通常、一度ロードされた後はすべてのゲームオブジェクトで共有されます。この共有の仕組みは、各ゲームオブジェクトに独自のスケルトンとAnimationStateを与えることで実現されており、インスタンスデータとも呼ばれています。

補足: ローディングの全体的なアーキテクチャの詳細については、全般的なSpineランタイムドキュメントを参照してください。

テクスチャアトラスのロード

テクスチャアトラスデータは、アトラスページ内の個々の画像の位置を記述したカスタムアトラス形式で保存されます。アトラスページ自体はアトラスとは別にプレーンな.pngファイルとして保存されます。

spine-cはこのタスクのためにspAtlas_createFromFileおよびspAtlas_createという関数を提供しています。前者はファイルからアトラスをロードし、後者はメモリからアトラスをロードします。アトラスのロードに失敗した場合、これらの関数は0を返します。

// ファイルからアトラスを読み込む。最後の引数は
// atlas->rendererObjectに格納されるvoid*。
spAtlas* atlas = spAtlas_createFromFile("myatlas.atlas", 0);

// アトラスをメモリから読み込む。メモリの場所、バイト数、
// アトラスページのテクスチャを読み込むディレクトリを
// 相対的に指定します。
// 最後の引数は、atlas-rendererObjectに格納されるvoid*。
spAtlas* atlas = spAtlas_create(atlasInMemory, atlasDataLengthInBytes, dir, 0);

スケルトンデータのロード

スケルトンデータ (ボーン、スロット、アタッチメント、スキン、アニメーション)は、ヒューマン・リーダブル(対人可読形式)なJSONまたはカスタムバイナリ形式でエクスポートできます。spine-cはスケルトン データをspSkeletonData構造体に格納します。

JSON形式のエクスポートファイルからスケルトンデータを読み込むためには、先に読み込んだspAtlasを受け取り、スケルトンのスケールを設定し、最後にファイルからスケルトンデータを読み込むspSkeletonJsonを作成します:

// ロードに使用するspSkeletonJsonを作成(create)し、
// ロードしたデータが元データの2倍の大きさになるようにスケールを設定する
spSkeletonJson* json = spSkeletonJson_create(atlas);
json->scale = 2;

// スケルトンの.jsonファイルをspSkeletonDataに読み込む
spSkeletonData* skeletonData = spSkeletonJson_readSkeletonDataFile(json, filename);

// ロードに失敗した場合は、エラーを表示してアプリを終了する
if (!skeletonData) {
   printf("%s\n", json->error);
   spSkeletonJson_dispose(json);
   exit(0);
}

// spSkeletonJsonは、ロード後、不要になったので破棄(dispose)する
spSkeletonJson_dispose(json);

バイナリ形式のエクスポートファイルからスケルトンデータを読み込むのも、代わりにspSkeletonBinaryを使用する以外は同じです:

// ロードに使用するspSkeletonBinaryを作成(create)し
// ロードしたデータが元データの2倍の大きさになるようにスケールを設定する
spSkeletonBinary* binary = spSkeletonBinary_create(atlas);
binary->scale = 2;

// スケルトンの.skelファイルをspSkeletonDataに読み込む
spSkeletonData* skeletonData = spSkeletonBinary_readSkeletonDataFile(binary, filename);

// ロードに失敗した場合は、エラーを表示してアプリを終了する
if (!skeletonData) {
   printf("%s\n", binary->error);
   spSkeletonBinary_dispose(json);
   exit(0);
}

// spSkeletonBinaryは、ロード後、不要になったので破棄(dispose)する
SkeletonBinary_dispose(json);

AnimationStateDataの準備

Spineは、あるアニメーションから別のアニメーションに切り替える際のスムーズなトランジション(クロスフェード)に対応しています。このクロスフェードは、あるアニメーションと別のアニメーションを指定したミックスタイムの間だけミックスすることで実現されています。spine-cランタイムはこれらのミックスタイムを定義するためにspAnimationStateData構造体を提供します:

// spAnimationStateDataを作成(create)する
spAnimationStateData* animationStateData = spAnimationStateData_create(skeletonData);

// 任意のアニメーションのペア間のデフォルトのミックスタイムを秒単位で設定する
spAnimationStateData_setDefaultMix(animationStateData, 0.1f);

// "jump"から"walk"までのアニメーション間のミックスタイムを0.2秒に設定し、
// この前後のペアのデフォルトのミックスタイムを上書きする
spAnimationStateData_setMixByName(animationStateData, "jump", "walk", 0.2f);

spAnimationStateDataで定義されたミックスタイムは、アニメーションの適用時に明示的に上書きすることも可能です(後述)。

スケルトン

セットアップポーズデータ(スケルトンデータ、テクスチャアトラス)は、各ゲームオブジェクト間で共有されることが前提になっています。spine-cでは、この共有を容易にするために、 spSkeleton構造体を提供しています。各ゲームオブジェクトはspSkeletonインスタンスを受け取り、データソースとしてspSkeletonDataおよびspAtlasインスタンスを参照します。

spSkeletonは、ボーンをプロシージャルに変更したり、アニメーションを適用したり、ゲームオブジェクトに固有のアタッチメントやスキンを設定するなど、自由に変更できますが、基盤となるスケルトンデータやテクスチャアトラスには何ら影響しません。この仕組みと設定により、spSkeletonDataおよびspAtlasインスタンスを、任意の数のゲームオブジェクト間で共有することができます。

スケルトンの作成

spSkeletonインスタンスを作成するには、 spSkeleton_createを呼び出します:

spSkeleton* skeleton = spSkeleton_create(skeletonData);

すべてのゲームオブジェクトは、それ自身のspSkeletonを必要とします。データの大部分はspSkeletonDataspAtlasに残り、すべてのspSkeletonインスタンスで共有され、メモリの消費とテクスチャの切り替えを大幅に削減します。したがって、spSkeletonのライフタイム(寿命)は、対応するゲームオブジェクトのライフタイムと連動しています。

ボーン

スケルトンはボーンによる階層構造で、ボーンにスロットが、スロットにアタッチメントが取り付けられています。

ボーンを探す

スケルトンに含まれるすべてのボーンは、そのスケルトンから取得できる一意の名前を持ちます:

// その名前のボーンが見つからなかった場合、0 を返します。
spBone* bone = spSkeleton_findBone("mybone");

ローカルトランスフォーム

ボーンは、rootボーンに戻るまで、その親ボーンに影響されます。例えば、あるボーンを回転させると、そのすべての子ボーンと、さらにそれらのすべての子ボーンも回転されます。これらの階層的なトランスフォームを達成するために、各ボーンは、以下の要素からなる親ボーンに相対するローカルトランスフォームを持っています:

  • 親を基準としたxy座標。
  • 度単位のrotation(回転)
  • scaleX(スケールX)scaleY(スケールY)
  • 度単位のshearX(シアーX)shearY(シアーY)

ボーンのローカルトランスフォームは、プロシージャルな方法、またはアニメーションの適用によって操作することができます。前者では、ボーンがマウスカーソルを指すようにしたり、足のボーンを地形に沿わせたりといった動的な動作を実装することができます。また、ローカルトランスフォームのプロシージャルな変更と、アニメーションの適用は同時に行うことができます。最終的には、1つのローカルトランスフォームを組み合わせたものになります。

ワールドトランスフォーム

ボーンのローカルトランスフォームをプロシージャルに変更するか、アニメーションを適用するかによって全てのローカルトランスフォームが設定されると、レンダリングと物理のために各ボーンのワールドトランスフォームが必要になります。

この計算はrootボーンから始まり、すべての子ボーンのワールドトランスフォームを再起的に計算します。この計算では、アーティストがSpineエディター内で定義したIKトランスフォームパスなどのコンストレイントも適用されます。

このようにワールドトランスフォームを計算するために、spine-cランタイムは以下の関数を提供しています:

spSkeleton_updateWorldTransform(skeleton);

結果は各ボーンに保存され、以下のように構成されます:

  • abcdは、ボーンの回転、スケール、シアーを符号化した2x2列のメジャー行列
  • worldXworldYは、ボーンのワールド座標を保管

worldXworldYは、skeleton->xskeleton->yによってオフセットされることに注意してください。これらの2つのフィールドは、ゲームエンジンのワールド座標系でスケルトンを配置するために使うことができます。

通常、ボーンのワールドトランスフォームは直接変更してはいけません。代わりに、spSkeleton_updateWorldTransformを呼び出すことで、常にスケルトン内のボーンのローカルトランスフォームから派生させる必要があります。ローカルトランスフォームは、プロシージャルに設定することができます。例えば、マウスカーソルを指すようにボーンの回転を設定したり、アニメーションを適用(下記参照)したり、その両方を同時に行うことができます。(プロシージャルな)アニメーションが適用されると、spSkeleton_updateWorldTransformが呼び出され、結果のワールドトランスフォームはローカルトランスフォームと、ボーンに適用されている全てのコンストレイントに基づいて再計算されます。

座標系の変換

他のエンティティや入力イベントからの座標が通常与えられる場所であるため、ワールド座標系でボーンを操作した方が簡単であることがよくあります。しかし、ワールドトランスフォームは直接操作されるべきではないので、ワールド座標系の計算に基づくボーンへの変更を、そのボーンのローカルトランスフォームに適用する必要があります。

spine-cランタイムは、ボーンの2x2ワールドトランスフォーム行列から回転とスケールの情報を抽出し、位置と回転をローカル空間からワールド空間へ、またはその逆に変換する関数を提供します。これらの関数はすべて、ボーンのワールドトランスフォームがspSkeleton_updateWorldTransformを呼び出すことによって事前に計算されていることを前提としています:

spBone* bone = spSkeleton_findBone("mybone");

// ワールド空間のX軸に対するボーンの回転を度数で取得する
float rotationX = spBone_getWorldRotationX(bone);

// ワールド空間のY軸に対するボーンの回転を度単位で取得する
float rotationY = spBone_getWorldRotationY(bone);

// ワールド空間のX軸に対するボーンのスケールを取得する
float scaleX = spBone_getWorldScaleX(bone);

// ワールド空間のY軸に対するボーンのスケールを取得する
float scaleY = spBone_getWorldScaleY(bone);

// ワールド空間で与えられた位置を、ボーンのローカル空間に変換する
float localX = 0, localY = 0;
spBone_worldToLocal(bone, worldX, worldY, &localX, &localY);

// ボーンのローカル空間で与えられた位置を、ワールド空間に変換する
float worldX = 0, worldY = 0;
spBone_localToWorld(bone, localX, localY, &worldX, &worldY);

// ワールド空間のx軸に相対するボーンのワールドトランスフォームで与えられた回転を、
// ローカル空間のx軸に相対するローカル空間で与えられた回転に度単位で変換する
float localRotationX = spBone_worldToLocalRotationX(bone)

// ワールド空間のy軸に相対するボーンのワールドトランスフォームで与えられた回転を、
// ローカル空間のy軸に相対するローカル空間で与えられた回転に度単位で変換する
float localRotationY = spBone_worldToLocalRotationY(bone)

注意: ボーン(およびそのすべての子)のローカルトランスフォームへの変更は、次にspSkeleton_updateWorldTransformを呼び出した後に、ボーンのワールドトランスフォームに反映されます。

ポジションの決定

デフォルトでは、スケルトンはゲームのワールド座標系の原点にあると仮定されます。ゲームのワールド座標系でスケルトンを配置するには、xおよびyフィールドを使用します:

// ワールド空間でゲームオブジェクトに追従するスケルトンを作成
skeleton->x = myGameObject->worldX;
skeleton->y = myGameObject->worldY;

注意: スケルトンのxおよびyフィールドに加えた変更は、次にspSkeleton_updateWorldTransformを呼び出した後に、ボーンのワールドトランスフォームに反映されます。

反転

スケルトンは垂直または水平に反転させることができます。これにより、ある方向用に作成したアニメーションを反対方向に再利用したり、Y軸が下向きの座標系で作業することができます(SpineはデフォルトでY軸が上向きの座標系を想定しています):

// x軸を中心に垂直に反転する
skeleton->flipX = 1;

// y軸を中心に水平反転しない
skeleton->flipY = 0;

注意: スケルトンのflipXおよびflipYフィールドに加えた変更は、次にspSkeleton_updateWorldTransformを呼び出した後に、ボーンのワールドトランスフォームに反映されます。

スキンの設定

Spineのスケルトンを作成したアーティストは、同じスケルトンに複数のスキンを追加して、女性版と男性版など、視覚的なバリエーションを追加することができます。spine-cランタイムはスキンをspSkinインスタンスに格納します。

ランタイムにおけるスキンは、どのアタッチメントをスケルトンのどのスロットに入れるかを定義したマップです。すべてのスケルトンは少なくとも1つのスキンを持っており、どのアタッチメントがスケルトンのセットアップ・ポーズのどのスロットにあるのかを定義しています。追加されたスキンは、それらを識別するための名前を持ちます。

spine-cでスケルトンにスキンを設定:

// 名前によってスキンを設定
spSkeleton_setSkin(skeleton, "my_skin_name");

// 0を渡してデフォルトのセットアップポーズのスキンを設定する
spSkeleton_setSkin(skeleton, 0);

注意: スキンの設定は、以前に設定されたスキンやアタッチメントが考慮されます。スキン設定の詳細については、総合的なランタイムガイドを参照してください。

アタッチメントの設定

spine-cは、例えば武器を切り替えたりするために、スケルトンのスロットに直接アタッチメントを1つ設定することができます。アタッチメントは、まずアクティブなスキンで検索され、これが失敗した場合は、デフォルトのセットアップポーズスキンで検索されます:

// "hand"スロットに"sword"というアタッチメントをセットする
spSkeleton_setAttachment(skeleton, "hand", "sword");

// スロット"hand"のアタッチメントをクリアして、何も表示されないようにする
spSkeleton_setAttachment(skeleton, "hand", 0);

ティント

スケルトンのカラーを設定することで、スケルトン内のすべてのアタッチメントをティント(着色)することができます:

spSkeleton* skeleton = ...

// すべてのアタッチメントを赤で着色し、スケルトンを半分だけ透明にする
skeleton->r = 1.0f;
skeleton->g = 0.0f;
skeleton->b = 0.0f;
skeleton->a = 0.5f;

注意: spine-cにおける色はRGBAで示され、各チャンネルの値は[0-1]の範囲になります。

スケルトンを描画するとき、レンダラーはスケルトン上のスロットの表示順序をたどり、各スロットに現在アクティブなアタッチメントを描画します。スケルトンのカラーに加えて、各スロットも実行時に操作可能なカラーを持っています:

spSlot* slot = skeleton->findSlotByName("mySlot");
slot->r = 0.0f;
slot->g = 1.0f;
slot->b = 0.0f;
slot->a = 1.0f;

なお、スロットカラーもアニメーションさせることができます。スロットカラーを手動で変更した後に、そのスロットカラーをキーにしたアニメーションを適用すると、手動での変更内容は上書きされることに注意してください。

アニメーションの適用

Spineエディターでは、アーティストが一意の名前のアニメーションを複数作成することができます。アニメーションとは、タイムラインの集合体です。各タイムラインは、ボーンやスケルトンのどのプロパティが、どのフレームでどのような値に変化するかを指定します。タイムラインには、時間経過によるボーンのトランスフォームを定義するタイムラインから、表示順序を変更するタイムラインまで、様々なタイプがあります。タイムラインはスケルトンデータの一部で、spine-cのspSkeletonDataに格納されています。

Timeline API

spine-cは、タイムラインを直接操作する必要が生じた場合に、Timeline APIを提供します。この低水準の機能により、アーティストが定義したアニメーションがスケルトンに適用される方法を完全にカスタマイズすることができます。

AnimationState API

ほとんどの場合、Timeline APIではなくAnimationState APIを使用する必要があります。AnimationState APIは、アニメーションの時間経過による適用、アニメーションのキューイング、アニメーション間のミックス、複数のアニメーションの同時適用などのタスクを、低水準のTimeline APIよりもかなり簡単に実行できます。AnimationState API は内部的には Timeline API を使用しており、ラッパーと見なすことができます。

spine-cはspAnimationState構造体によってアニメーションの状態を表現します。スケルトンと同じように、spAnimationStateはゲームオブジェクトごとにインスタンス化されます。一般的には、ゲーム内のゲームオブジェクトごとに1つのspSkeletonと1つのspAnimationStateインスタンスを持つことになります。そしてspSkeletonと同様に、spAnimationState も、(アニメーションとそのタイムラインが格納されている)spSkeletonDataと、(ミックスタイムが格納されている)spAnimationStateDataを、同じスケルトンデータを取得する他のすべてのspAnimationStateインスタンスと共有し、同じスケルトンデータをソースとすることができます。

AnimationStateの作成

spAnimationStateインスタンスを作成するには、以下のようにspAnimationState_createを呼び出します:

spAnimationState* animationState = spAnimationState_create(animationStateData);

この関数は、通常スケルトンデータのロードの際に作成されるspAnimationStateDataを受け取り、デフォルトのミックスタイムやクロスフェード用の特定のアニメーション間のミックスタイムを定義します。

トラックとキューイング

AnimationStateは、1つまたは複数のトラックを管理します。各トラックはアニメーションのリストで、トラックに追加された順番に再生します。これはキューイングと呼ばれます。トラックは0から始まるインデックスを持っています。

以下のようにして、トラック上にアニメーションをキューすることができます:

// アニメーション"walk"をトラック0に、遅延(delay)なしで追加し、無限にループさせる
int track = 0;
int loop = 1;
float delay = 0;
spAnimationState_addAnimationByName(animationState, track, "walk", loop, delay);

また、複数のアニメーションを一度にキューして、アニメーションシーケンスを作成することができます:

// walkをスタート (ループ有り)
spAnimationState_addAnimationByName(animationState, 0, "walk", 1, 0);

// 3秒後にジャンプ
spAnimationState_addAnimationByName(animationState, 0, "jump", 0, 3);

// ジャンプが完了したら、無限に待機(idle)を再生
spAnimationState_addAnimationByName(animationState, 0, "idle", 1, 0);

また、トラック内にキューされているすべてのアニメーションをクリアすることができます:

// トラック0にキューされているアニメーションを全てクリアする
spAnimationState_clearTrack(animationState, 0);

// 全トラックでキューされているアニメーションを全てクリアする
spAnimationState_clearTracks(animationState);

クリアして新しいアニメーションをトラックに追加する代わりに、spAnimationState_setAnimationByNameを呼び出すこともできます。これはすべてのトラックをクリアしますが、クリアする前に最後に再生されたアニメーションが何であったかを記憶し、新たに設定されたアニメーションにクロスフェードします。これにより、あるアニメーション・シーケンスから次のアニメーション・シーケンスへスムーズに移行することができます。spAnimationState_setAnimationByNameを呼び出した後にspAnimationState_addAnimationByNameを呼び出すと、さらにアニメーションをトラックに追加することができます:

// 現在トラック0で再生されているものが何であれ、トラックをクリアして
// "shot"アニメーションにクロスフェードする。これはループしません(最後のパラメータで指定)
spAnimationState_setAnimationByName(animationState, 0, "shot", 0);

// 射撃の後、再び待機(idle)させる
spAnimationState_addAnimationByName(animationState, 0, "idle", 1, 0);

アニメーションからスケルトンのセットアップポーズにクロスフェードするには、spAnimationState_setEmptyAnimationspAnimationState_addEmptyAnimationを使います。前者は現在のトラックをクリアしてスケルトンにクロスフェードし、後者はトラック上のアニメーションシーケンスの一部としてセットアップポーズにクロスフェードをエンキューします。

// 現在トラック0で再生されているものが何であれ、トラックをクリアして
// セットアップポーズに0.5秒(ミックスタイム)、1秒間の遅延(delay)でクロスフェードする
spAnimationState_setEmptyAnimation(animationState, 0, 0.5f, 1);

// トラック0のアニメーションシーケンスの一部として、
// セットアップポーズに0.5秒間のクロスフェードを追加する
spAnimationState_addEmptyAnimation(animationState, 0, 0.5f)

シンプルなゲームでは、通常、1つのトラックだけ使用すれば十分に目的を達成できます。より複雑なゲームでは、例えば、射撃中に歩行アニメーションを同時に再生するなど、別々のトラックでアニメーションをキューイングすることもできます。ここでSpineの真価が発揮されます:

// トラック0に"walk"アニメーションを無限に適用する
spAnimationState_setAnimationByName(animationState, 0, "walk", 1);

// トラック1に"shot"アニメーションを 1 回だけ適用する
spAnimationState_setAnimationByName(animationState, 1, "shot", 0);

このように同時にアニメーションを適用すると、両方のアニメーションがキーにしているすべての値について、上位トラックのアニメーションが下位トラックのアニメーションを上書きしてしまうことに注意してください。つまり、アニメーションのオーサリング(組み合わせ)を行う場合、同時に再生する2つのアニメーションが、そのスケルトンで同じ値(同じボーン、アタッチメント、色など)をキーにしないことを確認してください。

TrackEntryにより、異なるトラックでのアニメーションのミキシングを制御することができます。

トラックエントリ

AnimationStateのトラックでアニメーションをエンキューするたびに、対応する関数はspTrackEntryインスタンスを返します。このトラックエントリにより、キューされたアニメーションや、同じトラックや他のトラック上のアニメーションとのミキシング挙動をさらにカスタマイズすることができます。変更可能なフィールドの完全なリストはTrackEntryのドキュメントを参照してください。

例えば、spAnimationStateDataで定義された"walk"と"run"アニメーションのミックスタイムが、このゲームオブジェクトの現在の状況に対して高すぎると仮定してみましょう。このキューに入れられたアニメーションのために、"walk "と "run "のミックスタイムをアドホックに変更することができます:

// 無限に"walk"する
spAnimationState_setAnimationByName(animationState, 0, "walk", 1);

// ある時点で、"run"アニメーションをキューに入れる。この特定の呼び出しを速くするために、
// `spAnimationStateData`で定義された"walk"と"run"の間のミックスタイム(仮に0.5秒とします)
// を速くします(ここでは0.1秒に設定)。
spTrackEntry* entry = spAnimationState_addAnimationByName(animationState, 0, "run", 1, 0);
entry->mixTime = 0.1f;

spTrackEntryを保持することで、時間の経過とともに変更することができます。spTrackEntryは、そのトラックにアニメーションがキューイングされている間、有効です。アニメーションが完了すると、spTrackEntryは割り当て解除されます。それ以降のアクセスは無効となり、セグメンテーション違反が発生するでしょう。リスナーを登録すれば、アニメーションやトラックエントリが無効になったときに通知を受けることができます。

イベント

AnimationStateは、キューイングされたアニメーションを再生しながらイベントを生成し、リスナーに以下の変更を通知します:

  • アニメーションが開始された(start)
  • トラックをクリアするなどにより、アニメーションが中断された(interrupt)
  • アニメーションが完了した(complete)。※ループしている場合は複数回発生
  • アニメーションが終了した(end)。※中断されたか、または完了しループされていない場合に発生。
  • アニメーションとそれに対応するspTrackEntry破棄された(dispose)、且つ無効になった。
  • ユーザーが定義したイベント(event)が発生した。

これらのイベントは、AnimationState、またはAnimationStateから返される個々の spTrackEntryインスタンスに関数を登録することで、リッスンすることができます。

// イベントが発生したときに呼び出される関数を定義する
void myListener(spAnimationState* state, spEventType type, spTrackEntry* entry, spEvent* event) {
   switch (type) {
   //
   case SP_ANIMATION_START:
      printf("Animation %s started on track %i\n", entry->animation->data->name, entry->trackIndex);
      break;
   case SP_ANIMATION_INTERRUPT:
      printf("Animation %s interrupted on track %i\n", entry->animation->data->name, entry->trackIndex);
      break;
   case SP_ANIMATION_END:
      printf("Animation %s ended on track %i\n", entry->animation->data->name, entry->trackIndex);
      break;
   case SP_ANIMATION_COMPLETE:
      printf("Animation %s completed on track %i\n", entry->animation->data->name, entry->trackIndex);
      break;
   case SP_ANIMATION_DISPOSE:
      printf("Track entry for animation %s disposed on track %i\n", entry->animation->data->name, entry->trackIndex);
      break;
   case SP_ANIMATION_EVENT:
      printf("User defined event for animation %s on track %i\n", entry->animation->data->name, entry->trackIndex);
      printf("Event: %s: %d, %f, %s\n", event->data->name, event->intValue, event->floatValue, event->stringValue);
      break;
   default:
      printf("Unknown event type: %i", type);
   }
}

// AnimationStateに関するリスナーとして関数を登録する。この関数は、
// AnimationStateにキューされたすべてのアニメーションに対して呼び出されます
animationState->listener = myListener;

// あるいは、エンキューされた特定のアニメーションのイベントのリスナーとしてこの関数を登録することもできます
spTrackEntry* trackEntry = spAnimationState_setAnimationByName(animationState, 0, "walk", 1);
trackEntry->listener = myListener;

ユーザー定義イベントは、アニメーションの中で足音などを再生する時間をマークアップするのに最適な機能です。

新しいアニメーションの設定など、リスナー内で行われたAnimationStateの変更は、次にspAnimationState_applyが呼び出されるまでスケルトンに適用されません。リスナー内で変更を即座に適用することができます。

void myListener(spAnimationState* state, spEventType type, spTrackEntry* entry, spEvent* event) {
   if (somecondition) {
      spAnimationState_setAnimation(state, 0, "run", 0);
      spAnimationState_update(0);
      spAnimationState_apply(skeleton);
   }
}

AnimationStateを適用する

AnimationStateは、本質的に時間ベースです。状態を変化させるには、tickごとに更新する必要があり、最後の更新からの経過時間を秒単位で指定します:

spAnimationState_update(deltaTime);

これにより、各トラックのアニメーションの再生を進めたり、クロスフェードを調整したり、登録したリスナーを呼び出したりすることができます。

AnimationStateを更新した後、それをスケルトンに適用して、そのボーンのローカルトランスフォーム、アタッチメント、スロットカラー、表示順序、その他アニメーションできるものを更新するには以下を行います:

spAnimationState_apply(skeleton);

スケルトンのポーズとアニメーションが完了したら、最後に各ボーンのワールドトランスフォームを更新して、レンダリングや物理処理の準備をします:

spSkeleton_updateWorldTransform(skeleton);

メモリ管理

私たちは、spine-cのメモリを可能な限り単純化するように努めました。spStructName_create によってアロケート(割り当て)された構造体は、対応するspStructName_disposeによって解放される必要があります。各構造体のライフタイムは、その構造体がどのようなタイプであるかに依存します。一般的なルールは以下の通りです:

  • ゲームまたはレベル起動時にインスタンスデータ(spAtlas, spSkeletonData, spAnimationStateData)と共有するセットアップポーズデータを作成し、ゲームまたはレベルの終了時に破棄します。
  • 対応するゲームオブジェクトの生成時にインスタンスデータ(spSkeleton, spAnimationState)を作成し、対応するゲームオブジェクトの破棄時に破棄します。

トラックエントリ(spTrackEntry)は、AnimationStateの関数(spAnimationState_setAnimationByName, spAnimationState_addAnimationByName, spAnimationState_setEmptyAnimation, spAnimationState_addEmptyAnimation)が呼び出されてからSP_ANIMATION_DISPOSEイベントがリスナーに送られるまで有効です。このイベントの後にトラックエントリにアクセスすると、セグメンテーション違反が発生する可能性があります。

構造体を作成する際、他の構造体を参照として渡すことがよくあります。参照元の構造体が、参照先の構造体を破棄することはありません。例えば、spSkeletonspSkeletonDataを参照し、そのspSkeletonDataspAtlasを参照しています。

  • spSkeletonを破棄しても、spSkeletonDataspAtlasは破棄されません。これは、spSkeletonDataは他のspSkeletonインスタンスと共有されている可能性が高いためです。
  • spSkeletonDataを破棄しても、spAtlasは破棄されません。これは、アトラスに複数のスケルトンの画像が含まれている場合など、 spAtlasが他のspSkeletonDataインスタンスと共有されることがあるためです。

カスタムアロケーターを使用する場合、Spineのアロケーションストラテジー(割り当て戦略)を上書きできます。(mallocreallocfreeを使用する場合は、extension.hの定義を変更してください)

全てをまとめた結果

ここでは、ロード、インスタンス化からアニメーションの適用まで、上記のすべてをまとめた簡単な例を示します(スクロールするとすべてのコードが表示されます):

// 全スケルトンで共有されるセットアップポーズデータ
spAtlas* atlas;
spSkeletonData* skeletonData;
spAnimationStateData* animationStateData;

// 5体のスケルトンインスタンスとそれらのAnimationState
spSkeleton* skeleton[5];
spAnimationState* animationState[5];
char* animationNames[] = { "walk", "run", "shot" };

void setup() {
   // アトラスのテクスチャを読み込めるようにエンジンをセットアップする、ウィンドウを作成する、など。
   engine_setup();

   // テクスチャアトラスのロード
   atlas = spAtlas_createFromFile("spineboy.atlas", 0);
   if (!atlas) {
      printf("Failed to load atlas");
      exit(0);
   }

   // スケルトンデータのロード
   spSkeletonJson* json = spSkeletonJson_create(atlas);
   skeletonData = spSkeletonJson_readSkeletonDataFile(json, "spineboy.json");
   if (!skeletonData) {
      printf("Failed to load skeleton data");
      spAtlas_dispose(atlas);
      exit(0);
   }
   spSkeletonJson_dispose(json);

   // 各ミックスタイムの設定
   animationStateData = spAnimationStateData_create(skeletonData);
   animationStateData->defaultMix = 0.5f;
   spAnimationStateData_setMixByName("walk", "run", 0.2f);
   spAnimationStateData_setMixByName("walk", "shot", 0.1f);
}

void mainLoop() {
   // 5つのゲームオブジェクトを表す5体のスケルトンインスタンスと
   // AnimationStateを作成。
   for (int i = 0; i < 5; i++) {
      // スケルトンを作成し、ランダムな位置に配置する
      spSkeleton* skeleton = spSkeleton_create(skeletonData);   
      skeleton->x = random(0, 200);
      skeleton->y = random(0, 200);

      // AnimationStateを作成し、ランダムなアニメーションをループさせながらエンキューする
      spAnimationState animationState = spAnimationState_create(animationStateData);
      spAnimationState_setAnimation(animationState, 0, animationNames[random(0, 3)], 1);
   }

   while (engine_gameIsRunning()) {      
      engine_clearScreen();

      // ゲームオブジェクトのアップデート
      for (int i = 0; i < 5; i++) {
         spSkeleton* skeleton = skeletons[i];
         spAnimationState* animationState = animationStates[i];

         // まず、AinmationStateをデルタタイムで更新する
         spAnimationState_update(animationState, engine_getDeltaTime());

         // 次に、スケルトンにそのAinmationStateを適用する
         spAnimationState_apply(animationState, skeleton);

         // レンダリングのためのワールドトランスフォームを計算する
         spAnimationState_updateWorldTransform(skeleton);

         // スケルトンのレンダリングをエンジンに引き継ぐ
         engine_drawSkeleton(skeleton);
      }
   }

   // インスタンスデータを破棄する。通常は、ゲームオブジェクトを
   // 破棄するときに行います。
   for (int i = 0; i < 5) {
      spSkeleton_dispose(skeleton);
      spAnimationState_dispose(animationState);
   }
}

void dispose() {
   // 共有リソースをすべて破棄する
   spAtlas_dispose(atlas);
   spSkeletonData_dispose(skeletonData);
   spAnimationStateData_dispose(animationStateData);
}

int main(int argc, char* argv) {
   setup();
   mainLoop();
   dispose();
}

セットアップポーズデータ(spAtlas, spSkeletonData, spAnimationStateData) とインスタンスデータ (spSkeleton, spAnimationState) の区別とそのライフタイムの違いに注意してください。

spine-cをエンジンに統合する

ソースの統合

spine-cは、ランタイムのGitリポジトリのspine-c/spine-cフォルダにあるCのヘッダーと実装のファイル群です。

  1. Spineランタイムのリポジトリをクローンするか、ZIP ファイルとしてダウンロードします。
  2. spine-c/spine-c/src/spineフォルダからご自身のプロジェクトのソースフォルダにソースをコピーし、それらがプロジェクトのコンピレーションステップの一部になっていることを確認します。
  3. ヘッダーの入ったspineフォルダをspine-c/spine-c/includeフォルダからご自身のプロジェクトのヘッダーフォルダーにコピーし、コンパイラーがヘッダーを探すのに使うインクルードパスの一部になっていることを確認します。spine-cのソースは#include "spine/xxx.h"でヘッダーをインクルードするので、spineフォルダは必ず残しておいてください。

プロジェクトをコンパイルすると、spine-cランタイムが実装することを期待する関数に対してリンカエラーが発生します。例えば、Clangでコンパイルすると、次のようなものが出てきます:

Undefined symbols for architecture x86_64:
"__spAtlasPage_createTexture", referenced from:
    _spAtlas_create in libspine-c.a(Atlas.c.o)
"__spAtlasPage_disposeTexture", referenced from:
    _spAtlasPage_dispose in libspine-c.a(Atlas.c.o)
"__spUtil_readFile", referenced from:
    _spAtlas_createFromFile in libspine-c.a(Atlas.c.o)
    _spSkeletonBinary_readSkeletonDataFile in libspine-c.a(SkeletonBinary.c.o)
    _spSkeletonJson_readSkeletonDataFile in libspine-c.a(SkeletonJson.c.o)
ld: symbol(s) not found for architecture x86_64

拡張機能の実装

リンカが見つけられない3つの関数は、拡張関数と呼ばれています。spine-cランタイムは、ご使用のエンジンが提供するAPIを介してこれらを実装することを期待します。拡張関数はextension.hで定義されています。

注意: spine-cランタイムをベースとしたSpine公式ランタイムを使用している場合、これらの関数とレンダリングは既に実装されていますので、このセクションは無視していただいて結構です。

_spUtil_readFileの実装

spine-cは_spUtil_readFile関数を使って、SpineエディタからエクスポートされるフルコンテンツのJSONやバイナリ(.json/.skel)形式のスケルトンファイルや、テクスチャアトラスファイル(.atlas)をメモリ上に読み込んでいます。C/C++ IDEの"Find usages"または同等の機能を使って、_spUtil_readFileが呼び出される場所を確認してください。

_spUtil_readFileのシグネチャは以下の通りです:

char* _spUtil_readFile (const char* path, int* length);

この関数は,読み込むべきファイルへのUTF-8パスと、ファイルから読み込んだバイト数を格納するint型へのポインタを受け付けます。この関数は,ファイルが読み込めなかった場合は0を返し,そうでない場合は,この関数が割り当てたメモリブロックへのポインタを返し,その中にファイルの全バイトを読み込ませます。

_spUtil_readFileを呼び出すコードは、データの処理が完了すると、返されたメモリを解放することになっています。この関数を使用するspine-cのすべてのコードでは、メモリが確実に割り当て解除されるため、心配する必要はありません。

メモリはextension.hで定義されたMALLOCまたはCALLOCマクロマクロで割り当てられ、spine-cのコードはFREEマクロを使用してメモリを解放します。MALLOCCALLOCFREEを再定義して、spine-cランタイム全体でカスタムのメモリ割り当てスキームを使用することができます。

最も単純な_spUtil_readFileの実装は次のようになります:

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

これは、extension.cでspine-cが提供している関数_readFileを使用します。_readFilefopenを使用して、現在の作業ディレクトリから相対的にファイルを読み取ります。

もし、お使いのエンジンがより洗練されたファイル管理方法、例えば、内部ファイル構造を持つ単一の圧縮ファイルにファイルをパックする方法を使用している場合、エンジンのファイルi/o APIを使用して_spUtil_readFileを実装することもできます。

_spAtlasPage_createTextureと_spAtlasPage_disposeTextureの実装

spine-cは_spAtlasPage_createTexture関数を使用して、SpineエディターによってSpineスケルトンにエクスポートされたテクスチャアトラスの1ページに対して、エンジン固有のテクスチャ表現をロードして作成します。この関数はspAtlas_createspAtlas_createFromFileの一部として呼び出され、 Spineのテクスチャアトラスファイル(.atlas)と各アトラスページを構成する対応イメージファイル(通常 .png)のロードに使用されます。

_spAtlasPage_createTextureのシグネチャは以下の通りです:

void _spAtlasPage_createTexture (spAtlasPage* self, const char* path);

この関数は、アトラスページのイメージファイルをエンジン固有のテクスチャにロードし、関数に渡されたspAtlasPageに格納することになっています。spAtlasPage構造体はvoid*型のrendererObjectという特別なフィールドを持っており、そこにエンジン固有のテクスチャを格納する必要があります。このrendererObjectに格納されたエンジン固有のテクスチャは、後でエンジンAPIを使用してSpineスケルトンをレンダリングするために使用されます。

また、この関数は、エンジンによってロードされたテクスチャファイルに従って、spAtlasPageの幅と高さをピクセル単位で設定することになっています。このデータはspine-cによってテクスチャ座標を計算するために必要です。

pathパラメータはページ画像ファイルのパスで、spAtlas_createFromFileに渡された.atlasファイルパスからの相対パス、またはspAtlas_createに渡されたdirパラメータからの相対パスとなります。これらの2つの関数は、それぞれファイルまたはメモリブロックからテクスチャアトラスをロードするために使用されます。

説明のために、お使いのエンジンがテクスチャを扱うために以下のようなAPIを提供していると仮定します:

typedef struct Texture {
   // ... OpenGL handle, image data, whatever ...
   int width;
   int height;
} Texture;

Texture* engine_loadTexture(const char* file);
void engine_disposeTexture(Texture* texture);

_spAtlasPage_createTextureの実装は、次のようにシンプルなものです:

void _spAtlasPage_createTexture (AtlasPage* self, const char* path){
   Texture* texture = engine_loadTexture(path);

   // テクスチャの読み込みに失敗した場合、self->rendererObject、self->width および
   // self->height は0のままで、単にリターンします。
   if (!texture) return;

   // rendererObjectにTextureを格納します。
   // 後でレンダリングのためにそれを取得します。
   self->rendererObject = texture;

   // spine-cがレンダリングのためのテクスチャ座標を計算できるように、
   // テクスチャの幅と高さを
   // spAtlasPageに格納します。
   self->width = texture->width;
   self->height = texture->height;
}

次に実装する必要があるのは、_spAtlasPage_disposeTexture関数です。これは、対応するspAtlasspAtlas_disposeの呼び出しによって破棄されるとき、spAtlasPageのテクスチャを破棄する役割を果たします。_spAtlasPage_disposeTextureのシグネチャは次のとおりです:

void _spAtlasPage_disposeTexture (spAtlasPage* self);

想定しているエンジンAPIを考えると、実装は次のようになります:

void _spAtlasPage_disposeTexture (spAtlasPage* self) {
   // rendererObjectが設定されていない場合、
   // 読み込みに失敗しているので、何も破棄する必要はありません。
   if (!self->rendererObject) return;

   // テクスチャの破棄
   Texture* texture = (Texture*)self->rendererObject;
   engine_disposeTexture(texture);
}

レンダリングの実装

Spineスケルトンのレンダリングは、現在アクティブなアタッチメントをすべて現在の表示順序でレンダリングすることを意味します。表示順序は、スケルトン上のスロットの配列として定義されます。

描画可能なアタッチメント(領域(変形可能な) メッシュ)は、UVマッピングされ、頂点カラーのついた三角形のメッシュを定義します。描画可能なアタッチメントのrendererObjectには、アタッチメント メッシュの三角形がマップされるテクスチャ アトラス領域への参照が格納されます。

スケルトンをプロシージャルに、またはAnimationStateでアニメーションさせ、spSkeleton_updateWorldTransformの呼び出しでスケルトンボーンのワールドトランスフォームをアップデートしたと仮定すると、次のようにスケルトンをレンダリングすることができます:

  • スケルトンの表示順序配列の各スロットにおいて
    • スロットから現在アクティブなアタッチメントを取得します (アタッチメントがアクティブでない場合は null を指定できます)
    • スロットからブレンドモードを取得し、お使いのエンジンのAPIに変換します
    • スケルトンとスロットのカラーを元にティントカラーを計算します
    • アタッチメントのタイプを確認します
      • それが領域アタッチメントであった場合
        • アタッチメントのレンダーオブジェクトからアトラスページのテクスチャを取得します
        • spRegionAttachment_computeWorldVerticesを呼び出して、ワールド頂点を計算します。
        • アタッチメントのティントカラーを、各チャンネルごとに [0-1] の範囲でRGBAとして与えられるスケルトンカラーとスロットカラーを乗算することによって計算します
        • ワールド空間の頂点、UV、カラーを三角形のメッシュに合成します
        • お使いのエンジンのAPIでテクスチャをバインドします
        • レンダリング用のメッシュをSubmitします
      • それがメッシュアタッチメントであった場合
        • アタッチメントのレンダーオブジェクトからアトラスページのテクスチャを取得する
        • spVertexAttachment_computeWorldVerticesを呼び出して、ワールド頂点を計算します。
        • アタッチメントのティントカラーを、各チャンネルごとに [0-1] の範囲で RGBA として与えられるスケルトンカラーとスロットカラーを乗算することによって計算します
        • ワールド空間の頂点、UV、カラーを三角形のメッシュに合成します
        • お使いのエンジンのAPIでテクスチャをバインドします
        • レンダリング用のメッシュをSubmitします

UVマップされた頂点カラーのついた三角メッシュのレンダリングが可能なエンジンであれば、これをお使いのエンジンに翻訳することは些細なことであるはずです。説明のために、次のようなエンジンAPIを想定してみましょう:

// UVを伴う単一の頂点
typedef struct Vertex {
   // x/y平面上の位置
   float x, y;

   // UV座標
   float u, v;

   // カラー、各チャンネルは0-1の範囲
   // (本当は32ビットRGBAパックカラーであるべきです)
   float r, g, b, a;
} Vertex;

enum BlendMode {
   // これらがOpenGLのソース/デスティネーションブレンドモードにどのように変換されるかについては、
   // こちらをご覧ください。 http://esotericsoftware.com/git/spine-runtimes/blob/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/BlendMode.java#L37
   BLEND_NORMAL,
   BLEND_ADDITIVE,
   BLEND_MULTIPLY,   
   BLEND_SCREEN,
}

// 与えられたメッシュを描画する。
// - vertices はVertex構造体の配列へのポインタです。
// - start は、頂点配列のどの頂点から開始するかを定義します。
// - count は、レンダリングに使用する頂点の数を定義します(三角形をレンダリングするため、3で割り切れる必要があり、各三角形には3つの頂点が必要です)。
// - texture は、使用するテクスチャです。
// - blendMode は、使用するブレンドモードです。
void engine_drawMesh(Vertex* vertices, int start, int count, Texture* texture, BlendMode blendmode);

そして、レンダリング処理は次のように実装することができます:

#define MAX_VERTICES_PER_ATTACHMENT 2048
float worldVerticesPositions[MAX_VERTICES_PER_ATTACHMENT];
Vertex vertices[MAX_VERTICES_PER_ATTACHMENT];

// スクラッチバッファに頂点を追加するための小さなヘルパー関数です。
// この関数を呼び出した後、indexが1つ増加します。
void addVertex(float x, float y, float u, float v, float r, float g, float b, float a, int* index) {
   Vertex* vertex = &vertices[*index];
   vertex->x = x;
   vertex->y = y;
   vertex->u = u;
   vertex->v = v;
   vertex->r = r;
   vertex->g = g;
   vertex->b = b;
   vertex->a = a;
   *index += 1;
}

void drawSkeleton(spSkeleton* skeleton) {
   // スケルトンの表示順配列の各スロットごとに処理する
   for (int i = 0; i < skeleton->slotsCount; ++i) {
      Slot* slot = skeleton->drawOrder[i];

      // 現在アクティブなアタッチメントを取得し、
      // そのスロットでアクティブなアタッチメントがない場合は
      // 次のスロットに進みます
      Attachment* attachment = slot->attachment;
      if (!attachment) continue;

      // スロットからブレンドモードを取得し、
      // エンジンのブレンドモードに変換する
      BlendMode engineBlendMode;
      switch (slot->data->blendMode) {
         case SP_BLEND_MODE_NORMAL:
            engineBlendMode = BLEND_NORMAL;
            break;
         case SP_BLEND_MODE_ADDITIVE:
            engineBlendMode = BLEND_ADDITIVE;
            break;
         case SP_BLEND_MODE_MULTIPLY:
            engineBlendMode = BLEND_MULTIPLY;
            break;
         case SP_BLEND_MODE_SCREEN:
            engineBlendMode = BLEND_SCREEN;
            break;
         default:
            // unknownのSpineブレンドモードだった場合、
            // 通常のブレンドモードにフォールバックする
            engineBlendMode = BLEND_NORMAL;
      }

      // スケルトンカラーとスロットカラーを元に、ティントカラーを計算する。
      // 各カラーチャンネルは、[0-1]の範囲で与えられます。
      // もし、ご使用のエンジンがカラーチャンネルに整数範囲を使用している場合は、
      // 255倍してintにキャストしなければいけない可能性があります。
      float tintR = skeleton->r * slot->r;
      float tintG = skeleton->g * slot->g;
      float tintB = skeleton->b * slot->b;
      float tintA = skeleton->a * slot->a;

      // アタッチメントのタイプに応じて頂点の配列を埋める
      Texture* texture = 0;
      int vertexIndex = 0;
      if (attachment->type == ATTACHMENT_REGION) {
         // spRegionAttachment にキャストして、レンダラーObject を取得し、
         // ワールド頂点を計算できるようにする。
         spRegionAttachment* regionAttachment = (spRegionAttachment*)attachment;

         // エンジン固有のTextureは、ロード時にアタッチメントに割り当てられた
         // spAtlasRegionに格納されています。これは、領域アタッチメントがマッピングされた画像を含む
         // テクスチャアトラスページを表します。
         texture = (Texture*)((spAtlasRegion*)regionAttachment->rendererObject)->page->rendererObject;

         // 矩形領域のアタッチメントを構成する4つの頂点のワールド頂点位置を計算する。
         // これは、スロット(とアタッチメント)がアタッチされているボーンのワールドトランスフォームが、
         // レンダリング前にspSkeleton_updateWorldTransformによって
         // 計算されていることを想定しています。
         spRegionAttachment_computeWorldVertices(regionAttachment, slot->bone, worldVerticesPositions, 0, 2);

         // 領域のワールド頂点の位置とそのUV座標(範囲は[0-1])からの3つずつの頂点で、
         // 2つの三角形を作成する。
         addVertex(worldVerticesPositions[0], worldVerticesPositions[1],
                regionAttachment->uvs[0], regionAttachment->uvs[1],
                tintR, tintG, tintB, tintA, &vertexIndex);

         addVertex(worldVerticesPositions[2], worldVerticesPositions[3],
                regionAttachment->uvs[2], regionAttachment->uvs[3],
                tintR, tintG, tintB, tintA, &vertexIndex);

         addVertex(worldVerticesPositions[4], worldVerticesPositions[5],
                regionAttachment->uvs[4], regionAttachment->uvs[5],
                tintR, tintG, tintB, tintA, &vertexIndex);

         addVertex(worldVerticesPositions[4], worldVerticesPositions[5],
                regionAttachment->uvs[4], regionAttachment->uvs[5],
                tintR, tintG, tintB, tintA, &vertexIndex);

         addVertex(worldVerticesPositions[6], worldVerticesPositions[7],
                regionAttachment->uvs[6], regionAttachment->uvs[7],
                tintR, tintG, tintB, tintA, &vertexIndex);

         addVertex(worldVerticesPositions[0], worldVerticesPositions[1],
                regionAttachment->uvs[0], regionAttachment->uvs[1],
                tintR, tintG, tintB, tintA, &vertexIndex);
      } else if (attachment->type == ATTACHMENT_MESH) {
         // spMeshAttachmentにキャストしてrendererObjectを取得し、
         // ワールド頂点を計算できるようにする
         spMeshAttachment* mesh = (spMeshAttachment*)attachment;

         // メッシュアタッチメントの頂点数を確認する。もしそれがスクラッチバッファよりも
         // 大きければ、メッシュをレンダリングしない。
         // ここでは簡単にするためにこのようにしていますが、実際のプロダクションでは
         // メッシュに合うようにスクラッチバッファを再割り当てすることになります。
         if (mesh->super.worldVerticesLength > MAX_VERTICES_PER_ATTACHMENT) continue;

         // エンジン固有のTextureは、ロード時にアタッチメントに割り当てられた
         // spAtlasRegionに格納されています。これは、メッシュアタッチメントがマッピングされた画像を
         // 含むテクスチャアトラスページを表します。
         texture = (Texture*)((spAtlasRegion*)mesh->rendererObject)->page->rendererObject;

         // メッシュアタッチメントを構成する頂点のワールド頂点位置を計算。
         // これは、スロット(とアタッチメント)がアタッチされているボーンのワールドトランスフォームが
         // spSkeleton_updateWorldTransformを使ってレンダリングする前に
         // 計算されていることを前提としています。
         spVertexAttachment_computeWorldVertices(SUPER(mesh), slot, 0, mesh->super.worldVerticesLength, worldVerticesPositions, 0, 2);

         // メッシュアタッチメントでは、頂点の配列と、各三角形を構成する3つの頂点を定義する
         // インデックスの配列が使用されます。すべての三角形のインデックスをループして を生成し、
         // 各三角形の頂点に対して単純に頂点を生成します。
         for (int i = 0; i < mesh->trianglesCount; ++i) {
            int index = mesh->triangles[i] << 1;
            addVertex(worldVerticesPositions[index], worldVerticesPositions[index + 1],
                   mesh->uvs[index], mesh->uvs[index + 1],
                   tintR, tintG, tintB, tintA, &vertexIndex);
         }
      }

      // アタッチメント用に作成したメッシュを描画する
      engine_drawMesh(vertices, 0, vertexIndex, texture, engineBlendMode);
   }
}

この素朴な実装なら、すぐに使い始められるでしょう。しかし、最適化という点では、まだいくつかの簡単な方法があります:

  • engine_drawMeshは、レンダリングのためにメッシュをすぐにSubmitすることを想定しています。つまり、スケルトンのアタッチメントごとに1回のドローコールを発行します。プロダクション段階の実装では、すべてのメッシュを1つのメッシュにバッチする必要があります。理想的なのは、すべてのアタッチメントが同じテクスチャアトラスページとブレンドモードを使用しており、スケルトンのレンダリングは1回のドローコールで済む場合です。さらに、シーン内のすべてのスケルトンが同じテクスチャアトラスページとブレンドモードを共有する場合は、すべてのスケルトンを単一のメッシュにバッチすることもでき、ドローコールも単一になります。
  • 現在のセットアップでは、インデックス付きメッシュは使用せず、1つの三角形につき3つの頂点をSubmitします。
  • 現在のセットアップでは、2色のティントには対応していません。
  • 現在のセットアップでは、クリッピングに対応していません。