// Stylized 3D water shader, assembled from the gameidea.org tutorial: // https://gameidea.org/2026/02/01/creating-a-stylized-3d-water-shader/ // Seascape-style FBM waves + depth fog, Beer-Lambert absorption, refraction, // caustics and voronoi foam. // // Adaptation for this project: wave displacement uses the `wave_time` uniform // (driven by ocean.gd) instead of TIME, so the CPU-side get_wave_height() // used for buoyancy stays in sync with the rendered surface. Purely cosmetic // animation (foam, caustics) still uses TIME. shader_type spatial; render_mode depth_draw_always; uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap; uniform sampler2D DEPTH_TEXTURE : hint_depth_texture, filter_linear_mipmap; uniform float max_depth : hint_range(0.1, 50.0, 0.1) = 10.0; uniform vec3 underwater_fog_color : source_color = vec3(0.0, 0.05, 0.1); uniform float fade_start_depth : hint_range(0.0, 50.0, 0.1) = 0.5; // wave parameters (pushed from ocean.gd -- change them there, not here, // or buoyancy physics will desync from the rendered surface). // wave_time is accumulated phase (delta * sea_speed integrated on the CPU), // so speed changes mid-game never make the surface jump. uniform float wave_time = 0.0; uniform float sea_height : hint_range(0.0, 5.0) = 1.3; uniform float sea_choppy : hint_range(0.0, 10.0) = 4.0; uniform float sea_freq : hint_range(0.0, 0.5) = 0.08; // iterations uniform int ITER_GEOMETRY = 3; uniform int ITER_FRAGMENT = 5; const mat2 octave_m = mat2(vec2(1.6, 1.2), vec2(-1.2, 1.6)); // refraction settings uniform float refraction_strength : hint_range(0.0, 2.0, 0.01) = 1.0; uniform float refraction_distance_fade : hint_range(0.0, 100.0, 1.0) = 5.0; // for smoothness part uniform float normal_epsilon : hint_range(0.001, 1.0) = 0.01; uniform float normal_smoothness_dist : hint_range(0.0, 1000.0) = 50.0; uniform vec3 base_tint_color : source_color = vec3(0.439, 0.973, 1.0); uniform vec3 deep_color : source_color = vec3(0.0, 0.341, 0.29); // beer-lambert absorption uniform vec3 water_absorption : source_color = vec3(0.3, 0.06, 0.02); uniform float roughness : hint_range(0.0, 1.0) = 0.125; uniform float metallic : hint_range(0.0, 1.0) = 0.0; uniform float specular : hint_range(0.0, 1.0) = 0.5; // caustics uniforms uniform sampler2D caustics_texture : filter_linear_mipmap, repeat_enable; uniform float caustics_scale : hint_range(0.1, 10.0, 0.1) = 2.0; uniform float caustics_speed : hint_range(0.0, 1.0, 0.01) = 0.1; uniform float caustics_intensity : hint_range(0.0, 2.0, 0.1) = 0.8; uniform float caustics_depth_fade : hint_range(0.0, 1.0, 0.01) = 0.7; // foam uniforms uniform vec3 foam_color : source_color = vec3(1.0, 1.0, 1.0); uniform float foam_depth_start : hint_range(0.0, 5.0, 0.01) = 0.8; uniform float foam_depth_end : hint_range(0.0, 5.0, 0.01) = 0.0; uniform float foam_noise_scale : hint_range(0.0, 10.0, 0.01) = 2.0; uniform float foam_noise_speed : hint_range(0.0, 2.0, 0.01) = 1.0; uniform float foam_cutoff : hint_range(0.0, 1.0, 0.01) = 0.7; uniform float foam_crest_threshold : hint_range(0.0, 1.0, 0.01) = 0.7; uniform float foam_crest_amount : hint_range(0.0, 10.0, 0.1) = 2.0; uniform vec3 foam_edge_color : source_color = vec3(0.0, 0.0, 0.05); uniform vec2 foam_edge_offset = vec2(0.02, 0.02); uniform float voronoi_scale : hint_range(0.0, 50.0) = 9.0; uniform float voronoi_strength : hint_range(0.0, 8.0) = 0.8; uniform vec2 wave_offset = vec2(0.0); // --------------------------------------------------------------------------- // wave functions // --------------------------------------------------------------------------- float hash12(vec2 p) { uvec2 q = uvec2(ivec2(p)) * uvec2(1597334677u, 3812015801u); uint n = (q.x ^ q.y) * 1597334677u; return float(n) * (1.0 / 4294967295.0); } float noise(in vec2 p) { vec2 i = floor(p); vec2 f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f); return -1.0 + 2.0 * mix(mix(hash12(i + vec2(0.0, 0.0)), hash12(i + vec2(1.0, 0.0)), u.x), mix(hash12(i + vec2(0.0, 1.0)), hash12(i + vec2(1.0, 1.0)), u.x), u.y); } float sea_octave(vec2 uv, float choppy) { uv += noise(uv); vec2 wv = 1.0 - abs(sin(uv)); vec2 swv = abs(cos(uv)); wv = mix(wv, swv, wv); return pow(1.0 - pow(wv.x * wv.y, 0.65), choppy); } float map(vec3 p, float time) { float freq = sea_freq; float amp = sea_height; float choppy = sea_choppy; vec2 uv = p.xz; uv.x *= 0.75; float h = 0.0; for (int i = 0; i < ITER_GEOMETRY; i++) { float d = sea_octave((uv + time) * freq, choppy); d += sea_octave((uv - time) * freq, choppy); h += d * amp; uv *= octave_m; freq *= 1.9; amp *= 0.22; choppy = mix(choppy, 1.0, 0.2); } return p.y - h; } float map_detailed(vec3 p, float time) { float freq = sea_freq; float amp = sea_height; float choppy = sea_choppy; vec2 uv = p.xz; uv.x *= 0.75; float h = 0.0; for (int i = 0; i < ITER_FRAGMENT; i++) { float d = sea_octave((uv + time) * freq, choppy); d += sea_octave((uv - time) * freq, choppy); h += d * amp; uv *= octave_m; freq *= 1.9; amp *= 0.22; choppy = mix(choppy, 1.0, 0.2); } return p.y - h; } vec3 get_normal_detailed(vec3 p, float eps, float time) { vec3 n; n.y = map_detailed(p, time); n.x = map_detailed(vec3(p.x + eps, p.y, p.z), time) - n.y; n.z = map_detailed(vec3(p.x, p.y, p.z + eps), time) - n.y; n.y = eps; return normalize(n); } // --------------------------------------------------------------------------- // foam noise functions // --------------------------------------------------------------------------- float hash13(vec3 p) { uvec3 q = uvec3(ivec3(p)) * uvec3(1597334677u, 3812015801u, 2798796415u); uint n = (q.x ^ q.y ^ q.z) * 1597334677u; return float(n) * (1.0 / 4294967295.0); } float hash3d(vec3 p) { return hash13(p); } vec2 hash22(vec2 p) { vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973)); p3 += dot(p3, p3.yzx + 33.33); return fract((p3.xx + p3.yz) * p3.zy); } // voronoi cell noise: distance to nearest animated point per grid cell, // ideal for simulating foam clumps and bubbles float voronoi(vec2 uv) { vec2 n = floor(uv); vec2 f = fract(uv); float m_dist = 1.0; for (int j = -1; j <= 1; j++) { for (int i = -1; i <= 1; i++) { vec2 g = vec2(float(i), float(j)); vec2 o = hash22(n + g); // animate the point within the cell o = 0.5 + 0.5 * sin(TIME * foam_noise_speed + 6.2831 * o); vec2 r = g - f + o; float d = dot(r, r); m_dist = min(m_dist, d); } } return m_dist; } float noise3d(in vec3 p) { vec3 i = floor(p); vec3 f = fract(p); f = f * f * (3.0 - 2.0 * f); return mix( mix( mix(hash3d(i + vec3(0.0, 0.0, 0.0)), hash3d(i + vec3(1.0, 0.0, 0.0)), f.x), mix(hash3d(i + vec3(0.0, 1.0, 0.0)), hash3d(i + vec3(1.0, 1.0, 0.0)), f.x), f.y), mix( mix(hash3d(i + vec3(0.0, 0.0, 1.0)), hash3d(i + vec3(1.0, 0.0, 1.0)), f.x), mix(hash3d(i + vec3(0.0, 1.0, 1.0)), hash3d(i + vec3(1.0, 1.0, 1.0)), f.x), f.y), f.z); } float fbm_voronoi(vec2 uv) { float v = 0.0; float a = 0.5; vec2 shift = vec2(100.0); mat2 rot = mat2(vec2(cos(0.5), sin(0.5)), vec2(-sin(0.5), cos(0.5))); for (int i = 0; i < 3; i++) { float val = 1.0 - voronoi(uv); val = pow(val, 2.0); // sharpen bubbles v += a * val; uv = rot * uv * 2.0 + shift; a *= 0.5; } return v; } // GET LINEAR DEPTH FROM DEPTH TEXTURE float get_linear_depth(sampler2D d_tex, vec2 uv, mat4 inv_proj) { float depth = texture(d_tex, uv).x; vec3 ndc = vec3(uv * 2.0 - 1.0, depth); #if CURRENT_RENDERER == RENDERER_COMPATIBILITY ndc.z = depth * 2.0 - 1.0; #endif vec4 view = inv_proj * vec4(ndc, 1.0); return -view.z / view.w; } varying vec3 world_vert; varying float wave_height; varying vec3 vertex_normal_world; void vertex() { vec3 world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; world_pos.xz += wave_offset; // ← add wave_height = -map(world_pos, wave_time); VERTEX.y = wave_height; world_vert = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; world_vert.xz += wave_offset; // ← add float vertex_eps = 0.1; float h_center = wave_height; float h_x = -map(world_pos + vec3(vertex_eps, 0.0, 0.0), wave_time); float h_z = -map(world_pos + vec3(0.0, 0.0, vertex_eps), wave_time); vec3 n_geom = normalize(vec3(h_center - h_x, vertex_eps, h_center - h_z)); vertex_normal_world = n_geom; } void fragment() { // linear depth of the water surface itself float water_linear_depth = -VERTEX.z; // linear depth of what's behind the water float bg_linear_depth = get_linear_depth(DEPTH_TEXTURE, SCREEN_UV, INV_PROJECTION_MATRIX); float thickness = max(0.0, bg_linear_depth - water_linear_depth); // normals & distance smoothing float dist_to_cam = length(VERTEX); float lod_epsilon = max(normal_epsilon, dist_to_cam * 0.005); vec3 detailed_normal = get_normal_detailed(world_vert, lod_epsilon, wave_time); float smooth_factor = clamp((dist_to_cam - normal_smoothness_dist) / 200.0, 0.0, 1.0); vec3 final_normal = mix(detailed_normal, vertex_normal_world, smooth_factor); NORMAL = (VIEW_MATRIX * vec4(final_normal, 0.0)).xyz; // refraction offset calculation vec3 view_vertex_normal = (VIEW_MATRIX * vec4(vertex_normal_world, 0.0)).xyz; vec2 normal_offset = NORMAL.xy - view_vertex_normal.xy; float ref_dist_factor = clamp(refraction_distance_fade / max(0.1, dist_to_cam), 0.0, 1.0); float ref_depth_mask = smoothstep(0.0, max_depth, thickness); vec2 refraction_offset = normal_offset * refraction_strength * ref_dist_factor * ref_depth_mask * 0.05; vec2 distorted_uv = SCREEN_UV + refraction_offset; // fix ghosting for pixels that are on boundary so they dont make water look weird float distorted_bg_depth = get_linear_depth(DEPTH_TEXTURE, distorted_uv, INV_PROJECTION_MATRIX); if (distorted_bg_depth < water_linear_depth - 0.001) { distorted_uv = SCREEN_UV; distorted_bg_depth = bg_linear_depth; } vec3 screen_color = texture(SCREEN_TEXTURE, distorted_uv).rgb; thickness = max(0.0, distorted_bg_depth - water_linear_depth); // caustics vec4 bg_ndc = vec4(distorted_uv * 2.0 - 1.0, texture(DEPTH_TEXTURE, distorted_uv).x, 1.0); #if CURRENT_RENDERER == RENDERER_COMPATIBILITY bg_ndc.z = bg_ndc.z * 2.0 - 1.0; #endif vec4 bg_world = INV_VIEW_MATRIX * INV_PROJECTION_MATRIX * bg_ndc; vec3 bg_world_pos = bg_world.xyz / bg_world.w; if (thickness > 0.0) { vec2 caustics_uv1 = bg_world_pos.xz * caustics_scale; caustics_uv1.x += TIME * caustics_speed; vec2 caustics_uv2 = bg_world_pos.xz * caustics_scale * 0.7; caustics_uv2.y -= TIME * caustics_speed * 0.8; float caustics_sample1 = texture(caustics_texture, caustics_uv1).r; float caustics_sample2 = texture(caustics_texture, caustics_uv2).r; float caustics_value = caustics_sample1 * caustics_sample2; float caustics_fade = 1.0 - clamp(thickness * caustics_depth_fade, 0.0, 1.0); screen_color += caustics_value * caustics_intensity * caustics_fade; } // underwater fog ratio float fade_range = max(0.001, max_depth - fade_start_depth); float depth_ratio = clamp((thickness - fade_start_depth) / fade_range, 0.0, 1.0); // beer-lambert absorption vec3 transmittance = exp(-thickness * water_absorption); vec3 water_volume_color = mix(base_tint_color, deep_color, depth_ratio); vec3 apparent_seabed_color = screen_color * water_volume_color; // screen_color = seabed color ALBEDO = mix(underwater_fog_color, apparent_seabed_color, transmittance); // foam calculation float foam_mask_primary = 0.0; float foam_mask_shadow = 0.0; // density factor: 1.0 at shore/crests, 0.0 at deep water float depth_foam_factor = smoothstep(foam_depth_start, foam_depth_end, thickness); float wave_crest_factor = smoothstep(foam_crest_threshold, foam_crest_threshold - 0.1, final_normal.y); wave_crest_factor *= foam_crest_amount; float foam_level = clamp(depth_foam_factor + wave_crest_factor, 0.0, 1.0); // domain warping for swirling, flowing foam motion vec2 flow_uv = world_vert.xz * 0.5 + TIME * 0.05 * foam_noise_speed; vec2 warp = vec2( noise(flow_uv), noise(flow_uv + vec2(5.2, 1.3)) ) * 0.5; if (distorted_bg_depth > water_linear_depth) { vec2 foam_uv = world_vert.xz * foam_noise_scale + warp; // emergence noise creates pulsing/clumping so foam doesn't appear everywhere at once float emergence_noise = noise3d(vec3(world_vert.xz * 0.5, TIME * 0.2 * foam_noise_speed)); emergence_noise = smoothstep(0.0, 1.0, emergence_noise * 0.5 + 0.5); // near shores and crests, emergence is forced to full strength float effective_emergence = mix(emergence_noise, 1.0, foam_level); // main foam sample float foam_noise_val = fbm_voronoi(foam_uv); foam_noise_val *= effective_emergence; // shadow sample at slight offset, faking thickness and separation from the water vec2 foam_uv_shadow = (world_vert.xz + foam_edge_offset) * foam_noise_scale + warp; float foam_noise_shadow_val = fbm_voronoi(foam_uv_shadow); foam_noise_shadow_val *= effective_emergence; // clumping/mask generation float combined_noise = foam_noise_val + foam_level; float combined_shadow = foam_noise_shadow_val + foam_level; foam_mask_primary = smoothstep(foam_cutoff + 0.5, foam_cutoff + 0.6, combined_noise); float shadow_shape = smoothstep(foam_cutoff + 0.5, foam_cutoff + 0.6, combined_shadow); foam_mask_shadow = clamp(shadow_shape - foam_mask_primary, 0.0, 1.0); } // voronoi bubble texture inside the foam float voronoi_val = 1.0 - sqrt(voronoi(world_vert.xz * voronoi_scale + warp)); voronoi_val = smoothstep(0.2, 0.8, voronoi_val); float bubble_alpha = 1.0 - clamp((1.0 - voronoi_val) * voronoi_strength, 0.0, 1.0); // apply foam ALBEDO = mix(ALBEDO, foam_edge_color, foam_mask_shadow); ALBEDO = mix(ALBEDO, foam_color, foam_mask_primary * bubble_alpha); // make foam rougher ROUGHNESS = mix(roughness, 0.8, (foam_mask_primary * bubble_alpha) + foam_mask_shadow); SPECULAR = specular; METALLIC = metallic; }