/////////////////////////////////////
// Unbound Visual + PT v9.6 by EminGT //
/////////////////////////////////////

//Common//
#include "/lib/common.glsl"

//////////Fragment Shader//////////Fragment Shader//////////Fragment Shader//////////
#ifdef FRAGMENT_SHADER

noperspective in vec2 texCoord;

flat in vec3 upVec, sunVec, eastVec;

#if defined LIGHTSHAFTS_ACTIVE && (LIGHTSHAFT_BEHAVIOUR == 1 && SHADOW_QUALITY >= 1 || defined END)
    flat in float vlFactor;
#endif

//Pipeline Constants//
const bool colortex0MipmapEnabled = true;

#if defined CUSTOM_GI_VOXEL_INTERNAL && defined OVERWORLD
    uniform sampler3D gi_voxel_sampler;
    uniform sampler3D gi_radiance_a_sampler;
    uniform sampler3D gi_radiance_b_sampler;
    #if GI_EMISSIVE_BLOCKS == 1
        // Needed here (in addition to prepare.glsl, which injects it into
        // the radiance grid) so GiSampleEmissiveDirect's MIS-style light
        // search can query emissive voxels directly - see giFloodfill.glsl.
        uniform sampler3D gi_emission_sampler;
    #endif
    #define GI_NEED_READ_SAMPLERS
    #include "/lib/voxelization/giFloodfill.glsl"
    #include "/lib/misc/reprojection.glsl"
#endif

//Common Variables//
float SdotU = dot(sunVec, upVec);
float sunFactor = SdotU < 0.0 ? clamp(SdotU + 0.375, 0.0, 0.75) / 0.75 : clamp(SdotU + 0.03125, 0.0, 0.0625) / 0.0625;
float sunVisibility = clamp(SdotU + 0.0625, 0.0, 0.125) / 0.125;
float sunVisibility2 = sunVisibility * sunVisibility;
float shadowTimeVar1 = abs(sunVisibility - 0.5) * 2.0;
float shadowTimeVar2 = shadowTimeVar1 * shadowTimeVar1;
float shadowTime = shadowTimeVar2 * shadowTimeVar2;
float farMinusNear = far - near;

vec2 view = vec2(viewWidth, viewHeight);

#ifdef OVERWORLD
    vec3 lightVec = sunVec * ((timeAngle < 0.5325 || timeAngle > 0.9675) ? 1.0 : -1.0);
#else
    vec3 lightVec = sunVec;
#endif

#if defined LIGHTSHAFTS_ACTIVE && (LIGHTSHAFT_BEHAVIOUR == 1 && SHADOW_QUALITY >= 1 || defined END)
#else
    float vlFactor = 0.0;
#endif

#ifdef IRIS_FEATURE_FADE_VARIABLE
    float chunkFade = 1.0;
#endif

//Common Functions//
float GetLinearDepth(float depth) {
    return (2.0 * near) / (far + near - depth * farMinusNear);
}

float GetLinearDepth(float depth, float far, float near) {
    return (2.0 * near) / (far + near - depth * (far - near));
}

#if SSAO_QUALI > 0 || SSGI > 0
    vec2 OffsetDist(float x, int s) {
        float n = fract(x * 1.414) * 3.1415;
        return pow2(vec2(cos(n), sin(n)) * x / s);
    }
#endif

#if SSAO_QUALI > 0
    float GetAmbientOcclusion(sampler2D depthtex, float z0, float linearZ0, float dither, float farM, float nearM, float aoWorldRange) {
        if (z0 < 0.56) return 1.0;
        float ao = 0.0;

        #if SSAO_QUALI == 1
            int samples = 2;
            float scm = 0.3;
        #elif SSAO_QUALI == 2
            int samples = 4;
            float scm = 0.4;
        #elif SSAO_QUALI == 3
            int samples = 12;
            float scm = 0.6;
        #endif

        #define SSAO_I_FACTOR 0.004

        float sampleDepth = 0.0, angle = 0.0, dist = 0.0;
        float fovScale = gbufferProjection[1][1];
        float distScale = max((farM - nearM) * linearZ0 + near, 3.0);
        vec2 scale = vec2(scm / aspectRatio, scm) * fovScale / distScale;

        for (int i = 1; i <= samples; i++) {
            vec2 offset = OffsetDist(i + dither, samples) * scale;
            if (i % 2 == 0) offset.y = -offset.y;

            vec2 coord1 = texCoord + offset;
            vec2 coord2 = texCoord - offset;

            sampleDepth = GetLinearDepth(texture2D(depthtex, coord1).r, farM, nearM);
            float aosample = aoWorldRange * (linearZ0 - sampleDepth) * 2.0;
            angle = clamp(0.5 - aosample, 0.0, 1.0);
            dist = clamp(0.5 * aosample - 1.0, 0.0, 1.0);

            sampleDepth = GetLinearDepth(texture2D(depthtex, coord2).r, farM, nearM);
            aosample = aoWorldRange * (linearZ0 - sampleDepth) * 2.0;
            angle += clamp(0.5 - aosample, 0.0, 1.0);
            dist += clamp(0.5 * aosample - 1.0, 0.0, 1.0);

            ao += clamp(angle + dist, 0.0, 1.0);
        }
        ao /= samples;

        #define SSAO_IM SSAO_I * SSAO_I_FACTOR
        return pow(ao, SSAO_IM);
    }
#endif

