~firefoxsimulationslandscape
6 itemsDownload ./*

..
build
jsm
models
textures
index.gpu.tesselldation.old.html
index.html


landscapeindex.html
150 KB• 84•  1 day ago•  DownloadRawClose
1 day ago•  84

{}
<!DOCTYPE html>
<!--
    Author: Twily                                           2025-2026
    Landscape/skybox webgl demo

    Keybinds:
    w       wireframe toggle on/off {sky,terrain,mountains}
    s	    shadows toggle on,debugdither,debugclean,off
    c       clouds toggle 2d2(default),2d1,ray[heavy],ray[medium],off
    d       depthwrite toggle skybox on/off
    t       time toggle realtime[run],testval[run],testval[pause]
    space   toggle terrain moving on/off
-->
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Three.js Infinite Landscape</title>
    <style>
        html,body { width: 100%; height: 100%; margin: 0; padding: 0;  overflow: hidden; user-select: none; background: #344C6C; color: #ccc; }
        canvas { display: block; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 100; }
        #notices {
            position: absolute;
            top: 10px;
            left: 10px;
            color: yellow;
            font-family: monospace;
            font-size: 12px;
            pointer-events: none;
            z-index: 1001;
            display: flex;
            flex-flow: column;
        }
        #notices > span {
            display: inline-block;
            background: rgba(0, 0, 0, 0.5);
            padding: 10px;
        }
        #info {
            position: absolute;
            top: 10px;
            right: 10px;
            background: rgba(0, 0, 0, 0.5);
            color: white;
            padding: 10px;
            font-family: monospace;
            font-size: 12px;
            pointer-events: none;
            z-index: 1000;
        }
        #loading {
            position: fixed; top: 0; left: 0;
            width: 100%; height: 100%;
            background: transparent;
            text-shadow: 0 0 6px #000;
            color: #fff;
            font-size: 22pt;
            text-align: center;
            opacity: 1;
            transition: opacity 1s ease;
            z-index: 1001;
        }
        #loadtxt {
            //width: 500px; text-align: left;
            display: inline-block;
        }
        .tbl { display: table; width: 100%; height: 100%; table-layout: fixed; }
        .tr { display: table-row; }
        .td { display: table-cell; vertical-align: middle;}
    </style>
</head>
<body>
    <div id="loading"><div class="tbl"><div class="tr"><div class="td"><span id="loadtxt">Loading...</span></div></div></div></div>
    <div id="notices"></div>
    <div id="info"></div>
    <script type="importmap">
        {
            "imports": {
                "three": "./build/three.module.js",
                "three/addons/": "./jsm/"
            }
        }
    </script>
    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        //import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'; // replaces orbitcontrols
        import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

        const tileSize = 4000; // Widened to hide edges easier
        const segments = 512; // Increased for better detail/resolution
        const heightScale = 50;
        const noiseScale = 100;
        const speed = 50; // Flying speed
        const numTiles = 5; // Keep this many tiles visible

const vertexShaderSky = `
    uniform mat4 mvpMatrix;
    varying vec3 vWorldPosition;
    varying vec3 vLocalPosition;
    //varying vec2 vNormal;
    varying vec2 vUv;
    varying vec3 ourNormal;
    void main() {
        //ourNormal=vNormal;
        ourNormal=normal;
        vLocalPosition = position;
        //uv = vec2(vNormal.x, vNormal.z);
        vUv = uv * 1.0;
        vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        //gl_Position = mvpMatrix * vec4(position, 1.0);
    }
`;

const fragmentShaderSky = `
    uniform vec3 topColor;
    uniform vec3 bottomColor;
    uniform vec3 moonColor;
    uniform vec3 sunColor;
    uniform vec3 sunDirection;
    //uniform vec3 moonDirection;
    uniform float sunSize;
    uniform float moonSize;
    varying vec3 vWorldPosition;
    varying vec3 vLocalPosition;
    varying vec3 ourNormal;
    varying vec2 vUv;
    uniform float xtime; // seconds~day
    uniform float daytime; // 1 0 1
    uniform vec3 lightDir;
    uniform vec3 lightDir2;
    uniform int cloudsReady;

    uniform float cloudDensity;
    uniform float cloudAbsorption;
    uniform float cloudScale;
    uniform float cloudSpeed;
    uniform float cloudBottom;
    uniform float cloudThickness;
    uniform float scatteringAniso;
    uniform int maxSteps;
    uniform float marchSize;
    uniform int lightSteps;
    uniform float frame;
    uniform int cloudsOn;
    uniform int starsOn;
    uniform float cDensity;

    uniform vec3 horizonGlowColorDay;
	uniform vec3 horizonGlowColorNight;
    uniform float horizonGlowIntensity;
    uniform float horizonGlowHeight;
    uniform float horizonGlowSharpness;
    uniform float horizonNoiseScale;
    uniform float horizonNoiseStrength;
	uniform int horizonGlowOn;
    
    const float PI = 3.1415926535897932384626433832795;
    
    // random2 noise2 fbm2 for clouds2 builtin~ simple clouds 2
    // Noise functions
    float random2(vec3 p) {
        return fract(sin(dot(p, vec3(12.9898, 78.233, 45.5432))) * 43758.5453123);
    }
    
    float noise2(vec3 p) {
        vec3 i = floor(p);
        vec3 f = fract(p);
        vec3 u = f * f * (3.0 - 2.0 * f);
        return mix(mix(mix(random2(i), random2(i + vec3(1.0, 0.0, 0.0)), u.x),
            mix(random2(i + vec3(0.0, 1.0, 0.0)), random2(i + vec3(1.0, 1.0, 0.0)), u.x), u.y),
            mix(mix(random2(i + vec3(0.0, 0.0, 1.0)), random2(i + vec3(1.0, 0.0, 1.0)), u.x),
            mix(random2(i + vec3(0.0, 1.0, 1.0)), random2(i + vec3(1.0, 1.0, 1.0)), u.x), u.y), u.z);
    }
    
    float fbm2(vec3 p) {
        float v = 0.0;
        float a = 0.5;
        vec3 shift = vec3(100.0);
        for (int i = 0; i < 2; ++i) { // default 6
            v += a * noise2(p);
            p = p * 2.0 + shift;
            a *= 0.2; // gain ( https://thebookofshaders.com/13/ )
        }
        return v;
    }

    // Pseudo-random hash function based on 2D coordinates
    float hash(vec2 p) {
        return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
    }
    
    // Generate four random values per grid cell
    vec4 hash42(ivec2 p) {
        vec2 p2 = vec2(p);
        return vec4(
            hash(p2),
            hash(p2 + vec2(1.0, 0.0)),
            hash(p2 + vec2(0.0, 1.0)),
            hash(p2 + vec2(1.0, 1.0))
        );
    }

// indent removed for section raymarched clouds
// standard GLSL 3D Perlin for clouds
float mod289(float x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 perm(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }

float noise3D(vec3 p) {
    vec3 a = floor(p);
    vec3 d = p - a;
    d = d * d * (3.0 - 2.0 * d);

    vec4 b = a.xxyy + vec4(0.0, 1.0, 0.0, 1.0);
    vec4 k1 = perm(b.xyxy);
    vec4 k2 = perm(k1.xyxy + b.zzww);

    vec4 c = k2 + a.zzzz;
    vec4 k3 = perm(c);
    vec4 k4 = perm(c + 1.0);

    vec4 o1 = fract(k3 * (1.0 / 41.0));
    vec4 o2 = fract(k4 * (1.0 / 41.0));

    vec4 o3 = o2 * d.z + o1 * (1.0 - d.z);
    vec2 o4 = o3.yw * d.x + o3.xz * (1.0 - d.x);

    return o4.y * d.y + o4.x * (1.0 - d.y);
}

float fbm(vec3 p) {
    float f = 0.0;
    float amp = 0.5;
    float freq = 1.0;
    mat3 rot = mat3(0.36, 0.93, 0.0, -0.93, 0.36, 0.0, 0.0, 0.0, 1.0); // Rotation to break artifacts

    for (int i = 0; i < 5; i++) {
        f += amp * noise3D(p * freq);
        p *= rot * 2.01;
        amp *= 0.45; // Slightly less persistence for fluff
        freq *= 2.0;
    }
    return f;
}

// Scattering and absorption
float hg(float g, float mu) {
    float gg = g * g;
    return (1.0 - gg) / (4.0 * PI * pow(1.0 + gg - 2.0 * g * mu, 1.5));
}

float beers(float dist, float abs) {
    return exp(-dist * abs);
}

// Cloud march (returns scattered light and transmittance for blending)
struct CloudResult {
    vec3 scattered;
    float transmittance;
};

// Updated raymarchClouds
CloudResult raymarchClouds(vec3 ro, vec3 rd) {
    vec3 scattered = vec3(0.0);
    float transmittance = 1.0;
    float depth = 0.0;

    vec2 frag = gl_FragCoord.xy + frame;
    float dither = (hash(frag.xy) - 0.5) * marchSize * 0.5;

    if (rd.y <= 0.0) return CloudResult(vec3(0.0), 1.0); // Skip below horizon

    float cloudTop = cloudBottom + cloudThickness;
    float tBottom = (cloudBottom - ro.y) / rd.y;
    float tTop = (cloudTop - ro.y) / rd.y;
    float tStart = max(0.0, min(tBottom, tTop));
    float tEnd = min(max(tBottom, tTop), 10000.0); // Cap max dist for perf

    if (tStart >= tEnd) return CloudResult(vec3(0.0), 1.0);

    depth = tStart + dither;
    int i = 0;
    while (i < maxSteps && depth < tEnd && transmittance > 0.01) {
        vec3 p = ro + depth * rd;

        vec3 noisePos = p * cloudScale + vec3(xtime * cloudSpeed * 0.3, 0.0, xtime * cloudSpeed);

        float heightFrac = (p.y - cloudBottom) / cloudThickness;
        float baseDensity = smoothstep(0.0, 0.1, heightFrac) * smoothstep(1.0, 0.6, heightFrac) * cloudDensity; // Softer fade, puffier top

        float noise = fbm(noisePos);
        float detail = fbm(noisePos * 4.0); // High-freq for erosion
        float density = max(0.0, baseDensity * (noise * 1.2 - 0.2) - detail * 0.3); // Gaps + fluff

        if (density > 0.001) {
            // Light march (short)
            float lightDepth = 0.0;
            float lightTrans = 1.0;
            vec3 lightDir = sunDirection;
            int j = 0;
            while (j < lightSteps) {
                vec3 lp = p + lightDepth * lightDir;
                float lheightFrac = (lp.y - cloudBottom) / cloudThickness;
                float lbase = smoothstep(0.0, 0.1, lheightFrac) * smoothstep(1.0, 0.6, lheightFrac);
                float lnoise = fbm(lp * cloudScale + vec3(xtime * cloudSpeed * 0.3, 0.0, xtime * cloudSpeed));
                float ldetail = fbm(lp * cloudScale * 4.0 + vec3(xtime * cloudSpeed * 0.3, 0.0, xtime * cloudSpeed));
                float ldens = max(0.0, lbase * (lnoise * 1.2 - 0.2) - ldetail * 0.3);
                lightTrans *= beers(marchSize, ldens * cloudAbsorption);
                lightDepth += marchSize;
                if (lightTrans < 0.01) break;
                j++;
            }

            float su = dot(rd, sunDirection);
            float mu = dot(rd, -sunDirection);
            float phasem = hg(scatteringAniso, su) + hg(-0.2, mu) * 0.1; // Multi-lobe for soft scatter
            float phases = hg(scatteringAniso, mu) + hg(-0.2, mu) * 0.3; // Multi-lobe for soft scatter

            float ds = marchSize; // Step size for integral
            float powder = 1.0 - exp(-2.0 * density * ds * cloudAbsorption); // Silver lining

            float luminance1 = density * phases * lightTrans * powder;
            float luminance2 = density * phasem * lightTrans * powder;

            vec3 cloudTint = mix(bottomColor, topColor, heightFrac * 0.8 + 0.2); // Subtle time-of-day blend

            scattered += ds * luminance1 * sunColor * cloudTint * transmittance; // Add ds, no arbitrary boost
            scattered += ds * luminance2 * moonColor * cloudTint * transmittance; // Add ds, no arbitrary boost

            transmittance *= beers(ds, density * cloudAbsorption);
        }

        depth += marchSize;
        i++;
    }

    return CloudResult(scattered, transmittance);
} // end ray clouds

    //--float getDaytimeFactor(float daytime) {
    //--    float t;
    //--    
    //--    if (daytime <= 0.5) {
    //--        // Stay very low most of the time → only rise quickly near 0.5
    //--        float u = daytime / 0.5;
    //--        t = pow(u, 5.0);                    // try 4.0 → 7.0
    //--    } else {
    //--        // Fast initial brightening after noon → then slow creep to full sun
    //--        float u = (daytime - 0.5) / 0.5;
    //--        t = 1.0 - pow(1.0 - u, 0.25);       // 0.2–0.4 feels nice
    //--    }
    //--    
    //--    return t;
    //--}
    float getDaytimeFactor(float daytime) {
        float t;
        float mid = 0.5;         // Adjust if "0.5" isn't exactly sunrise/noon; e.g., 0.3 for earlier sunrise
        float y_mid = 0.75;      // Value at mid-point (low = linger moon; high = quick to sun)
        float ny_mid = 0.25;      // Value at mid-point (low = linger moon; high = quick to sun)
        float gamma = 3.0;       // Higher = slower majority (linger moon), sharper rise near mid
        float delta = 3.0;       // Lower = faster jump after mid, slower tail to full sun
    
        if (daytime <= mid) {
            // Very slow majority → sharp rise near mid (exclude most night, quick fade-in)
            float u = daytime / mid;
            t = y_mid * pow(u, gamma);
        } else {
            // Fast jump after mid → slow majority to full (quick start, exclude most day by stabilizing high)
            float u = (daytime - mid) / (1.0 - mid);
            t = y_mid + (1.0 - y_mid) * pow(u, delta);
        }
    
        return t;
    }

    void main() {
        vec3 viewDir = normalize(vWorldPosition - cameraPosition);
        float height = normalize(vWorldPosition).y - 0.15; // -.15 pulls up the gradient
        
        vec2 uv = vUv;
        
        float rad=PI/180.0;
        float phase=xtime/86400.0; // realtime
        //phase=xtime/2000.0; // debugtime testval
        
        //--float daytime=abs((xtime/(86400.0*.5))-1.0); // do on cpu instead

        float twinkleSpeed = 86400.0 / 50.0;
        //-- float twinkle = sin(uTime * twinkleSpeed);

        //float time = sin(phase * twinkleSpeed);
        float time = phase / (2.0 * 3.14159); // Normalizes phase from 0-2π to 0-1

        time *= twinkleSpeed;


        // Configuration parameters (adjustable)
        float numCells = 43.0;      // Number of grid cells across UV space (20x20 grid)
        float maxBrightness = 2.0 * (1.0-((daytime*.5)+.5));  // Maximum star brightness
        //float twinkleSpeed = 2.0;   // Speed of twinkling
        //float sigma = 0.0000002;      // Glow size (in UV space, tweak based on resolution)
        float sigma = 0.0000002;      // Glow size (in UV space, tweak based on resolution)
        float starDensity = 0.5;    // Fraction of cells with stars (0.0 to 1.0)
        
        float angle = PI / 2.0; // Or use a fixed value like PI / 2.0 for 90 deg

        // Center the UVs
        vec2 centered_uv = uv - 0.5;

        // Calculate rotation factors
        float cos_angle = cos(angle);
        float sin_angle = sin(angle);

        // Apply rotation matrix (mat2)
        // [ cos(a)  -sin(a) ] [x]   [x*cos(a) - y*sin(a)]
        // [ sin(a)   cos(a) ] [y] = [x*sin(a) + y*cos(a)]
        vec2 rotated_uv = vec2(
            centered_uv.x * cos_angle - centered_uv.y * sin_angle,
            centered_uv.x * sin_angle + centered_uv.y * cos_angle
        );

        // Translate back to original space
        vec2 final_uv = rotated_uv + 0.5;


        vec2 moving_uv = final_uv + vec2(0.0, time / (2.0 * PI));
        
        ivec2 cell = ivec2(floor(moving_uv * numCells));

        vec3 totalColor = vec3(0.0);  // Accumulate star contributions
        
        float starHeight=0.0; // stops at pulled up horizon
        if(height>starHeight) {
        // Check the current cell and its 8 neighbors for star contributions
        for (int i = -1; i <= 1; i++) {
            for (int j = -1; j <= 1; j++) {
                ivec2 neighbor = cell + ivec2(i, j);
                vec4 h = hash42(neighbor);

                // Only create a star if the random value meets density threshold
                if (h.w < starDensity) {
                    // Random position offset within the cell
                    vec2 starOffset = h.xy;
                    vec2 starPos = (vec2(neighbor) + starOffset) / numCells;

                    // Distance from fragment to star
                    float d = length(moving_uv - starPos);

                    // Star brightness with twinkling
                    float baseBrightness = h.z * maxBrightness;
                    float twinklePhase = h.w * 6.28318; // 2π for phase
                    float twinkle = 0.5 + 0.5 * sin(phase * twinkleSpeed + twinklePhase);
                    float totalBrightness = baseBrightness * twinkle;

                    // Glow effect using Gaussian falloff
                    float glow = exp(-d * d / sigma);
                    totalColor += vec3(totalBrightness * glow);
                }
            }
        }
        }
        
        float t = max(height, 0.0);
        // Apply smoothstep to remap the linear 't' value to a curved one
        //float curved_t = smoothstep(0.0, 0.1, t); 
        float curved_t = pow(t, .666); 
        //float curved_t = t * t * (3.0 - 2.0 * t);
        vec3 gradient = mix(bottomColor, topColor, curved_t); // Gradient based on y
        float sunDot = dot(viewDir, sunDirection);
        float moonDot = dot(viewDir, -sunDirection);
        float moonGlow = pow(smoothstep(1.0 - moonSize, 1.0, moonDot),.8); // Blended sun disc
        float moonRim = smoothstep(1.0 - moonSize * 1.05, 1.0, moonDot); // Blended moon disc
        float moonAura = smoothstep(1.0 - moonSize * 420.0, 1.0, moonDot); // Blended moon disc
        float moonAura2 = smoothstep(1.0 - moonSize * 20.0, 1.0, moonDot); // Blended moon disc
        float sunGlow = pow(smoothstep(1.0 - sunSize, 1.0, sunDot),.8); // Blended sun disc
        float sunRim = smoothstep(1.0 - sunSize * 1.05, 1.0, sunDot); // Blended sun disc
        float sunAura = smoothstep(1.0 - sunSize * 1200.0, 1.0, sunDot); // Blended sun disc
        float sunAura2 = smoothstep(1.0 - sunSize * 150.0, 1.0, sunDot); // Blended sun disc
        float sunAura3 = smoothstep(1.0 - sunSize * 30.0, 1.0, sunDot); // Blended sun disc
        //float sunHalo = smoothstep(0.95, 1.0, pow(sunDot, 150.0));

        vec3 sunColor2=sunColor;
        vec3 moonColor2=moonColor;
        //sunColor2.b+=(height * .25);
        //sunColor2.g+=(height * .55);
        //sunColor2.r=1.0;
        //sunColor2.g=0.0;
        //sunColor2.b=0.0;
        //sunColor2.g-=1.0-((daytime * .25) + .75);
        //sunColor2.b-=1.0-((daytime * .25) + .75);
        //sunColor2.r+=1.0-((daytime * .25) + .75);
        

		//float moonBlend = ((moonAura * 0.05) + (moonAura2 * 0.05) + (moonRim) + (moonGlow * 5.0)) * (1.0-getDaytimeFactor(daytime));
        //float sunBlend = ((sunAura3 * .2) + (sunAura2 * 0.3) + (sunAura * (daytime * 0.5)) + (sunRim) + (sunGlow * 15.0)) * getDaytimeFactor(daytime);
        float moonBlend=((moonAura * .03) + (moonAura2 * .05) + (moonRim) + (moonGlow * 2.0)) * max((1.0-((daytime*2.0)-1.0)),0.0);
        float sunBlend=((sunAura3 * .1) + (sunAura2 * .15) + (sunAura * (daytime * .5)) + (sunRim) + (sunGlow * 5.0)) * max(((daytime*.5)+0.5),0.0);
    
        vec3 skyColor=vec3(1.0,0.0,1.0);
        if(starsOn==1 && height>starHeight) {
            skyColor=mix(mix(gradient+(totalColor * max((1.0-((daytime*.5)+.0)),0.0)),moonColor2,moonBlend),sunColor2,sunBlend) * 1.02;
        } else {
            skyColor=mix(mix(gradient,moonColor2,moonBlend),sunColor2,sunBlend) * 1.02;
        }
        skyColor+=sunGlow * sunColor2;
        skyColor+=moonGlow * moonColor2;


        // -------------------
        // Horizon Glow Gradient (bottom bleed-up)
        // -------------------
        float horizonFactor = pow(1.0 - height-.15, horizonGlowSharpness); // sharp falloff upward
        horizonFactor = smoothstep(0.0, 1.0, horizonFactor);           // soften base
        
        // Optional: stronger near actual horizon, weaker high up
        horizonFactor *= (1.0 - smoothstep(0.0, horizonGlowHeight * 2.0, height));
        
        // Mild noise perturbation (using your existing fbm2 / noise2)
        vec3 noisePos = vec3(vUv * horizonNoiseScale, xtime * 0.05); // slow animate
        float noise = fbm2(noisePos * 2.0) * 2.0 - 1.0;               // -1..1 range
        noise *= horizonNoiseStrength;
        
        // Modulate glow with noise + height falloff
        float glowAmount = horizonFactor * (1.0 + noise * 0.5);       // subtle variation
        glowAmount = clamp(glowAmount, 0.0, 1.5);                     // prevent overbright
        
        // Final additive glow — use your moon/sun color or a dedicated one
        vec3 horizonGlow = mix(horizonGlowColorNight,horizonGlowColorDay,daytime) * glowAmount * horizonGlowIntensity * (1.0-((cDensity*.5)+.5));
        // recommend mix intensity here with cloud density or other weather in future
        // (1.0-(max((daytime*2.0),0.0)))=mixn


        vec3 finalColor = skyColor;
        if(cloudsReady==1) {
        if(cloudsOn==1) {
            // Clouds: Assume ro ≈ vec3(0), but pass cameraPosition if offset
            CloudResult clouds = raymarchClouds(cameraPosition, viewDir);

            // Blend: Scattered + sky through clouds (stars dim behind)
            finalColor = clouds.scattered + clouds.transmittance * skyColor;

            finalColor += (1.0 - clouds.transmittance) * 0.1 * gradient; // Subtle night glow
            if(starsOn==1) {
                finalColor += (1.0 - clouds.transmittance) * 0.1 * totalColor; // Subtle night glow
            }
        } else if(cloudsOn==2) {
            vec3 pos = normalize(vLocalPosition);
            //vec3 npos = vec3(pos.y, -pos.x, pos.z); // random flip to fit

            vec3 cloudColor = vec3(1.0,1.0,1.0);
            //vec3 finalColor = vec3(0.0,0.0,0.0);
            float alpha=0.0;
            vec3 lightColor = vec3(0.0,0.0,0.0);
            if(height>-0.15) { // avoid calc for bottom half

            //float cloudtime = abs((xtime / 43200.0)-1.0);
            //float cloudtime = abs((xtime / 10.0)-4320.0);
            float phasetime = xtime / 5.0; // match in shadow shader if using
            float phaselen = (86400.0 / 5.0) * .5; // realtime
            //float phaselen = (2000.0 / 5.0) * .5; // debug
            float cloudtime = abs(phasetime-phaselen);
            
            float scale = 5.0;
            vec3 flow1 = pos * scale + vec3(cloudtime * 0.10);
            vec3 flow2 = pos * scale - vec3(cloudtime * 0.06);
            vec3 flow3 = pos * scale + vec3(cloudtime * 0.03);
            float n1 = noise2(flow1);
            float n2 = noise2(flow2);
            float n3 = noise2(flow3);
            float baseNoise = (n2 + n3 - n1) * cDensity; // Base detail
            float detailNoise = fbm2(pos * scale * 4.2 + vec3(cloudtime * 0.1)); // Smudged variation
            float _cloudDensity = smoothstep(0.3, .7, baseNoise + detailNoise * 0.2); // Increased coverage
            cloudColor = vec3(.5, .5, .5);
            alpha = _cloudDensity;
            
            // taken from https://twily.info/plainC/terrain/data/shaders/frag_shader_clouds.glsl#view
            vec3 norm=normalize(ourNormal);
            vec3 sunColor = vec3(1.0, 0.45, 0.0); 
            vec3 moonColor = vec3(0.0, 0.8, 1.0);
            float mixn=min((daytime*2.0),1.0); // 0 - 0.5 // night half
            float mixt=max((daytime*2.0)-1.0,0.0); // 0.5 - 1 // day half
            //float sunStrength=(0.5-mixt)+.5;
            //float moonStrength=1.0-(mixn);
            float sunStrength=0.5-mixt;
            float moonStrength=0.5-mixn;
            vec3 alterSunDirection=vec3(sunDirection.x,-sunDirection.y,sunDirection.z); // flipped side y
            vec3 fullColor=mix(moonColor,sunColor,getDaytimeFactor(daytime));

            float diff=max(dot(norm, alterSunDirection), 0.0);
            vec3 diffuse=diff*sunColor*sunStrength;
            float diff2=max(dot(norm, -alterSunDirection), 0.0);
            vec3 diffuse2=diff2*moonColor*moonStrength;

            lightColor = ((diffuse+diffuse2+fullColor)/3.0);

            //vec3 lightColor = vec3(1.0,1.0,1.0);
           
            // Edge tint based on alpha and nightFactor (approximated from light)
            float edgeFactor = smoothstep(0.2, 0.8, 1.0 - (alpha * 1.0)); // Higher alpha = less edge

            vec3 colorNight=vec3(0.2,0.3,0.5);
            vec3 colorDay=vec3(0.6,0.3,0.1);
            vec3 colorTwilight=vec3(0.8,0.1,0.2);
            float moonCloudBrightness=(daytime*.25)+.25;
            float sunCloudBrightness=(daytime*.25)+1.25-(mixt*.5);
            //float cloudBrightness=mix(moonCloudBrightness,sunCloudBrightness,mixn);
            float cloudBrightness=mix(moonCloudBrightness,sunCloudBrightness,getDaytimeFactor(daytime) * 2.0);
            vec3 edgeTint = mix(
                colorNight, // 0
                mix(
                    colorTwilight, // 0.5
                    colorDay, // 1
                mixt),
            mixn) * 2.0;
            lightColor *= edgeTint * 5.0 * (1.0-(cDensity*.5)) * (1.0-min(abs(((daytime*2.0)-1.0)*1.0),0.5)) * (((mixn*.5)+.45));
            // * (((mixn*.5)+.25)) // tone down at night
            // * (1.00-min(abs(((daytime*2.0)-1.0)*1.0),0.5)) // tone down midday
            //lightColor *= edgeTint * 2.0 * (1.0-(cDensity*.5)); // 
            cloudColor *= (daytime*.5)+.25;
            //cloudColor *= max(abs(((daytime*2.0)-1.0)*1.0),0.5);
            //edgeTint = edgeTint * mix(diff,diff2,max(1.0-((daytime*.5)+.25),0.0));
            edgeTint = edgeTint * mix(diff2,diff,getDaytimeFactor(daytime)) * (1.0-(cDensity*.5));
            cloudColor *= mix(cloudColor, edgeTint, edgeFactor) + cloudBrightness; // Apply tint
            
            //cloudColor = colorNight;
            
            // used for subtracting clouds at horizon currently, no glow, and not in shadow
            alpha *= 1.0-min(glowAmount * horizonGlowIntensity,1.0);
            
            //vec3 black=vec3(0.0);
            //finalColor = mix(black,cloudColor.rgb * ((lightColor * .2)+.8),alpha);
            finalColor = mix(finalColor, cloudColor.rgb + lightColor, alpha);
            } // if vUv>0.5
        }
        } // clouds ready
		
		if(horizonGlowOn==1) {
			// Blend modes to try (additive is most glow-y / natural for atmosphere)
			finalColor += horizonGlow;                          // pure additive glow (recommended)
			//finalColor = mix(finalColor, horizonGlow, 0.4);   // softer tint blend (alternative)
			//finalColor = finalColor * (1.0 + horizonGlow);    // multiplicative boost (stronger)
		}

        //finalColor=mix(finalColor,finalClouds,cloudAlpha);

        gl_FragColor=vec4(finalColor,1.0);

        //gl_FragColor = vec4(vec3(clouds.transmittance), 1.0);
        //gl_FragColor = vec4(clouds.scattered, 1.0);


        // ------------- old below
        //gl_FragColor = vec4(mix(gradient+totalColor, sunColor2, (sunAura3 * .1) + (sunAura2 * .1) + (sunAura * .1) + (sunRim * 1.0) + (sunGlow * 1.0)), 1.0);
        
        // final with sun
        //gl_FragColor = vec4(mix(gradient, sunColor2, ((sunAura3 * .01) + (sunAura2 * .02) + (sunAura * .1) + (sunRim * 0.5) + (sunGlow * 0.5)) * daytime * 5.0) * 1.00, 1.0); // * 5.0 harder edges between glow and rim but overexposure color
        
        //vec3 color=mix(mix(gradient,moonColor2,0.5),sunColor2,0.5);
        // final with sun and moon
        //gl_FragColor = vec4(mix(mix(gradient, moonColor2, ((moonAura * .002) + (moonAura2 * .02) + (moonRim * 1.0) + (moonGlow * 1.0)) * max(2.0-daytime,0.5) * 2.0), sunColor2, ((sunAura3 * .01) + (sunAura2 * .02) + (sunAura * .1) + (sunRim * 1.0) + (sunGlow * 1.0)) * daytime * 5.0) * 1.02, 1.0); // * 5.0 harder edges between glow and rim but overexposure color
        //gl_FragColor = vec4(mix(gradient, moonColor2, moonGlow), 1.0); // moon test
        
        //gl_FragColor = vec4(daytime,daytime,daytime,1.0);
    }
`;

//. custom shadow for 2d slouds simple2 [default]
// replicate procedural clouds here for custom packing
// does not work currently~
const fragmentShaderShadow = `
    #include <packing>
    uniform sampler2D uAlphaMap;
    uniform float uAlphaThreshold;
    varying vec3 vWorldPosition;

    uniform float xtime;
    varying vec3 vPosition;
    varying vec3 vLocalPosition;
    varying vec3 vViewPosition;
    varying vec2 TexCoord;
    varying vec3 v_Normal;
    uniform vec3 sunDirection;
    //uniform vec3 moonDirection;
    uniform float sunSize;
    uniform float daytime;
    uniform vec3 lightDir;
    uniform vec3 lightDir2;
    varying vec2 vUv;
    uniform float cDensity;
    uniform int cloudsOn;
    
    const float PI = 3.1415926535897932384626433832795;

    // Noise functions
    float random(vec3 p) {
        return fract(sin(dot(p, vec3(12.9898, 78.233, 45.5432))) * 43758.5453123);
    }
    
    float noise(vec3 p) {
        vec3 i = floor(p);
        vec3 f = fract(p);
        vec3 u = f * f * (3.0 - 2.0 * f);
        return mix(mix(mix(random(i), random(i + vec3(1.0, 0.0, 0.0)), u.x),
            mix(random(i + vec3(0.0, 1.0, 0.0)), random(i + vec3(1.0, 1.0, 0.0)), u.x), u.y),
            mix(mix(random(i + vec3(0.0, 0.0, 1.0)), random(i + vec3(1.0, 0.0, 1.0)), u.x),
            mix(random(i + vec3(0.0, 1.0, 1.0)), random(i + vec3(1.0, 1.0, 1.0)), u.x), u.y), u.z);
    }
    
    float fbm(vec3 p) {
        float v = 0.0;
        float a = 0.5;
        vec3 shift = vec3(100.0);
        for (int i = 0; i < 2; ++i) { // default 6
            v += a * noise(p);
            p = p * 2.0 + shift;
            a *= 0.2;
        }
        return v;
    }

    void main() {
        //--vec4 texColor = texture2D(uAlphaMap, vUv);
        //--if (texColor.a < uAlphaThreshold) discard;

        if(cloudsOn==2) {

        float height = normalize(vWorldPosition).y - 0.15; // -.15 pulls up the gradient

        vec2 uv = vUv;
        vec3 pos = normalize(vLocalPosition);

        // Center the UVs
        vec2 centered_uv = uv - 0.5;

        float angle = PI / 2.0; // Or use a fixed value like PI / 2.0 for 90 deg
        // Calculate rotation factors
        float cos_angle = cos(angle);
        float sin_angle = sin(angle);

        // Apply rotation matrix (mat2)
        // [ cos(a)  -sin(a) ] [x]   [x*cos(a) - y*sin(a)]
        // [ sin(a)   cos(a) ] [y] = [x*sin(a) + y*cos(a)]
        vec2 rotated_uv = vec2(
            centered_uv.x * cos_angle - centered_uv.y * sin_angle,
            centered_uv.x * sin_angle + centered_uv.y * cos_angle
        );

        // Translate back to original space
        vec2 final_uv = rotated_uv + 0.5;


        vec3 cloudColor = vec3(1.0,1.0,1.0);
        vec3 finalColor = vec3(0.0,0.0,0.0);
        float alpha=0.0;
        //if(rotated_uv.x>=0.5) { // avoid calc for bottom half
        if(height>-0.15) { // avoid calc for bottom half

        //float time = abs((xtime / 10.0)-4320.0);
        float phasetime = xtime / 5.0;
        float phaselen = (86400.0 / 5.0) * .5; // realtime
        //float phaselen = (2000.0 / 5.0) * .5; // debug
        float time = abs(phasetime-phaselen);

        float scale = 5.0;
        vec3 flow1 = pos * scale + vec3(time * 0.10);
        vec3 flow2 = pos * scale - vec3(time * 0.06);
        vec3 flow3 = pos * scale + vec3(time * 0.03);
        float n1 = noise(flow1);
        float n2 = noise(flow2);
        float n3 = noise(flow3);
        float baseNoise = (n2 + n3 - n1) * cDensity; // Base detail
        float detailNoise = fbm(pos * scale * 4.2 + vec3(time * 0.01)); // Smudged variation
        float cloudDensity = smoothstep(0.3, .7, baseNoise + detailNoise * 0.3); // Increased coverage
        cloudColor = vec3(.5, .5, .5);
        alpha = cloudDensity;

        // Edge tint based on alpha and nightFactor (approximated from light)
        float edgeFactor = smoothstep(0.2, 0.8, 1.0 - alpha); // Higher alpha = less edge
        float nightFactor = daytime;
        nightFactor = smoothstep(0.2, 0.8, nightFactor);
        vec3 edgeTint = mix(
            vec3(0.2, 0.2, 0.2), // Grey
            mix(
                vec3(0.5, 0.6, 0.7),
                vec3(0.8, 0.5, 0.5),
            nightFactor * 5.0), // Blue to red
        0.5 - (nightFactor * .25)); // Darker at edges
        cloudColor = mix(cloudColor, edgeTint, edgeFactor) * 2.0; // Apply tint
        alpha = mix(alpha,0.0,edgeFactor) * 2.0;

        finalColor = cloudColor.rgb;
        }
    
        //uniform float alphaThreshold;
        //float alphaThreshold = 0.3;
        if (alpha < uAlphaThreshold) discard;
    
        //gl_FragColor = vec4(finalColor, alpha);
        //gl_FragColor = vec4(vec3(alpha,0.0,0.0), alpha); // debug


        // This line is essential for depth packing
        gl_FragColor = packDepthToRGBA(gl_FragCoord.z);
        } else {
        gl_FragColor = packDepthToRGBA(gl_FragCoord.z);
        }
    }
`;

const vertexShaderClouds = `
    uniform mat4 mvpMatrix;
    
    varying vec3 ourNormal;
    varying vec2 vUv;
    varying float ypos;
    
    void main()
    {
       ourNormal=normal;
       vUv = uv;
       gl_Position = mvpMatrix * vec4(position, 1.0);
       ypos=position.x*0.001f; // -1000 to 1000 = -1 to 1
    }
`;
const fragmentShaderClouds = `
    #define PI 3.14159265359
    uniform float xtime; // seconds~day
    //uniform vec3 data;  // data.x time, data.y freq, data.z amp
    uniform float daytime;  // -1-0-1
    
    uniform vec3 sunDirection;
    //uniform vec3 moonDirection;
    uniform float sunSize;
    
    varying vec3 ourNormal;
    varying vec2 vUv;     // UV coordinates from the vertex shader
    
    // Pseudo-random hash function based on 2D coordinates
    float hash(vec2 p) {
        return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
    }
    
    // 2D value noise with smooth interpolation
    float noise(vec2 p) {
        vec2 i = floor(p);          // Integer part
        vec2 f = fract(p);          // Fractional part
        float a = hash(i);
        float b = hash(i + vec2(1.0, 0.0));
        float c = hash(i + vec2(0.0, 1.0));
        float d = hash(i + vec2(1.0, 1.0));
        vec2 u = f * f * (3.0 - 2.0 * f); // Cubic interpolation
        return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
    }
    
    // Fractal Brownian motion for detailed cloud patterns
    float fbm(vec2 p) {
        float value = 0.0;
        //float amplitude = 0.5;
        float frequency = 0.222;
        float amplitude = 0.222;
        //float frequency = data.y;
        for (int i = 0; i < 4; i++) { // 4 octaves for detail
            value += amplitude * noise(p * frequency);
            frequency *= 2.0;         // Double frequency each octave
            amplitude *= 0.5;         // Halve amplitude each octave
        }
        return value;
    }
    
    // clouds by grok - butthole above
    void main() {
        //float time = xtime;
        float rad=PI/180.0;
        //float time=(xtime * 1.0 / 86400) * 25000; // 50 breaks
        float phase = xtime;
    
        vec2 uv = vUv;
   
        vec3 finalColor=vec3(0.0,0.0,0.0);
        float alpha=0.0;
    
        if(vUv.y>=0.5) { // avoid calc for bottom half

        float twinkleSpeed = 86400.0 / 3.0;
       //-- float twinkle = sin(uTime * twinkleSpeed);
    
        //float time = sin(phase * twinkleSpeed);
    
        float time = phase / (2.0 * 3.14159) * twinkleSpeed;
        ///vec2 flame_uv = uv + vec2(0.0, offset_y);
    
        float speed = 0.1;        // Speed of cloud movement
        float cloudScale = 100.0;   // Scale of cloud patterns
        // Circular offset for looping animation
        //vec2 offset = vec2(sin(time * speed), cos(time * speed)) * 0.5;
        vec2 offset = vec2(sin(time), cos(time)) * 0.5;
    
        
        // Scale UV coordinates and apply offset
        vec2 scaledUV = uv * cloudScale;
        scaledUV.x*=2.0; //
        float cloudValue = fbm(scaledUV + offset);
        
        // Map noise to alpha for cloud density with soft edges
        alpha = smoothstep(0.0, 0.7, cloudValue);
        //float alpha=0.5;
        // Purplish cloud color (stylized)
        //vec3 cloudColor = vec3(0.6, 0.6, 0.66) * (1.0 - daytime);
    
        // color blendings
        float methodA=1.0-daytime;
        float methodD=(methodA*2.0);
        float methodE=(methodA*2.0)-1.0;
    
        // day/night cloud colors  blend
        vec3 color1 = vec3(0.5,0.3,0.5); // day end
        vec3 color2 = vec3(0.6,0.6,0.7);
        vec3 color3 = vec3(0.7,0.6,0.7); // <<day begin
        //
        vec3 color4 = vec3(0.63,0.43,0.43); // night end
        vec3 color5 = vec3(0.43,0.36,0.43);
        vec3 color6 = vec3(0.15,0.16,0.43); // night begin
    
        vec3 ourColor = vec3(mix(color3,color2,methodD));
        ourColor = mix(ourColor,color1,methodE);
        vec3 ourColor2 = vec3(mix(color5,color4,methodD));
        ourColor2 = mix(ourColor2,color6,methodE);
        vec3 cloudColor = vec3(mix(ourColor,ourColor2,daytime));
    
       vec3 norm=normalize(ourNormal);
       vec3 sunColor = vec3(1.0f, 0.93f, 0.74f); // sky sphere 
       vec3 moonColor = vec3(0.7f, 1.0f, 0.8f); // lumination
       float sunStrength=10.0f;
       float moonStrength=3.0f;
       float diff=max(dot(norm, sunDirection), 0.0);
       vec3 diffuse=diff*sunColor*sunStrength;
       float diff2=max(dot(norm, -sunDirection), 0.0);
       vec3 diffuse2=diff2*moonColor*moonStrength;

       finalColor = (cloudColor + ((diffuse + diffuse2) * 0.1)) * cloudColor;

       }

        // Output color with transparency
        //gl_FragColor = vec4(cloudColor, alpha);
        gl_FragColor = vec4(finalColor, alpha);
        //gl_FragColor = vec4(1.0,0.0,0.0,1.0);
    }
`;
// clouds2 moved to skymaterial not using clouds geometry
const vertexShaderClouds2 = `
    uniform mat4 mvpMatrix;
    varying vec3 vPosition;
    varying vec3 vLocalPosition;
    uniform mat3 nMatrix; // CPU-computed normal matrix
    uniform mat4 vMatrix; // View matrix (for view-space lighting)
    varying vec3 vViewPosition; // View-space position
    varying vec2 TexCoord;
    varying vec3 v_Normal;
    varying vec3 ourNormal;
    varying vec2 vUv;
    //varying float ypos;
    
    void main() {
        //ourNormal=vNormal;
        ourNormal=normal;
        vUv = uv;
        vLocalPosition = position;
        vPosition = (modelMatrix * vec4(position, 1.0)).xyz;
        vViewPosition = (modelViewMatrix * vec4(position, 1.0)).xyz; // View-space position
        TexCoord = vec2(vPosition.x, vPosition.z);

        vec3 worldNormal = normalize(nMatrix * normal);
        v_Normal = normalize((vMatrix * vec4(worldNormal, 0.0)).xyz);
        
        //ypos=position.y*0.0005f; // -1000 to 1000 = -1 to 1

        gl_Position = mvpMatrix * vec4(position, 1.0);
    }
`;
const fragmentShaderClouds2 = `
    uniform float xtime;
    //uniform mat4 invView; // Inverse view matrix
    //uniform mat4 invProjection; // Inverse view matrix
    varying vec3 vPosition;
    varying vec3 vLocalPosition;
    varying vec3 vViewPosition;
    varying vec2 TexCoord;
    varying vec3 v_Normal;
    //varying vec3 v_vertToLight;
    //varying vec4 viewSunPos;
    uniform vec3 sunDirection;
    //uniform vec3 moonDirection;
    uniform float sunSize;
    uniform float daytime;
    uniform vec3 lightDir;
    uniform vec3 lightDir2;
    varying vec3 ourNormal;
    varying vec2 vUv;
    uniform float cDensity;
    //varying float ypos;
    
    const float PI = 3.1415926535897932384626433832795;

    // Noise functions
    float random(vec3 p) {
        return fract(sin(dot(p, vec3(12.9898, 78.233, 45.5432))) * 43758.5453123);
    }
    
    float noise(vec3 p) {
        vec3 i = floor(p);
        vec3 f = fract(p);
        vec3 u = f * f * (3.0 - 2.0 * f);
        return mix(mix(mix(random(i), random(i + vec3(1.0, 0.0, 0.0)), u.x),
            mix(random(i + vec3(0.0, 1.0, 0.0)), random(i + vec3(1.0, 1.0, 0.0)), u.x), u.y),
            mix(mix(random(i + vec3(0.0, 0.0, 1.0)), random(i + vec3(1.0, 0.0, 1.0)), u.x),
            mix(random(i + vec3(0.0, 1.0, 1.0)), random(i + vec3(1.0, 1.0, 1.0)), u.x), u.y), u.z);
    }
    
    float fbm(vec3 p) {
        float v = 0.0;
        float a = 0.5;
        vec3 shift = vec3(100.0);
        for (int i = 0; i < 2; ++i) { // default 6
            v += a * noise(p);
            p = p * 2.0 + shift;
            a *= 0.2;
        }
        return v;
    }

    //float linearizeDepth(float depth) {
    //    float near = 0.1;
    //    float far = 500.0; // Adjust to match your camera
    //    float z = depth * 2.0 - 1.0; // NDC
    //    return 2.0 * near * far / (far + near - z * (far - near));
    //}
    //
    //vec3 getWorldPosition(float depth, vec2 uv) {
    //    float z = linearizeDepth(depth);
    //    vec4 clipSpace = vec4(uv * 2.0 - 1.0, z, 1.0);
    //    vec4 viewSpace = invProjection * clipSpace;
    //    viewSpace /= viewSpace.w;
    //    vec4 worldSpace = invView * viewSpace;
    //    return worldSpace.xyz;
    //}
    
    void main() {
        // Noise for cloud pattern
        vec3 pos = normalize(vLocalPosition);

        vec3 cloudColor = vec3(1.0,1.0,1.0);
        vec3 finalColor = vec3(0.0,0.0,0.0);
        float alpha=0.0;
        if(vUv.y>=0.5) { // avoid calc for bottom half

        //float time = abs((xtime / 43200.0)-1.0);
        //float time = abs((xtime / 10.0)-4320.0);
        float phasetime = xtime / 5.0; // match in shadow shader if using
        float phaselen = (86400.0 / 5.0) * .5; // realtime
        //float phaselen = (2000.0 / 5.0) * .5; // debug
        float time = abs(phasetime-phaselen);

       //float loopedRealtime = mod(time, 365.0 * 24.0 * 3600.0); // 365 days
       //float globalAngle = (loopedRealtime / (365.0 * 24.0 * 3600.0)) * 2.0 * PI;
       //float localAngle = globalAngle; // Optional latitude factor if desired

       //-- float cosR = cos(localAngle);
       //-- float sinR = sin(localAngle);
       //-- vec3 pos2 = vec3(
       //--     cosR * pos.x + sinR * pos.z,
       //--     pos.y,
       //--     -sinR * pos.x + cosR * pos.z
       //-- );


        float scale = 5.0;
        vec3 flow1 = pos * scale + vec3(time * 0.10);
        vec3 flow2 = pos * scale - vec3(time * 0.06);
        vec3 flow3 = pos * scale + vec3(time * 0.03);
        float n1 = noise(flow1);
        float n2 = noise(flow2);
        float n3 = noise(flow3);
        float baseNoise = (n2 + n3 - n1) * cDensity; // Base detail
        float detailNoise = fbm(pos * scale * 4.2 + vec3(time * 0.01)); // Smudged variation
        float cloudDensity = smoothstep(0.3, .7, baseNoise + detailNoise * 0.2); // Increased coverage
        cloudColor = vec3(.5, .5, .5);
        alpha = cloudDensity;
    
// taken from https://twily.info/plainC/terrain/data/shaders/frag_shader_clouds.glsl#view
   vec3 norm=normalize(ourNormal);
   vec3 sunColor = vec3(1.0, 0.93, 0.74); 
   vec3 moonColor = vec3(0.7, 0.5, 0.9);
   float sunStrength=1.0;
   float moonStrength=0.2;
   float diff=max(dot(norm, sunDirection), 0.0);
   vec3 diffuse=diff*sunColor*sunStrength;
   float diff2=max(dot(norm, -sunDirection), 0.0);
   vec3 diffuse2=diff2*moonColor*moonStrength;

   vec3 lightColor = mix(diffuse,diffuse2,1.0-daytime);


        // Edge tint based on alpha and nightFactor (approximated from light)
        float edgeFactor = smoothstep(0.2, 0.8, 1.0 - (alpha * 1.5)); // Higher alpha = less edge

        vec3 colorNight=vec3(0.2,0.3,0.5);
        vec3 colorDay=vec3(0.6,0.3,0.1);
        vec3 colorTwilight=vec3(0.8,0.1,0.2);
        float mixn=min((daytime*2.0),1.0); // 0 - 0.5
        float mixt=max((daytime*2.0)-1.0,0.0); // 0.5 - 1
        float cloudBrightness=max((max(daytime,0.25)*2.0),0.0); // *half+half = ambient
        vec3 edgeTint = mix(
            colorNight, // 0
            mix(
                colorTwilight, // 0.5
                colorDay, // 1
            mixt),
        mixn);
        edgeTint = edgeTint * mix(diff,diff2,1.0-daytime);
        cloudColor = cloudColor * ((mix(diff,diff2,1.0-daytime)*.2)+.8);
        cloudColor = mix(cloudColor, edgeTint, edgeFactor) * cloudBrightness * 2.0; // Apply tint
        

        //cloudColor = colorNight;

        //vec3 P = getWorldPosition(vPosition.z, TexCoord); 
        
        //finalColor = cloudColor.rgb * ((lightColor * .8)+.8);
        //vec3 black=vec3(0.0);
        //finalColor = mix(black,cloudColor.rgb * ((lightColor * .2)+.8),alpha);
        finalColor = mix(finalColor, cloudColor.rgb * ((lightColor * .2)+.8), alpha);
        }
    
        //vec3 testColor = vec3(0,.5,0);
        // Apply lighting to color
        //testColor *= lightIntensity;

        //uniform float alphaThreshold;
        float alphaThreshold = 0.3;
        if (alpha < alphaThreshold) discard;
    
        gl_FragColor = vec4(finalColor, alpha);
        //gl_FragColor = vec4(vec3(cloudColor), alpha);
        //gl_FragColor = vec4(testColor, 1);
        //gl_FragColor = vec4(P, alpha);
    }
`;

const vertexShaderTerrain = `
    varying vec2 vUv;
    varying float vHeight;
    varying vec3 vNormal;
    varying vec3 vWorldPos;
    uniform float repeatScale;
    //varying float vFogDepth;
    varying vec3 vWorldPosition;

    // Manual shadow coord
    varying vec4 vSunShadowCoord;
    uniform float shadowNormalBias;
    
    uniform mat4 sunShadowMatrix;  // from uniform
    uniform int shadowOn;

    ${THREE.ShaderChunk['common']}
    ${THREE.ShaderChunk['fog_pars_vertex']}

    void main() {
        vUv = uv * repeatScale; // Scale UVs in vertex for repeating
        vHeight = position.y / ${heightScale}.0; // Normalized height for splatting
        vNormal = normal;
        vec4 worldPos = modelMatrix * vec4(position, 1.0);
        vWorldPos = worldPos.xyz;
        vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;

        vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
        //vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
        vFogDepth = -mvPosition.z;

        //gl_Position = projectionMatrix * mvPosition;

        // Offset for sun
        vec3 offset = normal * shadowNormalBias; // object-space offset (assumes uniform scale; if not, use worldNormal below)
        // For non-uniform scale: vec3 offset = vWorldNormal * shadowNormalBias;
        vec4 offsetWorldPos = modelMatrix * vec4(position + offset, 1.0);
        
        if(shadowOn >= 1) {
          vSunShadowCoord = sunShadowMatrix * offsetWorldPos;
          // Repeat for moon with another offsetWorldPos if separate bias, but same for now
        }

        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);

        ${THREE.ShaderChunk['fog_vertex']}
    }
`;
const fragmentShaderTerrain = `
    precision highp float;

    ${THREE.ShaderChunk['common']}
    ${THREE.ShaderChunk['packing']}
    ${THREE.ShaderChunk['fog_pars_fragment']}

    uniform sampler2D grassTex;
    uniform sampler2D rockTex;
    uniform sampler2D snowTex;
    uniform sampler2D normalGrass;
    uniform sampler2D normalRock;
    uniform sampler2D normalSnow;
    uniform sampler2D roughGrass;
    uniform sampler2D roughRock;
    uniform sampler2D roughSnow;
    uniform float repeatScale;
    uniform vec3 lightDir;
    uniform vec3 lightDir2;
    uniform float daytime; // 1 0 1

    // Manual shadow uniforms
    uniform float shadowBias;
    uniform float shadowRadius;
    uniform sampler2D sunShadowMap;
    uniform mat4 sunShadowMatrix;
    varying vec4 vSunShadowCoord;
    uniform int shadowOn;

    varying vec2 vUv;
    varying float vHeight;
    varying vec3 vNormal;
    varying vec3 vWorldPos;
    varying vec3 vWorldPosition;

    void main() {
        //vec2 uv=vUv * repeatScale;
        vec2 uv = vUv;

        // Splat based on height (grass low, rock mid, snow high)
        vec3 albedo = mix(texture2D(grassTex, uv).rgb, texture2D(rockTex, uv).rgb, smoothstep(0.2, 0.5, vHeight));
        albedo = mix(albedo, texture2D(snowTex, uv).rgb, smoothstep(0.6, 0.8, vHeight));

        // Splat normals
        vec3 norm = mix(texture2D(normalGrass, uv).rgb, texture2D(normalRock, uv).rgb, smoothstep(0.2, 0.5, vHeight));
        norm = mix(norm, texture2D(normalSnow, uv).rgb, smoothstep(0.6, 0.8, vHeight));
        norm = norm * 2.0 - 1.0; // Unpack

        // Hardcoded ORM: AO=1.0 (no occlusion), rough=0.5, metal=0.0
        float ao = 1.0;
        //float rough = 0.5;
        float rough=mix(texture2D(roughGrass, uv).r, texture2D(roughRock, uv).r, smoothstep(0.2, 0.5, vHeight));
        rough = mix(rough, texture2D(roughSnow, uv).r, smoothstep(0.6, 0.8, vHeight));
        float metal = 0.0;

        // Simple PBR lighting (diffuse + specular approximation)
        vec3 viewDir = normalize(cameraPosition - vWorldPosition);
        vec3 finalNormal = normalize(vNormal + norm * 0.5); // Reduced normal strength if too bumpy
        // View-space normal + tangent-space bump
        

        vec3 lightDir2Mod=vec3(-lightDir2.x,-lightDir2.y,-lightDir2.z);
        float diff = max(dot(lightDir, finalNormal), 0.0) * ao * (((daytime*.5)+.5));
        vec3 halfway = normalize(lightDir + viewDir);
        float spec = pow(max(dot(finalNormal, halfway), 0.0), 32.0 * (rough)) * (0.04 + metal) * (((daytime*.5)+.5));
        float diffMoon = max(dot(lightDir2Mod, finalNormal), 0.0) * ao * max((1.0-((daytime*2.0)-1.0)),0.0) * .4;
        vec3 halfwayMoon = normalize(lightDir2Mod + viewDir);
        float specMoon = pow(max(dot(finalNormal, halfwayMoon), 0.0), 32.0 * (rough)) * (0.04 + metal) * (1.0-((daytime*.5)+.5));
        vec3 color = albedo * max(diff + diffMoon,.3) + vec3(spec + specMoon); // Increased ambient contribution for brightness


        float shadow = 1.0; // no shadow
        if(shadowOn>=1) {
        // Manual shadow sampling for sun
        shadow=abs((daytime*2.0)-1.0); // 1 0 1 0 = 0.5 0 0.5 1 ~;
        vec4 shadowCoord = vSunShadowCoord / vSunShadowCoord.w;
        
        shadowCoord = shadowCoord * 0.5 + 0.5; // NDC to [0,1]

        if (shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 &&
            shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0 &&
            shadowCoord.z >= 0.0 && shadowCoord.z <= 1.0) {
        // Circle check: distance from center (0.5,0.5) <= radius 0.5 (fits [0,1])
        //vec2 centerDist = shadowCoord.xy - vec2(0.5, 0.5); // avoid circular in orthographic?
        //if (length(centerDist) <= 0.5 &&
        //    shadowCoord.z >= 0.0 &&
        //    shadowCoord.z <= 1.0) {
        float shadowDepth=0.0;
        float bias=0.0;
        shadowDepth = unpackRGBAToDepth(texture(sunShadowMap, shadowCoord.xy));
        //bias = shadowBias / max(0.01, dot(finalNormal, lightDir)); // dynamic bias
        //bias = shadowBias * max(0.05, dot(finalNormal, lightDir)); // dynamic bias
        float ndotl=max(dot(finalNormal, lightDir), 0.01);
        float slopeFactor = sqrt(1.0 - ndotl * ndotl) / ndotl; // tan(acos(ndotl))
        bias = shadowBias + 0.0001 * slopeFactor; // tune 0.0001 as slope bias; small to avoid leaks
        bias = clamp(bias, shadowBias, 0.001); // cap to prevent excessive
        //float bias = shadowBias;
        shadow = shadowCoord.z > shadowDepth + bias ? 0.0 : 1.0;

if(shadowOn<=2) {

        vec2 texelSize = 1.0 / vec2(4096.0, 4096.0); // match mapSize

        // Add to fragmentShader uniforms or defines
const int numSamples = 16;
vec2 poissonDisk[16] = vec2[](
    vec2(-0.94201624, -0.39906216), vec2(0.94558609, -0.76890725),
    vec2(-0.094184101, -0.92938870), vec2(0.34495938, 0.29387760),
    vec2(-0.91588581, 0.45771432), vec2(-0.81544232, -0.87912464),
    vec2(-0.38277543, 0.27676845), vec2(0.97484398, 0.75648379),
    vec2(0.44323325, -0.97511554), vec2(0.53742981, -0.47373420),
    vec2(-0.26496911, -0.41893023), vec2(0.79197514, 0.19090188),
    vec2(-0.24188840, 0.99706507), vec2(-0.81409955, 0.91437590),
    vec2(0.19984103, 0.78641367), vec2(0.14383161, -0.14100790)
);

// In the loop:
shadow = 0.0;  // Reset to accumulate lit
for (int i = 0; i < numSamples; i++) {
    vec2 offset = poissonDisk[i] * shadowRadius * texelSize;
    // Optional: Rotate disk for variety (reduces repetition)
    float angle = fract(sin(dot(shadowCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) * 6.2832;
    offset = vec2(cos(angle) * offset.x - sin(angle) * offset.y, sin(angle) * offset.x + cos(angle) * offset.y);
    float d = unpackRGBAToDepth(texture(sunShadowMap, shadowCoord.xy + offset));
    shadow += (shadowCoord.z > d + bias) ? 0.0 : 1.0;
}
shadow /= float(numSamples);
} // dithering shadow
        }
        // Real shadow receive
        //float shadow = getShadow();               // from shadowmask chunk
        if(daytime>0.5) {
            color *= (shadow * 0.6 + 0.6);           // shadow factor + ambient day
        } else {
            color *= (shadow * 0.3 + 0.9);           // shadow factor + ambient night
        }
        } // shadowOn>=1


        // Fog application (using your vFogDepth)
        float fogFactor = 1.0 - exp(-fogDensity * fogDensity * vFogDepth * vFogDepth);
        color *= max(fogColor,.2) * 2.0;
        color = mix(color, fogColor, fogFactor);


        //#include <fog_fragment>

        if(shadowOn>=2) {
            gl_FragColor = vec4(vec3(shadow), 1.0);
        } else {
            gl_FragColor = vec4(color, 1.0);
        }
        //gl_FragColor = vec4(rough, rough, rough, 1.0);
    }
`;

const vertexShaderMountain = `
    varying vec2 vUv;
    varying vec3 vWorldNormal;      // world-space normal (transformed)
    varying vec3 vWorldPos;         // world-space position
    //varying float vFogDepth;

    // Manual shadow coord
    varying vec4 vSunShadowCoord;
    uniform float shadowNormalBias;
    
    uniform mat4 sunShadowMatrix;  // from uniform
    uniform int shadowOn;

    ${THREE.ShaderChunk['common']}
    ${THREE.ShaderChunk['fog_pars_vertex']}

    void main() {
        vUv = uv;
        //vNormal = normal;
        
        // World-space normal (correct for random rotation)
        mat3 normalMat3 = mat3(transpose(inverse(modelMatrix))); // proper normal transform
        vWorldNormal = normalize(normalMat3 * normal);

        vec4 worldPos = modelMatrix * vec4(position, 1.0);
        vWorldPos = worldPos.xyz;

        vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);

        vFogDepth = -mvPosition.z;

        // Offset for sun
        vec3 offset = normal * shadowNormalBias; // object-space offset (assumes uniform scale; if not, use worldNormal below)
        // For non-uniform scale: vec3 offset = vWorldNormal * shadowNormalBias;
        vec4 offsetWorldPos = modelMatrix * vec4(position + offset, 1.0);
        
        if(shadowOn >= 1) {
          vSunShadowCoord = sunShadowMatrix * offsetWorldPos;
          // Repeat for moon with another offsetWorldPos if separate bias, but same for now
        }


        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        
        ${THREE.ShaderChunk['fog_vertex']}
    }
`;

const fragmentShaderMountain = `
    precision highp float;

    ${THREE.ShaderChunk['common']}
    ${THREE.ShaderChunk['packing']}
    ${THREE.ShaderChunk['fog_pars_fragment']}


    uniform sampler2D map;
    uniform sampler2D normalMap;
    uniform sampler2D roughnessMap;
    uniform vec3 lightDir;
    uniform vec3 lightDir2;
    uniform float daytime; // 1 0 1

    // Manual shadow uniforms
    uniform float shadowBias;
    uniform float shadowRadius;
    uniform sampler2D sunShadowMap;
    uniform mat4 sunShadowMatrix;
    varying vec4 vSunShadowCoord;
    uniform int shadowOn;

    varying vec2 vUv;
    varying vec3 vWorldNormal;
    varying vec3 vWorldPos;
    //varying float vFogDepth;

    void main() {
        vec4 albedo = texture(map, vUv);
    
        float rough = texture2D(roughnessMap, vUv).r;

        // Object-space normal map perturbation
        vec3 normalTex = texture(normalMap, vUv).rgb * 2.0 - 1.0;

        // Add bump to world-space normal
        vec3 finalNormal = normalize(vWorldNormal + normalTex * 0.8); // strength 0.8, tune as needed

        vec3 viewDir = normalize(cameraPosition - vWorldPos);

        float ao=1.0;
        float metal=0.0;
        
        vec3 lightDir2Mod=vec3(-lightDir2.x,-lightDir2.y,-lightDir2.z);
        float diff = max(dot(finalNormal, lightDir), 0.0) * ao * (((daytime*.5)+.5));
        vec3 halfway = normalize(lightDir + viewDir);
        float spec = pow(max(dot(finalNormal, halfway), 0.0), 32.0 * (rough)) * (0.04 + metal) * (((daytime*.5)+.5));
        float diffMoon = max(dot(lightDir2Mod, finalNormal), 0.0) * ao * max((1.0-((daytime*2.0)-1.0)),0.0) * .6;
        // -1 0 1 x 2 = -2 0 2
        // -2 0 2 - 1 = -3 0 1
        // 1 - -3 0 1 = 
        vec3 halfwayMoon = normalize(lightDir2Mod + viewDir);
        float specMoon = pow(max(dot(finalNormal, halfwayMoon), 0.0), 32.0 * (rough)) * (0.04 + metal) * (1.0-((daytime*.5)+.5));
        vec3 color = albedo.rgb * max(diff + diffMoon,.3) * 3.0 + vec3(spec + specMoon); // Increased ambient contribution for brightness

        float shadow = 1.0; // no shadow
        if(shadowOn>=1) {
        // Manual shadow sampling for sun
        shadow=abs((daytime*2.0)-1.0); // 1 0 1 0 = 0.5 0 0.5 1 ~;

        vec4 shadowCoord = vSunShadowCoord / vSunShadowCoord.w;
        
        shadowCoord = shadowCoord * 0.5 + 0.5; // NDC to [0,1]

        if (shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 &&
            shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0 &&
            shadowCoord.z >= 0.0 && shadowCoord.z <= 1.0) {
        // Circle check: distance from center (0.5,0.5) <= radius 0.5 (fits [0,1])
        //vec2 centerDist = shadowCoord.xy - vec2(0.5, 0.5); // avoid circular in orthographic?
        //if (length(centerDist) <= 0.5 &&
        //    shadowCoord.z >= 0.0 &&
        //    shadowCoord.z <= 1.0) {
        float shadowDepth=0.0;
        float bias=0.0;
        shadowDepth = unpackRGBAToDepth(texture(sunShadowMap, shadowCoord.xy));
        //bias = shadowBias / max(0.01, dot(finalNormal, lightDir)); // dynamic bias
        //bias = shadowBias * max(0.05, dot(finalNormal, lightDir)); // dynamic bias
        float ndotl=max(dot(finalNormal, lightDir), 0.01); 
        float slopeFactor = sqrt(1.0 - ndotl * ndotl) / ndotl; // tan(acos(ndotl))
        bias = shadowBias + 0.0001 * slopeFactor; // tune 0.0001 as slope bias; small to avoid leaks
        bias = clamp(bias, shadowBias, 0.001); // cap to prevent excessive
        //float bias = shadowBias;
        shadow = shadowCoord.z > shadowDepth + bias ? 0.0 : 1.0;

if(shadowOn<=2) {
        vec2 texelSize = 1.0 / vec2(4096.0, 4096.0); // match mapSize

        // Add to fragmentShader uniforms or defines
const int numSamples = 16;
vec2 poissonDisk[16] = vec2[](
    vec2(-0.94201624, -0.39906216), vec2(0.94558609, -0.76890725),
    vec2(-0.094184101, -0.92938870), vec2(0.34495938, 0.29387760),
    vec2(-0.91588581, 0.45771432), vec2(-0.81544232, -0.87912464),
    vec2(-0.38277543, 0.27676845), vec2(0.97484398, 0.75648379),
    vec2(0.44323325, -0.97511554), vec2(0.53742981, -0.47373420),
    vec2(-0.26496911, -0.41893023), vec2(0.79197514, 0.19090188),
    vec2(-0.24188840, 0.99706507), vec2(-0.81409955, 0.91437590),
    vec2(0.19984103, 0.78641367), vec2(0.14383161, -0.14100790)
);

// In the loop:
shadow = 0.0;  // Reset to accumulate lit
for (int i = 0; i < numSamples; i++) {
    vec2 offset = poissonDisk[i] * shadowRadius * texelSize;
    // Optional: Rotate disk for variety (reduces repetition)
    float angle = fract(sin(dot(shadowCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) * 6.2832;
    offset = vec2(cos(angle) * offset.x - sin(angle) * offset.y, sin(angle) * offset.x + cos(angle) * offset.y);
    float d = unpackRGBAToDepth(texture(sunShadowMap, shadowCoord.xy + offset));
    shadow += (shadowCoord.z > d + bias) ? 0.0 : 1.0;
}
shadow /= float(numSamples);
} // dithering shadows
        }
        // Real shadow receive
        //float shadow = getShadow();               // from shadowmask chunk

        if(daytime>0.5) {
            color *= (shadow * 0.6 + 0.6);           // shadow factor + ambient day
        } else {
            color *= (shadow * 0.3 + 0.9);           // shadow factor + ambient night
        }
        } // shadowOn>=1


        // Fog application (using your vFogDepth)
        float fogFactor = 1.0 - exp(-fogDensity * fogDensity * vFogDepth * vFogDepth);
        color *= max(fogColor,.2) * 2.0;
        color = mix(color, fogColor, fogFactor);


        //#include <fog_fragment>

        if(shadowOn>=2) {
            gl_FragColor = vec4(vec3(shadow), albedo.a);
        } else {
            gl_FragColor = vec4(color, albedo.a);
        }
        //gl_FragColor = vec4(rough, rough, rough, 1.0);
        //gl_FragColor = vec4(vViewNormal, 1.0);
    }
`;


const vertexShaderFirework = `
    varying vec2 vUv;

    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
`;
const fragmentShaderFirework = `
    uniform sampler2D diffuseMapFirework;
    uniform vec2 u_offset;
    uniform vec2 u_repeat;
    uniform float u_opacity;
    varying vec2 vUv;
    
    void main() {
      vec2 uv = vUv * u_repeat + u_offset;
      vec4 diffuseColor = texture2D(diffuseMapFirework, uv);
      vec3 color = diffuseColor.rgb * 2.0;
      float alpha = diffuseColor.a * u_opacity;
      if (alpha < 0.3) discard;
      gl_FragColor = vec4(color, alpha);
    }
`;
const fragmentShaderFireworkDEBUG = `
void main() {
  gl_FragColor = vec4(1.0, 0.0, 1.0, 0.8);   // solid bright magenta
}
`;
        
        // Classic Perlin Noise implementation (3D for terrain variation)
        class ClassicalNoise {
            constructor(r = Math) {
                this.grad3 = [[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],
                              [1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],
                              [0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]];
                this.p = [];
                for (let i = 0; i < 256; i++) {
                    this.p[i] = Math.floor(r.random() * 256);
                }
                this.perm = [];
                for (let i = 0; i < 512; i++) {
                    this.perm[i] = this.p[i & 255];
                }
            }

            dot(g, x, y, z) {
                return g[0] * x + g[1] * y + g[2] * z;
            }

            mix(a, b, t) {
                return (1.0 - t) * a + t * b;
            }

            fade(t) {
                return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
            }

            noise(x, y, z) {
                let X = Math.floor(x), Y = Math.floor(y), Z = Math.floor(z);
                x -= X; y -= Y; z -= Z;
                X &= 255; Y &= 255; Z &= 255;
                let gi000 = this.perm[X + this.perm[Y + this.perm[Z]]] % 12;
                let gi001 = this.perm[X + this.perm[Y + this.perm[Z + 1]]] % 12;
                let gi010 = this.perm[X + this.perm[Y + 1 + this.perm[Z]]] % 12;
                let gi011 = this.perm[X + this.perm[Y + 1 + this.perm[Z + 1]]] % 12;
                let gi100 = this.perm[X + 1 + this.perm[Y + this.perm[Z]]] % 12;
                let gi101 = this.perm[X + 1 + this.perm[Y + this.perm[Z + 1]]] % 12;
                let gi110 = this.perm[X + 1 + this.perm[Y + 1 + this.perm[Z]]] % 12;
                let gi111 = this.perm[X + 1 + this.perm[Y + 1 + this.perm[Z + 1]]] % 12;
                let n000 = this.dot(this.grad3[gi000], x, y, z);
                let n100 = this.dot(this.grad3[gi100], x - 1, y, z);
                let n010 = this.dot(this.grad3[gi010], x, y - 1, z);
                let n110 = this.dot(this.grad3[gi110], x - 1, y - 1, z);
                let n001 = this.dot(this.grad3[gi001], x, y, z - 1);
                let n101 = this.dot(this.grad3[gi101], x - 1, y, z - 1);
                let n011 = this.dot(this.grad3[gi011], x, y - 1, z - 1);
                let n111 = this.dot(this.grad3[gi111], x - 1, y - 1, z - 1);
                let u = this.fade(x), v = this.fade(y), w = this.fade(z);
                let nx00 = this.mix(n000, n100, u);
                let nx01 = this.mix(n001, n101, u);
                let nx10 = this.mix(n010, n110, u);
                let nx11 = this.mix(n011, n111, u);
                let nxy0 = this.mix(nx00, nx10, v);
                let nxy1 = this.mix(nx01, nx11, v);
                return this.mix(nxy0, nxy1, w);
            }
        }
        
        // linear(k): Returns k unchanged (constant speed)
        function linear(k) {
          return k;
        }
        
        // easeIn(k): Starts slow and speeds up (e.g., using a power function).
        function easeIn(k) {
          return Math.pow(k, 2); // Quadratic ease-in
        }
        
        // easeOut(k): Starts fast and slows down (a common natural effect, similar to the continuous lerp approach).
        function easeOut(k) {
          return 1 - Math.pow(1 - k, 2); // Quadratic ease-out
        }
        
        // easeInOut(k): Starts slow, speeds up in the middle, and slows down at the end.
        function easeInOut(k) {
          return k < 0.5 ? 2 * Math.pow(k, 2) : 1 - Math.pow(1 - k, 2) * 2; // Quadratic ease-in-out
          // A common alternative using Math.sin for a smoother curve:
          // return 0.5 * (Math.sin((k - 0.5) * Math.PI) + 1);
        }

        // standard lerp t=0-1
        function lerp(start,end,t) {
            return start * (1 - t) + end * t;
        }

        // To implement a smoothstep version of your custom lerp in C# for 2026, you can use the standard cubic Hermite interpolation formula. This function creates an "S-curve" that starts and ends slowly, making transitions feel more natural
        function SmoothStep(start,end,t) {
            // Clamp t between 0 and 1 to prevent overshoot
            if(t>1.0) t=1.0;
            else if(t<0.0) t=0.0;

            // The smoothstep cubic formula: 3t^2 - 2t^3
            t = t * t * (3.0 - 2.0 * t);

            // Standard interpolation using the smoothed 't'
            //return start * (1f - t) + end * t;
            return lerp(start,end,t);
        }
        
        const radian=Math.PI / 180.0;
        
        const $=function(id) { return document.getElementById(id); }
        const rndMinMax=function(min,max) { return Math.floor(Math.random()*(max-min+1)+min); }
        
        const perlin = new ClassicalNoise();

        // Scene setup
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
        camera.position.set(-21.15, 60.31, -195.56); // Flying height
        //camera.position.set(-46.15, 1908.31, -2053.56); // far view debug
        //camera.lookAt(0, 0, -100); // Look forward along -z
        //camera.rotation.set(-6.0*radian, 180.0*radian, 0.0); // (if not using OrbitControls)

        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        
        //renderer.sortObjects = false;

        const controls = new OrbitControls(camera, renderer.domElement);
        controls.target.set(.0+camera.position.x, .5+camera.position.y, 5.+camera.position.z);
        // alternative -- see bottom controls.lock~ cursor lock
        //const controls = new PointerLockControls( camera, renderer.domElement );

        camera.updateProjectionMatrix();

        function getDirection(theta, phi) { // dir = return
            let theta_left = theta + 90.0;
            let phi_up = phi - 90.0;
            if(theta_left>180.0) theta_left -=360.0;
            if(phi_up<-180.0) phi_up += 360.0;
           
            //const x=Math.sin(90.0 * radian) * Math.cos(theta_left * radian);
            //const y=Math.cos(90.0 * radian);
            //const z=Math.sin(90.0 * radian) * Math.sin(theta_left * radian);
            
            const x=Math.sin(phi_up * radian) * Math.cos(theta * radian);
            const y=Math.cos(phi_up * radian);
            const z=Math.sin(phi_up * radian) * Math.sin(theta * radian);
            
            //const x=Math.sin(phi * radian) * Math.cos(theta * radian);
            //const y=Math.cos(phi);
            //const z=Math.sin(phi * radian) * Math.sin(theta * radian);

            //Vec3 XZonly={ // for player full directional speed not Y
            //    cos(theta * radian),
            //    0,
            //    sin(theta * radian)
            //};
            return new THREE.Vector3(x,y,z);
        }
        var sunTime=getDirection(170,90); // theta orient around horizontally, phi vertical/altitude
        var moonTime=getDirection(-170,-90); // theta orient around horizontally, phi vertical/altitude
        // sunrise/sunset = phi at 180, above ground at 170, below ground at 190
        // front facing with camera at 90, back facing at -90 or 270
        // still need the light position itself


        // Lights (PBR-friendly)
        const ambientLight = new THREE.AmbientLight(0xffffff, 0.8); // Increased ambient for better brightness
        scene.add(ambientLight);
        const sunLight = new THREE.DirectionalLight(0xffffff, 1.5); // Increased intensity
        //sunLight.position.set(100, 100, 100); // Sun position for shadows/lighting (match sunDirection in skyMaterial)
        sunLight.position.set(sunTime.x*8000,sunTime.y*8000,sunTime.z*8000); // Sun position from sphere coords
        scene.add(sunLight);
        
        sunLight.target = new THREE.Object3D();
        sunLight.target.position.set(0, 0, 0); // world origin or camera ground
        sunLight.target.updateMatrixWorld();

        //const helper=new THREE.CameraHelper(sunLight.shadow.camera);
        //scene.add(helper);

        // shadow maps
        renderer.shadowMap.enabled = true;
        renderer.shadowMap.type = THREE.PCFSoftShadowMap; // nicer
        
        const shadowBias = -0.0001; // slight negative for acne
        const shadowNormalBias = 0.1; // for bumpy terrain/mountains
        const shadowRadius = 4; // softens edges (if using BasicShadowMap, ignore for PCF)
        const shadowMapWidth = 4096;
        const shadowMapHeight = 4096;
        
        sunLight.castShadow = true;
        sunLight.shadow.mapSize.width = shadowMapWidth;
        sunLight.shadow.mapSize.height = shadowMapHeight;
        sunLight.shadow.bias = shadowBias;        
        sunLight.shadow.normalBias = shadowNormalBias;     
        sunLight.shadow.radius = shadowRadius;            
        
        // Base frustum (will update dynamically in animate)
        const shadowCamWidth = 12000;  // covers ~1.5 tiles wide (tune if too small/large)
        const shadowCamHeight = 12000; // covers height variation
        const shadowCamFar = 6000;    // light-to-ground distance + margin
        const shadowCamNear = -6000;     // close to light, avoids near-clip issues
        
        sunLight.shadow.camera = new THREE.OrthographicCamera(
            -shadowCamWidth / 2,
            shadowCamWidth / 2,
            shadowCamHeight / 2,
            -shadowCamHeight / 2,
            shadowCamNear,
            shadowCamFar
        );

        //sunLight.shadow.camera.updateProjectionMatrix();
        //helper.update();

        // Sun plane (red)
        /*const sunShadowPlane = new THREE.Mesh(
            new THREE.PlaneGeometry(6000, 6000),
            new THREE.MeshBasicMaterial({ color: 0xff0000, transparent: true, opacity: 0.3, side: THREE.DoubleSide })
        );
        scene.add(sunShadowPlane);*/
        
        // Moon shadow plane (green)

        scene.fog = new THREE.FogExp2(0xaaccff, 0.0008); // color ≈ sky horizon, very low density
