WisdomSoft - for your serial experiences.

カスタムエフェクト

プロジェクトが HiDef プロファイルの場合は、組み込みのエフェクトだけではなく、独自に HLSL で記述したシェーダを利用できます。シェーダをコンテンツプロジェクトに追加して、ゲームで使用する方法を紹介します。

シェーダを書く

これまでは XNA Framework が標準で用意している BasicEffect クラスを使って頂点を描画しましたが、HLSL を記述することによって頂点の描画を自分でプログラムできます。ただし Reach プロファイルの場合は組み込みのエフェクトしか使えないため、HLSL で記述したカスタムエフェクトは使えません。カスタムエフェクトを使用する HiDef プロファイルに設定してください。

HLSL で書かれたプログラムは、ビルド時にコンパイルされてコンテンツとして組み込まれます。実行時には、コンテンツから Effect オブジェクトとして読み込み、BasicEffect と同じように利用できます。DirectX で HLSL をすでに経験しているのであれば、XNA Framework ゲームでも、同じように HLSL でシェーダを書くことができます。

HLSL は、他の一般的なプログラミング言語と同じように、テキストファイルに定められた構文に従ってシェーダを記述します。かつては GPU 用のプログラムをアセンブリで記述していたシェーダですが、HLSL の登場によって C 言語に近い高水準な言語として表現できるようになりました。HLSL のソースファイルには FX という拡張子を付けます。

XNA Game Studio では、画像やフォントと同じように HLSL もコンテンツとして扱います。HLSL ソースファイルを新しい項目として追加するには Content サブフォルダを選択して「プロジェクト」メニューの「新らしい項目の追加」を選択します。「新しい項目の追加」ダイアログが表示されるので、左側の「カテゴリ」リストの中から「XNA Game Studio 3.0」を選択して、右の「テンプレート」リストの中から「Effect File」を選択してください。最後に、任意のファイル名を指定して「追加」ボタンを押します。

図1 エフェクトの追加
図1 エフェクトの追加

ここで追加したファイルはエフェクトファイルと呼ばれます。エフェクトファイルの中身は HLSL 言語で書かれたテキストですが、ビルド時にコンパイルされバイトコードに変換され、実行時には ContentManager クラスの Load() メソッドから Effect オブジェクトとして受け取ることができます。Effect クラスは BasicEffect クラスの基底クラスなので、パスやテクニックの取得方法は BasicEffect クラスと同じです。

シェーダや HLSL は、XNA Framework とは独立した別の技術です。これらの技術は DirectX でも使われているものであり、現在も GPU と DirectX に合わせて進化し続けています。HLSL の文法やエフェクトの詳細については本稿の範囲外となるため DirectX やシェーダの専門書籍をご覧ください。この場では、作成したエフェクトファイルから実行時に Effect オブジェクトを取得する方法を紹介します。

コード1
float4 TestVertexShader(float4 input : POSITION) : POSITION
{
    return input;
}

float4 TestPixelShader(float4 input : POSITION) : COLOR0
{
    return float4(0, 0, 0, 1);
}

technique TestTechnique
{
    pass TestPass
    {
        VertexShader = compile vs_2_0 TestVertexShader();
        PixelShader = compile ps_2_0 TestPixelShader();
    }
}
コード2
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

struct VertexPosition : IVertexType
{
    public static readonly VertexDeclaration VertexDeclaration;
    static VertexPosition()
    {
        VertexElement element = new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0);
        VertexDeclaration = new VertexDeclaration(element);
    }

    public Vector3 Position;
    public VertexPosition(Vector3 position)
    {
        this.Position = position;
    }

    VertexDeclaration IVertexType.VertexDeclaration
    {
        get { return VertexPosition.VertexDeclaration; }
    }
}

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

    private GraphicsDeviceManager graphicsDeviceManager;
    private Effect effect;
    private VertexPosition[] vertices;

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

    protected override void Initialize()
    {
        vertices = new VertexPosition[] {
	        new VertexPosition(new Vector3(0, 0.5F, 0)),
	        new VertexPosition(new Vector3(0.5F, -0.5F, 0)),
	        new VertexPosition(new Vector3(-0.5F, -0.5F, 0)),
        };

        base.Initialize();
    }

    protected override void LoadContent()
    {
        effect = Content.Load<Effect>("TestEffect");
        base.LoadContent();
    }

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

        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Apply();
            GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, 1);
        }

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

コード1はパラメータを受け取らず、入力された頂点の座標からプリミティブを黒で塗りつぶすだけの単純な HLSL です。TestVertexShader 頂点シェーダと TestPixelShader ピクセルシェーダを記述し、これらのシェーダを TestTechnique テクニックの TestPass パスに設定しています。このエフェクトは、頂点を変換せずに全てのピクセルを無条件で黒に塗りつぶします。頂点シェーダは入力された座標をそのまま出力し、ピクセルシェーダでは常に黒を返しています。

