XNAメモ – テクスチャアに描画する方法 (XNA 4)

Posted: 2012年1月10日 カテゴリー: プログラミングメモ, XNA, XNAメモ
タグ:, , , , ,

今回はRenderTarget2Dを使い、テクスチャアに描画する方法をメモります。RenderTarget2Dに関しては、細かい説明は載せてないので、MSDNをご参照ください。

 

テクスチャアへの描画は様々な状況で役に立ちます。例を挙げると、簡単に残像の処理をしたり、影を作るために使用したり、他にもエフェクト関係だけでもかなり使えます。

 

まず、RenderTarget2Dクラスのインスタンスを作ります。ここで設定するサイズがテクスチャのサイズになります。

RenderTarget2D m_target;
int m_screenWidth = 800;
int m_screenHeight = 600;

...

m_target = new RenderTarget2D( GraphicsDevice,
m_screenWidth,
m_screenHeight );

次は実際にRenderTarget2Dに描画します。ちなみに、コンストラクタ内で設定したサイズを超えた位置に描画すると、保存されません。

private void RenderToTexture()
{
// 描画するレンダーターゲットを設定する
GraphicsDevice.SetRenderTarget(m_target);

// 描画先のレンダーターゲットの内容を消す
GraphicsDevice.Clear(Color.Transparent);

// レンダーターゲットへの描画を開始
m_spriteBatch.Begin();

//...ここで描画する

// レンダーターゲットへの描画を終了
m_spriteBatch.End();

// 描画先を、画面に変える
GraphicsDevice.SetRenderTarget(null);
}

それで後は、RenderTarget2Dに保存されている画像を使うだけです。今回は単純に描画するだけにします。

protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
m_spriteBatch.Begin();

// レンダーターゲットに保存されている画像をスケールダウンして描画
m_spriteBatch.Draw(m_target, Vector2.Zero, null, Color.White, 0.0f, Vector2.Zero, 0.7f, SpriteEffects.None, 0.0f);

m_spriteBatch.End();

base.Draw(gameTime);
}

 

今回は一つの使用例として、前に機能だけを実装したパズルゲームを紹介します。

上の動画内で、ゲーム内のブロックを回転させる時、サイズを変えたり全てのブロックを移動させたりするのが面倒だったので(本当はその方がいい気もしましたが)、ボード部分だけをレンダーターゲットに保存し、それのサイズを変更したり回転させたりしてます。

 

余談ですが、XNA4からはRenderTarget2Dはテクスチャアを保存するのではなく、テクスチャアの一部になりました(それっぽい書き方で書くと、Texture2Dとの関係が ”has-a”から “is-a”に変わりました)。なので、描画時にSpriteBatch.Draw()メソッドにRenderTarget2Dをそのまま渡せる用になりました(以前はGetTexture()メソッドを使う必要がありました)。

 

今回はココまでです。最近書くネタが思いつかないので、何かリクエストがあればコメントかメールをください m_ _m

