Wet Surfaces

I threw together a “needs work, but it works, so I’ll share it” water surface shader with rain ripples. I’ll detail the basics below, but I don’t have time for a thorough theory post today.

Before we get into it – a shout out to Sébastien Lagard, who’s comprehensive research much of this shader relies upon. You can read it, in all it’s glory, here.

What does the shader do?

A few things!

  • Firstly, it calculates standard surface properties, including a parallax offset (because that really adds to the sense of water depth in the crevices).
  • Next, taking some input and a puddle map, we calculate which pixels are wet and which are dry.
  • Then we do a porosity calculation – this adjusts the smoothness values and darkens the Albedo
  • From there we use a ripple texture and apply this to wet areas to simulate water ripples
  • We then adjust the underlying surface textures (Albedo, normal and so on) on flooded areas to simulate a water surface (which is smooth and glossy)
  • Finally I’ve added similar overrides to the dry areas, as I’ve found these useful on occasion, but you may not need these at all

Wait… What’s a Puddle Map?

This defines how water accumulates on the surface (I also cheat here and use it as the parallax map, you can split these out if you need to). It looks identical to most height maps, and those will mostly work.

The difference arises if your texture has raised areas with their own recesses – in a height map, these recesses would be higher than recesses in the lower area – in a puddle map, these values should be the same because the water accumulates at the same rate.

Hopefully this helps explain a bit:

Left image contains recesses on the higher area – but if we use this as a puddle map, they’ll fill much later than the image on the right.

Wait… What’s a Ripple Texture?

Good question! Its one of these:

Again, you can read details on Sébastien’s blog: 


That is based on work by Antoine Zanuttini. Essentially it uses the different colour channels, that are passed through a function to generate values we can modify the normal map with

It’s worth noting that I project these in the Y-up space, because this shader assumes you’re using it on a floor (because walls don’t usually accumulate puddles…). This means that it may stretch awkwardly on non-horizontal surfaces (as I said, its thrown together). You can just make a shader that uses porosity adjustment for walls, should you wish to make them look wet.

Finally, here’s the code. As I said above – its thrown together, not optimized, your mileage may vary, usual disclaimers apply!

// Upgrade NOTE: upgraded instancing buffer 'Props' to new syntax.

