Ocean Update #3

Managed to squeeze a tad more in today that I’d expected. Quick update:

I wanted to get some basic intersection set up for foam around objects set up. This is fairly-well documented effect, but most examples and references use a vert/frag setup for the shader, and not Unity’s vert/surf approach.

I really want to see how far I can get with the surf approach – it makes the code significantly simpler to look at and maintain, and Unity does all the ‘fun’ jobs, such as setting up the different passes for forward and deferred rendering – so I get some free wins. 

I’m not convinced it’ll work, but I’m confident I can get all the way. Once the intersection work is done, the rest is mostly just traditional surface shading (or so I hope).

To do the intersection, I need to get the depth buffer – I need to sample the wave’s mesh depth against the depth buffer to see what’s intersecting. Unity conveniently gives you screenPos in the Input struct, which helps to sample the linear eye depth:

float sceneZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(IN.screenPos)));

The problem is getting the object depth. Normally you can compute the depth in the vertex shader and pass this through to the fragment shader – COMPUTE_EYEDEPTH(i) does this for you. This even works in the vertex shader of a vert/surf setup under normal circumstance!… So what’s the big problem? I can’t use this with tessellation! Unity’s implementation of tessellation means that when using it, you can’t have a void vert(<args>) function with more than one argument, i.e.:

void vert(inout appdata_full vertexData, out Input o) { ... }

This would normally work, but with tessellation you get a nice error telling you that you can’t have more than one argument in your vertex program.

So, getting the depth buffer in a surface shader isn’t as straightforward as I would have liked. How can we do it, then?

Well, Unity conveniently also provides worldPos in the Input struct in your surface program, and also gives you some useful matrices you can use against that worldPos, namely:

UNITY_MATRIX_V

You can see more here: https://github.com/UnityCommunity/UnityLibrary/wiki/Built-in-Shader-Variables

We can now use these to find the intersection and give us a useful value to use in our shading. Its worth noting that the intersection code itself is everywhere – you can find examples even in the Unity Standard Shader library – all I’ve done is add in a new range slider, called _FoamFade that allows me to control the falloff.

// Get mesh intersection
			float sceneZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(IN.screenPos)));
			float surfZ = -mul(UNITY_MATRIX_V, float4(IN.worldPos.xyz, 1)).z;
			float diff = sceneZ - surfZ;
			float intersect = 1 - saturate(diff / _FoamFade);

For now, we’ll just shove this in as it is, and go from here. Here’s the full source:

Shader "Custom/Waves" {
	Properties{
		[Header(Standard Properties)]
		_Color("Color", Color) = (1,1,1,1)
		_MainTex("Albedo (RGB)", 2D) = "white" {}
		_Glossiness("Smoothness", Range(0,1)) = 0.5
		_SpecColor("Specular Color", Color) = (1,1,1,1)

		[Header(Water Surface Properties)]
		_FoamColor("Foam Color", Color) = (1,1,1,1)
		_FoamTex("Foam Texture (RGB)", 2D) = "white" {}
		_FoamFade("Foam Intersection Falloff", Range(0.01,10)) = 1

		[Header(Wave Properties)]
		[Tooltip(dir xy, steepness, wavelength)]
		_WaveA("Wave A", Vector) = (1,0,0.5,10)
		_WaveB("Wave B", Vector) = (0,1,0.25,20)
		_WaveC("Wave C", Vector) = (1,1,0.15,10)
		[Tooltip(wave A speed, wave B speed, wave C speed, final multiplier)]
		_WaveSpeed("Wave Speed", Vector) = (1,1,1,1)
		[Header(Tessellation Properties)]
		_Tess("Tessellation", Range(1,32)) = 1
	}

	SubShader{
		Tags{ "RenderType" = "Transparent" "Queue" = "Transparent" }
		Blend SrcAlpha OneMinusSrcAlpha
		ZWrite Off
		LOD 200

		CGPROGRAM
		#pragma surface surf StandardSpecular fullforwardshadows vertex:vert tessellate:tess addshadow alpha:fade
		#pragma target 4.6
		#include "Tessellation.cginc"

		sampler2D _MainTex, _FoamTex, _CameraDepthTexture;
		half _Glossiness, _Tess, _FoamFade;
		fixed4 _Color, _FoamColor, _WaveA, _WaveB, _WaveC, _WaveSpeed;

		struct Input {
			float2 uv_MainTex;
			float2 uv2_FoamTex;
			float4 color : COLOR;
			float4 screenPos;
			float3 worldPos;
		};

		float4 tess() {
			return _Tess;
		}

		float3 GerstnerWave(float4 wave, float3 p, inout float3 tangent, inout float3 binormal, float speedModifier) {
			float k = 2 * UNITY_PI / wave.w;
			float c = sqrt(9.81 / k);
			float2 d = normalize(wave.xy);
			float f = k * (dot(d, p.xz) - c * _Time.y * speedModifier *_WaveSpeed.w);
			float a = wave.z / k;

			/*
			> sin(f), cos(f) are used a few times in the final calulations, so pre-clculate them once
			> We also multiply these a few times by steepness, so pre-calculate these as well
			> Store it as a float4 to keep memory footprint small
			> It's a bit tricky to read, but the gains are worthwhile
			*/
			float4 sinFCosF = float4(sin(f), cos(f), 0, 0);
			sinFCosF.z = sinFCosF.x * wave.z;
			sinFCosF.w = sinFCosF.y * wave.z;

			tangent += float3(
				-d.x * d.x * sinFCosF.z,
				d.x * sinFCosF.w,
				-d.x * d.y * sinFCosF.z
				);
			binormal += float3(
				-d.x * d.y * sinFCosF.z,
				d.y * sinFCosF.w,
				-d.y * d.y * sinFCosF.z
				);
			return float3(
				d.x * (a * sinFCosF.y),
				a * sinFCosF.x,
				d.y * (a * sinFCosF.y)
				);
		}

		void vert(inout appdata_full vertexData) {
			
			float3 gridPoint = vertexData.vertex.xyz;
			float3 tangent = float3(1, 0, 0);
			float3 binormal = float3(0, 0, 1);
			float3 p = gridPoint;
			float3 a, b, c;
			a = GerstnerWave(_WaveA, gridPoint, tangent, binormal, _WaveSpeed.x);
			b = GerstnerWave(_WaveB, gridPoint, tangent, binormal, _WaveSpeed.y);
			c = GerstnerWave(_WaveC, gridPoint, tangent, binormal, _WaveSpeed.z);
			p += a + b + c;
			float3 normal = normalize(cross(binormal, tangent));
			vertexData.vertex.xyz = p;
			vertexData.normal = normal;
			vertexData.color.r = -UnityObjectToViewPos(vertexData.vertex).z;
			// For later: Get current wave height offset and assign per vertex color RGB
			// vertexData.color.rgb = saturate(float3(a.y, b.y, c.y));
		}

		void surf(Input IN, inout SurfaceOutputStandardSpecular o) {
			
			// Base Water Color
			fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
			// Base Foam Color
			fixed4 f = tex2D(_FoamTex, IN.uv2_FoamTex) * _FoamColor;

			// Get mesh intersection
			float sceneZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(IN.screenPos)));
			float surfZ = -mul(UNITY_MATRIX_V, float4(IN.worldPos.xyz, 1)).z;
			float diff = sceneZ - surfZ;
			float intersect = 1 - saturate(diff / _FoamFade);
			
			// Compute final surface
			o.Albedo = c.rgb + f * intersect;
			o.Specular = _SpecColor;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}
		ENDCG
	}
	FallBack "Unlit"
}

Leave a Reply

Your email address will not be published. Required fields are marked *