WisdomSoft - for your serial experiences.

テクスチャ生成

通常は元となる画像ファイルからテクスチャを読み込みますが、実行時にプログラムからピクセルデータを設定してテクスチャを生成できます。

プログラムからテクスチャを作る

一般的なゲームで扱うスプライトの多くは、すでに作られている画像ファイルをテクスチャとして読み込んで描画する形になりますが、ゲーム実行時にプログラムから動的にテクスチャを生成してスプライトとして描画することもできます。この方法の場合、コンストラクタから初期化されていない Texture2D インスタンスを生成し、実行時にピクセルを設定します。

Texture2D クラスのコンストラクタ
public Texture2D (
         GraphicsDevice graphicsDevice,
         int width,
         int height,
)

graphicsDevice パラメータには、このテクスチャを描画する GraphicsDevice オブジェクトを指定します。width パラメータにはテクスチャの幅を、height パラメータにはテクスチャの高さを、それぞれピクセル単位で指定します。

Texture2D クラスのコンストラクタから生成した Texture2D オブジェクトのすべての要素は、初期値の 0 に設定されています。そのため、生成した Texture2D を描画しても何も表示されません。テクスチャにデータを設定するには SetData() メソッドを使います。

Texture2D クラス SetData() メソッド
public void SetData<T> (T[] data) where T : ValueType

data パラメータには、テクスチャのフォーマットに従った任意の型の配列を指定します。この配列は、常にテクスチャの幅と高さを乗算した要素数を持たなければなりません。すなわち、配列の 1 要素が、テクスチャの 1 ピクセルに相当します。このメソッドを実行すると、配列がテクスチャにコピーされます。

ただし、テクスチャのデータはデバイスに設定されてから変更することはできません。SetData() メソッドによるテクスチャの設定は、テクスチャを描画する前に行い、描画後は変更しないでください。SpriteBatch クラスの Draw() メソッドは、テクスチャを GraphicsDevice に設定します。デバイスに設定されているリソースを変更しようとした場合、例外が発生してしまいます。 

コード1
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

public class TestGame : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new TestGame()) game.Run();
    }

    private GraphicsDeviceManager graphicsDeviceManager;
    private SpriteBatch sprite;
    private Texture2D texture;

    public TestGame()
    {
        graphicsDeviceManager = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
    }

    protected override void LoadContent()
    {
        sprite = new SpriteBatch(GraphicsDevice);

        texture = new Texture2D(GraphicsDevice, 400, 300);
        Color[] data = new Color[texture.Width * texture.Height];

        for (int index = 0, v = 0; v < texture.Height; v++)
        {
            for (int h = 0; h < texture.Width; h++)
            {
                byte red = (byte)(0xFF * ((float)h / texture.Width));
                data[index] = new Color(red, 0, 0);
                index++;
            }
        }
        texture.SetData(data);

        base.LoadContent();
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.White);

        sprite.Begin();
        sprite.Draw(texture, Vector2.Zero, Color.White);
        sprite.End();

        base.Draw(gameTime);
    }
}
実行結果
コード1 実行結果

コード1は LoadContent() メソッド内で Texture2D コンストラクタから幅 400 ピクセル、高さ 300 ピクセルの Texture2D オブジェクトを生成しています。既定のフォーマットは、1 ピクセルを Color 構造体と互換の 32 ビットの値で表現します。

ピクセルを表す値の配列は、走査線と同じように水平方向に向かい、端に到達すると一段下がります。よって、2 階層の内側の for 文で水平方向に向かって赤色になるグラデーションのピクセルを設定し、端に到達すると内側の for 文を抜け出し、外側(トップレベル)の for 文に戻って次の段の処理に移行させています。これを配列の末尾に到達するまで繰り返しています。

data 変数を初期化したあと、SetData() メソッドを使って配列の値をテクスチャにコピーします。実行結果をみると、生成したピクセルが正しく描画されていることを確認できます。

動的にテクスチャを変更する

SpriteBatch クラスの Draw() メソッドは、内部で GraphicsDevice オブジェクトにアクセスしてテクスチャを設定します。GraphicsDevice オブジェクトにテクスチャが設定されている状態で SetData() メソッドを呼び出すと例外がスローされるので、コード1のテクスチャのデータを Update() メソッドのような動的な処理の中で定期的に更新することができません。描画したテクスチャのデータを変更するには、GraphicsDevice オブジェクトからテクスチャを外す必要があります。

