Stylized Ocean Waves Shader (Mobile)
Overview
Over the past few weeks, I’ve been working on a stylized ocean water shader for a mobile game, Amikin Village. I’m really happy with how the wave animation turned out, so I wanted to share the result and the core idea behind it.
In-game view:
Top view:
- Waves start at the edge of the map and move toward the center.
- Waves are dampened when they hit obstacles (rocks, terrain, etc.).
- Wave height increases closer to the coast.
How It Works
The water is rendered by a single shader. No runtime simulations.
However, several pieces of precomputed data are baked into the water surface mesh in Houdini and used in the shader:
- Wave path distance - for wave phase
- Wave mask - for wave damping
- Vertical distance to the bottom - for coastal wave amplification
The vertex shader applies vertical displacement based on the wave phase and masks.
The fragment shader renders foam using the same baked data.
Path Distance & Wave Phase (Houdini side)
First, I define the region from which waves start - a circular boundary around the island:
Then, I temporarily remove the water surface under the terrain:
For each vertex, I compute the shortest path distance to the nearest wave start point. For example, this vertex:
This gives clean path distances:
Wave Mask & Damping (Houdini side)
I dampen the waves using a mask generated by checking whether they hit obstacles along their path. To do this, I compare the path distance with the direct distance to the wave start point:
- If the distances are similar, the wave reaches the point without hitting obstacles → white mask
- If the path distance is longer, it means the wave bypasses geometry → black mask
Vertical distance to the bottom (Houdini side)
This one is straightforward. I raycast down to the terrain and store the vertical distance as water depth.
Wave Form (vertex shader side)
All the data is baked, so everything is ready to shape the waves in the vertex shader.
Applying frac() to the distance generates the wave phase. Adding a time offset animates it:
half distanceToWaveStart = input.uv1.x;
half wavePhase = distanceToWaveStart / _WaveLength + _Time.y / _WaveSpeed;
wavePhase = frac(wavePhase);
Then I wrap the wave phase with a ramp (1D texture) to fine-tune the wave height.
Both the wave mask and the baked vertical distance to the bottom scale the wave height, damping waves behind obstacles and increasing the amplitude near the shore.
half2 waveRampUv = half2(wavePhase, 0.5);
half4 waveRamp = SAMPLE_TEXTURE2D_LOD(_WaveShape, sampler_WaveShape, waveRampUv, 0);
half vertDistanceToBottom = input.uv1.y;
half waveMaskByVertDistToBottom = Remap(vertDistanceToBottom, ...);
half wavePropagationMask = input.color.y;
half waveMask = waveMaskByVertDistToBottom * wavePropagationMask;
half heightOffset = waveRamp.x * _WaveHeight * waveMask;
...
positionWS.y += heigthOffset;
All together:
Wave Foam (fragment shader side)
I render wave foam using the same baked data, along with another ramp around the wave phase to control timing and fine-tune the look.
Each wave foam consists of three separate “layers” that I calculate individually:
- Wave front
- Wave tail
- Wave rollback
Additional Features:
On top of that:
- Depth gradient
- Specular highlights
- Caustic
- Refraction
- Other typical water shading components, depending on performance budget