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

Shader編
その4 サーフェイスシェーダでガラスを作る


 実践としてサーフェイスシェーダでガラスを作ってみます。うまくいくかな…(^-^;



@ ガラスたらしむ要素

 ガラスは透明です。透明と言う事は向こうが透けて見える訳ですが、ただ透けるだけだと「半透明の何か」にしかなりません:

これはサーフェイスシェーダのSurfaceOutput.Alphaに0.3を単純に入れて出力した画です(地の色は白)。…透明な何かでして、ガラスっぽくはありませんよね…(-_-;。ガラスをガラスたらしめている要素は何なのでしょうか?

 家にあるコップを撮影してみました。これを見ると、まずガラスというのは大変に透明度が高いのがわかります。しかし、その透明度は場所によって異なっているのもわかります。コップの縁の方は背景が透けておらず、周囲の景色がかなりくっきり映り込んでいます。逆にコップの中心は周囲の映り込みは殆ど無く、背景が透けています。また、コップの内側の景色が歪んでいます。これは屈折が起こっているためです。歪みはコップの縁ほど強いですね。

 まとめると、

・ 縁は不透明で周囲の景色が映る
・ 中央(正面気味の所)は透明で背景が透ける
・ 景色が歪む

こういう要素があるとガラスっぽく見えそうです。これらを一つずつ試行錯誤しながら入れて行きましょう。



A 景色を面に映す(環境マップ)

 ガラス化させたいモデルの表面に周囲の景色を映したいのですが、シェーダは仕組み上自分の周囲を見渡す事ができません。そこで「環境マップ」という周囲の景色を予め映した特殊なテクスチャが良く使われます。Unityももちろん環境マップを使えます。

 環境マップにはいくつか種類があるのですが、比較的使いやすいのがキューブマップです。これは自分の前後左右上下の6面(立方体)に景色を投影したものです。Unityにはキューブマップ用のサンプル素材がありますので、今はそれを使う事にしましょう。

 Unityのキューブマップサンプルを使うには、メニューの[Assets]→[Import Package]→[Skyboxes]を選択して、Importing Packageウィンドウが開いたら右下にある[Import]をクリックすると、Projectにスカイボックスのパッケージがインポートされます:

今回は「Sunny1 Skybox」を使う事にします。上に並んでいるのはマテリアルで、これはキューブテクスチャではありません。元テクスチャはTexturesフォルダ内にあります。

 キューブテクスチャは6枚の2Dテクスチャで1つのテクスチャとなります。Unityはキューブテクスチャを作る事ができて、Projectウィンドウ内で右クリックし[Create]→[Cubemap]とすると新規のキューブマップが作成されます。続いて上の[Textures]→[Sunny1]の中にあるテクスチャをそのキューブマップの中にドラッグしていきます:

Sunny1_backがBack(-Z)、Sunny1_downがBottom(-Y)など該当箇所にドラッグしていきます。すると…:

Previewにきれ〜な球体ができあがります。ぐりぐり動かしてみると分かりますが、境目もなく綺麗に周囲の世界が出来あがっています。この見た目、もう何だかガラスっぽいですよね。これをシェーダ内で再現するのがガラスを作る第一歩です。

 では、シェーダ側の話へ。まず作ったキューブテクスチャをシェーダ内で読み込んでみましょう。それにはPropertiesの中に、

Properties {
    _EnvMap ("EnvMap", Cube) = "white" {}
}

とキューブテクスチャを受け付けるようにします。キューブテクスチャもテクスチャなので、UV的な物を指定すると色を取得する事ができます。キューブマップのUV的な物はキューブマップの中心点から伸びる「方向ベクトル」です。その方向ベクトルの先にある色が取られるわけです。

 UnityのSurfaceシェーダは嬉しい事に、視線ベクトルがポリゴン面に反射した時の反射ベクトルをデフォルトで計算してくれています。それをSurfaceシェーダ内で取得するには、Input構造体に「worldRefl」という変数名を追加します:

struct Input {
    float3 worldRefl;
};

そして組み込み関数であるtexCUBE関数にこの反射ベクトルworldReflとキューブテクスチャ_EnvMapを渡すと、その面に反射する背景の色を取得してくれます:

Shader "Custom/Glass" {
    Properties {
        _EnvMap ("EnvMap", Cube) = "white" {}
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf Lambert

        samplerCUBE _EnvMap;

        struct Input {
            float3 worldRefl;
        };

        void surf (Input IN, inout SurfaceOutput o) {
            o.Albedo = texCUBE( _EnvMap, IN.worldRefl ).rgb;
            o.Alpha = 1.0;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

 このシェーダを適用してみると…:

てっかてかの球体が出来あがりました(^-^)。キューブテクスチャはUVに関係なく反射ベクトル(実際は視線ベクトルと法線ベクトルから計算されます)だけで貼り付ける事ができるんです。

 さて、これはこれで楽しいのですが、上の球体はガラスではなくて鏡です。本当はこんな感じが理想です:


※加工させて頂きました。元写真はこちら(http://shibanomaru.blog43.fc2.com/blog-entry-659.html)

 これを見ると先程の鏡面球体とはいくつか違いがあるのがわかります。まず何と言っても、景色が逆さまに映っているのは大きな違いです。これは球体のガラスが視線の先を屈折させるために起こっています。これを実現するには「屈折ベクトル」を導入しなければなりません。ありがたい事に、シェーダには屈折を扱うrefract関数が組み込みで用意されています。

 周りの景色自体も球体に反射して映っていますが、これは案外うっすらとしか映らないのもわかります。というより屈折光が強すぎて反射光を見え辛くしているという感覚でしょうか。かろうじて球の縁辺部分が反射光をそれなりに映しています。ガラスっぽさのポイントは「屈折」にありそうです。



B 屈折光

 屈折効果はrefract関数を使うと比較的簡単に実現できます。ただし、これもキューブテクスチャを反映するだけで、自分の周囲にあるものを屈折させて映すわけではない事に注意です。

 refract関数には「視線ベクトル(正規化)」と「法線」そして「屈折率」を与えます。Surfaceシェーダでは視線ベクトルも法線もInput構造体の引数に渡す事ができます:

struct Input {
    float3 worldRefl;
    float3 viewDir;      // 視線ベクトル
    float3 worldNormal;  // 法線ベクトル
};

 屈折率ですが、これは「スネルの法則」という法則の式から求めます。これは、

という比率の式です。分母のRefract_Inは入射先の物質の屈折係数(Index of refraction)で、Refract_Outは出光先の物質の屈折係数です。屈折係数は物質によって決まっていて、Wikipediaの「屈折率」によるとガラスはおよそ1.5くらい、空気は1.000292のようです。今ガラス内に入った光が空気中に出ると考えるので、1.0/1.5=0.667が屈折率となります。

 という事で、Surfaceシェーダの中に「refract関数」を追加してみましょう:

Shader "Custom/Glass" {
    Properties {
        _EnvMap ("EnvMap", Cube) = "white" {}
    }

    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf Lambert

        samplerCUBE _EnvMap;

        struct Input {
            float3 worldRefl;
            float3 viewDir;
            float3 worldNormal;
        };

        void surf (Input IN, inout SurfaceOutput o) {
            float3 refractVect = refract( normalize( IN.viewDir ), normalize( IN.worldNormal ), 0.667 );
            o.Albedo  = texCUBE( _EnvMap, refractVect ).rgb;
            o.Albedo += texCUBE( _EnvMap, IN.worldRefl ).rgb * 0.1;
            o.Alpha = 1.0;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

 追加した所を太文字で示しました。屈折ベクトル(refractVect)をrefract関数から求めています。視線ベクトル(IN.viewDir)も法線(IN.worldNormal)も正規化している所に注意です。こうして求めた屈折ベクトルを使ってキューブテクスチャの色を取得すると屈折を再現できます。上のシェーダではそこに反射成分も10%だけ足しこんでいます。

 この段階の描画結果はこんな感じになります:

先程と違い空の転置が逆転して描画されていますね。これがrefract関数で得た屈折ベクトルによるものです。良く見ると球体の上側にうっすらと空が映っています。これが10%だけ足しこんだ反射ベクトルによる部分。ん〜大分らしくなってきましたが、ガラスの大切な要素である「縁辺部分の映り込みはより強い」という所がまだ入っていません。



C 縁辺部ほど反射を強く

 縁辺部分というのは要は「視線に対してそっぽを向いている面」、もう少し算数で言えば「視線ベクトルと法線ベクトルの角度が90度に近い面」という事です。これは視線ベクトルと法線ベクトルとの内積(dot)を取ると計算できます。面が正面を向いていると内積の結果は1に近くなります。逆に計算結果が0に近い程面が縁辺であると判断します。

 試しに内積の計算値(を逆にしたもの)をそのまま反射光の色の足し込み率として採用してみます:

void surf (Input IN, inout SurfaceOutput o) {
    float3 refractVect = refract( normalize( IN.viewDir ), normalize( IN.worldNormal ), 0.667 );
    o.Albedo = texCUBE( _EnvMap, refractVect ).rgb;

    float margin = 1.0 - dot( normalize( IN.viewDir ), normalize( IN.worldNormal ) );
    o.Albedo += texCUBE( _EnvMap, IN.worldRefl ).rgb * margin;

    o.Alpha = 1.0;
}

こうすると、

おっ、ちょっとらしくなってきました。ただ、どうも何かが足りない気がします。それは「輝き」かなと思います。明るい部分がピカーっと光る感じです。光るというとEmission(自発光)が使えるかもしれません。そこで、先程までAlbedoとして出力していた色をEmissionに変えてみます:

void surf (Input IN, inout SurfaceOutput o) {
    float3 refractVect = refract( normalize( IN.viewDir ), normalize( IN.worldNormal ), 0.667 );
    o.Emission = texCUBE( _EnvMap, refractVect ).rgb;

    float margin = 1.0 - dot( normalize( IN.viewDir ), normalize( IN.worldNormal ) );
    o.Emission += texCUBE( _EnvMap, IN.worldRefl ).rgb * margin;

    o.Alpha = 1.0;
}

これで描画すると:

おお!凄くそれっぽくなりました。

 今は屈折光に対して縁辺部の反射光を足し算しています。こうすると縁辺部の色味が白く飛びがちになります。上の画が実際にそうですよね。でも実際はきっと「縁辺ほど屈折光は見えにくくなって、その分反射光が見えやすくなる」んじゃないかなと思います。なので、屈折光成分を縁辺部に行く程小さくします:

void surf (Input IN, inout SurfaceOutput o) {
    float margin = 1.0 - dot( normalize( IN.viewDir ), normalize( IN.worldNormal ) );

    float3 refractVect = refract( normalize( IN.viewDir ), normalize( IN.worldNormal ), 0.667 );
    o.Emission = texCUBE( _EnvMap, refractVect ).rgb * ( 1.0 - margin );

    o.Emission += texCUBE( _EnvMap, IN.worldRefl ).rgb * margin;
    o.Alpha = 1.0;
}

うん、落ち着きました。簡単ですがこれだけでも十分にガラスっぽいです。



D スカイボックスを設定

 試しにGame画面にキューブテクスチャを「天球」として充ててみましょう。天球はカメラにつける事で実現できます。

 Hierarchy上にあるカメラ(メインカメラ)を選択し、メニューの[Component]→[Renderling]から[Skybox]をクリックしてカメラにSkyboxコンポーネントをアタッチします。後はキューブテクスチャを適用したMaterial(キューブテクスチャそのものではないです)をSkyboxコンポーネントのInspectorにある「Custom Skybox」にドロップすると、Game画面全体にスカイボックスが張りめぐらされます:

おほほ、ガラスっぽい(^-^)



 シェーダの反射・屈折はあくまでもキューブテクスチャを反映するに過ぎないためフェイクなのは否めません。でも、私達は案外反射している物をちゃんと見ていないもんで、何となく景色っぽいのが反射していると「あ、ガラスっぽい」とか「金属っぽい」と感じます。うまくフェイクして画面のリッチ感を稼いで下さい(^-^)