#if SSGI > 0
    // Manual population count (bit count), used instead of the GLSL
    // built-in bitCount() so this doesn't depend on the exact GLSL
    // version Iris ends up compiling this pack with. Standard
    // parallel bit-counting trick (SWAR), 32-bit.
    int GiPopCount(uint x) {
        x = x - ((x >> 1u) & 0x55555555u);
        x = (x & 0x33333333u) + ((x >> 2u) & 0x33333333u);
        x = (x + (x >> 4u)) & 0x0F0F0F0Fu;
        return int((x * 0x01010101u) >> 24u);
    }

    // Visibility Bitmask Screen-Space GI, based on:
    // O. Therrien, Y. Levesque, G. Gilet, "Screen Space Indirect Lighting
    // with Visibility Bitmask", The Visual Computer, 2023
    // (arXiv:2301.11376, DOI 10.1007/s00371-022-02703-y).
    //
    // Replaces the old SSR-style cosine-hemisphere ray-march GI above
    // (which is exactly the "SSR-like tracing" the paper's own related-
    // work section and Figure 16 identify as the noisy alternative to
    // this technique - most rays either miss and contribute nothing, or
    // hit and contribute their full unweighted sample, so the result is
    // effectively binary per-sample and needs heavy denoising to clean up).
    //
    // Core idea (paper section 3, Algorithm 1): instead of the usual
    // horizon-based approach of keeping just the two furthest occluder
    // angles per side of a slice (which implicitly treats the depth
    // buffer as a solid height-field - light can never pass behind
    // anything, however thin), keep a 32-bit BITMASK of Nb angular
    // sectors around the hemisphere slice. Each depth-buffer sample
    // covers a small angular RANGE (front angle to back angle, using an
    // assumed constant surface thickness t) instead of pushing a single
    // running max/min angle, and sets exactly those bits. A later sample
    // can only contribute GI through sectors that are still unset - i.e.
    // light can still reach around/behind a thin occluder through
    // whichever angular sectors it didn't actually cover, instead of the
    // entire direction going dark the instant anything in front of it is
    // hit. bitCount()/bitwise-AND make "how many NEW sectors did this
    // sample just uncover" an O(1) check, so this stays roughly the same
    // cost as a normal horizon search (paper Table 1: ~0.02ms overhead).
    //
    // NOTE: this is a from-scratch GLSL port of the paper's algorithm
    // adapted to this engine's existing view-space GI plumbing (same
    // function signature/call site as the old sampler, same colortex0
    // light buffer, same depthtex0). It hasn't been visually verified
    // against a live render on my end - the math follows Algorithm 1 as
    // closely as I could translate it, but if it looks wrong in-game
    // (too dark/too leaky/banding) let me know exactly what's off and
    // I'll correct the specific constant (GI_BITMASK_THICKNESS is the
    // single most impactful one per the paper's own Figure 7 comparison).
    const uint  GI_BITMASK_SECTORS   = 32u;  // Nb - the paper found 32 crosses the threshold where banding becomes near-invisible, and fits one uint (section 4.1)
    const int   GI_BITMASK_SLICES    = 2;    // Nd - kept low since this runs every frame; the pack's existing temporal/spatial denoiser downstream covers the rest, same as the old sampler did
    const int   GI_BITMASK_STEPS     = 6;    // Ns per side
    const float GI_BITMASK_THICKNESS = 0.2;  // t (view-space blocks) - the paper explicitly recommends a SMALL constant (section 3.1: over-occlusion around thin objects is much more visually objectionable than a light leak behind a thick one)
    const float PI_H = 1.57079632679; // pi/2

    vec3 GetScreenSpaceGI(vec3 viewPos0, vec3 viewNormal, float dither) {
        // Same branchless orthonormal basis as the old sampler (Duff et
        // al.) - tangent/bitangent span the surface's local plane, used
        // here as the slice-rotation axes instead of hemisphere sample
        // directions.
        float dsign = viewNormal.z >= 0.0 ? 1.0 : -1.0;
        float da = -1.0 / (dsign + viewNormal.z);
        float db = viewNormal.x * viewNormal.y * da;
        vec3 tangent   = vec3(1.0 + dsign * viewNormal.x * viewNormal.x * da, dsign * db, -dsign * viewNormal.x);
        vec3 bitangent = vec3(db, dsign + viewNormal.y * viewNormal.y * da, -viewNormal.y);

        float linearZ0 = length(viewPos0);
        // World-space slice radius, growing a little with distance so far
        // surfaces still get a geometrically meaningful search (same idea
        // as the old sampler's stepDist scaling).
        float radius = clamp(linearZ0 * 0.25, 0.5, 3.0);
        float stepSize = radius / float(GI_BITMASK_STEPS + 1);

        vec3 giColor = vec3(0.0);

        for (int slice = 0; slice < GI_BITMASK_SLICES; slice++) {
            // Random per-pixel/per-frame slice rotation around the
            // surface normal (Algorithm 1 line 7, "directions with random
            // offset") - denoised downstream by the pack's existing
            // temporal/spatial filter, same as the old sampler relied on.
            float sliceAngle = (float(slice) / float(GI_BITMASK_SLICES) + dither) * 3.14159265;
            vec3 sliceDir = tangent * cos(sliceAngle) + bitangent * sin(sliceAngle);

            // One shared 32-sector bitmask per slice, covering the FULL
            // hemisphere arc (both sides of the pixel along this azimuth,
            // matching Figure 3/5 in the paper) - side 0 owns the lower
            // half of the bitmask, side 1 the upper half, so a sample on
            // one side can never accidentally clear/skip the other side's
            // bits.
            uint bitmask = 0u;

            for (int side = 0; side < 2; side++) {
                vec3 dir2D = (side == 0) ? sliceDir : -sliceDir;

                for (int j = 1; j <= GI_BITMASK_STEPS; j++) {
                    // Jittered stepping (paper section 5 / Figure 14:
                    // evenly-spaced steps cause a visible banding pattern;
                    // jittering along the sampling direction masks it).
                    float dist = stepSize * float(j) * (0.85 + 0.3 * dither);
                    vec3 frontSamplePos = viewPos0 + dir2D * dist;

                    vec4 clip = gbufferProjection * vec4(frontSamplePos, 1.0);
                    if (clip.w <= 0.0) break; // behind camera
                    clip.xyz /= clip.w;
                    vec2 screenPos = clip.xy * 0.5 + 0.5;
                    if (screenPos.x < 0.0 || screenPos.x > 1.0 || screenPos.y < 0.0 || screenPos.y > 1.0) break;

                    float sampleDepth = texture2D(depthtex0, screenPos).r;
                    if (sampleDepth >= 1.0 || sampleDepth < 0.56) continue; // sky/hand

                    // Real view-space position the depth buffer reports at
                    // this screen location - this is "sf" (front sample)
                    // in Algorithm 1, found by projecting along the slice
                    // direction and reading back the ACTUAL surface there,
                    // rather than trusting the projected position itself.
                    vec4 iProjDiag = vec4(gbufferProjectionInverse[0].x,
                                          gbufferProjectionInverse[1].y,
                                          gbufferProjectionInverse[2].zw);
                    vec3 ndc = vec3(screenPos, sampleDepth) * 2.0 - 1.0;
                    vec4 sfH = iProjDiag * ndc.xyzz + gbufferProjectionInverse[3];
                    vec3 sf = sfH.xyz / sfH.w;

                    // Back sample (Algorithm 1 line 15): pushed toward the
                    // shaded point by the assumed constant thickness t.
                    vec3 sb = sf - normalize(sf - viewPos0) * GI_BITMASK_THICKNESS;

                    // Elevation angles of front/back samples above this
                    // slice's local horizon (the tangent plane at p,
                    // oriented by the surface normal) - Algorithm 1 line
                    // 16, converted from cosine space to angular space via
                    // acos (paper section 3.1's explicit note that this
                    // conversion is required for correct angular spacing).
                    vec3 toFront = sf - viewPos0;
                    vec3 toBack  = sb - viewPos0;
                    float thetaF = PI_H - acos(clamp(dot(normalize(toFront), viewNormal), -1.0, 1.0));
                    float thetaB = PI_H - acos(clamp(dot(normalize(toBack),  viewNormal), -1.0, 1.0));

                    float thetaMin = min(thetaF, thetaB);
                    float thetaMax = max(thetaF, thetaB);

                    // Map this sample's covered angle range [thetaMin,
                    // thetaMax] (each spans [-PI/2, +PI/2] on its own
                    // side) into this slice's shared Nb-sector bitmask
                    // (Algorithm 1 line 18) - side 0 uses the lower half
                    // of the bitmask directly, side 1 mirrors into the
                    // upper half so both sides of one slice coexist in
                    // one bitmask without overwriting each other.
                    float halfNb = float(GI_BITMASK_SECTORS) * 0.5;
                    int a, b;
                    if (side == 0) {
                        a = int(floor((thetaMin + PI_H) / 3.14159265 * halfNb));
                        b = int(ceil ((thetaMax + PI_H) / 3.14159265 * halfNb));
                    } else {
                        int rawA = int(floor((thetaMin + PI_H) / 3.14159265 * halfNb));
                        int rawB = int(ceil ((thetaMax + PI_H) / 3.14159265 * halfNb));
                        a = int(GI_BITMASK_SECTORS) - 1 - rawB;
                        b = int(GI_BITMASK_SECTORS) - 1 - rawA;
                    }
                    a = clamp(a, 0, int(GI_BITMASK_SECTORS) - 1);
                    b = clamp(b, 0, int(GI_BITMASK_SECTORS) - 1);

                    uint sampleMask = 0u;
                    for (int k = a; k <= b; k++) sampleMask |= (1u << uint(k));

                    // Algorithm 1 line 23: only NEWLY-uncovered sectors get
                    // to contribute light - this is the actual mechanism
                    // that lets light pass behind a thin occluder instead
                    // of the whole direction going dark the instant
                    // anything is hit (the paper's headline result,
                    // compare Figures 6/7/11).
                    uint newBits = sampleMask & ~bitmask;
                    if (newBits != 0u) {
                        vec3 sampleColor = texelFetch(colortex0, ivec2(screenPos * view), 0).rgb;
                        float sampleLum = GetLuminance(sampleColor);
                        if (sampleLum > 0.01) {
                            float nDotL = max(0.0, dot(viewNormal, normalize(toFront)));
                            float weight = float(GiPopCount(newBits)) / float(GI_BITMASK_SECTORS);
                            giColor += sampleColor * nDotL * weight;
                        }
                    }
                    bitmask |= sampleMask;
                }
            }
        }

        return giColor / float(GI_BITMASK_SLICES);
    }

    // --- Old SSR-style cosine-hemisphere ray-march GI, kept for
    // reference / as an easy fallback: if the bitmask version above turns
    // out to have an issue in-game, renaming this back to
    // GetScreenSpaceGI (and the one above to something else) restores the
    // previous behaviour immediately without needing a full re-edit. ---
    vec3 GetScreenSpaceGI_OldSSRStyle(vec3 viewPos0, vec3 viewNormal, float dither) {

        vec3 giColor = vec3(0.0);

        // Orthonormal basis around the surface normal, to map 2D hemisphere
        // samples into view space.
        // FIX (pasal "proyeksi bounce ke sudut, bukan ke atas"): the old
        // hard switch (abs(viewNormal.z) < 0.999 ? ... : ...) picks a
        // reference axis to build the tangent frame from, and that pick
        // becomes numerically unstable exactly as viewNormal approaches
        // whichever axis the branch is testing - which is precisely what
        // happens for an UP-facing surface (a floor) viewed close to
        // straight-down, since in VIEW space that normal ends up pointing
        // almost exactly along +-Z, right at the 0.999 threshold. Near that
        // threshold, cross(upHint, viewNormal) is a tiny, noise-dominated
        // vector, and normalizing it snaps the whole tangent/bitangent
        // frame toward whatever direction rounding error favoured - which
        // is what showed up as bounce samples clustering toward one block
        // corner/edge instead of spreading evenly "upward" off the surface.
        // Side-facing walls never approach that axis, so they never hit the
        // unstable zone - matching "kanan kiri dan depan sudah pas".
        // Replaced with Duff et al.'s branchless orthonormal basis
        // construction: mathematically continuous for EVERY possible
        // normal direction, no threshold, no unstable zone, no branch.
        float dsign = viewNormal.z >= 0.0 ? 1.0 : -1.0;
        float da = -1.0 / (dsign + viewNormal.z);
        float db = viewNormal.x * viewNormal.y * da;
        vec3 tangent   = vec3(1.0 + dsign * viewNormal.x * viewNormal.x * da, dsign * db, -dsign * viewNormal.x);
        vec3 bitangent = vec3(db, dsign + viewNormal.y * viewNormal.y * da, -viewNormal.y);

        float currentDist = length(viewPos0);
        // World-scale march step: a fraction of a block near the camera,
        // growing with distance so far-away surfaces still get a
        // geometrically meaningful search radius instead of a sub-pixel one.
        float stepDist = clamp(currentDist * 0.12, 0.05, 1.5);

        // FIX (pasal "render lambat"): 3 hit-test steps per sample was more
        // than needed once sample count itself was already cut (see
        // SSGI_SAMPLES) - 2 short steps is enough for a plausible nearby
        // hit at this march distance, and the temporal/spatial denoiser
        // downstream covers the rest.
        const int SSGI_RAY_STEPS = 2;

        for (int i = 1; i <= SSGI_SAMPLES; i++) {
            // R2 low-discrepancy 2D sequence (Martin Roberts), reseeded by
            // the per-pixel/per-frame `dither` so the pattern still rotates
            // every frame like the old sampling did (denoised by TAA / the
            // shared temporal filter downstream).
            float u1 = fract(dither + float(i) * 0.7548776662);
            float u2 = fract(dither * 1.3247179572 + float(i) * 0.5698402910);

            float r = sqrt(u1);
            float phi = 6.28318530718 * u2;
            vec3 sampleDir = tangent * (r * cos(phi)) + bitangent * (r * sin(phi))
                            + viewNormal * sqrt(max(0.0, 1.0 - u1));

            bool hit = false;
            vec2 hitCoord = vec2(0.0);
            float hitDist = 0.0;

            for (int s = 1; s <= SSGI_RAY_STEPS; s++) {
                vec3 rayPos = viewPos0 + sampleDir * (stepDist * float(s));

                vec4 rayClip = gbufferProjection * vec4(rayPos, 1.0);
                if (rayClip.w <= 0.0) break; // behind camera
                rayClip.xyz /= rayClip.w;
                vec2 rayScreen = rayClip.xy * 0.5 + 0.5;
                if (rayScreen.x < 0.0 || rayScreen.x > 1.0 || rayScreen.y < 0.0 || rayScreen.y > 1.0) break;

                float sampleDepth = texture2D(depthtex0, rayScreen).r;
                if (sampleDepth >= 1.0 || sampleDepth < 0.56) continue; // sky/hand

                float surfaceLinearZ = GetLinearDepth(sampleDepth);
                float rayLinearZ = length(rayPos);

                // Hit test: count it as a hit once the ray has reached (or
                // just passed) the depth buffer's real surface at this
                // screen coord, within a thickness tolerance - the same
                // kind of test SSR uses to find where a ray intersects
                // on-screen geometry, applied here to find a plausible
                // bounce source instead of a reflection.
                float thickness = max(0.15, surfaceLinearZ * 0.05);
                if (rayLinearZ >= surfaceLinearZ - thickness && rayLinearZ <= surfaceLinearZ + thickness * 3.0) {
                    hit = true;
                    hitCoord = rayScreen;
                    hitDist = length(rayPos - viewPos0);
                    break;
                }
            }

            if (!hit) continue;

            vec3 sampleColor = texelFetch(colortex0, ivec2(hitCoord * view), 0).rgb;
            float sampleLum = GetLuminance(sampleColor);
            if (sampleLum < 0.01) continue; // only lit surfaces plausibly bounce light back

            float atten = 1.0 / (1.0 + hitDist * hitDist);
            giColor += sampleColor * atten;
        }

        // Proper Monte Carlo average over ALL samples cast (not just hits):
        // a hemisphere direction that found no nearby surface within the
        // short march (e.g. it points at open air/sky) correctly
        // contributes zero, the same way it would in a real GI integral,
        // rather than being excluded from the average and inflating the
        // result.
        return giColor / float(SSGI_SAMPLES);
    }
#endif

#if RTAO > 0
    // Screen-space Ray-Traced Ambient Occlusion (RTAO).
    //
    // Reuses the same Visibility Bitmask horizon-tracing technique as
    // GetScreenSpaceGI above (O. Therrien, Y. Levesque, G. Gilet, "Screen
    // Space Indirect Lighting with Visibility Bitmask", The Visual
    // Computer, 2023, arXiv:2301.11376), but AO-only: no color sampling,
    // just the leftover UNCOVERED sector count at the end of each slice
    // converted into a visibility fraction. This replaces the old
    // depth-difference GetAmbientOcclusion() above with an actual
    // per-direction horizon search against the depth buffer - it
    // correctly lets visibility pass behind thin occluders and holds up
    // at grazing angles far better than a flat depth-diff test.
    //
    // Kept fully self-contained (own bit-count helper, own constants)
    // instead of reusing GetScreenSpaceGI's, so RTAO works independently
    // of whether SSGI is also enabled - the two are separate toggles and
    // either one can compile without the other.
    int RtaoPopCount(uint x) {
        x = x - ((x >> 1u) & 0x55555555u);
        x = (x & 0x33333333u) + ((x >> 2u) & 0x33333333u);
        x = (x + (x >> 4u)) & 0x0F0F0F0Fu;
        return int((x * 0x01010101u) >> 24u);
    }

    const uint  RTAO_SECTORS   = 32u;  // Nb, same as the paper's recommendation for GI above
    #if RTAO == 1
        const int RTAO_SLICES = 2;
        const int RTAO_STEPS  = 4;
    #elif RTAO == 2
        const int RTAO_SLICES = 3;
        const int RTAO_STEPS  = 6;
    #else
        const int RTAO_SLICES = 4;
        const int RTAO_STEPS  = 8;
    #endif
    const float RTAO_THICKNESS = 0.2; // t (view-space blocks) - same reasoning as GI_BITMASK_THICKNESS: a small constant avoids over-occluding thin objects
    const float RTAO_PI_H = 1.57079632679; // pi/2

    // aoRadius: view-space search radius in blocks. Kept as a parameter
    // (rather than a fixed const like the GI version) so the call site can
    // scale it with distance falloff if wanted.
    float GetRTAO(vec3 viewPos0, vec3 viewNormal, float dither, float aoRadius) {
        float dsign = viewNormal.z >= 0.0 ? 1.0 : -1.0;
        float da = -1.0 / (dsign + viewNormal.z);
        float db = viewNormal.x * viewNormal.y * da;
        vec3 tangent   = vec3(1.0 + dsign * viewNormal.x * viewNormal.x * da, dsign * db, -dsign * viewNormal.x);
        vec3 bitangent = vec3(db, dsign + viewNormal.y * viewNormal.y * da, -viewNormal.y);

        float stepSize = aoRadius / float(RTAO_STEPS + 1);
        float visibility = 0.0;

        for (int slice = 0; slice < RTAO_SLICES; slice++) {
            float sliceAngle = (float(slice) / float(RTAO_SLICES) + dither) * 3.14159265;
            vec3 sliceDir = tangent * cos(sliceAngle) + bitangent * sin(sliceAngle);

            uint bitmask = 0u;

            for (int side = 0; side < 2; side++) {
                vec3 dir2D = (side == 0) ? sliceDir : -sliceDir;

                for (int j = 1; j <= RTAO_STEPS; j++) {
                    float dist = stepSize * float(j) * (0.85 + 0.3 * dither);
                    vec3 frontSamplePos = viewPos0 + dir2D * dist;

                    vec4 clip = gbufferProjection * vec4(frontSamplePos, 1.0);
                    if (clip.w <= 0.0) break; // behind camera
                    clip.xyz /= clip.w;
                    vec2 screenPos = clip.xy * 0.5 + 0.5;
                    if (screenPos.x < 0.0 || screenPos.x > 1.0 || screenPos.y < 0.0 || screenPos.y > 1.0) break;

                    float sampleDepth = texture2D(depthtex0, screenPos).r;
                    if (sampleDepth >= 1.0 || sampleDepth < 0.56) continue; // sky/hand

                    vec4 iProjDiag = vec4(gbufferProjectionInverse[0].x,
                                          gbufferProjectionInverse[1].y,
                                          gbufferProjectionInverse[2].zw);
                    vec3 ndc = vec3(screenPos, sampleDepth) * 2.0 - 1.0;
                    vec4 sfH = iProjDiag * ndc.xyzz + gbufferProjectionInverse[3];
                    vec3 sf = sfH.xyz / sfH.w;

                    vec3 sb = sf - normalize(sf - viewPos0) * RTAO_THICKNESS;

                    vec3 toFront = sf - viewPos0;
                    vec3 toBack  = sb - viewPos0;
                    float thetaF = RTAO_PI_H - acos(clamp(dot(normalize(toFront), viewNormal), -1.0, 1.0));
                    float thetaB = RTAO_PI_H - acos(clamp(dot(normalize(toBack),  viewNormal), -1.0, 1.0));

                    float thetaMin = min(thetaF, thetaB);
                    float thetaMax = max(thetaF, thetaB);

                    float halfNb = float(RTAO_SECTORS) * 0.5;
                    int a, b;
                    if (side == 0) {
                        a = int(floor((thetaMin + RTAO_PI_H) / 3.14159265 * halfNb));
                        b = int(ceil ((thetaMax + RTAO_PI_H) / 3.14159265 * halfNb));
                    } else {
                        int rawA = int(floor((thetaMin + RTAO_PI_H) / 3.14159265 * halfNb));
                        int rawB = int(ceil ((thetaMax + RTAO_PI_H) / 3.14159265 * halfNb));
                        a = int(RTAO_SECTORS) - 1 - rawB;
                        b = int(RTAO_SECTORS) - 1 - rawA;
                    }
                    a = clamp(a, 0, int(RTAO_SECTORS) - 1);
                    b = clamp(b, 0, int(RTAO_SECTORS) - 1);

                    uint sampleMask = 0u;
                    for (int k = a; k <= b; k++) sampleMask |= (1u << uint(k));

                    bitmask |= sampleMask;
                }
            }

            // Fraction of sectors still UNCOVERED after tracing both sides
            // of this slice = how much of the hemisphere is open sky/air
            // in this direction, i.e. visibility. 1.0 = fully open,
            // 0.0 = fully enclosed.
            visibility += float(RTAO_SECTORS - uint(RtaoPopCount(bitmask))) / float(RTAO_SECTORS);
        }

        visibility /= float(RTAO_SLICES);
        return pow(clamp(visibility, 0.0, 1.0), RTAO_INTENSITY * 0.01);
    }
