ホーム < ゲームつくろー! < Unity/Shader編

Shader編
その3 サーフェイスシェーダ


 Unityのシェーダには3種類ありますが、そのうちサーフェイスシェーダはUnityのライティングパイプラインに組み込まれるシェーダです。サーフェイスシェーダを使えば、Unityのシーンに置いたライトがちゃんと反映されるわけです。

 サーフェイスシェーダの直接的なマニュアルはこちらです:
Unity.Writing Surface Shader: http://docs.unity3d.com/Documentation/Components/SL-SurfaceShaders.html



@ サーフェイスシェーダの書き方

 サーフェイスシェーダはシェーダ内のSubShaderに書きます。この時、いくつかのお約束があります。まず、SubShaderに「シェーダをこの範囲に書きますよ〜」という範囲指定をしてあげる必要があります。これは

CGPROGRAM
    ...
    ...
    ...

    ...
    ...
ENDCG

のようにCGPROGRAM〜ENDCGというおまじないを置いてあげます。

 次に、SubShaderがサーフェイスシェーダである事をUnityに教える必要があります。そのために、SubShader内のおまじないの間に次のような#pragmaディレクティブを記述します:

#pragma surface surfaceFunction lightModel [optionalparams]

#pragma surface と記述すると、UnityはそのSubShaderがサーフェイスシェーダであると認識します。
続くsurfaceFunctionにはシェーダのエントリ関数名を記述します。
lightModelはサーフェイスシェーダでのライティングの仕方を指定します。これには"Lambert"もしくは"BlinnPhong"という指定、もしくはカスタムライティングを指定できます。LambertはDiffuse(拡散反射光)のライティングモデル、BlinnPhongはSpecular(鏡面反射光)の代表的なモデルです。
optionalparamには、そのサーフェイスシェーダについてさらに細かい指定ができるのですが…沢山あり過ぎて説明しきれないので、上記リンク先の「Optional parameters」をご覧下さい(T-T)


 エントリ関数の形は決まっていて、次のような引数を取ります:

void surfaceFunction( Input IN, inout SurfaceOutput o ) {
   ...
}

Inputは描画時にUnityから渡されるシェーダ引数が入った構造体です。この中身はプログラマが設定します。構造体の引数の名前の付け方は決まっています

 まずテクスチャのUV値は、「uv*****」という名前になります。*****はPropertiesで指定したテクスチャ名です。例えばPropertiesで「_HogeTex」という変数名にしたなら、そのUV名は「uv_HogeTex」になります。次に頂点カラーはfloat4型の変数名にセマンティクス「COLOR」を付けると取得できます。例えばそれらを指定した構造体は次のようになります:

Shader "Custom/testShader" {
    Properties {
        _HogeTex ( "Base", 2D ) = "white" {}
    }

    SubShader {
        CGPROGRAM

        #pragma surface surf Lambert
       
        struct Input {
            float2 uv_HogeTex;
            float4 vtxColor : COLOR;
        };

        void surf( Input IN, inout SurfaceOutput o ) {
        }

        ENDCG
    }
}

このシェーダコードは実はもうサーフェイスシェーダとして動きます。実際にマテリアルに適用して球にくっつけるとこんな感じで真っ黒になります:

真っ黒になるのはサーフェイスシェーダの出力であるSurfaceOutputに何も値を入れていないからです。SufaceOutputも構造体で、主に次のような出力項目があります:

struct SurfaceOutput {
    half3 Albedo;    // 拡散反射光(=Diffuse)
    half3 Normal;    // 法線ベクトル
    half3 Emission;  // エミッション
    half Specular;   // スペキュラ
    half Gloss;      // 輝き
    half Alpha;      // 透過度
};

Albedo(アルベド)というのは聞きなれない言葉ですが、「天体の外部からの入射光に対する反射率」を表す天文学の用語のようです。意味はDiffuseと同じです。

 albedoに色情報を渡すとモデルに色が付きます。試しに適当な色を付けてみましょう:

void surf( Input IN, inout SurfaceOutput o ) {
    o.Albedo = half3( 0.5, 0.8, 0.6 );
}

注目は色もそうなんですが、ライティングがもう施されているという所です。コード内のどこにもそういう記述はありません。このライティング処理はサーフェイス関数(surf)の後にUnity側のライティングパイプラインで行われています。



A テクスチャを貼る

 サーフェイスシェーダ内でテクスチャを貼ってみましょう。テクスチャを貼るには、

・ Properetiesでテクスチャを一つ指定
・ テクスチャ名と同名のサンプラ変数を一つ定義
・ Input構造体に「uv*****」としてUVを入力するようにする
・ サーフェイスシェーダ内でtex2D関数を使う

以上をシェーダ内に実装します。試しに適当なテクスチャを貼ってみます:

Shader "Custom/TestShader2" {
    Properties {
        _HogeTex ( "Base", 2D ) = "white" {}
    }

    SubShader {
        CGPROGRAM

        #pragma surface surf Lambert

        sampler _HogeTex;

        struct Input {
            float2 uv_HogeTex;
            float4 vtxColor : COLOR;
        };

        void surf( Input IN, inout SurfaceOutput o ) {
            o.Albedo = tex2D( _HogeTex, IN.uv_HogeTex ).rgb;
        }

        ENDCG
    }
}

