locked
効率の良い描画の仕方について RRS feed

  • 質問

  • はじめまして、皆様。

     

    最近XNAで2Dシューティングゲームを作り始めました。

    最初はSpriteBatchを使って描画していたのですが、HLSLを使って、

    きれいなエフェクトをしてみたく、板ポリゴンを作って描画するように、

    改造したところ、SpriteBatchより描画に時間がかかるようになってしまいました。

     

    大量の画像を効率よく描画するのに、いい手段やおかしいところなどあれば教えていただけないでしょうか?

     

    なお、SpriteBatchで描画するソースと、3Dで描画するソースはそれぞれ以下のようになっています。

     

     

    ■ SpriteBatchのコード

     

    Code Snippet

    protected override void LoadContent() {
      image = Content.Load("tex");
      spriteBatch = new SpriteBatch(GraphicsDevice);
      imageRect = new Rectangle(0, 0, image.Width, image.Height);
    }

     

    protected override void Draw(GameTime gameTime) {
      graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
     
      // FPS計測用
      float elapsed = (float)gameTime.ElapsedRealTime.TotalSeconds;
      framecount++;
      timeSinceLastUpdate += elapsed;
      if (timeSinceLastUpdate > updateInterval) {
        fps = framecount / timeSinceLastUpdate;
       
        Window.Title = "FPS: " + fps.ToString() + " - RT: " + gameTime.ElapsedRealTime.TotalSeconds.ToString() + " - GT: " + gameTime.ElapsedGameTime.TotalSeconds.ToString();
        framecount = 0;
        timeSinceLastUpdate -= updateInterval;
      }
      

      // 描画開始
      Vector2 pos = new Vector2(5, 5);
     
      spriteBatch.Begin();
      for (int i = 0; i < IMAGE_COUNT; i++) {
        spriteBatch.Draw(image, pos, imageRect, Color.White, 0f, new Vector2(image.Width / 2, image.Height / 2), 1f, SpriteEffects.None, 0f);
       
        if ((i + 1) % 160 == 0) {
          pos.X  = 5;
          pos.Y += 5;
        } else
          pos.X += 5;
      }
      spriteBatch.End();
    }

     

     

    ■ 3Dで描画

     

    Code Snippet

    Effect texEffect;                             // テクスチャ向けエフェクト
    EffectParameter texEffectWorld;               // テクスチャ向けエフェクトパラメータ
    EffectParameter texEffectTexture;             // テクスチャ向けエフェクトパラメータ
    VertexDeclaration vertexDeclaration = null;   // 頂点データ定義
    VertexBuffer vertexBuffer = null;             // 頂点バッファ
    Matrix matProj;                               // カメラのプロジェクション変換行列
    Matrix matView;                               // カメラの
    float originW;                                // スクリーン座標の幅を3次元座標であらわした大きさ(W)、簡単に言うと3次元座標での画面の幅
    float originH;                                // スクリーン座標の高さを3次元座標であらわした大きさ(H)、簡単に言うと3次元座標での画面の高さ
    float pixelX;                                 // スクリーン座標の1pixelを3次元座標であらわした大きさ(X)
    float pixelY;                                 // スクリーン座標の1pixelを3次元座標であらわした大きさ(Y)
       
    protected override void LoadContent() {
      image = Content.Load("tex");
     
      // シェーダー宣言
      texEffect = Content.Load("texture");
      texEffectWorld   = texEffect.Parameters["fx_WVP"];
      texEffectTexture = texEffect.Parameters["fx_Texture"];
     
      // 頂点定義データを作成
      vertexDeclaration = new VertexDeclaration(graphics.GraphicsDevice, VertexPositionColorTexture.VertexElements);
     
      // ビューマトリックスをあらかじめ設定 ((0, 0, 10) から原点を見る)
      matView = Matrix.CreateLookAt(
          new Vector3(0.0f, 0.0f, 10.0f),                                        // カメラの位置
          Vector3.Zero,                                                // カメラの向き
          Vector3.Up);                                                // カメラの傾き
     
      // プロジェクションマトリックスをあらかじめ設定
      matProj = Matrix.CreatePerspectiveFieldOfView(
          MathHelper.ToRadians(45.0f),                                        // カメラの画角
          GraphicsDevice.Viewport.Width / GraphicsDevice.Viewport.Height,                        // 画面のアスペクト比
          1.0f,                                                    // Nearプレーン
          10.0f);                                                    // Farプレーン
     
      // 頂点バッファ作成
      vertexBuffer = new VertexBuffer(graphics.GraphicsDevice, VertexPositionColorTexture.SizeInBytes * 4, BufferUsage.None);
     
      // 頂点データを作成する
      VertexPositionColorTexture[] pointList = new VertexPositionColorTexture[4];
     
      pointList[0] = new VertexPositionColorTexture(new Vector3(-0.125f,  0.125f, 0.0f), Color.White , new Vector2(0.0f, 0.0f));
      pointList[1] = new VertexPositionColorTexture(new Vector3( 0.125f,  0.125f, 0.0f), Color.White , new Vector2(1.0f, 0.0f));
      pointList[2] = new VertexPositionColorTexture(new Vector3(-0.125f, -0.125f, 0.0f), Color.White , new Vector2(0.0f, 1.0f));
      pointList[3] = new VertexPositionColorTexture(new Vector3( 0.125f, -0.125f, 0.0f), Color.White , new Vector2(1.0f, 1.0f));
     
      // 頂点データを頂点バッファに書き込む
      vertexBuffer.SetData(pointList);
     
      // スクリーンサイズと3次元座標の変換用変数設定
      Vector3 originPos = GraphicsDevice.Viewport.Unproject(new Vector3(0f, 0f, 1f), matProj, matView, Matrix.Identity);
      Vector3 maxPos    = GraphicsDevice.Viewport.Unproject(new Vector3(GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height, 1f), matProj, matView, Matrix.Identity);
      originW = maxPos.X - originPos.X;
      originH = originPos.Y - maxPos.Y;
      pixelX  = originW / GraphicsDevice.Viewport.Width;
      pixelY  = originH / GraphicsDevice.Viewport.Height;
    }

    protected override void Draw(GameTime gameTime) {
      graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
     
      // FPS計測用
      float elapsed = (float)gameTime.ElapsedRealTime.TotalSeconds;
      framecount++;
      timeSinceLastUpdate += elapsed;
      if (timeSinceLastUpdate > updateInterval) {
        fps = framecount / timeSinceLastUpdate;
       
        Window.Title = "FPS: " + fps.ToString() + " - RT: " + gameTime.ElapsedRealTime.TotalSeconds.ToString() + " - GT: " + gameTime.ElapsedGameTime.TotalSeconds.ToString();
        framecount = 0;
        timeSinceLastUpdate -= updateInterval;
      }
     
      Vector2 pos = new Vector2(5, 5);
     
      // 描画開始
     
      // テクスチャを設定
      texEffectTexture.SetValue(image);
     
      // 描画に使用する頂点バッファをセットします
      graphics.GraphicsDevice.Vertices[0].SetSource(vertexBuffer, 0, VertexPositionColorTexture.SizeInBytes);
      graphics.GraphicsDevice.VertexDeclaration = vertexDeclaration;
     
      texEffect.Begin();
      texEffect.Techniques[0].Passes[0].Begin();
     
      for (int i = 0; i < IMAGE_COUNT; i++) {
        // 描画する
        Matrix matWorld = Matrix.CreateTranslation((pos.X * pixelX) - (originW / 2), -((pos.Y * pixelY) - (originH / 2)), 0.0f);
        texEffectWorld.SetValue(matWorld * matView * matProj);
        texEffect.CommitChanges();
       
        graphics.GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleStrip, 0, 2);
       
        if ((i + 1) % 160 == 0) {
          pos.X  = 5;
          pos.Y += 5;
        } else
          pos.X += 5;
      }
     
      texEffect.Techniques[0].Passes[0].End();
      texEffect.End();
     
      base.Draw(gameTime);
    }

     

     

    上記3D描画で利用しているtextureのシェーダーファイル

     

    Code Snippet

    float4x4 fx_WVP : WORLDVIEWPROJ;
    uniform extern texture fx_Texture;

    sampler textureSampler = sampler_state {
        Texture = <fx_Texture>;
        magfilter = LINEAR;
        minfilter = LINEAR;
        mipfilter = LINEAR;
    };

    struct VS_INPUT {
        float4 f4Position                       : POSITION0;
        float4 f4Color                          : COLOR0;
        float2 textureCoordinate                : TEXCOORD0;
    };

    struct VS_OUTPUT {
        float4 f4Position                       : POSITION0;
        float4 f4Color                          : COLOR;
        float2 textureCoordinate                : TEXCOORD0;
    };

    struct PS_OUTPUT {
        float4 f4Color : COLOR0;
    };

    void vertex_shader(in VS_INPUT IN, out VS_OUTPUT OUT) {
        OUT.f4Position = mul(IN.f4Position, fx_WVP);
        OUT.f4Color = IN.f4Color;
        OUT.textureCoordinate = IN.textureCoordinate;
    }

    void pixel_shader(in VS_OUTPUT IN, out PS_OUTPUT OUT) {
        OUT.f4Color = tex2D(textureSampler, IN.textureCoordinate);
        OUT.f4Color *= IN.f4Color;
    }

    technique mytechnique {
        pass p0 {
            sampler[0] = (textureSampler);
            vertexshader = compile vs_1_1 vertex_shader();
            pixelshader  = compile ps_1_1 pixel_shader();
        }
    }

     

     

    FPSを表示できるようにしてみたのですが、私のPCで動作させると、以下のようになりました。

     

      ・ SpriteBatch描画時のFPS : 60

      ・ 3D描画時のFPS : 30

     

    なお、私のPCの環境は以下のようになっています。

     

      OS : Windows Vista Home Premiam

      CPU : intel Core 2 Duo T9400

      メモリ : 3GB

      グラフィックボード : NVIDA GeForce 9800GTS 512MB

     

     

    2008年11月15日 17:15

