spine c++ Implement

spineのC++組み込みをちょっとやってみました。
オフシャルgithubでテストコードやライブラリは落とせますが、オフィシャルドキュメントにある最小限の構成が欲しかったので、VisualStdio2015で構築したいと思います。

  1. 生成と解放
  2. メモリ上読み込み
  3. モーション設定
  4. 動かす
  5. ポリゴンログの表示
  6. イベントの取得

あたりが目標です。

github

VS2015プロジェクト一式セットですが、Spine-c のランタイムは抜いてありますので揃えてください。
https://github.com/Ko-Ta2142/Spine_MinimalTest

ドキュメント

C++版spineに関する情報は以下から入手できます。
http://ja.esotericsoftware.com/spine-c
前半は基本操作説明、後半に組み込みに必要な実装がかかれています。
が、ちと資料が古いのでいくつか命令が無かったり、引数が省略されていたり、コードがなんとなくな感じだったりしますので、まんまでは動きません。

まずはライブラリ、ランタイムを入手

githubからcloneして頂きます。読み込むアニメーションサンプルも居るので全部貰っちゃいましょう。
https://github.com/EsotericSoftware/spine-runtimes
今回使うのはC++ランタイムの「spine-c\」と読み込ませるサンプルが入っている「exsample\」です。

VSでプロジェクトを作る

spine-cは環境依存性が無いピュア言語な作りをしているので、作るプロジェクトはCUIの「win32 console application」で作成します。
afxがいる?とかmain生成する?32bit?64bit?とか、このあたりは好みでどうぞ。

プロジェクト設定

まずはライブラリ、インクルードパスを設定しましょう。最後のspine\フォルダまでは含めないように。

[spineランタイムパス]\spine-c\spine-c\include
[spineランタイムパス]\spine-c\spine-c\src

次にライブラリのコンパイルですが面倒なのでこのプロジェクトで一緒にやっちゃいましょう(本番は分けて効率化してください)。
\spine-c\spine-c\src\spine\*.c/cpp を全部プロジェクトのソースファイルに突っ込みます。
見にくくなるのでフィルタを作ってそこに入れると良いでしょう。

SpineTest(プロジェクト)
  参照
  外部参照
  ソースコード
    spine(フィルタ)
      Animation.c
      AnimationState.c
      AnimationStateData.c
      ....
      VertexEffect.c
    main.cpp
    stdafx.c
  ヘッダーファイル
    stdafx.h

次にセキュリティや警告に関する設定が必要です。

全般
  構成の種類:exe
  文字セット:unicodeを使用する
  プラットフォームセット:Visual Studio 2015 (v140)(お好みで)
c/c++
  警告レベル:3
  警告をエラーとして扱う:いいえ
  SDLチェック:いいえ
  基本ランタイムチェック:既定
  ランタイムライブラリ:お好みで
  浮動小数の例外を有効:いいえ    (*重要)
プリコンパイル済みヘッダ
  プリコンパイル済みヘッダ:使用しない
プリプロセッサ
  定義:_CRT_SECURE_NO_WARNINGS の追加(警告レベルを2へ下げるのが良いのかは好みで)

これで動くとは限りませんが、spineとは別の問題なので省略します。
上の構成とプラットフォームがコンパイルしようとしているターゲットと同じになっているかはご注意ください。たまに変なタイミングで変わるので。
浮動小数の例外設定はspineでは重要です。

浮動小数計算の例外は無効化をお忘れ無く

spine-cではあらゆる箇所で zero divide が発生します。vc++では初期状態でfpuの警告は無効化されているようですが、他のコンパイラを使用する場合は気をつけてください。
コンパイラで設定する方法の他には。プログラム内で設定する方法があります。

  • _controlfp

https://msdn.microsoft.com/ja-jp/library/ms350048(v=vs.71).aspx

int fpuword = _controlfp(_EM_INVALID | _EM_ZERODIVIDE | _EM_OVERFLOW | _EM_UNDERFLOW | _EM_INEXACT, _MCW_EM);

