
uniform sampler2D shadowtex1;
uniform sampler2D shadowcolor0;
uniform sampler2D shadowcolor1;

#define PTGI_STYLE_GI 1              // [0 1] Enable PTGI-like noise-based GI
#define PTGI_BOUNCE_INTENSITY 0.8    // [0.3 0.5 0.8 1.0 1.5 2.0] GI bounce light intensity
#define PTGI_NOISE_SCALE 0.7         // [0.3 0.5 0.7 1.0 1.5] Noise overlay strength
#define PTGI_TEMPORAL_BLEND 0.85     // [0.7 0.8 0.85 0.9 0.95] Temporal stability (higher = more stable)

// Multi-bounce GI settings
#define PTGI_BOUNCES 2               // [1 2 3] Number of light bounces
#define PTGI_SAMPLES_PER_BOUNCE 16   // [8 16 32] Samples per bounce level

//============================================================================//
// COLORED BLOCKS CONFIGURATION
//============================================================================//

#define COLORED_BLOCKS_ENABLED 1     // [0 1] Enable colored block GI
#define COLORED_BLOCK_INTENSITY 1.5  // [1.0 1.5 2.0 3.0 5.0] Color brightness multiplier
#define COLORED_BLOCK_RADIUS 10.0    // [6.0 8.0 10.0 12.0 16.0] Max distance for colored light

//============================================================================//
// VXGI CONFIGURATION
//============================================================================//

#define VXGI_ENABLED
#define VOXEL_RADIUS 64              // Half-size of voxel grid [32 64 128]

// VOXEL AO SETTINGS
#define VXGI_INTENSITY 0.4           // [0.0 0.3 0.4 0.6 0.8 1.0] AO darkening amount
#define VXGI_NORMAL_OFFSET 0.5       // [0.0 0.5 1.0 2.0] Bias to avoid self-shadowing

//============================================================================//
// RSM CONFIGURATION (улучшенный из RSM.glsl)
//============================================================================//

#define RSM_SAMPLES 32               // [4 8 12 16 20 24 32 48 64 96 128 256]
#define RSM_RADIUS 8.0               // [1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0 12.0 15.0 20.0 25.0 30.0 40.0 50.0]
#define RSM_BRIGHTNESS 1.0           // [0.5 0.6 0.7 0.8 0.9 1.0 1.1 1.2 1.3 1.4 1.6 1.8 2.0 2.5 3.0 5.0 10.0]

//============================================================================//
// SSPT CONFIGURATION (Screen-Space Path Tracing)
//============================================================================//

#define SSPT_ENABLED 1               // [0 1] Enable Screen-Space Path Tracing
#define SSPT_SPP 24                   // [1 2 3 4 5 6 7 8 9 10 12 14 16] Samples per pixel
#define SSPT_RT_STEPS 16             // [8 12 16 20 24 32 48 64] Raymarching steps
#define SSPT_BRIGHTNESS 1.0          // [0.5 0.75 1.0 1.25 1.5 2.0] SSPT brightness multiplier

const int shadowMapResolution = 2048;  // Shadowmap resolution [1024 2048 4096 8192 16384 32768]

//============================================================================//
// INCLUDE MODULES
//============================================================================//

#include "ShadowDistortion.glsl"
#include "/lib/Voxel/VoxelStructure.glsl"
#include "/lib/Voxel/VoxelSampling.glsl"

//============================================================================//
// PTGI-STYLE NOISE FUNCTIONS
//============================================================================//

// Blue noise sampling для устранения banding
vec3 BlueNoise3D(vec2 coord, int frame) {
    vec3 noise;
    noise.x = fract(texture(noisetex, coord * 0.5 + vec2(frame * 0.618, 0.0)).r + float(frame) * 0.618);
    noise.y = fract(texture(noisetex, coord * 0.5 + vec2(0.0, frame * 0.618)).g + float(frame) * 0.754);
    noise.z = fract(texture(noisetex, coord * 0.7 + vec2(frame * 0.5, frame * 0.5)).b + float(frame) * 0.832);
    return noise;
}

// Temporal dithering pattern (как в PTGI)
float TemporalDither(vec2 coord, int frame) {
    float dither = texture(noisetex, coord / 256.0).r;
    dither = fract(dither + float(frame & 255) / 256.0);
    return dither;
}

