OverviewFilesCommitsIssuesDocsReleasesPeers

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

276 lines · gdshader ref:
shader_type spatial;
// TEXTURED TERRAIN, MASKED BY OUR COLOURING (user 2026-10-06: "use spom textures and use
// our current coloring as a mask"). The same masks undersea_rock.gdshader colours with —
// the altitude bands sand -> ochre -> olive -> slate and sediment on up-facing surfaces —
// become WEIGHTS over five PBR texture layers from the texture packs (TexturePacks.build:
// albedo / normal / ORM+height arrays, the ones the level's parallax tier uses).
//
//   projection  WORLD-SPACE TRIPLANAR. DCTerrain chunks carry no UVs and no tangents
//               (dc_terrain.cpp vertex format), so each axis plane gets its own (U, V, N)
//               with V = cross(N, U): right-handed, so the normal maps decode exactly as
//               the level shader does them (rgb - 0.5, sn.x*U + sn.y*V + sn.z*N; the packs
//               are DirectX convention, which that wants with no flip — dc_level.gdshaderinc).
//   layers      the two strongest mask weights, HEIGHT-BLENDED with the ORM alpha (height),
//               so pebbles sit in the slate's cracks instead of cross-fading through it
//   parallax    POM on the dominant axis and the strongest layer only, fading out by
//               pom_fade_end (the level tier's 8-14 m), 8-24 steps. NOT silhouette POM: a
//               discard breaks Godot's shared depth/shadow material (measured for the level
//               tier), and terrain has no face edges to carve a silhouette against.
//   tint        tint_strength pulls each layer toward its band's colour, so the palette the
//               masks came from still reads.
//
// World position / normal via INV_VIEW_MATRIX (dc-terrain chunks are raw RS instances).

uniform sampler2DArray tex_atlas : source_color, filter_linear_mipmap_anisotropic, repeat_enable;
uniform sampler2DArray norm_atlas : filter_linear_mipmap_anisotropic, repeat_enable;
uniform sampler2DArray orm_atlas : filter_linear_mipmap_anisotropic, repeat_enable;
// Layer (array index), tile size (m) and relief (m) per mask: sand, ochre, olive, slate,
// sediment — filled from TexturePacks' info by the map.
uniform int layer_index[5];
uniform float tile_m[5];
uniform float height_m[5];
// Per-layer UV transform (the Terrain material panel). Rotation turns the plane's U/V
// BASIS, not the uv, so the parallax march and the tangent-space normals turn with it.
uniform float uv_rot_deg[5];
uniform vec2 uv_off[5];
// Scales every layer's parallax depth (height_m): 0 = flat, 1 = authored, > 1 = deeper.
uniform float pom_scale : hint_range(0.0, 4.0) = 1.0;
// Anti-tiling (2026-10-06): a second, far_scale x larger tiling blended in between
// far_start and far_end (weight far_blend); a macro brightness wash of ±macro_strength.
uniform float far_scale = 6.0;
uniform float far_start = 10.0;
uniform float far_end = 60.0;
uniform float far_blend : hint_range(0.0, 1.0) = 0.5;
uniform float macro_strength : hint_range(0.0, 0.5) = 0.2;

uniform float ground_y = 0.0;
uniform float band_lo = -15.0;
uniform float band_hi = 15.0;
uniform float sediment_start = 0.70;
uniform float sediment_full = 0.92;
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 col_sediment = vec3(0.62, 0.58, 0.46);
uniform float tint_strength : hint_range(0.0, 1.0) = 0.35;
uniform float normal_strength : hint_range(0.0, 2.0) = 1.0;
uniform float blend_depth = 0.25;           // height-blend softness (fraction of relief)
uniform float pom_fade_start = 8.0;
uniform float pom_fade_end = 14.0;
uniform int pom_min_steps = 8;
uniform int pom_max_steps = 24;
// 0 shaded · 1 layer weights as colour (sand R, ochre G, olive B, slate+sediment grey)
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);
}

