# 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:

https://seblagarde.wordpress.com/2013/01/03/water-drop-2b-dynamic-rain-and-its-effects/

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.

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)

_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
_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

_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
}
CGINCLUDE
#define UNITY_SETUP_BRDF_INPUT SpecularSetup
ENDCG
Tags{ "RenderType" = "Opaque" }
LOD 200

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

// General properties
sampler2D _MainTex,
_MetallicGlossMap,
_BumpMap,
_OcclusionMap,
_EmissionMap,
_ParallaxMap,
_DetailAlbedoMap,
_DetailNormalMap;

float _GlossMapScale,
_Parallax,
_OcclusionStrength,
_BumpScale,
_DetailNormalMapScale;

fixed4 _EmissionColor,
_Color;

// Wetness
int _WetMetallic;
float _WetLevel,
_FloodLevel,
_GlossMulti,
_MetMulti,
_NormMulti,
_GlossMultiDry,
_MetMultiDry,
_NormMultiDry,
_WaterRippleMultiDry,
_WaterRippleMulti;
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;
INTERNAL_DATA
};

{
}

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.
// #pragma instancing_options assumeuniformscaling
UNITY_INSTANCING_BUFFER_START(Props)
// put more per-instance properties here
UNITY_INSTANCING_BUFFER_END(Props)

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
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(
normalTangent,
detailNormalTangent,

// 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);
normalTangent = smoothed + ripples;
Smoothness = lerp(Metallic, _GlossMulti, 1);
Metallic = lerp(Smoothness, _MetMulti, 1);
}
else
{
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;
}
}

o.Normal = normalTangent;
o.Albedo = Albedo;
o.Specular = Metallic;
o.Smoothness = Smoothness;
o.Occlusion = tex2D(_OcclusionMap, uvs) * _OcclusionStrength;
o.Emission = (tex2D(_EmissionMap, uvs) * _EmissionColor);
}
ENDCG
}
FallBack "Standard"
}
``````

## 1 thought on “Wet Surfaces”

1. Ras says:

Nice work Matt – looks great.