スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

C++のクラスとLuaの連携 with Direct3D

今回のエントリは、瀕死のようで実は生きているLua(version 5.2.1)をC++で書かれているホストプログラムに組み込み、外部で用意したLuaのスクリプトからホストを制御できるようにするまでをできるだけ詳細に綴ったものです。ちなみに、with Direct3D9と書いてはありますが微妙な注意点以外はほとんど出てきません。
つまりは、「C++&Direct3Dでコンテンツを作るっているんだけど、Luaでスクリプティングするにはどうすればいいの?」という私のようなLua初心者のための忘備録です。

……では、本題。ウェブページや参考書を繙き々々実装したものなので、間違っていたらすいません。
以下ではホスト(C++)スクリプト(Lua)というふうに色分けします。

§0  環境
OS:Windows 7
IDE:VisualStudio 2010
ホスト:C++
スクリプト:Lua 5.2.1
その他のライブラリ:Direct3D9

あくまで私の場合ですが、参考までに。

§1  導入
Windows用のインクルードファイルとライブラリがセットで落ちているので、まずはこれを落としてパスを通します。ちなみに私の場合、初めはDLLでやっていましたがエントリポイント云々で怒られて面倒になったので、LIBですべてやってしまうことにしました。

§2  動作確認(Direct3D併用のための)
パスが通り、Luaがホストで叩ける状態になっているか確認します。以下はDirect3D9に関する初期化がすべて終わった状態で行っているものとします。lua_dofile関数は外部に用意したLuaファイルを実行するものなので、無視でも可。
lua_State* L=luaL_newstate();
luaL_openlibs(L);
luaL_dofile(L,"xxxxxx.lua");
lua_close(L);
これで何も起きなかった人、あるいはそもそもDirect3D系を使っていない人はスルーしてOKです。
ここでLuaとDirect3Dを併用しているとランタイムライブラリエラーということで落ちてしまう場合があります。Luaの公式ドキュメントによるとこれはLuaのバグではなくて、Luaが依存しているABI絡みでDirect3D側がバイオレーションを起しているかららしいです。
これを直すのは簡単で、FPUに関するDirect3Dデバイス生成のオプションフラグ(BehaviorFlags)にD3DCREATE_FPU_PRESERVEを合わせてセットして解決できます。Direct3Dに関するお話はこれでおしまい。

§3  コーディングとプログラムの流れ

前置きは終わりにして、ここからは実装についてです。
プログラムの流れは、おおよそ上の図のようになります。Lua側からホストのメソッドなりクラスなりをいじれる or ホスト側から何かを受け取ってLuaに渡せるようなグルー関数をLuaステート内のスタックを使って実装し、それをLua側から呼んでやるということですね。
それではまず、具体的に何をしたいか考えたものを以下に列挙します。
  1. C++で実装されたクラス・メンバ変数・メンバ関数をLuaから自由に操作したい
    - ホスト側の参照可能スコープを守る範囲で
  2. Luaでスクリプティングするときは、できるだけC++やJavaライクにメンバ関数を呼び出したい
    - obj.method()のように
  3. toluaやluabindなどの支援ツールを使わない
    - 使おうとしましたがうまくグルーコードを吐きだしてくれなくて挫折しました
    - どちらもLua-5.1準拠なのでそもそもアヤシイですが……

§4  実装
左のクラス図(テキトー)のようなクラスをホスト側で定義し、その生成から関数の呼び出し、最後はオブジェクトの破棄までをすべてスクリプト側から行えるように実装します。
関数はコンストラクタ・デストラクタ、それにアクセサだけとなっており、getValue関数では戻り値が複数個存在するのがミソです。次節からは、各関数に対して以下のようなグルー関数を実装する前提で話を進めます。getValue関数が2種類あることに留意してください。なお、引数・戻り値はLuaの規約に従いすべてint xxxxxx(lua_State*)です。
  • lua_TestClass
  • lua_TestClass_delete
  • lua_TestClass_setValue
  • lua_TestClass_getValueT
  • lua_TestClass_getValueE

§4.1 ホスト側のクラスの実装
≪クラスの定義≫
class TestClass
{
private:
double valueA,valueB;
public:
TestClass();
~TestClass();
void setValue(double argA,double argB);
void getValue(double* dst);
};
≪クラスの実装≫
TestClass::TestClass()
:valueA(0),valueB(0)
{
}

TestClass::~TestClass()
{
}

void TestClass::setValue(double argA,double argB)
{
this->valueA=argA;
this->valueB=argB;
}

void TestClass::getValue(double* dst)
{
dst[0]=this->valueA;
dst[1]=this->valueB;
}

