shader_type spatial;
render_mode unshaded, blend_mix, cull_disabled, depth_draw_never, depth_test_disabled, fog_disabled;
// THE WATER VOLUME, applied to the whole frame when the camera is submerged.
//
// A surface shader cannot express this: when you are underwater EVERYTHING you
// see is seen through water, so absorption applies to the scene, not to the
// water's surface. 68% of this world is below the waterline and the player
// spawns at -6455 m, so this is the common case here, not an edge case.
//
// Beer-Lambert with per-channel extinction. Red is absorbed roughly ten times
// faster than blue, which is why water goes blue-green then black with depth --
// "deeper is darker" is not a ramp to tune, it falls out of the physics.
uniform sampler2D screen_tex : hint_screen_texture, filter_linear_mipmap;
uniform sampler2D depth_tex : hint_depth_texture, filter_nearest;
// Per metre, per channel. Clear ocean water: red is gone by ~10 m, blue carries
// for hundreds. Raise all three together for murkier water.
// ⚠ NOT PHYSICAL VALUES. Clear seawater is roughly (0.28, 0.055, 0.028) per
// metre, which puts red at zero by ~10 m and blue by ~150 m -- correct, and
// unplayable: everything past a hundred metres is the inscatter colour and the
// world goes black. Every game cheats this. These give a few hundred metres of
// visibility while keeping the RATIO between channels, which is what makes
// water read as water rather than as grey fog.
uniform vec3 extinction = vec3(0.030, 0.0075, 0.0040);
// What the light that never reaches the eye is replaced by -- in-scattered
// ambient. Without this, depth just goes black instead of blue.
uniform vec3 inscatter : source_color = vec3(0.06, 0.20, 0.28);
// 0 = above water (the pass draws nothing), 1 = submerged.
uniform float submerged = 0.0;
// Clamp so the far plane does not evaluate exp() on 40 km.
uniform float max_path : hint_range(1.0, 5000.0) = 1200.0;
void vertex() {
// Fullscreen regardless of where the node sits: write clip space directly.
POSITION = vec4(VERTEX.xy * 2.0, 1.0, 1.0);
}
void fragment() {
if (submerged < 0.5) {
ALPHA = 0.0;
} else {
float raw = texture(depth_tex, SCREEN_UV).r;
vec4 ndc = vec4(SCREEN_UV * 2.0 - 1.0, raw, 1.0);
vec4 view = INV_PROJECTION_MATRIX * ndc;
// Reverse-Z with an infinite far plane sends the sky to w -> 0; that is
// not "no water in the way", it is the maximum of it.
float path = (abs(view.w) < 1e-6) ? max_path
: min(length(view.xyz / view.w), max_path);
vec3 t = exp(-extinction * path);
vec3 scene = texture(screen_tex, SCREEN_UV).rgb;
ALBEDO = scene * t + inscatter * (1.0 - t);
ALPHA = 1.0;
}
}