/*
    Multi-layer clouds:
    layer 1 (VC_BASE ~140, thickness VC_THICK): volumetric cumulus - flat
      pancakes (~26 blocks) everywhere, mid domes up to ~70 blocks on masses
      >= 140 wide, rare grand cores up to ~140 on systems >= 280 wide
      (measured, textured path), dark flat base, broad cauliflower tops,
      eroded wispy edges. Column height is WIDTH-GATED (morphological erosion
      of the coverage, lib/lighting.glsl): a small cell gives a low pancake,
      never a pillar. (VC_CUMULUS_SHAPES=0 falls back to the "cloud sea".)
    layer 2 (~340): volumetric altocumulus (thin 6-step slab) + 2D cirrus above.
    Requires common.glsl and lighting.glsl included first.
*/

// Cloud banks vs clear patches
float cloudPatch(vec2 xz, float t) {
    vec2 wind = t * vec2(7.0, 2.4) * VC_WIND;
    return vnoise((xz + wind * 0.6) * 0.0010);
}

// Fallback cloud density (VC_CUMULUS_SHAPES=0). PLANET_RADIUS is large enough
// to stay subtle yet visible at the horizon: far clouds sink and curve away.
float cloudDensity(vec3 wp, float t) {
    // Curvature: a point far from the player "falls" by d^2/(2R), so the sheet
    // follows the planet instead of being an infinite flat ceiling
    float yEff = wp.y;
    #ifdef CLOUD_CURVATURE
        vec2 dxz = wp.xz - cameraPosition.xz;
        // From above the layer the additive term overshoots the bounds and
        // clips, so the curvature is faded out from the upper half of the slab.
        float curvFade = 1.0 - clamp((cameraPosition.y - VC_BASE - VC_THICK * 0.5) / (VC_THICK * 0.5), 0.0, 1.0);
        yEff += dot(dxz, dxz) / (2.0 * PLANET_RADIUS) * curvFade;
    #endif
    float h = (yEff - VC_BASE) / VC_THICK;
    if (h < 0.0 || h > 1.0) return 0.0;

    float cov = clamp(mix(VC_COVERAGE, 1.0, rainStrength * 0.95), 0.0, 1.0); // full rain = near-total overcast
    // The cloudPatch low threshold drops with coverage: at VC_COVERAGE=1.0
    // nearly every cell contributes -> a fully overcast sky is reachable.
    float covLo = max(0.20 - (cov - 0.45) * 0.44, 0.0);
    float covL = cov * smoothstep(covLo, 0.70, cloudPatch(wp.xz, t)) * 1.5;
    if (covL < max(0.03 * (1.0 - cov * 0.9), 0.003)) return 0.0;

    vec2 wind = t * vec2(7.0, 2.4) * VC_WIND;
    vec2 pp = wp.xz + wind;
    // Small tight cells (mackerel pattern)
    float base = fbm(pp * 0.0060);

    // The densest cells dome upward
    float colH = clamp(base * 1.7 - 0.50, 0.18, 1.0);
    // Slightly sharper top taper (0.55): reads better when seen from above
    float profile = smoothstep(0.0, 0.07, h) * smoothstep(colH, colH * 0.55, h);

    float d = base * profile - (1.0 - covL) * 0.52;
    if (d <= 0.0) return 0.0;

    // Billowy 3D erosion: cauliflower edges
    vec3 dp = vec3(pp.x, wp.y * 2.4, pp.y);
    float ero = vnoise3(dp * 0.030) * 0.65 + vnoise3(dp * 0.085) * 0.35;
    ero = 1.0 - abs(ero * 2.0 - 1.0);
    d -= (1.0 - ero) * 0.13 * (1.0 - clamp(d * 5.0, 0.0, 1.0));

    return clamp(d * 6.0, 0.0, 1.0) * VC_DENSITY;
}

#if VC_CUMULUS_SHAPES == 1
#if VC_TEXTURED == 1
// ---- TEXTURED CUMULUS (Nubis / Horizon Zero Dawn technique) --------------
// Density is read from PRECOMPUTED tileable textures instead of stacked runtime
// noise. Tap budget per marched sample: 1 (2D) on clear sky; 9 (2D) inside
// coverage (width-gated height driver); a LIT sample adds 1 curl + 1-2 erosion
// 3D taps (VC_DETAIL=1) and 1 refresh 2D tap in the light march. Worst case
// 13 taps - the procedural path evaluated ~27 noise octaves per lit sample.
//   . vlCumulusBase    1024^2 R8         2D coverage, ~4000 blocks per tile
//   . vlCumulusDetail1 128^3  R8         cauliflower erosion, ~54-block tile
//   . vlCumulusDetail2 128^3  R8         fine fringe erosion, ~18-block tile
//   . vlCumulusCurl    64^3 RGBA8_SNORM  swirl, ALREADY in [-1,1] (no *2-1)
// The COVERAGE and the column-height driver live in lib/lighting.glsl
// (vlCumulusField): that file is included BEFORE this one and cloudShadow must
// run the EXACT same formula. Single source of truth -> ground shadows can no
// longer desync from the visible clouds.
// MEASURED shapes (formula replayed on cumulus_base.bin) at defaults
// VC_CUMULUS_SCALE=1.5 / VC_THICK=160 / VC_COVERAGE=0.40:
//   . 30.6% of the sky covered, real blue gaps between the masses;
//   . median cloud: 26-block pancake; mid domes to 70 blocks on masses >= 140
//     wide (~5.1% of the covered sky is 48+ tall, soft-ramp replay); grand cores ~107 blocks
//     on systems >= 280 wide, width/height p10 = 2.2;
//   . "towers" (width < 2*height): 0.26% of the covered surface (shallow necks
//     between merged masses) - the pointwise driver this replaces measured 84%.
#ifndef VL_CUMULUS_BASE_DECL
#define VL_CUMULUS_BASE_DECL
uniform sampler2D vlCumulusBase;
#endif
uniform sampler3D vlCumulusDetail1;
uniform sampler3D vlCumulusDetail2;
uniform sampler3D vlCumulusCurl;