コメント
  1. vic より:

    残像を残す処理(前に描いた絵をずっと残す)を作ろうと考えたのですが、
    出来そうでできません。
    グラフィックClearのコードをなくしても描画前に勝手にクリアしてしまう感じです。
    レンダーターゲット2D を Texture2D で利用とするとエラーになり、
    描画した画像をインスタンスにコピーできず、勝手にクリアされる感じです。
    クラスのコピーはポインタのコピーにどうしてもなってしまうため、
    値のコピーが出来ません。前のフレームの画像を保存するだけなのにこんなに苦労するとは
    思いませんでした。

    対策はあるのでしょうか?

    以下はうまくいかなくて四苦八苦したソースです。
    コメントアウトした部分をいろいろ試しました。

    namespace drawZanzou
    {
    public class Game1 : Microsoft.Xna.Framework.Game
    {
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;

    Random r = new Random();
    RenderTarget2D target,target_backup;
    const int ScreenWidth = 800;
    const int ScreenHeight = 600;
    Color[] bbvram=new Color[ScreenWidth*ScreenHeight];

    Texture2D dot,backup;

    public Game1()
    {
    graphics = new GraphicsDeviceManager(this);
    Content.RootDirectory = “Content”;
    graphics.PreferredBackBufferWidth = ScreenWidth;
    graphics.PreferredBackBufferHeight = ScreenHeight;

    }

    protected override void Initialize()
    {
    // TODO: ここに初期化ロジックを追加します。
    target = new RenderTarget2D(graphics.GraphicsDevice , ScreenWidth , ScreenHeight);

    dot=new Texture2D(GraphicsDevice,1,1);
    dot.SetData(new []{Color.White});

    base.Initialize();
    }

    protected override void LoadContent()
    {
    // 新規の SpriteBatch を作成します。これはテクスチャーの描画に使用できます。
    spriteBatch = new SpriteBatch(GraphicsDevice);

    // TODO: this.Content クラスを使用して、ゲームのコンテンツを読み込みます。
    }

    protected override void Update(GameTime gameTime)
    {
    // ゲームの終了条件をチェックします。
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
    this.Exit();

    // TODO: ここにゲームのアップデート ロジックを追加します。

    base.Update(gameTime);
    }

    private void RenderToTexture()
    {
    Texture2D temp = new Texture2D(graphics.GraphicsDevice , ScreenWidth , ScreenHeight);
    if (bbvram != null) temp.SetData(bbvram);

    // 描画するレンダーターゲットを設定する
    GraphicsDevice.SetRenderTarget(target);

    // 描画先のレンダーターゲットの内容を消す
    GraphicsDevice.Clear(Color.Transparent);

    // レンダーターゲットへの描画を開始
    spriteBatch.Begin();
    //if (bbvram != null) target.SetData(bbvram);//NG レンダー ターゲットをテクスチャーとして使用するときは、デバイスにレンダー ターゲットを設定できません。
    //if (backup != null) spriteBatch.Draw(backup , Vector2.Zero , Color.White);
    if (backup != null) spriteBatch.Draw(temp , Vector2.Zero , Color.White);
    // 前の画面をコピー

    //…ここで描画する
    // spriteBatch.Draw(dot , new Vector2() , Color.FromNonPremultiplied(255 , 255 , 255 , 255));
    // spriteBatch.Draw(dot , new Vector2(r.Next(ScreenWidth) , r.Next(ScreenHeight)) , Color.White);
    spriteBatch.Draw(dot , new Rectangle(r.Next(ScreenWidth) , r.Next(ScreenHeight) , 10 , 10) , Color.White);

    // レンダーターゲットへの描画を終了
    spriteBatch.End();
    //target.GetData(bbvram);NG
    //backup = target as Texture2D;
    // backup = (Texture2D)target;
    // (Texture2D)target.GetData(bbvram);
    //target.GetData(bbvram); //NG レンダー ターゲットをテクスチャーとして使用するときは、デバイスにレンダー ターゲットを設定できません。
    //temp = target.clone(); // ポインタコピーだからだめだよね
    // target_backup = target.clone(); // ポインタ保存を嫌ってこんな感じでコピーしたいが・・・できない

    // 描画先を、画面に変える
    GraphicsDevice.SetRenderTarget(null);
    }

    ///
    /// ゲームが自身を描画するためのメソッドです。
    ///
    /// ゲームの瞬間的なタイミング情報
    protected override void Draw(GameTime gameTime)
    {
    //for (int i = 0 ; i < 1000 ; i++)
    RenderToTexture();

    GraphicsDevice.Clear(Color.CornflowerBlue);

    // TODO: ここに描画コードを追加します。
    spriteBatch.Begin();
    // レンダーターゲットに保存されている画像をスケールダウンして描画
    //spriteBatch.Draw(target , Vector2.Zero , null , Color.White , 0.0f , Vector2.Zero , 0.7f , SpriteEffects.None , 0.0f);
    spriteBatch.Draw(target , Vector2.Zero ,Color.White);
    // spriteBatch.Draw(dot , new Rectangle(r.Next(ScreenWidth) , r.Next(ScreenHeight) , 10 , 10) , Color.White);

    spriteBatch.End();

    base.Draw(gameTime);
    }
    }
    }

    • K1 より:

      細かく調べていないので何をやろうとしていたのかは憶測になりますが、前回描画した物を使って残像を残したいのであれば、

      – RenderTargetを現在のフレーム保存用の物に設定 (target?)
      – SpriteBatchで前回のフレームを描画
      – 今回のフレームでrenderTargetに描画する物を描画
      – 前回のフレーム保存用のRenderTargetに切り替え(target_backup?)
      – Clear(), その後に上で使用したRenderTargetをSpriteBatchで描画
      – RenderTargetをnullに戻し、後は描画する

      見たいな流れで実装してみるのはどうでしょうか?

      あと他の方法としては、位置だけを何フレーム分か保存し、その情報を使用して描画する手も有りますので、使用用途に合いそうな方を試してみては如何でしょうか?

  2. vic より:

    回答ありがとうございます。

    おっしゃるっとりのことをいろいろ行ったつもりですが、
    うまくいかなかったサンプルは、エラーの行を消すと点がちかちか表示されるだけです。
    実装したかった内容は、点が画面に残っていくだけのものです。

    画像が残っていく処理を作りたいと考えています。

    いっそ画面をクリアせずに描画を繰り返せば、前の画像が残っていくはず・・・
    ですが(前バージョンではできたらしい)
    XNA4.0では、spriteBatch.Begin();直前か直後に勝手に紫色で画面をクリアしてしまうようです。

    レンダーターゲットを画像とコピーしようとすると、
    「そういう使い方はゆるさん」エラーとなり(え~Texture2Dを継承しているクラスなのに)と困惑。
    正確なエラーメッセージ
    「レンダー ターゲットをテクスチャーとして使用するときは、デバイスにレンダー ターゲットを設定できません。」
    さらには、レンダーターゲットのバックアップも、ポインタのコピーしか生成できないので、
    実質不可能。
    SetData、GetData をうまく使用して画像をバックアップしようものなら、
    「そういう使い方はゆるさん」エラー
    なんでもレンダーターゲットはTexture2Dの扱いをしてはいけないとの実行エラー。
    ビルド通ります。

    アルゴリズム自体は単純なのですが、
    とにかく1フレーム前の画像が保存できないのです。
    C++でのサンプルでは同様な処理は作るのには成功しています。
    (点を連ねて曲線を描くサンプル)

    ここら辺が失敗したソースの残骸です。
    //target.GetData(bbvram);NG
    //backup = target as Texture2D;
    // backup = (Texture2D)target;
    // (Texture2D)target.GetData(bbvram);
    //target.GetData(bbvram); //NG レンダー ターゲットをテクスチャーとして使用するときは、デバイスにレンダー ターゲットを設定できません。
    //temp = target.clone(); // ポインタコピーだからだめだよね
    // target_backup = target.clone(); // ポインタ保存を嫌ってこんな感じでコピーしたいが・・・できない

    実際には .clone() のメソッドはありません。
    ポインタではなく実体をコピーしたいというイメージです。
    なぞです。

    過去にこちらの記事のコードをXNA4.0に移植した際も失敗しました。
    バックバッファの保存方法がまったくもってうまくいかないです。
    http://d.hatena.ne.jp/sanoh/20070530/1180195452#c

    これができれば、モーションブラーっぽいのもできて面白いんじゃないだろうか?
    とか考えています。

    • K1 より:

      実際にビルドしてみたのですが、目指す挙動としては白い四角が徐々に増えていけば有っていますか?

      上記の挙動であっているなら、上のコメントで紹介したようにレンダーターゲットに別のレンダーターゲットの中身を描画する事で実装可能だと思います(別々のレンダーターゲットと言うことで、上記のコードならtargetとtarget_backupで出来ると思います)。

  3. vic より:

    おっしゃるとおり四角い点が増えていく表示です。

    targetとtarget_backupのは消してしまったので、コードに残っていませんでした。
    これはしょっぱなに試したのですがうまくいかず断念しました。
    正確にはバックアップが作る方法(単純に = でできない)が分からずストップ。
    簡単そうに見えますが、まとめると以下の問題点があります。

    ●クラスは参照型なのでコピーできないです。
    ●テキスチャだけ取り出そうとするとエラーになる(ビルドは通る)
    ●spriteBatch関連の仕様なのか勝手にクリアされる

    アルゴリズム的には、C++で問題ない動作を作ることに成功しました。

    XNAではアルゴリズム以前に、コピーできない、勝手に消される、
    この2点で詰まっている状況です。

    紹介したサイトの人も、どうしてもできずに断念しました。
    ( ■[XNA]脱線、ジジイは喜ぶクイックス )

    結論的には、仕様的に無理なので、
    同じ絵をずらして表示、か、
    グラディウスのオプションのような処理を作るしかないだろうか・・・
    と考えています。

    外人サイトも探すと自分同様、困っている人がいるようです。

    • K1 より:

      バックアップ(コピー)を作る方法としてSpriteBatch.Draw()で中身を描画するのはダメなのでしょうか?

      一応試してみた所、最初の説明で上解した方法で上記の挙動は得られたと思います。
      普段コードを書いて答えるのは避けたいのですが、一応今回試してみた描画部分を載せます
      (上に張ってあるコードをコピペした後に下記になる様にの変更を加えました)

      private void RenderToTexture()
      {
      // 描画するレンダーターゲットを設定する
      GraphicsDevice.SetRenderTarget(target);

      // 描画先のレンダーターゲットの内容を消す
      GraphicsDevice.Clear(Color.Transparent);

      // レンダーターゲットへの描画を開始
      spriteBatch.Begin();
      spriteBatch.Draw(target_backup, Vector2.Zero, Color.White);

      //…ここで描画する
      spriteBatch.Draw(dot, new Rectangle(r.Next(ScreenWidth), r.Next(ScreenHeight), 10, 10), Color.White);

      // レンダーターゲットへの描画を終了
      spriteBatch.End();

      GraphicsDevice.SetRenderTarget(target_backup);
      GraphicsDevice.Clear(Color.Transparent);

      spriteBatch.Begin();
      spriteBatch.Draw(target, Vector2.Zero, Color.White);
      spriteBatch.End();

      // 描画先を、画面に変える
      GraphicsDevice.SetRenderTarget(null);
      }

      protected override void Draw(GameTime gameTime)
      {
      RenderToTexture();

      GraphicsDevice.Clear(Color.CornflowerBlue);
      spriteBatch.Begin();
      spriteBatch.Draw(target, Vector2.Zero, Color.White);
      spriteBatch.End();

      base.Draw(gameTime);
      }

      内容が求めていた物とあっていなければ、またコメントください。

      • vic より:

        ああ、なんてことでしょう。
        ありがとうございます。

        target = new RenderTarget2D(graphics.GraphicsDevice , ScreenWidth , ScreenHeight);
        target_backup = new RenderTarget2D(graphics.GraphicsDevice , ScreenWidth , ScreenHeight);

        と初期段階で複数 new して生成して貼り付けてコピーせずに描画で行うと! さすがですね!
        この方向性は全く気がつきませんでした。
        まさにコロンブスの卵です。

        コピーすることばかり考えて、
        clone()メソッドに相当するものを探したり、
        GetDataやSetDataを利用して・・・とかとんだ遠回りしていました。
        この考えは袋小路でした。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中