この命令はvc依存かなと思いますが、fpuに対して例外を制御できます。それにしてもこの関数ちょっと使い方に癖が……。delphiだとSet8087CWとかありました。

サンプルの配置

githubでcloneした中に入っているので、「example\」を実行パスの直下に配置しましょう。
デフォルトだと $(ProjectDir) とあるので、プロジェクトファイルがあるところ、main.cppがあるところになってます。


これでようやく準備が出来ました…。

まず読み込みの前に

組み込み用途やピュア○○なライブラリは、大抵機種依存部分を関数ポインタやユーザー定義によって補うのが一般的です。
spine-cもこれが必要で、まず最初にそれらを補ってやる必要があり、定義は extention.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 も不要となります。nullptrでも返してやれば良いです。
というわけで以下のようになります。

// spine sample code
// document http://ja.esotericsoftware.com/spine-c#SpineC-Runtime-Documentation

#include "stdafx.h"
#include <stdio.h>
#include <string>
#include <vector>

#include "spine\extension.h"
#include "spine\spine.h"

//implement function
void _spAtlasPage_createTexture(spAtlasPage* self, const char* path) {
	self->rendererObject = nullptr;	//set user class or record pointer
	self->width = 2048;
	self->height = 2048;

	printf("texture.create:%s\n", path);
	//printf("texture.create:%s\n", self->name);
}

void _spAtlasPage_disposeTexture(spAtlasPage* self) {
	//dispose self->rendererObject

	printf("texture.dispose:%s\n", self->name);
}

//not use
char* _spUtil_readFile(const char* path, int* length){
	printf("readfile:%s\n", path);
	return nullptr;	//return byte array pointer
}

int main(int argc, char* argv[])
{
  //ここにテストコードを書いていきます
  return 0;
}

これに近いコードは以下にあります。
https://github.com/EsotericSoftware/spine-runtimes/blob/3.6/spine-c/spine-c-unit-tests/main.cpp

読み込み

exsampleにあるサンプルを読み込んでみます。とりあえずspineboyでも。
使うデータはspine本体で使うspineファイルでは無く export\ にある物を使用します。今回は。

spineboy.atlas
spineboy.png
spineboy-pro.json
spineboy-pro.skel

pro と ess はプロバージョンと機能限定版によるもので、メッシュ変形などの有無によるデータの違いです。
テストとしてはプロバージョンでいいので pro をつかいます。
読み込みの手順は画像(.atlas)を読み込んで、スケルトン(.json)を読み込みます。
.skel は .json のバイナリ型のようです。他環境だとjsonを推奨しているような雰囲気です。今回は使いません。


読み込むにはドキュメントLoadAssetあたりになります。
http://ja.esotericsoftware.com/spine-c#Loading-Spine-assets
今回はメモリ上読み込みを行うので使う関数は以下になります。

spAtlas_create
spSkeletonJson_create
spSkeletonJson_readSkeletonData
spAnimationStateData_create
spAnimationStateData_dispose
spSkeletonData_dispose
spAtlas_dispose

さてメモリ上で読み込むので受け渡しはcharなどの1byte配列です。
ファイル読み込んで配列にする関数が必要になるので、お好みで作成してください。
includeにあたりを追加してください。
DelphiだとTMemoryStream.LoadFromFileなのは皆さんもご存じで…無い!?。

std::vector<char>* _readfile(const std::string &filename) {
	std::ifstream fs;
	fs.open(filename, std::ios::in | std::ios::binary);
	if (!fs) {
		printf("error.file.open:%s\n",filename.c_str());
		getchar();
		return nullptr;
	}

	//get file size
	fs.seekg(0, std::ios::end);
	std::streampos size = fs.tellg();
	fs.seekg(0, std::ios::beg);
	
	//read from filestream
	std::vector<char>* buf = new std::vector<char>();
	buf->resize(size);
	fs.read(buf->data(), size);
	
	fs.close();
	return buf;
}