// Tune density: 0.00005 – 0.00015 depending on how fast you want fade
// Color should roughly match your sky bottomColor or horizon

        const skyColors={
            topColor: [
                [100,94,160], // morning 1
                [100,106,190], // day begin 3
                [101,115,185], // midday 5
                [100,105,170], // day end 3
                [55,35,140],   // evening 1
                [14,7,60],     // night begin 3
                [1,0,35],     // midnight 5
                [8,10,56],     // night end 3
            ],
            bottomColor: [
                [165,145,175],// morning
                [164,178,160],// day begin
                [165,174,204],// midday
                [178,170,180],// day end
                [215,87,137], // evening
                [130,30,45],   // night begin
                [20,10,30],     // midnight
                [25,40,75],   // night end
            ],
        };
        // calculate the 86400 seconds to 4 segments time,
        // but not equally sized? day/night = 16hr~, morning/evening=8hr~
        // 86400/4 = 21600
        // 16/2 = 8 * 60 * 60 = 28800 / 2 = 14400 (4hr)
        //
        // morning= 1 to 14400 (+14400)
        // day= 14401 to 43200 (+28800)
        // evening= 43201 to 57600 (+14400)
        // night= 57601 to 86400 (+28800)
        //
        // cont in animate
 
        var cDensity=0.5;
        var skyDepthOn=true;
        // Sky sphere (WoW-like gradient with sun)
        const skyGeometry = new THREE.SphereGeometry(3000, 32, 24); // 3000 with depth write to make 'growing in' muntains
        const skyMaterial = new THREE.ShaderMaterial({
            uniforms: {
                //topColor: { value: new THREE.Color(0x0077ff) }, // Blue sky
                //bottomColor: { value: new THREE.Color(0xffffff) }, // Horizon haze
                topColor: { value: new THREE.Color(0x6677ff) }, // Blue sky
                bottomColor: { value: new THREE.Color(0xffeeff) }, // Horizon haze
                moonColor: { value: new THREE.Color(0xeeffff) }, // Sun glow
                sunColor: { value: new THREE.Color(0xfffeee) }, // Sun glow
                //sunColor: { value: new THREE.Color(0xffa600) }, // Sun glow
                //sunDirection: { value: new THREE.Vector3(0.2, 0.2, 1).normalize() }, // Sun pos (normalized)
                sunDirection: { value: sunTime.normalize() }, // Sun pos (normalized) from sphere coords
                //moonDirection: { value: moonTime.normalize() }, // Sun pos (normalized) from sphere coords
                sunSize: { value: 0.00125 }, // Sun disc size
                moonSize: { value: 0.0025 }, // Sun disc size
                xtime: { value: 0.0 },
                daytime: { value: 0.0 },
                cloudDensity: { value: 0.5 }, // Lower base fluff
                cloudAbsorption: { value: 0.01 }, // Much lower = less darkening, trans >0
                cloudScale: { value: 0.0015 }, // Finer detail
                cloudSpeed: { value: 0.0003 }, // Slower drift
                cloudBottom: { value: 200.0 }, // Closer for testing visibility
                cloudThickness: { value: 50.0 }, // Thinner layer = less accum up
                scatteringAniso: { value: 0.7 }, // Stronger rims
                maxSteps: { value: 6 }, // Higher for horizon detail
                lightSteps: { value: 2 }, // Short shadows
                marchSize: { value: 40.0 }, // Smaller = finer, but balance perf
                frame: { value: 0.0 },
                cloudsOn: { value: 2 }, // match with cloudquality startup
                starsOn: { value: 1 },
                lightDir: { value: sunLight.position.normalize() },
                lightDir2: { value: sunLight.position.normalize().negate() },
                nMatrix: { value: new THREE.Matrix3() }, // Initialize empty mat3
                vMatrix: { value: new THREE.Matrix4() }, // View matrix
                mvpMatrix: { value: new THREE.Matrix4() },
                cloudsReady: { value: (loading==-1)?1:0 }, // wait render clouds for loading screen
                cDensity: { value: cDensity }, // multiplier 0.0-1.0
                // Example colors — warm sunrise/sunset orange → soft yellow, or cool moon blue
                horizonGlowColorDay: { value: new THREE.Color(1.0, 0.6, 0.3) }, // warm sun-like
                horizonGlowColorNight: { value: new THREE.Color(0.4, 0.6, 1.0) }, // alternative: cool moon
                horizonGlowIntensity: { value: 1.0 },     // 0.3–1.2 range, subtle to strong
                horizonGlowHeight: { value: 0.25 },       // Where sharpness peak ~ middle (0.3–0.6)
                horizonGlowSharpness: { value: 6.0 },     // 2.0 soft → 8.0+ sharp edge
                horizonNoiseScale: { value: 12.0 },       // Noise frequency (higher = finer)
                horizonNoiseStrength: { value: 0.12 },    // 0.05–0.25 subtle variation
				horizonGlowOn: { value: 1 },
            },
            vertexShader: vertexShaderSky,
            fragmentShader: fragmentShaderSky,
            side: THREE.BackSide, // Inside out
            depthWrite: skyDepthOn,
            fog: false,
        });
        const sky = new THREE.Mesh(skyGeometry, skyMaterial);
        scene.add(sky);
        sky.rotation.set(0.0,0.0,90.0*radian);

        const normalMatrix = new THREE.Matrix3(); // create once and reuse
        const mvpMatrix = new THREE.Matrix4();

        const customDepthMat = new THREE.ShaderMaterial({ // used with sky
            uniforms: {
                //uAlphaMap: { value: myAlphaTexture },
                uAlphaThreshold: { value: 0.3 },
                sunDirection: { value: sunTime.normalize() }, // Sun pos (normalized) from sphere coords
                //moonDirection: { value: moonTime.normalize() }, // Sun pos (normalized) from sphere coords
                sunSize: { value: 0.00125 }, // Sun disc size
                moonSize: { value: 0.0025 }, // Sun disc size
                xtime: { value: 0.0 },
                daytime: { value: 0.0 },
                lightDir: { value: sunLight.position.normalize() },
                lightDir2: { value: sunLight.position.normalize().negate() },
                nMatrix: { value: new THREE.Matrix3() }, // Initialize empty mat3
                vMatrix: { value: new THREE.Matrix4() }, // View matrix
                mvpMatrix: { value: new THREE.Matrix4() },
                cDensity: { value: cDensity }, // multiplier 0.0-1.0
                cloudsOn: { value: 2 }, // match with cloudquality startup

            },
            //vertexShader: vertexShaderClouds2, // Re-use your main vertex shader for consistency
            vertexShader: vertexShaderSky,
            fragmentShader: fragmentShaderShadow,
            side: THREE.DoubleSide, // Inside out?
            transparent: false, // clip instead
            alphaTest: 0.3, // same as threshold
            depthWrite: true,
        });

        // simple 2d clouds
        //const cloudsGeometry = new THREE.SphereGeometry(3100, 32, 24); // more than skybox needs renderorder and depthtest false
        const cloudsGeometry = new THREE.SphereGeometry(2950, 32, 24); // 3000 with depth write to make 'growing in' muntains
        const cloudsMaterial = new THREE.ShaderMaterial({
            uniforms: {
                sunDirection: { value: sunTime.normalize() }, // Sun pos (normalized) from sphere coords
                //moonDirection: { value: moonTime.normalize() }, // Sun pos (normalized) from sphere coords
                sunSize: { value: 0.00125 }, // Sun disc size
                moonSize: { value: 0.0025 }, // S1un disc size
                xtime: { value: 0.0 },
                daytime: { value: 0.0 },
                lightDir: { value: sunLight.position.normalize() },
                lightDir2: { value: sunLight.position.normalize().negate() },
                nMatrix: { value: new THREE.Matrix3() }, // Initialize empty mat3
                vMatrix: { value: new THREE.Matrix4() }, // View matrix
                mvpMatrix: { value: new THREE.Matrix4() },
                cDensity: { value: cDensity }, // multiplier 0.0-1.0

            },
            //vertexShader: vertexShaderClouds2,
            //fragmentShader: fragmentShaderClouds2,
            vertexShader: vertexShaderSky,
            fragmentShader: fragmentShaderShadow, // debug
            side: THREE.BackSide, // Inside out
            depthWrite: false,
            fog: false,
            transparent: true,
            alphaTest: 0.3,
        });
        const clouds = new THREE.Mesh(cloudsGeometry, cloudsMaterial);
        clouds.visible=false;
        //clouds.rotation.copy(sky.rotation);
        clouds.renderOrder = 0;
        scene.add(clouds); // builtin with skymaterial
        //clouds.customDepthMaterial = customDepthMat;
        //clouds.customDepthMaterial.shadowSide = THREE.BackSide;
        sky.castShadow = true;
        sky.customDepthMaterial = customDepthMat;
        sky.customDepthMaterial.shadowSide = THREE.FrontSide;
        clouds.receiveShadow = false;
        sky.receiveShadow = false;
        //sky.rotation.set(0.0,0.0,90.0*radian);

        // Terrain setup 
        const terrainGroup = new THREE.Group();
        scene.add(terrainGroup);

        // LOD config – fine-tuned for your scale (4000 tile size, flying speed ~50)
        const lodLevels = [
            { maxDist:  3000, segments: 256 },   // very close – max detail
            { maxDist:  8000, segments: 128 },   // medium distance
            { maxDist: Infinity, segments:  64 }, // everything else – good enough
            // Optional extra low LOD for very far
            // { maxDist: Infinity, segments: 32 }
        ];
        
        var shadowOn=1; // start with shadow
        //var shadowOn=0; // start no shadow

        const textureLoader = new THREE.TextureLoader();
        // Load textures (replace paths; ensure they exist)
        const grassTex = textureLoader.load('./textures/grass1.png');
        grassTex.wrapS = grassTex.wrapT = THREE.RepeatWrapping;
        grassTex.repeat.set(80.0, 80.0); // Increased repeat for larger tiles/better scaling
        const rockTex = textureLoader.load('./textures/rock1.png');
        rockTex.wrapS = rockTex.wrapT = THREE.RepeatWrapping;
        rockTex.repeat.set(80.0, 80.0);
        const snowTex = textureLoader.load('./textures/snow.png');
        snowTex.wrapS = snowTex.wrapT = THREE.RepeatWrapping;
        snowTex.repeat.set(80.0, 80.0);
        const normalGrass = textureLoader.load('./textures/grass1_normal.png');
        normalGrass.wrapS = normalGrass.wrapT = THREE.RepeatWrapping;
        normalGrass.repeat.set(80.0, 80.0);
        const normalRock = textureLoader.load('./textures/rock1_normal.png');
        normalRock.wrapS = normalRock.wrapT = THREE.RepeatWrapping;
        normalRock.repeat.set(80.0, 80.0);
        const normalSnow = textureLoader.load('./textures/snow_normal.png');
        normalSnow.wrapS = normalSnow.wrapT = THREE.RepeatWrapping;
        normalSnow.repeat.set(80.0, 80.0);
        const roughGrass = textureLoader.load('./textures/grass1_rough.png');
        roughGrass.wrapS = roughGrass.wrapT = THREE.RepeatWrapping;
        roughGrass.repeat.set(80.0, 80.0); // Increased repeat for larger tiles/better scaling
        const roughRock = textureLoader.load('./textures/rock1_rough.png');
        roughRock.wrapS = roughRock.wrapT = THREE.RepeatWrapping;
        roughRock.repeat.set(80.0, 80.0);
        const roughSnow = textureLoader.load('./textures/snow_rough.png');
        roughSnow.wrapS = roughSnow.wrapT = THREE.RepeatWrapping;
        roughSnow.repeat.set(80.0, 80.0);

        // Removed ORM for now; hardcoded values
        const terrainMaterial = new THREE.ShaderMaterial({
            //uniforms: {
            uniforms: THREE.UniformsUtils.merge([
                THREE.UniformsLib.lights,
                THREE.UniformsLib.fog,
                {
                grassTex: { value: grassTex },
                rockTex: { value: rockTex },
                snowTex: { value: snowTex },
                normalGrass: { value: normalGrass },
                normalRock: { value: normalRock },
                normalSnow: { value: normalSnow },
                roughGrass: { value: roughGrass },
                roughRock: { value: roughRock },
                roughSnow: { value: roughSnow },
                repeatScale: { value: 80.0 }, // Match repeat
                lightDir: { value: sunLight.position.normalize() },
                lightDir2: { value: sunLight.position.normalize().negate() },
                daytime: { value: 0.0 },
                cameraPosition: { value: camera.position.clone() }, // Match repeat
                fogColor: { value: new THREE.Color(0xffeeff) }, // Horizon haze
                fogDensity: { value: 0.0008 },
                // Manual shadow map for sun (first directional light)
                sunShadowMap: { value: null },  // will set in animate
                sunShadowMatrix: { value: new THREE.Matrix4() },  // will set in animate
                shadowBias: { value: shadowBias },
                shadowNormalBias: { value: shadowNormalBias }, // pass your const 0.1
                shadowRadius: { value: shadowRadius },
                shadowOn: { value: shadowOn },
            }]),
            //},
            vertexShader: vertexShaderTerrain,
            fragmentShader: fragmentShaderTerrain,
            //side: THREE.DoubleSide,
            side: THREE.FrontSide,
            fog: true,
            lights: true,
        });
        //terrainMaterial.wireframe = true;

        // Function to get height at world position (using same noise)
        function getHeightAt(worldX, worldZ) {
            return heightScale * (perlin.noise(worldX / noiseScale, worldZ / noiseScale, 0) + 0.5);
        }

        // Function to create/update a tile
        function createTile(zOffset, segments) {
            const geometry = new THREE.PlaneGeometry(tileSize, tileSize, segments, segments);
            geometry.rotateX(-Math.PI / 2);
            const mesh = new THREE.Mesh(geometry, terrainMaterial);
            mesh.castShadow = true;
            mesh.position.z = zOffset;
            mesh.userData.segments = segments; // for debug/info if needed
        
            updateTileHeights(mesh); // extract height update to separate function
        
            return mesh;
        }
        
        function updateTileHeights(tile) {
            const geometry = tile.geometry;
            const vertices = geometry.attributes.position.array;
            for (let i = 0; i < vertices.length; i += 3) {
                const worldX = vertices[i] + tile.position.x;
                const worldZ = vertices[i + 2] + tile.position.z;
                vertices[i + 1] = getHeightAt(worldX, worldZ);
            }
            geometry.attributes.position.needsUpdate = true;
            geometry.computeVertexNormals();
        }

        var mountainMaterial=null; // shared, initial only
        function createCustomMountainMaterial(originalMat) {
            if(mountainMaterial==null) {
                //return new THREE.ShaderMaterial({
                mountainMaterial=new THREE.ShaderMaterial({
                    //uniforms: {
                    uniforms: THREE.UniformsUtils.merge([
                        THREE.UniformsLib.lights,
                        THREE.UniformsLib.fog,
                        {
                        map: { value: originalMat.map },
                        normalMap: { value: originalMat.normalMap },
                        roughnessMap: { value: originalMat.roughnessMap },
                        // Add more if needed: metalnessMap, aoMap, etc.
                        lightDir: { value: sunLight.position.normalize() },
                        lightDir2: { value: sunLight.position.normalize().negate() },
                        daytime: { value: 0.0 },
                        cameraPosition: { value: camera.position.clone() },
                        fogColor: { value: new THREE.Color(0xffeeff) }, // Horizon haze
                        fogDensity: { value: 0.0008 },
                        sunShadowMap: { value: null },  // will set in animate
                        sunShadowMatrix: { value: new THREE.Matrix4() },  // will set in animate
                        shadowBias: { value: shadowBias },
                        shadowNormalBias: { value: shadowNormalBias }, // pass your const 0.1
                        shadowRadius: { value: shadowRadius },
                        shadowOn: { value: shadowOn },
                    }]),
                    //},
                    vertexShader: vertexShaderMountain,
                    fragmentShader: fragmentShaderMountain,
                    side: THREE.FrontSide,
                    fog: true,
                    lights: true,
                });
            }

            return mountainMaterial;
        }
        
        var loading=0;

        var mountainlist=[
            { name: "mountain_1", ref_lod0: null, ref_lod1: null },
            { name: "mountain_2", ref_lod0: null, ref_lod1: null },
            { name: "mountain_3", ref_lod0: null, ref_lod1: null },
            { name: "mountain_4", ref_lod0: null, ref_lod1: null },
            { name: "mountain_5", ref_lod0: null, ref_lod1: null },
            { name: "mountain_6", ref_lod0: null, ref_lod1: null },
            { name: "mountain_7", ref_lod0: null, ref_lod1: null },
            { name: "mountain_8", ref_lod0: null, ref_lod1: null },
            { name: "mountain_9", ref_lod0: null, ref_lod1: null },
            { name: "mountain_10", ref_lod0: null, ref_lod1: null },
            { name: "mountain_11", ref_lod0: null, ref_lod1: null },
            { name: "mountain_12", ref_lod0: null, ref_lod1: null },
            { name: "mountain_13", ref_lod0: null, ref_lod1: null },
            { name: "mountain_14", ref_lod0: null, ref_lod1: null },
            { name: "mountain_15", ref_lod0: null, ref_lod1: null },
            { name: "mountain_16", ref_lod0: null, ref_lod1: null },
        ];

        var mountainReady=false;
        function load_mountains() {
            const loader = new GLTFLoader();
            const promises = [];
            
            for(let i=0;i<mountainlist.length;i++) {
                const url = './models/mountainpack/' + mountainlist[i]['name']+".gltf";
                const url_low = './models/mountainpack/' + mountainlist[i]['name']+"_lod1.gltf";
                // Push a promise into the array that also carries the index 'i' in its resolved value
                promises.push(
                    loader.loadAsync(url).then(gltf => ({ gltf, index: i, lod: 0 }))
                );
                promises.push(
                    loader.loadAsync(url_low).then(gltf => ({ gltf, index: i, lod: 1 }))
                );
            }
            Promise.all(promises)
                .then(results => {
                    // results is an array like: [{ gltf: ..., index: 0 }, { gltf: ..., index: 1 }, ...]
                    console.log("All models loaded (mountains)");
                    results.forEach(({ gltf, index, lod }) => {
                        console.log("Processing index:", index);
                        console.log("Model name:", mountainlist[index]['name']+" LOD-"+lod);
                        
                        // Add your logic here using 'index' and 'gltf'
                        mountainlist[index]['ref_lod'+lod] = gltf.scene;
                        
                        if(lod==0) {
                            mountainlist[index]['ref_lod'+lod].traverse(child => {
                                if (child.isMesh && child.material) {
                                    child.castShadow = true; // prep for shadows later
                                    child.material = createCustomMountainMaterial(child.material);
                                }
                            });
                        } else {
                            mountainlist[index]['ref_lod'+lod].traverse(child => {
                                //if (child.isMesh && child.material) {
                                if (child.isMesh) {
                                    //child.material = child.material.clone(); // separate material
                                    //--child.material.transparent = true;
                                    //--child.material.opacity = 0; // start hidden
                                    child.visible = false; // start hidden
                                    child.castShadow = true; // prep for shadows later
                                    child.material = createCustomMountainMaterial(child.material);
                                }
                            });
                        }
                    });
                    console.log("All models processed.(mountains)");
                    mountainReady=true;
                    initTiles();
                    loading=-1;
                    $('loadtxt').innerHTML="Loading...";
                    $('loading').style.opacity=0;
                    $('loading').style.pointerEvents="none";
                    if(cloudQuality==4) {
                        clouds.visible=true;
                    }
                    skyMaterial.uniforms.cloudsReady.value = 1;
                    setTimeout(function() { $('loading').style.display="none"; },1000);
                })
                .catch(error => {
                    console.error("An error occurred during loading:", error);
                });
        }
        load_mountains();

        const spacing=500.0; // clear the center path
        // Function to add mountains to a tile (called on create and recycle)
        function addMountainsToTile(tile) {
            if (!mountainReady) return;
        
            //const mountainGeo = new THREE.ConeGeometry(20, 40, 32); // Larger mountains
            //const mountainMat = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.8, metalness: 0 });

            const spawnC=rndMinMax(2,44);
            for (let i = 0; i < spawnC; i++) { // Add a few per tile
                const posX = rndMinMax(spacing, tileSize / 2) * (Math.random() > 0.5 ? 1 : -1);
                const posZ = Math.random() * tileSize - tileSize / 2;
                const height = getHeightAt(posX + tile.position.x, posZ + tile.position.z);
                
                const pick = Math.floor(Math.random() * mountainlist.length);
                const scale = rndMinMax(150, 250);
                
                const mountain = deepCloneGeometry(mountainlist[pick]['ref_lod1']);
                mountain.position.set(posX, height - 50, posZ); // Slightly below surface
                mountain.rotation.y = Math.random() * Math.PI * 2; // Random rotate
                //mountain.scale.set(scale * 0.01, scale * 0.01, scale * 0.01); // start tiny
                mountain.scale.set(scale, scale, scale);
                mountain.userData = {
                    pick: pick,
                    targetScale: scale,
                    lod0: mountainlist[pick].ref_lod0,  // reference to high detail
                    lod1: mountainlist[pick].ref_lod1,  // reference to low
                    currentLod: 1,        // 1 = low (current)
                    switchDist: 2000,     // switch to high below this
                    fadeDist: 6000,       // fading dist for opacity (4000) - currently "pop" dist (6000)
                    visible: false        // start hidden
                };
                
                // Start fully transparent
                mountain.traverse(child => {
                    //if (child.isMesh && child.material) {
                    if (child.isMesh) {
                        //--child.material.transparent = true;
                        //--child.material.opacity = 0;
                        //child.renderOrder = 5;
                        child.visible = false;
                    }
                });
                
                //mountain.renderOrder = 5;
                
                tile.add(mountain);
            }
        }

        // Initialize tiles
        const tiles = [];
        const zInitOffset=4000.0;
        function initTiles() {
            for (let i = 0; i < numTiles; i++) {
                const tile = createTile(i * tileSize - (numTiles / 2 * tileSize) - zInitOffset, lodLevels[0].segments);
                //tile.renderOrder = 3;
                addMountainsToTile(tile);
                tiles.push(tile);
                terrainGroup.add(tile);
            }
        }
        
        let fireworkTemplate = null;           // ← global
        let fireworkAnimations = [];           // ← global array of clips
        const mixers = []; // Array for per-instance AnimationMixers
        
        
        const fwmaterials = {}; // Store refs
        const fwOffsetY=-5.0;
        function load_fireworks() {
            const loader = new GLTFLoader();
            loader.load('./models/fireworks2/fireworks3.gltf', (gltf) => {
                fireworkTemplate = gltf.scene;
                fireworkAnimations = gltf.animations || [];   // save animations here

                fireworkTemplate.scale.set(50.0,50.0,50.0);

                // fix rotation in blender instead and re-export~
                // if need rotation fixing, make empty non animated parent and try these
                // THIS IS THE MAGIC LINE
                //fireworkTemplate.rotation.y = Math.PI;   // –90° instead of +90° (Blender Z-up → Three Y-up)

                // AND THIS ONE — re-orients the animation tracks themselves
                //fireworkTemplate.traverse((child) => {
                //  if (child.isBone || child.isObject3D) {
                //    child.quaternion.premultiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1,0,0), Math.PI));
                //  }
                //});

                fireworkTemplate.traverse((child) => {
                  if (child.isMesh) {
                    //if(!firstSca) {
                        child.scale.setScalar(1);   // force initial scale to 1 so baked tracks start from visible size
                    //} else { // mp3 page
                    //    child.scale.setScalar(64);   // force initial scale to 1 so baked tracks start from visible size
                    //}
                  }
                });

                // Optional but helps with bounding boxes
                fireworkTemplate.updateMatrixWorld(true);

                // Traverse to log/access objects/materials for debugging
                fireworkTemplate.traverse((child) => {
                  if (child.isMesh) {
                    console.log('Mesh:', child.name, 'Material:', child.material ? child.material.name : 'None');
                  }
                });

                fireworkAnimations.forEach((clip, idx) => {
                    console.log(`Clip ${idx} tracks:`, clip.tracks.map(t => t.name));
                    clip.tracks.forEach(track => {
                      if (track.name.includes('.scale') && track.values.length >= 3) {
                        track.values[0] = 1; // X
                        track.values[1] = 1; // Y
                        track.values[2] = 1; // Z first keyframe
                        console.log('Modified initial scale for', track.name);
                      }
                    });
                });
                console.log('Firework model loaded – ready to spawn!');
                // optional: spawn one immediately for testing
                // spawnFirework(somePos, someNorm);
            }, undefined, (error) => console.error(error));
        }
        load_fireworks();

        
        const atlasTexture = new THREE.TextureLoader().load('./models/fireworks2/finalworks.png');
        // atlasTexture.flipY = false; // Uncomment if UVs invert
        atlasTexture.magFilter = THREE.LinearFilter; // Or Nearest for pixel art
        

        let baseFireworkMaterial = new THREE.ShaderMaterial({
          uniforms: {
            diffuseMapFirework: { value: atlasTexture },
            u_offset: { value: new THREE.Vector2(0, 0) },
            u_repeat: { value: new THREE.Vector2(0.05, 0.05) }, // 1/20
            u_opacity: { value: 1.0 }
          },
          vertexShader: vertexShaderFirework,
          fragmentShader: fragmentShaderFirework,
          //fragmentShader: fragmentShaderFireworkDEBUG,
          transparent: true,
          depthWrite: false, // Helps with blending in space
          side: THREE.DoubleSide // If models need backface
        });
        baseFireworkMaterial.blending = THREE.AdditiveBlending;   // glowy fireworks!
        baseFireworkMaterial.depthWrite = false;
        //--baseFireworkMaterial.dispose(); // Free old program

        // Re-create from scratch (copy-paste your original definition)
        //--baseFireworkMaterial = new THREE.ShaderMaterial({
        //--  uniforms: {
        //--    diffuseMapFirework: { value: atlasTexture },
        //--    u_offset: { value: new THREE.Vector2(0, 0) },
        //--    u_repeat: { value: new THREE.Vector2(0.05, 0.05) },
        //--    u_opacity: { value: 1.0 }
        //--  },
        //--  vertexShader: vertexShaderFirework,
        //--  fragmentShader: fragmentShaderFirework,
        //--  //fragmentShader: fragmentShaderFireworkDEBUG,
        //--  transparent: true,
        //--  transparent: true,
        //--  depthWrite: false,
        //--  side: THREE.DoubleSide,
        //--  blending: THREE.AdditiveBlending
        //--});


        // NEW: Distance-based brightness boost (makes far fireworks pop)
        /*baseFireworkMaterial.onBeforeCompile = (shader) => {
          shader.uniforms.cameraPos = { value: camera.position };
          shader.uniforms.distanceScale = { value: 4.0 }; // Tweak 1.5–4.0
        
          shader.fragmentShader = shader.fragmentShader.replace(
            '#include <fog_fragment>',
            `
            #include <fog_fragment>
            float dist = length(vViewPosition);
            float boost = 1.0 + (dist / 50.0) * distanceScale;  // Adjust 50.0 to your orbit distance
            gl_FragColor.rgb *= boost;
            `
          );
        };*/


        const activeFireworks = [];
        const clock = new THREE.Clock(); // Global
        const fwFPS = 5; // Low as mentioned
        const ANIM_COLS = 4;
        const FRAMES_PER_ANIM = 5;
        const TOTAL_FRAME_COLS = ANIM_COLS * FRAMES_PER_ANIM; // 20
        
        function updateFireworkUV(fw, globalTime) {
          const age = globalTime - fw.startTime;
          const phase = age < fw.launchDuration ? 'ascent' :
                        age < fw.launchDuration + fw.explodeDuration ? 'explode' : 'fade';
        
          let localFrame = 0;
          if (phase === 'ascent') {
            localFrame = Math.floor(age * fwFPS); // Normal for trail/ball
          } else if (phase === 'explode') {
            localFrame = Math.floor((age - fw.launchDuration) * fwFPS);
          } else { // Fade: Slow to last frames or blank
            localFrame = 23; // Freeze on last frame, or set to 0 for first/blank
          }
        
          const frame = localFrame % 24;
          const frameCol = frame % FRAMES_PER_ANIM;
          const frameRow = Math.floor(frame / FRAMES_PER_ANIM);
        
          fw.instance.traverse((child) => {
            if (child.isMesh && child.material && child.material.userData.animIndex !== undefined) {
              let animIndex = child.material.userData.animIndex;
        
              // Caps: Freeze/hide during ascent, blank on fade
              if (animIndex >= 3 && animIndex <= 12) { // Stars/trails range
                if (phase === 'ascent') {
                  animIndex = 15; // Switch to blank anim slot during ascent
                } else if (phase === 'fade') {
                  animIndex = 15; // Blank for off
                }
              }
        
              const animCol = animIndex % ANIM_COLS;
              const animRow = Math.floor(animIndex / ANIM_COLS);
        
              const globalCol = animCol * FRAMES_PER_ANIM + frameCol;
              const globalRowFromTop = animRow * FRAMES_PER_ANIM + frameRow;
        
              const offsetX = globalCol / TOTAL_FRAME_COLS;
              const offsetY = (TOTAL_FRAME_COLS - 1 - globalRowFromTop) / TOTAL_FRAME_COLS;
        
              child.material.uniforms.u_offset.value.set(offsetX, offsetY);
        
              // Opacity fade (already in your loop, but sync here if needed)
              if (phase === 'fade') {
                const fadeProgress = (age - fw.launchDuration - fw.explodeDuration) / 5; // 5s fade
                child.material.uniforms.u_opacity.value = Math.max(0, 1 - fadeProgress);
              } else if (phase === 'ascent' && age < 0.5) {
  child.material.uniforms.u_opacity.value = age / 0.5 * 0.7 + 0.3;
}
              }
          });
        }

        function spawnFirework(localPos, tile, starVariant = Math.floor(Math.random() * 5), trailVariant = Math.floor(Math.random() * 5)) {
          if (!fireworkTemplate) {
            console.warn("Firework model not loaded yet!");
            return;
          }
        
          const instance = fireworkTemplate.clone();
          instance.position.copy(localPos);
          //instance.lookAt(instance.position.clone().add(localNorm));
        
          tile.add(instance);
          
          // DEBUG LINE — shoots 10 units along normal so you can see the exact spot
          //const lineMaterial = new THREE.LineBasicMaterial({ color: 0x00ffff, linewidth: 3 });
          //const lineGeo = new THREE.BufferGeometry().setFromPoints([
          //  new THREE.Vector3(0,0,0),
          //  localNorm.clone().multiplyScalar(10)   // 10 units long cyan line
          //]);
          //const line = new THREE.Line(lineGeo);
          //earth.add(line);   // attached to the firework root

        
          // Mixer for launch anim (same as before)
          const mixer = new THREE.AnimationMixer(instance);
          if (fireworkAnimations.length > 0) {
            let clip = THREE.AnimationClip.findByName(fireworkAnimations, 'launch') || fireworkAnimations[0];
            if (clip) {
              const action = mixer.clipAction(clip);
              action.setLoop(THREE.LoopOnce);
              action.clampWhenFinished = true;
              action.play();

              action.getClip().tracks.forEach(track => {
              if (track.name.includes('.scale')) {
                  console.log('Scale track:', track.name, 'values:', track.values); // See if keys are non-1
                }
              });
            }
          }
        
          // Material overrides (unchanged)
          instance.traverse((child) => {
            if (child.isMesh && child.material) {
              child.renderOrder = 10; // Higher than clouds (assume clouds=0)
              const matName = child.material.name.toLowerCase();
              child.material = baseFireworkMaterial.clone();

              if (matName.includes('fireball')) child.material.userData.animIndex = 0;
              else if (matName.includes('firetrail')) child.material.userData.animIndex = 1;
              else if (matName.includes('firestar')) {
                child.material.userData.animIndex = 3 + starVariant;
              } else if (matName.includes('startrail')) {
                child.material.userData.animIndex = 8 + trailVariant;
              } else {
                child.material.userData.animIndex = 15; // Fallback to blank/empty anim slot
              }
            }
          });
        
          activeFireworks.push({
            instance: instance,
            mixer: mixer,
            startTime: clock.getElapsedTime(),
            launchDuration: 1.8,   // Match your ascent time in seconds
            explodeDuration: 5.0,  // How long the full explosion lasts
            totalDuration: 8.0,     // Total visible time before fade
            tileRef: tile,
          });
        }
        
        var aSFT;
        var currentTime=null;
        var baseTime=0;
        var timeOffset=0;
        function autoSpawnFirework() {
            clearTimeout(aSFT);
            
            let seltile=null;
            //console.log("auto fireworks initiated");
            //let lastDist=-10000.0;
            //for(let i=0;i<tiles.length;i++) {
            //	const tile=tiles[i];
                // distance calculation
                /*const dx = tile.position.x - camera.position.x;
                const dy = tile.position.y - camera.position.y;
                const dz = tile.position.z - camera.position.z;
                const sum_squares = dx * dx + dy * dy + dz * dz; // Sum of squared differences
                const distance = Math.sqrt(sum_squares); // Square root to get distance
                console.log("tile z: "+tile.position.z+" tile dist: "+distance);
                if(distance>lastDist) lastDist=distance;*/
            //};
            if(tiles.length>4) {
                let sel=3;
                //let add=0;
                //if(terrainGroupZCount>0) add=1;
                if(terrainGroupZ>tileSize*.5) {
                    sel=2;
                }
                seltile=tiles[(sel)];
                //console.log("seltile="+(sel)+" terrainGroupZ="+terrainGroupZ);
            }
            if(!seltile) {
                aSFT=setTimeout(function() { autoSpawnFirework(); },5000); // try again, not ready
                return;
            }
            
            currentTime = new Date();

            //const currentSimDate = new Date(baseTime + timeOffset);
            const currentSimDate = new Date();
            //console.log(currentSimDate.getHours());
            //const isNYEPeriod = currentSimDate >= new Date(currentTime.getFullYear(), 11, 14, 18) && // Dec 14 18:00 UTC
            //                    currentSimDate < new Date(currentTime.getFullYear(), 11, 15, 18);  // Dec 15 18:00 UTC TEST
            const isNYEPeriod = currentSimDate >= new Date(currentTime.getFullYear(), 11, 31, 18) || // Dec 31 18:00 UTC (start christmas island)
                                currentSimDate < new Date(currentTime.getFullYear(), 0, 1, 6);  // Jan 1 14:00 UTC (end hawaii) // currentTime is currentSimDate, no need to +1 year
            
            //console.log(currentTime.getMinutes());
            //const NYEquiet=(currentTime.getMinutes()<=30)?1.0:0.5; // most active first 30 min past every hour
            const NYEquiet=1.0; // full active whole hour
            const spawnChance = isNYEPeriod ? 1.0 : 0.1; // 100% during NYE, 10% otherwise
            if (Math.random() > spawnChance || Math.random() > NYEquiet || !fireworkTemplate) {
                aSFT=setTimeout(function() { autoSpawnFirework(); },3000);
                return;
            }
            //const lat=0;
            //const lon=0;
            let spawnNum=rndMinMax(1,2);
            if(isNYEPeriod) {
                if(NYEquiet==1.0) {
                    //if(!firstSca) {
                        spawnNum=rndMinMax(5,50);
                    //} else { // mp3 page
                    ///    spawnNum=rndMinMax(2,15);
                    //}
                    //console.log("Is NYE now! (quiet time)");
                } else {
                    spawnNum=rndMinMax(1,10);
                    //console.log("Is NYE now!");
                }
            }
            const fwsizeZ = tileSize;
            const fwsizeX = tileSize / 8;
            
            let fspawn=0;
            while(fspawn<spawnNum) {
                //const posX = rndMinMax(spacing, tileSize / 2) * (Math.random() > 0.5 ? 1 : -1);
                const selected = { x: Math.random() * fwsizeX - fwsizeX / 2, z: Math.random() * fwsizeZ - fwsizeZ / 2 };
                
                //console.dir(selected);
                let fwpos={x: 0.0, y: 0.0, z: 0.0 };
                fwpos.x = selected.x + rndMinMax(-100,100); // Cluster around city
                fwpos.z = selected.z + rndMinMax(-100,100);
                fwpos.y = getHeightAt(fwpos.x + seltile.position.x, fwpos.z + seltile.position.z) + fwOffsetY;


                const spawnWick=rndMinMax(10,600);
                setTimeout(spawnFirework,spawnWick,fwpos,seltile); // fwnorm
                fspawn++;
                //console.log("new fireworks at x:"+fwpos.x+" y:"+fwpos.y+" z:"+fwpos.z);
            }

            let nextFwSpawn=rndMinMax(100,5000);
            aSFT=setTimeout(function() { autoSpawnFirework(); },nextFwSpawn*1);
        }
        autoSpawnFirework();


        const geometryPool = new Map(); // key = segments, value = PlaneGeometry

        function deepCloneGeometry(original) {
            //console.log(original);
            const clone = original.clone();
        
            // Force new buffers for all attributes that might be modified
            for (const name in clone.attributes) {
                clone.attributes[name] = clone.attributes[name].clone();
            }
        
            if (clone.index) {
                clone.index = clone.index.clone();
            }
        
            return clone;
        }

        // In updateTileLODs() – use squared distance to avoid sqrt cost
        function updateTileLODs() {
            const camPos = camera.position;
            tiles.forEach(tile => {
                const tileCenter = new THREE.Vector3(0, 0, tile.position.z).applyMatrix4(terrainGroup.matrixWorld);
                const distSq = camPos.distanceToSquared(tileCenter);
        
                let targetSegments = lodLevels[lodLevels.length - 1].segments;
                for (const level of lodLevels) {
                    if (distSq < level.maxDist * level.maxDist) {
                        targetSegments = level.segments;
                        break;
                    }
                }
        
                if (targetSegments !== tile.userData.segments) {
                    const oldGeo = tile.geometry;
        
                    // Create new geometry
                    const newGeo = new THREE.PlaneGeometry(tileSize, tileSize, targetSegments, targetSegments);
                    //const newGeo = deepCloneGeometry(geometryPool.get(segments));

                    newGeo.rotateX(-Math.PI / 2);
                    //console.log('Switching tile to segments:', targetSegments, 'clone position count:', newGeo.attributes.position.count);
        
                    tile.geometry = newGeo;
                    tile.userData.segments = targetSegments;
        
                    // Re-apply heights & normals
                    updateTileHeights(tile);
        
                    oldGeo.dispose(); // Free memory immediately
                }
            });
        }
        
        let mountainTimer = 0;
        const MOUNTAIN_UPDATE_INTERVAL = 0.3; // every 300ms
        function updateMountainLODsAndVisibility(dt) {
            mountainTimer += dt;
            if (mountainTimer < MOUNTAIN_UPDATE_INTERVAL) return;
            mountainTimer = 0;

            const camPos = camera.position;

            terrainGroup.traverse(obj => {
                if (!obj.userData.pick) return; // not a mountain

                const worldPos = obj.getWorldPosition(new THREE.Vector3());
                const dist = camPos.distanceTo(worldPos);

                const ud = obj.userData;

                // 1. Handle visibility (fade in from far)
                const shouldBeVisible = dist < ud.fadeDist;
                if (shouldBeVisible && !ud.visible) {
                    ud.visible = true;
                    // Trigger fade-in
                    obj.traverse(child => {
                        //if (child.isMesh && child.material) {
                        if (child.isMesh) {
                            //child.material.opacity = 0;
                            //--child.material.opacity = 1.0;
                            child.visible = true;
                            //child.material.needsUpdate = true;
                        }
                    });
                }

                // 2. Fade opacity
                if (ud.visible) {
                    obj.traverse(child => {
                        //if (child.isMesh && child.material && child.material.transparent) {
                        if (child.isMesh) {
                            if (dist < ud.fadeDist - 200) {
                                //--child.material.opacity = 1.0;
                                child.visible = true;
                            } else {
                                // Smooth fade in over 800 units
                                //child.material.opacity = 1.0 - (dist - (ud.fadeDist - 800)) / 800;
                                //child.material.opacity = Math.max(0, Math.min(1, child.material.opacity));
                                //--child.material.opacity = 0.0;
                                child.visible = false;
                            }
                        }
                    });
                }

                // 3. LOD switch (only when close enough)
                const targetLod = dist < ud.switchDist ? 0 : 1;

                if (targetLod !== ud.currentLod) {
                    const parent = obj.parent;
                    if (!parent) return;

                    const newModel = targetLod === 0 
                        ? ud.lod0.clone() 
                        : ud.lod1.clone();

                    // Copy transform
                    newModel.position.copy(obj.position);
                    newModel.rotation.copy(obj.rotation);
                    newModel.scale.copy(obj.scale);

                    // Copy visibility state
                    newModel.userData = { ...ud, currentLod: targetLod };
                    if (!shouldBeVisible) {
                        newModel.traverse(child => {
                            //if (child.isMesh && child.material) {
                            if (child.isMesh) {
                                //--child.material.transparent = true;
                                //--child.material.opacity = 0;
                                child.visible = false;
                            }
                        });
                    }

                    parent.remove(obj);
                    parent.add(newModel);
                }
            });
        }
        
        var initShadowMaps=false;
        function updateShadowCameras() {
            if(!initShadowMaps) {
                if (sunLight.shadow.map) {
                    sunLight.shadow.map.texture.wrapS = THREE.ClampToEdgeWrapping;
                    sunLight.shadow.map.texture.wrapT = THREE.ClampToEdgeWrapping;
                    sunLight.shadow.map.texture.borderColor = new THREE.Vector4(1, 1, 1, 1); // 1.0 = lit (white), hides excess shadows outside map

                    // Force update (optional, but good for test)
                    sunLight.shadow.map.needsUpdate = true;

                    initShadowMaps=true;
                    console.log("Shadow map init");
                }
            }


            const terrainZ = terrainGroup.position.z; // track terrain movement
            const camZ = camera.position.z;
            const centerZ = camZ - terrainZ; // world z-center of visible area
    
            if (sunLight.shadow.map) {
                // Sun
                const sunPos = sunLight.position.clone();
                const sunDir = new THREE.Vector3();
                sunLight.getWorldDirection(sunDir);
                
                // Target: world center z, with orbit offset to center frustum during side angles
                const orbitOffset = sunDir.clone().multiplyScalar(2000); // pull toward sun, tune 1000–3000
                //const sunTarget = new THREE.Vector3(camera.position).add(orbitOffset);
                const sunTarget = new THREE.Vector3(camera.position.x, camera.position.y, centerZ).add(orbitOffset);

                // Position: behind sun along its dir
                //sunLight.shadow.camera.position.copy(sunPos).addScaledVector(sunDir, -shadowCamFar / 2);
                
                // Lock rotation to scene axes (no tilt/moire)
                //sunLight.shadow.camera.rotation.set(0, 0, 0);
                //sunLight.shadow.camera.up.set(0, 1, 0);
    
                //sunLight.shadow.camera.updateProjectionMatrix();
                sunLight.shadow.camera.updateMatrixWorld();
                sunLight.shadow.matrix.copy(sunLight.shadow.camera.projectionMatrix).multiply(sunLight.shadow.camera.matrixWorldInverse.clone());
            
                terrainMaterial.uniforms.sunShadowMatrix.value.copy(sunLight.shadow.matrix);
                terrainMaterial.uniforms.sunShadowMap.value = sunLight.shadow.map.texture;
                
                if(mountainMaterial!=null) {
                    mountainMaterial.uniforms.sunShadowMap.value = sunLight.shadow.map.texture;
                    mountainMaterial.uniforms.sunShadowMatrix.value.copy(sunLight.shadow.matrix);
                }
                
                if(typeof sunShadowPlane!=="undefined") {
                    // Update sun cover plane
                    sunShadowPlane.position.copy(sunTarget);
                    sunShadowPlane.rotation.set(-Math.PI / 2, 0, 0); // lay flat on XZ
                    sunShadowPlane.updateMatrixWorld();
                }
            }
        }

        var notices=[];
        var nid=-1;
        function pushNotice(txt="") {
            let ts=new Date().getTime();
            let dur=5*1000; // 5 sec
            nid++;

            let obj={
                idx: nid,
                ts: ts,
                dur: dur,
                opa: 100.0,
            };
            // create html here
            let html="<span id=\"notice_"+nid+"\">"+txt+"</span>";
            //$('notices').insertAdjacentHTML('beforeend',html);
            $('notices').insertAdjacentHTML('afterbegin',html);

            notices.push(obj);
        }
        function updateNotices(ts=0, dt=0) {
            if(notices.length==0) return;

            for(let i=0;i<notices.length;i++) {
                if((notices[i]['ts']+notices[i]['dur'])<ts) {
                    if(notices[i]['opa']<=0) {
                        // delete
                        $('notice_'+notices[i]['idx']).parentNode.removeChild($('notice_'+notices[i]['idx']));
                        notices.splice(i,1);
                        i--;

                    } else {
                        // fade out
                        notices[i]['opa']-=100.0 * dt;
                        $('notice_'+notices[i]['idx']).style.opacity=notices[i]['opa']/100;
                    }
                }
            }
        }

        var dd=null;
        var hh=0;
        var mm=0;
        var ss=0;
        var tsp=0;
        var ss_theta=0;
        var ss_phi=0;
        var testval=0;
        var tmpColor={};
        var testValOn=false;
        var testValAuto=true;
        
        var waitDensity=0.0;
        var nextDensity=0.0;
        var cloudDensityTarget=cDensity;

        var windY=0.0; // negative/positive range? make dome full sphere to do windX and windZ as well
        var waitWindY=0;
        var nextWindY=0;
        var windVelocityY=0.0;

        let lodUpdateTimer = 0;
        const LOD_UPDATE_INTERVAL = 0.25; // seconds

        let loadtime=0.0;
        let loaddelta=0.0;
        let initloading=true;
        // Animation loop
        let lastTime = 0;
        const infoDiv = document.getElementById('info');
        let terrainGroupZ=0;
        let terrainGroupZCount=0;
        let terrainMove=true;
        let gradientBG="";

        var lastPos=new THREE.Vector3(0.0,0.0,0.0);
        var lastRot=new THREE.Vector3(0.0,0.0,0.0);
        var infoOpa=300;
        function animate(time) {
            requestAnimationFrame(animate);
            const dt = (time - lastTime) / 1000;
            lastTime = time;
            
            loaddelta += dt;
            
            if(loading!=-1 && loaddelta>0.3) {
                loadtime += loaddelta;
                loaddelta=0.0;
                loading++;
                if(loading>3) loading=0;
                let loadtxt="Loading";
                for(let i=0;i<loading;i++) loadtxt+=".";
                if(loadtime>12.0) {
                    loadtxt+="<br /><span style=\"white-space: nowrap; break-word: none; font-size: 10pt;\">(Taking a long time to load models,<br />try refresh in case it got stuck again x))";
                }
                $('loadtxt').innerHTML=loadtxt;
                if(initloading) {
                    const loadtxtwidth=$('loadtxt').clientWidth;
                    $('loadtxt').style.width=(loadtxtwidth+5)+"px"; // hardset
                    $('loadtxt').style.textAlign="left";
                    initloading=false;
                }
            }

            updateNotices(Date.now(), dt);
        
            if(waitDensity>nextDensity) {
                cloudDensityTarget = rndMinMax(10,90) * .01; // %
                nextDensity = rndMinMax(5,50); // sec
                waitDensity = 0;
				//console.log("cloudDensityTarget="+cloudDensityTarget+", cDensity="+cDensity);
            } else {
                waitDensity+=dt;
            }
            
            if(cDensity<cloudDensityTarget) {
                cDensity+=0.001 * dt;
                if(cDensity>0.9) cDensity=0.9;
            } else if(cDensity>cloudDensityTarget) {
                cDensity-=0.001 * dt;
                if(cDensity<0.1) cDensity=0.1;
            }
            skyMaterial.uniforms.cDensity.value=cDensity;
            customDepthMat.uniforms.cDensity.value=cDensity;
            //console.log("cDensity="+cDensity);
            
            // only used for test clouds sphere simple cloud 1
            const cloudsSpinY=windVelocityY * dt; // dt = 1.0 = sec
            
            if(waitWindY>nextWindY) {
                windY = rndMinMax(-1.0,1.0);
                nextWindY = rndMinMax(5,50); // sec
                waitWindY = 0;
            } else {
                waitWindY+=dt;
            }
            
            if(windVelocityY<windY*.001) {
                windVelocityY+=0.001 * dt;
                if(windVelocityY>1.0) windVelocityY=1.0;
            } else if(windVelocityY>windY*.001) {
                windVelocityY-=0.001 * dt;
                if(windVelocityY>0.0) windVelocityY=0.0;
            }
            
            clouds.rotation.y -= cloudsSpinY;
           
            if(terrainMove) {
                // Move world toward camera
                const terrainMoveZ=speed * dt;
                
                terrainGroupZ += terrainMoveZ;
                if(terrainGroupZ>=tileSize) { // keeping track and for fireworks
                    terrainGroupZ-=tileSize;
                    terrainGroupZCount++;
                }
                terrainGroup.position.z -= terrainMoveZ;
            }

            // Recycle tiles
            const firstTile = tiles[0];
            if(firstTile) {
            if (firstTile.position.z + terrainGroup.position.z < -tileSize-zInitOffset) {
                tiles.shift();
                firstTile.position.z += numTiles * tileSize-zInitOffset;
                // Update heights for new position
                const vertices = firstTile.geometry.attributes.position.array;
                for (let i = 0; i < vertices.length; i += 3) {
                    const worldX = vertices[i] + firstTile.position.x;
                    const worldZ = vertices[i + 2] + firstTile.position.z;
                    vertices[i + 1] = getHeightAt(worldX, worldZ);
                }
                firstTile.geometry.attributes.position.needsUpdate = true;
                firstTile.geometry.computeVertexNormals();
                // Clear old children
                while (firstTile.children.length) firstTile.remove(firstTile.children[0]);
                // Re-add new mountains
                addMountainsToTile(firstTile);
                tiles.push(firstTile);
            }
            }

            // Update info box
            const pos = camera.position;
            const rot = camera.rotation;
            infoDiv.innerHTML = `
                Camera Position:<br>
                X: ${pos.x.toFixed(2)}<br>
                Y: ${pos.y.toFixed(2)}<br>
                Z: ${pos.z.toFixed(2)}<br><br>
                Camera Rotation:<br>
                X: ${(rot.x * 180 / Math.PI).toFixed(2)}°<br>
                Y: ${(rot.y * 180 / Math.PI).toFixed(2)}°<br>
                Z: ${(rot.z * 180 / Math.PI).toFixed(2)}°
            `;
            if(lastRot.x!=rot.x || lastRot.y!=rot.y || lastRot.z !=rot.z ||
                lastPos.x!=pos.x || lastPos.y!=pos.y || lastPos.z!=pos.z) {
                infoOpa=300.0; // 100 is fade time + 2x 100 wait
                lastRot.copy(rot);
                lastPos.copy(pos);
                //console.log("has moved");
            }
            //console.log("last rotation"+lastRot.x+" infoOpa: "+infoOpa);
            if(infoOpa>0.0) {
                infoOpa-=100.0 * dt; // 1 sec
                $('info').style.opacity=(Math.min(infoOpa,100)*.01);
            } else {
                infoOpa=0.0;
                $('info').style.opacity=infoOpa*.01;
            }

            lodUpdateTimer += dt;
            if (lodUpdateTimer >= LOD_UPDATE_INTERVAL) {
                updateTileLODs();
                lodUpdateTimer = 0;
            }
            
            updateMountainLODsAndVisibility(dt);

            var lapse=86400; // realtime
            if(testValOn) {
                lapse=2000; // debugtime testval uncomment
            }

            //sky.position.copy(camera.position).setY(0);
            //sky.position.set(0.0, 0.0, camera.position.z);

            // sun position calculations
            dd=new Date();
            hh=dd.getHours()*60*60;
            mm=dd.getMinutes()*60;
            ss=dd.getSeconds()+mm+hh;
            //ss+=(6*60*60); // offset time, - 4-5 hours seem about accurate for sunset at 6pm front
            ss-=(6*60*60);
            if(ss>86400) ss-=86400;
            if(ss<0) ss+=86400;

            if(testValOn) {
                // debugtime
                if(testValAuto) {
                    testval++;
                }
                if(testval>2000) testval-=2000;
                tsp=testval/1000; // debugtime
                ss=testval;
            } else {
                // realtime
                tsp=ss/43200; // realtime comment out for debugtime sunpos
            }

            ss_phi=((tsp)*180)+90;
            if(ss_phi>360) ss_phi-=360;
            
            //ss_theta=(-45);
            var scal=0;
            if(ss_phi<180) {
            // make 86400 into 360 be 0 to 180 and 180 to 360 into 0 to 2 then 0 to 2 into -1 to 0 to 1, then math abs -1 0 1 into 1 0 1 then make 1 0 1 into 0 1 0 with 1- and 0 -1 0 with -1-
                scal=Math.abs((ss_phi/90)-1); // 0 1 0
                if(ss_phi<90) scal=-scal; //negative edge case
            } else {
                scal=1-Math.abs(1-((ss_phi-180)/90)-1); // 0 -1 0
            }
            //ss_theta=(scal*45);
            //let smoothscal=SmoothStep(-1.0,1.0,scal);
            let smoothscal=(easeInOut((scal+1.0)*.5)*2.0)-1.0; // curve the midnight/midday corner point up/down while arriving at center
            ss_theta=(smoothscal*45);

            let ssTun = ss - (25*lapse/100);
            if(ssTun<0) ssTun+=lapse;
            //console.log(Math.abs((ss/(lapse*.5))-1.0));
            const daytime=Math.abs((ssTun/(lapse*.5))-1.0); // 1 0 1

            //console.log("ss_phi: "+ss_phi+" ss_theta: "+ss_theta+" scal="+scal);
            //console.log(" scal="+scal);

            // phi and theta have been swapped here intentionally to tilt on a different axis than top/bottom~
            sunTime=getDirection(ss_phi,ss_theta); // 90,170 = default front above ground, 180 phi = on ground
            moonTime=getDirection(-ss_phi,-ss_theta); // 90,170 = default front above ground, 180 phi = on ground
            //sunPhi should be 180 at 6am and 6pm, going to 300 at night and 60 on day
            //sunTheta should be 90 at 6pm, -90 or 270 at 6am
           
            if(daytime>=0.5) {
                sunLight.position.set(sunTime.x*8000,sunTime.y*8000,sunTime.z*8000);
            } else {
                sunLight.position.set(-moonTime.x*8000,moonTime.y*8000,moonTime.z*8000);
            }
            skyMaterial.uniforms.sunDirection.value = sunTime.normalize();
            cloudsMaterial.uniforms.sunDirection.value = sunTime.normalize();
            customDepthMat.uniforms.sunDirection.value = sunTime.normalize();
            //skyMaterial.uniforms.moonDirection.value = moonTime.normalize();
            
            terrainMaterial.uniforms.lightDir.value.copy(sunLight.position.normalize());
            terrainMaterial.uniforms.cameraPosition.value.copy(camera.position);
            if(mountainMaterial!=null) {
                mountainMaterial.uniforms.cameraPosition.value.copy(camera.position);
                mountainMaterial.uniforms.lightDir.value.copy(sunLight.position).normalize();
                mountainMaterial.uniforms.lightDir2.value.copy(sunLight.position).normalize().negate();
            }
            
            camera.updateMatrixWorld();
            skyMaterial.uniforms.lightDir.value.copy(sunLight.position.normalize());
            skyMaterial.uniforms.lightDir2.value.copy(sunLight.position.normalize().negate());
            cloudsMaterial.uniforms.lightDir.value.copy(sunLight.position.normalize());
            cloudsMaterial.uniforms.lightDir2.value.copy(sunLight.position.normalize().negate());

            clouds.updateMatrixWorld();
            normalMatrix.getNormalMatrix(clouds.matrixWorld);
            skyMaterial.uniforms.nMatrix.value = normalMatrix;
            skyMaterial.uniforms.vMatrix.value.copy(camera.matrixWorldInverse);
            cloudsMaterial.uniforms.nMatrix.value = normalMatrix;
            cloudsMaterial.uniforms.vMatrix.value.copy(camera.matrixWorldInverse);
            customDepthMat.uniforms.nMatrix.value = normalMatrix;
            customDepthMat.uniforms.vMatrix.value.copy(camera.matrixWorldInverse);
            //cloudsMaterial.uniforms.invView.value.copy(camera.matrixWorldInverse).invert();
            //cloudsMaterial.uniforms.invProjection = { value: new THREE.Matrix4() };
            //cloudsMaterial.uniforms.invProjection.value.copy(camera.projectionMatrix).invert();
            mvpMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse).multiply(clouds.matrixWorld);
            cloudsMaterial.uniforms.mvpMatrix.value = mvpMatrix;
            skyMaterial.uniforms.mvpMatrix.value = mvpMatrix;
            customDepthMat.uniforms.mvpMatrix.value = mvpMatrix;

            
            // sky color calculations
            var l=0; // segment selection
            
            
            var t=ss; // range current

            //const hr_1=(4.2*lapse/100); // percentage
            const hr_1=(lapse/24); // percentage
            const hr_3=hr_1*3;
            const hr_4=hr_3+hr_1;
            const hr_5=hr_4+hr_1;
            const hr_9=hr_5+hr_4;
            const hr_12=hr_9+hr_3;
            const hr_13=hr_12+hr_1;
            const hr_16=hr_13+hr_3;
            const hr_21=hr_16+hr_5;

            //console.log("ss="+ss);
            //console.log("hr_1="+hr_1+" ss>hr_1?:"+((ss>hr_1)?"true":"false"));
            //console.log("hr_="+hr_3+" ss>hr_3?:"+((ss>hr_3)?"true":"false"));
            //console.log("hr_="+hr_4+" ss>hr_4?:"+((ss>hr_4)?"true":"false"));
            //console.log("hr_="+hr_5+" ss>hr_5?:"+((ss>hr_5)?"true":"false"));
            //console.log("hr_="+hr_9+" ss>hr_9?:"+((ss>hr_9)?"true":"false"));
            //console.log("hr_="+hr_12+" ss>hr_12?:"+((ss>hr_12)?"true":"false"));
            //console.log("hr_="+hr_13+" ss>hr_13?:"+((ss>hr_13)?"true":"false"));
            //console.log("hr_="+hr_16+" ss>hr_16?:"+((ss>hr_16)?"true":"false"));
            //console.log("hr_="+hr_21+" ss>hr_21?:"+((ss>hr_21)?"true":"false"));

            //ss-=(10*lapse/100);
            //if(ss<0) ss+=lapse; // rollover

            var m=hr_1; // range max
            if(ss>hr_21) { // night end 3
                l=7;
                t=ss-hr_21;
                m=hr_3;
            } else if(ss>hr_16) { // midnight 5
                l=6;
                t=ss-hr_16;
                m=hr_5;
            } else if(ss>hr_13) { // night begin 3
                l=5;
                t=ss-hr_13;
                m=hr_3;
            } else if(ss>hr_12) { // evening 1
                l=4;
                t=ss-hr_12;
                m=hr_1;
            } else if(ss>hr_9) { // day end 3
                l=3;
                t=ss-hr_9;
                m=hr_3;
            } else if(ss>hr_4) { // midday 5
                l=2;
                t=ss-hr_4;
                m=hr_5;
            } else if(ss>hr_1) { // day begin 3
                l=1;
                t=ss-hr_1;
                m=hr_3;
            }
            var l2=l+1;
            //if(l2>3) l2=0;
            if(l2>7) l2=0;
            //console.log("l="+l+" l2="+l2+" t="+t+" m="+m);

            tmpColor['r1']=skyColors['topColor'][l][0];
            tmpColor['g1']=skyColors['topColor'][l][1];
            tmpColor['b1']=skyColors['topColor'][l][2];
            tmpColor['r2']=skyColors['topColor'][l2][0];
            tmpColor['g2']=skyColors['topColor'][l2][1];
            tmpColor['b2']=skyColors['topColor'][l2][2];
            
            tmpColor['r3']=skyColors['bottomColor'][l][0];
            tmpColor['g3']=skyColors['bottomColor'][l][1];
            tmpColor['b3']=skyColors['bottomColor'][l][2];
            tmpColor['r4']=skyColors['bottomColor'][l2][0];
            tmpColor['g4']=skyColors['bottomColor'][l2][1];
            tmpColor['b4']=skyColors['bottomColor'][l2][2];
            
            let p = Math.min(t / m, 1); // progress
            //const eP=p; // linear
            const eP = easeInOut(p); // easedProgress
            //const currentValue = lerp(startValue, endValue, eP);

            tmpColor['rlt']=lerp(tmpColor['r1'],tmpColor['r2'],eP);
            tmpColor['glt']=lerp(tmpColor['g1'],tmpColor['g2'],eP);
            tmpColor['blt']=lerp(tmpColor['b1'],tmpColor['b2'],eP);

            tmpColor['rlb']=lerp(tmpColor['r3'],tmpColor['r4'],eP);
            tmpColor['glb']=lerp(tmpColor['g3'],tmpColor['g4'],eP);
            tmpColor['blb']=lerp(tmpColor['b3'],tmpColor['b4'],eP);
        
            tmpColor['fr']=lerp(tmpColor['rlb'],tmpColor['rlt'],.66)*.66;
            tmpColor['fg']=lerp(tmpColor['glb'],tmpColor['glt'],.66)*.46;
            tmpColor['fb']=lerp(tmpColor['blb'],tmpColor['blt'],.66)*.16;

            //if(document.body) {
            //    gradientBG="linear-gradient(90deg, rgb("+tmpColor['rlt']+","+tmpColor['glt']+","+tmpColor['blt']+"), rgb("+tmpColor['rlb']+","+tmpColor['glb']+","+tmpColor['blb']+"))";
            //    document.body.style.backgroundColor="rgb("+tmpColor['fr']+","+tmpColor['fg']+","+tmpColor['fb']+")";
            //    //document.body.style.background=gradientBG;
            //}

            scene.fog = new THREE.FogExp2(new THREE.Color(tmpColor['fr']/255,tmpColor['fg']/255,tmpColor['fb']/255), 0.0005);
            
            //console.log("lerp r1->r2 t/m="+t+"/"+m+" rlt: "+tmpColor['rlt']+" p="+p+" eP="+eP+" l="+l+" l2="+l2);
            
            skyMaterial.uniforms.topColor.value=new THREE.Color(tmpColor['rlt']/255,tmpColor['glt']/255,tmpColor['blt']/255);
            skyMaterial.uniforms.bottomColor.value=new THREE.Color(tmpColor['rlb']/255,tmpColor['glb']/255,tmpColor['blb']/255);
            // sun and sky calculations end

            const milliseconds=ss+(dd.getMilliseconds()/1000.0);
            skyMaterial.uniforms.xtime.value=milliseconds;
            cloudsMaterial.uniforms.xtime.value=milliseconds;
            customDepthMat.uniforms.xtime.value=milliseconds;
            //skyMaterial.uniforms.daytime.value=Math.abs((ss/(86400.0*.5))-1.0); 
            skyMaterial.uniforms.daytime.value=daytime;
            cloudsMaterial.uniforms.daytime.value=daytime;
            customDepthMat.uniforms.daytime.value=daytime;
            terrainMaterial.uniforms.daytime.value=daytime;
            if(mountainMaterial!=null) {
                mountainMaterial.uniforms.daytime.value=daytime;
            }
            
            updateShadowCameras();
            
            
            
            const delta = clock.getDelta(); // For mixers
            const globalTime = clock.getElapsedTime();
            
            // UPDATE ALL ACTIVE FIREWORKS
            for (let i = activeFireworks.length - 1; i >= 0; i--) {
                const fw = activeFireworks[i];
                const tile = activeFireworks[i]['tileRef'];

                // 1. launch keyframe animation
                fw.mixer.update(delta);

                // 2. sprite sheet animation
                updateFireworkUV(fw, globalTime);

                //--// 3. fade out + destroy after ~15 minutes (900 seconds)
                const age = globalTime - fw.startTime;
                const action = fw.mixer._actions[0]; // Assume first action=launch
                if (action && action.time >= action.getClip().duration) { // Finished
                    // Start fade or destroy
                    if (age > action.getClip().duration + 0) { // Extra 5s linger
                        //scene.remove(fw.instance);
                        if(tile) {
                            tile.remove(fw.instance);
                        }

                        fw.instance.traverse((child) => {
                            if (child.isMesh) {
                                if (child.geometry) child.geometry.dispose();
                                if (child.material) {
                                    if (Array.isArray(child.material)) {
                                        child.material.forEach(mat => mat.dispose());
                                    } else {
                                        child.material.dispose();
                                    }
                                }
                            }
                        });
                        if (fw.mixer) fw.mixer.uncacheRoot(fw.instance); // Clean mixer

                        activeFireworks.splice(i, 1);
                    } else if (age > fw.totalDuration - 4) { // Start fade 4 seconds before end (tune 4→6 for slower)
                        const fadeTime = fw.totalDuration - age; // Remaining seconds
                        const fade = fadeTime / 4; // 1 → 0 over 4 sec
                    
                        // Slow opacity ramp + freeze UV on last frame (soft texture fade)
                        fw.instance.traverse((child) => {
                            if (child.isMesh && child.material?.uniforms?.u_opacity) {
                              //child.material.uniforms.u_opacity.value = fade; // Linear fade
                              child.material.uniforms.u_opacity.value = Math.pow(fade, 0.5);
                              // Optional ease-out: Math.pow(fade, 0.5) for slower start
                            }
                        });
                    } else {
                        const fade = 1 - (age - action.getClip().duration) / 5;
                        fw.instance.traverse(child => {
                            if (child.material) child.material.uniforms.u_opacity.value = fade;
                        });
                    }
                }
            }

            controls.update();
            renderer.render(scene, camera);
        }
        animate(0);

        var wireframeMode=false;
        var cloudQuality=3; // start simple2
        //var cloudQuality=2; // start off

        var wrT;
        function window_resize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        }

        // Resize handler
        window.addEventListener('resize', () => {
            clearTimeout(wrT);
            wrT=setTimeout(function() { window_resize(); },100);
        });
        
        function extCloudOff() {
            cloudQuality=2; // off
            skyMaterial.uniforms.cloudsOn.value=0;
            clouds.visible=false;
            // clouds toggled off by parent, update parent with time of day for sunlight?
        }
        //parent.postMessage(['new_tim',timeString,testEarthAngle,testTheta],'*');
        if(window.self!==window.parent) {
            window.addEventListener('message', (event) => {
            //if (event.origin === 'https://links.analiestar.com') {
                console.log('Message from parent:', event.data[0]);
                switch(event.data[0]) {
                    case "cloudsoff":
                        extCloudOff();
            			break;
                    default:
                }
            //}
            });
            
            parent.postMessage(['cloudcheck',''],'*');
        }
        
        const canvas = renderer.domElement;
        
        // Function to hide the cursor
        function hideCursor() {
            canvas.style.cursor = 'none';
            // Prevent default behavior like text selection which interferes with drag
            event.preventDefault(); 
        }
        function showCursor() {
            // You can set it to default, auto, or any other cursor style you prefer
            canvas.style.cursor = 'auto'; 
        }
        canvas.addEventListener('mousedown', hideCursor);
        document.addEventListener('mousedown', hideCursor);
        // Use 'mouseup' on the window/document to ensure the cursor reappears 
        // even if the mouse button is released outside the canvas area
        document.addEventListener('mouseup', showCursor);
        
        /* // alternative PointerLockControls library FPS intended, esc to 'unlock'
        document.addEventListener( 'click', function () {
            controls.lock();
        }, false );
        // The cursor is automatically hidden when the pointer is locked.
        // You can listen for the 'unlock' event to perform other UI actions.
        controls.addEventListener( 'unlock', function () {
            console.log( 'Pointer unlocked, cursor visible.' );
        });*/

        const pressedKeys = new Set;

        document.addEventListener('keydown', (e) => {
            pressedKeys.add(e.key);

            //if (e.key === 'Shift') {
            //    controls.screenSpacePanning = false;
            //}
        });
        document.addEventListener('keyup', (e) => {
            pressedKeys.delete(e.key);

            if (event.key === 'w') { // Toggle with 'w' key
                wireframeMode = !wireframeMode;
                terrainMaterial.wireframe = wireframeMode;
                skyMaterial.wireframe = wireframeMode;
                cloudsMaterial.wireframe = wireframeMode;
                
                if(mountainMaterial!=null) {
                    mountainMaterial.wireframe = wireframeMode;
                }
                if(wireframeMode) {
                    pushNotice("'w': toggle wireframe mode on");
                } else {
                    pushNotice("'w': toggle wireframe mode off");
                }
            }
            if (event.key === 's') { // shadow on - debug dither - debug clean - off
                //shadowOn = (shadowOn==1)?0:1;
                shadowOn++;
                if(shadowOn>3) shadowOn=0;
                terrainMaterial.uniforms.shadowOn.value = shadowOn;
                
                if(mountainMaterial!=null) {
                    mountainMaterial.uniforms.shadowOn.value = shadowOn;
                }
                if(shadowOn==0) {
                    pushNotice("'s': toggle shadow all off");
                } else if(shadowOn==1) {
                    pushNotice("'s': toggle shadow all on");
                } else if(shadowOn==2) {
                    pushNotice("'s': toggle shadow + dither map");
                } else {
                    pushNotice("'s': toggle shadow clean map");
                }
            }
            if (event.key === 'd') { // depthwrite off - on
                skyDepthOn=!skyDepthOn;
                skyMaterial.depthWrite=skyDepthOn;
                if(skyDepthOn) {
                    pushNotice("'d': toggle depthWrite on sky on");
                } else {
                    pushNotice("'d': toggle depthWrite on sky off");
                }
            }
            if (event.key === 't') { // testval debug toggle on - off
                //--testValOn=!testValOn;

                if(testValOn && testValAuto) { // true true
                    testValAuto=false;

                    pushNotice("'t': toggle testVal on with fixed");
                } else if(testValOn && !testValAuto) { // true false
                    testValOn=false;
                    testValAuto=true;
                    pushNotice("'t': toggle testVal off (realtime)");
                } else { // false true ? 
                    testValOn=true;
                    pushNotice("'t': toggle testVal on with auto on");
                }
            }
            if (event.key === 'c') { // cloud high - low - off
                cloudQuality++;
                if(cloudQuality>4) cloudQuality=0;

                clouds.visible = false;
                sky.castShadow=false;
                customDepthMat.uniforms.cloudsOn.value=0;
                if(cloudQuality==0) { // high
                    skyMaterial.uniforms.cloudsOn.value=1;
                    skyMaterial.uniforms.marchSize.value=40.0;
                    skyMaterial.uniforms.maxSteps.value=6;
                    pushNotice("'c': toggle cloud raymarched high");
                } else if(cloudQuality==1) { // low
                    skyMaterial.uniforms.cloudsOn.value=1;
                    skyMaterial.uniforms.marchSize.value=80.0;
                    skyMaterial.uniforms.maxSteps.value=2;
                    pushNotice("'c': toggle cloud raymarched low");
                } else if(cloudQuality==2) { // all off [default]
                    skyMaterial.uniforms.cloudsOn.value=0;
                    pushNotice("'c': toggle cloud all off");
                } else if(cloudQuality==3) { // 2d / simple2 [default] - clouds2 now built into skymaterial (has customdepthmat)
                    skyMaterial.uniforms.cloudsOn.value=2;
                    customDepthMat.uniforms.cloudsOn.value=2;
                    sky.castShadow=true;
                    pushNotice("'c': toggle cloud simple2 [default]");
                } else if(cloudQuality==4) { // 2d / simple1 (separate sphere)
                    skyMaterial.uniforms.cloudsOn.value=0;
                    clouds.visible = true;
                    cloudsMaterial.vertexShader=vertexShaderClouds;
                    cloudsMaterial.fragmentShader=fragmentShaderClouds;
                    //--cloudsMaterial.vertexShader=vertexShaderClouds2;
                    //--cloudsMaterial.fragmentShader=fragmentShaderClouds2;
                    //cloudsMaterial.vertexShader=vertexShaderSky;
                    //cloudsMaterial.fragmentShader=fragmentShaderShadow; // debug
                    //clouds.customDepthMaterial = customDepthMat;
                    cloudsMaterial.needsUpdate=true;
                    pushNotice("'c': toggle cloud simple1");
                }
                //skyMaterial.needsUpdate=true;
            } else if (event.code === 'Space') { //
                terrainMove=!terrainMove;
                if(terrainMove) {
                    pushNotice("'Space': toggle terrainMove moving");
                } else {
                    pushNotice("'Space': toggle terrainMove stopped");
                }
            }
            

            pressedKeys.clear();
        });
        
        var anaS=`%c                                        
                                ,/#(,   
                              ./%%%%(,    
                            .*#%%%%#,     
                           ,/#%%%%%%*      
                         ./%%%%%%%%%%*       
                        *%%%%%%%%%%%%/.       
                      ,#%%%%%%%%%%%%(.        
                    ./%%%%#*/%%%%%%/.  /%(,*(
                  ./#%%%%/. ./%%#/,,%%%%%%%%%%%/
                 *(%%##%%%%%%%%%%%%%%(,./%%%%%%#*. 
               *#%%#*  ../%%%%#/. ./%%%%/.   
            .*(%%#*.    ./%%%%#,  *%%%%(,    
    .*#%%###%%%%%%#/,     .*%%%%#, .*%%%%#,    *
   ./#%%/.*(%%(*.       *%%%%%%%%/.,#%%%%*     *
   .(%%%%%%%%%%/,          ,*//*,           ,
                                        
                                        `

        function printascii() { setTimeout(console.log.bind(console,anaS,'background:  #2a1e27; color: #fbbb57;')); }
        printascii();
    </script>
</body>
</html>

Top
©twily.info 2013 - 2026
twily at twily dot info



2 550 797 visits
... ^ v