Wind Waker Water – NO Textures needed!

Recreation of the water from the Legend of Zelda – The Wind Waker

NO Texture needed! Just drag and drop!

Usually you need a texture to do this kind of stuff but I like to keep my things CC0 so I wanted to avoid using any textures.

Luckily I found this: 2D Wind Waker Water – Shadertoy

Which procedurally generates the foam, so I:

  • Modified it to work with Godot
  • Created a vertex shader for it so it moves like a wave
  • And used some Fractal Brownian Motion to make it more interesting!

The default colors are actually the colors pulled straight from the original 2002 GameCube release of the game (or at least as close I could get to it using my screen to read the hex colors).

The settings I used are in the screenshots.

This shader is best experienced listening to this: Wind Waker The Great Sea (Ocean) Music Extended – YouTube

 

Shader code
// Wind Waker style water - NekotoArts
// Adapted from https://www.shadertoy.com/view/3tKBDz
// After which I added in some fractal Brownian motion
// as well as vertex displacement

shader_type spatial;

uniform vec4 WATER_COL : hint_color =  vec4(0.04, 0.38, 0.88, 1.0);
uniform vec4 WATER2_COL : hint_color =  vec4(0.04, 0.35, 0.78, 1.0);
uniform vec4 FOAM_COL : hint_color = vec4(0.8125, 0.9609, 0.9648, 1.0);
uniform float distortion_speed = 2.0;
uniform vec2 tile = vec2(5.0, 5.0);
uniform float height = 2.0;
uniform vec2 wave_size = vec2(2.0, 2.0);
uniform float wave_speed = 1.5;

const float M_2PI = 6.283185307;
const float M_6PI = 18.84955592;

float random(vec2 uv) {
    return fract(sin(dot(uv.xy,
        vec2(12.9898,78.233))) *
            43758.5453123);
}

float noise(vec2 uv) {
    vec2 uv_index = floor(uv);
    vec2 uv_fract = fract(uv);

    // Four corners in 2D of a tile
    float a = random(uv_index);
    float b = random(uv_index + vec2(1.0, 0.0));
    float c = random(uv_index + vec2(0.0, 1.0));
    float d = random(uv_index + vec2(1.0, 1.0));

    vec2 blur = smoothstep(0.0, 1.0, uv_fract);

    return mix(a, b, blur.x) +
            (c - a) * blur.y * (1.0 - blur.x) +
            (d - b) * blur.x * blur.y;
}

float fbm(vec2 uv) {
    int octaves = 6;
    float amplitude = 0.5;
    float frequency = 3.0;
	float value = 0.0;
	
    for(int i = 0; i < octaves; i++) {
        value += amplitude * noise(frequency * uv);
        amplitude *= 0.5;
        frequency *= 2.0;
    }
    return value;
}

float circ(vec2 pos, vec2 c, float s)
{
    c = abs(pos - c);
    c = min(c, 1.0 - c);

    return smoothstep(0.0, 0.002, sqrt(s) - sqrt(dot(c, c))) * -1.0;
}

