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

シェーダシステム編
その4 やっぱりシェーダ本文の生成支援もしよう


 前章でシェーダコードを作成するための様々な支援クラスを作成してきました。これらを使用することでコード作成の頑健性がそこそこ向上しました。ただ・・・前章のサンプルを作成していて、シェーダ本文の見た目がかなりつらいのがやっぱり気になりました。だって、こんなのですよ・・・:

// 頂点シェーダコード
void MyShaderCreator::vertexShaderCode( std::string &out, VertexInputSemanticsStruct &In, VertexOutputSemanticsStruct &Out ) {

    out += "\tVS_OUTPUT Out = (VS_OUTPUT)0;\n\t";
    out += Out.pos.n() + " = " + In.pos.n() + ";\n\t";
    out += Out.tex0.xy() + " = " + In.texCoord.n() + ";\n\t";
    out += "return Out;\n";

}

この部分を展開したシェーダコードはこんな感じです:

VS_OUTPUT vs_main( VS_INPUT In ) {

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

ん〜、展開前のシェーダコードがお世辞にも可読性が良いとは言えないです・・・。やっぱり限りなく展開後のシェーダコードのように書けた方が良いに決まっています。

 そこで本章ではC++プログラム内でのシェーダソースの可読性を高めるため、展開後の書式に近いシェーダコードから展開後のシェーダコードを生成する支援機構を確立してみたいと思います。



@ 展開前のコードをこう書ければいいね

 冒頭の展開前のコードがなぜ可読性が低いのか?それは無駄な演算子と「""」の嵐のせいです。もし次のように書けたらかなり可読性が高いのではないでしょうか:

// 頂点シェーダコード
void MyShaderCreator::vertexShaderCode( std::string &out, VertexInputSemanticsStruct &In, VertexOutputSemanticsStruct &Out ) {

    out +=
        "VS_OUTPUT Out = (VS_OUTPUT)0; \n"
        "$(Out.pos) = mul( $(In.pos), &(worldMatrix) ); \n"
        "$(Out.tex0.xy) = $(In.texCoord); \n"
        "return Out; \n"

}

これならば展開後のコードとほぼ同じです。しかも一つの文字列なので「ファイル等の外部から読み込む」という技だってできそうです。「$(...)」という囲みは、自動生成が置き換える変数である事を表しています。

 つまり、上のようなちょっとした独自コードをパースして展開後のシェーダコードに変換してしまおうと考えているわけです。



A 展開前の置き換え変数を書き換える

 では実際にパースしてみましょう。そのためには簡単なコンバータを作る必要があります。これはShaderCreatorの内部で完結するだけで良いので、ShaderCreatorの入れ子クラスにしちゃいます。

 CodeConverter入れ子クラスは、上のような展開前の文字列を解析して、置き換え構文があったらそこをShaderCreatorに定義してあるグローバル変数やセマンティクス、テクスチャ、サンプラなどあらゆる変数や呼び出し関数に置き換えます。これを実現するにはShaderCreator内に定義してある各変数をCodeConverterに登録する必要があります。

 少し面倒が増えますが、この登録はShaderCreatorのコンストラクタ内で行ってしまいましょう:

// コンバータへ各変数を登録
void MyShaderCreator::registUserVals() {
    conv.regist(diffuseTexture);
    conv.regist(diffuseSampler);
    conv.regist("diffuseFunc", diffuseFunc);
}

CodeConverterクラスにregistメソッドを追加し、内部で各変数を登録します。これでいわば「予約語」が登録された事になります。

 次に予約語を使って書いたシェーダ文をトレースして置き換え作業を行います。これはもう、泥臭く泥臭くやります。一応ソースを書くとこんな感じです:

// 頂点シェーダコンバート
std::string ShaderCreator::CodeConverter::convert(std::string src) {
    std::string out;

    // 「$」が出てきたら置き換え
    size_t sz = src.size();
    for ( size_t i = 0; i < sz; i++ ) {
        if ( src[i] == '$' ) {
            // 置き換え名抽出
            std::string name;
            i+=2;
            while(i < sz && src[i] != ')')
                name += src[i++];
            // 置き換え名を変換
            out += find(name);
            continue;
        }
        out += src[i];
    }
    return out;
}

このコードを通すと$(In.pos)がメンバ変数のIn.posとして解釈されて、その部分が適切な変数名に置き換えられます。・・・あ、支援と言ってもこんなもんですね(笑)


 逆に言えばこれだけの事でプログラム内でのシェーダ文が随分と書きやすくなりました。詳しい実装はサンプルプログラムにありますのでご覧下さい。