// Tile periods in world BLOCKS, tied to VC_CUMULUS_SCALE so the cauliflower
// stays proportional to the clouds. They are exact INTEGER RATIOS of each other
// (curl = 3 x detail1 = 9 x detail2) so ONE mod() folds the wind advection for
// all three lookups without shifting any of the three patterns.
#define VL_DET2_TILE (12.0 * VC_CUMULUS_SCALE)   // ~18 blocks at defaults
#define VL_DET1_TILE (36.0 * VC_CUMULUS_SCALE)   // ~54 blocks at defaults
#define VL_CURL_TILE (108.0 * VC_CUMULUS_SCALE)  // ~162 blocks at defaults

// Shared 2D field - SAME CONTRACT as the procedural path:
// x = coverage in [0,1], y = height driver in [0,0.993].
// COST: 1 texture2D tap on clear sky, 9 inside coverage (see lib/lighting.glsl).
vec2 cloudFieldCumulus(vec2 xz, float t) {
    return vlCumulusField(xz, t);
}

// hiRes=false: cheap variant (no 3D erosion) for the light march.
// fld = field precomputed by the caller. The light march reuses it for the two
// near steps and REFRESHES the coverage once at step 2 (measured on the .bin:
// coverage is 48% decorrelated at a 63-block lag and 92% at 148, so freezing
// it across the whole march shadowed each cloud with its neighbour's coverage;
// see cloudOpticalDepthCumulus).
// Vertical profile: flat base (condensation ramp over 6% of the slab), body,
// short rounded cap. Height comes from the WIDTH-GATED driver only.
float cloudDensityCumulusCov(vec3 wp, float t, bool hiRes, vec2 fld) {
    float h = (wp.y - VC_BASE) / VC_THICK;
    if (h < 0.0 || h > 1.0) return 0.0;

    if (fld.x < 0.015) return 0.0;

    // Column height driven by the WIDTH-GATED driver only (lib/lighting.glsl):
    // floor 0.16 = 26-block pancakes, tier A domes to 70 blocks (width >= 140
    // guaranteed), tier B grand cores to 140 (width >= 280) - each tier caps
    // its height at half its guaranteed width, so ratio >= 2 by construction.
    // VL_CUM_COLCAP re-clamps the FLOOR too for pathological VC_THICK/scale
    // combos (e.g. scale 0.5 + VC_THICK 512, where 0.16 alone = 82 blocks).
    // Never feed fine detail into colH: that is the "towers" bug.
    // Floor scaled with the guaranteed width (verifier fix): a fixed 0.16 slab
    // floor (26 blocks) broke the >2 ratio at VC_CUMULUS_SCALE 0.5 where cells
    // shrink to ~45 blocks. Identical at default scale (0.306 > 0.16).
    float colH = min(min(0.16, max(VL_CUM_RAD * 0.35 / VC_THICK, 0.05)) + 0.72 * fld.y, VL_CUM_COLCAP);
    // Flat base (ramp over 6% of VC_THICK) + short rounded cap toward colH.
    float profile = smoothstep(0.0, 0.06, h) * smoothstep(colH, colH * 0.74, h);
    // Shoulders drawing in with altitude (free) -> domed flanks, not cylinders.
    float d = (fld.x - h * h * 0.16) * profile;
    if (d <= 0.003) return 0.0;

    if (hiRes && VC_CUMULUS_EROSION > 0.001) {
        // ADVECTION: the detail drifts 12% faster than the coverage, so the
        // silhouettes keep EVOLVING instead of being frozen onto the mass; the
        // slow -y drift reads as buds growing upward. mod() folds the offset
        // into one curl tile - exact for all three lookups (integer ratios) and
        // it keeps the uv small enough for float32 on very old worlds.
        vec3 wD = mod(vec3(t * 7.0, t * -0.35, t * 2.4) * (VC_WIND * 1.12), VL_CURL_TILE);
        vec3 pD = wp + wD;
        // Erosion weights: nothing at the flat belly, edges only, never cores.
        float wH   = smoothstep(0.05, 0.45, h);      // 0 at the base -> crisp belly
        float edge = 1.0 - clamp(d * 2.4, 0.0, 1.0); // 1 at the edge, 0 in the core
        // Cap zone (top ~third of the column): erosion boosted there so the
        // rounded cap is gnawed by the 3D detail instead of ending on smooth
        // colH isolines - seen at grazing angles from above, those isolines
        // read as stacked terraces. Zero at the belly/flanks (capW = 0).
        float capW = smoothstep(colH * 0.68, colH, h);
        #if VC_DETAIL == 1
        // CURL: RGBA8_SNORM, values are ALREADY in [-1,1] - do NOT do *2-1.
        // Amplitude grows with altitude (~4 -> 13 blocks at defaults):
        // turbulent moutonne tops, undisturbed flat base.
        vec3 curl = texture3D(vlCumulusCurl, pD * (1.0 / VL_CURL_TILE)).xyz;
        pD += curl * ((2.5 + 6.0 * h) * VC_CURL * VC_CUMULUS_SCALE);
        #endif
        // Cauliflower erosion: a LOW texel carves matter away. Routed through
        // wH/edge so the belly stays crisp and the cores stay solid.
        float n1 = texture3D(vlCumulusDetail1, pD * (1.0 / VL_DET1_TILE)).r;
        d -= (1.0 - n1) * 0.14 * (1.0 + 0.6 * capW) * VC_CUMULUS_EROSION * wH * max(edge, 0.28);
        if (d <= 0.0) return 0.0;
        #if VC_DETAIL == 1
        // Fine fringe (~18-block tile) -> ragged outlines. Only paid for when
        // the edge survived the first pass.
        if (edge > 0.18 && VC_DETAIL_STRENGTH > 0.001) {
            float n2 = texture3D(vlCumulusDetail2, pD * (1.0 / VL_DET2_TILE)).r;
            d -= (1.0 - n2) * 0.075 * (1.0 + 0.6 * capW) * VC_DETAIL_STRENGTH * edge * max(wH, 0.30);
            if (d <= 0.0) return 0.0;
        }
        #endif
        d *= 0.80 + 0.20 * n1; // light internal structure, solid matter
    }

    // Gain 2.6: saturated cores (opaque in 2-3 steps), wispy fringe
    return clamp(d * 2.6, 0.0, 1.0) * VC_DENSITY;
}
#else
// ---- PROCEDURAL v5 FALLBACK (VC_TEXTURED=0), kept BIT-IDENTICAL ------------
// ---- Cumulus shapes: wide flat HORIZONTAL masses, much wider than tall.
// SILHOUETTE and HEIGHT are deliberately DECOUPLED:
//   . SILHOUETTE (covF) : sharply thresholded cells;
//   . HEIGHT     (massN): SMOOTH large-scale mass field (2 octaves >= ~230
//     blocks, WIDE 0.30 threshold band) - only the CORES of big systems rise,
//     edges and small cells stay LOW and FLAT.
// Coupling them (height following covF and a fine dome field) is what produces
// vertical walls/pillars: any narrow cell above the threshold then fills the
// whole slab. Do not reintroduce it.
// Expected shapes (VC_CUMULUS_SCALE=1.5, VC_THICK=160):
//   . big systems (rare): ~350-600 blocks wide, 60-120 filled height (160 only
//     in the core of very large masses) -> width/height ratio ~2.5-5x, core
//     rise spread over ~130-150 horizontal blocks;
//   . scattered small cumulus (frequent): 40-100 blocks, ~38-tall pancakes;
//   . top domes: radius ~55-70 blocks (rounded, not cones);
//   . banks vs clear sky: ~3-4 km scale.
// SANITY CHECK: an isolated 50-block cell above the threshold gives massN=0
// (the smooth field does not see it) -> colH floor -> ~27-38 block pancake,
// NOT a 160-block pillar.
// SYNC: this mask is DUPLICATED constant by constant in cloudShadow
// (lib/lighting.glsl) - any change here must be mirrored there.

