/*
 * Visuality fluffy curved clouds
 *
 * A lightweight 2D cloud layer: three noise taps for the shape and one
 * directional tap for fake sunlight. The layer is intersected as a gently
 * curved shell instead of an infinite flat plane, so the distant cloud bank
 * falls toward the horizon and the sky feels rounder. No volumetric ray march.
 */

#define CLOUD_QUALITY 0 //[0 1 2] //2 = Volumetric: lapisan 2D lama jadi kabur/tipis (awan tinggi), diganti awan volumetrik 1-layer di bawahnya
#define CLOUD_COVERAGE 0.68 //[0.35 0.45 0.55 0.62 0.68 0.74 0.80 0.85]
#define CLOUD_DENSITY 1.00 //[0.65 0.80 0.95 1.00 1.15 1.30 1.50]
#define CLOUD_SPEED 1.00 //[0.00 0.25 0.50 0.75 1.00 1.25 1.50 2.00]
#define CLOUD_SUNSET_GLOW 1.25 //[0.50 0.75 1.00 1.25 1.50 1.75 2.00]
#define CLOUD_FLUFFINESS 1.15 //[0.50 0.70 0.85 1.00 1.15 1.30 1.50]
#define CLOUD_HORIZON_CURVE 1.00 //[0.00 0.25 0.50 0.75 1.00 1.25 1.50]
#define CLOUD_SELF_SHADOW 0.82 //[0.00 0.20 0.40 0.60 0.75 0.82 0.90 1.00 1.50]
#define CLOUD_TOP_LIGHT 1.12 //[0.40 0.50 0.60 0.80 1.00 1.12 1.25 1.40 1.60]
#define CLOUD_UNDERSIDE_DARKNESS 0.74 //[0.30 0.45 0.60 0.74 0.85 1.00 1.50]
#define CLOUD_DAY_BRIGHTNESS 0.70 //[0.40 0.55 0.60 0.65 0.70 0.75 0.80 0.90 1.00]
#define CLOUD_TWILIGHT_BRIGHTNESS 0.88 //[0.65 0.70 0.75 0.80 0.85 0.88 0.92 1.00]
#define CLOUD_NIGHT_BRIGHTNESS 1.35 //[0.85 1.00 1.15 1.25 1.35 1.50 1.70 1.90]

// --- Dipakai HANYA saat CLOUD_QUALITY == 2 (Volumetric), digabung di sini
// biar tetap satu tempat "settingan cloud" saja, bukan kategori terpisah ---
#define VLCLOUD_SCALE 0.00090 //[0.00050 0.00065 0.00080 0.00090 0.00110 0.00140 0.00180] ukuran gumpalan awan volumetrik (besar/kecil) -- makin kecil angkanya makin besar gumpalannya
#define CLOUD_VOLUMETRIC_2D_VEIL 0.26 //[0.10 0.16 0.20 0.26 0.32 0.40 0.50] seberapa kelihatan lapisan awan 2D lama saat mode Volumetric aktif -- dibuat rendah supaya jadi kabut tipis/buram di ketinggian, bukan awan penuh
// Sedikit warm/kuning saat siang, tetapi tetap dekat putih agar tidak menjadi kuning pekat.
const vec3 VLCLOUD_DAY_TINT = vec3(1.08, 1.02, 0.86);
#define VLCLOUD_SELF_SHADOW_STRENGTH 1.25 //[1.00 1.10 1.20 1.25 1.35 1.50 1.70] lebih hitam pada bayangan dalam awan volumetrik
#define VLCLOUD_DAY_BRIGHTNESS 0.62 //[0.40 0.48 0.54 0.58 0.62 0.68 0.75 0.85 1.00]

const float CLOUD_HEIGHT = 1900.0;
const float CLOUD_SCALE  = 0.000025;

