OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / assets / ocean / shaders / underwater.gdshader

56 lines · gdshader ref:
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;
	}
}