というわけでエラー処理を飛ばして書くと以下になります。

int main(int argc, char* argv[])
{
	//_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);	//memory leak debug
	std::string base = "examples\\spineboy\\export\\";
	std::string f;

	//atlas
	//call create texture
	f = base + "spineboy.atlas";
	std::vector<char>* ms = _readfile(f);
	spAtlas* atlas = spAtlas_create(ms->data(), ms->size(), base.c_str(), 0);
	delete ms;
	printf("Atlas.complete\n");

	//SkeletonData
	ms = _readfile(base + "spineboy-pro.json");
	spSkeletonJson* skeletonJson = spSkeletonJson_create(atlas);
	spSkeletonData* skeletonData = spSkeletonJson_readSkeletonData(skeletonJson, ms->data());
	delete ms;
	spSkeletonJson_dispose(skeletonJson);
	printf("SkeletonData.complete\n");

	//AnimationData
	spAnimationStateData* animationStateData = spAnimationStateData_create(skeletonData);
	printf("AnimationStateData.complete\n");
...
	spAnimationStateData_dispose(animationStateData);
	spSkeletonData_dispose(skeletonData);
	spAtlas_dispose(atlas);

Atlas、Jsonと読み込んで、JsonからskeletonDataを生成、SkeletonDataからAnimationStateDataを生成します。
ただしこれらはまだデータです。静的なもので、ここからアニメーションの動的な情報を保持するものを生成して使用します。
クローンを作成する場合は「このデータからいっぱい作れば早いよー」ということだけ覚えておきましょう。

実態の生成

表示させるにはデータから実態となるSkeletonとAnimationStateを生成する必要があります。
実際の動きの管理はこの実態となる2つのインスタンスで制御を行います。

	spSkeleton* skeleton = spSkeleton_create(skeletonData);
	spAnimationState* animationState = spAnimationState_create(animationStateData);
....
	spAnimationState_dispose(animationState);
	spSkeleton_dispose(skeleton);

テクスチャ読み込みはいつ?

さて、ログを取ると以下のようになります。

texture.create:examples\spineboy\export\spineboy.png
Atlas.complete
SkeletonData.complete
AnimationStateData.complete
texture.dispose

テクスチャの生成はAtlasの読み込みで行われるようです。最初に定義した _spAtlasPage_createTexture が呼ばれています。
テクスチャパスも spAtlas_create で指定したパス指定に画像ファイル名を付加したものになっています。

アニメーション

ちょっと複雑ですが、ものによってデータ(AnimationStateData)に仕込む場合と、実際のアニメーションインスタンス(AnimationState)の方に仕込む場合があるので注意が必要です。


・アニメーション切り替え時のブレンド
これはAnimationStateDataに仕込みます。
spAnimationStateData_setDefaultMix は廃止命令です。

	//spAnimationStateData_setDefaultMix(animationStateData);	//probabry disuse
	animationStateData->defaultMix = 0.1f;
	spAnimationStateData_setMixByName(animationStateData, "walk", "run", 0.2f);
	spAnimationStateData_setMixByName(animationStateData, "walk", "shot", 0.1f);


・アニメーションの設定
AnimationState(実態)の方で設定します。

	spAnimationState_clearTracks(animationState);						//animation clear
	spAnimationState_setAnimationByName(animationState, 0, "walk", 1);	//animationstate , track , name , loop

spAnimationState_setAnimationByName の他に、後方(現在のアニメーションが終点に達したら)に追加する spAnimationState_addAnimationByName があります。
しかし、このByNameですが、実際に使うには大変危険な命令です。存在しない名前を渡すと例外を返すと思いきや、NULLポインタアクセスで停止します……。なのでサンプルでしか使っちゃいけません。
spSkeletonData_findAnimation があるのでそれで名前引き、nullじゃなければ spAnimationState_setAnimation でせっとしましょう。

spTrackEntry* spAnimationState_setAnimationByNameFix (spAnimationState* self, int trackIndex, const char* animationName, int/*bool*/loop) {
	spAnimation* animation = spSkeletonData_findAnimation(self->data->skeletonData, animationName);
	if (animation != nullptr)
		return spAnimationState_setAnimation(self, trackIndex, animation, loop);
	else
		return nullptr;
}

またByName系はスケルトンにも存在します。そっちも似たような命令形なので自作して対処しましょう。

座標設定

座標設定はskeletonで行います。
設定後は必ず spSkeleton_updateWorldTransform が必要です。

	skeleton->x = 500;
	skeleton->y = 500;
	spSkeleton_updateWorldTransform(skeleton);	//x,y transform after , next call spSkeleton_updateWorldTransform

メインループ

以下のような感じで回すだけです。
なお、ドキュメントにある spAnimationState_updateWorldTransform は廃止命令だと思われます。ポリゴンを描画する際に計算するようになったみたいです。

for (int i = 0; i < 1; i++) {
	//update (time move) 60fps
	spAnimationState_update(animationState, 1.0f/60);	//probably trunc micro seconds better

	//apply skeleton 
	spAnimationState_apply(animationState, skeleton);

	//transform world
	//spAnimationState_updateWorldTransform(skeleton);	//probabry disuse
	spSkeleton_updateWorldTransform(skeleton);

	//custom draw function
	myCustomDraw(skeleton);
}

spAnimationState_updateWorldTransform はたぶん spSkeleton_updateWorldTransform のことなんじゃないかと思います。

イベントの取得

spineは基本的にサウンドの再生を持ちません。
ただし、再生中に文字列イベントを投げてくれるので、この起用で代用します。
・event
http://ja.esotericsoftware.com/spine-c#Events
バージョンで若干構造に変化があったので以下のようになります。

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->name, entry->trackIndex);
		break;
	case SP_ANIMATION_INTERRUPT:
		printf("Animation %s interrupted on track %i\n", entry->animation->name, entry->trackIndex);
		break;
	case SP_ANIMATION_END:
		printf("Animation %s ended on track %i\n", entry->animation->name, entry->trackIndex);
		break;
	case SP_ANIMATION_COMPLETE:
		printf("Animation %s completed on track %i\n", entry->animation->name, entry->trackIndex);
		break;
	case SP_ANIMATION_DISPOSE:
		printf("Track entry for animation %s disposed on track %i\n", entry->animation->name, entry->trackIndex);
		break;
	case SP_ANIMATION_EVENT:
		printf("User defined event for animation %s on track %i\n", entry->animation->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);
	}
}