// Shared 2D field: returns vec2(thresholded coverage, smooth height driver)
vec2 cloudFieldCumulus(vec2 xz, float t) {
    vec2 wind = t * vec2(7.0, 2.4) * VC_WIND;
    vec2 p = xz + wind;
    // Smooth large-scale mass (~536x395 then ~232x171 blocks at defaults),
    // anisotropic along x = wind direction -> slightly stretched systems.
    vec2 qm = vec2(p.x * 0.0028, p.y * 0.0038) / VC_CUMULUS_SCALE;
    float M = vnoise(qm) * 0.62 + vnoise(qm * 2.31 + 19.7) * 0.26;
    // Silhouette: mass + medium octave (~73 blocks) -> crisp bays and capes
    float mk = M + vnoise(qm * 7.3 + 2.9) * 0.12;
    #if VC_DETAIL == 1
    // Fine octave (~32 blocks) on the OUTLINE only, renormalized - height
    // never depends on it, so no pillar can appear.
    mk = mk * 0.92 + vnoise(qm * 16.9 + 5.1) * 0.08;
    #endif
    // Banks vs large clear patches: a bank RAISES THE THRESHOLD (large-scale
    // modulator).
    float bank = smoothstep(0.28, 0.62, fbm3(p * 0.00027 + 17.3));
    float cov = clamp(mix(VC_COVERAGE, 1.0, rainStrength * 0.95), 0.0, 1.0); // full rain = near-total overcast
    float thB = mix(0.60, 0.34, cov) + (1.0 - bank) * 0.12;
    float big = smoothstep(thB, thB + 0.13, mk);
    // 2nd family: scattered low cumulus (~40-100 blocks), frequent inside
    // banks, a few survivors in clear areas (adds variety).
    vec2 qs = vec2(p.x * 0.010, p.y * 0.013) / VC_CUMULUS_SCALE;
    float sc = vnoise(qs) * 0.70 + vnoise(qs * 2.63 + 7.7) * 0.30;
    float thS = mix(0.74, 0.58, cov);
    float sm = smoothstep(thS, thS + 0.10, sc) * (0.30 + 0.70 * bank);
    // Kill switch: at VC_COVERAGE=0 the sky is CLEAR (rain brings clouds back
    // through cov).
    float covF = max(big, sm * 0.85) * smoothstep(0.0, 0.05, cov);
    // Rain overcast floor (verifier fix): thresholds alone left 15-25% blue sky
    // at full rain on this procedural path. MIRRORED in cloudShadow.
    covF = max(covF, rainStrength * rainStrength * 0.85);
    // HEIGHT driver: the smooth mass M ONLY (WIDE 0.30 band) - big system
    // cores rise, everything else stays flat.
    // Rain flattens the deck (nimbostratus are flat, not towering); MIRRORED
    // in cloudShadow (lib/lighting.glsl).
    float massN = smoothstep(thB, thB + 0.30, M) * (1.0 - rainStrength * 0.65);
    return vec2(covF, massN);
}