// Foam pattern for the water constructed out of a series of circles
float waterlayer(vec2 uv)
{
    uv = mod(uv, 1.0); // Clamp to [0..1]
	
    float ret = 1.0;
    ret += circ(uv, vec2(0.37378, 0.277169), 0.0268181);
    ret += circ(uv, vec2(0.0317477, 0.540372), 0.0193742);
    ret += circ(uv, vec2(0.430044, 0.882218), 0.0232337);
    ret += circ(uv, vec2(0.641033, 0.695106), 0.0117864);
    ret += circ(uv, vec2(0.0146398, 0.0791346), 0.0299458);
    ret += circ(uv, vec2(0.43871, 0.394445), 0.0289087);
    ret += circ(uv, vec2(0.909446, 0.878141), 0.028466);
    ret += circ(uv, vec2(0.310149, 0.686637), 0.0128496);
    ret += circ(uv, vec2(0.928617, 0.195986), 0.0152041);
    ret += circ(uv, vec2(0.0438506, 0.868153), 0.0268601);
    ret += circ(uv, vec2(0.308619, 0.194937), 0.00806102);
    ret += circ(uv, vec2(0.349922, 0.449714), 0.00928667);
    ret += circ(uv, vec2(0.0449556, 0.953415), 0.023126);
    ret += circ(uv, vec2(0.117761, 0.503309), 0.0151272);
    ret += circ(uv, vec2(0.563517, 0.244991), 0.0292322);
    ret += circ(uv, vec2(0.566936, 0.954457), 0.00981141);
    ret += circ(uv, vec2(0.0489944, 0.200931), 0.0178746);
    ret += circ(uv, vec2(0.569297, 0.624893), 0.0132408);
    ret += circ(uv, vec2(0.298347, 0.710972), 0.0114426);
    ret += circ(uv, vec2(0.878141, 0.771279), 0.00322719);
    ret += circ(uv, vec2(0.150995, 0.376221), 0.00216157);
    ret += circ(uv, vec2(0.119673, 0.541984), 0.0124621);
    ret += circ(uv, vec2(0.629598, 0.295629), 0.0198736);
    ret += circ(uv, vec2(0.334357, 0.266278), 0.0187145);
    ret += circ(uv, vec2(0.918044, 0.968163), 0.0182928);
    ret += circ(uv, vec2(0.965445, 0.505026), 0.006348);
    ret += circ(uv, vec2(0.514847, 0.865444), 0.00623523);
    ret += circ(uv, vec2(0.710575, 0.0415131), 0.00322689);
    ret += circ(uv, vec2(0.71403, 0.576945), 0.0215641);
    ret += circ(uv, vec2(0.748873, 0.413325), 0.0110795);
    ret += circ(uv, vec2(0.0623365, 0.896713), 0.0236203);
    ret += circ(uv, vec2(0.980482, 0.473849), 0.00573439);
    ret += circ(uv, vec2(0.647463, 0.654349), 0.0188713);
    ret += circ(uv, vec2(0.651406, 0.981297), 0.00710875);
    ret += circ(uv, vec2(0.428928, 0.382426), 0.0298806);
    ret += circ(uv, vec2(0.811545, 0.62568), 0.00265539);
    ret += circ(uv, vec2(0.400787, 0.74162), 0.00486609);
    ret += circ(uv, vec2(0.331283, 0.418536), 0.00598028);
    ret += circ(uv, vec2(0.894762, 0.0657997), 0.00760375);
    ret += circ(uv, vec2(0.525104, 0.572233), 0.0141796);
    ret += circ(uv, vec2(0.431526, 0.911372), 0.0213234);
    ret += circ(uv, vec2(0.658212, 0.910553), 0.000741023);
    ret += circ(uv, vec2(0.514523, 0.243263), 0.0270685);
    ret += circ(uv, vec2(0.0249494, 0.252872), 0.00876653);
    ret += circ(uv, vec2(0.502214, 0.47269), 0.0234534);
    ret += circ(uv, vec2(0.693271, 0.431469), 0.0246533);
    ret += circ(uv, vec2(0.415, 0.884418), 0.0271696);
    ret += circ(uv, vec2(0.149073, 0.41204), 0.00497198);
    ret += circ(uv, vec2(0.533816, 0.897634), 0.00650833);
    ret += circ(uv, vec2(0.0409132, 0.83406), 0.0191398);
    ret += circ(uv, vec2(0.638585, 0.646019), 0.0206129);
    ret += circ(uv, vec2(0.660342, 0.966541), 0.0053511);
    ret += circ(uv, vec2(0.513783, 0.142233), 0.00471653);
    ret += circ(uv, vec2(0.124305, 0.644263), 0.00116724);
    ret += circ(uv, vec2(0.99871, 0.583864), 0.0107329);
    ret += circ(uv, vec2(0.894879, 0.233289), 0.00667092);
    ret += circ(uv, vec2(0.246286, 0.682766), 0.00411623);
    ret += circ(uv, vec2(0.0761895, 0.16327), 0.0145935);
    ret += circ(uv, vec2(0.949386, 0.802936), 0.0100873);
    ret += circ(uv, vec2(0.480122, 0.196554), 0.0110185);
    ret += circ(uv, vec2(0.896854, 0.803707), 0.013969);
    ret += circ(uv, vec2(0.292865, 0.762973), 0.00566413);
    ret += circ(uv, vec2(0.0995585, 0.117457), 0.00869407);
    ret += circ(uv, vec2(0.377713, 0.00335442), 0.0063147);
    ret += circ(uv, vec2(0.506365, 0.531118), 0.0144016);
    ret += circ(uv, vec2(0.408806, 0.894771), 0.0243923);
    ret += circ(uv, vec2(0.143579, 0.85138), 0.00418529);
    ret += circ(uv, vec2(0.0902811, 0.181775), 0.0108896);
    ret += circ(uv, vec2(0.780695, 0.394644), 0.00475475);
    ret += circ(uv, vec2(0.298036, 0.625531), 0.00325285);
    ret += circ(uv, vec2(0.218423, 0.714537), 0.00157212);
    ret += circ(uv, vec2(0.658836, 0.159556), 0.00225897);
    ret += circ(uv, vec2(0.987324, 0.146545), 0.0288391);
    ret += circ(uv, vec2(0.222646, 0.251694), 0.00092276);
    ret += circ(uv, vec2(0.159826, 0.528063), 0.00605293);
	return max(ret, 0.0);
}