/* Rounded cellular noise from the red channel makes sheep-wool-like lobes. */
float cloudShape(vec2 uv, vec2 windUV) {
    mat2 cloudRotation = mat2(0.8870, -0.4617,
                                    0.4617,  0.8870);

    float broad = texture2D(noisetex,
        uv * vec2(0.55, 0.72) + vec2(0.071, 0.193)).r;

    vec2 rotatedUV = cloudRotation * uv;
    float lobes = texture2D(noisetex,
        rotatedUV * vec2(1.45, 1.80) + windUV * 0.22
        + vec2(0.413, 0.127)).r;

    float wool = texture2D(noisetex,
        uv * vec2(3.40, 4.60) - windUV * 0.16
        + vec2(0.237, 0.681)).r;

    float shape = broad * 0.56 + lobes * 0.31 + wool * 0.13;

    /* Raise isolated round lobes so the outline is puffy rather than smeared. */
    shape += max(lobes - broad * 0.72, 0.0)
           * (0.070 * CLOUD_FLUFFINESS);

#if CLOUD_QUALITY == 1
    float fineWool = texture2D(noisetex,
        rotatedUV * vec2(7.20, 9.10) + windUV * 0.11
        + vec2(0.611, 0.347)).r;
    shape += (fineWool - 0.24) * 0.040;
#endif

    return shape;
}

/*
 * Intersect a ray with a parabolic approximation of a spherical cloud shell.
 * CLOUD_HORIZON_CURVE 0 is almost flat; larger values bend the distant layer
 * downward more strongly. This costs only a square root per sky pixel.
 */
float curvedCloudDistance(vec3 rayDir, float layerHeight, out float shellDrop) {
    if (CLOUD_HORIZON_CURVE <= 0.001) {
        shellDrop = 0.0;
        if (rayDir.y <= 0.004) return -1.0;
        return layerHeight / rayDir.y;
    }

    float curve = max(CLOUD_HORIZON_CURVE, 0.02);
    float fakeRadius = 48000.0 / curve;
    float horizontal2 = dot(rayDir.xz, rayDir.xz);
    float a = horizontal2 / (2.0 * fakeRadius);
    float b = rayDir.y;

    shellDrop = 0.0;

    if (a < 0.0000001) {
        if (b <= 0.0001) return -1.0;
        return layerHeight / b;
    }

    float discriminant = b * b + 4.0 * a * layerHeight;
    float distanceToLayer = (-b + sqrt(max(discriminant, 0.0))) / (2.0 * a);
    float horizontalDistance = sqrt(horizontal2) * distanceToLayer;
    shellDrop = horizontalDistance * horizontalDistance / (2.0 * fakeRadius);

    return distanceToLayer;
}