spineboyでwalkさせると「footstep」という文字列のイベントが飛んできます。
intやfloatもやりとり出来るようですが、文字列があるので「*playsound,id:walk02,vol:128,pan:127,loop:0」みたいな文字だけで完結するかと思います。

描画・基礎編

さて、最後に難物、表示についてです。
表示はほぼすべて3頂点によるポリゴン+テクスチャで行われます。簡単にいってしまえば、計算後の頂点とUV情報が得られるので各自で描いてくださいというものです。
リスナー機能のようなイベント型、関数ポインタではありません。


表示に関する情報はSkeletonインスタンスが保持しています。AnimationStateがSkeletonを操作するので、その結果を画面に表示してやります。
今回はCUIなのでprintfで情報を書き出して終了です。

	//custom draw function
	myCustomDraw(skeleton);

どのように実装するかはドキュメントにもあります。
・Implementing Rendering
http://ja.esotericsoftware.com/spine-c#Implementing-Rendering
色々書いてますが、要約すれば以下のシンプルなコードです。

void myCustomDraw(spSkeleton* skeleton) {
	for (int i = 0; i < skeleton->slotsCount; i++) {
		spSlot* slot = skeleton->drawOrder[i];
	
		spAttachment* attachment = slot->attachment;
		if (!attachment) continue;

		if (attachment->type == SP_ATTACHMENT_REGION) {
			myCustomDraw_region(slot,attachment...);
		}
		else if (attachment->type == SP_ATTACHMENT_MESH) {
			myCustomDraw_mesh(slot,attachment...);
		}
		else {
			//unsupport
		}

	}
}

