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

シェーダシステム編
その3 シェーダフラグメントコードの生成支援


 前章で考えた理屈を元にしてサンプルコードを実際に作ってみました(サンプルはこちらです)。もちろん叩き台でして、色々と改良が必要だなぁと感じる部分がありました。その中で苦労したのが「シェーダコードの打ち間違いと食い違い」です。サンプルコードにシェーダチェッカーも入れておいたので、ミスがあった場合にはコンパイラが教えてくれるのですが、それを直す作業は結構な手間でした。

 シェーダにはお決まりコードが沢山あります。そういうのはできればテンプレート化してしまってプログラマが打たずに済む様にするのが理想かなと思います。そこで本章では前章の叩き台コードを改良するために、シェーダコードの部品(フラグメントコード)の生成支援をする方法を考えていきます。



@ たとえばテクスチャサンプリング

 前章でテクスチャからサンプリングするための関数を作る場面がありました。サンプルコードの一部を抜粋します:

// テクスチャサンプリング関数実装部
void ShaderCreator::samplerFunctionsCode( std::string &out, TextureInfo &texInfo ) {
    if ( !texInfo.find("DiffuseTexture") ) {
        out += "float4 getDiffuseTexture( float2 uv ) {\n"
        "\treturn float4(1.0f, 1.0f, 1.0f, 1.0f);\n"
    "};\n";
    } else {
        out += "float4 getDiffuseTexture( float2 uv ) {\n"
        "\treturn tex2D( diffuseSampler, uv );\n"
        "};\n";
    }
    out += "\n";
}

これはDiffuseテクスチャがあった場合はそのサンプラを通して値を取得し、そうで無い場合は初期値として白色を返す関数を挿入している部分です。もしテクスチャが何枚もあった時にこれを毎回書くのは非常に手間ですしエラーも発生しやすくなります。つまりメンテナンス性が低いわけです。

 この関数は純粋にテクスチャサンプリングのラッパー関数です。ですから他のテクスチャでもほぼ同様の書き方になります。例えば法線マップからのサンプリングのラッパー関数は次のようになります:

// テクスチャサンプリング関数実装部
void ShaderCreator::samplerFunctionsCode( std::string &out, TextureInfo &texInfo ) {
    if ( !texInfo.find("NormalTexture") ) {
        out += "float4 getNormalTexture( float2 uv ) {\n"
        "\treturn float4(0.5f, 0.5f, 1.0f, 1.0f);\n"
    "};\n";
    } else {
        out += "float4 getNormalTexture( float2 uv ) {\n"
        "\treturn tex2D( normalSampler, uv );\n"
        "};\n";
    }
    out += "\n";
}

上の赤い部分が変更箇所です。関数名、初期値、サンプラー名が変わっています。という事は、少なくともこれらの情報を内部に保持するクラスがあれば、そこから上の文字列を作る事ができるわけです。

 テクスチャサンプリング関数コードを作成する支援クラスは、例えば次のような実装になります:

#include <string>
#include <d3dx9.h>

class SamplerFunc {
    std::string funcName;    // 関数名
    D3DXVECTOR4 initVal;     // 初期カラー
    std::string samplerName; // サンプラー名

public:
    // コンストラクタ
    SamplerFunc(std::string funcName, D3DXVECTOR4 &initVal, std::string samplerName) :
        funcName(funcName), initVal(initVal), samplerName(samplerName) {}

    // 初期値関数出力
    std::string initCode() {
        char str[1024];
        sprintf( str,
            "float4 %s( float2 uv ) {\n"
            "\treturn float4(%f, %f, %f, %f);\n"
            "}\n\n", funcName.c_str(), initVal.x, initVal.y, initVal.z, initVal.w);
        return str;
    }

    // サンプリング関数出力
    std::string samplerCode() {
        char str[1024];
        sprintf( str,
            "float4 %s( float2 uv ) {\n"
            "\treturn tex2D(%s, uv);\n"
            "}\n\n", funcName.c_str(), samplerName.c_str());
        return str;
    }
};

このクラスを通すと、先ほどのDiffuseテクスチャのコードは次のように置き換わります:

// テクスチャサンプリング関数実装部
void ShaderCreator::samplerFunctionsCode( std::string &out, TextureInfo &texInfo ) {
    SamplerFunc diffuseFunc("getDiffuseTexture", D3DXVECTOR4(1.0f, 1.0f, 1.0f, 1.0f), "diffuseSampler");
    if ( !texInfo.find("DiffuseTexture") )
        out += diffuseFunc.initCode();
    else
        out += diffuseFunc.samplerCode();
}

