ChakraCore implement 2
長くなるので分けました。
使用する関数は1つ前の記事を参照と言うことで。
エラーハンドリング
開発で一番大切なところ、エラー表示です。
組み込む以上、nodeJs開発環境みたいな気の利いたデバッガは存在しません。でもせめて、エラー箇所の特定ぐらいの情報は欲しいのですが、残念ながらサンプルにはその破片も書かれておりません。
探したところ、ChakraCoreソリューションにある ch プロジェクトのソースがヒントになります。WScriptJsrt.cpp の PrintException がそのものズバリです。
使用箇所を抜粋すれば
... runScript = ChakraRTInterface::JsRun(scriptSource, sourceContext, fname, JsParseScriptAttributeNone, nullptr /*result*/ ); if(runScript != JsNoError){ WScriptJsrt::PrintException(fileName, runScript); }else{ ...
RunScriptでエラーが出たらそのまま収集を開始しています。特にcontextやruntimeなどの情報は要らないようです。
PrintExceptionの中身はgithubでご覧ください。
一目見てうっ!って感じですが、大まかな流れはシンプルです。
bool WScriptJsrt::PrintException(LPCSTR fileName, JsErrorCode jsErrorCode) { LPCWSTR errorTypeString = ConvertErrorCodeToMessage(jsErrorCode); ChakraRTInterface::JsGetAndClearException(&exception); if (exception != nullptr) { if (jsErrorCode == JsErrorCode::JsErrorScriptCompile || jsErrorCode == JsErrorCode::JsErrorScriptException) { if (jsErrorCode == JsErrorCode::JsErrorScriptCompile) { } else { } } else { fwprintf(stderr, _u("Error : %ls\n"), errorTypeString); } return true; } else { fwprintf(stderr, _u("Error : %ls\n"), errorTypeString); } return false; }
errorによって大きく3つにわかれます。コンパイルエラー、実行時エラー、それ以外のエラー。
詳しく調べられるのは、コンパイルと実行時の2つだけ。その他は基本的に JsErrorCode の列挙名を文字列にして返すだけみたいです。
エラーは JsGetAndClearException で得られる exception が詳しい内容を保持しています。exception もJsValueRefであることから、propertyをもった単なるjavascript上のオブジェクトです。(たぶんjavascript上からも値を取得してゴニョゴニョ出来るでしょう)
と思って JsGetValueType するとJsError型と帰ってきました。一応型としては分かれているみたいです。
Exceptionの中身
エラー情報を取得する jsGetAndClearException には以下の説明が載っています。
JsGetAndClearException 関数 現在のコンテキストのランタイムを例外状態にした例外を返し、そのランタイムの例外状態をリセットします。 現在のコンテキストのランタイムが例外状態でない場合、この API は JsErrorInvalidArgument を返します。ランタイムが無効である場合は、スクリプトが終了したことを示す例外を返しますが、例外はクリアしません (JsEnableRuntimeExecution を使用してランタイムを再度有効化すると、例外はクリアされます)。
プログラムが複雑になってくると JsEnableRuntimeExecution なども使用することになってきそうです。
取得出来る exception にどんな property があるのか知りたかったのですがちょっと自力で調べないといけないようです。
エラー型もオブジェクト型みたいなものなので、普通のオブジェクトと同じように中身を出力してみると。
・JsErrorScriptCompile { message : Expected '(' line : 10 column : 4 length : 2 source : for in c of{ url : sample01.js } ・JsErrorScriptExceprion { message : 'o' is not defined description : 'o' is not defined number : -2146823279 stack : ReferenceError: 'o' is not defined \r\n at callback_test (sample01.js:12:5) }
期待したほど入っていませんでした。ほぼ上記のサンプルで情報は出し切っているみたいです。
エラー出力
exception の内容はスッキリしてるので、ぶっちゃけ、エラー表示はオブジェクト内容をそのまま出力すれば良いだけなような気がしてきました。
というわけで、オブジェクト内容を表示するコードさえ書けば以下のような感じに。
exception の型がJsError型であることに注意してください。
// print object property function PrintObject(obj:JsValueRef; space:string=''):string; function inToString(ref:JsValueRef):string; var t : JsValueType; strref : JsValueRef; begin result := ''; JsGetValueType(ref,t); if t = JsString then begin result := GetString(ref); end else begin if jsConvertValueToString(ref,strref) = JsNoError then result := GetString(strref); end; end; var names : JsValueRef; namecount : integer; ref,idref : JsValueRef; t : JsValueType; i,n : integer; name,value : string; begin // is object or error? JsGetValueType(obj,t); if (t <> JsObject)and(t <> jsError) then begin result := inToString(obj); exit; end; result := ''; JsGetOwnPropertyNames(obj,names); // get propert name array object : ["aaa","bbb","cccc"] ref := GetProperty(names,'length',JsNumber); // get array count if ref = nil then exit; JsNumberToInt(ref,namecount); for i:=0 to namecount-1 do begin ref := GetArray(names,i,JsString); if ref = nil then continue; name := GetString(ref); if JsGetPropertyIdFromName(pwchar_t(name),idref) <> JsNoError then continue; if JsGetProperty(obj,idref,ref) <> JsNoError then continue; value := inToString(ref); value := space + name + ' : ' + value; if result = '' then result := result + value else result := result + _nn + value; end; end; // error handling function ScriptErrorHandling(error:JsErrorCode; const filename:string=''):string; var exception : JsValueRef; begin if error = JsNoError then exit; result := 'filename : ' + filename; // file , url , function name result := result + _nn + 'error : ' + jsErrorToString(error); // JsErrorCode(int) -> string //exception object if jsGetAndClearException(exception) = JsNoError then begin result := result + _nn + PrintObject(exception,''); end; result := '----------' + _nn + result + _nn + '----------'; end;
こんなにスッキリとしたエラーハンドルになります。
ついでに exception.toString() もやってみたんですが、出力される内容が異なりました。
具体的には行数などのnumber型が抜かれちゃったので、やっぱり全propertyを調べ上げて全出力するのが良さそうです。
関数実行のエラーハンドリング
JsRunScript の他にもエラーハンドリングが必要な箇所があります。関数実行 JsCallFunction です。
この2つの関数は引数も似ていれば、大本(c++)の内部も似ています。
なので、luaでもそうでしたが、同じようにエラーハンドリング出来るかな?と思ったら出来ました。
// Run the script. error := JsRunScript(pwchar_t(script), @currentSourceContext, pwchar_t(filename), resultValue); // error result := ScriptErrorHandling(error,filename); ... //call function error := jsCallFunction(ref,PJsValueRef(params),paramcount,retvalue); // error result := ScriptErrorHandling(error,'*function:'+name);
エラーをわざと発生させると
・JsErrorScriptCompile ---------- filename : sample01.js error : SyntaxError message : Expected ';' line : 10 column : 23 length : 9 source : dsnfkjssnfkjsmdljkdss()dfslkdlas; url : sample01.js ---------- ・JsErrorScriptException ---------- filename : *function:callback_test error : ScriptError message : 'o' is not defined description : 'o' is not defined number : -2146823279 stack : ReferenceError: 'o' is not defined at callback_test (sample01.js:14:5) ----------
やったー!
おまけ グローバルオブジェクトの中身
中身です。最後の imp_??? はプログラム側から追加した関数です。
NaN : NaN Infinity : Infinity undefined : undefined eval : function eval() { [native code] } parseInt : function parseInt() { [native code] } parseFloat : function parseFloat() { [native code] } isNaN : function isNaN() { [native code] } isFinite : function isFinite() { [native code] } decodeURI : function decodeURI() { [native code] } decodeURIComponent : function decodeURIComponent() { [native code] } encodeURI : function encodeURI() { [native code] } encodeURIComponent : function encodeURIComponent() { [native code] } escape : function escape() { [native code] } unescape : function unescape() { [native code] } CollectGarbage : function CollectGarbage() { [native code] } Object : function Object() { [native code] } Array : function Array() { [native code] } Boolean : function Boolean() { [native code] } Symbol : function Symbol() { [native code] } Proxy : function Proxy() { [native code] } Reflect : [object Object] Promise : function Promise() { [native code] } Date : function Date() { [native code] } Function : function Function() { [native code] } Math : [object Math] Number : function Number() { [native code] } String : function String() { [native code] } RegExp : function RegExp() { [native code] } ArrayBuffer : function ArrayBuffer() { [native code] } DataView : function DataView() { [native code] } Int8Array : function Int8Array() { [native code] } Uint8Array : function Uint8Array() { [native code] } Uint8ClampedArray : function Uint8ClampedArray() { [native code] } Int16Array : function Int16Array() { [native code] } Uint16Array : function Uint16Array() { [native code] } Int32Array : function Int32Array() { [native code] } Uint32Array : function Uint32Array() { [native code] } Float32Array : function Float32Array() { [native code] } Float64Array : function Float64Array() { [native code] } SharedArrayBuffer : function SharedArrayBuffer() { [native code] } Atomics : [object Atomics] JSON : [object JSON] Intl : Map : function Map() { [native code] } Set : function Set() { [native code] } WeakMap : function WeakMap() { [native code] } WeakSet : function WeakSet() { [native code] } Error : function Error() { [native code] } EvalError : function EvalError() { [native code] } RangeError : function RangeError() { [native code] } ReferenceError : function ReferenceError() { [native code] } SyntaxError : function SyntaxError() { [native code] } TypeError : function TypeError() { [native code] } URIError : function URIError() { [native code] } WebAssembly : [object WebAssembly] imp_print : function () { [native code] } imp_function : function () { [native code] }
ChakraCore implement
こっそり出したことで有名なMiscrosoft社製JavaScriptエンジンがChakraCoreです。格好よすぎる…。
DLLが出力できるので、C#じゃないDelphiで読み込めるか試してみたので一通り残しておきます。他言語でもヘッダがあれば大体同じなはずです。
- メモリ上読み込みと実行(文字列データから実行)
- 関数の登録、JS関数の呼び出し
- 値の設定と取得
- ユーザー定義クラスの受け渡しと解放
- array,object操作
- エラーハンドリング
あたりが出来ればとりあえずなんとか出来そうですね。(エラーハンドリングは長いので次に記事に分けました)
github
完成ソースコードを置いておきます。ChakraCoreとそのライブラリヘッダ抜かれています。導入については後記。
https://github.com/Ko-Ta2142/ChakraCore_DelphiSample01
ChakraCoreのコンパイル
・ChakraCore
https://github.com/Microsoft/ChakraCore
コンパイルはお任せ致します。
今回は32bitアプリケーションで動作を行うので「x86-release」で作ってみてください。
DLLヘッダファイル
こちらの方のを使用させて貰いましょう。
・ChakraCore Hello-world Sample
https://github.com/hsreina/Chakra-Samples-Delphi
ChakraCommon.h を整形したもので、命令名やパラメータなどは元ソースとほぼ同一です。C#などのサンプルともほぼ同じです。
使用には ツール - オプション - ライブラリ - ライブラリパス にこのライブラリヘッダのパスを追加するだけです。
ちょっとバグがあるので、あとで修正箇所も説明しますね。
DLLとのやりとりは stdcall になります。これは大本(c++)の CHAKRA_API にてexportと一緒に定義されています。
文字コード
ChakraCoreの文字列はwindowsではutf16でのやりとりが一般的です。
なのでDelphiであればstringのままでいいですし、c++ならstd::wstringで受け渡しすることになります。
幸いutfは変換コストが軽いので、utf8との連携でも大したことないでしょう。
まずは最小構成
中にあるサンプルがほぼオフィシャルのサンプルです。
・Delphi
https://github.com/hsreina/Chakra-Samples-Delphi/blob/develop/ChakraCore%20Samples/Hello%20World/Windows/Delphi/HelloWorld.dpr
・C++
https://github.com/Microsoft/Chakra-Samples/blob/master/ChakraCore%20Samples/Hello%20World/Windows/C%2B%2B/HelloWorld/HelloWorld.cpp
さらに実行のみ切り出せば以下になります。
JsCreateRuntime(JsRuntimeAttributeNone, nil, runtime); JsCreateContext(runtime, context); JsSetCurrentContext(context); JsRunScript(pwchar_t(script), @currentSourceContext, pwchar_t(''), result); JsSetCurrentContext(JS_INVALID_REFERENCE); JsDisposeRuntime(runtime);
JsRunScriptでutf16のpwcharバッファからスクリプトを読み込んでいますので、この段階でメモリ読み込みになっててありがたい。
引数の3番目はデバッグ用のファイル特定用のファイル名などを設定します。分かれば良いので何でも良いです。
最後の result は実行後の返値。最初にルート実行して構築するのはluaなどと同じようです。
サンプルの後半はこの return の値を取得して表示させるコードになっています。
script := '(()=>{return "Hello world!";})()';
即時実行と言うんでしたっけ。括弧が多いですが、無名関数がこんにちわ!をreturnするだけです。
function main(){ return "Hello world!"; } main(); //run
アプリに投げるのってreturn main(); じゃないんですね。関数実行を考えればそうかな?そうかもって感じです。
要素(property)操作
Chakraにおいてもluaよろしく、変数データはオブジェクト(テーブル)で構成されているので、まず必要になるのがそれらの要素 property の操作命令です。
一貫して値の操作はオブジェクトの何々〜という操作でやりとりを行います。
get [対象オブジェクト].[要素ID] -> [値] set [対象オブジェクト].[要素ID] <- [値]
みたいな感じですね。
この操作がこれから多くなるので、関数化しておきましょう。
要素IDは JsGetPropertyIdFromName で要素名を設定すれば作ってくれるの楽ちんです。
get とついてますが JsValueRef を生成するのでmakeとかcreateみたいな命令です。既存のものを指定しても新しく作られて置き換えられます。
function SetProperty(obj:JsValueRef; const name:string; value:JsValueRef):boolean; var idref : JsPropertyIdRef; begin result := true; if JsGetPropertyIdFromName(pwchar_t(name),idref) = JsNoError then if JsSetProperty(obj,idref,value,true) = JsNoError then exit; result := false; end; function GetProperty(obj:JsValueRef; const name:string; _type:JsValueType):JsValueRef; var idref : JsPropertyIdRef; t : JsValueType; begin if JsGetPropertyIdFromName(pwchar_t(name),idref) = JsNoError then if JsGetProperty(obj,idref,result) = JsNoError then if JsGetValueType(result,t) = JsNoError then if _type = t then exit; result := nil; end; function DeleteProperty(obj:JsValueRef; const name:string):boolean; var idref : JsPropertyIdRef; ret : JsValueRef; // Whether the property was deleted. begin result := true; if JsGetPropertyIdFromName(pwchar_t(name),idref) = JsNoError then if jsDeleteProperty(obj,idref,true,ret) = JsNoError then exit; result := false; end;
取得の際は厳格性がほしいので型チェックを入れてあります。
ごめん、動かないわ
ソースは問題ないのですが、ここで、ヘッダに少々修正が必要です。
具体的には JsGetValueType を使用すると指定した JsValueRef(const) が変な数値になります。利かねぇconstだな!
原因は列挙型がChakraCoreは4byte(32bit)、Delphi側が初期では最小1byte(8bit)なのが問題です。
解決方法は2つで、プロジェクト設定で列挙型の最小値を4byteにするか、ChakraCommon.pasの先頭にコンパイルオプションを入れます。
unit ChakraCommon; //enum size 32bit {$Z4} interface
この方法はソースコード単位で利くので、こっちをおすすめします。
値の取得と設定
まずはグローバルに変数を作ったり、設定したり、取得したり、削除したりしましょう。
グローバルといっても、操作対象はグローバルという名前のオブジェクトです。このあたりもluaと同じです。
global : JsValueRef; ref : JsValueRef; a : integer; ... JsGetGlobalObject(global); // set / new JsIntToNumber(123456,ref); // double値の生成 SetProperty(global,'g_int',ref); // get ref := GetProperty(global,'g_int',JsNumber); JsNumberToInt(ref,a); Writeln('a is ' + IntToStr(a)); //print // delete DeleteProperty(global,'g_int');
グローバルオブジェクトは JsGetGlobalObject() だけで取得出来ます。あれ?これでは複数仮想機械を作った場合は…。
たぶん JsSetCurrentContext(context); このあたりで切り替えられると思っては居るのですが、まだ試してません。
任意のオブジェクトに対して操作する場合は、global を他のテーブルにすり替えるだけです。
ごめん、また動きません
豪快にクラッシュすると思います。ごめんね。
JsIntToNumber の宣言に問題があるので、ChakraCommon.pas の JsIntToNumber の箇所を検索で見つけて以下に修正してください。
function JsIntToNumber( intValue : int; //var intValue : int; //** fix var value: JsValueRef ): JsErrorCode; stdcall; external DLL_NAME;
var抜くだけです。
さて、これで IntToNumber がちゃんと ValueRef かえすか確認してみると。
ref : 0x0000001F
というアドレスじゃ無い変な値が返ってきます。
あわわクラッシュじゃ!と思ったらちゃんと動きます…。不安。
大本のソースコードを見てみると
//If number is not heap allocated then we don't need to record/track the creation for time-travel if (Js::JavascriptNumber::TryToVarFast(intValue, asValue)) { return JsNoError; }
最適化してるみたいですね。
ししし知ってるわよ!タイムをトラベル、タイムトラベラーでしょ!
文字列
文字列も数値と同じです。
が、やりとりはutf16文字列の生ポインタを返すので、delphiだとstring、c++だとstd::wstringで扱うにはちょいと領域確保+コピーが必要です。
対象JsValueRefの生存期間も考えると、整数や浮動小数と同じくNativeで値を保持しておくのが安全です。
procedure MakeString(var value:JsValueRef; const s:string); begin if JsPointerToString(pwchar_t(s),length(s),value) = JsNoError then exit; raise exception.Create('CreateString.error'); end; function GetString(value:JsValueRef):string; var wptr : pwchar_t; len : size_t; t : JsValueType; begin result := ''; if JsGetValueType(value,t) = JsNoError then if t = JsString then if jsStringToPointer(value,wptr,len) = JsNoError then begin SetLength(result,len); move(wptr[0],result[1],len*sizeof(wchar_t)); exit; end; raise exception.Create('GetString.error'); end;
jsStringToPointer では文字列の先頭アドレスと文字の長さ(個数)を返します。なので、utf16ですからバイト長に変換するなら2 (sizeof(wchar_t)) をかける必要があります。
Delphiのmoveって気持ち悪いよね…。アドレスでは無く実体指定だから異質すぎる。
ユーザー定義オブジェクト型
いわゆるクラスとか構造体をぶち込みたいあなたの願いを叶えるものです。
luaにもありましたがChakraの指定はそれよりもシンプルで且つ分かりやすくなっています。
JsFinalizeCallback = procedure(data: Pointer); stdcall; function JsCreateExternalObject( data: Pointer; finalizeCallback: JsFinalizeCallback; var _object: JsValueRef ): JsErrorCode; stdcall; external DLL_NAME;
ユーザー定義型(ポインタつっこむ型)はintじゃなく専用のを使って定義します。
作るときに解放処理コールバック関数つけちゃえばいいよね。いい…。luaはごにょごにょする必要があったのでとても有り難い仕様です。
procedure ObjectDispose_callback(data: Pointer); stdcall; begin TObject(data).Free; // call super class dispose Writeln( 'object.dispose:0x' + IntToHex(Integer(data),8) ); end; procedure MakeObject(var value:JsValueRef; obj:TObject); begin Writeln( 'object.entry:0x' + IntToHex(Integer(obj),8) ); if JsCreateExternalObject(pointer(obj),ObjectDispose_callback,value) = JsNoError then exit; raise exception.Create('CreateObject.error'); end; function GetObject(value:JsValueRef):TObject; begin // valuetype is JsObject result := nil; if JsGetExternalData(value,pointer(result)) = JsNoError then exit; raise exception.Create('GetObject.error'); end;
DelphiなのでクラスはすべてTObjectの派生という特性を利用してますが、このへんはおこのみで。
解放タイミング、ガベージコレクト
さて、ユーザー定義型でついて回るのがガベージコレクトによる解放タイミング。
luaは解放すると即 dispose 命令を発効してくれましたが、Chakraではそうはいきません。
オブジェクト生成、即解放するような処理でログを出してみると
object.create object.entry:0x023D8120 object.free get array[1]:45600 12300 45600 object.dispose:0x023D8120 // 解放コールバック関数が最後に呼ばれてる
ものの見事に最後でした。
一応ガベージコレクト全域に対してクリアする命令は存在します。
function JsCollectGarbage( runtime: JsRuntimeHandle ): JsErrorCode; stdcall; external DLL_NAME;
これで即時呼ばれると思ったのですが、ちょっと特性があるみたいです。
JsDeleteProperty -> 呼ばれない JsSetPropertyで値を上書き -> 呼ばれる
この挙動はよく分かりません。
関数の登録
変数が出来たので次は関数です。
Chakraの関数はクラスメソッドと同じで、すべて this が最初についた形で読んだり呼ばれたりします。
グローバルであれば、グローバルオブジェクトが this に入ります。
また、JavaScriptはthisの変更を許容しています。変えて良いんですね。変えませんけど…。
大本(c++)の定義を見てみると
CHAKRA_API JsCallFunction( _In_ JsValueRef function , _In_reads_(argumentCount) JsValueRef *arguments , _In_ unsigned short argumentCount , _Out_opt_ JsValueRef *result);
JsValueRef *arguments 、つまりarray(配列)とその個数指定することになっています。
なので、キャスト用構造体でも作っておくと便利です。
type TJsParamsRecord = packed record //packed _this : JsValueRef; values : array [0..31-1] of JsValueRef; //適当な個数です end; PJsParamsRecord = ^TJsParamsRecord;
ではまず関数を登録しましょう。関数はヘッダで宣言されている型と全く同じでないといけません。ちょっとでも違うとメモリ破壊が始まります。コワイ!
JsNativeFunction型がそれです。
type JsNativeFunction = function( callee: JsValueRef; isConstructCall: bool; arguments: PJsValueRef; argumentCount: Word; callbackState: Pointer ): JsValueRef; stdcall;
arguments: PJsValueRef が最初に説明したthisを含む引数の配列です。nil/nullptrで終わってはいないのでちゃんと argumentCount で終端を限定する必要があります。
まずは print(コンソール出力) 的なものから作るのが世の常です。
// JS : print(s:string) // s : string // rreturn : none function implement_print(callee: JsValueRef; isConstructCall: bool; arguments: PJsValueRef; argumentCount: Word; callbackState: Pointer):JsValueRef; stdcall; var s : string; pParams : PJsParamsRecord; begin if argumentCount <> 2 then begin raise exception.Create('callback implement_printf.param error'); end; pParams := PJsParamsRecord(arguments); //cast s := GetString(pParams^.values[0]); Writeln(s); result := nil; end;
文字列引数を1つ持つため、this も足して引数カウントは2になります。
登録はいくつも行うので適当な関数を作って利用するといいでしょう。
procedure EntryFunction(obj : JsValueRef; const name:string; func:JsNativeFunction; state:pointer); var ref : JsValueRef; begin if jsCreateFunction(func,state,ref) = JsNoError then if SetProperty(obj,name,ref) then exit; raise exception.Create('EntryFunction.error'); end; ... EntryFunction(global,'imp_print',implement_print,nil);
JavaScript側からは以下のように使用できます。
imp_print"hello world :");
JavaScript側の関数をnative側から呼ぶ
この場合は jsCallFunction を使用します。
function JsCallFunction( _function: JsValueRef; arguments: PJsValueRef; argumentCount: short; var result: JsValueRef ): JsErrorCode; stdcall; external DLL_NAME;
関数を登録した際と同じように、引数は JsValueRef の配列で指定します。なので先ほど作った構造体を使用すると楽ちんです。
JavaScript側で定義した、文字列を表示させるグローバルなテスト関数を定義しておいて、それを呼ぶ場合は。
params : TJsParamsRecord; ref,subret : JsValueRef; ... // params params._this := global; // need "this." MakeString(ref,'sample message'); params.values[0] := ref; // call ref := GetProperty(global,'callback_test',JsFunction); jsCallFunction(ref,PJsValueRef(@params),2,subret); // callback_test('sample message');
callback_test('sample message'); という形でJavaScript上の関数が呼ばれます。
最後が返値のJsValueRefです。返さない場合もnilではない値が返ります。なんだろう?
さてこうしてみると、返値や、params.values[0] につっこんだ JsValueRef はちゃんと解放されるのか心配です。
リファレンスのRefによれば、「スタックに乗る限り自動的に解放されます」とのことです。なので返値や引数は大丈夫みたいです。
JsGetPropertyIdFromName で作った値なんかもスタックに乗るのかなどちょっと不安ですが、JsGetPropertyを通す以上乗ると思うのでたぶん大丈夫でしょう。
解放に関しては JsRelease という命令がありますが、基本は addRef 使用時に対で使うことぐらいしか触れられていません。
array配列操作
配列操作です。以下のようなものに対して操作します。
[1,2,5,6,8,9,100]
配列操作は JsGetProperty の配列版 JsGetIndexedProperty でおこないますが、インデックスの指定がintではなくJsValueRefになっています。
なので関数を作っておかないとちょっと面倒です。
procedure SetArray(_array:JsValueRef; index:integer; value:JsValueRef); var indexref : JsValueRef; begin if JsIntToNumber(index,indexref) = JsNoError then if JsSetIndexedProperty(_array,indexref,value) = JsNoError then exit; raise exception.Create('SetArray.error'); end; function GetArray(_array:JsValueRef; index:integer; _type:JsValueType):jsValueRef; var t : JsValueType; //len : integer; indexref : JsValueRef; begin result := nil; // length check //JsNumberToInt( GetProperty(_array,'length',JsValueNumber) , len ); //if (len >= index)or(index < 0) then //begin // raise exception.Create('GetArray.out of index'); //end; if JsIntToNumber(index,indexref) = JsNoError then if JsGetIndexedProperty(_array,indexref,result) = JsNoError then if JsGetValueType(result,t) = JsNoError then if t = _type then exit; raise exception.Create('GetArray.error'); end;
配列の長さ取得はjavascript上同様 length プロパティで出来るみたいです。
が、配列外を指定しても JsGetIndexedProperty がエラーを出してくれるので、ここでは必要なさそうです。
オブジェクト操作
もう今までで散々やってきたので、もういいよね。
オブジェクトの中身を一覧表示させたい場合はちょっと特殊な JsGetOwnPropertyNames を使用します。処理としてはpropertyの要素名の配列を取得して、あとはGetPropertyで回すだけ。
procedure PrintObject(obj:JsValueRef); var names : JsValueRef; namecount : integer; ref,idref : JsValueRef; t : JsValueType; d : double; i,n : integer; name,value : string; begin JsGetOwnPropertyNames(obj,names); // get propert name array object : ["aaa","bbb","cccc"] ref := GetProperty(names,'length',JsNumber); // get array count if ref = nil then exit; JsNumberToInt(ref,namecount); for i:=0 to namecount-1 do begin ref := GetArray(names,i,JsString); if ref = nil then continue; name := GetString(ref); if JsGetPropertyIdFromName(pwchar_t(name),idref) <> JsNoError then continue; if JsGetProperty(obj,idref,ref) <> JsNoError then continue; JsGetValueType(ref,t); value := ''; if t = JsString then begin value := GetString(ref); end; if t = JsNumber then begin JsNumberToDouble(ref,d); value := FloatToStr(d); end; WriteLn(' ' + name + ' : ' + value); end; end;
spine c++ Implement
spineのC++組み込みをちょっとやってみました。
オフシャルgithubでテストコードやライブラリは落とせますが、オフィシャルドキュメントにある最小限の構成が欲しかったので、VisualStdio2015で構築したいと思います。
- 生成と解放
- メモリ上読み込み
- モーション設定
- 動かす
- ポリゴンログの表示
- イベントの取得
あたりが目標です。
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)
最後に
とりあえず、これで最小限の情報は得られたかと思います。
Metasequoia station plugin "ObjectComposition ver2"
オブジェクトを立方体に変形投影するMetasequoiaプラグインです。
同じ形状のものを何個もクローンさせる場合に効果を発揮します。
投影なので配置後も元オブジェクトを修正するとすべてに反映されます。
8頂点(1x1x1)、28頂点(2x2x2)の立方体に対応、また入子型(コンポ内コンポ)にも対応しました。
windows 32bit(x86),x64(x64)にて動作します。
ダウンロード
DropBoxよりダウンロードください。
https://www.dropbox.com/s/in3w93ssgjmhi9z/mqdll_ObjectComposition.zip?dl=0
github
ソースコードをgithubに公開しておきます。
https://github.com/Ko-Ta2142/mq_ObjectComposition
コンパイルはVisualStudio2015で行っています。
あとでまた整理、書き足します。
ローカルなんとか
c++で関数内関数を使いたいんじゃ!
int hoge::hoge(){ struct Inner{ int addadd(int a){ return a+a; } } return Inner::addadd(4); }
こんなことって普通は無くて、クラスメソッドが使いたいんじゃ!
int hoge::hoge(){ struct Inner{ int addadd(const hoge* me,int a){ return me->hogehoge(a) + me->hogehoge(a); } } return Inner::addadd(&this,4); }
コンパイラ「そのメソッドはpublicではありません」。
そっかーやめとくか。
4x4matrixのちょっとした最適化
matrix計算も思い出すのに30分かかりました。もう6〜8年前のコードだしそりゃ忘れるって話ですよ…。
行列の結合
mulとかcompositeとか色々言われる、行列同士のかけ算です。
function matrix4composite(m1,m2) local t = { m1[1+0]*m2[1+0] + m1[2+0]*m2[1+4] + m1[3+0]*m2[1+8] + m1[4+0]*m2[1+12], m1[1+0]*m2[2+0] + m1[2+0]*m2[2+4] + m1[3+0]*m2[2+8] + m1[4+0]*m2[2+12], m1[1+0]*m2[3+0] + m1[2+0]*m2[3+4] + m1[3+0]*m2[3+8] + m1[4+0]*m2[3+12], m1[1+0]*m2[4+0] + m1[2+0]*m2[4+4] + m1[3+0]*m2[4+8] + m1[4+0]*m2[4+12], m1[1+4]*m2[1+0] + m1[2+4]*m2[1+4] + m1[3+4]*m2[1+8] + m1[4+4]*m2[1+12], m1[1+4]*m2[2+0] + m1[2+4]*m2[2+4] + m1[3+4]*m2[2+8] + m1[4+4]*m2[2+12], m1[1+4]*m2[3+0] + m1[2+4]*m2[3+4] + m1[3+4]*m2[3+8] + m1[4+4]*m2[3+12], m1[1+4]*m2[4+0] + m1[2+4]*m2[4+4] + m1[3+4]*m2[4+8] + m1[4+4]*m2[4+12], m1[1+8]*m2[1+0] + m1[2+8]*m2[1+4] + m1[3+8]*m2[1+8] + m1[4+8]*m2[1+12], m1[1+8]*m2[2+0] + m1[2+8]*m2[2+4] + m1[3+8]*m2[2+8] + m1[4+8]*m2[2+12], m1[1+8]*m2[3+0] + m1[2+8]*m2[3+4] + m1[3+8]*m2[3+8] + m1[4+8]*m2[3+12], m1[1+8]*m2[4+0] + m1[2+8]*m2[4+4] + m1[3+8]*m2[4+8] + m1[4+8]*m2[4+12], m1[1+12]*m2[1+0] + m1[2+12]*m2[1+4] + m1[3+12]*m2[1+8] + m1[4+12]*m2[1+12], m1[1+12]*m2[2+0] + m1[2+12]*m2[2+4] + m1[3+12]*m2[2+8] + m1[4+12]*m2[2+12], m1[1+12]*m2[3+0] + m1[2+12]*m2[3+4] + m1[3+12]*m2[3+8] + m1[4+12]*m2[3+12], m1[1+12]*m2[4+0] + m1[2+12]*m2[4+4] + m1[3+12]*m2[4+8] + m1[4+12]*m2[4+12] } return t end
すごい…目がチカチカする。
forで回そうが焼け石に水です。
これだと新たな行列が生成されて毎回メモリを食っちゃうので、以下のように一個目に反映させてecoしたりします。
function matrix4pushcomposite(m1,m2) local h1,h2,h3,h4 h1 = m1[1+0] h2 = m1[2+0] h3 = m1[3+0] h4 = m1[4+0] m1[1+0] = h1*m2[1+0] + h2*m2[1+4] + h3*m2[1+8] + h4*m2[1+12] m1[2+0] = h1*m2[2+0] + h2*m2[2+4] + h3*m2[2+8] + h4*m2[2+12] m1[3+0] = h1*m2[3+0] + h2*m2[3+4] + h3*m2[3+8] + h4*m2[3+12] m1[4+0] = h1*m2[4+0] + h2*m2[4+4] + h3*m2[4+8] + h4*m2[4+12] h1 = m1[1+4] h2 = m1[2+4] h3 = m1[3+4] h4 = m1[4+4] m1[1+4] = h1*m2[1+0] + h2*m2[1+4] + h3*m2[1+8] + h4*m2[1+12] m1[2+4] = h1*m2[2+0] + h2*m2[2+4] + h3*m2[2+8] + h4*m2[2+12] m1[3+4] = h1*m2[3+0] + h2*m2[3+4] + h3*m2[3+8] + h4*m2[3+12] m1[4+4] = h1*m2[4+0] + h2*m2[4+4] + h3*m2[4+8] + h4*m2[4+12] ...省略 end
そろそろhatenaさんに怒られちゃう文字数なので省略します。
あんまり変わりませんが、実際の速度はこれで結構マシになったりします。以上でお膳立ては完了です。
回転行列と結合したい
本題。回転を行列に加えたいなって場合は以下のような感じで組むと思います。
local matrix1 = matrix4indentity() --単位行列 matrix4pushcomposite(matrix1 , matrix4rotatex(rotx)) --X軸回転を反映
毎回行う行列の結合が重いのなんのって。
なんですがX軸回転の行列なんて
function matrix4rotatex(r) local s = math.sin(r) local c = math.cos(r) local m2 = {} m2[1+0] = 1.0 m2[2+0] = 0.0 m2[3+0] = 0.0 m2[4+0] = 0.0 m2[1+4] = 0.0 m2[2+4] = c m2[3+4] = s m2[4+4] = 0.0 m2[1+8] = 0.0 m2[2+8] = -s m2[3+8] = c m2[4+8] = 0.0 m2[1+12] = 0.0 m2[2+12] = 0.0 m2[3+12] = 0.0 m2[4+12] = 1.0 return m2 end
と、縦に見て、2行目と3行目ぐらいしか使ってませんし、1行目と4行目は単位行列です。
最適化しましょう。
まず、回転と結合を1つの関数にまとめます。
function matrix4pushrotatex(m1,x) local s = math.sin(r) local c = math.cos(r) local m2 = {} m2[1+0] = 1.0 m2[2+0] = 0.0 m2[3+0] = 0.0 m2[4+0] = 0.0 m2[1+4] = 0.0 m2[2+4] = c m2[3+4] = s m2[4+4] = 0.0 m2[1+8] = 0.0 m2[2+8] = -s m2[3+8] = c m2[4+8] = 0.0 m2[1+12] = 0.0 m2[2+12] = 0.0 m2[3+12] = 0.0 m2[4+12] = 1.0 return m2 local h1,h2,h3,h4 h1 = m1[1+0] h2 = m1[2+0] h3 = m1[3+0] h4 = m1[4+0] m1[1+0] = h1*m2[1+0] + h2*m2[1+4] + h3*m2[1+8] + h4*m2[1+12] m1[2+0] = h1*m2[2+0] + h2*m2[2+4] + h3*m2[2+8] + h4*m2[2+12] m1[3+0] = h1*m2[3+0] + h2*m2[3+4] + h3*m2[3+8] + h4*m2[3+12] m1[4+0] = h1*m2[4+0] + h2*m2[4+4] + h3*m2[4+8] + h4*m2[4+12] ...省略 end
バーン。でかい…省略するね。
1行目と2行目は使ってないので削除しましょう。ついでにsin,cosも直接指定します。
function matrix4pushrotatex(m1,x) local s = math.sin(r) local c = math.cos(r) local m2 = {} -- m2[2+4] = c m2[3+4] = s -- m2[2+8] = -s m2[3+8] = c local h1,h2,h3,h4 h1 = m1[1+0] h2 = m1[2+0] h3 = m1[3+0] h4 = m1[4+0] m1[2+0] = h1*m2[2+0] + h2*c + h3*-s + h4*m2[2+12] m1[3+0] = h1*m2[3+0] + h2*s + h3*c + h4*m2[3+12] ...省略 end
回転行列の残りの部分は0.0ですから、あらこれイラネ。
function matrix4pushrotatex(m1,x) local s = math.sin(r) local c = math.cos(r) local h2,h3 h2 = m1[2+0] h3 = m1[3+0] m1[2+0] = h2*c + h3*-s m1[3+0] = h2*s + h3*c ...省略 end
となります。省略部分もこれと全く同じように最適化してできあがりです。
以上はx軸回転ですが、あとy軸、z軸、スケール、移動に関しても、全く同様の点順で最適化出来ます。
もっとケースを絞り込めば最適化も出来ますが、とりあえず汎用的に使うならこれぐらいでしょうか。
github
https://github.com/Ko-Ta2142/lua_matrix
適当においておきます。適当にお使いください。
クォーターニオンとかはありません。掃きだめにそこまで求めないでください:Q
githubのtab値かえられるの!?
urlに「?ts=4」で変えられるんですね。
しらなかった
そんなの