XNA Game Studio 4.0のエフェクト・コンパイラーとコンテント・パイプライン・オートメーション

今までのXNAにはEffect.CompileEffectFromSourceメソッドがありました。Xbox 360上ではランタイム時にシェーダーのコンパイルは使えず、Windows上でのみ使えるメソッドでXbox 360とwindows用のシェーダーをコンパイルすることができました。しかし、この方法では以下の問題がありました。

  • ランタイム用のAPIのように見えるのに、すべてのプラットフォームで使えない
  • Windows再配布コンポーネントにはXbox 360用のシェーダーコンパイラーが含まれている。これはWindows専用のゲームを作っている人達にとってはスペースの無駄遣いになる
  • 新しいプラットフォームを追加するたびに、正規のバージョンとは別のパッケージを追加してきました。これはバージョン管理としては良い方法ではなく、無関係のプラットフォームのシェーダーコンパイラーをアップデートする度にWindows版のフレームワークDLLを更新しないといけません。

Game Studio 4.0ではシェーダーコンパイラーをWindows版 XNAフレームワークからコンテント・パイプラインへと移しました。Effect.CompilerEffectFromSourceの代わりにEffectProcessorを使うようになりました。

コンテント・パイプライン内でエフェクトをコンパイルするには幾つかの方法があります。

  • .fxファイルをコンテントフォルダに追加してF5でコンパイル。簡単、でも柔軟性がない
  • .fxファイルをMSBuildプロジェクトに追加(別の機会で紹介します)し、コマンドラインからmsbuild.exeを使う。この方法はとても便利なのですが、活用している人が少なくて残念です。
  • MSBuild APIを使ってプログラム的にプロジェクトをメモリ内に作って実行する。これはレベルエディタなどの大きなプロジェクト向けの方法ですが、設定するのに手間が掛かります。
  • XNA Game Studio 4.0の新機能: MSBuildのプロセスを使わずに直接インポーターとプロセッサーをC#コードから呼び出す。これ以降は、この新しいやり方を紹介します。

ContentPipelineの機能を使うにはMicrosoft.Xna.Framework.Content.Pipeline.dllを参照する必要があります。しかし、参照追加のダイアログを開いても、このアセンブリは表示されないでしょう。どこへ行ったのでしょうか?

.Net Framework 4.0にはClient Profileと呼ばれるものがあります。これは.Net フレームワーク全体ではなく、クライアント向けアプリケーション用に最適化されたサブセットとなっていて、フレームワークのダウンロードサイズが小さいなどの利点があります。XNA Game Studio 4.0ではWindows向けのゲームを作った場合、デフォルトでClient Profileを使うようになっています。コンテント・パイプラインのアセンブリを参照するには対象プラットフォームを.Net Framework 4にする必要があります。

やり方は以下のとおりです。

  • Visual Studio上でプロジェクトのプロパティを開く
  • アプリケーションタブを選択
  • 対象のフレームワークを「.Net Framework 4 Client Profile」から「.Net Framework 4」に変更する
  • 参照の追加を選択
  • .Netタブを選択
  • Microsoft.Xna.Framework.Content.Pipelineを選択。必要に応じて他のアセンブリファイルも追加する。(ここではMicrosoft.Xna.Framework.Content.Pipeline.TextureImporterを追加しています)

full-framework

インポーターを呼び出す

まずはテクスチャをインポートするサンプルを紹介します。まず最初にusingステートメントを記述します。

 using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;

次にカスタム・ロガー(ビルド状態をログするもの)を作ります。ここでは宣言だけで、なんの処理もしていませんが、実際にはこれらの情報をどこかに表示する必要があります。

 class MyLogger : ContentBuildLogger
{
    public override void LogMessage( string message,
                                     params object[] messageArgs) { }

    public override void LogImportantMessage (string message,
                                            params object[] messageArgs) { }

    public override void LogWarning( string helpLink,
                                    ContentIdentity contentIdentity,
                                    string message,
                                    params object[] messageArgs) { }
}

ようやっとカスタム・インポーター・コンテキストの記述です。このコンテキストを通してインポーターとホスト(インポーターの呼び出し側)との、情報のやりとりをします。

 class MyImporterContext : ContentImporterContext
{
    public override string IntermediateDirectory { get { return string.Empty; } }
    public override string OutputDirectory { get { return string.Empty; } }

    public override ContentBuildLogger Logger { get { return logger; } }
    ContentBuildLogger logger = new MyLogger();

    public override void AddDependency(string filename) { }
}

