ホーム < ゲームつくろー! < プログラマブルシェーダ編

シェーダシステム編
その2 デフォルト値と置き換えでシェーダを柔軟に


 シェーダを柔軟に自動的に作る方法を考えるシェーダシステム編。一番の鍵となる「柔軟性」を実現するにはどうしたら良いのか?この章ではその辺りの考え方について見ていきましょう。



@ 値が無いならデフォルト値で

 例えばです。あるモデルは頂点座標とマテリアル情報としてディフューズカラー(グローバル値)を持っていたとします。それを頂点シェーダに入れて、ピクセルシェーダに頂点カラーとして出力するとします。それを図で表すと次のようになります:


 上の頂点シェーダはこの2つの情報を使ってピクセルシェーダに値を渡します。別の見方をするならば、上の頂点シェーダは「この2つの情報が必要」です。所が、別のモデルは頂点情報しか持っていなかったとします。すると、上の図はこうなってしまいます:


 入力情報のマテリアルDiffuseが欠けてしまっています。固定的な頂点シェーダの場合、グローバル変数に値が入っていない場合は「何らかの」値がそこに入ります。もちろん不定値です。頂点シェーダ側では実はグローバル変数に対して「初期値」は設定できません。そういう仕様です。

 柔軟性を確保する第1歩は、上のように頂点シェーダが要求するグローバル変数をモデルが持っていない場合に、代替値を入れてそれを補う仕組みを考える事です。



A グローバル変数はconstです

 グローバル変数が与えられなかった場合に初期化するというのは、実は結構面倒なんです。というのも「シェーダのグローバル変数はconst扱い」なためです。例えば、

float4 material_duffuse;

VS_OUTPUT vs_main( VS_INPUT In ) {
    VS_OUTPUT Out = (VS_OUTPUT)0;
    material_duffuse = float4(1.0f, 1.0f, 1.0f, 1.0f);
    Out.color = material_diffuse;
    return Out;
}

上の太字のように、グローバル変数にシェーダ内で値を入れようとするとコンパイルエラーになります。しかし、const扱いという事は宣言と同時に初期化はできます。つまり、

float4 material_duffuse = float4(1.0f, 1.0f, 1.0f, 1.0f);

VS_OUTPUT vs_main( VS_INPUT In ) {
    VS_OUTPUT Out = (VS_OUTPUT)0;
    Out.color = material_diffuse;
    return Out;
}

これは合法です。そして嬉しい事に、もし上のマテリアルDiffuseが外部で初期化されなかった場合は上の値を、外部で初期化された場合はその値が入れ替わって使われます。グローバル変数は「必ず初期化する」。こうする事で、グローバル変数(一般変数)については書き分けの必要が無くなります。



B セマンティクスが無い場合

 次の図をご覧下さい:

今度は頂点シェーダが頂点座標とUVを要求している例です。もちろん、これらが揃っているモデルであれば上の頂点シェーダは動きます。所が、UVが欠ける事がありえます:

 この時にどう対処するか?これが難しいところです。先ほどのグローバル変数と違い、UV値は「頂点シェーダ入力セマンティクス」の一員です。そして悲しいかな「入力セマンティクス」は宣言時初期化が無効化されてしまいます。すなわち、

VS_OUTPUT vs_main( float4 pos : POSITION, float2 uv : TEXCOORD0 = float2(1.0f, 1.0f) ) {
    VS_OUTPUT Out = (VS_OUTPUT)0;
    Out.pos = pos;
    Out.uv = uv;
    return Out;
}

上の太文字で示しているような初期化は意味を成しません(コンパイルは通ります)。

 そこで取るべき道は2つあります。1つは頂点シェーダの最初にUV座標の初期値を入れる一文を挿入する方法です:

VS_OUTPUT vs_main( float4 pos : POSITION, float2 uv : TEXCOORD0 ) {

    uv = float2(0.0f, 0.0f);

    VS_OUTPUT Out = (VS_OUTPUT)0;
    Out.pos = pos;
    Out.uv = uv;
    return Out;
}

頂点シェーダの引数はconstでは無いためこれは合法です。もちろんシェーダは正常に動きます。もう1つはそもそもUVは無いとして、頂点シェーダ内で引数の"uv"を使っている所をがっつり削ってしまう方法です:

VS_OUTPUT vs_main( float4 pos : POSITION ) {
    VS_OUTPUT Out = (VS_OUTPUT)0;
    Out.pos = pos;
    return Out;
}

どちらが良いかというと、う〜〜ん、多分前者かなと思います。後者の方はシェーダが複雑になった時に対処箇所が格段に増えてしまいます。「でも前者の方は無駄な計算をする事になるんじゃない?」と思われた方は鋭いです。確かにその通りです。ただ、例えば次のような計算はパフォーマンスの犠牲になりません:

