Stylized Water Shader for Unity URP
A URP stylized water shader for Unity 6: rolling vertex waves, a deep-to-horizon colour ramp, crest foam, a sun glint and optional shore foam from the depth texture.
Also called: toon water shader, low poly water, cartoon water, ocean shader.
Environment URP, Unity 6000.0+ intermediate MIT licensed
Use it for: Lakes, rivers and seas in a stylized or low-poly game, where readable colour and moving foam matter more than physically accurate reflections.
The preview is a WebGL copy of the shader's maths with one fixed light, so it shows no shadows, Bloom or textures. Shore foam needs Unity's depth texture and only appears in your scene.
What it does
- Two crossed sine waves move the vertices, with matching normals
- Colour ramps from the deep colour (looking down) to the horizon colour (grazing angles)
- Drifting noise foam on the wave crests
- Sun glint from the main directional light
- Optional shore foam where the water meets geometry, read from the depth texture
- Transparent with per-colour alpha
How it works
The vertex stage raises each vertex by sin(x) × cos(z) of its world position plus time, and computes the slope of that surface so the normal follows the waves. The fragment stage uses the normal and view direction for a fresnel ramp between the two colours, then adds foam where drifting noise and the wave crest together pass a threshold.
With Shore Foam on, the shader compares the depth of the scene behind the water with the depth of the water surface. Where they are close, the water is shallow, and foam is added.
Setup
- Save the file as StylizedWater.shader under Assets and create a material that uses Scripts For Unity > Stylized Water.
- Put it on a subdivided plane: waves move vertices, so a mesh needs enough of them. Unity's built-in Plane (10 × 10 quads) works for small ponds; use a denser mesh for larger water.
- For shore foam, enable Depth Texture on your URP asset, then tick Shore Foam on the material.
Common mistakes
- The waves look like flat panels: the mesh has too few vertices. Waves can only bend where there are vertices to move.
- Shore foam never appears: the URP asset's Depth Texture option is off, or the camera overrides it. Check both.
- Objects under the water look wrong: the shader does not refract or fog what is under the surface; lower the alpha of the colours if you need to see further down.
Questions
- Does the water reflect the scene?
- No. It is a stylized shader: colour comes from a deep-to-horizon ramp, foam and a sun glint rather than real reflections.
- Why do the waves look like flat panels?
- Waves move vertices, so the mesh needs enough of them. Unity's built-in Plane works for small ponds; use a denser mesh for larger water.
- What does shore foam need?
- Turn on Depth Texture on your URP asset, then tick Shore Foam on the material. The shader compares the water's depth with the scene behind it to find shallow edges.
Source
StylizedWater.shader · 174 lines · ShaderLab and HLSL for the Universal Render Pipeline 17 (Unity 6)
// Stylized Water: rolling vertex waves, a deep-to-horizon colour ramp,
// noise foam on the crests, a sun glint, and optional shore foam from the
// depth texture. Transparent. URP 17 (Unity 6).
// Scripts For Unity, MIT licensed.
Shader "Scripts For Unity/Stylized Water"
{
Properties
{
_DeepColor ("Deep Color", Color) = (0.05, 0.32, 0.5, 0.92)
_HorizonColor ("Horizon Color", Color) = (0.35, 0.78, 0.86, 0.8)
_FresnelPower ("Fresnel Power", Range(0.5, 8)) = 3
_WaveHeight ("Wave Height", Range(0, 1)) = 0.12
_WaveFrequency ("Wave Frequency", Range(0.1, 5)) = 1.2
_WaveSpeed ("Wave Speed", Range(0, 5)) = 1.1
_FoamColor ("Foam Color", Color) = (1, 1, 1, 1)
_FoamScale ("Foam Scale", Range(0.1, 10)) = 6
_FoamAmount ("Foam Amount", Range(0, 1)) = 0.25
_GlintStrength ("Sun Glint", Range(0, 4)) = 1.2
[Toggle(_SHORE_FOAM)] _ShoreFoam ("Shore Foam (needs Depth Texture)", Float) = 0
_ShoreDistance ("Shore Foam Distance", Range(0.05, 3)) = 0.6
}
SubShader
{
Tags
{
"RenderType" = "Transparent"
"Queue" = "Transparent"
"RenderPipeline" = "UniversalPipeline"
}
Pass
{
Name "Water"
Tags { "LightMode" = "UniversalForward" }
Blend SrcAlpha OneMinusSrcAlpha
ZWrite Off
Cull Back
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma shader_feature_local _SHORE_FOAM
#pragma multi_compile_fog
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"
CBUFFER_START(UnityPerMaterial)
half4 _DeepColor;
half4 _HorizonColor;
float _FresnelPower;
float _WaveHeight;
float _WaveFrequency;
float _WaveSpeed;
half4 _FoamColor;
float _FoamScale;
float _FoamAmount;
float _GlintStrength;
float _ShoreFoam;
float _ShoreDistance;
CBUFFER_END
float SfuHash13(float3 p)
{
p = frac(p * 0.1031);
p += dot(p, p.zyx + 31.32);
return frac((p.x + p.y) * p.z);
}
float SfuValueNoise(float3 p)
{
float3 i = floor(p);
float3 f = frac(p);
float3 u = f * f * (3.0 - 2.0 * f);
float n000 = SfuHash13(i);
float n100 = SfuHash13(i + float3(1, 0, 0));
float n010 = SfuHash13(i + float3(0, 1, 0));
float n110 = SfuHash13(i + float3(1, 1, 0));
float n001 = SfuHash13(i + float3(0, 0, 1));
float n101 = SfuHash13(i + float3(1, 0, 1));
float n011 = SfuHash13(i + float3(0, 1, 1));
float n111 = SfuHash13(i + float3(1, 1, 1));
return lerp(lerp(lerp(n000, n100, u.x), lerp(n010, n110, u.x), u.y),
lerp(lerp(n001, n101, u.x), lerp(n011, n111, u.x), u.y), u.z);
}
// Four octaves, normalised back to 0..1
float SfuFractalNoise(float3 p)
{
float n = SfuValueNoise(p) * 0.5;
n += SfuValueNoise(p * 2.03) * 0.25;
n += SfuValueNoise(p * 4.01) * 0.125;
n += SfuValueNoise(p * 8.05) * 0.0625;
return n / 0.9375;
}
struct Attributes
{
float4 positionOS : POSITION;
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float3 positionWS : TEXCOORD0;
float3 normalWS : TEXCOORD1;
float4 screenPos : TEXCOORD2;
float wave : TEXCOORD3;
float fogFactor : TEXCOORD4;
};
Varyings vert(Attributes input)
{
Varyings output;
float3 positionWS = TransformObjectToWorld(input.positionOS.xyz);
// Two crossed sine waves and their slopes, for the normal
float t = _Time.y * _WaveSpeed;
float ax = positionWS.x * _WaveFrequency + t;
float az = positionWS.z * _WaveFrequency * 0.8 + t * 0.9;
float wave = sin(ax) * cos(az);
float slopeX = cos(ax) * cos(az) * _WaveFrequency * _WaveHeight;
float slopeZ = -sin(ax) * sin(az) * _WaveFrequency * 0.8 * _WaveHeight;
positionWS.y += wave * _WaveHeight;
output.positionWS = positionWS;
output.positionCS = TransformWorldToHClip(positionWS);
output.normalWS = normalize(float3(-slopeX, 1.0, -slopeZ));
output.screenPos = ComputeScreenPos(output.positionCS);
output.wave = wave;
output.fogFactor = ComputeFogFactor(output.positionCS.z);
return output;
}
half4 frag(Varyings input) : SV_Target
{
float3 normalWS = normalize(input.normalWS);
float3 viewDirWS = GetWorldSpaceNormalizeViewDir(input.positionWS);
// Deep colour looking down, horizon colour at grazing angles
float fresnel = pow(1.0 - saturate(dot(normalWS, viewDirWS)), _FresnelPower);
half4 water = lerp(_DeepColor, _HorizonColor, fresnel);
// Foam: drifting noise, boosted on the wave crests and faded in the troughs
float2 drift = float2(1.0, 0.6) * _Time.y * 0.15;
float noise = SfuFractalNoise(float3(input.positionWS.xz * _FoamScale + drift, _Time.y * 0.1));
float crest = saturate(input.wave * 0.5 + 0.5);
float foamMask = noise * lerp(0.6, 1.25, crest);
float foam = smoothstep(1.0 - _FoamAmount, 1.02 - _FoamAmount, foamMask);
#if defined(_SHORE_FOAM)
// Foam where the water surface is close to the geometry behind it
float2 screenUV = input.screenPos.xy / input.screenPos.w;
float sceneEyeDepth = LinearEyeDepth(SampleSceneDepth(screenUV), _ZBufferParams);
float shore = 1.0 - saturate((sceneEyeDepth - input.screenPos.w) / _ShoreDistance);
foam = max(foam, step(0.5, shore * (0.5 + noise)));
#endif
// Sun glint from the main light
Light mainLight = GetMainLight();
float3 halfDirWS = normalize(mainLight.direction + viewDirWS);
float glint = smoothstep(0.92, 0.95, pow(saturate(dot(normalWS, halfDirWS)), 64.0)) * _GlintStrength;
float3 color = lerp(water.rgb, _FoamColor.rgb, foam) + glint * mainLight.color;
float alpha = saturate(max(water.a, foam * _FoamColor.a) + glint);
return half4(MixFog(color, input.fogFactor), alpha);
}
ENDHLSL
}
}
FallBack "Hidden/Universal Render Pipeline/FallbackError"
}
More shaders
- Wind Foliage ShaderA URP grass and leaves shader for Unity 6: alpha-cut foliage that sways in gusts of wind, with a root-to-tip colour gradient. Shadows and depth sway with it.
- Gradient Skybox ShaderA URP stylized skybox shader for Unity 6: a three-colour gradient sky with a sun disc and glow that follow your main directional light. No textures needed.
- Toon Lit Shader with OutlineA URP toon shader for Unity 6: two-band cel shading that receives shadows, a hard specular highlight, a rim light and an inverted-hull outline, in one .shader file.
- Dissolve ShaderA URP dissolve shader for Unity 6 that burns a mesh away along procedural noise with a glowing HDR edge. No noise texture needed, and its shadow dissolves with it.