OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / maps / undersea / undersea_rock.gdshader

148 lines · gdshader ref:
shader_type spatial;
// OUR GEISS ROCK SHADING (demo sprint §4; density normal + palette 2026-10-06).
// Replaces geiss_dc_color.gdshader, which included NVIDIA's draw_rock.psh port and
// their altmap.tga ramp. Nothing here is from that code or data: palette, bands and
// noise are ours. Two ideas are kept because they are what made the old map read:
//
//   DENSITY NORMAL  the normal comes from the FIELD, per pixel, not from the mesh.
//                   dc-terrain's per-vertex normal on the dual-contoured mesh shows
//                   its facets; the old shader measured the field normal as both less
//                   noisy and more detailed (gate_geiss_normal.py: mean 2.0 vs 3.1 deg
//                   at 0.05 m) and used it everywhere. Same here, written back to
//                   NORMAL so Godot LIGHTS the detailed surface, not just paints it.
//   ALTITUDE COLOUR colour by height above the ground plane, banded, so the floors,
//                   the slopes and the tops read as different places.
//
//   ramp      sand (floors) -> ochre -> olive -> slate (tops), over band_lo..band_hi m
//             relative to ground_y
//   strata    thin bands on the ramp with a wobble, faded by screen footprint
//   sediment  pale fill on up-facing surfaces (by the FIELD normal)
//   depth     below water_y the colour cools toward the abyss
//
// THE FIELD IS SPLICED IN AT RUNTIME. map_geiss.gd puts `#define HAS_DENSITY` and the
// generator's own compiled GLSL (get_compiled_glsl — the text the mesher runs, so the
// shading cannot describe a surface the geometry does not have) at the marker below,
// through BridgedDensityMaterial's transforms. Without it this compiles and shades with
// the mesh normal. evaluate_density is in the TERRAIN frame; map_geiss's DCTerrain sits
// at the origin, so that is the world frame.
//
// World position / normal via INV_VIEW_MATRIX (dc-terrain chunks are raw RS instances).
// ⚠ No `return` in fragment(): Godot refuses it and the terrain falls back to white.

// @DENSITY@

uniform float ground_y = 0.0;
uniform float water_y = 0.0;
uniform float band_lo = -15.0;          // m above ground_y where the ramp starts (floors)
uniform float band_hi = 15.0;           // m where it ends (tops)
uniform vec3 col_sand = vec3(0.56, 0.50, 0.38);
uniform vec3 col_ochre = vec3(0.52, 0.40, 0.22);
uniform vec3 col_olive = vec3(0.33, 0.38, 0.20);
uniform vec3 col_slate = vec3(0.26, 0.31, 0.38);
uniform vec3 sediment = vec3(0.62, 0.58, 0.46);
uniform float sediment_start = 0.70;    // field-normal y where sediment begins
uniform float sediment_full = 0.92;
uniform float strata_period = 1.9;      // m between bands
uniform float strata_strength = 0.30;
uniform float abyss_depth = 400.0;      // m below water_y at full abyss tint
uniform vec3 abyss_tint = vec3(0.10, 0.20, 0.30);
// DENSITY NORMAL. 1 = the field's normal everywhere (the old map's setting).
uniform float density_normal_blend : hint_range(0.0, 1.0) = 1.0;
// The central difference's epsilon is the pixel footprint, clamped: below ~0.5 m it
// differentiates noise finer than the mesh can show (and float32 starts to bite);
// past 4 m the gradient is just the ground plane.
uniform float dn_eps_min = 0.5;
uniform float dn_eps_max = 4.0;
uniform float dn_px_angle = 0.0011;     // radians per pixel, for the angular footprint
// 0 shaded · 1 normal as colour · 2 albedo unlit · 3 the field normal's blend (1 = in use)
uniform int debug_view = 0;

float hash13(vec3 p) {
	p = fract(p * 0.1031);
	p += dot(p, p.zyx + 31.32);
	return fract((p.x + p.y) * p.z);
}