回答

  • このコードを見る限りでは以下の2つが問題になると思います。
    1. スプライト毎にworld,view, projectionの行列計算をCPU側でしている
    2. スプライト毎にDrawPrimitiveを呼び出している
    1については、viewとprojectionはスプライト毎には変更しないので、シェーダーの定数として設定し、シェーダー内でworld,view,projectionの計算をした方が遙かに効率的です。

    そして2がこのコードで最大の速度低下の原因と思われます。XNA GS、DirectXに関わらずDrawPrimitiveメソッド内ではドライバを介してGPU描画命令を発行するのですが、この発行自体に時間が掛かります。CPUとGPUのバランスにもよりますが、目安としては1シーン内でのDrawPrimitive呼び出し回数は2千回以下、DrawPrimitive一回の呼び出しで1000ポリゴン以上の描画をするのが理想的です。

    SpriteBatchクラスは、内部にスプライト2048個分のバッファを持っていて複数のスプライトをまとめて描画するようになっています。また、Xbox 360用にはvfetchを使って更に最適化されています。

    改善方法としては自前でSpriteBatchと同様の処理をするか、単純にスプライト描画にピクセルシェーダーを使ったエフェクトを加えたい場合は、SpriteBatch.BeginにSpriteSortMode.Immediateを指定してから自前のエフェクトを指定する方法があります。

    前者はクリエータークラブオンラインのPrimitivesサンプルが参考になると思います。
    http://creators.xna.com/en-US/sample/primitives

    後者についてはSpriteEffectsサンプルが参考になります。
    http://creators.xna.com/en-US/sample/spriteeffects

    また、SpriteBatchのシェーダーはの以下のURLにあるので、独自のシェーダーを作るときに参考になると思います。
    http://creators.xna.com/en-US/utilities/spritebatchshader
    2008年11月16日 2:59

