//Lighting Includes//
#include "/lib/colors/lightAndAmbientColors.glsl"
#include "/lib/lighting/ggx.glsl"
#include "/lib/lighting/minimumLighting.glsl"

#if SHADOW_QUALITY > -1 && (defined OVERWORLD || defined END) && !defined DH_TERRAIN && !defined DH_WATER && !defined VOXY_PATCH
    #include "/lib/lighting/shadowSampling.glsl"
#endif

#if HELD_LIGHTING_MODE >= 1
    #include "/lib/lighting/heldLighting.glsl"
#endif

#ifdef CLOUD_SHADOWS
    #include "/lib/lighting/cloudShadows.glsl"
#endif

#ifdef LIGHT_COLOR_MULTS
    #include "/lib/colors/colorMultipliers.glsl"
#endif

#if defined MOON_PHASE_INF_LIGHT || defined MOON_PHASE_INF_REFLECTION
    #include "/lib/colors/moonPhaseInfluence.glsl"
#endif

#if COLORED_LIGHTING_INTERNAL > 0
    #include "/lib/voxelization/lightVoxelization.glsl"
    #if BLOCKLIGHT_POINT_SHADOWS == 1
        #include "/lib/lighting/blockLightPointShadows.glsl"
    #endif
#endif

#ifdef DO_PIXELATION_EFFECTS
    #include "/lib/misc/pixelation.glsl"
#endif

vec3 highlightColor = normalize(pow(lightColor, vec3(0.37))) * (0.3 + 1.5 * sunVisibility2) * (1.0 - 0.85 * rainFactor);