さらにこれは3項演算子で書けますね:

// テクスチャサンプリング関数実装部
void ShaderCreator::samplerFunctionsCode( std::string &out, TextureInfo &texInfo ) {
    SamplerFunc diffuseFunc("getDiffuseTexture", D3DXVECTOR4(1.0f, 1.0f, 1.0f, 1.0f), "diffuseSampler");
    out += ( texInfo.find("DiffuseTexture") ? diffuseFunc.samplerCode() : diffuseFunc.initCode() );
}

さらにさらに、自分自身のシェーダ生成クラスを作って、そこにSamplerFuncオブジェクトをメンバとして持たせてしまえば、生成コードすらここから無くなってしまいます:

// テクスチャサンプリング関数実装部
void MyShaderCreator::samplerFunctionsCode( std::string &out, TextureInfo &texInfo ) {
    out += ( texInfo.find("DiffuseTexture") ? diffuseFunc.samplerCode() : diffuseFunc.initCode() );
}

関数置き換え部分のコードは1行になってしまいました。これをもっと追求してShaderCreatorにsamplerFuncOutメンバメソッドを作ったとすると、こうなります:

// テクスチャサンプリング関数実装部
void MyShaderCreator::samplerFunctionsCode( std::string &out, TextureInfo &texInfo ) {
    out += samplerFuncOut(texInfo, "DiffuseTexture", diffuseFunc);
}

やった、すげー短い!ここまで出来れば打ち間違いは皆無でしょう。



A グローバル変数コード支援

 では上からずばーーっとやっていきましょう。まずはグローバル変数支援です。グローバル変数はシェーダコード全体で固定的に使用されます。そのため、何度も変数名を打つべきではありません。

 グローバル変数の宣言に必要なのは「型名」、「変数名」、「セマンティクス(あれば)」、そして前章で取り決めた「初期値」です。これらを内部に保持して文字列化をサポートするクラスは例えばこうなります:

#include <string>

class GlobalVal {
protected:
    std::string typeName;
    std::string valName;
    std::string semanticsName;
    std::string initName;

public:
    GlobalVal(std::string typeName, std::string valName, std::string initName, std::string semanticsName) :
        typeName(typeName), valName(valName), initName(initName), semanticsName(semanticsName) {}
    GlobalVal(std::string typeName, std::string valName, std::string initName) :
        typeName(typeName), valName(valName), initName(initName) {}

    // 宣言部出力
    virtual std::string decl() {
        std::string res = typeName + " " + valName;
        if (semanticsName.size()) {
            res += " : ";
            res += semanticsName;
        }
        res += " = " + initName + ";\n";
        return res;
    }

    // 変数名使用
    std::string n() const { return valName; }

    // 各種メンバ変数出力
#define OutStr(name) std::string name() const { return valName + "." + #name; }
    OutStr(x);
    OutStr(y);
    OutStr(z);
    OutStr(w);
    OutStr(r);
    OutStr(g);
    OutStr(b);
    OutStr(a);
    OutStr(xy);
    OutStr(xyz);
    OutStr(rg);
    OutStr(rgb);
#undef OutStr

    // 代入用演算子
    operator std::string () {
        return valName;
    }
};

 コンストラクタで型名と変数名と初期値文字列、そしてあればセマンティクスを設定します。宣言部ではdeclメソッドを呼び出す事で宣言用の文字列が出力されます。一方変数を使用する時にはnメンバメソッドを使って明示的にしても良いですし、代入用演算子を用いてこのクラスをstd::stringとして扱う事もできます。

 シェーダ変数は例えばvalue.xyzのようにメンバの一部を使用する書き方ができます。これを、

out += value + ".xyz";

と書いても良いのですが、可読性を少し挙げるために専用のメソッド呼び出しで、

out += value.xyz();

と書けるようにしました。


 GlobalValクラスは汎用性がありますが、実はsamplerやtextureではちょっと扱いがうまくありません。そこで、これらはこのクラスを継承して特殊化します:

テクスチャ用
class GlobalTextureVal : public GlobalVal {
public:
    GlobalTextureVal(std::string valName) :
        GlobalVal("texture", valName, "") {}
    GlobalTextureVal(std::string valName, std::string semanticsName) :
        GlobalVal("texture", valName, "", semanticsName) {}