vec3 cloud2D(vec3 fragpos, vec3 col) {
    vec3 rayDir = normalize(fragpos);

    if (cameraPosition.y >= CLOUD_HEIGHT) return col;
    if (CLOUD_HORIZON_CURVE < 0.05 && rayDir.y <= 0.004) return col;
    if (rayDir.y <= -0.10) return col;

    float layerHeight = CLOUD_HEIGHT - cameraPosition.y;
    float shellDrop;
    float layerDistance = curvedCloudDistance(rayDir, layerHeight, shellDrop);
    if (layerDistance <= 0.0) return col;
    layerDistance = min(layerDistance, 180000.0);

    vec2 windWorld = vec2(frameTimeCounter * 18.0,
                          frameTimeCounter * 5.0) * CLOUD_SPEED;
    vec2 windUV = windWorld * CLOUD_SCALE;

    vec2 cloudWorld = cameraPosition.xz
                    + rayDir.xz * layerDistance
                    + windWorld;
    vec2 uv = cloudWorld * CLOUD_SCALE;

    float shape = cloudShape(uv, windUV);

    /* Larger coverage lowers the threshold and fills more of the sky.
     * As rainBuild rises, the threshold drop is much bigger than before so
     * separate puffs visibly merge into a gathered bank instead of just a
     * slight coverage bump, and the edge softness shrinks at the same time
     * so the bank reads as packed-together rather than still fluffy. */
    float threshold = 0.350 - CLOUD_COVERAGE * 0.205;
    float rainBuild = smoothstep(0.02, 0.92, rainStrength);
    threshold -= rainBuild * 0.145;

    float fluffAmount = clamp((CLOUD_FLUFFINESS - 0.50) / 1.00, 0.0, 1.0);
    float edgeWidth = mix(0.110, 0.178, fluffAmount);
    edgeWidth *= mix(1.0, 0.70, rainBuild);
    float cloudMask = smoothstep(threshold, threshold + edgeWidth, shape);
    float coreMask = smoothstep(threshold + edgeWidth * 0.30,
                                threshold + edgeWidth * 0.98,
                                shape);
    float woolEdge = max(cloudMask - coreMask, 0.0);

    vec3 sunDir = normalize(sunVec);
    vec2 sunXZ = sunDir.xz;
    float sunXZLength = sqrt(max(dot(sunXZ, sunXZ), 0.0001));
    vec2 sunPlanar = sunXZ / sunXZLength;
    float sunHeight = clamp(sunDir.y, 0.0, 1.0);
    float sunVisible = smoothstep(-0.12, 0.08, sunDir.y);
    float moonVisible = smoothstep(-0.12, 0.08, -sunDir.y);

    /*
     * Dynamic cloud self-shadowing. The offset follows the horizontal sun
     * direction, while the reach grows as the sun gets lower. This makes the
     * darker side of the cloud bank move naturally from morning to noon to
     * evening instead of staying fixed in one direction.
     */
    float lowSunShadowReach = 1.0 / max(sunHeight, 0.10);
    float lightOffset = clamp(0.008 + lowSunShadowReach * 0.010, 0.008, 0.115);
    float towardSunNoise = texture2D(noisetex,
        (uv + sunPlanar * lightOffset) * vec2(0.55, 0.72)
        + vec2(0.071, 0.193)).r;
    float towardSunShape = towardSunNoise * 0.72 + shape * 0.28;
    float towardSunMask = smoothstep(threshold + 0.004,
                                     threshold + edgeWidth,
                                     towardSunShape);

    /*
     * A dense lobe between the current fragment and the sun blocks direct
     * light. The shadow strength rises when the sun is high, while the
     * longer low-sun offset creates the stretched, directional evening look.
     */
    float selfShadow = towardSunMask
                     * CLOUD_SELF_SHADOW
                     * sunVisible
                     * (0.42 + sunHeight * 0.58);
    float sunAccess = clamp(1.0 - selfShadow, 0.0, 1.0);
    float litEdge = max(cloudMask - towardSunMask, 0.0);

    float daytime = sunVisible;
    float lowSun = (1.0 - smoothstep(0.075, 0.42, sunHeight)) * daytime;
    float sunsetAmount = clamp(lowSun * CLOUD_SUNSET_GLOW, 0.0, 1.35);
    vec3 sunsetTint = mix(vec3(1.0), vec3(1.22, 0.61, 0.25), sunsetAmount);

    float viewSun = max(dot(rayDir, sunDir), 0.0);
    float forwardScatter = pow(viewSun, 6.0);

    /*
     * Side-aware lighting: when the camera is below the cloud deck we see the
     * underside, so the direct sun contribution is deliberately suppressed.
     * When the camera is above the deck, the top surface receives the sun and
     * becomes brighter. This keeps the cloud tops bright at noon and the
     * underside dark, matching the natural light direction.
     */
    float cameraAboveClouds = step(CLOUD_HEIGHT + 18.0, cameraPosition.y);
    float topFacingLight = mix(0.66, CLOUD_TOP_LIGHT, sunHeight) * sunVisible;
    float undersideDark = mix(1.0, 1.0 - CLOUD_UNDERSIDE_DARKNESS * sunHeight,
                              1.0 - cameraAboveClouds);
    float moonFill = moonVisible * (0.58 + 0.30 * (1.0 - rainStrength));

    vec3 darkCloud = mix(col * 0.55, cloudCol * 0.105, 0.70);
    vec3 brightCloud = cloudCol * sunsetTint
                     * (0.48 + sunAccess * 0.64)
                     * max(topFacingLight, 0.20);
    brightCloud = mix(brightCloud,
                      cloudCol * vec3(0.55, 0.63, 0.78) * moonFill,
                      moonVisible * 0.32);

    vec3 cloudColor = mix(darkCloud, brightCloud, 0.24 + sunAccess * 0.62);
    cloudColor *= undersideDark;

    /* Soft white wool rim plus warmer sun-facing silver lining. */
    cloudColor += cloudCol * sunsetTint * woolEdge
                * (0.10 + fluffAmount * 0.15)
                * (0.60 + topFacingLight * 0.45);
    cloudColor += cloudCol * sunsetTint * litEdge
                * (0.24 + forwardScatter * 1.05 + lowSun * 0.26)
                * (0.70 + topFacingLight * 0.35);

    /*
     * Strengthen the underside shadow when viewed from below. The horizon
     * curve still darkens distant cloud banks gently, but no longer flattens
     * the whole cloud layer into one uniform shade.
     */
    float curvedDistance = clamp(shellDrop / max(layerHeight, 1.0), 0.0, 1.0);
    float undersideView = 1.0 - cameraAboveClouds;
    cloudColor *= 1.0
                - coreMask * curvedDistance * (0.16 + 0.12 * undersideView)
                - selfShadow * (0.10 + 0.18 * undersideView);

    float luminance = dot(cloudColor, vec3(0.2126, 0.7152, 0.0722));
    cloudColor = mix(cloudColor,
                     vec3(luminance) * vec3(0.68, 0.76, 0.86),
                     rainStrength * 0.62);

    // Independent HDR control for cloud light: high noon is intentionally
    // softer than morning/evening so cloud tops keep shape instead of clipping.
    float dayWeight = smoothstep(0.22, 0.62, sunDir.y);
    float nightWeight = 1.0 - smoothstep(-0.12, 0.10, sunDir.y);
    float twilightWeight = clamp(1.0 - dayWeight - nightWeight, 0.0, 1.0);
    float cloudPhaseBrightness = dayWeight * CLOUD_DAY_BRIGHTNESS
                              + twilightWeight * CLOUD_TWILIGHT_BRIGHTNESS
                              + nightWeight * CLOUD_NIGHT_BRIGHTNESS;
    cloudColor *= cloudPhaseBrightness;

    float rainDensityBoost = mix(1.0, 1.28, rainBuild);
    float opacity = clamp(cloudMask * CLOUD_DENSITY * rainDensityBoost, 0.0, 0.95);

    /* Keep a visible curved cloud bank right down to the horizon. */
    opacity *= smoothstep(-0.020, 0.008, rayDir.y);
    opacity *= 1.0 - smoothstep(90000.0, 170000.0, layerDistance);

    return mix(col, cloudColor, opacity);
}

