float cloud_shadows(vec3 WorldPos) {
    vec3 CloudSamplePos = WorldPos;
    if (CloudSamplePos.y < CLOUD_LOWER_PLANE) {
        CloudSamplePos = intersectRayWithPlane(CloudSamplePos, view_player(sLightPosN), CLOUD_LOWER_PLANE);
    }
    float CloudCoverage = noise_clouds_base_simple(CloudSamplePos);

    return 1;
}

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));
        IsWater += float(ShadowCol.a == 1); // Water is the only transparent thing that has alpha 1
        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;
}

vec3 get_shadow(vec3 PlayerPos, vec3 FlatNormal, float Material, float Skylight, bool DoSSS) {
    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
        );

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

    vec3 ShadowFinal = vec3(1);
    if (DoSSS) {
        PenumbraSize *= 10;
    }

    vec3 ShadowPos = distort(ShadowPosUndistorted);
    ShadowPos = ShadowPos * 0.5 + 0.5;

    float IsWater = 0;
    #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);
    }

    //Lightshafts from clouds
    float CloudCoverage = cloud_shadows(PlayerPos + cameraPosition);

    return ShadowFinal * CloudCoverage;
}