§4.2 インタフェースを準備する
まずは、Luaとホストを橋渡しするためにlua_TestClass関数を定義します。この関数にはTestClassクラスのインスタンスを返してくれ、なおかつ必要な関数の登録などをすべてやってくれるコンストラクタの役割を持たせています。
lua_register(L,"TestClass",lua_TestClass);
定義を見るとわかりますが、これはマクロです。これをホストで記述することによって、Lのグローバル変数TestClassにlua_TestClass関数のポインタをセットしたことになります。ちなみに、プッシュしてポップしているためにこの処理の後のスタックは空です。
これで、Lua側にTestClass()と記述するとホストのlua_TestClass関数がコールされるようになりました。

§4.3 C++クラス&メンバ関数をLua側に公開する
それでは次に、lua_TestClass関数を詳しく見ます。
int lua_TestClass(lua_State* L)
{
// TextClassオブジェクトをメモリアロケートし,ユーザデータ型としてポインタをプッシュ
TestClass** object=(TestClass**)lua_newuserdata(L,sizeof(TestClass*));
*object=new TestClass();

// 関数を登録するためのテーブルとメタテーブルのためのテーブルをプッシュ
lua_newtable(L);
lua_newtable(L);

// テーブルに関数を登録(フィールド:関数名,値:関数ポインタ)
lua_pushcfunction(L,lua_TestClass_delete);
lua_setfield(L,3,"delete");
lua_pushcfunction(L,lua_TestClass_setValue);
lua_setfield(L,3,"setValue");
lua_pushcfunction(L,lua_TestClass_getValueT);
lua_setfield(L,3,"getValueT");
lua_pushcfunction(L,lua_TestClass_getValueE);
lua_setfield(L,3,"getValueE");

// 関数を登録したテーブルをユーザデータ型のメタテーブルとしてセット
lua_setfield(L,2,"__index");
lua_setmetatable(L,1);

// スタックトップのオブジェクト1個(ユーザデータ型)をリターン
return 1;
}
ここが勘所です。テーブルを使ってグルー関数を登録するだけでなく、グルー関数群が定義されたテーブルをユーザデータ型(TestClassオブジェクト)のメタテーブルに指定することで§3で書いたようなメソッドコール方式を実現しています。
最後に戻り値ですが、上の処理が終わった段階でグローバルなコンストラクタのほかはスタックにクラスのポインタが載っているだけになります。戻り値1個(return 1)でこのユーザデータ型がスクリプト側に戻るため、ホストで
TestClass* object=new TestClass();
と記述するのと同じ感覚でobjectを扱えるようになります。(※追記あり)
処理手順を追いやすいように、このときのスタックの動きを以下に図示します。



スタックにインスタンス化したTestClassオブジェクトのポインタをプッシュし、関数登録用のテーブルとメタテーブル用のテーブルをプッシュします。赤が前者、青が後者のテーブルです。



Lua側からコールしたいメソッド名をキーとして、テーブルにグルー関数のポインタを登録します。



メタテーブルに設定するためのテーブルの__indexフィールドに、グルー関数が登録されたテーブルを登録します。



ユーザデータ型にメタテーブルを登録します。

これで登録した関数がメタメソッドとしてユーザデータ型に関連付けられました。
一連の流れはうまくLuaのメタテーブルを利用しているところで、テーブルはテーブルごとにメタテーブルを持つことができるという特性を活かしています。
つまり、あるデータ型のメタテーブルに__indexフィールドがあり、かつそのフィールドにテーブルが登録されている場合はさらに登録先のテーブルの中も探索するというものです。これにより、ユーザデータにメソッドコールするとテーブルを辿って登録された関数名で直接呼び出せるというわけです。
ユーザデータも型に対して1つではなく、それぞれのデータごとにメタテーブルを持つことができます。ですから、同じ型のオブジェクトでも別々にメンバを扱うことができるということですね。

§4.4 その他の関数
残りのアクセサ・デストラクタのグルー関数を実装します。
int lua_TestClass_delete(lua_State* L)
{
TestClass* object=*(TestClass**)lua_touserdata(L,1);
delete object;
return 0;
}

int lua_TestClass_setValue(lua_State* L)
{
TestClass* object=*(TestClass**)lua_touserdata(L,1);
object->setValue(lua_tonumber(L,-2),lua_tonumber(L,-1));
return 0;
}

