海洋シェーダ(海の色)

海洋シェーダ(海の色)

 前回の記事(海洋シェーダ(波の形))では、ゲルストナー波をノイズで変形させることで 海洋における波の形を作成しました。この波に、フレネル反射等を追加することによって海の色を表現します。

基本の色

 海の基本となる色(_SeaBaseColor)にランバート反射で凹凸を付けています。また、波の高さ(wave_height)によって色を変えるために、_SeaShallowColor * wave_height * 0.5 + 0.2) * _ColorHightOffsetを加算しています。

float3 lightDir = normalize(UnityWorldSpaceLightDir(world_pos));
float diff = saturate(dot(normal, lightDir)) * _LightColor0;
float sea_height = world_pos.y - geo_pos.y;
sea_color = _SeaBaseColor * diff * _BaseColorStrength + _SeaShallowColor * (wave_height * 0.5 + 0.2) * _ColorHightOffset;

これを実行すると以下のような結果が得られます。各プロパティの値は次の通りです。

_SeaBaseColor(27, 57, 77)、 _SeaShallowColor (75, 89, 35)、_BaseColorStrength(1.5)、_ColorHightOffset(0.15)

フレネル反射

 フレネル反射を追加しました。これには海が反射する空の色が必要となるので、以下のコードで反射ベクトルから空の色を計算しています。

float3 GetSkyColor(float3 dir, float3 c){
	dir.y = max(0.0, dir.y);
	float et = 1.0 - dir.y;
	return (1.0 - c) * et + c;
}
・・・
float3 viewDir = normalize(UnityWorldSpaceViewDir(world_pos));
float3 reflectDir = reflect(-viewDir, normal);
float3 sea_reflect_color = GetSkyColor(reflectDir, _SkyColor);

_SkyColor(0, 104, 255)としたとき、空の色は以下のようになります。

この結果をフレネル反射に利用します。

//fresnel
float r = 0.02;
float facing = saturate(1.0 - dot(normal, viewDir));
float fresnel = r + (1.0 - r) * pow(facing, 5.0);
・・・
float3 sea_base_color = _SeaBaseColor * diff * _BaseColorStrength;
float3 water_color = lerp(sea_base_color, sea_reflect_color, fresnel);
float3 sea_color = water_color + _SeaShallowColor * (wave_height * 0.5 + 0.2) * _ColorHightOffset;

以下の結果が得られます。_BaseColorStrengthを1.5から1.2へ変更しています。

基本色の変更

_SeaBaseColorを変更しても、あまり良い結果が得られませんでした。そこで、基本色の計算式に_SeaShallowColorを加えた以下の計算方法に変更してみました。

・・・
float3 sea_base_color = _SeaBaseColor * diff * _BaseColorStrength + lerp(_SeaBaseColor, _SeaShallowColor * _ShallowColorStrength, diff);
・・・

実行すると以下のようになります。 _BaseColorStrengthを0.9、_ShallowColorStrengthを0.35としています。

反射光

 海には反射光(スペキュラー)が生じます。そこで、フォン鏡面反射を追加してみました。

・・・
float3 halfDir = normalize(lightDir + viewDir);
float spec = pow(max(0.0, dot(normal, halfDir)), _Shininess * 128.0) * _LightColor0;
・・・

結果は以下のようになります。

他の方法を探したところRendering Water as a Post-process Effectに別の方法が掲載されていたので試してみました。

・・・
float dotSpec = saturate(dot(reflectDir, lightDir) * 0.5 + 0.5);
float spec = (1.0 - fresnel) * saturate(lightDir.y) * pow(dotSpec, 512.0) * (_Shininess * 1.8 + 0.2);
spec += spec * 25.0 * saturate(_Shininess - 0.05) * _LightColor0;
・・・

結果は以下のようになります。

