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

その8 波:プレートの頂点を揺らして波にする

 ジオメトリレベルの波は細かくメッシュ化されたプレートの頂点を直接動かすという実直な方法で実現します。頂点を動かすのでこれは頂点シェーダでのお仕事となりますが、陰影も必要なためピクセルシェーダとも連携して表現する事になります。



@ メッシュの頂点座標を直接変更する

 頂点シェーダは言わずもがなですがローカル空間で定義されたポリゴンの頂点をあれこれ移動させるフェーズです。通常はワールドビュー射影変換を掛けて描画空間へ変換するのですが、入力されてきた頂点座標を直接変更する事ももちろん可能です。

 前章で新規作成したランバート反射のみの空っぽシェーダにある頂点シェーダ部分をちょっと見てみましょう:

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;
}

 v.vertexにモデルを構成するポリゴンのローカル頂点座標が入力されてきます。モデルがY軸方向を法線とするプレート(前章でBlenderで作成)とすると、ここには、

という座標が来ているはずです。このxとz成分から何らかの方法で高さyを計算してここに入れ直せば、平面だったプレートがぐにゃぐにゃしだすという訳です。例えば、次の式を高さとしてみます:

頂点シェーダに上の式を書き込みます:

v2f vert (appdata v)
{
    v2f o;
    const float PI = 3.1415926535f;
    v.vertex.y = 0.05f * sin( 3.0f * v.vertex.x * 2.0f * PI );
    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;
}

これだけでプレートが次のようにsinカーブを描き出します:

この「高さをシェーダ内で計算して水面の波を表現しよう」というのがジオメトリレベルの波のすべてです!



A (x, z)の値に対応した直進波を発生させてみる

 では実際に波打たせてみましょう。ある座標(x, z)に対応した波の高さの計算方法は既にその4でガッツリ説明しました。環状波でも良いのですが、ジオメトリレベルの波は大きなうねりのある直進的な波を表現するのに良く使われるため、ここでは直進波の式を組み込んでみます。その4で導き出した直進波の高さの式(XZ平面版)を以下に示します:

 Hが求める波の高さです。Lは波長で任意の長さを指定出来ます。Qは「波やべぇ係数」で、波高の激しさを0〜1の範囲で指定します。0にすると全く波立たず、1にすると自然の波の限界近い波長の1/7の波高に相当する波の高さになります。dは波の基準ラインから(x, z)までの距離を計算しています。Bは波の進む方向ベクトルです。gは重力加速度(9.8m/s^2)、そしてtは経過時刻となります。詳細はその4をご参照ください。

 この式をそのまんま頂点シェーダ内にガリっと書いてテストしてみます:

v2f vert (appdata v)
{
    v2f o;

    // ローカル座標のY値=波の高さを(x,z)から計算
    float _t = 0.0f;
    float grav = 9.8f;
    float _ampRate = 1.0f;
    float _waveLen = 0.2f;
    float _2pi_per_L = 2.0f * 3.14159265f / _waveLen;
    float2 _browDir = float2( 0.4f, 0.7f );
    _browDir = normalize( _browDir );
    float d = dot( _browDir, v.vertex.xz );
    float H = _ampRate * _waveLen / 14.0f * sin( _2pi_per_L * d - sqrt( _2pi_per_L * grav ) * _t );
    v.vertex.y = H;

    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;
}

 アンダーバーが付いた変数は後々でシェーダ外部から与えるつもりの物です。時刻も外部から与えます。波の絶対的な速さをそれで調整出来ます。このシェーダを書いてUnity Editorに戻るとプレートがこんな感じに波打ちました:

 うねっております(^-^)。風の向きBを変えれば任意の方向に直進波が出来ますし、時刻tを進めれば波の波頂位置が平行移動していきます。

 所で、上の波をワイヤーフレームではなくてポリゴンとして描画するとどうなるか?こうなります:

 陰影が付いていません。理由は頂点を動かした後の法線を計算していないからです。そうなんです、頂点を動かすジオメトリレベルの波では、陰影を付けるために法線の計算もセットで行わないといけないんです。



B 直進sin波の法線

 Aの直進sin波の式には(x,z)の2つの座標成分があります。そういう関数からある点(x,z)での法線を求めるには「偏微分」を求める必要があります。

 まず先程の式のごちゃごちゃした係数を整理して、次のように書き直します:

 A〜Dはすべて係数です。次にこの関数Hをx及びzで偏微分します。Hの右辺は単なるsin関数なので高校数学の微分で簡単に解けます。偏微分は以下の通りです:

偏微分というのはある軸方向の傾きです。上の2つの偏微分は点(x,z)でのそれぞれその軸方向に1進んだ時の高さHの変化量になっています。それをベクトルで書くと、

  

となります。この2つのベクトルTとBは点(x.z)を通る接平面に共に含まれています。求めたいのはその接平面の法線です。法線はTにもBにも直行しているので、TとBの外積で求める事が出来ます:

左手系の場合は上の式となります。

 この法線を求める式を頂点シェーダに追加します:

v2f vert (appdata v)
{
    v2f o;

    ...

    // 頂点法線を算出
    float A = _ampRate * _waveLen / 14.0f;
    float C = 2.0f * 3.14159265f / _waveLen;
    float D = sqrt( C * grav ) * _t;
    float cosV = cos( C * dot( _browDir, v.vertex.xz ) - D );
    float dx = A * C * _browDir.x * cosV;
    float dz = A * C * _browDir.y * cosV;
    float3 normal = normalize( float3( -dx, 1.0f, -dz ) );


    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    UNITY_TRANSFER_FOG(o,o.vertex);

    o.normal = UnityObjectToWorldNormal( normal );
    return o;
}

法線は正規化してワールド座標系へ変換しておきます。法線を計算したので波には陰影が付くようになります:


 という事で、真っ平らだったプレートが綺麗に波立ちました。上は直進波の式を使いましたが、環状波も全く同じプロセスで高さと法線を算出する事が出来ます。一応高さの式と法線を求めるための偏微分の式を掲載しておきますね:

A,C,Dは直進波の時と同じです。直進波と環状波は距離dの計算だけが異なるだけで、Hの式の形自体は双方同じです。上式をシェーダに同じように書くと、

綺麗に環状波になりました(^-^)

 ところで、その5「波頭は尖る!」で波はトロコイドというsin波よりも尖りのある曲線になるというお話をしましたが、ジオメトリレベルの波頭もやっぱり尖った方が波らしさが増すんです。そこで、次の章ではジオメトリで尖った波を実現できる「ゲルストナー波(Gerstner wave)」をご紹介します。