結果だけでなく過程も見てください

日々の奮闘を綴る日記です。

DirectXで円形ワイプエフェクト

言葉でどう表現していいかわからないのでタイトル名が正しいかわからないのですが、
要は下の図のようなエフェクトの作り方を説明します。

実際のゲームで使われているのは、スーパーマリオワールドのゴール後とか、
スーファミゼルダの伝説でダンジョンからフィールドに移動するとき、でわかりますかね?

ワイプエフェクトで使用する技術

方法は色々あると思いますが、ここではDirectXのステンシルバッファを使います。
ステンシルとは元々型抜き染めという意味があり、DirectXでは、オブジェクトの一部だけを描画させない、といったことを実現するために使用されます。

なんとなくわかってきたと思いますが、イメージとしては画面を黒一色で描画しているうち、
円形部分だけを描画しないようにし、さらにその円形の大きさを徐々に変えていけば今回のエフェクトが実現できそうですね?

ステンシルバッファについてはまるぺけさんのサイトが非常〜〜〜〜〜〜に
詳しい(自分もここで勉強しました)のでここでは説明しません。
http://marupeke296.com/DXG_No18_WhatIsStencilBuffer.html

ステンシルバッファの作り方

ステンシルバッファはDirect3DDevice(以下D3Dデバイス)作成時に行います。
D3DPRESENT_PARAMETERS構造体のEnableAutoDepthStencilをTRUEにし、
AutoDepthStencilFormatでステンシルバッファを使用するフォーマットを選択します(D3DFMT_D24S8など)

ただしハードウェアがステンシルバッファをサポートしていないときがあるので、事前にチェックする必要があります。
ここではめんどくさいのでハードウェアがステンシルバッファをサポートしているものとして話を進めます。

d3dppApp.EnableAutoDepthStencil  = TRUE;
d3dppApp.AutoDepthStencilFormat  = D3DFMT_D24S8;

これでデバイス作成時に、バックバッファと同じ大きさの1ドットあたり8bitのステンシルバッファが作成されました。
D24S8とあることからも、必ず深度バッファとセットで作られることがわかります。

ステンシルバッファの使い方

ステンシルバッファはバックバッファと同じ大きさで、1ドットあたり8bitの領域です。
この領域に格納されているデータによって、描画しろだとか、描画するなだとか、そういうことを指示することができます。

簡単な例を示すと、以下の図はステンシルバッファに格納されている値とします。

円とその内面が「1」となっており、それ以外は「0」です。
このとき1は描画するな、0は描画しろ、という決まりを指示しておけば
1の部分だけが型抜きされた状態で描画されるというわけです。
この状態で、画面を黒く塗りつぶすと、以下のようになるわけですね。
このゼロイチのことをマスクと呼びます。

イメージが沸いたところで、マスクの作り方を入りましょう。
まさか手動で全てのドットに対して0と1を設定するわけにもいきません。

まずステンシルバッファを全て0にクリアしたあと、"ステンシルバッファに対してだけ"描画をします。
描画できた範囲だけ「1」に設定する、という方法がDirectXに用意されているので、それを使えば簡単にマスクが生成できるでしょう。

DirectXに用意されている方法、ということでデバイスに対してSetRenderStateします。
上で"ステンシルバッファに対してだけ"描画をする、といいました。
これは今回の描画はステンシルバッファのマスクを作成するためだけに使い、
Zバッファなどの値を変化させたくないためです。
そのためZバッファの値を変化させないようにする設定も必要です。

IDirect3DDevice* pDev;  // 構築済とします
DWORD g_dwCurZTest = 0;
DWORD g_dwCurZFunc = 0;

void setBackStencilBufferMask()
{
  // ステンシルバッファだけを0クリアします
  pDev->Clear( 0, NULL, D3DCLEAR_STENCIL, 0, 1.0f, static_cast<DWORD>(0) );

  // Zバッファの設定を変更するので、現在の状態を保存しておく
  pDev->GetRenderState( D3DRS_ZENABLE, &g_dwCurZTest );
  pDev->GetRenderState( D3DRS_ZFUNC,   &g_dwCurZFunc );

  // Zバッファに書き込まないようにします
  pDev->SetRenderState( D3DRS_ZENABLE, true );
  pDev->SetRenderState( D3DRS_ZFUNC, D3DCMP_NEVER );

  // ステンシルバッファの設定です
  pDev->SetRenderState( D3DRS_STENCILENABLE, true );  // ステンシルバッファ有効
  pDev->SetRenderState( D3DRS_STENCILFUNC,   D3DCMP_ALWAYS ); // ステンシルテストは常に行う
  pDev->SetRenderState( D3DRS_STENCILPASS,  D3DSTENCILOP_REPLACE );
  pDev->SetRenderState( D3DRS_STENCILZFAIL,  D3DSTENCILOP_REPLACE );
  pDev->SetRenderState( D3DRS_STENCILREF,  0x01 );
  pDev->SetRenderState( D3DRS_STENCILMASK,   0xff );
}

D3DRS_STENCILFUNCの説明です。これは、以下で説明しますが、ステンシルテストと呼ばれるテストの挙動を決めるものです。
ステンシルバッファは1ドットあたりに情報が格納されています。上で言う0とか1とかですね。
そして描画時には、この1ドットあたりの情報を見て、1だからこのドットは描画しないんだな、とか
0だからこのドットは描画するんだな、と決めるわけです。これをステンシルテストといいます。

