シェーダー管理
シェーダー管理
今までいろいろとシェーダーを書いてきましたが,現在行っている管理の仕方を紹介します。
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のエントリポイント名や,マクロ定義などを含みます。
例えば,上の画像のような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として出力します
あとは,実際にゲームでシェーダーを利用するときに,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形式で記述していたのですが,手作業でバリエーションを書くのは辛い作業でした。
そこで,スクリプト言語であるLuaをC++に組み込んで,シェーダーの定義をプログラマブルに行えるようにしました。
こんな感じのコードです。
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名用のタグとマクロ定義の組から,組み合わせを作成するヘルパー関数です。 単にJSONやXMLのようにデータだけを記述するよりも,自由度が高く,かつ手作業での定義も可能で良い方法だと思います。
あと,JSON形式では使えないコメント行を作れるのも利点です。