海洋シェーダ(波の形)

海洋シェーダ(波の形)

 以前、ゲルストナー波(Gerstner Wave)についての記事を掲載しました。このゲルストナー波を用いて海の波をシェーダで作成しました。

ゲルストナー波(8個)

shader

 単純にゲルストナー波を単純に8個組み合わせるシェーダです。法線は近接する頂点を利用して計算しています。また、ランバート反射によって波の凹凸が分かるように処理をしています。

Shader "Ocean/Gerstner_Geo_8_Waves"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "gray" {}

		_WaveSpeed("Wave Speed", Float) = 0.0

		[Header(GerstnerWave)]
		_Amplitude ("Amplitude", Vector) = (0.78, 0.81, 0.6, 0.27)
		_Frequency ("Frequency", Vector) = (0.16, 0.18, 0.21, 0.27)
		_Steepness ("Steepness", Vector) = (1.70, 1.60, 1.20, 1.80)
		_Speed ("Speed", Vector) = (24, 40, 48, 60)
		_DirectionA ("Wave A(X,Y) and B(Z,W)", Vector) = (0.35, 0.31, 0.08, 0.60)
		_DirectionB ("C(X,Y) and D(Z,W)", Vector) = (-0.95, -0.74, 0.7, -0.5)

		_Amplitude2 ("Amplitude", Vector) = (0.17, 0.12, 0.21, 0.06)
		_Frequency2 ("Frequency", Vector) = (0.7, 0.84, 0.54, 0.80)
		_Steepness2 ("Steepness", Vector) = (1.56, 2.18, 2.80, 1.90)
		_Speed2 ("Speed", Vector) = (32, 40, 48, 60)
		_DirectionC ("Wave A(X,Y) and B(Z,W)", Vector) = (0.7, 0.6, 0.10, 0.38)
		_DirectionD ("C(X,Y) and D(Z,W)", Vector) = (0.43, 0.07, 0.42, 0.61)
	}
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
			
            #include "UnityCG.cginc"
			#include "Assets/Shaders/Ocean.cginc"

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

            struct v2f
            {
				float2 uv : TEXCOORD0;
				float4 vertex : SV_POSITION;
				float3 worldPos : TEXCOORD1;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

			static const int wave_number = 8;

            v2f vert (appdata v)
            {
				v2f o;
				float4 vt = v.vertex;
				float4 worldPos = mul(unity_ObjectToWorld, vt);
				o.worldPos = worldPos.xyz;

				float time = _Time.x * _WaveSpeed;

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

				worldPos.xyz += p;

				vt = mul(unity_WorldToObject, worldPos);

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

				return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
				//CalcNormal
				float3 worldPos = i.worldPos;
				float3 geo_pos = worldPos;

				float time = _Time.x * _WaveSpeed;

				float3 p = 0.0;
				float3 pb = float3(0.05, 0.0, 0.0);
				float3 pt =float3(0.0, 0.0, 0.05);
				float3 v_bi = worldPos.xyz + float3(0.05, 0.0, 0.0);
				float3 v_tan = worldPos.xyz + float3(0.0, 0.0, 0.05);
				for(int m = 0; m < wave_number; m++){
					p += GerstnerWave(amp[m], freq[m], steep[m], speed[m], dir[m], worldPos.xz, time, m);
					pb += GerstnerWave(amp[m], freq[m], steep[m], speed[m], dir[m], v_bi.xz, time, m);
					pt += GerstnerWave(amp[m], freq[m], steep[m], speed[m], dir[m], v_tan.xz, time, m);
				}
				worldPos += p;
				float3 dist = worldPos - geo_pos;
				float3 normal = normalize(cross(pt - p, pb - p));

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

				float diff = saturate(dot(normal, lightDir)) * _LightColor0;

				float3 result = diff;
                fixed4 col = fixed4(result, 1.0);
                return col;
            }
            ENDCG
        }
    }
}

Ocean.cginc

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