#endif


#if defined DISTANT_HORIZONS || defined VOXY
    float GetLinearDepth(float depth, mat4 invProjMatrix) {
        depth = depth * 2.0 - 1.0;
        vec2 zw = depth * invProjMatrix[2].zw + invProjMatrix[3].zw;
        return -zw.x / zw.y;
    }

    float GetLODShadows(vec3 viewPos, vec3 nViewPos, sampler2D depthtex, mat4 projection, mat4 projectionInverse, float dither) {
        #if defined OVERWORLD || defined END
            float shadow = 1.0;
            vec3 tracePos = viewPos.xyz;
            vec3 traceStep = normalize(lightVec) * 2.5;
            
            #ifdef TAA
                tracePos += traceStep * (fract(dither + frameCounter * 0.618) + 0.2);
            #else
                tracePos += traceStep * (dither + 0.2);
            #endif

            float traceZ = 0.0;
            float zDelta = 0.0;

            #ifdef VOXY
                vec3 texture6 = texelFetch(colortex6, texelCoord, 0).rgb;
                int materialMaskInt = int(texture6.g * 255.1);

                if (materialMaskInt != 253) // Reduced Edge TAA (Leaves)
                    tracePos -= nViewPos * 1.5; // Tweak to imitate shadow bias
            #endif

            for (int i = 0; i < 32; i++) {
                vec4 pos = projection * vec4(tracePos.xyz, 1.0);
                pos = pos / pos.w * 0.5 + 0.5;

                if (pos.x < 0.0 || pos.x > 1.0 || pos.y < 0.0 || pos.y > 1.0) break;

                #ifdef VOXY
                    traceZ = texture2D(depthtex0, pos.xy).r;
                    
                    if (traceZ < 1.0) {
                        zDelta = -tracePos.z - GetLinearDepth(traceZ, gbufferProjectionInverse);
                    } else
                #endif
                {
                    traceZ = texture2D(depthtex, pos.xy).r;
                    zDelta = -tracePos.z - GetLinearDepth(traceZ, projectionInverse);
                }

                shadow *= 1.0 - smoothstep(0.0, 0.01, zDelta) * smoothstep(5.0, 4.0, zDelta);
                tracePos += traceStep;
            }

            return shadow;
        #else
            return 1.0;
        #endif
    }
#endif

//Includes//
#include "/lib/util/spaceConversion.glsl"
#include "/lib/util/dither.glsl"
#include "/lib/atmospherics/fog/mainFog.glsl"
#include "/lib/colors/skyColors.glsl"
#include "/lib/colors/lightAndAmbientColors.glsl"

#if AURORA_STYLE > 0
    #include "/lib/atmospherics/auroraBorealis.glsl"
#endif

#if NIGHT_NEBULAE == 1
    #include "/lib/atmospherics/nightNebula.glsl"
#endif

#ifdef VL_CLOUDS_ACTIVE
    #include "/lib/atmospherics/clouds/mainClouds.glsl"
#endif

#ifdef END
    #include "/lib/atmospherics/enderStars.glsl"
    #ifdef ENDER_LIGHTNING
        #include "/lib/atmospherics/enderLightning.glsl"
    #endif
#endif

#ifdef WORLD_OUTLINE
    #include "/lib/misc/worldOutline.glsl"
#endif

#ifdef DARK_OUTLINE
    #include "/lib/misc/darkOutline.glsl"
#endif

#ifdef ATM_COLOR_MULTS
    #include "/lib/colors/colorMultipliers.glsl"
#endif
#ifdef MOON_PHASE_INF_ATMOSPHERE
    #include "/lib/colors/moonPhaseInfluence.glsl"
#endif

#ifdef DISTANT_LIGHT_BOKEH
    #include "/lib/misc/distantLightBokeh.glsl"
#endif