int lua_TestClass_getValueT(lua_State* L)
{
TestClass* object=*(TestClass**)lua_touserdata(L,1);
double temp[]={0,0};
object->getValue(temp);
lua_newtable(L);
lua_pushnumber(L,temp[0]);
lua_pushnumber(L,temp[1]);
lua_setfield(L,2,"valueB");
lua_setfield(L,2,"valueA");
return 1;
}

int lua_TestClass_getValueE(lua_State* L)
{
TestClass* object=*(TestClass**)lua_touserdata(L,1);
double temp[]={0,0};
object->getValue(temp);
lua_pushnumber(L,temp[0]);
lua_pushnumber(L,temp[1]);
return 2;
}
各メソッドを実装しました。必要に応じて戻り値の数が変わっていることに注意してください。getValue関数はLua側の記述を比較するため、新しいテーブルを作ってリターンするもの(newした配列のポインタを返すイメージ)と、順番に値をスタックに積んでそのまま返すものを実装しました。
また、このプログラムでは関数が呼ばれたときに必ずスタックのインデクス1にオブジェクトのポインタが入っている前提になっていることも見て取れると思います。
ちなみに、関数が呼ばれるたびにスタックには引数がプッシュされますが、チャンク(実行単位)ごとに自動的にスタックはクリアされているようなので、ゴミが溜まってインデクスが狂う心配はないようです。

チョット余談ですが、資料によると、そこまでクリティカルではないにしろテーブルの利用は速度の観点からしてできるだけ避けたほうがよいようです。グローバル変数に要素をガシガシ登録していくのも同じ観点からあまりよくないそうで、これは連想配列の検索時間が増えることを考えると当然でしょうか。

※追記
上記に関連して、Lua-5.2ではできるだけグローバル領域を汚さないことに配慮した仕様変更がなされたらしく、いくつか関数が追加されたようです。
これにより、関数をひとつひとつ登録していた部分(§4.3のサンプルコードのスタック番号3の部分)を構造体を使って一気に設定することができるようになりました。その例を示します。
	luaL_Reg tagTestClass[]=
{
{"delete",lua_TestClass_delete},
{"getValueT",lua_TestClass_getValueT},
{"getValueE",lua_TestClass_getValueE},
{"setValue",lua_TestClass_setValue},
{NULL,NULL}
};
luaL_setfuncs(L,tagTestClass,0);
関数名とポインタをセットで登録したluaL_Reg構造体の配列を用意し、その配列をluaL_setfuncs関数でスタックトップのテーブルにすべて登録します。なお、一番後ろの要素は番兵です。記述方法が違うだけですので、置き換えても問題なく動くはずです。


§4.5 Lua
ホスト側の実装を考慮して、LuaでTestClassをハンドルするコードは以下のようになります。といっても、今回のものはホスト側との連携がちゃんととれているかの確認を行うためのものでしかありません。
local obj=TestClass()
obj:setValue(100.0,200.0)
local temp=obj:getValueT()
obj:setValue(temp.valueA+150.0,temp.valueB-40.0)
local vA,vB=obj:getValueE()
obj:setValue(vA+300.0,vB+500.0);
obj:delete()
全ての関数はスタックのインデクス1にTestClassオブジェクトが入っている前提で実装してあるので、第1引数にオブジェクト自身をとるシンタックスシュガーを利用して関数を呼んでいることに注意してください。換言すると、以下のように関数を呼んでいることと同じになります。
【例】 obj.setValue(obj,100.0,200.0)

§5  出力実験
全て実装したうえで、コンソールアプリケーションに移し替えたものを動かして出力を確認してみます。Luaからオブジェクトの生成と破棄、関数が呼ばれるたびにvalueAとvalueBの変化をチェックし、まとめて表示したものが左の図になります。確認の方法がショボイのは汗顔の至りですが、複数の戻り値や即値計算を混ぜた引数などもちゃんと機能しているようです。

§6  ソースコード
コンソールアプリケーション向けに一部書き換えたものを置いておきます。
embeddedLua.zip

§7  リファレンス
Lua 5.2 リファレンスマニュアル
スクリプト言語による効率的ゲーム開発
空想具現化プログラミング
マルペケつくろーどっとこむ
こたつつきみかん
ずぼらのプログラム入門
karetta.jp

以下、後記。
これでLuaとC++を連携させることができました。まことに重畳です。
しかしまぁ、tolua++やluabindの導入失敗から完成まで、えらい苦労をした気がします。Direct3Dとの併用でいきなりクラッシュする問題から始まったわけですが、やはりLuaはデータの受け渡しにスタックしか使わないのが最大の利点であり欠点であるからだと思います。
1ステップごとにスタックがどう変わっているのかがあまりわからなかったため、プリント用紙にスタックの図を描き、それでも初心者の私にはイマイチわからなくて「キーーーーッッッッ!!」とか喚きながらなんとか組み上げました。それ故に、このエントリでは一部ですがスタックの遷移図を入れたのです(笑)。
スポンサーサイト

