ゲルストナー波(Gerstner Wave)

ゲルストナー波(Gerstner Wave)

※2019/06/16 各シェーダを修正しました。

 海洋の荒い波は単純なsin波とは異なり、波の山は鋭く、谷はより広く平になります。このような波はゲルストナー波を用いることで表現することができます。以下にGPU Gems:Chapter 1. Effective Water Simulation from Physical Modelsに掲載されているゲルストナー波の式を示します。3次元における表面の座標位置\(P(x,y)\)は以下の通りです。

\( \boldsymbol{P}(x,y,t)= \left[ \begin{align} &x+\sum(Q_{i}A_{i}\times\boldsymbol{D_{i}}.x\times cos(w_{i}\boldsymbol{D_{i}}\cdot (x,y)+\varphi_{i}t)),\\ &y+\sum(Q_{i}A_{i}\times\boldsymbol{D_{i}}.y\times cos(w_{i}\boldsymbol{D_{i}}\cdot (x,y)+\varphi_{i}t)),\\ &\sum(A_{i}sin(w_{i}\boldsymbol{D_{i}}\cdot(x,y)+\varphi_{i}t)) \end{align} \right] \)

\(Q_{i}\)は波の鋭さ、\(A_{i}\)は振幅、 \(\boldsymbol{D}\)は波の進行方向、\(w\)は波数、\(\varphi\)は角周波数です。また、従法線ベクトル\(\boldsymbol B\)及び接ベクトル\( \boldsymbol T\)はそれぞれ以下の式で表せます。

\(\boldsymbol{B}= \left[ \begin{align} &1-\sum(Q_{i}\times \boldsymbol{D_{i}}.x^{2}\times WA \times S),\\ &-\sum(Q_{i}\times \boldsymbol{D_{i}}.x\times \boldsymbol{D_{i}}.y\times WA \times S),\\ &\sum(\boldsymbol{D_{i}}.x\times WA \times C) \end{align} \right] \)

\(\boldsymbol{T}= \left[ \begin{align} &-\sum(Q_{i}\times \boldsymbol{D_{i}}.x\times \boldsymbol{D_{i}}.y\times WA \times S),\\ &1-\sum(Q_{i}\times \boldsymbol{D_{i}}.y^{2}\times WA \times S),\\ &\sum(\boldsymbol{D_{i}}.y\times WA \times C) \end{align} \right] \)

ここで、\(WA\)、\(S\)及び\(C\)は以下の通りです。

\( \begin{align} &WA=w_{i}\times A_{i}\\ &S=sin(w_{i}\boldsymbol{D_{i}}\cdot \boldsymbol{P}+\varphi_{i}t))\\ &C=cos(w_{i}\boldsymbol{D_{i}}\cdot \boldsymbol{P}+\varphi_{i}t)) \end{align} \)

これらの式を用いてシェーダにより海洋の波を表現します。

shder

 頂点シェーダ内で座標位置、従法線ベクトル及び接ベクトルを計算しています。求めた座標位置により平面の各頂点を移動しています。さらに、従法線ベクトルと接ベクトルの外積を計算し、法線を求めています。フラブメントシェーダ内ではライティングの計算及び当ブログに掲載したグリッドシェーダによってグリッド状の線を表示する処理を行っています。