// hiRes=false: cheap variant (no 3D erosion) for the light march.
// fld = precomputed field. In this PROCEDURAL fallback the light march keeps
// the origin field for ALL its steps - a v5 perf trade kept bit-identical.
// Honest caveat: the silhouette octaves go down to ~73 blocks, so the outer
// steps can shadow with stale coverage (the textured path refreshes instead).
// Vertical profile: common FLAT base (condensation over ~6% of the slab),
// SHORT rounded cap (top 24% of the column, avoids cones), shoulders drawing
// in with altitude -> domed flanks rather than cylinders.
float cloudDensityCumulusCov(vec3 wp, float t, bool hiRes, vec2 fld) {
    float h = (wp.y - VC_BASE) / VC_THICK;
    if (h < 0.0 || h > 1.0) return 0.0;

    if (fld.x < 0.015) return 0.0;

    vec2 wind = t * vec2(7.0, 2.4) * VC_WIND;
    vec2 pp = wp.xz + wind;
    // WIDE top domes (~143x111 blocks -> radius 55-70): a single SMOOTH octave
    // gives round cauliflowers (a fine octave here produces spikes).
    vec2 qd = pp * vec2(0.0105, 0.0135) / VC_CUMULUS_SCALE;
    float dome = vnoiseTex(qd); // baked-texture tap (NOISE_TEX); not part of the cloudShadow mirror

    // Column height. Floor 0.17 keeps isolated 40-60 block puffs at ~27 blocks
    // tall (wider than tall); the +0.07*smoothstep term restores ~0.24 as soon
    // as the smooth mass exists. The rise is driven by the SMOOTH mass alone.
    // Cores land at 0.54-0.96 = 86-154 filled blocks.
    float colH = 0.17 + 0.07 * smoothstep(0.0, 0.25, fld.y) + fld.y * (0.30 + dome * 0.42);
    // Flat base (ramp over 6% of VC_THICK) + short cap toward colH.
    float profile = smoothstep(0.0, 0.06, h) * smoothstep(colH, colH * 0.76, h);
    // Drawn-in shoulders: the silhouette narrows with altitude (free, no extra
    // noise) -> domed tops even where covF is saturated.
    float d = (fld.x - h * h * 0.16) * profile;
    if (d <= 0.003) return 0.0;

    // 3-octave billowy erosion: zero at the base (crisp flat belly), sculpts
    // flanks and tops, leaves cores solid; features slightly stretched along x
    // (downwind). hiRes only.
    if (hiRes && VC_CUMULUS_EROSION > 0.001) {
        vec3 dp = vec3(pp.x * 0.85, wp.y * 2.1, pp.y);
        float ero = vnoise3(dp * 0.026) * 0.50 + vnoise3(dp * 0.074) * 0.32
                  + vnoise3(dp * 0.19) * 0.18;
        ero = 1.0 - abs(ero * 2.0 - 1.0);
        float wH = smoothstep(0.05, 0.45, h);        // 0 at the base -> crisp belly
        float edge = 1.0 - clamp(d * 2.4, 0.0, 1.0); // 1 at the edge, 0 in the core
        d -= (1.0 - ero) * 0.17 * VC_CUMULUS_EROSION * wH * max(edge, 0.28);
        if (d <= 0.0) return 0.0;
        #if VC_DETAIL == 1
        // Micro-erosion (~3-5 blocks) on edges -> fine ragged outlines.
        // Only paid for when the edge survived the previous pass.
        if (edge > 0.18 && VC_DETAIL_STRENGTH > 0.001) {
            float hf = 1.0 - abs(vnoise3(dp * 0.34) * 2.0 - 1.0);
            d -= (1.0 - hf) * 0.085 * VC_DETAIL_STRENGTH * edge * max(wH, 0.30);
            if (d <= 0.0) return 0.0;
        }
        #endif
        d *= 0.80 + 0.20 * ero; // light internal structure, solid matter
    }

    // Gain 2.6: saturated cores (opaque in 2-3 steps), wispy fringe
    return clamp(d * 2.6, 0.0, 1.0) * VC_DENSITY;
}
#endif
// Convenience entry point: recomputes the field itself. SHARED by both paths
// (textured and procedural). volumetricClouds hoists the field instead (one
// evaluation for density + light march); kept for any external caller.
float cloudDensityCumulus(vec3 wp, float t, bool hiRes) {
    float h = (wp.y - VC_BASE) / VC_THICK;
    if (h < 0.0 || h > 1.0) return 0.0;
    return cloudDensityCumulusCov(wp, t, hiRes, cloudFieldCumulus(wp.xz, t));
}

// Optical depth toward the sun: PROGRESSIVE steps (tight near the point ->
// fine self-shadowing of the buds, wider afterwards -> overall mass). Uses the
// cheap density (hiRes=false): the light march never pays for the 3D erosion.
// fld = field at wp, hoisted by the caller (volumetricClouds already paid for
// it for the density) - no redundant evaluation here any more.
float cloudOpticalDepthCumulus(vec3 wp, vec3 lightDir, float t, vec2 fld) {
    #ifdef CLOUD_FAST_SHADOW
        const int CLO_STEPS = 3;
    #else
        const int CLO_STEPS = 4;
    #endif
    float acc = 0.0;
    float fr = 0.10;
    float prev = 0.0;
    vec2 fldOD = fld;
    for (int j = 0; j < CLO_STEPS; j++) {
        vec3 p = wp + lightDir * (VC_THICK * fr);
#if VC_TEXTURED == 1
        // Steps land at 16/34/71/148 blocks along lightDir. MEASURED on the
        // .bin: coverage is 48% decorrelated at 63 blocks and 92% at 148 -
        // frozen at the origin, the outer steps integrated the WRONG cloud's
        // coverage (visible as bad self-shadowing at low sun). One refresh tap
        // at step 2 re-anchors both outer steps; it is paid for by the
        // redundant origin tap this rework removed. Refreshing the height
        // driver would cost 8 more taps; instead it is TIGHTENED for free:
        // a column is never taller than its point-coverage bound.
        if (j == 2) {
            float covR = vlCumulusCoverage(p.xz, t);
            fldOD = vec2(covR, min(fldOD.y, vlCumulusTiers(covR, covR)));
        }
#endif
        acc += cloudDensityCumulusCov(p, t, false, fldOD) * (VC_THICK * (fr - prev));
        prev = fr;
        fr *= 2.1;
    }
    return acc;
}
#endif

// Secondary march toward the light: cloud self-shadowing (Beer's law)
float cloudLightMarch(vec3 wp, vec3 lightDir, float t) {
    float acc = 0.0;
    #ifdef CLOUD_FAST_SHADOW
        const int CL_STEPS = 3; // 3-step self-shadowing instead of 4 (perf)
    #else
        const int CL_STEPS = 4;
    #endif
    float st = VC_THICK / float(CL_STEPS);
    vec3 p = wp;
    for (int j = 0; j < CL_STEPS; j++) {
        p += lightDir * st;
        acc += cloudDensity(p, t);
    }
    return exp(-acc * st * 0.032 * min(VC_OPACITY * 0.75, 4.0));
}

float hgPhase(float c, float g) {
    return (1.0 - g * g) / (12.566 * pow(1.0 + g * g - 2.0 * g * c, 1.5));
}