float4 _Amplitude2;
float4 _Frequency2;
float4 _Steepness2;
float4 _Speed2;
float4 _DirectionC;
float4 _DirectionD;
float4 _FoamSpeed2;

static const float amp[8] = {_Amplitude.x, _Amplitude.y, _Amplitude.z, _Amplitude.w, _Amplitude2.x, _Amplitude2.y, _Amplitude2.z, _Amplitude2.w};
static const float freq[8] = {_Frequency.x, _Frequency.y, _Frequency.z, _Frequency.w, _Frequency2.x, _Frequency2.y, _Frequency2.z, _Frequency2.w};
static const float steep[8] = {_Steepness.x, _Steepness.y, _Steepness.z, _Steepness.w, _Steepness2.x, _Steepness2.y, _Steepness2.z, _Steepness2.w};
static const float speed[8] = {_Speed.x, _Speed.y, _Speed.z, _Speed.w, _Speed2.x, _Speed2.y, _Speed2.z, _Speed2.w};
static const float2 dir[8] = {_DirectionA.xy, _DirectionA.zw, _DirectionB.xy, _DirectionB.zw, _DirectionC.xy, _DirectionC.zw, _DirectionD.xy, _DirectionD.zw};

float _WaveSpeed;

float4 _LightColor0;

float3 GerstnerWave(float2 amp, float freq, float steep, float speed, float2 dir, float2 v, float time, int seed)
{
	float3 p;
	float2 d = normalize(dir.xy);
	float q = steep;

	seed *= 3;
	float f = dot(d, v) * freq + time * speed;
	p.xz = q * amp * d.xy * cos(f);
	p.y = amp * sin(f);

	return p;
}

実行結果

 実行結果は以下の通りです。同じような模様が繰り返し現れており、あまり海らしくありません。

直行するゲルストナー波を加える

全てのゲルストナー波に直行する波を加える

shader

 ゲルストナー波を単純に組み合わせるだけでなく、直行するゲルストナー波を発生させ、組み合わせてみました。

・・・
for(int i = 0; i < wave_number; i++){
	p += GerstnerWave_Cross(amp[i], freq[i], steep[i], speed[i], dir[i], worldPos.xz, time, i);
}
・・・

Ocean.cginc

・・・
float3 GerstnerWave_Cross(float2 amp, float freq, float steep, float speed, float2 dir, float2 v, float time, int seed)
{
	float3 p;
	float2 d = normalize(dir.xy);
	float q = steep;
	seed *= 3;

	float3 p1;
	float3 p2;
	float2 d1 = normalize(dir.xy);
	float2 d2 = float2(-d.y, d.x);

	float2 f1 = dot(d1, v) * freq + time * speed;
	float2 f2 = dot(d2, v) * freq + time * speed;
	p1.xz = q * amp * d1.xy * cos(f1);
	p1.y = amp * sin(f1);
	p2.xz = q * amp * d2.xy * cos(f2);
	p2.y = amp * sin(f2);

	p = lerp(p1, p2, 0.5);

	return p;
}

実行結果

 実行結果は以下の通りです。先ほどより良くなりましたが、波を16個も合成するのは多すぎるので、以下のように、直行するゲルストナー波を半分に減らしてみました。

直行するゲルストナー波を減らす

shader

 先ほどのシェーダでは全てのゲルストナー波に対して直行する波を発生させていましたが、振幅の大きい4個のゲールトナー波はそのまま、残りのゲルストナー波に直行するゲルストナー波を発生させ組み合わせました。つまり、合計で12個のゲルストナー波を発生させています。

・・・
int count = 4;
for(int i = 0; i < count; i++){
	p += GerstnerWave(amp[i], freq[i], steep[i], speed[i], dir[i], worldPos.xz, time, i);
}
for(int j = wave_number - count; j < wave_number; j++){
	p += GerstnerWave_Cross(amp[j], freq[j], steep[j], speed[j], dir[j], worldPos.xz, time, j);
}
・・・