Shader "Custom/Wet Surface" {
		[Header(Standard Properties)]
		_Color("Color", Color) = (1,1,1,1)
		_MainTex("Albedo (RGB)", 2D) = "white" {}
		[NoScaleOffset] _BumpMap("Normal Map", 2D) = "bump" {}
		_BumpScale("Normal Map Scale", Float) = 1
		[NoScaleOffset] _MetallicGlossMap("Metallic/Smoothness", 2D) = "black" {}
		//_Glossiness("Smoothness", Range(0,1)) = 0.5
		//_GlossMapScale("Smoothness Scale", Range(0,1)) = 0.5
		//_Metallic("Metallic", Range(0,1)) = 0.0
		[NoScaleOffset] _OcclusionMap("Occlusion", 2D) = "grey" {}
		_OcclusionStrength("Occlusion Strength", Range(0,1) ) = 1
		_ParallaxMap("Puddle Map", 2D) = "black" {}
		_Parallax("Parallax", Range(0, 0.1)) = 0
		[NoScaleOffset] _EmissionMap("Emission", 2D) = "black" {}
		_EmissionColor("Emission Color", Color) = (1,1,1,1)

		[Header(Wet Properties)]
		_FloodLevel("Flood level", Range(0,1)) = 0.25
		_WetLevel("Porosity", Range(0,1.5)) = 0.5
		_GlossMulti("Maximum Smoothness", Range(0,1)) = 1
		_MetMulti("Maximum Glossiness", Range(0,1)) = 0.5
		_NormMulti("Water Normal Smoothing", Range(0,1)) = 1
		_WaterRippleMulti("Water Ripple Strength", Range(0,20)) = 7
		_RippleTexture("Water Ripple Texture", 2D) = "bump" {}
		_RainIntensity("Rain Intensity", Vector) = (1, 0.6, 0.3, 0)
		[Toggle]_WetMetallic("Use Metallic Map for wetness?", Int) = 0
		[Header(Dry Area Overrides)]
		_GlossMultiDry("Maximum Smoothness", Range(0,1)) = 0
		_MetMultiDry("Maximum Glossiness", Range(0,1)) = 0
		_NormMultiDry("Water Normal Smoothing", Range(0,1)) = 0
		_WaterRippleMultiDry("Water Ripple Strength", Range(0,20)) = 0.1

		[Header(Detail Map)]
		_DetailMask("Detail Mask", 2D) = "black" {}
		_DetailAlbedoMap("Detail Albedo x2", 2D) = "black" {}
		_DetailNormalMapScale("Scale", Float) = 1.0
		_DetailNormalMap("Normal Map", 2D) = "bump" {}

		[ToggleOff] _SpecularHighlights("Specular Highlights", Float) = 1.0
		[ToggleOff] _GlossyReflections("Glossy Reflections", Float) = 1.0
			#define UNITY_SETUP_BRDF_INPUT SpecularSetup
		Tags{ "RenderType" = "Opaque" }
		LOD 200

		// Physically based Standard lighting model, and enable shadows on all light types
		#pragma surface surf StandardSpecular fullforwardshadows
		// Use shader model 3.0 target, to get nicer looking lighting
		#pragma target 3.0

		#pragma shader_feature _ _SPECULARHIGHLIGHTS_OFF
		#pragma shader_feature _ _GLOSSYREFLECTIONS_OFF

		// General properties
		sampler2D _MainTex,

		float _GlossMapScale, 

		fixed4 _EmissionColor, 

		// Wetness
		int _WetMetallic;
		float _WetLevel,
		float4 _RainIntensity, _RippleTexture_ST;
		sampler2D _RippleTexture;

		struct Input {
			float2 uv_MainTex;
			float2 uv_DetailNormalMap;
			float2 uv2_RippleTexture;
			float3 viewDir;
			float3 worldPos;
			float3 worldNormal;
			float3 worldRefl;

		half DetailMask(float2 uv)
			return tex2D(_DetailMask, uv).r;

		float3 ComputeRipple(float2 UV, float CurrentTime, float Weight)
			CurrentTime = _Time.y;
			float4 Ripple = tex2D(_RippleTexture, UV);
			Ripple.yz = Ripple.yz * 2 - 1; // Decompress perturbation

			float DropFrac = frac(Ripple.w + CurrentTime); // Apply time shift
			float TimeFrac = DropFrac - 1.0f + Ripple.x;
			float DropFactor = saturate(0.2f + Weight * 0.8f - DropFrac);
			float FinalFactor = DropFactor * Ripple.x * sin(clamp(TimeFrac * 9.0f, 0.0f, 3.0f) * 3.1415926);

			return float3( Ripple.yz * FinalFactor * 0.35f , 1.0f);

		void DoWetProcess(inout float3 Albedo, inout float Gloss, float2 uv)
			float Porosity = saturate((((1 - Gloss) - 0.5)) / 0.4);
			// Calc diffuse factor
			float Metalness = _WetMetallic > 0.0 ? tex2D(_MetallicGlossMap, uv).r : saturate((dot(Gloss, 0.33) * 1000 - 500));
			float factor = lerp(1, 0.2, (1 - Metalness) * Porosity);
			// Water influence on material BRDF
			Albedo *= lerp(1.0, factor, _WetLevel); // Attenuate diffuse
			Gloss = lerp(1.0, Gloss, lerp(1, factor, 0.5 * _WetLevel));

		// Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
		// See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
		// #pragma instancing_options assumeuniformscaling
		// put more per-instance properties here

		void surf(Input IN, inout SurfaceOutputStandardSpecular o) {

			// First, we need to do the parallax UV offset
			float Heightmap = tex2D(_ParallaxMap, IN.uv_MainTex).r;
			float2 offset = ParallaxOffset(Heightmap, _Parallax, IN.viewDir);
			float2 uvs = IN.uv_MainTex.xy + offset;

			// calculate base Albedo
			float3 Albedo = tex2D(_MainTex, uvs).rgb * _Color;

			// Modify Albedo by detail map information
			half mask = DetailMask(uvs);
			half3 detailAlbedo = tex2D(_DetailAlbedoMap, uvs).rgb;
			Albedo *= LerpWhiteTo(detailAlbedo * unity_ColorSpaceDouble.rgb, mask);

			// Grab metallic/smoothness input
			float2 metallicSmoothness = tex2D(_MetallicGlossMap, uvs).ra;
			float Metallic = metallicSmoothness.x;
			float Smoothness = metallicSmoothness.y;

			// Get normal map, and apply detail map information
			half3 normalTangent = UnpackScaleNormal(tex2D(_BumpMap, uvs), _BumpScale);
			half3 detailNormalTangent = UnpackScaleNormal(tex2D(_DetailNormalMap, IN.uv_DetailNormalMap), _DetailNormalMapScale);
			normalTangent = lerp(
			// Do wet calculations only if we've set it to
			if (_WetLevel > 0)
				// #1 Adjust Albedo and Smoothness based on our Porosity value
				DoWetProcess(Albedo, Smoothness, uvs);

				// We apply the ripple texture based on World Space Y-Up and animate it
				float2 uvY = (IN.worldPos.xz * _RippleTexture_ST.xy + _RippleTexture_ST.zy);
				float rippleMask = tex2D(_ParallaxMap, IN.uv_MainTex).a * _FloodLevel;
				float timer = _Time.w;
				float4 TimeMul = float4(1.0f, 0.85f, 0.93f, 1.13f);
				float4 TimeAdd = float4(0.0f, 0.2f, 0.45f, 0.7f);
				float4 Times = (timer * TimeMul + TimeAdd) * 100.6f;
				float4 Weights = _RainIntensity - float4(0, 0.25, 0.5, 0.75);
				Weights = saturate(Weights * 4);

				// Generate four shifted layer of animated circle
				float3 Ripple1 = ComputeRipple(uvY + float2(0.25f, 0.0f), Times.x, Weights.x);
				float3 Ripple2 = ComputeRipple(uvY + float2(-0.55f, 0.3f), Times.y, Weights.y);
				float3 Ripple3 = ComputeRipple(uvY + float2(0.6f, 0.85f), Times.z, Weights.z);
				float3 Ripple4 = ComputeRipple(uvY + float2(0.5f, -0.75f), Times.w, Weights.w);
				// Compose normal of the four layer based on weights
				float4 Z = lerp(float4(1, 1, 1, 1), float4(Ripple1.z, Ripple2.z, Ripple3.z, Ripple4.z), Weights);
				float3 Normal = float3(Weights.x * Ripple1.xy + Weights.y * Ripple2.xy + Weights.z * Ripple3.xy + Weights.w * Ripple4.xy, Z.x * Z.y * Z.z * Z.w);
				// return result                             
				float4 normalResult = float4(normalize(Normal), 1);

				// We smooth out "flooded" areas as water is flatter than the underlying texture
				if (Heightmap <= _FloodLevel)
					float3 smoothed = lerp(normalTangent, float3(0, 0, 1), _NormMulti);
					float3 ripples = normalResult * float3(_WaterRippleMulti, _WaterRippleMulti, 1) * float3(rippleMask, rippleMask,1);
					normalTangent = smoothed + ripples;
					Smoothness = lerp(Metallic, _GlossMulti, 1);
					Metallic = lerp(Smoothness, _MetMulti, 1);
					Smoothness = lerp(Metallic, _GlossMultiDry, 1);
					Metallic = lerp(Smoothness, _MetMultiDry, 1);
					float3 smoothed = lerp(normalTangent, float3(0, 0, 1), _NormMultiDry);
					float3 ripples = normalResult * float3(_WaterRippleMultiDry, _WaterRippleMultiDry, 1) * float3(rippleMask, rippleMask, 1);
					normalTangent = smoothed + ripples;

			// Assign Standard shader properties
			o.Normal = normalTangent;
			o.Albedo = Albedo;
			o.Specular = Metallic;
			o.Smoothness = Smoothness;
			o.Occlusion = tex2D(_OcclusionMap, uvs) * _OcclusionStrength;
			o.Emission = (tex2D(_EmissionMap, uvs) * _EmissionColor);
	FallBack "Standard"

1 thought on “Wet Surfaces”

Leave a Reply

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