/*
    Main cumulus raymarch. Returns (color, alpha).
    maxDist: clouds are drawn IN FRONT of distant terrain and can be flown over.
*/
vec4 volumetricClouds(vec3 camPos, vec3 dir, vec3 lightDirW, vec3 fogCol, float dither, float t, float maxDist) {
    float top = VC_BASE + VC_THICK;
    float t0;
    float t1;
    if (abs(dir.y) < 1e-3) {
        if (camPos.y < VC_BASE || camPos.y > top) return vec4(0.0);
        t0 = 0.0;
        // Shortened range for a horizontal view from inside the layer:
        // 24 samples over 900 blocks = 37-block steps, far better resolved
        // than 83-block steps over 2000 blocks (clouds "vanishing")
        t1 = 900.0;
    } else {
        float ta = (VC_BASE - camPos.y) / dir.y;
        float tb = (top - camPos.y) / dir.y;
        t0 = max(min(ta, tb), 0.0);
        t1 = max(ta, tb);
        if (t1 <= 0.0) return vec4(0.0);
        // Grazing view toward the horizon: curvature brings very distant
        // clouds into frame, so the march is extended. ONLY below the layer -
        // looking DOWN from above, the extension diluted VC_SAMPLES over empty
        // space under the sheet (thin, noisy clouds).
        if (dir.y > -0.001 && dir.y < 0.18 && camPos.y < top) t1 = max(t1, VC_RENDER_DISTANCE); // downward aim from inside the sheet is excluded on purpose
    }
    t1 = min(min(t1, t0 + VC_RENDER_DISTANCE), maxDist);
    if (t1 <= t0 + 0.01) return vec4(0.0);

    float dt = (t1 - t0) / float(VC_SAMPLES);
#if BLUE_NOISE == 1
    // Real jitter ABOVE the slab too: blue noise is uncorrelated between
    // neighbours and TAA averages it, unlike the old 8x8 Bayer whose +-dt
    // pattern made purple dots. The former 0.5 pin removed ALL dithering seen
    // from above -> the march quantized the tops into dt-thick terraces.
    float jitter = dither;
#else
    // Seen from above, the 8x8 Bayer pattern creates purple dots: the +-dt
    // offset between adjacent pixels alternates hit/miss on fine cloud detail.
    // Jitter is pinned to 0.5 (fixed mid-step) there to kill that noise.
    float jitter = (camPos.y > top) ? 0.5 : dither;
#endif
    float tt = t0 + dt * jitter;

    float cosT = dot(dir, lightDirW);
#if VC_CUMULUS_SHAPES == 1
    // ---- Contrasted cumulus lighting: cream sun faces, blue-grey bellies ----
    // Tight forward lobe (g=0.72): the sunlit face pops, the shaded face falls
    // back -> the relief is modelled instead of reading as a flat mass.
    float phase = hgPhase(cosT, 0.72) + hgPhase(cosT, -0.22) * 0.45 + 0.030;

    float duskC = clamp(1.0 - abs(sunHeight()) * 4.0, 0.0, 1.0) * smoothstep(-0.14, -0.04, sunHeight()); // asymmetric (night fix)
    vec3 sunC = getLightColor() * 3.6 * CLOUD_BRIGHTNESS;
    // Alto-matched palette (layer 2 look): tops are near-PURE sky ambient -
    // the old 0.10 light term tinted every core a dirty warm grey at noon -
    // and the belly is a brighter, CLEANER blue-grey (never green/yellow).
    // CLOUD_WHITE (CONFIG_CLOUDS): creator dial on the sunlit-top brightness.
    vec3 ambT = (getAmbientColor() * 1.12 + getLightColor() * 0.04) * CLOUD_WHITE;
    // Blue-grey belly (the mass blocks the sky) + golden grazing light at dusk.
    // CLOUD_BELLY_COOL shifts the belly tint around its grey mean (delta form:
    // exact no-op at 1.0); CLOUD_BELLY_BRIGHT scales the sky-ambient share only.
    vec3 bellyTint = vec3(0.33, 0.37, 0.46)
                   + (vec3(0.33, 0.37, 0.46) - vec3(0.3867)) * (CLOUD_BELLY_COOL - 1.0);
    vec3 ambB = getAmbientColor() * bellyTint * CLOUD_BELLY_BRIGHT + getLightColor() * duskC * 0.45;

    // Rain overcast: nimbostratus. The direct sun collapses, the phase goes
    // near-isotropic (a flat deck models no relief) and both ambients drop to
    // a dark neutral slate; getLightColor/getAmbientColor already rain-dim.
    float rainG = rainStrength;
    phase = mix(phase, 0.08, rainG * 0.85);
    sunC *= 1.0 - rainG * 0.72;
    vec3 slate = getAmbientColor() * vec3(0.66, 0.66, 0.70);
    ambT = mix(ambT, slate, rainG * 0.80);
    ambB = mix(ambB, slate * 0.62, rainG * 0.80);

    // Light extinction with a FLOOR (decoupled from a low VC_OPACITY): cores
    // still self-shadow on "light" clouds -> real contrast.
    float sigL = 0.032 * clamp(VC_OPACITY * 0.75, 1.1, 4.0);

    #if VC_DETAIL == 1
    // Wind precomputed to advect the wisps along with the cloud mass
    vec2 wWind = t * vec2(7.0, 2.4) * VC_WIND;
    #endif

    vec3 acc = vec3(0.0);
    float trans = 1.0;

    for (int i = 0; i < VC_SAMPLES; i++) {
        vec3 wp = camPos + dir * tt;
        // Field hoisted out of cloudDensityCumulus: ONE evaluation feeds the
        // density AND the light march (they used to evaluate it twice).
        // Out-of-slab samples (grazing-view march extension) still pay zero
        // taps, exactly like the old h early-out.
        float hIn = (wp.y - VC_BASE) / VC_THICK;
        vec2 fld = (hIn < 0.0 || hIn > 1.0) ? vec2(0.0) : cloudFieldCumulus(wp.xz, t);
        float d = cloudDensityCumulusCov(wp, t, true, fld);
        if (d > 0.002) {
            float od = cloudOpticalDepthCumulus(wp, lightDirW, t, fld);
            // Multi-scatter approximation (2 Beer octaves): the wider 2nd
            // octave keeps cores alto-white instead of dull grey, still modelled
            float lt = exp(-od * sigL) * 0.55 + exp(-od * sigL * 0.20) * 0.45;
            float h = clamp((wp.y - VC_BASE) / VC_THICK, 0.0, 1.0);
            float powder = 1.0 - exp(-d * 7.0);
            // DARK flat base: the belly blocks the sky (sun AND ambient)
            float baseDark = 1.0 - VC_CUMULUS_BASE_DARK * (1.0 - smoothstep(0.0, 0.50, h)) * powder;
            // Seen from above, a uniform zenith boost flattens everything into
            // a white carpet with no visible shadows. It is modulated by lt
            // here (the troughs between domes do not get it) and the ambient
            // leans more on lt -> relief is drawn by the shadowing.
            float zenith = (camPos.y > top) ? h * h * 0.30 * lt : 0.0;
            vec3 S = sunC * lt * phase * (0.30 + powder * 0.95)
                   + mix(ambB, ambT * (1.0 + zenith), h) * (0.34 + 0.40 * lt); // raised ambient floor: shaded faces stay clean, not dead grey
            S *= baseDark;
            #if VC_DETAIL == 1
            // Wisps: internal albedo micro-variation (~20-block features,
            // advected by the wind). Modulates the SOURCE, not the density, so
            // internal structure shows without altering the silhouette.
            // Costs 1 vnoise3 per lit sample.
            float wisp = vnoise3(vec3(wp.x + wWind.x, wp.y * 1.8, wp.z + wWind.y) * 0.052);
            S *= 1.0 + (wisp - 0.5) * 0.55 * VC_DETAIL_WISPS;
            #endif
            // Silver lining: thin sunward edge where light punches through
            float silver = pow(max(cosT, 0.0), 8.0) * lt * (1.0 - powder);
            S += sunC * silver * 0.28 * CLOUD_SILVER * VC_CUMULUS_SILVER * (1.0 - rainG); // no edge flare on a rain deck

            float a = exp(-d * 0.05 * dt * VC_OPACITY);
            acc += trans * S * (1.0 - a);
            trans *= a;
            if (trans < 0.01) break;
        }
        tt += dt;
    }
#else
    float phase = hgPhase(cosT, 0.60) * 1.0 + hgPhase(cosT, -0.20) * 0.30 + 0.035;

    float duskC = clamp(1.0 - abs(sunHeight()) * 4.0, 0.0, 1.0) * smoothstep(-0.14, -0.04, sunHeight()); // asymmetric (night fix)
    vec3 sunC = getLightColor() * 2.2 * CLOUD_BRIGHTNESS;
    // CONFIG_CLOUDS dials mirrored from the textured path (exact no-op at 1.0),
    // so the sliders are never silently dead on this legacy raymarch.
    vec3 ambT = (getAmbientColor() * 0.90 + getLightColor() * 0.08) * CLOUD_WHITE;
    // Golden hour: cloud bellies catch fire under grazing light.
    // Cool tint = textured bellyTint normalized by its grey mean, delta form.
    vec3 bellyCool = vec3(1.0) + vec3(-0.147, -0.043, 0.190) * (CLOUD_BELLY_COOL - 1.0);
    vec3 ambB = getAmbientColor() * 0.35 * bellyCool * CLOUD_BELLY_BRIGHT + getLightColor() * duskC * 0.55;

    vec3 acc = vec3(0.0);
    float trans = 1.0;

    for (int i = 0; i < VC_SAMPLES; i++) {
        vec3 wp = camPos + dir * tt;
        float d = cloudDensity(wp, t);
        if (d > 0.002) {
            float lt = cloudLightMarch(wp, lightDirW, t);
            float h = clamp((wp.y - VC_BASE) / VC_THICK, 0.0, 1.0);
            float powder = 1.0 - exp(-d * 3.5);
            // From above: tops receive more zenith light
            float zenith = (camPos.y > top) ? h * h * 0.45 : 0.0;
            vec3 S = sunC * lt * phase * (0.6 + powder * 0.7) + mix(ambB, ambT * (1.0 + zenith), h) * 0.50;

            #ifdef CLOUD_SILVER_LINING
                // Silver lining: looking toward the sun (cosT->1) through a thin
                // area where light passes (high lt), the cloud edge blazes.
                // Purely additive term.
                float silver = pow(max(cosT, 0.0), 6.0) * lt;
                S += sunC * silver * 0.6 * CLOUD_SILVER;
            #endif

            float a = exp(-d * 0.05 * dt * VC_OPACITY);
            acc += trans * S * (1.0 - a);
            trans *= a;
            if (trans < 0.01) break;
        }
        tt += dt;
    }
#endif

    float alpha = 1.0 - trans;
    if (alpha < 1e-4) return vec4(0.0);

    // Fade distant clouds into the horizon haze.
    // Range must be measured as the HORIZONTAL distance of the entry point:
    // fading on t0 (distance ALONG the ray) dissolves the cloud carpet into a
    // RING around the nadir when seen from altitude, because t0 climbs fast on
    // oblique aims.
    float tHor = t0 * length(dir.xz);
    // Aerial perspective: far clouds turn a discreet blue-grey (thin distant
    // bands) instead of a milky white veil, and the haze rises more slowly.
    // The VC_CUMULUS_SHAPES=0 branch keeps the original values.
    #if VC_CUMULUS_SHAPES == 1
    const float fogK = 0.00030;
    vec3 fogC = fogCol * vec3(0.80, 0.87, 1.00) * 0.92;
    #else
    const float fogK = 0.00038;
    vec3 fogC = fogCol;
    #endif
    #ifdef CLOUD_EDGE_FADE
    // EDGE fade: clouds whose ENTRY t0 approaches the range cutoff
    // (VC_RENDER_DISTANCE) dissolve into haze instead of stopping abruptly.
    // Based on t0 vs VC_RENDER_DISTANCE: nearby/overhead clouds (small t0) stay
    // OPAQUE, only the far arc fades.
    // At short ranges (< 1000) the fade starts later (~90-block band) instead
    // of eating 25% of the field, so the near sky does not look empty.
    float edgeStart = (VC_RENDER_DISTANCE < 1000.0) ? max(0.75, 1.0 - 100.0 / VC_RENDER_DISTANCE) : 0.75;
    float edgeF = smoothstep(VC_RENDER_DISTANCE * edgeStart, VC_RENDER_DISTANCE * 0.98, tHor);
    float fogT = max(1.0 - exp(-tHor * fogK), edgeF);
    vec3 col = acc / alpha;
    col = mix(col, fogC, fogT);
    alpha *= 1.0 - fogT;
    #else
    float fogT = 1.0 - exp(-tHor * fogK);
    vec3 col = acc / alpha;
    col = mix(col, fogC, fogT);
    // Reduced alpha fade: more opaque clouds at the horizon
    alpha *= 1.0 - fogT * 0.50;
    #endif

    #if VC_CUMULUS_SHAPES == 1
    // Horizon flattening: from the ground, grazing clouds (< ~6 deg) thin into
    // translucent bluish bands instead of a white wall. Neutralized when the
    // camera is inside or above the sheet (flying through: clouds stay solid).
    float belowF = clamp((VC_BASE - camPos.y) / 40.0, 0.0, 1.0);
    float lowBand = smoothstep(0.015, 0.11, abs(dir.y));
    alpha *= 1.0 - (1.0 - lowBand) * 0.62 * belowF;
    #endif

    return vec4(col, alpha);
}

