~firefoxsimulationsdemo
8 itemsDownload ./*

..
build
jsm
models
textures
ui
index.coins.html
index.html
index.og.html


demoindex.coins.html
467 KB• 34•  13 hours ago•  DownloadRawClose
13 hours ago•  34

{}
<!DOCTYPE html>
<!--
    Author: Twily                                           2025-2026
    Website: twily.info
    Description: threejs playground splat skybox shadows ui joysticks

    music: 
      Witch House / Darkwave / GothMusic | Goth Mix 2026 - Grimelody
      Dark Industrial Deep House Mix 2026 | Desert Survival - Night Driver Music
-->
<html lang="en">
<head>
    <meta charset="UTF-8">
    <!--<meta name="viewport" content="width=device-width, initial-scale=1.0">-->
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
    <title>Three.js FPS Demo Engine</title>
    <style>
        html,body {
            font-size: 10pt;
            font-family: "Droid Sans", "Liberation Sans", "DejaVu Sans", "Segoe UI", Sans;
            width: 100%; height: 100%;
            margin: 0; padding: 0;
            overflow: hidden;
            user-select: none;
            background: #344C6C; color: #b5857f;
            overscroll-behavior: none; /* Stops the pull-down glow/refresh */
            touch-action: none;        /* Prevents browser handling of gestures */
    -webkit-overflow-scrolling: auto;
        }
        canvas {
            display: block;
            position: relative; top: 0; left: 0;
            width: 100%; height: 100%;
            z-index: 1;
        }

        /** { box-sizing: border-box; }*/
        *:focus { outline: none !important; }
        /** { outline: 1px solid #f0f; }*/

        /* Firefox fix (The "Shift" & "Skyward View")
        "manual F11" context is the smoking gun. Firefox handles the UI transition for F11 differently than a programmatic Fullscreen API request. ..the browser keeps a "hit zone" at the top that causes a coordinate shift during the first frame of movement after a lock. */
        body.is-locked {
            position: fixed;
            overflow: hidden;
            width: 100vw;
            height: 100vh;
        }
        :fullscreen, :-moz-full-screen {
            padding: 0 !important;
            margin: 0 !important;
        }

        #maintable {
            display: block;
            position: relative; top: 0; left: 0;
            width: 100%; height: 100%;
            z-index: 1;
        }
        #mainframe {
            position: relative;
        }
        .mnu-spr {
            background-image: url('./ui/menu_159.webp?v=6.4');
            background-repeat: repeat-x;
        }
        #topbar {
            display: none;
            color: #dba72d;
            /*background: #000;*/
            /*background: rgba(0,0,0,.5);*/
            background-position: 0px -2px;
            position: fixed; top: 0; left: 0; z-index: 900;
            text-shadow: 0 0 3px #000;
            height: 23px;
            box-shadow: 0 5px 5px rgba(0,0,0,.25);
        }
        #tdate {
            background-position: 0px -139px;
            padding: 2px 8px;
            display: inline-block; height: 100%;
        }
        #testshadow {
            width: 150px; height: 150px;
            background: #cae;
            position: absolute; top: 50%; left: 50%;
            margin-left: -75px; margin-top: -75px;
            z-index: 999;
            box-shadow: 0 5px 5px rgba(0,0,0,.5);
        }
        #statusMode { }
        #statusMode_info { color: #b5857f; font-style: italic; }
        .emode { color: #FF7A51; }
        .gmode { color: #33FF5C; }

        .selistbg {
            background-image: url('./ui/selist_207.webp?v=3');
        }

        .selist {
            display: inline-block; cursor: pointer; position: relative;
            color: #dbb7a5; /*background: #57412E;*/
            height: 27px; /* 23px */
            width: /*auto*/ 116px;
            text-align: left;
            /*border: 1px solid #f0f;*/
            background-position: -3px -34px;
        }
        .selist:hover {
            background-position: -3px -65px;
        }
        .selist.active {
            background-position: -3px -96px !important;
        }
        .selist span { display: inline-block; padding: 5px 6px; width: 100%; }
        .selist .ult {
            display: block; margin: 0; padding: 0;
            visibility: hidden;
            position: absolute; right: 0;
            z-index: 421; /*background: #5E513A;*/ text-align: left;
            transition: .2s ease; margin-top: 3px;
        }
        .selist ul {
        /*.selist ul {*/
            display: block; list-style: none; margin: 0; padding: 0;
            /*visibility: hidden;*/

            /*box-shadow: 0 5px 5px rgba(0,0,0,.25);*/
            /*background: #222;*/
            background-position: -123px -6px;
            width: 81px;
            overflow: hidden;
        }
        .selisttop {
            background-position: -123px -4px;
            height: 2px;
        }
        .selistbot {
            background-position: -123px -200px;
            height: 3px;
        }

        .selist ul li { white-space: nowrap; background-position: 0px -58px; height: auto;}
        /*.selist ul li:hover { background-position: 0px -111px; }*/

        .selist a:link, .selist a:visited { display: inline-block; width: 100%; height: 100%; color: #dbb7a5; padding: 2px 8px; text-decoration: none; }
        .selist a:hover, .selist a:active { color: #f0d6c9; background: #7B653F; }

        .menu {
            display: inline-block; cursor: default; position: relative;
            color: #dbb7a5; /*background: #57412E;*/
            background-position: 0px -29px;
            width: 84px;
            height: 20px; /* 23px */
            text-align: center;
        }
        .menu:hover { 
            background-position: 0px -83px;
        }
        .menu span { display: inline-block; padding: 1px 8px; }
        .menu ul {
            display: block; list-style: none; margin: 0; padding: 0;
            visibility: hidden; opacity: 0; position: absolute; right: 0;
            z-index: 421; /*background: #5E513A;*/ text-align: left;
            transition: .2s ease; margin-top: 3px;

            box-shadow: 0 5px 5px rgba(0,0,0,.25);
        }
        .menu ul li { white-space: nowrap; background-position: 0px -58px; height: auto;}
        .menu ul li:hover { background-position: 0px -111px; }
        .menu:hover ul { visibility: visible; opacity: 1; }
        
        .menu a:link, .menu a:visited { display: inline-block; width: 100%; height: 100%; color: #dbb7a5; padding: 2px 8px; text-decoration: none; }
        .menu a:hover, .menu a:active { color: #f0d6c9; /*background: #7B653F;*/ }
        
        /*#mnu1 { position: absolute; left: 0; top: 0; }
        #mnu2 { position: absolute; right: 0; bottom: 0; }
        #mnu2 ul { right: 0; bottom: 100%; text-align: right; }*/

        #overlay {
            display: block;
            position: absolute; top: 0; left: 0;
            width: 100%; height: 100%;
            z-index: 100;
            background: transparent;
            user-select: none;
        }
        #ui {
            position: absolute;
            top: 10px;
            left: 10px;
            color: white;
            background: rgba(0, 0, 0, 0.5);
            padding: 10px;
            pointer-events: none;
            border-radius: 5px;
            z-index: 50;
            display: none;
        }
        /*.ui-spr::before,
        .ui-spr::after {*/
        .ui-spr {
            background-image: url('./ui/ui_306.webp');
            background-repeat: no-repeat;
            display: inline-block;
        }
        .button_group {
            display: flex;
            align-items: center;
            justify-content: center;
            position: absolute; bottom: -71px; left: 0;
            width: 100%;
            z-index: 9;
        }
        .button {
            width: 114px; height: 42px;
            background-position: -4px -5px;
            cursor: pointer;
            color: #fdd5b1;
            display: flex;
            align-items: center;
            justify-content: center;
            /*border-image-source: url('./ui/button_48.webp');
            border-image-slice: 16 fill;
            border-image-repeat: repeat;
            border-width: 16px;*/
        }
        .button:hover {
            background-position: -4px -50px;
        }
        .button:active {
            background-position: -4px -95px;
        }
        .button:disabled {
            background-position: -4px -140px;
        }
        .checkbox {
            width: 30px; height: 31px;
            background-position: -239px 8px;
            cursor: pointer;
        }
        .checkbox:hover {
            background-position: -202px 8px;
        }
        .checkbox:active {
            background-position: -165px 8px;
        }
        .checkbox:disabled {
            background-position: -274px 8px;
        }
        .checkbox_checked {
            width: 30px; height: 31px;
            background-position: -239px -27px;
            cursor: pointer;
        }
        .checkbox_checked:hover {
            background-position: -202px -27px;
        }
        .checkbox_checked:active {
            background-position: -165px -27px;
        }
        .checkbox_checked:disabled {
            background-position: -274px -27px;
        }
        .radio {
            width: 30px; height: 31px;
            background-position: -239px -93px;
            cursor: pointer;
        }
        .radio:hover {
            background-position: -202px -93px;
        }
        .radio:active {
            background-position: -165px -93px;
        }
        .radio:disabled {
            background-position: -274px -93px;
        }
        .radio_checked {
            width: 30px; height: 31px;
            background-position: -239px -61px;
            cursor: pointer;
        }
        .radio_checked:hover {
            background-position: -202px -61px;
        }
        .radio_checked:active {
            background-position: -165px -61px;
        }
        .radio_checked:disabled {
            background-position: -274px -61px;
        }
        .close {
            width: 20px; height: 21px;
            background-position: -279px -241px;
            cursor: pointer;
            position: absolute; top: -63px; right: -54px;
        }
        .close:hover {
            background-position: -279px -210px;
        }
        .close:active {
            background-position: -279px -177px;
        }
        .close:disabled {
            background-position: -279px -142px;
        }
        .slider-h {
            width: 100px; height: 22px;
            background-position: -165px -148px;
            position: relative;
        }
        .slider-h .handle {
            width: 17px; height: 27px;
            background-position: -135px -152px;
            position: absolute;
            left: calc( 50% - 8px ); top: -2px;
        }
        .slider-h:hover .handle {
            background-position: -121px -152px;
        }
        .slider-h:disabled { }
        .slider-h:disabled .handle { }
        .slider-v {
            width: 22px; height: 100px;
            background-position: -118px 2px;
            position: relative;
        }
        .slider-v .handle {
            width: 27px; height: 17px;
            background-position: -127px -112px;
            position: absolute;
            top: calc( 50% - 8px ); left: 0px;
        }
        .slider-v:hover .handle {
            background-position: -127px -126px;
        }
        .slider-v:disabled { }
        .slider-v:disabled .handle { }
        .textinput, .textinput::before, .textinput::after {
            background-image: url('./ui/ui_306.webp');
            background-repeat: no-repeat;
            width: 13px;
            height: 33px;
            cursor: text;
            margin: 0; padding: 0;
        }
        .textinputwrap {
            display: inline-block;
        }
        .textinput {
            width: 13px; height: 33px;
            background-position: -24px -228px;
            position: relative; /* Set to relative so pseudos anchor here */
            margin: 0 13px;      /* Add side margins so pseudos don't overlap neighbors */
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .textinput:before {
            content: '';
            width: 13px; height: 33px;
            background-position: -2px -228px;
            position: absolute;
            left: -13px; /* Moves it to the left of the main box */
            top: 0;
        }
        .textinput:after {
            content: '';
            width: 13px; height: 33px;
            background-position: -239px -228px;
            position: absolute;
            right: -13px; /* Moves it to the right of the main box */
            top: 0;
        }
        .textinput:hover {
            background-position: -24px -193px;
        }
        .textinput:hover::before {
            background-position: -2px -193px;
        }
        .textinput:hover::after {
            background-position: -239px -193px;
        }
        .textinput:disabled { }
        input[type="text"] {
            border: 0; background: transparent;
            color: #fdd5b1;
            appearance: none;
        }
        input[type="text"]:focus {
            border: 0; outline: none;
        }
        .cmwindow {
            position: absolute; top: 50px; left: 50px;
            z-index: 230;
            box-shadow: 0 5px 5px rgba(0,0,0,.25);
        }
        #cmw_quality { display: none; }
        #cmw_audio { display: none; }
        #cmw_keybind { display: none; }
        #cmw_joystick { display: none; }
        #cmw_debug { display: none; }
        .cwindow {
            /*background-image: url('./ui/frame_1096.webp');*/
            background-image: url('./ui/window_1096.webp');
            background-repeat: no-repeat;
            width: 71px;
            height: 71px;
            position: relative;
        }
        .cwindow_title {
            position: absolute; top: -62px; left: -50px;
            color: #db9471;
        }
        .cwindow.top-l { width: 71px; height: 71px; background-position: 0px 0px; }
        .cwindow.top-c { min-width: 71px; width: 100%; height: 71px; background-position: -72px 0px; }
        .cwindow.top-r { width: 71px; height: 71px; background-position: -953px 0px; }
        .cwindow.mid-l { width: 71px; min-height: 71px; height: 100%; background-position: 0px -72px; }
        .cwindow.mid-c { width: 100%; height: 100%; background-position: -72px -72px; white-space: nowrap; }
        .cwindow.mid-r { width: 71px;  min-height: 71px; height: 100%; background-position: -953px -72px; }
        .cwindow.bot-l { width: 71px; height: 91px; background-position: 0px -1005px; }
        .cwindow.bot-c { min-width: 71px; width: 100%; height: 91px; background-position: -72px -1005px;  }
        .cwindow.bot-r { width: 71px; height: 91px; background-position: -953px -1005px; }
        .cgroup {
            /*background-image: url('./ui/group_979.webp');
            background-repeat: no-repeat;
            width: 13px;
            height: 33px;*/
            width: 100%; height: 100%;
            color: #ddc6ad;
        }

        #editor-pane {
            position: absolute;
            right: 10px;
            top: 10px;
            width: 200px;
            background: rgba(20, 20, 20, 0.8);
            color: #b5857f;
            padding: 15px;
            display: none; /* Toggle this when an object is clicked */
        }
        input { width: 50px; background: #333; color: white; border: 1px solid #555; }
    
        /*#notices {
            position: absolute;
            top: 10px; left: 10px;
            color: yellow;
            font-family: monospace;
            font-size: 12px;
            pointer-events: none;
            display: flex; flex-flow: column;
            z-index: 1001;
        }
        #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;
            display: none; opacity: 0;
        }*/
        #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: center;
            display: inline-block;
        }
        .tbl { display: table; table-layout: fixed; }
        .tr { display: table-row; }
        .td { display: table-cell; vertical-align: middle;}

        .tbl.full { width: 100%; height: 100%; }

        #keysWrap {
            font-size: 12pt;
            font-family: "Droid Sans", "Liberation Sans", "DejaVu Sans", "Segoe UI", Sans;
            position: absolute; top: 0; left: 0;
            width: 100%; height: 100%;
            pointer-events: none;
            display: /*flex*/ none;
            justify-content: center; align-items: center;
            /*visibility: hidden;*/
            z-index: 1002;
        }
        #keys {
            display: inline-block;
            /*width: 400px; height: 400px;*/
            width: auto; min-width: 200px; height: auto;
            white-space: nowrap;
        }
        .pad18 {
            padding: 18px;
            background: rgba(11,11,13,.5);
            box-shadow: 0 0 10px 0 #111113;
            border-radius: 16px;
            display: inline-block;
        }
        .grey { color: /*#686868*/ #aaa; }
        .yellow { color: #fad900; }
        .pink { color: #fa00d9; }
        #keys > .tbl > .tr > .td:nth-child(1) {
            font-weight: bold;
        }
        .keypad {
            padding: 4px 8px;
            border: 2px solid #b5857f;
            border-radius: 10px;
            display: inline-block;
        }
        
        #leftJoyContainer {
            background: rgba(255,255,255,.1);
            width: 100px; height: 100px;
            border-radius: 200px;
            position: absolute; bottom: 50px; left: 50px;
            z-index: 999;
            display: none;
            touch-action: none; /* Prevents the browser from stealing the touch for scrolling */
        }
        #rightJoyContainer {
            background: rgba(255,255,255,.1);
            width: 100px; height: 100px;
            border-radius: 200px;
            position: absolute; bottom: 50px; right: 50px;
            z-index: 999;
            display: none;
            touch-action: none; /* Prevents the browser from stealing the touch for scrolling */
        }
        #leftJoyStick {
            background: rgba(255,255,255,.3);
            width: 50px; height: 50px;
            border-radius: 100px;
            position: absolute; top: 50%; left: 50%;
            margin-left: -25px; margin-top: -25px;
        }
        #rightJoyStick {
            background: rgba(255,255,255,.3);
            width: 50px; height: 50px;
            border-radius: 100px;
            position: absolute; top: 50%; left: 50%;
            margin-left: -25px; margin-top: -25px;
        }
    </style>
</head>
<body>
    <div id="loading"><div class="tbl full"><div class="tr"><div class="td"><span id="loadtxt">Loading...</span></div></div></div></div>
    <!--<div id="notices"></div>
    <div id="info"></div>
    <div id="keysWrap">
        <div class="pad18">
            <div id="keys"></div>
        </div>
    </div>-->
    <audio id="musicplayer"> <!-- no loop to enable track swap -->
        <source src="" type="audio/mpeg" />
        Your browser does not support the audio element.
    </audio>
    
<div class="mnu-spr" id="topbar"> <!-- separated for z-index -->

<div class="tbl full">
<div class="tr">
<div class="td" style="width: 50%; vertical-align: top;">
    <span id="statusMode" class="emode">Edit mode</span>
    <span id="statusMode_info">(Press "g" to enter game mode)</span>
</div>
<div class="td" style="width: 75px; text-align: center; vertical-align: top;">
<span class="mnu-spr" id="tdate">00:00:00</span>
</div>
<div class="td" style="text-align: right; width: 50%; vertical-align: top;">

<div class="menu mnu-spr" id="mnu1">
    <span>Menu</span>
    <ul>
        <li class="mnu-spr"><a href="#" id="imu1_1" target="_self">Toggle [J]oysticks</a></li>
        <li class="mnu-spr"><a href="#" id="imu1_2" target="_self">Toggle Clouds</a></li>
        <li class="mnu-spr"><a href="#" id="imu1_3" target="_self">Toggle Shadows</a></li>
        <li class="mnu-spr"><a href="#" id="imu1_4" target="_self">Toggle Helpers</a></li>
        <li class="mnu-spr"><a href="#" id="imu1_5" target="_self">Toggle WaterRT</a></li>
        <li class="mnu-spr"><a href="#" id="imu1_6" target="_self">Toggle RayLines</a></li>
        <li class="mnu-spr"><a href="#" id="imu1_7" target="_self">Toggle RendMode</a></li>
        <li class="mnu-spr"><a href="#" id="imu1_8" target="_self">Toggle Flying</a></li>
        <li class="mnu-spr"><a href="#" id="imu1_9" target="_self">Toggle Music</a></li>
        <li class="mnu-spr"><a href="#" id="imu1_10" target="_self">Toggle Mark</a></li>
        <!--<li><a href="#" id="imu4" target="_self">Toggle UI Lock</a></li>-->
    </ul>
</div>
<div class="menu mnu-spr" id="mnu2">
    <span>Settings</span>
    <ul>
        <li class="mnu-spr"><a href="#" id="imu2_1" target="_self">Keybind</a></li>
        <li class="mnu-spr"><a href="#" id="imu2_2" target="_self">Joystick</a></li>
        <li class="mnu-spr"><a href="#" id="imu2_3" target="_self">Quality</a></li>
        <li class="mnu-spr"><a href="#" id="imu2_4" target="_self">Audio</a></li>
        <li class="mnu-spr"><a href="#" id="imu2_5" target="_self">Debug</a></li>
    </ul>
</div>

</div>
</div>
</div>

</div> <!-- topbar end -->

    <div class="tbl" id="maintable">
    <div class="tr">
    <div class="td" style="height: 100%;">
        <div id="mainframe">
            <div id="overlay"></div>
        </div>
    </div>
    </div>
    </div>

    <div id="ui">
        <!--<b>Controls:</b> WASD (Move) | Arrows (Turn) | Q/E (Strafe) <br>-->
        <b>Controls:</b> WASD / Arrows (Move) | Q/E (Strafe)<br>
        Edit mode A/D (Turn) | Play mode A/D (Strafe)<br>
        Shift (Sprint) | Space (Jump) | C (Crouch) <br>
        Click Plane to Test Raycast or press G for Play mode
    </div>

    <div id="editor-pane" id="editor">
        <h3>Transform</h3>
        X: <input type="number" id="posX"> <br>
        Y: <input type="number" id="posY"> <br>
        Z: <input type="number" id="posZ">
    </div>
    
    <div id="leftJoyContainer">
        <div id="leftJoyStick"></div>
    </div>
    <div id="rightJoyContainer">
        <div id="rightJoyStick"></div>
    </div>

    <script type="importmap">
        {
            "imports": {
                "three": "./build/three.module.js",
                "three/addons/": "./jsm/"
            }
        }
    </script>

    <script type="module">
        import * as THREE from 'three';
        import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
        //import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
        //import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';

        const $=function(id) { return document.getElementById(id); }
        String.prototype.replaceAt=function(index,replacement) { return this.substr(0,index)+replacement+this.substr(index+replacement.length); }
        const rndMinMax=function(min,max) { return Math.floor(Math.random()*(max-min+1)+min); }
        var IsJsonString=function(str) { try { JSON.parse(str); } catch(e) { return false; } return true; }

        function b64Enc(str) {
            return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,function(match,p1) {
                return String.fromCharCode(parseInt(p1,16));
            }));
        }
        function b64Dec(str) {
            try {
                return decodeURIComponent(Array.prototype.map.call(atob(str),function(c) {
                    return '%'+('00'+c.charCodeAt(0).toString(16)).slice(-2);
                }).join(''));
            } catch(e) {
                return str;
            }
        }
        var months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
        var days=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];

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 int cloudsOn;
    uniform int starsOn;
    uniform int shadowOn;

    // for clouds2 simple
    uniform float cDensity;
    uniform float cloudSquish;
    uniform float cloudRampLow;
    uniform float cloudRampHigh;
    uniform float cloudRampStrength;

    uniform vec3 horizonGlowColorDay;
    uniform vec3 horizonGlowColorNight;
    uniform vec3 horizonGlowColorDead;
    uniform float horizonGlowIntensity;
    uniform float horizonGlowHeight;
    uniform float horizonGlowSharpness;
    uniform float horizonNoiseScale;
    uniform float horizonNoiseStrength;
    uniform int horizonGlowOn;

    uniform int vortexOn;
    uniform float vortexSpeed;
    uniform float vortexNumArms;
    uniform float vortexSwirl;
    uniform float vortexNoiseScale;
    uniform float vortexNoiseDetail;
    uniform float vortexIntensity;
    uniform vec3 vortexColorDark;
    uniform vec3 vortexColorLight;
    uniform vec3 vortexGlowColor;
    uniform float vortexGlowSize; 
    uniform float vortexGlowIntensity;
    uniform float vortexGlowSharpness;
    
    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))
        );
    }

    //--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
        vec3 dir = normalize(vLocalPosition);
        
        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;
 
        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;


        vec3 totalColor = vec3(0.0);  // Accumulate star contributions
        float starHeight=0.0; // stops at pulled up horizon
        if(height>starHeight && vortexOn==0 && daytime<=0.5) {
            // Configuration parameters (adjustable)
            float numCells = 80.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.7;    // Fraction of cells with stars (0.0 to 1.0)

            sigma*=height; // fade to horizon
        
            vec2 moving_uv = final_uv + vec2(0.0, time / (2.0 * PI));
            
            ivec2 cell = ivec2(floor(moving_uv * numCells));
            // 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 = vec3(0.0, 0.0, 0.0);
        if(vortexOn==0) {
            gradient = mix(bottomColor, topColor, curved_t); // Gradient based on y
        } else { // or use centralDot method below
            float capHeight = 0.05;      // How much of the top stays bright (0.15–0.35)
            float capSharpness = 22.5;    // Higher = sharper transition to dark
            
            // Remap height to make top very bright, then quick fade
            float remapped_t = pow(max(height, 0.0), capSharpness);
            remapped_t = smoothstep(0.0, capHeight, remapped_t);  // 1 at very top → 0 below capHeight
            
            vec3 brightCap = vec3(1.0, 1.0, 1.0);  // pure white, or tint slightly (1.0,0.98,0.9)
            gradient = mix(vec3(0.0), brightCap, remapped_t);  // bottom dark → top bright
            
            // Optional: keep some of original gradient underneath
            // gradient = mix(gradient, brightCap, remapped_t * 0.7);
        }
        float d_moonSize=moonSize * (abs((daytime)-0.25)+0.75);
        float d_sunSize=sunSize * (abs((daytime)-0.25)+0.75);
        float sunDot = dot(viewDir, sunDirection);
        float moonDot = dot(viewDir, -sunDirection);
        float moonGlow = pow(smoothstep(1.0 - d_moonSize, 0.999, moonDot),.8); // Blended sun disc
        float moonRim = smoothstep(1.0 - d_moonSize * 1.05, 1.0, moonDot); // Blended moon disc
        float moonAura = smoothstep(1.0 - d_moonSize * 600.0, 1.0, moonDot); // Blended moon disc
        float moonAura2 = smoothstep(1.0 - d_moonSize * 30.0, 1.0, moonDot); // Blended moon disc
        float sunGlow = pow(smoothstep(1.0 - d_sunSize, 1.0, sunDot),.8); // Blended sun disc
        float sunRim = smoothstep(1.0 - d_sunSize * 1.1, 1.0, sunDot); // Blended sun disc
        float sunAura = smoothstep(1.0 - d_sunSize * 1600.0, 1.0, sunDot); // Blended sun disc
        float sunAura2 = smoothstep(1.0 - sunSize * 300.0, 1.0, sunDot); // Blended sun disc
        float sunAura3 = smoothstep(1.0 - d_sunSize * 50.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 * .03) + (moonRim) + (moonGlow * 1.0)) * max((1.0-((daytime*2.0)-1.0)),0.0);
        float sunBlend=((sunAura3 * .05) + (sunAura2 * .05) + (sunAura * (daytime * .5)) + (sunRim) + (sunGlow * 5.0)) * max(((daytime*.5)+0.5),0.0);
    
        //vec3 skyColor=vec3(1.0,0.0,1.0);
        vec3 skyColor=gradient * 1.02;
        if(vortexOn==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;
        //} else { // using alternative gradient squish above
        //    float centralDot = dot(dir, vec3(1.0, 0.0, 0.0));  // Y-up in local space (top of dome)
        //    // If your sphere is rotated sideways and "top" is along another axis, use e.g. dot(dir, vec3(1.0,0.0,0.0))
        //    // Soft pow glow (similar to your sun/moon)
        //    float centralGlow = pow(smoothstep(0.0, vortexGlowSize, centralDot), vortexGlowSharpness);
        //    centralGlow *= vortexGlowIntensity;
        //    // Optional: make it brighter/more intense at exact center
        //    centralGlow = pow(centralGlow, 0.7);  // steepens the peak
        //    skyColor += vortexGlowColor.rgb * centralGlow;
        //    // Alternative blend: skyColor = mix(skyColor, vortexGlowColor.rgb, centralGlow * 0.6);
        }


        // -------------------
        // 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 = vec3(0.0, 0.0, 0.0);
        if(vortexOn==0) {
            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
        } else {
            horizonGlow = horizonGlowColorDead;
        }

        vec3 finalColor = skyColor;
        //if(cloudsReady==1 && vortexOn==0) {
        if(cloudsReady==1) {
        if(cloudsOn==2 || vortexOn==1) {
            //vec3 ndir = vec3(dir.y, -dir.x, dir.z); // random flip to fit
            float c2_density=cDensity;
            if(vortexOn==1) {
                c2_density=0.7;
            }
            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;

            // Height-based squish: Stretch x/z more at low y (horizon) for distant/laid-out look
            float heightFactor = max(0.01, dir.x); // Avoid div0, clamp low
            float squishFactor = 1.0 + (cloudSquish - 1.0) * (1.0 - heightFactor); // Stronger at horizon
            vec3 squishedDir = dir;
            squishedDir.yz *= squishFactor; // Stretch horiz, compress vert perspective

            //--vec3 flow1 = dir * scale + vec3(cloudtime * 0.10);
            //--vec3 flow2 = dir * scale - vec3(cloudtime * 0.06);
            //--vec3 flow3 = dir * scale + vec3(cloudtime * 0.03);
            // Use squishedDir for flows (replaces dir)
            vec3 flow1 = squishedDir * scale + vec3(cloudtime * 0.10);
            vec3 flow2 = squishedDir * scale - vec3(cloudtime * 0.06);
            vec3 flow3 = squishedDir * scale + vec3(cloudtime * 0.03);
            float n1 = noise2(flow1);
            float n2 = noise2(flow2);
            float n3 = noise2(flow3);
            float baseNoise = (n2 + n3 - n1) * c2_density; // Base detail
            //--float detailNoise = fbm2(dir * scale * 4.2 + vec3(cloudtime * 0.1)); // Smudged variation
            float detailNoise = fbm2(squishedDir * scale * 4.2 + vec3(cloudtime * 0.1)); // Smudged variation
            float _cloudDensity = smoothstep(0.3, .7, baseNoise + detailNoise * 0.2); // Increased coverage

            // Height gradient ramp (black-white-black on y): Multiply density for mid-layer focus
            float ramp = smoothstep(cloudRampLow-.25, cloudRampHigh, heightFactor*1.25); // Fade in low-mid and pull clouds down
            smoothstep(1.0, cloudRampHigh, heightFactor); // Fade out mid-high (invert for top)
            _cloudDensity *= clamp(ramp * cloudRampStrength,0.0,1.0); // Apply ramp (peaks mid, fades top/bottom)

            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);
            vec3 deadColor = vec3(0.03, 0.03, 0.04);
            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
            float diff=max(dot(norm, alterSunDirection), 0.0);
            float diff2=max(dot(norm, -alterSunDirection), 0.0);
            vec3 diffuse=diff*sunColor*sunStrength;
            vec3 diffuse2=diff2*moonColor*moonStrength;
            if(vortexOn==1) {
                float centralDot = dot(dir, vec3(1.0, 0.0, 0.0));  // Y-up in local space (top of dome)
                sunColor=deadColor;
                moonColor=deadColor;
                diff=max(dot(norm, dir), 0.0);
                diff2=max(dot(norm, -dir), 0.0);
                diffuse=diff*sunColor*1.0;
                diffuse2=diff2*moonColor*1.0;
            }
            vec3 fullColor=mix(moonColor,sunColor,getDaytimeFactor(daytime));

            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);
            vec3 colorDead=vec3(0.5,0.5,0.5);
            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 = colorDead;
            if(vortexOn==1) {
                moonCloudBrightness=0.0;
                sunCloudBrightness=0.0;
                cloudBrightness=0.5;
            
                lightColor *= edgeTint * 5.0 * (1.0-(c2_density*.5)) * (1.0-min(abs(((0.0*2.0)-1.0)*1.0),0.5)) * (((0.0*.5)+.45)) * (height);
                cloudColor *=height*.5;

                float capHeight = 0.05;      // How much of the top stays bright (0.15–0.35)
                float capSharpness = 22.5;    // Higher = sharper transition to dark
                // Remap height to make top very bright, then quick fade
                float remapped_t = pow(max(height, 0.0), capSharpness);
                remapped_t = smoothstep(0.0, capHeight, remapped_t);  // 1 at very top → 0 below capHeight
                vec3 brightCap = vec3(1.0, 1.0, 1.0);  // pure white, or tint slightly (1.0,0.98,0.9)
                cloudColor = mix(cloudColor, brightCap, remapped_t);  // bottom dark → top bright
            
                edgeTint = edgeTint * diff2 * (1.0-(c2_density*.5)) * (height*.5);
            } else {
                edgeTint = mix(
                    colorNight, // 0
                    mix(
                        colorTwilight, // 0.5
                        colorDay, // 1
                    mixt),
                mixn) * 2.0;
                // * (((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 * 5.0 * (1.0-(c2_density*.5)) * (1.0-min(abs(((daytime*2.0)-1.0)*1.0),0.5)) * (((mixn*.5)+.45));
                cloudColor *= (daytime*.5)+.25;
                edgeTint = edgeTint * mix(diff2,diff,getDaytimeFactor(daytime)) * (1.0-(c2_density*.5));
            }
            cloudColor *= mix(cloudColor, edgeTint, edgeFactor) + cloudBrightness; // Apply tint

            if(shadowOn>1) {
                cloudColor=vec3(1.0,0.0,0.0); // debug visual
                if(alpha>.3) alpha=1.0;
            }
            
            //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)
        }


        // -------------------
        // Vortex Whirl Effect (WoW death sky style)
        // -------------------
        vec3 vortexColor = vec3(0.0);
        if (vortexOn == 1 && height > 0.0) {  // Active above horizon only
            // Local pos(dir = vLocalPosition) for seamless mesh fit
            float angle = atan(dir.y, dir.z);      // Angle around X (swap y/z if flipped)
        
            // Polar around "sideways up" (X axis for laying sphere)
            float radius = length(dir.zy) * .5;         // Radial from axis
            
            // Create stretched version JUST for swirl strength
            float swirlRadius = 1.0 - pow(1.0 - radius, 5.0);   // ← your chosen long-reach curve
            // Alternative tries:
            // swirlRadius = pow(radius, 0.3);
            // swirlRadius = (radius - 0.15) * 1.6;
            // swirlRadius = clamp(swirlRadius, 0.0, 2.0);  // allow >1 if you want extra twist at edges
            
            // Swirl distortion + time rotation (now with mod for extra wrap safety)
            float rot = xtime * vortexSpeed;
            //angle = mod(angle + rot + (radius * vortexSwirl), 2.0 * PI) - PI;  // Seamless loop

            swirlRadius*=5.0;
            //swirlRadius*=5.0*((1.0-cDensity)*5.0); // music
            float swirlAmount = swirlRadius * vortexSwirl;
            //float swirlAmount = swirlRadius * vortexSwirl * (1.0+cDensity); // music

            // Seamless wrap
            angle = mod(angle + PI, 2.0 * PI) - PI;
            // Alternative (sometimes more stable):
            //angle = fract(angle / (2.0*PI)) * (2.0*PI) - PI;
            
            //angle += rot + swirlAmount;
            angle += rot + swirlAmount + ((1.0+cDensity) * (2.0 * PI)); // music
            
            // Multi-arm branching (persistent)
            float arms = sin(vortexNumArms * angle);
            arms = smoothstep(-1.0, 1.0, arms * 0.8 + 0.2);
        
            // Seamless cartesian polar UV (fixes seam/line where arms meet)
            vec2 polarUV = vec2(cos(angle), sin(angle)) * radius * vortexNoiseScale;
        
            // Optional: 4D noise for time-wrapped variability (if temporal seams bother later)
             //vec4 polar4D = vec4(polarUV, sin(xtime * 0.1) * 2.0, cos(xtime * 0.1) * 2.0);  // Uncomment + use in fbm2 if needed
             //float noiseBase = fbm2(polar4D.xyz);  // But stick to vec3 for now
        
            // Base noise (high contrast)
            float noiseBase = fbm2(vec3(polarUV, xtime * 0.1));
            noiseBase = smoothstep(0.2, 0.8, noiseBase * 1.4 - 0.2);
        
            // Detail layer for jagged/variable arms (toned to reduce stick-out)
            float detail = fbm2(vec3(polarUV * 4.0, xtime * 0.2));
            noiseBase = mix(noiseBase, detail, vortexNoiseDetail);
            noiseBase = clamp(noiseBase, 0.0, 1.0);  // Prevent overbright highlights at seams
        
            // Combine
            float vortexPattern = (noiseBase + detail) * arms;
        
            // Blend dark/light
            vec3 vortexBlend = mix(vortexColorDark, vortexColorLight, vortexPattern);
        
            // Fade: Fuller coverage, softer edges
            float fade = smoothstep(0.0, 0.8, 1.0 - radius) * smoothstep(0.0, 1.0, height * 1.5);
            vortexColor = vortexBlend * fade * vortexIntensity;
        
            // Apply to sky
            finalColor += vortexColor;  // Additive ethereal
        }

        gl_FragColor=vec4(finalColor,1.0);

        //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 daytime;
    uniform vec3 lightDir;
    uniform vec3 lightDir2;
    varying vec2 vUv;
    uniform float cDensity;
    uniform int cloudsOn;

    uniform float cloudSquish;
    uniform float cloudRampLow;
    uniform float cloudRampHigh;
    uniform float cloudRampStrength;
    
    const float PI = 3.1415926535897932384626433832795;
    
    uniform int vortexOn;
    //uniform float vortexSpeed;
    //uniform float vortexNumArms;
    //uniform float vortexSwirl;
    //uniform float vortexNoiseScale;
    //uniform float vortexNoiseDetail;
    //uniform float vortexIntensity;
    //uniform vec3 vortexColorDark;
    //uniform vec3 vortexColorLight;
    
    //uniform int shadowOn;

    // 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 && vortexOn==0) { // 2 = 2d2 default 

        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;

        // Height-based squish: Stretch x/z more at low y (horizon) for distant/laid-out look
        float heightFactor = max(0.01, pos.x); // Avoid div0, clamp low
        float squishFactor = 1.0 + (cloudSquish - 1.0) * (1.0 - heightFactor); // Stronger at horizon
        vec3 squishedDir = pos;
        squishedDir.yz *= squishFactor; // Stretch horiz, compress vert perspective

        // Use squishedDir for flows (replaces dir)
        vec3 flow1 = squishedDir * scale + vec3(time * 0.10);
        vec3 flow2 = squishedDir * scale - vec3(time * 0.06);
        vec3 flow3 = squishedDir * 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(squishedDir * scale * 4.2 + vec3(time * 0.1)); // Smudged variation
        float _cloudDensity = smoothstep(0.3, .7, baseNoise + detailNoise * 0.2); // Increased coverage

        // Height gradient ramp (black-white-black on y): Multiply density for mid-layer focus
        float ramp = smoothstep(cloudRampLow-.25, cloudRampHigh, heightFactor*1.25); // Fade in low-mid and pull clouds down
        smoothstep(1.0, cloudRampHigh, heightFactor); // Fade out mid-high (invert for top)
        _cloudDensity *= clamp(ramp * cloudRampStrength,0.0,1.0); // Apply ramp (peaks mid, fades top/bottom)

        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);

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

        //gl_FragColor = packDepthToRGBA(gl_FragCoord.z);
        gl_FragColor = vec4(1.0, 1.0, 1.0, 0.0);
        }
    }
`;

        var fogDensity=0.0005; // 0.0008
        
        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,5],     // midnight 5 (5 or 35 blue)
                [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

// terrain splat used for "floor" in demo
const vertexShaderTerrain = `
    varying vec2 vUv;
    varying vec3 vNormal;
    varying vec3 vWorldPos;
    uniform float repeatScale;
    uniform float splatScale;
    varying vec2 vSplatUv;
    //--varying float vFogDepth;
    varying vec3 vWorldPosition;

    // Manual shadow coord
    varying vec4 vSunShadowCoord;
    varying vec4 vClockShadowCoord;
    uniform float shadowNormalBias;
    uniform float clockShadowNormalBias;

    uniform mat4 sunShadowMatrix;  // from uniform
    uniform mat4 clockShadowMatrix;
    uniform int shadowOn;
    uniform mat4 uTextureMatrix;
    varying vec4 vProjectedCoords;
    
    const float PI = 3.1415926535897932384626433832795;

    ${THREE.ShaderChunk['common']}
    ${THREE.ShaderChunk['fog_pars_vertex']}
    void main() {
        vec4 worldPos = modelMatrix * vec4(position, 1.0);
        vWorldPos = worldPos.xyz;
        vProjectedCoords = uTextureMatrix * worldPos;

        vUv = uv * repeatScale; // Scale UVs in vertex for repeating

        float rad = radians(90.0); // 45deg worsens seamlessness
        float c = cos(rad);
        float s = sin(rad);

        // Center pivot (optional but nicer — rotates around UV center)
        //vec2 centered = vUv - 0.5;
        vec2 centered = vUv;

        // Rotate
        vec2 rotated;
        rotated.x = centered.x * c - centered.y * s;
        rotated.y = centered.x * s + centered.y * c;

        // Back to original space
        //vUv = rotated + 0.5;
        vUv = rotated;

        // Optional: you can also scale after rotation if needed
        // vUv *= someScaleFactor;

        // Rotate splatmap too? → apply same rotation to vSplatUv

        vSplatUv = uv * splatScale; // Different scale for splatmap
        vNormal = normal;

        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)
        vec3 clockOffset = normal * clockShadowNormalBias; // 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);
        vec4 clockOffsetWorldPos = modelMatrix * vec4(position + clockOffset, 1.0);

        if(shadowOn >= 1) {
          //vSunShadowCoord = sunShadowMatrix * offsetWorldPos;
          vSunShadowCoord = sunShadowMatrix * (modelMatrix *vec4(position, 1.0));
          // Repeat for moon with another offsetWorldPos if separate bias, but same for now
        //vClockShadowCoord = clockShadowMatrix * clockOffsetWorldPos; // sundial always on
        vClockShadowCoord = clockShadowMatrix * (modelMatrix *vec4(position, 1.0));
        }

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

        ${THREE.ShaderChunk['fog_vertex']}
    }
`;

// Updated fragmentShaderTerrain
// Replaced height-based splatting with splatmap (RGBA channels for grass1-3, dirt1)
// Added slope-based mixing to rocks using dot product with up vector
// Used shared normal/rough for grass1-3 + rocks1-4, but mixed normals for dirt1 vs grass1-3
// Roughness shared as roughGrass for ground (grass/dirt), roughRock for rocks
// Removed snow-related code and height-based mixes
// Kept lighting, shadows, fog as-is
// Tuned steepFactor thresholds (0.6-0.8; adjust as needed for slope sensitivity)
// Assumes splatmap weights sum to ~1; added normalization for safety

const fragmentShaderTerrain = `
    precision highp float;

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

    uniform sampler2D splatMix1;
    uniform sampler2D splatMix2;
    uniform sampler2D splatMix3;
    uniform sampler2D splatMix4;
    uniform sampler2D normalSplatMix1;
    uniform sampler2D normalSplatMix2;
    uniform sampler2D normalSplatMix3;
    uniform sampler2D normalSplatMix4;
    uniform sampler2D roughSplatMix1;
    uniform sampler2D roughSplatMix2;
    uniform sampler2D roughSplatMix3;
    uniform sampler2D roughSplatMix4;
    uniform sampler2D splatTex;
    uniform int splatMix; // 0 - 1 - 2 - 3
    uniform float repeatScale;
    uniform float splatScale;
    uniform vec3 lightDir;
    uniform vec3 lightDir2;
    uniform float daytime; // 1 0 1
    uniform float xtime;

    // Manual shadow uniforms
    uniform float shadowBias;
    uniform float clockShadowBias;
    uniform float shadowRadius;
    uniform float clockShadowRadius;
    uniform float shadowRes;
    uniform float clockShadowRes;
    uniform sampler2D sunShadowMap;
    uniform sampler2D clockShadowMap;
    uniform mat4 sunShadowMatrix;
    varying vec4 vSunShadowCoord;
    varying vec4 vClockShadowCoord;
    uniform int shadowOn;
    uniform float rimShineStrength;
    uniform vec3 rimShineColor;

    varying vec2 vUv;
    varying vec3 vNormal;
    varying vec3 vWorldPos;
    varying vec2 vSplatUv;
    //--varying float vFogDepth;

    uniform float ambientMulti;
    uniform float normalStrength;

    uniform sampler2D uProjectionTexture;
    uniform int uToggleActive;
    varying vec4 vProjectedCoords;

void main() {
        vec2 uv = vUv;
        vec2 splatUv = vSplatUv;

        // Splatmap-based texturing (RGB for grass1-3, black/default for dirt1)
        vec3 splat = texture2D(splatTex, splatUv).rgb;
        //float sumSplat = splat.r + splat.g + splat.b;
        float sumSplat = 0.0;
        if(splatMix==3) {
            sumSplat = splat.r + splat.g + splat.b;
        } else if(splatMix==2) {
            sumSplat = splat.r + splat.g;
        } else if(splatMix==1) {
            sumSplat = splat.r;
        }
        float splatMix1Weight = max(0.0, 1.0 - sumSplat);
        float totalWeight = sumSplat + splatMix1Weight;

        float w1 = 0.0;
        float w2 = 0.0;
        float w3 = 0.0;
        float w4 = 1.0; // Default to full dirt if totalWeight == 0
        if (totalWeight > 0.0) {
            w1 = splat.r / totalWeight;
            w2 = splat.g / totalWeight;
            w3 = splat.b / totalWeight;
            w4 = splatMix1Weight / totalWeight;
        }

        // Slope calculation (dot product with up; uses geometric normal for accuracy)
        float ndotup = dot(normalize(vNormal), vec3(0.0, 1.0, 0.0));
        float slope = 1.0 - ndotup; // 0 = flat, 1 = vertical
        float steepFactor = smoothstep(0.025, 0.15, slope); // Tune thresholds for slope sensitivity

        // Albedo calculation
        vec3 groundAlbedo = texture2D(splatMix2, uv).rgb * w1 +
                            texture2D(splatMix4, uv).rgb * w2 +
                            texture2D(splatMix3, uv).rgb * w3 +
                            texture2D(splatMix1, uv).rgb * w4;
        //vec3 rockAlbedo = texture2D(rock2Tex, uv).rgb * w1 +
        //                  texture2D(rock3Tex, uv).rgb * w2 +
        //                  texture2D(rock4eex, uv).rgb * w3 +
        //                  texture2D(rock1Tex, uv).rgb * w4;
        //vec3 albedo = mix(groundAlbedo, rockAlbedo, steepFactor);
        vec3 albedo = groundAlbedo; // skip slope variation
        //vec3 albedo = mix(vec3(1.0,0.0,0.0), vec3(0.0,1.0,0.0), steepFactor);

        //vec3 groundNormal = texture2D(normalSplatMix2, uv).rgb * w1 +
        //                    texture2D(normalSplatMix4, uv).rgb * w2 +
        //                    texture2D(normalSplatMix3, uv).rgb * w3 +
        //                    texture2D(normalSplatMix1, uv).rgb * w4;
        vec3 groundNormal = texture2D(normalSplatMix1, uv).rgb * w1 + 
                            texture2D(normalSplatMix2, uv).rgb * w2 + 
                            texture2D(normalSplatMix3, uv).rgb * w3 + 
                            texture2D(normalSplatMix4, uv).rgb * w4;
        vec3 norm = groundNormal; // skip slope variation
        norm = norm * 2.0 - 1.0; // Unpack

        // 2. Flip the channels you need
        norm.x *= -1.0; // Flip horizontal (Red)
        //norm.y *= -1.0; // Flip vertical (Green) - MOST COMMON FIX

        float groundRough = texture2D(roughSplatMix2, uv).r * w1 +
                           texture2D(roughSplatMix4, uv).r * w2 +
                           texture2D(roughSplatMix3, uv).r * w3 +
                           texture2D(roughSplatMix1, uv).r * w4;
        float rough = groundRough;
        float ao = 1.0;
        float metal = 0.0;

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

        float zeron = smoothstep(0.0, 0.2, abs((daytime * 2.0) - 1.0)); // 1 0 1
        // Force a dip at the horizon (0.5)
float transitionDip = smoothstep(0.0, 0.2, abs(daytime - 0.5) * 2.0);

        // https://gemini.google.com/share/dd85a5f93d28 
        // Normalize daytime so 0.5 (horizon) is our new "zero" for these calculations
float sunHeight  = max(0.0, (daytime - 0.5) * 2.0);  // 0.5 to 1.0 becomes 0.0 to 1.0
float moonHeight = max(0.0, (0.5 - daytime) * 2.0);  // 0.5 to 0.0 becomes 0.0 to 1.0

// Use power to create a curve.
// Higher power = longer transition/softer start.
float sunCurve  = pow(sunHeight, 2.0);
float moonCurve = pow(moonHeight, 2.0);

// Independent Intensity Scales
float sunIntensity  = 1.0;  // Full brightness at noon
float moonIntensity = 0.3;  // Moon is much dimmer

float mixt = sunCurve  * sunIntensity  * transitionDip;
float mixn = moonCurve * moonIntensity * transitionDip;

if (daytime > 0.5) {
    // DAY PHASE: Stretch the transition
    // We use a lower power (e.g., 1.5) to let light kick in faster but stay smooth
    mixt = pow((daytime - 0.5) * 2.0, 1.5) * 1.0;
    mixn = 0.0;
} else {
    // NIGHT PHASE: Sharper or shorter transition
    mixt = 0.0;
    mixn = pow((0.5 - daytime) * 2.0, 2.5) * 0.3;
}

        vec3 lightDir2Mod = vec3(-lightDir2.x, -lightDir2.y, -lightDir2.z);
        
        // Diffuse (sun + moon)
        float diff     = max(dot(lightDir,   finalNormal), 0.0) * ao * mixt;
        float diffMoon = max(dot(lightDir2Mod, finalNormal), 0.0) * ao * mixn;
        
        // Specular (Blinn-Phong)
        vec3 halfway     = normalize(lightDir   + viewDir);
        vec3 halfwayMoon = normalize(lightDir2Mod + viewDir);
        float spec     = pow(max(dot(finalNormal, halfway),     0.0), 32.0) * (rough) * (0.04 + metal) * mixt;
        float specMoon = pow(max(dot(finalNormal, halfwayMoon), 0.0), 32.0) * (rough) * (0.04 + metal) * mixn;

        // ────────────────────────────────────────────────
        // NEW: View-dependent Fresnel rim (subtle shine on edges)
         
        float NdotV_geo   = max(0.0, dot(normalize(vNormal), viewDir));
        float NdotV_bump  = max(0.0, dot(finalNormal,        viewDir));
        
        float NdotV = mix(NdotV_geo, NdotV_bump, 0.25); // ← tune 0.0 to 0.4
        
        float fresnel = pow(1.0 - NdotV, 3.0);          // or 4.0 for sharper
        
        float rimRoughnessMod = mix(0.6, 1.4, rough);   // smoother = more rim
        float rimStrengthNight = mix(0.5, 1.8, mixn);   // stronger at night
        float rim = fresnel * rimShineStrength * rimRoughnessMod * rimStrengthNight;
        
        vec3 color = albedo.rgb * max(diff + diffMoon, 0.3) * 4.8
                   + vec3(spec + specMoon);
                   + rimShineColor * rim;
        // ────────────────────────────────────────────────

        float shadow = 1.0; // no shadow
        if(shadowOn>=1) {
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)
);

        float ndotl=max(dot(finalNormal, -lightDir), 0.01);
        float slopeFactor = sqrt(1.0 - ndotl * ndotl) / ndotl; // tan(acos(ndotl)); // webgl fast
        //float slopeFactor = sqrt(1.0 - ndotl * ndotl) / max(0.0001, ndotl); // webgl safe
        //float slopeFactor = tan(acos(saturate(ndotl))); // glsl/hlsl safe (fast without saturate)

        // Manual shadow sampling for sun
        shadow=abs((daytime*2.0)-1.0); // 1 0 1 0 = 0.5 0 0.5 1 ~;

        vec4 clockShadowCoord = vClockShadowCoord / vClockShadowCoord.w;

        // ndc replaced with shadowBiasMatrix in js
        //clockShadowCoord = clockShadowCoord * 0.5 + 0.5; // NDC to [0,1]

        float clockShadow = 1.0;
        if ((clockShadowCoord.x >= 0.0 && clockShadowCoord.x <= 1.0) &&
           (clockShadowCoord.y >= 0.0 && clockShadowCoord.y <= 1.0) &&
           (clockShadowCoord.z >= 0.0 && clockShadowCoord.z <= 1.0)) {

            float clockShadowDepth = texture(clockShadowMap, clockShadowCoord.xy).r;
            //float clockBias = 0.0005; // Start very small
            //clockShadow = (clockShadowCoord.z - clockBias) > clockShadowDepth ? 0.0 : 1.0; // debug

            //float baseBias = 0.0002;
            float baseBias = clockShadowBias;
            float slopeBias = 0.000005 * slopeFactor; // Add more bias on steeper angles
            float clockBias = baseBias + slopeBias;
            clockShadow = (clockShadowCoord.z - clockBias) > clockShadowDepth ? 0.0 : 1.0;

            if(shadowOn<=2) { // dither shadows
                vec2 clockTexelSize = 1.0 / vec2(clockShadowRes, clockShadowRes); // match mapSize

                // Add to fragmentShader uniforms or defines
const int clockNumSamples = 16;

// Dynamically shrink radius based on distance to light for sharper contact
float distToLight = clockShadowCoord.z; // Depth in light space
float adaptiveRadius = clockShadowRadius * clamp(distToLight * 0.5, 0.2, 1.0);

// Prefetch rotation to avoid redundant trig inside the loop
float clockAngle = fract(sin(dot(clockShadowCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) * 6.2832;
float s = sin(clockAngle);
float c = cos(clockAngle);
mat2 rotationMat = mat2(c, -s, s, c);

// In the loop:
clockShadow = 0.0;  // Reset to accumulate lit
for (int i = 0; i < clockNumSamples; i++) {
    // Apply precomputed rotation matrix for speed
    vec2 rotatedOffset = rotationMat * poissonDisk[i];
    //vec2 finalOffset = rotatedOffset * clockShadowRadius * clockTexelSize;
    vec2 finalOffset = rotatedOffset * adaptiveRadius * clockTexelSize;

    float cd = unpackRGBAToDepth(texture(clockShadowMap, clockShadowCoord.xy + finalOffset));
    // Branchless accumulation: sum += float(test) avoids 'if' overhead
    clockShadow += (clockShadowCoord.z > cd + clockBias) ? 0.0 : 1.0;
}
clockShadow /= float(clockNumSamples);
} // dithering clockshadows <=2

            // Optional: Add a small random dither to break up remaining patterns
            //float dither = fract(sin(dot(clockShadowCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) * 0.001;
            //clockShadow += dither - 0.5 * 0.001; // subtle variation
            //clockShadow = clamp(clockShadow, 0.0, 1.0);
        } // clockShadowCoords<>

        //shadow = min(shadow, clockShadow); // or multiply for stronger effect
        shadow = clockShadow;

        //if(shadowOn>=1) { moved back up - not always on
        vec4 shadowCoord = vSunShadowCoord / vSunShadowCoord.w;

        // ndc replaced with shadowBiasMatrix in js
        //shadowCoord = shadowCoord * 0.5 + 0.5; // NDC to [0,1]

        float sunShadow = 1.0;
        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)) {
            float shadowDepth = texture(sunShadowMap, shadowCoord.xy).r;

            //float baseBias = 0.0002;
            float baseBias = shadowBias;
            float slopeBias = 0.000005 * slopeFactor; // Add more bias on steeper angles
            float bias = baseBias + slopeBias;
            sunShadow = (shadowCoord.z - bias) > shadowDepth ? 0.0 : 1.0;

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

                // Add to fragmentShader uniforms or defines
const int numSamples = 16;

// Prefetch rotation to avoid redundant trig inside the loop
float angle = fract(sin(dot(shadowCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) * 6.2832;
float s2 = sin(angle);
float c2 = cos(angle);
mat2 rotationMat2 = mat2(c2, -s2, s2, c2);

// In the loop:
sunShadow = 0.0;  // Critical: Reset to accumulate lit
for (int i = 0; i < numSamples; i++) {
    // Apply precomputed rotation matrix for speed
    vec2 rotatedOffset2 = rotationMat2 * poissonDisk[i];
    vec2 finalOffset2 = rotatedOffset2 * shadowRadius * texelSize;
    //vec2 finalOffset2 = rotatedOffset2 * adaptiveRadius * texelSize;

    float d = unpackRGBAToDepth(texture(sunShadowMap, shadowCoord.xy + finalOffset2));
    // Branchless accumulation: sum += float(test) avoids 'if' overhead
    sunShadow += (shadowCoord.z > d + bias) ? 0.0 : 1.0;
}
sunShadow /= float(numSamples);
} // dithering shadows <=2

            // TERRAIN
            // Blend with main shadow (or use only clockShadow for the clock)
            //shadow = min(1.0-(sunShadow * .5), clockShadow); // or multiply for stronger effect
            //shadow = (sunShadow + clockShadow) * .5;
            //shadow = (sunShadow * clockShadow);

        } // shadowCoords<>
        // Real shadow receive
        //float shadow = getShadow(); // from shadowmask chunk
            float sunEval=((sunShadow)*.9)+.1;
            sunShadow*=zeron;
            clockShadow*=zeron;

            // shadow strength .4 yo .6 = night to day
            // ambiance strength .6 to .4 = night to day
            float shadowStrength=(daytime*.2)+.4; // .4 to .6
            float ambianceStrength=((1.0-daytime)*.6)+.6*ambientMulti; // .6 to .4
            //color *= sunShadow; // shadow factor + ambient day
            //color *= ((max(clockShadow*sunEval,0.0)) * shadowStrength) + ambianceStrength; // shadow factor + ambient day
            //shadow = min(sunShadow, clockShadow);
            //color *= (shadow * shadowStrength) + ambianceStrength;

            //color=vec3(sunShadow);
            //color=vec3(sunShadow,clockShadow,0.0);
            shadow = min(sunShadow, clockShadow);
            color *= vec3((shadow * shadowStrength) + ambianceStrength);
        } else { // else( shadowOn<1 )
            // always shadow sundial
            shadow*=zeron;

            float shadowStrength = (daytime * .2) + .4; // .4 to .6
            float ambianceStrength = (daytime * .2) + .6; // .6 to .4
            color *= (shadow * shadowStrength) + ambianceStrength; // shadow factor + ambient day
        } // 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);

        vec3 projCoords = vProjectedCoords.xyz / vProjectedCoords.w;
        vec2 puv = projCoords.xy * 0.5 + 0.5;
        // animated pulse
        //float pulse = 1.0 + (sin(xtime * 5.0) * 0.025); // subtle 2.5% pulse
        //vec2 puv = (projCoords.xy / pulse) * 0.5 + 0.5;

        // Check if we are inside the projection frustum
        bool inBounds = puv.x >= 0.0 && puv.x <= 1.0 && puv.y >= 0.0 && puv.y <= 1.0;

        // Check if the surface is facing the projector (optional but cleaner)
        // and if the Z is within range (prevents projection through the whole world)
        // The Dot Product Check:
        // vNormal is your terrain normal. 
        // If it's facing up (0,1,0), the dot product is ~1.0. 
        // If it's a steep cliff, it will fade out.
        float dotFace = dot(vNormal, vec3(0.0, 0.0, 1.0));

        //if (uToggleActive == 1 && inBounds && projCoords.z > -1.0 && projCoords.z < 1.0) {
        //if (uToggleActive == 1 && inBounds ) {
        if (uToggleActive == 1 && inBounds && dotFace > 0.1) {
            float distFromCenter = length(projCoords.xy);
            float projIntensity = 1.3; // Values > 1.0 make it pop

            // Soften the edges of the projection
            float mask = smoothstep(1.0, 0.8, distFromCenter); 

            vec4 projectedTexel = texture2D(uProjectionTexture, puv);
            projectedTexel *= projIntensity;
            
            // Blend with your terrain (e.g., using alpha)
            //color = mix(color, projectedTexel.rgb, projectedTexel.a); 

            // Multiply alpha by dotFace for a smoother transition on slopes
            //color = mix(color, projectedTexel.rgb, projectedTexel.a * clamp(dotFace * 2.0, 0.0, 1.0));

            color = mix(color, projectedTexel.rgb, projectedTexel.a * mask);

            //color = vec3(dotFace,0,0);
            //color = vec3(puv.x, puv.y, 0.0);
        }
        // Temporary debug in Fragment Shader
        //if (uToggleActive == 1) {
        //    // If you move the mouse and the floor color changes, worldPos is correct.
        //    // If the floor color is a static gradient that doesn't change when you move,
        //    // then worldPosition is actually localPosition.
        //    gl_FragColor = vec4(vWorldPos.x * 0.1, vWorldPos.z * 0.1, 0.0, 1.0);
        //    return;
        //}
        //if (uToggleActive == 1 && inBounds) {
        //    // This will turn the square from black to white based on "distance" from projector
        //    float zDepth = projCoords.z * 0.5 + 0.5;
        //    gl_FragColor = vec4(vec3(zDepth), 1.0);
        //    return;
        //}

        if (shadowOn >= 2) {
            gl_FragColor = vec4(vec3(shadow), 1.0);
            //gl_FragColor = vec4(vec3(shadow , 0.0, 1.0-shadow), 1.0);
        } else {
            gl_FragColor = vec4(color, 1.0);
        }
        //gl_FragColor = vec4(viewDir, 1.0);
        //gl_FragColor = vec4(finalNormal, 1.0);
        //gl_FragColor = vec4(finalNormal - viewDir, 1.0);
        //gl_FragColor = vec4(finalNormal - viewDir, 1.0);
        //gl_FragColor = vec4(finalNormal - vNormal, 1.0);
    }
`;

// Updated vertexShaderMountain
// Kept world normal/pos for lighting
// mountain shader used for distant mountain backdrop in demo
const vertexShaderMountain = `
    varying vec2 vUv;
    varying vec3 vWorldNormal; // world-space normal (transformed)
    varying vec3 vWorldPos;    // world-space position
    //--varying float vFogDepth;
    uniform float repeatScale;

    // 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 * 2.0; // hardcoded texture scale
        vUv = uv * repeatScale; // Scale UVs in vertex for repeating
        
        // 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;
          vSunShadowCoord = sunShadowMatrix * (modelMatrix *vec4(position, 1.0));
          // 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']}
    }
`;

// Updated fragmentShaderMountain
// Moved textures to folder-loaded (but kept 'map' as baseAlbedo for original model texture; if replacing, change to rockTex)
// Added snow mixing based on world height (normalized by heightScale=50)
// Added rim-based extension for snow lower on hard edges (fresnel effect)
// Used shared snowTex, normalSnow, roughSnow
// Tuned with uniforms for snow levels, rim power, extend amount
// Kept alpha discard if needed (for original texture)

const fragmentShaderMountain = `
    precision highp float;

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

    uniform sampler2D map;           // base albedo (original or moved to ./textures/mountain_base.png)
    uniform sampler2D normalMap;     // base normal
    uniform sampler2D roughnessMap;  // base rough
    uniform sampler2D snowTex;       // snow albedo
    uniform sampler2D normalSnow;    // snow normal
    uniform sampler2D roughSnow;     // snow rough
    uniform vec3 lightDir;
    uniform vec3 lightDir2;
    uniform float daytime; // 1 0 1
    uniform float snowStart;       // e.g. 0.6 normalized height for snow start
    uniform float snowEnd;         // e.g. 0.8
    uniform float rimPower;        // e.g. 3.0 for fresnel strength
    uniform float rimExtend;       // e.g. 0.3 normalized extend down on edges
    uniform float heightScale;     // 50.0 from terrain
    uniform float edgeMin;         // e.g. 0.01 lower threshold for edge detection
    uniform float edgeMax;         // e.g. 0.05 upper threshold for edge detection

    // Manual shadow uniforms
    uniform float shadowBias;
    uniform float shadowRadius;
    uniform float shadowRes;
    uniform sampler2D sunShadowMap;
    uniform mat4 sunShadowMatrix;
    varying vec4 vSunShadowCoord;
    uniform int shadowOn;
    uniform float alphaThreshold;
    
    uniform float rimShineStrength;
    uniform vec3 rimShineColor;

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

    void main() {
        vec4 baseAlbedoFull = texture2D(map, vUv);
        vec3 baseAlbedo = baseAlbedoFull.rgb;
    
        // Geometric normal only for rim/fresnel (stable, no feedback loop)
        vec3 geoNormal = normalize(vWorldNormal);

        // View dir (normalized already)
        vec3 viewDir = normalize(cameraPosition - vWorldPos);

        // snow rim
        // Optional: Fresnel rim using geometric normal (if you want view boost on edges)
        float ndotv = max(0.0, dot(geoNormal, viewDir));
        float rim = pow(1.0 - ndotv, rimPower);

        // Hard edge detection (view-independent curvature via screen-space derivatives)
        vec3 dNx = dFdx(vWorldNormal);
        vec3 dNy = dFdy(vWorldNormal);
        float edgeStrength = length(dNx) + length(dNy);  // Simple sum of magnitudes
        edgeStrength = smoothstep(edgeMin, edgeMax, edgeStrength);  // Normalize to 0-1, tune min/max

        // Optional: Boost with rim for hybrid (stronger from sides)
        edgeStrength *= rim;

        // Extend snow downward on detected edges
        float extend = edgeStrength * rimExtend;

        // Effective thresholds (lower on edges)
        float effectiveStart = snowStart - extend;
        float effectiveEnd = snowEnd - extend;

        // World height normalized (like terrain vHeight)
        float heightNorm = vWorldPos.y / heightScale;

        // Snow factor
        float snowFactor = smoothstep(effectiveStart, effectiveEnd, heightNorm);

        // Now mix textures/normals/rough AFTER deciding snowFactor
        vec3 albedo = mix(baseAlbedo, texture2D(snowTex, vUv).rgb, snowFactor);
        
        vec3 baseNormTangent   = texture2D(normalMap,   vUv).rgb * 2.0 - 1.0;
        vec3 snowNormTangent   = texture2D(normalSnow,  vUv).rgb * 2.0 - 1.0;
        vec3 mixedNormTangent  = mix(baseNormTangent, snowNormTangent, snowFactor);

        // 2. Flip the channels you need
        mixedNormTangent.x *= -1.0; // Flip horizontal (Red)
        //mixedNormTangent.y *= -1.0; // Flip vertical (Green) - MOST COMMON FIX
        
        // Final normal = geometric + tangent-space bump (now safe)
        vec3 finalNormal = normalize(vWorldNormal + mixedNormTangent * 1.0);  // strength 0.8, tune
        
        float baseRough = texture2D(roughnessMap, vUv).r;
        float snowRough = texture2D(roughSnow,    vUv).r;
        float rough     = mix(baseRough, snowRough, snowFactor);

        float ao = 1.0;
        float metal = 0.0;
        
        float zeron = smoothstep(0.0, 0.2, abs((daytime * 2.0) - 1.0)); // 1 0 1
        //float mixn = (1.0 - ((daytime * 0.5) + 0.5)) * zeron; // 0 - 0.5 // night half
        //float mixt = ((daytime * 0.5) + 0.5) * zeron; // 0.5 - 1 // day half
        
        // Force a dip at the horizon (0.5)
float transitionDip = smoothstep(0.0, 0.2, abs(daytime - 0.5) * 2.0);

        // https://gemini.google.com/share/dd85a5f93d28 
        // Normalize daytime so 0.5 (horizon) is our new "zero" for these calculations
float sunHeight  = max(0.0, (daytime - 0.5) * 2.0);  // 0.5 to 1.0 becomes 0.0 to 1.0
float moonHeight = max(0.0, (0.5 - daytime) * 2.0);  // 0.5 to 0.0 becomes 0.0 to 1.0

// Use power to create a curve.
// Higher power = longer transition/softer start.
float sunCurve  = pow(sunHeight, 2.0);
float moonCurve = pow(moonHeight, 2.0);

// Independent Intensity Scales
float sunIntensity  = 1.0;  // Full brightness at noon
float moonIntensity = 0.3;  // Moon is much dimmer

float mixt = sunCurve  * sunIntensity  * transitionDip;
float mixn = moonCurve * moonIntensity * transitionDip;

if (daytime > 0.5) {
    // DAY PHASE: Stretch the transition
    // We use a lower power (e.g., 1.5) to let light kick in faster but stay smooth
    mixt = pow((daytime - 0.5) * 2.0, 1.5) * 1.0;
    mixn = 0.0;
} else {
    // NIGHT PHASE: Sharper or shorter transition
    mixt = 0.0;
    mixn = pow((0.5 - daytime) * 2.0, 2.5) * 0.3;
}

        vec3 lightDir2Mod = vec3(-lightDir2.x, -lightDir2.y, -lightDir2.z);

        // Diffuse (sun + moon)
        float diff     = max(dot(lightDir,   finalNormal), 0.0) * ao * mixt;
        float diffMoon = max(dot(lightDir2Mod, finalNormal), 0.0) * ao * mixn;
        
        // Specular (Blinn-Phong)
        vec3 halfway     = normalize(lightDir   + viewDir);
        vec3 halfwayMoon = normalize(lightDir2Mod + viewDir);
        float spec     = pow(max(dot(finalNormal, halfway),     0.0), 32.0) * (rough) * (0.04 + metal) * mixt;
        float specMoon = pow(max(dot(finalNormal, halfwayMoon), 0.0), 32.0) * (rough) * (0.04 + metal) * mixn;

        // ────────────────────────────────────────────────
        // NEW: View-dependent Fresnel rim (subtle shine on edges)
        
        float NdotV_geo   = max(0.0, dot(normalize(vWorldNormal), viewDir));
        float NdotV_bump  = max(0.0, dot(finalNormal,        viewDir));
        
        float NdotV = mix(NdotV_geo, NdotV_bump, 0.25); // ← tune 0.0 to 0.4
        
        float fresnel = pow(1.0 - NdotV, 3.0);          // or 4.0 for sharper
        
        float rimRoughnessMod = mix(0.6, 1.4, rough);   // smoother = more rim
        float rimStrengthNight = mix(0.5, 1.8, mixn);   // stronger at night
        float rimShine = fresnel * rimShineStrength * rimRoughnessMod * rimStrengthNight;
        
        vec3 color = albedo.rgb * max(diff + diffMoon, 0.3) * 2.8 
                   + vec3(spec + specMoon)
                   + rimShineColor * rimShine;
        // ────────────────────────────────────────────────

        float ndotl=max(dot(finalNormal, -lightDir), 0.01);
        float slopeFactor = sqrt(1.0 - ndotl * ndotl) / ndotl; // tan(acos(ndotl)); // webgl fast

        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;
            
            // ndc replaced with shadowBiasMatrix in js
            //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)) {
                float shadowDepth = texture(sunShadowMap, shadowCoord.xy).r;
                //float baseBias = 0.0002;
                float baseBias = shadowBias;
                float slopeBias = 0.000005 * slopeFactor; // Add more bias on steeper angles
                float bias = baseBias + slopeBias;
                shadow = (shadowCoord.z - bias) > shadowDepth ? 0.0 : 1.0;

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

                    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)
                    );

                    shadow = 0.0;  // Reset to accumulate lit
                    for (int i = 0; i < numSamples; i++) {
                        vec2 offset = poissonDisk[i] * shadowRadius * texelSize;
                        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

            shadow *= zeron;

            // MOUNTAIN
            float shadowStrength = (daytime * .2) + 0.4; // .4 to .6
            float ambianceStrength = ((1.0 - daytime) * .6) + .6; // .6 to .4
            color *= (shadow * shadowStrength) + ambianceStrength; // shadow factor + ambient day
        } // shadowOn>=1
        else { color*=1.2; }

        if (baseAlbedoFull.a < alphaThreshold) discard; // Clip transparent pixels (no blending, but depth sorting works)

        // 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), baseAlbedoFull.a);
        } else {
            gl_FragColor = vec4(color, baseAlbedoFull.a);
        }
        //gl_FragColor = vec4(rough, rough, rough, 1.0);
        //gl_FragColor = vec4(vViewNormal, 1.0);
        //gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // red debug
        //gl_FragColor = vec4(vec3(snowFactor), 1.0); // white = full snow
        //gl_FragColor = vec4(vec3(rim), 1.0);        // white = strong rim/edge
        //gl_FragColor = vec4(geoNormal * 0.5 + 0.5, 1.0); // visualize geometric normals
    }
`;

const vertexShaderImage = `
    varying vec2 vUv;
    varying vec3 vViewPosition;
    varying vec3 vNormal;
    
    ${THREE.ShaderChunk['common']}
    ${THREE.ShaderChunk['fog_pars_vertex']}
    void main() {
        vUv = uv;
        vNormal = normal;
        vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
        vViewPosition = -mvPosition.xyz;
        gl_Position = projectionMatrix * mvPosition;
    }
`;
const fragmentShaderImage = `
    precision highp float;

    uniform sampler2D tDiffuse;
    uniform vec3 uColor; // Custom tint
    uniform float uOpacity;
    varying vec3 vNormal;
    uniform float daytime; // 1 0 1
    
    varying vec2 vUv;
    
    #include <fog_pars_fragment>

    void main() {
        vec4 texColor = texture2D(tDiffuse, vUv);
    
        // If the plane is double-sided, this ensures lighting/normals 
        vec3 normal = vNormal * (gl_FrontFacing ? -1.0 : 1.0);

float zeron = smoothstep(0.0, 0.2, abs((daytime * 2.0) - 1.0)); // 1 0 1
        float mixn = (1.0 - ((daytime * 0.5) + 0.5)); // 0 - 0.5 // night half
        float mixt = ((daytime * .5) + .5); // 0.5 - 1 // day half
        //float mixn = (1.0 - daytime) + .5; // 1 - 0 // night half
        //float mixt = (daytime * .5) + .5; // 1 - 0.5 // day half
        
        // Calculate the 'Ambient' light of the world
        // We start with a base neutral grey so it's never pitch black
        vec3 ambientLight = vec3(0.1, 0.1, 0.15);
        
        // Add the sun and moon contributions
        //vec3 dayTint = vec3(1.0, 0.9, 0.8) * mixt;   // Warm sun
        //vec3 nightTint = vec3(0.2, 0.4, 0.8) * mixn; // Cool blue moon
        vec3 dayTint = vec3(1.0, 0.9, 0.8) * mixt;   // Warm sun
        vec3 nightTint = vec3(0.5, 0.7, 1.0) * mixn; // Cool blue moon
        
        vec3 finalTint = (ambientLight + dayTint + nightTint);
        
        // Apply to your diffuse
        // 1. Apply a base color and tint or variation
        vec3 diffuseColor = texColor.rgb * uColor * finalTint;

        // 2. Manual Alpha Discard (Crucial for unlit planes in 3D space)
        if (texColor.a < 0.05) discard;

        vec4 outgoingLight = vec4(diffuseColor, texColor.a * uOpacity);

        // 3. Apply Fog (so the image fades into the distance)
        #include <fog_fragment>

        gl_FragColor = outgoingLight;
        //gl_FragColor = vec4(daytime,daytime,daytime,1.0);
    } 
`;

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

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

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

    void main() {
        vUv = uv;
        vObjectNormal = 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)
        vec3 clockOffset = normal * clockShadowNormalBias; // 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);
        vec4 clockOffsetWorldPos = modelMatrix * vec4(position + clockOffset, 1.0);
        
        if(shadowOn >= 1) {
          vSunShadowCoord = sunShadowMatrix * offsetWorldPos;
          // Repeat for moon with another offsetWorldPos if separate bias, but same for now
        }
        vClockShadowCoord = clockShadowMatrix * clockOffsetWorldPos; // sundial always on

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

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

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

    // Manual shadow uniforms
    uniform float shadowBias;
    uniform float clockShadowBias;
    uniform float shadowRadius;
    uniform float clockShadowRadius;
    uniform float shadowRes;
    uniform float clockShadowRes;
    varying vec4 vSunShadowCoord;
    varying vec4 vClockShadowCoord;
    uniform sampler2D sunShadowMap;
    uniform sampler2D clockShadowMap;
    uniform int shadowOn;
    uniform float alphaThreshold;

    uniform float rimShineStrength;
    uniform vec3 rimShineColor;

    varying vec2 vUv;
    varying vec3 vWorldNormal;
    varying vec3 vObjectNormal; // old vNormal but vNormal from twily include skinned mesh is view-space
    varying vec3 vWorldPos;
    //varying float vFogDepth;

    uniform float ambientMulti;
    uniform float normalStrength;

    void main() {
        vec4 albedo = texture(map, vUv);

        // 1. Convert the atlas UV back into a clean 0.0 to 1.0 texture coordinate space
        // fract(vUv * 4.0) isolates the local position inside any 4x4 grid quadrant
        vec2 sharedUv = fract(vUv * 4.0);
    
        // 2. Sample the shared map using the corrected coordinate layout
        float rough = texture2D(roughnessMap, sharedUv).g;
        float metal = texture2D(roughnessMap, sharedUv).b;

        vec4 emissiveColor = texture2D(emissiveMap, vUv);
        vec3 totalEmissiveRadiance = emissiveColor.rgb * emissive;

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

        // 2. Flip the channels you need
        //normalTex.x *= -1.0; // Flip horizontal (Red)
        //normalTex.y *= -1.0; // Flip vertical (Green) - MOST COMMON FIX
        // google ai solved my final dual shadow issues https://share.google/aimode/OfmzmnqFXiKydjrBE

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

        vec3 viewDir = normalize(cameraPosition - vWorldPos);

        float ao=1.0;
        //float metal=0.0;
        
        float zeron=smoothstep(0.0,0.2,abs((daytime*2.0)-1.0)); // 1 0 1
        float mixn=(1.0-((daytime*0.5)+0.5))*zeron; // 0 - 0.5 // night half
        float mixt=((daytime*0.5)+0.5)*zeron; // 0.5 - 1 // day half

        vec3 lightDir2Mod=vec3(-lightDir2.x,-lightDir2.y,-lightDir2.z);

        // Diffuse (sun + moon)
        float diff     = max(dot(lightDir,   finalNormal), 0.0) * ao * mixt;
        float diffMoon = max(dot(lightDir2Mod, finalNormal), 0.0) * ao * mixn;

        // ──────────────────────────────────────────────────────
        // STANDARD SPECULAR (BLINN-PHONG)
        // ──────────────────────────────────────────────────────
        vec3 halfway     = normalize(lightDir   + viewDir);
        vec3 halfwayMoon = normalize(lightDir2Mod + viewDir);
        
        // Base specular highlights
        float specDayBase  = pow(max(dot(finalNormal, halfway),     0.0), 64.0); // Higher power for tighter glare glint
        float specMoonBase = pow(max(dot(finalNormal, halfwayMoon), 0.0), 32.0);
        
        float spec     = specDayBase  * rough * (0.04 + metal) * mixt;
        float specMoon = specMoonBase * rough * (0.04 + metal) * mixn;

        // ──────────────────────────────────────────────────────
        // DIRECTIONAL FRESNEL RIM (BACKLIGHT/RIM GLOW)
        // ──────────────────────────────────────────────────────
        float NdotV_geo   = max(0.0, dot(normalize(vObjectNormal), viewDir));
        float NdotV_bump  = max(0.0, dot(finalNormal,            viewDir));
        float NdotV       = mix(NdotV_geo, NdotV_bump, 0.6);

        // Base edge ring intensity
        float fresnel     = pow(max(1.0 - NdotV, 0.0), 3.0);

        // Alignment vectors for directional backlighting
        // We look for where the view directory aligns with the outgoing light path
        float rimAlignmentDay   = max(dot(-lightDir, viewDir), 0.0);
        float rimAlignmentNight = max(dot(-lightDir2Mod, viewDir), 0.0);

        // Sharpen the alignment window so it concentrates specifically on back edges
        float directionalMaskDay   = pow(rimAlignmentDay, 2.0);
        float directionalMaskNight = pow(rimAlignmentNight, 2.0);

        // Assemble directional weights linked directly to active environmental time blocks
        float dynamicDirectionalRim = (directionalMaskDay * mixt) + (directionalMaskNight * mixn);

        // Environmental multipliers
        float rimRoughnessMod  = mix(0.6, 1.4, rough);
        float rimStrengthNight = mix(0.5, 1.8, mixn);

        // Final combined rim value masked by the directional vectors
        float rimShine = fresnel * rimShineStrength * rimRoughnessMod * rimStrengthNight * dynamicDirectionalRim;

        // ──────────────────────────────────────────────────────
        // NEW: FAKE METALLIC SUN/MOON GLARE (SPECULAR BOOST)
        // ──────────────────────────────────────────────────────
        // Compute the reflection vector of the sun/moon light off the face normal
        vec3 reflectDirDay  = reflect(-lightDir, finalNormal);
        vec3 reflectDirMoon = reflect(-lightDir2Mod, finalNormal);
        //reflectDirDay.z=reflectDirDay.z;
        reflectDirMoon.z=-reflectDirMoon.z;

        // Check how closely the camera view vector lines up with the reflection angle
        float glareFactorDay  = max(dot(reflectDirDay, viewDir), 0.0) * 1.02;
        float glareFactorMoon = max(dot(reflectDirMoon, viewDir), 0.0) * 1.0;

        // Sharpen the reflection heavily so it only catches at the perfect viewing angle
        // True metals have intensely concentrated reflection highlights
        float metallicGlareDay  = pow(glareFactorDay, 128.0) * metal * (1.0 - rough) * mixt * 5.5;
        float metallicGlareMoon = pow(glareFactorMoon, 64.0) * metal * (1.0 - rough) * mixn * 1.5;

        // Metallic reflection rule: Gold/Bronze metals tint their reflections with their own color
        vec3 sunGlareColor  = mix(vec3(1.0, 0.95, 0.85), albedo.rgb, metal) * metallicGlareDay;
        vec3 moonGlareColor = mix(vec3(0.7, 0.85, 1.0), albedo.rgb, metal) * metallicGlareMoon;
        vec3 totalMetallicGlare = sunGlareColor + moonGlareColor;

        // ──────────────────────────────────────────────────────
        // COMPOSITING THE FINAL PASS (WITH BALANCED SUNLIGHT INTENSITY)
        // ──────────────────────────────────────────────────────
        // Boosted base diffuse multiplier slightly to counter the early morning sun angle dimness
        vec3 diffuseComposition = albedo.rgb * max(diff + diffMoon, 0.25) * 4.2;
        vec3 standardSpecular   = vec3(spec + specMoon);
        vec3 directionalRim     = rimShineColor * rimShine;

        // Add the new glare step straight on top of the composition matrix
        vec3 color = diffuseComposition + standardSpecular + directionalRim + totalMetallicGlare;

        float shadow = 1.0; // no shadow
        if(shadowOn>=1) {
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)
);

        float ndotl=max(dot(finalNormal, -lightDir), 0.01); 
        float slopeFactor = sqrt(1.0 - ndotl * ndotl) / ndotl; // tan(acos(ndotl)); // webgl fast

        // Manual shadow sampling for sun
        shadow=abs((daytime*2.0)-1.0); // 1 0 1 0 = 0.5 0 0.5 1 ~;

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

        float clockShadow = 1.0;
        if (clockShadowCoord.x >= 0.0 && clockShadowCoord.x <= 1.0 &&
            clockShadowCoord.y >= 0.0 && clockShadowCoord.y <= 1.0 &&
            clockShadowCoord.z >= 0.0 && clockShadowCoord.z <= 1.0) {

            float clockShadowDepth=0.0;
            float clockBias=0.0;
            clockShadowDepth = unpackRGBAToDepth(texture(clockShadowMap, clockShadowCoord.xy));
            float biasMultiplier = 0.0001;
            clockBias = clockShadowBias + biasMultiplier * slopeFactor;
            //clockBias = clamp(clockBias, 0.0001, 0.001); // cap to prevent extreme swaps/full cover
            // Reduce the multiplier and clamp the factor to avoid extreme jumps
            //clockBias = clockShadowBias + clamp(0.0001 * slopeFactor, 0.0, 0.005);
            clockShadow = clockShadowCoord.z > clockShadowDepth + clockBias ? 0.0 : 1.0;

if(shadowOn<=2) { // dither shadows
    vec2 clockTexelSize = 1.0 / vec2(clockShadowRes, clockShadowRes); // match mapSize

    // Add to fragmentS)hader uniforms or defines
    const int clockNumSamples = 16;
    
    // Dynamically shrink radius based on distance to light for sharper contact
    float distToLight = clockShadowCoord.z; // Depth in light space
    float adaptiveRadius = clockShadowRadius * clamp(distToLight * 0.5, 0.2, 1.0);
    
    // Prefetch rotation to avoid redundant trig inside the loop
    float clockAngle = fract(sin(dot(clockShadowCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) * 6.2832;
    float s = sin(clockAngle);
    float c = cos(clockAngle);
    mat2 rotationMat = mat2(c, -s, s, c);
    
    // In the loop:
    clockShadow = 0.0;  // Critical: Reset to accumulate lit
    for (int i = 0; i < clockNumSamples; i++) {
        // Apply precomputed rotation matrix for speed
        vec2 rotatedOffset = rotationMat * poissonDisk[i];
        //vec2 finalOffset = rotatedOffset * clockShadowRadius * clockTexelSize;
        vec2 finalOffset = rotatedOffset * adaptiveRadius * clockTexelSize;
    
        float cd = unpackRGBAToDepth(texture(clockShadowMap, clockShadowCoord.xy + finalOffset));
        // Branchless accumulation: sum += float(test) avoids 'if' overhead
        clockShadow += (clockShadowCoord.z > cd + clockBias) ? 0.0 : 1.0;
    }
    clockShadow /= float(clockNumSamples);
} // dithering clockshadows <=2

            // Optional: Add a small random dither to break up remaining patterns
            //float dither = fract(sin(dot(clockShadowCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) * 0.001;
            //clockShadow += dither - 0.5 * 0.001; // subtle variation
            //clockShadow = clamp(clockShadow, 0.0, 1.0);
        } // clockShadowCoords<>

        //shadow = min(shadow, clockShadow); // or multiply for stronger effect
        shadow = clockShadow;

        //if(shadowOn>=1) {
        vec4 shadowCoord = vSunShadowCoord / vSunShadowCoord.w;
        
        //shadowCoord = shadowCoord * 0.5 + 0.5; // NDC to [0,1]

        float sunShadow = 1.0;
        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 + 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;
        sunShadow = shadowCoord.z > shadowDepth + bias ? 0.0 : 1.0;

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

    // Add to fragmentShader uniforms or defines
    const int numSamples = 16;
    
    // Prefetch rotation to avoid redundant trig inside the loop
    float angle = fract(sin(dot(shadowCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) * 6.2832;
    float s2 = sin(angle);
    float c2 = cos(angle);
    mat2 rotationMat2 = mat2(c2, -s2, s2, c2);
    
    // In the loop:
    sunShadow = 0.0;  // Critical: Reset to accumulate lit
    for (int i = 0; i < numSamples; i++) {
        // Apply precomputed rotation matrix for speed
        vec2 rotatedOffset2 = rotationMat2 * poissonDisk[i];
        vec2 finalOffset2 = rotatedOffset2 * shadowRadius * texelSize;
        //vec2 finalOffset2 = rotatedOffset2 * adaptiveRadius * texelSize;
    
        float d = unpackRGBAToDepth(texture(sunShadowMap, shadowCoord.xy + finalOffset2));
        // Branchless accumulation: sum += float(test) avoids 'if' overhead
        sunShadow += (shadowCoord.z > d + bias) ? 0.0 : 1.0;
    }
    sunShadow /= float(numSamples);
} // dithering shadows <=2

        // COIN
        // Blend with main shadow (or use only clockShadow for the clock)
        //shadow = min(1.0-(sunShadow * .5), clockShadow); // or multiply for stronger effect
        //shadow = (sunShadow + clockShadow) * .5;
        //shadow = (sunShadow * clockShadow);

        } // shadowCoords<>
        // Real shadow receive
        //float shadow = getShadow();               // from shadowmask chunk
            float sunEval=((sunShadow)*.9)+.1;
            sunShadow*=zeron;
            clockShadow*=zeron;

            // shadow strength .4 yo .6 = night to day
            // ambiance strength .6 to .4 = night to day
            float shadowStrength=(daytime*.2)+.4; // .4 to .6
            float ambianceStrength=((1.0-daytime)*.6)+.6*ambientMulti; // .6 to .4
            //color *= ((max(clockShadow*sunEval,0.0)) * shadowStrength) + ambianceStrength;           // shadow factor + ambient day
            shadow = min(sunShadow, clockShadow);
            color *= vec3((shadow * shadowStrength) + ambianceStrength);

        } else { // else( shadowOn<1 )
            // always shadow sundial
            shadow*=zeron;

            // shadow strength .4 yo .6 = night to day
            float shadowStrength=(daytime*.2)+.8; // .4 to .6
            float ambianceStrength=((1.0-daytime)*.2)+.6*ambientMulti; // .6 to .4
            color *= (shadow * shadowStrength) + ambianceStrength;           // shadow factor + ambient day
        }

        if (albedo.a < alphaThreshold) discard; // Clip transparent pixels (no blending, but depth sorting works)

        // 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>
        color+=totalEmissiveRadiance;

        if(shadowOn>=2) {
            gl_FragColor = vec4(vec3(shadow , 0.0, 1.0-shadow), albedo.a);
        } else {
            gl_FragColor = vec4(color, albedo.a);
        }
        //gl_FragColor = vec4(rough, rough, rough, 1.0);
        //gl_FragColor = vec4(finalNormal, 1.0);
        //gl_FragColor = vec4(halfway, 1.0);
        //gl_FragColor = vec4(normalTex, 1.0);
        //gl_FragColor = vec4(vWorldNormal, 1.0);

        //gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // red debug
    }
`;

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

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

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

    void main() {
        TexCoord = uv;
        vObjectNormal = 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)
        vec3 clockOffset = normal * clockShadowNormalBias; // 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);
        vec4 clockOffsetWorldPos = modelMatrix * vec4(position + clockOffset, 1.0);
        
        if(shadowOn >= 1) {
          vSunShadowCoord = sunShadowMatrix * offsetWorldPos;
          // Repeat for moon with another offsetWorldPos if separate bias, but same for now
        }
        vClockShadowCoord = clockShadowMatrix * clockOffsetWorldPos; // sundial always on

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

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

    uniform sampler2D map;
    uniform sampler2D normalMap;
    uniform sampler2D roughnessMap;
    uniform sampler2D emissiveMap;
    uniform vec3 emissive; // color
    uniform vec3 lightDir;
    uniform vec3 lightDir2;
    uniform float daytime; // 1 0 1
    //uniform float xtime;
    uniform float repeatScaleX;
    uniform float repeatScaleY;

    // Manual shadow uniforms
    uniform float shadowBias;
    uniform float clockShadowBias;
    uniform float shadowRadius;
    uniform float clockShadowRadius;
    uniform float shadowRes;
    uniform float clockShadowRes;
    varying vec4 vSunShadowCoord;
    varying vec4 vClockShadowCoord;
    uniform sampler2D sunShadowMap;
    uniform sampler2D clockShadowMap;
    uniform int shadowOn;
    uniform float alphaThreshold;
    uniform float roughness;
    uniform float metallic;
    uniform int transparent; // for clip or alpha
    uniform int metarough; // 0(none/values) 1(rough r) or 2(metarough bg)
    uniform int flipNormal; // 0 1(x) 2(y) or 3(xy)
    uniform int flatFace;

    uniform float rimShineStrength;
    uniform vec3 rimShineColor;

    varying vec2 TexCoord; // vUv
    varying vec3 vWorldNormal;
    varying vec3 vObjectNormal; // old vNormal but vNormal from twily include skinned mesh is view-space
    varying vec3 vWorldPos;
    //varying float vFogDepth;

    uniform float ambientMulti;
    uniform float normalStrength;

    void main() {
        vec2 vUv = vec2(TexCoord.x * repeatScaleX,TexCoord.y * repeatScaleY); // Scale UVs in vertex for repeating

        vec4 albedo = texture(map, vUv);

        float rough=roughness; // 0.5
        float metal=metallic; // 0.0
        if(metarough==1) {
            rough=texture2D(roughnessMap, vUv).r; // or b/w?
        } else if(metarough==2) {
            rough=texture2D(roughnessMap, vUv).g;
            metal = texture2D(roughnessMap, vUv).b;
        }

        vec4 emissiveColor = texture2D(emissiveMap, vUv);
        vec3 totalEmissiveRadiance = emissiveColor.rgb * emissive;

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

        // 2. Flip the channels you need
        if(flipNormal==1 || flipNormal==3) {
            normalTex.x *= -1.0; // Flip horizontal (Red)
        }
        if(flipNormal==2 || flipNormal==3) {
            normalTex.y *= -1.0; // Flip vertical (Green) - MOST COMMON FIX
        }
        // google ai solved my final dual shadow issues https://share.google/aimode/OfmzmnqFXiKydjrBE

        vec3 normal = vWorldNormal;
        if(flatFace==1) {
            normal = vWorldNormal * (gl_FrontFacing ? 1.0 : -1.0);
        }

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

        vec3 viewDir = normalize(cameraPosition - vWorldPos);

        float ao=1.0;
        
        float zeron=smoothstep(0.0,0.2,abs((daytime*2.0)-1.0)); // 1 0 1
        float mixn=(1.0-((daytime*0.5)+0.5))*zeron; // 0 - 0.5 // night half
        float mixt=((daytime*0.5)+0.5)*zeron; // 0.5 - 1 // day half

        vec3 lightDir2Mod=vec3(-lightDir2.x,-lightDir2.y,-lightDir2.z);

        // Diffuse (sun + moon)
        float diff     = max(dot(lightDir,   finalNormal), 0.0) * ao * mixt;
        float diffMoon = max(dot(lightDir2Mod, finalNormal), 0.0) * ao * mixn;

        // ──────────────────────────────────────────────────────
        // STANDARD SPECULAR (BLINN-PHONG)
        // ──────────────────────────────────────────────────────
        vec3 halfway     = normalize(lightDir   + viewDir);
        vec3 halfwayMoon = normalize(lightDir2Mod + viewDir);
        
        // Base specular highlights
        float specDayBase  = pow(max(dot(finalNormal, halfway),     0.0), 64.0); // Higher power for tighter glare glint
        float specMoonBase = pow(max(dot(finalNormal, halfwayMoon), 0.0), 32.0);
        
        float spec     = specDayBase  * rough * (0.04 + metal) * mixt;
        float specMoon = specMoonBase * rough * (0.04 + metal) * mixn;

        // ──────────────────────────────────────────────────────
        // DIRECTIONAL FRESNEL RIM (BACKLIGHT/RIM GLOW)
        // ──────────────────────────────────────────────────────
        float NdotV_geo   = max(0.0, dot(normalize(vObjectNormal), viewDir));
        float NdotV_bump  = max(0.0, dot(finalNormal,            viewDir));
        float NdotV       = mix(NdotV_geo, NdotV_bump, 0.6);

        // Base edge ring intensity
        float fresnel     = pow(max(1.0 - NdotV, 0.0), 3.0);

        // Alignment vectors for directional backlighting
        // We look for where the view directory aligns with the outgoing light path
        float rimAlignmentDay   = max(dot(-lightDir, viewDir), 0.0);
        float rimAlignmentNight = max(dot(-lightDir2Mod, viewDir), 0.0);

        // Sharpen the alignment window so it concentrates specifically on back edges
        float directionalMaskDay   = pow(rimAlignmentDay, 2.0);
        float directionalMaskNight = pow(rimAlignmentNight, 2.0);

        // Assemble directional weights linked directly to active environmental time blocks
        float dynamicDirectionalRim = (directionalMaskDay * mixt) + (directionalMaskNight * mixn);

        // Environmental multipliers
        float rimRoughnessMod  = mix(0.6, 1.4, rough);
        float rimStrengthNight = mix(0.5, 1.8, mixn);

        // Final combined rim value masked by the directional vectors
        float rimShine = fresnel * rimShineStrength * rimRoughnessMod * rimStrengthNight * dynamicDirectionalRim;

        // ──────────────────────────────────────────────────────
        // NEW: FAKE METALLIC SUN/MOON GLARE (SPECULAR BOOST)
        // ──────────────────────────────────────────────────────
        // Compute the reflection vector of the sun/moon light off the face normal
        vec3 reflectDirDay  = reflect(-lightDir, finalNormal);
        vec3 reflectDirMoon = reflect(-lightDir2Mod, finalNormal);
        //reflectDirDay.z=reflectDirDay.z;
        reflectDirMoon.z=-reflectDirMoon.z;

        // Check how closely the camera view vector lines up with the reflection angle
        float glareFactorDay  = max(dot(reflectDirDay, viewDir), 0.0) * 1.02;
        float glareFactorMoon = max(dot(reflectDirMoon, viewDir), 0.0) * 1.0;

        // Sharpen the reflection heavily so it only catches at the perfect viewing angle
        // True metals have intensely concentrated reflection highlights
        float metallicGlareDay  = pow(glareFactorDay, 128.0) * metal * (1.0 - rough) * mixt * 5.5;
        float metallicGlareMoon = pow(glareFactorMoon, 64.0) * metal * (1.0 - rough) * mixn * 1.5;

        // Metallic reflection rule: Gold/Bronze metals tint their reflections with their own color
        vec3 sunGlareColor  = mix(vec3(1.0, 0.95, 0.85), albedo.rgb, metal) * metallicGlareDay;
        vec3 moonGlareColor = mix(vec3(0.7, 0.85, 1.0), albedo.rgb, metal) * metallicGlareMoon;
        vec3 totalMetallicGlare = sunGlareColor + moonGlareColor;

        // ──────────────────────────────────────────────────────
        // COMPOSITING THE FINAL PASS (WITH BALANCED SUNLIGHT INTENSITY)
        // ──────────────────────────────────────────────────────
        // Boosted base diffuse multiplier slightly to counter the early morning sun angle dimness
        vec3 diffuseComposition = albedo.rgb * max(diff + diffMoon, 0.25) * 4.2;
        vec3 standardSpecular   = vec3(spec + specMoon);
        vec3 directionalRim     = rimShineColor * rimShine;

        // Add the new glare step straight on top of the composition matrix
        vec3 color = diffuseComposition + standardSpecular + directionalRim + totalMetallicGlare;

        float shadow = 1.0; // no shadow
        if(shadowOn>=1) {
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)
);

        float ndotl=max(dot(finalNormal, -lightDir), 0.01); 
        float slopeFactor = sqrt(1.0 - ndotl * ndotl) / ndotl; // tan(acos(ndotl)); // webgl fast

        // Manual shadow sampling for sun
        shadow=abs((daytime*2.0)-1.0); // 1 0 1 0 = 0.5 0 0.5 1 ~;

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

        float clockShadow = 1.0;
        if (clockShadowCoord.x >= 0.0 && clockShadowCoord.x <= 1.0 &&
            clockShadowCoord.y >= 0.0 && clockShadowCoord.y <= 1.0 &&
            clockShadowCoord.z >= 0.0 && clockShadowCoord.z <= 1.0) {

            float clockShadowDepth=0.0;
            float clockBias=0.0;
            clockShadowDepth = unpackRGBAToDepth(texture(clockShadowMap, clockShadowCoord.xy));
            float biasMultiplier = 0.0001;
            clockBias = clockShadowBias + biasMultiplier * slopeFactor;
            //clockBias = clamp(clockBias, 0.0001, 0.001); // cap to prevent extreme swaps/full cover
            // Reduce the multiplier and clamp the factor to avoid extreme jumps
            //clockBias = clockShadowBias + clamp(0.0001 * slopeFactor, 0.0, 0.005);
            clockShadow = clockShadowCoord.z > clockShadowDepth + clockBias ? 0.0 : 1.0;

if(shadowOn<=2) { // dither shadows
    vec2 clockTexelSize = 1.0 / vec2(shadowRes, shadowRes); // match mapSize

    // Add to fragmentS)hader uniforms or defines
    const int clockNumSamples = 16;
    
    // Dynamically shrink radius based on distance to light for sharper contact
    float distToLight = clockShadowCoord.z; // Depth in light space
    float adaptiveRadius = clockShadowRadius * clamp(distToLight * 0.5, 0.2, 1.0);
    
    // Prefetch rotation to avoid redundant trig inside the loop
    float clockAngle = fract(sin(dot(clockShadowCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) * 6.2832;
    float s = sin(clockAngle);
    float c = cos(clockAngle);
    mat2 rotationMat = mat2(c, -s, s, c);
    
    // In the loop:
    clockShadow = 0.0;  // Critical: Reset to accumulate lit
    for (int i = 0; i < clockNumSamples; i++) {
        // Apply precomputed rotation matrix for speed
        vec2 rotatedOffset = rotationMat * poissonDisk[i];
        //vec2 finalOffset = rotatedOffset * clockShadowRadius * clockTexelSize;
        vec2 finalOffset = rotatedOffset * adaptiveRadius * clockTexelSize;
    
        float cd = unpackRGBAToDepth(texture(clockShadowMap, clockShadowCoord.xy + finalOffset));
        // Branchless accumulation: sum += float(test) avoids 'if' overhead
        clockShadow += (clockShadowCoord.z > cd + clockBias) ? 0.0 : 1.0;
    }
    clockShadow /= float(clockNumSamples);
} // dithering clockshadows <=2

            // Optional: Add a small random dither to break up remaining patterns
            //float dither = fract(sin(dot(clockShadowCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) * 0.001;
            //clockShadow += dither - 0.5 * 0.001; // subtle variation
            //clockShadow = clamp(clockShadow, 0.0, 1.0);
        } // clockShadowCoords<>

        //shadow = min(shadow, clockShadow); // or multiply for stronger effect
        shadow = clockShadow;

        //if(shadowOn>=1) {
        vec4 shadowCoord = vSunShadowCoord / vSunShadowCoord.w;
        
        //shadowCoord = shadowCoord * 0.5 + 0.5; // NDC to [0,1]

        float sunShadow = 1.0;
        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 + 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;
        sunShadow = shadowCoord.z > shadowDepth + bias ? 0.0 : 1.0;

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

    // Add to fragmentShader uniforms or defines
    const int numSamples = 16;
    
    // Prefetch rotation to avoid redundant trig inside the loop
    float angle = fract(sin(dot(shadowCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) * 6.2832;
    float s2 = sin(angle);
    float c2 = cos(angle);
    mat2 rotationMat2 = mat2(c2, -s2, s2, c2);
    
    // In the loop:
    sunShadow = 0.0;  // Critical: Reset to accumulate lit
    for (int i = 0; i < numSamples; i++) {
        // Apply precomputed rotation matrix for speed
        vec2 rotatedOffset2 = rotationMat2 * poissonDisk[i];
        vec2 finalOffset2 = rotatedOffset2 * shadowRadius * texelSize;
        //vec2 finalOffset2 = rotatedOffset2 * adaptiveRadius * texelSize;
    
        float d = unpackRGBAToDepth(texture(sunShadowMap, shadowCoord.xy + finalOffset2));
        // Branchless accumulation: sum += float(test) avoids 'if' overhead
        sunShadow += (shadowCoord.z > d + bias) ? 0.0 : 1.0;
    }
    sunShadow /= float(numSamples);
} // dithering shadows <=2

        // STANDARD
        // Blend with main shadow (or use only clockShadow for the clock)
        //shadow = min(1.0-(sunShadow * .5), clockShadow); // or multiply for stronger effect
        //shadow = (sunShadow + clockShadow) * .5;
        //shadow = (sunShadow * clockShadow);

        } // shadowCoords<>
        // Real shadow receive
        //float shadow = getShadow();               // from shadowmask chunk
            float sunEval=((sunShadow)*.9)+.1;
            sunShadow*=zeron;
            clockShadow*=zeron;

            // shadow strength .4 yo .6 = night to day
            // ambiance strength .6 to .4 = night to day
            float shadowStrength=(daytime*.2)+.4; // .4 to .6
            float ambianceStrength=((1.0-daytime)*.6)+.6*ambientMulti; // .6 to .4
            //color *= ((max(clockShadow*sunEval,0.0)) * shadowStrength) + ambianceStrength;           // shadow factor + ambient day
            shadow = min(sunShadow, clockShadow);
            color *= vec3((shadow * shadowStrength) + ambianceStrength);

        } else { // else( shadowOn<1 )
            // always shadow sundial
            shadow*=zeron;

            // shadow strength .4 yo .6 = night to day
            float shadowStrength=(daytime*.2)+.8; // .4 to .6
            float ambianceStrength=((1.0-daytime)*.2)+.6*ambientMulti; // .6 to .4
            color *= (shadow * shadowStrength) + ambianceStrength;           // shadow factor + ambient day
        }

        if (albedo.a < alphaThreshold && transparent == 0) discard; // Clip transparent pixels (no blending, but depth sorting works)

        // 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>
        color+=totalEmissiveRadiance;

        if(shadowOn>=2) {
            gl_FragColor = vec4(vec3(shadow , 0.0, 1.0-shadow), albedo.a);
        } else {
            gl_FragColor = vec4(color, albedo.a);
        }
        //gl_FragColor = vec4(rough, rough, rough, 1.0);
        //gl_FragColor = vec4(finalNormal, 1.0);
        //gl_FragColor = vec4(halfway, 1.0);
        //gl_FragColor = vec4(normalTex, 1.0);
        //gl_FragColor = vec4(vWorldNormal, 1.0);

        //gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // red debug
    }
`;

const vertexShaderPost=`
    varying vec2 vUv;
    varying vec3 vViewPosition;
    varying vec3 vNormal;
    
    varying vec3 FragPos;
    
    void main() {
        vUv = uv;
        vNormal = normal;
        FragPos = (modelMatrix * vec4(position, 1.0)).xyz;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        //vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
        //vViewPosition = -mvPosition.xyz;
        //gl_Position = projectionMatrix * mvPosition;
    }
`;
// alt version -- not active
const fragmentShaderPost=`
varying vec2 vUv;

uniform sampler2D screenTexture;
uniform sampler2D sceneDepthTex;  // Scene with EVERYTHING (including water)
uniform sampler2D waterDepthTex;  // Scene WITHOUT water (just background terrain/objects)
uniform sampler2D waterHeightTex; // Your displacement map

uniform mat4 invProjection;
uniform mat4 invView;
//uniform vec3 cameraPosition;
uniform float daytime;
uniform vec3 data; // x = time, y = wlev (base), z = hScale
uniform float tileSize;
uniform int rendMode;
uniform vec3 viewPos;
uniform vec3 playerOffset;
    
    varying vec3 FragPos;
// --- PRECISION WORLD RECONSTRUCTION ---
vec3 getWorldPosition(float rawDepth, vec2 uv) {
    float z = rawDepth * 2.0 - 1.0; 
    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;
}

// --- EXACT COPY OF THE WATER MESH UV TRANSFORM ---
vec2 getWaterUV(vec3 worldPos) {
    // Mirrors line 6607: vec2 heightUV = (worldCoord.xz + playerOffset.xz) * 0.00025;
    vec2 uv = (worldPos.xz + playerOffset.xz) * 0.00025;
    return clamp(uv, 0.0, 1.0);
}

void main() {
    vec2 uv = vUv;
    vec3 baseColor = texture(screenTexture, uv).rgb;

    float rawTerrainDepth = texture(sceneDepthTex, uv).r;
    float rawWaterDepth   = texture(waterDepthTex, uv).r;

    vec3 P_terrain = getWorldPosition(rawTerrainDepth, uv);
    vec3 P_water   = getWorldPosition(rawWaterDepth, uv);

    float wlev = data.y;     // e.g. -12.5
    float heightScale = data.z * 2.0;

    // --- EXACT RECONSTRUCTION OF PER-FRAGMENT WAVES ---
    float waveHeightAtCam     = texture(waterHeightTex, getWaterUV(viewPos)).r * heightScale + wlev;
    float waveHeightAtTerrain = texture(waterHeightTex, getWaterUV(P_terrain)).r * heightScale + wlev;

    // Check if a water fragment exists closer than the terrain backdrop
    bool isWaterInFront = rawWaterDepth < rawTerrainDepth && rawWaterDepth < 1.0;
    bool isCamSubmerged = viewPos.y < waveHeightAtCam;

    float waterSubmergedTravel = 0.0;
    float verticalDepthFactor = 0.0;
    vec3 debugView = vec3(0.0);

    if (isCamSubmerged) {
        if (isWaterInFront) {
            // Camera is UNDERWATER, looking up at the underside of a wave
            waterSubmergedTravel = distance(viewPos, P_water);
            verticalDepthFactor = max(0.0, waveHeightAtCam - viewPos.y);
            debugView = vec3(0.0, 0.0, 0.5); // Blue
        } else {
            // Camera is UNDERWATER, looking downward at submerged seabed terrain
            waterSubmergedTravel = distance(viewPos, P_terrain);
            verticalDepthFactor = max(0.0, waveHeightAtTerrain - P_terrain.y);
            debugView = vec3(0.0, 0.5, 0.0); // Green
        }
    } else {
        if (isWaterInFront) {
            // Camera is ABOVE water, looking down at a wave surface
            waterSubmergedTravel = distance(P_water, P_terrain);
            verticalDepthFactor = max(0.0, P_water.y - P_terrain.y);
            debugView = vec3(0.5, 0.0, 0.0); // Red
        } else {
            // Camera is ABOVE water, looking at dry land geometry
            waterSubmergedTravel = 0.0;
            verticalDepthFactor = 0.0;
            debugView = vec3(0.2, 0.2, 0.2); // Grey
        }
    }

    // --- APPLY EXPONENTIAL WATER FOG ---
    float dt = abs(daytime);
    vec3 waterFogDay   = vec3(0.02, 0.22, 0.28);
    vec3 waterFogNight = vec3(0.005, 0.015, 0.03);
    vec3 currentFogColor = mix(waterFogDay, waterFogNight, dt);

    float density = 0.025; 
    float fogFactor = 1.0 - exp(-waterSubmergedTravel * density);
    //float fogFactor = 1.0 - exp(viewPos.y * .0001);
    fogFactor = clamp(fogFactor, 0.0, 0.95);

    float lightAbsorption = exp(-verticalDepthFactor * 0.08);
    currentFogColor *= clamp(lightAbsorption, 0.15, 1.0);

    vec3 finalColor = mix(baseColor, currentFogColor, fogFactor);

    gl_FragColor = vec4(finalColor, 1.0);

    if (rendMode == 1) gl_FragColor = vec4(vec3(rawTerrainDepth), 1.0);
    if (rendMode == 2) gl_FragColor = vec4(vec3(rawWaterDepth), 1.0);
    if (rendMode == 4) gl_FragColor = vec4(debugView, 1.0);
}
`;
// post from twily plainC terrain opengl - currently active
const fragmentShaderPost2=`
    varying vec2 vUv;
    
    uniform sampler2D screenTexture;
    uniform sampler2D sceneDepthTex;
    uniform sampler2D waterDepthTex;
    uniform sampler2D waterHeightTex;
    
    //uniform vec3 cameraPosition;
    uniform mat4 invProjection;
    uniform mat4 invView;
    uniform float daytime;
    uniform vec3 data;
    uniform float tileSize;
    uniform int rendMode;
    uniform vec3 viewPos;
    uniform vec3 playerOffset;

    varying vec3 FragPos;

    float linearizeDepth(float depth) {
        float near=0.1f;
        float far=5000.0f;
    
        float z = depth * 2.0 - 1.0; // NDC
        return 2.0 * near * far / (far + near - z * (far - near));
    }
    vec3 getWorldPosition(float depth, vec2 uv, bool linearize) {
        float z = linearize ? linearizeDepth(depth) : (depth * 2.0 - 1.0);
        vec4 clipSpace = vec4(uv * 2.0 - 1.0, linearize ? depth : z, 1.0);
        vec4 viewSpace = invProjection * clipSpace;
        viewSpace /= viewSpace.w;
        vec4 worldSpace = invView * viewSpace;
        return worldSpace.xyz;
    }

    // Map world XZ positions perfectly to your isolated heightmap bounds
    vec2 getWaterUV(vec3 worldPos) {
       //--- float scale = tileSize;
       //--- vec2 uv = (worldPos.xz + scale * 0.5) / scale;
    
       //--- // If waves move backward relative to camera movement, uncomment one of these:
       //--- // uv.y = 1.0 - uv.y; // Invert Z-axis mapping
       //--- // uv.x = 1.0 - uv.x; // Invert X-axis mapping
    
       //--- return clamp(uv, 0.0, 1.0);
        vec2 uv = (worldPos.xz + playerOffset.xz) * 0.00025;
        return clamp(uv, 0.0, 1.0);
    }

    void main() {
        vec2 uv = vUv;
        //vec2 uv = FragCoord.xy;
        //vec2 uv = gl_FragCoord.xy / vec2(viewport_width,viewport_height);

        //ivec2 vpCoords = ivec2(gl_FragCoord.x, gl_FragCoord.y);

        vec3 color = texture(screenTexture, uv).rgb;
    
        float sceneDepth = texture(sceneDepthTex, uv).r;
        float waterDepth = texture(waterDepthTex, uv).r;

        float depthDiff = (sceneDepth - waterDepth)*1000.0;
    
        // Water height at this point
        float wlev = data.y;
        float hScale = data.z * 2.0;
        //float scale=4000.0;
        float scale=tileSize;
        vec2 worldMin = vec2(-scale / 2.0, -scale / 2.0);
        vec2 worldMax = vec2(scale / 2.0, scale / 2.0);
        
        vec3 P = getWorldPosition(sceneDepth, uv, false);

        //// Camera water height (more stable)
        vec2 waterUV = (viewPos.xz - worldMin) / (worldMax - worldMin);
        //vec2 waterUV = (P.xz - worldMin) / (worldMax - worldMin);
        waterUV = clamp(waterUV, 0.0, 1.0);

        float waterHeight = texture(waterHeightTex, getWaterUV(viewPos)).r * hScale + wlev;

        float camDiff = waterHeight - viewPos.y;
        //float camDiff = waterHeight - P.y;

        float distance = length(P - viewPos);
        //float distance = length(FragPos - viewPos);
        float dt=abs(1.0-daytime);

        float methodA=dt;
        float methodD=(methodA*2.0);
        float methodE=(methodA*2.0)-1.0;

        // day/night water fog colors  blend
        vec3 color1 = vec3(0.0,0.1,0.1); // day
        vec3 color2 = vec3(0.1,0.26,0.26);
        vec3 color3 = vec3(0.1,0.3,0.36); // <<day begin
        //
        vec3 color4 = vec3(0.05,0.04,0.08); // night
        vec3 color5 = vec3(0.05,0.08,0.11);
        vec3 color6 = vec3(0.07,0.10,0.14);
 
        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 fogColor = vec3(mix(ourColor,ourColor2,dt));

        vec3 underwaterColor = vec3(0.2, 0.6, 0.8) * (1.15-dt*1.45); // Tint

        //bool cameraUnder = cameraPosition.y < waterHeight - 0.4;   // bias to prevent early trigger
        //bool pointUnder  = P.y < waterHeight - 0.008;  // small bias for surface

        bool isCameraUnderwater = camDiff > 1.0;
        bool isLookingAtWaterSurface = depthDiff > 0.0 && waterDepth < 1.0; // Water in front and not at far plane


        //bool isCameraUnderwater = cameraUnder;
        //bool isCameraUnderwater = pointUnder;
        //bool isLookingAtWaterSurface = pointUnder;

        vec3 debug=vec3(0.0,0.0,0.0);
        float waterSurfaceFac = 0.0;
        float terrainSurfaceFac = 0.0;
        if(isCameraUnderwater) { // fully submerged only--
            if(isLookingAtWaterSurface) { // surface from below
                //if(stencilDiff < 0.0) { // stencil front of water
                //    color = mix(color,underwaterColor,1.0-stencilDepth);
                //    debug=vec3(0.5,0.0,0.0); // half red
                //} else { // water surface
                    debug=vec3(0.0,0.0,0.5); // half blue
                    waterSurfaceFac = 1.0;

                    //color = mix(fogColor, color, clamp(1.0 - (waterSurfaceFac * .3 +(distance*.01)),0.0,1.0)); // surface below
                    color *= clamp(1.0-(-P.y)*.001,0.2,1.0);
                //}
            } else { // underwater render
                debug=vec3(0.0,0.25,0.0); // quart green
                terrainSurfaceFac = 1.0;

                color = mix(fogColor, color, clamp(1.0 - (terrainSurfaceFac * (1.0 - waterDepth * 100.0)),0.0,1.0)); // underwater fog
                color *= clamp(1.0-(-P.y)*.001,0.2,1.0);
                //color = vec3(1.0,0.0,0.0); // red
            }
        } else { // not entierly under water/above water
            //if(stencilDiff < 0.0) { // stencil front of water
            //    color = mix(color,underwaterColor,1.0-stencilDepth);
            //    debug=vec3(1.0,0.0,0.0); // red
            //} else {
                float epsilon = 0.0025; // Small threshold for blending
                float underwaterFactor = smoothstep(waterHeight - epsilon, waterHeight + epsilon, P.y);

                if(underwaterFactor <= 0.0) { // below water pixel
                    if(!isLookingAtWaterSurface) {
                        debug=vec3(0.0,0.5,0.0); // half green
                        terrainSurfaceFac = 1.0;

                        // the actual underwater fog here
                        // other lines marked //---- post sshould have press pass
                        color = mix(fogColor, color, clamp(1.0 - (terrainSurfaceFac * (1.0 - waterDepth * 100.0)),0.0,1.0)); // underwater fog
                        color *= clamp(1.0-(-P.y)*.001,0.2,1.0);
                        //color = vec3(0.5,0.0,0.0); dark red
                    } else {
                        debug=vec3(0.0,0.0,0.5); // half blue
                        waterSurfaceFac = 1.0;

                        //--color = mix(fogColor, color, clamp(1.0 - (waterSurfaceFac * .3 +(distance*.01)),0.0,1.0));
                        color *= clamp(1.0-(-P.y)*.001,0.2,1.0);
                    }
                } else { // above water pixel
                    if(!isLookingAtWaterSurface) {
                        debug=vec3(0.5,1.0,0.0); // double green
                        terrainSurfaceFac = .5;
                    } else {
                        debug=vec3(0.0,0.0,1.0); // blue
                        waterSurfaceFac = 1.0;
                        //color = vec3(1.0,0.0,0.0);

                        //color = mix(fogColor, color, clamp(1.0 - (waterSurfaceFac * .3 +(distance*.01)),0.0,1.0));
                        //color = mix(fogColor, color, clamp(1.0 - (waterSurfaceFac * (1.0 - waterDepth * 1000.0)),0.0,1.0));
                    }
                }
                //// Apply depth-based adjustment if needed
                //if (depthDiff/100 > 0.0) { // Water is in front
                //    underwaterFactor = min(underwaterFactor + depthDiff * 10.0, 1.0); // Fine-tune multiplier
                //}
            //} -- stencil
            //underwaterFactor = 1.0; // clear for debug
        }

        //// day/night terrain fog colors  blend
        //color1 = vec3(0.5,0.3,0.3); // day
        //color2 = vec3(0.3,0.2,0.3);
        //color3 = vec3(0.3,0.2,0.2); // <<day begin
        ////
        //color4 = vec3(0.3,0.0,0.02); // night
        //color5 = vec3(0.0,0.05,0.03);
        //color6 = vec3(0.0,0.0,0.03);

        //ourColor = vec3(mix(color3,color2,methodD));
        //ourColor = mix(ourColor,color1,methodE);
        //ourColor2 = vec3(mix(color5,color4,methodD));
        //ourColor2 = mix(ourColor2,color6,methodE);
        //vec3 terrainFogBlend = vec3(mix(ourColor,ourColor2,dt)) * (2.0-dt*4.0);

        //float terrainFogDist = 50.0;
        //float terrainFogFac = distance * terrainFogDist *.001;
        //terrainFogFac=clamp(terrainFogFac,0.0,0.1);
        
        // post color tune or neutral
        //color=vec3(color.r*1.5,color.g,color.b); // red boost
        //color=vec3(color.r,color.g,color.b*1.5); // blue boost
        gl_FragColor = vec4(color, 1.0);

        if(rendMode==1) {
            float single=(color.r+color.g+color.b)/3.0 * 2.0; // B/W
            float single2=0.0;
            float single3=0.0;
            //single2 *= sin(uv.x * 3.14f); // gradient sides
            single2 = depthDiff; // waterDepth - sceneDepth = green
            gl_FragColor = vec4(single,single2,single3,1.0);
        } else if(rendMode==2) {
            float single=(color.r+color.g+color.b)/3.0 * 2.0;
            float single2=0.0;
            float single3=0.0;

            single = depthDiff; // waterDepth - sceneDepth = red
            gl_FragColor = vec4(single,single2,single3,1.0);
        } else if(rendMode==3) {
        //    float single=(color.r+color.g+color.b)/3 * 2;
        //    float single2=0.0;
        //    float single3=0.0;

        //    single = 1.0 - stencilDiff; // sceneDepth - stencilDepth = red
        //    gl_FragColor = vec4(single,single2,single3,1.0);
            gl_FragColor = vec4(P*(sceneDepth*1000.0), 1.0);
            //gl_FragColor = vec4(viewPos*(sceneDepth*1000.0), 1.0);
            //gl_FragColor = vec4(distance*2.0, 0.0, 0.0, 1.0);
        } else if(rendMode==4) { // debug mode
            gl_FragColor = vec4(debug, 1.0);
        //} else { // 0 using underwater above
            //gl_FragColor = vec4(color,1.0);
        }
    }
`;


// additional to be added
// sunclock shader used as default shader for static objects in demo

// twily vertex + sunclock frag for animated skinned meshes

// tree shader for vegetation and simple 2d plane alpha clipping geometry

// fireworks shader for animated sprite images and opacity fade

//  end of custom shaders
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

        // 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;
        
        let helperGrids = false; // debug
        let helperGridsChk = { value: helperGrids };
        let helperRef=[null,null];
        function grid_toggle() {
            helperGrids=!helperGrids;
            helperGridsChk.value=helperGrids;
            set_check('chk_axis_grid',helperGrids);
            if(helperGrids) {
                const helper = new THREE.GridHelper(500, 500);
                helper.material.opacity = 0.25;
                helper.material.transparent = true;
                helper.layers.enable(0);
                scene.add(helper);
                const axis = new THREE.AxesHelper(1000);
                axis.layers.enable(0);
                scene.add(axis);
                helperRef[0]=helper;
                helperRef[1]=axis;
            } else {
                helperRef[0].material.dispose();
                scene.remove(helperRef[0]);
                scene.remove(helperRef[1]);
            }

            setModeStatus();
        }

        let helperLoaded=false;
        let clockHelper=null;
        let helper=null;
        function loadHelpers() {
            helperLoaded=true;
            // debug shadow camera2
            clockHelper = new THREE.CameraHelper(refList["clockShadowLight"].shadow.camera);
            clockHelper.layers.enable(0);
            scene.add(clockHelper);
            // debug shadow camera
            helper=new THREE.CameraHelper(refList["sunLight"].shadow.camera);
            helper.layers.enable(0);
            scene.add(helper);
        }
        function unloadHelpers() {
            helperLoaded=false;
            scene.remove(clockHelper);
            scene.remove(helper);
        }

        function setModeStatus() {
            let sMc="gmode";
            if((isLocked || useJoysticks) && !helperGrids) {
                $('statusMode').innerHTML="Game mode";
                $('statusMode_info').innerHTML="(Press \"g\" to enter edit mode)";
            } else {
                sMc="emode";
                $('statusMode').innerHTML="Edit mode";
                $('statusMode_info').innerHTML="(Press \"g\" to enter game mode)";

            }
            $('statusMode').className=sMc;
        }

        function ajaxSource(file="",cb) {
            var xhr;
            if(window.XMLHttpRequest) xhr=new XMLHttpRequest();
            else                      xhr=new ActiveXObject("Microsoft.XMLHTTP");
        
            var data;
            xhr.onreadystatechange=function() {
                if(xhr.readyState==4 && xhr.status==200) {
                    if(typeof cb==='function') {
                        cb(xhr.responseText);
                    }
                }
            }
        
            xhr.open("GET","./"+file,true);
            xhr.send(null);
        }
        var assetLoaded=[];
        var cointextures={diffuse:[],normal:[]}; 
        function readAssets() {
            ajaxSource("cointextures.txt",function(data) {
                //console.log(data);
                const list=data.split("\n");
                for(let i=0;i<list.length;i+=2) {
                    cointextures['diffuse'].push(list[i]);
                    cointextures['normal'].push(list[(i+1)]);
                    // shared metarough
                }
            });
        }
        //readAssets();

        // example to load one asset with multiple objects
        const loader = new GLTFLoader();
        function load_gltf() {
            loader.load('./models/model/model.gltf', (gltf) => {
                const model = gltf.scene;
            
                // Create an array to hold children to avoid issues
                // while modifying the scene graph during traversal
                const childrenToSeparate = [];
                model.traverse((child) => {
                    if (child.isMesh) {
                        childrenToSeparate.push(child);
                    }
                });
            
                // Move children to top-level scene, preserving world transform
                childrenToSeparate.forEach((mesh) => {
                    // Important: Attach preserves world position/rotation/scale
                    // when changing parents, unlike scene.add()
                    scene.attach(mesh);
            
                    // Now you can transform them individually
                    mesh.position.set(Math.random() * 10, 0, 0);
                });
            
                // Optional: Remove original container
                // scene.remove(model);
            });
        }
        // but for coins may want 1 geometry with different materials

        //var coinatlaspath="./textures/coinatlases/";
        var coinatlaspath="./textures/coinatlases/resized/";
        var coinmetarough="./models/coinmodel/vagina-coin3_metaroughpng_resized.webp";
        var coinatlases={
            diffuse: [
"diffuse_atlas_0.webp",
"diffuse_atlas_1.webp",
"diffuse_atlas_2.webp",
"diffuse_atlas_3.webp",
"diffuse_atlas_4.webp",
"diffuse_atlas_5.webp",
"diffuse_atlas_6.webp",
"diffuse_atlas_7.webp",
"diffuse_atlas_8.webp",
"diffuse_atlas_9.webp",
"diffuse_atlas_10.webp",
"diffuse_atlas_11.webp",
"diffuse_atlas_12.webp",
"diffuse_atlas_13.webp",
"diffuse_atlas_14.webp",
"diffuse_atlas_15.webp",
            ],
            normal: [
"normal_atlas_0.webp",
"normal_atlas_1.webp",
"normal_atlas_2.webp",
"normal_atlas_3.webp",
"normal_atlas_4.webp",
"normal_atlas_5.webp",
"normal_atlas_6.webp",
"normal_atlas_7.webp",
"normal_atlas_8.webp",
"normal_atlas_9.webp",
"normal_atlas_10.webp",
"normal_atlas_11.webp",
"normal_atlas_12.webp",
"normal_atlas_13.webp",
"normal_atlas_14.webp",
"normal_atlas_15.webp",
            ]
        };

        // Array to keep references of generated coins for later color/animation control
        var activeCoins = []; 
        var coinRef={
            coinMaterials: [],
            textureLoader: new THREE.TextureLoader(),
            metaRoughTex: null,
            baseMesh: null,
            spawnIndex: 0,
            totalCoins: 253,
            //totalCoins: 250,
            spawnReady: false,
            coinRadius: 0,
            coinHeight: 0,
        };

        function spawn_coin() {
            // 2. Define grid presentation layout variables
            const spacingX = 5.0;    // Step distance on X
            const spacingY = 7.88; // 0.12 to make spacing of 1 across 12 rows changing offset 48 to 47.5 to center coins
            const offsetX = -47.5;
            const offsetZ = -47.5;
            const columns = 20;     // Number of coins per row
            const baseGeometry = coinRef['baseMesh'].geometry;
            let i=coinRef['spawnIndex'];
            let t=i+columns;
            if(t>coinRef['totalCoins']) t=coinRef['totalCoins'];
            const posY=.125;
            coinRef['spawnReady']=false;

            //const i=coinRef['spawnIndex'];
            for(i;i<t;i++) {
        
                // Find which atlas (0-15) and which grid slot (0-15) inside that atlas this index uses
                const atlasIndex = Math.floor(i / 16);
                const slotIndex = i % 16;

                //const inverted1 = 15 - slotIndex; // Math approach
                //const inverted2 = ~slotIndex & 15; // Bitwise approach
        
                // Fallback safety barrier if totalCoins exceeds generated textures
                if (atlasIndex >= coinRef['coinMaterials'].length) return;
        
                // Generate unique UV adjusted geometry for this exact slot
                const instanceGeo = createGeometryForSlot(baseGeometry, slotIndex);
                
                // Build the distinct independent scene object
                const coinClone = new THREE.Mesh(instanceGeo, coinRef['coinMaterials'][atlasIndex]);
                
                coinClone.castShadow = true;
                coinClone.receiveShadow = true;
        
                // Compute orderly mathematical positions
                const posX = offsetX + (i % columns) * spacingX;
                const posZ = offsetZ + Math.floor(i / columns) * spacingY;
                
                coinClone.position.set(posX, posY, posZ);

                coinClone.scale.set(2.0, 2.0, 2.0);
                coinClone.layers.enable(2);
        
                // Add straight into runtime render sequence
                scene.add(coinClone);
        
                // Track reference for your future custom color pipelines
                const coinDat={
                    mesh: coinClone,
                    originalMaterial: coinRef['coinMaterials'][atlasIndex]
                };
                activeCoins.push(coinDat);

                //coinRef['spawnIndex']++;
                coinRef['spawnIndex']=i;
            }

            coinRef['spawnReady']=true;
        }
 
        function load_coin() {
            loader.load('./models/coinmodel/twilyheightcoin.gltf', (gltf) => {
                let baseMesh = null;
        
                // 1. Extract the base geometry from the source file
                gltf.scene.traverse((child) => {
                    if (child.isMesh && !baseMesh) {
                        baseMesh = child; 
                    }
                });
        
                if (!baseMesh) {
                    console.error("No mesh found inside coin GLTF!");
                    return;
                }
        
                //baseMesh.layers.enable(2);
                coinRef['baseMesh']=baseMesh;
                coinRef['spawnReady']=true;

                baseMesh.geometry.computeBoundingBox();
                const size = new THREE.Vector3();
                baseMesh.geometry.boundingBox.getSize(size);
                
                // Multiply by your scale factor (2.0)
                coinRef['coinRadius'] = (size.x / 2) * 2.0;
                coinRef['coinHeight'] = size.y * 2.0;

                // moved to spawn_coin by frame instead of big loop
                // 3. Main deployment loop 
                //for (let i = 0; i < totalCoins; i++) {
                //    // Find which atlas (0-15) and which grid slot (0-15) inside that atlas this index uses
                //    const atlasIndex = Math.floor(i / 16);
                //    const slotIndex = i % 16;
        
                //    // Fallback safety barrier if totalCoins exceeds generated textures
                //    if (atlasIndex >= coinRef['coinMaterials'].length) break;
        
                //    // Generate unique UV adjusted geometry for this exact slot
                //    const instanceGeo = createGeometryForSlot(baseGeometry, slotIndex);
                //    
                //    // Build the distinct independent scene object
                //    const coinClone = new THREE.Mesh(instanceGeo, coinRef['coinMaterials'][atlasIndex]);
                //    
                //    coinClone.castShadow = true;
                //    coinClone.receiveShadow = true;
        
                //    // Compute orderly mathematical positions
                //    const posX = (i % columns) * spacing;
                //    const posZ = Math.floor(i / columns) * spacing;
                //    
                //    coinClone.position.set(posX, 0, posZ);
        
                //    // Add straight into runtime render sequence
                //    scene.add(coinClone);
        
                //    // Track reference for your future custom color pipelines
                //    activeCoins.push(coinClone);
                //}
                
                // Template source model is purposefully skipped and never attached to scene
            });
        }

        // Function to clone your coin base geometry and map it to a specific slot (0 to 15)
        function createGeometryForSlot(baseGeometry, slotIndex) {
            const geo = baseGeometry.clone();
            const uvAttr = geo.attributes.uv;
            if (!uvAttr) return geo;
        
            const col = slotIndex % 4;
            //const row = Math.floor(slotIndex / 4);
            const row = 3 - Math.floor(slotIndex / 4);
        
            // 4x4 scale factor
            const uScale = 0.25;
            const vScale = 0.25;
        
            // Grid coordinates
            const uOffset = col * uScale;
            const vOffset = (3 - row) * vScale; // Flip Y for WebGL matching standard images
        
            for (let i = 0; i < uvAttr.count; i++) {
                let u = uvAttr.getX(i);
                let v = uvAttr.getY(i);
        
                u = u * uScale + uOffset;
                v = v * vScale + vOffset;
        
                uvAttr.setXY(i, u, v);
            }
            
            uvAttr.needsUpdate = true;
            return geo;
        }

        function createBeams() {
            const beamHeight=1000.0;
            const beamSpan=2.0;
            
            const textureLoader = new THREE.TextureLoader(manager);
            const beamDiffuse = textureLoader.load('./textures/beams2_double.webp');
            const beamMetarough = textureLoader.load('./textures/beams2_double_metarough.webp');
            const beamNormal = textureLoader.load('./textures/beams2_double_normal.webp');
            // repeat scale separatly handled with custom shader*
            [beamDiffuse, beamNormal, beamMetarough].forEach(tex => {
                tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
                tex.repeat.set(1, beamHeight * .25); // Adjust based on your scene scale
            });

            const beamGeo = new THREE.PlaneGeometry(2.0, beamHeight);
            //--const beamMat = new THREE.MeshStandardMaterial({
            //--    map: beamDiffuse,
            //--    normalMap: beamNormal,
            //--    roughnessMap: beamMetarough,
            //--    alphaTest: 0.5,
            //--    transparent: false, // false for cutout
            //--    side: THREE.DoubleSide,
            //--    // Set influence: x and y usually set to same value
            //--    normalScale: new THREE.Vector2(0.5, 0.5),
            //--    depthWrite: true,
            //--    //depthTest: false,
            //--});
            let beamMat=null;
            if(!refList['beamMaterial']) {
                beamMat = refList['standardMat'].clone();
                beamMat.uniforms.map.value=beamDiffuse;
                beamMat.uniforms.normalMap.value=beamNormal;
                beamMat.uniforms.roughnessMap.value=beamMetarough;
                beamMat.uniforms.metarough.value=2;
                beamMat.uniforms.transparent.value=0;
                beamMat.uniforms.repeatScaleX.value=1.0;
                beamMat.uniforms.repeatScaleY.value=beamHeight * .25;
                beamMat.side=THREE.DoubleSide;
                beamMat.transparent=false;
            
                refList['beamMaterial']=beamMat;
                refList['standardMatUpdate'].push(beamMat);
                beamMat.needsUpdate = true;
            } else {
                beamMat = refList['beamMaterial'];
            }

            let posX=1.0;
            let posZ=0.0;
            let rotY=Math.PI * .5;
            for (let i = 0; i < 4; i++) {
                const beamside = new THREE.Mesh(beamGeo, beamMat);
                // Set the beams to be rendered after the floor
                beamside.renderOrder=1;
                beamside.name="beamside"+i;
                beamside.castShadow = true;
                beamside.receiveShadow = true;
                //beamside.visible = false;
                //refList['railList'].push(rail);
                beamside.frustumCulled = false;
                beamside.layers.enable(1);
                beamside.layers.enable(2);

                switch(i) {
                    case 1:
                        posX=0.0
                        posZ=1.0
                        rotY-=Math.PI * .5; // +90deg
                        break;
                    case 2:
                        posX=-1.0
                        posZ=0.0
                        rotY-=Math.PI * .5; // +90deg
                        break;
                    case 3:
                        posX=0.0
                        posZ=-1.0
                        rotY-=Math.PI * .5; // +90deg
                        break;
                }

                beamside.rotation.y = rotY; // flipped normal
                beamside.position.set(posX, beamHeight * .5, posZ);

                refList['beamGroup'].add(beamside);
            }
            
            refList['beamGroup'].visible = false;
            //refList['terrainGroup'].add(refList['beamGroup']);

            let stepX=0;
            let stepZ=0;
            for(let i=0;i<12;i++) {
                if(stepZ==0 && stepX<3) {
                    stepX+=1;
                } else if(stepX==3 && stepZ<3) {
                    stepZ+=1;
                } else if(stepZ==3 && stepX>0) {
                    stepX-=1;
                } else if(stepX==0 && stepZ>0) {
                    stepZ-=1;
                }
                const x=(stepX * 32.0) - 48.0;
                const z=(stepZ * 32.0) - 48.0;

                const beamClone=refList['beamGroup'].clone();
                beamClone.position.set(x,beamClone.position.y-beamHeight,z);
                beamClone.frustumCulled = false;
                //beamClone.visible = true;
                refList['terrainGroup'].add(beamClone);
                refList['beamList'].push(beamClone);
            }
        }

        function createRailings() {
            const railHeight = 1.2; // 1.2 Standard height in meters
            const floorSize = 102;
            const edgeWidth = 1; // Your edgeWidth
            const totalHalfSize = (floorSize / 2) + edgeWidth; // 51 meters

            const textureLoader = new THREE.TextureLoader(manager);
            const railingDiffuse = textureLoader.load('./textures/railing2.webp');
            const railingMetarough = textureLoader.load('./textures/railing2_metarough.webp');
            const railingNormal = textureLoader.load('./textures/railing2_normal.webp');
            // repeat scale separatly handled with custom shader*
            [railingDiffuse, railingNormal, railingMetarough].forEach(tex => {
                tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
                tex.repeat.set(50, 1); // Adjust based on your scene scale
            });
        
            //const railGeo = new THREE.PlaneGeometry(floorSize, railHeight);
            const railGeoLong = new THREE.PlaneGeometry(floorSize + (edgeWidth * 2), railHeight);
            //--const railMat = new THREE.MeshStandardMaterial({
            //--    map: railingDiffuse,
            //--    normalMap: railingNormal,
            //--    roughnessMap: railingMetarough,
            //--    alphaTest: 0.5,
            //--    transparent: false, // false for cutout
            //--    side: THREE.DoubleSide,
            //--    // Set influence: x and y usually set to same value
            //--    normalScale: new THREE.Vector2(0.5, 0.5),
            //--    depthWrite: true,
            //--    //depthTest: false,
            //--});
            let railSMat=null;
            let railFlipMat=null;
            if(!refList['railMaterial']) {
                railSMat = refList['standardMat'].clone();
                railSMat.uniforms.map.value=railingDiffuse;
                railSMat.uniforms.normalMap.value=railingNormal;
                railSMat.uniforms.flipNormal.value=1; // x only default
                railSMat.uniforms.roughnessMap.value=railingMetarough;
                railSMat.uniforms.metarough.value=2;
                railSMat.uniforms.transparent.value=0; // clip
                railSMat.uniforms.repeatScaleX.value=50.0;
                railSMat.uniforms.repeatScaleY.value=1.0;
                railSMat.side=THREE.DoubleSide;
                railSMat.transparent=false;
            
                refList['railFlipMaterial']=railSMat;
                refList['standardMatUpdate'].push(railSMat);
                railSMat.needsUpdate = true;
                refList['railMaterial']=railSMat;

                railFlipMat=railSMat.clone();
                railFlipMat.uniforms.flipNormal.value=0; // none
                refList['railFlipMaterial']=railFlipMat;
                refList['standardMatUpdate'].push(railFlipMat);
                railFlipMat.needsUpdate = true;
                refList['railFlipMaterial']=railFlipMat;
            } else {
                railSMat = refList['railMaterial'];
                railFlipMat = refList['railFlipMaterial'];
            }
        
            for (let i = 0; i < 4; i++) {
                const railMat=(i<1 || i>2)?railSMat:railFlipMat;
                //const railMat=railSMat;
                const rail = new THREE.Mesh(railGeoLong, railMat);
                // Set the railings to be rendered after the floor
                rail.renderOrder=10;
                rail.name="rail"+i;
                rail.castShadow = true;
                rail.receiveShadow = true;
                rail.visible = false;
                rail.layers.enable(1);
                rail.layers.enable(2);
                refList['railList'].push(rail);
        
                // No rotation.x here! We want it vertical (standing up)
        
                if (i === 0) { // North [West ingame - sunset (Z+)]
                    rail.rotation.y = -Math.PI; // flipped normal
                    rail.position.set(0, railHeight / 2, totalHalfSize);
                }
                if (i === 1) { // South [East ingame - sunrise]
                    //rail.rotation.y = -Math.PI; // flip
                    rail.position.set(0, railHeight / 2, -totalHalfSize);
                }
                if (i === 2) { // East [South ingame - toward sun (X+)]
                    rail.rotation.y = -(Math.PI / 2); // flipped normal
                    rail.position.set(totalHalfSize, railHeight / 2, 0);
                }
                if (i === 3) { // West [North ingame]
                    rail.rotation.y = Math.PI / 2;
                    rail.position.set(-totalHalfSize, railHeight / 2, 0);
                }
        
                refList["terrainGroup"].add(rail);
                // Add to physics if you want the player to "bump" the fence
                physicsBodies.push(rail);
            }
        }

        function createGlassEdges() {
            const edgeWidth = 2;
            const floorSize = 100;
            const halfSize = floorSize / 2;
        
            const textureLoader = new THREE.TextureLoader(manager);
            const glassfloorDiffuse = textureLoader.load('./textures/glass_floor.webp?v=2');
            const glassfloorMetarough = textureLoader.load('./textures/glass_floor_metarough.webp?v=2');
            const glassfloorNormal = textureLoader.load('./textures/glass_floor_normal.webp?v=2');
            
            // Configure Tiling
            [glassfloorDiffuse, glassfloorNormal, glassfloorMetarough].forEach(tex => {
                tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
                //tex.repeat.set(floorSize, edgeWidth); // Adjust based on your scene scale
                tex.repeat.set(52,1); // Adjust based on your scene scale
            });

            //--const glassMat = new THREE.MeshStandardMaterial({
            //--    //opacity: 0.4,
            //--    //roughness: 0.1,
            //--    //metalness: 0.5,
            //--    map: glassfloorDiffuse,
            //--    normalMap: glassfloorNormal,
            //--    roughnessMap: glassfloorMetarough,
            //--    transparent: true,
            //--    side: THREE.DoubleSide,
            //--    // Set influence: x and y usually set to same value
            //--    normalScale: new THREE.Vector2(0.5, 0.5),
            //--    depthWrite: false,
            //--    //depthTest: false,
            //--});
            let glassMat=null;
            if(!refList['glassMaterial']) {
                glassMat = refList['standardMat'].clone();
                glassMat.uniforms.map.value=glassfloorDiffuse;
                glassMat.uniforms.normalMap.value=glassfloorNormal;
                glassMat.uniforms.roughnessMap.value=glassfloorMetarough;
                glassMat.uniforms.metarough.value=2;
                glassMat.uniforms.transparent.value=1;
                glassMat.uniforms.repeatScaleX.value=52.0;
                glassMat.uniforms.repeatScaleY.value=1.0;
                glassMat.side=THREE.DoubleSide;
                glassMat.transparent=true;
            
                refList['glassMaterial']=glassMat;
                refList['standardMatUpdate'].push(glassMat);
                glassMat.needsUpdate = true;
            } else {
                glassMat = refList['glassMaterial'];
            }
            refList['edgeMaterial']=glassMat;

            const edgeGeoLong = new THREE.PlaneGeometry(floorSize + (edgeWidth * 2), edgeWidth);
const edgeGeoShort = new THREE.PlaneGeometry(floorSize, edgeWidth);
        
            // Create 4 edges
            for (let i = 0; i < 4; i++) {
                // North/South use the Long geometry to cover the corners
                const isLongSide = (i === 0 || i === 1);
                const edge = new THREE.Mesh(isLongSide ? edgeGeoLong : edgeGeoShort, glassMat);
                edge.name="edge"+i;
                edge.castShadow = true;
                edge.receiveShadow = true;
                edge.visible = false;
                edge.layers.enable(1);
                edge.layers.enable(2);
                refList['edgeList'].push(edge);

                edge.rotation.x = -Math.PI / 2; // Flat on floor

                if (i === 0) edge.position.set(0, 0, halfSize + (edgeWidth / 2)); // North
                if (i === 1) edge.position.set(0, 0, -halfSize - (edgeWidth / 2)); // South
    
                // Set the floor to be rendered first
                edge.renderOrder = 11;
                physicsBodies.push(edge);
        
                // East/West stay the same length
                if (i === 2) { 
                    edge.rotation.z = Math.PI / 2;
                    edge.position.set(halfSize + (edgeWidth / 2), 0, 0);
                }
                if (i === 3) {
                    edge.rotation.z = Math.PI / 2;
                    edge.position.set(-halfSize - (edgeWidth / 2), 0, 0);
                }
        
                refList["terrainGroup"].add(edge);
            }
        }

        function createUnlitPlane(imageElement) {
            // 1. Create Texture
            const texture = new THREE.Texture(imageElement);
            texture.needsUpdate = true; // Required since image is already loaded
        
            // 2. Calculate Aspect Ratio
            const aspect = imageElement.width / imageElement.height;
        
            // 3. Setup Material (Unlit)
           // //const material = new THREE.MeshBasicMaterial({
           // const material = new THREE.MeshStandardMaterial({
           //     map: texture,
           //     side: THREE.DoubleSide,
           //     //transparent: true
           // });
            const material=refList['imageMaterial'].clone();
            material.uniforms.tDiffuse.value=texture;
        
            // 4. Create Geometry and Mesh
            // We use a unit size of 1 and scale it to preserve aspect ratio
            const geometry = new THREE.PlaneGeometry(1, 1);
            const mesh = new THREE.Mesh(geometry, material);
            mesh.castShadow=true;
            mesh.layers.enable(2);
            //mesh.frustumCulled = false;
        
            mesh.scale.set(aspect, 1, 1);
            //mesh.scale.set(aspect*scale, 1*scale, 1);
            //mesh.position.copy(position);
        
            return [mesh,material,aspect];
        }
        function despawnPlane(mesh, group) {
            if (!mesh) return;
        
            // 1. Dispose Geometry
            mesh.geometry.dispose();

            // 2. Dispose Material & Texture (Shader specific)
            //if (mesh.material.map) {
            //    mesh.material.map.dispose();
            //}
            // custom shader despawn
            if (mesh.material.uniforms && mesh.material.uniforms.tDiffuse) {
                mesh.material.uniforms.tDiffuse.value.dispose();
            }
            mesh.material.dispose();
        
            // 3. Scene Cleanup
            group.remove(mesh);
            scene.remove(group);

            //console.log("Plane and Texture fully purged.");
        }
        function pointAtPlayer(planeMesh, lookPos) {
            // Create a temporary vector for the target (player head at 1.7m)
            const look = new THREE.Vector3(
                lookPos.x,
                1.7, // Your specified player height
                lookPos.z
            );
        
            planeMesh.lookAt(look);
        }
        function pointAtPlayerYOnly(planeMesh, lookPos) {
            // Set the target Y to be exactly the same as the plane's Y
            // This forces the rotation to stay vertical
            const look = new THREE.Vector3(
                lookPos.x,
                planeMesh.position.y,
                lookPos.z
            );
        
            planeMesh.lookAt(look);
        }
        var imagePlanes=[];
        //{
        //    expire: 5.3, // delta subtract
        //    meshRef: null, // mesh for disposal
        //    imageData: null, // preload here
        //    pos: THREE.Vector3(0,0,0),
        //}
//var imageList=[];
        function ajaxThis(x,y,z,cb) {
            var xhr;
            if(window.XMLHttpRequest) xhr=new XMLHttpRequest();
            else                      xhr=new ActiveXObject("Microsoft.XMLHTTP");
        
            var data;
            xhr.onreadystatechange=function() {
                if(xhr.readyState==4 && xhr.status==200) {
        
                    if(typeof cb==='function') {
                        cb(xhr.responseText);
                    }
                }
            }
        
            xhr.open("POST","/demo/ajax.php",true);
            xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
            xhr.send("setting="+x+"&value="+b64Enc(y)+"&key="+b64Enc(z));
        
            // or with json to prevent issues with special chars and url encoding~
            
            //xhr.setRequestHeader("Content-Type","application/json");
            //xhr.send(JSON.stringify({setting: x, value: b64Enc(y), key: b64Enc(z)}));
        }
        let ajaxWait=false;
        let imageWait=0;
        function getNewImage() { // optional replace with imageList above
            if(!ajaxWait) {
                ajaxWait=true;
                ajaxThis('getrandomimagename','','',function(data) {
                    //console.log(data);
                    //ajaxWait=false;
                    loadNewImage(data);
                });
            }
        }
        function loadNewImage(name="") {
            //const pick=rndMinMax(0, imageList.length-1);
            //const name=imageList[pick];
            const img=new Image();

            // 1. Get player's current Y rotation in Radians
            const playerRotY = -refList['playerGroup'].rotation.y-(90 * radian);
            
            // 2. Define the spread (40 degrees converted to Radians)
            const spread = 40 * (Math.PI / 180);
            
            // 3. Pick a random angle within that spread relative to the player
            // This centers the spawn point on the player's forward vector
            const spawnAngle = playerRotY + (Math.random() * (spread * 2) - spread);

            const posOrbit=rndMinMax(1500,2500);
            const posX=Math.cos(spawnAngle) * posOrbit;
            const posZ=Math.sin(spawnAngle) * posOrbit;
            const posY=rndMinMax(820,1020);
            const scale=rndMinMax(3,12);
            let expire=rndMinMax(15,30);
            if(winState['debug']['chk_intrusive_on']) {
                expire=rndMinMax(25,45);
            }
            //const distanceInFront=rndMinMax(1,3);
            const distanceInFront=rndMinMax(2,10);
            //const pulseMax=rndMinMax(10,30)*.1;
            const pulseMax=rndMinMax(Math.floor(distanceInFront*.5),distanceInFront);
            //const pulseMax=(13-Math.floor(distanceInFront))-(rndMinMax(0,3));
            const pulseSpeed=rndMinMax(20,80)*.1;
            const difDecayRate=(distanceInFront - 1) / (expire);
            const intrusiveBoost=rndMinMax(45,145)*.1;
            const intrusiveExtend=rndMinMax(40,80); // 100 is normal 50 is double
            //console.log("distanceInFront/dif="+distanceInFront);
            //console.log("pulseMax="+pulseMax);
            //console.log("expire="+expire);
            //console.log("difDecayRate="+difDecayRate);

            let obj={
                name: name,
                expire: expire, // delta subtract
                count: 0,
                meshRef: null, // mesh for disposal
                materialRef: null, // mesh for disposal
                emptyRef: null, // wrap container -
                imageData: img, // preload here
                pos: new THREE.Vector3(posX,posY,posZ),
                scale: scale,
                stage: 0,
                pulseDist: 0,
                pulseMax: pulseMax,
                pulseSpeed: pulseSpeed,
                pulseDir: 0,
                intrusiveBoost: intrusiveBoost,
                intrusiveExtend: intrusiveExtend,
                dif: distanceInFront,
                difdr: difDecayRate,
                aspect: 1,
                ready: false,
                boundBox: null
            }
            //console.dir(obj);
            imagePlanes.push(obj);
            
            img.onload=function() {
                let found=false;
                for(let i=0;i<imagePlanes.length;i++) {
                    //console.log(imagePlanes[i].name+" vs "+this._name);
                    if(imagePlanes[i].name===this._name) {
                        if(found) {
                            imagePlanes.splice(i,1);
                            i--;
                        } else {
                            found=true;
                            const obj=imagePlanes[i];
                            const imgWrap = new THREE.Group();
                            scene.add(imgWrap);

                            obj.emptyRef=imgWrap;

                            var arr=createUnlitPlane(this);
                            obj.meshRef=arr[0];
                            obj.materialRef=arr[1];
                            obj.aspect=arr[2];
                            obj.meshRef.rotation.set(0,0,0);

                            physicsImages.push(arr[0]); //  plane
            
                            obj.meshRef.name = "image_plane_" + i;
                            obj.emptyRef.name = "image_plane_node_" + i;
                            const scale=obj.scale;
                            imgWrap.scale.set(scale,scale,scale);
                            imgWrap.position.copy(obj.pos);
                            imgWrap.add(obj.meshRef);

                            obj.pos.copy(refList['playerGroup'].position);
                            obj.ready=true;

                            // 1. Create a box3 object
                            const box = new THREE.Box3();
                            obj.boundBox=box;

                            //console.dir(physicsBodies);
                            //console.dir(imagePlanes);

                            ajaxWait=false; // unlock
                            //break;
                        }
                    }
                }
            }
            img._name=name;
            img.src=""+name;
            // ajax a php for image or ajax a text for file names
            // preload new image into imagePlanes with a obj entiity
            // add ready state for spawn?
            // ready rrun create mesh
            // pick spawn pos, deterine seed, fly in to player
            // stop in front of player facing player
            // fly out of scene facing player
            // continue spawn
            //rotating images add to array, splice from array?
        }

        function handleCoinCollisions(nextPos) {
            const playerRadius = 0.6; 
            const playerHeight = player.height; 
            
            const coinRadius = coinRef['coinRadius'] || 1.0; 
            const coinHeight = coinRef['coinHeight'] || 0.25;
            const collisionRadius = playerRadius + coinRadius;
        
            let stoodOnCoin = false; // Track if we are standing on ANY coin this frame
        
            for (let i = 0; i < activeCoins.length; i++) {
                const coin = activeCoins[i].mesh;
                
                // 1. BROADPHASE: Distance check
                const dx = nextPos.x - coin.position.x;
                const dz = nextPos.z - coin.position.z;
                const distanceXZSq = (dx * dx) + (dz * dz);
        
                if (distanceXZSq > collisionRadius * collisionRadius) continue;
        
                const currentTargetHeight = player.isCrouching ? 0.8 : 1.7;

                // 2. VERTICAL HEIGHT CHECK
                const coinMinY = coin.position.y;
                const coinMaxY = coin.position.y + coinHeight;
                
                // Use current real height for feet position calculation
                //const playerMinY = nextPos.y - playerHeight;
                const playerMinY = nextPos.y - currentTargetHeight;
                const playerMaxY = nextPos.y;
        
                // --- SCENARIO A: STEPPING ONTO / STANDING ON THE COIN ---
                // Increase stepUpTolerance if the coins are taller (e.g., 0.5 or 0.6)
                const stepUpTolerance = 0.6; 
        
                // If your feet are within the step zone or already on top of the coin
                //console.log(coinMinY+" "+playerMinY);
                //if (playerMinY >= coinMinY && playerMinY <= coinMaxY + 0.05) {
                if (playerMinY <= coinMaxY + 0.05) {
                    
                    // Check if we are close enough to the top to snap/step up
                    if (playerMinY + currentTargetHeight >= coinMaxY - stepUpTolerance) {
                        // Snap player feet exactly to the top of the coin
                        //nextPos.y = coinMaxY + playerHeight + currentTargetHeight;
                        nextPos.y = coinMaxY + currentTargetHeight;
                        
                        // Physics reset: player is structurally supported
                        if (player.velocity.y < 0) {
                            player.velocity.y = 0;
                        }
                        
                        // GROUNDING FLAG: Essential for enabling jumps!
                        stoodOnCoin = true; 
                        
                        continue; // Skip horizontal push since we are safely on top
                    }
                }
        
                // --- SCENARIO B: HORIZONTAL CYLINDER WALL COLLISION ---
                // If we didn't qualify to step over it, we must hit the wall
                if (playerMinY < coinMaxY && playerMaxY > coinMinY) {
                    const currentDistanceXZ = Math.sqrt(distanceXZSq);
                    if (currentDistanceXZ > 0) {
                        const penetration = collisionRadius - currentDistanceXZ;
                        
                        const pushX = (dx / currentDistanceXZ) * penetration;
                        const pushZ = (dz / currentDistanceXZ) * penetration;
        
                        nextPos.x += pushX;
                        nextPos.z += pushZ;
        
                        const normal = new THREE.Vector3(dx / currentDistanceXZ, 0, dz / currentDistanceXZ);
                        const dot = player.velocity.dot(normal);
                        if (dot < 0) {
                            player.velocity.sub(normal.multiplyScalar(dot));
                        }
                    }
                }
            }
        
            // Safety sync: If your handleGrounding function runs AFTER this loop,
            // it might overwrite player.isGrounded back to false because its ray missed the floor.
            // We can use a global flag or handle sequence order to protect this.
            if (stoodOnCoin) {
                player.velocity.y = 0;
                player.isGrounded = true;
                doublejumpready = 1;
                doublejumpdelta = 0;
                //console.log("is grounded coin");
                if(player.isFalling) {
                    player.isFalling=false;
                    checkFall(refList['playerGroup'].position.y,"coin");
                }
            }
        }

        function sphereCast(origin, radius, direction, maxDistance, objects) {
    // Create a bounding sphere representing the cast
            const castSphere = new Sphere(origin.clone(), radius);
            let closestHit = null;
            let minDistance = maxDistance;
        
            for (const obj of objects) {
                // Ensure object has a bounding sphere computed
                if (!obj.geometry.boundingSphere) obj.geometry.computeBoundingSphere();
        
                // Create the object's world space bounding sphere
                const worldSphere = obj.geometry.boundingSphere.clone();
                worldSphere.applyMatrix4(obj.matrixWorld);
        
                // Calculate if our moving sphere intersects the object's sphere along the direction
                const centerDiff = new Vector3().subVectors(worldSphere.center, castSphere.center);
                const len = centerDiff.length();
        
                // Fast fail: check if they are too far apart for an intersection to even be possible
                if (len > castSphere.radius + worldSphere.radius + maxDistance) {
                    continue;
                }
        
                // Test intersection: project the difference vector onto the direction
                const proj = centerDiff.dot(direction);
        
                // Check if the closest approach lies within the max distance
                if (proj >= 0 && proj <= maxDistance) {
                    // Find closest point on ray to target center
                    const closestPoint = new Vector3().copy(castSphere.center).addScaledVector(direction, proj);
                    const distanceToCenter = closestPoint.distanceTo(worldSphere.center);
        
                    // If the distance is smaller than sum of radii, we have a collision
                    if (distanceToCenter < castSphere.radius + worldSphere.radius) {
                        if (proj < minDistance) {
                            minDistance = proj;
                            closestHit = {
                                object: obj,
                                distance: proj,
                                point: closestPoint
                            };
                        }
                    }
                }
            }
            return closestHit;
        }

        function addRayLine() {
            const length = 100;
            const origin = new THREE.Vector3(0.0,1.0,0.0);
            const direction = new THREE.Vector3(0.0,0.0,-1.0);
            
            // Calculate the end point: origin + (direction * length)
            const dest = new THREE.Vector3();
            dest.copy(origin).addScaledVector(direction, length);
            
            const points = [origin, dest];
            const geometry = new THREE.BufferGeometry().setFromPoints(points);
            //const material = new THREE.LineBasicMaterial({ color: 0xff0000 });
            //const line = new THREE.Line(geometry, material);
            const line = new THREE.Line(geometry, refList['lineRedMat']);
            
            scene.add(line);
            return line;
        }
        function removeRayLine(ridx) {
            scene.remove(refList['rayLines'][ridx]);
            refList['rayLines'].splice(ridx,1);
        }

        function updateRayLine(line,origin,direction,length) {
            // 1. Calculate new destination
            //origin=new THREE.Vector3(0.0,1.0,0.0); // debug
            const newDest = new THREE.Vector3();
            newDest.copy(origin).addScaledVector(direction, length);
        
            // 2. Update the buffer geometry positions
            const positions = line.geometry.attributes.position.array;
            
            // Origin point (x, y, z)
            positions[0] = origin.x;
            positions[1] = origin.y;
            positions[2] = origin.z;
            
            // Destination point (x, y, z)
            positions[3] = newDest.x;
            positions[4] = newDest.y;
            positions[5] = newDest.z;
            
            line.geometry.attributes.position.needsUpdate = true;

            // FIX: Force Three.js to re-calculate bounds so it doesn't vanish when looking away from 0,0,0
            line.geometry.computeBoundingBox();
            line.geometry.computeBoundingSphere();
        }

        function planeHitChecker(hit,stepBack,nextpos,delta,pushBack=true) {
            // INCREASE THIS: If planes are pulsing/moving, we need a larger
            // "active zone" to catch them before they overlap the player center.
            const personalSpace = .2;
        
            // 1. Get the direction the plane is ACTUALLY facing in the world
            // We get the world rotation of the mesh to transform the local normal
            const worldNormal = new THREE.Vector3(0, 0, 1); // Plane's default local normal
            worldNormal.applyQuaternion(hit.object.getWorldQuaternion(new THREE.Quaternion()));
            
            const penetration = (stepBack + personalSpace) - hit.distance;
            
            if (penetration > 0) {
                if(pushBack) {
                    // 2. SHOVE: Apply the push-back to the ghost position
                    nextpos.add(worldNormal.clone().multiplyScalar(penetration));
                }
            
                // 3. SLIDE: Remove velocity moving INTO the plane
                const dot = player.velocity.dot(worldNormal);
                if (dot < 0) {
                    // Subtract the inward velocity component
                    player.velocity.sub(worldNormal.clone().multiplyScalar(dot));
                    
                    // 4. RE-SYNC Trajectory
                    // Re-calculate nextpos from real position + corrected velocity
                    nextpos.copy(refList["playerGroup"].position)
                           .add(player.velocity.clone().multiplyScalar(delta))
                           .add(worldNormal.clone().multiplyScalar(penetration));
                }
            }
        }
        
        const playerRaycaster = new THREE.Raycaster();
        // Create a list for physics
        const physicsBodies = [];
        const physicsImages = [];
        const _v1 = new THREE.Vector3(); // reusable helper
        function handlePlaneCollisions(collidableObjects, nextpos, delta,pushBack) {
            //const moveDir = player.velocity.clone().setY(0).normalize();
            // If player isn't moving, we still check collisions in case the PLANE moves into US
            //const checkDir = moveDir.lengthSq() < 0.001 ? new THREE.Vector3(0, 0, -1) : moveDir;
            //let rotY = -refList['playerGroup'].rotation.y-(90 * radian);
            let rotY=-player.rotation+(90*radian);
            //console.log(rotY);

            const checkDir=new THREE.Vector3(-Math.cos(rotY),0.0,-Math.sin(rotY));
            //console.dir(checkDir);
        
            const stepBack = 0.6;

            // --- Adjustable Settings ---
            let sideAngleDeg = 30; // 22 - 45
            if(readStrafe!=0) {
                sideAngleDeg = 45; // 45 - 90
            }
            const hOffsetDist = 0.4;         // Horizontal spread distance for side columns
            
            const angleRad = sideAngleDeg * radian;
            const upAxis = new THREE.Vector3(0, 1, 0);
            
            // Perpendicular vector for shifting the left/right column origins sideways
            const sideDir = new THREE.Vector3(-checkDir.z, 0, checkDir.x);
            
            // Compute the 3 column directions
            const dirCenter = checkDir.clone();
            const dirLeft = checkDir.clone().applyAxisAngle(upAxis, angleRad);
            const dirRight = checkDir.clone().applyAxisAngle(upAxis, -angleRad);
            
            // Structural 3x3 layout data
            const columns = [
                { dir: dirLeft,   hOff: -hOffsetDist, isSide: true },  // Left Column
                { dir: dirCenter, hOff: 0.0,          isSide: false }, // Center Column
                { dir: dirRight,  hOff: hOffsetDist,  isSide: true }   // Right Column
            ];
            const verticalOffsets = [-0.6, -0.3, 0.0]; // 3 Vertical stacks per column
            
            let ridx = 0;
            const visualRayLength = 15; // Define your visual length up here

            // Loop through columns (Left, Center, Right)
            for (let col of columns) {
                // Loop through vertical stack rows for the current column
                for (let vOff of verticalOffsets) {
            
                    // Calculate unique origin point for this specific ray
                    const origin = refList["playerGroup"].position.clone()
                        .addScaledVector(sideDir, col.hOff)        // Push sideways out to its column line
                        .addScaledVector(col.dir, -stepBack)       // Step back along its shooting direction
                        .setY(refList["playerGroup"].position.y + vOff); // Set vertical height layer
            
                    // Fire Raycaster
                    playerRaycaster.set(origin, col.dir);
                    const hits = playerRaycaster.intersectObjects(collidableObjects);
            
                    // Update debug line visualization
                    if (refList['rayLines'][ridx]) {
                        updateRayLine(refList['rayLines'][ridx], origin, col.dir, visualRayLength);
                    }
                    ridx++;
            
                    // Process collisions
                    if (hits.length > 0) {
                        // col.isSide 
                        planeHitChecker(hits[0], stepBack, nextpos, delta, pushBack);
                    }
                }
            }
        }

        function sphereCollide(sphereMesh, radius, nextpos) {
            const spherePos = new THREE.Vector3();
            sphereMesh.getWorldPosition(spherePos); // Get position in case it's in a group
        
            const playerPos = nextpos;
            const dist = playerPos.distanceTo(spherePos);
            const combinedRadius = radius + (player.radius || 0.5);
        
            if (dist < combinedRadius) {
                // 1. Calculate Normal (Direction from sphere center to player)
                const collisionNormal = new THREE.Vector3().subVectors(playerPos, spherePos).normalize();
        
                // 2. Push player out of the mesh so they don't get stuck inside
                const overlap = combinedRadius - dist;
                playerPos.add(collisionNormal.clone().multiplyScalar(overlap));
        
                // 3. Slide the velocity
                const dot = player.velocity.dot(collisionNormal);
                if (dot < 0) {
                    const friction = collisionNormal.multiplyScalar(dot);
                    player.velocity.sub(friction);
                }
            }
        }
        function handleWorldBoundaries(nextpos) {
            // 50 (floor) + 2 (glass) = 52
            const limit = 52 - (player.radius || 0.5);
            const pos = nextpos;
        
            // X Axis Clamp
            if (Math.abs(pos.x) > limit) {
                pos.x = Math.sign(pos.x) * limit;
                player.velocity.x = 0;
            }
        
            // Z Axis Clamp
            if (Math.abs(pos.z) > limit) {
                pos.z = Math.sign(pos.z) * limit;
                player.velocity.z = 0;
            }
        }

        // water height calc js clone from waterrt shader
        // --- Pre-calculated Vector Helpers to prevent garbage collection overhead ---
        const DIRS = [
            new THREE.Vector2(1.0, 0.0),
            new THREE.Vector2(0.0, 1.0),
            new THREE.Vector2(1.0, 1.0).normalize(),
            new THREE.Vector2(-1.0, 1.0).normalize()
        ];
        const FREQS = [2.0, 2.0, 3.0, 3.0];
        const SPEEDS = [1.0, 1.2, 1.5, 1.3];
        const AMPS = [0.2, 0.2, 0.1, 0.1];
        
        // --- Core Mathematical Porting Functions ---
        function noise3(x, y) {
            const dot = x * 12.9898 + y * 78.233;
            const sinVal = Math.sin(dot) * 43758.5453;
            const fract = sinVal - Math.floor(sinVal);
            return fract < 0 ? fract + 1.0 : fract; // Keeps it strictly 0.0 -> 1.0 matching WebGL specs
        }
        
        function smoothNoise(x, y) {
            const ix = Math.floor(x);
            const iy = Math.floor(y);
            const fx = x - ix;
            const fy = y - iy;
        
            // GLSL smoothstep interpolation curve: f * f * (3.0 - 2.0 * f)
            const ux = fx * fx * (3.0 - 2.0 * fx);
            const uy = fy * fy * (3.0 - 2.0 * fy);
        
            const a = noise3(ix, iy);
            const b = noise3(ix + 1.0, iy);
            const c = noise3(ix, iy + 1.0);
            const d = noise3(ix + 1.0, iy + 1.0);
        
            // Replicate mix(mix(a, b, u.x), mix(c, d, u.x), u.y)
            const mixAB = a + ux * (b - a);
            const mixCD = c + ux * (d - c);
            return mixAB + uy * (mixCD - mixAB);
        }
        
        /**
         * Calculates the exact water height value at a given UV position.
         * @param {number} u - The texture U coordinate (0.0 to 1.0)
         * @param {number} v - The texture V coordinate (0.0 to 1.0)
         * @param {number} phase - The current time scalar (data.x) passed into the uniform
         * @returns {number} The raw height value (un-mapped from standard shader outputs)
         */

        var lastWaterHeight=0.0;
        function getWaterHeightAt(worldX, worldZ, rawDataX) {
            // 1. --- CORRECT THE TIME BLOCK (Lines 7459 - 7463) ---
            const phase = rawDataX * 0.0001;
            const twinkleSpeed = 86400.0 / 1.0;
            const rawTime = (phase / (2.0 * Math.PI)) * twinkleSpeed;
            
            let timeV = rawTime % 6000.0;
            timeV = Math.abs(timeV - 3000.0);

            // Ensure we handle absolute world positions wrapping correctly 
            // across your 4000-unit infinite ocean boundary
            const worldSize = 4000.0;
            
            // 1. Get the combined world position (mimicking the shader's doubling)
            const combinedX = worldX + worldX;
            const combinedZ = worldZ + worldZ;
            
            // 2. Map to UV space (0.0 to 1.0)
            let u = combinedZ * 0.00025;
            let v = combinedX * 0.00025;
            
            // 3. --- THE CRITICAL CENTER OFFSET ---
            // If your world (0,0) is meant to be the center of the texture:
            //u += 0.5;
            //v += 0.5;
            
            // Replicate GLSL Repeat wrapping behavior cleanly for negative and positive bounds
            u = u - Math.floor(u);
            v = v - Math.floor(v);
            // --- THE INVERSION TOGGLE MATRIX ---
            // If the wave moves opposite to your character trajectory, uncomment these:
            //u = 1.0 - u;
            //v = 1.0 - v;

            // 3. --- GENERATE NOISE MAP USING SATELLITE GENERATOR ALGORITHM ---
            const scale = 128.0; // Resolution scale matching your generator texture setup
            const iTexCoordX = u * scale;
            const iTexCoordY = v * scale;

            // Calculate Perturbation Noise (Lines 7176-7177)
            const noiseValue = smoothNoise(iTexCoordX + timeV * 0.1, iTexCoordY + timeV * 0.05);
            const uvPerturbedX = iTexCoordX + noiseValue * 0.1;
            const uvPerturbedY = iTexCoordY + noiseValue * 0.1;

            // Wave Summation Loops
            let heightMapVal = 0.0;
            for (let i = 0; i < 4; i++) {
                const dotProduct = DIRS[i].x * uvPerturbedX + DIRS[i].y * uvPerturbedY;
                const phaseVal = dotProduct * FREQS[i] + timeV * SPEEDS[i];
                heightMapVal += AMPS[i] * Math.sin(phaseVal);
            }

            // Add Micro-Noise Overlay (Line 7185)
            heightMapVal += 0.05 * smoothNoise(iTexCoordX * 5.0 + timeV * 0.2, iTexCoordY * 5.0);

            // 4. --- MAP BACK INTO ABSOLUTE WORLD SPACE ---
            // Ground generator returns values packed between 0-1 for textures: (height * 0.5 + 0.5)
            const textureSampleR = heightMapVal * 0.5 + 0.5; 

            // Apply your final mesh height modifier (the data.z value from uniforms)
            const heightScale = 2.0; 
            return waterLevel + (textureSampleR * heightScale);
        }

        // Keep a reusable 4-element array for the RGBA pixel data
      //--  const pixelBuffer = new Float32Array(4); // Optional Uint8Array but RT is FloatType or HalfFloatType must be UnsignedByteType

        const waterLevel=-12.5;
      //--  function getWaterHeightAt(pX,pZ,playerPos) {
      //--      return waterLevel;
      //--    //--  const heightScale = 2.0 * 2.0; // Your data.z * 2.0

      //--    //--  // 1. Replicate shader UV math
      //--    //--  let u = (pX + playerPos.x) * 0.00025;
      //--    //--  let v = (pZ + playerPos.z) * 0.00025;

      //--    //--  // 2. Clamp or Wrap UVs to [0, 1] mirroring your texture settings
      //--    //--  u = u - Math.floor(u);
      //--    //--  v = v - Math.floor(v);

      //--    //--  // 3. Map UV [0, 1] to Render Target pixel space (Width / Height)
      //--    //--  const rt = refList["waterHeightRT"];
      //--    //--  const texX = Math.floor(u * rt.width);
      //--    //--  const texY = Math.floor(v * rt.height);

      //--    //--  // 4. Read the single pixel from the GPU
      //--    //--  refList["renderer"].readRenderTargetPixels(rt, texX, texY, 1, 1, pixelBuffer);
      //--    //--  //console.log("texX: "+texX+" texY: "+texY);
      //--    //--  // Calling renderer.readRenderTargetPixels() forces the CPU to stop and wait for the GPU to finish rendering and hand over the data. This creates a CPU-GPU stall
      //--    //--  // copy your mathematical wave generation logic (the code running inside waterHeightMaterial's shader) directly into Javascript functions and calculate it purely on the CPU for gameplay physics

      //--    //--  // 5. Decode the red channel
      //--    //--  // If UnsignedByteType (0-255), normalize it to 0.0 - 1.0
      //--    //--  //const rValue = pixelBuffer[0] / 255.0; // unsignedint8
      //--    //--  const rValue = pixelBuffer[0]; // float32
      //--    //--  // If FloatType, it's just: const rValue = pixelBuffer[0];

      //--    //--  // 6. Replicate the vertex displacement math (Line 7189 / 7320)
      //--    //--  const trueHeight = waterLevel + (rValue * heightScale);

      //--    //--  //return trueHeight;
      //--    //--  return trueHeight;
      //--  }
        
        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);
        }

        // --- Core Variables ---
        let scene, clock, raycaster, mouse, mousep, mousem;

        
        const dayTint = new THREE.Vector3(1.0, 0.9, 0.8);   // Warm sun
        const nightTint = new THREE.Vector3(0.5, 0.7, 0.8); // Cool blue moon
        let currentColor = new THREE.Vector3(1.0,1.0,1.0);

        // --- Player State ---
        const player = {
            height: 1.7,
            radius: .5,
            velocity: new THREE.Vector3(),
            speed: 5.0,
            rotation: 0,     // Yaw (Left/Right)
            pitch: 0,        // Pitch (Up/Down)
            isGrounded: true,
            isCrouching: false,
            isSwimming: false,
            isFalling: false,
            isFlying: false,
            sensitivity: 0.002
        };

        const keys = {};
        
        var joy={
            left: $('leftJoyStick'), leftC: $('leftJoyContainer'),
            right: $('rightJoyStick'), rightC: $('rightJoyContainer')
        };
        var touchIndex={touch1: null, touch2: null};
        var leftJoyHold=false;
        var rightJoyHold=false;
        let ovrJoysticks = false; // for isLocked auto hide joy
        
        function refreshJoysticks() {
            if(useJoysticks && !ovrJoysticks) {
                $('leftJoyContainer').style.display="block";
                $('rightJoyContainer').style.display="block";
            } else {
                $('leftJoyContainer').style.display="none";
                $('rightJoyContainer').style.display="none";
            }
            set_check("chk_joy_on",useJoysticks);

            useJoysticksChk.value=useJoysticks;

            setModeStatus();
        }

        function removePinchPointer(e) {
            pinchPointers = pinchPointers.filter(p => p.pointerId !== e.pointerId);
            if (pinchPointers.length < 2) {
                prevPinchDist = -1; // Reset tracking distance
            }
        }

        let pinchPointers = [];
        let prevPinchDist = -1;

        //var touchIds = { left: null, right: null };
        // Map to track which pointer (mouse or finger) controls which side
        var activePointers = { left: null, right: null };
        // Keep track of which joystick the mouse is physically dragging
        var mouseDraggingSide = null;
        function holdJoy(e,hold=false,side="left") {
            console.log("hold joy trig side="+side);
            var register=false;
            if(hold) {
                // Store the touch ID that started this hold
                // e is the touchstart event
                //touchIds[side] = e.changedTouches[0].identifier;
                // pointerId works for both mouse (usually id 1) and multiple fingers
                // If it's a mouse (pointerType === 'mouse'), don't lock the real pointerId to both sides.
                // Instead, use a pseudo-id for the right side so it doesn't collide with the left.
                if(touchIndex['touch1']==null) {
                    touchIndex['touch1']=side;
                    register=true;
                } else if(touchIndex['touch2']==null) {
                    touchIndex['touch2']=side;
                    register=true;
                }
                if(side=="left") {
                    leftJoyHold=true;
                } else {
                    rightJoyHold=true;
                }

                if (e.pointerType === 'mouse') {
                    mouseDraggingSide = side; // Lock the mouse to this specific container
                } else {
                    activePointers[side] = e.pointerId; // Lock touch pointer ID
                }

                if (e.target.setPointerCapture) {
                    e.target.setPointerCapture(e.pointerId);
                }
            } else {
                activePointers[side] = null;

                joy[side].style.left="50%";
                joy[side].style.top="50%";
                joyRead[side].isHeld=false;
                joyRead[side].ratio=0;
                joyRead[side].tx=0;
                joyRead[side].ty=0;
                if(touchIndex['touch1']==side) {
                    touchIndex['touch1']=null;
                }
                if(touchIndex['touch2']==side) {
                    touchIndex['touch2']=null;
                }
                
                if(side=="left") {
                    leftJoyHold=false;
                } else {
                    rightJoyHold=false;
                };

                if (e.pointerType === 'mouse') {
                    if (mouseDraggingSide === side) mouseDraggingSide = null;
                } else {
                    activePointers[side] = null;
                }
            }
        }
        var joyRead={
            left: { isHeld: false, ratio: 0, angle: 0, tx: 0, ty: 0 },
            right: { isHeld: false, ratio: 0, angle: 0, tx: 0, ty: 0 },
        };

        var orbit=25;
        function moveJoy(e) {
            //e=event || window.event;
            //console.log(e);

            //var X=e.clientX || e.targetTouches[0].pageX;
            //var Y=e.clientY || e.targetTouches[0].pageY;

            //var sY=window.scrollY;
            //var sX=window.scrollX;
            //let pointerId=e.pointerId;
            //if(pointerId==0) {
            //    if(rightJoyHold) {
            //        pointerId=activePointers.right;
            //    } else if(leftJoyHold) {
            //        pointerId=activePointers.left;
            //    }
            //}
            ////console.log(pointerId);
            //let side = null;
            //if (pointerId === activePointers.left) side = "left";
            //else if (pointerId === activePointers.right) side = "right";

            let side = null;
            //if (e.pointerId === activePointers.left) side = "left";
            //else if (e.pointerId === activePointers.right) side = "right";

            // 1. Check if this is a mouse event we are dragging
            if (e.pointerType === 'mouse') {
                side = mouseDraggingSide;
            } else { // 2. Otherwise, treat it as a multi-touch event
                if (e.pointerId === activePointers.left) side = "left";
                else if (e.pointerId === activePointers.right) side = "right";
            }

            // Check match for normal touch/left mouse, or our simulated right mouse id
            //console.log(e.pointerId);
            //if (e.pointerId === activePointers.left) side = "left";
            //else if (e.pointerId === activePointers.right || (e.pointerType === 'mouse' && activePointers.right === "mouse_right_sim")) {
            //    side = "right";
            //}
            //console.log(side);

            console.log("Pointer ID:", e.pointerId, "Evaluated Side:", side);


            //console.log(side);
            // Only process if this specific pointer is mapped to a side
            if (side) {
                e.preventDefault();
                //const X = e.touchX;
                //const Y = e.touchY;
                
                // Uniform coordinates for mouse AND touch
                const X = e.clientX;
                const Y = e.clientY;

                //console.log("X: "+X+", Y: "+Y);;
                const container = joy[side + "C"];
                const eL = container.offsetLeft;
                const eT = container.offsetTop;
                const eW = container.clientWidth;
                const eH = container.clientHeight;

                var eX=X-eL; // bounding box = box to circle ?
                if(eX<0) eX=0;

                var eY=Y-eT;
                if(eY<0) eY=0;
                
                if(eX>eW) eX=eW;
                if(eY>eH) eY=eH;

                var pX=eX*100/eW; // percent 0 - 100
                var pY=eY*100/eH;

                var cangle=(Math.atan2(pY-50,pX-50) * 180) / Math.PI;

                var rX=Math.round(Math.cos(cangle*radian)*orbit+25)+25;
                var rY=Math.round(Math.sin(cangle*radian)*orbit+25)+25;
                //console.log("pY: "+pY+" pX: "+pX);
                //console.log("rY: "+pY+" rX: "+pX);
                //console.log("cangle: "+cangle);
                
                if(pX<50) {
                	if(pX<rX) pX=rX;
                } else {
                	if(pX>rX) pX=rX;
                }
                if(pY<50) {
                	if(pY<rY) pY=rY;
                } else {
                	if(pY>rY) pY=rY;
                }

                joy[side].style.left=(pX)+"px";
                joy[side].style.top=(pY)+"px";
                
                var a=50-pX;
                var b=50-pY;
                var c=Math.sqrt( a*a + b*b );
                var v=cangle+180;

                joyRead[side].ratio=c*4;
                joyRead[side].angle=v;
                joyRead[side].isHeld=true;

                //console.log('c * 4 ='+(c*4)+" angle="+cangle);
                // angle = 0 = right but v+180 = left = 0
                // -180/+180 = left             right = +180
                // -90 = forward                backward  + 270
                // 90 = backward                forward = +90

                //console.dir(joyRead[side1]);

                joyRead[side].tx=Math.cos(joyRead[side].angle*radian)*(joyRead[side].ratio*.01);
                joyRead[side].ty=Math.sin(joyRead[side].angle*radian)*(joyRead[side].ratio*.01);
                //let tx=Math.cos(v*radian)*((c*4)*.01);
                //let ty=Math.sin(v*radian)*((c*4)*.01);
                //ty = 1 to -1 forward to backward
                //tx = 1 to -1 left to right

                //console.log("tx: "+tx+" ty: "+ty);
            }
        }
        
        function mouse_position(e) {
            if(mousem.x === null || mousem.y === null ) {
                return; // iniitialize only
            }

            const X=mousep.x;
            const Y=mousep.y;

            var sY=window.scrollY;
            var sX=window.scrollX;

            //console.log("mouse_position X: "+X+" Y: "+Y);
            if(sliderHold!="" && winHold!="") {
                const bounds=$(sliderHold).getBoundingClientRect();
                const childPin=$(sliderHold).getElementsByClassName('handle')[0];
                const childBounds=childPin.getBoundingClientRect();
                if(childPin.className.includes("slider-v")) {
                    //console.log("vertical bar - checking height");
                    //console.dir(childPin);
                    const minPx=0;
                    const maxPx=bounds.height-16;

                    let newposV=Y-bounds.top-6;
                    //console.log("childBounds.top: "+childBounds.top);
                    //console.log("minPx: "+minPx+" maxPx: "+maxPx+" currentPx: "+(newposV));
                    if(newposV<minPx) newposV=minPx;
                    else if(newposV>maxPx) newposV=maxPx;

                    const percent=Math.floor(100-(newposV*100/maxPx));
                    //console.log("new vol: "+(percent));
                    $('musicplayer').volume=percent*.01;
                    $('lb_slider_vol1p').innerHTML=percent+"%";
                    //console.log(winHold);
                    winState[winHold][sliderHold]=percent;
                    saveSetting("music-volume",percent);

                    childPin.style.top=(newposV)+"px";
                } else if(childPin.className.includes("slider-h")) {
                    //console.log("horizontal bar - checking width");
                    //console.dir(childPin);
                    const minPx=0;
                    const maxPx=bounds.width-16;

                    let newposH=X-bounds.left-6;
                    //console.log("childBounds.top: "+childBounds.top);
                    //console.log("minPx: "+minPx+" maxPx: "+maxPx+" currentPx: "+(newposV));
                    if(newposH<minPx) newposH=minPx;
                    else if(newposH>maxPx) newposH=maxPx;

                    const percent=Math.floor(newposH*100/maxPx);
                    //console.log("new vol: "+(percent));
                    winState['quality']['slider_sway1']=percent;
                    $('lb_slider_sway1p').innerHTML=percent+"%";
                    //console.log(winHold);
                    winState[winHold][sliderHold]=percent;
                    saveSetting("natural-camera",percent);

                    childPin.style.left=(newposH)+"px";
                }

                return;
            } else if(winHold!="") {
                const cmw = $('cmw_' + winHold);

                cmw.style.top=(Y - cmw.dragOffsetY)+"px";
                cmw.style.left=(X - cmw.dragOffsetX)+"px";
                cmw.style.right="auto";
                cmw.style.bottom="auto";
            
                return; 
            } else if(sliderHold!="") {
                console.log("catch error - winHold not set but sliderHold is")
                return;
            }
            // isLocked

            // Screen warp handling: if delta is huge, it's the browser centering the hidden cursor
            //if (Math.abs(mousem.x) > 100 || Math.abs(mousem.y) > 100) return;
            
            // 1. Update Yaw (Left/Right)
            //player.rotation -= e.movementX * player.sensitivity;
            player.rotation -= mousem.x * player.sensitivity;
        
            // 2. Update Pitch (Up/Down)
            //player.pitch -= e.movementY * player.sensitivity;
            player.pitch -= mousem.y * player.sensitivity;
        
            // Clamp pitch to 90 degrees up/down
            player.pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, player.pitch));

            //if(e.movementX != 0 || e.movementY != 0) {
            if(mousem.x != 0 || mousem.y != 0) {
                idleTime = 0;
            }
        }

        var musicOn=false;
        var musicOnChk={ value: musicOn }; // only object can store as ref in js

        var markOn=false;
        var vortexOn=false;
        var vortexOnChk={ value: vortexOn };
        var cloudsOn=2; // start 2 on 0 off
        var shadowOn=1; // 1 start with shadow 2 debug shadows
        var rendmode=0;

        let useJoysticks = false; // set default start

        var cloudsOnChk = { value: (cloudsOn>1)?true:false };
        var shadowOnChk = { value: (shadowOn>0)?true:false };
        var useJoysticksChk = { value: useJoysticks };
        let helperLoadedChk={ value: helperLoaded };

        var lsPrefix="twily-demo-";
        function saveSetting(name="",val=null) {
            if(val!=null) { // save
                localStorage.setItem(lsPrefix+name,val);
            } else { // delete
                if(localStorage.getItem(lsPrefix+name)) {
                    localStorage.removeItem(lsPrefix+name);
                }
            }
        }
        function getSetting(name="") {
            if(localStorage.getItem(lsPrefix+name)) {
                return localStorage.getItem(lsPrefix+name);
            }
            return null;
        }

        // extra data and variables stored in sync with cmwindows instead of data- attributes
        // local variables a selfref, global require obj wrap
        let winState={
            keybind: {
                state: 0,
                windex: 420,
                local: [],
                global: [],
                evl: [],
            },
            joystick: {
                state: 0,
                windex: 419,
                chk_joy_strafe: false,
                chk_joy_inv_x: false,
                chk_joy_inv_y: false,
                local: [
                    ["chk_joy_strafe",joy_strafe_toggle],
                    ["chk_joy_inv_x",joy_inv_x_toggle],
                    ["chk_joy_inv_y",joy_inv_y_toggle],
                ],
                global: [
                    ["chk_joy_on",joy_toggle,useJoysticksChk],
                ],
                evl: [],
            },
            quality: {
                state: 0,
                windex: 418,
                chk_shadow_ui: true,
                slider_sway1: 100,
                sel_mountain_res: false,
                sel_render_res: false,
                local: [
                    ["chk_shadow_ui",shadow_ui_toggle],
                    ["slider_sway1",natural_cam_set],
                    ["sel_mountain_res",sel_mountain_res],
                    ["sel_render_res",sel_render_res],
                ],
                global: [
                    ["chk_shadow_on",shadow_toggle,shadowOnChk],
                    ["chk_clouds_on",clouds_toggle,cloudsOnChk],
                ],
                evl: [],
            },
            audio: {
                state: 0,
                windex: 417,
                chk_music_clouds: true,
                slider_vol1: 100,
                local: [
                    ["chk_music_clouds",music_clouds_toggle],
                    ["slider_vol1",music_vol_set],
                ],
                global: [
                    ["chk_music_on",music_toggle,musicOnChk],
                ],
                evl: [],
            },
            debug: {
                state: 0,
                windex: 416,
                chk_shadow_debug: false,
                chk_wireframe: false,
                chk_anime_on: false,
                chk_intrusive_on: false,
                local: [
                    ["chk_shadow_debug",shadow_debug_toggle],
                    ["chk_wireframe",wireframe_toggle],
                    ["chk_anime_on",anime_toggle],
                    ["chk_intrusive_on",intrusive_toggle],
                ],
                global: [
                    ["chk_shadow_helpers",shadow_helper_toggle,helperLoadedChk],
                    ["chk_vortex_on",vortex_toggle,vortexOnChk],
                    ["chk_axis_grid",grid_toggle,helperGridsChk],
                ],
                evl: [],
            }
        }
        function window_toggle(win="") { // window_open
            if($('cmw_'+win) && winState[win]) {
                if(winState[win]['state']==0) {
                    $('cmw_'+win).style.display="table";
                    if(!$('cmw_'+win).getAttribute('data-first')) {
                        const bounds = $('cmw_'+win).getBoundingClientRect();
                        const margin=20;
                        const topbar=20;
                        // default left top
                        let setL=margin;
                        let setT=margin+topbar;
                        switch(win) {
                            case "debug": // left bottom
                                setT=SCR_HEIGHT-bounds.height-margin;
                                break;
                            case "quality": // right top
                                setL=SCR_WIDTH-bounds.width-margin;
                                break;
                            case "audio": // right bottom
                                setL=SCR_WIDTH-bounds.width-margin;
                                setT=SCR_HEIGHT-bounds.height-margin;
                                break;
                            case "keybind": // center
                                setL=(SCR_WIDTH*.5)-(bounds.width*.5);
                                setT=(SCR_HEIGHT*.5)-(bounds.height*.5);
                                break;
                            //case "joystick": // left top
                            //    setL=margin;
                            //    break;
                            default:
                        }
                        if(setT<margin+topbar) setT=margin+topbar;

                        $('cmw_'+win).style.left=setL+"px";
                        $('cmw_'+win).style.top=setT+"px";
                        $('cmw_'+win).style.right="auto";
                        $('cmw_'+win).style.bottom="auto";
                        
                        $('cmw_'+win).setAttribute('data-first',1);
                    }
                    winState[win]['state']=1; // show

                    // add rewindex on mouse down within window
                    const cmw=$('cmw_'+win);
                    cmw.winName=win;
                    cmw.evltype="window_body";
                    cmw._onDown=function(e) { rewindex(this.winName); };
                    cmw._onUp=function(e) { cmwMouseUpExec(this.winName); };
                    cmw.addEventListener('pointerdown', cmw._onDown);
                    cmw.addEventListener('pointerup', cmw._onUp);

                    for(let i=0;i<winState[win]['local'].length;i++) {
                        let cname=winState[win]['local'][i][0];
                        let cfunc=winState[win]['local'][i][1];
                        let cstate=winState[win][cname];

                        if(cname.substr(0,4)=="chk_") {
                            set_check(cname,cstate); // load checks

                            const chk=$(cname);
                            chk.winName=win;
                            chk.evltype="chk_click";
                            chk._onClick=cfunc;
                            chk.addEventListener('click', chk._onClick);
                            winState[win]['evl'].push(chk);
                        } else if(cname.substr(0,4)=="sel_") {
                            //
                            const sel=$(cname);
                            sel.winName=win;
                            sel.evltype="sel_click";
                            sel._onClick=cfunc;
                            sel.addEventListener('click', sel._onClick);
                            winState[win]['evl'].push(sel);
                        } else if(cname.substr(0,7)=="slider_") {
                            const slider=$(cname)
                            slider.winName=win;
                            slider.evltype="slider_hold";
                            slider.sliderName=cname;
                            slider._onHold=cfunc;
                            //slider.addEventListener('mousedown', slider._onHold);
                            //slider.addEventListener('touchstart', slider._onHold);
                            slider.addEventListener('pointerdown', slider._onHold);
                            const childPin=slider.getElementsByClassName("handle")[0];
                            if(childPin.className.includes("slider-v")) {
                                // vertical
                                let offsetS=((100-winState[win][cname])*.01)*16;
                                childPin.style.top="calc("+(100-winState[win][cname])+"% - "+offsetS+"px)";
                            } else if(childPin.className.includes("slider-h")) {
                                // horizontal
                                let offsetS=(winState[win][cname]*.01)*16;
                                childPin.style.left="calc("+(winState[win][cname])+"% - "+offsetS+"px)";
                            }
                            $('lb_'+cname+'p').innerHTML=winState[win][cname]+"%";
                            winState[win]['evl'].push(slider);
                        }
                    }
                    for(let i=0;i<winState[win]['global'].length;i++) {
                        let cname=winState[win]['global'][i][0];
                        let cfunc=winState[win]['global'][i][1];
                        let cstate=winState[win]['global'][i][2];
                        
                        //console.log('global; cname = '+cname+' cstate = '+cstate);
                        if(cname.substr(0,4)=="chk_") {
                            set_check(cname,cstate.value); // load checks
                            const chk=$(cname);
                            chk.winName=win;
                            chk.evltype="chk_click";
                            chk._onClick=cfunc;
                            chk.addEventListener('click', chk._onClick);
                            winState[win]['evl'].push(chk);
                        } else if(cname.substr(0,4)=="sel_") {
                            //
                            const sel=$(cname);
                            sel.winName=win;
                            sel.evltype="sel_click";
                            sel._onClick=cfunc;
                            sel.addEventListener('click', sel._onClick);
                            winState[win]['evl'].push(sel);
                        } else if(cname.substr(0,7)=="slider_") {
                            const slider=$(cname)
                            slider.winName=win;
                            slider.evltype="slider_hold";
                            slider.sliderName=cname;
                            slider._onHold=cfunc;
                            //slider.addEventListener('mousedown', slider._onHold);
                            //slider.addEventListener('touchstart', slider._onHold);
                            slider.addEventListener('pointerdown', slider._onHold);
                            const childPin=slider.getElementsByClassName("handle")[0];
                            if(childPin.className.includes("slider-v")) {
                                // vertical
                                let offsetS=((100-winState[win][cname])*.01)*16;
                                childPin.style.top="calc("+(100-winState[win][cname])+"% - "+offsetS+"px)";
                            } else if(childPin.className.includes("slider-h")) {
                                // horizontal
                                let offsetS=(winState[win][cname]*.01)*16;
                                childPin.style.left="calc("+(winState[win][cname])+"% - "+offsetS+"px)";
                            }
                            winState[win]['evl'].push(slider);
                        }
                    }

                    // add window drag
                    const cmw_top=cmw.getElementsByClassName("top-c")[0];
                    winState[win]['evl'].push(cmw_top);
                    //console.dir(cmw_top);
                    cmw_top.winName=win;
                    cmw_top.evltype="window_hold";
                    cmw_top._onHold=function(e) { window_drag(e,this.winName); };
                    //cmw_top.addEventListener('mousedown', cmw_top._onHold);
                    //cmw_top.addEventListener('touchstart', cmw_top._onHold);
                    cmw_top.addEventListener('pointerdown', cmw_top._onHold);

                    const btnExit=$('win_btn_close_'+win);
                    const btnClose=$('btn_close_'+win);

                    winState[win]['evl'].push(btnExit);
                    winState[win]['evl'].push(btnClose);

                    if(btnExit) {
                        btnExit.winName=win;
                        cmw_top.evltype="button_close";
                        btnExit._onClose=function(e) { window_close(this.winName); };
                        btnExit.addEventListener('click', btnExit._onClose);
                    }
                    if(btnClose) {
                        btnClose.winName=win;
                        cmw_top.evltype="button_close";
                        btnClose._onClose=function(e) { window_close(this.winName); };
                        btnClose.addEventListener('click', btnClose._onClose);
                    }

                    rewindex(win);
                } else {
                    $('cmw_'+win).style.display="none";
                    winState[win]['state']=0; // hide
                }
            }
        }
        function window_close(win="") {
            if($('cmw_'+win) && winState[win]) {
                $('cmw_'+win).style.display="none";
                winState[win]['state']=0;

                console.dir(winState[win]['evl']);
                for(let i=0;i<winState[win]['evl'].length;i++) {
                    let evl=winState[win]['evl'][i];
                    //console.log('removing evl: '+evl);

                    if(evl.evlType=="button_close") {
                        evl.removeEventListener('click', evl._onClose); // cleanup per window eventlisteners
                    } else if(evl.evlType=="chk_click") {
                        evl.removeEventListener('click', evl._onClick);
                    } else if(evl.evlType=="window_body") {
                        evl.removeEventListener('pointerdown', evl._onDown);
                        evl.removeEventListener('pointerup', evl._onUp);
                    } else if(evl.evlType=="window_hold") {
                        evl.removeEventListener('pointerdown', evl._onHold);
                    } else if(evl.evlType=="slider_hold") {
                        evl.removeEventListener('pointerdown', evl._onHold);
                    }
                    winState[win]['evl'].splice(i,1);
                    i--;
                }
            }
        }

        function cmwMouseUpExec() {
            if(winState['quality']['sel_mountain_res']) {
                sel_mountain_res(); // toggle
            } else if(winState['quality']['sel_render_res']) {
                sel_render_res(); // toggle
            }
        }

        const windexRange=10;
        const windexMax=420;
        function rewindex(win="") {
            let windex=winState[win]['windex'];
            let cindex=windex+1;
            //console.log("rewindex win="+win+" cindex="+cindex+" windex="+winState[win]['windex']);
            let maxtries=100;
            if(cindex>windexMax) cindex=windexMax;
            while(cindex<=windexMax && maxtries>0) {
                //console.log("checking cindex "+cindex);
                Object.keys(winState).forEach(key => {
                    //console.log("checking key "+key+" for cindex "+cindex);
                    if(winState[key]['windex']==cindex) {
                        winState[key]['windex']=cindex-1;
                        $("cmw_"+key).style.zIndex=cindex-1;
                        //console.log("setting key "+key+" (cindex-1) "+(cindex-1));
                    }
                });
                cindex++;
                maxtries--; // failsafe while
            }
            //console.log("setting win "+win+" (cindex-1) "+(cindex-1));
            winState[win]['windex']=cindex-1; // 420
            $("cmw_"+win).style.zIndex=cindex-1;

            //console.dir(winState);
        }

        var winHold="";
        function window_drag(e,win="")  {
            winHold=win;
            console.log("holding "+win);

            const cmw = $('cmw_' + winHold);
            const bounds = cmw.getBoundingClientRect();
            cmw.dragOffsetX = e.clientX - bounds.left;
            cmw.dragOffsetY = e.clientY - bounds.top;

            rewindex(win);
        }

        function set_check(targ,s=false) {
            if($(targ)) {
                $(targ).className="ui-spr checkbox"+((s)?"_checked":"")
            }
        }

        // check with clock update in animate
        function check_music() { // only check if musicOn
            if($('musicplayer').paused) {
                music_play(true);
            }
        }
        let tracklist=["https://twily.info/demo/goth.mp3","https://twily.info/demo/stillhere.mp3"];
        let firstplay=true; // from beginning first time always
        function music_play(reload=false) {
            mF.player = $('musicplayer');
    
            // If we need a new track and are currently playing, start Fade Out
            if (reload && !mF.player.paused && mF.player.volume > 0) {
                mF.fadeDir = -1; 
                return; // Loop will call execute_track_load once volume hits 0
            }

            execute_track_load();

            function execute_track_load() {
                // dice roll to even or odd
                //let pick = (rndMinMax(1, 6) % 2 === 0) ? 0 : 1;
                // more than 2 tracks
                let pick = rndMinMax(0, tracklist.length-1);
                //if(firstplay) {
                //    pick=0; // start high.mp3 first always
                //}
                mF.player.src = tracklist[pick];

                mF.player.addEventListener('loadedmetadata', function() {
                    let beginning = (rndMinMax(1, 6) === 6) ? true : false;
                    if(firstplay) {
                        firstplay=false;
                        beginning=true;
                    }
                    if(!beginning) {
                        const maxTime = mF.player.duration*.8; // avoid last 20%
                        mF.player.currentTime = Math.random() * maxTime;
                        mF.fadeSpeed = .05;
                    } else {
                        console.log("music from beginning track");
                        mF.fadeSpeed = .5;
                    }
                    mF.player.volume = 0;
                    mF.player.play();
                    mF.fadeDir = 1; // Start Fade In
                }, { once: true });

                musicLoaded=true;

                if(!audioContextInitiated) {
                    createAudioContext();
                }
            }
        }
        function music_pause() {
            mF.fadeSpeed = .05;
            mF.fadeDir = -1;
            //$('player').pause(); // replaced with fade
        }
        function music_toggle() {
            musicOn=!musicOn;
            musicOnChk.value=musicOn; // updates in dialog window
            if(musicOn) {
                music_play(true); // reload every time
                saveSetting("music-on",1);
            } else {
                music_pause();
                saveSetting("music-on",null);
            }
            set_check("chk_music_on",musicOn);
            //if(!musicOn){
            //    musicOn=true; // keep on for fadeout to finish
            //}
        }
        var sliderHold="";
        function music_vol_set(e) {
            sliderHold=this.sliderName;
            winHold=this.winName;
            console.log("holding slider "+this.sliderName);

            rewindex(this.winName);
        }
        function music_clouds_toggle() {
            winState['audio']['chk_music_clouds']=!winState['audio']['chk_music_clouds'];
            set_check('chk_music_clouds',winState['audio']['chk_music_clouds']);
            saveSetting("clouds-music",(winState['audio']['chk_music_clouds'])?null:true);

        }
        function natural_cam_set(e) {
            sliderHold=this.sliderName;
            winHold=this.winName;
            console.log("holding slider "+this.sliderName);

            rewindex(this.winName);
        }
        function sel_mountain_res() { // trig on selection open
            let nstate=false;
            winState['quality']['sel_mountain_res'] = nstate = !winState['quality']['sel_mountain_res'];

            if(nstate) {
                // open list
                $('sel_mountain_res').parentNode.className="selist selistbg active";
                //const ul=$('sel_mountain_res').parentNode.getElementsByTagName('ul')[0];
                const ul=$('sel_mountain_res').parentNode.getElementsByClassName('ult')[0];
                ul.style.visibility="visible";
                // initiate event listeners evl?
                $('selist_mr_512')._onClick=function(e) { set_mountain_res(512); };
                $('selist_mr_256')._onClick=function(e) { set_mountain_res(256); };
                $('selist_mr_128')._onClick=function(e) { set_mountain_res(128); };
                $('selist_mr_512').addEventListener('pointerdown', $('selist_mr_512')._onClick);
                $('selist_mr_256').addEventListener('pointerdown', $('selist_mr_256')._onClick);
                $('selist_mr_128').addEventListener('pointerdown', $('selist_mr_128')._onClick);
            } else {
                // close list? also for any other click
                selist_close('sel_mountain_res');
            }
        }
        function sel_render_res() { // trig on selection open
            let nstate=false;
            winState['quality']['sel_render_res'] = nstate = !winState['quality']['sel_render_res'];

            if(nstate) {
                // open list
                $('sel_render_res').parentNode.className="selist selistbg active";
                //const ul=$('sel_render_res').parentNode.getElementsByTagName('ul')[0];
                const ul=$('sel_render_res').parentNode.getElementsByClassName('ult')[0];
                ul.style.visibility="visible";
                // initiate event listeners evl?
                $('selist_rr_100')._onClick=function(e) { set_render_res(100); };
                $('selist_rr_80')._onClick=function(e) { set_render_res(80); };
                $('selist_rr_66')._onClick=function(e) { set_render_res(66); };
                $('selist_rr_50')._onClick=function(e) { set_render_res(50); };
                $('selist_rr_33')._onClick=function(e) { set_render_res(33); };
                $('selist_rr_25')._onClick=function(e) { set_render_res(25); };
                $('selist_rr_100').addEventListener('pointerdown', $('selist_rr_100')._onClick);
                $('selist_rr_80').addEventListener('pointerdown', $('selist_rr_80')._onClick);
                $('selist_rr_66').addEventListener('pointerdown', $('selist_rr_66')._onClick);
                $('selist_rr_50').addEventListener('pointerdown', $('selist_rr_50')._onClick);
                $('selist_rr_33').addEventListener('pointerdown', $('selist_rr_33')._onClick);
                $('selist_rr_25').addEventListener('pointerdown', $('selist_rr_25')._onClick);
            } else {
                // close list? also for any other click
                selist_close('sel_render_res');
            }
        }
        function selist_close(name="") { // trig selection close
            //alert('sel close');
            if(name=="sel_mountain_res") {
                $('sel_mountain_res').parentNode.className="selist selistbg";
                //const ul=$('sel_mountain_res').parentNode.getElementsByTagName('ul')[0];
                const ul=$('sel_mountain_res').parentNode.getElementsByClassName('ult')[0];
                ul.style.visibility="hidden";
                $('selist_mr_512').removeEventListener('pointerdown', $('selist_mr_512')._onClick);
                $('selist_mr_256').removeEventListener('pointerdown', $('selist_mr_256')._onClick);
                $('selist_mr_128').removeEventListener('pointerdown', $('selist_mr_128')._onClick);
            } else if(name=="sel_render_res") {
                $('sel_render_res').parentNode.className="selist selistbg";
                const ul=$('sel_render_res').parentNode.getElementsByClassName('ult')[0];
                ul.style.visibility="hidden";
                $('selist_rr_100').removeEventListener('pointerdown', $('selist_rr_100')._onClick);
                $('selist_rr_80').removeEventListener('pointerdown', $('selist_rr_80')._onClick);
                $('selist_rr_66').removeEventListener('pointerdown', $('selist_rr_66')._onClick);
                $('selist_rr_50').removeEventListener('pointerdown', $('selist_rr_50')._onClick);
                $('selist_rr_33').removeEventListener('pointerdown', $('selist_rr_33')._onClick);
                $('selist_rr_25').removeEventListener('pointerdown', $('selist_rr_25')._onClick);
            }
        }
        function set_mountain_res(res=512) { // selection buttons
            //alert(res);
            $('sel_mountain_res').innerHTML=res+"x"+res;
            //selist_close('sel_mountain_res');
            sel_mountain_res();

            saveSetting("mountain-resolution",res);

            // call dispose and regen mountains here
            // save setting and init with load
            disposeTile();
            const mountains=createTile(0, res);

            mountains.receiveShadow = true;
            mountains.name = "Mountains";
            mountains.visible = (tileInitialized)?true:false;

            refList['terrainGroup'].add(mountains);
            refList["tile"]=mountains;
        }
        function set_render_res(res=100) { // selection buttons
            //alert(res);
            $('sel_render_res').innerHTML=res+"%";
            //selist_close('sel_mountain_res');
            sel_render_res();

            REN_WIDTH=res*.01;
            REN_HEIGHT=res*.01;

            SHADOW_SCALE=res*.01;
            set_shadow_res();

            saveSetting("render-resolution-width",res);
            //saveSetting("render-resolution-height",res);

            onWindowResize();
        }
        function set_shadow_res() { // scale with render res
            refList['sunLight'].shadow.mapSize.width = shadowMapRes*SHADOW_SCALE;
            refList['sunLight'].shadow.mapSize.height = shadowMapRes*SHADOW_SCALE;
            refList['customSunShadowTarget'].setSize(shadowMapRes*SHADOW_SCALE,shadowMapRes*SHADOW_SCALE);
            refList['clockShadowLight'].shadow.mapSize.width = shadowMapRes*SHADOW_SCALE;
            refList['clockShadowLight'].shadow.mapSize.height = shadowMapRes*SHADOW_SCALE;
            refList['customClockShadowTarget'].setSize(shadowMapRes*SHADOW_SCALE,shadowMapRes*SHADOW_SCALE);
            // --

            refList['terrainMaterial'].uniforms.shadowRes.value = shadowMapRes*SHADOW_SCALE;
            refList['terrainMaterial'].uniforms.clockShadowRes.value = clockShadowMapRes*SHADOW_SCALE;
            refList['mountainMaterial'].uniforms.shadowRes.value = shadowMapRes*SHADOW_SCALE;
            refList['waterMaterial'].uniforms.shadowRes.value = shadowMapRes*SHADOW_SCALE;
            refList['waterMaterial'].uniforms.clockShadowRes.value = clockShadowMapRes*SHADOW_SCALE;

            for(let i=0;i<coinRef['coinMaterials'].length;i++) {
                coinRef['coinMaterials'][i].uniforms.shadowRes.value = shadowMapRes*SHADOW_SCALE;
                coinRef['coinMaterials'][i].uniforms.clockShadowRes.value = clockShadowMapRes*SHADOW_SCALE;
            }
            for(let i=0;i<refList['standardMatUpdate'].length;i++) {
                refList['standardMatUpdate'][i].uniforms.shadowRes.value = shadowMapRes*SHADOW_SCALE;
                refList['standardMatUpdate'][i].uniforms.clockShadowRes.value = clockShadowMapRes*SHADOW_SCALE;
            }
        }
        
        function shadow_helper_toggle() {
            if(helperLoaded) {
            	unloadHelpers();
            } else {
            	loadHelpers();
            }
            set_check("chk_shadow_helpers",helperLoaded);

            helperLoadedChk.value=helperLoaded;
        }

        function shadow_toggle() {
            shadowOn=(shadowOn>0)?0:1;
            if(winState['debug']['chk_shadow_debug'] && shadowOn==1) {
                shadowOn=2;
            }
            shadowOnChk.value=(shadowOn>0)?true:false;
            if(shadowOn==0) {
                set_check("chk_shadow_on",false);
            } else if(shadowOn==1) {
                set_check("chk_shadow_on",true);
            } else if(shadowOn==2) {
                set_check("chk_shadow_on",true);
            }
            saveSetting("shadow-on",(shadowOn)?null:true);

            refList['skyMaterial'].uniforms.shadowOn.value=shadowOn;
            refList['terrainMaterial'].uniforms.shadowOn.value=shadowOn;
            refList['mountainMaterial'].uniforms.shadowOn.value=shadowOn;
            refList['waterMaterial'].uniforms.shadowOn.value=shadowOn;

            for(let i=0;i<coinRef['coinMaterials'].length;i++) {
                coinRef['coinMaterials'][i].uniforms.shadowOn.value = shadowOn;
            }
            for(let i=0;i<refList['standardMatUpdate'].length;i++) {
                refList['standardMatUpdate'][i].uniforms.shadowOn.value = shadowOn;
            }

        }
        function clouds_toggle() {
            if(cloudsOn==2) {
                cloudsOn=0;
                refList["sky"].castShadow = false;
                set_check("chk_clouds_on",false);
            } else {
                cloudsOn=2;
                refList["sky"].castShadow = true;
                set_check("chk_clouds_on",true);
            }
            refList['skyMaterial'].uniforms.cloudsOn.value=cloudsOn;
            refList["customDepthMat"].uniforms.cloudsOn.value=cloudsOn;
            saveSetting("clouds-on",(cloudsOn)?null:true);

            cloudsOnChk.value=cloudsOn;
        }
        function shadow_ui_toggle(e,setstate=false) {
            if(!setstate) { // used by getsetting
                winState['quality']['chk_shadow_ui']=!winState['quality']['chk_shadow_ui'];
                set_check("chk_shadow_ui",winState['quality']['chk_shadow_ui']);
                saveSetting("shadow-ui",(winState['quality']['chk_shadow_ui'])?null:true);
            }

            let nStyle="none";
            if(winState['quality']['chk_shadow_ui']) {
                nStyle=""; // reset default

                var styles=document.getElementsByTagName('style');
                for(var i=0;i<styles.length;i++) {
                    if(styles[i].innerHTML.indexOf('/* override css shadows */')!=-1) {
                        styles[i].parentNode.removeChild(styles[i]);
                        //break;
                        i--;
                    }
                }
            } else { // override
                var customCSS="/* override css shadows */";
                customCSS+=".cmwindow, .menu ul { box-shadow: none; }";

                var style=document.createElement('style');
                style.textContent=customCSS;
                document.getElementsByTagName('head')[0].appendChild(style);
            }

            $('topbar').style.boxShadow=nStyle;
        }
        function mark_toggle() {
            markOn=!markOn; // mark_toggle
            refList["terrainMaterial"].uniforms.uToggleActive.value=(markOn)?1:0;
        }
        function waterrt_toggle() {
            waterHeightShow=!waterHeightShow;
            //alert("toggle waterHeightShow");
        }
        function rayshow_toggle() {
            showRayLines=!showRayLines;
            if(showRayLines) {
                for(let i=0;i<9;i++) {
                    const line=addRayLine();
                    refList['rayLines'].push(line);
                }
            } else {
                for(let i=0;i<9;i++) {
                    removeRayLine(0);
                }
            }
            
        }
        function flying_toggle() {
            player.isFlying=!player.isFlying;
            if(player.isFlying) player.isSwimming=false;
            //console.log((player.isFlying)?"is flying":"is not flying");
            if(player.isFalling) {
                player.isFalling=false;
                console.log("Falling cancelled by flying");
                lockHeight=0.0;
            }

        }
        function rendmode_toggle() {
            rendmode++;
            if(rendmode>4) rendmode=0
            refList["postMaterial"].uniforms.rendMode.value = rendmode;
        }
        function setRendMode(x) {
            rendmode=x;
            refList["postMaterial"].uniforms.rendMode.value = rendmode;
        }

        function anime_toggle(e) {
            winState['debug']['chk_anime_on']=!winState['debug']['chk_anime_on'];

            set_check("chk_anime_on",winState['debug']['chk_anime_on']);
            saveSetting("anime-on",(winState['debug']['chk_anime_on'])?true:null);
        }
        function intrusive_toggle(e) {
            winState['debug']['chk_intrusive_on']=!winState['debug']['chk_intrusive_on'];

            set_check("chk_intrusive_on",winState['debug']['chk_intrusive_on']);
            saveSetting("intrusive-on",(winState['debug']['chk_intrusive_on'])?true:null);
        }
        
        function vortex_toggle(e,setstate=false) {
            if(!setstate) { // used by getsetting
                vortexOn=!vortexOn;
                vortexOnChk.value = vortexOn;

                set_check("chk_vortex_on",vortexOn);
                saveSetting("vortex-on",(vortexOn)?true:null);
            }

            if(vortexOn) {
                refList["skyMaterial"].uniforms.vortexOn.value = 1;
                refList["customDepthMat"].uniforms.vortexOn.value = 1;
                fogDensity=0.001;
            } else {
                refList["skyMaterial"].uniforms.vortexOn.value = 0;
                refList["customDepthMat"].uniforms.vortexOn.value = 0;
                fogDensity=0.0005;
            }
            refList["terrainMaterial"].uniforms.fogDensity.value = fogDensity;
            refList["mountainMaterial"].uniforms.fogDensity.value = fogDensity;
        }
        function joy_toggle() {
            useJoysticks=!useJoysticks;
            saveSetting("use-joysticks",(useJoysticks)?true:null);

            refreshJoysticks();
        }
        function joy_strafe_toggle() {
            winState['joystick']['chk_joy_strafe']=!winState['joystick']['chk_joy_strafe'];
            set_check('chk_joy_strafe',winState['joystick']['chk_joy_strafe']);
            saveSetting("joy-strafe",(winState['joystick']['chk_joy_strafe'])?true:null);
        }
        function joy_inv_x_toggle() {
            winState['joystick']['chk_joy_inv_x']=!winState['joystick']['chk_joy_inv_x'];
            set_check('chk_joy_inv_x',winState['joystick']['chk_joy_inv_x']);
            saveSetting("joy-inv-x",(winState['joystick']['chk_joy_inv_x'])?true:null);
        }
        function joy_inv_y_toggle() {
            winState['joystick']['chk_joy_inv_y']=!winState['joystick']['chk_joy_inv_y'];
            set_check('chk_joy_inv_y',winState['joystick']['chk_joy_inv_y']);
            saveSetting("joy-inv-y",(winState['joystick']['chk_joy_inv_y'])?true:null);
        }
        function shadow_debug_toggle() {
            if(shadowOn==0) shadowOn=0;
            else if(shadowOn>1) shadowOn=1;
            else if(shadowOn>0) shadowOn=2;
            winState['debug']['chk_shadow_debug']=!winState['debug']['chk_shadow_debug'];
            if(shadowOn==0) {
                set_check("chk_shadow_debug",winState['debug']['chk_shadow_debug']);
            } else if(shadowOn==1) {
                set_check("chk_shadow_debug",winState['debug']['chk_shadow_debug']);
            } else if(shadowOn==2) {
                set_check("chk_shadow_debug",winState['debug']['chk_shadow_debug']);
            }

            refList['skyMaterial'].uniforms.shadowOn.value=shadowOn;
            refList['terrainMaterial'].uniforms.shadowOn.value=shadowOn;
            refList['mountainMaterial'].uniforms.shadowOn.value=shadowOn;
            refList['waterMaterial'].uniforms.shadowOn.value=shadowOn;

            for(let i=0;i<coinRef['coinMaterials'].length;i++) {
                coinRef['coinMaterials'][i].uniforms.shadowOn.value = shadowOn;
            }
            for(let i=0;i<refList['standardMatUpdate'].length;i++) {
                refList['standardMatUpdate'][i].uniforms.shadowOn.value = shadowOn;
            }
        }
        function wireframe_toggle() {
            let nstate=false;
            winState['debug']['chk_wireframe'] = nstate = !winState['debug']['chk_wireframe'];
            set_check("chk_wireframe",nstate);

            refList['terrainMaterial'].wireframe = nstate;
            //refList['edgeMaterial'].wireframe = nstate;
            //refList['railMaterial'].wireframe = nstate;
            //console.dir(refList['sky2']); // sphere
            refList['sky2'].material.wireframe = nstate;
            //refList['mountainMaterial'].wireframe = nstate;
            refList['skyMaterial'].wireframe = nstate;
            refList['waterMaterial'].wireframe = nstate;
            for(let i=0;i<coinRef['coinMaterials'].length;i++) {
                coinRef['coinMaterials'][i].wireframe = nstate;
            }
            for(let i=0;i<refList['standardMatUpdate'].length;i++) {
                refList['standardMatUpdate'][i].wireframe = nstate;
            }
            //refList['skyGroup'].visible=!nstate;
            //refList['sky'].visible = !nstate;
        }

        const shadowBias = 0.002; // slight negative for acne
        const shadowNormalBias = 0.01; // for bumpy terrain/mountains
        const shadowRadius = 4; // softens edges (if using BasicShadowMap, ignore for PCF)
        const shadowMapRes = 4096;

        // Base frustum (will update dynamically in animate)
        const shadowCamWidth = 12000;  // 
        const shadowCamHeight = 12000; // covers height variation
        const shadowCamFar = 12000;    // light-to-ground distance + margin
        const shadowCamNear = 1;     // close to light, avoids near-clip issues
        //const shadowCamFar = 6000;    // light-to-ground distance + margin
        //const shadowCamNear = -6000;     // close to light, avoids near-clip issues

        const clockShadowBias = 0.0008; // not negative ?
        const clockShadowNormalBias = 0.01; // for bumpy terrain/mountains
        const clockShadowRadius = 4; // softens edges (if using BasicShadowMap, ignore for PCF)
        const clockShadowMapRes = 1024;

        const clockShadowCamWidth = 120;  // 
        const clockShadowCamHeight = 120; // covers height variation
        const clockShadowCamFar = 120;    // light-to-ground distance + margin
        const clockShadowCamNear = 1;     // close to light, avoids near-clip issues

        const sunOrbit = 6000.0; // shadowCamW/H *.5
        const nearShadowOrbit = 60.0; // clockShadowCamW/H *.5

        // Define this once somewhere global/top-scope
        const shadowBiasMatrix = new THREE.Matrix4().set(
            0.5, 0.0, 0.0, 0.5,
            0.0, 0.5, 0.0, 0.5,
            0.0, 0.0, 0.5, 0.5,
            0.0, 0.0, 0.0, 1.0
        );

        function getHeightAt(worldX, worldZ) {
            if (!heightData) return offsetHeight;
        
            // 1. Map world coords to 0-1 range based on tile size
            // Assuming tile is centered at 0,0
            let u = (worldX / tileSize) + 0.5;
            let v = (worldZ / tileSize) + 0.5;
        
            // 2. Clamp to edge to prevent errors
            u = Math.max(0, Math.min(1, u));
            v = Math.max(0, Math.min(1, v));
        
            // 3. Convert to pixel coordinates
            const x = Math.floor(u * (imgWidth - 1));
            const y = Math.floor(v * (imgHeight - 1));
        
            // 4. Index into the RGBA array (4 bytes per pixel)
            const index = (y * imgWidth + x) * 4;
            const r = heightData[index];
        
            // Normalize 0-255 to 0-1
            const h = r / 255.0;
        
            return (heightScale * h) + offsetHeight;
        }

        const tileSize = 4000; // Widened to hide edges easier
        let tileSegments = 512; // Increased for better detail/resolution
        const noiseScale = 100;
        const numTiles = 5; // Keep this many tiles visible
        const heightScale = 2000;
        const offsetHeight = -1000;

        let markScale=5;
        let heightData = null;
        let imgWidth = 0, imgHeight = 0;

        // Function to create/update a tile
        function createTile(zOffset, segments) {
            console.log('creating tile zoffset='+zOffset+' segments='+segments);
            const geometry = new THREE.PlaneGeometry(tileSize, tileSize, segments, segments);
            geometry.rotateX(-Math.PI / 2);
            const mesh = new THREE.Mesh(geometry, refList["mountainMaterial"]);
            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 disposeTile() {
            const mesh=refList['tile'];
            if (!mesh) return;
        
            // 1. Dispose Geometry
            mesh.geometry.dispose();

            // 2. Dispose Material & Texture (Shader specific)
            //if (mesh.material.map) {
            //    mesh.material.map.dispose();
            //}
            // custom shader despawn
            //if (mesh.material.uniforms && mesh.material.uniforms.tDiffuse) {
            //    mesh.material.uniforms.tDiffuse.value.dispose();
            //}
            //mesh.material.dispose();
            //refList['mountainMaterial'].dispose();
        
            // 3. Scene Cleanup
            refList['terrainGroup'].remove(mesh);
            //scene.remove(group);
        }

        function updateTileHeights(mesh) {
            const geo = mesh.geometry;
            const pos = geo.attributes.position;
        
            for (let i = 0; i < pos.count; i++) {
                // Get current vertex coords
                const x = pos.getX(i);
                const z = pos.getZ(i); // Note: Plane is rotated, so Y is height
        
                // Calculate height
                const h = getHeightAt(x, z);
        
                // Update the Y component (height)
                pos.setY(i, h);
            }
        
            pos.needsUpdate = true;
            geo.computeVertexNormals(); // Essential for correct lighting/shading
        }
        
        const normalMatrix = new THREE.Matrix3(); // create once and reuse
        const mvpMatrix = new THREE.Matrix4();

        const up = new THREE.Vector3(0, 0, -1); // Depends on your texture orientation

        const tilt = new THREE.Matrix4().makeRotationX(Math.PI / 2); // tilt used for projector hitproject
        
        // One-time setup
        var loading=0;
        var cDensity=0.5;
        var skyDepthOn=true;
        var musicLoaded=false;
        var audioContextInitiated=false;
        var audioContext, source, analyser, bufferLength, dataArray;
        var sunTime = {}; // theta orient around horizontally, phi vertical/altitude
        var moonTime = {};

        let refList = {
            "sky": null,
            "skyGroup": null,
            "terrainGroup": null,
            "ambientLight": null,
            "sunLight": null,
            "customDepthMat": null,
            "skyMaterial": null,
            "floor": null,
            "sky2": null,
            "clockShadowLight": null,
            "camera": null,
            "playerGroup":null, // wrapper for camera
            "tile": null,
            "terrainMaterial": null, // for floor
            "mountainMaterial": null, // distant mountain tile
            "imageMaterial": null,
            "railMaterial": null,
            "railFlipMaterial": null,
            "edgeMaterial": null,
            "edgeList": [],
            "railList": [],
            "projectorMatrix": null,
            "projectionMatrix": null,
            "projectionMatrix": null,
            "tempEuler": null,
            "rotMat": null,
            "beamGroup": null,
            "beamList": [],
            "rayLines": [],
            "lineRedMat": null,
            "customDepthMaterial": null, // for post
            "customWaterDepthMaterial": null, // for post
            "cachedEnv": [],
            "standardMat": null,
            "standardMatUpdate": [], // for update uniforms
        }

        const manager = new THREE.LoadingManager();
        manager.onLoad = function ( ) {
            console.log( "All textures loaded!" );
            // Set a flag that all assets are ready for the simulation
            refList['assetsReady'] = true;
        };

        function isFloorReady() {
            const floor = refList['floor'];
            if (!floor) return false;
        
            let ret=false;
            // Check if geometry is actually populated
            const hasGeometry = floor.geometry && floor.geometry.attributes.position.count > 0;
        
            // In edit mode, you might have a different check
            //return hasGeometry;
            if(hasGeometry) {
                const origin = refList['playerGroup'].position.clone().add(new THREE.Vector3(0, 1.0, 0));
                downRay.set(origin, downVec);
        
                const terrainObjects = [refList["floor"], ...physicsBodies]; 
                const hits = downRay.intersectObjects(terrainObjects);
        
                if (hits.length > 0) {
                    const groundY = hits[0].point.y;
                    //if(hits[0].object.name=="Floor") {
                        // truly grounded?
                        //gravityInitialized=true;
                        //fakeGround=false;
                        ret=true;
                        console.log('pre-hit '+hits[0].object.name+', setting gavityInitialized');
                    //}
                }
            }

            return ret;
        }

        let isLocked = false;
        // isLocked input mode disable joystick
        // joystick enable separate with J ?
        let initialized = false;
        let tileInitialized = false; // since async
        let gravityInitialized = false;
        var lastPlayerY = 0.0;
        let documentReady = false;
        let tabTime = 0;
        let fakeGround = true;
        let tabHidden = false;
        let cameraFov = 75;
        let waterHeightShow = false;
        let showRayLines=false;
        document.body.onload=function() {
        onWindowResize();
        setTimeout(function() {
            documentReady=true;
            },3000);
        }
        let autoStartMusic = false;
        function init() {
            const get_mv=getSetting("music-volume");
            const get_mo=getSetting("music-on");
            const get_nc=getSetting("natural-camera");
            if(get_mv!=null) {
                winState['audio']['slider_vol1']=get_mv;
            }
            if(get_nc!=null) {
                winState['quality']['slider_sway1']=get_nc;
            }
            if(get_mo!=null) {
                musicOn=true;
                musicOnChk.value=musicOn;
                set_check("chk_music_on",musicOn);
            }
            const get_so=getSetting("shadow-on");
            const get_su=getSetting("shadow-ui");
            const get_co=getSetting("clouds-on");
            if(get_so!=null) {
                shadowOn=!get_so; // invert default
                shadowOnChk.value=shadowOn;
                set_check("chk_shadow_on",shadowOn);
            }
            if(get_su!=null) {
                winState['quality']['chk_shadow_ui']=!get_su;
                set_check("chk_clouds_on",!get_su);
                shadow_ui_toggle(null,true);
            }
            if(get_co!=null) {
                cloudsOn=!get_co;
                cloudsOnChk.value=cloudsOn;
                set_check("chk_clouds_on",cloudsOn);
            }
            const get_vo=getSetting("vortex-on");
            const get_cm=getSetting("clouds-music");
            const get_uj=getSetting("use-joysticks");
            if(get_vo!=null) {
                vortexOn=get_vo;
                vortexOnChk.value=vortexOn;
                set_check("chk_vortex_on",vortexOn);
            }
            if(get_cm!=null) {
                winState['audio']['chk_music_clouds']=!get_cm;
                set_check("chk_music_clouds",!get_cm);
            }
            if(get_uj!=null) {
                useJoysticks=get_uj;
                useJoysticksChk.value=useJoysticks;
                set_check("chk_joy_on",useJoysticks);
            }
            const get_js=getSetting("joy-strafe");
            const get_jx=getSetting("joy-inv-x");
            const get_jy=getSetting("joy-inv-y");
            if(get_js!=null) {
                winState['joystick']['chk_joy_strafe']=get_js;
                set_check("chk_joy_strafe",get_js);
            }
            if(get_jx!=null) {
                winState['joystick']['chk_joy_inv_x']=get_jx;
                set_check("chk_joy_inv_x",get_jx);
            }
            if(get_jy!=null) {
                winState['joystick']['chk_joy_inv_y']=get_jy;
                set_check("chk_joy_inv_y",get_jy);
            }
            const get_ao=getSetting("anime-on");
            const get_io=getSetting("intrusive-on");
            const get_mr=getSetting("mountain-resolution");
            if(get_ao!=null) {
                winState['debug']['chk_anime_on']=get_ao;
                set_check("chk_anime_on",get_ao);
            }
            if(get_io!=null) {
                winState['debug']['chk_intrusive_on']=get_io;
                set_check("chk_intrusive_on",get_io);
            }
            if(get_mr!=null) {
                $('sel_mountain_res').innerHTML=get_mr+"x"+get_mr;
                tileSegments=get_mr;
            }
            const get_rrw=getSetting("render-resolution-width");
            //const get_rrh=getSetting("render-resolution-height");
            if(get_rrw!=null) {
                $('sel_render_res').innerHTML=get_rrw+"%";
                REN_WIDTH=get_rrw*.01;
                REN_HEIGHT=get_rrw*.01;
                // shadow set at bottom
            }

            const textureLoader = new THREE.TextureLoader(manager);

            // Load Textures
            // for terrain splat
            const grassDiffuse = textureLoader.load('./textures/grass2.webp');
            const grassNormal = textureLoader.load('./textures/grass2_normal.webp');
            const grassRough = textureLoader.load('./textures/grass2_rough.webp');
            const dirtDiffuse = textureLoader.load('./textures/dirt2.webp');
            const dirtNormal = textureLoader.load('./textures/dirt2_normal.webp');
            const dirtRough = textureLoader.load('./textures/dirt2_rough.webp');
            const roadDiffuse = textureLoader.load('./textures/road1.webp');
            const roadNormal = textureLoader.load('./textures/road1_normal.webp');
            const roadRough = textureLoader.load('./textures/road1_rough.webp');
            // for mountain
            const rockDiffuse = textureLoader.load('./textures/rock2.webp');
            const rockNormal = textureLoader.load('./textures/rock2_normal.webp');
            const rockRough = textureLoader.load('./textures/rock2_rough.webp');
            const snowDiffuse = textureLoader.load('./textures/snow.webp');
            const snowNormal = textureLoader.load('./textures/snow_normal.webp');
            const snowRough = textureLoader.load('./textures/snow_rough.webp');
            //
            const splat1 = textureLoader.load('./textures/splat1.webp?v=4');
            const mark1 = textureLoader.load('./textures/mark.001.webp?v=2');
            
            [grassDiffuse, grassNormal, grassRough].forEach(tex => {
                tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
                tex.repeat.set(100, 100); // Adjust based on your scene scale
            });
            [dirtDiffuse, dirtNormal, dirtRough].forEach(tex => {
                tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
                tex.repeat.set(100, 100); // Adjust based on your scene scale
            });
            [roadDiffuse, roadNormal, roadRough].forEach(tex => {
                tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
                tex.repeat.set(100, 100); // Adjust based on your scene scale
            });
            [rockDiffuse, rockNormal, rockRough].forEach(tex => {
                tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
                tex.repeat.set(100, 100); // Adjust based on your scene scale
            });
            [snowDiffuse, snowNormal, snowRough].forEach(tex => {
                tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
                tex.repeat.set(100, 100); // Adjust based on your scene scale
            });
            mark1.wrapS = mark1.wrapT = THREE.ClampToEdgeWrapping;

            // 1. Scene & Camera
            scene = new THREE.Scene();
            //scene.background = new THREE.Color(0x87ceeb); // Sky blue
            //scene.background = new THREE.Color(0x000000); // black
            //scene.fog = new THREE.Fog(0x87ceeb, 10, 50);
            
            
            
            sunTime=getDirection(170,90); // theta orient around horizontally, phi vertical/altitude
            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)
            // light intensities may be separate handled in custom shaderrs*
            const ambientLight = new THREE.AmbientLight(0xffffff, 0.05); // Increased ambient for better brightness
            scene.add(ambientLight);
            const sunLight = new THREE.DirectionalLight(0xffffff, 1.5); // Increased intensity

            
            //Add this right after creating the lights (before adding to scene):
            // Main shadow map never sees layer 5
            //sunLight.shadow.camera.layers.disableAll(); // Clear all layers first
            //sunLight.shadow.camera.layers.enable(3);    // enable mountain/terrain
            //sunLight.shadow.camera.layers.enable(1);    // enable clouds from sky
            
            scene.fog = new THREE.FogExp2(0xaaccff, fogDensity); // 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


            // 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.0006 }, // Sun disc size
                    moonSize: { value: 0.0024 }, // Sun disc size
                    xtime: { value: 0.0 },
                    daytime: { value: 0.0 },
                    cloudsOn: { value: cloudsOn }, // 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: (initialized==-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
                    cloudSquish: { value: 2.5 }, // 1.0 = no squish, 2.0+ = stronger horizon stretch (distant feel)
                    cloudRampLow: { value: 0.01 }, // Ramp start (bottom fade-in, 0-0.3)
                    cloudRampHigh: { value: 0.99 }, // Ramp peak/end (top fade-out, 0.5-0.8)
                    cloudRampStrength: { value: 1.2 }, // Overall multiplier intensity (0.5-1.5 for subtlety)
                    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
                    horizonGlowColorDead: { value: new THREE.Color(0.7, 0.7, 0.76) },
                    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 },
                    vortexOn: { value: vortexOn },          // 0=off, 1=on (toggle for death mode)
                    vortexSpeed: { value: -0.2 },     // Rotation speed (0.1 slow ethereal → 0.5 fast storm)
                    vortexNumArms: { value: 4.0 },  // 2.0–4.0 for visible arms (always present)
                    vortexSwirl: { value: 0.8 },    // Tighter for defined arms
                    vortexNoiseScale: { value: 5.0 },  // Slightly higher for variability
                    vortexNoiseDetail: { value: 1.0 }, // Strength of extra noise layer (0.2–0.5)
                    vortexIntensity: { value: 2.0 }, // Overall strength (0.5 subtle → 1.2 dramatic)
                    vortexColorDark: { value: new THREE.Color(0.01, 0.01, 0.01) },  // Black-ish base
                    vortexColorLight: { value: new THREE.Color(0.37, 0.37, 0.39) }, // White/gray highlights
                    vortexGlowColor: { value: new THREE.Color(0.95, 0.95, 1.0) },  // soft bright white-yellow
                    vortexGlowSize: { value: 3.0 },     // 0.7–0.95 = size of the bright area
                    vortexGlowIntensity: { value: 1.2 }, // 0.8–2.0 strength
                    vortexGlowSharpness: { value: 1.5 }, // 1.0 soft → 3.0+ sharper falloff
                    shadowOn: { value: shadowOn },
                },
                vertexShader: vertexShaderSky,
                fragmentShader: fragmentShaderSky,
                side: THREE.BackSide, // Inside out
                depthWrite: skyDepthOn,
                depthTest: true,    // sky reads depth (but since it's first, no issue)
                fog: false,
            });
            const skyGroup = new THREE.Group();
            scene.add(skyGroup);
            
            //console.dir(skyGroup);

            const sky = new THREE.Mesh(skyGeometry, skyMaterial);
            //sky.layers.set(1);
            //scene.add(sky);
            skyGroup.add(sky);
            sky.name="Sky";
            sky.rotation.set(0.0,0.0,90.0*radian);

            // During your initial scene/object initialization setup:
            // Layer 0 = Default Scene (Water)
            // Layer 1 = Sun Shadows Casting (sky)
            // Layer 2 = Clock Shadows Castine (everything else)
            // layers 1 and 2 are strictly reserved by Three.js to process the left and right eye configurations. For XR projects
            sky.layers.enable(1);
            
            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
                    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: cloudsOn }, // match with cloudquality startup
                    vortexOn: { value: vortexOn },          // 0=off, 1=on (toggle for death mode)
                    //vortexSpeed: { value: -0.2 },     // Rotation speed (0.1 slow ethereal → 0.5 fast storm)
                    //vortexNumArms: { value: 4.0 },  // 2.0–4.0 for visible arms (always present)
                    //vortexSwirl: { value: 0.8 },    // Tighter for defined arms
                    //vortexNoiseScale: { value: 5.0 },  // Slightly higher for variability
                    //vortexNoiseDetail: { value: 1.0 }, // Strength of extra noise layer (0.2–0.5)
                    //vortexIntensity: { value: 2.0 }, // Overall strength (0.5 subtle → 1.2 dramatic)
                    //vortexColorDark: { value: new THREE.Color(0.01, 0.01, 0.01) },  // Black-ish base
                    //vortexColorLight: { value: new THREE.Color(0.37, 0.37, 0.39) }, // White/gray highlights
                    cloudSquish: { value: 2.5 }, // 1.0 = no squish, 2.0+ = stronger horizon stretch (distant feel)
                    cloudRampLow: { value: 0.01 }, // Ramp start (bottom fade-in, 0-0.3)
                    cloudRampHigh: { value: 0.99 }, // Ramp peak/end (top fade-out, 0.5-0.8)
                    cloudRampStrength: { value: 1.2 }, // Overall multiplier intensity (0.5-1.5 for subtlety)

                },
                //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,
            });
        
            sky.castShadow = true;
            //sky.customDepthMaterial = null;
            sky.customDepthMaterial = customDepthMat;
            sky.customDepthMaterial.shadowSide = THREE.FrontSide;
            sky.receiveShadow = false;
            //sky.rotation.set(0.0,0.0,90.0*radian);
        
            //camera = new THREE.PerspectiveCamera(75, SCR_WIDTH / SCR_HEIGHT, 0.1, 1000);
            const camera = new THREE.PerspectiveCamera(cameraFov, SCR_WIDTH / SCR_HEIGHT, 0.1, 10000);

            //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)
            camera.position.set(0, 0, 0);
            
            camera.updateProjectionMatrix();

            const playerGroup = new THREE.Group();
            scene.add(playerGroup);

            playerGroup.position.set(0, player.height, 15);
            playerGroup.add(camera);
            //shadowCamera.layers.enableAll(); // full scene for shadows
            //shadowCamera.layers.disable(2); // norther lights
            ////shadowCamera.layers.disable(5); // sunclock
            //shadowCamera.matrixAutoUpdate = false; // manual control if needed

            SCR_WIDTH=window.innerWidth;
            SCR_HEIGHT=window.innerWidth;

            // 2. Renderer
            const renderer = new THREE.WebGLRenderer({ antialias: true, stencil: true });
            renderer.setSize(SCR_WIDTH, SCR_HEIGHT);
            renderer.setPixelRatio(window.devicePixelRatio);
            renderer.shadowMap.enabled = false;
            renderer.shadowMap.type = THREE.PCFSoftShadowMap;

            // To use LinearFilter for VSMs, you might need more specific texture handling:
             //renderer.shadowMap.type = THREE.VSMShadowMap; // This uses LinearFilter by default
            // If using VSM and needing manual control:
             //renderer.shadowMap.type = THREE.VSMShadowMap; // looks great but heavy - enable msgFilter where Wrap and shadowinit x)

            //document.body.appendChild(renderer.domElement);
            $('mainframe').appendChild(renderer.domElement);
            
            // One-time setup (after main sunLight)
            const clockShadowLight = new THREE.DirectionalLight(0xffffff, 1.0); // dummy intensity?

            // Clock shadow light: ONLY layer 5 (sunclock)
            //clockShadowLight.shadow.camera.layers.disableAll(); // Clear all layers
            //clockShadowLight.shadow.camera.layers.enable(5);    // Only enable layer 5
            clockShadowLight.shadow.camera.updateProjectionMatrix();


            //sunLight.position.set(100, 100, 100); // Sun position for shadows/lighting (match sunDirection in skyMaterial)
            sunLight.position.set(sunTime.x*sunOrbit,sunTime.y*sunOrbit,sunTime.z*sunOrbit); // Sun position from sphere coords
            
            sunLight.target = new THREE.Object3D();
            sunLight.target.position.set(0, 0, 0); // world origin or camera ground
            sunLight.target.updateMatrixWorld();

            // shadow maps               
            sunLight.castShadow = true;
            sunLight.shadow.mapSize.width = shadowMapRes;
            sunLight.shadow.mapSize.height = shadowMapRes;
            sunLight.shadow.bias = shadowBias;        
            sunLight.shadow.normalBias = shadowNormalBias;     
            sunLight.shadow.radius = shadowRadius;

            const customSunShadowTarget = new THREE.WebGLRenderTarget(shadowMapRes, shadowMapRes, {
                minFilter: THREE.LinearFilter,
                magFilter: THREE.LinearFilter,
                format: THREE.RGBAFormat // Or DepthFormat if WebGL2 is explicitly preferred
            });
                
            sunLight.shadow.camera = new THREE.OrthographicCamera(
                -shadowCamWidth / 2,
                 shadowCamWidth / 2,
                 shadowCamHeight / 2,
                -shadowCamHeight / 2,
                 shadowCamNear,
                 shadowCamFar
            );

            // 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);*/

            clockShadowLight.castShadow = true;
            clockShadowLight.shadow.mapSize.width = clockShadowMapRes;
            clockShadowLight.shadow.mapSize.height = clockShadowMapRes;
            clockShadowLight.shadow.bias = clockShadowBias; // lower for small scale (tune -0.00005 to -0.0005)
            clockShadowLight.shadow.normalBias = clockShadowNormalBias; // lighter for clock bumps
            clockShadowLight.shadow.radius = clockShadowRadius;
            const customClockShadowTarget = new THREE.WebGLRenderTarget(clockShadowMapRes, clockShadowMapRes, {
                minFilter: THREE.LinearFilter,
                magFilter: THREE.LinearFilter,
                format: THREE.RGBAFormat // Or DepthFormat if WebGL2 is explicitly preferred
            });

            clockShadowLight.shadow.camera = new THREE.OrthographicCamera(
                -clockShadowCamWidth / 2,
                 clockShadowCamWidth / 2,
                 clockShadowCamHeight / 2,
                -clockShadowCamHeight / 2,
                 clockShadowCamNear,
                 clockShadowCamFar
            );

            clockShadowLight.position.set(sunTime.multiplyScalar(nearShadowOrbit));
            clockShadowLight.shadow.camera.updateProjectionMatrix();

            //console.log(sunLight.position);
            //console.dir(clockShadowLight);
            //clockShadowLight.target.position.copy(refList["camera"].position);
            clockShadowLight.target.position.set(0,0,0);
            clockShadowLight.target.updateMatrixWorld();

            skyGroup.add(sunLight);
            skyGroup.add(sunLight.target);
            skyGroup.add(clockShadowLight); // or keep hidden if not needed visually
            skyGroup.add(clockShadowLight.target); // Must add target too!
            skyGroup.add(ambientLight);
            
            clockShadowLight.castShadow = true;

            sunLight.shadow.camera.layers.set(1);
            clockShadowLight.shadow.camera.layers.set(2);
            
            renderer.sortObjects = true;
            renderer.autoClear = false; // Prevents implicit clearing between renders
            renderer.autoClearColor = false;
            renderer.autoClearDepth = false;
            renderer.autoClearStencil = false;
            renderer.shadowMap.autoUpdate = false; // Stop automatic shadow map update

            // A standard depth material to overwrite mesh materials during the shadow pass
            const shadowDepthMaterial = new THREE.MeshDepthMaterial({
                depthPacking: THREE.RGBADepthPacking, // Packs 24-bit depth into RGBA for excellent precision
                //depthPacking: THREE.BasicDepthPacking, // single channel
                //side: THREE.BackSide // Try BackSide instead of DoubleSide to clear up top-surface artifacts
                side: THREE.DoubleSide,
                transparent: false,
                alphaThreshold: 0.5, // clip
            });

            clock = new THREE.Clock();

            const standardMat=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 },
                    map: { value: null },
                    normalMap: { value: null },
                    roughnessMap: { value: null },
                    repeatScaleX: { value: 1.0 },
                    repeatScaleY: { value: 1.0 },
                    flipNormal: { value: 0 }, // 0 1(x) 2(y) or 3(xy)
                    flatFace: { value: 1 },
                    // Add more if needed: metalnessMap, aoMap, etc.
                    lightDir: { value: sunLight.position.normalize() },
                    lightDir2: { value: sunLight.position.normalize().negate() },
                    daytime: { value: 0.0 },
                    //xtime: { value: 0.0 },
                    cameraPosition: { value: camera.position.clone() },
                    fogColor: { value: new THREE.Color(0xffeeff) }, // Horizon haze
                    fogDensity: { value: fogDensity },
                    sunShadowMap: { value: null },  // will set in animate
                    shadowBias: { value: shadowBias },
                    shadowNormalBias: { value: shadowNormalBias }, // pass your const 0.1

                    sunShadowMap: { value: null },
                    sunShadowMatrix: { value: new THREE.Matrix4() },
                    clockShadowMap: { value: null },
                    clockShadowMatrix: { value: new THREE.Matrix4() },
                    shadowRadius: { value: shadowRadius },
                    clockShadowRadius: { value: clockShadowRadius },
                    shadowRes: { value: shadowMapRes },
                    clockShadowRes: { value: clockShadowMapRes },
                    shadowOn: { value: shadowOn },
                    shadowBias: { value: shadowBias },
                    clockShadowBias: { value: clockShadowBias },
                    shadowNormalBias: { value: shadowNormalBias },
                    clockShadowNormalBias: { value: clockShadowNormalBias },
                    shadowOn: { value: shadowOn },

                    alphaThreshold: { value: 0.5 }, // clip
                    transparent: { value: 0 }, // clip or alpha
                    metarough: { value: 0 }, // 0, 1 or 2
                    roughness: { value: 0.5 }, // default val 0.5
                    metallic: { value: 0.0 }, // default val 0.0
                    rimShineStrength: { value: 0.28 },
                    rimShineColor: { value: new THREE.Color(1.0, 0.98, 0.92) },
                    ambientMulti: { value: 1.0 },
                    
                    normalStrength: { value: 0.8 },
                    //uTextureMatrix: { value: new THREE.Matrix4().makeTranslation(9999, 9999, 9999) }, // prevent init stretch 9999
                    //uProjectionTexture: { value: mark1 },
                    //uToggleActive: { value: (markOn)?1:0 },
                }]),
                //},
                vertexShader: vertexShaderStandard,
                fragmentShader: fragmentShaderStandard,
                side: THREE.FrontSide,
                fog: true,
                lights: true,
                transparent: false,
                depthWrite: true,
                depthTest: true,
            });

            //const mountainMat = new THREE.MeshStandardMaterial({
            //    //color: { value: new THREE.Color(0xffffff) },
            //    map: rockDiffuse,
            //    normalMap: rockNormal,
            //    roughnessMap: rockRough,
            //    //roughness: 0.8
            //});
            const mountainMat=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 },
                    map: { value: rockDiffuse },
                    normalMap: { value: rockNormal },
                    roughnessMap: { value: rockRough },
                    snowTex: { value: snowDiffuse },
                    normalSnow: { value: snowNormal },
                    roughSnow: { value: snowRough },
                    repeatScale: { value: 250.0 },
                    // 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: fogDensity },
                    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 },
                    shadowRes: { value: shadowMapRes },
                    shadowOn: { value: shadowOn },
                    alphaThreshold: { value: 0.5 },
                    snowStart: { value: 0.6 },     // Tune these
                    snowEnd: { value: 0.8 },
                    rimPower: { value: 5.0 }, // snow rim
                    rimExtend: { value: 0.6 },
                    heightScale: { value: 420.0 },
                    edgeMin: { value: 0.005 },      // Lower for more sensitive edges
                    edgeMax: { value: 0.025 },      // Higher for stricter sharp edges
                    rimShineStrength: { value: 0.28 }, // shine rim
                    rimShineColor: { value: new THREE.Color(1.0, 0.98, 0.92) },
                }]),
                //},
                vertexShader: vertexShaderMountain,
                fragmentShader: fragmentShaderMountain,
                side: THREE.FrontSide,
                fog: true,
                lights: true,
                transparent: false,
                depthWrite: true,
                depthTest: true,
            });
            
            const terrainGroup = new THREE.Group();
            scene.add(terrainGroup);
            
            const beamGroup = new THREE.Group();
            scene.add(beamGroup);

            // 4. Floor (The Collider)
            const floorGeo = new THREE.PlaneGeometry(100, 100);
            //const floorMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.8 });
            //const floorMat = new THREE.MeshStandardMaterial({
            //    map: grassDiffuse,
            //    normalMap: grassNormal,
            //    roughnessMap: grassRough,
            //    roughness: 0.8
            //}); 
            const terrainMat = new THREE.ShaderMaterial({
                uniforms: THREE.UniformsUtils.merge([
                    THREE.UniformsLib.lights,
                    THREE.UniformsLib.fog,
                    {
                    splatMix: { value: 3 }, // 0 = base only, 1 = base + red
                    splatMix1: { value: grassDiffuse },
                    splatMix2: { value: roadDiffuse },
                    splatMix3: { value: dirtDiffuse },
                    splatMix4: { value: rockDiffuse },
                    normalSplatMix1: { value: grassNormal },
                    normalSplatMix2: { value: roadNormal },
                    normalSplatMix3: { value: dirtNormal },
                    normalSplatMix4: { value: rockNormal },
                    roughSplatMix1: { value: grassRough },
                    roughSplatMix2: { value: roadRough },
                    roughSplatMix3: { value: dirtRough },
                    roughSplatMix4: { value: rockRough },
                    splatTex: { value: splat1 },
                    repeatScale: { value: 50.0 },
                    splatScale: { value: 1.0 }, // Matches splatTex.repeat; adjust for desired UV scaling
                    lightDir: { value: sunLight.position.normalize() },
                    lightDir2: { value: sunLight.position.normalize().negate() },
                    daytime: { value: 0.0 },
                    xtime: { value: 0.0 },
                    cameraPosition: { value: camera.position.clone() },
                    fogColor: { value: new THREE.Color(0xffeeff) },
                    fogDensity: { value: fogDensity },
                    sunShadowMap: { value: null },
                    sunShadowMatrix: { value: new THREE.Matrix4() },
                    clockShadowMap: { value: null },
                    clockShadowMatrix: { value: new THREE.Matrix4() },
                    shadowBias: { value: shadowBias },
                    clockShadowBias: { value: clockShadowBias },
                    shadowNormalBias: { value: shadowNormalBias },
                    clockShadowNormalBias: { value: clockShadowNormalBias },
                    shadowRadius: { value: shadowRadius },
                    clockShadowRadius: { value: clockShadowRadius },
                    shadowRes: { value: shadowMapRes },
                    clockShadowRes: { value: clockShadowMapRes },
                    shadowOn: { value: shadowOn },
                    rimShineStrength: { value: 0.28 },
                    rimShineColor: { value: new THREE.Color(1.0, 0.98, 0.92) },
                    ambientMulti: { value: 1.0 },
                    
                    normalStrength: { value: 0.8 },
                    uTextureMatrix: { value: new THREE.Matrix4().makeTranslation(9999, 9999, 9999) }, // prevent init stretch 9999
                    uProjectionTexture: { value: mark1 },
                    uToggleActive: { value: (markOn)?1:0 },
                }]),
                vertexShader: vertexShaderTerrain,
                fragmentShader: fragmentShaderTerrain,
                //side: THREE.FrontSide,
                side: THREE.DoubleSide,
                fog: true,
                lights: true,
                depthWrite: true,
                depthTest: true,
            });

            //const floor = new THREE.Mesh(floorGeo, floorMat);
            const floor = new THREE.Mesh(floorGeo, terrainMat);
            floor.rotation.x = -Math.PI / 2;
            floor.receiveShadow = true;
            floor.castShadow = true;
            floor.name = "Floor";
            floor.visible = false;
            floor.layers.enable(1);
            floor.layers.enable(2);
            terrainGroup.add(floor);

            floor.geometry.computeBoundingBox();
            floor.geometry.computeBoundingSphere();
            floor.updateMatrixWorld(); // Ensure world matrix is updated for the raycaster

            // other misc objects
            const skyGeo2 = new THREE.SphereGeometry(3, 32, 24);
            const skyMat2 = new THREE.MeshStandardMaterial({
                color: { value: new THREE.Color(0x330099) },
                roughness: 0.2,
                //side: THREE.DoubleSide
            });
            const sky2 = new THREE.Mesh(skyGeo2, skyMat2);
            //sky2.material = skyMat2;
            sky2.rotation.x = -Math.PI / 2;
            sky2.position.set(0, 3, 0);
            sky2.receiveShadow = true;
            sky2.castShadow = true;
            sky2.name = "sky2";
            sky2.visible = false;
            sky2.layers.enable(2);
            terrainGroup.add(sky2);
            
            //const loader = new THREE.TextureLoader();
            //loader.load('textures/dispheight.webp', (texture) => {
            textureLoader.load('./textures/dispnoise.webp?v=2', (texture) => {
                const img = texture.image;
                imgWidth = img.width;
                imgHeight = img.height;
            
                const canvas = document.createElement('canvas');
                canvas.width = imgWidth;
                canvas.height = imgHeight;
                const ctx = canvas.getContext('2d');
                ctx.drawImage(img, 0, 0);
            
                // Get RGBA data
                const imageData = ctx.getImageData(0, 0, imgWidth, imgHeight);
                heightData = imageData.data; // This is a Uint8ClampedArray
                
                // Now trigger your tile creation
                const mountains = createTile(0, tileSegments); 
                mountains.receiveShadow = true;
                mountains.name = "Mountains";
                mountains.visible = false;
                mountains.layers.enable(1);

                terrainGroup.add(mountains);
                refList["tile"]=mountains;

                tileInitialized=true;

                setTimeout(function() {
                    if(initialized) {
                        makeEnvVisible();
                    }
                });
            });
            // gpu computed variant ie no collision needed
            // if using this, drop the canvas method
            //const terrainMat = new THREE.MeshStandardMaterial({
            //    map: colorTexture,
            //    displacementMap: heightmapTexture,
            //    displacementScale: heightScale,
            //    displacementBias: offsetHeight
            //});

            // Main resolved scene color (what you see)
            const mainColorRT = new THREE.WebGLRenderTarget(SCR_WIDTH, SCR_HEIGHT, {
                minFilter: THREE.LinearFilter,
                magFilter: THREE.LinearFilter,
                format: THREE.RGBAFormat,
                type: THREE.UnsignedByteType
            });

            // Water Heightmap RT
            // (for render target reading) Ensure waterHeightRT is
            // created with THREE.FloatType or THREE.HalfFloatType
            // if you need high-precision heights, though THREE.UnsignedByteType
            // can work if your height fits nicely into a 0–255 normalized scale.
            const waterHeightRT = new THREE.WebGLRenderTarget(128, 128, {
                minFilter: THREE.LinearFilter,
                magFilter: THREE.LinearFilter,
                format: THREE.RedFormat,      // or RGBA for debugging
                type: THREE.FloatType,
                depthBuffer: false
            });

            // Better depth render targets
            const depthRTSettings = {
                minFilter: THREE.LinearFilter,
                magFilter: THREE.LinearFilter,
                format: THREE.RedFormat,
                type: THREE.FloatType,
                depthBuffer: true,
                stencilBuffer: false,
                generateMipmaps: false
            };

            const waterDepthRT = new THREE.WebGLRenderTarget(SCR_WIDTH, SCR_HEIGHT, depthRTSettings);
            const sceneDepthRT = new THREE.WebGLRenderTarget(SCR_WIDTH, SCR_HEIGHT, depthRTSettings);


            const rayLineRedMat = new THREE.LineBasicMaterial({
                color: 0xff0000, // or whatever bright color you use
                depthTest: false,  // CRITICAL: Tells the GPU not to check if things are blocking it
                depthWrite: false, // CRITICAL: Tells the GPU not to output its own depth
                transparent: true  // Helps override standard rendering bucket sorts
            });

            // Custom depth material (forces pure depth output)
            const customDepthMaterial = new THREE.MeshDepthMaterial({
                depthPacking: THREE.BasicDepthPacking,   // Important!
                side: THREE.DoubleSide
            });
            
            // Stencil can be handled via a separate render or by using the built-in stencil buffer

            const uiRT = new THREE.WebGLRenderTarget(128, 128, {
                minFilter: THREE.LinearFilter,
                magFilter: THREE.LinearFilter,
                format: THREE.RedFormat,      // or RGBA for debugging
                type: THREE.FloatType,
                depthBuffer: false
            });
            
            const heightQuadGeo = new THREE.PlaneGeometry(2, 2); // NDC quad
            const waterHeightMaterial = new THREE.ShaderMaterial({
                //uniforms: {
                uniforms: THREE.UniformsUtils.merge([
                    THREE.UniformsLib.lights,
                    THREE.UniformsLib.fog,
                    {
                    data: { value: new THREE.Vector3(0, -12.1, 2.0) }, // time, y-offset, scale
                    viewport_width: { value: 128 },
                    viewport_height: { value: 128 }
                    }
                ]),
                //},
                vertexShader: `
varying vec2 TexCoord;

void main() {
    TexCoord = uv;
    gl_Position = vec4(position, 1.0);
}
                `,
                fragmentShader: `
#define PI 3.14159265359

varying vec2 TexCoord;
uniform vec3 data;           // x = time
uniform int viewport_width;
uniform int viewport_height;

float noise3(vec2 p) {
    return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}

float smoothNoise(vec2 p) {
    vec2 i = floor(p);
    vec2 f = fract(p);
    vec2 u = f * f * (3.0 - 2.0 * f);
    float a = noise3(i);
    float b = noise3(i + vec2(1.0, 0.0));
    float c = noise3(i + vec2(0.0, 1.0));
    float d = noise3(i + vec2(1.0, 1.0));
    return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}

void main() {
    float scale = 128.0;
    float phase = data.x;
    float twinkleSpeed = 86400.0 / 1.0;
    float rawTime = phase / (2.0 * PI) * twinkleSpeed;
    float timeV = mod(rawTime, 6000.0);
    timeV = abs(timeV - 3000.0);

    vec2 iTexCoord = TexCoord * scale;

    // Wave parameters
    vec2 dirs[4] = vec2[](
        vec2(1.0, 0.0),
        vec2(0.0, 1.0),
        normalize(vec2(1.0, 1.0)),
        normalize(vec2(-1.0, 1.0))
    );
    float freqs[4] = float[](2.0, 2.0, 3.0, 3.0);
    float speeds[4] = float[](1.0, 1.2, 1.5, 1.3);
    float amps[4] = float[](0.2, 0.2, 0.1, 0.1);

    float height = 0.0;
    for (int i = 0; i < 4; i++) {
        float phaseVal = dot(dirs[i], iTexCoord) * freqs[i] + timeV * speeds[i];
        height += amps[i] * sin(phaseVal);
    }

    float noiseValue = smoothNoise(iTexCoord + vec2(timeV * 0.1, timeV * 0.05));
    vec2 uvPerturbed = iTexCoord + noiseValue * 0.1;
    
    height = 0.0;
    for (int i = 0; i < 4; i++) {
        float phaseVal = dot(dirs[i], uvPerturbed) * freqs[i] + timeV * speeds[i];
        height += amps[i] * sin(phaseVal);
    }

    height += 0.05 * smoothNoise(iTexCoord * 5.0 + vec2(timeV * 0.2));

    gl_FragColor = vec4(height * 0.5 + 0.5, 0.0, 0.0, 1.0); // map to 0-1 for visibility
}
                `,
                depthWrite: false,
                depthTest: false,
                lights: false,
                fog: false,
            });
            
            // Simple quad mesh for RTT
            const heightQuad = new THREE.Mesh(heightQuadGeo, waterHeightMaterial);
            heightQuad.frustumCulled = false;
            
            // Ortho camera for height render (top-down)
            const heightOrthoCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);

            const GRID_SIZE = 512; // match your C define
            const waterGeo = new THREE.PlaneGeometry(tileSize, tileSize, GRID_SIZE, GRID_SIZE); // world size tunable
            waterGeo.rotateX(-Math.PI / 2);

            const waterDepthMaterial = new THREE.ShaderMaterial({
                //uniforms: {
                //uniforms: THREE.UniformsUtils.merge([
                //    THREE.UniformsLib.lights,
                //    THREE.UniformsLib.fog,
                uniforms:
                    {
                    waterHeightTex: { value: waterHeightRT.texture },
                    MVP: { value: new THREE.Matrix4() }, // or compute manually if needed
                    data: { value: new THREE.Vector3(0, -1.1, 2.0) }, // time, ylev, heightScale
                    tileSize: { value: tileSize },
                    modelMatrixV: { value: new THREE.Matrix4() },
                    playerOffset: { value: new THREE.Vector3() },
                    },
                //]),
                //},
                vertexShader: `
varying float depth;
varying vec3 FragPos;     // WORLD SPACE

uniform mat4 MVP;
uniform mat4 modelMatrixV;        // ← important
uniform vec3 data;
uniform float tileSize;
uniform sampler2D waterHeightTex;
uniform vec3 playerOffset;

void main() {
    float wlev = data.y;
    float heightScale = data.z * 2.0;
    float scale = tileSize;

    vec3 worldCoord = (modelMatrixV * vec4(position, 1.0)).xyz;

    vec2 heightUV = (worldCoord.xz + playerOffset.xz) * 0.00025;

    float h = texture(waterHeightTex, heightUV).r;

    vec3 sPos = vec3(worldCoord.x, wlev, worldCoord.z);
    sPos.y += h * heightScale;

    vec4 localPos = vec4(sPos, 1.0);

    gl_Position = MVP * localPos;
    FragPos = (modelMatrixV * localPos).xyz;

    // IMPORTANT: Output correct depth after displacement
    depth = gl_Position.z;                    // non-linear depth
    // depth = gl_Position.z / gl_Position.w; // if you want linear
}
`,
fragmentShader: `
varying float depth;
void main() {
    gl_FragColor = vec4(depth, 0.0, 0.0, 1.0);
}
`,
                side: THREE.DoubleSide, // or FrontSide
                //fog: true,
                //lights: true, // if mixing with built-ins
                depthWrite: true,
                depthTest: true,
            });

            
            const waterMaterial = new THREE.ShaderMaterial({
                //uniforms: {
                uniforms: THREE.UniformsUtils.merge([
                    THREE.UniformsLib.lights,
                    THREE.UniformsLib.fog,
                    {
                    waterHeightTex: { value: waterHeightRT.texture },
                    MVP: { value: new THREE.Matrix4() }, // or compute manually if needed
                    data: { value: new THREE.Vector3(0, -1.1, 2.0) }, // time, ylev, heightScale
                    sunPos: { value: new THREE.Vector3() },
                    moonPos: { value: new THREE.Vector3() },
                    viewPos: { value: new THREE.Vector3() },
                    daytime: { value: 0.0 },
                    invProjection: { value: new THREE.Matrix4() },
                    invView: { value: new THREE.Matrix4() },
                    tileSize: { value: tileSize },
                    modelMatrixV: { value: new THREE.Matrix4() },
                    // Add more from your frag (fog colors, etc.)
                    sunShadowMap: { value: null },
                    sunShadowMatrix: { value: new THREE.Matrix4() },
                    clockShadowMap: { value: null },
                    clockShadowMatrix: { value: new THREE.Matrix4() },
                    shadowBias: { value: shadowBias },
                    clockShadowBias: { value: clockShadowBias },
                    shadowNormalBias: { value: shadowNormalBias },
                    clockShadowNormalBias: { value: clockShadowNormalBias },
                    shadowRadius: { value: shadowRadius },
                    clockShadowRadius: { value: clockShadowRadius },
                    shadowRes: { value: shadowMapRes },
                    clockShadowRes: { value: clockShadowMapRes },
                    shadowOn: { value: shadowOn },
                    ambientMulti: { value: 1.0 },
                    normalStrength: { value: 0.8 },
                    playerOffset: { value: new THREE.Vector3() },
                    }
                ]),
                //},
                vertexShader: `
varying vec3 FragPos;     // WORLD SPACE
varying vec3 ourNormal;
varying vec3 vNormal;
varying vec2 TexCoord;
varying float timeV;
varying float height;
//flat varying int underWater;

uniform mat4 MVP;
uniform mat4 modelMatrixV;        // ← important
uniform vec3 data;
uniform float tileSize;
uniform sampler2D waterHeightTex;
uniform vec3 viewPos;
uniform vec3 playerOffset;

//varying float vFogDepth;

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

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

float noise3(vec2 p) {
    return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}

float smoothNoise(vec2 p) {
    vec2 i = floor(p);
    vec2 f = fract(p);
    vec2 u = f * f * (3.0 - 2.0 * f);
    float a = noise3(i);
    float b = noise3(i + vec2(1.0, 0.0));
    float c = noise3(i + vec2(0.0, 1.0));
    float d = noise3(i + vec2(1.0, 1.0));
    return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}

void main() {
    float wlev = data.y;
    float heightScale = data.z * 2.0;
    //float scale = 128.0;
    float scale = tileSize;
    float phase = data.x / 100.0;
    float twinkleSpeed = 86400.0 / 100.0;
    float rawTime = phase / (2.0 * PI) * twinkleSpeed;
    timeV = mod(rawTime, 12000.0);
    timeV = abs(timeV - 6000.0);
    //underWater=0;

    vec3 worldCoord = (modelMatrixV * vec4(position, 1.0)).xyz;
    TexCoord = worldCoord.xz;  // for color/noise too if wanted

    vec2 heightUV = (worldCoord.xz + playerOffset.xz) * 0.00025;   // tune scale 4000 to 1

    float h = texture(waterHeightTex, heightUV).r;
    height = h;

    // make modified localpos in spos
    vec3 sPos = vec3(TexCoord.x-playerOffset.x, -worldCoord.y+wlev, TexCoord.y-playerOffset.z);
    sPos.y += h * heightScale;

    vec3 localDisplacedPos = position;
    localDisplacedPos.y += h * heightScale;

    //bool cameraUnder = cameraPosition.y < (sPos.y+worldCoord.y) - 0.4;   // bias to prevent early trigger
    //bool pointUnder  = viewPos.y < (sPos.y+worldCoord.y) + 0.5;  // small bias for surface
    // bad result in vert, moved to frag
    //if(viewPos.y<sPos.y+worldCoord.y) {
    //if(pointUnder) {
    //if(cameraUnder) {
    //    underWater=1;
    //}

    // === BETTER NORMALS ===
    float eps = 0.003;
    float hL = texture(waterHeightTex, heightUV + vec2(-eps, 0.0)).r;
    float hR = texture(waterHeightTex, heightUV + vec2( eps, 0.0)).r;
    float hD = texture(waterHeightTex, heightUV + vec2(0.0, -eps)).r;
    float hU = texture(waterHeightTex, heightUV + vec2(0.0,  eps)).r;

    vec3 dx = vec3(eps*scale, (hR - hL)*heightScale, 0.0);
    vec3 dz = vec3(0.0, (hU - hD)*heightScale, eps*scale);
    ourNormal = normalize(cross(dz, dx));
    vNormal = normal;

    // === WORLD SPACE ===
    vec4 localPos = vec4(sPos, 1.0);
    FragPos = (modelMatrixV * localPos).xyz;           // WORLD SPACE

        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)
        vec3 flatUpNormal = vec3(0.0, 1.0, 0.0); // Change to 0,0,1 if Z is up in your world
        vec3 clockOffset = normal * clockShadowNormalBias; // 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);
        vec4 clockOffsetWorldPos = modelMatrix * vec4(position + clockOffset, 1.0);

        if(shadowOn >= 1) {
            //vSunShadowCoord = sunShadowMatrix * offsetWorldPos;
            //vSunShadowCoord = sunShadowMatrix * (modelMatrix *vec4(position, 1.0));
            // Repeat for moon with another offsetWorldPos if separate bias, but same for now
            //vClockShadowCoord = clockShadowMatrix * clockOffsetWorldPos; // sundial always on
            //vClockShadowCoord = clockShadowMatrix * (modelMatrix *vec4(position, 1.0));
            // Pass this identical world space vector to your clock mapping matrix
            vSunShadowCoord = sunShadowMatrix * (modelMatrix *vec4(localDisplacedPos, 1.0));
            vClockShadowCoord = clockShadowMatrix * (modelMatrix * vec4(localDisplacedPos, 1.0));
        }

    //gl_Position = MVP * vec4(FragPos,1.0);
    gl_Position = MVP * localPos;

        ${THREE.ShaderChunk['fog_vertex']}
}
`,
                fragmentShader: `
   precision highp float;

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

varying vec3 FragPos;
varying vec3 ourNormal;
varying vec3 vNormal;
varying vec2 TexCoord;
varying float timeV;
varying float height;
//flat varying int underWater;

uniform mat4 invProjection;         // Inverse projection matrix
uniform mat4 invView;               // Inverse view matrix
uniform vec3 sunPos;
uniform vec3 moonPos;
uniform vec3 viewPos;
uniform float daytime; // 1-0-1 night-day-night

// Manual shadow uniforms
uniform float shadowBias;
uniform float clockShadowBias;
uniform float shadowRadius;
uniform float clockShadowRadius;
uniform float shadowRes;
uniform float clockShadowRes;
varying vec4 vSunShadowCoord;
varying vec4 vClockShadowCoord;
uniform sampler2D sunShadowMap;
uniform sampler2D clockShadowMap;
uniform int shadowOn;

    uniform float ambientMulti;
    uniform float normalStrength;

float linearizeDepth(float depth) {
    float near=0.1f;
    float far=5000.0;

    float z = depth * 2.0 - 1.0; // NDC
    return 2.0 * near * far / (far + near - z * (far - near));
}

vec3 getWorldPosition(float depth, vec2 uv, bool linearize) {
    float z = linearize ? linearizeDepth(depth) : (depth * 2.0 - 1.0);
    vec4 clipSpace = vec4(uv * 2.0 - 1.0, linearize ? depth : z, 1.0);
    vec4 viewSpace = invProjection * clipSpace;
    viewSpace /= viewSpace.w;
    vec4 worldSpace = invView * viewSpace;
    return worldSpace.xyz;
}

// Sine-based noise function for wave generation
float sineFunction(vec2 uv) {
    // Adjusting the frequency based on distance from origin
    //float frequency = smoothstep(0.1, 1000.0) * 10.0;
    float frequency = smoothstep(0.1, 1000.0, 0.2);
    //float frequency = 1.0;

    float x = sin(uv.y * frequency) + sin(uv.x * frequency);
    return (x / 2.0) - 0.5;
}

// Simple 2D noise function
float noise3(vec2 p) {
    // A basic pseudo-random function (not true Perlin noise, but sufficient for perturbation)
    return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
    //return sin(dot(p, vec2(25, 125))) * 5;
}

// Simple fractal noise with wrapping behavior
vec3 fractalNoise(vec2 uv) {
    vec3 result;

    // Generate a smooth sine wave that wraps around horizontally
    float x = sin(uv.y * 2.0 + timeV) * 0.5; // Frequency (25 is a good starting point)
    x += sin(((uv.x * 0.5) * 5.0 + timeV) * 0.3); // Add a sub-wave for more detail
    
    // Create a smoother transition between waves
    x = x * 0.8 + 0.4; // Offset to prevent sharp peaks
    
    // Apply the noise pattern to UV coordinates
    result.x = x;
    result.y = uv.y;
    //result.z = (x > 0.5) ? sqrt(x - 0.5) : 0; // Create a dynamic depth effect that fades out
    result.z = x;
    
    return result;
}

//float noise3(vec2 p) {
//    return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
//}

float smoothNoise(vec2 p) {
    vec2 i = floor(p);
    vec2 f = fract(p);
    vec2 u = f * f * (3.0 - 2.0 * f);
    float a = noise3(i);
    float b = noise3(i + vec2(1.0, 0.0));
    float c = noise3(i + vec2(0.0, 1.0));
    float d = noise3(i + vec2(1.0, 1.0));
    return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}

// === PROCEDURAL DETAIL NORMALS (better quality) ===
vec3 getProceduralNormal3(vec2 uv, float t) {
    // Multiple frequencies for natural look
    float n1 = smoothNoise(uv * 4.5 + vec2(t * 0.08, t * 0.05));
    float n2 = smoothNoise(uv * 19.0 + vec2(-t * 0.15, t * 0.12));
    float n3 = smoothNoise(uv * 118.0 + vec2(t * 0.22, -t * 0.18));

    float height = n1 * 0.6 + n2 * 0.3 + n3 * 0.15;

    // Finite difference for normal (much better than direct noise)
    float eps = 0.008;
    float hL = smoothNoise(uv * 4.5 + vec2(t*0.08 - eps, t*0.05));
    float hR = smoothNoise(uv * 4.5 + vec2(t*0.08 + eps, t*0.05));
    float hD = smoothNoise(uv * 4.5 + vec2(t*0.08, t*0.05 - eps));
    float hU = smoothNoise(uv * 4.5 + vec2(t*0.08, t*0.05 + eps));

    vec3 detailNormal = vec3(
        (hL - hR) * 12.8,     // X component
        1.0,
        //height,
        (hD - hU) * 12.8     // Y component (height)
    );

    // Add gentle vertical pulsing / breathing
    detailNormal.y += sin(t * 1.3) * 0.15 + sin(t * 2.7) * 0.1;

    return normalize(detailNormal);
}
vec3 getProceduralNormal2(vec2 uv, float t) {
    float t1 = t * 10.15;
    float t2 = t * 1.85;
    float t3 = t * 0.72;

    // Multiple overlapping ripple layers
    float n1 = smoothNoise(uv * 6.5 + vec2(t1 * 0.08,  t1 * 0.05));
    float n2 = smoothNoise(uv * 13.5 + vec2(-t2 * 0.15, t2 * 0.12));
    float n3 = smoothNoise(uv * 27.0 + vec2(t3 * 0.22, -t3 * 0.18));

    // Finite difference for normal
    float eps = 0.008;
    float hL = smoothNoise(uv * 4.5 + vec2(t1*0.08 - eps, t1*0.05));
    float hR = smoothNoise(uv * 4.5 + vec2(t1*0.08 + eps, t1*0.05));
    float hD = smoothNoise(uv * 4.5 + vec2(t1*0.08, t1*0.05 - eps));
    float hU = smoothNoise(uv * 4.5 + vec2(t1*0.08, t1*0.05 + eps));

    vec3 detailNormal = vec3(
        (hL - hR) * 12.8,     // X component
        1.0,
        //height,
        (hD - hU) * 12.8     // Y component (height)
    );

    // Add gentle vertical pulsing / breathing
    detailNormal.y += sin(t * 1.3) * 0.15 + sin(t * 2.7) * 0.1;

    return normalize(detailNormal);
}
// === DETAIL NORMALS USING SAME WAVE STYLE AS HEIGHTMAP ===
vec3 getProceduralNormal(vec2 uv, float t) {
    // Same wave directions as heightmap for consistency
    vec2 dirs[4] = vec2[](
        vec2(1.0, 0.0),
        vec2(0.0, 1.0),
        normalize(vec2(1.0, 1.1)),
        normalize(vec2(-1.0, 0.9))
    );

    float freqs[4] = float[](6.0, 6.5, 9.0, 8.5);   // higher frequency = smaller ripples
    float speeds[4] = float[](1.1, 1.3, 1.6, 1.4);
    float amps[4]  = float[](0.6, 0.55, 0.4, 0.35);

    float waveHeight = 0.0;
    for (int i = 0; i < 4; i++) {
        float phaseVal = dot(dirs[i], uv) * freqs[i] + t * speeds[i];
        waveHeight += amps[i] * sin(phaseVal);
    }

    // Add noise perturbation like in heightmap
    float noiseVal = smoothNoise(uv * 3.5 + vec2(t * 0.2, t * 0.15));
    vec2 perturbed = uv + noiseVal * 0.08;

    float waveHeight2 = 0.0;
    for (int i = 0; i < 4; i++) {
        float phaseVal = dot(dirs[i], perturbed) * freqs[i] * 1.3 + t * speeds[i] * 1.2;
        waveHeight2 += amps[i] * 0.7 * sin(phaseVal);
    }

    // Finite difference to create normal
    float eps = 0.006;
    float hL = 0.0, hR = 0.0, hD = 0.0, hU = 0.0;

    // Simplified finite difference using the wave function
    hL = sin(dot(dirs[0], uv + vec2(-eps,0)) * freqs[0] + t * speeds[0]);
    hR = sin(dot(dirs[0], uv + vec2( eps,0)) * freqs[0] + t * speeds[0]);

    vec3 detailNormal = vec3(
        (hL - hR) * 6.0,
        1.0,
        waveHeight2 * 4.0   // use combined height for Z
    );

    return normalize(detailNormal);
}

void main() {
    vec3 resultColor = vec3(0.0, 0.18, 0.35);

    float uvscale = 0.05; // space out the dark/blue sine grid formation
    
    // Calculate UV coordinates based on position
    vec2 uv = TexCoord.xy * uvscale; // max w/h view
    vec3 basePlaneTexture = vec3(0.036,0.28,0.76);
    vec3 waveTexture = vec3(0.04, 0.3, 0.8);
    
    // Combine base plane with vertex-generated waves and ripples
    resultColor = mix(basePlaneTexture, waveTexture, sin(uv.x)-sin(uv.y));
    resultColor = resultColor * .8 + .22;
    
    vec3 noise = fractalNoise(uv);
    
    vec3 norm = normalize(ourNormal);
    //vec3 norm = ourNormal * (gl_FrontFacing ? 1.0 : 1.0);

    // Procedural detail
    //vec3 detailNormal = getProceduralNormal(TexCoord * 3.5, timeV * 2.2);
    //vec3 detailNormal = getProceduralNormal(TexCoord * .025, timeV);   // tune UV scale here
    vec3 detailNormal2 = getProceduralNormal2(TexCoord * 1.2, timeV);
    //vec3 detailNormal3 = getProceduralNormal3(TexCoord * 2.2, timeV);
    
    // Mix with geometric normal (important!)
    //vec3 finalNormal = normalize( mix(norm, detailNormal, 0.45) );   // 0.25 ~ 0.45 strength
    vec3 finalNormal = normalize( mix(norm, detailNormal2, 0.45) );   // 0.25 ~ 0.45 strength
    //vec3 finalNormal = normalize( mix(norm, detailNormal3, 0.45) );   // 0.25 ~ 0.45 strength
    //vec3 finalNormal = normalize( mix(norm, ((detailNormal * .1) + detailNormal2) * .9, 0.45) );

    //finalNormal = norm; // debug
        
    float zeron=smoothstep(0.0,0.2,abs((daytime*2.0)-1.0)); // 1 0 1
    float mixn=(1.0-((daytime*0.5)+0.5))*zeron; // 0 - 0.5 // night half
    float mixt=((daytime*0.5)+0.5)*zeron; // 0.5 - 1 // day half

    vec3 viewDir = normalize(viewPos - FragPos);

    vec3 tsunPos = sunPos;
    vec3 tmoonPos = moonPos;

    //if(underWater==1) {
    if(viewPos.y<FragPos.y) {
        resultColor*=.6; // debug or optional surface darken from below
        //sunDir=-sunDir;
        //moonDir=-moonDir;
        tsunPos=moonPos;
        tmoonPos=sunPos;
        tsunPos.y=-tsunPos.y;
        tmoonPos.y=-tmoonPos.y;
    }

    //vec3 sunDir  = normalize(tsunPos - FragPos);
    //vec3 moonDir = normalize(tmoonPos - FragPos);
    vec3 sunDir  = normalize(tsunPos);
    vec3 moonDir = normalize(tmoonPos);
    
    float ambientStrength = 0.4;                    // ↑ was 0.4
    vec3 sunColor = vec3(1.0, 0.95, 0.82);
    vec3 moonColor = vec3(0.50, 0.60, 0.75);

    float sunStrength = clamp(tsunPos.y * 0.012, 0.0, 1.1);
    float moonStrength = clamp(1.0 - sunStrength * 0.8, 0.0, 0.7);

    vec3 ambient = ambientStrength * mix(moonColor, sunColor, sunStrength);

    // Diffuse
    float diff  = max(dot(finalNormal, sunDir), 0.0) * mixt;
    float diff2 = max(dot(finalNormal, moonDir), 0.0) * mixn;
    vec3 diffuse = (diff * sunColor * sunStrength + diff2 * moonColor * moonStrength) * 0.8;

    // Specular - tighter & stronger
    vec3 reflectDirSun  = reflect(-sunDir, finalNormal);
    vec3 reflectDirMoon = reflect(-moonDir, finalNormal);
    float spec  = pow(max(dot(viewDir, reflectDirSun),  0.0), 64.0) * mixt;
    float spec2 = pow(max(dot(viewDir, reflectDirMoon), 0.0), 32.0) * mixn;
    vec3 specular = (spec * sunColor + spec2 * moonColor) * 25.2;

    resultColor = (ambient + diffuse + specular) * resultColor;
    //resultColor +=+ 0.32;   // lift blacks

// === REALISTIC WATER DEPTH + FRESNEL ===
float distToCam = length(FragPos - viewPos);

// Water column thickness (horizontal + vertical)
float waterColumn = distToCam * 0.00085;                          // far across surface
waterColumn += max(0.0, (viewPos.y - FragPos.y) * 0.023);       // looking down deep

// Base transparency
float alpha = 1.0 - clamp(waterColumn * 1.05, 0.0, 0.82);
alpha = clamp(alpha * 1.18, 0.32, 0.94);                         // keep near areas visible

// === FRESNEL (edge highlight) ===
float fresnel = pow(1.0 - max(0.0, dot(normalize(ourNormal), viewDir)), 2.8);
alpha = min(alpha + fresnel * 0.35, 0.97);                       // stronger at edges

// Color absorption (deep water gets darker)
float absorption = clamp(waterColumn * 1.15, 0.0, 0.88);

vec3 deepDay   = vec3(0.040, 0.065, 0.105) * 5.0;
vec3 deepNight = vec3(0.004, 0.012, 0.048);
vec3 deepColor = mix(deepNight, deepDay, daytime);

resultColor = mix(resultColor, deepColor, absorption * 0.82);

// SHADOW HANDLING

        float shadow = 1.0; // no shadow
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)
);

        float ndotl=max(dot(finalNormal, -sunDir), 0.01); 
        float slopeFactor = sqrt(1.0 - ndotl * ndotl) / ndotl; // tan(acos(ndotl)); // webgl fast

        // Manual shadow sampling for sun
        shadow=abs((daytime*2.0)-1.0); // 1 0 1 0 = 0.5 0 0.5 1 ~;

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

        float clockShadow = 1.0;
        if (clockShadowCoord.x >= 0.0 && clockShadowCoord.x <= 1.0 &&
            clockShadowCoord.y >= 0.0 && clockShadowCoord.y <= 1.0 &&
            clockShadowCoord.z >= 0.0 && clockShadowCoord.z <= 1.0) {

            float clockShadowDepth=0.0;
            float clockBias=0.0;
            clockShadowDepth = unpackRGBAToDepth(texture(clockShadowMap, clockShadowCoord.xy));
            float biasMultiplier = 0.0001;
            //clockBias = clockShadowBias + biasMultiplier * slopeFactor;
            //clockBias = clockShadowBias + (0.001 * slopeFactor); // scale the bias down?
            //clockBias = clamp(clockBias, 0.0001, 0.001); // cap to prevent extreme swaps/full cover
            // Reduce the multiplier and clamp the factor to avoid extreme jumps
            //clockBias = clockShadowBias + clamp(0.0001 * slopeFactor, 0.0, 0.005);
            clockShadow = clockShadowCoord.z > clockShadowDepth + clockBias ? 0.0 : 1.0;

if(shadowOn<=2) { // dither shadows
    vec2 clockTexelSize = 1.0 / vec2(clockShadowRes, clockShadowRes); // match mapSize

    // Add to fragmentShader uniforms or defines
    const int clockNumSamples = 16;
    
    // Dynamically shrink radius based on distance to light for sharper contact
    float distToLight = clockShadowCoord.z; // Depth in light space
    float adaptiveRadius = clockShadowRadius * clamp(distToLight * 0.5, 0.2, 1.0);
    
    // Prefetch rotation to avoid redundant trig inside the loop
    float clockAngle = fract(sin(dot(clockShadowCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) * 6.2832;
    float s = sin(clockAngle);
    float c = cos(clockAngle);
    mat2 rotationMat = mat2(c, -s, s, c);
    
    // In the loop:
    clockShadow = 0.0;  // Reset to accumulate lit
    for (int i = 0; i < clockNumSamples; i++) {
        // Apply precomputed rotation matrix for speed
        vec2 rotatedOffset = rotationMat * poissonDisk[i];
        vec2 finalOffset = rotatedOffset * adaptiveRadius * clockTexelSize;
    
        float cd = unpackRGBAToDepth(texture(clockShadowMap, clockShadowCoord.xy + finalOffset));
        // Branchless accumulation: sum += float(test) avoids 'if' overhead
        clockShadow += (clockShadowCoord.z > cd + clockBias) ? 0.0 : 1.0;
    }
clockShadow /= float(clockNumSamples);
} // dithering clockshadows <=2

        } // clockShadowCoords<>

        //shadow = min(shadow, clockShadow); // or multiply for stronger effect
        shadow = clockShadow;

        if(shadowOn>=1) {
        vec4 shadowCoord = vSunShadowCoord / vSunShadowCoord.w;
        
        //shadowCoord = shadowCoord * 0.5 + 0.5; // NDC to [0,1]

        float sunShadow = 1.0;
        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])
        float shadowDepth=0.0;
        float bias=0.0;
        shadowDepth = unpackRGBAToDepth(texture(sunShadowMap, shadowCoord.xy));
        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
        sunShadow = shadowCoord.z > shadowDepth + bias ? 0.0 : 1.0;

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

    // Add to fragmentShader uniforms or defines
    const int numSamples = 16;
    
    // Prefetch rotation to avoid redundant trig inside the loop
    float angle = fract(sin(dot(shadowCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) * 6.2832;
    float s2 = sin(angle);
    float c2 = cos(angle);
    mat2 rotationMat2 = mat2(c2, -s2, s2, c2);
    
    // In the loop:
    sunShadow = 0.0;  // Critical: Reset to accumulate lit
    for (int i = 0; i < numSamples; i++) {
        // Apply precomputed rotation matrix for speed
        vec2 rotatedOffset2 = rotationMat2 * poissonDisk[i];
        vec2 finalOffset2 = rotatedOffset2 * shadowRadius * texelSize;
    
        float d = unpackRGBAToDepth(texture(sunShadowMap, shadowCoord.xy + finalOffset2));
        // Branchless accumulation: sum += float(test) avoids 'if' overhead
        sunShadow += (shadowCoord.z > d + bias) ? 0.0 : 1.0;
    }
    sunShadow /= float(numSamples);
} // dithering shadows <=2

        // WATER
        // Blend with main shadow (or use only clockShadow for the clock)
        //shadow = min(1.0-(sunShadow * .5), clockShadow); // or multiply for stronger effect
        //shadow = (sunShadow + clockShadow) * .5;
        //shadow = (sunShadow * clockShadow);

        } // shadowCoords<>
        // Real shadow receive
        //float shadow = getShadow();               // from shadowmask chunk
            float sunEval=((sunShadow)*.9)+.1;
            sunShadow*=zeron;
            clockShadow*=zeron;

            // SUNDIAL
            // shadow strength .4 yo .6 = night to day
            // ambiance strength .6 to .4 = night to day
            float shadowStrength=(daytime*.2)+.2; // .4 to .6
            float ambianceStrength=((1.0-daytime)*.2)+.6*ambientMulti; // .6 to .4
            //resultColor *= ((max(clockShadow*sunEval,0.0)) * shadowStrength) + ambianceStrength;           // shadow factor + ambient day
            //shadow = min(sunShadow, clockShadow);
            //resultColor *= (shadow * shadowStrength) + ambianceStrength;
            //shadow = min(sunShadow, clockShadow);
            shadow = sunShadow; // inverted issue with clockshadow and water
            resultColor *= vec3((shadow * shadowStrength) + ambianceStrength);
        } else { // else shadowOn<1
            // always shadow sundial
            shadow*=zeron;

            // SUNDIAL
            // shadow strength .4 yo .6 = night to day
            float shadowStrength=(daytime*.2)+.2; // .4 to .6
            float ambianceStrength=((1.0-daytime)*.2)+.6*ambientMulti; // .6 to .4
            resultColor *= (shadow * shadowStrength) + ambianceStrength;           // shadow factor + ambient day
        }

      //  if (albedo.a < alphaThreshold) discard; // Clip transparent pixels (no blending, but depth sorting works)

      //  // 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>
      //  color+=totalEmissiveRadiance;

        if(viewPos.y<-12.5) {
            alpha+=max(0.0,abs(viewPos.y+12.5)*.001);
            //resultColor*=min(1.0,abs(1000.0-(viewPos.y+12.5))*.001);
        }

        if(shadowOn>=2) {
            gl_FragColor = vec4(vec3(shadow , 0.0, 1.0-shadow), alpha);
        } else {
            gl_FragColor = vec4(resultColor, alpha);
            //gl_FragColor = vec4(vNormal, 1.0);
        }

// END SHADOW HANDLING

    //gl_FragColor = vec4(resultColor, alpha);
    //gl_FragColor = vec4(FragPos, alpha);
    //gl_FragColor = vec4(0.75 - distance * .008,0.0,0.0,1.0);
    //gl_FragColor = vec4(distance,0.0,0.0, alpha);
    //gl_FragColor = vec4(depthDark,0.0,0.0, alpha);
    //gl_FragColor = vec4(alpha,0.0,0.0,1.0);
    //gl_FragColor = vec4(finalNormal,1.0);
}
`,
                transparent: true,
                side: THREE.DoubleSide, // or FrontSide
                fog: true,
                lights: true, // if mixing with built-ins
                depthWrite: true,
                //depthTest: false,
            });
 
            const waterMesh = new THREE.Mesh(waterGeo, waterMaterial);
            waterMesh.name="water";
            waterMesh.position.y = -1.0; // base level, adjusted by shader
            waterMesh.visible = false;
            waterMesh.castShadow = false;
            waterMesh.receiveShadow = true;
            waterMesh.renderOrder = 5;
            waterMesh.layers.enable(0);
            //scene.add(waterMesh); // or add to terrainGroup
            skyGroup.add(waterMesh);

            const imageMaterial = new THREE.ShaderMaterial({
                uniforms: THREE.UniformsUtils.merge([
                    THREE.UniformsLib.lights,
                    THREE.UniformsLib.fog,
                    {
                        tDiffuse: { value: null },
                        uColor: { value: new THREE.Color(0xffffff) },
                        uOpacity: { value: 1.0 },
                        daytime: { value: 0.0 }
                    }
                ]),
                vertexShader: vertexShaderImage,
                fragmentShader: fragmentShaderImage,
                transparent: true,
                side: THREE.DoubleSide, // for shadows
                //side: THREE.FrontSide,
                fog: true,
                lights: true, // Set to true if you want to use lighting chunks
                depthWrite: true,
                depthTest: true,
            });

            const postMaterial = new THREE.ShaderMaterial({
                //uniforms: THREE.UniformsUtils.merge([
                //    THREE.UniformsLib.lights,
                //    THREE.UniformsLib.fog,
                uniforms:
                    {
                        screenTexture: { value: mainColorRT.texture },           // your resolved scene texture
                        sceneDepthTex: { value: sceneDepthRT.texture },
                        waterDepthTex: { value: waterDepthRT.texture },
                        waterHeightTex: { value: waterHeightRT.texture },
                        cameraPosition: { value: new THREE.Vector3() },
                        invProjection: { value: new THREE.Matrix4() },
                        invView: { value: new THREE.Matrix4() },
                        daytime: { value: 0 },
                        data: { value: new THREE.Vector3(0, -1.1, 2.0) }, // time, ylev, heightScale
                        tileSize: { value: tileSize },
                        viewport_width: { value: SCR_WIDTH },
                        viewport_height: { value: SCR_HEIGHT },
                        rendMode: { value: rendmode },
                        viewPos: { value: new THREE.Vector3() },
                        playerOffset: { value: new THREE.Vector3() },
                    },
                //]),
                vertexShader: vertexShaderPost,
                fragmentShader: fragmentShaderPost2,
                //transparent: true,
                //side: THREE.DoubleSide, // for shadows
                //side: THREE.FrontSide,
                //fog: true,
                //lights: true, // Set to true if you want to use lighting chunks
            });

            // Post-processing full-screen quad
            const postGeo = new THREE.PlaneGeometry(2, 2);
            const postCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
            const postScene = new THREE.Scene();
            const postQuad = new THREE.Mesh(postGeo, postMaterial);
            postScene.add(postQuad);
            
            refList["ambientLight"] = ambientLight;
            refList["sunLight"] = sunLight;
            refList["skyMaterial"] = skyMaterial;
            refList["skyGroup"] = skyGroup;
            refList["sky"] = sky;
            refList["customDepthMat"] = customDepthMat; // for cloud shadows
            refList["camera"] = camera;
            refList["playerGroup"] = playerGroup;
            refList["renderer"] = renderer;
            refList["clockShadowLight"] = clockShadowLight;
            refList["mountainMaterial"]=mountainMat;
            refList["terrainMaterial"]=terrainMat;
            refList["terrainGroup"] = terrainGroup;
            refList["beamGroup"] = beamGroup;
            refList["floor"] = floor;
            refList['sky2']=sky2; // center sphere mesh
            refList["imageMaterial"] = imageMaterial;
            refList["projectorMatrix"] = new THREE.Matrix4();
            refList["projectionMatrix"] = new THREE.Matrix4();
            refList["tempEuler"] = new THREE.Euler();
            refList["rotMat"] = new THREE.Matrix4();
            refList["waterHeightRT"] = waterHeightRT;
            refList["mainColorRT"] = mainColorRT;
            refList["waterDepthRT"] = waterDepthRT;
            refList["sceneDepthRT"] = sceneDepthRT;
            refList["customDepthMaterial"] = customDepthMaterial; // for post
            refList["customWaterDepthMaterial"] = waterDepthMaterial; // for post
            refList["uiRT"] = uiRT;
            refList["heightOrthoCam"] = heightOrthoCam;
            refList["heightQuad"] = heightQuad;
            refList["waterHeightMaterial"] = waterHeightMaterial;
            refList["waterMaterial"] = waterMaterial;
            refList['waterMesh'] = waterMesh;
            refList["postMaterial"] = postMaterial;
            refList["postScene"] = postScene;
            refList["postCamera"] = postCamera;
            refList['lineRedMat'] = rayLineRedMat;
            refList['customSunShadowTarget']=customSunShadowTarget;
            refList['customClockShadowTarget']=customClockShadowTarget;
            refList['shadowDepthMaterial']=shadowDepthMaterial;
            refList['standardMat']=standardMat;

            createGlassEdges();
            createRailings();
            createBeams();

            if(get_rrw!=null) {
                SHADOW_SCALE=get_rrw*.01;
                set_shadow_res();
            }

            // --- move to init ---
            coinRef['metaRoughTex']=coinRef['textureLoader'].load(coinmetarough);
            coinRef['metaRoughTex'].flipY = false;

            // Pre-create the 16 standard materials
            for (let i = 0; i < coinatlases.diffuse.length; i++) {
                const diffuseTex = coinRef['textureLoader'].load(coinatlaspath + coinatlases.diffuse[i]);
                diffuseTex.flipY = false;
                
                // Fix typo in index 15 extension if present ("webpp")
                //const normalFile = coinatlases.normal[i].replace('.webpp', '.webp');
                const normalFile = coinatlases.normal[i];
                const normalTex = coinRef['textureLoader'].load(coinatlaspath + normalFile);
                normalTex.flipY = false;
            
                // Using MeshStandardMaterial now, easily swappable with ShaderMaterial later
                //const mat = new THREE.MeshStandardMaterial({
                //    map: diffuseTex,
                //    normalMap: normalTex,
                //    //roughnessMap: coinRef['metaRoughTex'],
                //    //metalnessMap: coinRef['metaRoughTex'], // Adjust if your metarough splits channels
                //    roughness: .2,
                //    metalness: .6,
                //    normalScale: new THREE.Vector2(0.5, 0.5)
                //});
                const mat = new THREE.ShaderMaterial({
                    uniforms: THREE.UniformsUtils.merge([
                        THREE.UniformsLib.lights,
                        THREE.UniformsLib.fog,
                        {
                            map: { value: diffuseTex }, // per-child texture
                            normalMap: { value: normalTex },
                            roughnessMap: { value: coinRef.metaRoughTex },
                            emissiveMap: { value: null },
                            emissive: { value: new THREE.Vector3(0,0,0) },
                            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) },
                            fogDensity: { value: fogDensity },
                            sunShadowMap: { value: null },
                            sunShadowMatrix: { value: new THREE.Matrix4() },
                            clockShadowMap: { value: null },
                            clockShadowMatrix: { value: new THREE.Matrix4() },
                            shadowBias: { value: shadowBias },
                            clockShadowBias: { value: clockShadowBias },
                            shadowNormalBias: { value: shadowNormalBias },
                            clockShadowNormalBias: { value: clockShadowNormalBias },
                            shadowRadius: { value: shadowRadius },
                            clockShadowRadius: { value: clockShadowRadius },

                            shadowRes: { value: shadowMapRes },
                            clockShadowRes: { value: clockShadowMapRes },
                            shadowOn: { value: shadowOn },
                            alphaThreshold: { value: 0.5 },
                            rimShineStrength: { value: 0.28 }, // shine rim
                            rimShineColor: { value: new THREE.Color(1.0, 0.98, 0.92) },
                            ambientMulti: { value: 0.5 },
                            normalStrength: { value: .5 },
                            daytime: { value: 0.0 },
                        }
                    ]),
                    vertexShader: vertexShaderCoin,
                    fragmentShader: fragmentShaderCoin,
                    transparent: false,
                    side: THREE.FrontSide, // for shadows
                    //side: THREE.FrontSide,
                    fog: true, // Set to false to prevent internal pipeline interference
                    lights: true, // Set to false to avoid automatic chunk requirements
                    depthWrite: true,
                    depthTest: true,
                });
            
                coinRef['coinMaterials'].push(mat);
            }

            load_coin();
            // --- -------- ---
            
            // 5. Input Listeners
            window.addEventListener('keydown', (e) => keys[e.code] = true);
            window.addEventListener('keyup', (e) => keys[e.code] = false);
            
            // 6. Interaction Setup
            raycaster = new THREE.Raycaster();
            mouse = new THREE.Vector2(); // -1 0 1
            mousep = new THREE.Vector2(); // 0 to w/h
            mousem = new THREE.Vector2(); // mx/my delta
            //window.addEventListener('click', onMouseClick);
            //console.dir(document.getElementsByTagName("canvas")[0]);
            //document.getElementsByTagName("canvas")[0].addEventListener('click', onMouseClick);
            $('overlay').addEventListener('click', onMouseClick);

            window.addEventListener('resize', onWindowResize);

            refList["renderer"].shadowMap.autoUpdate = false;

            document.addEventListener("visibilitychange", function() {
               if (document.hidden){
                    //console.log("Browser tab is hidden")
                    tabTime=new Date().getTime();
                    //tabTime=lastTime;
                    if((isLocked || useJoysticks) && !player.isFlying && !player.isSwimming) {
                        fakeGround=true;
                        tabHidden=true;
                        gravityInitialized=false;
                        lastPlayerY=refList['playerGroup'].position.y;
                        console.log('tab hidden in game mode');
                    }
               } else {
                    tabHidden=false;
                    console.log('tab unhidden');
                    //fakeGround=false;
                    //console.log("Browser tab is visible")
                    var nTime=new Date().getTime();
                    //if(nTime-tabTime>(tabRefS*1000) && tabTime!=0) {
                    //    location.reload();
                    //} else {
                        //tabTime=nTime-tabTime;
                    //}
               }
            });

            window.addEventListener('keydown', (e) => {
                keys[e.code] = true;
            
                // Toggle Play/Edit Mode
                if (e.code === 'KeyG') {
                    if (document.pointerLockElement) {
                        document.exitPointerLock();
                    } else {
                        // A 50ms delay bypasses the Chrome "already locked/requesting" glitch
                        setTimeout(() => {
                            refList["renderer"].domElement.requestPointerLock();
                        }, 50);
                    }
                }

                if (e.code === 'KeyJ') {
                    if (!isLocked) {
                        useJoysticks=!useJoysticks;

                        refreshJoysticks();
                    }
                }
                if (e.code === 'KeyC') {
                    player.isCrouching = !player.isCrouching;
                }
                if (e.code === 'KeyM') {
                    mark_toggle();
                }

                if (e.code === 'Digit0' || e.code === "Numpad0") setRendMode(0); // normal
                if (e.code === 'Digit1' || e.code === "Numpad1") setRendMode(1); // B/W + depthDiff
                if (e.code === 'Digit2' || e.code === "Numpad2") setRendMode(2); // depthDiff
                if (e.code === 'Digit3' || e.code === "Numpad3") setRendMode(3); // stencil || debug
                if (e.code === 'Digit4' || e.code === "Numpad4") setRendMode(4); // debug colors
                if (e.code === 'Digit5' || e.code === "Numpad5") {
                    rayshow_toggle();
                }
                if (e.code === 'Digit6' || e.code === "Numpad6") {
                    flying_toggle();
                }
                if (e.code === 'Digit7' || e.code === "Numpad7") {
                    waterrt_toggle();
                }

                //if (e.code === 'KeyP') {
                //    refList["camera"].position.copy(refList["sunLight"].position);
                //}
            });

            document.addEventListener('pointerlockchange', () => {
                isLocked = document.pointerLockElement === refList["renderer"].domElement;

                ovrJoysticks=isLocked;
                refreshJoysticks();

                if (isLocked) {
                    document.body.classList.add('is-locked');
                } else {
                    document.body.classList.remove('is-locked');
                }
            
                // Visual feedback (optional)
                //const ui = document.getElementById('ui');
                //ui.style.border = isLocked ? "2px solid lime" : "2px solid red";
                //ui.innerHTML = isLocked ? "<b>MODE: PLAY</b> (G to Edit)" : "<b>MODE: EDIT</b> (G to Play)";
            });

            document.addEventListener('wheel', (e) => {
                //const step=e.deltaY;
                const step=.5;
                //console.log("step="+step);
                if(e.deltaY>0) {
                    markScale+=step
                } else {
                    markScale-=step
                }
                if(markScale<1) markScale=1;
                else if(markScale>5) markScale=5;
            });

            window.addEventListener('pointerdown', (e) => {
                // Standard joystick/window checks...
            
                // Only cache touch pointers for pinching if we aren't dragging UI elements
                if (e.pointerType === 'touch' && !leftJoyHold && !rightJoyHold && winHold === "" && sliderHold === "") {
                    pinchPointers.push(e);
                }
            });

            window.addEventListener('mousemove', (e) => {
                if(isLocked) {
                    let mX=e.movementX || 0;
                    let mY=e.movementY || 0;

                    // --- FIREFOX SKYWARD DRIFT SANITIZATION ---
                  //  if (mY !== 0 && mY === lastMoveY) {
                  //      consecutiveIdenticalDeltas++;
                  //  } else {
                  //      consecutiveIdenticalDeltas = 0;
                  //  }
                  //  lastMoveY = mY;

                  //  if (consecutiveIdenticalDeltas >= 1) mY = 0; // Kill the infinite loop loop
                    // ------------------------------------------

                    mousem.x = mX;
                    mousem.y = mY;
                    mouse_position(e);
                }
                //if(markOn) {
                if (markOn && !leftJoyHold && !rightJoyHold && winHold === "" && sliderHold === "") {
                    mouse.x = (e.clientX / SCR_WIDTH) * 2 - 1;
                    mouse.y = -(e.clientY / SCR_HEIGHT) * 2 + 1;
                }
            });
            // window.addEventListener('pointermove', moveJoy);
            window.addEventListener('pointermove',function(e) {
                if(leftJoyHold || rightJoyHold) {
                    moveJoy(e);
                } else if (winHold !== "" || sliderHold !== "") {
                    e.preventDefault();
                    
                    // e.clientX and e.clientY are uniform for mouse AND pointers on mobile!
                    const X = e.clientX;
                    const Y = e.clientY;

                    mousem.x = X - mousep.x;
                    mousem.y = Y - mousep.y;
                    mousep.x = X;
                    mousep.y = Y;

                    mouse_position(e);
                }
                // PINCH TO ZOOM LOGIC: Trigger only if 2 fingers are touching the background
                else if (pinchPointers.length === 2 && e.pointerType === 'touch') {
                    e.preventDefault();

                    // Update the coordinates for the current moving finger in our cache
                    for (let i = 0; i < pinchPointers.length; i++) {
                        if (e.pointerId === pinchPointers[i].pointerId) {
                            pinchPointers[i] = e;
                            break;
                        }
                    }

                    // Calculate current distance between the two cached fingers
                    const dx = pinchPointers[0].clientX - pinchPointers[1].clientX;
                    const dy = pinchPointers[0].clientY - pinchPointers[1].clientY;
                    const currentDist = Math.sqrt(dx * dx + dy * dy);

                    if (prevPinchDist > 0) {
                        const step = 0.1; // Smooth stepping value for mobile scale adjustments

                        if (currentDist < prevPinchDist) {
                            // Fingers moving closer together -> Zoom Out / Grow Scale
                            markScale += step;
                        } else if (currentDist > prevPinchDist) {
                            // Fingers moving apart -> Zoom In / Shrink Scale
                            markScale -= step;
                        }

                        // Bound the scale exactly like your wheel event bounds it
                        if (markScale < 1) markScale = 1;
                        else if (markScale > 5) markScale = 5;
                    }

                    prevPinchDist = currentDist;
                }
            },{ passive: false });
            window.addEventListener('touchmove',function(e) {
                e.preventDefault(); 
                //if(markOn) {
                if (markOn && !leftJoyHold && !rightJoyHold && winHold === "" && sliderHold === "") {
                    mouse.x = (e.clientX / SCR_WIDTH) * 2 - 1;
                    mouse.y = -(e.clientY / SCR_HEIGHT) * 2 + 1;
                }
            }, { passive: false }); // <--- THIS is critical
            

            // Remove touchstart, touchend, mousedown, mouseup
            // Use pointerdown and pointerup for everything
            $('leftJoyContainer').addEventListener('pointerdown', (e) => { holdJoy(e, true, 'left') });
            $('rightJoyContainer').addEventListener('pointerdown', (e) => { holdJoy(e, true, 'right') });

            // Use window for releases so you catch them even if the finger slides off the joystick
            window.addEventListener('pointerup', (e) => {
                if (e.pointerType === 'mouse') {
                    if (mouseDraggingSide) {
                        holdJoy(e, false, mouseDraggingSide);
                    }
                } else {
                    // Multi-touch fallback
                    if (e.pointerId === activePointers.left) holdJoy(e, false, 'left');
                    if (e.pointerId === activePointers.right) holdJoy(e, false, 'right');
                }

                if(sliderHold!="") {
                    sliderHold="";
                    winHold="";
                    console.log('slider hold cleared');
                } else if(winHold!="") {
                    fitWindow(winHold);

                    winHold="";
                    console.log('window hold cleared');
                }

                if (e.pointerType === 'touch') {
                    removePinchPointer(e);
                }
            });
            window.addEventListener('pointercancel', (e) => {
                if (e.pointerType === 'touch') {
                    removePinchPointer(e);
                }
            });

            $('imu2_1').addEventListener('click', (e) => {
                window_toggle('keybind');
            });
            $('imu2_2').addEventListener('click', (e) => {
                window_toggle('joystick');
            });
            $('imu2_3').addEventListener('click', (e) => {
                window_toggle('quality');
            });
            $('imu2_4').addEventListener('click', (e) => {
                window_toggle('audio');
            });
            $('imu2_5').addEventListener('click', (e) => {
                window_toggle('debug');
            });

            $('imu1_1').addEventListener('click', joy_toggle);
            $('imu1_2').addEventListener('click', clouds_toggle);
            $('imu1_3').addEventListener('click', shadow_toggle);
            $('imu1_4').addEventListener('click', shadow_helper_toggle);
            $('imu1_5').addEventListener('click', waterrt_toggle);
            $('imu1_6').addEventListener('click', rayshow_toggle);
            $('imu1_7').addEventListener('click', rendmode_toggle);
            $('imu1_8').addEventListener('click', flying_toggle);
            $('imu1_9').addEventListener('click', music_toggle);
            $('imu1_10').addEventListener('click', mark_toggle);

            if(get_vo!=null) { // late settings
                vortex_toggle(null,true);
            }

            initialized = true;
            hideLoadScreen();

            setTimeout(function() {
                if(tileInitialized) {
                    makeEnvVisible();
                }
            });
        }

        function makeEnvVisible() {
            refList["skyMaterial"].uniforms.cloudsReady.value = 1;
            //refList['sky2'].visible = true; // debug
            refList['floor'].visible = true;
            refList['tile'].visible = true;
            refList['waterMesh'].visible = true;
            for(let i=0;i<refList['beamList'].length;i++) {
                refList['beamList'][i].visible = true;
            }
            for(let i=0;i<refList['edgeList'].length;i++) {
                refList['edgeList'][i].visible = true;
            }
            for(let i=0;i<refList['railList'].length;i++) {
                refList['railList'][i].visible = true;
            }

            const environmentObjects = [];
            scene.traverse((obj) => {
                if((obj.isMesh && obj !== refList['waterMesh'])) {
                    environmentObjects.push({
                        mesh: obj,
                        originalMaterial: obj.material
                    });
                    console.log("depth swap added "+obj.name);
                }
            });
            refList["cachedEnv"] = environmentObjects;           
        }
        
        //let maxFpsProject=30*.001;
        //let maxFpsProject=30*.01;
        //let markWait=0;
        function markActive() {
            //markWait+=maxFpsProject;
            raycaster.setFromCamera(mouse, refList["camera"]);
            //const intersects = raycaster.intersectObjects(scene.children);
            // Only check the terrain
            const intersects = raycaster.intersectObjects([refList["floor"]]);

            if (intersects.length > 0) {
                const hit = intersects[0];
                //console.log("Hit:", hit.object.name, "at", hit.point);
                // logic for placing or selecting object would go here
            
                hitProject(hit);
            }
        }
        // https://gemini.google.com/share/9d249a4670d0
        function hitProject(hit) {
            const shaderMat = refList["terrainMaterial"].uniforms.uTextureMatrix.value;
            const viewMat = refList["projectorMatrix"];
            const projMat = refList["projectionMatrix"];
        
            // --- SCALE ---
            //let targetSize = 5.0; // Tie this to your toggle or scroll wheel
            let targetSize = markScale;
            projMat.makeOrthographic(-targetSize, targetSize, targetSize, -targetSize, 0.1, 50);

            // --- TRANSLATION ---
            // Position the "camera" at the hit point
            viewMat.makeTranslation(-hit.point.x, -hit.point.y - 10.0, -hit.point.z);
 
            // 2. Rotate it to match the Player/Camera Heading (Y-axis)
            // Assuming your player or camera is in refList["camera"]
            //const playerRotation = new THREE.Matrix4().extractRotation(refList["playerGroup"].matrixWorld);
            refList['tempEuler'].setFromQuaternion(refList["playerGroup"].quaternion, 'YXZ');
            refList['rotMat'].makeRotationY(-refList['tempEuler'].y);
            // We only want the Y-axis rotation (Yaw), so zero out X and Z if needed, 
            // but usually extractRotation is enough for a "follow" feel.

           // // 3. Optional: If your image is sideways, apply a rotation to the viewMat here
           // const rot = new THREE.Matrix4().makeRotationX(Math.PI / 2);
           // viewMat.premultiply(rot);

           // 3. Combine: Tilt first, then Player's Rotation, then Translation
            viewMat.premultiply(refList['rotMat']);
            viewMat.premultiply(tilt);
        
            shaderMat.multiplyMatrices(projMat, viewMat);
            //// FORCED TEST: Inside hitProject
            //// Every time this runs, move the projection 1 unit to the right manually
            //const testOffset = new THREE.Matrix4().makeTranslation(Math.sin(Date.now() * 0.005) * 10.0, 0, 0);
            //shaderMat.multiplyMatrices(projMat, viewMat).multiply(testOffset);
        }

        function onMouseClick(event) {
            if(winState['quality']['sel_mountain_res']) {
                sel_mountain_res(); // toggle
                return;
            } else if(winState['quality']['sel_render_res']) {
                sel_render_res(); // toggle
                return;
            }

            // Calculate mouse position in normalized device coordinates
            mouse.x = (event.clientX / SCR_WIDTH) * 2 - 1;
            mouse.y = -(event.clientY / SCR_HEIGHT) * 2 + 1;

            raycaster.setFromCamera(mouse, refList["camera"]);
            const intersects = raycaster.intersectObjects(scene.children);

            if (intersects.length > 0) {
                const hit = intersects[0];
                console.log("Hit:", hit.object.name, "at", hit.point);
                // logic for placing or selecting object would go here
            }
        }

        const downRay = new THREE.Raycaster();
        const downVec = new THREE.Vector3(0, -1, 0);
       
        function handleGrounding(nextPos) {
            // BYPASS: If a coin collider already grounded us this frame, 
            // don't let floor raycasting change our grounded state.
            if (player.isGrounded && player.velocity.y === 0) {
                // Still let it clean up subtle micro-movements if needed, 
                // but DO NOT set player.isGrounded = false;
                return; 
            }

            // Start ray above the predicted position
            const origin = nextPos.clone().add(new THREE.Vector3(0, 1.0, 0));
            downRay.set(origin, downVec);
        
            const terrainObjects = [refList["floor"], ...physicsBodies]; 
            const hits = downRay.intersectObjects(terrainObjects);
        
            if (hits.length > 0) {
                const groundY = hits[0].point.y;
                
                // targetHeight is your "standing" eye level (1.7 or 0.8 for crouch)
                const currentTargetHeight = player.isCrouching ? 0.8 : 1.7;
        
                // If predicted feet position is below the ground
                //if (nextPos.y < groundY + currentTargetHeight) {
                if (nextPos.y < groundY + currentTargetHeight + 0.01) {
                    nextPos.y = groundY + currentTargetHeight; // Snap to ground
                    player.velocity.y = 0;
                    player.isGrounded = true;
                    doublejumpready = 1;
                    doublejumpdelta = 0;
                    //console.log("is grounded terrain");
                    if(player.isFalling) {
                        player.isFalling=false;
                        checkFall(refList['playerGroup'].position.y,"terrain");
                    }
                   // if(hits[0].object.name === "sky2") {
                   //     // This keeps the player on the SURFACE of the sphere
                   //     // rather than just snapping the Y height
                   //     console.log("grounded on sphere");

                   //     // 1. Calculate the 'Outward' normal of the sphere at the hit point
                   //     // This is just the direction from the sphere center to the hit point
                   //     const sphereCenter = new THREE.Vector3();
                   //     hits[0].object.getWorldPosition(sphereCenter);

                   //     const outwardNormal = new THREE.Vector3().subVectors(hits[0].point, sphereCenter).normalize();

                   //     // 2. If the slope is too steep, you could let them slide,
                   //     // but for now, this ensures nextPos is exactly on the shell
                   //     const shellPoint = sphereCenter.add(outwardNormal.multiplyScalar(3)); // 3 is your sphere radius

                   //     // Position the 'ghost' exactly at the surface + player height
                   //     nextPos.y = shellPoint.y + currentTargetHeight;
                   // }
                } else {
                    player.isGrounded = false;
                    //console.log("is Not Grounded - but hits name="+hits[0].object.name);
                }
            } else {
                player.isGrounded = false;
                //console.log("is Not Grounded - no hits");
            }
        }

        let idleTime=0; // keeps count
        let activeTime=10; // cap at 10s
        let sprintSpeed = 1.8;
        let readStrafe=0;
        let readForward=0;
        //let readDir=0;
        let lockForwardVec=new THREE.Vector3(0,0,0);
        let lockRightVec=new THREE.Vector3(0,0,0);
        let lockSpeed=0.0;
        let lockHeight=0.0;
        function checkFall(posY,name="") {
            //console.log("lockHeight: "+lockHeight+" posY: "+posY);
            if(lockHeight>posY) {
                if(posY<0.0) {
                    lockHeight+=Math.abs(posY);
                } else {
                    lockHeight-=posY;
                }

                if(lockHeight>1.0) {
                    let str="into ground hit "+name;
                    if(name=="water") str="into water";

                    console.log("Fell "+lockHeight+" meter "+str);
                }
            } else {
                console.log("Landed higher than fell from cancelled");
            }
            lockHeight=0.0;
        }
        function handleMovement(delta,current,lapse) {
            if(!initialized) return;
            if(delta>.1) delta=.1; // delta cap for vel y fall after hidden tab

            let currentSpeed = player.speed;
            if(player.isFlying) currentSpeed*=2.0;
            else if(player.isSwimming) currentSpeed*=.5;
            //if (keys['ShiftLeft']) currentSpeed *= sprintSpeed;
            // built in sprint at 99%
            let isSprinting=false;
            let joySprint=Math.abs(joyRead['left'].tx)+Math.abs(joyRead['left'].ty);
            if(joySprint>=.99 || keys['ShiftLeft']) {
                currentSpeed *= sprintSpeed;
                isSprinting=true;
            }
                
            // 1. Determine Direction Scalars (Cancels out if both keys pressed)
            let moveForward = 0;
            if (keys['KeyW'] || keys['ArrowUp']) moveForward += 1;
            if (keys['KeyS'] || keys['ArrowDown']) moveForward -= 1;
 
            let moveStrafe = 0;
            let turnDir = 0;
            if(isLocked) { // play mode controls
                //if (keys['KeyQ']) document.exitPointerLock();
                if (keys['KeyE']) turnDir -= 1;
                if (keys['KeyQ']) turnDir += 1;
        
                if (keys['KeyA']) moveStrafe -= 1;
                if (keys['KeyD']) moveStrafe += 1;
                if (keys['ArrowLeft']) turnDir += 1;
                if (keys['ArrowRight']) turnDir -= 1;
            } else { // edit mode controls
                if (keys['KeyE']) moveStrafe += 1;
                if (keys['KeyQ']) moveStrafe -= 1;

                if (keys['KeyA'] || keys['ArrowLeft']) turnDir += 1;
                if (keys['KeyD'] || keys['ArrowRight']) turnDir -= 1;
            }

            if(useJoysticks) {
                if(winState['joystick']['chk_joy_inv_y']) {
                    moveForward-=joyRead['left'].ty;
                } else {
                    moveForward+=joyRead['left'].ty;
                }
                if(winState['joystick']['chk_joy_inv_x']) {
                    if(winState['joystick']['chk_joy_strafe']) {
                        moveStrafe+=joyRead['right'].tx; // swapped
                    } else {
                        moveStrafe+=joyRead['left'].tx;
                    }
                } else {
                    if(winState['joystick']['chk_joy_strafe']) {
                        moveStrafe-=joyRead['right'].tx; // swapped
                    } else {
                        moveStrafe-=joyRead['left'].tx;
                    }
                }

                if(moveForward>1) moveForward=1;
                else if(moveForward<-1) moveForward=-1;

                if(moveStrafe>1) moveStrafe=1;
                else if(moveStrafe<-1) moveStrafe=-1;

                // smooth camera joy
                let eRx=easeIn(joyRead['right'].tx);
                let eRy=easeIn(joyRead['right'].ty);
                let eRxn=(joyRead['right'].tx<0)?true:false;
                let eRyn=(joyRead['right'].ty<0)?true:false;
                if(winState['joystick']['chk_joy_strafe']) { // swapped
                    eRx=easeIn(joyRead['left'].tx);
                    eRxn=(joyRead['left'].tx<0)?true:false;
                }
                if(winState['joystick']['chk_joy_inv_x']) {
                    eRxn=!eRxn;
                }
                if(winState['joystick']['chk_joy_inv_y']) {
                    eRyn=!eRyn;
                }
                if(eRxn) eRx=-eRx;
                if(eRyn) eRy=-eRy;

                //turnDir+=joyRead['right'].tx;
                turnDir+=eRx;

                // up / down
                //player.pitch += joyRead['right'].ty * delta;
                player.pitch += eRy * delta;
                player.pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, player.pitch));
            }
        
            // 2. Apply Turning
            const turnSpeed = 2.5 * delta;
            player.rotation += turnDir * turnSpeed;

            // 3. Calculate Final Movement Vector
            // We clone the vectors so we don't mutate the camera's base orientation
            //let forwardVec = new THREE.Vector3(0, 0, -1).applyQuaternion(refList["playerGroup"].quaternion);
            //forwardVec.y = 0; 

            const forwardVec = new THREE.Vector3(0, 0, -1);

            // 1. Get player heading (Yaw)
            const playerQ = refList["playerGroup"].quaternion;
            
            if(player.isFlying || player.isSwimming) {
                // 2. Get camera pitch (X-axis rotation only)
                const cameraPitchQ = new THREE.Quaternion().setFromEuler(
                    new THREE.Euler(refList['camera'].rotation.x, 0, 0)
                );
                
                // 3. Combine them: Player Yaw * Camera Pitch
                const combinedQ = new THREE.Quaternion().multiplyQuaternions(playerQ, cameraPitchQ);

                forwardVec.applyQuaternion(combinedQ);
                //player.velocity.y = 0;
                if (player.isSwimming) {
                    player.velocity.x *= 0.85; // Water drag
                    // 1. Apply stronger specific drag to Y to prevent "bobbing" or shooting up
                    player.velocity.y *= 0.8; // Slightly stronger than horizontal drag (0.85)
                    player.velocity.z *= 0.85;
                } else if (player.isFlying) {
                    player.velocity.x *= 0.55; // Air drag
                    player.velocity.y *= 0.55;
                    player.velocity.z *= 0.55;
                }
            } else {
                forwardVec.applyQuaternion(playerQ);
                forwardVec.y = 0;

                // Reset horizontal velocity but keep vertical (for gravity/jumping)
                player.velocity.x = 0;
                player.velocity.z = 0;
            }
            //console.log(player.pitch);
            forwardVec.normalize();
        
            let rightVec = new THREE.Vector3().crossVectors(forwardVec, new THREE.Vector3(0, 1, 0)).normalize();

            //if (!player.isGrounded && !player.isSwimming && !player.isFlying && ((doublejumpready>0 && doublejumpdelta>0) || doublejumpready==0)) { // jump falling
            if (!player.isGrounded && !player.isSwimming && !player.isFlying) { // falling
                moveForward=readForward;
                moveStrafe=readStrafe;
                lockForwardVec.y=0;
                forwardVec.copy(lockForwardVec);
                rightVec.copy(lockRightVec);
                currentSpeed=lockSpeed;
                //console.log("falling");
                if(!player.isFalling) player.isFalling=true;
                //lockForwardVec.y*=.55; // water bounde high
                if(refList['playerGroup'].position.y>lockHeight) {
                    lockHeight=refList['playerGroup'].position.y;
                }
            } else {
                readForward=moveForward;
                readStrafe=moveStrafe;
                //readDir=turnDir;
                lockForwardVec.copy(forwardVec);
                lockRightVec.copy(rightVec);
                lockSpeed=currentSpeed;
                lockHeight=refList['playerGroup'].position.y;
            }

            const moveStep = new THREE.Vector3();
        
            // Add Forward/Back contribution
            if (moveForward !== 0) {
                // .clone() or copy ensures you don't permanently mutate forwardVec
                moveStep.copy(forwardVec).multiplyScalar(moveForward * currentSpeed); 
                //player.velocity.add(forwardVec.multiplyScalar(moveForward * currentSpeed));
                player.velocity.add(moveStep);
            }
            // Add Strafe contribution
            if (moveStrafe !== 0) {
                moveStep.copy(rightVec).multiplyScalar(moveStrafe * currentSpeed);
                //player.velocity.add(rightVec.multiplyScalar(moveStrafe * currentSpeed));
                player.velocity.add(moveStep);
            }
            //console.dir(forwardVec);
            // Add idle movement to camera in a Lemniscate of Gerono (infinity symbol or a figure-eight (8))
            if(moveForward != 0 || moveStrafe != 0 || turnDir != 0) {
                idleTime=0;
                activeTime+=delta*10;
                if(activeTime>10) activeTime=10;
            } else {
                if(idleTime>1) {
                    activeTime-=delta;
                    if(activeTime<0) activeTime=0;
                }
            }

            // https://gemini.google.com/share/18a942a56efd
            // Natural Camera / Idle Sway / Camera Bobbing
            // Idle Breathing / Camera Sway / Ambient Motion
            //const intensity=.1*(Math.min(idleTime,1));
            const intensity=(.1*(1.0-(activeTime*.1)))*(winState['quality']['slider_sway1']*.01);
            const angle=current/(lapse*.000001);
            // pitch needs to oscillate twice as fast for infinity symbol pattern
            const addpit=Math.sin(angle * radian * 2)*intensity*.25;
            const addrot=Math.sin(angle * radian)*(intensity*2);

            // 4. Update Camera Orientation
            //refList["camera"].quaternion.setFromEuler(new THREE.Euler(player.pitch+addpit, player.rotation+addrot, 0, 'YXZ'));
            refList["playerGroup"].quaternion.setFromEuler(new THREE.Euler(0, player.rotation, 0, 'YXZ'));
            refList["camera"].quaternion.setFromEuler(new THREE.Euler(player.pitch+addpit, addrot, 0, 'YXZ'));

            // head bob walking
            // strideSpeed: slow for walk, fast for sprint
            const intensityBob=(moveForward*.1)*(winState['quality']['slider_sway1']*.01);
            const strideSpeed = isSprinting ? 2.0 : 1.0;
            const walkAngle = current / (lapse * 0.000000025) * strideSpeed;
            
            // 1. Vertical Bob (Position Y) - Moves up/down twice per stride
            const bobY = Math.sin(walkAngle * radian * 2.0) * (intensityBob * 0.5);
            
            // 2. Side Sway (Position X or Rotation Z) - Moves once per stride
            const bobX = Math.cos(walkAngle * radian) * (intensityBob * .2);
            
            // 3. Apply to Camera Position (Mental 'Neck' Offset)
            const bobMove=new THREE.Vector3(bobX,bobY,0);
            //const bobMove=new THREE.Vector3(0,0,0,);
            //console.log("bobX: "+bobX+" bobY: "+bobY);
            
            // 4. Subtle Tilt (Roll) - Adds a lot of realism
            const bobRoll = Math.cos(walkAngle * radian) * (intensityBob * 0.01);
            if(intensity<=0) { // or else will override idle above
                refList["camera"].quaternion.setFromEuler(new THREE.Euler(player.pitch, 0, bobRoll, 'YXZ'));
            }

            // 5. Physics & Gravity
            if(player.isFlying || player.isSwimming) {
                let speedAscend=3.0;
                let speedDescend=-2.0;
                if(player.isFlying) {
                    speedAscend=12.0;
                    speedDescend=-12.0;
                }

                if (keys['Space']) player.velocity.y = speedAscend; // swim up
                else if (keys['KeyC']) player.velocity.y = speedDescend; // swim up
               // else {
               //     player.velocity.y *= 0.95;
               //     if (Math.abs(player.velocity.y) < 0.1) player.velocity.y = 0;
               //}
            } else if(((player.isGrounded || (doublejumpready>0 && doublejumpdelta>0))) && keys['Space']) {
                keys['Space']=false;
                player.velocity.y = 6.0
                player.isCrouching = false;
                if(!player.isGrounded) {
                    doublejumpready--;
                    doublejumpdelta=0;
                    console.log("double jump");
                } else {
                    doublejumpdelta=0.6;
                    console.log("single jump");
                }
                player.isGrounded = false;
            }
        
            // --- 3. Vector Clamping & Anti-Cheat Engine ---
            let maxAllowedSpeed = currentSpeed;
            if (player.isSwimming) maxAllowedSpeed = currentSpeed * 0.6; // Swim slower than walk
            if (player.isFlying)   maxAllowedSpeed = currentSpeed * 2.5; // Fly faster

            if (player.isSwimming || player.isFlying) {
                // Calculate total 3D speed across all three axes (X, Y, and Z)
                const totalSpeed3D = player.velocity.length();
            
                if (totalSpeed3D > maxAllowedSpeed) {
                    // .setLength scales X, Y, and Z proportionally, preserving the direction 
                    // you are aiming while cleanly keeping your speed at the maximum limit.
                    player.velocity.setLength(maxAllowedSpeed);
                }
            } else {
                // Standard grounded/falling state: Only clamp horizontal XZ to allow normal gravity falls
                const speedXZ = Math.sqrt(player.velocity.x * player.velocity.x + player.velocity.z * player.velocity.z);
                
                if (speedXZ > maxAllowedSpeed) {
                    player.velocity.x = (player.velocity.x / speedXZ) * maxAllowedSpeed;
                    player.velocity.z = (player.velocity.z / speedXZ) * maxAllowedSpeed;
                }
            }
            
            // 4. NOW calculate your next position safely
            const nextPos = refList["playerGroup"].position.clone().add(
                player.velocity.clone().multiplyScalar(delta)
            );
            refList["camera"].position.copy(bobMove);
            if (!gravityInitialized) {
                // Safe fallback: Hard-code the floor at y=0 or player.height
                //if (nextPos.y < player.height) {
                if (nextPos.y < lastPlayerY) {
                    nextPos.y = lastPlayerY;
                    player.velocity.y = 0;
                    player.isGrounded = true;
                    console.log("uninitialized collision preventing gravity falling");
                    console.log("nextPos.y: "+nextPos.y);
                }
                return;
            }
            //if(nextPos.y<0) console.log(nextPos);

            if((isLocked || useJoysticks) && (!helperGrids && !fakeGround)) { // game mode 
                // 2. Run Collisions (Modifies player.velocity)
               // sphereCollide(refList["sky2"], 3, nextPos); 
                
                //const waterDiff = refList['playerGroup'].position.y - waterLevel;
                //const waterHeight = getWaterHeightAt(refList['playerGroup'].position.x,refList['playerGroup'].position.z,refList['playerGroup'].position);
                const waterHeight = getWaterHeightAt(refList['playerGroup'].position.x,refList['playerGroup'].position.z,current);
                const waterDiff = refList['playerGroup'].position.y-waterHeight;
                //console.log("waterHeight="+waterHeight);
                //console.log("waterDiff:"+waterDiff);
                const waterDiffLast=waterHeight-lastWaterHeight;
                lastWaterHeight=waterHeight;

                if (waterDiff < player.height + 0.1) { // 0.1 above water
                //if (waterDiff < 0.5) { // 0.5 above water
                    player.isSwimming = true;
                    // damp velocity.y, buoyancy, etc.
                    //player.velocity.y *= 0.55;
                    //if(player.velocity.y<.1) player.velocity.y=0.0;
                    //console.log("in water player.velocity="+player.velocity.y);
                    if(player.isCrouching) player.isCrouching = false;
                    //console.log("is swimming");
                    if(player.isFalling) {
                        player.isFalling=false;
                        checkFall(refList['playerGroup'].position.y,"water");
                    }

                    if(waterDiff>-2.0 && !player.isFlying) {
                        //console.log("waterDiffLast:"+waterDiffLast);
                        // e.g., if you want them floating chest-deep: waterHeight - (player.height * 0.4)
                      //--  const targetFloatingY = waterHeight + (player.height);

                      //--  // Directly set the prediction vector to stop physics drift dead in its tracks
                      //--  nextPos.y = targetFloatingY;

                      //--  // Zero out vertical velocity so gravity additions don't fight the wave lock
                      //--  player.velocity.y = 0;
                        // Tie the player's vertical position directly to the wave height
                        //nextPos.y = waterHeight + player.height; 
                        //player.velocity.y = 0; // Prevent gravity from dragging you through the floor
                        //nextPos.y -= waterDiffLast; 
                        nextPos.y += waterDiffLast;
                    }
                } else {
                    if(player.isSwimming) {
                        player.isSwimming = false;
                        player.isCrouching = false; // unset

                        // --- WATER SLINGSHOT CURE ---
                        // If they are heading up out of the water, clamp the upward boost
                        // so they don't launch into orbit.
                        if (player.velocity.y > 2.0) {
                            player.velocity.y = 2.0; // Gives a tiny, satisfying splash jump but no rocket launch
                        }
                        //console.log("is not swimming");
                    }
                }

                if(!player.isFlying) {
                    handlePlaneCollisions(physicsImages, nextPos, delta,true); // Pass your array of images/planes

                    handlePlaneCollisions(physicsBodies, nextPos, delta,false); // Pass your array of other/planes
                    handleCoinCollisions(nextPos);
                    
                    //handleWorldBoundaries(nextPos); // no escape platform
            
                    handleGrounding(nextPos);
                }

                // 4. Update the actual position
                refList["playerGroup"].position.copy(nextPos);
            } else { // old simple / Floor Snap used for edit mode
                if(player.isSwimming) player.isSwimming = false;
                // 4. Update the actual position
                refList["playerGroup"].position.copy(nextPos);
         
                const targetHeight = player.isCrouching ? 0.8 : 1.7;
                player.height = THREE.MathUtils.lerp(player.height, targetHeight, 0.1);
        
                if (refList["playerGroup"].position.y < player.height) {
                    refList["playerGroup"].position.y = player.height;
                    player.velocity.y = 0;
                    player.isGrounded = true;
                    doublejumpready=1;
                    doublejumpdelta=0;
                    //console.log("is grounded simple floor");
                    if(player.isFalling) {
                        player.isFalling=false;
                        checkFall(refList['playerGroup'].position.y,"floor");
                    }
                }
            }
            
            if(!player.isSwimming && !player.isFlying) {
                player.velocity.y -= 15.0 * delta;
            }

            // --- Global safety valve at the bottom of your physics loop ---

            if (player.isSwimming) {
                // Treat swimming as a unified 3D sphere speed limit (e.g., max 5.0 units/sec total)
                const maxSwimSpeed = 5.0; 
                const currentTotalSpeed = player.velocity.length();
                
                if (currentTotalSpeed > maxSwimSpeed) {
                    player.velocity.setLength(maxSwimSpeed);
                }
            } else if (player.isFlying) {
                const maxFlySpeed = 20.0;
                if (player.velocity.length() > maxFlySpeed) {
                    player.velocity.setLength(maxFlySpeed);
                }
            } else {
                // Standard walking/falling logic
                const maxVelocityY = 25.0;
                const maxVelocityXZ = 12.0;
                
                player.velocity.y = Math.max(-maxVelocityY, Math.min(maxVelocityY, player.velocity.y));
                
                const speedXZ = Math.sqrt(player.velocity.x ** 2 + player.velocity.z ** 2);
                if (speedXZ > maxVelocityXZ) {
                    player.velocity.x = (player.velocity.x / speedXZ) * maxVelocityXZ;
                    player.velocity.z = (player.velocity.z / speedXZ) * maxVelocityXZ;
                }
            }
        }

        var SCR_WIDTH=1;
        var SCR_HEIGHT=1;
        var REN_WIDTH=1;
        var REN_HEIGHT=1;
        var SHADOW_SCALE=1.0;
        function onWindowResize() {
            SCR_WIDTH=window.innerWidth;
            SCR_HEIGHT=window.innerHeight;

            refList["camera"].aspect = SCR_WIDTH / SCR_HEIGHT;
            refList["camera"].updateProjectionMatrix();
            refList["renderer"].setSize(SCR_WIDTH, SCR_HEIGHT);
            refList['mainColorRT'].setSize(SCR_WIDTH * REN_WIDTH,SCR_HEIGHT * REN_HEIGHT);
            refList['waterDepthRT'].setSize(SCR_WIDTH * REN_WIDTH,SCR_HEIGHT * REN_HEIGHT);
            refList['sceneDepthRT'].setSize(SCR_WIDTH * REN_WIDTH,SCR_HEIGHT * REN_HEIGHT);

            Object.keys(winState).forEach(key => {
                 fitWindow(key);
            });
        }

        function fitWindow(win="") {
            const cmw=$("cmw_"+win);
            const bounds=cmw.getBoundingClientRect();
            //console.log("bounds "+win+" left: "+bounds.left+" width: "+bounds.width+" top: "+bounds.top+" height: "+bounds.height);

            if(bounds.left>SCR_WIDTH-200) { // right
                cmw.style.left=(SCR_WIDTH-200)+"px";
                cmw.style.right="auto";
            } else if(bounds.left<-(bounds.width-200)) { // left
                cmw.style.left=(-(bounds.width-200))+"px";
                cmw.style.right="auto";
            }

            if(bounds.top>SCR_HEIGHT-100) { // bottom
                cmw.style.top=(SCR_HEIGHT-100)+"px";
                cmw.style.bottom="auto";
            } else if(bounds.top<23) { // top
                cmw.style.top="23px";
                cmw.style.bottom="auto";
            }
        }

        function renderCustomShadows(directionalLight, customShadowTarget, renderer) {
            // 1. Create vectors to hold true absolute world coordinates
            const cameraWorldPos = new THREE.Vector3();
            const targetWorldPos = new THREE.Vector3();
            
            if (directionalLight.target) {
                // Force the parent structures to update their worldwide paths completely
                refList["skyGroup"].updateMatrixWorld(true);
                directionalLight.target.updateMatrixWorld(true);
                directionalLight.updateMatrixWorld(true);
        
                // Extract the exact world positions of where the light is and where it looks
                directionalLight.getWorldPosition(cameraWorldPos);
                directionalLight.target.getWorldPosition(targetWorldPos);
        
                // Position the shadow camera at the true moving world coordinates
                directionalLight.shadow.camera.position.copy(cameraWorldPos);
                
                // Force it to point directly at the moving target world coordinates
                directionalLight.shadow.camera.lookAt(targetWorldPos);
            } else {
                directionalLight.shadow.camera.lookAt(0, 0, 0);
            }
            
            // 2. CRITICAL: Force the shadow camera to bake these new world coordinates into its matrix
            directionalLight.shadow.camera.updateMatrixWorld(true);
            
            // Now extract the accurate world view matrix for your shader project calculations
            directionalLight.shadow.camera.matrixWorldInverse.copy(directionalLight.shadow.camera.matrixWorld).invert();
        
            // 3. Build the texture projection matrix
            const shadowMatrix = new THREE.Matrix4();
            shadowMatrix.set(
                0.5, 0.0, 0.0, 0.5,
                0.0, 0.5, 0.0, 0.5,
                0.0, 0.0, 0.5, 0.5,
                0.0, 0.0, 0.0, 1.0
            );
            shadowMatrix.multiply(directionalLight.shadow.camera.projectionMatrix);
            shadowMatrix.multiply(directionalLight.shadow.camera.matrixWorldInverse);
        
            // 4. Render Pass
            //scene.overrideMaterial = refList['shadowDepthMaterial'];
            //scene.overrideMaterial = refList['customDepthMat'];

            renderer.setRenderTarget(customShadowTarget);
            renderer.clear(); 
            renderer.render(scene, directionalLight.shadow.camera);
            
            //scene.overrideMaterial = null;
            renderer.setRenderTarget(null);
        
            return shadowMatrix;
        }

        var initShadowMaps=false;
        var initClockShadowMaps=false;
        var firstFrameRendered=false;

        function positionShadowCameras(sunLight, clockShadowLight) {
            if (!initialized) return;
            
            const sunShadowMap = refList['customSunShadowTarget'];
            const clockShadowMap = refList['customClockShadowTarget'];
        
            if (sunShadowMap && clockShadowMap && firstFrameRendered) {
                if (sunLight) {
                    sunLight.shadow.camera.position.copy(sunLight.position);
                    sunLight.shadow.camera.needsUpdate = true;
                }
        
                if (clockShadowLight && sunLight) {
                    const localSunDirection = sunLight.position.clone().normalize();
                    //const localSunDirection = sunLight.position.clone().normalize().negate();
                    clockShadowLight.position.set(0, 0, 0).addScaledVector(localSunDirection, nearShadowOrbit);
                    
                    clockShadowLight.shadow.camera.position.copy(clockShadowLight.position);
                    clockShadowLight.shadow.camera.needsUpdate = true;
                }
            } else if (!firstFrameRendered) {
                firstFrameRendered = true;
            }
        }

        function applyShadowUniforms(sunShadowMatrix,clockShadowMatrix) {
            if(!initialized) return;

            //const sunShadowMap=sunLight.shadow.map.texture;
            //const clockShadowMap=clockShadowLight.shadow.map.texture;
            //const sunShadowMatrix=sunLight.shadow.matrix;
            //const clockShadowMatrix=clockShadowLight.shadow.matrix;
            const sunShadowMap=refList['customSunShadowTarget'];
            const clockShadowMap=refList['customClockShadowTarget'];
            
            if (sunShadowMap && clockShadowMap && firstFrameRendered) {
                refList["terrainMaterial"].uniforms.sunShadowMatrix.value.copy(sunShadowMatrix);
                refList["terrainMaterial"].uniforms.sunShadowMap.value = sunShadowMap.texture;
                refList["waterMaterial"].uniforms.sunShadowMatrix.value.copy(sunShadowMatrix);
                refList["waterMaterial"].uniforms.sunShadowMap.value = sunShadowMap.texture;

                refList["mountainMaterial"].uniforms.sunShadowMatrix.value.copy(sunShadowMatrix);
                refList["mountainMaterial"].uniforms.sunShadowMap.value = sunShadowMap.texture;


                refList["terrainMaterial"].uniforms.clockShadowMatrix.value.copy(clockShadowMatrix);
                refList["terrainMaterial"].uniforms.clockShadowMap.value = clockShadowMap.texture;
                refList["waterMaterial"].uniforms.clockShadowMatrix.value.copy(clockShadowMatrix);
                refList["waterMaterial"].uniforms.clockShadowMap.value = clockShadowMap.texture;
                
                for(let i=0;i<coinRef['coinMaterials'].length;i++) {
                    const coinMat=coinRef['coinMaterials'][i];
                    coinMat.uniforms.sunShadowMatrix.value.copy(sunShadowMatrix);
                    coinMat.uniforms.sunShadowMap.value = sunShadowMap.texture;

                    coinMat.uniforms.clockShadowMatrix.value.copy(clockShadowMatrix);
                    coinMat.uniforms.clockShadowMap.value = clockShadowMap.texture;
                }

                for(let i=0;i<refList['standardMatUpdate'].length;i++) {
                    const smuMat=refList['standardMatUpdate'][i];
                    smuMat.uniforms.sunShadowMatrix.value.copy(sunShadowMatrix);
                    smuMat.uniforms.sunShadowMap.value = sunShadowMap.texture;

                    smuMat.uniforms.clockShadowMatrix.value.copy(clockShadowMatrix);
                    smuMat.uniforms.clockShadowMap.value = clockShadowMap.texture;
                }
            }
        }

        var uinames=["./ui/menu_159.webp","./ui/ui_306.webp","./ui/window_1096.webp","./ui/selist_207.webp"];
        var uiimgs=[];
        function preload_ui() {
            uiimgs=[]; // clear
            for(let i=0;i<uinames.length;i++) {
                var img=document.createElement('img');
                img.onload=function() {
                    console.log('preloaded ui');
                }
                img.src=uinames[i];
                uiimgs.push(img);
            }
        }
        
        function hideLoadScreen() {
            //initTiles();
            loading=-1;
            $('loadtxt').innerHTML="Loading...";
            $('loading').style.opacity=0;
            $('loading').style.pointerEvents="none";
            $('topbar').style.display="block";
            //if(musicOn) {
            //    pushNotice("'.': music playing toggle off/on");
            //}
            //pushNotice("'k': show keybinds toggle visible/hidden");
            //if(cloudQuality==4) {
            //    clouds.visible=true;
            //}
            //infoDiv.style.display="block";

            setTimeout(function() { $('loading').style.display="none"; },1000);
            
            refreshJoysticks();
            preload_ui();
        }

        let mF={ // musicfade
            fadeDir: 0, // 1 for fade-in, -1 for fade-out, 0 for none
            fadeSpeed: .05, // Amount of volume change per second (0.5 = 2 second fade)
            player: null, // ref to music player later
        }
        
        var dd=null;
        var hh=0;
        var mm=0;
        var ss=0;
        var phh="00";
        var pmm="00";
        var pss="00";
        var tsp=0;
        var ss_theta=0;
        var ss_phi=0;
        var testval=500; // set 0 - 2000 (500 = noon, 1500 = midnight)
        var tmpColor={};
        var testValOn=false; // set
        var testValAuto=true; // set
        
        var waitDensity=0.0;
        var nextDensity=0.0;
        var cloudDensityTarget=cDensity;
        
        let loadtime=0.0;
        let loaddelta=0.0;
        let doublejumpready=1;
        let doublejumpdelta=0.0;
        
        let lastTime = 0;
        //let gradientBG="";
        let updateClock = 0;
        let fadeStatusLast = 0;

        let dt=0;
        function animate(time) {
            requestAnimationFrame(animate);

            dt = (time - lastTime) / 1000;
            lastTime = time;
            
            loaddelta += dt;

            idleTime += dt;

            // CAP DELTA: If the tab was hidden, delta might be huge.
            // We "freeze" time at 0.1s max to prevent quantum tunneling through floors.
            if (dt > 0.1) dt = 0.1;

            if(coinRef['spawnIndex']<(coinRef['totalCoins']-1) && coinRef['spawnReady']) {
                spawn_coin();
                //console.log(coinRef['spawnIndex']); // spawn check
            }

            let activeImages=0;
            let easeSpeed = 0;
            for(let i=0;i<imagePlanes.length;i++) {
                if(imagePlanes[i]['ready']) {
                    //  Get the direction the camera is facing
                    const direction = new THREE.Vector3();
                    refList['camera'].getWorldDirection(direction);
                    direction.y = 0;
                    // Re-normalize to make length 1 again
                    direction.normalize();
                    let dispose=false;
                    // Calculate the spot 2 meters in front of the camera
                    if (isNaN(direction.x) || isNaN(direction.y) || isNaN(direction.z)) {
                        console.error("CRITICAL: Direction vector is NaN!");
                        return; // Stop the loop for this plane
                    }
                    //const distanceInFront = 3.0;
                    //const distanceInFront = imagePlanes[i]['dif'];
                    //const distanceInFront = imagePlanes[i]['dif']-imagePlanes[i]['pulseDist'];
                    const distCalc = imagePlanes[i]['dif'] - imagePlanes[i]['pulseDist'];
                    //console.log("i="+i+"] dif: "+imagePlanes[i]['dif']+" pulseDist: "+imagePlanes[i]['pulseDist']+" pulseMax: "+imagePlanes[i]['pulseMax']);
                    const distanceInFront = isNaN(distCalc) ? 1 : distCalc;
                    let targetDestination;
                    let targetLookat = new THREE.Vector3()
                            .copy(refList['playerGroup'].position)
                            .addScaledVector(direction,- distanceInFront*15); // look behind *3 to *15

                    easeSpeed = .5 * dt;
                    if (imagePlanes[i]['stage'] < 2) {
                        // STAGE 0 & 1: Stay in front of the player
                        targetDestination = new THREE.Vector3()
                            .copy(refList['playerGroup'].position)
                            .addScaledVector(direction, distanceInFront+.5); // +.2 for collider avoid or will push
                        targetDestination.y=0; // ground image planes instead of player height/pos

                        if(winState['debug']['chk_intrusive_on'] && imagePlanes[i]['stage']==1) {
                            easeSpeed = (0.5 + Math.min(imagePlanes[i]['count'],imagePlanes[i]['intrusiveBoost'])) * dt; // 4.5 to 14.5 * dt intrusive mode
                        }
                        activeImages++; 
                    
                        // lerp only good for ease out lerping from currnt position to point
                        // SAFETY CHECK: Only lerp if we have a valid number
                        //if (!isNaN(easeSpeed) && easeSpeed !== 0) {
                        //    imagePlanes[i]['emptyRef'].position.lerp(targetDestination, easeSpeed);
                        if (!isNaN(easeSpeed) && easeSpeed > 0) {
                            const safeEase = Math.min(easeSpeed, 0.99); // Never allow it to hit or exceed 1.0
                            imagePlanes[i]['emptyRef'].position.lerp(targetDestination, safeEase)
                        }
                        //console.dir(imagePlanes[i]['emptyRef']['position']);
                    } else {
                        // STAGE 2: Fly to the specific random exit point
                        // We don't add the camera direction here, just go to the point
                        targetDestination = imagePlanes[i]['pos'];

                        // Move at a constant speed instead of a percentage (lerp)
                        const speed = 2 * dt;
                        const dir = new THREE.Vector3().subVectors(targetDestination, imagePlanes[i]['emptyRef'].position).normalize();
                        imagePlanes[i]['emptyRef'].position.addScaledVector(dir, speed);

                        // Dispose when far enough away
                        if (imagePlanes[i]['emptyRef'].position.distanceTo(targetDestination) < 50) {
                            dispose = true;
                        }
                    }
                    if(imagePlanes[i]['stage']>0) {
                        if(imagePlanes[i]['stage']==2) {
                            easeSpeed = .2 * dt;
                        }
                        imagePlanes[i]['count']+=dt;
                        //imagePlanes[i]['dif']-=.1*dt;
                        imagePlanes[i]['dif'] -= imagePlanes[i]['difdr'] * dt;
                        //console.log("subtracting diffdec*dt ("+((imagePlanes[i]['difdr'] * dt))+") from 'dif': "+imagePlanes[i]['dif']+" (after sub)");
                        if(imagePlanes[i]['dif'] < 1) imagePlanes[i]['dif'] = 1;
                        // Keep pulseDist in bounds of the shrinking pulseMax
                        if(imagePlanes[i]['pulseDist'] > imagePlanes[i]['pulseMax']) {
                            imagePlanes[i]['pulseDist'] = imagePlanes[i]['pulseMax'];
                        }

                        if(imagePlanes[i]['pulseMax'] > imagePlanes[i]['dif']) {
                            imagePlanes[i]['pulseMax'] = imagePlanes[i]['dif'];
                        }
                        // Safety Check: Never let pulseMax be 0
                        if(imagePlanes[i]['pulseMax'] < .1) {
                            imagePlanes[i]['pulseMax'] = .1;
                        }

                        //if(imagePlanes[i]['pulseDist']<10.0) {
                        //    cameraFov = ((imagePlanes[i]['pulseDist']*.118)+1.0) * 75.0;
                        //    if(cameraFov>150) cameraFov=150;
                        //    
                        //} else {
                        //    cameraFov=75;
                        //}
                        //refList['camera'].fov = cameraFov;
                        // Ensure pulseDist never goes below 0
                        let safeDist = Math.max(0, imagePlanes[i]['pulseDist']);
                        
                        // Calculate FOV safely
                        cameraFov = ((safeDist * 0.118) + 1.0) * 75.0;
                        
                        // Hard clamp at 130-140 to prevent Three.js tangent inversion glitches
                        if (cameraFov > 135) cameraFov = 135;
                        if (cameraFov < 75) cameraFov = 75; // Prevent 0 or negative FOV
                        
                       // refList['camera'].fov = cameraFov;

                        // -----------
                        if(imagePlanes[i]['pulseDir']==0) {
                            imagePlanes[i]['pulseDist']+=imagePlanes[i]['pulseSpeed'] * dt;
                            if(imagePlanes[i]['pulseDist']>=imagePlanes[i]['pulseMax']) {
                                imagePlanes[i]['pulseDist']=imagePlanes[i]['pulseMax'];
                                // swap
                                imagePlanes[i]['pulseDir']=1;
                            }

                            // squish images with pulse in
                            const scale=imagePlanes[i]['scale'];
                            imagePlanes[i]['emptyRef'].scale.set(scale,(1.0-((imagePlanes[i]['pulseDist']/imagePlanes[i]['pulseMax'])*.25))*scale,scale);
                            //imagePlanes[i]['meshRef'].scale.set(imagePlanes[i]['aspect'],1.0-((imagePlanes[i]['pulseDist']/imagePlanes[i]['pulseMax'])*.25),1);
                            //imagePlanes[i]['meshRef'].scale.set(imagePlanes[i]['aspect']*((1.0+(imagePlanes[i]['pulseDist']/imagePlanes[i]['pulseMax']))*.25),1,1);
                            //console.log(aspect+":"+imagePlanes[i]['pulseDist']);
                            //console.log("0: "+((1.0-((imagePlanes[i]['pulseDist']/imagePlanes[i]['pulseMax'])*.25))*scale));

                        } else { // pulseDir==1
                            imagePlanes[i]['pulseDist']-=imagePlanes[i]['pulseSpeed'] * dt;
                            if(imagePlanes[i]['pulseDist']<=0) {
                                // swap
                                imagePlanes[i]['pulseDist']=0;
                                imagePlanes[i]['pulseDir']=0;
                            }

                            // squish images with pulse out
                            const scale=imagePlanes[i]['scale'];
                            imagePlanes[i]['emptyRef'].scale.set(scale,(1.0-((imagePlanes[i]['pulseDist']/imagePlanes[i]['pulseMax'])*.25))*scale,scale);
                            //imagePlanes[i]['meshRef'].scale.set(imagePlanes[i]['aspect'],1.0-((imagePlanes[i]['pulseDist']/imagePlanes[i]['pulseMax'])*.25),1);
                            //imagePlanes[i]['meshRef'].scale.set(imagePlanes[i]['aspect']*((1.0+(imagePlanes[i]['pulseDist']/imagePlanes[i]['pulseMax']))*.25),1,1);
                            //console.log("1: "+((1.0-((imagePlanes[i]['pulseDist']/imagePlanes[i]['pulseMax'])*.25))*scale));
                        }
                        //console.dir(imagePlanes[i]['emptyRef'].scale);
                        // -----------

                        imagePlanes[i]['emptyRef'].updateMatrix();
                        imagePlanes[i]['emptyRef'].updateMatrixWorld(true); // Force child to inherit immediately

                        imagePlanes[i]['meshRef'].geometry.computeBoundingBox();
                        imagePlanes[i]['meshRef'].geometry.computeBoundingSphere();
                        imagePlanes[i]['meshRef'].updateMatrixWorld();
                        //imagePlanes[i]['meshRef'].updateMatrixWorld();
                    }
                    
                    // always look at player
                    //pointAtPlayerYOnly(imagePlanes[i]['emptyRef'],refList['playerGroup'].position);
                    pointAtPlayerYOnly(imagePlanes[i]['emptyRef'],targetLookat);
                    
                    if(imagePlanes[i]['stage']==0) {
                        // move towards
                        const v1=imagePlanes[i]['emptyRef'].position;
                        const v2=targetDestination;
                        const dist=v1.distanceTo(v2);
                        
                        //console.log("i="+i+" dist="+dist);
                        if(dist<=distanceInFront+10.0) {
                            imagePlanes[i]['stage']=1;
                            //console.log("i="+i+" entered stage 1");
                        }
                    } else if(imagePlanes[i]['stage']==1) {
                        // wait duration
                        if(!winState['debug']['chk_intrusive_on']) {
                            imagePlanes[i]['expire']-=dt;
                            //imagePlanes[i]['expire']+=dt*.5; // extend
                        } else {
                            imagePlanes[i]['expire']-=dt * (imagePlanes[i]['intrusiveExtend']*.01);
                        }

                        imagePlanes[i]['pos'].copy(refList['playerGroup'].position);
                        if(imagePlanes[i]['expire']<=0) {
                            // 1. Get player's current Y rotation in Radians
                            const playerRotY = -refList['playerGroup'].rotation.y-(90 * radian);
                            
                            // 2. Define the spread (40 degrees converted to Radians)
                            const spread = 40 * (Math.PI / 180);
                            
                            // 3. Pick a random angle within that spread relative to the player
                            // This centers the spawn point on the player's forward vector
                            const despawnAngle = (playerRotY + Math.PI) + (Math.random() * (spread * 2) - spread);

                            const posOrbit=rndMinMax(1500,2500);
                            const posX=Math.cos(despawnAngle) * posOrbit;
                            const posZ=Math.sin(despawnAngle) * posOrbit;
                            const posY=rndMinMax(820,1020);

                            imagePlanes[i]['pos']=new THREE.Vector3(posX,posY,posZ);
                            imagePlanes[i]['stage']=2;
                            imagePlanes[i]['pulseMax']=Math.min(imagePlanes[i]['pulseMax'],5);
                            imagePlanes[i]['pulseSpeed']=Math.min(imagePlanes[i]['pulseSpeed'],.5);

                            //console.log("i="+i+" entered stage 2");
                        }
                    } else if(imagePlanes[i]['stage']==2) {
                        // moving away to new random position
                        const v1=imagePlanes[i]['emptyRef'].position;
                        const v2=imagePlanes[i]['pos'];
                        const dist=v1.distanceTo(v2);
                        //console.dir(v1);
                        //console.dir(v2);
                        //console.log('dist='+dist);
                        if(dist<=distanceInFront+10.0) {
                            //console.log("i="+i+" entered stage 3 cleanup");
                            imagePlanes[i]['stage']=3; // dispose delete
                            dispose=true;
                        }
                    }
                    if(imagePlanes[i]['meshRef']) { 
                        // 2. Set it from the group (this calculates world bounds)
                        imagePlanes[i]['boundBox'].setFromObject(imagePlanes[i]['emptyRef']);
                        
                        const min = imagePlanes[i]['boundBox'].min;
                        const currentY = imagePlanes[i]['emptyRef'].position.y;
                        
                        // 3. If the bottom (min.y) is below ground, push the whole object up
                        if (min.y < 0) {
                            // The offset is exactly how far below 0 the minimum point is
                            imagePlanes[i]['emptyRef'].position.y = currentY + Math.abs(min.y);
                        }
                       // // 3. Get the minimum (lower) bound
                       // const min = imagePlanes[i]['boundBox'].min; // Vector3 {x: minX, y: minY, z: minZ}
                       // const max = imagePlanes[i]['boundBox'].max; // Vector3 {x: minX, y: minY, z: minZ}
                       // 
                       // //console.log('Lower bounds:', min);
                       // if(min.y<0) {
                       //     //imagePlanes[i]['emptyRef'].position.y+=1.0*dt; // keep above ground
                       //     let overlap=Math.abs(min.y);
                       //     let totalheight=max.y+overlap;
                       //     let centerpoint=totalheight*.5;
                       //     imagePlanes[i]['emptyRef'].position.y=centerpoint;
                       // }

                        if(dispose) {
                            for(let j=0;j<physicsImages.length;j++) {
                                if(physicsImages[j]==imagePlanes[i]['meshRef']) {
                                    physicsImages.splice(j,1);
                                    break;
                                }
                            }
                            despawnPlane(imagePlanes[i]['meshRef'], imagePlanes[i]['emptyRef']);
                            imagePlanes.splice(i,1);
                            i--;
                            //console.dir(imagePlanes);
                        }
                    }
                }
            }
            if(winState['debug']['chk_anime_on']) {
                while(imagePlanes.length>10) {
                    for(let j=0;j<physicsImages.length;j++) {
                        if(physicsImages[j]==imagePlanes[0]['meshRef']) {
                            physicsImages.splice(j,1);
                            break;
                        }
                    }
                    despawnPlane(imagePlanes[0]['meshRef'], imagePlanes[0]['emptyRef']);
                    // remove oldest early, keep max 10
                    imagePlanes.splice(0,1);
                }
                imageWait-=dt;
                if(activeImages==0 && imageWait<=0) {
                    getNewImage();

                    imageWait=rndMinMax(30,60);
                }
                //console.log("imageWait = "+imageWait);
            }

            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>30.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;
                }
            }

            if(doublejumpdelta>0) {
                doublejumpdelta-=dt;
            }
                
            const delta = clock.getDelta(); // For mixers
            const globalTime = clock.getElapsedTime();
            
            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;
            }

            let barHeight=cDensity;
            if((musicOn || fadeStatusLast != 0) && audioContextInitiated) {
                if(winState['audio']['chk_music_clouds']) {
                    // Get the current frequency data
                    analyser.getByteFrequencyData(dataArray);

                    // Draw bars for each frequency bin
                    for (let i = 0; i < bufferLength; i++) {
                        barHeight += dataArray[i]/255; // Amplitude value (0-255)
                    }
                    barHeight/=bufferLength;
                    //barHeight*=2;
                }
            }
            //refList["skyMaterial"].uniforms.cDensity.value=cDensity;
            //refList["customDepthMat"].uniforms.cDensity.value=cDensity;
            refList["skyMaterial"].uniforms.cDensity.value=barHeight;
            refList["customDepthMat"].uniforms.cDensity.value=barHeight;
            //console.log("cDensity="+cDensity);

            // --- MUSIC FADE ENGINE ---
            if (mF.fadeDir !== 0) {
                mF.player = $('musicplayer');
                let targetVol = (winState['audio']['slider_vol1'] || 0) * 0.01;

                if (mF.fadeDir === 1) { // Fading In
                    mF.player.volume = Math.min(targetVol, mF.player.volume + (mF.fadeSpeed * dt));

                    const fadeStatusNew=Math.floor(mF.player.volume*100);
                    //const fadeStatusNew=Math.floor((fadeStatusVol*100)/winState['audio']['slider_vol1']);
                    if(fadeStatusLast!=fadeStatusNew) {
                        fadeStatusLast=fadeStatusNew;
                        $('fadeStatus').innerHTML=" [Fading in "+fadeStatusNew+"]";
                    }
                    if (mF.player.volume >= targetVol) {
                        mF.fadeDir = 0;
                        $('fadeStatus').innerHTML="";
                    }
                    //console.log("music fade in vol="+mF.player.volume);
                } 
                else if (mF.fadeDir === -1) { // Fading Out
                    mF.player.volume = Math.max(0, mF.player.volume - (mF.fadeSpeed * dt));
                    //console.log("music fade out vol="+mF.player.volume);
                    const fadeStatusNew=Math.floor(mF.player.volume*100);
                    //const fadeStatusNew=Math.floor((fadeStatusVol*100)/winState['audio']['slider_vol1']);
                    if(fadeStatusLast!=fadeStatusNew) {
                        fadeStatusLast=fadeStatusNew;
                        $('fadeStatus').innerHTML=" [Fading out "+fadeStatusNew+"]";
                    }
                    if (mF.player.volume <= 0) {
                        mF.fadeDir = 0;
                        $('fadeStatus').innerHTML="";
                        
                        // DECISION: Reload or just Stop?
                        if (musicOn) {
                            music_play(true); // Logic for picking new track
                            console.log("player new track start");
                        } else {
                            mF.player.pause();   // Logic for manual toggle off
                            console.log("player music paused");
                        }
                    }
                }
            }
            
            var lapse=86400; // realtime
            if(testValOn) {
                lapse=2000; // debugtime testval uncomment
            }
            
            // sun position calculations
            dd=new Date();
            if(updateClock<=0) {
                hh=dd.getHours();
                mm=dd.getMinutes();
                ss=dd.getSeconds();
                phh=(hh<10)?"0"+hh:hh;
                pmm=(mm<10)?"0"+mm:mm;
                pss=(ss<10)?"0"+ss:ss;
                hh=hh*60*60;
                mm=mm*60;
                ss+=mm+hh;
            
                let tdate=phh+":"+pmm+":"+pss;
                $('tdate').innerHTML=tdate;
                updateClock=0.49;

                if(musicOn && documentReady) {
                    check_music();
                }
            } else {
                hh=dd.getHours()*60*60;
                mm=dd.getMinutes()*60;
                ss=dd.getSeconds()+mm+hh;
                updateClock-=dt;
            }
            //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;

            const milliseconds=ss+(dd.getMilliseconds()/1000.0);

            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
                let tsdiff=milliseconds-ss;
                tsp=(ss-(1.0-tsdiff))/43200.0; // 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
            
            let daynightIntensity=0;
            let ambientIntensity=0;

            //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.49) { // sun below horizon
                refList["sunLight"].position.set(sunTime.x*sunOrbit,sunTime.y*sunOrbit,sunTime.z*sunOrbit);
                daynightIntensity=((daytime - 0.49) * 2.0) * 5;
                ambientIntensity=(1.0 - (daytime * 2.0)) * .3;
                //console.dir(refList["sunLight"].position);
            } else {
                refList["sunLight"].position.set(-moonTime.x*sunOrbit,moonTime.y*sunOrbit,moonTime.z*sunOrbit);
                daynightIntensity=(1.0 - (daytime * 2.0)) * 3;
                ambientIntensity=(daytime * 2.0) * .3;
                //console.dir(refList["sunLight"].position);
            }
            currentColor.lerpVectors(nightTint, dayTint, daytime);
            refList["sunLight"].color.setFromVector3(currentColor);
            //console.log("daynightIntensity: "+daynightIntensity+" ambientIntensity: "+ambientIntensity+" ("+((daytime>=0.49)?"day":"night")+")"+" daytime = "+daytime); 
            
            // 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);

            if(vortexOn) {
                tmpColor['rlt']=0;
                tmpColor['glt']=0;
                tmpColor['blt']=3;
                                
                tmpColor['rlb']=2;
                tmpColor['glb']=2;
                tmpColor['blb']=6;
                
                tmpColor['fr']=25;
                tmpColor['fg']=25;
                tmpColor['fb']=28;
            } else {
                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), fogDensity);
            
            //console.log("lerp r1->r2 t/m="+t+"/"+m+" rlt: "+tmpColor['rlt']+" p="+p+" eP="+eP+" l="+l+" l2="+l2);
            
            refList["skyMaterial"].uniforms.topColor.value=new THREE.Color(tmpColor['rlt']/255,tmpColor['glt']/255,tmpColor['blt']/255);
            refList["skyMaterial"].uniforms.bottomColor.value=new THREE.Color(tmpColor['rlb']/255,tmpColor['glb']/255,tmpColor['blb']/255);
            // sun and sky calculations end
            
            refList["skyMaterial"].uniforms.xtime.value=milliseconds;
            refList["customDepthMat"].uniforms.xtime.value=milliseconds;
            refList["terrainMaterial"].uniforms.xtime.value=milliseconds;
            //if(treesMaterial!=null) {
            //    treeMat.uniforms.xtime.value=milliseconds;
            //}
            
            refList["skyMaterial"].uniforms.daytime.value=daytime;
            refList["customDepthMat"].uniforms.daytime.value=daytime;
            
            refList["terrainMaterial"].uniforms.daytime.value=daytime;
            refList["mountainMaterial"].uniforms.daytime.value=daytime; 

            if(imagePlanes.length>0) {
                for(let i=0;i<imagePlanes.length;i++) {
                    if(imagePlanes[i]['materialRef']) {
                        //console.log('test');
                        //console.dir(imagePlanes[i]['meshRef']);
                        imagePlanes[i]['materialRef'].uniforms.daytime.value=daytime;
                    }
                }
            }

            
            //sky.position.copy(camera.position).setY(0);
            //sky.position.set(0.0, 0.0, camera.position.z);
            //refList["sky"].position.copy(refList["camera"].position);

            if(!initialized) return;

            if (!gravityInitialized && !tabHidden && isFloorReady()) {
                // Optional: wait 2-3 frames to ensure the Raycaster can actually
                // compute intersections against the bounding boxes
                gravityInitialized = true;
                fakeGround = false;
            }

            const cam = refList["camera"];
            const renderer=refList["renderer"]; 

            // Update water heightmap
            refList["waterHeightMaterial"].uniforms.data.value.set(milliseconds * 0.0001, waterLevel, 2.0); // tune time scale
            renderer.setRenderTarget(refList["waterHeightRT"]);
            renderer.clear();
            renderer.render(refList["heightQuad"], refList["heightOrthoCam"]);  // or use a scene with just the quad
            renderer.setRenderTarget(null);

            handleMovement(delta,milliseconds,lapse);
            
            refList["skyGroup"].position.copy(refList["playerGroup"].position);

            //if(markOn && markWait<=0) {
                markActive();
            //} else if(markOn) {
            //    markWait-=dt;
            //}
            
            //controls.update();

// ==================== WATER UNIFORMS - CRITICAL ORDER ==========
const waterMesh = refList['waterMesh'];

// 1. Make sure lights are positioned correctly
// 2. CRITICAL MOVE: Position the lights/cameras BEFORE calculating world matrices
positionShadowCameras(refList["sunLight"], refList["clockShadowLight"]);

refList["sunLight"].target.updateMatrixWorld();
refList["clockShadowLight"].target.updateMatrixWorld();

refList["skyGroup"].updateMatrixWorld();
waterMesh.updateMatrixWorld(true);           // ← Important
cam.updateProjectionMatrix(); // Apply the update
cam.updateMatrixWorld(true);

const cameraWorldPosition = new THREE.Vector3();
            cam.getWorldPosition(cameraWorldPosition);
            
            refList["sunLight"].intensity=daynightIntensity;
            refList["clockShadowLight"].intensity=daynightIntensity;

            refList["skyMaterial"].uniforms.sunDirection.value = sunTime.normalize();
            refList["customDepthMat"].uniforms.sunDirection.value = sunTime.normalize();
            //skyMaterial.uniforms.moonDirection.value = moonTime.normalize();
            refList["terrainMaterial"].uniforms.cameraPosition.value.copy(cameraWorldPosition);
            refList["terrainMaterial"].uniforms.lightDir.value.copy(refList["sunLight"].position).normalize();
            refList["terrainMaterial"].uniforms.lightDir2.value.copy(refList["sunLight"].position).normalize().negate();

            refList["mountainMaterial"].uniforms.cameraPosition.value.copy(cameraWorldPosition);
            refList["mountainMaterial"].uniforms.lightDir.value.copy(refList["sunLight"].position).normalize();
            refList["mountainMaterial"].uniforms.lightDir2.value.copy(refList["sunLight"].position).normalize().negate();


            const waterMat = refList["waterMaterial"];
            //if(cameraWorldPosition.y<getWaterHeightAt(cameraWorldPosition.x,cameraWorldPosition.z)) {
            //    // inverted
            //    waterMat.uniforms.underWater.value = 1;
            //} else {
            //    waterMat.uniforms.underWater.value = 0;
            //}
            waterMat.uniforms.sunPos.value.copy(refList["sunLight"].position);
            waterMat.uniforms.moonPos.value.copy(refList["sunLight"].position).negate(); // rough moon
            waterMat.uniforms.daytime.value = daytime;
 
            for(let i=0;i<coinRef['coinMaterials'].length;i++) {
                const coinMat=coinRef['coinMaterials'][i];
                coinMat.uniforms.daytime.value = daytime;
                //coinMat.uniforms.xtime.value = milliseconds;
                coinMat.uniforms.cameraPosition.value.copy(cameraWorldPosition);
                coinMat.uniforms.lightDir.value.copy(refList["sunLight"].position).normalize();
                coinMat.uniforms.lightDir2.value.copy(refList["sunLight"].position).normalize().negate();
            }
            for(let i=0;i<refList['standardMatUpdate'].length;i++) {
                const smuMat=refList['standardMatUpdate'][i];
                smuMat.uniforms.daytime.value = daytime;
                //smuMat.uniforms.xtime.value = milliseconds;
                smuMat.uniforms.cameraPosition.value.copy(cameraWorldPosition);
                smuMat.uniforms.lightDir.value.copy(refList["sunLight"].position).normalize();
                smuMat.uniforms.lightDir2.value.copy(refList["sunLight"].position).normalize().negate();
            }


            // all traversing uniform updates moved down below to 
            // include calculated daytime and avoid double iteration
            
            normalMatrix.getNormalMatrix(refList["sky"].matrixWorld);
            refList["skyMaterial"].uniforms.nMatrix.value = normalMatrix;
            refList["skyMaterial"].uniforms.vMatrix.value.copy(cam.matrixWorldInverse);
            refList["customDepthMat"].uniforms.nMatrix.value = normalMatrix;
            refList["customDepthMat"].uniforms.vMatrix.value.copy(cam.matrixWorldInverse);
            mvpMatrix
                .multiplyMatrices(cam.projectionMatrix.clone(),
                    cam.matrixWorldInverse.clone())
                .multiply(refList["sky"].matrixWorld);
            refList["skyMaterial"].uniforms.mvpMatrix.value = mvpMatrix;
            refList["customDepthMat"].uniforms.mvpMatrix.value = mvpMatrix;

// 2. Build matrices
const modelM = waterMesh.matrixWorld;
const viewM  = cam.matrixWorldInverse.clone();
const projM  = cam.projectionMatrix;

waterMat.uniforms.MVP.value
    .copy(projM)
    .multiply(viewM)
    .multiply(modelM);

waterMat.uniforms.modelMatrixV.value.copy(modelM);

//waterMat.uniforms.invProjection.value.copy(projM).invert();
//waterMat.uniforms.invView.value.copy(viewM).invert();
waterMat.uniforms.invProjection.value.copy(cam.projectionMatrixInverse);
waterMat.uniforms.invView.value.copy(cam.matrixWorld);

waterMat.uniforms.viewPos.value.copy(cameraWorldPosition);
waterMat.uniforms.waterHeightTex.value = refList["waterHeightRT"].texture;

waterMat.uniforms.playerOffset.value.copy(refList["playerGroup"].position);

// Tune these
waterMat.uniforms.data.value.set(milliseconds, waterLevel, 2.0); // heightScale
//waterMat.uniforms.data.value.set(daytime, -1.1, 2.0); // heightScale
// ==============================================================
            
            if (initialized && renderer.domElement.width > 0) {
                //renderer.state.buffers.color.setMask(true);
                //renderer.state.buffers.depth.setMask(true);
                //renderer.state.buffers.stencil.setMask(0xff); // Ensure stencil is clearable
                //const gl = renderer.getContext();
                //const bitmask = gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT;

                //
                // Pipeline-
                //  Render Scene Without Water (Depth)
                //  Render Scene With Water (Depth)
                //  Render Custom Sun Shadows
                //  Render Custom Clock (near) Shadows
                //  Render Custom Sky (clouds) Shadow to Sun Shadows
                //  Render Main Color
                //  Render Post Scene
                //  Render Ray Lines/Helpers
                //  Render WaterRT/Scissor
                
                // === DEPTH PASSES FOR UNDERWATER ===
                cam.layers.set(2); // set to clear all other
                cam.layers.enable(1); // enable accumulate
                const env = refList['cachedEnv'];
               
                // Force all objects to use depth material

                //scene.overrideMaterial = refList["customDepthMaterial"];
                for(let i=0;i<env.length;i++) env[i].mesh.material = refList["customDepthMaterial"];
                for(let i = 0; i < activeCoins.length; i++) activeCoins[i].mesh.material = refList["customDepthMaterial"];
                //waterMesh.material = refList["customWaterDepthMaterial"];

                // 2. Render scene WITHOUT water → waterDepthRT
                renderer.setRenderTarget(refList["waterDepthRT"]);
                renderer.clear(true, true, true);
                //waterMesh.visible = false;
                renderer.render(scene, cam);
                //waterMesh.visible = waterWasVisible;

                cam.layers.enable(0);
                
                // 3. Render scene WITH water → sceneDepthRT + main color
                renderer.setRenderTarget(refList["sceneDepthRT"]);
                renderer.clear(true, true, true);
                renderer.render(scene, cam);
                //scene.overrideMaterial = null;           // restore

                // ================================================
                // FORCE SHADOW MAPS TO UPDATE NOW WITH ORIGINAL MATERIALS ACTIVE
                // ================================================

                //----renderer.shadowMap.autoUpdate = true; // Temporarily enable for correct timing
                //----renderer.shadowMap.needsUpdate = true; // Force if needed
                // This forces Three.js to update all shadow maps NOW based on current positions (not needed with hack below)
                //renderer.compile(scene, refList["camera"]);

                // Hack: Render with a tiny viewport to minimize cost (shadow map is internal)
                const oldViewport = renderer.getViewport(new THREE.Vector4());
                //----renderer.setViewport(0, 0, 1, 1); // tiny 1x1 pixel
                //----renderer.render(scene, refList["shadowCamera"]); // dummy render to trigger shadow map
                //----renderer.setViewport(oldViewport); // restore

                // STEP A: Turn off the sky entirely for a split second
                refList['sky'].visible = false;
                
                // STEP B: Bind the physical landscape geometry to bake solid shadows first
                for(let i = 0; i < env.length; i++) env[i].mesh.material = refList["shadowDepthMaterial"];
                for(let i = 0; i < activeCoins.length; i++) activeCoins[i].mesh.material = refList["shadowDepthMaterial"];
                
                // STEP C: Initialize the custom shadow targets and render the raw landscape depth
                // This guarantees the backside of mountains stores real, close-up geometric depth limits!
                const sunMatrix = renderCustomShadows(refList["sunLight"], refList['customSunShadowTarget'], renderer);
                const clockMatrix = renderCustomShadows(refList["clockShadowLight"], refList['customClockShadowTarget'], renderer);
                
                // STEP D: Turn the sky back on and switch it to your cloud depth compiler
                refList['sky'].visible = true;
                refList['sky'].material = refList['customDepthMat'];
                
                // STEP E: Append the cloud textures onto the target without cleaning out the mountain data
                // We override renderer.autoClear to false so the sky supplements the map instead of wiping it out!
                const originalAutoClear = renderer.autoClear;
                renderer.autoClear = false;
                
                // Force render just the sky layer from the light perspectives
                renderer.setRenderTarget(refList['customSunShadowTarget']);
                renderer.render(refList['sky'], refList["sunLight"].shadow.camera);
                
                // STEP F: Reset pipeline configurations safely for the frame beauty passes
                renderer.autoClear = originalAutoClear;
                renderer.setRenderTarget(null);
                refList['sky'].material = refList['skyMaterial'];
                
                for(let i = 0; i < env.length; i++) env[i].mesh.material = env[i].originalMaterial;
                for(let i = 0; i < activeCoins.length; i++) activeCoins[i].mesh.material = activeCoins[i].originalMaterial;
                
                applyShadowUniforms(sunMatrix, clockMatrix);
            
                // 4. Copy color to mainColorRT for post-processing
                renderer.setRenderTarget(refList["mainColorRT"]);
                renderer.clear(true, true, true);
                renderer.render(scene, cam);

                // Back to main render
                renderer.setRenderTarget(null);
                //if (bitmask !== 0) {
                //    renderer.clear(true, true, true);
                //}
            
                // 5. FINAL POST-PROCESSING
                const postMat=refList["postMaterial"];

                postMat.uniforms.screenTexture.value = refList["mainColorRT"].texture;
                postMat.uniforms.sceneDepthTex.value = refList["sceneDepthRT"].texture;
                postMat.uniforms.waterDepthTex.value = refList["waterDepthRT"].texture;
                postMat.uniforms.waterHeightTex.value = refList["waterHeightRT"].texture;

                //postMat.uniforms.cameraPosition.value.copy(cameraWorldPosition);
                //postMat.uniforms.invProjection.value.copy(cam.projectionMatrix).invert();
                //postMat.uniforms.invView.value.copy(cam.matrixWorldInverse).invert();
                postMat.uniforms.invProjection.value.copy(cam.projectionMatrixInverse);
                postMat.uniforms.invView.value.copy(cam.matrixWorld);
                postMat.uniforms.viewPos.value.copy(cameraWorldPosition);

                postMat.uniforms.data.value.set(milliseconds * 0.001, waterLevel, 2.0);
                postMat.uniforms.daytime.value = daytime;
                postMat.uniforms.playerOffset.value.copy(refList["playerGroup"].position);

                // Render post-processing quad
                renderer.render(refList["postScene"], refList["postCamera"]);

                // 6. DEBUG OVERLAY PASS
                // Now we draw the lines ON TOP of the post-processed frame.
                if (showRayLines) {
                    renderer.autoClear = false; // VERY IMPORTANT: Don't wipe the water we just drew
                
                    for (let i = 0; i < refList['rayLines'].length; i++) {
                        const line = refList['rayLines'][i];
                        if (line) {
                            // Ensure they use their bright material
                            line.material = refList['lineRedMat'];
                
                            // OPTIONAL: If you want them to pierce through terrain too:
                            // line.material.depthTest = false;
                
                            renderer.render(line, cam);
                        }
                    }
                
                    renderer.autoClear = true; // Reset for next frame
                }

                if(waterHeightShow) {
                    const smallest = Math.min(renderer.domElement.width, renderer.domElement.height);
                    const size = 888;
                    //const x = 20;
                    const x = (renderer.domElement.width * .5) - (size * .5);
                    const y = renderer.domElement.height - size - 20;

                    // Render height texture to screen directly (easiest)
                    //renderer.setRenderTarget(null);
                    renderer.setViewport(x, y, size, size);
                    
                    // You can use a simple full-screen quad with the height texture or just blit
                    // For now, let's make a temporary debug plane if you don't have one
                    if (!refList['debugHeightPlane']) {
                        const debugMat = new THREE.MeshBasicMaterial({ 
                            map: refList["waterHeightRT"].texture,
                            side: THREE.DoubleSide 
                        });
                        const debugPlane = new THREE.Mesh(new THREE.PlaneGeometry(2,2), debugMat);
                        refList['debugHeightPlane'] = debugPlane;
                    }

                    // Quick & dirty: just set scissor + viewport and render a small ortho quad
                    renderer.setScissor(x, y, size, size);
                    renderer.setScissorTest(true);
                    renderer.render(refList["heightQuad"], refList["heightOrthoCam"]); // reuse the same height quad (it already samples nothing)
                    renderer.setScissorTest(false);
                    //renderer.setViewport(0, 0, renderer.domElement.width, renderer.domElement.height);
                    renderer.setViewport(oldViewport); // restore
                }            
            }
        }

        init();
        animate(0);

var audioFilters=[ // hz,db,obj
    [60,0.0,null],
    [170,0.0,null],
    [310,0.0,null],
    [600,0.0,null],
    [1000,0.0,null],
    [3000,0.0,null],
    [6000,0.0,null],
    [12000,0.0,null],
    [14000,0.0,null],
    [16000,0.0,null]
];

function createAudioContext() {
    audioContextInitiated=true;

    audioContext = new (window.AudioContext || window.webkitAudioContext)();

    // Create a source node from the audio element
    source = audioContext.createMediaElementSource($('musicplayer'));

    // Create an AnalyserNode for audio analysis
    analyser = audioContext.createAnalyser();
    analyser.fftSize = 128; // Number of samples for FFT (higher = more detail)
    bufferLength = analyser.frequencyBinCount; // Half of fftSize
    dataArray = new Uint8Array(bufferLength); // Array to store frequency data

    for(var i=0;i<audioFilters.length;i++) {
        audioFilters[i][2] = audioContext.createBiquadFilter();
        var type="lowshelf";
        if(i>6) {
            type="highshelf";
        } else if(i>3) {
            type="peaking";
        }
        audioFilters[i][2].type=type;
        audioFilters[i][2].frequency.value=audioFilters[i][0];
    }

    // Connect the audio source to the analyser and then to the output (speakers)
    source.connect(analyser);

    analyser.connect(audioFilters[0][2]);
    for(var i=0;i<audioFilters.length-1;i++) {
        audioFilters[i][2].connect(audioFilters[(i+1)][2]);
    }
    //analyser.connect(audioContext.destination);
    audioFilters[9][2].connect(audioContext.destination);
}
    </script>
        
    <div class="tbl cmwindow" id="cmw_audio" style="width: 460px; height: auto; right: 0px; bottom: 100px; top: auto; left: auto;">
        <div class="tr">
        <div class="td cwindow top-l"></div>
        <div class="td cwindow top-c"></div>
        <div class="td cwindow top-r"></div>
        </div>
        <div class="tr">
        <div class="td cwindow mid-l"></div>
        <div class="td cwindow mid-c">
            <span class="cwindow_title">Audio settings</span>
            <div class="ui-spr close" id="win_btn_close_audio"></div>
            <div class="cgroup">
                <div class="ui-spr checkbox" id="chk_music_on"></div>
                <label for="chk_music_on">Music On</label><span id="fadeStatus"></span>
                <br />
                <div class="ui-spr checkbox_checked" id="chk_music_clouds"></div>
                <label for="chk_music_clouds">Visualize Clouds</label>
                <br />
                <div class="ui-spr slider-v" id="slider_vol1">
                    <div class="ui-spr slider-v handle"></div>
                </div>
                <br />
                <label for="slider_vol1" id="lb_slider_vol1">Music<br />(<span id="lb_slider_vol1p">100%</span>)</label>
                <br />
            </div><!-- cgroup -->
            <div class="button_group">
                <div class="ui-spr button" id="btn_close_audio">
                    <span>Close</span>
                </div>
            </div>
        </div>
        <div class="td cwindow mid-r"></div>
        </div>
        <div class="tr">
        <div class="td cwindow bot-l"></div>
        <div class="td cwindow bot-c"></div>
        <div class="td cwindow bot-r"></div>
        </div>
    </div>

    <div class="tbl cmwindow" id="cmw_joystick" style="width: 460px; height: auto; left: 0px; top: 32px;">
        <div class="tr">
        <div class="td cwindow top-l"></div>
        <div class="td cwindow top-c"></div>
        <div class="td cwindow top-r"></div>
        </div>
        <div class="tr">
        <div class="td cwindow mid-l"></div>
        <div class="td cwindow mid-c">
            <span class="cwindow_title">Joystick settings</span>
            <div class="ui-spr close" id="win_btn_close_joystick"></div>
            <div class="cgroup">
                <div class="ui-spr checkbox" id="chk_joy_on"></div>
                <label for="chk_joy_on">[J]oysticks On</label>
                <br />
                <div class="ui-spr checkbox" id="chk_joy_strafe"></div>
                <label for="chk_joy_strafe">Joysticks Camera Strafe</label>
                <br />
                <div class="ui-spr checkbox" id="chk_joy_inv_x"></div>
                <label for="chk_joy_inv_x">Joysticks Invert X</label>
                <br />
                <div class="ui-spr checkbox" id="chk_joy_inv_y"></div>
                <label for="chk_joy_inv_y">Joysticks Invert Y</label>
                <br />
                <!--<div class="ui-spr checkbox_checked" id="chk_joy_smooth"></div>
                <label for="chk_joy_smooth">Smooth Joysticks</label>
                <br />-->
            </div><!-- cgroup -->
            <div class="button_group">
                <div class="ui-spr button" id="btn_close_joystick">
                    <span>Close</span>
                </div>
            </div>
        </div>
        <div class="td cwindow mid-r"></div>
        </div>
        <div class="tr">
        <div class="td cwindow bot-l"></div>
        <div class="td cwindow bot-c"></div>
        <div class="td cwindow bot-r"></div>
        </div>
    </div>

    <div class="tbl cmwindow" id="cmw_keybind" style="width: 460px; height: auto; left: calc(50% - 230px); top: calc(50% - 250px);">
        <div class="tr">
        <div class="td cwindow top-l"></div>
        <div class="td cwindow top-c"></div>
        <div class="td cwindow top-r"></div>
        </div>
        <div class="tr">
        <div class="td cwindow mid-l"></div>
        <div class="td cwindow mid-c">
            <span class="cwindow_title">Keybind settings</span>
            <div class="ui-spr close" id="win_btn_close_keybind"></div>
            <div class="cgroup">
<pre>
[0-4]   toggle rendmode
[5]     toggle rayshow
[6]     toggle flying
[7]     toggle waterrt

-Edit mode-
  [WASD]   +[arrows] move/turn
  [QE]     strafe
  [C]      crouch
  [Space]  jump
  [Shift]  sprint
  [G]      toggle cursorlock (game mode)
  [J]      toggle joysticks (game mode)
  [M]      toggle groundtarget mark

-Game mode-
  [WASD]   move/strafe
  [arrows] move/turn
  [QE]     turn
  [C]      crouch
  [Space]  jump
  [Shift]  sprint
  [G]      toggle cursorlock (edit mode)
  [J]      toggle joysticks (edit mode)
  [M]      toggle groundtarget mark

-Joysticks-
  Left     move/strafe
  Right    turn/pitch(camera)
</pre>
            </div><!-- cgroup -->
            <div class="button_group">
                <div class="ui-spr button" id="btn_close_keybind">
                    <span>Close</span>
                </div>
            </div>
        </div>
        <div class="td cwindow mid-r"></div>
        </div>
        <div class="tr">
        <div class="td cwindow bot-l"></div>
        <div class="td cwindow bot-c"></div>
        <div class="td cwindow bot-r"></div>
        </div>
    </div>

    <div class="tbl cmwindow" id="cmw_debug" style="width: 460px; height: auto; left: -0px; bottom: 100px; top: auto;">
        <div class="tr">
        <div class="td cwindow top-l"></div>
        <div class="td cwindow top-c"></div>
        <div class="td cwindow top-r"></div>
        </div>
        <div class="tr">
        <div class="td cwindow mid-l"></div>
        <div class="td cwindow mid-c">
            <span class="cwindow_title">Debug settings</span>
            <div class="ui-spr close" id="win_btn_close_debug"></div>
            <div class="cgroup">
                <div class="ui-spr checkbox" id="chk_shadow_debug"></div>
                <label for="chk_shadow_debug">Shadow Debug On (Shadows must be On)</label>
                <br />
                <div class="ui-spr checkbox" id="chk_shadow_helpers"></div>
                <label for="chk_shadow_helpers">Shadow Helpers On</label>
                <br />
                <div class="ui-spr checkbox" id="chk_axis_grid"></div>
                <label for="chk_axis_grid">Axis Grid On</label>
                <br />
                <div class="ui-spr checkbox" id="chk_wireframe"></div>
                <label for="chk_wireframe">Wireframe On</label>
                <br />
                <div class="ui-spr checkbox" id="chk_vortex_on"></div>
                <label for="chk_vortex_on">Sky Vortex On</label>
                <br />
                <div class="ui-spr checkbox" id="chk_anime_on"></div>
                <label for="chk_anime_on">Anime On</label>
                <div class="ui-spr checkbox" id="chk_intrusive_on"></div>
                <label for="chk_intrusive_on">Intrusive</label>
                <br />
                <!--<label for="chk_mountain_res">Player speed</label>
                <div class="textinputwrap">
                <div class="ui-spr textinput" style="width: 50px;">
                    <input type="text" value="1" style="width: 100%;" />
                </div>
                </div>
                <br />
                <div class="ui-spr checkbox" id="chk_gravity_off"></div>
                <label for="chk_gravity_off">Gravity Off</label>
                <br />
                <div class="ui-spr checkbox" id="chk_test_time"></div>
                <label for="chk_test_time">Use Test Time (Default Realtime)</label>
                <br />-->
            </div><!-- cgroup -->
            <div class="button_group">
                <div class="ui-spr button" id="btn_close_debug">
                    <span>Close</span>
                </div>
            </div>
        </div>
        <div class="td cwindow mid-r"></div>
        </div>
        <div class="tr">
        <div class="td cwindow bot-l"></div>
        <div class="td cwindow bot-c"></div>
        <div class="td cwindow bot-r"></div>
        </div>
    </div>

    <div class="tbl cmwindow" id="cmw_quality" style="width: 460px; height: auto; top: 32px; right: 0px; left: auto;">
        <div class="tr">
        <div class="td cwindow top-l"></div>
        <div class="td cwindow top-c"></div>
        <div class="td cwindow top-r"></div>
        </div>
        <div class="tr">
        <div class="td cwindow mid-l"></div>
        <div class="td cwindow mid-c">
            <span class="cwindow_title">Quality settings</span>
            <div class="ui-spr close" id="win_btn_close_quality"></div>
            <div class="cgroup">
                <div class="ui-spr checkbox_checked" id="chk_shadow_on"></div>
                <label for="chk_shadow_on">Shadows On</label>
                <br />
                <div class="ui-spr checkbox_checked" id="chk_clouds_on"></div>
                <label for="chk_clouds_on">Clouds On</label>
                <br />
                <div class="ui-spr checkbox_checked" id="chk_shadow_ui"></div>
                <label for="chk_shadow_ui">UI Shadows On</label>
                <br />
                <br />
                <label for="sel_mountain_res">Mountain Resolution</label>
                <div class="selist selistbg">
                    <span id="sel_mountain_res">512x512</span>
                    <div class="tbl ult">
                    <div class="tr">
                    <div class="td selistbg selisttop">
                    </div>
                    </div>
                    <div class="tr">
                    <div class="td">

                    <ul class="selistbg">
                        <li><a href="#" id="selist_mr_512">512x512</a></li>
                        <li><a href="#" id="selist_mr_256">256x256</a></li>
                        <li><a href="#" id="selist_mr_128">128x128</a></li>
                    </ul>

                    </div>
                    </div>
                    <div class="tr">
                    <div class="td selistbg selistbot">
                    </div>
                    </div>
                    </div> <!-- tbl -->
                </div>
                <br />
                <br />
                <label for="sel_render_res">Render Resolution</label>
                <div class="selist selistbg">
                    <span id="sel_render_res">100%</span>
                    <div class="tbl ult">
                    <div class="tr">
                    <div class="td selistbg selisttop">
                    </div>
                    </div>
                    <div class="tr">
                    <div class="td">

                    <ul class="selistbg">
                        <li><a href="#" id="selist_rr_100">100%</a></li>
                        <li><a href="#" id="selist_rr_80">80%</a></li>
                        <li><a href="#" id="selist_rr_66">66%</a></li>
                        <li><a href="#" id="selist_rr_50">50%</a></li>
                        <li><a href="#" id="selist_rr_33">33%</a></li>
                        <li><a href="#" id="selist_rr_25">25%</a></li>
                    </ul>

                    </div>
                    </div>
                    <div class="tr">
                    <div class="td selistbg selistbot">
                    </div>
                    </div>
                    </div> <!-- tbl -->
                </div>
                <br />
                <br />
                <div class="ui-spr slider-h" id="slider_sway1">
                    <div class="ui-spr slider-h handle"></div>
                </div>
                <label for="slider_sway1" id="lb_slider_sway1">Natural Camera (<span id="lb_slider_sway1p">100%</span>)</label>
                <br />
                <!--<label for="chk_cloud_detail">Clouds detail</label>
                <div class="textinputwrap">
                <div class="ui-spr textinput" style="width: 50px;">
                    <input type="text" value="2" style="width: 100%;" />
                </div>
                </div>
                <br />-->
            </div><!-- cgroup -->
            <div class="button_group">
                <div class="ui-spr button" id="btn_close_quality">
                    <span>Close</span>
                </div>
            </div>
        </div>
        <div class="td cwindow mid-r"></div>
        </div>
        <div class="tr">
        <div class="td cwindow bot-l"></div>
        <div class="td cwindow bot-c"></div>
        <div class="td cwindow bot-r"></div>
        </div>
    </div>

    <!--<div id="testshadow"></div>-->
</body>
</HTML>

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



2 779 485 visits
... ^ v