本文書は、吉里吉里で、トランジションプラグイン(.dll)の作り方について述べる。
吉里吉里でのトランジションプラグインは少し独特だ。吉里吉里プラグインとも 違って、定型の「書き方」がある。中身を知らないと多分理解できないと思うので、 ここに知ってる限りの情報を書き出す次第ナリ。
…は、吉里吉里プラグインの作り方と 同じなんでそっち参照してくだされ。
こちらも、用意するファイル以外は 吉里吉里プラグインの作り方と 同じなんでそっち参照。あらかじめ用意するファイルは以下二つだけ。
ファイル種別 | ファイル名 | 吉里吉里ソースコード中の場所 | 備考 |
---|---|---|---|
ソースファイル | tp_stub.cpp | kirikiri2/src/plugins/win32/ | - |
ヘッダファイル | tp_stub.h | kirikiri2/src/plugins/win32/ | - |
トランジションプラグインでは、二つのクラスを定義する。
この二つのクラスを自前で定義し、あとはトランジションハンドラプロバイダを V2Link()で吉里吉里に登録すれば、トランジションは可能となる。その意味では 思いのほかシンプル。
KAGから[trans method=xxx ...]が実行されてトランジションが開始された時 (TJSではLayer.beginTransition()ね)、実際に上のクラスのどのメソッドが どういう順番で呼ばれるかというあたりの概要を以下に示す。 本当はこれら以外にも呼ばれるものはあるけれど、まぁ大まかに、ということで。
上のような大まかな流れがわかっていれば、何をどう作るのかがわかり易いと思う。 というかこのくらいどっかにサマリで書いといて欲しいです。一応ソースコード上に 書いてあるんだけど、我輩頭悪いからソースコード見てもよくわかんなかったよ!
で、文章だけだとやっぱ判りづらいので、こんなカンジという図解を以下に示す。 ごめんけど我輩にはこれ以上わかりやすく書くのはムリのムリムリだったわいな。 プラグイン登録とトランジション開始にのみ絞って書いてみたので、他の細かいことは 書いてないでありますよ。
上の説明を踏まえつつ、中身を解説しながらクロスフェードプラグインを作ってみる。 既に存在するやん、とか言わないで! 解説付きなところが違うと考えて頂ければ 幸いですホントに。なぜクロスフェードなのかというと、一番簡単そうだから。 それ以外に理由はないッ!
今回作成する自前トランジションdllの名前を、MyCF.dllとする。My Cross Fadeの 略なんだ、と思ってください是非。長い名前だと書くの面倒だから…。
上で作成したビルド環境に加え、今回は以下のファイルを作成する。これらの ファイルが揃えば、トランジションプラグイン(.dll)はビルドできる。
ファイル種別 | ファイル名 | 定義物 |
---|---|---|
ソースファイル | MyCF.cpp | トランジションハンドラ及びトランジションハンドラプロバイダ |
main.cpp | トランジションハンドラプロバイダを登録するためのV2Link()とV2Unlink() | |
ヘッダファイル | MyCF.h | トランジションハンドラプロバイダをmain.cppから利用するためのプロトタイプ宣言 |
リソースファイル | MyCF.rc | dllのバージョン情報等を追加するリソースファイル |
MyCF.h は、まぁ言えば(ファイルを纏めれば)無くてもいいんだが、 トランジションハンドラプロバイダを吉里吉里に登録するための関数をプロトタイプ 宣言している。もちろんこの他に便利そうなマクロ・インライン関数や使用する 定数を定義してもよい。以下は最低レベルのもの。
登録関数は RegisterMyCFTransHandlerProvider()、削除関数は UnregisterMyACFTransHandlerProvider() として定義する。実体はこの後、 main.cppの中で定義。
#ifndef MyCF_H #define MyCF_H // トランジションハンドラプロバイダの登録関数 void RegisterMyCFTransHandlerProvider(); // トランジションハンドラプロバイダの削除関数 void UnregisterMyCFTransHandlerProvider(); #endif |
このファイルはほぼ定型。吉里吉里に外部DLLを登録するためには、V2Link()と V2Unlink()という二つの関数が必要になる。これは実際には TJSの Plugins.link('dllファイル') を実行した時に吉里吉里が呼び出す。 そのため、この関数は外部に対してexportされて いなければならない。昔は .def ファイルを作ってそこでexportしていたが、 VC++2008なら関数定義の頭に __declspec(dllexport) を付加すればexportされるので、 .defファイルは不要になった。 DLLからのエクスポート(MSDNドキュメント)を参照。DllEntryPoint()は リンカに指定された「最初に実行される関数」。ちうても return 1 だけでよろし。
V2Link()中で、上で定義したトランジションハンドラプロバイダの登録関数を呼び、 V2Unlink()中で、トランジションハンドラプロバイダの削除関数を呼ぶだけ。 スタブの初期化・使用終了というのは必要なものらしいので、何も考えずに くっつけておけばいい、そうな。
#include <windows.h> #include "tp_stub.h" #include "MyCF.h" //--------------------------------------------------------------------------- //#pragma argsused int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void* lpReserved) { // DLL エントリポイント return 1; } //--------------------------------------------------------------------------- // V2Link は DLL がリンクされるときに実行される extern "C" HRESULT __declspec(dllexport) V2Link(iTVPFunctionExporter *exporter) { // スタブの初期化 TVPInitImportStub(exporter); // 必ずこれは記述 /* TVPInitImpotStub を実行した後は吉里吉里内部の各関数や tTJSVariant 等の 基本的な型が使用可能になる。 */ // トランジションハンドラプロバイダの登録 RegisterMyCFTransHandlerProvider(); return S_OK; } //--------------------------------------------------------------------------- // V2Unlink は DLL がアンリンクされるときに実行される extern "C" HRESULT __declspec(dllexport) V2Unlink() { // トランジションハンドラプロバイダの登録削除 UnregisterMyCFTransHandlerProvider(); // スタブの使用終了 TVPUninitImportStub(); // 必ずこれは記述 /* TVPUninitImpotStub は TVPInitImportStub で確保したメモリを解放する ので必ず記述する。 */ return S_OK; } |
※ここだけ、説明のためにソースコードの記述順が違うので要注意。
上で述べたように、トランジションハンドラプロバイダは トランジションハンドラを吉里吉里に登録するためにラッピングするものだ。 大事な概念が以下三つある。
特に見て欲しいのは StartTransition() メソッド。ここでは以下の動作を行う。
正直、上で書いている『参照カウンタ』がイマイチよくわかんないんだけど。 レイヤごとに独立して同じトランジションを実行した時の話かしら。 でも、だとすると ローカル変数持っちゃいけないことになっちゃうし。吉里吉里がキャッシュみたいな 形で掴むのかな。
(トランジションハンドラ定義からの続き) // トランジションハンドラプロバイダのクラス定義 // 必ず iTVPTransHandlerProvider から派生させること。 class tTVPMyCFTransHandlerProvider : public iTVPTransHandlerProvider { tjs_uint RefCount; // 参照カウンタ public: // コンストラクタ tTVPMyCFTransHandlerProvider() { RefCount = 1; } // デストラクタ ~tTVPMyCFTransHandlerProvider() { // 特にすることなし } // 参照された時に呼ばれる tjs_error TJS_INTF_METHOD AddRef() { // iTVPBaseTransHandler の AddRef // 参照カウンタをインクリメント RefCount ++; return TJS_S_OK; } // 参照が解除された時に呼ばれる tjs_error TJS_INTF_METHOD Release() { // iTVPBaseTransHandler の Release // 参照カウンタをデクリメントし、0 になるならば自身を delete if(RefCount == 1) delete this; else RefCount--; return TJS_S_OK; } // トランジションの名前を返す。[trans method=xxx] のxxx部。 tjs_error TJS_INTF_METHOD GetName( /*out*/const tjs_char ** name) { if(name) *name = TJS_W("MyCF"); return TJS_S_OK; } // KAGの[trans ...]、TJSのLayer.StartTransition()の実体 tjs_error TJS_INTF_METHOD StartTransition( /*in*/iTVPSimpleOptionProvider *options, // option provider /*in*/iTVPSimpleImageProvider *imagepro, // image provider /*in*/tTVPLayerType layertype, // destination layer type /*in*/tjs_uint src1w, tjs_uint src1h, // source 1 size /*in*/tjs_uint src2w, tjs_uint src2h, // source 2 size /*out*/tTVPTransType *type, // transition type /*out*/tTVPTransUpdateType * updatetype, // update typwe /*out*/iTVPBaseTransHandler ** handler // transition handler ) { if(type) *type = ttExchange; // transition type : exchange if(updatetype) *updatetype = tutDivisible; // update type : divisible if(!handler) return TJS_E_FAIL; if(!options) return TJS_E_FAIL; if(src1w != src2w || src1h != src2h) return TJS_E_FAIL; // src1 と src2 のサイズが一致していないと駄目 // KAG→TJSで渡されたオプションを得る tTJSVariant tmp; // [trans method=MyCF time=2000] なら ↓では tmp には 2000 が入る if (TJS_FAILED(options->GetValue(TJS_W("time"), &tmp)) || tmp.Type() == tvtVoid) return TJS_E_FAIL; // time 属性が指定されていない tjs_int64 time = (tjs_int64)tmp; if(time < 2) time = 2; // あまり小さな数値を指定すると問題が起きるので // トランジションハンドラのインスタンスを作成 try { *handler = new tTVPMyCFTransHandler(time, src1w, src1h); } catch(...) { throw; } return TJS_S_OK; } }; (トランジションハンドラプロバイダの登録・削除へ続く) |
トランジションハンドラは、トランジションハンドラプロバイダから渡された 引数と、吉里吉里から渡された時間情報を元に、トランジション中の中間画像を もりもり作る。まずはクラス定義と、その前にヘッダをincludeしている部分を示す。
また例によって『参照カウンタ』の意味が我輩よくわかんないんですがさておき、 やってることはトランジションハンドラプロバイダと同じなので説明は省略する。
メンバ変数として、First、StartTick、Time、CurRatio を定義している。
それぞれの説明はソースコード中に書いたので参照して欲しい。吉里吉里は
時間をTickという単位(1/1000秒単位)で管理している。
System.getTickCount()あたりを見れば判るかも。StartTickを
コンストラクタ中で代入しておらず、StartProcess()中で代入しているのは、
コンストラクタで現在のTickを取得する方法がないため。…じゃないかなぁ…。
本当は、TVPGetTickCount()なんてグローバルな関数を実行すれば現在のTickは
得られるし、そのほうがFirstなんてヘンなメンバ変数が要らなくなって助かるけれど。
でもTVPStartTickCount()は呼べないんだよなー。
──うーんとね、多分、
コンストラクタが呼ばれた後にStartProcess()が呼ばれるまでに時間がかかると、
その分StartTickと現在のTickとの間に差ができてしまい、
トランジションの出だしが「カクッ」となるのを防ぐためじゃないかな!(妄想)
SetOption()とか、やることないんなら定義しなきゃいいのに、と思うかもしれないが、 親クラスで virtual って書いてあるのでここで定義しないとダメなのでした。 キビシーッ!
#include <windows.h> #include "tp_stub.h" #include "MyCF.h" // MyCF(My CrossFade)トランジションハンドラクラスを定義 class tTVPMyCFTransHandler : public iTVPDivisibleTransHandler { tjs_int RefCount; // このハンドラの参照カウンタ protected: bool First; // 一番最初の呼び出しかどうか tjs_int64 StartTick; // トランジションを開始した tick count tjs_int64 Time; // トランジションに要する時間 double CurRatio; // 現在の変化率(動的に変化させる) public: // コンストラクタ tTVPMyCFTransHandler(tjs_uint64 time, tjs_int width, tjs_int height) { RefCount = 1; // 新規なので参照カウンタは1 First = true; // 「一番最初である」フラグ Time = time; // トランジションにかける時間を保存 } // デストラクタ ~tTVPMyCFTransHandler() { } tjs_error TJS_INTF_METHOD AddRef() { // iTVPBaseTransHandler の AddRef // 参照カウンタをインクリメント RefCount++; return TJS_S_OK; } tjs_error TJS_INTF_METHOD Release() { // iTVPBaseTransHandler の Release // 参照カウンタをデクリメントし、0 になるならば delete this if(RefCount == 1) { delete this; } else { RefCount--; } return TJS_S_OK; } tjs_error TJS_INTF_METHOD SetOption( /*in*/iTVPSimpleOptionProvider *options // option provider ) { // iTVPBaseTransHandler の SetOption // とくにやることなし return TJS_S_OK; } // トランジション中の中間画像一枚生成開始 tjs_error TJS_INTF_METHOD StartProcess(tjs_uint64 tick); // トランジション中の中間画像一枚生成完了 tjs_error TJS_INTF_METHOD EndProcess(); // トランジション中の中間画像一枚生成(画像は分割され、本関数が複数同時に実行されることがある) tjs_error TJS_INTF_METHOD Process( tTVPDivisibleData *data); void Blend(tTVPDivisibleData *data); // 最終画像にする tjs_error TJS_INTF_METHOD MakeFinalImage( iTVPScanLineProvider ** dest, iTVPScanLineProvider * src1, iTVPScanLineProvider * src2) { *dest = src2; // 常に最終画像は src2 return TJS_S_OK; } }; (実際のメソッド定義に続く) |
このクラスの実際のメソッド定義は以下の通り。上で説明したとおり、 一枚の絵を生成するために、StartProcess()x1→Process()x分割数回→EndProcess()x1 を延々繰り返す。 下の関数定義のキモは、Process()→Blend()。この中で、実際の画像を 経過時間に応じてクロスフェードしている。
Process()に渡される引数data(tTVPDivisibleData型、 tp_stub.hで定義されている)には、 主に三種類の値が入っている。詳細はヘッダの中を見て欲しい。
画像が分割されていた場合、画像サイズとその位置は、以下のようになっている。
(トランジションハンドラクラス定義からの続き) tjs_error TJS_INTF_METHOD tTVPMyCFTransHandler::StartProcess(tjs_uint64 tick) { // トランジションの画面更新一回ごとに呼ばれる // トランジションの画面更新一回につき、まず最初に StartProcess が呼ばれる // そのあと Process が複数回呼ばれる ( 領域を分割処理している場合 ) // 最後に EndProcess が呼ばれる // 最初の実行なら、StartTick を設定 if (First) { StartTick = tick; First = false; } // CurRatio(現在のトランジション進行度(0〜1.0))を求める CurRatio = (double)(tick-StartTick)/Time; CurRatio = (CurRatio < 0.0) ? 0.0 : (CurRatio > 1.0) ? 1.0 : CurRatio; return TJS_S_TRUE; } //--------------------------------------------------------------------------- tjs_error TJS_INTF_METHOD tTVPMyCFTransHandler::EndProcess() { // トランジションの画面更新一回分が終わるごとに呼ばれる if(CurRatio >= 1.0) return TJS_S_FALSE; // トランジション終了 return TJS_S_TRUE; // トランジション継続 } //--------------------------------------------------------------------------- tjs_error TJS_INTF_METHOD tTVPMyCFTransHandler::Process( tTVPDivisibleData *data) { // トランジション画像(分割されている場合はその各領域)ごとに呼ばれる // 吉里吉里は画面を更新するときにいくつかの領域に分割しながら処理する // ことがあり、このメソッドは画面更新一回につき複数回呼ばれる場合がある // data には領域や画像に関する情報が入っている if (CurRatio == 0.0) { // 一番最初の呼び出しなら、最初の画像(=data->Src1)にする data->Dest = data->Src1; data->DestLeft = data->Src1Left; data->DestTop = data->Src1Top; } else if (CurRatio >= 1.0) { // 一番最後の呼び出しなら、最後の画像(=data->Src2)にする data->Dest = data->Src2; data->DestLeft = data->Src2Left; data->DestTop = data->Src2Top; } else { //中間画像を作る Blend(data); } return TJS_S_OK; } //--------------------------------------------------------------------------- // CurRatioに応じて data->Src1 と data->Src2 をブレンドして data->Dest を作成する void tTVPMyCFTransHandler::Blend(tTVPDivisibleData *data) { // 中間画像作成先(=dest)、元画像(src1)、先画像(src2)のバッファアドレスを得る tjs_uint8 *dest; const tjs_uint8 *src1, *src2; data->Dest->GetScanLineForWrite(data->DestTop, (void**)&dest); data->Src1->GetScanLine(data->Src1Top, (const void**)&src1); data->Src2->GetScanLine(data->Src2Top, (const void**)&src2); // 各画像のピッチ(縦1ドット移動した時のメモリ上の移動バイト数)を得る tjs_int destpitch, src1pitch, src2pitch; data->Dest->GetPitchBytes(&destpitch); data->Src1->GetPitchBytes(&src1pitch); data->Src2->GetPitchBytes(&src2pitch); // 画像の分割状態に合わせ、dest/src1/src2の開始位置を変更する dest += data->DestLeft * sizeof(tjs_uint32); src1 += data->Src1Left * sizeof(tjs_uint32); src2 += data->Src2Left * sizeof(tjs_uint32); // data->Width x data->Height の画像データをブレンドする // 単純なクロスフェードならこう。他のトランジションならここがもっと複雑になる tjs_int h = data->Height; for (tjs_int y = 0; y < data->Height; y++) { for (tjs_int x = 0; x < data->Width; x++) { // 以下は非常に効率が悪いので注意。自前で作るときはもっと最適化してくだされ tjs_int idx = x*sizeof(tjs_uint32); dest[idx+0] = (tjs_uint32)(src1[idx+0]*(1.0-CurRatio) + src2[idx+0]*CurRatio); dest[idx+1] = (tjs_uint32)(src1[idx+1]*(1.0-CurRatio) + src2[idx+1]*CurRatio); dest[idx+2] = (tjs_uint32)(src1[idx+2]*(1.0-CurRatio) + src2[idx+2]*CurRatio); dest[idx+3] = (tjs_uint32)(src1[idx+3]*(1.0-CurRatio) + src2[idx+3]*CurRatio); } dest += destpitch; src1 += src1pitch; src2 += src2pitch; } } (トランジションハンドラプロバイダのクラス定義へ続く) |
ここまできたらラストスパート、あとはトランジションハンドラプロバイダを 登録・削除する RegisterMyCFTransHandlerProvider()と UnregisterMyCFTransHandlerProvider()を定義するのみ。 この二つは、static に定義してあるトランジションハンドラプロバイダの ポインタを使って作成したインスタンスを、TVPAddTransHandlerProvider()で 吉里吉里に登録、TVPRemoveTransHandlerProvider()で削除するだけだ。 参照カウンタがあるので、単純に delete できないことに注意。
(トランジションハンドラプロバイダのクラス定義からの続き) // 登録・削除の際に使用する静的変数 static class tTVPMyCFTransHandlerProvider *MyCFTransHandlerProvider; //--------------------------------------------------------------------------- void RegisterMyCFTransHandlerProvider() { // TVPAddTransHandlerProvider を使ってトランジションハンドラプロバイダを // 登録する MyCFTransHandlerProvider = new tTVPMyCFTransHandlerProvider(); TVPAddTransHandlerProvider(MyCFTransHandlerProvider); } //--------------------------------------------------------------------------- void UnregisterMyCFTransHandlerProvider() { // TVPRemoveTransHandlerProvider を使ってトランジションハンドラプロバイダを // 登録抹消する TVPRemoveTransHandlerProvider(MyCFTransHandlerProvider); MyCFTransHandlerProvider->Release(); } //--------------------------------------------------------------------------- |
DLLにバージョン入れたり作成者の名前入れたりするために、リソースファイルを 作っておこう。詳細は 吉里吉里プラグインの作り方の 『リソースファイルを書く』項に譲る。ここでは「こういうの書きました」 という例を貼っておく。
#include <winver.h> VS_VERSION_INFO VERSIONINFO FILEVERSION 0,1,0,0 PRODUCTVERSION 0,1,0,0 FILEFLAGSMASK VS_FFI_FILEFLAGSMASK FILEFLAGS 0x00000000L FILEOS VOS_NT_WINDOWS32 FILETYPE VFT_DLL FILESUBTYPE VFT2_UNKNOWN BEGIN BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x0409, 1200 END BLOCK "StringFileInfo" BEGIN BLOCK "040904b0" BEGIN VALUE "CompanyName", "KAICHO Soft" VALUE "FileDescription", "MyCF.dll for 吉里吉里2" VALUE "FileVersion", "0, 1, 0, 0\0" VALUE "InternalName", "MyCF\0" VALUE "LegalCopyright", "Copyright (C) 2012 KAICHO\0" VALUE "OriginalFilename", "MyCF.dll\0" VALUE "ProductName", "MyCF\0" VALUE "ProductVersion", "0, 1, 0, 0\0" VALUE "SpecialBuild", "\0" END END END // 0.1.0.0 新規作成 |
これで一通りクロスフェードトランジションができるトランジションプラグインが
完成した。buildして、first.ks の先頭くらいでPlugin.link('MyCF.dll')して、
確かに[trans method=MyCF time=2000][wt]とかで トランジションできることを
確認してほしい。
今回はレイヤモードによって処理を変える、のような高級なことは全然してない。
そのあたりはご容赦。吉里吉里デフォルトのクロスフェードトランジションでは
そういうことちゃんとやってるけどね!(ダメじゃん)。
最初に『思いのほかシンプル』って書いたけど、これだけ並べるとやっぱり やることに対して書かなきゃいけないことが多いね。もうちょっとシンプルに なるように、テンプレートでも書こうかしらん。
さて、こっちでも知ってることは全部書いた! 間違いがあれば是非正してくだされ。 公開→指摘→修正→再公開のループで、コンテンツがよりよくなることを 望むよ!
…を、ここに書いてたんだけど、長くなったので こちらに独立させた。