#if defined STORY_MODE_CLOUDS && !defined FORCE_DISABLE_CLOUDS

// ------- Const ------- //
const float CLOUD_BASE_HEIGHT     = 0.0 + 320.0;
const float CLOUD_TOP_HEIGHT      = 48.0 + 320.0;

const float CLOUD_COVERAGE        = 0.45;
const float CLOUD_SIGMA_T         = 25.0;
const float CLOUD_SIGMA_SHADOW    = 5.0;
const float CLOUD_AMBIENT         = 5.0;
const float CLOUD_SCATTER_STR     = 0.2;
const float CLOUD_G1              =  0.10;
const float CLOUD_G2              = -0.10;
const float CLOUD_G_MIX           =  0.25;

// ------- Helpers ------- //
float fetchNoiseSeamless(vec2 uv, float scale, vec2 offset) {
    return texture(noisetex, (uv * scale + offset) / 256.0).x;
}

vec3 desaturate(vec3 col, float factor) {
    return mix(col, vec3(dot(col, vec3(0.299, 0.587, 0.114))), factor);
}

vec3 getCloudLightDir() {
    #if defined WORLD_LIGHT && !defined FORCE_DISABLE_DAY_CYCLE
        return normalize(mat3(gbufferModelViewInverse) * normalize(shadowLightPosition));
    #else
        return vec3(0.0, 1.0, 0.0);
    #endif
}

vec3 getSunMoonCol() {
    #ifdef WORLD_LIGHT
        #ifdef FORCE_DISABLE_DAY_CYCLE
            return lightCol;
        #else
            return mix(moonCol, sunCol, dayCycleAdjust);
        #endif
    #else
        return vec3(1.0);
    #endif
}

float cloudPhase(float cosTheta) {
    float g1sq = CLOUD_G1 * CLOUD_G1;
    float g2sq = CLOUD_G2 * CLOUD_G2;
    float p1 = (1.0 - g1sq) / (4.0 * 3.14159 * pow(max(1.0 + g1sq - 2.0 * CLOUD_G1 * cosTheta, 1e-4), 1.5));
    float p2 = (1.0 - g2sq) / (4.0 * 3.14159 * pow(max(1.0 + g2sq - 2.0 * CLOUD_G2 * cosTheta, 1e-4), 1.5));
    return mix(p1, p2, CLOUD_G_MIX);
}

float smin(float a, float b, float k) {
    float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
    return mix(b, a, h) - k * h * (1.0 - h);
}

float roundedCubeSDF(vec3 p, vec3 size, float radius) {
    vec3 q = abs(p) - size + radius;
    return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0) - radius;
}

// ------- Cubic Cloud Density ------- //
float cloudDensity(vec3 worldP, float time) {
    float rainExpand = rainStrength * 15.0;  // Tune: meters of vertical growth
    
    float baseHeight = CLOUD_BASE_HEIGHT - rainExpand;
    float topHeight  = CLOUD_TOP_HEIGHT + rainExpand * 0.15;

    float h = (worldP.y - baseHeight) / (topHeight - baseHeight);
    if (h < 0.0 || h > 1.0) return 0.0;

    // Symmetric fade profile
    float fadeWidth = 0.35 + rainStrength * 0.1;
    float profile = smoothstep(0.0, fadeWidth, h) * 
                    smoothstep(1.0, 1.0 - fadeWidth, h);

    vec2 wind    = vec2(time * 32.0 * CLOUD_SPEED, 0.0);
    vec2 xzRaw   = worldP.xz + wind;
    vec2 xzSnap  = floor(xzRaw / CLOUD_VOXEL_SIZE) * CLOUD_VOXEL_SIZE;
    
    float mass   = fetchNoiseSeamless(xzSnap, 1.0 / (CLOUD_CELL_SIZE * 1.8), vec2(0.0,  0.0));
    float detail = fetchNoiseSeamless(xzSnap, 1.0 / (CLOUD_CELL_SIZE * 0.55), vec2(31.7, 67.3));

    float n = mass * 0.70 + detail * 0.30;

    float coverage = clamp(CLOUD_COVERAGE * (1.0 + rainStrength * 0.5), 0.0, 1.0);
    n = clamp((n - (1.0 - coverage)) / coverage, 0.0, 1.0);

    n *= 1.0 + rainStrength * 0.0001;
    n = clamp(n, 0.0, 1.0);

    // Contrast curve for defined edges
    n = n * n;  // ^2
    n = n * n;  // ^4
    
    // Time-based cloud shape
    n *= exp(sin(fragmentFrameTime/256)*2);

    return n * profile;
}