// Procedural texture generation for the water
vec3 water(vec2 uv, vec3 cdir, float iTime)
{
    uv *= vec2(0.25);
	uv += fbm(uv) * 0.2;

    // Parallax height distortion with two directional waves at
    // slightly different angles.
    vec2 a = 0.025 * cdir.xz / cdir.y; // Parallax offset
    float h = sin(uv.x + iTime); // Height at UV
    uv += a * h;
    h = sin(0.841471 * uv.x - 0.540302 * uv.y + iTime);
    uv += a * h;
    
    // Texture distortion
    float d1 = mod(uv.x + uv.y, M_2PI);
    float d2 = mod((uv.x + uv.y + 0.25) * 1.3, M_6PI);
    d1 = iTime * 0.07 + d1;
    d2 = iTime * 0.5 + d2;
    vec2 dist = vec2(
    	sin(d1) * 0.15 + sin(d2) * 0.05,
    	cos(d1) * 0.15 + cos(d2) * 0.05
    );
    
    vec3 ret = mix(WATER_COL.rgb, WATER2_COL.rgb, waterlayer(uv + dist.xy));
    ret = mix(ret, FOAM_COL.rgb, waterlayer(vec2(1.0) - uv - dist.yx));
    return ret;
}


void vertex(){
	float time = TIME * wave_speed;
	vec2 uv = UV * wave_size;
	float d1 = mod(uv.x + uv.y, M_2PI);
    float d2 = mod((uv.x + uv.y + 0.25) * 1.3, M_6PI);
    d1 = time * 0.07 + d1;
    d2 = time * 0.5 + d2;
    vec2 dist = vec2(
    	sin(d1) * 0.15 + sin(d2) * 0.05,
    	cos(d1) * 0.15 + cos(d2) * 0.05
    );
	VERTEX.y += dist.y * height;
}

void fragment()
{
	vec2 uv = UV;
	
    ALBEDO = vec3(water(uv * tile, vec3(0,1,0), TIME * distortion_speed));
}
Tags
toon, water, wind waker, zelda
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from NekotoArts

Omen’s Smoke Bomb from Valorant

3D Edge Detection Shader (Borderlands-Style)

Basic Vector Sprite Upscaling

Related shaders

Wind Waker 2d Water Shader Canvas_Item

Car Tracks On Snow Or Sand – Using viewport textures and particles

Wind line

Subscribe
Notify of
guest

11 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Verbante
Verbante
2 years ago

Heya, I used the shader in my game and made 2 modifications:

for(int i = 0; i < octaves; i++)  

changed to

for(int i = 0; i < 6; i++) 

this was necessary for the shader to compile and run in my browser (firefox).

Also added a uv_offset, because my game had a big scale and using a mesh that big caused jitter in the web browser. So, I used a small mesh and moved it around with the player and updated the uv_offset to create an illusion of movement.

ALBEDO = vec3(water(uv * tile, vec3(0,1,0), TIME * distortion_speed));

changed to

uniform vec2 uv_offset = vec2(0.0,0.0)
...
ALBEDO = vec3(water( ( uv + uv_offset ) * tile, vec3(0,1,0), TIME * distortion_speed));

Thanks for the great shader!

Last edited 2 years ago by Verbante
Verbante
Verbante
2 years ago
Reply to  Verbante

The loop problem might be caused by my graphics card aswell. Here’s a stack overflow entry about it: https://stackoverflow.com/questions/38986208/webgl-loop-index-cannot-be-compared-with-non-constant-expression/39298265

Wittawat Keawcharoen
2 years ago

Hi,

thanks for this great shader.

While this is working fine on 2D plane. I’m struggling to make this work on the surface of 3D sphere. Any idea or help would be appreciate!

Wittawat Keawcharoen
2 years ago

I have posted a picture from my game here

https://github.com/aik6980/fj2021_game/issues/1

Last edited 2 years ago by Wittawat Keawcharoen
Ombremonde
Ombremonde
11 months ago

Hi, thanks for the great shader it’s really good!! I’ve used it in my game, mixed with 2 other shaders. Though there’s some issue with the tiling that I don’t know how to fix. Do you have any idea why it cuts the foam as if the texture was not seamless?

Nolan
Nolan
7 months ago

Hi sorry but do you have instructions on how to input this shader? do we make a script and put it in?

Plagueheart
2 months ago
Reply to  Nolan

In your scene create a MeshInstance3d -> Plane -> Subdivision Width, Depth = 100,100
You can make the mesh Unique -> Save As
Open up your new mesh and apply a Spatial Material, Copy + Paste shader -> Unique -> Save As

Jontay
Jontay
5 months ago

Hey, some of us are complete noobs at this, can anyone point me to a video or explain in a lot more detail on what I do with shader code inside of Godot?

Josh
Josh
5 months ago

I am trying to use this in a game jam game I’m working on. How can I replicate the logic for the vertex shader in a script attached to a Mesh Node to create buoyancy and such? I can’t copy the logic straight up because it uses UVs over things like worldposition. I’m fairly new to Godot so I’m struggling to figure out how to make it happen, any suggestions would be awesome!

Plagueheart
2 months ago
Reply to  Josh

Not sure if you found a answer. But you would create MeshInstance3d -> Plane -> Click on Mesh -> Subdivision Width, Depth = 100, 100