<!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