//Program//
void main() {
    vec4 color = vec4(texelFetch(colortex0, texelCoord, 0).rgb, 1.0);
    float z0 = texelFetch(depthtex0, texelCoord, 0).r;

    vec4 screenPos = vec4(texCoord, z0, 1.0);
    vec4 viewPos = gbufferProjectionInverse * (screenPos * 2.0 - 1.0);
    viewPos /= viewPos.w;
    float lViewPos = length(viewPos);
    vec3 nViewPos = normalize(viewPos.xyz);
    vec3 playerPos = ViewToPlayer(viewPos.xyz);

    float dither = texture2DLod(noisetex, texCoord * vec2(viewWidth, viewHeight) / 128.0, 0.0).b;
    #ifdef TAA
        dither = fract(dither + goldenRatio * mod(float(frameCounter), 3600.0));
    #endif

    #ifdef ATM_COLOR_MULTS
        atmColorMult = GetAtmColorMult();
        sqrtAtmColorMult = sqrt(atmColorMult);
    #endif

    #if defined VOXY && SHADOW_QUALITY > -1
        float lodShadow = 0.0;
    #endif

    float VdotU = dot(nViewPos, upVec);
    float VdotS = dot(nViewPos, sunVec);
    float skyFade = 0.0;
    vec3 waterRefColor = vec3(0.0);
    vec3 auroraBorealis = vec3(0.0);
    vec3 nightNebula = vec3(0.0);
    #if defined CUSTOM_GI_VOXEL_INTERNAL && defined OVERWORLD
        vec4 giHistoryOut = vec4(0.0);
    #endif

    vec3 normalM = vec3(0);
    float fresnelM = 0.0;

    if (z0 < 1.0) {
        #ifdef DISTANT_LIGHT_BOKEH
            int dlbo = 1;
            vec3 dlbColor = color.rgb;
            dlbColor += texelFetch(colortex0, texelCoord + ivec2( 0, dlbo), 0).rgb;
            dlbColor += texelFetch(colortex0, texelCoord + ivec2( 0,-dlbo), 0).rgb;
            dlbColor += texelFetch(colortex0, texelCoord + ivec2( dlbo, 0), 0).rgb;
            dlbColor += texelFetch(colortex0, texelCoord + ivec2(-dlbo, 0), 0).rgb;
            dlbColor = max(color.rgb, dlbColor * 0.2);
            float dlbMix = GetDistantLightBokehMix(lViewPos);
            color.rgb = mix(color.rgb, dlbColor, dlbMix);
        #endif

        #if SSAO_QUALI > 0 || defined WORLD_OUTLINE || SSGI > 0
            float linearZ0 = GetLinearDepth(z0);
        #endif

        #if SSAO_QUALI > 0
            float ssao = GetAmbientOcclusion(depthtex0, z0, linearZ0, dither, far, near, far - near);
        #else
            float ssao = 1.0;
        #endif

        // --- RTAO (Ray-Traced Ambient Occlusion, Visibility Bitmask) ---
        // Horizon-traced AO against the actual depth buffer, see GetRTAO
        // above. Independent toggle from SSAO_QUALI - when RTAO is on it
        // multiplies into ssao the same way Contact AO does below, so all
        // three occlusion sources (broad SSAO, horizon-traced RTAO, tight
        // Contact AO) layer instead of one replacing another.
        #if RTAO > 0
            // Distance cutoff (FPS safety valve): the per-pixel horizon
            // trace in GetRTAO() is the most expensive AO term here (up to
            // RTAO_SLICES * 2 sides * RTAO_STEPS depth-buffer fetches PLUS
            // the projection math per fetch). A large fraction of a typical
            // frame - distant terrain, mountains, anything near the fog/
            // render-distance edge - is far enough that this extra detail
            // is invisible on screen anyway, so tracing it there is pure
            // wasted GPU time. Skipping it past RTAO_MAX_DISTANCE and
            // falling back to the already-computed cheap depth-diff SSAO
            // (or 1.0 if SSAO_QUALI is off) recovers most of RTAO's cost
            // with no visible quality loss, which is what makes it safe to
            // turn on at lower profiles instead of ULTRA/CINEMATIC only.
            // Faded (not a hard cut) over the last quarter of the range so
            // there's no visible pop as things cross the cutoff.
            // RTAO_MAX_DISTANCE is defined in lib/common.glsl.
            if (z0 < 1.0 && z0 >= 0.56 && linearZ0 < RTAO_MAX_DISTANCE) {
                vec3 rtaoNormal;
                #ifdef PBR_REFLECTIONS
                    vec3 rtaoTex4 = texelFetch(colortex4, texelCoord, 0).rgb;
                    rtaoNormal = normalize(rtaoTex4);
                #else
                    rtaoNormal = normalize(cross(dFdx(playerPos), dFdy(playerPos)));
                #endif
                // Same grazing-angle bend fix used for giNormal/caoGiNormal -
                // keeps the search hemisphere on the correct, camera-facing
                // side of the surface instead of mirroring past it.
                {
                    vec3 rtaoViewDir = normalize(playerPos);
                    float rtaoNdotV = dot(rtaoNormal, rtaoViewDir);
                    if (rtaoNdotV > 0.0) {
                        rtaoNormal = normalize(rtaoNormal - rtaoViewDir * (rtaoNdotV + 1e-3));
                    }
                }
                vec3 viewNormalForRTAO = normalize(mat3(gbufferModelView) * rtaoNormal);
                float rtaoRadius = clamp(linearZ0 * RTAO_RADIUS * 0.25, 0.25, RTAO_RADIUS);
                float rtaoDistFade = 1.0 - smoothstep(RTAO_MAX_DISTANCE * 0.75, RTAO_MAX_DISTANCE, linearZ0);
                float rtaoResult = GetRTAO(viewPos.xyz, viewNormalForRTAO, dither, rtaoRadius);
                ssao *= mix(1.0, rtaoResult, rtaoDistFade);
            }
        #endif

        // --- Contact AO (Lumen-style per-pixel occlusion) ---
        // Adds fine-grain ambient occlusion that the coarse voxel grid
        // can't provide. Traces short hemisphere rays in screen space
        // and checks depth buffer hits. Combined with SSAO for layered
        // occlusion: SSAO provides broad ambient darkening, Contact AO
        // adds tight contact shadows at geometric detail level.
        #if CONTACT_AO_ENABLED == 1 && defined CUSTOM_GI_VOXEL_INTERNAL && defined OVERWORLD
            float contactAO = 1.0;
            if (z0 < 1.0 && z0 >= 0.56) {
                // Need giNormal for AO hemisphere orientation
                vec3 caoGiNormal;
                #ifdef PBR_REFLECTIONS
                    vec3 caoTex4 = texelFetch(colortex4, texelCoord, 0).rgb;
                    caoGiNormal = normalize(caoTex4);
                #else
                    caoGiNormal = normalize(cross(dFdx(playerPos), dFdy(playerPos)));
                #endif
                // Same grazing-angle bend fix as giNormal above, instead of
                // a full mirror-flip - keeps the AO hemisphere on the
                // correct side of the surface at grazing angles.
                {
                    vec3 caoViewDir = normalize(playerPos);
                    float caoNdotV = dot(caoGiNormal, caoViewDir);
                    if (caoNdotV > 0.0) {
                        caoGiNormal = normalize(caoGiNormal - caoViewDir * (caoNdotV + 1e-3));
                    }
                }
                contactAO = IC_ContactAO(depthtex0, texCoord, caoGiNormal, dither);
            }
            ssao *= contactAO;
        #endif

        vec3 texture6 = texelFetch(colortex6, texelCoord, 0).rgb;
        bool entityOrParticle = z0 < 0.56;
        int materialMaskInt = int(texture6.g * 255.1);
        float intenseFresnel = 0.0;
        float smoothnessD = texture6.r;
        vec3 reflectColor = vec3(1.0);

        #include "/lib/materials/materialHandling/deferredMaterials.glsl"

        #ifdef PBR_REFLECTIONS
            vec3 texture4 = texelFetch(colortex4, texelCoord, 0).rgb;
            normalM = mat3(gbufferModelView) * texture4;
            float fresnel = clamp(1.0 + dot(normalM, nViewPos), 0.0, 1.0);
        #else
            float fresnel = 0.0;
        #endif

        // Shared GI surface normal (world/player-space, corrected to always
        // face the camera side) - used by BOTH the screen-space GI below
        // and the voxel GI further down, so the two techniques agree on
        // which way the surface actually faces instead of each deriving
        // their own estimate. Prefers the real geometric normal buffer
        // over a screen-space derivative when it's already available (the
        // derivative normal is flat per-triangle/per-quad and gets
        // visibly faceted on curved/angled surfaces).
        //
        // FIX ("light bounce keliatan miring/salah arah, nggak nyebar ke
        // semua arah"): texture4 (PBR_REFLECTIONS path) is the FULL
        // normal-mapped shading normal (tbnMatrix * normal-map texel), not
        // the flat geometric one - on bumpy/detailed textures viewed at a
        // grazing angle it can legitimately end up pointing slightly away
        // from the camera even though the real surface still faces it.
        // The old code handled that by doing `giNormal = -giNormal`, which
        // MIRRORS the whole vector to the opposite side instead of nudging
        // it back - for a pixel that failed the test, the cosine-hemisphere
        // basis below then got built around a normal pointing roughly INTO
        // the surface rather than out of it. That's exactly what reads as
        // bounce rays going in a "miring"/wrong direction and hemisphere
        // rays failing to spread evenly (many immediately re-enter solid
        // voxels via GiTraceRay's self-intersection guard instead of
        // fanning out over the true hemisphere). Bending the normal back
        // just far enough to be tangent to the view direction (instead of
        // flipping past it) keeps it on the correct, camera-facing side
        // while preserving as much of the original bump direction as
        // possible - the standard fix for grazing-angle normal-map
        // artifacts.
        #if SSGI > 0 || (defined CUSTOM_GI_VOXEL_INTERNAL && defined OVERWORLD)
            #ifdef PBR_REFLECTIONS
                vec3 giNormal = normalize(texture4);
            #else
                vec3 giNormal = normalize(cross(dFdx(playerPos), dFdy(playerPos)));
            #endif
            {
                vec3 giViewDir = normalize(playerPos);
                float giNdotV = dot(giNormal, giViewDir);
                if (giNdotV > 0.0) {
                    // Bend towards grazing (tangent to view dir) instead of
                    // mirroring past it.
                    giNormal = normalize(giNormal - giViewDir * (giNdotV + 1e-3));
                }
            }
        #endif

        #ifdef WORLD_OUTLINE
            #ifndef WORLD_OUTLINE_ON_ENTITIES
                if (!entityOrParticle)
            #endif
            DoWorldOutline(color.rgb, linearZ0, playerPos, fresnel, dither);
        #endif

        #ifdef DARK_OUTLINE
            DoDarkOutline(color.rgb, z0);
        #endif

        color.rgb *= ssao;

        vec3 giScreenSpace = vec3(0.0);
        #if SSGI > 0
            if (!entityOrParticle) {
                // Conservative blend: bounce light biased towards
                // occluded/shaded areas (ssao<1) where real bounce light
                // would matter most. Not added to color yet - either fed
                // into the voxel GI's shared temporal denoiser below (see
                // "Hybrid GI combine") or, if voxel GI is off, added
                // directly further down.
                vec3 viewNormalForSSGI = normalize(mat3(gbufferModelView) * giNormal);
                giScreenSpace = GetScreenSpaceGI(viewPos.xyz, viewNormalForSSGI, dither) * (SSGI_INTENSITY * 0.01) * mix(1.0, 0.5, ssao);
            }
        #endif

        #ifdef PBR_REFLECTIONS
            #if WORLD_SPACE_REFLECTIONS_INTERNAL == -1 && !defined END
                // Way steeper fresnel falloff on SSR-only mode to hide SSR limitation and gain performance
                float fresnelFactor = (1.0 - smoothnessD) * 0.7;
                fresnelM = max(fresnel - fresnelFactor, 0.0) / (1.0 - fresnelFactor);
            #else
                fresnelM = fresnel;
            #endif

            // ---- BRDF: real Schlick-Fresnel reflectance (F0 + (1-F0) * (1-cosTheta)^5) ----
            // "fresnel" above is already the (1-cosTheta) grazing-angle term (0 = looking
            // straight at the surface, 1 = perfectly grazing/edge-on view). The previous
            // version boosted it linearly (fresnel*0.7+0.3) then used pow2(), which has
            // the wrong curve shape: it ramps up reflectivity too early across mid-range
            // viewing angles instead of staying near the base reflectance and only
            // spiking near true grazing angles like a real Fresnel term does. Using the
            // correct exponent (pow5) instead keeps the same reflectivity at dead-on and
            // grazing angles (so the overall look/exposure doesn't shift) but gives the
            // correct "rim brightening" falloff in between - most noticeable on water,
            // wet stone and glass, where reflections should stay subtle head-on and
            // flare up sharply only right near the horizon/edge.
            float schlickTerm = pow5(clamp(fresnelM, 0.0, 1.0));
            const float F0_DIELECTRIC = 0.3; // matches this pack's previous baseline reflectance so overall brightness is unchanged
            const float F0_METAL = 0.75;     // metals stay highly (and fairly flatly) reflective at all angles, tinted via reflectColor
            float fresnelDielectric = F0_DIELECTRIC + (1.0 - F0_DIELECTRIC) * schlickTerm;
            float fresnelMetal = F0_METAL + (1.0 - F0_METAL) * schlickTerm;
            fresnelM = mix(fresnelDielectric, fresnelMetal, intenseFresnel);
            fresnelM = fresnelM * sqrt1(smoothnessD) - dither * 0.01;
        #endif

        #ifdef IRIS_FEATURE_FADE_VARIABLE
            chunkFade = texture6.b > 0.50001 ? (1.0 - texture6.b) * 2.0 : 1.0;
        #endif

        waterRefColor = color.rgb;

        vec3 giVoxel = vec3(0.0);
        #if defined CUSTOM_GI_VOXEL_INTERNAL && defined OVERWORLD
            {
                // ============================================================
                //  Irradiance Cache GI (Lumen-Style)
                // ============================================================
                //  Replaces stochastic path tracing with probe-based caching:
                //   1. WSRC (World Cache): existing voxel radiance grid,
                //      propagated by prepare passes. Whole-scene coverage.
                //   2. SSRC (Screen Cache): built per-frame at 1/IC_SCREEN_SCALE
                //      resolution by tracing rays into the WSRC. Fine detail.
                //   3. Each pixel interpolates from both caches (distance-weighted).
                //   4. Temporal blending stabilizes across frames.
                //   5. Contact AO adds per-pixel occlusion detail.
                // Nudge the sample point off the surface along its own
                // normal before converting to voxel space ("siang hari
                // light bounce ke tanah kebanyakan/aneh" fix). Without this,
                // giSampleScenePos sits exactly ON the lit surface, so
                // GiSceneToVoxel(playerPos) + the blurred trilinear sample
                // below lands mostly (up to the center tap's 0.28 weight)
                // back on the SAME solid voxel that prepare.glsl just wrote
                // this surface's own fresh direct-sun term into - i.e. the
                // ground re-samples a big chunk of its own direct light as
                // if it were "bounced".
                // ("posisi light bounce kurang ke bawah, dibanding
                // ForkReimagined" fix): offset ini dulunya ~1.5 voxel. Untuk
                // permukaan LANTAI (normal menghadap ke atas), itu artinya
                // titik baca GI digeser 1.5 BLOK KE ATAS dari lantai
                // sebelum disample - jadi cahaya yang tampil di lantai
                // sebenarnya "dipinjam" dari titik 1.5 blok di udara di
                // atasnya, bukan persis di permukaan lantai itu sendiri,
                // yang kerasa seperti bounce tidak turun sampai ke bawah.
                // Diturunkan ke 0.85 voxel - masih cukup untuk melompat ke
                // voxel tetangga (menghindari bug self-sampling di atas),
                // tapi titik baca jauh lebih dekat ke permukaan asli, jadi
                // posisi bounce terasa lebih menempel ke lantai/dinding yang
                // sebenarnya.
                // ("posisi meleset & bocor lagi setelah GI_VOXEL_SIZE
                // dinaikkan ke 1.0" fix, dibandingkan dengan Derivative):
                // 0.85 di atas itu SATUAN VOXEL, dikonversi ke satuan dunia
                // lewat GI_VOXEL_SIZE. Waktu di-tuning ke 0.85, GI_VOXEL_SIZE
                // masih 0.5-0.65 (jadi offset dunia sebenarnya cuma ~0.43-0.55
                // blok). Begitu GI_VOXEL_SIZE dinaikkan ke 1.0 (fix radius/
                // glitch sebelumnya), offset ini OTOMATIS ikut naik jadi 0.85
                // BLOK PENUH - hampir 2x lipat dari yang sudah pas - dan itu
                // yang bikin bounce kerasa ngambang/meleset dari permukaan
                // lagi, DAN bisa nembus tembok tipis 1 blok (0.85 + jitter
                // ~0.15 = hampir 1.0 blok, cukup buat kelewat ke sisi lain
                // tembok, dibaca sebagai "bocor"). Derivative (VXGI_ShadowTrace.glsl)
                // pakai offset fix 0.51 VOXEL dengan grid yang SELALU 1 voxel
                // = 1 blok (tidak ada opsi ubah ukuran voxel), jadi offset
                // dunianya nggak pernah "kebawa naik" seperti di sini. Karena
                // GI_VOXEL_SIZE kita sekarang juga sudah 1.0 (basis yang sama
                // persis), 0.85 diturunkan ke 0.51 supaya balik ke jarak yang
                // aman (masih cukup lompat ke voxel tetangga - GiTraceRay
                // punya escape-guard cadangan kalau titik masih di dalam solid)
                // tanpa nembus tembok tipis atau ngambang jauh dari permukaan.
                // ("posisi light bounce melenceng dari target block" fix):
                // dinaikkan sedikit dari 0.51 -> 0.60 voxel. 0.51 kadang
                // masih terlalu dekat ke permukaan sehingga sample GI ikut
                // "menyerempet" voxel tetangga yang salah di sudut/tepi
                // block (bounce terlihat menempel di block sebelahnya,
                // bukan block yang sebenarnya dituju). 0.60 masih jauh di
                // bawah 0.85 (nilai yang pernah terbukti bikin bounce
                // "ngambang"/lepas dari permukaan), jadi titik sample lebih
                // akurat menempel ke block targetnya tanpa membawa balik
                // masalah lama.
                vec3 giSampleScenePos = playerPos + giNormal * (GI_VOXEL_SIZE * 0.51);

                // --- IC Interpolate: blend WSRC + SSRC --------------------
                // The IC_SampleWorldCacheSmooth function trilinearly samples
                // the voxel radiance grid with tangent-plane blur (noise-free).
                // For SSRC, we trace rays into the WSRC at reduced resolution
                // and bilinearly interpolate for full-res pixels.
                vec3 voxelCoord = GiSceneToVoxel(giSampleScenePos);
                // Sub-voxel jitter: when voxelCoord falls exactly on an integer
                // (block boundary), trilinear sampling hits the hard edge between
                // two voxels with very different radiance - this shows as a
                // "garis dobel" / doubled line in the bounce. A small world-space
                // stable offset breaks this alignment so trilinear always blends
                // smoothly across the boundary. World-anchored (no frameCounter)
                // so it's stable across frames and TAA-compatible.
                {
                    vec3 js = giSampleScenePos + cameraPosition;
                    float jx = GiHash22(js.xz * 1.73 + js.y * 4.11).x - 0.5;
                    float jy = GiHash22(js.xz * 6.37 + js.y * 2.89).x - 0.5;
                    float jz = GiHash22(js.xz * 3.91 + js.y * 5.67).x - 0.5;
                    voxelCoord += vec3(jx, jy, jz) * 0.15;
                }
                vec3 giSample;

                // WSRC sample (always available, smooth, whole-scene)
                vec3 wsrcSample = GiSampleRadianceBlurred(
                    #if (GI_BOUNCE_PASSES - 1) % 2 == 0
                        gi_radiance_b_sampler,
                    #else
                        gi_radiance_a_sampler,
                    #endif
                    voxelCoord, giNormal);

                // Room-aware ray-traced bounce: cast GI_RAYS_PER_PIXEL rays
                // across the WHOLE hemisphere above the surface (DDA through
                // the voxel grid, stops at walls) so bounce spreads in all
                // directions through the room and the per-voxel "bulet-bulet"
                // blob pattern disappears. The seed is pixel-stable (no
                // frameCounter) so standing still gives an identical result
                // every frame - no "gerak-gerak / flicker". Blended with the
                // smooth WSRC sample (GI_RAY_BLEND) so broad outdoor bounce
                // is kept.
                // ("bulet-bulet gerak-gerak" fix): seed HARUS dikunci ke posisi
                // DUNIA (world-space absolute), bukan ke gl_FragCoord (posisi
                // pixel di layar). Kalau dikunci ke layar, satu titik fisik di
                // tembok jatuh di pixel yang beda tiap kali kamera gerak/muter,
                // jadi ray yang ditembak ke titik itu ikut berubah acak tiap
                // frame -> noise-nya "berenang" mengikuti kamera, bukan diam
                // menempel di permukaan (ini yang terlihat sebagai blob gerak
                // baik dari jauh maupun dekat, indoor maupun outdoor). Dengan
                // world-space absolute (giSampleScenePos + cameraPosition),
                // titik fisik yang sama SELALU dapat set ray yang sama persis,
                // apapun posisi/arah kamera - noise-nya jadi menempel diam di
                // permukaan seperti dithering, bukan swimming.
                // FIX ("peredam noise nga kerasa" - denoiser has no visible
                // effect): the seed used to be a pure function of world
                // position with NO frame dependence at all, so a static
                // camera resampled the exact same GI_RAYS_PER_PIXEL set of
                // directions every single frame. The temporal accumulator
                // below therefore had nothing to actually average - history
                // and "fresh" sample were the same noisy value forever, so
                // its only real denoising came from the one-shot spatial
                // blur later on (worth much less than temporal convergence).
                // Cycling the seed through a small, fixed-length set of
                // offsets (8 steps) instead of leaving it frame-invariant
                // means each of those 8 "phases" samples a genuinely
                // different set of hemisphere directions, so accumulating
                // them over a handful of frames converges toward the true
                // average like real temporal supersampling - while staying
                // bounded/periodic (not frameCounter directly) so it can't
                // drift into an unbounded random walk that would reintroduce
                // the flicker the original static seed was written to avoid.
                vec3 giTraceSeedWorld = giSampleScenePos + cameraPosition;
                float giSeedPhase = float(frameCounter % 8);
                vec2 giTraceSeed = GiHash22(giTraceSeedWorld.xz * 3.73 + giTraceSeedWorld.y * 7.91 + giSeedPhase * 17.23);
                float giAvgHitDist, giEnclosure;
                vec3 giTraceSample = GiSampleRadianceRayTraced(
                    #if (GI_BOUNCE_PASSES - 1) % 2 == 0
                        gi_radiance_b_sampler,
                    #else
                        gi_radiance_a_sampler,
                    #endif
                    voxelCoord, giNormal, giTraceSeed, sunVisibility, giAvgHitDist, giEnclosure);

                // --- Adaptive room response ("otomatis nyesuaiin ruangan,
                // bukan slider manual") ------------------------------------
                // The hemisphere rays we just cast already told us how big
                // and how enclosed the space around this surface is - no
                // extra pass, no fixed number needed:
                //   giEnclosure  : 0 = standing in the open (most rays
                //                  escaped to sky), 1 = fully boxed in
                //                  (every ray hit something nearby).
                //   giAvgHitDist : average voxels travelled before hitting
                //                  something - small in a closet, large in
                //                  a hall/cave, maxed out outdoors.
                // A small enclosed space (bathroom, 2-block-wide tunnel)
                // gets its bounce concentrated/boosted, because in reality
                // indirect light dominates there (few bounces saturate a
                // tight box). A big open room or the outdoors gets pulled
                // back automatically, because direct sun/sky already does
                // most of the lighting work and an unscaled bounce would
                // over-brighten/blob. This replaces having to manually
                // tune GI_INTENSITY differently for "indoors" vs
                // "outdoors" - the ray hits already know which is which.
                float giRoomSize   = clamp(giAvgHitDist / float(GI_TRACE_STEPS), 0.0, 1.0); // 0 tight, 1 open/far
                // ("area gelap terang banget / kaya night vision" fix): previously
                // mix(1.35, 0.85, giRoomSize) * mix(0.75, 1.15, giEnclosure) boosted a
                // small enclosed room up to ~1.55x and the Nether read as a bright
                // no-contrast wash. Flattened so enclosed rooms stay ~1.0x while big
                // open areas still pull back - keeps contrast between dark and lit.
                float giAdaptiveIntensity = mix(1.10, 0.95, giRoomSize) * mix(0.90, 1.0, giEnclosure);
                // (edit: was mix(1.05,0.85,giRoomSize) * mix(0.75,1.0,giEnclosure))
                // narrowed both ranges so the worst-case damping in open/
                // less-enclosed spots goes from ~0.64x up to ~0.855x - bounce
                // now reads at a much more consistent strength wherever you
                // stand, instead of visibly fading in bigger/open rooms.

                // SSRC probe: trace rays at screen-probe resolution
                // (only computed for pixels aligned to IC_SCREEN_SCALE grid)
                // DISABLED: ray-tracing screen-anchored (SSRC) is the source
                // of the "gerak-gerak / flicker" shimmer (screen-space probe
                // re-rolls as the camera pans + checkerboard vs WSRC
                // neighbours). Re-enable below when stability is not a
                // priority (replace `0` with `1`).
                #define IC_USE_SSRC_PROBES 0
                vec3 ssrcSample = wsrcSample; // default to WSRC
                #if IC_USE_SSRC_PROBES == 1
                vec2 ssProbeCenter = floor(texCoord * vec2(viewWidth, viewHeight)
                                           / float(IC_SCREEN_SCALE)) + 0.5;
                bool isProbeCenter = all(lessThan(
                    abs(texCoord * vec2(viewWidth, viewHeight) -
                        ssProbeCenter * float(IC_SCREEN_SCALE)),
                    vec2(0.5)));

                // Time-stable probe seed ("bounce gerak-gerak/liar" fix).
                // Deriving the seed from the probe's own grid position (not
                // frameCounter / dither) makes every probe produce the SAME
                // ray set every single frame, so the SSRC only changes when
                // the surface itself changes, not just because the clock
                // ticked. The old per-frame dither re-rolled 1/16 of the
                // screen's probes every frame (IC_RAYS_PER_PROBE rays each,
                // each returning a DIFFERENT average radiance), so while
                // standing still it shimmered and while moving it read as
                // noisy loops/wandering - "gerak-gerak nggak terkontrol".
                vec2 probeSeedUV = ssProbeCenter * float(IC_SCREEN_SCALE)
                                   / vec2(viewWidth, viewHeight);
                float probeSeed = fract(dot(probeSeedUV, vec2(12.9898, 78.233))) * 43758.5453;
                probeSeed = fract(probeSeed); // drop to [0,1)

                // SSRC sample: trace rays into WSRC for fine detail, but ONLY
                // at the sparse probe grid so it stays cheap and stable. All
                // other pixels fall back to the WSRC (already smooth).
                if (isProbeCenter) {
                    ssrcSample = IC_UpdateScreenProbe(
                        #if (GI_BOUNCE_PASSES - 1) % 2 == 0
                            gi_radiance_b_sampler,
                        #else
                            gi_radiance_a_sampler,
                        #endif
                        gi_voxel_sampler,
                        giSampleScenePos, giNormal, probeSeed);
                }
                #endif

                // Distance-based interpolation: SSRC near, WSRC far.
                // Kept conservative - WSRC carries the bulk of the bounce.
                // ("gerak-gerak/flicker" fix): SSRC (probe ray-tracing yang
                // dijangkarkan di LAYAR) masih menyisakan shimmer "gerak terus"
                // saat kamera diputar - probe hanya ada di 1/16 pixel, jadi
                // ada checkerboard tak terlihat antara probe (ter-ray) & tetangga
                // (WSRC polos), dan karena ray di screen-space, sampel berubah
                // mengikuti posisi probe tiap gerakan. Ganti jadi 100% WSRC yang
                // ter-anchor di dunia (bukan layar) -> bounce stabil & halus.
                // v5.4 blob fix: WSRC single-point blurred sample is what read
                // as the visible "bulet-bulet" blobs (its footprint is ~1-2
                // voxels, so every bright voxel painted a soft circle that
                // moved as the grid re-anchored). Blend it down against the
                // world-anchored hemisphere ray trace, which is wide, smooth
                // and blob-free.
                giSample = mix(wsrcSample, giTraceSample, float(GI_RAY_BLEND));

                // --- MIS-style direct light sampling (GI improvement #1) --
                // Adds a dedicated, reliably-aimed sample toward nearby
                // emissive voxels on top of the hemisphere-only bounce above
                // - see the full explanation on GiSampleEmissiveDirect in
                // giFloodfill.glsl. No-op (compiles out entirely) unless
                // GI_EMISSIVE_BLOCKS is on, so this changes nothing at
                // default settings.
                #if GI_EMISSIVE_BLOCKS == 1 && GI_MIS_LIGHT_SAMPLING == 1
                    giSample += GiSampleEmissiveDirect(gi_emission_sampler, voxelCoord, giNormal, giTraceSeed);
                #endif

                // --- Intensity --------------------------------------------
                // GI_INTENSITY tetap ada sebagai overall master slider (buat
                // selera/gaya), tapi giAdaptiveIntensity yang sekarang
                // ngurusin PERBEDAAN antar ruangan secara otomatis - jadi
                // GI_INTENSITY nggak perlu di-set beda-beda buat tiap
                // ruangan/outdoor secara manual lagi.
                giSample *= float(GI_INTENSITY) * giAdaptiveIntensity;

                // Flat-surface bounce attenuation: softened further (0.72 ->
                // 0.90) per latest request - the reference corridor shot
                // shows strong, consistent colored bounce across the WHOLE
                // flat floor, so this should only take the harshest edge off,
                // not visibly dim the floor at all.
                float flatness = abs(giNormal.y); // 1.0 for flat floor/ceiling, 0.0 for vertical walls
                float flatAtten = smoothstep(0.72, 0.98, flatness);
                giSample *= mix(1.0, 0.90, flatAtten);

                // FIX ("rumput/tanah terbuka jadi putih pudar"): boost di atas
                // (dan saturation boost + soft-knee headroom di bawah) ditulis
                // berdasarkan referensi LANTAI DALAM RUANGAN (Cornell-box style),
                // tapi flatAtten sebelumnya memiliki bug logika (bernilai 0.0 di tanah datar, 
                // sehingga penekanan ini tidak berjalan di tanah terbuka).
                // ("bounce hilang deket kamera / cuma keliatan di angle
                // tertentu" fix): 0.15 di sini adalah biang keroknya -
                // giEnclosure ikut berubah tergantung posisi kamera (deket
                // ujung lorong/pintu = enclosure kebaca rendah walau masih
                // di dalam ruangan yang sama), jadi lantai berwarna bisa
                // jatuh sampai 15% kekuatannya persis waktu kamera mendekat
                // ke area yang lebih terbuka. Dinaikkan ke 0.55 supaya masih
                // ada sedikit redaman untuk tanah terbuka/rumput di luar,
                // tapi lorong/ruangan seperti referensi tidak lagi kehilangan
                // bounce lantainya tiap kali enclosure sedikit berubah.
                // Bounce color processing: keep the bounce color vivid and
                // carry its color as brightness. The bounce color from the
                // voxel grid already contains the correct tint (e.g. red
                // wool bounce = red color). We just ensure it's saturated
                // enough to be visible as colored brightness, not washed out.
                // No luminance normalization - the bounce color IS the
                // brightness color. Soft-knee prevents overexposure.
                {
                    float giSampleLum = max(dot(giSample, vec3(0.2126, 0.7152, 0.0722)), 1e-4);
                    // Mild saturation boost to keep bounce colorful
                    giSample = mix(vec3(giSampleLum), giSample, 1.2);
                }
                // Soft-knee energy compression (restored from v8.4). Without
                // this, bright bounce values (e.g. sunlit wall close to camera)
                // hit the firefly clamp hard and get harshly cut, causing
                // overexposure that looks "too white/bright". The soft-knee
                // smoothly compresses values above the threshold instead of
                // hard-clamping, preserving color saturation while preventing
                // blow-out. Knee at 1.7 matches v8.4 balance.
                const float giSoftKnee = 1.7;
                vec3 giOverKnee = max(giSample - giSoftKnee, vec3(0.0));
                giSample = min(giSample, vec3(giSoftKnee)) + giOverKnee / (vec3(1.0) + giOverKnee);
                giSample = min(giSample, vec3(IC_FIREFLY_CLAMP));

                // --- SSGI merge (if enabled) -----------------------------
                // NOISE FIX: previously giScreenSpace was kept separate and
                // added back in AFTER the temporal + spatial "watercolor"
                // denoiser below (see old comment at the combine site), on
                // the theory that the existing denoiser "covers the rest".
                // It never actually did - that addition happened downstream
                // of both denoise steps, so the raw per-frame bitmask-GI
                // signal (GI_BITMASK_SLICES=2, brand new random slice angle
                // every frame, zero history) reached the screen completely
                // unfiltered. That is the dominant source of the visible GI
                // grain/noise. Folding it into giSample HERE instead means
                // it rides through the exact same temporal accumulation and
                // spatial blur as the voxel bounce, at no extra cost (same
                // passes, same taps) - it just stops skipping them.
                #if SSGI > 0
                    if (!entityOrParticle) giSample += giScreenSpace;
                #endif

                // --- Temporal accumulation -------------------------------
                // IC produces less noisy per-frame samples than stochastic PT,
                // so temporal filter uses higher history weight (smoother)
                // and simpler clamping (less ghosting risk).
                vec3 giAccumulated = giSample;
                #if GI_TEMPORAL_ACCUM == 1
                {
                    vec3 cameraOffset = cameraPosition - previousCameraPosition;
                    vec2 giPrevCoord = SHalfReprojection(playerPos, cameraOffset);

                    // ("light bounce ke-cut/potong di tepi layar saat gerak"
                    // fix): sebelumnya batas valid persis [0,1] - begitu
                    // koordinat reprojeksi meleset SEDIKIT saja ke luar itu
                    // (umum terjadi saat kamera menoleh/berjalan dan area
                    // baru masuk dari tepi layar), history langsung jatuh ke
                    // jalur fallback yang jauh lebih kasar (tanpa 8-tap
                    // smoothing, alpha minimum 30%) - loncatan kualitas yang
                    // tiba-tiba persis di garis tepi layar itulah yang
                    // terlihat seperti bounce "terpotong". Memberi sedikit
                    // margin overscan (giBoundsMargin) supaya pixel yang
                    // hanya meleset tipis ke luar tetap boleh lewat jalur
                    // smoothing yang bagus (koordinat DI-CLAMP dulu sebelum
                    // dipakai untuk texture2D, jadi tetap valid) - transisi
                    // ke fallback jadi lebih jarang & lebih halus, bukan
                    // menghilang tiba-tiba.
                    const float giBoundsMargin = 0.02;

                    // ("noise/flicker nggak pernah bener-bener hilang" fix):
                    // colortex9's alpha channel was documented up in
                    // pipelineSettings.glsl as holding "how many frames of
                    // valid history it has (a, 0-32)" - a proper converging
                    // accumulator - but the code below never actually used
                    // it that way; it always wrote 1.0 and blended in AT
                    // LEAST 25% of a brand-new sample every single frame,
                    // forever, even standing still staring at one wall.
                    // Since the ray-traced sample re-rolls a different set
                    // of hemisphere directions every frame (giSeedPhase
                    // cycles through 8 phases so there's something to
                    // actually average), that permanent 25% floor meant the
                    // image never converged - it kept re-injecting a
                    // slightly-different noisy sample forever, which reads
                    // as grain that never settles down, and beats visibly
                    // with the 8-frame seed cycle as a slow flicker. Real
                    // convergent accumulation (how a static camera in
                    // PTGI-style packs ends up genuinely clean) needs alpha
                    // to keep SHRINKING the longer a surface has been
                    // stable, not sit at a fixed floor.
                    //
                    // ("light bounce kelihatan berbintik/noise" request):
                    // raised 32 -> 56. This only changes how many frames the
                    // convergence curve keeps shrinking over before it
                    // bottoms out (1/56 min blend vs 1/32) - same single
                    // history texture read + blend as before, so it costs
                    // NOTHING extra per frame. colortex9 stores this as a
                    // float (16-bit float channel), so counting higher than
                    // 32 isn't a precision problem. The only real trade-off
                    // is a slightly longer settle time after a big lighting
                    // change (e.g. torch just placed) before it reaches
                    // full smoothness - motion itself is unaffected since
                    // the reprojection/edge checks below still reset it
                    // immediately whenever a surface is disoccluded.
                    const float giMaxHistoryFrames = 80.0;

                    if (clamp(giPrevCoord, -giBoundsMargin, 1.0 + giBoundsMargin) == giPrevCoord) {
                        vec2 giPrevCoordSafe = clamp(giPrevCoord, 0.0, 1.0);
                        vec4 giHistory = texture2D(colortex9, giPrevCoordSafe);

                        // 8-tap edge-aware history smoothing (same as before
                        // but with softer weights since IC is less noisy)
                        vec2 giTexel = 1.0 / vec2(viewWidth, viewHeight);
                        float giCenterLinZ = GetLinearDepth(texture2D(depthtex0, giPrevCoordSafe).r);
                        vec3  giCenterN    = normalize(texture2D(colortex4, giPrevCoordSafe).rgb);

                        float giBlurRadiusPx = 1.2;
                        vec2 giTapOffsets[8] = vec2[8](
                            vec2( giTexel.x, 0.0) * giBlurRadiusPx, vec2(-giTexel.x, 0.0) * giBlurRadiusPx,
                            vec2(0.0,  giTexel.y) * giBlurRadiusPx, vec2(0.0, -giTexel.y) * giBlurRadiusPx,
                            vec2( giTexel.x,  giTexel.y) * giBlurRadiusPx * 0.7071, vec2(-giTexel.x,  giTexel.y) * giBlurRadiusPx * 0.7071,
                            vec2( giTexel.x, -giTexel.y) * giBlurRadiusPx * 0.7071, vec2(-giTexel.x, -giTexel.y) * giBlurRadiusPx * 0.7071
                        );

                        vec3 giHistorySum    = giHistory.rgb * 0.50;
                        float giHistoryWeight = 0.50;
                        for (int t = 0; t < 8; t++) {
                            vec2  tapCoord = clamp(giPrevCoordSafe + giTapOffsets[t], 0.0, 1.0);
                            float tapLinZ  = GetLinearDepth(texture2D(depthtex0, tapCoord).r);
                            vec3  tapN     = normalize(texture2D(colortex4, tapCoord).rgb);

                            float depthWeight  = exp(-abs(tapLinZ - giCenterLinZ) / max(giCenterLinZ * 0.06, 0.06));
                            float normalWeight = pow(max(dot(giCenterN, tapN), 0.0), 12.0);
                            float w = 0.12 * depthWeight * normalWeight;

                            giHistorySum    += texture2D(colortex9, tapCoord).rgb * w;
                            giHistoryWeight += w;
                        }
                        vec3 giHistorySmooth = giHistorySum / max(giHistoryWeight, 0.50);

                        // Tighter temporal clamp (ghosting fix): IC is nearly
                        // noise-free per frame, so we can afford to trust the
                        // fresh sample more and clamp the history harder - a
                        // stale history value can't drift far from the current
                        // sample, which kills the long ghosting trails behind
                        // moving light/geometry without reintroducing noise.
                        // Narrowed further 0.85x-1.15x -> 0.90x-1.10x to
                        // reduce edge smear when camera moves. History that
                        // drifts more than 10% from current gets pulled back
                        // faster, preventing the "pulled/ stretched" look at
                        // screen edges during movement.
                        // Wider clamp (0.90-1.10 -> 0.85-1.15): bounce stays
                        // more stable during camera motion, less shape-shifting.
                        vec3 minHist = giHistorySmooth * 0.85 - vec3(0.01);
                        vec3 maxHist = giHistorySmooth * 1.15 + vec3(0.02);

                        // FIX ("duplikat" / ghost trail): disocclusion used to
                        // be measured between the history and the RAW sample
                        // AFTER it had already been clamped into 0.80x-1.25x
                        // of that same history. That clamp forces the two
                        // values to stay close by construction, so the
                        // disocclusion metric derived from them could barely
                        // ever read as "different" - even when the true,
                        // unclamped sample said the surface underneath this
                        // pixel had completely changed (camera turned a
                        // corner, a block was placed/broken, a shadow moved
                        // in). Low measured disocclusion means low alpha
                        // (see mix() below) means the OLD history keeps
                        // dominating the blend - which is exactly what reads
                        // as a stale/duplicated bounce-light "ghost" of
                        // geometry that isn't there anymore. Measuring
                        // disocclusion from the RAW, unclamped sample fixes
                        // that: real scene changes are now actually detected,
                        // so the accumulator lets go of stale history instead
                        // of quietly keeping it forever. The clamp is still
                        // applied afterwards (as giSampleClamped) purely to
                        // damp per-frame noise/fireflies once we know whether
                        // we're really looking at the same surface.
                        float histLum = dot(giHistorySmooth, vec3(0.2126, 0.7152, 0.0722));
                        float rawLum  = dot(giSample, vec3(0.2126, 0.7152, 0.0722));
                        float disocclusion = clamp(abs(histLum - rawLum) / (max(histLum, rawLum) + 0.10), 0.0, 1.0);

                        vec3 giSampleClamped = clamp(giSample, max(vec3(0.0), minHist), maxHist);
                        // On a strong disocclusion, trust the fresh sample
                        // outright instead of clamping it toward a history
                        // value that no longer describes the same surface.
                        giSampleClamped = mix(giSampleClamped, giSample, disocclusion);
                        // ("light bounce berubah-ubah/menghilang saat gerak"
                        // fix): reactiveAlpha ini menentukan seberapa besar
                        // sampel BARU (belum halus) boleh langsung menimpa
                        // history yang sudah stabil begitu ada gerakan -
                        // sebelumnya sampai 75%, dan cameraMotion sudah
                        // dianggap "penuh" (1.0) hanya dengan sedikit
                        // pergeseran kamera (dikali 1.6), jadi jalan kaki
                        // biasa saja sudah cukup untuk membuat bounce
                        // ter-reset besar-besaran tiap frame - itulah yang
                        // terlihat sebagai warna berubah-ubah/berkedip saat
                        // bergerak. cameraMotion diturunkan sensitivitasnya
                        // (1.6 -> 1.0, butuh gerakan lebih jauh untuk
                        // dianggap "gerakan besar") dan plafon reactiveAlpha
                        // diturunkan (0.75 -> 0.55) supaya transisinya lebih
                        // halus. disocclusion (deteksi permukaan yang benar-
                        // benar berubah, misal balok baru terlihat) TIDAK
                        // diubah, jadi tetap responsif kalau memang ada
                        // perubahan nyata - bukan cuma karena kamera goyang.
                        // ("ghosting masih ada" fix, v9.0): cameraMotion
                        // sensitivity diturunkan lagi 1.0->0.8 dan reactiveAlpha
                        // ceiling 0.55->0.45 supaya temporal accumulator
                        // lebih agresif update ke fresh sample saat gerak,
                        // mengurangi smear/trailing.
                        // ("ghosting pinggiran layar" fix): cameraMotion
                        // sensitivity dinaikkan 0.8->1.2 supaya bahkan
                        // gerakan kecil kamera langsung meningkatkan alpha,
                        // dan reactiveAlpha ceiling dinaikkan 0.45->0.55
                        // supaya fresh sample lebih cepat menggantikan
                        // history stale di tepi layar.
                        // Further reduced 1.2 -> 0.8 for stability: bounce
                        // doesn't change shape as easily during movement.
                        float cameraMotion = min(1.0, length(cameraOffset) * 0.8);

                        // frameAlpha: standard running-average weight based on
                        // how many consecutive frames this pixel has had
                        // undisturbed history (giHistory.a). Frame 1 is 100%
                        // fresh, frame 2 is 50/50, frame 10 is ~9% fresh, and
                        // so on - shrinking toward a small floor (1/33 at the
                        // 32-frame cap) instead of getting stuck at 25%. This
                        // is what actually lets the image go quiet on a
                        // static camera instead of grain/flicker persisting
                        // indefinitely.
                        float giHistoryCount = giHistory.a;
                        float frameAlpha = 1.0 / (min(giHistoryCount, giMaxHistoryFrames) + 1.0);

                        // Real scene changes (camera turning a corner, a
                        // torch placed/removed, geometry changing) still need
                        // to react fast - keep the old disocclusion/motion
                        // reactive term as a ceiling on top of the convergent
                        // one. Whichever is higher wins, same intent as
                        // before, just without an artificial floor once a
                        // surface has actually been stable for a while.
                        // Reduced ceiling 0.55 -> 0.40 for more stability:
                        // less fresh sample injection = less flickering.
                        float reactiveAlpha = mix(0.05, 0.40, max(disocclusion * 0.6, cameraMotion * 0.4));
                        float alpha = max(frameAlpha, reactiveAlpha);

                        // Entities (player, mobs) ghost badly through the
                        // temporal accumulator - their history is reprojected
                        // from a DIFFERENT world position each frame (they
                        // move independently of the camera), so blending with
                        // it smears a fading GI trail behind them. For
                        // entities just use the fresh sample outright; only
                        // world/static pixels get the temporal accumulation.
                        if (entityOrParticle) alpha = 1.0;

                        giAccumulated = mix(giHistorySmooth, giSampleClamped, alpha);

                        // Advance (or reset) the history counter for next
                        // frame. If the reactive term is what actually drove
                        // this frame's alpha (a real disocclusion/motion
                        // event, not just normal convergence), knock the
                        // counter back down so the accumulator honestly
                        // starts re-converging instead of claiming a long,
                        // stable history it never had. Otherwise, count one
                        // more stable frame.
                        float giNextHistoryCount = (reactiveAlpha > frameAlpha)
                            ? mix(giHistoryCount * 0.25, 0.0, disocclusion)
                            : giHistoryCount + 1.0;
                        if (entityOrParticle) giNextHistoryCount = 0.0;

                        giHistoryOut = vec4(giAccumulated, giNextHistoryCount);
                    } else {
                        vec2 giEdgeCoord = clamp(giPrevCoord, 0.0, 1.0);
                        vec4 giEdgeHistoryFull = texture2D(colortex9, giEdgeCoord);
                        vec3 giEdgeHistory = giEdgeHistoryFull.rgb;
                        // ("bounce ke-cut/potong di tepi layar" fix, lanjutan):
                        // menambah 4-tap blur ringan (lebih murah dari 8-tap
                        // di jalur utama) di fallback ini juga, supaya area
                        // yang baru masuk dari tepi layar tidak langsung
                        // menampilkan satu sampel mentah yang beda jelas
                        // dari history halus di sebelahnya - transisi jadi
                        // lebih rata, bukan garis potong yang kentara.
                        vec2 giEdgeTexel = 1.0 / vec2(viewWidth, viewHeight);
                        vec3 giEdgeSum = giEdgeHistory * 0.40;
                        float giEdgeWeight = 0.40;
                        giEdgeSum += texture2D(colortex9, clamp(giEdgeCoord + vec2( giEdgeTexel.x, 0.0), 0.0, 1.0)).rgb * 0.1125; giEdgeWeight += 0.1125;
                        giEdgeSum += texture2D(colortex9, clamp(giEdgeCoord + vec2(-giEdgeTexel.x, 0.0), 0.0, 1.0)).rgb * 0.1125; giEdgeWeight += 0.1125;
                        giEdgeSum += texture2D(colortex9, clamp(giEdgeCoord + vec2(0.0,  giEdgeTexel.y), 0.0, 1.0)).rgb * 0.1125; giEdgeWeight += 0.1125;
                        giEdgeSum += texture2D(colortex9, clamp(giEdgeCoord + vec2(0.0, -giEdgeTexel.y), 0.0, 1.0)).rgb * 0.1125; giEdgeWeight += 0.1125;
                        giEdgeHistory = giEdgeSum / giEdgeWeight;
                        // Same convergence idea for the screen-edge fallback,
                        // with a 30% ceiling so it can't freeze completely
                        // while the camera is turning (which is exactly when
                        // pixels fall off-screen).
                        float edgeAlpha = entityOrParticle ? 1.0 :
                            max(0.50, 1.0 / (min(giEdgeHistoryFull.a, giMaxHistoryFrames) + 1.0));
                        giAccumulated = mix(giEdgeHistory, giSample, edgeAlpha);
                        float giEdgeNextCount = entityOrParticle ? 0.0 : giEdgeHistoryFull.a + 1.0;
                        giHistoryOut = vec4(giAccumulated, giEdgeNextCount);
                    }
                }

                // Multi-pass edge-avoiding (a-trous) spatial denoise,
                // variance-adaptive by temporal history confidence.
                //
                // FIX ("noise/flicker masih terlihat" - motion-time grain):
                // the previous version was a single 8-tap pass at one FIXED
                // radius (~5.5px) with a FIXED blend strength (~0.30) for
                // every pixel, converged or not. That caught steady-state
                // grain fine, but during camera motion - exactly when the
                // temporal accumulator's reactiveAlpha (see above) is
                // forced high and keeps re-injecting fresh, noisy samples -
                // one fixed-radius/fixed-strength pass can't cover enough
                // area fast enough, so grain kept flickering through while
                // walking/turning. That's the dominant remaining
                // difference vs. a converged PT-style denoiser (SEUS PTGI/
                // SVGF-family packs all use a multi-pass wavelet filter for
                // exactly this reason, not a single blur).
                //
                // Two changes, same tap budget as before (8 taps/pass):
                //  1. Two passes at GEOMETRICALLY INCREASING step size (1px,
                //     then 3px "a-trous" spacing) instead of one pass at a
                //     mid-size radius. This covers a much larger effective
                //     search area for the same total texture reads, which
                //     is what actually clears grain a single-radius pass
                //     can't reach, while each pass's own depth/normal
                //     weights still stop it bleeding across edges/corners.
                //  2. Blend strength is now driven by giHistoryOut.a (how
                //     many stable frames of temporal history this pixel
                //     already has, tracked above) instead of one constant
                //     for every pixel: a pixel that just disoccluded
                //     (history reset to 0 this frame) is high-variance and
                //     gets denoised hard; a pixel that's been stable 8+
                //     frames is already temporally converged and gets a
                //     light touch, so static/slow-moving scenes stay crisp
                //     instead of permanently soft.
                //
                // ("transisi terang-gelap terlalu halus" fix, v9.0):
                // depthWeight threshold diturunkan dari 0.05 ke 0.02 supaya
                // blur lebih agresif menolak tap yang depth-nya berbeda.
                // Ini menjaga kontras tajam di tepi pintu/bukaan di mana
                // area terang dan gelap berdekatan di surface yang sama
                // (flat wall). wcBaseRadius dan wcBaseStrength juga
                // diturunkan sedikit supaya spatial blur tidak terlalu
                // agresif menghaluskan bounce.
                {
                    vec2  wcTexel      = 1.0 / vec2(viewWidth, viewHeight);
                    float wcCenterLinZ = GetLinearDepth(z0);
                    vec3  wcCenterN    = giNormal;

                    // 0 = brand-new/just-disoccluded sample, 1 = already
                    // has >=8 frames of stable convergent history.
                    float historyConfidence = clamp(giHistoryOut.a / 8.0, 0.0, 1.0);

                    // Same "cheaper profile leans harder on neighbours" idea
                    // as before, now applied as a base radius/strength that
                    // the variance term scales further per-pixel.
                    // Increased base radius and strength for less noise.
                    const float wcBaseRadiusPx = clamp(3.5 + (8.0 - float(GI_RAYS_PER_PIXEL)) * 0.25, 2.5, 5.0);
                    const int   wcTaps = 8;
                    const float wcBaseStrength = clamp(0.55 + (8.0 - float(GI_RAYS_PER_PIXEL)) * 0.03, 0.45, 0.70);
                    float wcStrength = mix(wcBaseStrength, 0.08, historyConfidence);

                    float atrousStep[2] = float[2](1.0, 3.0);

                    for (int pass = 0; pass < 2; pass++) {
                        float wcRadiusPx = wcBaseRadiusPx * atrousStep[pass];

                        vec3  wcSum    = giAccumulated;
                        float wcWeight = 1.0;
                        for (int t = 0; t < wcTaps; t++) {
                            float ang = (6.28318530718 / float(wcTaps)) * float(t) + dither * 6.28318530718;
                            vec2  tapCoord = texCoord + vec2(cos(ang), sin(ang)) * wcTexel * wcRadiusPx;
                            if (clamp(tapCoord, 0.0, 1.0) != tapCoord) continue;

                            float tapLinZ = GetLinearDepth(texture2D(depthtex0, tapCoord).r);
                            vec3  tapN    = normalize(texture2D(colortex4, tapCoord).rgb);

                            // Tighter depth weight (0.05 -> 0.02): tap yang
                            // depth-nya berbeda lebih cepat ditolak, menjaga
                            // kontras tajam di tepi terang/gelap
                            float depthWeight  = exp(-abs(tapLinZ - wcCenterLinZ) / max(wcCenterLinZ * 0.02, 0.03));
                            float normalWeight = pow(max(dot(wcCenterN, tapN), 0.0), 16.0);
                            float w = depthWeight * normalWeight;

                            wcSum    += texture2D(colortex9, tapCoord).rgb * w;
                            wcWeight += w;
                        }
                        vec3 giSoftBlurred = wcSum / max(wcWeight, 1.0);
                        // Each successive (wider) pass contributes a little
                        // less than the first, so the fine 1px pass does
                        // most of the edge-preserving work and the 3px pass
                        // only mops up what it missed - avoids the wide
                        // pass alone over-softening detail.
                        giAccumulated = mix(giAccumulated, giSoftBlurred, wcStrength / float(pass + 1));
                    }
                    giHistoryOut = vec4(giAccumulated, giHistoryOut.a);
                }
                #else
                    giHistoryOut = vec4(giSample, 1.0);
                #endif

                giVoxel = giAccumulated * (1.0 - ssao * 0.5);

                // OUTLIER CAP (anti-firefly, inspired by Foxy Shader's
                // sspt_outlier_cap): computes robust median luminance of
                // 5-tap cross (center + 4 neighbors) and caps any pixel
                // that exceeds median * 1.35. This kills firefly noise
                // without over-blurring legitimate bright spots, because
                // the median is resistant to outliers (unlike mean).
                // Only applied to GI, not the scene color.
                {
                    vec2 texelSize = 1.0 / vec2(viewWidth, viewHeight);
                    vec2 texCoord = gl_FragCoord.xy * texelSize;
                    float centerLum = dot(giVoxel, vec3(0.2126, 0.7152, 0.0722));
                    float l0 = dot(texture2D(colortex0, texCoord + vec2(-texelSize.x, 0.0)).rgb, vec3(0.2126, 0.7152, 0.0722));
                    float l1 = dot(texture2D(colortex0, texCoord + vec2( texelSize.x, 0.0)).rgb, vec3(0.2126, 0.7152, 0.0722));
                    float l2 = dot(texture2D(colortex0, texCoord + vec2(0.0, -texelSize.y)).rgb, vec3(0.2126, 0.7152, 0.0722));
                    float l3 = dot(texture2D(colortex0, texCoord + vec2(0.0,  texelSize.y)).rgb, vec3(0.2126, 0.7152, 0.0722));
                    // Robust median: sort 5 values, pick middle
                    float vals[5] = float[5](centerLum, l0, l1, l2, l3);
                    // Simple insertion sort for 5 elements
                    for (int i = 1; i < 5; i++) {
                        float key = vals[i];
                        int j = i - 1;
                        while (j >= 0 && vals[j] > key) {
                            vals[j + 1] = vals[j];
                            j--;
                        }
                        vals[j + 1] = key;
                    }
                    float medianLum = vals[2]; // middle of 5
                    float cap = medianLum * 1.35 + 0.020;
                    float currentLum = dot(giVoxel, vec3(0.2126, 0.7152, 0.0722));
                    if (currentLum > cap && cap > 0.001) {
                        giVoxel *= cap / currentLum;
                    }
                }
            }
        #endif

// GI combine: when voxel GI is active, giVoxel already includes the
        // SSGI contribution - it was folded into giSample BEFORE the
        // temporal/spatial denoiser above (see "NOISE FIX" note), so it
        // comes out the other side already smoothed together with the
        // voxel bounce instead of being pasted on raw. When voxel GI is
        // off, SSGI's own (now normal-aware, occlusion-tested) signal is
        // added directly - that path still has no denoiser of its own, so
        // it will look noisier; turning CUSTOM_GI_VOXEL on is the fix for
        // that case, since it's what carries the denoise pipeline.
        // Entities (player, mobs) get NO voxel GI ("bounce hanya ke block"
        // fix): the voxel grid samples the surface they stand on, not their
        // own body, so any amount of bounce there was really just the
        // ground's light glued onto the model, which then visibly drags/
        // ghosts behind them as they move (they reproject independently of
        // the camera - see the temporal accumulation notes above). Entities
        // still get normal direct sun/sky/block light, just not this extra
        // borrowed-from-the-floor bounce term.
        #if defined CUSTOM_GI_VOXEL_INTERNAL && defined OVERWORLD
            #if GI_COLOR_ONLY == 1
                // Colored brightness bounce: adds a small amount of
                // brightness IN THE BOUNCE COLOR (not white). Red wool
                // bounce = slightly pink brightness, not white brightness.
                {
                    vec3 bounceColor = clamp(giVoxel, 0.0, 1.0);
                    float bounceLum = dot(bounceColor, vec3(0.2126, 0.7152, 0.0722));
                    if (bounceLum > 0.01) {
                        // Boost saturation so bounce color shows the actual
                        // block color (red, green, blue) not the white sunlight
                        vec3 bounceDesat = vec3(bounceLum);
                        vec3 bounceSaturated = mix(bounceDesat, bounceColor, 1.8);
                        bounceSaturated = max(bounceSaturated, vec3(0.0));
                        float satLum = dot(bounceSaturated, vec3(0.2126, 0.7152, 0.0722));
                        vec3 bounceHue = bounceSaturated / max(satLum, 0.001);
                        // Add brightness IN THE BOUNCE COLOR, not white
                        float brightnessAdd = bounceLum * GI_COLOR_TINT_STRENGTH;
                        color.rgb += bounceHue * brightnessAdd;
                        color.rgb = min(color.rgb, vec3(1.5));
                    }
                }
            #else
                color.rgb += giVoxel * (entityOrParticle ? 0.0 : 1.0);
            #endif
        #elif SSGI > 0
            color.rgb += giScreenSpace;
        #endif

        DoFog(color, skyFade, lViewPos, playerPos, VdotU, VdotS, dither, false, 0.0);
    } else {
        #if defined DISTANT_HORIZONS || defined VOXY
            #ifdef DISTANT_HORIZONS
                float z0lod = texelFetch(dhDepthTex, texelCoord, 0).r;
            #elif defined VOXY
                float z0lod = texelFetch(vxDepthTexTrans, texelCoord, 0).r;
            #endif
            if (z0lod < 1.0) { // Lod Chunks
                vec4 screenPosLod = vec4(texCoord, z0lod, 1.0);
                #ifdef DISTANT_HORIZONS
                    vec4 viewPosLod = dhProjectionInverse * (screenPosLod * 2.0 - 1.0);
                    viewPosLod /= viewPosLod.w;

                    #if SHADOW_QUALITY > -1
                        color.rgb *= 0.5 + 0.5 * GetLODShadows(viewPosLod.xyz, nViewPos, dhDepthTex, dhProjection, dhProjectionInverse, dither);
                    #endif
                #elif defined VOXY
                    vec4 viewPosLod = vxProjInv * (screenPosLod * 2.0 - 1.0);
                    viewPosLod /= viewPosLod.w;

                    #if SHADOW_QUALITY > -1
                        lodShadow = GetLODShadows(viewPosLod.xyz, nViewPos, vxDepthTexTrans, vxProj, vxProjInv, dither);
                        lodShadow += OSIEBCA; // For being able to check if a calculation has been done;
                    #endif

                    #if SSAO_QUALI > 0
                        float farLod = 16*20, nearLod = 16;
                        float aoWorldRange = (farLod - nearLod);
                        float ssao = GetAmbientOcclusion(vxDepthTexTrans, z0lod, GetLinearDepth(z0lod, farLod, nearLod), dither, farLod, nearLod, aoWorldRange);
                        color.rgb *= pow2(pow2(ssao));
                    #endif
                #endif

                lViewPos = length(viewPosLod.xyz);
                playerPos = ViewToPlayer(viewPosLod.xyz);
                waterRefColor = color.rgb;
                
                DoFog(color, skyFade, lViewPos, playerPos, VdotU, VdotS, dither, false, 0.0);
            } else
        #endif
        { // Sky
            skyFade = 1.0;

            #ifdef OVERWORLD
                #if AURORA_STYLE > 0
                    auroraBorealis = GetAuroraBorealis(viewPos.xyz, VdotU, dither);
                    color.rgb += auroraBorealis;
                #endif
                #if NIGHT_NEBULAE == 1
                    nightNebula += GetNightNebula(viewPos.xyz, VdotU, VdotS);
                    color.rgb += nightNebula;
                #endif
            #endif
            #ifdef NETHER
                color.rgb = netherColor * (1.0 - maxBlindnessDarkness);

                #ifdef ATM_COLOR_MULTS
                    color.rgb *= atmColorMult;
                #endif
            #endif
            #ifdef END
                color.rgb = endSkyColor;
                color.rgb += GetEnderStars(viewPos.xyz, VdotU);

                #ifdef ENDER_LIGHTNING
                    float enderSkyFlash = GetEnderLightningFlash() * ENDER_LIGHTNING_I;
                    color.rgb += enderSkyFlash * vec3(0.55, 0.60, 0.70);
                #endif

                color.rgb *= 1.0 - maxBlindnessDarkness;

                #ifdef ATM_COLOR_MULTS
                    color.rgb *= atmColorMult;
                #endif
            #endif
        }
    }

    float cloudLinearDepth = 1.0;
    vec4 clouds = vec4(0.0);

    #ifdef VL_CLOUDS_ACTIVE
        float cloudZCheck = 0.56;

        if (z0 > cloudZCheck) {
            clouds = GetClouds(cloudLinearDepth, skyFade, vec3(0.0), playerPos,
                               lViewPos, VdotS, VdotU, dither, auroraBorealis, nightNebula);

            color = mix(color, vec4(clouds.rgb, 0.0), clouds.a);
        }
    #endif

    #ifdef SKY_EFFECT_REFLECTION
        waterRefColor = mix(waterRefColor, clouds.rgb, clouds.a);
    #endif
    waterRefColor = sqrt(waterRefColor) * 0.5;

    #if defined LIGHTSHAFTS_ACTIVE && (LIGHTSHAFT_BEHAVIOUR == 1 && SHADOW_QUALITY >= 1 || defined END)
        if (viewWidth + viewHeight - gl_FragCoord.x - gl_FragCoord.y < 1.5)
            cloudLinearDepth = vlFactor;
    #endif

    #if defined OVERWORLD && defined ATMOSPHERIC_FOG && (defined SPECIAL_BIOME_WEATHER || RAIN_STYLE == 2)
        if (isEyeInWater == 0) {
            float altitudeFactorRaw = GetAtmFogAltitudeFactor(playerPos.y + cameraPosition.y);
            vec3 atmFogColor = GetAtmFogColor(altitudeFactorRaw, VdotS);
            #ifdef ATM_COLOR_MULTS
                atmFogColor *= atmColorMult;
            #endif

            #if RAIN_STYLE == 2
                float factor = 1.0;
            #else
                float factor = max(inSnowy, inDry);
            #endif

            color = mix(color, vec4(atmFogColor, 0.0), 0.5 * rainFactor * factor * sqrt1(skyFade));
        }
    #endif

    /*DRAWBUFFERS:05*/
    gl_FragData[0] = vec4(color.rgb, 1.0);
    gl_FragData[1] = vec4(waterRefColor, cloudLinearDepth);

    // same check as #ifdef PBR_REFLECTIONS but for Optifine to understand:
    #if BLOCK_REFLECT_QUALITY >= 2 && RP_MODE >= 1
        /*DRAWBUFFERS:054*/
        gl_FragData[2] = vec4(mat3(gbufferModelViewInverse) * normalM, sqrt(fresnelM * color.a));

        #ifdef VOXY
            /* RENDERTARGETS: 0,5,4,19 */
            gl_FragData[3] = vec4(waterRefColor, cloudLinearDepth);

            #if SHADOW_QUALITY > -1
                #if defined CUSTOM_GI_VOXEL_INTERNAL && defined OVERWORLD
                    /* RENDERTARGETS: 0,5,4,19,18,9 */
                    gl_FragData[4] = vec4(lodShadow, 1.0, 1.0, 1.0);
                    gl_FragData[5] = giHistoryOut;
                #else
                    /* RENDERTARGETS: 0,5,4,19,18 */
                    gl_FragData[4] = vec4(lodShadow, 1.0, 1.0, 1.0);
                #endif
            #else
                #if defined CUSTOM_GI_VOXEL_INTERNAL && defined OVERWORLD
                    /* RENDERTARGETS: 0,5,4,19,9 */
                    gl_FragData[4] = giHistoryOut;
                #endif
            #endif
        #else
            #if defined CUSTOM_GI_VOXEL_INTERNAL && defined OVERWORLD
                /* RENDERTARGETS: 0,5,4,9 */
                gl_FragData[3] = giHistoryOut;
            #endif
        #endif
    #elif defined VOXY
        /* RENDERTARGETS: 0,5,19 */
        gl_FragData[2] = vec4(waterRefColor, cloudLinearDepth);

        #if SHADOW_QUALITY > -1
            #if defined CUSTOM_GI_VOXEL_INTERNAL && defined OVERWORLD
                /* RENDERTARGETS: 0,5,19,18,9 */
                gl_FragData[3] = vec4(lodShadow, 1.0, 1.0, 1.0);
                gl_FragData[4] = giHistoryOut;
            #else
                /* RENDERTARGETS: 0,5,19,18 */
                gl_FragData[3] = vec4(lodShadow, 1.0, 1.0, 1.0);
            #endif
        #else
            #if defined CUSTOM_GI_VOXEL_INTERNAL && defined OVERWORLD
                /* RENDERTARGETS: 0,5,19,9 */
                gl_FragData[3] = giHistoryOut;
            #endif
        #endif
    #elif defined CUSTOM_GI_VOXEL_INTERNAL && defined OVERWORLD
        /* RENDERTARGETS: 0,5,9 */
        gl_FragData[2] = giHistoryOut;
    #endif
}