Shader "Ocean/GerstnerWave_local_1"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "gray" {}
		_Color ("MainColor", Color) = (0, 0, 0, 1)
		[MaterialToggle]
		_ShowGrid ("Show Grid", Int) = 1
		_GridColor ("GridColor", Color) = (1, 1, 1, 1)
		_GridSize ("GridSize", Range(0, 20)) = 1
		_LineWidth ("LineWidth", Range(0.4, 1)) = 0.1
		_Edge ("Edge", Range(0.001, 0.8)) = 0.1

		_Amplitude ("Amplitude", Vector) = (0.8, 0.8, 0.4, 0.9)
		_Frequency ("Frequency", Vector) = (0.4, 1.8, 1.0, 1.2)
		_Steepness ("Steepness", Vector) = (0.2, 0.3, 0.7, 0.4)
		_Speed ("Speed", Vector) = (20, 30, 10, 30)
		_DirectionA ("Wave A(X,Y) and B(Z,W)", Vector) = (0.47, 0.35, -0.96, 0.23)
		_DirectionB ("C(X,Y) and D(Z,W)", Vector) = (0.77, -1.47, -0.3, -0.2)

		_Shininess ("Shininess", Range(0 ,1)) = 0.7
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }

		Pass
		{
 			Tags { "LightMode"="ForwardBase" }

			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"

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

			struct v2f
			{
				float2 uv : TEXCOORD0;
				float4 vertex : SV_POSITION;
				float3 normal : NORMAL;
				float4 pos : TEXCOORD1;
				float4 scrPos : TEXCOORD2;
				float3 worldPos : TEXCOORD3;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;

			int _ShowGrid;
			float4 _GridColor;
			float4 _Color;
			float _GridSize;
			float _LineWidth;
			float _Edge;

			float4 _Amplitude;
			float4 _Frequency;
			float4 _Steepness;
			float4 _Speed;
			float4 _DirectionA;
			float4 _DirectionB;

			static const float amp[4] = {_Amplitude.x, _Amplitude.y, _Amplitude.z, _Amplitude.w};
			static const float freq[4] = {_Frequency.x, _Frequency.y, _Frequency.z, _Frequency.w};
			static const float steep[4] = {_Steepness.x, _Steepness.y, _Steepness.z, _Steepness.w};
			static const float speed[4] = {_Speed.x, _Speed.y, _Speed.z, _Speed.w};
			static const float2 dir[4] = {_DirectionA.xy, _DirectionA.zw, _DirectionB.xy, _DirectionB.zw};

			float4 _LightColor0;
			float _Shininess;

			float3 GerstnerWave(float2 amp, float freq, float steep, float speed, float2 dir, float2 v, float time)
			{
				float3 p;
				float2 d = normalize(dir.xy);
				float q = steep;
				float f = (dot(d, v) * freq + time * speed);
				p.xz = q * amp * d.xy * cos(f);
				p.y = amp * sin(f);

				return p;
			}

			float3 CalcBinormal(float2 amp, float freq, float steep, float speed, float2 dir, float2 v, float time){
				float2 d = normalize(dir.xy);
				float q = steep;
				float wa = freq * amp;

				float pf = dot(d, v) * freq + time * speed;
				float sp = sin(pf);
				float cp = cos(pf);

				float3 binormal;
				binormal.xz = -q * d.x * d.xy * wa * sp;
				binormal.y = d.x * wa * cp;
				
				return binormal;
			}

			float3 CalcTangent(float2 amp, float freq, float steep, float speed, float2 dir, float2 v, float time){
				float2 d = normalize(dir.xy);
				float q = steep;
				float wa = freq * amp;

				float pf = dot(d, v) * freq + time * speed;
				float sp = sin(pf);
				float cp = cos(pf);
				
				float3 tangent;
				tangent.xz = -q * d.xy * d.y * wa * sp;
				tangent.y = d.y * wa * cp;

				return tangent;
			}

			v2f vert (appdata v)
			{
				v2f o;
				float4 vt = v.vertex;
				float3 g_binormal = float3(1.0, 0.0, 0.0);
				float3 g_tangent = float3(0.0, 0.0, 1.0);

				float time = _Time.x;

				float3 p = 0.0;
				for(int i = 0; i < 4; i++){
					p += GerstnerWave(amp[i], freq[i], steep[i], speed[i], dir[i], vt.xz, time);
					g_binormal += CalcBinormal(amp[i], freq[i], steep[i], speed[i], dir[i], vt.xz, time);
					g_tangent += CalcTangent(amp[i], freq[i], steep[i], speed[i], dir[i], vt.xz, time);
				}

				vt.xz += p.xz;
				vt.y = p.y;

				o.normal = normalize(cross(g_tangent, g_binormal));

				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				o.vertex = UnityObjectToClipPos(vt);
				o.pos = vt;
				o.worldPos = mul(unity_ObjectToWorld, vt).xyz;
				o.scrPos = ComputeScreenPos(o.vertex);

				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				float4 pos = i.pos;

				//GridLine
				float4 scrPos = i.scrPos;
				float lw = _LineWidth * scrPos.z * scrPos.z * 11;
				float eg = _Edge;
				float gs = _GridSize;
				float3 f = smoothstep(lw, eg + lw, frac(pos.xyz * gs))
						 + smoothstep(1 - lw, 1 - eg - lw, frac(pos.xyz * gs));
				f *= step(0.001, fwidth(pos.xyz));
				float grid = max(f.x, f.z);

				//lighting
				float3 worldPos = i.worldPos;
				float3 normal = UnityObjectToWorldNormal(i.normal);

				float3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
				float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
				float3 halfDir = normalize(lightDir + viewDir);

				float diff = saturate(dot(normal, lightDir)) * _LightColor0;
				float spec = pow(max(0.0, dot(normal, halfDir)), _Shininess * 128.0) * _LightColor0;

				fixed4 col = tex2D(_MainTex, i.uv) * _Color;
				col.rgb = col.rgb * diff + spec;
				grid *= _ShowGrid;
				col.rgb = col.rgb * (1.0 - grid) + grid * _GridColor.rgb;
				return col;
			}
			ENDCG
		}
	}
}

