ホーム < ゲームつくろー! < DirectX技術編

その54 ステンシルによるクリッピング描画


 画面の一部分にだけ描画したいという状況が生じることがあります。例えばレーシングゲームのバックミラーの映像とか、スクロールするメッセージウィンドウなどです。これには色々な方法が考えられるのですが、その中で割とシンプルで使いやすいのが「ステンシル」です。

 ステンシルはDirect3Dが持っている基本機能の一つで、スクリーンに点を打つか打たないかを直接決めることが出来ます。クリッピングに持って来いの性質ですね。本章ではステンシルを用いて画面の一部分だけを描画する方法を紹介します。尚、ステンシルについてはDirectX技術編その18「ステンシルバッファって?」でも軽く触れていますが、本章でももう一度おさらいします。



@ 点を打つのも大変です

 Direct3Dが画面に点を打つまでには、長い長い過程を経ます。プログラマがIDirect3DDevice9::DrawPrimitiveメソッドやID3DXMesh::DrawSubsetメソッドのようないわゆる描画メソッドを呼び出すと、Direct3Dは描画を開始します。描画の最初の入り口は頂点シェーダです。ここではポリゴンとカメラの情報を元に「スクリーンのどの領域に点を打つか」という打ち込み範囲(三角形範囲)を決定します。次に訪れるのがピクセルシェーダ(注:DirectX9の場合)。ここでは先に決まった範囲内のある一点についてその色を決めます。基本的にはこの2つのシェーダを通るとスクリーンの1点に打つ点が決定しますが、この先には「本当にその点を打つの?」というテストが2つ入ります。

 ピクセルシェーダ後に来る最初のテストは「ステンシルテスト」です。これはスクリーンと同じ大きさの「マスク」があって、打ち込もうとしている点がそのマスクで覆われているか否かで打点の可否を判断します。美術をやったことのある人なら「マスキングテープ」、Photoshopを持っている人ならば「選択範囲」と同じです。マスクに隠されている点は見えないので、打たずに破棄してしまいます。

 続いて行われるテストはZテストです。これは打とうとしている点がオブジェクトの背後に隠れているかどうか判断するテスト、つまり「陰面処理」を行うテストです。背後に隠れている点は見えないので打つ必要がありません。

 頂点シェーダからはじまるこのいくつもの試練を乗り越えて、スクリーンには点が打たれます。点を打つのも大変ですね。

 本章でターゲットにしているステンシルテストは、ピクセルシェーダの後、そしてZテストの前に行われます。この位置関係が大切です。



A ステンシル値は生成してから使う

 ステンシルテストは「マスク」だと説明しました。このマスク生成がステンシルのほぼすべてと言っても過言ではありません。「マスクって画像を用意するの?」とステンシルを始めて知った時に私は思いましたが、そうではありません。ステンシルは毎フレームポリゴンを描画する事で生成される動的で特殊な絵です。このマスクですが、今後混乱を避けるために「ステンシル値」と呼ぶ事にします。

 ピクセルシェーダから点がやって来ると、ステンシルは同じ座標にあるステンシル値と「参照値」と呼ばれる別の値とを比較します。比較の結果通して良い(合格:Pass)か通しては駄目(不合格:Fail)が決まります。この比較の事を「ステンシルテスト」と呼びます。

 「なるほど、でも肝心のステンシル値はいつどうやって用意するの?」と感じた方はすばらしい。ここが大切な部分です。先のステンシルテストで点に対して合格もしくは不合格が決まります。この後、ステンシルテストでは「合格した時にステンシル値を書き換える」及び「不合格した時にステンシル値を書き換える」という書き換え作業が入ります。ステンシル値を変更できるのはこのタイミングです。つまり、ステンシル値の生成はステンシルテスト自体で行うんです。これが意味する所は、「ステンシルは『ステンシル値の生成』と『ステンシル値の比較による合否判定』の2段階に分かれる」という事です。



B ステンシル値でマスクを作成しよう

 ではここからステンシルをクリッピング描画に絞って使っていくことにしましょう。

 まずステンシルを使うには「ステンシルバッファ」を作成します。これはデバイスを作成する時に「深度ステンシルバッファフォーマット」を指定することで作成されます。具体的にはD3DPRESENT_PARAMETERS構造体のAutoDepthStencilFormatメンバに画像フォーマットを指定します。指定可能なフォーマットは次の通りです:


・ D3DFMT_D15S1
・ D3DFMT_D24X4S4
・ D3DFMT_D24S8
・ D3DFMT_D24FS8

 「S」と書かれているのがステンシル(Stencil)を意味し、その後ろの整数はステンシル値を表現するビット数を表します。例えばD3DFMT_D24S8の場合、ステンシル値は8bit、すなわち0〜255の整数で表現される事になります。一般的にはD3DFMT_D24S8が良く使用されているように思います。ここからわかるのが、ステンシルバッファは常に深度バッファと一心同体であるという事です。

 デバイスにステンシルバッファを設定したら、最初にステンシルバッファをクリアします。これはお馴染みのIDirect3DDevice9::Clearメソッドを用います。このメソッドではカラーバッファ(バックバッファ)、深度バッファそしてステンシルバッファの3つをクリアできますが、ステンシルバッファのみクリアするには、

ステンシルバッファのクリア
pDev->Clear( 0, NULL, D3DCLEAR_STENCIL, 0, 1.0f, 0 );

と第3引数にD3DCLEAR_STENCILフラグのみを、そして第6引数にクリアする整数値を指定します。ステンシルバッファが綺麗になったら、続いてステンシル値の生成に映ります。2段階あると言った作業の第1段階です。


 今私たちがやりたい事は、マスクとなるステンシル値を生成する事です。マスクの役目をするには「0」もしくは「1」の2値で十分ですよね。よってここからは「0」が透明、つまり透過して点を打つ、そして「1」が不透明で点を打たない事を意味するとします。

 ステンシル値はポリゴンを描画してステンシルテストにパスした時に更新されるようにします。まずはその設定をご覧下さい:

ステンシル値生成用のステンシル関連設定
// ステンシル設定
pDev->SetRenderState( D3DRS_STENCILENABLE, true );
pDev->SetRenderState( D3DRS_STENCILFUNC,   D3DCMP_ALWAYS );
pDev->SetRenderState( D3DRS_STENCILREF,    0x01 );
pDev->SetRenderState( D3DRS_STENCILPASS,   D3DSTENCILOP_REPLACE );
pDev->SetRenderState( D3DRS_STENCILZFAIL,  D3DSTENCILOP_REPLACE );
pDev->SetRenderState( D3DRS_STENCILMASK,   0xff );

// Z設定
pDev->SetRenderState( D3DRS_ZENABLE, true );
pDev->SetRenderState( D3DRS_ZFUNC,   D3DCMP_NEVER );

 ステンシルの設定はすべてIDirect3DDevice9::SetRenderStateメソッドを通して行います。
 まずD3DRS_STENCILENABLEでステンシルテストを有効(使用)にしています。
 ステンシル値の生成に当たり、以後のポリゴンの書き込みを全部有効にしたいと考えました。そこでD3DRS_STENCILEFUNC(比較関数の選択をするフラグ)をD3DCMP_ALWAYS(すべて合格)に設定しています。
 描いた物は全部合格、ステンシル値が更新されます。その更新する値はD3DRS_STENCILREF(参照値設定)で1(不透明)とします。
 テストに合格した後の書き込みについてはD3DRS_STENCILPASSでD3DSTENCILOP_REPLACE(置き換え)に設定しています。置き換えられる値は先に設定した参照値の1です。
 もう1つ大切な設定がD3DRS_STENCILEZFAILです。これはステンシルテストに合格したけれどZテストに失敗した場合にステンシル値をどうするかという挙動を決めます。今回はZテストをすべて「不合格」とすることにしました。つまり、ステンシル値生成用のポリゴンは何一つ画面に描かれません。でも、ステンシル値は更新して欲しいので、D3DSTENCILOP_REPLACEになっています。
 D3DRS_STENCILMASKは現在のステンシル値とAND演算する値です。今回はそのままステンシル値を使いたいので0xffとマスク無しの状態にしました。

 Zテストについても設定します。マスク用の描画が実際に描かれるのは嫌なので、Zテストはすべて失敗させます。D3DRS_ZFUNCをD3DCMP_NEVER(全失敗)に設定すればOKです。


 上の設定をすると、以後のポリゴン描画はすべてステンシル値の更新(0を1にする更新)のためだけに使われます。



