vec3 do_water_vl(vec3 StartPos, vec3 EndPos, vec3 PlayerPosN, vec3 Color, float Depth, const int STEP_COUNT) {
    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 * far;
    }

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

    float Dither = dither(gl_FragCoord.xy);
    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);

    vec3 Transmittance = exp(-Density * WATER_EXTINCTION);
    float SunPhase = ISOTROPIC_PHASE;

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

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

        vec3 ShadowViewPosC = (shadowModelView * vec4(PlayerPosC, 1)).xyz;
        vec3 ShadowNDCPosC = project_and_divide(shadowProjection, ShadowViewPosC);
        vec3 ShadowPosC = distort(ShadowNDCPosC);
        ShadowPosC = ShadowPosC * 0.5 + 0.5;
        float ExpectedDepth = ShadowPosC.z;
        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 * ShadowFactor;
        }
        PlayerPosC += Step;
        TotalTransmittance *= Transmittance;
    }

    TotalScattering *= (WATER_SCATTERING / WATER_EXTINCTION) * (1 - Transmittance);

    return Color * TotalTransmittance + TotalScattering;
}

vec3 do_overworld_vl(vec3 StartPos, vec3 EndPos, vec3 PlayerPosN, vec3 Color, const int STEP_COUNT) {
    const float MAX_HEIGHT = 150;
    const float SIGMA_AIR = 0.5;
    const float DENSITY = 1e-3;

    vec3 WorldPos = cameraPosition + StartPos;
    // Fog gets strong at morning
    float DaytimeFactor = worldTime > 12000 ? smoothstep(20000, 24000, worldTime) : 1 - smoothstep(0, 2000, worldTime);
    DaytimeFactor = max(DaytimeFactor, 0.2);

    if (WorldPos.y <= MAX_HEIGHT) {
        if (EndPos.y + cameraPosition.y > MAX_HEIGHT)
            EndPos = intersectRayWithPlane(WorldPos, PlayerPosN, MAX_HEIGHT);
        if (EndPos == vec3(0)) return Color;
    }
    else {
        if (EndPos.y + cameraPosition.y > MAX_HEIGHT) return Color;
        StartPos = intersectRayWithPlane(WorldPos, PlayerPosN, MAX_HEIGHT);
        if (StartPos == vec3(0)) return Color;
    }

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

    vec3 AmbientColor = dataBuf.AmbientColor * vec3(1, 2, 4); // This looks good
    AmbientColor = mix(MinLight, AmbientColor, isIndoorsSmooth);

    float VdotL = dot(view_player(sLightPosN), PlayerPosN);
    float MiePhase = cs_phase(VdotL, 0.6) * 0.75 + cs_phase(VdotL, -0.3) * 0.25;
    float DensityConstant = DaytimeFactor * StepSize * DENSITY;
    for (int i = 1; i <= STEP_COUNT && length(PlayerPosC.xz) < far; i++) {
        vec3 WorldPosC = PlayerPosC + cameraPosition;

        float Detail = texture(worleyNoiseTexture, WorldPosC / vec3(256, 32, 256) * 3 + frameTimeCounter * 0.02).r;
        float Density = Detail;
        Density *= DensityConstant;
        float HeightFalloff = 1 - smoothstep(MAX_HEIGHT - 50, MAX_HEIGHT, WorldPosC.y);
        Density *= HeightFalloff;
        if (Density <= 0) {
            PlayerPosC += Step;
            continue;
        }

        vec3 Transmittance = vec3(exp(-Density * SIGMA_AIR));
        FinalColor += AmbientColor * Density * TotalTransmittance;

        vec3 ShadowViewPosC = (shadowModelView * vec4(PlayerPosC, 1)).xyz;
        vec3 ShadowNDCPosC = project_and_divide(shadowProjection, ShadowViewPosC);
        vec3 ShadowPosC = distort(ShadowNDCPosC);
        ShadowPosC = ShadowPosC * 0.5 + 0.5;
        float ExpectedDepth = ShadowPosC.z;
        float ShadowFactor = get_shadow_unfiltered(PlayerPosC, ShadowPosC);

        if (ShadowFactor > 0.01) { // Is in sun
            FinalColor += LightColorDirect * MiePhase * Density * TotalTransmittance * ShadowFactor;
        }
        PlayerPosC += Step;
        TotalTransmittance *= Transmittance;
    }

    return Color * TotalTransmittance + FinalColor;
}

vec3 do_nether_vl(vec3 EndPos, vec3 PlayerPosN, vec3 Color, const int STEP_COUNT) {
    if(isEyeInWater != 0) return Color;
    
    const float LOWER_PLANE = 30.0;
    const float UPPER_PLANE = 70.0;

    const float SIGMA_SMOKE = .5;

    vec3 StartPos = vec3(0), PlaneEndPos;
    bool Hit = intersect_with_cloud_plane(cameraPosition.xyz, StartPos, PlaneEndPos, PlayerPosN, LOWER_PLANE, UPPER_PLANE);
    if (!Hit) return Color;

    if(dot(PlaneEndPos, PlaneEndPos) < dot(EndPos, EndPos)) {
        EndPos = PlaneEndPos;
    }

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

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

    float TotalTransmittance = 1;

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

        float Detail = noise_smoke(WorldPosC);
        float Density = Detail;
        float HeightFalloff = 1 - smoothstep(UPPER_PLANE - 50, UPPER_PLANE, WorldPosC.y);
        Density *= HeightFalloff;
        float DistFalloff = min(length(PlayerPosC) / 16, 1);
        Density *= DistFalloff;
        if (Density <= 0) {
            PlayerPosC += Step;
            continue;
        }

        TotalTransmittance *= exp(-Density * SIGMA_SMOKE);
        PlayerPosC += Step;
    }

    return Color * TotalTransmittance;
}

vec3 do_vl(vec3 StartPos, vec3 EndPos, vec3 PlayerPosN, vec3 Color, float Depth, const int STEP_COUNT) {
    if (isEyeInWater == 1) return do_water_vl(StartPos, EndPos, PlayerPosN, Color, Depth, STEP_COUNT * 2);

    #ifdef DIMENSION_OVERWORLD
        return do_overworld_vl(StartPos, EndPos, PlayerPosN, Color, STEP_COUNT);
    #elif defined DIMENSION_NETHER
        return do_nether_vl(EndPos, PlayerPosN, Color, STEP_COUNT / 2);
    #else
        return vec3(1);
    #endif
}
