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

シェーダシステム編
その5 シェーダ自動生成システムを使ってみる


 前章までで、シェーダの自動化と柔軟化の仕組みの根っこのような物が、まだまだ叩き台の段階ではありますが、一先ずできました。さっそくこれを使ってみたいと思います。何度か使ってみれば、足りない機能や修正すべき項目などが見えてくるはずです。



@ ディフューズ+スペキュラマップを貼ってみる

 テストとして、ディフューズマップとスペキュラマップを貼り付けた板ポリゴンを描画するシェーダを作ってみます。ついでにShaderCreatorの使い方講座のようなものになるかな(^-^;

○ 新規クラス作成

 まずShaderCreatorの派生クラスとしてDSShaderCreatorクラスを作ります。そのクラスにShaderCreatorクラス内の仮想メソッドの宣言をごっそりと持ってきます:

#ifndef IKD_DIX_DSDHADERCREATOR_H
#define IKD_DIX_DSDHADERCREATOR_H

#include "ShaderCreator.h"
#include "GlobalVal.h"
#include "GlobalTextureVal.h"
#include "GlobalSamplerVal.h"
#include "SamplerFunc.h"

namespace Dix {
class DSShaderCreator : public ShaderCreator {
    GlobalTextureVal diffuseTexture;        // ディフューズテクスチャ
    GlobalTextureVal specularTexture;       // スペキュラテクスチャ
    GlobalSamplerVal diffuseSampler;        // ディフューズサンプラ
    GlobalSamplerVal specularSampler;       // スペキュラサンプラ
    SamplerFunc diffuseFunc, specularFunc;  // サンプラ関数

    GlobalVal lightDir;                     // ライト方向
    GlobalVal worldMatrix, viewMatrix, projMatrix; // WVP行列

public:
    DSShaderCreator();
    virtual ~DSShaderCreator();

    // 変数を登録
    virtual void registUserVals();

    // グローバル変数コード
    virtual void globalValCode( std::string &out );

    // 頂点シェーダの欠落セマンティクスの初期化コード挿入
    virtual void missingVertexSemanticsInitializeCode( std::string &out, SemanticsInfo &modelSemInfo, VertexInputSemanticsStruct &In );

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

    // テクスチャサンプリング関数実装部
    virtual void samplerFunctionsCode( std::string &out, TextureInfo &texInfo );

    // ピクセルシェーダコード
    virtual void pixelShaderCode( std::string &out, PixelInputSemanticsStruct &In, PixelOutputSemanticsStruct &Out );
    };
}

#endif

 現段階で仮想メソッドは6つあります。これを一つ一つ設定していって自動生成システムを構築していきます。
ディフューズマップとスペキュラマップが当然必要になるので、GlobalTextureValクラスでそれらを宣言します。そのテクスチャをサンプリングするためのサンプラ(GlobalSamplerValクラス)、そしてサンプリング関数を作成するSamplerFuncオブジェクトがテクスチャ周りで必要なクラス群です。この辺、ちょっと乱雑かもしれません。改良候補ですね。

 ディフューズ(拡散反射光)は目線の位置に関係なく全ての方向に等しく広がります。拡散の強さはポリゴンの面に対する光の入射角度できまります。という事は光がどの方向から差しているか、すなわち「ライトの角度」が必要ですね。スペキュラマップ(鏡面反射光)はある方向から面を見たときにきらっと輝く効果を出します。これは光の反射光が目線に飛び込んでくるために起こります。光の入射角度はディフューズ計算でも使うため共通です。一方目線位置(カメラ位置)が新たに必要となります。しかし、実は直接はいりません。なぜか?カメラの位置はビュー空間において常に原点、そして方向はZ軸と定められています。この情報はすべて「ビュー行列」に含まれているわけです。

 という事で、必要なグローバル変数は各種行列とライト方向(ワールド空間)です。これらをメンバとして宣言します。


○ コンストラクタで変数の初期化

 コンストラクタではシェーダ内で使用されるすべての変数の初期化を行います。このクラスで割りと面倒なのはここかもしれませんが絶対に必要です:

#define initFloat4x4 "float4x4(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0)"

namespace Dix {
    DSShaderCreator::DSShaderCreator() :
        diffuseTexture ("diffuseTexture", "DIFFUSETEXTURE"),
        specularTexture("specularTexture", "SPECULARTEXTURE"),
        diffuseSampler ("diffuseSampler"),
        specularSampler("specularSampler"),
        diffuseFunc("getDiffuseTexture", D3DXVECTOR4(0.0f, 0.0f, 0.0f, 0.0f), diffuseSampler.n()),
        specularFunc("getSpecularTexture", D3DXVECTOR4(0.0f, 0.0f, 0.0f, 0.0f), specularSampler.n()),
      lightDir("float3", "lightDir", "float3(0.0f, 0.0f, 1.0f)", "LIGHTDIR"),
        worldMatrix("float4x4", "worldMatrix", initFloat4x4, "WORLDMATRIX"),
        viewMatrix ("float4x4", "viewMatrix" , initFloat4x4, "VIEWMATRIX"),
        projMatrix ("float4x4", "projMatrix" , initFloat4x4, "PROJMATRIX")
    {
        // セマンティクス構造体の初期化
        vtxInput = VertexInputSemanticsStruct("VS_INPUT",
                        SemanticsInfo::POSITION |
                        SemanticsInfo::TEXCOORD |
                        SemanticsInfo::NORMAL
                        );
        vtxOutput = VertexOutputSemanticsStruct("VS_OUTPUT",
                        SemanticsInfo::POSITION |
                        SemanticsInfo::TEXCOORD0 | // uv
                        SemanticsInfo::TEXCOORD1 | // normal(view)
                        SemanticsInfo::TEXCOORD2 | // light direct(view)
                        );
        pixelInput = PixelInputSemanticsStruct("PS_INPUT",
                        SemanticsInfo::POSITION |
                        SemanticsInfo::TEXCOORD0 | // uv
                        SemanticsInfo::TEXCOORD1 | // normal
                        SemanticsInfo::TEXCOORD2   // light direct(view)
                        );
        pixelOutput = PixelOutputSemanticsStruct("PS_OUTPUT",
                        SemanticsInfo::COLOR0
                        );
}

 各変数には初期値が必要です。これはお約束でした。セマンティクスの初期化はコンストラクタ内部で代入形式で行います(親クラスに実体があるため)。出力セマンティクスには座標の他にUV、頂点法線、そしてライトの方向が必要です(スペキュラ計算をピクセルシェーダで行うため)。この初期化方法は至って形式的な物なので、静々と行いましょう。


○ 変数の登録

 続いて、上で定義した変数をコンバータに教える必要があります。それを担うのがregistUserValsメソッドです。これも、形式的に静々とです:

// 派生クラスの変数を登録
void DSShaderCreator::registUserVals() {
    conv.regist(diffuseTexture);
    conv.regist(specularTexture);
    conv.regist(diffuseSampler);
    conv.regist(specularSampler);
    conv.regist("diffuseFunc", diffuseFunc);
    conv.regist("specularFunc", specularFunc);
    conv.regist(lightDir);
    conv.regist(worldMatrix);
    conv.regist(viewMatrix);
    conv.regist(projMatrix);
}

親クラスにconvという変数があります。このregistメソッドを通して登録を行います。これでシェーダコードにメンバ変数を直接記述できるようになります。サンプラ関数の登録がちょっとなぁという感じです。



○ グローバル変数の書き込み

 ここからはシェーダコードの書き込みです。まずはグローバル変数。これも書き方がありまして、次のようにdeclメソッドを呼び出すだけです:

// グローバル変数設定
void DSShaderCreator::globalValCode( std::string &out ) {
    out += diffuseTexture.decl();
    out += specularTexture.decl();
    out += diffuseSampler.decl(diffuseTexture);
    out += specularSampler.decl(specularTexture);
    out += lightDir.decl();
    out += worldMatrix.decl();
    out += viewMatrix.decl();
    out += projMatrix.decl();
}

ここに少し改良の余地があるかなぁという感じです。サンプラの宣言を書き込む時に上のようにテクスチャを渡していますが、これはここでしなくてもコンストラクタで定義できるようにするべきですよね。ん〜チェック1〜(-_-;



○ 頂点シェーダの欠落セマンティクスに対する初期化

 もしモデルがこのシェーダが要求する入力セマンティクス(座標、UV、法線)のどれかを持っていない場合、入力値を初期化しておく必要があります。それをmissingVertexSemanticsInitializeCodeメソッドに記述します:

// 頂点シェーダの欠落セマンティクスの初期化コード挿入
void DSShaderCreator::missingVertexSemanticsInitializeCode( std::string &out, SemanticsInfo &modelSemInfo, VertexInputSemanticsStruct &In ) {
    // 座標欠落
    if ( !modelSemInfo.find(SemanticsInfo::POSITION) )
        out += "\t$(In.pos) = float4(0.0f, 0.0f, 0.0f, 0.0f);\n";

    // UV欠落
    if ( !modelSemInfo.find(SemanticsInfo::TEXCOORD) )
        out += "\t$(In.uv) = float2(0.0f, 0.0f);\n";

    // 法線欠落
    if ( !modelSemInfo.find(SemanticsInfo::NORMAL) )
        out += "\t$(In.normal) = float3(0.0f, 0.0f, 0.0f);\n";

    out += "\n";
}

このメソッドの引数のmodelSemInfoにはモデルが持つ頂点入力セマンティクスの情報が入っています。その中にこのシェーダが必要とする座標、UV、法線セマンティクスがあるかをfindメソッドでチェックします。もし無い場合はそれの代替となる初期値を上のように書き込みます。$(In.pos)のように$が付いているのは予約語でして、入出力セマンティクスとregistUserValsメソッド内で登録したグローバル変数を上のように使えます。初期値は適当ですが、普通0.0かなと思います。



○ 頂点シェーダコード

 ようやっと頂点シェーダの作成です。するべき事は「頂点の変換」、「ビュー空間でのライト方向の計算」、「ビュー空間での法線の計算」です。ビュー空間ベースにする事で目線の情報を省略できます。

// 頂点シェーダコード
void DSShaderCreator::vertexShaderCode( std::string &out, VertexInputSemanticsStruct &In, VertexOutputSemanticsStruct &Out ) {
    out +=
        "\tVS_OUTPUT Out = (VS_OUTPUT)0;\n"
        // 座標
        "\t$(Out.pos) = mul( $(In.pos), $(worldMatrix) );\n"
        "\t$(Out.pos) = mul( $(Out.pos), $(viewMatrix) );\n"
        "\t$(Out.pos) = mul( $(Out.pos), $(projMatrix) );\n"

        // UVはそのまま
        "\t$(Out.tex0).xy = $(In.uv);\n"

        // ライト方向をビュー空間に
        "\t$(Out.tex1).xyz = mul( $(lightDir), $(worldMatrix) );\n"
        "\t$(Out.tex1).xyz = mul( $(Out.tex1).xyz, $(viewMatrix) );\n"

        // 法線をビュー空間に
        "\t$(Out.tex2).xyz = mul( $(In.normal), $(worldMatrix) );\n"
        "\t$(Out.tex2).xyz = mul( $(Out.tex2).xyz, $(viewMatrix) );\n"

        "\treturn Out;\n\n";
}

ご覧頂いてわかるように、通常のシェーダとあまり変わらない形で書き込んでいけます。可読性は悪くないと思います。しかもプログラム上でのコメントも書けます。



○ テクスチャサンプリング置き換え処理

 続いてピクセルシェーダ内での処理です。モデルが必ずしもディフューズやスペキュラマップを持っているとは限りません。そういう欠落テクスチャがあった場合に対処するため、テクスチャからのサンプリングはシェーダ内関数を通すようにしています。そのシェーダ内関数を書き込むのがここ(samplerFunctionsCodeメソッド)です。これはヘルパー関数が用意されていますので、形式的に書けます:

// テクスチャサンプリング関数実装部
void DSShaderCreator::samplerFunctionsCode( std::string &out, TextureInfo &texInfo ) {
    // ディフューズテクスチャの置き換え関数作成
    out += samplerFuncOut(texInfo, diffuseTexture.n().c_str(), diffuseFunc);

    // スペキュラテクスチャの置き換え関数作成
    out += samplerFuncOut(texInfo, specularTexture.n().c_str(), specularFunc);
}

ヘルパー関数のお陰で非常に短く書けるのですが、まず第2引数がえ〜っという感じですね。後、例えばシェーダ内にテクスチャが10枚あったとしたら、きっと10回これを書きます。それはさすがに面倒ですよね。そもそも「サンプラ」「テクスチャ」「サンプラ関数」は常に一塊なのですから、そういう塊(構造体)を作ってしまい、上の関数呼び出しを裏でやってもらえば、わざわざ派生クラスで書く必要が無くなってしまいます。これも改良箇所ですね。



○ ピクセルシェーダコード

 最後はピクセルシェーダです。ここではディフューズカラーとスペキュラカラーを計算して穿つだけです。必要な情報は引数の入力セマンティクスにすでに渡されています:

void DSShaderCreator::pixelShaderCode( std::string &out, PixelInputSemanticsStruct &In, PixelOutputSemanticsStruct &Out ) {
    out +=
        "\tPS_OUTPUT Out = (PS_OUTPUT)0;\n"

        // ディフューズカラー算出
        "\tfloat4 diffuseColor = abs(dot( -normalize($(In.tex1).xyz), normalize($(In.tex2).xyz) )) * $(diffuseFunc)($(In.tex0).xy);\n"

        // スペキュラカラー算出
        "\tfloat3 halfVec = 0.5f * (float3(0.0f, 0.0f, -1.0f) - normalize($(In.tex1).xyz));\n"
        "\tfloat4 specularPower = pow( abs( dot( halfVec, $(In.tex2).xyz ) ), 10.0f);\n"
        "\tfloat4 specularColor = specularPower * $(specularFunc)($(In.tex0).xy);\n"

        // カラー作成
        "\t$(Out.color0) = diffuseColor + specularColor;\n"

        "\treturn Out;\n";
}

 スペキュラの計算はライトと目線とがなすベクトルの真ん中となる「ハーフベクトル(halfVec)」を算出し、それと法線との内積で目に飛び込んでくる強さを計算しています。


 さ、これで全部揃いました。シェーダの自動生成ができます(^-^)。
以上の実装をベースとしたサンプルを作成してテストしてみました。コードの詳細はサンプルをご覧頂くとして、スクリーンショットはこんな感じになりました:

4枚の板ポリゴンにそれぞれ別の特性を与え、それぞれに対応したシェーダを自動生成して描画しました。特性は次の通り:

・左上: ディフューズ+スペキュラマップ
・左下: ディフューズマップのみ
・右上: スペキュラマップのみ
・右下: 頂点座標のみ(テクスチャなし、UV無し)

概ねうまく動いているようです。



A 例を作って見えてきた改良すべき点

 @でちまちまと実装してきたディフューズ+スペキュラマップを適用できる自動生成シェーダですが、実装の過程で色々と改良すべき点が見えてきました。ひとまず本章はこれで閉じるとしまして、その改良点については次章で検証します。改良をする事で、自動生成システムはさらに使い易くなるのですよ(^-^)