float vnoise(vec3 p) {
	vec3 i = floor(p);
	vec3 f = fract(p);
	f = f * f * (3.0 - 2.0 * f);
	return mix(mix(mix(hash13(i), hash13(i + vec3(1, 0, 0)), f.x),
			mix(hash13(i + vec3(0, 1, 0)), hash13(i + vec3(1, 1, 0)), f.x), f.y),
			mix(mix(hash13(i + vec3(0, 0, 1)), hash13(i + vec3(1, 0, 1)), f.x),
			mix(hash13(i + vec3(0, 1, 1)), hash13(i + vec3(1, 1, 1)), f.x), f.y), f.z);
}

// vnoise with a QUINTIC fade (C2): no derivative crease at the lattice planes either.
float vnoise5(vec3 p) {
	vec3 i = floor(p);
	vec3 f = fract(p);
	f = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
	return mix(mix(mix(hash13(i), hash13(i + vec3(1, 0, 0)), f.x),
			mix(hash13(i + vec3(0, 1, 0)), hash13(i + vec3(1, 1, 0)), f.x), f.y),
			mix(mix(hash13(i + vec3(0, 0, 1)), hash13(i + vec3(1, 0, 1)), f.x),
			mix(hash13(i + vec3(0, 1, 1)), hash13(i + vec3(1, 1, 1)), f.x), f.y), f.z);
}

// The axis plane's (U, V, N): N = the face's side of the axis, V = cross(N, U).
void axis_frame(int a, vec3 wn, out vec3 U, out vec3 V, out vec3 N) {
	if (a == 0) {
		N = vec3(sign(wn.x) + (wn.x == 0.0 ? 1.0 : 0.0), 0.0, 0.0);
		U = vec3(0.0, 0.0, 1.0);
	} else if (a == 1) {
		N = vec3(0.0, sign(wn.y) + (wn.y == 0.0 ? 1.0 : 0.0), 0.0);
		U = vec3(1.0, 0.0, 0.0);
	} else {
		N = vec3(0.0, 0.0, sign(wn.z) + (wn.z == 0.0 ? 1.0 : 0.0));
		U = vec3(1.0, 0.0, 0.0);
	}
	V = cross(N, U);
}

// Parallax offset (in uv) for one plane: march the view ray down the layer's height map.
vec2 pom_uv(vec2 uv, vec3 vdir, vec3 U, vec3 V, vec3 N, int layer, float relief_m, float tile, float fade) {
	if (fade <= 0.0 || relief_m <= 0.0) {
		return uv;
	}
	float vn = dot(vdir, N);
	if (vn >= -0.05) {
		return uv;
	}
	// uv shift per metre of depth along the ray (in uv units: metres / tile).
	vec2 per_m = vec2(dot(vdir, U), dot(vdir, V)) / (-vn) / tile;
	float depth_total = relief_m * fade;
	int steps = int(mix(float(pom_max_steps), float(pom_min_steps), clamp(-vn, 0.0, 1.0)));
	float step_d = 1.0 / float(steps);
	vec2 dUV = per_m * depth_total * step_d;
	vec2 cur = uv;
	float layer_d = 0.0;
	float h = 1.0 - textureLod(orm_atlas, vec3(cur, float(layer)), 0.0).a;
	vec2 prev_uv = cur;
	float prev_h = h;
	float prev_layer = 0.0;
	for (int i = 0; i < 64; i++) {
		if (i >= steps || layer_d >= h) {
			break;
		}
		prev_uv = cur;
		prev_h = h;
		prev_layer = layer_d;
		cur += dUV;
		layer_d += step_d;
		h = 1.0 - textureLod(orm_atlas, vec3(cur, float(layer)), 0.0).a;
	}
	// Interpolate the crossing between the last two steps.
	float after = h - layer_d;
	float before = prev_h - prev_layer;
	float wgt = after / (after - before + 1e-5);
	return mix(cur, prev_uv, clamp(wgt, 0.0, 1.0));
}

