Files
2026-07-07 08:17:45 +02:00

364 lines
14 KiB
Plaintext

// 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;
}