最も単純な方法は新しい Texture2D インスタンスを生成することですが、フレームごとにテクスチャを生成する処理はあまり効率的ではありません。SpriteBatch クラスを用いて描画したテクスチャのデータを更新するには、一度グラフィックスから解除します。GraphicsDevice オブジェクトに設定されているテクスチャは Textures プロパティで表されます。

GraphicsDevice クラス Textures プロパティ
public TextureCollection Textures { get; }

このプロパティは、グラフィックスデバイスに割り当てられているテクスチャのコレクションを表す Microsoft.Xna.Framework.Graphics.TextureCollection クラスのオブジェクトを返します。

Microsoft.Xna.Framework.Graphics.TextureCollection クラス
public sealed class TextureCollection

このクラスはコレクション関連のインタフェースは実装されておらず、 Texture オブジェクトをインデックスで管理するためのインデクサのみが公開されています。 

TextureCollection クラスのインデクサ
public Texture this [int index] { get; set; }

ここに、グラフィックスデバイスが処理するテクスチャが含まれているので、設定されているテクスチャ番号に null を代入することで、テクスチャを変更できるようになります。グラフィックスデバイスに割り当てられるテクスチャの数は環境によって異なりますが、この場では 1 つのテクスチャしか描画しないため、0 番に設定されます。

コード2
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

public class TestGame : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new TestGame()) game.Run();
    }

    private GraphicsDeviceManager graphicsDeviceManager;
    private SpriteBatch sprite;
    private Texture2D texture;
    private Random random;
    Color[] currentFrame, nextFrame;

    public TestGame()
    {
        graphicsDeviceManager = new GraphicsDeviceManager(this);
        TargetElapsedTime = new TimeSpan(0, 0, 0, 0, 100);
        random = new Random();
    }

    protected override void LoadContent()
    {
        sprite = new SpriteBatch(GraphicsDevice);

        texture = new Texture2D(GraphicsDevice, 128, 128);
        currentFrame = new Color[texture.Width * texture.Height];
        nextFrame = new Color[texture.Width * texture.Height];

        //セルを乱数で初期化
        for (int i = 0; i < currentFrame.Length; i++)
            currentFrame[i] = (random.Next(4) == 0 ? Color.Black : Color.White);

        base.LoadContent();
    }

    protected override void Update(GameTime gameTime)
    {
        for (int i = 0; i < currentFrame.Length; i++)
        {
            int v = (int)(i / texture.Width);   //垂直位置
            int h = i - (texture.Width * v);    //水平位置

            int alive = 0;  //周囲の生きているセル数

            //左
            if (h - 1 >= 0 && currentFrame[i - 1] == Color.Black) alive++;
            //右
            if (h + 1 < texture.Width && currentFrame[i + 1] == Color.Black) alive++;
            //上
            if (v - 1 >= 0 && currentFrame[i - texture.Width] == Color.Black) alive++;
            //下
            if (v + 1 < texture.Height && currentFrame[i + texture.Width] == Color.Black) alive++;
            //左上
            if (h - 1 >= 0 && v - 1 >= 0 && currentFrame[i - 1 - texture.Width] == Color.Black) alive++;
            //左下
            if (h - 1 >= 0 && v + 1 < texture.Height && currentFrame[i - 1 + texture.Width] == Color.Black) alive++;
            //右上
            if (h + 1 < texture.Width && v - 1 >= 0 && currentFrame[i + 1 - texture.Width] == Color.Black) alive++;
            //右下
            if (h + 1 < texture.Width && v + 1 < texture.Height && currentFrame[i + 1 + texture.Width] == Color.Black) alive++;

            if (currentFrame[i] == Color.White)  //生きているセルの処理
            {
                if (alive == 3) nextFrame[i] = Color.Black;
                else nextFrame[i] = Color.White;
            }
            else //死んでいるセルの処理
            {
                if (alive == 2 || alive == 3) nextFrame[i] = Color.Black;
                else nextFrame[i] = Color.White;
            }
        }

        GraphicsDevice.Textures[0] = null;
        texture.SetData(nextFrame); //テクスチャを更新

        Color[] temp = currentFrame;
        currentFrame = nextFrame;
        nextFrame = temp;

        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        Window.Title = gameTime.TotalGameTime.ToString();
        GraphicsDevice.Clear(Color.White);

        sprite.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, null, null, null);
        sprite.Draw(texture, new Rectangle(0, 0, 400, 400), Color.White);
        sprite.End();

        base.Draw(gameTime);
    }
}
実行結果
コード2 実行結果