    // 宣言部出力
    virtual std::string decl() {
        std::string res = typeName + " " + valName;
        if (semanticsName.size()) {
            res += " : ";
            res += semanticsName;
        }
        res += ";\n";
        return res;
    }
};
サンプラ用
class GlobalSamplerVal : public GlobalVal {
public:
    GlobalSamplerVal(std::string valName) :
        GlobalVal("sampler", valName, "") {}

    // 宣言部出力
    virtual std::string decl(std::string texName) {
        std::string res = typeName + " " + valName;
        res += " = sampler_state {\n";
        res += "\ttexture = <" + texName + ">;\n";
        res += "};\n";
        return res;
    }
};

これで変数の宣言やShaderCreator内での変数の使用が一本化されました。具体的に使用前後を比較してみます:

GlobalValクラス使用前後のグローバル変数の宣言部 使用後
// グローバル変数コード
void MyShaderCreator::globalValCode( std::string &out ) {
    out +=  "texture diffuseTexture;\n"
            "sampler diffuseSampler = sampler_state {\n"
            "\ttexture = <diffuseTexture>;\n"
            "};\n";
    out += "\n";
}
// グローバル変数コード
void MyShaderCreator::globalValCode( std::string &out ) {
    out += diffuseTexture.decl();
    out += diffuseSampler.decl(diffuseTexture);

    out += "\n";
}

右側の使用後は随分とわかりやすくなりました。



B シェーダ用構造体宣言支援

 シェーダの入力セマンティクスや出力セマンティクスは構造体にまとめるのが一般的です。これは、

・ 頂点シェーダ入力セマンティクス
・ 頂点シェーダ出力セマンティクス
・ ピクセルシェーダ出力セマンティクス

の3つです。「ピクセルシェーダの入力セマンティクスは?」と思われるかもしれませんが、これは頂点シェーダの出力セマンティクスと同じです。むしろそうした方がパフォーマンス向上にも繋がります。

 さてこれらの構造体の設定部分はこんな感じでした:

// 頂点シェーダの構造体宣言
void MyShaderCreator::vertexShaderStructsCode( std::string &out ) {
    out += "struct VS_INPUT {\n"
           "\tfloat4 pos : POSITION;\n"
            "\tfloat2 uv : TEXCOORD0;\n"
            "};\n\n";

    out += "struct VS_OUTPUT {\n"
            "\tfloat4 pos : POSITION;\n"
            "\tfloat2 uv : TEXCOORD0;\n"
            "};\n\n";
}

各メンバは型、変数名、セマンティクスからなっています。お、これはAで作ったGlobalValクラスで表現できるではないですか(^-^)。それはもちろん活用します。クラスの機能として必要なのは使用するセマンティクスの選択機能と構造体文字列の出力、そしてシェーダ内での変数の使用です。ちょっと問題なのは使う時です。例えば、POSITIONセマンティクスを使う時に、

// 頂点シェーダコード
void MyShaderCreator::vertexShaderCode( std::string &out, SemanticsStruct &In, SemanticsStruct &Out ) {
    out += Out.zeroReset() + "\n";
    out += "\t" + Out.get("pos") + " = mul(" + In.get("pos") + ", worldMatrix );\n";
    out += "\treturn Out;\n";
}

というのはどうかなぁと思うわけです。入力セマンティクスの種類は実は限定的で固定的です。よって、メンバ変数として直接アクセスできてもまぁ良いかなと思うわけです。つまり、

// 頂点シェーダコード
void MyShaderCreator::vertexShaderCode( std::string &out, VertexInputSemanticsStruct &In, VertexOutputSemanticsStruct &Out ) {
    out += Out.zeroReset() + "\n";
    out += "\t" + Out.pos + " = mul(" + In.pos + ", worldMatrix );\n";
    out += "\treturn Out;\n";
}

とすればかなりすっきりします。これ以上可読性を上げる方法は、ん〜ちょっと思いつかないのでこの位にしておきましょう。

 セマンティクス構造体のベースとなるSemanticsStructクラスはこういう感じになります:

class SemanticsStruct {
protected:
    std::string structName; // 構造体名

public:
    // セマンティクス用変数
    class SemanticsVal : public GlobalVal {
        std::string structName; // 構造体名
    public:
        SemanticsVal(std::string structName, std::string typeName, std::string valName, std::string semanticsName) :
            GlobalVal(typeName, valName, "", semanticsName), structName(structName) {}

        // 各種メンバ変数出力
#define OutStr(name) virtual std::string name() const { return structName + "." + valName + "." + #name; }
            OutStr(x);
            OutStr(y);
            OutStr(z);
            OutStr(w);
            OutStr(r);
            OutStr(g);
            OutStr(b);
            OutStr(a);
            OutStr(xy);
            OutStr(xyz);
            OutStr(rg);
            OutStr(rgb);
#undef OutStr
    };

public:
    SemanticsStruct(std::string structName) : structName(structName) {}
};