ケルトンの分だけレンダリングします。階層構造じゃないんですね。色々と処理した結果並び替えて配列して用意してくれてるみたいです。
空も存在するのでその場合はスキップ、typeによって表示は四角形とメッシュがありますよー。と言ったものです。
マニュアルにある ATTACHMENT_REGION などは廃止され、SP_ATTACHMENT_REGION など列挙型になったようです。

ブレンドモードとカラー

ブレンドモード slot->data->blendmode から得ることができます。
情報が得られるだけなので、合成式は各自で実装する必要があります。Direct3DにおけるBllendOPにあたります。

int blendmode = 0;
switch (slot->data->blendMode) {
	case SP_BLEND_MODE_NORMAL:		//alpha blend : src.rgb * (src.a) + dest.rgb * (1.0-src.a)
		blendmode = 0;
		break;
	case SP_BLEND_MODE_ADDITIVE:	//add : src.rgb * src.a + dest.rgb
		blendmode = 1;
		break;
	case SP_BLEND_MODE_MULTIPLY:	//multiply/modulate : (1.0 + (src.rgb-1.0) * src.a) * dest.rgb
		blendmode = 2;
		break;
	case SP_BLEND_MODE_SCREEN:		//screen : 1.0 - (1.0-(src.rgb * src.a)) * (1.0-dest.rgb)
		blendmode = 3;
		break;
}

カラーはver3.6より2つ設定できるようになりました。いままでの乗算色(slot->color : ModulateColor)とティン色(slot->darkColor : tintColor)の2つがあります。
この計算は上記のブレンドより前のテクスチャカラー計算、Direct3DのPixelShader/TextureStateにあたります。
計算式は以下に・・・なりません!

out.rgb = Texture.rgb * color
out.rgb = 1.0 - (1.0-out.rgb) * (1.0-tintcolor.rgb)
out.a = Texture.a * color.a

tint単体では合成はスクリーンのようです。
ただし、tintcolorとcolor両方が指定された場合、乗算した結果にスクリーンした場合とは違う色を返します。
例えば、黒を乗算した後に白をスクリーンした場合、普通なら白です。spineのエディタ上では反転した結果が表示されます。
これから分かるのは、スクリーン合成部分が元の色とスクリーンした結果の色との差分を足しているようです。乗算結果との差分ではないようです。
というわけで、乗算で真っ黒になった結果に、元の色と白色の差分、つまり黒かった部分が大きな値を持つので反転するみたいです。

mul.rgb = Texture.rgb * color
tint.rgb = 1.0 - (1.0-Texture.rgb) * (1.0-tintcolor.rgb)
out.rgb = Texture.rgb + (mul.rgb - Texture.rgb) + (tint.rgb - Texture.rgb)
out.a = Texture.a * color.a

かな?ちょっと特殊すぎる…。乗算のみ、スクリーンのみで使用するならまぁ大丈夫でしょう。
レンダリングが乗っ取れるので、エディタ上では異なる表示になりますが、プログラムから自由に使える色みたいに使ってもいいかもしれません。unityでは難しそうですが…。(あれだけランタイムの構成が大きく異なるんです)
色の取得は以下になります。

spColor mulcol,addcol;
mulcol.r = skeleton->color.r * slot->color.r;
mulcol.g = skeleton->color.g * slot->color.g;
mulcol.b = skeleton->color.b * slot->color.b;
mulcol.a = skeleton->color.a * slot->color.a;
screencol.r = 0.0f;
screencol.g = 0.0f;
screencol.b = 0.0f;
screencol.a = 0.0f;
// gui deactive , set null
if (slot->darkColor != nullptr) {
	screencol.r = slot->darkColor->r;
	screencol.g = slot->darkColor->g;
	screencol.b = slot->darkColor->b;
	screencol.a = slot->darkColor->a;
}