コード2は、テクスチャに設定する配列の 1 ピクセルを 1 セルに見立てたライフゲームです。ライフゲームとは、格子上のセルに生と死の状態があり、周囲のセルの状態によって次の世代の状態が決定されるいう一種のシミュレーションです。基本的なルールは、過密状態でも過疎状態でもセルは死滅するというもので、次の 3 つのルールが一般に知られているライフゲームです。

  • 死んでいるセルの周囲に 3 つの生きているセルがあれば、そのセルは次の世代で誕生する
  • 生きているセルの周囲の 2 つまたは 3 つの生きているセルがあれば、次の世代も生き残る
  • そうでなければ、セルは次の世代で死滅する

コード2は Update() メソッドの呼び出しで次の世代を計算するため 1 フレームが 1 世代に対応しています。描画速度を調整するため、このプログラムでは TargetElapsedTime プロパティの値を変更して 100 ミリ秒間隔でフレームを描画するように設定しています。テクスチャのサイズが大きいと計算に時間がかかるため幅 128 ピクセル、高さ 128 ピクセルのテクスチャを拡大して描画しています。 

テクスチャからデータを取得する

画像ファイルなどから生成したテクスチャの編集するには、テクスチャのデータを配列に読み込む必要があります。テクスチャにデータを設定する SetData() メソッドに対して、テクスチャのデータを配列に複製する GetData() メソッドが用意されているので、このメソッドからテクスチャの現在のデータを取得できます。

Texture2D クラス GetData() メソッド
public void GetData<T> (T[] data) where T : ValueType

data パラメータには、テクスチャのデータを保存する任意の構造体型の配列を指定します。このメソッドと SetData() メソッドによるデータ編集を組み合わせることで、読み込んだ画像を実行時にメモリ上で編集できます。ただし、サイズが大きなテクスチャのデータを複製するには大きな配列が必要で、これを 1 要素単位で処理すると CPU に大きな負担がかかるので注意してください。

コード3
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

public class TestGame : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new TestGame()) game.Run();
    }

    private GraphicsDeviceManager graphicsDeviceManager;
    private SpriteBatch sprite;
    private Texture2D texture;

    public TestGame()
    {
        graphicsDeviceManager = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
    }

    protected override void LoadContent()
    {
        sprite = new SpriteBatch(GraphicsDevice);
        texture = Content.Load<Texture2D>("TestImage");
        Color[] data = new Color[texture.Width * texture.Height];

        texture.GetData(data);
        for (int i = 0; i < data.Length; i++)
        {
            byte gray = (byte)(0.29 * data[i].R + 0.58 * data[i].G + 0.11 * data[i].B);
            data[i] = new Color(gray, gray, gray, data[i].A);
        }
        texture.SetData(data);

        base.LoadContent();
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.White);

        sprite.Begin();
        sprite.Draw(texture, Vector2.Zero, Color.White);
        sprite.End();

        base.Draw(gameTime);
    }
}
実行結果
コード3 実行結果

コード3は読み込んだフルカラーのテクスチャをグレースケールに変換して描画するプログラムです。LoadContent() メソッドで読み込んだテクスチャから GetData() メソッドを用いて Color 構造体の配列にピクセルの色を読み込み、NTSC系加重平均法を用いて変換しています。このように、GetData() メソッドを用いることで読み込んだテクスチャから個々のピクセルの色を取得し、個別に処理することができます。

テクスチャのデータをメモリに展開して CPU で計算させるという処理は、通常のアプリケーション開発者にとって違和感のない行為です。しかし、XNA Framework ゲームの世界では毎秒 60 回というフレームの更新が求められます。最新のコンピュータや Xbox 360 の CPU は高速であるという印象を持っているかもしれませんが、ゲームで必要になる画像処理を CPU で計算すると驚くほど遅いのです。

ゲームの演出で必要な画像処理は、画像処理を専門とする GPU で行うべきです。XNA Framework ゲームが動作する環境では GPU にグラフィックに関する計算をさせるシェーダが実行可能です。シェーダについては後述します。この場では、XNA Framework で十分なパフォーマンスを発揮させるには GPU に仕事を与えることが重要になるということを覚えておいてください。