// ("awan tipis, harus tebal dan ada volume" fix): cloudStretch controls the
// vertical half-thickness of the cloud layer (cloudTallness = stretch * 2).
// Raised across all quality tiers so the cloud band is noticeably deeper/
// taller, reading as a real 3D volume instead of a flat sheet.
#if CLOUD_QUALITY == 1 || !defined DEFERRED1
    const float cloudStretchRaw = 28.0;
#elif CLOUD_QUALITY == 2
    const float cloudStretchRaw = 38.0;
#elif CLOUD_QUALITY == 3
    const float cloudStretchRaw = 44.0;
#endif
#if CLOUD_UNBOUND_SIZE_MULT <= 100
    const float cloudStretch = cloudStretchRaw;
#else
    const float cloudStretch = cloudStretchRaw / float(CLOUD_UNBOUND_SIZE_MULT_M);
#endif

#if CLOUD_QUALITY > 1
    const float cloudNarrowness = 0.00012;
#else
    const float cloudNarrowness = 0.00006;
#endif

const float cloudTallness = cloudStretch * 2.0;

// cloudLayer: 0 = primary layer (CLOUD_ALT1) - the original chunky,
// tightly-clustered cumulus-style shapes. 1 = secondary layer
// (CLOUD_ALT2, only traced when DOUBLE_UNBOUND_CLOUDS is active) - a
// visually distinct variant: stretched/streaky and finer-detailed, so it
// reads as a different cloud type instead of the same shape repeated at
// another height. This is what keeps the sky from looking like "one
// style" everywhere.
float GetCloudNoise(vec3 tracePos, int cloudAltitude, float lTracePosXZ, float cloudPlayerPosY, int cloudLayer) {
    vec3 tracePosM = tracePos.xyz * cloudNarrowness;

    #if defined DOUBLE_UNBOUND_CLOUDS
        // Anisotropic stretch + separate seed offset for the secondary
        // layer: pulls its noise field long and thin along one axis
        // (streaky/wispy, like cirrus) instead of the primary layer's
        // even puffy cumulus shape, and offsets it in space so the two
        // layers never line up/repeat the same pattern above one another.
        if (cloudLayer == 1) {
            tracePosM.x *= 0.45;
            tracePosM += vec3(311.0, 0.0, -197.0) * cloudNarrowness * 4000.0;
        }
    #endif

    float wind = 0.0006;
    float noise = 0.0;
    float currentPersist = 1.0;
    float total = 0.0;

    #if CLOUD_SPEED_MULT == 100
        #define CLOUD_SPEED_MULT_M CLOUD_SPEED_MULT * 0.01
        wind *= syncedTime;
    #else
        #define CLOUD_SPEED_MULT_M CLOUD_SPEED_MULT * 0.01
        wind *= frameTimeCounter * CLOUD_SPEED_MULT_M;
    #endif
    #if CLOUD_UNBOUND_SIZE_MULT != 100
        tracePosM *= CLOUD_UNBOUND_SIZE_MULT_M;
        wind *= CLOUD_UNBOUND_SIZE_MULT_M;
    #endif

    #if defined DOUBLE_UNBOUND_CLOUDS
        // Secondary layer drifts at a different speed/direction than the
        // primary layer so the two never visually lock together.
        if (cloudLayer == 1) wind *= 1.6;
    #endif

    #if CLOUD_QUALITY == 1
        int sampleCount = 2;
        float persistance = 0.6;
        float noiseMult = 0.95;
        wind *= 0.5;
    #elif CLOUD_QUALITY == 2 || !defined DEFERRED1
        int sampleCount = 4;
        // ("awan realistic, ada yang pisah ada yang berkelompok" fix):
        // dinaikkan dikit dari 0.5 -> 0.56 supaya detail skala-besar
        // (gumpalan utama) lebih dominan dibanding noise skala-kecil,
        // hasilnya bentuk awan lebih "chunky"/berbentuk jelas alih-alih
        // kabut noise yang halus rata.
        float persistance = 0.56;
        float noiseMult = 1.14;
    #elif CLOUD_QUALITY == 3
        int sampleCount = 4;
        float persistance = 0.56;
        float noiseMult = 1.0;
    #endif

    #if defined DOUBLE_UNBOUND_CLOUDS
        // The wispy secondary layer leans on the higher-frequency octaves
        // more than the primary layer does, which combined with the
        // anisotropic stretch above is what makes it read as thin
        // streaks/patches rather than solid puffs.
        if (cloudLayer == 1) {
            persistance = 0.68;
            noiseMult *= 0.8;
        }
    #endif

    #ifndef DEFERRED1
        noiseMult *= 1.2;
    #endif

    for (int i = 0; i < sampleCount; i++) {
        #if CLOUD_QUALITY >= 2
            noise += Noise3D(tracePosM - vec3(0.0, 0.0, wind)) * currentPersist;
        #else
            noise += texture2DLod(noisetex, tracePosM.xz - vec2(0.0, wind), 0.0).b * currentPersist;
        #endif
        total += currentPersist;

        tracePosM *= 3.0;
        wind *= 0.5;
        currentPersist *= persistance;
    }
    noise = pow2(noise / total);

    #if CLOUD_QUALITY >= 2
        // ("perbagus awan, gaya lebih detail/bertekstur" request - safe
        // reimplementation, TIDAK menyalin kode dari shaderpack manapun):
        // detail/erosion noise ini adalah teknik umum yang dipublikasikan
        // di industri (dipakai a.l. oleh Nubis - Guerrilla Games, dan
        // banyak volumetric cloud shader lain termasuk Alpha Piscium yang
        // jadi referensi gaya visual untuk fitur ini) - bukan kode yang
        // di-port dari source manapun, ditulis ulang dari nol di sini.
        // Efeknya: noise frekuensi tinggi "mengikis" tepi gumpalan awan
        // (area noise rendah) jauh lebih kuat daripada bagian inti yang
        // sudah padat, sehingga siluet awan kelihatan bertekstur/berongga
        // ("cauliflower") alih-alih gumpalan mulus rata.
        float detailNoise = Noise3D(tracePosM * 1.5 - vec3(0.0, 0.0, wind * 2.0));
        // ("bintik-bintik" fix): kekuatan erosion noise ini diturunkan dari
        // 0.15 -> 0.10 - di 0.15, dipadu dengan CLOUD_UNBOUND_AMOUNT yang
        // lebih tinggi, tepi gumpalan awan tererosi terlalu agresif sampai
        // pecah jadi bintik-bintik kecil terpisah alih-alih tepi
        // "cauliflower" yang bertekstur tapi tetap menyatu.
        noise -= (1.0 - noise) * detailNoise * 0.10;
        noise = max0(noise);
    #endif

    // Large-scale coverage/weather mask: sampled at a much lower frequency
    // than the shape noise above (one texel here spans many cloud shapes),
    // so it groups whole regions of sky into "mostly clear" vs "densely
    // packed" instead of every gap being the same texture-scale size. This
    // is what produces real clusters of many clouds together next to wide
    // open patches, rather than an evenly-spaced field of similar-sized
    // gaps everywhere.
    float coverage = texture2DLod(noisetex, tracePos.xz * (cloudNarrowness * 0.035) - vec2(0.0, wind * 0.2), 0.0).b;
    #if defined DOUBLE_UNBOUND_CLOUDS
        // Secondary layer reads its own coverage cell (offset + different
        // frequency) so its clusters don't land in the same places as the
        // primary layer's.
        if (cloudLayer == 1) coverage = texture2DLod(noisetex, tracePos.xz * (cloudNarrowness * 0.065) + vec2(150.0, wind * 0.35), 0.0).b;
    #endif
    // ("lebih bagus berkelompoknya" fix): skala noise coverage di atas
    // diperbesar (0.05 -> 0.035, layer kedua 0.09 -> 0.065) supaya satu
    // "sel" coverage mencakup area lebih luas - hasilnya klaster awan
    // jadi lebih besar & lebih terpisah jelas satu sama lain, bukan
    // tersebar kecil-kecil merata.
    // Range mix juga dilebarkan ke bawah (0.55 -> 0.35) supaya area
    // coverage rendah benar-benar nyaris kosong (celah langit lebih
    // lebar di antar klaster), sementara upper bound (1.25 -> 1.15)
    // tetap dijaga sedang supaya klaster padat tidak menyatu jadi satu
    // hamparan menutupi langit.
    float coverageMult = mix(0.38, 1.18, coverage);

    #define CLOUD_BASE_ADD 0.8
    //#define CLOUD_FAR_ADD -0.005
    #define CLOUD_ABOVE_ADD 0.1

    noiseMult *= CLOUD_BASE_ADD
                //+ CLOUD_FAR_ADD * sqrt(lTracePosXZ + 10.0) // more/less clouds far away
                + CLOUD_ABOVE_ADD * clamp01(-cloudPlayerPosY / cloudTallness) // more clouds when camera is above them
                + CLOUD_UNBOUND_RAIN_ADD * rainFactor; // more clouds during rain
    noise *= noiseMult * CLOUD_UNBOUND_AMOUNT * coverageMult;

    float threshold = clamp(abs(cloudAltitude - tracePos.y) / cloudStretch, 0.001, 0.999);
    threshold = pow2(pow2(pow2(threshold)));
    // ("awan realistic, ada yang pisah ada yang berkelompok" fix, sesuai
    // referensi foto): base subtract dinaikkan lagi dari 0.16 -> 0.20.
    // 0.16 terlalu rendah (nyaris semua noise lolos threshold) jadi awan
    // membentuk satu lapisan menerus tanpa celah. 0.20 membiarkan area
    // ber-noise rendah tetap kosong (celah biru terbuka / awan terpisah)
    // sementara area ber-noise tinggi tetap menggumpal tebal (kelompok
    // awan padat) - hasilnya variasi seperti campuran cumulus terpisah +
    // gumpalan tebal, bukan satu hamparan rata. The coverage mask above
    // now does the heavy lifting for *where* those dense/empty regions
    // sit at a much bigger scale.
    // ("awan jangan terlalu menutupi langit" fix): base subtract dinaikkan
    // sedikit lagi dari 0.20 -> 0.23 untuk membuka lebih banyak celah
    // langit secara keseluruhan, selaras dengan CLOUD_UNBOUND_AMOUNT yang
    // juga diturunkan.
    // ("bintik-bintik/cat air" fix): dinaikkan lagi 0.23 -> 0.27, dipasangkan
    // dengan CLOUD_UNBOUND_AMOUNT yang dinaikkan ke 1.18 di common.glsl.
    // Tanpa ini, menaikkan AMOUNT saja membuat noise oktaf-halus yang
    // tadinya tersaring ikut lolos sebagai bercak kecil terpisah (speckle)
    // di sekujur langit alih-alih hanya menambah ketebalan/besar gumpalan
    // yang sudah ada - subtract yang lebih tinggi ini menyaring balik noise
    // halus itu sementara noise skala-besar (yang membentuk gumpalan utama)
    // tetap lolos karena magnitude-nya sudah dikalikan AMOUNT yang lebih
    // besar duluan.
    return noise - (threshold * 0.2 + 0.22);
}

