<!DOCTYPE html>
<!--
Author: Twily 2025-2026
Website: twily.info
Description: threejs playground splat skybox shadows ui joysticks
music: Look How High I Took You - Dark Hypnotic Techno
Dark & Progressive House Set at the Grand Canyon - DΛRK SØUND
-->
<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 Music</a></li>
<li class="mnu-spr"><a href="#" id="imu1_6" 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 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(1024.0, 1024.0); // 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(4096.0, 4096.0); // 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:
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
// 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)+.4; // .4 to .6
float ambianceStrength=((1.0-daytime)*.2)+.6*ambientMulti; // .6 to .4
color *= ((max(clockShadow*sunEval,0.0)) * shadowStrength) + ambianceStrength; // shadow factor + ambient day
} else { // else( shadowOn<1 )
// always shadow sundial
shadow*=zeron;
// TERRAIN
float shadowStrength = (daytime * .2) + .4; // .4 to .6
float ambianceStrength = (daytime * .1) + .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 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) * 1.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(4096.0, 4096.0); // 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) + .4; // .4 to .6
float ambianceStrength = ((1.0 - daytime) * .1) + .6; // .6 to .4
color *= (shadow * shadowStrength) + ambianceStrength; // shadow factor + ambient day
} // shadowOn>=1
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);
}
`;
// 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;
scene.add(helper);
const axis = new THREE.AxesHelper(1000);
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);
scene.add(clockHelper);
// debug shadow camera
helper=new THREE.CameraHelper(refList["sunLight"].shadow.camera);
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","./assets/"+file,true);
xhr.send(null);
}
var assetLoaded=[];
function readAssets() {
ajaxSource("assets.json",function(data) {
//console.log(data);
});
}
//readAssets();
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,
transparent: true,
side: THREE.DoubleSide,
// Set influence: x and y usually set to same value
normalScale: new THREE.Vector2(0.5, 0.5)
});
refList['railMaterial']=railMat;
// Optional: If the railing has an alpha map/holes,
// use alphaTest to prevent "ghosting"
railMat.alphaTest = 0.5;
railMat.needsUpdate = true;
for (let i = 0; i < 4; i++) {
const rail = new THREE.Mesh(railGeoLong, railMat);
// Set the railings to be rendered after the floor
rail.renderOrder=2;
rail.name="rail"+i;
rail.castShadow = true;
rail.receiveShadow = true;
rail.visible = false;
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 - runrise]
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)
});
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;
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 = 1;
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.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(20,80);
//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?
}
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) {
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;
const sideDir = new THREE.Vector3(-checkDir.z, 0, checkDir.x);
const horizontalOffsets = [-0.4, 0, 0.4];
const stepBack = 0.6;
// 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;
for (let offset of horizontalOffsets) {
const origin = refList["playerGroup"].position.clone()
.addScaledVector(sideDir, offset)
.addScaledVector(checkDir, -stepBack)
.setY(refList["playerGroup"].position.y + .4);
playerRaycaster.set(origin, checkDir);
const hits = playerRaycaster.intersectObjects(collidableObjects);
if (hits.length > 0) {
const hit = hits[0];
// 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) {
// 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));
}
}
}
}
}
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;
}
}
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,
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
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,
local: [
["chk_shadow_ui",shadow_ui_toggle],
["slider_sway1",natural_cam_set],
["sel_mountain_res",sel_mountain_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";
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
}
}
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/high.mp3","https://twily.info/demo/canyon.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;
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 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);
}
}
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 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;
}
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 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;
}
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['skyGroup'].visible=!nstate;
//refList['sky'].visible = !nstate;
}
const shadowBias = 0.0002; // slight negative for acne
const shadowNormalBias = 0.001; // for bumpy terrain/mountains
const shadowRadius = 4; // softens edges (if using BasicShadowMap, ignore for PCF)
const shadowMapWidth = 4096;
const shadowMapHeight = 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.0002; // not negative ?
const clockShadowNormalBias = 0.01; // for bumpy terrain/mountains
const clockShadowRadius = 4; // softens edges (if using BasicShadowMap, ignore for PCF)
const clockShadowMapWidth = 1024;
const clockShadowMapHeight = 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
"shadowCamera": null, // dummy for trigger
"tile": null,
"terrainMaterial": null, // for floor
"mountainMaterial": null, // distant mountain tile
"imageMaterial": null,
"railMaterial": null,
"edgeMaterial": null,
"edgeList": [],
"railList": [],
"projectorMatrix": null,
"projectionMatrix": null,
"projectionMatrix": null,
"tempEuler": null,
"rotMat": null,
}
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"], refList["sky2"], ...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 floor, 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;
let documentReady = false;
var tabTime = 0;
var fakeGround = true;
var tabHidden = false;
document.body.onload=function() {
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 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
//sunLight.shadow.camera.updateProjectionMatrix();
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);
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, window.innerWidth / window.innerHeight, 0.1, 1000);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 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 shadowCamera = camera.clone(); // just a dummy
const playerGroup = new THREE.Group();
scene.add(playerGroup);
playerGroup.position.set(0, player.height, 15);
playerGroup.add(camera);
playerGroup.add(shadowCamera);
//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
// 2. Renderer
const renderer = new THREE.WebGLRenderer({ antialias: true, stencil: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
//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
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // nicer looks ok but light
// 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)
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = shadowMapWidth;
sunLight.shadow.mapSize.height = shadowMapHeight;
sunLight.shadow.bias = shadowBias;
sunLight.shadow.normalBias = shadowNormalBias;
sunLight.shadow.radius = shadowRadius;
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 = clockShadowMapWidth;
clockShadowLight.shadow.mapSize.height = clockShadowMapHeight;
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;
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;
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
clock = new THREE.Clock();
//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 },
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);
// 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 },
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.4 },
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,
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;
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;
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;
terrainGroup.add(mountains);
refList["tile"]=mountains;
tileInitialized=true;
setTimeout(function() {
if(initialized) {
refList["skyMaterial"].uniforms.cloudsReady.value = 1;
refList['sky2'].visible = true;
refList['floor'].visible = true;
refList['tile'].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;
}
}
});
});
// 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
//});
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
});
refList["ambientLight"] = ambientLight;
refList["sunLight"] = sunLight;
refList["skyMaterial"] = skyMaterial;
refList["skyGroup"] = skyGroup;
refList["sky"] = sky;
refList["customDepthMat"] = customDepthMat;
refList["camera"] = camera;
refList["playerGroup"] = playerGroup;
refList["shadowCamera"]=shadowCamera;
refList["renderer"] = renderer;
refList["clockShadowLight"] = clockShadowLight;
refList["mountainMaterial"]=mountainMat;
refList["terrainMaterial"]=terrainMat;
refList["terrainGroup"] = terrainGroup;
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();
createGlassEdges();
createRailings();
// 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) {
fakeGround=true;
tabHidden=true;
gravityInitialized=false;
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 === 'KeyM') {
mark_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 / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 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 / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 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', music_toggle);
$('imu1_6').addEventListener('click', mark_toggle);
if(get_vo!=null) { // late settings
vortex_toggle(null,true);
}
initialized = true;
hideLoadScreen();
setTimeout(function() {
if(tileInitialized) {
refList["skyMaterial"].uniforms.cloudsReady.value = 1;
refList['sky2'].visible = true;
refList['floor'].visible = true;
refList['tile'].visible = true;
}
});
}
//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;
}
// Calculate mouse position in normalized device coordinates
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 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) {
// 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"], refList["sky2"], ...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 = keys['KeyC'] ? 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");
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;
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 (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
const forwardVec = new THREE.Vector3(0, 0, -1).applyQuaternion(refList["playerGroup"].quaternion);
forwardVec.y = 0;
forwardVec.normalize();
const rightVec = new THREE.Vector3().crossVectors(forwardVec, new THREE.Vector3(0, 1, 0)).normalize();
// Reset horizontal velocity but keep vertical (for gravity/jumping)
player.velocity.x = 0;
player.velocity.z = 0;
// Add Forward/Back contribution
if (moveForward !== 0) {
player.velocity.add(forwardVec.multiplyScalar(moveForward * currentSpeed));
}
// Add Strafe contribution
if (moveStrafe !== 0) {
player.velocity.add(rightVec.multiplyScalar(moveStrafe * currentSpeed));
}
// 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["shadowCamera"].quaternion.setFromEuler(new THREE.Euler(player.pitch, player.rotation, 0, 'YXZ'));
//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["shadowCamera"].quaternion.setFromEuler(new THREE.Euler(player.pitch, 0, 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, player.rotation, bobRoll, 'YXZ'));
refList["camera"].quaternion.setFromEuler(new THREE.Euler(player.pitch, 0, bobRoll, 'YXZ'));
refList["shadowCamera"].quaternion.setFromEuler(new THREE.Euler(player.pitch, 0, 0, 'YXZ'));
}
// 5. Physics & Gravity
if (((player.isGrounded || (doublejumpready>0 && doublejumpdelta>0))) && keys['Space']) {
keys['Space']=false;
player.velocity.y = 6.0
if(!player.isGrounded) {
doublejumpready--;
doublejumpdelta=0;
console.log("double jump");
} else {
doublejumpdelta=0.6;
console.log("single jump");
}
player.isGrounded = false;
}
//refList["playerGroup"].position.add(player.velocity.clone().multiplyScalar(delta));
const nextPos = refList["playerGroup"].position.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) {
nextPos.y = player.height;
player.velocity.y = 0;
player.isGrounded = true;
}
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);
handlePlaneCollisions(physicsImages, nextPos, delta); // Pass your array of images/planes
handleWorldBoundaries(nextPos);
handleGrounding(nextPos);
// 4. Update the actual position
refList["playerGroup"].position.copy(nextPos);
} else { // old simple / Floor Snap
// 4. Update the actual position
refList["playerGroup"].position.copy(nextPos);
const targetHeight = keys['KeyC'] ? 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;
}
}
player.velocity.y -= 15.0 * delta;
}
function onWindowResize() {
refList["camera"].aspect = window.innerWidth / window.innerHeight;
refList["camera"].updateProjectionMatrix();
refList["renderer"].setSize(window.innerWidth, window.innerHeight);
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>window.innerWidth-200) { // right
cmw.style.left=(window.innerWidth-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>window.innerHeight-100) { // bottom
cmw.style.top=(window.innerHeight-100)+"px";
cmw.style.bottom="auto";
} else if(bounds.top<23) { // top
cmw.style.top="23px";
cmw.style.bottom="auto";
}
}
var initShadowMaps=false;
var initClockShadowMaps=false;
var firstFrameRendered=false;
function updateShadowCameras(sunLight,clockShadowLight) {
if(!initialized) return;
//const terrainZ = refList["terrainGroup"].position.z; // track terrain movement
//const camZ = refList["camera"].position.z;
//const centerZ = camZ - terrainZ; // world z-center of visible area
if (sunLight.shadow.map && clockShadowLight.shadow.map && firstFrameRendered) {
if(!initShadowMaps) {
initShadowMaps=true;
sunLight.shadow.map.texture.wrapS = THREE.ClampToEdgeWrapping;
sunLight.shadow.map.texture.wrapT = THREE.ClampToEdgeWrapping;
sunLight.shadow.map.texture.borderColor = new THREE.Vector4(1, 1, 1, 1); // 1.0 = lit (white), hides excess shadows outside map
//refList["renderer"].shadowMap.texture.minFilter = THREE.LinearFilter; // Explicitly set
//refList["renderer"].shadowMap.texture.magFilter = THREE.LinearFilter;
console.log("Shadow map init");
}
if(!initClockShadowMaps) {
initClockShadowMaps=true;
clockShadowLight.shadow.map.texture.wrapS = THREE.ClampToEdgeWrapping;
clockShadowLight.shadow.map.texture.wrapT = THREE.ClampToEdgeWrapping;
clockShadowLight.shadow.map.texture.borderColor = new THREE.Vector4(1, 1, 1, 1); // 1.0 = lit (white), hides excess shadows outside map
console.log("Clock Shadow map init");
}
// Sun
//const sunPos = sunLight.position.clone();
//const sunDir = new THREE.Vector3();
//sunLight.getWorldDirection(sunDir);
//clockShadowLight.getWorldDirection(sunDir);
if(sunLight) {
const sunDir = sunLight.position.clone().normalize();
// Position: behind sun along its dir
//sunLight.shadow.camera.position.copy(sunPos).addScaledVector(sunDir, -shadowCamFar / 2);
//sunLight.shadow.camera.position.set(0,0,0).addScaledVector(sunDir, shadowCamFar / 2);
sunLight.shadow.camera.position.set(0,0,0).addScaledVector(sunDir, sunOrbit);
//sunLight.shadow.camera.position.set(sunDir*sunOrbit);
// Lock rotation to scene axes (no tilt/moire)
//sunLight.shadow.camera.rotation.set(0, 0, 0);
//sunLight.shadow.camera.up.set(0, 1, 0);
//sunLight.shadow.camera.updateProjectionMatrix();
sunLight.shadow.camera.updateMatrixWorld();
sunLight.shadow.matrix.copy(shadowBiasMatrix)
.multiply(sunLight.shadow.camera.projectionMatrix)
.multiply(sunLight.shadow.camera.matrixWorldInverse.clone());
//sunLight.shadow.matrix.copy(sunLight.shadow.camera.projectionMatrix).multiply(sunLight.shadow.camera.matrixWorldInverse.clone());
refList["terrainMaterial"].uniforms.sunShadowMatrix.value.copy(sunLight.shadow.matrix);
refList["terrainMaterial"].uniforms.sunShadowMap.value = sunLight.shadow.map.texture;
refList["mountainMaterial"].uniforms.sunShadowMatrix.value.copy(sunLight.shadow.matrix);
refList["mountainMaterial"].uniforms.sunShadowMap.value = sunLight.shadow.map.texture;
if(clockShadowLight.target!=null && initShadowMaps && refList["terrainMaterial"]!=null) {
// Use normalized dir from main light (correct for orbit)
// Position behind the clock, along light dir (200 is good — tune 100–400)
//clockShadowLight.position.copy(refList["camera"].position).add(sunDir.multiplyScalar(60));
//clockShadowLight.position.set(sunDir.multiplyScalar(60));
clockShadowLight.position.set(0,0,0).addScaledVector(sunDir, nearShadowOrbit);
//console.log(sunLight.position);
//console.dir(clockShadowLight);
//clockShadowLight.target.position.copy(refList["camera"].position);
clockShadowLight.target.updateMatrixWorld();
clockShadowLight.updateMatrixWorld();
//clockShadowLight.shadow.updateMatrices(clockShadowLight);
clockShadowLight.shadow.camera.updateMatrixWorld();
clockShadowLight.shadow.matrix.copy(shadowBiasMatrix)
.multiply(clockShadowLight.shadow.camera.projectionMatrix)
.multiply(clockShadowLight.shadow.camera.matrixWorldInverse.clone());
clockShadowLight.shadow.map.needsUpdate = true;
refList["terrainMaterial"].uniforms.clockShadowMatrix.value.copy(clockShadowLight.shadow.matrix);
refList["terrainMaterial"].uniforms.clockShadowMap.value = clockShadowLight.shadow.map.texture;
}
sunLight.shadow.map.needsUpdate = true;
}
// if(typeof sunShadowPlane!=="undefined") {
// // Update sun cover plane
// sunShadowPlane.position.copy(sunTarget);
// sunShadowPlane.rotation.set(-Math.PI / 2, 0, 0); // lay flat on XZ
// sunShadowPlane.updateMatrixWorld();
// }
//
} else
if(!firstFrameRendered) {
firstFrameRendered=true;
}
}
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;
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]['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;
if(testValOn) {
// debugtime
if(testValAuto) {
testval++;
}
if(testval>2000) testval-=2000;
tsp=testval/1000; // debugtime
ss=testval;
} else {
// realtime
tsp=ss/43200; // realtime comment out for debugtime sunpos
}
ss_phi=((tsp)*180)+90;
if(ss_phi>360) ss_phi-=360;
//ss_theta=(-45);
var scal=0;
if(ss_phi<180) {
// make 86400 into 360 be 0 to 180 and 180 to 360 into 0 to 2 then 0 to 2 into -1 to 0 to 1, then math abs -1 0 1 into 1 0 1 then make 1 0 1 into 0 1 0 with 1- and 0 -1 0 with -1-
scal=Math.abs((ss_phi/90)-1); // 0 1 0
if(ss_phi<90) scal=-scal; //negative edge case
} else {
scal=1-Math.abs(1-((ss_phi-180)/90)-1); // 0 -1 0
}
//ss_theta=(scal*45);
//let smoothscal=SmoothStep(-1.0,1.0,scal);
let smoothscal=(easeInOut((scal+1.0)*.5)*2.0)-1.0; // curve the midnight/midday corner point up/down while arriving at center
ss_theta=(smoothscal*45);
let ssTun = ss - (25*lapse/100);
if(ssTun<0) ssTun+=lapse;
//console.log(Math.abs((ss/(lapse*.5))-1.0));
const daytime=Math.abs((ssTun/(lapse*.5))-1.0); // 1 0 1
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);
const cameraWorldPosition = new THREE.Vector3();
refList["camera"].getWorldPosition(cameraWorldPosition);
refList["sunLight"].target.updateMatrixWorld();
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();
// 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(refList["camera"].matrixWorldInverse);
refList["customDepthMat"].uniforms.nMatrix.value = normalMatrix;
refList["customDepthMat"].uniforms.vMatrix.value.copy(refList["camera"].matrixWorldInverse);
mvpMatrix.multiplyMatrices(refList["camera"].projectionMatrix, refList["camera"].matrixWorldInverse).multiply(refList["sky"].matrixWorld);
refList["skyMaterial"].uniforms.mvpMatrix.value = mvpMatrix;
refList["customDepthMat"].uniforms.mvpMatrix.value = mvpMatrix;
// sky color calculations
var l=0; // segment selection
var t=ss; // range current
//const hr_1=(4.2*lapse/100); // percentage
const hr_1=(lapse/24); // percentage
const hr_3=hr_1*3;
const hr_4=hr_3+hr_1;
const hr_5=hr_4+hr_1;
const hr_9=hr_5+hr_4;
const hr_12=hr_9+hr_3;
const hr_13=hr_12+hr_1;
const hr_16=hr_13+hr_3;
const hr_21=hr_16+hr_5;
//console.log("ss="+ss);
//console.log("hr_1="+hr_1+" ss>hr_1?:"+((ss>hr_1)?"true":"false"));
//console.log("hr_="+hr_3+" ss>hr_3?:"+((ss>hr_3)?"true":"false"));
//console.log("hr_="+hr_4+" ss>hr_4?:"+((ss>hr_4)?"true":"false"));
//console.log("hr_="+hr_5+" ss>hr_5?:"+((ss>hr_5)?"true":"false"));
//console.log("hr_="+hr_9+" ss>hr_9?:"+((ss>hr_9)?"true":"false"));
//console.log("hr_="+hr_12+" ss>hr_12?:"+((ss>hr_12)?"true":"false"));
//console.log("hr_="+hr_13+" ss>hr_13?:"+((ss>hr_13)?"true":"false"));
//console.log("hr_="+hr_16+" ss>hr_16?:"+((ss>hr_16)?"true":"false"));
//console.log("hr_="+hr_21+" ss>hr_21?:"+((ss>hr_21)?"true":"false"));
//ss-=(10*lapse/100);
//if(ss<0) ss+=lapse; // rollover
var m=hr_1; // range max
if(ss>hr_21) { // night end 3
l=7;
t=ss-hr_21;
m=hr_3;
} else if(ss>hr_16) { // midnight 5
l=6;
t=ss-hr_16;
m=hr_5;
} else if(ss>hr_13) { // night begin 3
l=5;
t=ss-hr_13;
m=hr_3;
} else if(ss>hr_12) { // evening 1
l=4;
t=ss-hr_12;
m=hr_1;
} else if(ss>hr_9) { // day end 3
l=3;
t=ss-hr_9;
m=hr_3;
} else if(ss>hr_4) { // midday 5
l=2;
t=ss-hr_4;
m=hr_5;
} else if(ss>hr_1) { // day begin 3
l=1;
t=ss-hr_1;
m=hr_3;
}
var l2=l+1;
//if(l2>3) l2=0;
if(l2>7) l2=0;
//console.log("l="+l+" l2="+l2+" t="+t+" m="+m);
tmpColor['r1']=skyColors['topColor'][l][0];
tmpColor['g1']=skyColors['topColor'][l][1];
tmpColor['b1']=skyColors['topColor'][l][2];
tmpColor['r2']=skyColors['topColor'][l2][0];
tmpColor['g2']=skyColors['topColor'][l2][1];
tmpColor['b2']=skyColors['topColor'][l2][2];
tmpColor['r3']=skyColors['bottomColor'][l][0];
tmpColor['g3']=skyColors['bottomColor'][l][1];
tmpColor['b3']=skyColors['bottomColor'][l][2];
tmpColor['r4']=skyColors['bottomColor'][l2][0];
tmpColor['g4']=skyColors['bottomColor'][l2][1];
tmpColor['b4']=skyColors['bottomColor'][l2][2];
let p = Math.min(t / m, 1); // progress
//const eP=p; // linear
const eP = easeInOut(p); // easedProgress
//const currentValue = lerp(startValue, endValue, eP);
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
const milliseconds=ss+(dd.getMilliseconds()/1000.0);
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;
}
}
}
refList["camera"].updateMatrixWorld();
//sky.position.copy(camera.position).setY(0);
//sky.position.set(0.0, 0.0, camera.position.z);
//refList["sky"].position.copy(refList["camera"].position);
refList["skyGroup"].position.copy(refList["playerGroup"].position);
refList["skyGroup"].updateMatrixWorld();
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;
}
handleMovement(delta,milliseconds,lapse);
//if(markOn && markWait<=0) {
markActive();
//} else if(markOn) {
// markWait-=dt;
//}
//controls.update();
//refList["shadowCamera"].copy(refList["camera"]);
refList["shadowCamera"].position.copy(refList["camera"].position);
refList["renderer"].shadowMap.autoUpdate = true; // Temporarily enable for correct timing
refList["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)
//refList["renderer"].compile(scene, refList["camera"]);
// Hack: Render with a tiny viewport to minimize cost (shadow map is internal)
const oldViewport = refList["renderer"].getViewport(new THREE.Vector4());
refList["renderer"].setViewport(0, 0, 1, 1); // tiny 1x1 pixel
refList["renderer"].render(scene, refList["shadowCamera"]); // dummy render to trigger shadow map
refList["renderer"].setViewport(oldViewport); // restore
updateShadowCameras(refList["sunLight"],refList["clockShadowLight"]);
// If updateShadowCameras is called before the shadow pass has happened for the current frame, clockShadowLight.shadow.camera.matrixWorldInverse might not be updated to the new player position yet.
//
// --
//
// It feels a bit like "black magic," but that 1x1 dummy render is essentially the only way to force the Three.js WebGLShadowMap internal logic to execute and finalize those matrices before you steal them for your custom shaders.
// By the time you reach updateShadowCameras now, the renderer has already:
//
// Updated the light's world matrices.
// Updated the shadow camera's matrixWorldInverse.
// Projected the scene onto the shadow depth map.
//
// Because you're using a cloned camera (shadowCamera) for that dummy pass, you're triggering the logic without messing up your main camera's state. -googleai
// Restore
//scene.fog = oldFog;
//scene.environment = oldEnv;
refList["renderer"].shadowMap.autoUpdate = false;
if (helperLoaded) {
// newer version threejs require update on helpers
clockHelper.update();
helper.update();
}
if (initialized && refList["renderer"].domElement.width > 0) {
//refList["renderer"].state.buffers.color.setMask(true);
//refList["renderer"].state.buffers.depth.setMask(true);
//refList["renderer"].state.buffers.stencil.setMask(0xff); // Ensure stencil is clearable
//const gl = refList["renderer"].getContext();
//const bitmask = gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT;
//if (bitmask !== 0) {
refList["renderer"].clear(true, true, true);
//}
// render by custom layer order or render all
refList["renderer"].render(scene, refList["camera"]);
}
}
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>
-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 />
<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