// Trilinear value noise on a unit lattice.
float vnoise(vec3 p) {
	vec3 i = floor(p);
	vec3 f = fract(p);
	f = f * f * (3.0 - 2.0 * f);
	float n000 = hash13(i);
	float n100 = hash13(i + vec3(1, 0, 0));
	float n010 = hash13(i + vec3(0, 1, 0));
	float n110 = hash13(i + vec3(1, 1, 0));
	float n001 = hash13(i + vec3(0, 0, 1));
	float n101 = hash13(i + vec3(1, 0, 1));
	float n011 = hash13(i + vec3(0, 1, 1));
	float n111 = hash13(i + vec3(1, 1, 1));
	return mix(mix(mix(n000, n100, f.x), mix(n010, n110, f.x), f.y),
			mix(mix(n001, n101, f.x), mix(n011, n111, f.x), f.y), f.z);
}

vec3 ramp(float t) {
	t = clamp(t, 0.0, 1.0);
	vec3 c = mix(col_sand, col_ochre, smoothstep(0.0, 0.35, t));
	c = mix(c, col_olive, smoothstep(0.35, 0.7, t));
	return mix(c, col_slate, smoothstep(0.7, 1.0, t));
}

void fragment() {
	vec3 wp = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
	vec3 wn = normalize((INV_VIEW_MATRIX * vec4(NORMAL, 0.0)).xyz);
	float used = 0.0;

#ifdef HAS_DENSITY
	if (density_normal_blend > 0.001) {
		// Pixel footprint: the smaller of fwidth (honest about resolution, but a
		// grazing surface reports tens of metres) and the angular estimate.
		float fp = min(length(fwidth(wp)), length(VERTEX) * dn_px_angle);
		float e = clamp(fp, dn_eps_min, dn_eps_max);
		vec3 g = vec3(
			evaluate_density(wp.x + e, wp.y, wp.z) - evaluate_density(wp.x - e, wp.y, wp.z),
			evaluate_density(wp.x, wp.y + e, wp.z) - evaluate_density(wp.x, wp.y - e, wp.z),
			evaluate_density(wp.x, wp.y, wp.z + e) - evaluate_density(wp.x, wp.y, wp.z - e));
		float gl = length(g);
		if (gl > 1e-6) {
			// Positive = solid, so the outward normal points DOWN the gradient.
			wn = normalize(mix(wn, -g / gl, density_normal_blend));
			used = density_normal_blend;
		}
	}
#endif
	NORMAL = normalize((VIEW_MATRIX * vec4(wn, 0.0)).xyz);

	// Altitude ramp with a low-frequency wobble so band edges are not contour lines.
	float h = wp.y - ground_y + (vnoise(wp * 0.045) - 0.5) * 5.0;
	vec3 col = ramp((h - band_lo) / (band_hi - band_lo));

	// Strata: thin bands along the ramp, faded once thinner than ~2 pixels.
	float s = (h + (vnoise(wp * 0.11) - 0.5) * 1.5) / strata_period;
	float fade = 1.0 - smoothstep(0.25, 0.6, fwidth(s));
	float pick = hash13(vec3(floor(s), 3.7, 1.3));
	col *= mix(1.0, 0.78 + 0.44 * pick, strata_strength * fade);
	col *= 0.88 + 0.24 * vnoise(wp * 0.8);

	// Sediment settles on what faces up — by the field normal, so ledges and floors
	// that the mesh renders as facets still collect it smoothly.
	float sed_w = smoothstep(sediment_start, sediment_full, wn.y);
	col = mix(col, sediment * (0.9 + 0.2 * vnoise(wp * 1.9)), sed_w);

	float depth = clamp((water_y - wp.y) / abyss_depth, 0.0, 1.0);
	col = mix(col, col * abyss_tint * 2.2, depth * 0.85);

	if (debug_view == 1) {
		ALBEDO = wn * 0.5 + 0.5;
	} else if (debug_view == 3) {
		ALBEDO = vec3(used, 0.0, 1.0 - used);
	} else {
		ALBEDO = col;
	}
	if (debug_view == 2) {
		EMISSION = col;
		ALBEDO = vec3(0.0);
	}
	ROUGHNESS = mix(0.90, 0.97, sed_w);
	METALLIC = 0.0;
}