すべての返信

  • このコードを見る限りでは以下の2つが問題になると思います。
    1. スプライト毎にworld,view, projectionの行列計算をCPU側でしている
    2. スプライト毎にDrawPrimitiveを呼び出している
    1については、viewとprojectionはスプライト毎には変更しないので、シェーダーの定数として設定し、シェーダー内でworld,view,projectionの計算をした方が遙かに効率的です。

    そして2がこのコードで最大の速度低下の原因と思われます。XNA GS、DirectXに関わらずDrawPrimitiveメソッド内ではドライバを介してGPU描画命令を発行するのですが、この発行自体に時間が掛かります。CPUとGPUのバランスにもよりますが、目安としては1シーン内でのDrawPrimitive呼び出し回数は2千回以下、DrawPrimitive一回の呼び出しで1000ポリゴン以上の描画をするのが理想的です。

    SpriteBatchクラスは、内部にスプライト2048個分のバッファを持っていて複数のスプライトをまとめて描画するようになっています。また、Xbox 360用にはvfetchを使って更に最適化されています。

    改善方法としては自前でSpriteBatchと同様の処理をするか、単純にスプライト描画にピクセルシェーダーを使ったエフェクトを加えたい場合は、SpriteBatch.BeginにSpriteSortMode.Immediateを指定してから自前のエフェクトを指定する方法があります。

    前者はクリエータークラブオンラインのPrimitivesサンプルが参考になると思います。
    http://creators.xna.com/en-US/sample/primitives

    後者についてはSpriteEffectsサンプルが参考になります。
    http://creators.xna.com/en-US/sample/spriteeffects

    また、SpriteBatchのシェーダーはの以下のURLにあるので、独自のシェーダーを作るときに参考になると思います。
    http://creators.xna.com/en-US/utilities/spritebatchshader
    2008年11月16日 2:59
  • ご回答ありがとうございます。

     

    示していただいた、SpriteBatchにピクセルシェーダーを適用する方法を試したところ、

    無事やりたいことが出来ました。

     

    また、DrawPrimiteveの呼び出し回数などの情報も非常に参考になりました。

    今回は、今の私に時間が無いのと、やりたい要件が満たされてしまったため、

    試すことが出来ないのですが、今後にとって、非常に有用な情報となりました。

    ご教授ありがとうございます。

     

    2008年11月16日 8:16