実行結果

 実行結果は以下の通りです。 波の数は16個から12個へ減りましたが、見た目はほとんど変わっていません。

ノイズによる歪みの追加

shader

 ノイズによってゲルストナー波を計算する座標を移動させることで、細かい凹凸を追加しました。

Shader "Ocean/Gerstner_Geo_Noise_4_Waves_4_Cross_Waves"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "gray" {}

		_WaveSpeed("Wave Speed", Float) = 0.0

		[Header(GerstnerWave)]
		_Amplitude ("Amplitude", Vector) = (0.78, 0.81, 0.6, 0.27)
		_Frequency ("Frequency", Vector) = (0.16, 0.18, 0.21, 0.27)
		_Steepness ("Steepness", Vector) = (1.70, 1.60, 1.20, 1.80)
		_Speed ("Speed", Vector) = (24, 40, 48, 60)
		_Noise ("Noise", Vector) = (0.39, 0.31, 0.27, 0.57) 
		_DirectionA ("Wave A(X,Y) and B(Z,W)", Vector) = (0.35, 0.31, 0.08, 0.60)
		_DirectionB ("C(X,Y) and D(Z,W)", Vector) = (-0.95, -0.74, 0.7, -0.5)

		_Amplitude2 ("Amplitude", Vector) = (0.17, 0.12, 0.21, 0.06)
		_Frequency2 ("Frequency", Vector) = (0.7, 0.84, 0.54, 0.80)
		_Steepness2 ("Steepness", Vector) = (1.56, 2.18, 2.80, 1.90)
		_Speed2 ("Speed", Vector) = (32, 40, 48, 60)
		_Noise2 ("Noise", Vector) = (0.33, 0.81, 0.39, 0.45) 
		_DirectionC ("Wave A(X,Y) and B(Z,W)", Vector) = (0.7, 0.6, 0.10, 0.38)
		_DirectionD ("C(X,Y) and D(Z,W)", Vector) = (0.43, 0.07, 0.42, 0.61)

		_NoiseSizeLerp("Noise Size", Range(0, 0.5)) = 0.5
		_NoiseStrength("Noise Strength", Range(0, 5)) = 1.26
	}
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
			
            #include "UnityCG.cginc"
			#include "Assets/Shaders/Ocean.cginc"

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

            struct v2f
            {
				float2 uv : TEXCOORD0;
				float4 vertex : SV_POSITION;
				float3 worldPos : TEXCOORD1;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

			static const int wave_number = 8;

            v2f vert (appdata v)
            {
				v2f o;
				float4 vt = v.vertex;
				float4 worldPos = mul(unity_ObjectToWorld, vt);
				o.worldPos = worldPos.xyz;

				float time = _Time.x * _WaveSpeed;

				float3 p = 0.0;
				int count = 4;
				for(int i = 0; i < count; i++){
					p += GerstnerWave(amp[i], freq[i], steep[i], speed[i], noise_size[i], dir[i], worldPos.xz, time, i);
				}
				///*
				for(int j = wave_number - count; j < wave_number; j++){
					p += GerstnerWave_Cross(amp[j], freq[j], steep[j], speed[j], noise_size[j], dir[j], worldPos.xz, time, j);
				}
				//*/
				worldPos.xyz += p;

				vt = mul(unity_WorldToObject, worldPos);

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

				return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
				//CalcNormal
				float3 worldPos = i.worldPos;
				float3 geo_pos = worldPos;

				float time = _Time.x * _WaveSpeed;
				//time = 1.55;

				float3 p = 0.0;
				float3 pb = float3(0.05, 0.0, 0.0);
				float3 pt =float3(0.0, 0.0, 0.05);
				float3 v_bi = worldPos.xyz + float3(0.05, 0.0, 0.0);
				float3 v_tan = worldPos.xyz + float3(0.0, 0.0, 0.05);
				int count = 4;
				for(int m = 0; m < count; m++){
					p += GerstnerWave(amp[m], freq[m], steep[m], speed[m], noise_size[m], dir[m], worldPos.xz, time, m);
					pb += GerstnerWave(amp[m], freq[m], steep[m], speed[m], noise_size[m], dir[m], v_bi.xz, time, m);
					pt += GerstnerWave(amp[m], freq[m], steep[m], speed[m], noise_size[m], dir[m], v_tan.xz, time, m);
				}
				///*
				for(int n = wave_number - count; n < wave_number; n++){
					p += GerstnerWave_Cross(amp[n], freq[n], steep[n], speed[n], noise_size[n], dir[n], worldPos.xz, time, n);
					pb += GerstnerWave_Cross(amp[n], freq[n], steep[n], speed[n], noise_size[n], dir[n], v_bi.xz, time, n);
					pt += GerstnerWave_Cross(amp[n], freq[n], steep[n], speed[n], noise_size[n], dir[n], v_tan.xz, time, n);
				}
				//*/
				worldPos += p;
				float3 dist = worldPos - geo_pos;
				float3 normal = normalize(cross(pt - p, pb - p));

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

				float diff = saturate(dot(normal, lightDir)) * _LightColor0;

				float3 result = diff;
                fixed4 col = fixed4(result, 1.0);
                return col;
            }
            ENDCG
        }
    }
}