#endif

//////////Vertex Shader//////////Vertex Shader//////////Vertex Shader//////////
#ifdef VERTEX_SHADER

noperspective out vec2 texCoord;

flat out vec3 upVec, sunVec, eastVec;

#if defined LIGHTSHAFTS_ACTIVE && (LIGHTSHAFT_BEHAVIOUR == 1 && SHADOW_QUALITY >= 1 || defined END)
    flat out float vlFactor;
#endif

//Attributes//

//Common Variables//

//Common Functions//

//Includes//

//Program//
void main() {
    gl_Position = ftransform();
    texCoord = (gl_TextureMatrix[0] * gl_MultiTexCoord0).xy;

    upVec = normalize(gbufferModelView[1].xyz);
    sunVec = GetSunVector();
    eastVec = normalize(gbufferModelView[0].xyz);

    #if defined LIGHTSHAFTS_ACTIVE && (LIGHTSHAFT_BEHAVIOUR == 1 && SHADOW_QUALITY >= 1 || defined END)
        vlFactor = texelFetch(colortex5, ivec2(viewWidth-1, viewHeight-1), 0).a;

        #ifdef END
            if (frameCounter % int(0.06666 / frameTimeSmooth + 0.5) == 0) { // Change speed is not too different above 10 fps

                #if MC_VERSION >= 12106
                    bool isEnderDragonDead = !heavyFog;
                #else
                    vec2 absCamPosXZ = abs(cameraPosition.xz);
                    float maxCamPosXZ = max(absCamPosXZ.x, absCamPosXZ.y);
                    bool isEnderDragonDead = gl_Fog.start / far > 0.5 || maxCamPosXZ > 350.0;
                #endif

                if (isEnderDragonDead) vlFactor = max(vlFactor - OSIEBCA*2, 0.0);
                else                   vlFactor = min(vlFactor + OSIEBCA*2, 1.0);
            }
        #endif
    #endif
}

#endif