実際の描画はこんな感じです:

チェッカーフラグのようなテクスチャを適用しています。一応貼れていますね。ただ。白い所は実は透過度100%にしてあって、抜ける事を想定していたのですが、地の色が出ていて透過していません。サーフェイスシェーダで透過させるには、2ヵ所ほど追加設定をする必要があるんです。

 透過の仕方。まず、SubSurface内のTagsを追加します。そこで「"Queue" = "Transparent"(透過フェーズで描画して下さい)」と指示します。次に#pragma surfaceに「alpha」オプションを追加します。これでサーフェイスシェーダ関数内のSurfaceOutput.Alphaに透過値を指定すると、抜けてくれます:

Shader "Custom/TestShader2" {
    Properties {
        _HogeTex ( "Base", 2D ) = "white" {}
    }

    SubShader {
        Tags {
            "Queue" = "Transparent"
        }

        CGPROGRAM

        #pragma surface surf Lambert alpha

        sampler _HogeTex;

        struct Input {
            float2 uv_HogeTex;
            float4 vtxColor : COLOR;
        };

        void surf( Input IN, inout SurfaceOutput o ) {
            half4 color = tex2D( _HogeTex, IN.uv_HogeTex );
            o.Albedo = color.rgb;
            o.Alpha = color.a;
        }

        ENDCG
    }
}

これで先程のチェッカーフラグな球はこうなります:

お〜抜けました。ただ、今度は裏面が描画されていないので何か変な感じです。せっかくなら裏面も描画して穴開きな球体にしたいですよね。



A サーフェイスシェーダでマルチパス

 上のようになる直接の理由は簡単で、カリングがバックカリング、つまり「裏面を描画しない」というモードになっているからです。んじゃあカリングをしない(カリングオフ)にするとうまくいくのか?やってみましょう:

Shader "Custom/TestShader2" {
    Properties {
        _HogeTex ( "Base", 2D ) = "white" {}
    }

    SubShader {
        Tags {
            "Queue" = "Transparent"
        }

        Cull Off   // カリングオフ

        CGPROGRAM

        #pragma surface surf Lambert alpha

        sampler _HogeTex;

        struct Input {
            float2 uv_HogeTex;
            float4 vtxColor : COLOR;
        };

        void surf( Input IN, inout SurfaceOutput o ) {
            half4 color = tex2D( _HogeTex, IN.uv_HogeTex );
            o.Albedo = color.rgb;
            o.Alpha = color.a;
        }

        ENDCG
    }
}

これで描画してみます:

お、何かうまくいって…ん?暗い面(裏面)が前に来て、明るい面(表面)が背後に行って…?そう、前後関係がおかしくなってしまっています。

 上のような球体を前後関係を正しく裏面もちゃんと描画しようと思うと、1パス(1回の描画)では実はちょっと難しい所があります。どうするかというと、最初に裏面を描画し、次に表面を描画します。一つのモデルを2回に分けて描画するとうまくいくんです。こういうのをマルチパスレンダリングと言います。

 サーフェイスシェーダでマルチパスレンダリングをするには、一つのSubShader内にシェーダを2個書きます。上の例では、1回目でカリングをFront(前面カリング)に、2回目でカリングをBack(背面カリング)にします。コードは次の通りです:

Shader "Custom/TestShader2" {
    Properties {
        _HogeTex ( "Base", 2D ) = "white" {}
    }

    SubShader {
        Tags {
            "Queue" = "Transparent"
        }

        // First Pass
        Cull Front

        CGPROGRAM
        #pragma surface surf Lambert alpha

        sampler _HogeTex;

        struct Input {
            float2 uv_HogeTex;
            float4 vtxColor : COLOR;
        };

        void surf( Input IN, inout SurfaceOutput o ) {
            half4 color = tex2D( _HogeTex, IN.uv_HogeTex );
            o.Albedo = color.rgb;
            o.Alpha = color.a;
        }

        ENDCG


        // Second Pass
        Cull Back

        CGPROGRAM
        #pragma surface surf Lambert alpha

        sampler _HogeTex;

        struct Input {
            float2 uv_HogeTex;
            float4 vtxColor : COLOR;
        };

        void surf( Input IN, inout SurfaceOutput o ) {
            half4 color = tex2D( _HogeTex, IN.uv_HogeTex );
            o.Albedo = color.rgb;
            o.Alpha = color.a;
        }

        ENDCG
    }
}


CGPROGRAM〜ENDCGで囲った部分が2ヵ所あります。こうすると、サーフェイスシェーダは最初の方を1パス目、次を2パス目と判断してくれます。カリング設定をコードの流れ順で切り替えてくれるのは面白いですね。実際これで先程の球体を描画するとこうなりました:

背後にパネルを置いてみました。ちゃんと前後関係も正しく透過しているのが分かると思います。


 サーフェイスシェーダについて、この章では触りだけですが扱ってきました。機を追って色々なシェーダを実装していければと思います(^-^)