//============================================================================//
//  COLORED BLOCK DETECTION
//============================================================================//

bool IsColoredBlock(vec3 blockColor) {
    if (length(blockColor - vec3(0.86, 0.78, 0.25)) < 0.2) return true; // Yellow
    if (length(blockColor - vec3(0.87, 0.29, 0.29)) < 0.2) return true; // Red
    if (length(blockColor - vec3(0.62, 0.25, 0.84)) < 0.2) return true; // Purple
    if (length(blockColor - vec3(0.27, 0.33, 0.77)) < 0.2) return true; // Blue
    if (length(blockColor - vec3(0.13, 0.60, 0.78)) < 0.2) return true; // Cyan
    if (length(blockColor - vec3(0.95, 0.49, 0.75)) < 0.2) return true; // Pink
    if (length(blockColor - vec3(0.6, 0.1, 0.1)) < 0.2) return true;    // Redstone
    return false;
}

//============================================================================//
// SHADOWMAP FUNCTIONS
//============================================================================//

vec3 WorldToShadowProjPos(in vec3 worldPos) {
	vec3 shadowPos = transMAD(shadowModelView, worldPos);
	return projMAD(shadowProjection, shadowPos);
}

// Старый вариант (оставлен для совместимости с PTGI/ColoredBlocks)
vec2 DistortShadowProjPos(in vec2 shadowClipPos) {
	shadowClipPos.xy *= rcp(DistortionFactor(shadowClipPos.xy));
	return shadowClipPos * 0.5 + 0.5;
}

//============================================================================//
//  PTGI-STYLE MULTI-BOUNCE GI
//============================================================================//

// Generate hemisphere sample direction using improved cosine distribution
vec3 CosineWeightedHemisphere(vec3 normal, vec2 random) {
    vec3 tangent = normalize(cross(normal, abs(normal.y) > 0.99 ? vec3(1,0,0) : vec3(0,1,0)));
    vec3 bitangent = cross(normal, tangent);

    float phi = 6.28318530718 * random.x;
    float cosTheta = sqrt(random.y);
    float sinTheta = sqrt(1.0 - random.y);

    vec3 direction = vec3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta);

    return normalize(tangent * direction.x + bitangent * direction.y + normal * direction.z);
}

