float cloud_shadows(vec3 WorldPos) {
    vec3 CloudSamplePos = WorldPos;
    if (CloudSamplePos.y < CLOUD_LOWER_PLANE) {
        CloudSamplePos = intersectRayWithPlane(CloudSamplePos, view_player(sLightPosN), CLOUD_LOWER_PLANE);
        if(CloudSamplePos == vec3(0))
            return 0;
        CloudSamplePos += WorldPos;
    } 
    else if(CloudSamplePos.y > CLOUD_UPPER_PLANE) {
        return 1;
    }

    float PlayerPosNy = normalize(CloudSamplePos - cameraPosition).y;
    float FogFactor = pow2(PlayerPosNy);
    FogFactor = 1 - exp(-30 * FogFactor);

    if(FogFactor < 0.01) return 1;

    float CloudCoverage = noise_clouds_base_simple(CloudSamplePos);

    return max(0.0, 1 - pow(CloudCoverage * FogFactor, 0.33));
}

vec3 get_shadow_transparent(vec3 SampleCoords, vec3 ShadowPosUndistorted, inout float IsWater) {
    float Depth1 = texture(shadowtex1HW, SampleCoords).x;
    if (Depth1 < 0.001) {
        return vec3(Depth1);
    }
    float Depth = texture(shadowtex0HW, SampleCoords).x;
    if (Depth < 1) {
        vec4 ShadowCol = texture(shadowcolor0, SampleCoords.xy);
        ShadowCol.rgb = pow(ShadowCol.rgb, vec3(2.2));
        if(ShadowCol.a < 0.1) {
            IsWater = 1; 
            ShadowCol.a = 0.8;
        }
        ShadowCol.rgb = mix(vec3(1), ShadowCol.rgb, ShadowCol.a * (1 - Depth));
        return ShadowCol.rgb * Depth1;
    }
    return vec3(Depth1);
}

vec3 pcf(float PenumbraSize, mat2 RotationOffset, vec3 ShadowPosUndistorted, inout float IsWaterFinal, bool DoSSS) {
    const int SAMPLE_COUNT = 12;

    vec3 ShadowColorFinal;
    for (int i = 0; i < SAMPLE_COUNT; i++) {
        vec2 OffsetP = RotationOffset * poisson_disk_2d[i] * PenumbraSize;
        vec3 ShadowPosD = ShadowPosUndistorted + vec3(OffsetP, 0);
        ShadowPosD = distort(ShadowPosD);

        ShadowPosD = ShadowPosD * 0.5 + 0.5; //convert from shadow ndc space to shadow screen space.
        float IsWater = 0;
        ShadowColorFinal += get_shadow_transparent(ShadowPosD, ShadowPosUndistorted, IsWater);
        IsWaterFinal += IsWater;
    }
    IsWaterFinal /= SAMPLE_COUNT;
    return ShadowColorFinal / SAMPLE_COUNT;
}

float pcss(vec3 ShadowPos, mat2 RotationOffset) {
    float ReceiverD = ShadowPos.z * 0.2 * 0.5 + 0.5;
    float BlockerD = 0;
    float Hits = 0;
    for (int i = 0; i < 8; i++) {
        vec2 OffsetP = RotationOffset * poisson_disk_2d[i] * 5 * SHADOW_FILTER_SIZE;
        vec2 ShadowPosD = ShadowPos.xy + OffsetP;
        ShadowPosD = distort(vec3(ShadowPosD, 0)).xy;
        ShadowPosD = ShadowPosD * 0.5 + 0.5;

        float Sample = texture(shadowtex1, ShadowPosD).x;
        if (Sample < ReceiverD) {
            BlockerD += ReceiverD - Sample;
            Hits++;
        }
    }
    if (Hits == 0) return SHADOW_FILTER_SIZE; // Prevent funny business
    BlockerD /= Hits;
    return min(BlockerD * far + 0.5, 5) * SHADOW_FILTER_SIZE;
}

// Used in rsm, vl, etc.
float get_shadow_unfiltered(vec3 PlayerPos, vec3 ShadowPos) {
    #ifdef DIMENSION_NETHER
    return 0.0;
    #endif

    float ShadowFinal = texture(shadowtex1HW, ShadowPos).x;
    #ifdef CLOUDS
        float CloudCoverage = cloud_shadows(PlayerPos + cameraPosition);
        ShadowFinal *= CloudCoverage;
    #endif
    return ShadowFinal;
}

vec3 get_shadow(vec3 PlayerPos, vec3 FlatNormal, float Material, float Skylight, bool DoSSS) {
    #ifdef DIMENSION_NETHER
    return vec3(0);
    #endif
    
    if (dot(sLightPosN, FlatNormal) <= 0 && !DoSSS) return vec3(0);

    vec3 bias = compute_bias(PlayerPos + gbufferModelViewInverse[3].xyz, view_player(FlatNormal), dot(FlatNormal, sLightPosN), Skylight);
    if (DoSSS) {
        bias *= vec3(0.05);
    }

    vec3 ShadowPosUndistorted = player_shadow(PlayerPos + bias);

    float Dither = dither(gl_FragCoord.xy) * TAU;
    vec2 Offset = vec2(cos(Dither), sin(Dither)) * shadowTexSize;
    mat2 RotationOffset = mat2(
            Offset.x, Offset.y,
            -Offset.y, Offset.x
        );

    vec3 ShadowFinal = vec3(1);
    float IsWater = 0;
    vec3 ShadowPos = distort(ShadowPosUndistorted);
    ShadowPos = ShadowPos * 0.5 + 0.5;

    float PenumbraSize;
    #if SHADOW_FILTER == 2
    if (!DoSSS)
        PenumbraSize = pcss(ShadowPosUndistorted, RotationOffset);
    else
        #endif
        PenumbraSize = SHADOW_FILTER_SIZE;

    if (DoSSS) {
        PenumbraSize *= 10;
    }

    #if SHADOW_FILTER != 0
    ShadowFinal *= pcf(PenumbraSize, RotationOffset, ShadowPosUndistorted, IsWater, DoSSS);
    #else

    ShadowFinal *= get_shadow_transparent(ShadowPos, ShadowPosUndistorted, IsWater);
    #endif
    if (IsWater > 0) {
        float Caustics = get_water_caustics(PlayerPos);
        ShadowFinal *= mix(1, Caustics, IsWater);
    }
    
    #ifdef CLOUDS
        float CloudCoverage = cloud_shadows(PlayerPos + cameraPosition);
        ShadowFinal *= CloudCoverage;
    #endif
    return ShadowFinal;
}