//Lighting//
void DoLighting(inout vec4 color, inout vec3 shadowMult, vec3 playerPos, vec3 viewPos, float lViewPos, vec3 geoNormal, vec3 normalM, float dither,
                vec3 worldGeoNormal, vec2 lightmap, bool noSmoothLighting, bool noDirectionalShading, bool noVanillaAO,
                bool centerShadowBias, int subsurfaceMode, float smoothnessG, float highlightMult, float emission) {
    #ifdef DO_PIXELATION_EFFECTS
        vec2 pixelationOffset = ComputeTexelOffset(tex, texCoord);

        #if defined PIXELATED_SHADOWS || defined PIXELATED_BLOCKLIGHT
            vec3 playerPosPixelated = TexelSnap(playerPos, pixelationOffset);
        #endif

        #ifdef PIXELATED_SHADOWS
            #ifdef GBUFFERS_ENTITIES
                if (entityId == 50076) { // Boats
                    playerPosPixelated.y += 0.38; // consistentBOAT2176
                }
            #endif
            #ifdef GBUFFERS_TERRAIN
                if (subsurfaceMode == 1) {
                    playerPosPixelated.y += 0.05; // Fixes grounded foliage having dark bottom pixels depending on the random y-offset
                }
            #endif
        #endif
        #ifdef PIXELATED_BLOCKLIGHT
            if (!noSmoothLighting) {
                lightmap = clamp(TexelSnap(lightmap, pixelationOffset), 0.0, 1.0);
            }
        #endif
    #endif

    float NdotN = dot(normalM, northVec);
    float absNdotN = abs(NdotN);
    float NdotE = dot(normalM, eastVec);
    float absNdotE = abs(NdotE);
    float NdotL = dot(normalM, lightVec);

    float lightmapY2 = pow2(lightmap.y);
    float lightmapYM = smoothstep1(lightmap.y);
    float subsurfaceHighlight = 0.0;
    float ambientMult = 1.0;
    vec3 lightColorM = lightColor;
    vec3 ambientColorM = ambientColor;
    vec3 nViewPos = normalize(viewPos);

    #if defined LIGHT_COLOR_MULTS && !defined GBUFFERS_WATER // lightColorMult is defined early in gbuffers_water
        lightColorMult = GetLightColorMult();
    #endif

    #ifdef OVERWORLD
        float skyLightShadowMult = pow2(pow2(lightmapY2));
    #else
        float skyLightShadowMult = 1.0;
    #endif

    #if defined CUSTOM_PBR || defined GENERATED_NORMALS
        float NPdotU = abs(dot(geoNormal, upVec));
    #endif

    // Shadows
    #if defined OVERWORLD || defined END
        #ifdef GBUFFERS_WATER
            //NdotL = mix(NdotL, 1.0, 1.0 - color.a);
        #endif
        #ifdef CUSTOM_PBR
            float geoNdotL = dot(geoNormal, lightVec);
            float geoNdotLM = geoNdotL > 0.0 ? geoNdotL * 10.0 : geoNdotL;
            NdotL = min(geoNdotLM, NdotL);

            NdotL *= 1.0 - 0.7 * (1.0 - pow2(pow2(NdotUmax0))) * NPdotU;
        #endif
        #if SHADOW_QUALITY == -1 && (defined GBUFFERS_TERRAIN || defined DH_TERRAIN || defined DH_WATER || defined VOXY_PATCH)
            if (subsurfaceMode == 1) {
                NdotU = 1.0;
                NdotUmax0 = 1.0;
                NdotL = dot(upVec, lightVec);
            } else if (subsurfaceMode == 2) {
                highlightMult *= NdotL;
                NdotL = mix(NdotL, 1.0, 0.35);
            }

            subsurfaceMode = 0;
        #endif
        float NdotLmax0 = max0(NdotL);
        float NdotLM = NdotLmax0 * 0.9999;

        #ifdef GBUFFERS_TEXTURED
            NdotLM = 1.0;
        #else
            #ifdef GBUFFERS_TERRAIN
                if (subsurfaceMode != 0) {
                    #if defined CUSTOM_PBR && defined POM && POM_QUALITY >= 128 && POM_LIGHTING_MODE == 2
                        shadowMult *= max(pow2(pow2(dot(normalM, geoNormal))), sqrt2(NdotLmax0));
                    #endif
                    NdotLM = 1.0;
                }
                #ifdef SIDE_SHADOWING
                    else
                #endif
            #endif
            #ifdef SIDE_SHADOWING
                NdotLM = max0(NdotL + 0.4) * 0.714;

                #ifdef END
                    NdotLM = sqrt3(NdotLM);
                #endif
            #endif
        #endif

        #if ENTITY_SHADOW == -1 && defined GBUFFERS_ENTITIES || ENTITY_SHADOW <= 1 && defined GBUFFERS_BLOCK
            lightColorM = mix(lightColorM * 0.75, ambientColorM, 0.5 * pow2(pow2(1.0 - NdotLM)));
            NdotLM = NdotLM * 0.75 + 0.25;
        #endif

        if (shadowMult.r > 0.00001) {
            #if SHADOW_QUALITY > -1 && !defined DH_TERRAIN && !defined DH_WATER && !defined VOXY_PATCH
                if (NdotLM > 0.0001) {
                    vec3 shadowMultBeforeLighting = shadowMult;
                    float shadowLength = shadowDistance * 0.9166667 - lViewPos; //consistent08JJ622

                    if (shadowLength > 0.000001) {
                        #if SHADOW_SMOOTHING == 4 || SHADOW_QUALITY == 0
                            float offset = 0.0018;
                        #elif SHADOW_SMOOTHING == 3
                            float offset = 0.0014;
                        #elif SHADOW_SMOOTHING == 2
                            float offset = 0.0010;
                        #elif SHADOW_SMOOTHING == 1
                            float offset = 0.0006;
                        #endif

                        vec3 playerPosM = playerPos;
                        vec3 centerPlayerPos = floor(playerPos + cameraPosition + worldGeoNormal * 0.01) - cameraPosition + 0.5;

                        #if defined DO_PIXELATION_EFFECTS && defined PIXELATED_SHADOWS
                            playerPosM = playerPosPixelated;
                            offset *= 0.75;
                        #endif

                        // Fix light leaking in caves //
                        #ifdef GBUFFERS_TERRAIN
                            if (centerShadowBias || subsurfaceMode == 1) {
                                #ifdef OVERWORLD
                                    playerPosM = mix(centerPlayerPos, playerPosM, 0.5 + 0.5 * lightmapYM);
                                #endif
                            } else {
                                float centerFactor = max(glColor.a, lightmapYM);

                                #if defined PERPENDICULAR_TWEAKS && SHADOW_QUALITY >= 2
                                    // Fake Variable Penumbra Shadows
                                    // Making centerFactor also work in daylight if AO gradient is facing towards sun
                                    if (geoNdotU > 0.99) {
                                        float dFdxGLCA = dFdx(glColor.a);
                                        float dFdyGLCA = dFdy(glColor.a);

                                        if (abs(dFdxGLCA) + abs(dFdyGLCA) > 0.00001) {
                                            vec3 aoGradView = dFdxGLCA * normalize(dFdx(playerPos.xyz))
                                                            + dFdyGLCA * normalize(dFdy(playerPos.xyz));
                                            if (dot(normalize(aoGradView.xz), normalize(ViewToPlayer(lightVec).xz)) < 0.3 + 0.4 * dither)
                                                if (dot(lightVec, upVec) < 0.99999) centerFactor = sqrt1(max0(glColor.a - 0.55) / 0.45);
                                        }
                                    }
                                #endif

                                playerPosM = mix(playerPosM, centerPlayerPos, 0.2 * (1.0 - pow2(pow2(centerFactor))));
                            }
                        #elif defined GBUFFERS_HAND
                            playerPosM = mix(vec3(0.0), playerPosM, 0.2 + 0.8 * lightmapYM);
                        #elif defined GBUFFERS_TEXTURED
                            playerPosM = mix(centerPlayerPos, playerPosM + vec3(0.0, 0.02, 0.0), lightmapYM);
                        #else
                            playerPosM = mix(playerPosM, centerPlayerPos, 0.2 * (1.0 - lightmapYM));
                        #endif

                        // Shadow bias without peter-panning //
                        #ifndef GBUFFERS_TEXTURED
                            #ifdef GBUFFERS_TERRAIN
                                if (subsurfaceMode != 1)
                            #endif
                            {
                                float distanceBias = pow(dot(playerPos, playerPos), 0.75);
                                distanceBias = 0.12 + 0.0008 * distanceBias;
                                vec3 bias = worldGeoNormal * distanceBias * (2.0 - 0.95 * NdotLmax0); // 0.95 fixes pink petals noon shadows

                                #ifdef GBUFFERS_TERRAIN
                                    if (subsurfaceMode == 2) {
                                        bias *= vec3(0.0, 0.0, -0.5);
                                        bias.z += 0.25 * signMidCoordPos.x * NdotE;
                                    }
                                #endif

                                playerPosM += bias;
                            }
                        #endif

                        vec3 shadowPos = GetShadowPos(playerPosM);

                        bool leaves = false;
                        #ifdef GBUFFERS_TERRAIN
                            if (subsurfaceMode == 0) {
                                #if defined PERPENDICULAR_TWEAKS && defined SIDE_SHADOWING
                                    offset *= 1.0 + pow2(absNdotN);
                                #endif
                            } else {
                                float VdotL = dot(nViewPos, lightVec);
                                float lightFactor = pow(max(VdotL, 0.0), 10.0) * float(isEyeInWater == 0);
                                if (subsurfaceMode == 1) {
                                    offset = 0.0005235 * lightmapYM + 0.0009765;
                                    shadowPos.z -= max(NdotL * 0.0001, 0.0) * lightmapYM;
                                    subsurfaceHighlight = lightFactor * 0.8;
                                    #ifndef SHADOW_FILTERING
                                        shadowPos.z -= 0.0002;
                                    #endif
                                } else if (subsurfaceMode == 2) {
                                    leaves = true;
                                    offset = 0.0005235 * lightmapYM + 0.0009765;
                                    shadowPos.z -= 0.000175 * lightmapYM;
                                    subsurfaceHighlight = lightFactor * 0.6;
                                    #ifndef SHADOW_FILTERING
                                        NdotLM = mix(NdotL, NdotLM, 0.5);
                                    #endif
                                } else {
                                    
                                }
                            }
                        #endif
                        
                        int shadowSampleBooster = int(subsurfaceMode > 0 && lViewPos < 10.0);
                        #if SHADOW_QUALITY == 2 // Medium
                            int shadowSamples = 2 + 2 * shadowSampleBooster;
                        #elif SHADOW_QUALITY == 3 // High
                            int shadowSamples = 4 + 4 * shadowSampleBooster;
                        #elif SHADOW_QUALITY == 4 // Very High
                            int shadowSamples = 8 + 8 * shadowSampleBooster;
                        #elif SHADOW_QUALITY == 5 // Ultra
                            int shadowSamples = 16 + 16 * shadowSampleBooster;
                        #else
                            int shadowSamples = 0;
                        #endif

                        shadowMult *= GetShadow(shadowPos, lightmap.y, offset, shadowSamples, leaves, playerPos);
                    }

                    float shadowSmooth = 16.0;
                    if (shadowLength < shadowSmooth) {
                        float shadowMixer = max0(shadowLength / shadowSmooth);

                        #ifdef GBUFFERS_TERRAIN
                            if (subsurfaceMode != 0) {
                                float shadowMixerM = pow2(shadowMixer);

                                if (subsurfaceMode == 1) skyLightShadowMult *= mix(0.6 + 0.3 * pow2(noonFactor), 1.0, shadowMixerM);
                                else skyLightShadowMult *= mix(NdotL * 0.4999 + 0.5, 1.0, shadowMixerM);

                                subsurfaceHighlight *= shadowMixer;
                            }
                        #endif

                        shadowMult = mix(vec3(skyLightShadowMult * shadowMultBeforeLighting), shadowMult, shadowMixer);
                    }
                }
            #else
                #if SHADOW_QUALITY == -1 || !defined VOXY_PATCH
                    shadowMult *= skyLightShadowMult;
                #else
                    // Screenspace shadows rendered in deferred1 of previous frame for Voxy
                    // Previous frame reprojection from Chocapic13
                    vec3 screenSpaceShadowPos = vec3(gl_FragCoord.xy / vec2(viewWidth, viewHeight), gl_FragCoord.z);
                    vec4 viewPosPrev = vxProjInv * vec4(screenSpaceShadowPos * 2.0 - 1.0, 1.0);
                    viewPosPrev /= viewPosPrev.w;
                    
                    viewPosPrev = vxModelViewInv * viewPosPrev;

                    vec4 previousPosition = viewPosPrev + vec4(cameraPosition - previousCameraPosition, 0.0);
                    previousPosition = vxModelViewPrev * previousPosition;
                    previousPosition = vxProjPrev * previousPosition;
                    screenSpaceShadowPos.xy = previousPosition.xy / previousPosition.w * 0.5 + 0.5;

                    float screenSpaceShadowSample = texture2D(colortex18, screenSpaceShadowPos.xy).r;
                    if (screenSpaceShadowSample >= OSIEBCA) {
                        if (subsurfaceMode == 1) {
                            shadowMult *= 0.82 * (0.2 + 0.8 * sqrt2(max(SdotU, nightFactor)));
                        } else if (subsurfaceMode == 2) {
                            float baseLeafShadowMult = 1.0; 

                            #ifdef LEAF_SHADOW_OPTIMISATION
                                float extraLeafShadeMix = 0.25 + 0.125 * pow2(noonFactor);
                            #else
                                float extraLeafShadeMix = 0.125 + 0.125 * pow2(noonFactor);
                            #endif

                            shadowMult *= mix(baseLeafShadowMult, NdotU, extraLeafShadeMix);
                        }
                        shadowMult *= screenSpaceShadowSample * skyLightShadowMult;
                    } else {
                        shadowMult *= skyLightShadowMult;
                    }
                #endif
            #endif

            #ifdef CLOUD_SHADOWS
                shadowMult *= GetCloudShadow(playerPos);
            #endif

            shadowMult *= max(NdotLM * shadowTime, 0.0);
        }
        #ifdef GBUFFERS_WATER
            else { // Low Quality Water
                shadowMult = vec3(pow2(lightmapY2) * max(NdotLM * shadowTime, 0.0));
            }
        #endif
    #endif

    // Subtle shadow-area deepening: preserve fully lit surfaces, but make
    // already-shadowed areas a little richer/darker without crushing them.
    // This is intentionally mild so ambient bounce still keeps detail.
    {
        float shadowDarkness = 1.0 - min1(GetLuminance(shadowMult));
        float shadowLiftCut = 1.0 - shadowDarkness * 0.02;
        shadowMult *= shadowLiftCut;
    }

    // Blocklight
    float lightmapXM;
    #ifdef NOBLE_BLOCKLIGHT_FALLOFF
        // v4.1: torch/blocklight radius+falloff shape ported from Noble Shaders'
        // getBlocklightFalloff() (a physically-based inverse-square falloff over
        // Minecraft's 0-15 lightmap steps). Rescaled to Unbound's own blocklight
        // intensity range (computed at lightmap.x == 1) so the overall brightness
        // balance elsewhere in the pipeline (ACT, held light, etc.) is unaffected -
        // only how far/large the light visually reaches changes.
        // ("torch radius kurang besar/luas" fix): eksponen 2.6 dulu
        // bikin cahaya turun terlalu cepat begitu menjauh dari sumbernya,
        // jadi jangkauannya kelihatan sempit. Diturunkan ke 2.2 supaya
        // falloff-nya lebih landai/lebar - torch/lampu menyebar lebih
        // jauh tanpa mengubah kecerahan tepat di sumbernya.
        // ("radius cahaya block di lebarkan lagi" fix, round 2): 2.2
        // diturunkan lagi ke 1.8 - makin kecil eksponennya, makin landai
        // kurva falloff-nya, jadi cahaya torch/lampu/lava menjangkau lebih
        // jauh lagi dari sumbernya sebelum benar-benar meredup ke nol,
        // TANPA membuat block sumber cahayanya sendiri lebih terang
        // (kecerahan di titik lightmap.x==1 tetap sama, cuma bentuk
        // kurva peluruhannya yang melebar).
        // ("radius torch/lantern jangan terlalu luas" fix, round 3):
        // dinaikkan lagi dari 1.8 -> 2.1. Makin besar eksponennya, makin
        // curam/cepat kurva falloff-nya meluruh, jadi jangkauan cahaya
        // block sedikit dipersempit lagi dibanding round 2, tanpa balik
        // sesempit versi lama (2.6/2.2).
        float nobleFalloffShape = linearStep(0.00390625, 1.0, 1.0 / pow(16.0 - 15.0 * lightmap.x, 2.1));

        if (!noSmoothLighting) {
            float unboundFullBrightSteep = (2.8 - 0.6 * vsBrightness + XLIGHT_CURVE);
            float unboundFullBrightCalm  = (2.8 + 0.6 * vsBrightness - XLIGHT_CURVE);
            float unboundFullBright = pow(unboundFullBrightSteep + unboundFullBrightCalm, 2.25);
            lightmapXM = nobleFalloffShape * unboundFullBright;
        } else {
            float xLightCurveM = XLIGHT_CURVE > 0.999 ? XLIGHT_CURVE : sqrt2(XLIGHT_CURVE);
            float unboundFullBright = pow(1.0, 3.0 * xLightCurveM) * 10.0;
            lightmapXM = nobleFalloffShape * unboundFullBright;
        }
    #else
    if (!noSmoothLighting) {
        float lightmapXMSteep = pow2(pow2(lightmap.x * lightmap.x))  * (2.8 - 0.6 * vsBrightness + XLIGHT_CURVE);
        float lightmapXMCalm = (lightmap.x) * (2.8 + 0.6 * vsBrightness - XLIGHT_CURVE);
        lightmapXM = pow(lightmapXMSteep + lightmapXMCalm, 2.25);
    } else {
        float xLightCurveM = XLIGHT_CURVE > 0.999 ? XLIGHT_CURVE : sqrt2(XLIGHT_CURVE);
        lightmapXM = pow(lightmap.x, 3.0 * xLightCurveM) * 10.0;
    }
    #endif

    float daylightFactor = lightmapYM * invRainFactor * sunVisibility;

    // --- Sun-suppressed emissive blocks ("torch mati kena matahari,
    // nyala lagi di tempat gelap") ---
    // Old version only used lightmapYM (sky exposure) - a block near an
    // open window could get dimmed even while sitting fully in shade,
    // since skylight leaks in regardless of whether the sun is actually
    // hitting THAT surface. shadowMult (computed above from the real
    // shadow map, same value the main diffuse lighting uses) already
    // accounts for actual sun occlusion by terrain/blocks/roof - so a
    // torch under a roof stays lit even in an otherwise sunlit room,
    // while one standing in the open goes dark, matching how the block
    // itself would look if the sun could actually reach it.
    float emissiveSunHit = daylightFactor * clamp(GetLuminance(shadowMult), 0.0, 1.0);
    emission *= 1.0 - EMISSIVE_SUN_SUPPRESS * emissiveSunHit;

    #ifdef GBUFFERS_TEXTURED
        lightmapXM *= 1.5 - 0.5 * daylightFactor; // Brighter lit particles
    #endif

    #if BLOCKLIGHT_FLICKERING > 0
        // v9.0: Enhanced torch/lantern flicker for more realistic firelight
        // Multi-frequency noise for natural flame-like movement
        vec2 flickerNoise = texture2DLod(noisetex, vec2(frameTimeCounter * 0.06), 0.0).rb;
        vec2 flickerNoise2 = texture2DLod(noisetex, vec2(frameTimeCounter * 0.15 + 0.5), 0.0).rg;
        float flicker = mix(1.0, min1(max(flickerNoise.r, flickerNoise.g) * 1.7), pow2(BLOCKLIGHT_FLICKERING * 0.1));
        flicker *= mix(1.0, min1(max(flickerNoise2.r, flickerNoise2.g) * 1.3), pow2(BLOCKLIGHT_FLICKERING * 0.05));
        lightmapXM *= flicker;
    #endif

    vec3 blockLighting = lightmapXM * blocklightCol;
    #if COLORED_LIGHTING_INTERNAL > 0 && SOLAR_GI == 1 && defined OVERWORLD
        vec3 solarGIIndirect = vec3(0.0);
    #endif

    #if COLORED_LIGHTING_INTERNAL > 0
        // Prepare
        #if defined GBUFFERS_HAND
            vec3 voxelPos = SceneToVoxel(vec3(0.0));
        #elif defined GBUFFERS_TEXTURED
            vec3 voxelPos = SceneToVoxel(playerPos);
        #else
            vec3 voxelPos = SceneToVoxel(playerPos);
            voxelPos = voxelPos + worldGeoNormal * 0.55; // should be close to 0.5 for ACT_CORNER_LEAK_FIX but 0.5 makes slabs flicker
        #endif

        vec3 specialLighting = vec3(0.0);
        vec4 lightVolume = vec4(0.0);
        if (CheckInsideVoxelVolume(voxelPos)) {
            vec3 voxelPosM = clamp01(voxelPos / vec3(voxelVolumeSize));
            lightVolume = GetLightVolume(voxelPosM);
            lightVolume = sqrt(lightVolume);
            specialLighting = lightVolume.rgb;

            #if SOLAR_GI == 1 && defined OVERWORLD
                // Solar GI shares ACT's volume, but must not be tied to the vanilla block-light
                // lightmap. The low-alpha band is a conservative marker for propagated
                // solar-bounce energy (see the vec4(pow2(sunBounce), luminance*0.026...0.085)
                // encoding in shadowcomp.glsl); high-alpha special lights (lava/portals/etc.,
                // encoded with alpha up to 1.0) are rejected so this never double-counts them.
                float solarGIMarker = smoothstep(0.012, 0.055, lightVolume.a)
                                    * (1.0 - smoothstep(0.30, 0.48, lightVolume.a));
                vec3 solarGIVolume = min(lightVolume.rgb, vec3(2.5));
                float solarGILum = max(GetLuminance(solarGIVolume), 0.0001);
                solarGIVolume *= min(1.0, 1.35 / solarGILum);
                solarGIIndirect = solarGIVolume * solarGIMarker * SOLAR_GI_SURFACE_FILL;

                // Spatial stability: fade Solar GI through the outer band of ACT's camera-
                // centered volume instead of letting newly entering/leaving voxels pop.
                #if SOLAR_GI_EDGE_FADE > 0
                    vec3 giEdgeDistance = min(voxelPosM, vec3(1.0) - voxelPosM);
                    float giNearestEdge = min(giEdgeDistance.x, min(giEdgeDistance.y, giEdgeDistance.z));
                    float giVolumeFade = smoothstep(0.0, SOLAR_GI_EDGE_FADE, giNearestEdge);
                    solarGIIndirect *= giVolumeFade;
                #endif
            #endif

            // Directional block/torch light shading: without this, block
            // light only ever comes from the flat lightmap.x value, so every
            // face around a torch reads the same brightness regardless of
            // which way it faces - a flat wash rather than light actually
            // coming from a point in space. Estimate the local light
            // gradient (the direction brightness increases fastest, i.e.
            // roughly toward the source) from 6 cheap raw texel taps around
            // this voxel, then shade by how directly this surface faces
            // that direction - the side of a block nearest the torch gets
            // brighter, the side facing away falls off, like a real point
            // light instead of an even fill.
            #if BLOCKLIGHT_NORMAL_SHADING == 1
                if (GetLuminance(specialLighting) > 0.001) {
                    ivec3 voxelPosI = ivec3(floor(voxelPos));

                    float lx1 = GetLuminance(GetLightVolumeRaw(voxelPosI + ivec3( 1, 0, 0)).rgb);
                    float lx0 = GetLuminance(GetLightVolumeRaw(voxelPosI + ivec3(-1, 0, 0)).rgb);
                    float ly1 = GetLuminance(GetLightVolumeRaw(voxelPosI + ivec3( 0, 1, 0)).rgb);
                    float ly0 = GetLuminance(GetLightVolumeRaw(voxelPosI + ivec3( 0,-1, 0)).rgb);
                    float lz1 = GetLuminance(GetLightVolumeRaw(voxelPosI + ivec3( 0, 0, 1)).rgb);
                    float lz0 = GetLuminance(GetLightVolumeRaw(voxelPosI + ivec3( 0, 0,-1)).rgb);

                    vec3 lightGradient = vec3(lx1 - lx0, ly1 - ly0, lz1 - lz0);
                    float gradLen = length(lightGradient);

                    if (gradLen > 0.0001) {
                        vec3 lightDir = lightGradient / gradLen; // points toward the brighter side (roughly toward the source)
                        float NdotL = clamp(dot(worldGeoNormal, lightDir), -1.0, 1.0);

                        // Remapped so a face directly toward the source stays
                        // at full brightness and a face directly away falls
                        // to (1 - strength) rather than pure black - a real
                        // torch's back side still gets a little bounce light
                        // from nearby surfaces, it isn't a hard shadow edge.
                        float dirFactor = mix(1.0, 0.35 + 0.65 * (NdotL * 0.5 + 0.5), BLOCKLIGHT_NORMAL_STRENGTH);
                        specialLighting *= dirFactor;
                    }
                }
            #endif

            #if BLOCKLIGHT_POINT_SHADOWS == 1
                // Block-Light Point Shadows: adds real per-source shadow
                // rays on top of the diffuse flood-fill above (see
                // blockLightPointShadows.glsl) - two lamps in the same
                // room stay two distinct, individually shadow-rayed light
                // pools instead of blending into the flood-fill's single
                // averaged glow. Now also direction-aware (VTXS SSLL-style
                // geometry term): passes worldGeoNormal in so each lamp's
                // contribution is weighted by how directly this face
                // points toward THAT specific lamp, not just distance.
                specialLighting += GetBlockLightPointShadows(voxelPos, worldGeoNormal, dither);
            #endif
        }

        // Add extra articial light for blocks that request it
        lightmapXM = max(lightmapXM, mix(lightmapXM, 10.0, lightVolume.a));
        specialLighting *= 1.0 + 50.0 * lightVolume.a;

        // Color Balance
        specialLighting = lightmapXM * ACT_BLOCKLIGHT_BRIGHTNESS * DoReducedLuminanceCorrection(specialLighting + blocklightCol * 0.04, 0.25);

        // Add some extra non-contrasty detail
        AddSpecialLightDetail(specialLighting, color.rgb, emission);

        #if COLORED_LIGHT_SATURATION != 100
            specialLighting = mix(blockLighting, specialLighting, COLORED_LIGHT_SATURATION * 0.01);
        #endif

        // Serve with distance fade
        vec3 absPlayerPosM = abs(playerPos);
        #if COLORED_LIGHTING_INTERNAL <= 512
            absPlayerPosM.y *= 2.0;
        #elif COLORED_LIGHTING_INTERNAL == 768
            absPlayerPosM.y *= 3.0;
        #elif COLORED_LIGHTING_INTERNAL == 1024
            absPlayerPosM.y *= 4.0;
        #endif
        float maxPlayerPos = max(absPlayerPosM.x, max(absPlayerPosM.y, absPlayerPosM.z));
        float blocklightDecider = pow2(min1(maxPlayerPos / effectiveACTdistance * 2.0));
        //if (heldItemId != 40000 || heldItemId2 == 40000) // Hold spider eye to see vanilla lighting
        blockLighting = mix(specialLighting, blockLighting, blocklightDecider);
        //if (heldItemId2 == 40000 && heldItemId != 40000) blockLighting = lightVolume.rgb; // Hold spider eye to see light volume
    #endif

    #ifdef OVERWORLD
        // [Edited per request] Originally: torches, lanterns, glowstone and other block light
        // sources stayed dormant through the day and gradually ignited as night falls
        // (nightFactor: 0.0 = day, 1.0 = night). Now block light stays on at full strength
        // at all times, day or night.
        //blockLighting *= nightFactor;
    #endif

    #if HELD_LIGHTING_MODE >= 1
        #if !defined DO_PIXELATION_EFFECTS || !defined PIXELATED_BLOCKLIGHT
            vec3 playerPosForHeldLighting = playerPos;
        #else
            vec3 playerPosForHeldLighting = playerPosPixelated;
        #endif

        vec3 heldLighting = GetHeldLighting(playerPosForHeldLighting, color.rgb, emission);

        #ifdef GBUFFERS_HAND
            blockLighting *= 0.5;
            heldLighting *= 2.0;
        #endif
    #endif

    vec3 minLighting = GetMinimumLighting(lightmapYM);
    vec3 shadowLightMult = shadowMult;
    float shadowMultFloat = min1(GetLuminance(shadowMult));

    // Lighting Tweaks
    #ifdef OVERWORLD
        ambientMult = mix(lightmapYM, pow2(lightmapYM) * lightmapYM, rainFactor);

        #if SHADOW_QUALITY == -1
            float tweakFactor = 1.0 + 0.6 * (1.0 - pow2(pow2(pow2(noonFactor))));
            lightColorM /= tweakFactor;
            ambientMult *= mix(tweakFactor, 1.0, 0.5 * NdotUmax0);
        #endif

        #if AMBIENT_MULT != 100
            #if AMBIENT_MULT < 100
                #define AMBIENT_MULT_M (AMBIENT_MULT - 100) * 0.006
                vec3 shadowMultP = shadowMult / (0.1 + 0.9 * sqrt2(max0(NdotLM)));
                ambientMult *= 1.0 + pow2(pow2(max0(1.0 - dot(shadowMultP, shadowMultP)))) * AMBIENT_MULT_M *
                            (0.5 + 0.2 * sunFactor + 0.8 * noonFactor) * (1.0 - rainFactor * 0.5);
            #else
                #define AMBIENT_MULT_M (AMBIENT_MULT - 100) * 0.002
                shadowLightMult = mix(shadowLightMult, vec3(1.0), AMBIENT_MULT_M * ambientMult);
                lightColorM = mix(lightColorM, GetLuminance(lightColorM) * DoLuminanceCorrection(ambientColorM), (1.0 - shadowMultFloat) * AMBIENT_MULT_M);
            #endif
        #endif

        if (isEyeInWater != 1) {
            float lxFactor = (sunVisibility2 * 0.4 + (0.6 - 0.6 * pow2(invNoonFactor))) * (6.0 - 5.0 * rainFactor);
            lxFactor *= lightmapY2 + lightmapY2 * 2.0 * pow2(shadowMultFloat);
            lxFactor = max0(lxFactor - emission * 1000000.0);
            blockLighting *= pow(lightmapXM / 60.0 + 0.001, 0.09 * lxFactor);

            // Less light in the distance / more light closer to the camera during rain or night to simulate thicker fog
            float rainLF = 0.1 * rainFactor;
            float lightFogTweaks = 1.0 + max0(96.0 - lViewPos) * (0.002 * (1.0 - sunVisibility2) + 0.0104 * rainLF) - rainLF;
            ambientMult *= lightFogTweaks;
            lightColorM *= lightFogTweaks;
        }
    #endif
    #ifdef END
        #ifdef END_FLASHES
            vec3 worldEndFlashPosition = mat3(gbufferModelViewInverse) * endFlashPosition;
            worldEndFlashPosition = normalize(vec3(
                worldEndFlashPosition.x,
                0.15 * (END_BEAM_CENTER_ALT - playerPos.y),
                worldEndFlashPosition.z
            ));
            float endFlashDirectionFactor = max0(1.0 + dot(worldGeoNormal, normalize(worldEndFlashPosition))) * 0.5;
                  endFlashDirectionFactor = pow2(pow2(endFlashDirectionFactor));

            vec3 endFlashColor = (endOrangeCol + endLightColor) * endFlashIntensityM * pow2(lightmapYM);
            ambientColorM += endFlashColor * (0.2 * endFlashDirectionFactor);
        #endif
    #endif

    #ifdef GBUFFERS_HAND
        ambientMult *= 1.3; // To improve held map visibility
    #endif

    // Directional Shading
    float directionShade = 1.0;
    #ifdef DIRECTIONAL_SHADING
        if (!noDirectionalShading) {
            float absNdotE2 = pow2(absNdotE);

            #if !defined NETHER
                float NdotUM = 0.75 + NdotU * 0.25;
            #else
                float NdotUM = 0.75 + abs(NdotU + 0.5) * 0.16666;
            #endif
            float NdotNM = 1.0 + 0.075 * absNdotN;
            float NdotEM = 1.0 - 0.1 * absNdotE2;
            directionShade = NdotUM * NdotEM * NdotNM;

            #ifdef OVERWORLD
                lightColorM *= 1.0 + absNdotE2 * 0.75;
            #elif defined NETHER
                directionShade *= directionShade;
                ambientColorM += lavaLightColor * pow2(absNdotN * 0.5 + max0(-NdotU)) * (0.7 + 0.35 * vsBrightness);
            #endif

            #if defined CUSTOM_PBR || defined GENERATED_NORMALS
                float cpbrAmbFactor = NdotN * NPdotU;
                cpbrAmbFactor = 1.0 - 0.3 * cpbrAmbFactor;
                ambientColorM *= cpbrAmbFactor;
                minLighting *= cpbrAmbFactor;
            #endif

            #if defined OVERWORLD && defined PERPENDICULAR_TWEAKS && defined SIDE_SHADOWING
                // Fake bounced light
                // ("light bounce ada garis dobel di tengah" fix): term ini
                // sebelumnya di-mix pakai lightmapY2 (skylight per-blok dari
                // vanilla smooth-lighting) yang sepenuhnya independen dari
                // bentuk shadow map sinar matahari yang sebenarnya. Di dekat
                // sudut ruangan/jendela, nilai skylight antar blok berubah
                // cukup tajam, sehingga glow palsu ini membentuk tepi
                // sendiri yang tidak sejajar dengan tepi sinar matahari asli
                // -> tampak seperti 2 proyeksi cahaya bertumpuk (garis
                // dobel). Dikalikan shadowMultFloat (visibilitas bayangan
                // real per-pixel, sudah dihitung di atas) supaya glow palsu
                // ini mengikuti footprint cahaya yang sebenarnya, bukan
                // cuma nilai skylight blok yang terputus-putus.
                ambientColorM = mix(ambientColorM, lightColorM, (0.05 + 0.03 * subsurfaceMode) * absNdotN * lightmapY2 * shadowMultFloat);

                // Get a bit more natural looking lighting during noon
                lightColorM *= 1.0 + max0(1.0 - subsurfaceMode) * pow(noonFactor, 20.0) * (pow2(absNdotN) * 0.8 - absNdotE2 * 0.2);
            #endif
        }
    #endif

    // Scene Lighting Stuff
    vec3 sceneLighting = lightColorM * shadowLightMult + ambientColorM * ambientMult;
    float dotSceneLighting = dot(sceneLighting, sceneLighting);

    #if HELD_LIGHTING_MODE >= 1
        blockLighting = sqrt(pow2(blockLighting) + heldLighting);
    #endif

    blockLighting *= XLIGHT_I;

    #ifdef LIGHT_COLOR_MULTS
        sceneLighting *= lightColorMult;
    #endif
    #ifdef MOON_PHASE_INF_LIGHT
        sceneLighting *= moonPhaseInfluence;
    #endif

    // Vanilla Ambient Occlusion
    float vanillaAO = 1.0;
    #if VANILLAAO_I > 0
        vanillaAO = glColor.a;

        #if defined DO_PIXELATION_EFFECTS && defined PIXELATED_AO
            vanillaAO = TexelSnap(vanillaAO, pixelationOffset);
        #endif

        if (subsurfaceMode != 0) vanillaAO = mix(min1(vanillaAO * 1.15), 1.0, shadowMultFloat);
        else if (!noVanillaAO) {
            #ifdef GBUFFERS_TERRAIN
                vanillaAO = min1(vanillaAO + 0.08);
                #ifdef OVERWORLD
                    vanillaAO = pow(
                        pow1_5(vanillaAO),
                        1.0 + dotSceneLighting * 0.02 + NdotUmax0 * (0.15 + 0.25 * pow2(noonFactor * pow2(lightmapY2)))
                    );
                #elif defined NETHER
                    vanillaAO = pow(
                        pow1_5(vanillaAO),
                        1.0 + NdotUmax0 * 0.5
                    );
                #else
                    vanillaAO = pow(
                        vanillaAO,
                        0.75 + NdotUmax0 * 0.25
                    );
                #endif
            #endif
            vanillaAO = vanillaAO * 0.9 + 0.1;

            #if VANILLAAO_I != 100
                #define VANILLAAO_IM VANILLAAO_I * 0.01
                vanillaAO = pow(vanillaAO, VANILLAAO_IM);
            #endif
        }
    #endif

    // Light Highlight
    vec3 lightHighlight = vec3(0.0);
    #ifdef LIGHT_HIGHLIGHT
        float specularHighlight = GGX(normalM, nViewPos, lightVec, NdotLmax0, smoothnessG);

        specularHighlight *= highlightMult;

        lightHighlight = isEyeInWater != 1 ? shadowMult : pow(shadowMult, vec3(0.25)) * 0.35;
        lightHighlight *= (subsurfaceHighlight + specularHighlight) * highlightColor;

        #ifdef LIGHT_COLOR_MULTS
            lightHighlight *= lightColorMult;
        #endif
        #ifdef MOON_PHASE_INF_REFLECTION
            lightHighlight *= pow2(moonPhaseInfluence);
        #endif
    #endif

    // Mix Colors
    #if COLORED_LIGHTING_INTERNAL > 0 && SOLAR_GI == 1 && defined OVERWORLD
        // Treat Solar GI as true indirect diffuse rather than colored block-light. Keep it
        // strongest in shadow, but preserve a user-controlled fraction under direct sunlight.
        float solarGIVisibility = mix(1.0, SOLAR_GI_SUNLIT_VISIBILITY, shadowMultFloat);
        solarGIVisibility *= mix(SOLAR_GI_SHADOW_BOOST, 1.0, shadowMultFloat);
        // ACT block light and Solar GI share one flood-fill volume, so low-energy ACT light can
        // overlap the GI marker band. Use vanilla skylight only as a final enclosure safety gate:
        // outdoor shadows retain full GI, cave mouths taper naturally, and zero-skylight cave
        // interiors cannot reinterpret ordinary ACT light as Solar GI.
        float solarGISkyAccess = smoothstep(0.025, 0.32, lightmapYM);
        solarGIVisibility *= solarGISkyAccess;
        vec3 solarGIDiffuse = solarGIIndirect * solarGIVisibility;
    #else
        vec3 solarGIDiffuse = vec3(0.0);
    #endif

    vec3 finalDiffuse = pow2(directionShade * vanillaAO) * (blockLighting + pow2(sceneLighting) + minLighting + solarGIDiffuse) + pow2(emission);
    finalDiffuse = sqrt(max(finalDiffuse, vec3(0.0))); // sqrt() for a bit more realistic light mix, max() to prevent NaNs

    // Apply Lighting
    color.rgb *= finalDiffuse;
    color.rgb += lightHighlight;
    color.rgb *= pow2(1.0 - darknessLightFactor);
}