/*
    VL_HALF_RES march (composite.fsh, lower-left quadrant, v120-safe).

    ADAPTED VERBATIM from the composite1.fsh VOLUMETRIC_LIGHT march (the
    VL_HALF_RES=0 reference path): any tuning there MUST be mirrored here.
    Differences vs the reference, and nothing else:
      1. Runs per HALF-RES sample (uvF = represented full-res pixel center),
         one march per 2x2 pixel block; composite1 upsamples bilaterally.
      2. wMirror (water-branch mirror dominance of the RECEIVING pixel) is
         unknown in this pass. The only wMirror-dependent term in the whole
         march is the submerged step density  uw * 0.008 * (1 - wMirror*0.85),
         so the integral is carried at BOTH endpoints:
           Hi = wMirror 0 (full submerged coefficient kUW)
           Lo = wMirror 1 (0.15 residual, floor of the receiver weight)
         plus odUW = sum(kUW * dt) (unweighted submerged optical depth).
         composite1 recombines: transF = transHi * exp(0.85*wMirror*odUW)
         (EXACT: equals transAir * exp(-odUW*(1-0.85*wMirror))) and
         scat = mix(scatHi, scatLo, wMirror) (exact at both endpoints; the
         validated sunset mirror sits at wMirror ~0.9-0.98, next to Lo).
      3. The VL_BLOCKLIGHT term (additive on final color, wMirror/transF
         independent) is folded into BOTH stored scats; the VL_HALF_RES=0
         apply in composite1 is gated off accordingly.
    Outputs: outA = vec4(scatHi, transHi), outB = vec4(scatLo, odUW)
    -> colortex13 / colortex14 (RGBA16F, quadrant texels only).

    Requires (included before this file by composite.fsh): settings, common,
    distort, lighting (shadow samplers/matrices, cloudShadow, getLightColor),
    water (causticShaftMask / causticSmall / waveCausticFull), the local
    toView()/toViewDH() helpers and the depthtex1/far/isEyeInWater/
    eyeBrightnessSmooth uniforms.
*/
#if defined VOLUMETRIC_LIGHT && VL_HALF_RES == 1