コード1をコンテンツプロジェクトに登録することで、ビルド時にコンテンツパイプラインによってアセットに変換(コンパイル)されます。コード2ではコンテンツマネージャの Load() メソッドで登録したエフェクトを Effect オブジェクトとして読み込んでいます。コード1のシェーダでは頂点の空間座標しか使用しないため、空間座標のみを持つカスタムの頂点型として VertexPosition 構造体を用意しています。

パラメータを渡す

シェーダで宣言されたグローバル変数は、XNA Framework 側ではエフェクトパラメータと呼ばれるオブジェクトとして識別できます。各種の変換行列などをシェーダに渡すには、エフェクトパラメータを取得して値を設定します。

XNA Framework のコードからシェーダのパラメータに値を渡すには Effect クラスの Parameters プロパティを用います。

Effect クラス EffectParameterCollection プロパティ
public EffectParameterCollection Parameters { get; }

このプロパティは、エフェクトが持つパラメータのコレクションを表す Microsoft.Xna.Framework.Graphics.EffectParameterCollection クラスのオブジェクトを返します。

Microsoft.Xna.Framework.Graphics.EffectParameterCollection クラス
public sealed class EffectParameterCollection : IEnumerable<EffectParameter>

このクラスは、エフェクトパラメータを管理するコレクションです。エフェクトパラメータの数はエフェクトによって異なり、EffectParameterCollection オブジェクトのインデクサを通じてエフェクトパラメータを取得します。

EffectParameterCollection クラスのインデクサ
public EffectParameter this [int index] { get; }
public EffectParameter this [string name] { get; }

int 型の index パラメータは、一般的な配列と同じように 0 番から数えたインデックスを指定し、インデクサはインデックスに対応するエフェクトパラメータを返します。string 型の name パラメータは、取得するエフェクトパラメータの名前を指定します。これは、シェーダの変数名に対応します。

エフェクトパラメータは Microsoft.Xna.Framework.Graphics.EffectParameter クラスによって表されます。

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

EffectParameter クラスは、エフェクトが持つパラメータに対応しています。これは実質的にシェーダのグローバル変数に対応しており、このオブジェクトを通じて値を設定または取得できます。

エフェクトパラメータに値を設定するには SetValue() メソッドを使います。このメソッドは、エフェクトパラメータが対応する値型ごとにオーバーロードされています。

EffectParameter クラス SetValue() メソッド
public void SetValue (bool value)
public void SetValue (bool[] value)
public void SetValue (int value)
public void SetValue (int[] value)
public void SetValue (Matrix value)
public void SetValue (Matrix[] value)
public void SetValue (Quaternion value)
public void SetValue (Quaternion[] value)
public void SetValue (float value)
public void SetValue (float[] value)
public void SetValue (string value)
public void SetValue (Texture value)
public void SetValue (Vector2 value)
public void SetValue (Vector2[] value)
public void SetValue (Vector3 value)
public void SetValue (Vector3[] value)
public void SetValue (Vector4 value)
public void SetValue (Vector4[] value)

value パラメータに、この EffectParameter オブジェクトに対応するエフェクトパラメータに設定する値を指定します。

オーバーロードされた各パラメータ型の SetValue() メソッドに対応する形で GetValue~() メソッドが提供されています。戻り値の型ではオーバーロードできないため、値を取得するメソッドは GetValue の後にデータ型の名前が続く規則で宣言されています。例えば float 型を返すメソッドは GetValueSingle() メソッド、Matrix 型を返すメソッドは GetValueMatrix() メソッド、Vector3 型を返すメソッドは GetValueVector3() という具合です。同様に配列を返す場合は末尾に Array という接尾辞が追加されます。例えば float 型の配列を返すメソッドは GetValueSingleArray() メソッド、Matrix 型の配列を返すメソッドは GetValueMatrixArray() メソッド、Vector3 型の配列を返すメソッドは GetValueVector3Array() という具合です。

EffectParameter クラス GetValueBoolean() メソッド
public bool GetValueBoolean ()
EffectParameter クラス GetValueBooleanArray() メソッド
public bool[] GetValueBooleanArray (int count)
EffectParameter クラス GetValueInt32() メソッド
public int GetValueInt32 ()
EffectParameter クラス GetValueInt32Array() メソッド
public int[] GetValueInt32Array (int count)
EffectParameter クラス GetValueMatrix() メソッド
public Matrix GetValueMatrix ()
EffectParameter クラス GetValueMatrixArray() メソッド
public Matrix[] GetValueMatrixArray (int count)
EffectParameter クラス GetValueQuaternion() メソッド
public Quaternion GetValueQuaternion ()
EffectParameter クラス GetValueQuaternionArray() メソッド
public Quaternion[] GetValueQuaternionArray (int count)
EffectParameter クラス GetValueSingle() メソッド
public float GetValueSingle ()
EffectParameter クラス GetValueSingleArray() メソッド
public float[] GetValueSingleArray (int count)
EffectParameter クラス GetValueString() メソッド
public string GetValueString ()
EffectParameter クラス GetValueTexture2D() メソッド
public Texture2D GetValueTexture2D ()
EffectParameter クラス GetValueTexture3D() メソッド
public Texture3D GetValueTexture3D ()
EffectParameter クラス GetValueTextureCube() メソッド
public TextureCube GetValueTextureCube ()
EffectParameter クラス GetValueVector2 () メソッド
public Vector2 GetValueVector2 ()
EffectParameter クラス GetValueVector2Array () メソッド
public Vector2[] GetValueVector2Array (int count)
EffectParameter クラス GetValueVector3 () メソッド
public Vector3 GetValueVector3 ()
EffectParameter クラス GetValueVector3Array () メソッド
public Vector3[] GetValueVector3Array (int count)
EffectParameter クラス GetValueVector4 () メソッド
public Vector4 GetValueVector4 ()
EffectParameter クラス GetValueVector4Array () メソッド
public Vector4[] GetValueVector4Array (int count)