// PTGI-inspired GI with noise overlay method
vec3 CalculatePTGI_GI(
    vec3 worldPos,
    vec3 worldNormal,
    vec3 viewPos,
    float skylight,
    vec2 screenCoord,
    int frameCounter
) {
    #if PTGI_STYLE_GI == 0
        return vec3(0.0);
    #endif

    vec3 totalGI = vec3(0.0);

    vec3 shadowPos = WorldToShadowProjPos(worldPos);
    const float realShadowMapRes = float(shadowMapResolution) * MC_SHADOW_QUALITY;

    vec3 blueNoise = BlueNoise3D(screenCoord, frameCounter);
    float temporalDither = TemporalDither(screenCoord, frameCounter);

    vec3 currentPos = worldPos;
    vec3 currentNormal = worldNormal;
    vec3 bounceEnergy = vec3(1.0);

    for (int bounce = 0; bounce < PTGI_BOUNCES; bounce++) {
        vec3 bounceLight = vec3(0.0);
        float bounceWeight = 0.0;

        for (int i = 0; i < PTGI_SAMPLES_PER_BOUNCE; i++) {
            float t = (float(i) + blueNoise.x) / float(PTGI_SAMPLES_PER_BOUNCE);
            vec2 random = vec2(
                fract(t + temporalDither),
                fract(t * 1.618033988749 + blueNoise.y)
            );

            vec3 sampleDir = CosineWeightedHemisphere(currentNormal, random);

            float rayLength = GI_RADIUS * (1.0 / (1.0 + float(bounce)));
            vec3 samplePos = currentPos + sampleDir * rayLength;

            vec3 sampleShadowPos = WorldToShadowProjPos(samplePos);
            vec2 sampleUV = DistortShadowProjPos(sampleShadowPos.xy);

            if (clamp(sampleUV, vec2(0.0), vec2(1.0)) != sampleUV) continue;

            ivec2 sampleTexel = ivec2(sampleUV * realShadowMapRes);

            #if defined DISTANT_HORIZONS && defined DH_SHADOW
                float sampleDepth = texelFetch(shadowtex1, sampleTexel, 0).x * 40.0 - 20.0;
            #else
                float sampleDepth = texelFetch(shadowtex1, sampleTexel, 0).x * 10.0 - 5.0;
            #endif

            vec3 sampleVector = vec3(sampleShadowPos.xy, sampleDepth) - shadowPos;
            float sampleDist = dot(sampleVector, sampleVector);

            float maxDist = rayLength * rayLength * 2.0;
            if (sampleDist > maxDist) continue;

            vec3 sampleDirNorm = normalize(sampleVector);

            float NdotL = max(0.0, dot(currentNormal, sampleDirNorm));
            if (NdotL < 0.001) continue;

            vec3 sampleAlbedo = pow(texelFetch(shadowcolor0, sampleTexel, 0).rgb, vec3(2.2));
            vec3 sampleData = texelFetch(shadowcolor1, sampleTexel, 0).rgb;
            vec3 sampleNormal = DecodeNormal(sampleData.xy);
            sampleNormal.xy = -sampleNormal.xy;

            float bounceNdotL = max(0.0, -dot(sampleNormal, sampleDirNorm));
            if (bounceNdotL < 0.001) continue;

            float falloff = 1.0 / (1.0 + sampleDist * 0.5);

            #if defined IS_OVERWORLD
                float skylightMatch = exp(-pow(sampleData.z - skylight, 2.0) * 5.0);
            #else
                float skylightMatch = 1.0;
            #endif

            vec3 bouncedLight = sampleAlbedo * NdotL * bounceNdotL * falloff * skylightMatch;
            bounceLight += bouncedLight;
            bounceWeight += NdotL;
        }

        if (bounceWeight > 0.001) {
            bounceLight /= bounceWeight;
            bounceLight *= bounceEnergy;
            totalGI += bounceLight * PTGI_BOUNCE_INTENSITY;
            bounceEnergy *= 0.5;
        } else {
            break;
        }
    }

    float noiseOverlay = InterleavedGradientNoise(screenCoord * vec2(1920, 1080) * 0.5);
    noiseOverlay = mix(1.0, noiseOverlay, PTGI_NOISE_SCALE);
    totalGI *= noiseOverlay;

    float stabilization = mix(0.7, 1.0, temporalDither);
    totalGI *= stabilization;

    return totalGI * (GI_RADIUS * GI_RADIUS) * 0.05;
}

//============================================================================//
// COLORED BLOCK LIGHT SAMPLING
//============================================================================//