/*
 * ============================================================================
 * AWAN VOLUMETRIK OVERWORLD -- aktif saat preset CLOUD_QUALITY == 2
 * ============================================================================
 * REVISI KE-2: sebelumnya cuma 1 sample analitik (ketebalan "diakali" lewat
 * panjang lintasan), naik jadi RAYMARCH VLCLOUD_LAYERS LAYER SUNGGUHAN
 * menembus slab Y184-Y200, karena 1 sample kelihatan terlalu tipis waktu
 * dilihat dari sisi/pinggir awan (grazing angle dekat ketinggian awan itu
 * sendiri). Tetap jauh lebih ringan dari kabut The End (8 layer, jarak
 * sampai 140 blok) -- di sini cuma sedikit layer dalam slab yang sangat
 * tipis (16 blok), jadi total biayanya masih jauh di bawah itu.
 * REVISI KE-3: jumlah sample sempat diturunkan ke 2 untuk menghemat FPS.
 * PATCH TAA-OFF: dinaikkan ke 6 sample dan titik sampling dibuat deterministik
 * di tengah tiap interval agar grain/dither tidak terlihat saat TAA dimatikan.
 *
 * Teknik intersect slab (vlIntersectSlab di bawah) dibuat menangani TIGA
 * posisi kamera sekaligus (di bawah awan / di dalam slab / di atas awan)
 * lewat min/max dua bidang datar Y184 & Y200 -- ini yang bikin "terbang di
 * atas awan otomatis kelihatan seperti terbang di atas awan" tanpa perlu
 * cabang if terpisah untuk tiap kasus.
 *
 * Kenapa BUKAN raw texture2D seperti cloudShape() di atas: awan ini jauh
 * lebih dekat ke kamera (~Y192) dibanding layer 2D (Y1900), jadi noise-nya
 * "diperbesar" jauh lebih banyak di layar -- bakal kelihatan pecah/blocky
 * kalau dibaca mentah. vlSmoothTex() menghaluskan pembacaan noise dengan
 * interpolasi smoothstep manual (pola sama seperti smoothfilter() yang
 * sudah ada di gbuffers_skybasic.fsh, ditulis ulang lokal di sini karena
 * urutan #include). ini "opsi lain" pengganti TAA untuk tepi awan.
 * Banding antar layer raymarch dikurangi pakai vlDither() -- hash spasial
 * per-pixel (dibumbui frameTimeCounter dikit spy tidak kelihatan statis),
 * BUKAN TAA: satu frame saja, tidak ada buffer histori/reprojection sama
 * sekali, beda konsep total dari TAA. Ditulis lokal (bukan pakai nrand()
 * dari lib/color_dither.glsl) karena gbuffers_water.fsh yang juga
 * meng-include file ini TIDAK meng-include color_dither.glsl -- kalau
 * dipanggil dari sana bisa gagal compile.
 *
 * BAYANGAN DI BAGIAN BAWAH AWAN SAJA (REVISI KE-3, ganti gradien bola):
 * sebelumnya tiap gumpalan awan punya gradien hitam->putih yang arahnya
 * ikut posisi matahari (sisi menghadap matahari terang, sisi membelakangi
 * gelap, gaya pencahayaan bola). Gradien itu SUDAH DIHAPUS sesuai
 * permintaan. Sekarang tiap sample raymarch cuma dibandingkan tinggi
 * lokalnya di dalam slab (heightInSlab, 0 di dasar Y184 -> 1 di atas
 * Y200): sample dekat dasar awan di-mix ke arah shadowColor (bayangan),
 * sample dekat atas awan tetap litColor (terang). Hasilnya bayangan cuma
 * muncul di bagian BAWAH awan saja -- tidak lagi mengikuti sisi kiri/kanan
 * relatif ke matahari. Kekuatan bayangannya pakai CLOUD_UNDERSIDE_DARKNESS
 * yang sudah ada (setting yang sama dipakai cloud2D() untuk menggelapkan
 * sisi bawah lapisan 2D lama), jadi tidak perlu setting baru.
 */

