シェーダー管理

シェーダー管理

今までいろいろとシェーダーを書いてきましたが,現在行っている管理の仕方を紹介します。

https://www.slideshare.net/siliconstudio/cedec2005-kawase
http://research.tri-ace.com/

Shader Package

前提としてDirectX11環境でHLSLでShader Model 5.0のターゲットでシェーダーを作成しています。 ゲームでシェーダーを利用するときには,コンパイル済みのバイナリを読み込むことになります。 このとき,特定のシーンで利用するシェーダーの名称をKeyとしてシェーダーバイナリを引っ張る辞書的な構造が必要となるのですが, これをShader Pacakgeと呼んで事前に作成しました。

ShaderPackageの構成要素は次の通りです。

Effect
ShaderPackageの大本となる一つのファイルを指します。1ShaderPackageにつき1Effectです。
Technique
1つのシェーダーファイルに対応する構造です。1つのEffectに複数のTechniqueが含まれます。
Pass
シェーダーのマクロ分岐や引数の違いによるバリエーションです。1つのTechniqueに複数のPassが含まれます。

かつてDirextXにあった,fx拡張子のエフェクトシステムに似た構造を利用します。 Passが最小単位で,利用するVertexShader,PixelShaderのエントリポイント名や,マクロ定義などを含みます。

f:id:hikita12312:20171202212647p:plain:w500

例えば,上の画像のようなEffectファイルを用意したとき,Tonemap.TypeAのPixelShaderは次のように事前にシェーダーコンパイルされることになります。

//// ShaderTonemap.fx
// Vertex Shader Entry
PPFX_OUT VS_Main(PPFX_IN In)
{
}
// Pixel Shader Entry
float4 PS_Main(PPFX_Out In)
{
#if TONEMAP==A
  return FuncA(In);
#endif
#if TONEMAP==B
  return FuncB(In);
#endif
}
fxc.exe ShaderTonemap.fx /Gfa /T ps_5_0 /E PS_Main /D TONEMAP=A /D GAMMA /Fo Tonemap_TypeA_PS.o

ビルドされたシェーダーバイナリを一つのファイルにまとめて, Technique名とPass名をKeyとして,シェーダーバイナリをValueとするような構造としたバイナリデータをShader Pacakgeとして出力します

f:id:hikita12312:20171202214842p:plain:w500

あとは,実際にゲームでシェーダーを利用するときに,Shader Pacakgeを利用して, 必要となるTechniqueとPassの辞書から必要なシェーダーバイナリを取り出して,DirextX11のAPIに渡してあげればOKです。

シェーダーの組合せ爆発の問題

高速に動作するシェーダーを記述する場合,極力if文による条件分岐やfor文によるイテレーションを避け,シェーダーアセンブリが展開されやすいコードを書くことになります。 そのため,シェーダーのバリエーションを作る場合には,条件分岐ごとにシェーダーを用意する必要があります。 組合せ爆発とは,そのバリエーションが簡単に膨れ上がってしまうことを指します

例えば,トーンマップを例に取って考えてみると,

  • トーンマップ関数の種類 5種類 (Linear,Reinhard,exp,log,Filmic)
  • カラーコレクション 3種 (OFF, ColorMatrix, LUT)
  • Noise ON/OFF 2種
  • グレアの合成 ON/OFF 2種
  • ガンマ変換 ON/OFF 2種
    (HDRディスプレイ出力とか,色空間変換をすると,もっと増える)

これだけで,5x3x2x2x2=120種類ものシェーダーをビルドして,バイナリに含めなければなりません。 もはやトーンマップのケースで言えば,これは半分しょうがない問題なのかもしれません。

しかし,実際のシェーダーバリエーションとしては,要らない組み合わせのものが含まれたりするかもしれません。 例えば,ゲーム内でトーンマップ関数のうち、Filmic以外はグレア処理は使わないとか....

シェーダー定義の設定ファイルとしてのLua

最初はShaderPacakgeのTechnique,Passの定義ファイルをJSON形式で記述していたのですが,手作業でバリエーションを書くのは辛い作業でした。
そこで,スクリプト言語であるLuaC++に組み込んで,シェーダーの定義をプログラマブルに行えるようにしました。

こんな感じのコードです。

local Util = require("script.Utility")
root = {}
-- ShaderPackage Name
root["name"] = "ShaderPackage_PFX"
--  Technique
root["technique"] = {
    -- DoFのソース作成パス
    ["DofSource"] = {
        ["src"]  = "src/ppfx/PfxDofSource.fx",
        ["pass"] = {
            ["ColorRGB_CoCA"] = {["vs"]="VS_ScreenRect", ["ps"]="PS_Source"},
        }
    },
    -- トーンマップ
    ["Tonemap"] = {
        ["src"]  = "src/ppfx/PfxTonemap.fx",
        ["pass"] = (function ()
            local pass = {}
            pass = Util:PassCombination(pass, {
                ["$name"]   = "{$1}_{$2}{$3}{$4}",
                ["vs"]      = "VS_ScreenRect",
                ["ps"]      = "PS_Tonemap",
                ["$map"] = {
                    [1] = {
                        {"Exp","/D TONEMAP_FUNC_TYPE=TMF_EXP"},
                        {"Log","/D TONEMAP_FUNC_TYPE=TMF_LOG"},
                        {"Reinhard","/D TONEMAP_FUNC_TYPE=TMF_REINHARD"},
                        {"ACESFilmic","/D TONEMAP_FUNC_TYPE=TMF_ACES"},
                    },
                    [2] = {
                        {"RGB",""},
                        {"LUM","/D LUMBASE"},
                    },
                    [3] = {
                        {"",""},
                        {"_Noise","/D NOISE"},
                    },
                    [4] = {
                        {"",""},
                        {"_CMAT","/D COLOR_CORRECTION"},
                        {"_CMATLUT","/D COLOR_CORRECTION /D COLOR_LUT"},
                    }
                }
            })
            pass["Linear"] = {["vs"]="VS_ScreenRect", ["ps"]="PS_Tonemap", ["define"]="/D TONEMAP_FUNC_TYPE=TMF_LINEAR"}
            pass["Linear_NOISE"] = {["vs"]="VS_ScreenRect", ["ps"]="PS_Tonemap", ["define"]="/D TONEMAP_FUNC_TYPE=TMF_LINEAR /D NOISE"}
            return pass
        end)()
    },
-- ...
}

Util::Passcombinationは,Pass名用のタグとマクロ定義の組から,組み合わせを作成するヘルパー関数です。 単にJSONXMLのようにデータだけを記述するよりも,自由度が高く,かつ手作業での定義も可能で良い方法だと思います。

あと,JSON形式では使えないコメント行を作れるのも利点です。