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