Volumetric Ocean Waves
The original shader belongs to:https://www.shadertoy.com/view/tdlXDM
This is an advanced Raymarching Shader adapted for Godot’s CanvasItem that procedurally simulates a dynamic underwater scene. The shader uses complex Signed Distance Fields (SDF) generated by layered noise (FBM) to define the rolling geometry of the ocean surface above the viewer.
The effect is achieved through several combined techniques:
-
SDF Mapping: Defines the dynamic wave surface using rotated and warped 2D noise functions.
-
Binary Search Raymarching: Calculates the exact point where the ray hits the water surface geometry.
-
Refraction and Illumination: Uses the calculated hit normal to simulate light refraction and light shafts breaking through the surface, creating a deep, volumetric feel.
-
Bubble Effects: Adds subtle movement and distortion (
bubblefunction) near the foreground for extra immersion.
This Godot adaptation provides comprehensive control over the scene’s color and animation:
Shader code
shader_type canvas_item;
uniform float time_speed : hint_range(0.0, 3.0) = 1.0;
uniform bool flip_y = false; // Activa para voltear verticalmente
uniform vec4 sea_color_dark : source_color = vec4(0.04, 0.32, 0.55, 1.0);
uniform vec4 sea_color_light : source_color = vec4(0.06, 0.47, 0.60, 1.0);
uniform vec4 refraction_color : source_color = vec4(0.96, 0.98, 0.86, 1.0);
uniform vec4 light_shaft_color : source_color = vec4(0.88, 0.90, 0.78, 1.0);
struct RaymarchResult {
vec3 hit_pos;
float hit_t;
float dist;
};
float hash(vec2 p) {
return 0.5 * (sin(dot(p, vec2(271.319, 413.975)) + 1217.13 * p.x * p.y)) + 0.5;
}
float noise(vec2 p) {
vec2 w = fract(p);
w = w * w * (3.0 - 2.0 * w);
p = floor(p);
return mix(mix(hash(p + vec2(0,0)), hash(p + vec2(1,0)), w.x),
mix(hash(p + vec2(0,1)), hash(p + vec2(1,1)), w.x), w.y);
}
float map_octave(vec2 uv) {
uv = (uv + noise(uv)) / 2.5;
uv = vec2(uv.x * 0.6 - uv.y * 0.8, uv.x * 0.8 + uv.y * 0.6);
vec2 uvsin = 1.0 - abs(sin(uv));
vec2 uvcos = abs(cos(uv));
uv = mix(uvsin, uvcos, uvsin);
float val = 1.0 - pow(uv.x * uv.y, 0.65);
return val;
}
float map(vec3 p) {
vec2 uv = p.xz + (TIME * time_speed) / 2.0;
float amp = 0.6, freq = 2.0, val = 0.0;
for(int i = 0; i < 3; ++i) {
val += map_octave(uv) * amp;
amp *= 0.3;
uv *= freq;
}
uv = p.xz - 1000.0 - (TIME * time_speed) / 2.0;
amp = 0.6; freq = 2.0;
for(int i = 0; i < 3; ++i) {
val += map_octave(uv) * amp;
amp *= 0.3;
uv *= freq;
}
return val + 3.0 - p.y;
}
vec3 getNormal(vec3 p, float eps) {
vec3 px = p + vec3(eps, 0.0, 0.0);
vec3 pz = p + vec3(0.0, 0.0, eps);
return normalize(vec3(map(px), eps, map(pz)));
}
RaymarchResult raymarch(vec3 ro, vec3 rd, float eps) {
RaymarchResult result;
float l = 0.0, r = 26.0;
int steps = 16;
float dist = 1000000.0;
for(int i = 0; i < steps; ++i) {
float mid = (r + l) / 2.0;
float mapmid = map(ro + rd * mid);
dist = min(dist, abs(mapmid));
if(mapmid > 0.0) {
l = mid;
} else {
r = mid;
}
if(r - l < eps) break;
}
result.hit_pos = ro + rd * l;
result.hit_t = l;
result.dist = dist;
return result;
}
float fbm(vec2 n) {
float total = 0.0, amplitude = 1.0;
for (int i = 0; i < 5; i++) {
total += noise(n) * amplitude;
n += n;
amplitude *= 0.4;
}
return total;
}
float lightShafts(vec2 st) {
float angle = -0.2;
vec2 _st = st;
float t = (TIME * time_speed) / 16.0;
st = vec2(st.x * cos(angle) - st.y * sin(angle),
st.x * sin(angle) + st.y * cos(angle));
float val = fbm(vec2(st.x * 2.0 + 200.0 + t, st.y / 4.0));
val += fbm(vec2(st.x * 2.0 + 200.0 - t, st.y / 4.0));
val = val / 3.0;
float mask = pow(clamp(1.0 - abs(_st.y - 0.15), 0.0, 1.0) * 0.49 + 0.5, 2.0);
mask *= clamp(1.0 - abs(_st.x + 0.2), 0.0, 1.0) * 0.49 + 0.5;
return pow(val * mask, 2.0);
}
vec2 bubble(vec2 uv, float scale) {
if(uv.y > 0.2) return vec2(0.0);
float t = (TIME * time_speed) / 4.0;
vec2 st = uv * scale;
vec2 _st = floor(st);
vec2 bias = vec2(0.0, 4.0 * sin(_st.x * 128.0 + t));
float mask = smoothstep(0.1, 0.2, -cos(_st.x * 128.0 + t));
st += bias;
vec2 _st_ = floor(st);
st = fract(st);
float size = noise(_st_) * 0.07 + 0.01;
vec2 pos = vec2(noise(vec2(t, _st_.y * 64.1)) * 0.8 + 0.1, 0.5);
if(length(st.xy - pos) < size) {
return (st + pos) * vec2(0.1, 0.2) * mask;
}
return vec2(0.0);
}
void fragment() {
float eps = SCREEN_PIXEL_SIZE.x;
vec3 ro = vec3(0.0, 0.0, 2.0);
vec3 lightPos = vec3(8.0, 3.0, -3.0);
vec2 res = 1.0 / SCREEN_PIXEL_SIZE;
vec2 uv = (-res + 2.0 * FRAGCOORD.xy) / res.y;
if (flip_y) {
uv.y *= -1.0;
}
uv.y *= 0.5;
uv.x *= 0.45;
uv += bubble(uv, 12.0) + bubble(uv, 24.0); // Añadir burbujas
vec3 rd = normalize(vec3(uv, -1.0));
vec3 color;
RaymarchResult result = raymarch(ro, rd, eps);
float diffuse = dot(getNormal(result.hit_pos, eps), rd) * 0.5 + 0.5;
color = mix(sea_color_dark.rgb, sea_color_light.rgb, diffuse);
color += pow(diffuse, 12.0);
vec3 ref = normalize(refract(result.hit_pos - lightPos, getNormal(result.hit_pos, eps), 0.05));
float refraction = clamp(dot(ref, rd), 0.0, 1.0);
color += refraction_color.rgb * 0.6 * pow(refraction, 1.5);
vec3 col = vec3(0.0);
col = mix(color, sea_color_dark.rgb, pow(clamp(0.0, 1.0, result.dist), 0.2));
col += light_shaft_color.rgb * lightShafts(uv);
col = (col * col + sin(col)) / vec3(1.8, 1.8, 1.9);
vec2 q = UV;
col *= 0.7 + 0.3 * pow(16.0 * q.x * q.y * (1.0 - q.x) * (1.0 - q.y), 0.2);
COLOR = vec4(col, 1.0);
}

Hello, I noticed that the shader code you shared here is extremely similar to one I originally published on Shadertoy a few years ago. I’m glad to see the shader being used and adapted, but I would appreciate it if you could add proper credit or a reference to the original source, as it helps honor the collaborative spirit of our community.
If this was an oversight, I understand—just wanted to ask for appropriate attribution to the original work. Thank you for understanding!
Thank you so much for the shader, I’ve already fixed the error. Sorry for not doing it sooner.