vec3 SampleColoredBlockLight(
    vec3 worldPos,
    vec3 normal,
    sampler3D voxelAlbedoTex,
    sampler2D blockIDTex
) {
    #if COLORED_BLOCKS_ENABLED == 0
        return vec3(0.0);
    #endif

    vec3 coloredLight = vec3(0.0);
    float totalWeight = 0.0;

    mat3 shadowToWorld = transpose(mat3(shadowModelView));
    vec3 sunDir = normalize(-shadowToWorld[2]);

    const int SAMPLES = 16;
    const float radius = COLORED_BLOCK_RADIUS;
    const float coneAngle = 0.8;

    for (int i = 0; i < SAMPLES; i++) {
        float phi = acos(1.0 - 2.0 * (float(i) + 0.5) / float(SAMPLES));
        float theta = 3.14159 * (1.0 + sqrt(5.0)) * float(i);

        vec3 sampleDir = sunDir;

        vec3 tangent = normalize(cross(sunDir, vec3(0.0, 1.0, 0.0)));
        if (length(tangent) < 0.1) tangent = normalize(cross(sunDir, vec3(1.0, 0.0, 0.0)));
        vec3 bitangent = cross(sunDir, tangent);

        float coneRadius = sin(phi) * coneAngle;
        vec2 coneOffset = vec2(cos(theta), sin(theta)) * coneRadius;
        sampleDir = normalize(sunDir + tangent * coneOffset.x + bitangent * coneOffset.y);

        if (dot(sampleDir, normal) < 0.0) continue;

        for (int dist = 1; dist <= 3; dist++) {
            vec3 samplePos = worldPos + sampleDir * float(dist);

            ivec3 voxel = WorldToVoxel(samplePos);
            if (!IsVoxelInBounds(voxel)) continue;

            vec3 texCoord = VoxelToTexCoord(voxel);
            vec4 voxelData = texture(voxelAlbedoTex, texCoord);

            if (voxelData.a < 0.1) continue;

            vec3 blockColor = voxelData.rgb;

            float blockID = 0.0;
            if      (length(blockColor - vec3(0.86, 0.78, 0.25)) < 0.2) blockID = 13070.0;
            else if (length(blockColor - vec3(0.87, 0.29, 0.29)) < 0.2) blockID = 13071.0;
            else if (length(blockColor - vec3(0.62, 0.25, 0.84)) < 0.2) blockID = 13072.0;
            else if (length(blockColor - vec3(0.27, 0.33, 0.77)) < 0.2) blockID = 13073.0;
            else if (length(blockColor - vec3(0.13, 0.60, 0.78)) < 0.2) blockID = 13074.0;
            else if (length(blockColor - vec3(0.95, 0.49, 0.75)) < 0.2) blockID = 13075.0;
            else if (length(blockColor - vec3(0.6,  0.1,  0.1 )) < 0.2) blockID = 10029.0;

            if (blockID < 0.5) continue;

            vec3 blockWorldPos = VoxelToWorld(voxel);
            vec3 shadowPos = WorldToShadowProjPos(blockWorldPos);
            vec2 shadowUV = DistortShadowProjPos(shadowPos.xy);

            float shadowDepth = texture(shadowtex1, shadowUV).r;
            float blockDepth = shadowPos.z;

            bool inSunlight = (blockDepth - shadowDepth) < 0.001;
            if (!inSunlight) continue;

            vec3 emission = blockColor * COLORED_BLOCK_INTENSITY;

            float distance = length(samplePos - worldPos);
            float attenuation = 1.0 / (1.0 + distance * distance * 0.1);

            float dirToSun = max(0.0, dot(sampleDir, sunDir));
            float dirWeight = max(0.0, dot(normal, sampleDir)) * (0.3 + 0.7 * dirToSun);

            coloredLight += emission * attenuation * dirWeight * voxelData.a;
            totalWeight += dirWeight * voxelData.a;
        }
    }

    if (totalWeight > 0.001) {
        coloredLight /= totalWeight;
    }

    coloredLight = clamp(coloredLight, vec3(0.0), vec3(50.0));

    return coloredLight;
}

//============================================================================//
// VOXEL AO
//============================================================================//

vec3 SampleVoxelAO(
    vec3 worldPos,
    vec3 normal,
    sampler3D voxelAlbedoTex
) {
    vec3 origin = worldPos + normal * VOXEL_SIZE * VXGI_NORMAL_OFFSET;

    float ao = 0.0;
    int samples = 0;

    const vec3 directions[6] = vec3[6](
        vec3(1, 0, 0), vec3(-1, 0, 0),
        vec3(0, 1, 0), vec3(0, -1, 0),
        vec3(0, 0, 1), vec3(0, 0, -1)
    );

    for (int i = 0; i < 6; i++) {
        vec3 dir = directions[i];

        if (dot(dir, normal) < 0.0) continue;

        for (int dist = 1; dist <= 3; dist++) {
            vec3 samplePos = origin + dir * VOXEL_SIZE * float(dist);
            vec3 texCoord = WorldToTexCoord(samplePos);

            if (any(lessThan(texCoord, vec3(0.0))) || any(greaterThan(texCoord, vec3(1.0)))) {
                continue;
            }

            float opacity = texture(voxelAlbedoTex, texCoord).a;

            float weight = 1.0 / float(dist);
            ao += opacity * weight;
            samples++;
        }
    }

    if (samples > 0) {
        ao = 1.0 - clamp(ao / float(samples) * 2.0, 0.0, 0.8);
    } else {
        ao = 1.0;
    }

    return vec3(ao);
}



