bool ray_intersect(vec3 WorldPos, inout vec3 StartPos, inout vec3 EndPos, vec3 PlayerPosN, const float MAX_HEIGHT) {
    if (WorldPos.y <= MAX_HEIGHT) {
        if (EndPos.y + cameraPosition.y > MAX_HEIGHT)
            EndPos = intersectRayWithPlane(WorldPos, PlayerPosN, MAX_HEIGHT);
        if (EndPos == vec3(0)) return false;
    }
    else {
        if (EndPos.y + cameraPosition.y > MAX_HEIGHT) return false;
        StartPos = intersectRayWithPlane(WorldPos, PlayerPosN, MAX_HEIGHT);
        if (StartPos == vec3(0)) return false;
    }

    return true;
}

float height_falloff(float Height, const float MAX_HEIGHT) {
    return 1 - smoothstep(MAX_HEIGHT - 50, MAX_HEIGHT, Height);
}

vec3 ambient_fog(vec3 StartPos, vec3 EndPos, vec3 PlayerPosN, vec3 Color, vec3 ScreenPos, const int STEP_COUNT, const bool DoRT, const bool DoDetail) {
    vec3 WorldPos = cameraPosition + StartPos;

    #ifdef DIMENSION_OVERWORLD
        const float DENSITY = 100;

        const float MAX_HEIGHT = CLOUD_UPPER_PLANE;

        vec3 AmbientColor = dataBuf.AmbientColor * vec3(1, 2, 4); // This looks good
        AmbientColor = mix(MinLight, AmbientColor, isIndoorsSmooth);
        vec3 DirectColor = LightColorDirect;     
    #elif defined DIMENSION_NETHER
        const float DENSITY = 5000;

        const float MAX_HEIGHT = 70.0;

        vec3 AmbientColor = vec3(0);
        vec3 DirectColor = vec3(0);

    #elif defined DIMENSION_END
        const float DENSITY = 5000;

        const float MAX_HEIGHT = 256;
        vec3 AmbientColor = vec3(0.);
        vec3 DirectColor = LightColorDirect;
    #endif

    if(!ray_intersect(WorldPos, StartPos, EndPos, PlayerPosN, MAX_HEIGHT)) return Color;

    if(length(EndPos) > farLod) {
        float StartPosL = length(StartPos);
        EndPos = StartPos + PlayerPosN * (farLod - StartPosL);
    }

    float StepCount = ceil(((1-abs(PlayerPosN.y)) * 0.5 + 0.5) * STEP_COUNT); // Nerf near the horizon 

    vec3 Step = (EndPos - StartPos) / StepCount;
    float StepSize = length(Step);
    vec3 TotalTransmittance = vec3(1);
    vec3 TotalScattering = vec3(0);
    float Dither = dither(gl_FragCoord.xy, true);
    vec3 PlayerPosC = StartPos + Dither * Step;

    float VdotL = dot(view_player(sLightPosN), PlayerPosN);
    float MiePhase = cs_phase(VdotL, anisotropy * 0.7) * 0.75 + cs_phase(VdotL, -anisotropy * 0.3) * 0.25;

    #ifdef DIMENSION_OVERWORLD
        // Fog gets strong at morning
        float DaytimeFactor = worldTime > 12000 ? smoothstep(20000, 24000, worldTime) : 1 - smoothstep(0, 2000, worldTime);
        DaytimeFactor = max(DaytimeFactor, 0.33);
        float DensityConstant = DENSITY * DaytimeFactor * StepSize * fogAmount;
    #else
        float DensityConstant = DENSITY * StepSize;
    #endif

    #ifdef DIMENSION_OVERWORLD
        if(!DoRT) { // Use an analytical model
            float Density = StepCount * DensityConstant * 0.5; // 0.5 accounts for the noise function
            vec3 WorldPos = EndPos + cameraPosition; 
            if(ScreenPos.z < 1)
            Density *= height_falloff(WorldPos.y, MAX_HEIGHT);

            TotalTransmittance = vec3(exp(-Density * BETA_M_E));
            vec3 Transmittance = TotalTransmittance;
            
            float ShadowFactor = eyeBrightnessSmooth.y / 240.0;
            if(StartPos != vec3(0)) {
                ShadowFactor *= texture(colortex5, ScreenPos.xy).r; // Sample shadowmap at start position when doing vl in reflections
            }

            TotalScattering = AmbientColor + DirectColor * MiePhase * ShadowFactor;
            TotalScattering *= BETA_M * (1 - Transmittance) / BETA_M_E;
        } else 
    #endif
    {
        for (int i = 1; i <= StepCount; i++) {
            vec3 WorldPosC = PlayerPosC + cameraPosition;

            #ifdef DIMENSION_OVERWORLD
            float Density;
                if(DoDetail)
                    Density = texture(worleyNoiseTexture, WorldPosC / vec3(256, 32, 256) * 3 + frameTimeCounter * 0.02).r;
                else
                    Density = 0.5;
            #elif defined DIMENSION_NETHER
                float Density = noise_smoke(WorldPosC);
            #else
                float Density = texture(worleyNoiseTexture, WorldPosC / vec3(256, 32, 256) * 3 + frameTimeCounter * 0.02).r;
            #endif

            Density *= DensityConstant;

            Density *= height_falloff(WorldPosC.y, MAX_HEIGHT);

            if (Density <= 0) {
                PlayerPosC += Step;
                continue;
            }

            vec3 Transmittance = vec3(exp(-Density * BETA_M_E));
            vec3 T = TotalTransmittance;
            vec3 ScatteringSample = AmbientColor * T;

            vec3 ShadowNDCPosC = player_shadow(PlayerPosC);
            vec3 ShadowPosC = distort(ShadowNDCPosC);
            ShadowPosC = ShadowPosC * 0.5 + 0.5;
            float ShadowFactor = get_shadow_unfiltered(PlayerPosC, ShadowPosC);

            if (ShadowFactor > 0.01) { // Is in sun
                ScatteringSample += DirectColor * T * MiePhase * ShadowFactor;
            }

            TotalScattering += TotalTransmittance * BETA_M * ScatteringSample * (1 - Transmittance) / BETA_M_E;

            TotalTransmittance *= Transmittance;
            PlayerPosC += Step;
        }
    }

    

    return Color * TotalTransmittance + TotalScattering;
}

