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

その7 波:ジオメトリレベルの波を作る準備

 波シリーズ、ここまでは「テクスチャレベルの波」、つまりハイトマップやノーマルマップ(バンプマップ)を作るための波を見てきました。バンプマップによる波を平たいプレートに適用すると、陰影により波が描き込まれます。そのテクスチャをUVスクロールで動かすと陰影も動き出し、波っぽいエフェクトとなります。しかし、この方法は振幅の大きい波を表現するにはちょっと無理があります。

 テクスチャ表現では厳しい程振幅が大きい波を表現するには「ジオメトリレベルの波」を作る必要があります。ジオメトリレベルの波とは細かく分割したプレートメッシュの頂点を直接動かしてしまう実直な方法です。これにより見た目にも本当にプレートがうねうねし出します。



@ メッシュの細かいプレートを作る

 まずはうねらす元であるプレートを作ります。これは正直どういうやり方で作っても良いかと思います(^-^;。一番簡単なのはモデラーソフト上で作ってしまいゲーム側に持ってきてしまう方法です。

 例えばBlenderで目の細かいメッシュを作成しFBXで出力、Unityに持っていく方法は以下の通りです。

 まず、ブレンダーを立ち上げたら、デフォルトで置いてある立方体を消します(選択してDeleteキー)。空っぽの作成空間を確保したら、[Create]タブにある[Mesh:Plane]を選択すると平たい四角ポリゴンが1枚空間に追加されます:

このプレートを細かく分割します。[Edit Mode]に切り替えたら、[Tools]タブにある[Add:Subdivide](細分化)をクリックします。すると平面が縦横に2等分されます。そのままもう一度Subdivideをクリックするとさらに2等分されて縦横4つに分割さます。後は必要な粒度まで細かくしていくだけです。メニューの右に出ているポリゴン数情報を参考にすると良いかと思います:

目安がある訳では無いのですが、粗いと波として非常に不自然になりますのでクオリティと相談です。出来れば三角ポリゴン数で1万枚以上は欲しい所です。

 続いて、Object Modeに戻り、このプレートをX軸で-90度回転させます。マイナス90度です。これはビュー画面の右上にある小さな「+」をクリックし、Transformにある[Rotation]のXに-90と入れるだけです。またその下にあるScaleをすべて0.5にしてプレートを半分の大きさにしてしまいます::

 なぜ-90度回転させて平面を立たせるのかというと、Blenderの座標系はZアップなのに対して、Unityの座標系がYアップなためです。要はUnityの上方向とプレートの上方向を一致させるための回転という事です。またスケールを半分にしたのは、BlenderのPlateはデフォルトで辺の長さが2のプレートなためです。これを1にする事で単位矩形となり、Unity側でのスケール操作が楽になります。

 ただし、実はこのままだとUnityへ持って行っても上手くいきません。最後に、この回転角度を「フリーズ」させます。フリーズというのは施した回転やスケールなどを実際のモデル頂点に適用して、それらを単位化(回転角度ゼロ、スケールは1)にしてしまう事を言います。これにより、このプレートは最初から辺の長さが1で立った状態のポリゴンメッシュとなります。

 フリーズをするには、画面左下の方にあるメニューの[Object]から[Apply]の[Rotation&Scale]を選択します。このApplyがフリーズ化です。これを選択するとプレートは立ったままなのに、先程設定した回転とスケールがデフォルト状態になっているのが分かると思います。これでプレートメッシュの完成です。

 プレートメッシュを作ったら[File]→[Export]から[FBX]を選びます。左下にある[Export FBX]内の項目に注目です:

 まずScale値を0.01に変更します。BlenderもUnityも長さの単位は1mを採用しているのですが、FBXでインポートするモデルについてはUnity側が勝手にFBX=1cmだと決めてしまっているため、Scale値を1にするとUnityのTransformのスケールに100を放り込んでくれます。かなり余計なお世話なので、ここで0.01にするとUnity側のスケールが1.0に補正されます。

 もう一つ出力項目を[Mesh]だけにします。デフォルトでEmptyやCameraなど全項目が出力されてしまいます。それをUnityに持っていくとカメラやライトなどが勝手に追加されてしまい面倒臭い事になります。Meshだけにすると純粋にモデル情報のみがFBXに出力されます。

 これで出力したFBXをUnityに持っていくとインポートされるので、後はそのインポートFBXをHierarchyにホイっとドラッグするだけです。ドラッグ直後はX軸を-90度してくれますが、それはもうBlender側でしたので0に直してあげると水平に戻ります:

上のスクリーンショットのプレートは、プレートのローカル座標が軸通りになっているかチェックするために色付けしてみたものです。X軸側が赤、Z軸側が青、でY軸側は描画される面(=面法線)になっていまして、しっかり対応出来ているのが分かります。これでばっちりです(^-^)



A 新規シェーダを立ち上げよう

 ジオメトリレベルの波はCPUではなくてGPUで直接計算します。その為シェーダが必須なので各環境で新規に用意します。DirectXやOpenGLであれば、腕まくりして「よーし作るか」と気合を入れて下さいw。ここではUnityで新規シェーダを立ち上げます。

 Unity Editorの[Project]ウィンドウのAssetsフォルダ以下の適当な所で右クリックし、[Create]→[Shader]→[Standard Unlit]を選択します。これでシンプルなモデル出力をするひな形コードが入ったシェーダが自動生成されます。これをカスタマイズするという魂胆です(今回作りたいのはサーフェイスシェーダではなくてShaderLabなので、Standerd Surface Shaderは選びません)。作るシェーダの名前は「WaterWave」とでもしておきましょう。

 このままでも良いのですが、ちょっと分類しておきたいので、シェーダを書き換えます。作成したWaterWaveシェーダをダブルクリックしてコード編集のエディタを立ち上げ、1行目にある「Shader "Unlit/WaterShader"」を「Shader "Custom/WaterWave"」に変更します。こうするとシェーダ選択時にCustomというカテゴリーが作られ、そこにWaterWaveシェーダが分類されます。

 次にWaterWaveシェーダへパラメータを与えるマテリアルを作成します。Assetsフォルダ以下で右クリックし[Create]→[Material]で新規マテリアル追加し適当に名前を付けます(ここでは「WaterWave」にしておきます)。デフォルトでStanderdシェーダがアタッチされているので、これを先ほど作成したWaterWaveシェーダに切り替えます:


Custom下にありますよね

切り替えると下のプレビューにある球体が真っ白になります。元のシェーダがUnlit、つまりライティング無しのシェーダであるためライトが反映されなくなった為です。これはすぐ後でちょっと追加します。

 このWaterWaveマテリアルを世界に追加したプレートの[MeshRenderer]コンポーネントの[Materials]に追加するとオブジェクトに反映されます:

これで新規シェーダをプレートに反映する準備が整いました。後はシェーダをいじれば即時反映されます。



B 法線情報とライティングを追加

 WaterWaveシェーダのベースにしたStandard Unlitシェーダはライティング等を行わない大変にシンプルなひな形です。このままだとモデルがシルエット状態になってしまうため、最低限のライティングだけ追加しておきましょう。

 ライティングを行うには法線の情報が必要です。まずこれをシェーダに入力されるようにするため、コード内のappdata構造体に法線を追加します:

struct appdata
{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
    float3 normal : NORMAL;
};

NORMALセマンティクスを付ければ入力に追加されます。続いて法線情報をフラグメントシェーダ(ピクセルシェーダ)で扱えるようにするため、頂点シェーダの出力にも法線を追加します。これはv2f構造体内にそれを追加するだけです:

struct v2f
{
    float2 uv : TEXCOORD0;
    UNITY_FOG_COORDS(1)
    float4 vertex : SV_POSITION;
    float3 normal : TEXCOORD2;
};

TEXCOORD2にしているのはTEXCOORD1がフォグ用に使われてしまっているためです。別にフォグを切っても良いのですが、冗長な改変なので今はやめておきます。

 次に頂点シェーダでローカル定義の法線の方向をワールド空間へ変換します。頂点シェーダに次の一行を追加します:

o.normal = UnityObjectToWorldNormal( v.normal );

UnityObjectToWorldNormal関数はUnityのビルトイン関数の一つで、名前の通り法線をワールド空間に変換してくれます。その変換後の値をフラグメントシェーダに渡しています。

 フラグメントシェーダでは渡された法線の方向とライトの向きからポリゴン表面の光の強さを計算します:

fixed4 frag (v2f i) : SV_Target
{
    // sample the texture
    fixed4 col = tex2D(_MainTex, i.uv);

    // ディフューズ
    float3 lightDir = normalize( _WorldSpaceLightPos0.xyz );
    float diffusePower = dot( normalize( i.normal ), lightDir );
    col.rgb = max( 0.0, diffusePower ) * _LightColor0.rgb * col.xyz;

    // apply fog
    UNITY_APPLY_FOG(i.fogCoord, col );
    return col;
}

 太文字部分が簡単なライティング処理の箇所です。_WorldSpaceLightPos0はライトの位置が格納されているfloat4型のビルトイン変数なのですが、ディレクショナルライトの場合xyz成分にライトへの方向(太陽の方向)が格納されます。

 頂点シェーダから渡されてきた法線(i.normal)とライトの方向の内積を取り、表面の拡散反射光の強さを計算します(ランバート反射)。後はそれをテクスチャ色(col)に掛け算するだけです。上の例ではさらにライト色も反映させています(乗算合成)。尚、ライトの色である_LightColor0を使うにはこの変数をuniformとして明示的に定義しておく必要があります。またこのパラメータに正しい値が入るようにするため、Tagにライトモードを"FowardBase"として追加します:

Tags {
    "RenderType"="Opaque"
    "LightMode" = "ForwardBase"
}

...

#include "UnityCG.cginc"

uniform float4 _LightColor0;

これで簡易的なライティングが入るようになります:


 これでシェーダを書いて色々と遊べる準備が整いました。次章で実際にこのプレートを凸凹させてみましょう。


本章で説明した法線計算だけした空に近いシェーダコード(ランバート反射)全文を掲載しておきます:

Shader "Custom/WaterWave"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags {
            "RenderType"="Opaque"
            "LightMode" = "ForwardBase" // ライトモードはFowardに
        }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float3 normal : TEXCOORD2;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            uniform float4 _LightColor0; // ディレクショナルライトカラー

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                o.normal = UnityObjectToWorldNormal( v.normal ); // 法線をワールド空間へ
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);

                // ディフューズ
                float3 lightDir = normalize( _WorldSpaceLightPos0.xyz );
                float diffusePower = dot( normalize( i.normal ), lightDir );
                col.rgb = max( 0.0, diffusePower ) * _LightColor0.rgb * col.xyz;

                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}