float hash21(vec2 p) {
    p = fract(p * vec2(127.1, 311.7));
    p += dot(p, p + 19.19);
    return fract(p.x * p.y);
}

float valueNoise(vec2 p) {
    vec2 i = floor(p);
    vec2 f = fract(p);
    vec2 u = f * f * (3.0 - 2.0 * f);
    return mix(
        mix(hash21(i),            hash21(i + vec2(1.0, 0.0)), u.x),
        mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0, 1.0)), u.x),
        u.y
    );
}

// Macro: rolling mounds and dunes, 4-octave FBM
float macroHeight(vec2 uv) {
    float h = 0.0, a = 1.0, f = 1.0, norm = 0.0;
    for (int i = 0; i < 4; i++) {
        h    += valueNoise(uv * f) * a;
        norm += a;
        a    *= 0.45;
        f    *= 2.17;
    }
    return h / norm;
}

// Micro: sand ripples — directional sine, warped by noise to stay organic
float rippleHeight(vec2 uv) {
    float warp = valueNoise(uv * 4.0) * 0.6;
    float r    = sin((uv.x * 0.6 + uv.y + warp) * 45.0);
    return r * 0.5 + 0.5;
}

float sampleSeafloor(vec2 uv) {
    float macro = macroHeight(uv);
    float micro = rippleHeight(uv) * 0.02;  // ~5% of macro amplitude
    return macro + micro;
}


const float heightScale = 1.0;

vec3 sampleSeafloorNormal(vec2 uv)
{
    float eps = 0.001;
    float str = 40.0;
    float eps2 = eps * 2.0;

    vec3 off = vec3(1.0, 1.0, 0.0)* eps;
    float hL = sampleSeafloor(uv.xy - off.xz) * heightScale;
    float hR = sampleSeafloor(uv.xy + off.xz) * heightScale;
    float hU = sampleSeafloor(uv.xy - off.zy) * heightScale;
    float hD = sampleSeafloor(uv.xy + off.zy) * heightScale;

    return -normalize(vec3(hR - hL, eps2 / str, hD - hU));
}


vec3 sandColorDry  = vec3(0.76, 0.65, 0.46);
vec3 sandColorWet  = vec3(0.38, 0.32, 0.22);
vec3 sandColorSilt = vec3(0.30, 0.28, 0.22); // fine sediment in low macro areas

float sandGrain(vec2 uv) {
    float f1 = valueNoise(uv * 280.0);
    float f2 = valueNoise(uv * 520.0);
    float grain = f1 * 0.6 + f2 * 0.4;
    return pow(grain, 0.6); // bias toward bright: grains > gaps
}

vec4 sampleSeafloorMaterial(vec2 uv) {
    float macro  = macroHeight(uv);
    float ripple = rippleHeight(uv);

    // Ripple crests are drier/lighter, troughs are wetter/darker
    vec3 albedo = mix(sandColorWet, sandColorDry, ripple);

    // Low macro areas accumulate silt — pull toward silt color in valleys
    albedo = mix(sandColorSilt, albedo, smoothstep(0.2, 0.5, macro));

    // Subtle large-scale color variation so flat areas don't look uniform
    float colorVar = valueNoise(uv * 1.3) * 0.12;
    albedo += vec3(colorVar * 0.8, colorVar * 0.6, colorVar * 0.3);

    // Roughness: wet troughs smooth, dry crests rough; silt areas very smooth
    float roughness = mix(0.5, 1.0, ripple);
    roughness = mix(0.4, roughness, smoothstep(0.2, 0.45, macro));

    float grain = sandGrain(uv);

    // Modulate albedo — subtle brightness variation
    albedo *= 0.88 + grain * 0.12;

    // Grain also affects roughness — packed wet sand is smoother between grains
    roughness += (grain - 0.2) * 0.2;
    roughness = clamp(roughness, 0.4, 1.0);

    return vec4(albedo, roughness);
}