気を付けるところは slot->darkColor が spColor* で定義されている点です。
color は普通の spColor なのになぜ違うのかはエディタでの仕様によるところのようです。darkColor は初期状態では未定義状態(機能が追加されていない状態)で、使用する際に有効、追加を行います。なので有効にされていない場合は nullptr として渡されるようです。


カラーはslotとskeletonの他にもう一個、attachment(画像やメッシュ)にも存在します。
エディタ上ではslotやskeletonは動的なアニメーション変化を与えられるのに対して、attachmentはsetup編集の時しか設定できない固定的な色です。
なので、計3段階の色計算が必要になります。尚、attachmentにはdarkColorは存在しません。
attachmentは型が確定するのがほぼ最後なので、drawする最終段階に計算することになるかと思います。

void myCustomDraw_region(spSlot* slot , spAttachment* attachment , spColor* col , int blendmode) {
	spRegionAttachment* regionAttachment = (spRegionAttachment*)attachment;
	// attachment color
	spColor mulcolor;spColor mulcolor;
	mulcolor.a = col->a  * regionAttachment->color.a;
	mulcolor.a = col->r  * regionAttachment->color.r;
	mulcolor.a = col->g  * regionAttachment->color.g;
	mulcolor.a = col->b  * regionAttachment->color.b;
...

描画・メッシュ編

メッシュ描画について。
メッシュに渡されるのは基本的にはプロフェッショナル版の機能です。スキニングメッシュの他にも、メッシュで透明色部分を最適化した場合、こちらが呼ばれます。

void myCustomDraw_mesh(spSlot* slot , spAttachment* attachment , spColor* col , int blendmode) {
	spMeshAttachment* mesh = (spMeshAttachment*)attachment;
	_worldVertices_setlength(mesh->super.worldVerticesLength);	//worldVerticesLength buffer length. x,y,x,y,x,y,x,y...x,y

	void* textureptr = nullptr;
	textureptr = (void*)((spAtlasRegion*)mesh->rendererObject)->page->rendererObject;	//set user pointer in _spAtlasPage_createTexture

	// SUPER = mesh->super
	spVertexAttachment_computeWorldVertices(SUPER(mesh), slot, 0, mesh->super.worldVerticesLength, _worldVertices.data(), 0, 2);	//probably stride value float x,y count 2. support 3D feature?
...
}

計算の流れはシンプルです。
まずメッシュなのでメッシュインスタンスにキャスト。
後で説明します計算領域の確保。
次に createTexture 時にセットしたテクスチャの実態を取得。テストなので残念ながらnullptrです。
最後に行列計算を行います。かんたん。
最後のstep数=2を指定するあたりが今後の可能性を感じます。


さて、行列計算の際に出力用のfloat配列を要求されます。
あらかじめ必要なサイズは mesh->super.worldVerticesLength にて取得出来るので、この段階でリサイズしてやる必要があります。
mesh->super.worldVerticesLength で取得出来るのはfloatの個数になります。(byte長ではありません)
こういう頻度が多い場合は、共有、サイズは基本的に拡大しかしないので _worldVertices_setlength 命令を噛ませてあります。

//computing vertex buffer
std::vector<float> _worldVertices;
void _worldVertices_setlength(int length) {
	if (length > _worldVertices.size()) {
		_worldVertices.resize(length);
	}
}
void _worldVertices_clear() {
	_worldVertices.resize(1024);
}

よしなに。


これで計算された頂点情報(UVなども)が手に入りましたので、ポリゴンに分けていきます。
ドキュメントによるとこうあります。

	// Mesh attachments use an array of vertices, and an array of indices to define which
	// 3 vertices make up each triangle. We loop through all triangle indices
	// and simply emit a vertex for each triangle's vertex.

	//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);
	//}

triangleCountだから1ポリゴンかと思ったら、そうでは無いようです。単純にx,yの頂点indexのようです。ちょっとわかりにくいのですが、addVertex も名前の通り、1頂点の登録です。
つまり、頂点indexがtriangleCount分、3頂点1ポリゴンとして並んでますということのようです。
おら!孕め!ポリゴンの子を!

	float xx[3], yy[3];	//vertex
	float uu[3], vv[3];	//uv
	int triangleindex = 0;
	float* sptr = (float*)_worldVertices.data();
	int n = mesh->trianglesCount / 3;
	for (int i = 0; i < n; i++) {
		//triangle
		for (int j = 0; j < 3; j++) {
			//vertex
			int idx = mesh->triangles[triangleindex] * 2;
			triangleindex++;
			xx[j] = sptr[idx + 0];
			yy[j] = sptr[idx + 1];
			uu[j] = mesh->uvs[idx + 0];
			vv[j] = mesh->uvs[idx + 1];
		}
		//add
		addTriangle(xx,yy,col,blendmode); 
	}

描画・四角形編

メッシュとだいたい一緒です。
ただし、四角形の4頂点分しか計算されないので、三角形の場合は、0-1-2 , 2-3-0 と頂点を使い回す必要があります。

void myCustomDraw_region(spSlot* slot , spAttachment* attachment , spColor* col , int blendmode) {
	spRegionAttachment* regionAttachment = (spRegionAttachment*)attachment;
	_worldVertices_setlength(4*2);	//4 vertex
	void* textureptr = nullptr;
	textureptr = (void*)((spAtlasRegion*)regionAttachment->rendererObject)->page->rendererObject;
	// Computed the world vertices positions for the 4 vertices that make up
	spRegionAttachment_computeWorldVertices(regionAttachment, slot->bone, _worldVertices.data(), 0, 2);

	const float idxtbl[6] = { 0,1,2,2,3,0 };
	float xx[3], yy[3];	//vertex
	float uu[3], vv[3];	//uv
	int triangleindex = 0;
	float* sptr = (float*)_worldVertices.data();
	int n = 2;
	for (int i = 0; i < n; i++) {
		//triangle
		for (int j = 0; j < 3; j++) {
			//vertex
			int idx = idxtbl[triangleindex] * 2;
			triangleindex++;
			xx[j] = sptr[idx + 0];
			yy[j] = sptr[idx + 1];
			uu[j] = regionAttachment->uvs[idx + 0];
			vv[j] = regionAttachment->uvs[idx + 1];
		}
		//add
		addTriangle(xx, yy, col, blendmode);
	}
}

頂点の回転方向が逆だったらごめんなさい。

ポリゴンログ

addTriangleの内容を出せば終了です。

void addTriangle(float* xx, float* yy, spColor* col, int blendmode) {
	printf("draw.triangle:vertex=(%f,%f)(%f,%f)(%f,%f),argb=(%f,%f,%f,%f),blend(%d)\n", xx[0], yy[0], xx[1], yy[1], xx[2], yy[2], col->a, col->r, col->g, col->b, blendmode);
}

あ、UV情報忘れちゃった。テストだし問題ないよね。
ちなみにメッシュだと1フレームで数百のログが流れますのでご注意ください。

draw.triangle:vertex=(544.877563,798.626404)(501.897675,779.607239)(466.691986,859.165771),argb=(1.000000,1.000000,1.000000,1.000000),blend(0)
draw.triangle:vertex=(561.553467,768.968384)(510.339386,746.316162)(481.215149,812.162842),argb=(1.000000,1.000000,1.000000,1.000000),blend(0)
draw.triangle:vertex=(729.151794,654.932861)(524.608582,607.371765)(478.632874,805.096924),argb=(1.000000,1.000000,1.000000,1.000000),blend(0)
draw.triangle:vertex=(562.501709,498.976868)(567.549072,540.956177)(572.811279,539.457520),argb=(1.000000,1.000000,1.000000,1.000000),blend(0)
draw.triangle:vertex=(562.501709,498.976868)(572.811279,539.457520)(588.472290,534.956665),argb=(1.000000,1.000000,1.000000,1.000000),blend(0)

最後に

とりあえず、これで最小限の情報は得られたかと思います。