vec3 CalculateRSM(in vec3 viewPos, in vec3 worldNormal, in float skyLightmap) {
    const float realShadowMapRes = float(shadowMapResolution) * MC_SHADOW_QUALITY;

    vec3 shadowNormal     = mat3(shadowModelView) * worldNormal;
    vec3 projectionScale    = vec3(shadowProjection[0].x,    shadowProjection[1].y,    shadowProjection[2].z);
    vec3 projectionInvScale = vec3(shadowProjectionInverse[0].x, shadowProjectionInverse[1].y, shadowProjectionInverse[2].z);

    vec3 worldPos      = transMAD(gbufferModelViewInverse, viewPos);
    vec3 shadowPos     = transMAD(shadowModelView, worldPos);
    vec3 shadowClipPos = projectionScale * shadowPos + shadowProjection[3].xyz;

    const float sqRadius = RSM_RADIUS * RSM_RADIUS;
    const float rSteps   = 1.0 / float(RSM_SAMPLES);

    const float goldenAngle = TAU / (PHI1 + 1.0);
    const mat2 goldenRotate = mat2(cos(goldenAngle), -sin(goldenAngle),
                                   sin(goldenAngle),  cos(goldenAngle));

    vec2 offsetRadius = RSM_RADIUS * projectionScale.xy;

    // Используем TemporalDither как шум (заменяет noise.x из RSM.glsl)
    float dither = TemporalDither(gl_FragCoord.xy * screenPixelSize, frameCounter);
    vec2 dir = sincos(dither * 16.0 * PI) * offsetRadius;
    float ditherY = fract(dither * 1.618033988749); // заменяет noise.y

    vec3 sum = vec3(0.0);

    for (uint i = 0u; i < RSM_SAMPLES; ++i, dir *= goldenRotate) {
        float sampleRad = (float(i) + ditherY) * rSteps;

        vec2 sampleClipCoord  = shadowClipPos.xy + dir * sampleRad;
        float distFactor      = DistortionFactor(sampleClipCoord);
        vec2 sampleScreenCoord = sampleClipCoord * rcp(distFactor) * 0.5 + 0.5;
        ivec2 sampleTexel     = ivec2(sampleScreenCoord * realShadowMapRes);

        #if defined DISTANT_HORIZONS && defined DH_SHADOW
            float sampleDepth = texelFetch(shadowtex1, sampleTexel, 0).x * 40.0 - 20.0;
        #else
            float sampleDepth = texelFetch(shadowtex1, sampleTexel, 0).x * 10.0 - 5.0;
        #endif
        vec3 sampleDelta = vec3(sampleClipCoord, sampleDepth) - shadowClipPos;
        sampleDelta      = projectionInvScale * sampleDelta;
        float sampleSqLen = dot(sampleDelta, sampleDelta);
        if (sampleSqLen > sqRadius) continue;
        vec3 sampleDir = sampleDelta * inversesqrt(sampleSqLen);
        float diffuse = dot(shadowNormal, sampleDir);
        if (diffuse < 1e-5) continue;
        vec3 sampleColor  = texelFetch(shadowcolor1, sampleTexel, 0).rgb;
        vec2 encodedNorm  = sampleColor.xy * 2.0 - 1.0;
        vec3 sampleNormal;
        sampleNormal.z    = 1.0 - abs(encodedNorm.x) - abs(encodedNorm.y);
        sampleNormal.xy   = sampleNormal.z >= 0.0 ? encodedNorm
                          : (1.0 - abs(encodedNorm.yx)) * sign(encodedNorm);
        sampleNormal      = normalize(sampleNormal);
        float bounce = dot(sampleNormal, -sampleDir);
        if (bounce < 1e-5) continue;
        float falloff = sampleRad / (sampleSqLen + 1e-5);
        #if defined IS_OVERWORLD
            float skylightWeight = saturate(1.0 - (sampleColor.z - skyLightmap) * (sampleColor.z - skyLightmap) * 2.0);
        #else
            float skylightWeight = 1.0;
        #endif

        vec3 albedo = pow(texelFetch(shadowcolor0, sampleTexel, 0).rgb, vec3(2.2));
        sum += diffuse * bounce * falloff * skylightWeight * albedo;
    }
    sum *= sqRadius * rSteps * RSM_BRIGHTNESS * PI;
    return saturate(sum);
}