shader

 作成したシェーダは以下の通りです。

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

		_WaveSpeed("Value", Float) = 1.0

		[Header(OceanColor)]
		_SeaBaseColor ("SeaBaseColor", Color) = (0, 0, 0, 1)
		_SeaShallowColor ("SeaShallowColor", Color) = (0, 0, 0, 1)
		_BaseColorStrength ("Base Color Strength", Range(0, 2.0)) = 0.5
		_ShallowColorStrength ("Shallow Color Strength", Range(0, 1.0)) = 0.36
		_ColorHightOffset ("Color Hight Offset", Range(0, 1.0)) = 0.15

		[Header(SkyColor)]
		_SkyColor ("SkyColor", Color) = (0, 0, 0, 1)

		[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

		_Shininess ("Shininess", Range(0 ,5)) = 0.27
    }
    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 world_pos : TEXCOORD1;
				float4 proj_coord : TEXCOORD5;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

			static const int wave_number = 8;
			static const int count = 4;

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

				float time = _Time.x * _WaveSpeed;

				float3 p = 0.0;
				for(int i = 0; i < count; i++){
					p += GerstnerWave(amp[i], freq[i], steep[i], speed[i], noise_size[i], dir[i], world_pos.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], world_pos.xz, time, j);
				}
				world_pos.xyz += p;

				vt = mul(unity_WorldToObject, world_pos);

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

				return o;
            }

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

				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 = world_pos.xyz + float3(0.05, 0.0, 0.0);
				float3 v_tan = world_pos.xyz + float3(0.0, 0.0, 0.05);
				for(int m = 0; m < count; m++){
					p += GerstnerWave(amp[m], freq[m], steep[m], speed[m], noise_size[m], dir[m], world_pos.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], world_pos.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);
				}
				world_pos += p;
				float3 normal = normalize(cross(pt - p, pb - p));

				float wave_height = world_pos.y - geo_pos.y;
				float3 result = OceanColor(world_pos, wave_height, normal, i.proj_coord);
                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};

float4 _SeaBaseColor;
float4 _SeaShallowColor;
float _SeaColorStrength;

float _BaseColorStrength;
float _ShallowColorStrength;
float _ColorHightOffset;

float _WaveSpeed;

float4 _LightColor0;
float _Shininess;

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);
	return p;
}

float4 _SkyColor;

float3 GetSkyColor(float3 dir, float3 c){
	dir.y = max(0.0, dir.y);
	float et = 1.0 - dir.y;
	return (1.0 - c) * et + c;
}
			
float3 OceanColor(float3 world_pos, float wave_height, float3 normal, float4 proj_coord){
	//lighting
	float3 lightDir = normalize(UnityWorldSpaceLightDir(world_pos));
	float3 viewDir = normalize(UnityWorldSpaceViewDir(world_pos));
	float3 halfDir = normalize(lightDir + viewDir);
	
	//fresnel
	float r = 0.02;
	float facing = saturate(1.0 - dot(normal, viewDir));
	float fresnel = r + (1.0 - r) * pow(facing, 5.0);
	float3 reflectDir = reflect(-viewDir, normal);
	
	float diff = saturate(dot(normal, lightDir)) * _LightColor0;
	//float spec = pow(max(0.0, dot(normal, halfDir)), _Shininess * 128.0) * _LightColor0;	//Blinn-Phong
	
	//https://www.gamedev.net/articles/programming/graphics/rendering-water-as-a-post-process-effect-r2642/
	float dotSpec = saturate(dot(reflectDir, lightDir) * 0.5 + 0.5);
	float spec = (1.0 - fresnel) * saturate(lightDir.y) * pow(dotSpec, 512.0) * (_Shininess * 1.8 + 0.2);
	spec += spec * 25.0 * saturate(_Shininess - 0.05) * _LightColor0;
	
	//reflection
	float3 sea_reflect_color = GetSkyColor(reflectDir, _SkyColor);
	float3 sea_base_color = _SeaBaseColor * diff * _BaseColorStrength + lerp(_SeaBaseColor, _SeaShallowColor * _ShallowColorStrength, diff);
	float3 water_color = lerp(sea_base_color, sea_reflect_color, fresnel);
	float3 sea_color = water_color + _SeaShallowColor * (wave_height * 0.5 + 0.2) * _ColorHightOffset;

	return sea_color;
}

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

参考サイト

Rendering Water as a Post-process Effect