これらのヘルパークラスをいったん記述すれば、テクスチャ・インポーターを簡単に使うことができます。

 TextureImporter importer = new TextureImporter();

TextureContent texture = importer.Import("cat.tga", new MyImporterContext());

プロセッサーを呼び出す

プロセッサーの呼び出しはインポーターと似ていますが、プロセッサー・コンテキストはインポーター・コンテキストより複雑になっています。

 class MyProcessorContext : ContentProcessorContext
{
    public override TargetPlatform TargetPlatform
    {
        get { return TargetPlatform.Windows; }
    }

    public override GraphicsProfile TargetProfile
    {
        get { return GraphicsProfile.Reach; }
    }

    public override string BuildConfiguration
    {
        get { return string.Empty; }
    }

    public override string IntermediateDirectory
    {
        get { return string.Empty; }
    }

    public override string OutputDirectory
    {
        get { return string.Empty; }
    }

    public override string OutputFilename
    {
        get { return string.Empty; }
    }

    public override OpaqueDataDictionary Parameters
    {
        get { return parameters; }
    }

    OpaqueDataDictionary parameters = new OpaqueDataDictionary();

    public override ContentBuildLogger Logger { get { return logger; } }
    ContentBuildLogger logger = new MyLogger();

    public override void AddDependency(string filename) { }
    public override void AddOutputFile(string filename) { }

    public override TOutput Convert<TInput, TOutput>(
                                    TInput input,
                                    string processorName,
                                    OpaqueDataDictionary processorParameters)
    {
        throw new NotImplementedException();
    }

    public override TOutput BuildAndLoadAsset<TInput, TOutput>(
                                    ExternalReference<TInput> sourceAsset,
                                    string processorName,
                                    OpaqueDataDictionary processorParameters,
                                    string importerName)
    {
        throw new NotImplementedException();
    }

    public override ExternalReference<TOutput> BuildAsset<TInput, TOutput>(
                                    ExternalReference<TInput> sourceAsset,
                                    string processorName,
                                    OpaqueDataDictionary processorParameters,
                                    string importerName,
                                    string assetName)
    {
        throw new NotImplementedException();
    }
}

TargetPlatformとTargetProfileにはビルドするプラットフォームやHiDefやReachなどのプロファイルを指定します。

AddDependencyメソッドを使うことによって、インポーターやプロセッサーで処理したファイルが依存しているファイルを追加することができます。例えば、エフェクトコンパイラーがエフェクトソースコード内に#inlcudeステートメントを見つけたときなどに呼び出されます。この情報はインクリメンタルビルドをする時に使用します。.fxファイルを変更していなくても、#includeで指定されたファイルが変更された場合にはエフェクトをコンパイルします。

以下の単純なサンプルでは、Convert、BuildLoadAndAssetやBuildAssetメソッドは使っていません。これらのメソッドは独立した単純なプロセッサーでは必要ありません。ですが、ModelProcessorなどのビルド中にMaterialProcessor、EffectProcessor、TextureProcessorなどの複数のプロセッサーを使う場合には呼び出す必要があります。

このカスタム・プロセッサー・コンテキストを実装したら、HLSLソースコード文字列をEffectContent.EffectCodeに設定し、EffectProcessorを使ってコンパイルします。

 EffectContent effectSource = new EffectContent
{
    Identity = new ContentIdentity { SourceFilename = "myshader.fx" },

    EffectCode =
    @"
        float4 MakeItPink() : COLOR0
        {
            return float4(1, 0, 1, 1);
        }

        technique Technique1
        {
            pass Pass1
            {
                PixelShader = compile ps_2_0 MakeItPink();
            }
        }
    ",
};


EffectProcessor processor = new EffectProcessor();

CompiledEffectContent compiledEffect = processor.Process(effectSource, new MyProcessorContext());

プロセッサー・パラメーターを設定することにより、コンパイラーの設定を変更することができます。

 EffectProcessor processor = new EffectProcessor();

processor.Defines = "ENABLE_FOG;NUM_LIGHTS=3";
processor.DebugMode = EffectProcessorDebugMode.Optimize;

CompiledEffectContent compiledEffect = processor.Process(effectSource, new MyProcessorContext());

コンテント・パイプラインは再配布用のランタイムには含まれていないので、このコードを動作させるにはXNA Game Studioがインストールされた環境が必要です。

原文:

http://blogs.msdn.com/b/shawnhar/archive/2010/05/07/effect-compilation-and-content-pipeline-automation-in-xna-game-studio-4-0.aspx