vec4 GetVolumetricClouds(int cloudAltitude, int cloudLayer, float distanceThreshold, inout float cloudLinearDepth, float skyFade, float skyMult0, vec3 cameraPos, vec3 nPlayerPos, float lViewPosM, float VdotS, float VdotU, float dither) {
    vec4 volumetricClouds = vec4(0.0);

    float higherPlaneAltitude = cloudAltitude + cloudStretch;
    float lowerPlaneAltitude  = cloudAltitude - cloudStretch;

    float lowerPlaneDistance  = (lowerPlaneAltitude - cameraPos.y) / nPlayerPos.y;
    float higherPlaneDistance = (higherPlaneAltitude - cameraPos.y) / nPlayerPos.y;
    float minPlaneDistance = min(lowerPlaneDistance, higherPlaneDistance);
          minPlaneDistance = max(minPlaneDistance, 0.0);
    float maxPlaneDistance = max(lowerPlaneDistance, higherPlaneDistance);
    if (maxPlaneDistance < 0.0) return vec4(0.0);
    float planeDistanceDif = maxPlaneDistance - minPlaneDistance;

    float aboveClouds = clamp01((cameraPos.y - lowerPlaneAltitude) * 0.01);
    float skyFadeM = mix(skyFade, 1.0, 0.4 * max(aboveClouds, rainFactor2));

    #ifndef DEFERRED1
        float stepMult = 64.0;
    #elif CLOUD_QUALITY == 1
        // (fix: step density was tied for backward/reversed vs QUALITY 2/3,
        // making the "Low" tier identical to "High" and the default/
        // "Standard-Ultra" tier the coarsest of all three. Now scales
        // monotonically: 1 = coarsest/cheapest, 3 = finest/priciest.)
        float stepMult = 48.0;
    #elif CLOUD_QUALITY == 2
        float stepMult = 24.0;
    #elif CLOUD_QUALITY == 3
        float stepMult = 16.0;
    #endif

    #if CLOUD_UNBOUND_SIZE_MULT > 100
        stepMult = stepMult / sqrt(float(CLOUD_UNBOUND_SIZE_MULT_M));
    #endif

    int sampleCount = int(planeDistanceDif / stepMult + dither + 1);
    vec3 traceAdd = nPlayerPos * stepMult;
    vec3 tracePos = cameraPos + minPlaneDistance * nPlayerPos;
    tracePos += traceAdd * dither;
    tracePos.y -= traceAdd.y;

    float firstHitPos = 0.0;
    float VdotSM1 = max0(sunVisibility > 0.5 ? VdotS : - VdotS);
    float VdotSM1M = VdotSM1 * invRainFactor;
    float VdotSM2 = pow2(VdotSM1) * abs(sunVisibility - 0.5) * 2.0;
    float VdotSM3 = VdotSM2 * (2.5 + rainFactor) + 1.5 * rainFactor;
    float VdotSM4 = pow(VdotSM1M, 100.0) * sunVisibility;

    #ifdef FIX_AMD_REFLECTION_CRASH
        sampleCount = min(sampleCount, 30); //BFARC
    #endif

    for (int i = 0; i < sampleCount; i++) {
        tracePos += traceAdd;

        if (abs(tracePos.y - cloudAltitude) > cloudStretch) break;

        vec3 cloudPlayerPos = tracePos - cameraPos;
        float lTracePos = length(cloudPlayerPos);
        float lTracePosXZ = length(cloudPlayerPos.xz);
        float cloudMult = 1.0;
        if (lTracePosXZ > distanceThreshold) break;
        if (lTracePos > lViewPosM) {
            if (skyFade < 0.7) continue;
            else cloudMult = skyMult0;
        }

        float cloudNoise = GetCloudNoise(tracePos, cloudAltitude, lTracePosXZ, cloudPlayerPos.y, cloudLayer);

        if (cloudNoise > 0.00001) {
            #if defined CLOUD_CLOSED_AREA_CHECK && SHADOW_QUALITY > -1 && !defined VOXY_PATCH
                float shadowLength = shadowDistance * 0.9166667; //consistent08JJ622
                if (shadowLength < lTracePos)
                if (GetShadowOnCloud(tracePos, cameraPos, cloudAltitude, lowerPlaneAltitude, higherPlaneAltitude)) {
                    if (eyeBrightness.y != 240) continue;
                }
            #endif

            if (firstHitPos < 1.0) {
                firstHitPos = lTracePos;
                #if CLOUD_QUALITY == 1 && defined DEFERRED1
                    tracePos.y += 4.0 * (texture2DLod(noisetex, tracePos.xz * cloudNarrowness * 16.0, 0.0).r - 0.5);
                #endif
            }

            // ("awan tipis" fix): opacity multiplier raised from 8.0 -> 11.0
            // so clouds ramp up to solid/thick opacity faster per step,
            // instead of staying semi-translucent/thin-looking.
            // ("awan nutupin langit semua" fix, lanjutan): multiplier
            // diturunkan dari 11.0 -> 7.5 supaya awan nggak langsung solid
            // setelah 1-2 step - dengan opacity yang lebih lembut, tepi dan
            // celah di antara gumpalan tetap kelihatan sehingga bentuk
            // kelompok awan terbaca, bukan jadi selimut tebal tanpa bentuk.
            float opacityFactor = min1(cloudNoise * 9.0);

            float cloudShading = 1.0 - (higherPlaneAltitude - tracePos.y) / cloudTallness;
            cloudShading *= 1.0 + 0.2 * VdotSM3 * (1.0 - opacityFactor) + VdotSM4;

            // ("light bounce/awan kelihatan 3D, area gelap ada AO-nya"
            // request): self-shadow/AO murah pakai data yang SUDAH ada -
            // volumetricClouds.a adalah opacity yang terkumpul dari semua
            // step sebelumnya di sepanjang ray yang sama (yaitu seberapa
            // "dalam" titik ini terkubur di balik massa awan yang sudah
            // dilewati kamera menuju titik ini). Makin banyak awan padat
            // yang sudah dilewati sebelum sampai di sini, makin gelap titik
            // ini - persis seperti area di balik/bawah gumpalan tebal
            // ternaungi oleh gumpalan itu sendiri. Ini TIDAK menambah
            // sample noise baru sama sekali (tidak ada Noise3D() ekstra),
            // cuma memanfaatkan angka yang sudah dihitung tiap step - jadi
            // biaya GPU-nya nyaris nol, aman dipakai di semua profile.
            //
            // ("bintik-bintik/cat air" fix): versi awal memakai
            // volumetricClouds.a (TOTAL yang terkumpul dari SEMUA step
            // sebelumnya) sebagai sumber AO. Karena raymarch awan ini cuma
            // beberapa step kasar (16-48 blok per step, lihat stepMult di
            // atas), angka itu melompat besar antar step alih-alih berubah
            // halus - hasilnya AO ikut melompat jadi "petak-petak" warna
            // gelap/terang yang terlihat seperti sapuan cat air, bukan
            // gradasi 3D yang mulus. Diganti ke opacityFactor - nilai
            // kepadatan LOKAL di titik ini saja (bukan akumulasi), yang
            // bervariasi mengikuti field noise 3D yang mulus - jadi
            // gradasinya halus mengikuti bentuk awan, bukan melompat per
            // step. Kekuatannya juga diturunkan (0.55 -> 0.30) supaya efek
            // 3D-nya tetap kelihatan tanpa menggelapkan berlebihan.
            float cloudSelfAO = 1.0 - 0.45 * opacityFactor;
            cloudShading *= cloudSelfAO;

            // ("perbagus atmosphere untuk adventure/survival melihat
            // pemandangan" fix - reimplementasi original, terinspirasi dari
            // referensi visual awan bergaya Nubis, TIDAK menyalin kode):
            // "silver lining" - tepi awan yang tipis (opacityFactor rendah)
            // dan menghadap matahari kena tambahan cahaya tembus, meniru
            // efek awan cumulus tebal yang disinari dari belakang di dunia
            // nyata. Dibatasi (min) supaya tidak overblow di kondisi ekstrem.
            float silverLining = pow(1.0 - opacityFactor, 3.0) * VdotSM2;
            cloudShading = min(cloudShading + silverLining, 3.5);

            vec3 colorSample = cloudAmbientColor * (0.4 + 0.6 * cloudShading) + cloudLightColor * cloudShading;
            //vec3 colorSample = 2.5 * cloudLightColor * pow2(cloudShading); // <-- Used this to take the Unbound logo
            // ("shaders kurang saturasi ... dari ACES juga" fix): default
            // tonemap pack ini sebenarnya AgX/Salicylic (TONEMAP_STYLE=5),
            // bukan ACES - tapi keduanya sama-sama menekan saturasi paling
            // keras justru di area TERANG (lihat HIGHLIGHT_SATURATION_ROLLOFF
            // di composite5.glsl: makin dekat ke putih, makin ditarik balik
            // ke netral). Awan jelas kena paling parah karena warnanya
            // memang nyaris putih/highlight duluan sebelum masuk tonemap.
            // Menaikkan HIGHLIGHT_SATURATION_ROLLOFF/T_SATURATION secara
            // global akan ikut membuat matahari/lava dari dekat jadi neon
            // (itu sebabnya rolloff itu ada). Jadi di sini warna awan
            // sendiri yang dikuatkan sedikit SEBELUM masuk tonemap - bukan
            // mengubah kurva tonemap-nya - supaya sinyal warnanya lebih
            // "kuat" duluan dan sisa saturasi yang selamat lewat rolloff
            // masih cukup terlihat, tanpa mengganggu tuning saturasi
            // global yang sudah dipakai elemen lain (matahari, lava, obor).
            {
                float cloudLuma = dot(colorSample, vec3(0.2126, 0.7152, 0.0722));
                colorSample = mix(vec3(cloudLuma), colorSample, 1.35);
            }
            vec3 cloudSkyColor = GetSky(VdotU, VdotS, dither, isEyeInWater == 0, false);
            #ifdef ATM_COLOR_MULTS
                cloudSkyColor *= sqrtAtmColorMult; // C72380KD - Reduced atmColorMult impact on some things
            #endif
            float distanceRatio = (distanceThreshold - lTracePosXZ) / distanceThreshold;
            float cloudDistanceFactor = clamp(distanceRatio, 0.0, 0.8) * 1.25;
            float cloudFogFactor = pow2(pow1_5(clamp(distanceRatio, 0.0, 1.0))) * 0.69;
            cloudFogFactor = mix(1.0, cloudFogFactor, skyFadeM);
            colorSample *= 1.0 - 0.2 * (1.0 - skyFadeM) * max(sunVisibility2, nightFactor);
            colorSample = mix(cloudSkyColor, colorSample, cloudFogFactor * 0.69);
            colorSample *= pow2(1.0 - maxBlindnessDarkness);

            // ("nembus awan jangan kaya menghilang tiba-tiba" fix): saat
            // kamera benar-benar MASUK ke dalam badan awan (terbang lewat
            // celah/gumpalan), titik-titik yang sangat dekat dengan kamera
            // sebelumnya langsung diberi opacity penuh dalam 1-2 step -
            // hasilnya layar mendadak jadi putih solid seolah awan "meledak"
            // menutupi semua, alih-alih terasa seperti masuk ke kabut tebal
            // secara bertahap. nearCamFade meredam opacity di ~28 blok
            // pertama di sekitar kamera saja (smoothstep, bukan potongan
            // tegas), jadi begitu kamu masuk ke gumpalan awan, yang terlihat
            // adalah kabut yang menebal perlahan (mirip masuk kabut asli)
            // bukan dinding putih instan. Di luar 28 blok itu (awan yang
            // dilihat dari jauh/luar) nilainya tetap 1.0, jadi tampilan awan
            // dari luar sama sekali tidak berubah. Cuma 1x smoothstep extra
            // per step yang sudah dijalankan, jadi biaya GPU-nya diabaikan.
            float nearCamFade = smoothstep(0.0, 28.0, lTracePos);
            volumetricClouds.rgb = mix(volumetricClouds.rgb, colorSample, 1.0 - min1(volumetricClouds.a));
            volumetricClouds.a += opacityFactor * pow(cloudDistanceFactor, 0.5 + 10.0 * pow(abs(VdotSM1M), 90.0)) * cloudMult * nearCamFade;

            if (volumetricClouds.a > 0.9) {
                volumetricClouds.a = 1.0;
                break;
            }
        }
    }

    if (volumetricClouds.a > 0.5) cloudLinearDepth = sqrt(firstHitPos / renderDistance);

    volumetricClouds.a *= 0.5 + 0.5 * skyFadeM;

    return volumetricClouds;
}