GPU Gemsに掲載されている式とシェーダではy軸とz軸が逆となります。そのため、座標位置、従法線ベクトル及び接ベクトルのy成分とz成分を入れ替えて計算しています。また、法線ベクトル\(\boldsymbol{N}\)は、以下の式に示すように従法線ベクトル \(\boldsymbol{B}\)と接ベクトル\(\boldsymbol{N}\)の外積を計算することで求まります。

$$ \boldsymbol{N}=\boldsymbol{B}\times \boldsymbol{T}= \begin{pmatrix} b_{1}\\ b_{2}\\ b_{3} \end{pmatrix} \times \begin{pmatrix} t_{1}\\ t_{2}\\ t_{3} \end{pmatrix} =\begin{pmatrix} b_{2}t_{3}-b_{3}t_{2}\\ b_{3}t_{1}-b_{1}t_{3}\\ b_{1}t_{2}-b_{2}t_{1} \end{pmatrix} $$

シェーダではy成分とz成分が逆となるため法線ベクトル\(\boldsymbol{N'}\)は以下の式となります。

$$ \boldsymbol{N'}= \begin{pmatrix} b_{2}t_{3}-b_{3}t_{2}\\ b_{1}t_{2}-b_{2}t_{1}\\ b_{3}t_{1}-b_{1}t_{3} \end{pmatrix} $$

この法線ベクトル \(\boldsymbol{N'}\)をy成分とz成分を入れ替えた従法線ベクトル\(\boldsymbol{B'}\)と接ベクトル \(\boldsymbol{T'}\) を用いて計算します。式は以下の通りです。

$$ \boldsymbol{N'}=\boldsymbol{T'}\times \boldsymbol{B'}= \begin{pmatrix} t_{1}\\ t_{3}\\ t_{2} \end{pmatrix} \times \begin{pmatrix} b_{1}\\ b_{3}\\ b_{2} \end{pmatrix} =\begin{pmatrix} b_{2}t_{3}-b_{3}t_{2}\\ b_{1}t_{2}-b_{2}t_{1}\\ b_{3}t_{1}-b_{1}t_{3} \end{pmatrix} $$

このように、従法線ベクトルと接ベクトルを逆にして外積を計算することでy成分とz成分を入れ替えた法線ベクトルを計算することができます。

実行結果

 上記シェーダの実行結果は以下の通りです。sin波より鋭い山となっていることがわかります。

以下のように数値を変更すると

_Amplitude ("Amplitude", Vector) = (1.0, 0.0, 0.0, 0.0)
_Frequency ("Frequency", Vector) = (2.0, 1.8, 1.0, 1.2)
・・・
float time = 0.03;

となります。メッシュのエッジ部分が明るくなっており、きれいにライティングができていないことがわかります。

ベクトル計算を変更

 ライティングをきれいに表示するため、各種ベクトルを頂点シェーダではなくフラグメントシェーダで計算するように変更しました。

shader

 従法線ベクトルと接ベクトルをフラグメントシェーダ内で計算し、法線ベクトルを求めています。

Shader "Ocean/GerstnerWave_local_2"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "gray" {}
		_Color ("MainColor", Color) = (0, 0, 0, 1)
		[MaterialToggle]
		_ShowGrid ("Show Grid", Int) = 1
		_GridColor ("GridColor", Color) = (1, 1, 1, 1)
		_GridSize ("GridSize", Range(0, 20)) = 1
		_LineWidth ("LineWidth", Range(0.4, 1)) = 0.1
		_Edge ("Edge", Range(0.001, 0.8)) = 0.1
		_Thickness("Thickness", Float) = 1

		_Amplitude ("Amplitude", Vector) = (0.8, 0.8, 0.4, 0.9)
		_Frequency ("Frequency", Vector) = (0.4, 1.8, 1.0, 1.2)
		_Steepness ("Steepness", Vector) = (0.2, 0.3, 0.7, 0.4)
		_Speed ("Speed", Vector) = (20, 30, 10, 30)
		_DirectionA ("Wave A(X,Y) and B(Z,W)", Vector) = (0.47, 0.35, -0.96, 0.23)
		_DirectionB ("C(X,Y) and D(Z,W)", Vector) = (0.77, -1.47, -0.3, -0.2)

		_Shininess ("Shininess", Range(0 ,1)) = 0.7
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }

		Pass
		{
 			Tags { "LightMode"="ForwardBase" }

			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"

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

			struct v2f
			{
				float2 uv : TEXCOORD0;
				float4 vertex : SV_POSITION;
				float4 pos : TEXCOORD1;
				float4 scrPos : TEXCOORD2;
				float3 localPos : TEXCOORD3;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;

			int _ShowGrid;
			float4 _GridColor;
			float4 _Color;
			float _GridSize;
			float _LineWidth;
			float _Edge;

			float4 _Amplitude;
			float4 _Frequency;
			float4 _Steepness;
			float4 _Speed;
			float4 _DirectionA;
			float4 _DirectionB;

			static const float amp[4] = {_Amplitude.x, _Amplitude.y, _Amplitude.z, _Amplitude.w};
			static const float freq[4] = {_Frequency.x, _Frequency.y, _Frequency.z, _Frequency.w};
			static const float steep[4] = {_Steepness.x, _Steepness.y, _Steepness.z, _Steepness.w};
			static const float speed[4] = {_Speed.x, _Speed.y, _Speed.z, _Speed.w};
			static const float2 dir[4] = {_DirectionA.xy, _DirectionA.zw, _DirectionB.xy, _DirectionB.zw};

			float4 _LightColor0;
			float _Shininess;

			float3 GerstnerWave(float2 amp, float freq, float steep, float speed, float2 dir, float2 v, float time)
			{
				float3 p;
				float2 d = normalize(dir.xy);
				float q = steep;
				float f = (dot(d, v) * freq + time * speed);
				p.xz = q * amp * d.xy * cos(f);
				p.y = amp * sin(f);

				return p;
			}

			float3 CalcBinormal(float2 amp, float freq, float steep, float speed, float2 dir, float2 v, float time){
				float2 d = normalize(dir.xy);
				float q = steep;
				float wa = freq * amp;

				float pf = dot(d, v) * freq + time * speed;
				float sp = sin(pf);
				float cp = cos(pf);

				float3 binormal;
				binormal.xz = -q * d.x * d.xy * wa * sp;
				binormal.y = d.x * wa * cp;
				
				return binormal;
			}

			float3 CalcTangent(float2 amp, float freq, float steep, float speed, float2 dir, float2 v, float time){
				float2 d = normalize(dir.xy);
				float q = steep;
				float wa = freq * amp;

				float pf = dot(d, v) * freq + time * speed;
				float sp = sin(pf);
				float cp = cos(pf);
				
				float3 tangent;
				tangent.xz = -q * d.xy * d.y * wa * sp;
				tangent.y = d.y * wa * cp;

				return tangent;
			}

			v2f vert (appdata v)
			{
				v2f o;
				o.localPos = v.vertex.xyz;
				float4 vt = v.vertex;

				float time = _Time.x;

				float3 p = 0.0;
				for(int i = 0; i < 4; i++){
					p += GerstnerWave(amp[i], freq[i], steep[i], speed[i], dir[i], vt.xz, time);
				}

				vt.xz += p.xz;
				vt.y = p.y;

				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				o.vertex = UnityObjectToClipPos(vt);
				o.pos = vt;
				o.scrPos = ComputeScreenPos(o.vertex);

				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				float4 pos = i.pos;

				//GridLine
				float4 scrPos = i.scrPos;
				float lw = _LineWidth * scrPos.z * scrPos.z * 11;
				float eg = _Edge;
				float gs = _GridSize;
				float3 f = smoothstep(lw, eg + lw, frac(pos.xyz * gs))
						 + smoothstep(1 - lw, 1 - eg - lw, frac(pos.xyz * gs));
				f *= step(0.001, fwidth(pos.xyz));
				float grid = max(f.x, f.z);

				//CalcNormal
				float3 localPos = i.localPos;
				float3 g_binormal = float3(1.0, 0.0, 0.0);
				float3 g_tangent = float3(0.0, 0.0, 1.0);

				float time = _Time.x;

				float3 p = 0.0;
				for(int m = 0; m < 4; m++){
					p += GerstnerWave(amp[m], freq[m], steep[m], speed[m], dir[m], localPos.xz, time);
					g_binormal += CalcBinormal(amp[m], freq[m], steep[m], speed[m], dir[m], localPos.xz, time);
					g_tangent += CalcTangent(amp[m], freq[m], steep[m], speed[m], dir[m], localPos.xz, time);
				}

				localPos.xz += p.xz;
				localPos.y = p.y;

				float3 g_normal = normalize(cross(g_tangent, g_binormal));

				//lighting
				float3 worldPos = mul(unity_ObjectToWorld, localPos);
				float3 normal = UnityObjectToWorldNormal(g_normal);

				float3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
				float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
				float3 halfDir = normalize(lightDir + viewDir);

				float diff = saturate(dot(normal, lightDir)) * _LightColor0;
				float spec = pow(max(0.0, dot(normal, halfDir)), _Shininess * 128.0) * _LightColor0;

				fixed4 col = tex2D(_MainTex, i.uv) * _Color;
				col.rgb = col.rgb * diff + spec;
				grid *= _ShowGrid;
				col.rgb = col.rgb * (1.0 - grid) + grid * _GridColor.rgb;
				return col;
			}
			ENDCG
		}
	}
}

実行結果

 上記シェーダの実行結果は以下の通りです。エッジ部分が明るく表示されずに、滑らかにライティングができていることがわかります。

ワールド座標で計算

 以上のシェーダはローカル座標で座標位置の計算を行っていました。ローカル座標で計算した場合、複数の平面を並べ、カメラからの距離に応じてメッシュを変更する、つまり、LODを用いることができません。そこで、ワールド座標で計算するようにシェーダを変更しました。

shader

 ゲルストナー波をワールド座標で計算するシェーダは以下の通りです。

Shader "Ocean/GerstnerWave_world"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "gray" {}
		_Color ("MainColor", Color) = (0, 0, 0, 1)
		[MaterialToggle]
		_ShowGrid ("Show Grid", Int) = 1
		_GridColor ("GridColor", Color) = (1, 1, 1, 1)
		_GridSize ("GridSize", Range(0, 20)) = 1
		_LineWidth ("LineWidth", Range(0.4, 1)) = 0.1
		_Edge ("Edge", Range(0.001, 0.8)) = 0.1
		_Thickness("Thickness", Float) = 1

		_Amplitude ("Amplitude", Vector) = (0.8, 0.8, 0.4, 0.9)
		_Frequency ("Frequency", Vector) = (0.1, 0.45, 0.25, 0.3)
		_Steepness ("Steepness", Vector) = (0.4, 1.2, 2.4, 1.8)
		_Speed ("Speed", Vector) = (20, 30, 10, 30)
		_DirectionA ("Wave A(X,Y) and B(Z,W)", Vector) = (0.47, 0.35, -0.96, 0.23)
		_DirectionB ("C(X,Y) and D(Z,W)", Vector) = (0.77, -1.47, -0.3, -0.2)

		_Shininess ("Shininess", Range(0 ,1)) = 0.7
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }

		Pass
		{
 			Tags { "LightMode"="ForwardBase" }
			
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"

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

			struct v2f
			{
				float2 uv : TEXCOORD0;
				float4 vertex : SV_POSITION;
				float4 pos : TEXCOORD1;
				float4 scrPos : TEXCOORD2;
				float3 worldPos : TEXCOORD3;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;

			int _ShowGrid;
			float4 _GridColor;
			float4 _Color;
			float _GridSize;
			float _LineWidth;
			float _Edge;

			float4 _Amplitude;
			float4 _Frequency;
			float4 _Steepness;
			float4 _Speed;
			float4 _DirectionA;
			float4 _DirectionB;

			static const float amp[4] = {_Amplitude.x, _Amplitude.y, _Amplitude.z, _Amplitude.w};
			static const float freq[4] = {_Frequency.x, _Frequency.y, _Frequency.z, _Frequency.w};
			static const float steep[4] = {_Steepness.x, _Steepness.y, _Steepness.z, _Steepness.w};
			static const float speed[4] = {_Speed.x, _Speed.y, _Speed.z, _Speed.w};
			static const float2 dir[4] = {_DirectionA.xy, _DirectionA.zw, _DirectionB.xy, _DirectionB.zw};

			float4 _LightColor0;
			float _Shininess;

			float3 GerstnerWave(float2 amp, float freq, float steep, float speed, float2 dir, float2 v, float time)
			{
				float3 p;
				float2 d = normalize(dir.xy);
				float q = steep;
				float f = (dot(d, v) * freq + time * speed);
				p.xz = q * amp * d.xy * cos(f);
				p.y = amp * sin(f);

				return p;
			}

			float3 CalcBinormal(float2 amp, float freq, float steep, float speed, float2 dir, float2 v, float time){
				float2 d = normalize(dir.xy);
				float q = steep;
				float wa = freq * amp;

				float pf = dot(d, v) * freq + time * speed;
				float sp = sin(pf);
				float cp = cos(pf);

				float3 binormal;
				binormal.xz = -q * d.x * d.xy * wa * sp;
				binormal.y = d.x * wa * cp;

				return binormal;
			}

			float3 CalcTangent(float2 amp, float freq, float steep, float speed, float2 dir, float2 v, float time){
				float2 d = normalize(dir.xy);
				float q = steep;
				float wa = freq * amp;

				float pf = dot(d, v) * freq + time * speed;
				float sp = sin(pf);
				float cp = cos(pf);

				float3 tangent;
				tangent.xz = -q * d.xy * d.y * wa * sp;
				tangent.y = d.y * wa * cp;

				return tangent;
			}

			v2f vert (appdata v)
			{
				v2f o;
				float4 vt = v.vertex;
				float3 g_normal = float3(0.0, 1.0, 0.0);
				float3 g_binormal = float3(1.0, 0.0, 0.0);
				float3 g_tangent = float3(0.0, 0.0, 1.0);

				float4 worldPos = mul(unity_ObjectToWorld, vt);
				o.worldPos = worldPos;

				float time = _Time.x;

				float3 p = 0.0;
				for(int i = 0; i < 4; i++){
					p += GerstnerWave(amp[i], freq[i], steep[i], speed[i], dir[i], worldPos.xz, time);
				}

				worldPos.xyz += p;
				vt = mul(unity_WorldToObject, worldPos);

				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				o.vertex = UnityObjectToClipPos(vt);
				o.pos = vt;
				o.scrPos = ComputeScreenPos(o.vertex);

				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				float4 pos = i.pos;

				//GridLine
				float4 scrPos = i.scrPos;
				float lw = _LineWidth * scrPos.z * scrPos.z * 11;
				float eg = _Edge;
				float gs = _GridSize;
				float3 f = smoothstep(lw, eg + lw, frac(pos.xyz * gs))
						 + smoothstep(1 - lw, 1 - eg - lw, frac(pos.xyz * gs));
				f *= step(0.001, fwidth(pos.xyz));
				float grid = max(f.x, f.z);

				//CalcNormal
				float3 worldPos = i.worldPos;
				float3 g_binormal = float3(1.0, 0.0, 0.0);
				float3 g_tangent = float3(0.0, 0.0, 1.0);

				float time = _Time.x;

				float3 p = 0.0;
				for(int m = 0; m < 4; m++){
					p += GerstnerWave(amp[m], freq[m], steep[m], speed[m], dir[m], worldPos.xz, time);
					g_binormal += CalcBinormal(amp[m], freq[m], steep[m], speed[m], dir[m], worldPos.xz, time);
					g_tangent += CalcTangent(amp[m], freq[m], steep[m], speed[m], dir[m], worldPos.xz, time);
				}
				worldPos += p;
				float3 normal = normalize(cross(g_tangent, g_binormal));

				//lighting
				float3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
				float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
				float3 halfDir = normalize(lightDir + viewDir);

				float diff = saturate(dot(normal, lightDir)) * _LightColor0;
				float spec = pow(max(0.0, dot(normal, halfDir)), _Shininess * 128.0) * _LightColor0;

				fixed4 col = tex2D(_MainTex, i.uv) * _Color;
				col.rgb = col.rgb * diff + spec;
				grid *= _ShowGrid;
				col.rgb = col.rgb * (1.0 - grid) + grid * _GridColor.rgb;
				return col;
			}
			ENDCG
		}
	}
}

実行結果

 上記シェーダの実行結果は以下の通りです。ワールド座標で計算しているため、平面を動かしてもそれに伴って波が移動することはありません。

参考サイト

GPU Gems:Chapter 1. Effective Water Simulation from Physical Models