void fragment() {
	vec3 wp = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
	vec3 wn = normalize((INV_VIEW_MATRIX * vec4(NORMAL, 0.0)).xyz);
	vec3 cam = (INV_VIEW_MATRIX * vec4(0.0, 0.0, 0.0, 1.0)).xyz;
	vec3 vdir = normalize(wp - cam);
	// MIP GRADIENTS FROM THE SURFACE, taken here — before the per-pixel axis skip and
	// the parallax offset. Implicit derivatives inside the axis loop (with its `continue`)
	// and of a post-POM uv are undefined / spiky: 1-pixel mip seams and sparkle
	// (anti-tiling research 2026-10-06). Every atlas fetch is textureGrad with these.
	vec3 dpx = dFdx(wp);
	vec3 dpy = dFdy(wp);

	// ---- the colouring's masks, as layer weights ----
	float h = wp.y - ground_y + (vnoise(wp * 0.045) - 0.5) * 5.0;
	float t = clamp((h - band_lo) / (band_hi - band_lo), 0.0, 1.0);
	float s1 = smoothstep(0.0, 0.35, t);
	float s2 = smoothstep(0.35, 0.7, t);
	float s3 = smoothstep(0.7, 1.0, t);
	float sed = smoothstep(sediment_start, sediment_full, wn.y);
	float w[5];
	w[0] = (1.0 - s1) * (1.0 - sed);
	w[1] = s1 * (1.0 - s2) * (1.0 - sed);
	w[2] = s2 * (1.0 - s3) * (1.0 - sed);
	w[3] = s3 * (1.0 - sed);
	w[4] = sed;
	vec3 tints[5] = vec3[5](col_sand, col_ochre, col_olive, col_slate, col_sediment);

	// ---- the two strongest masks ----
	int a = 0;
	for (int i = 1; i < 5; i++) { if (w[i] > w[a]) { a = i; } }
	int b = (a == 0) ? 1 : 0;
	for (int i = 0; i < 5; i++) { if (i != a && w[i] > w[b]) { b = i; } }

	// ---- triplanar weights; the dominant axis gets the parallax ----
	vec3 bw = pow(abs(wn), vec3(4.0));
	bw /= (bw.x + bw.y + bw.z + 1e-5);
	int dom = (bw.x > bw.y && bw.x > bw.z) ? 0 : ((bw.y > bw.z) ? 1 : 2);
	float dist = length(wp - cam);
	float fade = 1.0 - smoothstep(pom_fade_start, pom_fade_end, dist);
	// SEAM-FREE PARALLAX: the offset is on ONE plane (dom) of ONE layer (a), so where the
	// dominant axis flips, or a and b swap, it used to jump — a hard seam inside the POM
	// range. Faded to 0 on both sides of each switch, it is continuous.
	float pom_seam = smoothstep(0.5, 0.8, bw[dom]) * smoothstep(0.0, 0.2, w[a] - w[b]);
	float far_w = smoothstep(far_start, far_end, dist) * far_blend;
	// MACRO VARIATION: a slow two-octave brightness wash (~40 m and ~14 m) across all
	// layers — no fetches, the cheapest fix for "wallpaper".
	// In a ROTATED domain: value noise is flat along its lattice planes, and an
	// axis-aligned plane (z = 0, 30 m ahead) read as a straight line across the floor
	// at ±20 % (measured, A/B with macro_strength 0). Off-axis, the lattice has no
	// world-straight lines to show.
	const mat3 MR = mat3(vec3(0.788, -0.506, 0.350), vec3(0.578, 0.507, -0.639), vec3(0.146, 0.698, 0.701));
	vec3 mp = MR * wp;
	float macro = vnoise5(mp * 0.025) * 0.65 + vnoise5(MR * mp * 0.071 + 17.3) * 0.35;
	float macro_mul = mix(1.0 - macro_strength, 1.0 + macro_strength, macro);

	vec3 alb[2];
	vec3 nrm[2];
	vec3 orm[2];
	float hgt[2];
	int pick[2] = int[2](a, b);
	for (int k = 0; k < 2; k++) {
		int m = pick[k];
		int L = layer_index[m];
		float tile = max(tile_m[m], 0.01);
		vec3 al = vec3(0.0);
		vec3 nw = vec3(0.0);
		vec3 om = vec3(0.0);
		float hh = 0.0;
		for (int ax = 0; ax < 3; ax++) {
			float wax = bw[ax];
			if (wax < 0.02) { continue; }
			vec3 U; vec3 V; vec3 N;
			axis_frame(ax, wn, U, V, N);
			float ra = radians(uv_rot_deg[m]);
			vec3 Ur = cos(ra) * U + sin(ra) * V;
			V = -sin(ra) * U + cos(ra) * V;
			U = Ur;
			vec2 uv = vec2(dot(wp, U), dot(wp, V)) / tile + uv_off[m];
			vec2 gx = vec2(dot(dpx, U), dot(dpx, V)) / tile;
			vec2 gy = vec2(dot(dpy, U), dot(dpy, V)) / tile;
			vec2 uv0 = uv;
			if (k == 0 && ax == dom) {
				uv = pom_uv(uv, vdir, U, V, N, L, height_m[m] * pom_scale * pom_seam, tile, fade);
			}
			vec3 c = textureGrad(tex_atlas, vec3(uv, float(L)), gx, gy).rgb;
			vec4 o = textureGrad(orm_atlas, vec3(uv, float(L)), gx, gy);
			vec3 sn = normalize(textureGrad(norm_atlas, vec3(uv, float(L)), gx, gy).rgb - 0.5);
			// FAR: the same texture at far_scale x the tile, blended in with distance
			// (Far Cry 5's two scales) — the near tile's repeat is what reads as
			// wallpaper across a valley. Albedo + ORM only; normals stay near-scale.
			if (far_w > 0.01) {
				vec2 fuv = uv0 / far_scale;
				vec3 fc = textureGrad(tex_atlas, vec3(fuv, float(L)), gx / far_scale, gy / far_scale).rgb;
				vec4 fo = textureGrad(orm_atlas, vec3(fuv, float(L)), gx / far_scale, gy / far_scale);
				c = mix(c, fc, far_w);
				o = mix(o, fo, far_w);
			}
			sn.xy *= normal_strength;
			al += c * wax;
			om += o.rgb * wax;
			hh += o.a * wax;
			nw += normalize(sn.x * U + sn.y * V + sn.z * N) * wax;
		}
		alb[k] = al * mix(vec3(1.0), tints[m] * 2.0, tint_strength) * macro_mul;
		nrm[k] = nw;
		orm[k] = om;
		hgt[k] = hh;
	}

	// ---- height blend of the two (the stronger mask's surface wins where it is higher) ----
	float wa = w[a] + hgt[0] * blend_depth;
	float wb = w[b] + hgt[1] * blend_depth;
	float top = max(wa, wb) - blend_depth;
	float ka = max(wa - top, 0.0);
	float kb = max(wb - top, 0.0);
	float ks = ka + kb + 1e-5;
	vec3 col = (alb[0] * ka + alb[1] * kb) / ks;
	vec3 o3 = (orm[0] * ka + orm[1] * kb) / ks;
	vec3 n3 = normalize(nrm[0] * ka + nrm[1] * kb + wn * 1e-3);

	if (debug_view == 1) {
		col = vec3(w[0], w[1], w[2]) + vec3(w[3] + w[4]) * 0.5;
	}
	ALBEDO = col;
	AO = o3.r;
	ROUGHNESS = o3.g;
	METALLIC = o3.b;
	NORMAL = normalize((VIEW_MATRIX * vec4(n3, 0.0)).xyz);
}