SpriteBatchクラス その2

SpriteBatch.Beginはレンダーステートを変更する

SpriteBatch.Beginメソッドにはブレンドモード、ソートモード、そしてステートモードの3つ引数を渡すメソッド以外に、引数を省略できる2つのオーバーライドがあります。引数を省略した場合、ブレンドモードはSpriteBlendMode.AlphaBlend、ソートモードはSpriteSortMode.Deferred、そしてステートモードはSaveStateMode.Noneとなります。

ここで重要なのはステートモードがSaveStateMode.Noneということで、SpriteBatch.Begin() 、またはSpriteBatch.Begin(SpriteBlendMode blendMode) を呼んだ場合は、レンダーステートが変更されてしまうということです。変更されるレンダーステートは指定するSpriteBlendModeによって変わりますが、共通して以下のレンダーステートを変更します。

     GraphicsDevice.RenderState.CullMode             = CullMode.CullCounterClockwiseFace;
    GraphicsDevice.RenderState.DepthBufferEnable    = false;

    GraphicsDevice.SamplerStates[0].AddressU = TextureAddressMode.Clamp;
    GraphicsDevice.SamplerStates[0].AddressV = TextureAddressMode.Clamp;

    GraphicsDevice.SamplerStates[0].MagFilter = TextureFilter.Linear;
    GraphicsDevice.SamplerStates[0].MinFilter = TextureFilter.Linear;
    GraphicsDevice.SamplerStates[0].MipFilter = TextureFilter.Linear;

    GraphicsDevice.SamplerStates[0].MipMapLevelOfDetailBias = 0.0f;
    GraphicsDevice.SamplerStates[0].MaxMipLevel             = 0;

これ以外にも、GraphicsDeviceのVertices、Indices、VertexDeclaration、VertexShader、そしてPIxelShaderが変更されます。

そして、以上に加えて、SpriteStateMode.Noneでは以下のレンダーステートが変更されます。

     GraphicsDevice.RenderState.AlphaBlendEnable = false;
    GraphicsDevice.RenderState.AlphaTestEnable  = false;

SpriteStateMode.AlphaBlendの場合は以下のレンダーステートが変更されます。

     GraphicsDevice.RenderState.AlphaBlendEnable         = true;
    GraphicsDevice.RenderState.AlphaBlendOperation      = BlendFunction.Add;
    GraphicsDevice.RenderState.SourceBlend              = Blend.SourceAlpha;
    GraphicsDevice.RenderState.DestinationBlend         = Blend.InverseSourceAlpha;
    GraphicsDevice.RenderState.SeparateAlphaBlendEnabled= false;

    GraphicsDevice.RenderState.AlphaTestEnable  = true;
    GraphicsDevice.RenderState.AlphaFunction    = CompareFunction.Greater;
    GraphicsDevice.RenderState.ReferenceAlpha   = 0;

SpriteStateMode.Addtiveの場合は以下のレンダーステートが変更されます。SpriteStateMode.AlphaBlendの時と一緒ですが、RenderState.DestinationBlendに設定される値が違います。

     GraphicsDevice.RenderState.AlphaBlendEnable         = true;
    GraphicsDevice.RenderState.AlphaBlendOperation      = BlendFunction.Add;
    GraphicsDevice.RenderState.SourceBlend              = Blend.SourceAlpha;
    GraphicsDevice.RenderState.DestinationBlend         = Blend.One;
    GraphicsDevice.RenderState.SeparateAlphaBlendEnabled= false;

    GraphicsDevice.RenderState.AlphaTestEnable  = true;
    GraphicsDevice.RenderState.AlphaFunction    = CompareFunction.Greater;
    GraphicsDevice.RenderState.ReferenceAlpha   = 0;

ここで注目すべきは、どの設定でもDepthBufferEnablefalseに設定されることです。特に3Dモデルを描いた後にSpriteBatchを使ってステータス画面やスコアを表示したりすると、深度バッファが効かない状態になってしまい、次のフレーム以降の3Dモデルの描画がおかしくなってしまいます。

この問題を解決するには、SpritaBatch.Endメソッドを呼んだ後に、以下のレンダーステートを設定しなおすと良いでしょう。

     GraphicsDevice.RenderState.DepthBufferEnable= true;
    GraphicsDevice.RenderState.AlphaBlendEnable = false;
    GraphicsDevice.RenderState.AlphaTestEnable  = false;

また、必要に応じて以下のテクスチャアドレッシングのステートも設定しなおします。

     GraphicsDevice.SamplerStates[0].AddressU = TextureAddressMode.Wrap;
    GraphicsDevice.SamplerStates[0].AddressV = TextureAddressMode.Wrap;

 

効率的なレンダーステート管理

もちろん、SpriteBatch.Beginメソッドを呼ぶときにSaveStateMode.SaveStateを指定して元のレンダーステートに戻すこともできます。ただし、私個人しては以下の理由から、あまりお勧めできません。

  • パフォーマンスの低下
  • 悪習慣化

SpriteBatchはSaveStateMode.SaveStateが指定されたときにStateBlockクラスを使ってレンダーステートを保存します。この時の処理に時間が掛かる、特にXbox 360側でのStateBlockのパフォーマンスはWindowsに比べて著しく低いです。とは言っても、Begin、Endの呼び出しが少ないうちは全体的パフォーマンスへの影響は無視できる範囲です。

パフォーマンスよりも問題なのは、レンダーステートの管理が面倒だからといって、至る所でStateBlockを使ってしまうことが習慣化してしまうことです。StateBlockがDirect Xでサポートされる以前に同じような仕組み作ったことがあるのですが、出来上がった時には便利なものができたと自負していました。グラフィクス・プログラマーの人達も便利だと言って使ってくれてたのですが、だんだんと彼らはレンダーステートがどうなっているかに無関心になっていき、とにかくレンダーステートを保存しておけば大丈夫というのが習慣化してしまいました。そして遂にはレンダーステート保存する部分のコードがゲーム全体のパフォーマンスに影響を与えるまでに乱用されるまでになってしまいました。

実際にゲームを作る上で、必要なレンダーステートは比較的簡単に分類化でき、その数も容易に管理できる範囲内で

  • 不透明な3Dモデル(Opaque3DModel)
  • 半透明な3Dモデル(Transparent3DModel)
  • アルファテストを使うモデル(AlphaTest)
  • パーティクル(Particle)
  • 2Dインターフェース(ScreenInterface)
  • ポストエフェクト(PostEffect)

と、いった感じです。これらの描画に必要なレンダーステートを、あらかじめ決めておいてメソッドとして呼び出すだけでレンダーステートを変更するようにしておくと、常にどんなレンダーステートを使用しているかが把握しやすくなります。