void vlHalfResMarch(vec2 uvF, out vec4 outA, out vec4 outB) {
    // Per-sample context (mirror of the composite1 main() trunk). The ray
    // direction through a pixel is depth-independent: unproject at z = 1.
    vec3 viewDir = normalize(toView(uvF, 1.0));
    vec3 worldDir = mat3(gbufferModelViewInverse) * viewDir;
    vec3 lightW = normalize(mat3(gbufferModelViewInverse) * shadowLightPosition);
    vec3 gLightC = getLightColor();
    vec3 gAmbC = getAmbientColor();
#if BLUE_NOISE == 1
    // Same jitter family as the reference march (blue noise, TAA-averaged)
    float vlDither = blueDither(gl_FragCoord.xy, frameCounter);
#else
    float vlDither = bayer8(gl_FragCoord.xy);
#endif
    // Screen-probe submerged-glass detection is disabled pack-wide (see
    // composite1): constant false keeps the copied blocks compiled.
    bool inGlassUW = false;

    // The march runs to OPAQUE terrain (depthtex1), not to glass or the water
    // surface: shafts and fog therefore cross transparent surfaces.
    float d1v = texture2D(depthtex1, uvF).r;
#ifdef DISTANT_HORIZONS
    // DH terrain: bound by the LOD (dhDepthTex1) when depthtex1 is empty.
    float dhD1v = texture2D(dhDepthTex1, uvF).r;
    float distVol = (d1v < 1.0) ? length(toView(uvF, d1v))
                  : ((dhD1v < 1.0) ? length(toViewDH(uvF, dhD1v)) : far);
#else
    float distVol = (d1v >= 1.0) ? far : length(toView(uvF, d1v));
#endif
    // F5 GHOST fix (translucent skins): bound the march by the VISIBLE surface
    // (depthtex0) for the entity flag band ONLY -- water (0.95+) and glass
    // stay traversed. Mirror of the reference march; keep bands in sync.
    {
        float d0vF5 = texture2D(depthtex0, uvF).r;
        if (d0vF5 < 1.0 && d0vF5 < d1v - 1e-6) {
            float matBvF5 = texture2D(colortex2, uvF).b;
            if (matBvF5 > 0.01 && matBvF5 < 0.30) distVol = min(distVol, length(toView(uvF, d0vF5)));
        }
    }
    float marchEnd = min(distVol, (isEyeInWater == 1) ? UW_RAY_DISTANCE : 160.0);
    float dt = marchEnd / float(VL_SAMPLES);
    vec3 stepV = viewDir * dt;
    // Init at (dither-1): the loop increments BEFORE sampling -> positions
    // dt*(i+dither) in [0..marchEnd[ (see the reference march for why).
    vec3 p = stepV * (vlDither - 1.0);
    #ifdef VOLUMETRIC_CLOUDS
    #ifdef VC_GROUND_SHADOWS
    // 2 cloud-shadow probes interpolated along the ray (reference pattern)
    float csA = cloudShadow(cameraPosition + worldDir * (marchEnd * 0.25), lightW);
    float csB = cloudShadow(cameraPosition + worldDir * (marchEnd * 0.75), lightW);
    #endif
    #endif
    // Dual-endpoint accumulators (see file header)
    vec3 scatHi = vec3(0.0);
    vec3 scatLo = vec3(0.0);
    float transHi = 1.0;
    float transLo = 1.0;
    float odUW = 0.0;
    vec3 blockFog = vec3(0.0);

    float duskF = clamp(1.0 - abs(sunHeight()) * 4.0, 0.0, 1.0) * smoothstep(-0.14, -0.04, sunHeight()); // asymmetric: keeps it off at night
    float eyeSky = float(eyeBrightnessSmooth.y) / 240.0;
    // Sky exposure of the EYE: gates every sky/dusk chroma applied below.
    float skyExpo = smoothstep(0.02, 0.40, eyeSky);
    // Nether fog is legitimately warm (same heuristic as the sky pass).
    if (fogColor.r > fogColor.g * 1.4) skyExpo = 1.0;
    float hfDens = 0.0;
    #ifdef HEIGHT_FOG
        hfDens = HEIGHT_FOG_DENSITY * (0.004 + duskF * 0.009 + rainStrength * 0.012)
               * (0.35 + 0.65 * eyeSky)
               * (1.0 + duskF * 0.35 * ATMO_FOG_DUSK_BOOST); // height fog reinforced at dusk
    #endif
    float airDens = 0.0008 * VL_STRENGTH;

    float cosT = dot(viewDir, normalize(shadowLightPosition));
    float gP = (isEyeInWater == 1) ? 0.58 : 0.60;
    // The 0.86 CLAMP IS MANDATORY: the HG phase degenerates (NaN/inf) as g -> 1.
    if (isEyeInWater == 0) gP = min(gP + 0.10 * duskF * VL_DUSK_BOOST, 0.86);
    float phase = (1.0 - gP * gP) / (12.566 * pow(1.0 + gP * gP - 2.0 * gP * cosT, 1.5));
    vec3 lightC = gLightC;
    lightC = max(lightC, vec3(0.10, 0.15, 0.26) * smoothstep(0.04, 0.30, lightHeight()));
    vec3 ambC = gAmbC;
    #ifdef FOG_BIOME_TINT
    // Biome tint - AIR ONLY (the UW path applies UW_BIOME_TINT itself).
    if (isEyeInWater == 0 && !inGlassUW && luma(fogColor) > 0.001) {
        vec3 fbCh = toLinear(fogColor) / max(luma(toLinear(fogColor)), 0.04);
        fbCh = min(fbCh, vec3(2.0));
        ambC *= mix(vec3(1.0), fbCh, FOG_BIOME_TINT_STRENGTH);
    }
    #endif
    // AIR PATH ONLY: underground, fade the fog chroma to a neutral cool gray.
    if (isEyeInWater == 0 && !inGlassUW) {
        vec3 coolN = vec3(0.85, 0.95, 1.10);
        lightC = mix(vec3(luma(lightC)) * coolN, lightC, skyExpo);
        ambC   = mix(vec3(luma(ambC))   * coolN, ambC,   skyExpo);
    }
    if (isEyeInWater == 1 || inGlassUW) {
        // Saturated blue-green tint (glass slightly dimmed vs direct water)
        float uwMult = (isEyeInWater == 1) ? 1.05 : 0.85;
        vec3 uwLightTint = vec3(0.32, 0.68, 0.92);
        vec3 uwAmbTint   = vec3(0.08, 0.28, 0.48);
        #ifdef UW_BIOME_TINT
          if (luma(fogColor) > 0.001) {
            vec3 fbN = toLinear(fogColor) / max(luma(toLinear(fogColor)), 0.04);
            float s = UW_BIOME_TINT_STRENGTH;
            uwLightTint = mix(uwLightTint, uwLightTint * fbN, s);
            uwAmbTint   = mix(uwAmbTint,   uwAmbTint   * fbN, s);
          }
        #endif
        lightC *= uwLightTint * uwMult;
        ambC   *= uwAmbTint;
    }
    // Directional shafts only exist when the star is up; window widened at dusk.
    float lightUp = smoothstep(-0.04, 0.14, lightHeight());
    lightUp = max(lightUp, smoothstep(-0.12, -0.02, lightHeight()) * 0.55);

    for (int i = 0; i < VL_SAMPLES; i++) {
        p += stepV;
        vec3 wp = mat3(gbufferModelViewInverse) * p + cameraPosition;

        // Submerged segment: either the eye IS in water, or this ray point is
        // BEHIND a water surface seen from outside (screen projection check).
        float uw = (isEyeInWater == 1) ? 1.0 : 0.0;
        // World Y of the water surface actually crossed by this march step.
        float uwSurfY = WATER_LEVEL;
        // Screen projection + colortex2 tap SHARED with the lava halo.
        vec2 puvS = vec2(-1.0);
        float matBS = 0.0;
        bool onScrS = false;
        if (isEyeInWater == 0) {
            vec4 pc = gbufferProjection * vec4(p, 1.0);
            puvS = ((pc.xy / pc.w) * 0.5 + 0.5) * TAAU_S; // TAAU: shared projection (VL water + lava halo)
            onScrS = (puvS.x > 0.0 && puvS.x < TAAU_S && puvS.y > 0.0 && puvS.y < TAAU_S);
            if (onScrS) matBS = texture2D(colortex2, puvS).b;
            vec2 puv = puvS;
            if (onScrS) {
                float matB = matBS;
                bool isWater = matB > 0.95;
                // Submerged-glass water proxy disabled (see reference march).
                bool isGlass = false;
                if (isWater || isGlass) {
                    vec3 wsv = toView(puv, texture2D(depthtex0, puv).r);
                    if (p.z < wsv.z) {
                        uw = isWater ? 1.0 : 0.55;
#if PIXEL_WATER_FIX == 1
                        // Real Y of the seen water surface replaces Y63 for
                        // god rays viewed from outside (no extra cost).
                        if (isWater) uwSurfY = (mat3(gbufferModelViewInverse) * wsv + cameraPosition).y;
#endif
                    }
                }
                // Submerged glass: areas below water level keep the shafts
                if (inGlassUW && uw < 0.5 && wp.y < WATER_LEVEL - 0.2) uw = 0.60;
            }
        }
        // Density split (VL_HALF_RES): densBase = every wMirror-INDEPENDENT
        // term, kUW = the submerged coefficient the receiver scales by
        // (1 - wMirror*0.85). MUST stay in sync with the VL_HALF_RES=0 term
        //   uw * 0.008 * (1.0 - wMirror * 0.85)
        // and with the wMirror export in the composite1 water branch.
        float densBase;
        float kUW = 0.0;
        if (isEyeInWater == 1 || (inGlassUW && uw > 0.4)) {
            densBase = 0.026 * WATER_FOG_DENSITY * UW_DENSITY_BOOST;
            #ifdef UW_VOLUMETRICS
#if PIXEL_WATER_FIX == 1
            // Estimated local surface >= submerged eye (high-lake fix)
            float uwY = clamp((max(WATER_LEVEL, cameraPosition.y + 0.4) - wp.y) / 16.0, 0.0, 1.0);
#else
            float uwY = clamp((WATER_LEVEL - wp.y) / 16.0, 0.0, 1.0);
#endif
            densBase *= (0.6 + 0.9 * uwY);
            #endif
            // A march step ABOVE the surface returns to AIR density.
            float subVL = smoothstep(0.0, 0.4, max(WATER_LEVEL, cameraPosition.y + 0.4) - wp.y);
            densBase = mix(airDens, densBase, subVL);
        } else {
            // PATCHY fog: irregular drifting banks
            float patchy = 0.45 + 1.10 * vnoiseTex(wp.xz * 0.012 + frameTimeCounter * 0.015);
            densBase = airDens + hfDens * exp(-max(wp.y - WATER_LEVEL, 0.0) * 0.025 / HEIGHT_FOG_FALLOFF) * patchy;
            kUW = uw * 0.008;
            // Mist sheet just above the water, at dawn and dusk
            #ifdef SEALEVEL_MIST
            densBase += exp(-abs(wp.y - (WATER_LEVEL + 1.3)) * 0.55) * duskF * 0.012 * HEIGHT_FOG_DENSITY
                      * (0.3 + 0.7 * vnoiseTex(wp.xz * 0.025 + frameTimeCounter * 0.02));
            #endif
            // Precipitation: downward-animated rain filaments in the shafts
            #ifdef VL_RAIN
            if (rainStrength > 0.01) {
                vec3 rp3 = vec3(wp.x * 3.5 + wp.z * 3.5,
                                wp.y * 0.05 - frameTimeCounter * 5.0,
                                wp.x * 2.1 - wp.z * 2.1);
                float rn = vnoise3(rp3);
                densBase += rainStrength * smoothstep(0.80, 0.94, rn) * 0.0028;
            }
            #endif
        }

        // Sun visibility (cast shadows in the haze, stained glass colors)
        vec3 vis = vec3(1.0);
        vec3 sc = toShadowClip(p, vec3(0.0));
        float f = getDistortFactor(sc.xy);
        vec3 sp = distortShadow(sc, f) * 0.5 + 0.5;
        if (sp.x > 0.0 && sp.x < 1.0 && sp.y > 0.0 && sp.y < 1.0) {
            float zb = sp.z - 0.0008 * (VLX_SHADOW_ZSCALE * 2.0); // constant world-space bias
            float s1 = step(zb, texture2D(shadowtex1, sp.xy).r);
            float s0 = step(zb, texture2D(shadowtex0, sp.xy).r);
            vec3 c = vec3(1.0);
#if SHADOW_TEX1_GATE_FIX == 1
            // RGB tag bands: mirror of the receiver in lighting.glsl and of the
            // reference march; encoding documented in shadow.fsh.
            if (s0 < s1) {
                vec4 scG = texture2D(shadowcolor0, sp.xy);
                float tagMxG = max(max(scG.r, scG.g), scG.b);
                if (tagMxG < 0.325)      c = clamp(scG.rgb / 0.30, 0.0, 1.0);
                else if (tagMxG < 0.675) c = clamp((scG.rgb - 0.35) / 0.30, 0.0, 1.0);
                else s1 = 0.0;
            }
#else
            if (s0 < s1) c = texture2D(shadowcolor0, sp.xy).rgb;
#endif
            vis = vec3(s1) * mix(c, vec3(1.0), s0);
            // Beyond the shadow frustum Z range: fail-open.
            vis = mix(vis, vec3(1.0), step(1.0, zb));
        }

        // Clouds above shade the haze (sky/ground consistency)
        #ifdef VOLUMETRIC_CLOUDS
        #ifdef VC_GROUND_SHADOWS
            vis *= mix(csA, csB, clamp((float(i) + 0.5) / float(VL_SAMPLES), 0.0, 1.0));
        #endif
        #endif

        // Underwater ambient is near zero (directional shafts); eye outside,
        // the water column DIFFUSES sky light in blue-green (reference notes).
        float eyeDry = (isEyeInWater == 1 || inGlassUW) ? 0.0 : 1.0;
        float ambM = (uw > 0.25) ? mix(0.008, 0.15, eyeDry) : 0.22;
        vec3 ambTint = (uw > 0.25 && eyeDry > 0.5) ? vec3(0.42, 0.75, 0.80) : vec3(1.0);
        vec3 S = lightC * vis * phase * lightUp + ambC * ambM * ambTint;
        // Caustic god-ray striations, computed PER MARCH STEP (uw > 0.25 gate,
        // NOT isEyeInWater: submerged steps of a bank ray get them too).
        #ifdef UW_VOLUMETRICS
        if (uw > 0.25 && lightW.y > 0.05) {
#if UW_SURFACE_CLAMP == 1
            // Anti-starburst (a): LOCAL surface level; the caustic term only
            // applies to the REALLY submerged segment (subW).
#if PIXEL_WATER_FIX == 1
            float wLev = (isEyeInWater == 1) ? max(WATER_LEVEL, cameraPosition.y + 0.4) : uwSurfY;
#else
            float wLev = (isEyeInWater == 1) ? max(WATER_LEVEL, cameraPosition.y + 0.4) : WATER_LEVEL;
#endif
            float toSurf = max(wLev - wp.y, 0.0);
            // 0.8 window: softer ENTER/EXIT fade right at the surface.
            float subW = smoothstep(0.0, 0.8, wLev - wp.y);
#else
            float toSurf = max(WATER_LEVEL - wp.y, 0.0);
            float subW = 1.0;
#endif
            // PER-STEP projection along the sun/moon axis: columns parallel to
            // lightW by construction.
            vec2 cprj = wp.xz + lightW.xz / max(lightW.y, 0.15) * toSurf;
#if UW_GODRAY_CAUSTIC_LOCK == 1
            float beerR = exp(-toSurf * 0.06); // extended range
#else
            float beerR = exp(-toSurf * 0.07); // legacy LOCK=0 extinction
#endif
            // Phase and tint DEDICATED to the striation term -- declared HERE,
            // in the COMMON trunk. #if trap: used after the LOCK #endif, so
            // they must never be declared inside a gated block.
            float phaseShaft = phase;
            vec3 teinteShaft = vec3(0.80, 1.0, 0.94);
#if UW_GODRAY_CAUSTIC_LOCK == 1
            // The shaft pattern IS the projected caustic (fine striations x
            // wide sheaves); see the reference march for the full rationale.
            float caWide = causticShaftMask(cprj, frameTimeCounter, toSurf);
            float shrpU = clamp(1.0 - toSurf / 45.0, 0.20, 1.0);
            float caFine = causticSmall(cprj, frameTimeCounter, shrpU);
            // Subtract the flat base (0.42): REAL penumbra between striations.
            float fineM = clamp((caFine - 0.42) * 1.45, 0.0, 1.5);
            float caOpen = fineM * (0.38 + 0.62 * caWide);
            float gmaD = mix(1.30, 1.10, clamp(toSurf * 0.045, 0.0, 1.0));
            float caShaft = clamp(0.02 + 2.9 * pow(clamp(caOpen, 0.0, 1.3), gmaD), 0.0, 3.0);
            float caDir = mix(0.22, caShaft, UW_GODRAY_DIRECTIONAL);
            // Isotropic floor for the STRIATION TERM ONLY (readable facing away).
            phaseShaft = mix(phase, 0.30, 0.35);
            // Golden surface tint cooling to cyan-green at depth.
            vec3 shChroma = min(gLightC / max(luma(gLightC), 1e-3), vec3(2.0));
            teinteShaft = mix(vec3(0.80, 1.0, 0.94), shChroma,
                              0.45 * clamp(1.0 - toSurf / 20.0, 0.0, 1.0));
#else
            vec2 caF = waveCausticFull(cprj, frameTimeCounter, toSurf);
            float caLarge = caF.y;
            float caSmall = clamp(caF.x - 0.20, 0.0, 1.0);
            float caM = clamp((0.35 + 0.65 * caLarge) * (0.4 + 1.1 * caSmall), 0.0, 1.5);
            // Light blades ALIGNED on the sun/moon axis (degeneracy-guarded).
            vec3 Ldir = normalize(lightW);
            vec3 upRef = (abs(Ldir.y) > 0.95) ? vec3(1.0, 0.0, 0.0) : vec3(0.0, 1.0, 0.0);
            vec3 baxA = normalize(cross(Ldir, upRef));
            vec3 baxB = cross(Ldir, baxA);
            vec3 rel = wp - cameraPosition;
            float sa = dot(rel, baxA);
            float sb = dot(rel, baxB);
            float shaftDir = 0.55 + 0.45 * sin(sa * UW_GODRAY_TILT
                           + sin(sb * UW_GODRAY_TILT * 0.7) * 1.3
                           + frameTimeCounter * 0.25);
            float caDir = caM * mix(1.0, shaftDir, UW_GODRAY_DIRECTIONAL);
#endif
#if UW_SUNGAZE_SOFT == 1
            // Anti-starburst (b): fade the caustic modulation near the axis.
            float gazeW = smoothstep(0.955, 0.9985, cosT);
            caDir = mix(caDir, 0.55, gazeW * 0.75);
#endif
            // God-ray term ONLY, with the eye-distance render limit fade.
            float shaftFade = 1.0 - smoothstep(UW_SHAFT_RENDER_DIST * 0.7, UW_SHAFT_RENDER_DIST, length(p));
            S += lightC * vis * phaseShaft * lightUp * caDir * beerR * 0.9 * UW_GODRAY_STRENGTH * subW
               * teinteShaft * shaftFade;
        }
        #endif
        // Dual-endpoint integration (see file header). kUW = 0 (eye in water /
        // pure air step) keeps both endpoints identical.
        float aHi = exp(-(densBase + kUW) * dt);
        float aLo = exp(-(densBase + kUW * 0.15) * dt);
        scatHi += transHi * S * (1.0 - aHi);
        scatLo += transLo * S * (1.0 - aLo);
        transHi *= aHi;
        transLo *= aLo;
        odUW += kUW * dt;
        // Slowest-decaying endpoint below 0.5% -> the rest of the march is
        // invisible for every wMirror (Hi decays even faster).
        if (transLo < 0.005) break;

        // Thermal halo of incandescent blocks (lava, fire, torches)
        #ifdef LAVA_ANIMATE
        // reuses the shared projection/tap from the top of the step (puvS/matBS)
        if (isEyeInWater == 0 && onScrS) {
            vec2 lvuv = puvS;
            {
                float matB = matBS;
                // Strict LAVA band around 0.58 (see reference march)
                if (matB > 0.55 && matB < 0.62) {
                    float lvd = texture2D(depthtex0, lvuv).r;
                    if (lvd < 1.0) {
                        float emDist = length(p - toView(lvuv, lvd));
                        vec3 lavaS = vec3(0.92, 0.36, 0.08) * exp(-emDist * 0.60)
                                   * 0.009 * LAVA_GLOW;
                        scatHi += lavaS * transHi;
                        scatLo += lavaS * transLo;
                    }
                }
            }
        }
        #endif

        #if defined VL_BLOCKLIGHT && (defined SS_LIGHTS || defined RTGI)
            vec4 fc = gbufferProjection * vec4(p, 1.0);
            vec2 fuv = ((fc.xy / fc.w) * 0.5 + 0.5) * TAAU_S; // TAAU: NDC -> sub-rect UV
            if (fuv.x > 0.0 && fuv.x < TAAU_S && fuv.y > 0.0 && fuv.y < TAAU_S) {
                float fd = texture2D(depthtex0, fuv).r;
                if (fd >= 1.0 || p.z > toView(fuv, fd).z) {
#if GI_HALF_RES == 1
                    // Half-res GI quadrant layout (1-frame-old colortex4 here:
                    // this pass writes it; fine for a temporally-smoothed fog).
                    blockFog += texture2D(colortex4,
                        min(fuv * 0.5, 0.5 * TAAU_S - 1.5 / vec2(viewWidth, viewHeight))).rgb;
#else
                    blockFog += texture2D(colortex4, fuv).rgb;
#endif
                }
            }
        #endif
    }

    // VL_BLOCKLIGHT fold: additive on final color and wMirror/transF
    // independent -> baked into BOTH stored endpoints (mix() preserves it
    // exactly). The VL_HALF_RES=0 apply in composite1 is gated off.
    #if defined VL_BLOCKLIGHT && (defined SS_LIGHTS || defined RTGI)
        blockFog /= float(VL_SAMPLES);
        vec3 bfAdd = blockFog * (1.0 - exp(-marchEnd * 0.04)) * 0.30 * VL_BLOCK_STRENGTH;
        scatHi += bfAdd;
        scatLo += bfAdd;
    #endif

    outA = vec4(scatHi, transHi);
    outB = vec4(scatLo, odUW);
}

#endif