// Ketinggian slab awan volumetrik -- SESUAI PERMINTAAN.
// Dasar Y184, pusat Y192, atas Y200 (tebal 16 blok).
const float VLCLOUD_BASE_Y    = 184.0;
const float VLCLOUD_CENTER_Y  = 192.0;
const float VLCLOUD_TOP_Y     = 200.0;
const float VLCLOUD_THICKNESS = VLCLOUD_TOP_Y - VLCLOUD_BASE_Y;

#define VLCLOUD_LAYERS 3          // 3 sample/layer: lebih ringan, tetap deterministik saat TAA OFF
#define VLCLOUD_EXTINCTION 0.065  // seberapa cepat tiap layer ini "memakan" cahaya per blok -- perlu di-tuning langsung di game

#if CLOUD_QUALITY == 2

vec4 vlSmoothTex(vec2 uv, float texRes) {
    uv = uv * texRes + 0.5;
    vec2 iuv = floor(uv);
    vec2 fuv = fract(uv);
    uv = iuv + fuv * fuv * (3.0 - 2.0 * fuv);
    uv = uv / texRes - 0.5 / texRes;
    return texture2D(noisetex, uv);
}

/* 2 tap noise saja (broad + 1 detail halus) per layer -- tetap murah walau sekarang dipanggil 4x (1x per layer raymarch). */
float vlCloudShape(vec2 uv, vec2 windUV) {
    mat2 cloudRotation = mat2(0.8870, -0.4617, 0.4617, 0.8870);

    float broad = vlSmoothTex(uv * vec2(0.60, 0.78) + vec2(0.071, 0.193), 1024.0).r;

    vec2 rotatedUV = cloudRotation * uv;
    float detail = vlSmoothTex(rotatedUV * vec2(2.60, 3.10) + windUV * 0.30
                              + vec2(0.413, 0.127), 1024.0).r;

    float shape = broad * 0.72 + detail * 0.28;
    shape += max(detail - broad * 0.70, 0.0) * (0.09 * CLOUD_FLUFFINESS);
    return shape;
}

/* Hash/dither helper retained for compatibility; volumetric path below does not use it
   so the cloud image stays clean and deterministic when TAA is disabled. */
