/////////////////////////////////////
// Unbound Visual + PT v7.3 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;
    #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 == 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 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"
#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

        // --- 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
                if (dot(caoGiNormal, playerPos) > 0.0) caoGiNormal = -caoGiNormal;
                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, flipped 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).
        #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
            if (dot(giNormal, playerPos) > 0.0) giNormal = -giNormal; // Face the camera
        #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". This is worst exactly at midday: a
                // flat, upward-facing surface (the ground) gets the highest
                // possible NdotL against a high sun, so the direct term
                // being self-sampled is at its brightest, reading as an
                // oversized, flat bounce glow. Offsetting by ~1.5 voxels
                // along the normal moves the sample into the neighboring
                // (air) voxel, which only ever receives light that actually
                // propagated there through the gather passes, not the raw
                // unpropagated direct term of the surface itself.
                vec3 giSampleScenePos = playerPos + giNormal * (GI_VOXEL_SIZE * 1.5);

                // --- 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.
                vec3 giTraceSeedWorld = giSampleScenePos + cameraPosition;
                vec2 giTraceSeed = GiHash22(giTraceSeedWorld.xz * 3.73 + giTraceSeedWorld.y * 7.91);
                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.05, 0.85, giRoomSize) * mix(0.75, 1.0, giEnclosure);

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

                // --- 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: on a perfectly flat floor
                // or ceiling (normal very close to straight up/down), the
                // bounce often over-accumulates because the direct sun term
                // is at its maximum NdotL and there is no surrounding geometry
                // to create meaningful indirect light. Reference photos (user-
                // provided) both show FLAT surfaces (floor) as the ones most
                // strongly covered in bounced color, not the least - so this
                // is now only a mild floor/ceiling taper (0.85), just enough
                // to avoid a completely flat/uniform wash, not a heavy cut.
                float flatness = 1.0 - abs(giNormal.y);
                float flatAtten = smoothstep(0.0, 0.15, flatness);
                giSample *= mix(0.85, 1.0, flatAtten);

                // --- Saturation boost ---
                // ("bounce kurang saturasi" fix, then increased to match
                // reference photos): reference shows strongly saturated,
                // room-filling bounce color (e.g. a fully orange floor next
                // to an orange wall), not a light tint.
                // ("block putih pekat di pojok/corner" fix): 1.6 sat boost
                // pushed individual channels toward 1.0 in corners where the
                // hemisphere over-accumulates (bounce from 2-3 faces at once),
                // reading as solid white. Lowered to 1.25 so color stays
                // vivid but corners keep detail.
                {
                    float giSampleLum = dot(giSample, vec3(0.2126, 0.7152, 0.0722));
                    giSample = mix(vec3(giSampleLum), giSample, 1.25);
                }

                // Soft-knee energy compression - knee raised so saturated
                // bounce color has more headroom before getting compressed
                // toward gray/white, keeping it vivid like the reference.
                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) -----------------------------
                vec3 giScreenSpaceContribution = vec3(0.0);
                #if SSGI > 0
                    if (!entityOrParticle) giScreenSpaceContribution = 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);

                    if (clamp(giPrevCoord, 0.0, 1.0) == giPrevCoord) {
                        vec4 giHistory = texture2D(colortex9, giPrevCoord);

                        // 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, giPrevCoord).r);
                        vec3  giCenterN    = normalize(texture2D(colortex4, giPrevCoord).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 = giPrevCoord + giTapOffsets[t];
                            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.
                        // ("ghosting masih agresif" fix): 0.55x-1.7x let a
                        // stale sample drift quite far before getting pulled
                        // back in line, which is what read as bounce light
                        // dragging/smearing behind moving lights or the
                        // player. Narrowed to 0.75x-1.35x.
                        vec3 minHist = giHistorySmooth * 0.80 - vec3(0.02);
                        vec3 maxHist = giHistorySmooth * 1.25 + vec3(0.04);
                        vec3 giSampleClamped = clamp(giSample, max(vec3(0.0), minHist), maxHist);

                        float histLum = dot(giHistorySmooth, vec3(0.2126, 0.7152, 0.0722));
                        float curLum  = dot(giSampleClamped, vec3(0.2126, 0.7152, 0.0722));
                        float disocclusion = clamp(abs(histLum - curLum) / (max(histLum, curLum) + 0.10), 0.0, 1.0);
                        float cameraMotion = min(1.0, length(cameraOffset) * 1.6);

                        // IC converges faster, so higher base alpha = smoother
                        // ("gerak-gerak terus" fix): naikkan berat history di
                        // kondisi statis agar GI tidak "lari" + flicker tiap
                        // frame. Saat ada gerakan/perubahan besar baru perbanyak
                        // sampling fresh.
                        // ("bounce/ghosting agresif banget" fix): 0.18 base
                        // meant 82% history even standing still, which is a
                        // lot of inertia for light to "catch up" through -
                        // reads as bounce lagging/smearing behind anything
                        // that changes (moving light, player, foliage). Raised
                        // the floor and the motion-reactive ceiling so fresh
                        // samples count for more throughout, while still
                        // smoothing out per-frame noise.
                        float alpha = mix(0.25, 0.75, max(disocclusion * 0.5, cameraMotion * 0.5));

                        // 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);
                        giHistoryOut = vec4(giAccumulated, 1.0);
                    } else {
                        vec2 giEdgeCoord = clamp(giPrevCoord, 0.0, 1.0);
                        vec3 giEdgeHistory = texture2D(colortex9, giEdgeCoord).rgb;
                        float edgeAlpha = entityOrParticle ? 1.0 : 0.40;
                        giAccumulated = mix(giEdgeHistory, giSample, edgeAlpha);
                        giHistoryOut = vec4(giAccumulated, 1.0);
                    }
                }

                // Soft watercolor denoise (lighter since IC is less noisy)
                {
                    // Radius/strength bumped up a bit ("bulet-bulet" fix,
                    // part 2): even with the world-locked seed above, 6 rays/
                    // pixel is still a low sample count, so single-frame
                    // variance is still visible as soft blotches right after
                    // the seed changes what it's sampling (e.g. a bounce
                    // light source itself moves/flickers, or GI_RAYS_PER_PIXEL
                    // is lowered in settings). A slightly wider/stronger
                    // spatial pass leans on neighbouring pixels more to hide
                    // that per-frame variance without needing more rays.
                    // Denoise radius/strength scale inversely with the ray
                    // budget (GI_RAYS_PER_PIXEL, a compile-time constant per
                    // profile) instead of being one fixed value for every
                    // profile. Cheaper profiles (fewer rays/pixel = noisier
                    // raw samples) lean harder on neighbouring pixels to stay
                    // clean; profiles with more rays already have clean raw
                    // samples so a lighter touch keeps detail crisp. This
                    // costs nothing extra at render time (same number of
                    // taps either way) - it just spends the existing
                    // denoise pass more effectively, so bounce reads less
                    // blotchy at the SAME per-frame ray cost rather than
                    // needing MORE rays (i.e. more GPU time) to look clean.
                    // Reference point: GI_RAYS_PER_PIXEL=8 (STANDARD) keeps
                    // this pack's previous tuned defaults (5.5px / 0.30)
                    // unchanged.
                    const float wcRadiusPx = clamp(5.5 + (8.0 - float(GI_RAYS_PER_PIXEL)) * 0.4, 4.0, 8.0);
                    const int   wcTaps = 8;
                    const float wcStrength = clamp(0.30 + (8.0 - float(GI_RAYS_PER_PIXEL)) * 0.03, 0.18, 0.42);
                    vec2  wcTexel  = 1.0 / vec2(viewWidth, viewHeight);
                    float wcCenterLinZ = GetLinearDepth(z0);
                    vec3  wcCenterN    = giNormal;

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

                        float depthWeight  = exp(-abs(tapLinZ - wcCenterLinZ) / max(wcCenterLinZ * 0.05, 0.05));
                        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);
                    giAccumulated = mix(giAccumulated, giSoftBlurred, wcStrength);
                    giHistoryOut  = vec4(giAccumulated, giHistoryOut.a);
                }
                #else
                    giHistoryOut = vec4(giSample, 1.0);
                #endif

                giVoxel = (giAccumulated + giScreenSpaceContribution) * (1.0 - ssao * 0.5);
            }
        #endif

// GI combine: when voxel GI is active, giVoxel already includes the
        // SSGI contribution - added back in AFTER the temporal/spatial
        // denoiser (see "SSGI is added HERE" above), specifically so SSGI
        // stays visible during motion instead of being clamped down to the
        // voxel path's slower-moving history. When voxel GI is off, SSGI's
        // own (now normal-aware, occlusion-tested) signal is added directly.
        // 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
            color.rgb += giVoxel * (entityOrParticle ? 0.0 : 1.0);
        #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);
                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