C マスクで打点を制限

 ステンシルバッファにマスクとなるステンシル値を刻印できたら、続いてクリッピング描画を行います。今ステンシルバッファは0か1で埋め尽くされているはずです。そして、0が透明、1が不透明と約束しました。この事から「ステンシル値が0だったら合格」するようにレンダリングステートを設定すればOKです:

クリッピング描画用のステンシル関連設定
pDev->SetRenderState( D3DRS_STENCILENABLE, true );
pDev->SetRenderState( D3DRS_STENCILFUNC,   D3DCMP_EQUAL );
pDev->SetRenderState( D3DRS_STENCILREF,    0x00 );
pDev->SetRenderState( D3DRS_STENCILPASS,   D3DSTENCILOP_KEEP );
pDev->SetRenderState( D3DRS_STENCILZFAIL,  D3DSTENCILOP_KEEP );
pDev->SetRenderState( D3DRS_STENCILMASK,   0xff );

pDev->SetRenderState( D3DRS_ZENABLE, true );
pDev->SetRenderState( D3DRS_ZFUNC,   D3DCMP_LESSEQUAL );

 比較関数は「等しかったら」を表すD3DCMP_EQULにします。比較する参照値は「0」です。ステンシルテストに合格してもステンシル値の更新はしないのでD3DSTENCILOP_KEEP(値をそのまま)です。Zテストは再開しますが、Zテストの合否にかかわらずやはりステンシル値の更新はしません。

 この設定をすると、ステンシルバッファの0の所だけが描画許可されます。これでマスク描画の出来上がりです。

 マスク描画をした後は、ステンシルバッファをもう一度綺麗にクリアした方が良いかもしれません。また、ステンシルテスト自体もOFFにします:

後片付け
pDev->SetRenderState( D3DRS_STENCILENABLE, false );
pDev->Clear( 0, NULL, D3DCLEAR_STENCIL, 0, 1.0f, 0 );



D クリッピング描画支援クラス

 せっかくですから、ここまでの説明を踏まえたクリッピング描画支援クラスを作りました。クラスはこちらからダウンロードできます。このクラスは例えば次のように使います:

クリッピング描画支援クラスの使い方
StencilClip clip;

// ステンシル値作成
clip.regionBegin( pDev ); // 作成開始
{
   // マスクを描画する場所
}
clip.regionEnd(); // 作成終了


// クリッピング描画
clip.drawBegin(); // 描画開始
{
   // クリッピング描画されます
}
clip.drawEnd(); // 描画終了


 StencilClipクラスが支援クラスです。まずStencilClip::regionBeginメソッドとStencilClip::regionEndメソッドの間で描画をするとステンシル値(マスク)を作成します。続いてStencilClip::drawBeginメソッドとStencilClip::drawEndメソッドの間で描画すると、先に作成したマスク部分を考慮したクリッピング描画になります。clipEndメソッドを呼んだ段階でステンシルバッファはクリアされます。

 マスクとして使用する色(値)と描画時の判定色は個別に設定できます:

マスク色の指定
// マスク色を指定
clip.setWriteMaskColor( Dix::StencilClip::MaskColor_Fill );

// マスク描画
clip.regionBegin( g_pD3DDev, Dix::StencilClip::MaskColor_Trans );
{
   // マスク描画
}
clip.regionEnd();

// マスクする色を指定
clip.setRefMaskColor( Dix::StencilClip::MaskColor_Fill );

// クリッピング描画
clip.drawBegin();
{
   // クリッピング描画
}
clip.drawEnd();


 setWriteMaskColorメソッドで書き込み時のマスクカラーを、setRefMaskColorメソッドで描画時のマスク色を指定できます。フラグはクラスの中に列挙型として定義されています。

マスクカラー
class StencilClip {
public:
   // マスク色
   enum MaskColor {
      MaskColor_Trans = 0x00, //! 透明色
      MaskColor_Fill = 0x01,  //! 塗りつぶし
      MaskColor_None = 0xff   //! 無効カラー
   };
};


 このクラスを用いたクリッピング描画のサンプルを作成しました。結構気軽にクリッピングできますのでお試し下さい。