vec3 rsm(vec3 PlayerPos, vec3 Normal, vec3 LightColor) {
    if (isIndoorsSmooth < 0.001) return vec3(0);
    if (isEyeInWater == 1) return vec3(0);

    vec3 ShadowPos = player_shadow(PlayerPos);
    const int SAMPLE_COUNT = 16;

    vec3 ShadowNormal = mat3(shadowModelView) * (mat3(gbufferModelViewInverse) * Normal);
    vec3 Sum = vec3(0);

    vec2 Pos = gl_FragCoord.xy;
    vec2 Noise = blue_noise(Pos).rg;

    for (int i = 1; i <= SAMPLE_COUNT; i++) {
        float OffsetAng = (i + Noise.x) * TAU / SAMPLE_COUNT;
        vec2 Offset = Noise.y * vec2(cos(OffsetAng), sin(OffsetAng)) * shadowTexSize * 96;
        Offset *= sign(dot(Offset, ShadowNormal.xy));

        vec3 OffsetPos = ShadowPos + vec3(Offset, 0);
        vec3 SamplePos = distort(OffsetPos) * 0.5 + 0.5;
        float RealDepth = texture(shadowtex1, SamplePos.xy).x;
        RealDepth = (RealDepth * 2 - 1) / 0.2;
        if (RealDepth < ShadowPos.z) continue;
        OffsetPos.z = RealDepth;

        vec3 SampleNormal = decodeUnitVector(texture(shadowcolor1, SamplePos.xy).rg * 2 - 1);

        vec3 RayDir = normalize(OffsetPos - ShadowPos);
        RayDir.z *= -1;
        float Flux = max(0, dot(RayDir, ShadowNormal.xyz));
        RayDir *= -1;
        Flux *= max(0, dot(RayDir, SampleNormal));

        float Dist = distance(OffsetPos, ShadowPos);
        Flux *= pow2(max(1 - Dist / (shadowTexSize * 96), 0));

        if (Flux < 0.0001) continue;

        vec3 ShadowColor = texture(shadowcolor0, SamplePos.xy).rgb;
        if (ShadowColor == vec3(1)) ShadowColor = WaterColor;
        else ShadowColor = pow(ShadowColor, vec3(2.2));
        Sum += ShadowColor * Flux;
    }
    return Sum * LightColor / SAMPLE_COUNT * isIndoorsSmooth;
}

float[5] weights = float[5](0.13298, 0.12579, 0.0866, 0.05455, 0.03316);
vec4 gi_denoise(sampler2D image, ivec2 FragCoord, ivec2 direction, float CurrentDepth) {
    vec4 color = vec4(0);
    float TotalWeight = 0;
    float CurrentData = texelFetch(colortex1, FragCoord, 0).w;
    CurrentDepth = l_depth(CurrentDepth);
    vec3 CurrentNormal = decodeUnitVector(unpackUnorm2x8(CurrentData) * 2 - 1);

    const int BLUR_SIZE = 3;
    const int MAGIC_NUMBER = 2;

    for (int i = -BLUR_SIZE; i <= BLUR_SIZE; i++) {
        ivec2 OffsetUV = FragCoord + ivec2(i) * direction * MAGIC_NUMBER;
        vec4 OffsetColor = texelFetch(image, OffsetUV, 0);

        float OffsetData = texelFetch(colortex1, OffsetUV, 0).w;
        float OffsetDepth = l_depth(texelFetch(depthtex0, OffsetUV, 0).r);
        vec3 OffsetNormal = decodeUnitVector(unpackUnorm2x8(OffsetData) * 2 - 1);
        float OffsetWeight = 1; //weights[abs(i)];
        OffsetWeight *= pow4(max(dot(CurrentNormal, OffsetNormal), 0));
        OffsetWeight *= pow4(clamp(1 - abs(CurrentDepth - OffsetDepth), 0, 1));

        color += OffsetColor * OffsetWeight;
        TotalWeight += OffsetWeight;
    }
    return color / TotalWeight;
}