// ============ VOLUMETRIC ALTOCUMULUS (2nd 3D layer) ============
// Thin slab [CLOUD_ALTO_HEIGHT .. +26] marched in 6 steps: real domed puffs
// above the cumulus instead of a flat 2D sheet. Same cell field as the 2D alto
// plus a domed vertical profile. Costs ~1/4 of the cumulus pass.
#if defined CLOUD_LAYER_ALTO && ALTO_VOLUMETRIC == 1
float altoDensity(vec3 wp, float t) {
    float h = (wp.y - CLOUD_ALTO_HEIGHT) / 26.0;
    if (h < 0.0 || h > 1.0) return 0.0;
    vec2 p = wp.xz + t * vec2(11.0, 4.0) * VC_WIND;
    vec2 qc = vec2(p.x * 0.011, p.y * 0.0148);
    float puff = vnoiseTex(qc) * 0.52 + vnoiseTex(qc * 2.63 + 7.7) * 0.31
               + vnoiseTex(qc * 6.1 + 2.9) * 0.17;
    float bank = smoothstep(0.42, 0.70, fbm3Tex(p * 0.00058 + 17.3));
    float prof = smoothstep(0.0, 0.25, h) * smoothstep(1.0, 0.45, h);
    // Rain fade (parity with the 2D alto/cirrus): the layer dissolves under
    // the overcast deck instead of poking through thin spots.
    return smoothstep(0.50, 0.72, puff) * bank * prof * 1.6 * CLOUD_ALTO_AMOUNT * (1.0 - rainStrength * 0.85);
}
vec4 volumetricAlto(vec3 camPos, vec3 dir, vec3 lightDirW, float dither, float t) {
    float aBase = CLOUD_ALTO_HEIGHT;
    float aTop = CLOUD_ALTO_HEIGHT + 26.0;
    if (abs(dir.y) < 1e-3) return vec4(0.0);
    float ta = (aBase - camPos.y) / dir.y;
    float tb = (aTop - camPos.y) / dir.y;
    float t0 = max(min(ta, tb), 0.0);
    float t1 = max(ta, tb);
    if (t1 <= 0.0 || t0 * max(abs(dir.y), 0.05) > 900.0) return vec4(0.0);
    t1 = min(t1, t0 + 220.0);
    float dt = (t1 - t0) / 6.0;
    float tt = t0 + dt * dither;
    vec3 sunC = getLightColor() * 2.6;
    vec3 ambT = getAmbientColor();
    float cosT = dot(dir, lightDirW);
    float phase = hgPhase(cosT, 0.55) + 0.06;
    vec3 acc = vec3(0.0);
    float trans = 1.0;
    for (int i = 0; i < 6; i++) {
        vec3 wp = camPos + dir * tt;
        float d = altoDensity(wp, t);
        if (d > 0.003) {
            // 1-tap self-shadowing: models each puff (sun face vs belly)
            float lt = exp(-altoDensity(wp + lightDirW * 14.0, t) * 5.0);
            float hN = clamp((wp.y - aBase) / 26.0, 0.0, 1.0);
            // CLOUD_ALTO_BRIGHT (CONFIG_CLOUDS): layer-2 source brightness, shading only.
            vec3 S = (sunC * lt * phase + mix(ambT * vec3(0.45, 0.50, 0.60), ambT, hN) * 0.55) * CLOUD_ALTO_BRIGHT;
            float aStep = exp(-d * 0.16 * dt);
            acc += trans * S * (1.0 - aStep);
            trans *= aStep;
            if (trans < 0.03) break;
        }
        tt += dt;
    }
    float alpha = 1.0 - trans;
    if (alpha < 1e-3) return vec4(0.0);
    // horizon fade on HORIZONTAL distance (consistent with the cumulus pass)
    float fogT = 1.0 - exp(-t0 * length(dir.xz) * 0.0009);
    alpha *= 1.0 - fogT;
    return vec4(acc / max(alpha, 1e-4), alpha);
}
#endif

