ステートオブジェクトとエフェクトファイル内でのレンダーステート

例えばXNA Game Studio 3.1で以下のコードがあったとします。

 // ゲームシーンの描画

GraphicsDevice.RenderState.DepthBufferEnable = true;
DrawGameScene();
// ブルーム部分の描画
DrawBloom();
// DrawBloom内で使っているシェーダー内で深度バッファを無効にしているので

// 再び有効にする
GraphicsDevice.RenderState.DepthBufferEnable = true;

このコードXNA Game Studio 4.0用に書き換えると、以下のようになると思います。

 // ゲームシーンの描画
GraphicsDevice.DepthStencilState = DepthStencilState.Default; 
DrawGameScene();
// ブルーム部分の描画
DrawBloom();
// DrawBloom内で使っているシェーダー内で深度バッファを無効にしているので

// 再び有効にする
GraphicsDevice.DepthStencilState = DepthStencilState.Default; 

このプログラムを実行すると、何故かゲームシーンの描画が深度バッファが無効になっているように見えてしまいます。

何故でしょうか?

レンダーステート設定回数を少なくする仕組み

XNA Game Studio 4.0以前は、レンダーステートのプロパティへ設定した値は以前と同じ値であっても直接デバイスへ設定するようになっていました。ですから、3.1で以下のコードを書いた場合、デバイスに同じ値を5回設定することになります。

     GraphicsDevice.RenderState.DepthBufferEnable = true;    // デバイスへ設定される
    GraphicsDevice.RenderState.DepthBufferEnable = true;    // デバイスへ設定される
    GraphicsDevice.DrawPrimitives();                        // 描画
    GraphicsDevice.RenderState.DepthBufferEnable = true;    // デバイスへ設定される
    GraphicsDevice.RenderState.DepthBufferEnable = true;    // デバイスへ設定される
    GraphicsDevice.RenderState.DepthBufferEnable = true;    // デバイスへ設定される
    GraphicsDevice.DrawPrimitives();                        // 描画

4.0ではステートオブジェクトが導入され、複数のレンダーステートを一気に変更することが出来るようになりました。ですが、4.0以前と同じ振る舞いにすると内部でのレンダーステートの設定が極端に多くなってしまい、パフォーマンスへの影響が大きくなってしまいます。例えば、BlendStateオブジェクトの中には12個のレンダーステートがあるので、BlendStateの設定を5回繰り返すと内部では60回ものレンダーステート変更が起きてしまいます。

そこで4.0では以下のルールによって実際にデバイスへの設定が行われます。

  1. ステートオブジェクトとして設定された値は一旦内部キャッシュへと格納される
  2. DrawPrimitive等の描画命令内では内部キャッシュに設定されたレンダーステートをデバイスへ設定する
  3. ただし、内部キャッシュのステートオブジェクトが以前にデバイスへ設定されたものと同じ場合はデバイスへの設定は行われない
  4. レンダーステートが同じかどうかは単にインスタンスが同じかどうかを調べるだけ

ですから、以下のコードを実行した場合、実際にデバイスへ設定されるのは1回になります。

     GraphicsDevice.DepthStencilState = BlendStae.Opaque;    // 内部キャッシュへ設定される
    GraphicsDevice.DepthStencilState = BlendStae.Opaque;    // 内部キャッシュへ設定される
    GraphicsDevice.DrawPrimitives();                        // 描画(デバイスへ設定される)
    GraphicsDevice.DepthStencilState = BlendStae.Opaque;    // 内部キャッシュへ設定される
    GraphicsDevice.DepthStencilState = BlendStae.Opaque;    // 内部キャッシュへ設定される
    GraphicsDevice.DepthStencilState = BlendStae.Opaque;    // 内部キャッシュへ設定される
    GraphicsDevice.DrawPrimitives();                        // 描画(デバイスへは設定されない)

この仕組みがあるので、デバイスに対してのステート設定の数を最小限に抑えることができ、パフォーマンス向上につながります。

Effect内のレンダーステート変更はステートオブジェクトを変更しない

ですが、この仕組みの副作用として出てきたのが、エフェクトファイル内でレンダーステート変更する記述をすると、その変更はステートオブジェクトとして反映されないという問題です。これはXNAフレームワーク内のステート管理と、Effect内のステート管理する部分が別々にあることが原因になっています。

ですから、冒頭のコードの場合、

  1. DepthStencilState.Defaultを設定、以前のステートオブジェクトと違うのでデバイスへ設定される
  2. ゲームシーンの描画
  3. Effectを使った描画。シェーダー内でZEnableを指定して深度バッファを無効に設定しているので、その設定は直接デバイスへと送られる
  4. DepthStencilState.Defaultを再び設定。以前のステートオブジェクトと同じなのでデバイスへは設定されない
  5. デバイスの深度バッファ設定は無効のままになってしまう

と、なってしまいます。

 

この問題を解決するには二つの方法が考えられます。

  1. エフェクトファイル内でレンダーステートの設定をするのではなく、ステートオブジェクトを使う
  2. エフェクトファイル内で変更したレンダーステートを元に戻すためのステートオブジェクトを作る(別インスタンスが必要)

判りやすさ、扱いのし易さから考えると1を奨励します。2はエフェクト内でどのステートを変更しているのかしっかりと把握する必要があり、ミスも多くなるので注意する必要があります。