この「1だからこのドットは描画しないんだな」部分を決めるのがD3DRS_STENCILFUNCです。
今回はマスクの生成が目的なので、全てのピクセルデータを通す(描画する)ことにします。
そのため「D3DCMP_ALWAYS」としています。

いくつか紹介しておきます。
D3DCMP_EQUAL…基準値と同じ値ならば合格
D3DCMP_NOTEQUAL…基準値と異なる値ならば合格
D3DCMP_LESS…基準値よりも小さければ合格
D3DCMP_GREATER…基準値よりも大きければ合格
D3DCMP_NEVER…常に不合格
D3DCMP_ALWAYS…合格

D3DRS_STENCILPASS、D3DRS_STENCILZFAIL、D3DRS_STENCILREF,の説明です。
これは、ステンシルテストおよびZテスト(オブジェクトが複数描画されている場合、背面のものは描画しないようにするテスト)
で、それぞれ合格、不合格だったときの挙動を決めるものです。

D3DRS_STENCILPASS…ステンシルテストに合格したとき
D3DRS_STENCILZFAIL…ステンシルテストには合格したが、Zテストは不合格だったとき
D3DRS_STENCILFAIL…両方不合格だったとき

今回はステンシルテストに合格したときは「1」にしてマスクを作るのでした。
そのため、上2つの場合、D3DSTENCILOP_REPLACE(値の置き換え)を行うことにしています。
置き換える値は、D3DRS_STENCILREFで指定した値、つまり「1」です。

最後のD3DRS_STENCILMASKはステンシルバッファの1ドットのサイズ(8bit)を入れるようです。
そのため0xffを入れています。

さて、この設定を行った上で円を描画してください。
自分は128x128くらいの円の画像を読み込んで描画しています。(画像の描画については今回は割愛します)

CTaiyakiPicture pic( "128x128circle.dds" );

// マスク生成の設定
setBackStencilBufferMask();

// 円画像の描画
pic->setPos( getScreenWidth()/2, getScreenHeight()/2 );  // 画面中央がワイプの中心点
pic->onDraw();

それから、マスク生成時にこっそりとZテストの情報を取得していましたので、
それも戻してあげます。

pDev->SetRenderState( D3DRS_ZENABLE, g_dwCurZTest );
pDev->SetRenderState( D3DRS_ZFUNC,   g_dwCurZFunc );

型抜き描画

上でマスクを作成しました。
次は型抜きしたい画像の描画です。
これは、SetRenderStateでいくつか設定を行ったあと、
各人がいつも行っている方法で描画すれば、円の部分が型抜きされた形でレンダリングされます。

void setBackStencilBufferDraw()
{
  pDev->SetRenderState( D3DRS_STENCILENABLE, true );
  pDev->SetRenderState( D3DRS_STENCILFUNC, D3DCMP_NOTEQUAL );
  pDev->SetRenderState( D3DRS_STENCILPASS, D3DSTENCILOP_KEEP );
  pDev->SetRenderState( D3DRS_STENCILZFAIL, D3DSTENCILOP_KEEP );
  pDev->SetRenderState( D3DRS_STENCILREF, 0x01 );
  pDev->SetRenderState( D3DRS_STENCILMASK, 0xff );
}

D3DRS_STENCILFUNCは上で説明したとおり、基準値(D3DRS_STENCILREFの値、つまり1)
と異なれば描画するという指示です。

D3DRS_STENCILPASSとD3DRS_STENCILZFAILについては、ステンシルバッファに合格したピクセルについては、
そのままのピクセルデータの値(D3DSTENCILOP_KEEP)を通す、という意味になります。

それ以外のパラメータについてはマスク生成時と同じなので、説明は不要ですよね。

描画が終わったら、ステンシルテストを無効にしておきましょう。
無駄な処理をしないため、型抜きをするときだけ有効にした方が好ましいでしょう。

SetRenderState( D3DRS_STENCILENABLE, false );

ワイプさせよう

ここまでくれば、あとは円の大きさを変えるだけです。
自分の場合は、1000ミリ秒でワイプする、というように時間指定させています。
以下のようなワイプ開始関数を作っておくと良いでしょう。

bool      g_bWiping = false;
DWORD g_dwStartTime = 0;
DWORD g_dwWipeTime = 0;
CTaiyakiPicture g_circle;  // 画像ロード済
CTaiyakiPicture g_back;   // ロード済。画面を真っ黒にします。

int startWipeout( DWORD dwMilliSec )
{
  g_bWiping = true;
  g_dwStartTime = ::timeGetTime();
  g_dwWipeTime = dwMilliSec;
}

次に、現在時刻から進捗を出して、円を描画(マスク生成)を行います。g_dwWipeTimeだけ経過したら終了とします。

if ( g_bWiping )
{
  setBackStencilBufferMask();

  double lfElapse = ( (::timeGetTime() - g_dwStartTime) / (double)g_dwWipeTime );
  g_circle.setSize( 1200 * lfElapse, 1200 * lfElapse );
  g_circle.onDraw();
}

続いて黒背景を描画します。マスク部分は描画されません。

if ( g_bWiping )
{
  setBackStencilBufferDraw();
  g_back.onDraw();
}

続いて、ワイプの終了チェックです。

if ( (::timeGetTime() - g_dwStartTime) >= g_dwWipeTime )
{
  g_bWiping = false;
}

おわりに

これでワイプエフェクトの一通りの処理は終わりです。
今回はワイプアウトのみを説明しましたが、ワイプインする場合は進捗を0%→100%の方向ではなく
100%→0%の方向にすればよいです。

プライバシーポリシー お問い合わせ