ちょっとごちゃごちゃとしているのですが、SemanticsStructクラス自体は実は構造体名とコンストラクタしか持っていません。このクラスは派生クラスの土台となるだけです。

 このクラスを親とした頂点シェーダの入力セマンティクス構造体を作るVertexInputSemanticsStructクラスは例えばこうなります:

class VertexInputSemanticsStruct : public SemanticsStruct {
public:
    SemanticsVal pos, blendWeight, blendIndices, normal, psize, texCoord, tangent, binormal, color, positionT;

public:
    VertexInputSemanticsStruct(std::string structName, unsigned semanticsBit) : SemanticsStruct(structName),
        pos         ("In", "float4", semanticsBit & SemanticsInfo::POSITION     ? "pos"          : "**/error/**", "POSITION" ),
        blendWeight ("In", "float4", semanticsBit & SemanticsInfo::BLENDWEIGHT  ? "blendWeight"  : "**/error/**", "BLENDWEIGHT" ),
        blendIndices("In", "int"   , semanticsBit & SemanticsInfo::BLENDINDICES ? "blendIndices" : "**/error/**", "BLENDINDICES"),
        normal      ("In", "float3", semanticsBit & SemanticsInfo::NORMAL       ? "normal"       : "**/error/**", "NORMAL" ),
        psize       ("In", "float" , semanticsBit & SemanticsInfo::PSIZE        ? "psize"        : "**/error/**", "PSIZE" ),
        texCoord    ("In", "float2", semanticsBit & SemanticsInfo::TEXCOORD     ? "uv"           : "**/error/**", "TEXCOORD0" ),
        tangent     ("In", "float4", semanticsBit & SemanticsInfo::TANGENT      ? "tangent"      : "**/error/**", "TANGENT" ),
        binormal    ("In", "float4", semanticsBit & SemanticsInfo::BINORMAL     ? "binormal"     : "**/error/**", "BINORMAL" ),
        color       ("In", "float4", semanticsBit & SemanticsInfo::COLOR        ? "color"        : "**/error/**", "COLOR0" ),
        positionT   ("In", "float4", semanticsBit & SemanticsInfo::POSITIONT    ? "positionT"    : "**/error/**", "POSITIONT" ) {}

    ~VertexInputSemanticsStruct() {}

    // 構造体宣言作成
    std::string decl() {
        SemanticsVal *v[] = {&pos, &blendWeight, &blendIndices, &normal, &psize, &texCoord, &tangent, &binormal, &color, &positionT};
        const int sz = sizeof(v) / sizeof(v[0]);
        std::string str = "struct " + this->structName + "{\n";
        for ( int i = 0; i < sz; i++ ) {
            if (v[i]->n() != "**/error/**")
                str += "\t" + v[i]->decl();
        }
        str += "};\n\n";
        return str;
    }
};

コンストラクタで入力セマンティクスで有効となる変数を確定させています。有効な物には名前が付きますが、無効のものには「**/error/**」という無効な文字列が入ります。もし間違ってシェーダ内で宣言されていないセマンティクスを使った場合、エラー文字列が刻印されてコンパイル時にエラーが検出されます。

 この辺り、こう記述はしてみたのですが、正直ぱっと見ではわかりませんよね(^-^;。このクラスを使ってセマンティクス宣言部分を比較してみます:

VertexInputSemanticsStructクラス使用前後 使用後
// 頂点シェーダの構造体宣言
void MyShaderCreator::vertexShaderStructsCode( std::string &out ) {
    out +=  "struct VS_INPUT {\n"
            "\tfloat4 pos : POSITION;\n"
            "\tfloat2 uv : TEXCOORD0;\n"
           "};\n\n";

    out +=  "struct VS_OUTPUT {\n"
            "\tfloat4 pos : POSITION;\n"
            "\tfloat2 uv : TEXCOORD0;\n"
           "};\n\n";
}
// 頂点シェーダの構造体宣言
void MyShaderCreator::vertexShaderStructsCode( std::string &out ) {
    out += vtxInput.decl();
    out += vtxOutput.decl();
}

打ち間違いがあり得ないほどすっきりしました(^-^)。


 これで入力支援はおおよそ終了です。実際に使ってみるとまた色々と変更したくなるかもしれません。その時はまた改良方法を検討します。