多くの取得系メソッドが用意されていますが、どれも形式は同じです。単一の値を返す GetValue~() メソッドは、この EffectParameter オブジェクトに関連付けられているパラメータに設定されている値を返します。配列を返す GetValue~Array() メソッドは count パラメータに指定した要素数の配列として、パラメータに設定されている値を返します。

コード3
float4x4 World;
float4 Color;

float4 TestVertexShader(float4 input : POSITION) : POSITION
{
    return mul(input, World);
}

float4 TestPixelShader(float4 input : POSITION) : COLOR0
{
    return Color;
}

technique TestTechnique
{
    pass TestPass
    {
        VertexShader = compile vs_2_0 TestVertexShader();
        PixelShader = compile ps_2_0 TestPixelShader();
    }
}
コード4
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

struct VertexPosition : IVertexType
{
    public static readonly VertexDeclaration VertexDeclaration;
    static VertexPosition()
    {
        VertexElement element = new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0);
        VertexDeclaration = new VertexDeclaration(element);
    }

    public Vector3 Position;
    public VertexPosition(Vector3 position)
    {
        this.Position = position;
    }

    VertexDeclaration IVertexType.VertexDeclaration
    {
        get { return VertexPosition.VertexDeclaration; }
    }
}

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

    private GraphicsDeviceManager graphicsDeviceManager;
    private Effect effect;
    private EffectParameter worldParameter;
    private EffectParameter colorParameter;
    private VertexPosition[] vertices;
    private Matrix rotation;

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

    protected override void Initialize()
    {
        vertices = new VertexPosition[] {
	        new VertexPosition(new Vector3(0, 0.5F, 0)),
	        new VertexPosition(new Vector3(0.5F, -0.5F, 0)),
	        new VertexPosition(new Vector3(-0.5F, -0.5F, 0)),
        };
        rotation = Matrix.Identity;
        base.Initialize();
    }

    protected override void LoadContent()
    {
        effect = Content.Load<Effect>("TestEffect");
        worldParameter = effect.Parameters["World"];
        colorParameter = effect.Parameters["Color"];

        colorParameter.SetValue(Color.Red.ToVector4());

        base.LoadContent();
    }

    protected override void Update(GameTime gameTime)
    {
        rotation *= Matrix.CreateRotationZ(MathHelper.ToRadians(1));
        worldParameter.SetValue(rotation);
        base.Update(gameTime);
    }

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

        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Apply();
            GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, 1);
        }

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

コード3は HLSL で書かれたシェーダです。ワールド変換行列を表す World 変数と、プリミティブの色を表す Color 変数をグローバル変数として宣言しています。これらの変数が XNA Framework ではエフェクトパラメータとして扱われます。

コード4は、コード3で作成したエフェクトを用いてプリミティブを描画しています。このシェーダでは頂点の座標しか使わないため、空間座標のみを持つカスタム頂点型の VertexPosition を用意しています。LoadContent() メソッドで Effect オブジェクトを読み込み、この Effect オブジェクトの Parameters プロパティから EffectParameter オブジェクトを取得しています。

名前の通り worldParameter フィールドにシェーダ側の World 変数に対応するエフェクトパラメータを、colorParameter フィールドにシェーダ側の Color 変数に対応するエフェクトパラメータを代入しています。その後、SetValue() メソッドでエフェクトパラメータに値を設定しています。例えば、シェーダ側では入力された頂点座標に World 変数の行列を乗算しています。C# のコードから worldParameter フィールドのエフェクトパラメータに回転行列を設定することで、プリミティブを回転させています。

エフェクトパラメータを設定または取得するとき、以下のように Parameters プロパティのインデクサを毎フレームごとに呼び出す形でも可能ですが、パフォーマンスが落ちます。

effect.Parameters["World"].SetValue(rotation);

これを Update() メソッドで実行してしまうと、フレームごとに文字列からエフェクトパラメータを検索するため効率が悪いです。コード4のように、使用するエフェクトパラメータを事前に取り出しておくと効率的です。