Direct3Dのテクスチャフィルタリング

大した記事ではないですが、Direct3Dでテクスチャマッピングされたポリゴンを描画するときに用いるフィルタ(補間手法)について忘備録を兼ねてちょっとだけ。

2Dのコンテンツを作るにしても3Dのコンテンツを作るにしても、画像をくるくる回したり、拡大縮小して表示する機会はきっとたくさんあるはず。ガチガチの3Dで描画するならまだしも、例えばノベルゲームのように絵の綺麗さや演出がウェイトを占めているようなコンテンツだと、綺麗に描画できないと死活問題になりかねないと思います。
なぜ今更こんな記事を書いたかというと、実は私自身がこれで随分悩んだからです。探したページが悪かったのか、キーワードが悪かったのか、はたまた参考書が悪かったのか運が悪かったのか……。

さて、本題。
フィルタリングといっても、ごく普通にDirect3Dのインタフェースとデバイスを初期化して描画に取り掛かるだけなのですが、デバイスにサンプリングステートをセットしてやるのを忘れるとラスタライズされた結果は以下の図のようになります。
いかにもニアレストネイバーっぽいジャギジャギ具合です。
等倍かつ回転無しの場合はPixeltoPixelできれいに表示されていますが、それ以外の場合は使えません。
これを直すためのコードはとても簡単で、SetSamplerStateというデバイスメソッドで下のように対応するステージとフィルタにフィルタリングモードを渡してやるだけ。列挙型によると、ちゃんと異方性フィルタリングなど色々な手法が準備されているようですが、完全な2Dなら平行投影なので拡大縮小ともにバイリニアで十分でしょう。もちろんシェーダでも記述できます。
// ホスト側でかくとこんなかんじ
pD3DDevice->SetSamplerState(0,D3DSAMP_MAGFILTER,D3DTEXF_LINEAR);
pD3DDevice->SetSamplerState(0,D3DSAMP_MINFILTER,D3DTEXF_LINEAR);

// シェーダ側でかくとこんなかんじ
sampler_state
{
MinFilter=LINEAR;
MagFilter=LINEAR;
};
というわけで、バイリニアで上の画像を描画したものが左図です。
ちゃんと拡大・回転したものはあらまほしい描画になっていることがわかります。ただ、当然ながら等倍の画像に対してもバイリニアでフィルタリングするので、微妙にボケが生じます。であるから、等倍表示の時はフィルタを切っておくのが無難だと思います。縮小したものについてはモアレが生じていますが、これはそうなってもしようがない元画像なので気にしません。ポリゴンにはアンチエイリアスがかからないので、袿の部分だけジャギーが出ているのも見て取れます。
結論。
2Dで何か作る&きれいに表示したいときは、動的に画像を弄るのではなくてできるだけ静的な画像を用意する。ヌルヌル動く回転などは……まあ、仕方がない。

以下、余談。
このメソッド、プログラマブルシェーダに手を付けるまで恥ずかしながら私は存在を知りませんでした。既に書きましたが、それのおかげでサンプリング手法をどのフラグで弄れるのかなど無駄に悩んだ感があります。2Dでゲームを作っている手前、ノベルゲームではないにしろきれいな補間はぜひ欲しいところです。
これにまつわるうれしいような悲しいようなニヤニヤしちゃうような四方山話が一つあって、ある市販のノベルゲームをやった時、キャラクタが主人公ににじり寄ってくる(立ち絵が拡大される)演出がありました。で、サンプラがちゃんと設定されていなかったらしく、キャラ絵が見事にドット絵と化したわけです。
今なら解決の手段を知っているし、これぐらいならテストプレイの時に潰せるでしょう。ですが、やっぱり開発者として知らないということは恐ろしいなと感じてゾゾーッとした一瞬でした。
sidetitlePROFILEsidetitle
AUTHOR: かいのしずく


詳しい自己紹介ページ
本棚(ブクログ)
平成20年以降にチェックしたのすべての本
本棚(読書メーター)
各月の読了ページ数管理用
チームカチューシャ
同人ゲームサークル MATRICES

sidetitleINFORMATIONsidetitle
VISITORS
CALENDAR
11 | 2012/12 | 01
- - - - - - 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 - - - - -
SEARCH
RECENT ENTRIES
CATEGORIES
ARCHIVES
sidetitleLINKsidetitle
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。