vec3 do_water_vl(vec3 StartPos, vec3 EndPos, vec3 PlayerPosN, vec3 Color, float Depth, bool IsDH, const int STEP_COUNT, const bool DoRT) {
    const vec3 WATER_ABSORBTION = WaterAbsorbtion;
    const vec3 WATER_SCATTERING = WaterColor;
    const float SIGMA_WATER = 1;
    const vec3 WATER_EXTINCTION = SIGMA_WATER * (WATER_ABSORBTION + WATER_SCATTERING);

    vec3 WorldPos = cameraPosition + StartPos;

    if (Depth == 1) {
        EndPos = PlayerPosN;
        EndPos *= IsDH ? dhRenderDistance : far;
    }

    vec3 Step = (EndPos - StartPos) / STEP_COUNT;
    float StepSize = length(Step);

    float Dither = dither(gl_FragCoord.xy, true);
    vec3 PlayerPosC = StartPos + Dither * Step;

    vec3 AmbientColor = dataBuf.AmbientColor;

    AmbientColor = mix(MinLight, AmbientColor, isIndoorsSmooth);
    float VdotL = dot(view_player(sLightPosN), PlayerPosN);

    float Density = StepSize;
    vec3 TotalScattering = vec3(0);
    vec3 TotalTransmittance = vec3(1), Transmittance;

    float SunPhase = cs_phase(VdotL, 0.8);

    vec3 fms = WATER_SCATTERING / WATER_EXTINCTION * (1 - exp(-5 * WATER_EXTINCTION));
    vec3 MS = ISOTROPIC_PHASE * fms / (1 - fms);

    if(!DoRT) {
        float Shadow = eyeBrightnessSmooth.y / 240.0;
        if(isEyeInWater == 0)
            Shadow = texture(colortex5, gl_FragCoord.xy * resolutionInv).r;


        TotalTransmittance = exp(-Density * WATER_EXTINCTION * STEP_COUNT);
        Transmittance = TotalTransmittance;

        float DistToSky = 5;
        float DistToSun = DistToSky / view_player(sLightPosN).y;
        vec3 SkyAttenuation = exp(-DistToSky * WATER_EXTINCTION);
        vec3 SunAttenuation = exp(-DistToSun * WATER_EXTINCTION);

        TotalScattering = AmbientColor * (ISOTROPIC_PHASE + MS) * SkyAttenuation + LightColorDirect * (SunPhase + MS) * Shadow * SunAttenuation;
    } else {
        Transmittance = exp(-Density * WATER_EXTINCTION);

        for (int i = 1; i <= STEP_COUNT; i++) {
            vec3 WorldPosC = PlayerPosC + cameraPosition;

            float DistToSky = 5;
            vec3 SkyAttenuation = exp(-DistToSky * WATER_EXTINCTION);
            TotalScattering += TotalTransmittance * AmbientColor * SkyAttenuation * (ISOTROPIC_PHASE + MS);

            vec3 ShadowNDCPosC = player_shadow(PlayerPosC);
            vec3 ShadowPosC = distort(ShadowNDCPosC);
            ShadowPosC = ShadowPosC * 0.5 + 0.5;
            float ShadowFactor = get_shadow_unfiltered(PlayerPosC, ShadowPosC);

            if (ShadowFactor > 0.01) { // Is in sun
                float DistToSun = DistToSky / view_player(sLightPosN).y;
                vec3 SunAttenuation = exp(-DistToSun * WATER_EXTINCTION);
                TotalScattering += TotalTransmittance * LightColorDirect * SunAttenuation * (SunPhase + MS) * ShadowFactor;
            }

            PlayerPosC += Step;
            TotalTransmittance *= Transmittance;
        }
        
    }  
    TotalScattering *= (WATER_SCATTERING / WATER_EXTINCTION) * (1 - Transmittance);

    


    return Color * TotalTransmittance + TotalScattering;
}

vec3 do_vl(vec3 StartPos, vec3 EndPos, vec3 PlayerPosN, vec3 Color, vec3 ScreenPos, bool IsDH, const int STEP_COUNT, const bool DoRT, const bool DoRTWater, const bool DoDetail) {
    switch(isEyeInWater) {
        case 0:
            return ambient_fog(StartPos, EndPos, PlayerPosN, Color, ScreenPos, STEP_COUNT, DoRT, DoDetail);
        case 1:
            // Light underwater fog enabled
            vec3 Step = (EndPos - StartPos);
            float WaterDistance = length(Step);
            float FogFactor = exp(-WaterDistance * 0.08);
            vec3 WaterFogColor = vec3(0.02, 0.1, 0.2) * (dataBuf.AmbientColor + 0.05);
            return mix(WaterFogColor, Color, FogFactor);
        default:
            return Color;
    }
}