// ------- Self Shadow ------- //
float cloudTransmittance(vec3 worldP, vec3 lightDir, float time) {
    // --- Smoother horizon fade ---
    // Widen transition: fade shadows from 0° to ~15° above horizon
    float horizonFade = smoothstep(0.0, 0.25, lightDir.y);
    
    // Optional: additional fade based on light angle relative to cloud layer
    // Makes shadows softer when light grazes the cloud layer sideways
    float layerAngleFade = smoothstep(0.0, 0.3, abs(lightDir.y));
    float angleBlend = mix(horizonFade, layerAngleFade, 0.4);
    
    // --- Adaptive march length ---
    float tMax = lightDir.y > 0.01
               ? (CLOUD_TOP_HEIGHT - worldP.y) / lightDir.y
               : (CLOUD_TOP_HEIGHT - CLOUD_BASE_HEIGHT);
    tMax = min(tMax, CLOUD_CELL_SIZE * 2.5);
    
    // Optional: reduce steps near horizon for performance + softer result
    float stepFactor = mix(0.5, 1.0, angleBlend);
    float stepSize = (tMax / float(SHADOW_STEPS)) * stepFactor;
    
    float n = 0.0;
    for (int i = 1; i <= SHADOW_STEPS; i++) {
        vec3 sampleP = worldP + lightDir * (float(i) * stepSize);
        // Early exit if we leave cloud layer vertically
        if (sampleP.y < CLOUD_BASE_HEIGHT || sampleP.y > CLOUD_TOP_HEIGHT) break;
        n += cloudDensity(sampleP, time) * stepSize;
    }

    // --- Softer shadow attenuation ---
    // Use a gentler exponential curve + optional density threshold
    float opticalDepth = n * CLOUD_SIGMA_SHADOW;
    float shadow = exp(-opticalDepth);
    
    // Fade shadow strength near horizon using smoother blend
    float shadowStrength = smoothstep(0.0, 1.0, angleBlend);
    shadow = mix(1.0, shadow, shadowStrength);
    
    return shadow;
}
// ------- Interior Renderer ------- //
vec3 renderCloudsInterior(vec3 nEyePlayerPos, vec3 currSkyCol,
                          float rainStrength, float fragmentFrameTime, float dither) {
    vec3  ld       = getCloudLightDir();
    vec3  amb      = currSkyCol * CLOUD_AMBIENT;
    vec3  cloudTint = desaturate(getSunMoonCol(), 0.7); // hoisted out of loop
    float cosTheta = dot(-nEyePlayerPos, ld);
    float phase    = cloudPhase(cosTheta);

    float tExit;
    if      (nEyePlayerPos.y >  1e-4) tExit = (CLOUD_TOP_HEIGHT  - cameraPosition.y) / nEyePlayerPos.y;
    else if (nEyePlayerPos.y < -1e-4) tExit = (CLOUD_BASE_HEIGHT - cameraPosition.y) / nEyePlayerPos.y;
    else                              tExit  = CLOUD_TOP_HEIGHT - CLOUD_BASE_HEIGHT;
    float dt = abs(tExit) / float(INTERIOR_STEPS);

    float transmit = 1.0;
    vec3  scatter  = vec3(0.0);
    for (int i = 0; i < INTERIOR_STEPS; i++) {
        if (transmit < 0.01) break;
        vec3  p = cameraPosition + nEyePlayerPos * (dt * (float(i) + dither));
        float d = cloudDensity(p, fragmentFrameTime);
        if (d < 0.001) continue;
        vec3  Lin = mix(amb, cloudTint * cloudTransmittance(p, ld, fragmentFrameTime)
                        * (1.0 + phase * CLOUD_SCATTER_STR), 0.85);
        float dT  = exp(-d * CLOUD_SIGMA_T * dt);
        scatter  += transmit * (1.0 - dT) * Lin;
        transmit *= dT;
    }

    float opacity = 1.0 - transmit;
    #ifndef FORCE_DISABLE_WEATHER
        scatter = mix(scatter, amb * opacity, rainStrength * 0.5);
        opacity = min(opacity * mix(1.0, 1.4, rainStrength), 1.0);
    #endif
    float fade = smoothstep(CLOUD_BASE_HEIGHT, CLOUD_BASE_HEIGHT + 3.0, cameraPosition.y)
               * smoothstep(CLOUD_TOP_HEIGHT,  CLOUD_TOP_HEIGHT  - 3.0, cameraPosition.y);
    return mix(currSkyCol, currSkyCol * transmit + scatter, opacity * fade);
}