/*
    High layers (2D, world-anchored): altocumulus + cirrus.
    Composited BEHIND the volumetric cumulus.
*/
#if CLOUD_HIGH_V3 == 1
// Cirrus: thin ice filaments stretched x13 along the wind, curved by a slow
// warp (uncinus hooks), grouped in BANKS separated by blue sky, strong forward
// scatter near the sun, blazing at dusk.
// Altocumulus: field of small puffs (~90 blocks) in finite sheets, relief lit
// toward the sun (bright faces / shaded troughs), realistic opacities.
vec4 highClouds(vec3 camPos, vec3 dir, vec3 sunDir, float t) {
    vec3 col = vec3(0.0);
    float alpha = 0.0;
    float horizF = smoothstep(0.015, 0.10, abs(dir.y));
    if (horizF <= 0.0) return vec4(0.0);
    float cosS = dot(dir, sunDir);
    float duskH = clamp(1.0 - abs(sunHeight()) * 3.5, 0.0, 1.0) * smoothstep(-0.14, -0.04, sunHeight()); // asymmetric (night fix)
    vec3 lc = getLightColor();
    vec3 amb = getAmbientColor();

    // --- Cirrus (~CLOUD_CIRRUS_HEIGHT) ---
    #ifdef CLOUD_LAYER_CIRRUS
    {
        float tc = (CLOUD_CIRRUS_HEIGHT - camPos.y) / dir.y;
        if (tc > 0.0) {
            vec2 p = camPos.xz + dir.xz * tc + t * vec2(18.0, 6.0) * VC_WIND;
            // Anisotropic frame: u along the wind, v across
            vec2 axW = normalize(vec2(18.0, 6.0));
            vec2 q = vec2(dot(p, axW), dot(p, vec2(-axW.y, axW.x)));
            // Slow streak curvature (transverse undulation ~240 blocks)
            q.y += (vnoise(vec2(q.x * 0.00040, q.y * 0.0014) + 3.1) - 0.5) * 480.0;
            // fbm stretched x13 -> filaments; + high-frequency cross fibres
            float n = fbm(vec2(q.x * 0.00032, q.y * 0.00415));
            n = n * 0.74 + vnoise(vec2(q.x * 0.0012, q.y * 0.0165)) * 0.26;
            // Low-frequency banks: cirrus sheets, not a uniform veil
            float bank = smoothstep(0.34, 0.78, fbm3(p * 0.00016 + 41.2));
            float cm = smoothstep(0.46, 0.80, n) * bank;
            cm = cm * cm * (3.0 - 2.0 * cm); // sharpen the streaks
            cm *= horizF * (1.0 - rainStrength * 0.7) * 0.34 * CLOUD_CIRRUS_AMOUNT;
            // Thin ice: forward scatter near the sun + blaze at sunset
            vec3 cirC = amb * 0.80 + lc * (0.55 + 0.90 * pow(max(cosS, 0.0), 6.0));
            cirC += lc * duskH * 0.80;
            alpha = clamp(cm, 0.0, 1.0);
            col = cirC * alpha; // premultiplied: no ghost cirrus tint under the alto
        }
    }
    #endif

    // --- Altocumulus (~CLOUD_ALTO_HEIGHT), composited IN FRONT of the cirrus ---
    // (this 2D version is DISABLED when ALTO_VOLUMETRIC=1: the 3D slab replaces it)
    #if defined CLOUD_LAYER_ALTO && ALTO_VOLUMETRIC == 0
    {
        float ta = (CLOUD_ALTO_HEIGHT - camPos.y) / dir.y;
        if (ta > 0.0) {
            vec2 p = camPos.xz + dir.xz * ta + t * vec2(11.0, 4.0) * VC_WIND;
            // Slightly anisotropic cell field -> near-regular rows
            vec2 q = vec2(p.x * 0.011, p.y * 0.0148);
            float puff = vnoiseTex(q) * 0.52 + vnoiseTex(q * 2.63 + 7.7) * 0.31
                       + vnoiseTex(q * 6.1 + 2.9) * 0.17;
            // Finite sheets (banks): never paves the whole sky
            float bank = smoothstep(0.42, 0.70, fbm3Tex(p * 0.00058 + 17.3));
            float am = smoothstep(0.505, 0.66, puff) * bank;
            if (am > 0.001) {
                // Relief: field gradient toward the sun -> bright face / shaded trough
                float sunLow = clamp(1.0 - sunDir.y * 1.6, 0.5, 1.8);
                vec2 so = normalize(sunDir.xz + vec2(1e-4)) * 0.045 * sunLow;
                float puff2 = vnoiseTex(q + so) * 0.52 + vnoiseTex((q + so) * 2.63 + 7.7) * 0.31
                            + vnoiseTex((q + so) * 6.1 + 2.9) * 0.17;
                float shade = clamp((puff - puff2) * 9.0 + 0.5, 0.0, 1.0);
                vec3 bright = amb * 0.92 + lc * (0.42 + 0.45 * duskH);
                vec3 darkC  = amb * vec3(0.38, 0.42, 0.52) + lc * duskH * 0.18;
                vec3 altoC = mix(bright, darkC, shade) * CLOUD_ALTO_BRIGHT; // CONFIG_CLOUDS layer-2 dial
                // Silver edge on the sun side of the thin puffs
                altoC += lc * pow(max(cosS, 0.0), 10.0) * 0.35 * (1.0 - am);
                am *= horizF * (1.0 - rainStrength * 0.5) * 0.62 * CLOUD_ALTO_AMOUNT;
                am = clamp(am, 0.0, 1.0);
                col = col * (1.0 - am) + altoC * am; // premultiplied "over"
                alpha = alpha * (1.0 - am) + am;
            }
        }
    }
    #endif

    alpha = clamp(alpha, 0.0, 1.0);
    // Un-premultiply: the consumer (deferred) does mix(sky, rgb, a)
    return vec4(col / max(alpha, 1e-4), alpha);
}
#else
vec4 highClouds(vec3 camPos, vec3 dir, vec3 sunDir, float t) {
    vec3 col = vec3(0.0);
    float alpha = 0.0;
    float horizF = smoothstep(0.015, 0.10, abs(dir.y));

    // --- Cirrus, ~520 blocks: filaments stretched by the wind ---
    #ifdef CLOUD_LAYER_CIRRUS
    {
        float tc = (CLOUD_CIRRUS_HEIGHT - camPos.y) / dir.y;
        if (tc > 0.0) {
            vec2 p = camPos.xz + dir.xz * tc + t * vec2(18.0, 6.0) * VC_WIND;
            float warp = vnoiseTex(p * 0.0016) * 60.0;
            float n = fbmTex(vec2((p.x + warp) * 0.0011, p.y * 0.0040));
            n = n * 0.75 + fbmTex(vec2(p.x * 0.0038, p.y * 0.0012)) * 0.25;
            float cm = pow(smoothstep(0.42, 0.86, n), 1.7) * 0.42;
            cm *= horizF * (1.0 - rainStrength * 0.6);
            vec3 cc = getLightColor() * 0.40 + getAmbientColor() * 0.90;
            col = cc;
            alpha = cm;
        }
    }
    #endif

    // --- Altocumulus, ~320 blocks: regular mackerel pattern ---
    #ifdef CLOUD_LAYER_ALTO
    {
        float ta = (CLOUD_ALTO_HEIGHT - camPos.y) / dir.y;
        if (ta > 0.0) {
            vec2 p = camPos.xz + dir.xz * ta + t * vec2(11.0, 4.0) * VC_WIND;
            float n = vnoiseTex(p * 0.0050) * 0.55 + vnoiseTex(p * 0.016) * 0.30 + vnoiseTex(p * 0.045) * 0.15;
            float sunLow = clamp(1.0 - sunDir.y * 2.0, 0.3, 1.6);
            vec2 so = normalize(sunDir.xz + vec2(1e-4)) * (22.0 * sunLow);
            float n2 = vnoiseTex((p + so) * 0.0050) * 0.55 + vnoiseTex((p + so) * 0.016) * 0.30 + vnoiseTex((p + so) * 0.045) * 0.15;
            float shade = clamp((n - n2) * 6.0 + 0.5, 0.0, 1.0);
            float am = smoothstep(0.54, 0.74, n) * 0.55;
            am *= horizF * (1.0 - rainStrength * 0.4);
            vec3 bright = getLightColor() * 0.35 + getAmbientColor() * 0.85;
            vec3 dark   = getAmbientColor() * 0.55;
            vec3 ac = mix(bright, dark, shade * 0.6) * CLOUD_ALTO_BRIGHT; // CONFIG_CLOUDS layer-2 dial

            col = mix(col, ac, am);
            alpha = alpha + am * (1.0 - alpha);
        }
    }
    #endif

    return vec4(col, clamp(alpha, 0.0, 1.0));
}
#endif
