Stylized water layers
This stylized water is 7 colours added together.
Left: base depth colour only.
Right: reflection, subsurface, foam, sun specular, sparkles and edge foam come in one at a time.
The problem
Stylized water often gets built as one big shader where every effect is tangled with the others. Change the foam and the colour shifts. This shader keeps each effect as a separate colour and adds them at the end:
color = base + reflection + sss + foam + sunSpecular + sparkle + edgeFoam
The video adds them one at a time.
How it works
Base: the scene behind the water, tinted from shallow to deep by exp(-0.5 × optical depth), then toward a far colour by distance.
Reflection: a cubemap scaled by a fresnel term, (1 + dot(n, -v))^5.
Subsurface: a colour scaled by wave height and by how much you look toward the sun.
Foam: a scrolling foam texture, its UVs pushed by the wave normal.
Sun specular: round(saturate(pow(dot(R, L), 3500))). The round makes it a hard-edged highlight, not a soft one.
Sparkles: 2 scrolling sparkle normal maps, ceil(pow(mask, 20000)). Single pixels flicker on and off.
Edge foam: round(exp(-depth / 0.25)). Anywhere shallower than about 0.17 m becomes a solid white band.
What I hit
The wave normal comes from cross(binormal, tangent), which points down. So the repo's (1 + dot(n, -v))^5 is Schlick's (1 - cos)^5 in disguise. I "fixed" it to 1 - dot on my first port and the whole lake turned white, fresnel 1 everywhere. I reverted to the repo form.
The same edge-foam rule turns a rock just under the surface into a solid white patch (right edge, around 8 s). That is the formula, not a glitch.
My build
three.js 0.170. Unity's GrabPass and depth texture became an opaque pass into a render target with a depth texture; the water draws after it with a manual depth test. The reflection probe is a CubeCamera captured once. Wave, sparkle and foam textures are my own. Values come from the repo's options figure.
Trade-offs
- Each term is independent, so art direction is one slider per layer
- Hard rounded highlights give a clean toon look without a post pass
- Needs scene colour and depth, so one extra opaque pass
- Edge foam depends on view depth, so it can flood flat shallow areas
- The static reflection probe won't show moving objects
When to use it
Stylized lakes, rivers and coasts. Swap the cubemap for screen-space reflection if things move near the water.
Source and credits
Technique: Stylized Water for Unity by Daniel Shervheim. BSD 3-Clause license, Copyright (c) 2019, Daniel Shervheim.
Repo: github.com/danielshervheim/unity-stylized-water
Code: I ported Stylized Water.shader and WaterUtilities.cginc to GLSL in three.js, with the parameter values from the repo's options panel. The normal maps, foam texture, island and jetty are my own; no textures, models or screenshots from the repo.
Topic inspired by @yolwoocle_'s Godot water shader breakdown. This build is three.js, not Godot.