Ocean.cginc

#include "Noise.cginc"
float _NoiseStrength;
float _NoiseSizeLerp;

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

float4 _Amplitude2;
float4 _Frequency2;
float4 _Steepness2;
float4 _Speed2;
float4 _Noise2;
float4 _DirectionC;
float4 _DirectionD;

static const float amp[8] = {_Amplitude.x, _Amplitude.y, _Amplitude.z, _Amplitude.w, _Amplitude2.x, _Amplitude2.y, _Amplitude2.z, _Amplitude2.w};
static const float freq[8] = {_Frequency.x, _Frequency.y, _Frequency.z, _Frequency.w, _Frequency2.x, _Frequency2.y, _Frequency2.z, _Frequency2.w};
static const float steep[8] = {_Steepness.x, _Steepness.y, _Steepness.z, _Steepness.w, _Steepness2.x, _Steepness2.y, _Steepness2.z, _Steepness2.w};
static const float speed[8] = {_Speed.x, _Speed.y, _Speed.z, _Speed.w, _Speed2.x, _Speed2.y, _Speed2.z, _Speed2.w};
static const float2 dir[8] = {_DirectionA.xy, _DirectionA.zw, _DirectionB.xy, _DirectionB.zw, _DirectionC.xy, _DirectionC.zw, _DirectionD.xy, _DirectionD.zw};
static const float noise_size[8] = {_Noise.x, _Noise.y, _Noise.z, _Noise.w, _Noise2.x, _Noise2.y, _Noise2.z, _Noise2.w};
static const float foam_speed[8] = {_FoamSpeed.x, _FoamSpeed.y, _FoamSpeed.z, _FoamSpeed.w, _FoamSpeed2.x, _FoamSpeed2.y, _FoamSpeed2.z, _FoamSpeed2.w};

float _WaveSpeed;

float4 _LightColor0;

float3 GerstnerWave(float2 amp, float freq, float steep, float speed, float noise, float2 dir, float2 v, float time, int seed)
{
	float3 p;
	float2 d = normalize(dir.xy);
	float q = steep;

	seed *= 3;
	v +=  noise2(v * noise + time, seed) * _NoiseStrength;
	float f = dot(d, v) * freq + time * speed;
	p.xz = q * amp * d.xy * cos(f);
	p.y = amp * sin(f);

	return p;
}