float vlHash(vec2 p) {
    return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}
float vlDither(vec2 screenPos) {
    return vlHash(screenPos + fract(frameTimeCounter) * 13.37);
}

/*
 * Cari rentang [tEntry, tExit] tempat ray menembus slab Y184-Y200, dengan
 * min/max dua bidang datar -- otomatis benar untuk kamera di BAWAH slab,
 * DI DALAM slab, maupun DI ATAS slab (menunduk melihat awan dari atas),
 * tanpa perlu cabang if terpisah per kasus.
 */
bool vlIntersectSlab(vec3 rayDir, float camY, out float tEntry, out float tExit) {
    if (abs(rayDir.y) < 0.0025) return false; // nyaris horizontal persis: hindari pembagian nyaris nol

    float tBase = (VLCLOUD_BASE_Y - camY) / rayDir.y;
    float tTop  = (VLCLOUD_TOP_Y  - camY) / rayDir.y;

    tEntry = min(tBase, tTop);
    tExit  = max(tBase, tTop);

    if (tExit <= 0.0) return false; // slab sepenuhnya di belakang kamera
    tEntry = max(tEntry, 0.0);      // kamera di DALAM slab -> mulai raymarch dari kamera sendiri

    return true;
}

vec3 volumetricCloud(vec3 rayDir, vec3 col) {
    float camY = cameraPosition.y;

    float tEntry, tExit;
    if (!vlIntersectSlab(rayDir, camY, tEntry, tExit)) return col;
    if (tEntry > 2600.0) return col; // terlalu jauh, tetap dijaga ringan

    float slabSpan = tExit - tEntry;
    if (slabSpan <= 0.0001) return col;

    vec2 windWorld = vec2(frameTimeCounter * 13.0, frameTimeCounter * 4.5) * CLOUD_SPEED;
    vec2 windUV = windWorld * VLCLOUD_SCALE;

    float threshold = 0.360 - CLOUD_COVERAGE * 0.210;
    float rainBuild = smoothstep(0.02, 0.92, rainStrength);
    threshold -= rainBuild * 0.150;
    float fluffAmount = clamp((CLOUD_FLUFFINESS - 0.50) / 1.00, 0.0, 1.0);
    float edgeWidth = mix(0.140, 0.220, fluffAmount);
    edgeWidth *= mix(1.0, 0.72, rainBuild);

    // --- pencahayaan global, dihitung sekali sebelum loop (murah, tanpa baca texture) ---
    vec3 sunDir = normalize(sunVec);
    float sunHeight = clamp(sunDir.y, 0.0, 1.0);
    float sunVisible = smoothstep(-0.12, 0.08, sunDir.y);
    float moonVisible = smoothstep(-0.12, 0.08, -sunDir.y);

    float lowSun = (1.0 - smoothstep(0.075, 0.42, sunHeight)) * sunVisible;
    float sunsetAmount = clamp(lowSun * CLOUD_SUNSET_GLOW, 0.0, 1.35);
    vec3 sunsetTint = mix(vec3(1.0), vec3(1.22, 0.61, 0.25), sunsetAmount);
    float moonFill = moonVisible * (0.58 + 0.30 * (1.0 - rainStrength));

    // Terbang di atas awan (Y>200) -> lihat permukaan atas yang lebih terang/putih,
    // bukan bagian bawah yang teduh. Murah: cuma 1 step() + 2 mix, tidak ada texture tambahan.
    float viewFromAbove = step(VLCLOUD_TOP_Y, camY);

    vec3 shadowColor = mix(col * 0.30, cloudCol * 0.045, 0.72);
    vec3 litColor = cloudCol * sunsetTint * mix(0.85, CLOUD_TOP_LIGHT, sunHeight) * sunVisible;
    litColor = mix(litColor, cloudCol * vec3(0.62, 0.72, 0.92) * moonFill, moonVisible * 0.52);
    litColor = max(litColor, cloudCol * vec3(0.42, 0.52, 0.70) * moonFill * moonVisible * 0.78);
    litColor = max(litColor, shadowColor * 1.05);
    shadowColor = mix(shadowColor, shadowColor * 1.4 + vec3(0.02), viewFromAbove * 0.5);
    litColor = mix(litColor, litColor * 1.15 + vec3(0.03), viewFromAbove * 0.6);

    // --- raymarch deterministik: sample di tengah tiap interval.
    // Tidak memakai frame-dependent dither agar TAA OFF tidak menghasilkan grain/noise.
    // Tiga layer dipakai untuk mengurangi beban GPU tanpa menambah layer tambahan.
    float stepSize = slabSpan / float(VLCLOUD_LAYERS);
    float rayStart = tEntry + stepSize * 0.5;

    float transmittance = 1.0;
    vec3 scattered = vec3(0.0);

    for (int i = 0; i < VLCLOUD_LAYERS; i++) {
        float t = rayStart + stepSize * float(i);
        vec3 worldPos = cameraPosition + rayDir * t;

        vec2 uv = (worldPos.xz + windWorld) * VLCLOUD_SCALE;
        float shape = vlCloudShape(uv, windUV);
        float cloudMask = smoothstep(threshold, threshold + edgeWidth, shape);

        // Gradien bola (mengikuti arah matahari) sudah dihapus. Sekarang cuma
        // bayangan di bagian BAWAH awan saja: makin dekat dasar slab (Y184)
        // makin gelap, makin dekat atas slab (Y200) tetap terang/litColor.
        // undersideBoost: bayangan dibuat makin kuat saat matahari makin
        // tinggi (siang, sunHeight -> 1) -- fix untuk "bayangan kurang gelap
        // pas siang". Saat sunHeight -> 0 (pagi/sore/malam) boost = 1.0,
        // sama seperti sebelumnya, jadi waktu lain tidak ikut berubah.
        float heightInSlab = clamp((worldPos.y - VLCLOUD_BASE_Y) / VLCLOUD_THICKNESS, 0.0, 1.0);
        float undersideBoost = mix(1.0, 1.55, sunHeight);
        float underShadow = clamp((1.0 - heightInSlab) * CLOUD_UNDERSIDE_DARKNESS * undersideBoost * VLCLOUD_SELF_SHADOW_STRENGTH, 0.0, 1.0);
        vec3 layerColor = mix(litColor, shadowColor, underShadow);

        float density = cloudMask * CLOUD_DENSITY;
        float stepTrans = exp(-density * VLCLOUD_EXTINCTION * stepSize);
        scattered += layerColor * (1.0 - stepTrans) * transmittance;
        transmittance *= stepTrans;
    }

    float dayWeight = smoothstep(0.22, 0.62, sunDir.y);
    float nightWeight = 1.0 - smoothstep(-0.12, 0.10, sunDir.y);
    float twilightWeight = clamp(1.0 - dayWeight - nightWeight, 0.0, 1.0);
    float cloudPhaseBrightness = dayWeight * VLCLOUD_DAY_BRIGHTNESS
                              + twilightWeight * CLOUD_TWILIGHT_BRIGHTNESS
                              + nightWeight * (CLOUD_NIGHT_BRIGHTNESS + 0.10);
    vec3 cloudColor = scattered * cloudPhaseBrightness;

    // Siang dibuat sedikit hangat supaya awan volumetrik tidak tampak putih menyilaukan.
    // Tint ini hanya bekerja pada bagian siang; warna pagi/sore/malam tetap dipertahankan.
    cloudColor *= mix(vec3(1.0), VLCLOUD_DAY_TINT, dayWeight);

    float luminance = dot(cloudColor, vec3(0.2126, 0.7152, 0.0722));
    cloudColor = mix(cloudColor, vec3(luminance) * vec3(0.68, 0.76, 0.86), rainStrength * 0.62);

    float rainCloudDensity = mix(1.0, 1.32, rainBuild);
    float opacity = clamp((1.0 - transmittance) * rainCloudDensity, 0.0, 0.95);
    opacity *= smoothstep(-0.010, 0.020, abs(rayDir.y)); // tetap kelihatan sampai dekat horizon, dari atas maupun bawah
    opacity *= 1.0 - smoothstep(1900.0, 2600.0, tEntry);  // memudar sebelum batas jarak

    return mix(col, cloudColor, opacity);
}

#endif // CLOUD_QUALITY == 2