vec3 SSPT_ViewToScreenSpace(vec3 vp) {
    vec4 clip = gbufferProjection * vec4(vp, 1.0);
    return (clip.xyz / clip.w) * 0.5 + 0.5;
}

// Линеаризация depth (NDC → view Z)
float SSPT_ScreenToViewDepth(float depth) {
    float d = depth * 2.0 - 1.0;
    return gbufferProjection[3].z / (d * gbufferProjection[2].w - gbufferProjection[2].z);
}
#if defined DISTANT_HORIZONS
float SSPT_ScreenToViewDepthDH(float depth) {
    float d = depth * 2.0 - 1.0;
    return dhProjection[3].z / (d * dhProjection[2].w - dhProjection[2].z);
}
float SSPT_ViewToScreenDepthDH(float linearDepth) {
    return (dhProjection[2].z * linearDepth + dhProjection[3].z)
         / (dhProjection[2].w * linearDepth + dhProjection[3].w) * 0.5 + 0.5;
}
#endif

// Косинусное семплирование полусферы
vec3 SSPT_CosineHemisphere(vec3 normal, vec2 r) {
    vec3 t = normalize(cross(normal, abs(normal.y) > 0.99 ? vec3(1,0,0) : vec3(0,1,0)));
    vec3 b = cross(normal, t);
    float phi      = 6.28318530718 * r.x;
    float cosTheta = sqrt(r.y);
    float sinTheta = sqrt(1.0 - r.y);
    return normalize(t * (cos(phi)*sinTheta) + b * (sin(phi)*sinTheta) + normal * cosTheta);
}

// Семплирование неба через colortex5 (сферическая проекция)
vec3 SSPT_SampleSky(vec3 worldDir) {
    vec2 uv;
    uv.x = atan(worldDir.x, worldDir.z) * (1.0 / 6.28318530718) + 0.5;
    uv.y = asin(clamp(worldDir.y, -1.0, 1.0)) * (1.0 / 3.14159265358) + 0.5;
    return texture(colortex5, uv).rgb;
}

vec3 SSPT_SampleRaytrace(in vec3 viewPos, in vec3 viewDir, in float dither, in vec3 rayPos) {
    if (viewDir.z > max(0.0, -viewPos.z)) return vec3(1e6);

    vec3 endPos  = SSPT_ViewToScreenSpace(viewDir + viewPos);
    vec3 rayDir  = normalize(endPos - rayPos);

    vec3 boundT  = (step(0.0, rayDir) - rayPos) / rayDir;
    float maxT   = min(min(boundT.x, boundT.y), boundT.z);
    float stepLen = maxT / float(SSPT_RT_STEPS);

    vec3 rayStep = rayDir * stepLen;
    rayPos      += rayStep * dither;

    rayPos.xy  *= screenSize;
    rayStep.xy *= screenSize;

    for (uint i = 0u; i < SSPT_RT_STEPS; ++i, rayPos += rayStep) {
        if (clamp(rayPos.xy, vec2(0.0), screenSize) != rayPos.xy) break;

        float sampleDepth = texelFetch(depthtex0, ivec2(rayPos.xy), 0).x;

        if (sampleDepth < rayPos.z) {
            float sampleLin = SSPT_ScreenToViewDepth(sampleDepth);
            float traceLin  = SSPT_ScreenToViewDepth(rayPos.z);

            #if defined DISTANT_HORIZONS
                if (sampleDepth > 1.0 - 1e-5)
                    sampleLin = SSPT_ScreenToViewDepthDH(texelFetch(dhDepthTex0, ivec2(rayPos.xy), 0).x);
            #endif

            if (traceLin - sampleLin > 0.2 * traceLin)
                return vec3(rayPos.xy, sampleDepth);
        }
    }

    return vec3(1e6);
}