float3 GerstnerWave_Cross(float2 amp, float freq, float steep, float speed, float noise, float2 dir, float2 v, float time, int seed)
{
	float3 p;
	float2 d = normalize(dir.xy);
	float q = steep;

	float noise_strength = _NoiseStrength;
	seed *= 3;

	float3 p1;
	float3 p2;
	float2 d1 = normalize(dir.xy);
	float2 d2 = float2(-d.y, d.x);

	float2 v1 = v + noise2(v * noise + time * d * 10.0, seed) * noise_strength;
	float2 v2 = v + noise2(v * noise + time * d * 10.0, seed + 12) * noise_strength;
	float2 f1 = dot(d1, v1) * freq + time * speed;
	float2 f2 = dot(d2, v2) * freq + time * speed;
	p1.xz = q * amp * d1.xy * cos(f1);
	p1.y = amp * sin(f1);
	p2.xz = q * amp * d2.xy * cos(f2);
	p2.y = amp * sin(f2);

	p = lerp(p1, p2, noise2(v * _NoiseSizeLerp + time, seed) * 0.5 + 0.5);
	//p = lerp(p1, p2, 0.5);

	return p;
}

Noise.cginc

float2 rand2d(float2 st, int seed)
{
	float2 s = float2(dot(st, float2(127.1, 311.7)) + seed, dot(st, float2(269.5, 183.3)) + seed);
	return -1.0 + 2.0 * frac(sin(s) * 43758.5453123);
}

float noise2(float2 st, int seed)
{
	float2 p = floor(st);
	float2 f = frac(st);

	float w00 = dot(rand2d(p, seed), f);
	float w10 = dot(rand2d(p + float2(1.0, 0.0), seed), f - float2(1.0, 0.0));
	float w01 = dot(rand2d(p + float2(0.0, 1.0), seed), f - float2(0.0, 1.0));
	float w11 = dot(rand2d(p + float2(1.0, 1.0), seed), f - float2(1.0, 1.0));
	
	float2 u = f * f * (3.0 - 2.0 * f);

	return lerp(lerp(w00, w10, u.x), lerp(w01, w11, u.x), u.y);
}

float3 rand3d(float3 p, int seed)
{
	float3 s = float3(dot(p, float3(127.1, 311.7, 74.7)) + seed,
					  dot(p, float3(269.5, 183.3, 246.1)) + seed,
					  dot(p, float3(113.5, 271.9, 124.6)) + seed);
	return -1.0 + 2.0 * frac(sin(s) * 43758.5453123);
}

float noise3(float3 st, int seed)
{
	float3 p = floor(st);
	float3 f = frac(st);

	float w000 = dot(rand3d(p, seed), f);
	float w100 = dot(rand3d(p + float3(1, 0, 0), seed), f - float3(1, 0, 0));
	float w010 = dot(rand3d(p + float3(0, 1, 0), seed), f - float3(0, 1, 0));
	float w110 = dot(rand3d(p + float3(1, 1, 0), seed), f - float3(1, 1, 0));
	float w001 = dot(rand3d(p + float3(0, 0, 1), seed), f - float3(0, 0, 1));
	float w101 = dot(rand3d(p + float3(1, 0, 1), seed), f - float3(1, 0, 1));
	float w011 = dot(rand3d(p + float3(0, 1, 1), seed), f - float3(0, 1, 1));
	float w111 = dot(rand3d(p + float3(1, 1, 1), seed), f - float3(1, 1, 1));
	
	float3 u = f * f * (3.0 - 2.0 * f);

	float r1 = lerp(lerp(w000, w100, u.x), lerp(w010, w110, u.x), u.y);
	float r2 = lerp(lerp(w001, w101, u.x), lerp(w011, w111, u.x), u.y);

	return lerp(r1, r2, u.z);
}

float fbm(float2 st, int seed){
	float val = 0.0;
	float a = 0.5;

	for(int i = 0; i < 6; i++){
		val += a * noise2(st, seed);
		st *= 2.0;
		a *= 0.5;
	}
	return val;
}

実行結果

 実行結果は以下の通りです。 ノイズによって細かい凹凸が追加され、より海らしくなりました。

参考サイト

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