// ------- Exterior Renderer ------- //
vec3 renderClouds(vec3 nEyePlayerPos, vec3 currSkyCol,
                  float rainStrength, float fragmentFrameTime) {
    if (cameraPosition.y > CLOUD_BASE_HEIGHT && cameraPosition.y < CLOUD_TOP_HEIGHT) {
        float dither = fract(texelFetch(noisetex, ivec2(gl_FragCoord.xy) & 255, 0).x + fragmentFrameTime * 0.618);
        return renderCloudsInterior(nEyePlayerPos, currSkyCol, rainStrength, fragmentFrameTime, dither);
    }

    bool above = cameraPosition.y >= CLOUD_TOP_HEIGHT;
    if ( above && nEyePlayerPos.y > -0.01) return currSkyCol;
    if (!above && nEyePlayerPos.y <  0.01) return currSkyCol;

    float tBase = (CLOUD_BASE_HEIGHT - cameraPosition.y) / nEyePlayerPos.y;
    float tTop  = (CLOUD_TOP_HEIGHT  - cameraPosition.y) / nEyePlayerPos.y;
    if (tBase > tTop) { float tmp = tBase; tBase = tTop; tTop = tmp; }
    if (tTop < 0.0) return currSkyCol;
    tBase = max(tBase, 0.0);

    vec3  ld        = getCloudLightDir();
    vec3  amb       = currSkyCol * CLOUD_AMBIENT;
    vec3  cloudTint = desaturate(getSunMoonCol(), 0.7); // hoisted out of loop
    float cosTheta  = dot(nEyePlayerPos, ld);
    float phase     = cloudPhase(cosTheta);
    float dither    = fract(texelFetch(noisetex, ivec2(gl_FragCoord.xy) & 255, 0).x + fragmentFrameTime * 0.618);
    float dt        = (tTop - tBase) / float(MARCH_STEPS);

    float transmit = 1.0;
    vec3  scatter  = vec3(0.0);
    for (int i = 0; i < MARCH_STEPS; i++) {
        if (transmit < 0.01) break;
        vec3  p = cameraPosition + nEyePlayerPos * (tBase + (float(i) + dither) * dt);
        float d = cloudDensity(p, fragmentFrameTime);
        if (d < 0.001) continue;
        vec3  Lin = mix(amb, cloudTint * cloudTransmittance(p, ld, fragmentFrameTime)
                        * (1.0 + phase * CLOUD_SCATTER_STR), 0.85);
        float dT  = exp(-d * CLOUD_SIGMA_T * dt);
        scatter  += transmit * (1.0 - dT) * Lin;
        transmit *= dT;
    }

    float opacity   = 1.0 - transmit;
    float angleFade = smoothstep(0.0, 0.2, abs(nEyePlayerPos.y));
    #ifndef FORCE_DISABLE_WEATHER
        scatter   = mix(scatter, amb * opacity, rainStrength * 0.55);
        opacity   = mix(opacity, min(opacity * 1.5, 1.0), rainStrength * 0.35);
        angleFade *= 1.0 - rainStrength * 0.45;
    #endif
    return mix(currSkyCol, currSkyCol * transmit + scatter, angleFade);
}

#endif