// Основная функция SSPT.
vec3 CalculateSSPT(
    in vec3 screenPos,
    in vec3 viewPos,
    in vec3 worldNormal,
    in float skylight,
    in sampler2D radianceTex
) {
    #if SSPT_ENABLED == 0
        return vec3(0.0);
    #endif

    float skylightCubed = skylight * skylight * skylight;
    vec3 viewNormal = mat3(gbufferModelView) * worldNormal;

    vec2 noiseUV = screenPos.xy + vec2(frameCounter) * vec2(0.618, 0.754);
    float noise0 = fract(texture(noisetex, noiseUV * 0.5).r      + float(frameCounter) * 0.618);
    float noise1 = fract(texture(noisetex, noiseUV * 0.5 + 0.5).g + float(frameCounter) * 0.382);

    #if defined DISTANT_HORIZONS
        float screenDepthMax = SSPT_ViewToScreenDepthDH(SSPT_ScreenToViewDepthDH(1.0));
    #else
        float screenDepthMax = 1.0;
    #endif

    vec3 sum = vec3(0.0);

    for (uint spp = 0u; spp < SSPT_SPP; ++spp) {
        float r0 = fract(noise0 + float(spp) * 0.618033988749);
        float r1 = fract(noise1 + float(spp) * 0.381966011250);
        vec3 rayDir = SSPT_CosineHemisphere(viewNormal, vec2(r0, r1));

        float dither = fract(noise0 * 1.732 + float(spp) * 0.5);

        vec3 hitPos = SSPT_SampleRaytrace(viewPos + viewNormal * 1e-2, rayDir, dither, screenPos);

        if (hitPos.z < screenDepthMax) {
            vec3 sampleRadiance = texelFetch(radianceTex, ivec2(hitPos.xy) >> 1, 0).rgb;
            sum += sampleRadiance;
        } else if (skylightCubed > 1e-5) {
            vec3 rayDirWorld = mat3(gbufferModelViewInverse) * rayDir;
            sum += SSPT_SampleSky(rayDirWorld) * skylightCubed;
            break;
        }
    }

    return sum / float(SSPT_SPP) * SSPT_BRIGHTNESS;
}

//============================================================================//
// WRAPPER-ФУНКЦИИ 
//============================================================================//

vec3 CalculateVoxelAO_Only(
    vec3 worldPos,
    vec3 viewPos,
    vec3 worldNormal,
    sampler3D voxelAlbedoTex
) {
    float distToCamera = length(viewPos);

    vec3 ao = vec3(1.0);
    #ifdef VXGI_ENABLED
        if (distToCamera < float(VOXEL_RADIUS) * 0.9) {
            ao = SampleVoxelAO(worldPos, worldNormal, voxelAlbedoTex);
            ao = mix(vec3(1.0), ao, VXGI_INTENSITY);

            float edgeFade = 1.0 - smoothstep(
                float(VOXEL_RADIUS) * 0.7,
                float(VOXEL_RADIUS) * 0.9,
                distToCamera
            );
            ao = mix(vec3(1.0), ao, edgeFade);
        }
    #endif

    return ao;
}

vec3 CalculateColoredBlockLight_Only(
    vec3 worldPos,
    vec3 viewPos,
    vec3 worldNormal,
    sampler3D voxelAlbedoTex,
    sampler2D blockIDTex
) {
    float distToCamera = length(viewPos);

    vec3 coloredLight = vec3(0.0);
    #if COLORED_BLOCKS_ENABLED == 1
        if (distToCamera < COLORED_BLOCK_RADIUS * 2.0) {
            coloredLight = SampleColoredBlockLight(worldPos, worldNormal, voxelAlbedoTex, blockIDTex);
        }
    #endif

    return coloredLight;
}

// Legacy wrappers
vec3 CalculateVXGI_Direct(
    vec3 worldPos,
    vec3 viewPos,
    vec3 worldNormal,
    sampler3D voxelAlbedoTex,
    sampler3D voxelGITex,
    sampler2D blockIDTex
) {
    return CalculateVoxelAO_Only(worldPos, viewPos, worldNormal, voxelAlbedoTex);
}

vec3 CalculateVXGI(
    vec3 worldPos,
    vec3 viewPos,
    vec3 worldNormal,
    sampler3D voxelAlbedoTex,
    sampler3D voxelGITex,
    sampler3D voxelGIFloodfill
) {
    return CalculateVoxelAO_Only(worldPos, viewPos, worldNormal, voxelAlbedoTex);
}