VS_OUTPUT vs_main( float4 pos : POSITION ) {
    VS_OUTPUT Out = (VS_OUTPUT)0;
    pos.x = (pox.x + 0.5f) * 0.0f;
    Out.pos = pos;
    return Out;
}

入力座標のX成分を0.5だけオフセットしてそれに0.0を掛けています。結局pos.xは常に0.0になります。こういう結果が同じになる計算がシェーダ内に含まれていた場合、コンパイラは上の計算式を、

VS_OUTPUT vs_main( float4 pos : POSITION ) {
    VS_OUTPUT Out = (VS_OUTPUT)0;
    pos.x = 0.0f;
    Out.pos = pos;
    return Out;
}

と解釈してくれます。こういうシェーダの最適化をうまく利用すると、入力セマンティクスを初期化しておく一文をはさむ事によるパフォーマンス低下をある程度低減できるんです。

 そういう事を鑑みると、「欠けている入力セマンティクスがあった場合、代わりに初期値を入れる一文を挿入する」という自動生成が有効そうです。そうすれば、頂点シェーダの内部自体は特に変更を必要としません。



C テクスチャとサンプラの欠如

 セマンティクスについては初期値を入れておく事で何とかなりそうですが、何ともならない厄介者がテクスチャとそれを利用するサンプラです。次の図をご覧下さい:

 これはテクスチャ(Tex0)から指定UV座標にある色をSampler0に従って貰いそれを利用するピクセルシェーダです。ここで、もしTex0が指定されていなかったらどうなるでしょうか:

この場合、グローバル変数にテクスチャ(texture tex0;)とサンプラがあれば、シェーダ自体は動きます。そして大抵の場合は完全透明な黒色(0.0f, 0.0f, 0.0f, 0.0f)が返ってきます。「ならいいじゃん」と言いたい所なのですが、テクスチャの場合はそうともいかない状況があります。例えば法線マップ。法線マップのデフォルト値(デフォルトカラー)は大抵の場合0では無くて(0.5f, 0.5f, 1.0f, 0.0f)です。これは、法線マップが色をベクトル値として考えているからで、マイナスを表現するため0.5fを0として扱うためです。つまり、テクスチャのデフォルト値は、そのテクスチャごとに違うという事になります。

 テクスチャがある時はそこから色をもらい、無い時にはそのテクスチャが規定する(ピクセルシェーダが想定する)デフォルトカラーを返す。そういう振る舞いをさせるには「テクスチャカラーの取得をシェーダ内関数に置き換える」しか無いかなと思います。どういう事か、次のシェーダソースをご覧下さい:

///////////////////////////////////
// Pixel Shader

// 指定UVのディフューズカラーを取得する
float4 getDiffuseColor(float2 uv) {
    return tex2D(texSampler, uv);
}

float4 ps_main( VS_OUTPUT In ) : COLOR0 {
    float4 color = getDiffuseColor(In.uv);  // 関数を通してディフューズカラーを取得
    return color;
}

 ピクセルシェーダ内でテクスチャからサンプリングしたい時に、シェーダ内に直接サンプラコードを書くのではなくて、太文字のように関数を通すようにします。こうすると、テクスチャがある場合は上のように書けばもちろん良く、無い場合は、

///////////////////////////////////
// Pixel Shader

// 指定UVのディフューズカラーを取得する(テクスチャ無し)
float4 getDiffuseColor(float2 uv) {
    return float4(1.0f, 1.0f, 1.0f, 1.0f);
}

float4 ps_main( VS_OUTPUT In ) : COLOR0 {
    float4 color = getDiffuseColor(In.uv);  // 関数を通してディフューズカラーを取得
    return color;
}

のようにピクセルシェーダが規定する初期値を返す関数に置き換えてしまいます。こうすればピクセルシェーダ内のサンプリング部分は一切変更しなくて良くなります。



D 初期化のまとめ

 以上から、初期化については次のようにまとめられます。

・ グローバル変数(一般変数)は宣言時初期化を必ず書いておく。そうすればコードの置き換えは不要になる。
・ モデル無いでの入力セマンティクスの欠如は、頂点シェーダ(ピクセルシェーダ)内でそのセマンティクスを初期化するコードを最初に挿入する。
・ サンプラを通して色を抽出する部分はシェーダ内関数に置き換えて対処。

これで頂点シェーダやピクセルシェーダの本文に殆ど手を加える事無くかなりのモデルバリエーションに対応できます。少しだけ整理ができました(^-^)