<!DOCTYPE html>
<!--
Author: Twily 2025-2026
Landscape/skybox webgl demo
Keybinds:
w wireframe toggle on/off {sky,terrain,mountains}
s shadows toggle on,debugdither,debugclean,off
h shadow camera helpers toggle on/off
c clouds toggle 2d2(default),2d1,ray[heavy],ray[medium],off
d depthwrite toggle skybox on/off
t time toggle realtime[run],testval[run],testval[pause]
v vortex toggle (dead) on/off
n nothern lights toggle alwayson true/false
r northern light re-position/orient
u hide sundial toggle visible/hidden
b hide benches toggle visible/hidden
m hide twily toggle visible/hidden
p cycle camera position {0,1,2,3,4,5}
k show keybinds toggle visible/hidden
space terrain toggle moving on/off
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Three.js Infinite Landscape</title>
<style>
html,body {
width: 100%; height: 100%;
margin: 0; padding: 0;
overflow: hidden;
user-select: none;
background: #344C6C; color: #ccc;
}
canvas {
display: block;
position: fixed; top: 0; left: 0;
width: 100%; height: 100%;
z-index: 100;
}
#notices {
position: absolute;
top: 10px; left: 10px;
color: yellow;
font-family: monospace;
font-size: 12px;
pointer-events: none;
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: left;
display: inline-block;
}
.tbl { display: table; width: 100%; height: 100%; table-layout: fixed; }
.tr { display: table-row; }
.td { display: table-cell; vertical-align: middle;}
#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: 8px;
border: 2px solid #ccc;
border-radius: 10px;
display: inline-block;
}
</style>
</head>
<body>
<div id="loading"><div class="tbl"><div class="tr"><div class="td"><span id="loadtxt">Loading...</span></div></div></div></div>
<div id="notices"></div>
<div id="info"></div>
<div id="keysWrap">
<div class="pad18">
<div id="keys"></div>
</div>
</div>
<script type="importmap">
{
"imports": {
"three": "./build/three.module.js",
"three/addons/": "./jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
//import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'; // replaces orbitcontrols
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const 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;
// for ray clouds
uniform float cloudDensity;
uniform float cloudAbsorption;
uniform float cloudScale;
uniform float cloudSpeed;
uniform float cloudBottom;
uniform float cloudThickness;
uniform float scatteringAniso;
uniform int maxSteps;
uniform float marchSize;
uniform int lightSteps;
uniform float frame;
uniform int cloudsOn;
uniform int starsOn;
// 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))
);
}
// indent removed for section raymarched clouds
// standard GLSL 3D Perlin for clouds
float mod289(float x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 perm(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }
float noise3D(vec3 p) {
vec3 a = floor(p);
vec3 d = p - a;
d = d * d * (3.0 - 2.0 * d);
vec4 b = a.xxyy + vec4(0.0, 1.0, 0.0, 1.0);
vec4 k1 = perm(b.xyxy);
vec4 k2 = perm(k1.xyxy + b.zzww);
vec4 c = k2 + a.zzzz;
vec4 k3 = perm(c);
vec4 k4 = perm(c + 1.0);
vec4 o1 = fract(k3 * (1.0 / 41.0));
vec4 o2 = fract(k4 * (1.0 / 41.0));
vec4 o3 = o2 * d.z + o1 * (1.0 - d.z);
vec2 o4 = o3.yw * d.x + o3.xz * (1.0 - d.x);
return o4.y * d.y + o4.x * (1.0 - d.y);
}
float fbm(vec3 p) {
float f = 0.0;
float amp = 0.5;
float freq = 1.0;
mat3 rot = mat3(0.36, 0.93, 0.0, -0.93, 0.36, 0.0, 0.0, 0.0, 1.0); // Rotation to break artifacts
for (int i = 0; i < 5; i++) {
f += amp * noise3D(p * freq);
p *= rot * 2.01;
amp *= 0.45; // Slightly less persistence for fluff
freq *= 2.0;
}
return f;
}
// Scattering and absorption
float hg(float g, float mu) {
float gg = g * g;
return (1.0 - gg) / (4.0 * PI * pow(1.0 + gg - 2.0 * g * mu, 1.5));
}
float beers(float dist, float abs) {
return exp(-dist * abs);
}
// Cloud march (returns scattered light and transmittance for blending)
struct CloudResult {
vec3 scattered;
float transmittance;
};
// Updated raymarchClouds
CloudResult raymarchClouds(vec3 ro, vec3 rd) {
vec3 scattered = vec3(0.0);
float transmittance = 1.0;
float depth = 0.0;
vec2 frag = gl_FragCoord.xy + frame;
float dither = (hash(frag.xy) - 0.5) * marchSize * 0.5;
if (rd.y <= 0.0) return CloudResult(vec3(0.0), 1.0); // Skip below horizon
float cloudTop = cloudBottom + cloudThickness;
float tBottom = (cloudBottom - ro.y) / rd.y;
float tTop = (cloudTop - ro.y) / rd.y;
float tStart = max(0.0, min(tBottom, tTop));
float tEnd = min(max(tBottom, tTop), 10000.0); // Cap max dist for perf
if (tStart >= tEnd) return CloudResult(vec3(0.0), 1.0);
depth = tStart + dither;
int i = 0;
while (i < maxSteps && depth < tEnd && transmittance > 0.01) {
vec3 p = ro + depth * rd;
vec3 noisePos = p * cloudScale + vec3(xtime * cloudSpeed * 0.3, 0.0, xtime * cloudSpeed);
float heightFrac = (p.y - cloudBottom) / cloudThickness;
float baseDensity = smoothstep(0.0, 0.1, heightFrac) * smoothstep(1.0, 0.6, heightFrac) * cloudDensity; // Softer fade, puffier top
float noise = fbm(noisePos);
float detail = fbm(noisePos * 4.0); // High-freq for erosion
float density = max(0.0, baseDensity * (noise * 1.2 - 0.2) - detail * 0.3); // Gaps + fluff
if (density > 0.001) {
// Light march (short)
float lightDepth = 0.0;
float lightTrans = 1.0;
vec3 lightDir = sunDirection;
int j = 0;
while (j < lightSteps) {
vec3 lp = p + lightDepth * lightDir;
float lheightFrac = (lp.y - cloudBottom) / cloudThickness;
float lbase = smoothstep(0.0, 0.1, lheightFrac) * smoothstep(1.0, 0.6, lheightFrac);
float lnoise = fbm(lp * cloudScale + vec3(xtime * cloudSpeed * 0.3, 0.0, xtime * cloudSpeed));
float ldetail = fbm(lp * cloudScale * 4.0 + vec3(xtime * cloudSpeed * 0.3, 0.0, xtime * cloudSpeed));
float ldens = max(0.0, lbase * (lnoise * 1.2 - 0.2) - ldetail * 0.3);
lightTrans *= beers(marchSize, ldens * cloudAbsorption);
lightDepth += marchSize;
if (lightTrans < 0.01) break;
j++;
}
float su = dot(rd, sunDirection);
float mu = dot(rd, -sunDirection);
float phasem = hg(scatteringAniso, su) + hg(-0.2, mu) * 0.1; // Multi-lobe for soft scatter
float phases = hg(scatteringAniso, mu) + hg(-0.2, mu) * 0.3; // Multi-lobe for soft scatter
float ds = marchSize; // Step size for integral
float powder = 1.0 - exp(-2.0 * density * ds * cloudAbsorption); // Silver lining
float luminance1 = density * phases * lightTrans * powder;
float luminance2 = density * phasem * lightTrans * powder;
vec3 cloudTint = mix(bottomColor, topColor, heightFrac * 0.8 + 0.2); // Subtle time-of-day blend
scattered += ds * luminance1 * sunColor * cloudTint * transmittance; // Add ds, no arbitrary boost
scattered += ds * luminance2 * moonColor * cloudTint * transmittance; // Add ds, no arbitrary boost
transmittance *= beers(ds, density * cloudAbsorption);
}
depth += marchSize;
i++;
}
return CloudResult(scattered, transmittance);
} // end ray clouds
//--float getDaytimeFactor(float daytime) {
//-- float t;
//--
//-- if (daytime <= 0.5) {
//-- // Stay very low most of the time → only rise quickly near 0.5
//-- float u = daytime / 0.5;
//-- t = pow(u, 5.0); // try 4.0 → 7.0
//-- } else {
//-- // Fast initial brightening after noon → then slow creep to full sun
//-- float u = (daytime - 0.5) / 0.5;
//-- t = 1.0 - pow(1.0 - u, 0.25); // 0.2–0.4 feels nice
//-- }
//--
//-- return t;
//--}
float getDaytimeFactor(float daytime) {
float t;
float mid = 0.5; // Adjust if "0.5" isn't exactly sunrise/noon; e.g., 0.3 for earlier sunrise
float y_mid = 0.75; // Value at mid-point (low = linger moon; high = quick to sun)
float ny_mid = 0.25; // Value at mid-point (low = linger moon; high = quick to sun)
float gamma = 3.0; // Higher = slower majority (linger moon), sharper rise near mid
float delta = 3.0; // Lower = faster jump after mid, slower tail to full sun
if (daytime <= mid) {
// Very slow majority → sharp rise near mid (exclude most night, quick fade-in)
float u = daytime / mid;
t = y_mid * pow(u, gamma);
} else {
// Fast jump after mid → slow majority to full (quick start, exclude most day by stabilizing high)
float u = (daytime - mid) / (1.0 - mid);
t = y_mid + (1.0 - y_mid) * pow(u, delta);
}
return t;
}
void main() {
vec3 viewDir = normalize(vWorldPosition - cameraPosition);
float height = normalize(vWorldPosition).y - 0.15; // -.15 pulls up the gradient
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==1 && vortexOn==0) {
// Clouds: Assume ro ≈ vec3(0), but pass cameraPosition if offset
CloudResult clouds = raymarchClouds(cameraPosition, viewDir);
// Blend: Scattered + sky through clouds (stars dim behind)
finalColor = clouds.scattered + clouds.transmittance * skyColor;
finalColor += (1.0 - clouds.transmittance) * 0.1 * gradient; // Subtle night glow
if(starsOn==1) {
finalColor += (1.0 - clouds.transmittance) * 0.1 * totalColor; // Subtle night glow
}
} else if(cloudsOn==2 || 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
//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;
float swirlAmount = swirlRadius * vortexSwirl;
// 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;
// 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
}
//finalColor=mix(finalColor,finalClouds,cloudAlpha);
gl_FragColor=vec4(finalColor,1.0);
//gl_FragColor = vec4(vec3(clouds.transmittance), 1.0);
//gl_FragColor = vec4(clouds.scattered, 1.0);
// ------------- old below
//gl_FragColor = vec4(mix(gradient+totalColor, sunColor2, (sunAura3 * .1) + (sunAura2 * .1) + (sunAura * .1) + (sunRim * 1.0) + (sunGlow * 1.0)), 1.0);
// final with sun
//gl_FragColor = vec4(mix(gradient, sunColor2, ((sunAura3 * .01) + (sunAura2 * .02) + (sunAura * .1) + (sunRim * 0.5) + (sunGlow * 0.5)) * daytime * 5.0) * 1.00, 1.0); // * 5.0 harder edges between glow and rim but overexposure color
//vec3 color=mix(mix(gradient,moonColor2,0.5),sunColor2,0.5);
// final with sun and moon
//gl_FragColor = vec4(mix(mix(gradient, moonColor2, ((moonAura * .002) + (moonAura2 * .02) + (moonRim * 1.0) + (moonGlow * 1.0)) * max(2.0-daytime,0.5) * 2.0), sunColor2, ((sunAura3 * .01) + (sunAura2 * .02) + (sunAura * .1) + (sunRim * 1.0) + (sunGlow * 1.0)) * daytime * 5.0) * 1.02, 1.0); // * 5.0 harder edges between glow and rim but overexposure color
//gl_FragColor = vec4(mix(gradient, moonColor2, moonGlow), 1.0); // moon test
//gl_FragColor = vec4(daytime,daytime,daytime,1.0);
}
`;
//. custom shadow for 2d slouds simple2 [default]
// replicate procedural clouds here for custom packing
// does not work currently~
const fragmentShaderShadow = `
#include <packing>
uniform sampler2D uAlphaMap;
uniform float uAlphaThreshold;
varying vec3 vWorldPosition;
uniform float xtime;
varying vec3 vPosition;
varying vec3 vLocalPosition;
varying vec3 vViewPosition;
varying vec2 TexCoord;
varying vec3 v_Normal;
uniform vec3 sunDirection;
//uniform vec3 moonDirection;
uniform float 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;
// 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;
//--vec3 flow1 = pos * scale + vec3(time * 0.10);
//--vec3 flow2 = pos * scale - vec3(time * 0.06);
//--vec3 flow3 = pos * scale + vec3(time * 0.03);
//--float n1 = noise(flow1);
//--float n2 = noise(flow2);
//--float n3 = noise(flow3);
//--float baseNoise = (n2 + n3 - n1) * cDensity; // Base detail
//--float detailNoise = fbm(pos * scale * 4.2 + vec3(time * 0.01)); // Smudged variation
//--float cloudDensity = smoothstep(0.3, .7, baseNoise + detailNoise * 0.3); // Increased coverage
//--cloudColor = vec3(.5, .5, .5);
//--alpha = cloudDensity;
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);
//gl_FragColor = vec4(vec3(alpha,0.0,0.0), alpha); // debug
// This line is essential for depth packing
gl_FragColor = packDepthToRGBA(gl_FragCoord.z);
} else {
gl_FragColor = packDepthToRGBA(gl_FragCoord.z);
}
}
`;
const vertexShaderClouds = `
uniform mat4 mvpMatrix;
varying vec3 ourNormal;
varying vec2 vUv;
varying float ypos;
void main()
{
ourNormal=normal;
vUv = uv;
gl_Position = mvpMatrix * vec4(position, 1.0);
ypos=position.x*0.001f; // -1000 to 1000 = -1 to 1
}
`;
const fragmentShaderClouds = `
#define PI 3.14159265359
uniform float xtime; // seconds~day
//uniform vec3 data; // data.x time, data.y freq, data.z amp
uniform float daytime; // -1-0-1
uniform vec3 sunDirection;
//uniform vec3 moonDirection;
varying vec3 ourNormal;
varying vec2 vUv; // UV coordinates from the vertex shader
// Pseudo-random hash function based on 2D coordinates
float hash(vec2 p) {
return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}
// 2D value noise with smooth interpolation
float noise(vec2 p) {
vec2 i = floor(p); // Integer part
vec2 f = fract(p); // Fractional part
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f); // Cubic interpolation
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}
// Fractal Brownian motion for detailed cloud patterns
float fbm(vec2 p) {
float value = 0.0;
//float amplitude = 0.5;
float frequency = 0.222;
float amplitude = 0.222;
//float frequency = data.y;
for (int i = 0; i < 4; i++) { // 4 octaves for detail
value += amplitude * noise(p * frequency);
frequency *= 2.0; // Double frequency each octave
amplitude *= 0.5; // Halve amplitude each octave
}
return value;
}
// clouds by grok - butthole above
void main() {
//float time = xtime;
float rad=PI/180.0;
//float time=(xtime * 1.0 / 86400) * 25000; // 50 breaks
float phase = xtime;
vec2 uv = vUv;
vec3 finalColor=vec3(0.0,0.0,0.0);
float alpha=0.0;
if(vUv.y>=0.5) { // avoid calc for bottom half
float twinkleSpeed = 86400.0 / 3.0;
//-- float twinkle = sin(uTime * twinkleSpeed);
//float time = sin(phase * twinkleSpeed);
float time = phase / (2.0 * 3.14159) * twinkleSpeed;
///vec2 flame_uv = uv + vec2(0.0, offset_y);
float speed = 0.1; // Speed of cloud movement
float cloudScale = 100.0; // Scale of cloud patterns
// Circular offset for looping animation
//vec2 offset = vec2(sin(time * speed), cos(time * speed)) * 0.5;
vec2 offset = vec2(sin(time), cos(time)) * 0.5;
// Scale UV coordinates and apply offset
vec2 scaledUV = uv * cloudScale;
scaledUV.x*=2.0; //
float cloudValue = fbm(scaledUV + offset);
// Map noise to alpha for cloud density with soft edges
alpha = smoothstep(0.0, 0.7, cloudValue);
//float alpha=0.5;
// Purplish cloud color (stylized)
//vec3 cloudColor = vec3(0.6, 0.6, 0.66) * (1.0 - daytime);
// color blendings
float methodA=1.0-daytime;
float methodD=(methodA*2.0);
float methodE=(methodA*2.0)-1.0;
// day/night cloud colors blend
vec3 color1 = vec3(0.5,0.3,0.5); // day end
vec3 color2 = vec3(0.6,0.6,0.7);
vec3 color3 = vec3(0.7,0.6,0.7); // <<day begin
//
vec3 color4 = vec3(0.63,0.43,0.43); // night end
vec3 color5 = vec3(0.43,0.36,0.43);
vec3 color6 = vec3(0.15,0.16,0.43); // night begin
vec3 ourColor = vec3(mix(color3,color2,methodD));
ourColor = mix(ourColor,color1,methodE);
vec3 ourColor2 = vec3(mix(color5,color4,methodD));
ourColor2 = mix(ourColor2,color6,methodE);
vec3 cloudColor = vec3(mix(ourColor,ourColor2,daytime));
vec3 norm=normalize(ourNormal);
vec3 sunColor = vec3(1.0f, 0.93f, 0.74f); // sky sphere
vec3 moonColor = vec3(0.7f, 1.0f, 0.8f); // lumination
float sunStrength=10.0f;
float moonStrength=3.0f;
float diff=max(dot(norm, sunDirection), 0.0);
vec3 diffuse=diff*sunColor*sunStrength;
float diff2=max(dot(norm, -sunDirection), 0.0);
vec3 diffuse2=diff2*moonColor*moonStrength;
finalColor = (cloudColor + ((diffuse + diffuse2) * 0.1)) * cloudColor;
}
// Output color with transparency
//gl_FragColor = vec4(cloudColor, alpha);
gl_FragColor = vec4(finalColor, alpha);
//gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`;
// clouds2 moved to skymaterial not using clouds geometry
const vertexShaderClouds2 = `
uniform mat4 mvpMatrix;
varying vec3 vPosition;
varying vec3 vLocalPosition;
uniform mat3 nMatrix; // CPU-computed normal matrix
uniform mat4 vMatrix; // View matrix (for view-space lighting)
varying vec3 vViewPosition; // View-space position
varying vec2 TexCoord;
varying vec3 v_Normal;
varying vec3 ourNormal;
varying vec2 vUv;
//varying float ypos;
void main() {
//ourNormal=vNormal;
ourNormal=normal;
vUv = uv;
vLocalPosition = position;
vPosition = (modelMatrix * vec4(position, 1.0)).xyz;
vViewPosition = (modelViewMatrix * vec4(position, 1.0)).xyz; // View-space position
TexCoord = vec2(vPosition.x, vPosition.z);
vec3 worldNormal = normalize(nMatrix * normal);
v_Normal = normalize((vMatrix * vec4(worldNormal, 0.0)).xyz);
//ypos=position.y*0.0005f; // -1000 to 1000 = -1 to 1
gl_Position = mvpMatrix * vec4(position, 1.0);
}
`;
const fragmentShaderClouds2 = `
uniform float xtime;
//uniform mat4 invView; // Inverse view matrix
//uniform mat4 invProjection; // Inverse view matrix
varying vec3 vPosition;
varying vec3 vLocalPosition;
varying vec3 vViewPosition;
varying vec2 TexCoord;
varying vec3 v_Normal;
//varying vec3 v_vertToLight;
//varying vec4 viewSunPos;
uniform vec3 sunDirection;
//uniform vec3 moonDirection;
uniform float daytime;
uniform vec3 lightDir;
uniform vec3 lightDir2;
varying vec3 ourNormal;
varying vec2 vUv;
uniform float cDensity;
//varying float ypos;
const float PI = 3.1415926535897932384626433832795;
// Noise functions
float random(vec3 p) {
return fract(sin(dot(p, vec3(12.9898, 78.233, 45.5432))) * 43758.5453123);
}
float noise(vec3 p) {
vec3 i = floor(p);
vec3 f = fract(p);
vec3 u = f * f * (3.0 - 2.0 * f);
return mix(mix(mix(random(i), random(i + vec3(1.0, 0.0, 0.0)), u.x),
mix(random(i + vec3(0.0, 1.0, 0.0)), random(i + vec3(1.0, 1.0, 0.0)), u.x), u.y),
mix(mix(random(i + vec3(0.0, 0.0, 1.0)), random(i + vec3(1.0, 0.0, 1.0)), u.x),
mix(random(i + vec3(0.0, 1.0, 1.0)), random(i + vec3(1.0, 1.0, 1.0)), u.x), u.y), u.z);
}
float fbm(vec3 p) {
float v = 0.0;
float a = 0.5;
vec3 shift = vec3(100.0);
for (int i = 0; i < 2; ++i) { // default 6
v += a * noise(p);
p = p * 2.0 + shift;
a *= 0.2;
}
return v;
}
//float linearizeDepth(float depth) {
// float near = 0.1;
// float far = 500.0; // Adjust to match your camera
// float z = depth * 2.0 - 1.0; // NDC
// return 2.0 * near * far / (far + near - z * (far - near));
//}
//
//vec3 getWorldPosition(float depth, vec2 uv) {
// float z = linearizeDepth(depth);
// vec4 clipSpace = vec4(uv * 2.0 - 1.0, z, 1.0);
// vec4 viewSpace = invProjection * clipSpace;
// viewSpace /= viewSpace.w;
// vec4 worldSpace = invView * viewSpace;
// return worldSpace.xyz;
//}
void main() {
// Noise for cloud pattern
vec3 pos = normalize(vLocalPosition);
vec3 cloudColor = vec3(1.0,1.0,1.0);
vec3 finalColor = vec3(0.0,0.0,0.0);
float alpha=0.0;
if(vUv.y>=0.5) { // avoid calc for bottom half
//float time = abs((xtime / 43200.0)-1.0);
//float time = abs((xtime / 10.0)-4320.0);
float phasetime = xtime / 5.0; // match in shadow shader if using
float phaselen = (86400.0 / 5.0) * .5; // realtime
//float phaselen = (2000.0 / 5.0) * .5; // debug
float time = abs(phasetime-phaselen);
//float loopedRealtime = mod(time, 365.0 * 24.0 * 3600.0); // 365 days
//float globalAngle = (loopedRealtime / (365.0 * 24.0 * 3600.0)) * 2.0 * PI;
//float localAngle = globalAngle; // Optional latitude factor if desired
//-- float cosR = cos(localAngle);
//-- float sinR = sin(localAngle);
//-- vec3 pos2 = vec3(
//-- cosR * pos.x + sinR * pos.z,
//-- pos.y,
//-- -sinR * pos.x + cosR * pos.z
//-- );
float scale = 5.0;
vec3 flow1 = pos * scale + vec3(time * 0.10);
vec3 flow2 = pos * scale - vec3(time * 0.06);
vec3 flow3 = pos * scale + vec3(time * 0.03);
float n1 = noise(flow1);
float n2 = noise(flow2);
float n3 = noise(flow3);
float baseNoise = (n2 + n3 - n1) * cDensity; // Base detail
float detailNoise = fbm(pos * scale * 4.2 + vec3(time * 0.01)); // Smudged variation
float cloudDensity = smoothstep(0.3, .7, baseNoise + detailNoise * 0.2); // Increased coverage
cloudColor = vec3(.5, .5, .5);
alpha = cloudDensity;
// taken from https://twily.info/plainC/terrain/data/shaders/frag_shader_clouds.glsl#view
vec3 norm=normalize(ourNormal);
vec3 sunColor = vec3(1.0, 0.93, 0.74);
vec3 moonColor = vec3(0.7, 0.5, 0.9);
float sunStrength=1.0;
float moonStrength=0.2;
float diff=max(dot(norm, sunDirection), 0.0);
vec3 diffuse=diff*sunColor*sunStrength;
float diff2=max(dot(norm, -sunDirection), 0.0);
vec3 diffuse2=diff2*moonColor*moonStrength;
vec3 lightColor = mix(diffuse,diffuse2,1.0-daytime);
// Edge tint based on alpha and nightFactor (approximated from light)
float edgeFactor = smoothstep(0.2, 0.8, 1.0 - (alpha * 1.5)); // Higher alpha = less edge
vec3 colorNight=vec3(0.2,0.3,0.5);
vec3 colorDay=vec3(0.6,0.3,0.1);
vec3 colorTwilight=vec3(0.8,0.1,0.2);
float mixn=min((daytime*2.0),1.0); // 0 - 0.5
float mixt=max((daytime*2.0)-1.0,0.0); // 0.5 - 1
float cloudBrightness=max((max(daytime,0.25)*2.0),0.0); // *half+half = ambient
vec3 edgeTint = mix(
colorNight, // 0
mix(
colorTwilight, // 0.5
colorDay, // 1
mixt),
mixn);
edgeTint = edgeTint * mix(diff,diff2,1.0-daytime);
cloudColor = cloudColor * ((mix(diff,diff2,1.0-daytime)*.2)+.8);
cloudColor = mix(cloudColor, edgeTint, edgeFactor) * cloudBrightness * 2.0; // Apply tint
//cloudColor = colorNight;
//vec3 P = getWorldPosition(vPosition.z, TexCoord);
//finalColor = cloudColor.rgb * ((lightColor * .8)+.8);
//vec3 black=vec3(0.0);
//finalColor = mix(black,cloudColor.rgb * ((lightColor * .2)+.8),alpha);
finalColor = mix(finalColor, cloudColor.rgb * ((lightColor * .2)+.8), alpha);
}
//vec3 testColor = vec3(0,.5,0);
// Apply lighting to color
//testColor *= lightIntensity;
//uniform float alphaThreshold;
float alphaThreshold = 0.3;
if (alpha < alphaThreshold) discard;
gl_FragColor = vec4(finalColor, alpha);
//gl_FragColor = vec4(vec3(cloudColor), alpha);
//gl_FragColor = vec4(testColor, 1);
//gl_FragColor = vec4(P, alpha);
}
`;
const heightScale = 120;
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;
uniform float shadowNormalBias;
uniform mat4 sunShadowMatrix; // from uniform
uniform int shadowOn;
const float PI = 3.1415926535897932384626433832795;
${THREE.ShaderChunk['common']}
${THREE.ShaderChunk['fog_pars_vertex']}
void main() {
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 worldPos = modelMatrix * vec4(position, 1.0);
vWorldPos = worldPos.xyz;
vWorldPosition = (modelMatrix * vec4(position, 1.0)).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)
vec4 offsetWorldPos = modelMatrix * vec4(position + offset, 1.0);
if(shadowOn >= 1) {
vSunShadowCoord = sunShadowMatrix * offsetWorldPos;
// Repeat for moon with another offsetWorldPos if separate bias, but same for now
}
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
${THREE.ShaderChunk['fog_vertex']}
}
`;
// 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 grass1Tex;
uniform sampler2D grass2Tex;
uniform sampler2D grass3Tex;
uniform sampler2D dirt1Tex;
uniform sampler2D rock1Tex;
uniform sampler2D rock2Tex;
uniform sampler2D rock3Tex;
uniform sampler2D rock4Tex;
uniform sampler2D normalGrass;
uniform sampler2D normalDirt;
uniform sampler2D normalRock;
uniform sampler2D roughGrass;
uniform sampler2D roughDirt;
uniform sampler2D roughRock;
uniform sampler2D splatTex;
uniform float repeatScale;
uniform float splatScale;
uniform vec3 lightDir;
uniform vec3 lightDir2;
uniform float daytime; // 1 0 1
// Manual shadow uniforms
uniform float shadowBias;
uniform float shadowRadius;
uniform sampler2D sunShadowMap;
uniform mat4 sunShadowMatrix;
varying vec4 vSunShadowCoord;
uniform int shadowOn;
uniform float rimShineStrength;
uniform vec3 rimShineColor;
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vWorldPos;
varying vec3 vWorldPosition;
varying vec2 vSplatUv;
//--varying float vFogDepth;
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 dirtWeight = max(0.0, 1.0 - sumSplat);
float totalWeight = sumSplat + dirtWeight;
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 = dirtWeight / 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(grass1Tex, uv).rgb * w1 +
texture2D(grass2Tex, uv).rgb * w2 +
texture2D(grass3Tex, uv).rgb * w3 +
texture2D(dirt1Tex, uv).rgb * w4;
vec3 rockAlbedo = texture2D(rock1Tex, uv).rgb * w1 +
texture2D(rock2Tex, uv).rgb * w2 +
texture2D(rock3Tex, uv).rgb * w3 +
texture2D(rock4Tex, uv).rgb * w4;
vec3 albedo = mix(groundAlbedo, rockAlbedo, steepFactor);
//vec3 albedo = mix(vec3(1.0,0.0,0.0), vec3(0.0,1.0,0.0), steepFactor);
// Normals (shared for grass1-3, mixed with dirt using w4, shared for rocks1-4)
vec3 normGrass = texture2D(normalGrass, uv).rgb;
vec3 normDirt = texture2D(normalDirt, uv).rgb;
vec3 groundNorm = mix(normGrass, normDirt, w4);
vec3 normRock = texture2D(normalRock, uv).rgb;
vec3 norm = mix(groundNorm, normRock, steepFactor);
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
// Roughness (shared for grass1-3, mixed with dirt using w4, shared for rocks1-4)
float roughGrassVal = texture2D(roughGrass, uv).r;
float roughDirtVal = texture2D(roughDirt, uv).r;
float groundRough = mix(roughGrassVal, roughDirtVal, w4);
float roughRockVal = texture2D(roughRock, uv).r;
float rough = mix(groundRough, roughRockVal, steepFactor);
float ao = 1.0;
float metal = 0.0;
// Simple PBR lighting (diffuse + specular approximation)
vec3 viewDir = normalize(cameraPosition - vWorldPosition);
vec3 finalNormal = normalize(vNormal + norm * 0.5); // Reduced normal strength if too bumpy
float zeron = smoothstep(0.0, 0.2, abs((daytime * 2.0) - 1.0)); // 1 0 1
float mixn = (1.0 - ((daytime * 0.5) + 0.5)) * zeron; // 0 - 0.5 // night half
float mixt = ((daytime * 0.5) + 0.5) * zeron; // 0.5 - 1 // day half
vec3 lightDir2Mod = vec3(-lightDir2.x, -lightDir2.y, -lightDir2.z);
// Diffuse (sun + moon)
float diff = max(dot(lightDir, finalNormal), 0.0) * ao * mixt;
float diffMoon = max(dot(lightDir2Mod, finalNormal), 0.0) * ao * mixn;
// 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) * 1.8
+ vec3(spec + specMoon)
+ rimShineColor * rim;
// ────────────────────────────────────────────────
float shadow = 1.0; // no shadow
if (shadowOn >= 1) {
// Manual shadow sampling for sun
shadow = abs((daytime * 2.0) - 1.0); // 1 0 1 0 = 0.5 0 0.5 1 ~;
vec4 shadowCoord = vSunShadowCoord / vSunShadowCoord.w;
shadowCoord = shadowCoord * 0.5 + 0.5; // NDC to [0,1]
if (shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 &&
shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0 &&
shadowCoord.z >= 0.0 && shadowCoord.z <= 1.0) {
float shadowDepth = 0.0;
float bias = 0.0;
shadowDepth = unpackRGBAToDepth(texture(sunShadowMap, shadowCoord.xy));
float ndotl = max(dot(finalNormal, lightDir), 0.01);
float slopeFactor = sqrt(1.0 - ndotl * ndotl) / ndotl;
bias = shadowBias + 0.0001 * slopeFactor;
bias = clamp(bias, shadowBias, 0.001);
shadow = shadowCoord.z > shadowDepth + bias ? 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 shadow
}
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);
if (shadowOn >= 2) {
gl_FragColor = vec4(vec3(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
const vertexShaderMountain = `
varying vec2 vUv;
varying vec3 vWorldNormal; // world-space normal (transformed)
varying vec3 vWorldPos; // world-space position
//--varying float vFogDepth;
// Manual shadow coord
varying vec4 vSunShadowCoord;
uniform float shadowNormalBias;
uniform mat4 sunShadowMatrix; // from uniform
uniform int shadowOn;
${THREE.ShaderChunk['common']}
${THREE.ShaderChunk['fog_pars_vertex']}
void main() {
vUv = uv * 2.0; // hardcoded texture scale
// 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)
vec4 offsetWorldPos = modelMatrix * vec4(position + offset, 1.0);
if(shadowOn >= 1) {
vSunShadowCoord = sunShadowMatrix * offsetWorldPos;
// Repeat for moon with another offsetWorldPos if separate bias, but same for now
}
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
${THREE.ShaderChunk['fog_vertex']}
}
`;
// 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
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) * 5.0
+ vec3(spec + specMoon)
+ rimShineColor * rimShine;
// ────────────────────────────────────────────────
float shadow = 1.0; // no shadow
if(shadowOn >= 1) {
// Manual shadow sampling for sun
shadow = abs((daytime * 2.0) - 1.0); // 1 0 1 0 = 0.5 0 0.5 1 ~;
vec4 shadowCoord = vSunShadowCoord / vSunShadowCoord.w;
shadowCoord = shadowCoord * 0.5 + 0.5; // NDC to [0,1]
if (shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 &&
shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0 &&
shadowCoord.z >= 0.0 && shadowCoord.z <= 1.0) {
float shadowDepth = 0.0;
float bias = 0.0;
shadowDepth = unpackRGBAToDepth(texture(sunShadowMap, shadowCoord.xy));
float ndotl = max(dot(finalNormal, lightDir), 0.01);
float slopeFactor = sqrt(1.0 - ndotl * ndotl) / ndotl; // tan(acos(ndotl)); // webgl fast
bias = shadowBias + 0.0001 * slopeFactor; // tune 0.0001 as slope bias; small to avoid leaks
bias = clamp(bias, shadowBias, 0.001); // cap to prevent excessive
shadow = shadowCoord.z > shadowDepth + bias ? 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) + .6; // .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 vertexShaderSunclock = `
varying vec2 vUv;
varying vec3 vWorldNormal; // world-space normal (transformed)
varying vec3 vWorldPos; // world-space position
varying vec3 vObjectNormal; // world-space position
//varying float vFogDepth;
// Manual shadow coord
varying vec4 vSunShadowCoord;
varying vec4 vClockShadowCoord;
uniform float shadowNormalBias;
uniform float clockShadowNormalBias;
uniform mat4 sunShadowMatrix; // from uniform
uniform mat4 clockShadowMatrix;
uniform int shadowOn;
${THREE.ShaderChunk['common']}
${THREE.ShaderChunk['fog_pars_vertex']}
void main() {
vUv = uv;
vObjectNormal = normal;
// World-space normal (correct for random rotation)
mat3 normalMat3 = mat3(transpose(inverse(modelMatrix))); // proper normal transform
vWorldNormal = normalize(normalMat3 * normal);
vec4 worldPos = modelMatrix * vec4(position, 1.0);
vWorldPos = worldPos.xyz;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
vFogDepth = -mvPosition.z;
// Offset for sun
vec3 offset = normal * shadowNormalBias; // object-space offset (assumes uniform scale; if not, use worldNormal below)
vec3 clockOffset = normal * clockShadowNormalBias; // object-space offset (assumes uniform scale; if not, use worldNormal below)
// For non-uniform scale: vec3 offset = vWorldNormal * shadowNormalBias;
vec4 offsetWorldPos = modelMatrix * vec4(position + offset, 1.0);
vec4 clockOffsetWorldPos = modelMatrix * vec4(position + clockOffset, 1.0);
if(shadowOn >= 1) {
vSunShadowCoord = sunShadowMatrix * offsetWorldPos;
// Repeat for moon with another offsetWorldPos if separate bias, but same for now
}
vClockShadowCoord = clockShadowMatrix * clockOffsetWorldPos; // sundial always on
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
${THREE.ShaderChunk['fog_vertex']}
}
`;
const fragmentShaderSunclock = `
precision highp float;
${THREE.ShaderChunk['common']}
${THREE.ShaderChunk['packing']}
${THREE.ShaderChunk['fog_pars_fragment']}
uniform sampler2D map;
uniform sampler2D normalMap;
uniform sampler2D roughnessMap;
uniform vec3 lightDir;
uniform vec3 lightDir2;
uniform float daytime; // 1 0 1
// Manual shadow uniforms
uniform float shadowBias;
uniform float clockShadowBias;
uniform float shadowRadius;
uniform float clockShadowRadius;
varying vec4 vSunShadowCoord;
varying vec4 vClockShadowCoord;
uniform sampler2D sunShadowMap;
uniform sampler2D clockShadowMap;
uniform int shadowOn;
uniform float alphaThreshold;
uniform float rimShineStrength;
uniform vec3 rimShineColor;
varying vec2 vUv;
varying vec3 vWorldNormal;
varying vec3 vObjectNormal; // old vNormal but vNormal from twily include skinned mesh is view-space
varying vec3 vWorldPos;
//varying float vFogDepth;
uniform float ambientMulti;
uniform float normalStrength;
void main() {
vec4 albedo = texture(map, vUv);
float rough = texture2D(roughnessMap, vUv).r;
// Object-space normal map perturbation
vec3 normalTex = texture(normalMap, vUv).rgb * 2.0 - 1.0;
// 2. Flip the channels you need
//normalTex.x *= -1.0; // Flip horizontal (Red)
normalTex.y *= -1.0; // Flip vertical (Green) - MOST COMMON FIX
// google ai solved my final dual shadow issues https://share.google/aimode/OfmzmnqFXiKydjrBE
// Add bump to world-space normal
vec3 finalNormal = normalize(vWorldNormal + normalTex * normalStrength); // strength 0.8, tune as needed
vec3 viewDir = normalize(cameraPosition - vWorldPos);
float ao=1.0;
float metal=0.0;
float zeron=smoothstep(0.0,0.2,abs((daytime*2.0)-1.0)); // 1 0 1
float mixn=(1.0-((daytime*0.5)+0.5))*zeron; // 0 - 0.5 // night half
float mixt=((daytime*0.5)+0.5)*zeron; // 0.5 - 1 // day half
vec3 lightDir2Mod=vec3(-lightDir2.x,-lightDir2.y,-lightDir2.z);
// Diffuse (sun + moon)
float diff = max(dot(lightDir, finalNormal), 0.0) * ao * mixt;
float diffMoon = max(dot(lightDir2Mod, finalNormal), 0.0) * ao * mixn;
// 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(vObjectNormal), 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) * 5.0
+ vec3(spec + specMoon)
+ rimShineColor * rimShine;
// ────────────────────────────────────────────────
float shadow = 1.0; // no shadow
//if(shadowOn>=1) { // moved down - sundial always on
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;
clockShadowCoord = clockShadowCoord * 0.5 + 0.5; // NDC to [0,1]
float clockShadow = 1.0;
if (clockShadowCoord.x >= 0.0 && clockShadowCoord.x <= 1.0 &&
clockShadowCoord.y >= 0.0 && clockShadowCoord.y <= 1.0 &&
clockShadowCoord.z >= 0.0 && clockShadowCoord.z <= 1.0) {
float clockShadowDepth=0.0;
float clockBias=0.0;
clockShadowDepth = unpackRGBAToDepth(texture(clockShadowMap, clockShadowCoord.xy));
float biasMultiplier = 0.0001;
clockBias = clockShadowBias + biasMultiplier * slopeFactor;
//clockBias = clamp(clockBias, 0.0001, 0.001); // cap to prevent extreme swaps/full cover
// Reduce the multiplier and clamp the factor to avoid extreme jumps
//clockBias = clockShadowBias + clamp(0.0001 * slopeFactor, 0.0, 0.005);
clockShadow = clockShadowCoord.z > clockShadowDepth + clockBias ? 0.0 : 1.0;
if(shadowOn<=2) { // dither shadows
vec2 clockTexelSize = 1.0 / vec2(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) {
vec4 shadowCoord = vSunShadowCoord / vSunShadowCoord.w;
shadowCoord = shadowCoord * 0.5 + 0.5; // NDC to [0,1]
float sunShadow = 1.0;
if (shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 &&
shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0 &&
shadowCoord.z >= 0.0 && shadowCoord.z <= 1.0) {
// Circle check: distance from center (0.5,0.5) <= radius 0.5 (fits [0,1])
//vec2 centerDist = shadowCoord.xy - vec2(0.5, 0.5); // avoid circular in orthographic?
//if (length(centerDist) <= 0.5 &&
// shadowCoord.z >= 0.0 &&
// shadowCoord.z <= 1.0) {
float shadowDepth=0.0;
float bias=0.0;
shadowDepth = unpackRGBAToDepth(texture(sunShadowMap, shadowCoord.xy));
//bias = shadowBias / max(0.01, dot(finalNormal, lightDir)); // dynamic bias
//bias = shadowBias * max(0.05, dot(finalNormal, lightDir)); // dynamic bias
//float ndotl=max(dot(finalNormal, lightDir), 0.01);
//float slopeFactor = sqrt(1.0 - ndotl * ndotl) / ndotl; // tan(acos(ndotl))
bias = shadowBias + 0.0001 * slopeFactor; // tune 0.0001 as slope bias; small to avoid leaks
bias = clamp(bias, shadowBias, 0.001); // cap to prevent excessive
//float bias = shadowBias;
sunShadow = shadowCoord.z > shadowDepth + bias ? 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(clockShadowCoord.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;
} // 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)+.4*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;
// SUNDIAL
// shadow strength .4 yo .6 = night to day
float shadowStrength=(daytime*.2)+.4; // .4 to .6
float ambianceStrength=((1.0-daytime)*.2)+.4*ambientMulti; // .6 to .4
color *= (shadow * shadowStrength) + ambianceStrength; // shadow factor + ambient day
}
if (albedo.a < alphaThreshold) discard; // Clip transparent pixels (no blending, but depth sorting works)
// Fog application (using your vFogDepth)
float fogFactor = 1.0 - exp(-fogDensity * fogDensity * vFogDepth * vFogDepth);
color *= max(fogColor,.2) * 2.0;
color = mix(color, fogColor, fogFactor);
//#include <fog_fragment>
if(shadowOn>=2) {
gl_FragColor = vec4(vec3(shadow , 0.0, 1.0-shadow), albedo.a);
} else {
gl_FragColor = vec4(color, albedo.a);
}
//gl_FragColor = vec4(rough, rough, rough, 1.0);
//gl_FragColor = vec4(vNormal, 1.0);
//gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // red debug
}
`;
const vertexShaderTwily = `
#include <common>
#include <uv_pars_vertex> // Added: declares attribute vec2 uv;
#include <displacementmap_pars_vertex>
#include <envmap_pars_vertex>
#include <color_pars_vertex>
#include <fog_pars_vertex>
#include <lights_pars_begin>
#include <normal_pars_vertex> // Declares varying vec3 vNormal (view-space)
#include <morphtarget_pars_vertex>
#include <skinning_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>
varying vec2 vUv;
varying vec3 vWorldNormal; // world-space normal (transformed)
varying vec3 vWorldPos; // world-space position
// varying vec3 vNormal; // Removed: redefined by <normal_pars_vertex>; it's now view-space if you need it
varying vec3 vObjectNormal;
// Manual shadow coord
varying vec4 vSunShadowCoord;
varying vec4 vClockShadowCoord;
uniform float shadowNormalBias;
uniform float clockShadowNormalBias;
uniform mat4 sunShadowMatrix;
uniform mat4 clockShadowMatrix;
uniform int shadowOn;
void main() {
#include <color_vertex>
#include <morphcolor_vertex>
#include <beginnormal_vertex>
#include <morphnormal_vertex>
#include <skinbase_vertex> // Computes skinMatrix from bone transforms
#include <skinnormal_vertex> // Applies skinMatrix to objectNormal
#include <defaultnormal_vertex> // Added: transforms objectNormal to view-space (transformedNormal = normalMatrix * objectNormal)
#include <normal_vertex> // Added: sets vNormal = normalize(transformedNormal) with flip if needed
#include <begin_vertex> // Sets transformed = position
#include <morphtarget_vertex>
#include <skinning_vertex> // Applies skinMatrix to transformed → now deformed!
#include <displacementmap_vertex> // Added: if you're using displacement (since you have the pars include)
#include <project_vertex> // Sets mvPosition and gl_Position
#include <logdepthbuf_vertex>
#include <clipping_planes_vertex>
vUv = uv;
vObjectNormal = normal;
// vNormal is now set to view-space normal via includes (if you need object-space, add a custom varying vec3 vObjectNormal = objectNormal;)
// World-space normal (using skinned objectNormal; correct for random rotation)
mat3 normalMat3 = mat3(transpose(inverse(modelMatrix))); // proper normal transform
vWorldNormal = normalize(normalMat3 * objectNormal);
vec4 worldPos = modelMatrix * vec4(transformed, 1.0); // Use transformed (skinned position)
vWorldPos = worldPos.xyz;
// Offset for sun (use skinned objectNormal and transformed)
vec3 offset = objectNormal * shadowNormalBias; // object-space offset (assumes uniform scale; if not, normalize first)
vec3 clockOffset = objectNormal * clockShadowNormalBias;
vec4 offsetWorldPos = modelMatrix * vec4(transformed + offset, 1.0);
vec4 clockOffsetWorldPos = modelMatrix * vec4(transformed + clockOffset, 1.0);
if(shadowOn >= 1) {
vSunShadowCoord = sunShadowMatrix * offsetWorldPos;
}
vClockShadowCoord = clockShadowMatrix * clockOffsetWorldPos;
vFogDepth = -mvPosition.z;
}
`;
const vertexShaderFirework = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShaderFirework = `
uniform sampler2D diffuseMapFirework;
uniform vec2 u_offset;
uniform vec2 u_repeat;
uniform float u_opacity;
varying vec2 vUv;
void main() {
vec2 uv = vUv * u_repeat + u_offset;
vec4 diffuseColor = texture2D(diffuseMapFirework, uv);
vec3 color = diffuseColor.rgb * 2.0;
float alpha = diffuseColor.a * u_opacity;
if (alpha < 0.3) discard;
gl_FragColor = vec4(color, alpha);
}
`;
const fragmentShaderFireworkDEBUG = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 1.0, 0.8); // solid bright magenta
}
`;
const vertexShaderNorthernLight = `
varying vec2 vUv;
uniform float time;
// Pseudo-random hash function based on 2D coordinates
float hash(vec2 p) {
return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}
// 2D value noise with smooth interpolation
float noise(vec2 p) {
vec2 i = floor(p); // Integer part
vec2 f = fract(p); // Fractional part
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f); // Cubic interpolation
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}
// Fractal Brownian motion for detailed cloud patterns
float fbm(vec2 p) {
float value = 0.0;
//float amplitude = 0.5;
float frequency = 0.222;
float amplitude = 0.222;
//float frequency = data.y;
for (int i = 0; i < 4; i++) { // 4 octaves for detail
value += amplitude * noise(p * frequency);
frequency *= 2.0; // Double frequency each octave
amplitude *= 0.5; // Halve amplitude each octave
}
return value;
}
void main() {
vUv = uv;
vec3 pos = position;
// Slow back/forth movement using noise (fixed to center/view)
vec2 noisePos = pos.xz * 0.005 + time * 0.8; // lower frequency + slow speed
float noiseVal = fbm(noisePos) * 2.0 - 1.0; // -1 to 1 range
pos.y += noiseVal * 550.0; // Amplitude, tune down for smoother
pos.x += noiseVal * 250.0; // Sideways wiggle, smaller for less sharp
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`;
const fragmentShaderNorthernLight = `
uniform int beamDensity;
uniform float beamStretch;
uniform vec3 glowColor;
varying vec2 vUv;
uniform float time;
uniform float fadeOpa; // later feature to fade out in daytime
uniform int debug;
//float noise(vec2 p) {
// return sin(p.x * 0.01) * cos(p.y * 0.01) * 5.0 + 0.5; // Simple placeholder noise, replace with your Perlin
//}
// Pseudo-random hash function based on 2D coordinates
float hash(vec2 p) {
return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}
// 2D value noise with smooth interpolation
float noise(vec2 p) {
vec2 i = floor(p); // Integer part
vec2 f = fract(p); // Fractional part
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f); // Cubic interpolation
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}
void main() {
vec3 color = vec3(0.0);
// Edge fade on all sides (no hard cutoff)
float edgeFade = smoothstep(0.0, 0.2, vUv.x) * smoothstep(1.0, 0.8, vUv.x) *
smoothstep(0.0, 0.2, vUv.y) * smoothstep(1.0, 0.8, vUv.y);
// Procedural vertical beams (pillars)
for (int i = 0; i < beamDensity; i++) {
float xPos = float(i) / float(beamDensity - 1); // Horizontal spacing
//xPos += (noise(vec2(float(i), 0.0)) - 0.5) * 0.03; // small random jitter per pillar
vec2 beamCenter = vec2(xPos, 0.5); // Center vertically
vec2 dist = vUv - beamCenter;
// Stretch tall (along y), narrow horizontally for pillar shape
dist.y *= beamStretch; // Higher = taller pillars (tune 4.0–10.0)
dist.x *= .3; // Narrower = thinner pillars (tune 0.2–0.6)
float beamGlow = smoothstep(0.12, 0.0, length(dist)); // Oval base -- tune first val as "density" in fade? 0.12 ~ 0.05
beamGlow *= 0.5 + 0.5 * sin(time * 1.5 + float(i) * 6.28); // Gentle pulse per pillar
beamGlow *= noise(vUv * 5.0 + time * 0.3); // Slow green variance
color += glowColor * beamGlow * 3.0; // Boost intensity
}
// Apply edge fade
color *= fadeOpa;
color *= edgeFade;
if(debug==0) {
gl_FragColor = vec4(color, edgeFade); // Alpha for transparency
//gl_FragColor = vec4(color, edgeFade * 0.8); // Alpha for soft blending
} else {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // red test
}
}
`;
const infoDiv = document.getElementById('info');
var notices=[];
var nid=-1;
function pushNotice(txt="") {
let ts=new Date().getTime();
let dur=5*1000; // 5 sec
nid++;
let obj={
idx: nid,
ts: ts,
dur: dur,
opa: 100.0,
};
// create html here
let html="<span id=\"notice_"+nid+"\">"+txt+"</span>";
//$('notices').insertAdjacentHTML('beforeend',html);
$('notices').insertAdjacentHTML('afterbegin',html);
notices.push(obj);
}
function updateNotices(ts=0, dt=0) {
if(notices.length==0) return;
for(let i=0;i<notices.length;i++) {
if((notices[i]['ts']+notices[i]['dur'])<ts) {
if(notices[i]['opa']<=0) {
// delete
$('notice_'+notices[i]['idx']).parentNode.removeChild($('notice_'+notices[i]['idx']));
notices.splice(i,1);
i--;
} else {
// fade out
notices[i]['opa']-=100.0 * dt;
$('notice_'+notices[i]['idx']).style.opacity=notices[i]['opa']/100;
}
}
}
}
// Classic Perlin Noise implementation (3D for terrain variation)
class ClassicalNoise {
constructor(r = Math) {
this.grad3 = [[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],
[1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],
[0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]];
this.p = [];
for (let i = 0; i < 256; i++) {
this.p[i] = Math.floor(r.random() * 256);
}
this.perm = [];
for (let i = 0; i < 512; i++) {
this.perm[i] = this.p[i & 255];
}
}
dot(g, x, y, z) {
return g[0] * x + g[1] * y + g[2] * z;
}
mix(a, b, t) {
return (1.0 - t) * a + t * b;
}
fade(t) {
return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}
noise(x, y, z) {
let X = Math.floor(x), Y = Math.floor(y), Z = Math.floor(z);
x -= X; y -= Y; z -= Z;
X &= 255; Y &= 255; Z &= 255;
let gi000 = this.perm[X + this.perm[Y + this.perm[Z]]] % 12;
let gi001 = this.perm[X + this.perm[Y + this.perm[Z + 1]]] % 12;
let gi010 = this.perm[X + this.perm[Y + 1 + this.perm[Z]]] % 12;
let gi011 = this.perm[X + this.perm[Y + 1 + this.perm[Z + 1]]] % 12;
let gi100 = this.perm[X + 1 + this.perm[Y + this.perm[Z]]] % 12;
let gi101 = this.perm[X + 1 + this.perm[Y + this.perm[Z + 1]]] % 12;
let gi110 = this.perm[X + 1 + this.perm[Y + 1 + this.perm[Z]]] % 12;
let gi111 = this.perm[X + 1 + this.perm[Y + 1 + this.perm[Z + 1]]] % 12;
let n000 = this.dot(this.grad3[gi000], x, y, z);
let n100 = this.dot(this.grad3[gi100], x - 1, y, z);
let n010 = this.dot(this.grad3[gi010], x, y - 1, z);
let n110 = this.dot(this.grad3[gi110], x - 1, y - 1, z);
let n001 = this.dot(this.grad3[gi001], x, y, z - 1);
let n101 = this.dot(this.grad3[gi101], x - 1, y, z - 1);
let n011 = this.dot(this.grad3[gi011], x, y - 1, z - 1);
let n111 = this.dot(this.grad3[gi111], x - 1, y - 1, z - 1);
let u = this.fade(x), v = this.fade(y), w = this.fade(z);
let nx00 = this.mix(n000, n100, u);
let nx01 = this.mix(n001, n101, u);
let nx10 = this.mix(n010, n110, u);
let nx11 = this.mix(n011, n111, u);
let nxy0 = this.mix(nx00, nx10, v);
let nxy1 = this.mix(nx01, nx11, v);
return this.mix(nxy0, nxy1, w);
}
}
// linear(k): Returns k unchanged (constant speed)
function linear(k) {
return k;
}
// easeIn(k): Starts slow and speeds up (e.g., using a power function).
function easeIn(k) {
return Math.pow(k, 2); // Quadratic ease-in
}
// easeOut(k): Starts fast and slows down (a common natural effect, similar to the continuous lerp approach).
function easeOut(k) {
return 1 - Math.pow(1 - k, 2); // Quadratic ease-out
}
// easeInOut(k): Starts slow, speeds up in the middle, and slows down at the end.
function easeInOut(k) {
return k < 0.5 ? 2 * Math.pow(k, 2) : 1 - Math.pow(1 - k, 2) * 2; // Quadratic ease-in-out
// A common alternative using Math.sin for a smoother curve:
// return 0.5 * (Math.sin((k - 0.5) * Math.PI) + 1);
}
// standard lerp t=0-1
function lerp(start,end,t) {
return start * (1 - t) + end * t;
}
// To implement a smoothstep version of your custom lerp in C# for 2026, you can use the standard cubic Hermite interpolation formula. This function creates an "S-curve" that starts and ends slowly, making transitions feel more natural
function SmoothStep(start,end,t) {
// Clamp t between 0 and 1 to prevent overshoot
if(t>1.0) t=1.0;
else if(t<0.0) t=0.0;
// The smoothstep cubic formula: 3t^2 - 2t^3
t = t * t * (3.0 - 2.0 * t);
// Standard interpolation using the smoothed 't'
//return start * (1f - t) + end * t;
return lerp(start,end,t);
}
const radian=Math.PI / 180.0;
const $=function(id) { return document.getElementById(id); }
const rndMinMax=function(min,max) { return Math.floor(Math.random()*(max-min+1)+min); }
const perlin = new ClassicalNoise();
// Scene setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
//camera.position.set(-46.15, 1908.31, -2053.56); // far view debug
//camera.lookAt(0, 0, -100); // Look forward along -z
//camera.rotation.set(-6.0*radian, 180.0*radian, 0.0); // (if not using OrbitControls)
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
//renderer.sortObjects = false;
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
// lookat / start set here
const controls = new OrbitControls(camera, renderer.domElement);
//const controls = new PointerLockControls( camera, renderer.domElement ); // alternative to orbiter -- see bottom controls.lock~ cursor lock
var cameraSet=-1;
function resetCamera(c=null,o=null) {
if(c==null || o==null) return;
cameraSet++;
if(cameraSet>5) cameraSet=0; // cycle
switch(cameraSet) {
case 0:
// Flying height behind sunclock looking forward
camera.position.set(0.0, 56.50, -10.83);
o.target.set(0.0, c.position.y, 0.0);
break;
case 1:
// Flying height sunclock ontop center
c.position.set(0.0, 59.98, -0.77);
o.target.set(0.0, c.position.y+.25, 0.0);
break;
case 2:
// Flying height above sunclock looking down
c.position.set(0.0, 60.98, 0.06);
o.target.set(0.0, c.position.y-5.0, 0.07);
break;
case 3:
// Flying height sunclock beinh center
//c.position.set(.0, 57.22, -3.8);
//o.target.set(0.0, c.position.y+.25, 0.0);
// Flying height sunclock beinh twily
c.position.set(1.5, 54, 0.0);
o.target.set(1.5, c.position.y+.25, 2.0);
break;
case 4:
// Flying height sunclock ontop side right
camera.position.set(-4.55, 55.0, -2.5);
o.target.set(c.position.x, c.position.y, c.position.z+.5);
break;
case 5:
// Flying height far out from sunclock looking at
c.position.set(-35.0, 67.98, 25.11);
o.target.set(0.0, c.position.y+10.0, -0.0);
break;
default:
}
}
resetCamera(camera,controls); // init set
camera.updateProjectionMatrix();
// One-time setup
const shadowCamera = camera.clone();
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
function getDirection(theta, phi) { // dir = return
let theta_left = theta + 90.0;
let phi_up = phi - 90.0;
if(theta_left>180.0) theta_left -=360.0;
if(phi_up<-180.0) phi_up += 360.0;
//const x=Math.sin(90.0 * radian) * Math.cos(theta_left * radian);
//const y=Math.cos(90.0 * radian);
//const z=Math.sin(90.0 * radian) * Math.sin(theta_left * radian);
const x=Math.sin(phi_up * radian) * Math.cos(theta * radian);
const y=Math.cos(phi_up * radian);
const z=Math.sin(phi_up * radian) * Math.sin(theta * radian);
//const x=Math.sin(phi * radian) * Math.cos(theta * radian);
//const y=Math.cos(phi);
//const z=Math.sin(phi * radian) * Math.sin(theta * radian);
//Vec3 XZonly={ // for player full directional speed not Y
// cos(theta * radian),
// 0,
// sin(theta * radian)
//};
return new THREE.Vector3(x,y,z);
}
var sunTime=getDirection(170,90); // theta orient around horizontally, phi vertical/altitude
var moonTime=getDirection(-170,-90); // theta orient around horizontally, phi vertical/altitude
// sunrise/sunset = phi at 180, above ground at 170, below ground at 190
// front facing with camera at 90, back facing at -90 or 270
// still need the light position itself
// Lights (PBR-friendly)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8); // Increased ambient for better brightness
scene.add(ambientLight);
const sunLight = new THREE.DirectionalLight(0xffffff, 1.5); // Increased intensity
//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();
// One-time setup (after main sunLight)
const clockShadowLight = new THREE.DirectionalLight(0xffffff, 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*8000,sunTime.y*8000,sunTime.z*8000); // 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)
const shadowBias = 0.0001; // slight negative for acne
const shadowNormalBias = 0.1; // for bumpy terrain/mountains
const shadowRadius = 4; // softens edges (if using BasicShadowMap, ignore for PCF)
const shadowMapWidth = 4096;
const shadowMapHeight = 4096;
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = shadowMapWidth;
sunLight.shadow.mapSize.height = shadowMapHeight;
sunLight.shadow.bias = shadowBias;
sunLight.shadow.normalBias = shadowNormalBias;
sunLight.shadow.radius = shadowRadius;
// Base frustum (will update dynamically in animate)
const shadowCamWidth = 12000; // covers ~1.5 tiles wide (tune if too small/large)
const shadowCamHeight = 12000; // covers height variation
const shadowCamFar = 6000; // light-to-ground distance + margin
const shadowCamNear = -6000; // close to light, avoids near-clip issues
sunLight.shadow.camera = new THREE.OrthographicCamera(
-shadowCamWidth / 2,
shadowCamWidth / 2,
shadowCamHeight / 2,
-shadowCamHeight / 2,
shadowCamNear,
shadowCamFar
);
// 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);*/
const clockShadowBias = 0.00005; // not negative ?
const clockShadowNormalBias = 0.1; // for bumpy terrain/mountains
const clockShadowRadius = 4; // softens edges (if using BasicShadowMap, ignore for PCF)
const clockShadowMapWidth = 1024;
const clockShadowMapHeight = 1024;
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;
const clockShadowCamWidth = 20; // covers ~1.5 tiles wide (tune if too small/large)
const clockShadowCamHeight = 20; // covers height variation
const clockShadowCamFar = 20; // light-to-ground distance + margin
const clockShadowCamNear = .01; // close to light, avoids near-clip issues
clockShadowLight.shadow.camera = new THREE.OrthographicCamera(
-clockShadowCamWidth / 2,
clockShadowCamWidth / 2,
clockShadowCamHeight / 2,
-clockShadowCamHeight / 2,
clockShadowCamNear,
clockShadowCamFar
);
clockShadowLight.shadow.camera.updateProjectionMatrix();
scene.add(sunLight);
scene.add(clockShadowLight); // or keep hidden if not needed visually
let sunclockVisible=true;
let benchesVisible=true;
let twilyVisible=true;
let helperLoaded=false;
let clockHelper=null;
let helper=null;
function loadHelpers() {
helperLoaded=true;
// debug shadow camera2
clockHelper = new THREE.CameraHelper(clockShadowLight.shadow.camera);
scene.add(clockHelper);
// debug shadow camera
helper=new THREE.CameraHelper(sunLight.shadow.camera);
scene.add(helper);
}
function unloadHelpers() {
helperLoaded=false;
scene.remove(clockHelper);
scene.remove(helper);
}
var fogDensity=0.0005; // 0.0008
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
const skyColors={
topColor: [
[100,94,160], // morning 1
[100,106,190], // day begin 3
[101,115,185], // midday 5
[100,105,170], // day end 3
[55,35,140], // evening 1
[14,7,60], // night begin 3
[1,0,35], // midnight 5
[8,10,56], // night end 3
],
bottomColor: [
[165,145,175],// morning
[164,178,160],// day begin
[165,174,204],// midday
[178,170,180],// day end
[215,87,137], // evening
[130,30,45], // night begin
[20,10,30], // midnight
[25,40,75], // night end
],
};
// calculate the 86400 seconds to 4 segments time,
// but not equally sized? day/night = 16hr~, morning/evening=8hr~
// 86400/4 = 21600
// 16/2 = 8 * 60 * 60 = 28800 / 2 = 14400 (4hr)
//
// morning= 1 to 14400 (+14400)
// day= 14401 to 43200 (+28800)
// evening= 43201 to 57600 (+14400)
// night= 57601 to 86400 (+28800)
//
// cont in animate
var cDensity=0.5;
var skyDepthOn=true;
var vortexOn=false;
// 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 },
cloudDensity: { value: 0.5 }, // Lower base fluff
cloudAbsorption: { value: 0.01 }, // Much lower = less darkening, trans >0
cloudScale: { value: 0.0015 }, // Finer detail
cloudSpeed: { value: 0.0003 }, // Slower drift
cloudBottom: { value: 200.0 }, // Closer for testing visibility
cloudThickness: { value: 50.0 }, // Thinner layer = less accum up
scatteringAniso: { value: 0.7 }, // Stronger rims
maxSteps: { value: 6 }, // Higher for horizon detail
lightSteps: { value: 2 }, // Short shadows
marchSize: { value: 40.0 }, // Smaller = finer, but balance perf
frame: { value: 0.0 },
cloudsOn: { value: 2 }, // match with cloudquality startup
starsOn: { value: 1 },
lightDir: { value: sunLight.position.normalize() },
lightDir2: { value: sunLight.position.normalize().negate() },
nMatrix: { value: new THREE.Matrix3() }, // Initialize empty mat3
vMatrix: { value: new THREE.Matrix4() }, // View matrix
mvpMatrix: { value: new THREE.Matrix4() },
cloudsReady: { value: (loading==-1)?1:0 }, // wait render clouds for loading screen
cDensity: { value: cDensity }, // multiplier 0.0-1.0
// Example colors — warm sunrise/sunset orange → soft yellow, or cool moon blue
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
},
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);
const sky = new THREE.Mesh(skyGeometry, skyMaterial);
sky.layers.set(1);
//scene.add(sky);
skyGroup.add(sky);
sky.rotation.set(0.0,0.0,90.0*radian);
const normalMatrix = new THREE.Matrix3(); // create once and reuse
const mvpMatrix = new THREE.Matrix4();
const customDepthMat = new THREE.ShaderMaterial({ // used with sky
uniforms: {
//uAlphaMap: { value: myAlphaTexture },
uAlphaThreshold: { value: 0.3 },
sunDirection: { value: sunTime.normalize() }, // Sun pos (normalized) from sphere coords
//moonDirection: { value: moonTime.normalize() }, // Sun pos (normalized) from sphere coords
xtime: { value: 0.0 },
daytime: { value: 0.0 },
lightDir: { value: sunLight.position.normalize() },
lightDir2: { value: sunLight.position.normalize().negate() },
nMatrix: { value: new THREE.Matrix3() }, // Initialize empty mat3
vMatrix: { value: new THREE.Matrix4() }, // View matrix
mvpMatrix: { value: new THREE.Matrix4() },
cDensity: { value: cDensity }, // multiplier 0.0-1.0
cloudsOn: { value: 2 }, // match with cloudquality startup
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,
});
// simple 2d clouds
//const cloudsGeometry = new THREE.SphereGeometry(3100, 32, 24); // more than skybox needs renderorder and depthtest false
const cloudsGeometry = new THREE.SphereGeometry(2950, 32, 24); // 3000 with depth write to make 'growing in' muntains
const cloudsMaterial = new THREE.ShaderMaterial({
uniforms: {
sunDirection: { value: sunTime.normalize() }, // Sun pos (normalized) from sphere coords
//moonDirection: { value: moonTime.normalize() }, // Sun pos (normalized) from sphere coords
xtime: { value: 0.0 },
daytime: { value: 0.0 },
lightDir: { value: sunLight.position.normalize() },
lightDir2: { value: sunLight.position.normalize().negate() },
nMatrix: { value: new THREE.Matrix3() }, // Initialize empty mat3
vMatrix: { value: new THREE.Matrix4() }, // View matrix
mvpMatrix: { value: new THREE.Matrix4() },
cDensity: { value: cDensity }, // multiplier 0.0-1.0
},
//vertexShader: vertexShaderClouds2,
//fragmentShader: fragmentShaderClouds2,
vertexShader: vertexShaderSky,
fragmentShader: fragmentShaderShadow, // debug
side: THREE.BackSide, // Inside out
depthWrite: false,
fog: false,
transparent: true,
alphaTest: 0.3,
});
const clouds = new THREE.Mesh(cloudsGeometry, cloudsMaterial);
clouds.visible=false;
clouds.castShadow = false;
//clouds.rotation.copy(sky.rotation);
clouds.renderOrder = 1;
clouds.layers.set(1);
//scene.add(clouds); // builtin with skymaterial
skyGroup.add(clouds);
//clouds.customDepthMaterial = customDepthMat;
//clouds.customDepthMaterial.shadowSide = THREE.BackSide;
sky.castShadow = true;
//sky.customDepthMaterial = null;
sky.customDepthMaterial = customDepthMat;
sky.customDepthMaterial.shadowSide = THREE.FrontSide;
clouds.receiveShadow = false;
sky.receiveShadow = false;
//sky.rotation.set(0.0,0.0,90.0*radian);
// Terrain setup
const terrainGroup = new THREE.Group();
scene.add(terrainGroup);
// LOD config – fine-tuned for your scale (4000 tile size, flying speed ~50)
const lodLevels = [
{ maxDist: 3000, segments: 256 }, // very close – max detail
{ maxDist: 8000, segments: 128 }, // medium distance
{ maxDist: Infinity, segments: 64 }, // everything else – good enough
// Optional extra low LOD for very far
// { maxDist: Infinity, segments: 32 }
];
// Northern lights setup (add after terrainGroup and lights)
const numSegments = 30; // Divisions for the strip (15-30 quads)
const stripWidth = 8000; // Length along z (tune for scene scale)
const stripHeight = 2000; // Y position in sky
const beamDensity = 4; // Beams per quad (2-6)
const beamStretch = 4.0; // Tall oval stretch
//const lightGroup = new THREE.Group();
//scene.add(lightGroup); // Parent to scene or camera if you want it fixed to view
var nextNorthernLight = 0.0; // check if night && cDensity<
var waitNorthernLight = 0.0; // wait for next to rnd?
var northernLightOpa = 0.0; //
var northernLightFadeState=0; // 0 = next is fade in, 1 = next is fade out
var northernLightAlwaysOn = 0;
function newNorthernPosition() {
// reposition nothernligght
const nX=rndMinMax(0,1000);
const nZ=rndMinMax(0,1000);
const nY=rndMinMax(900,1200);
//northernStrip.position.set(500, 1000, 500);
northernStrip.position.set(nX, nY, nZ);
const deg=rndMinMax(0,359);
northernStrip.rotation.y = (90-45+deg) * radian;
}
// GLSL Perlin for shader (copy your ClassicalNoise port from earlier, or use simple 2D noise for beams)
// Custom shader for glowing beams
const northernMaterial = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0.0 }, // for slow movement
beamDensity: { value: beamDensity },
beamStretch: { value: beamStretch },
glowColor: { value: new THREE.Color(0x00ffaa) }, // Green variance base
fadeOpa: { value: 0.0 },
debug: { value: 0 }
},
vertexShader: vertexShaderNorthernLight,
fragmentShader: fragmentShaderNorthernLight,
blending: THREE.AdditiveBlending, // Glowing effect
side: THREE.DoubleSide,
depthTest: false, // IGNORE depth → blends into sky
depthWrite: false, // do NOT write depth → doesn't block anything
transparent: true,
});
const stripGeometry = new THREE.PlaneGeometry(stripWidth, stripHeight, numSegments, 1);
//stripGeometry.rotateX(-Math.PI / 2); // Lay flat
stripGeometry.rotateY(Math.PI / 2); // or -Math.PI / 2 to face opposite// Generate the strip geometry (single mesh for perf)
// Optional: slight curve or tilt
stripGeometry.rotateZ(-Math.PI / 12); // small tilt for sky ribbon feel
//stripGeometry.translate(0, stripHeight, 0); // High in sky
const effectGroup = new THREE.Group();
scene.add(effectGroup);
const northernStrip = new THREE.Mesh(stripGeometry, northernMaterial);
northernStrip.position.set(500, 1000, 500); // High up, far in front (adjust z for distance)
//northernStrip.rotation.y = Math.PI - (Math.PI / 2 * .1); // Flip to face camera if needed
//northernStrip.rotation.y = (90+45) * radian;
northernStrip.rotation.y = (90-45) * radian;
//lightGroup.add(northernStrip);
//scene.add(northernStrip);
northernStrip.layers.set(2); // northernStrip inherits this
northernStrip.castShadow = false;
effectGroup.add(northernStrip);
skyGroup.layers.set(1); // skyGroup.children (the sphere) inherit this
effectGroup.layers.set(2); // northernStrip inherits this
terrainGroup.layers.set(3); // or .set(0) if you want exclusive
// or use built in render order
//sky.renderOrder = 0; // sky renders FIRST
//northernStrip.renderOrder = 2; // render after sky
//terrainGroup.renderOrder = 5; // render all opaque
var shadowOn=1; // start with shadow
//var shadowOn=0; // start no shadow
const tileSize = 4000; // Widened to hide edges easier
const segments = 512; // Increased for better detail/resolution
const noiseScale = 100;
const speed = 50; // Flying speed
const numTiles = 5; // Keep this many tiles visible
const textureLoader = new THREE.TextureLoader();
const grass1Tex = textureLoader.load('./textures/grass2.png');
grass1Tex.wrapS = grass1Tex.wrapT = THREE.RepeatWrapping;
grass1Tex.repeat.set(250.0, 250.0);
const grass2Tex = textureLoader.load('./textures/grass3.png');
grass2Tex.wrapS = grass2Tex.wrapT = THREE.RepeatWrapping;
grass2Tex.repeat.set(250.0, 250.0);
const grass3Tex = textureLoader.load('./textures/grass4.png');
grass3Tex.wrapS = grass3Tex.wrapT = THREE.RepeatWrapping;
grass3Tex.repeat.set(250.0, 250.0);
const dirt1Tex = textureLoader.load('./textures/dirt2.png');
dirt1Tex.wrapS = dirt1Tex.wrapT = THREE.RepeatWrapping;
dirt1Tex.repeat.set(250.0, 250.0);
const rock1Tex = textureLoader.load('./textures/rock2.png');
rock1Tex.wrapS = rock1Tex.wrapT = THREE.RepeatWrapping;
rock1Tex.repeat.set(250.0, 250.0);
const rock2Tex = textureLoader.load('./textures/rock3.png');
rock2Tex.wrapS = rock2Tex.wrapT = THREE.RepeatWrapping;
rock2Tex.repeat.set(250.0, 250.0);
const rock3Tex = textureLoader.load('./textures/rock4.png');
rock3Tex.wrapS = rock3Tex.wrapT = THREE.RepeatWrapping;
rock3Tex.repeat.set(250.0, 250.0);
const rock4Tex = textureLoader.load('./textures/rock5.png');
rock4Tex.wrapS = rock4Tex.wrapT = THREE.RepeatWrapping;
rock4Tex.repeat.set(250.0, 250.0);
const normalGrass = textureLoader.load('./textures/grass2_normal.png');
normalGrass.wrapS = normalGrass.wrapT = THREE.RepeatWrapping;
normalGrass.repeat.set(250.0, 250.0);
const normalDirt = textureLoader.load('./textures/dirt2_normal.png');
normalDirt.wrapS = normalDirt.wrapT = THREE.RepeatWrapping;
normalDirt.repeat.set(250.0, 250.0);
const normalRock = textureLoader.load('./textures/rock2_normal.png');
normalRock.wrapS = normalRock.wrapT = THREE.RepeatWrapping;
normalRock.repeat.set(250.0, 250.0);
const roughGrass = textureLoader.load('./textures/grass2_rough.png');
roughGrass.wrapS = roughGrass.wrapT = THREE.RepeatWrapping;
roughGrass.repeat.set(250.0, 250.0);
const roughDirt = textureLoader.load('./textures/dirt2_rough.png');
roughDirt.wrapS = roughGrass.wrapT = THREE.RepeatWrapping;
roughDirt.repeat.set(250.0, 250.0);
const roughRock = textureLoader.load('./textures/rock2_rough.png');
roughRock.wrapS = roughRock.wrapT = THREE.RepeatWrapping;
roughRock.repeat.set(250.0, 250.0);
const splatTex = textureLoader.load('./textures/splat2.png'); // Assume this is your RGBA splatmap (R=grass1, G=grass2, B=grass3, A=dirt1/black default)
splatTex.wrapS = splatTex.wrapT = THREE.RepeatWrapping;
splatTex.repeat.set(1.0, 1.0); // Different scale (less repeating for larger features; adjust as needed)
const diffSnow = textureLoader.load('./textures/snow.png');
diffSnow.wrapS = diffSnow.wrapT = THREE.RepeatWrapping;
diffSnow.repeat.set(128.0, 128.0);
const normalSnow = textureLoader.load('./textures/snow_normal.png');
normalSnow.wrapS = normalSnow.wrapT = THREE.RepeatWrapping;
normalSnow.repeat.set(128.0, 128.0);
const roughSnow = textureLoader.load('./textures/snow_rough.png');
roughSnow.wrapS = roughSnow.wrapT = THREE.RepeatWrapping;
roughSnow.repeat.set(128.0, 128.0);
//const diffMountain = textureLoader.load('./textures/mountain1.png');
//diffMountain.wrapS = diffMountain.wrapT = THREE.RepeatWrapping;
//diffMountain.repeat.set(128.0, 128.0);
//const normalMountain = textureLoader.load('./textures/mountain1_normal.png');
//normalMountain.wrapS = normalMountain.wrapT = THREE.RepeatWrapping;
//normalMountain.repeat.set(128.0, 128.0);
//const roughMountain = textureLoader.load('./textures/mountain1_rough.png');
//roughMountain.wrapS = roughMountain.wrapT = THREE.RepeatWrapping;
//roughMountain.repeat.set(128.0, 128.0);
const terrainMaterial = new THREE.ShaderMaterial({
uniforms: THREE.UniformsUtils.merge([
THREE.UniformsLib.lights,
THREE.UniformsLib.fog,
{
grass1Tex: { value: grass1Tex },
grass2Tex: { value: grass2Tex },
grass3Tex: { value: grass3Tex },
dirt1Tex: { value: dirt1Tex },
rock1Tex: { value: rock1Tex },
rock2Tex: { value: rock2Tex },
rock3Tex: { value: rock3Tex },
rock4Tex: { value: rock4Tex },
normalGrass: { value: normalGrass },
normalDirt: { value: normalDirt },
normalRock: { value: normalRock },
roughGrass: { value: roughGrass },
roughDirt: { value: roughDirt },
roughRock: { value: roughRock },
splatTex: { value: splatTex },
repeatScale: { value: 250.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 },
cameraPosition: { value: camera.position.clone() },
fogColor: { value: new THREE.Color(0xffeeff) },
fogDensity: { value: fogDensity },
sunShadowMap: { value: null },
sunShadowMatrix: { value: new THREE.Matrix4() },
shadowBias: { value: shadowBias },
shadowNormalBias: { value: shadowNormalBias },
shadowRadius: { value: shadowRadius },
shadowOn: { value: shadowOn },
rimShineStrength: { value: 0.28 },
rimShineColor: { value: new THREE.Color(1.0, 0.98, 0.92) },
}
]),
vertexShader: vertexShaderTerrain,
fragmentShader: fragmentShaderTerrain,
side: THREE.FrontSide,
fog: true,
lights: true,
depthWrite: true,
depthTest: true,
});
// Function to get height at world position (using same noise)
//function getHeightAt(worldX, worldZ) {
// return heightScale * (perlin.noise(worldX / noiseScale, worldZ / noiseScale, 0) + 0.5);
//}
// Updated ClassicalNoise class remains the same, but updating getHeightAt for multi-octave noise to reduce repetitiveness
// This creates a more cloud-like, natural terrain heightmap similar to Blender's smooth interpolated noise clouds by using fBm (fractional Brownian motion)
const offsetHeight=-35;
function getHeightAt(worldX, worldZ) {
let h = 0.0;
let amp = 1.0;
let freq = 1.0 / noiseScale;
const octaves = 2; // 5
let totalAmp = 0.0;
for (let i = 0; i < octaves; i++) {
h += amp * perlin.noise(worldX * freq, worldZ * freq, 0);
totalAmp += amp;
amp *= 0.5;
freq *= 2.0;
}
h = (h / totalAmp + 1.0) / 2.0; // Normalize to 0-1 range
return (heightScale * h) + offsetHeight;
}
// Function to create/update a tile
function createTile(zOffset, segments) {
const geometry = new THREE.PlaneGeometry(tileSize, tileSize, segments, segments);
geometry.rotateX(-Math.PI / 2);
const mesh = new THREE.Mesh(geometry, terrainMaterial);
mesh.castShadow = true;
mesh.position.z = zOffset;
mesh.userData.segments = segments; // for debug/info if needed
updateTileHeights(mesh); // extract height update to separate function
return mesh;
}
function updateTileHeights(tile) {
const geometry = tile.geometry;
const vertices = geometry.attributes.position.array;
for (let i = 0; i < vertices.length; i += 3) {
const worldX = vertices[i] + tile.position.x;
const worldZ = vertices[i + 2] + tile.position.z;
vertices[i + 1] = getHeightAt(worldX, worldZ);
}
geometry.attributes.position.needsUpdate = true;
geometry.computeVertexNormals();
}
var mountainMaterial=null; // shared, initial only
function createCustomMountainMaterial(originalMat) {
if(mountainMaterial==null) {
//return new THREE.ShaderMaterial({
mountainMaterial=new THREE.ShaderMaterial({
//uniforms: {
uniforms: THREE.UniformsUtils.merge([
THREE.UniformsLib.lights,
THREE.UniformsLib.fog,
{
map: { value: originalMat.map },
normalMap: { value: originalMat.normalMap },
roughnessMap: { value: originalMat.roughnessMap },
snowTex: { value: diffSnow },
normalSnow: { value: normalSnow },
roughSnow: { value: roughSnow },
// 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: 850.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,
});
}
return mountainMaterial;
}
var loading=0;
var mountainlist=[
{ name: "mountain_1", ref_lod0: null, ref_lod1: null },
{ name: "mountain_2", ref_lod0: null, ref_lod1: null },
{ name: "mountain_3", ref_lod0: null, ref_lod1: null },
{ name: "mountain_4", ref_lod0: null, ref_lod1: null },
{ name: "mountain_5", ref_lod0: null, ref_lod1: null },
{ name: "mountain_6", ref_lod0: null, ref_lod1: null },
{ name: "mountain_7", ref_lod0: null, ref_lod1: null },
{ name: "mountain_8", ref_lod0: null, ref_lod1: null },
{ name: "mountain_9", ref_lod0: null, ref_lod1: null },
{ name: "mountain_10", ref_lod0: null, ref_lod1: null },
{ name: "mountain_11", ref_lod0: null, ref_lod1: null },
{ name: "mountain_12", ref_lod0: null, ref_lod1: null },
{ name: "mountain_13", ref_lod0: null, ref_lod1: null },
{ name: "mountain_14", ref_lod0: null, ref_lod1: null },
{ name: "mountain_15", ref_lod0: null, ref_lod1: null },
{ name: "mountain_16", ref_lod0: null, ref_lod1: null },
];
var mountainReady=false;
function load_mountains() {
const loader = new GLTFLoader();
const promises = [];
for(let i=0;i<mountainlist.length;i++) {
const url = './models/mountainpack/' + mountainlist[i]['name']+".gltf";
const url_low = './models/mountainpack/' + mountainlist[i]['name']+"_lod1.gltf";
// Push a promise into the array that also carries the index 'i' in its resolved value
promises.push(
loader.loadAsync(url).then(gltf => ({ gltf, index: i, lod: 0 }))
);
promises.push(
loader.loadAsync(url_low).then(gltf => ({ gltf, index: i, lod: 1 }))
);
}
Promise.all(promises)
.then(results => {
// results is an array like: [{ gltf: ..., index: 0 }, { gltf: ..., index: 1 }, ...]
console.log("All models loaded (mountains)");
results.forEach(({ gltf, index, lod }) => {
console.log("Processing index:", index);
console.log("Model name:", mountainlist[index]['name']+" LOD-"+lod);
// Add your logic here using 'index' and 'gltf'
mountainlist[index]['ref_lod'+lod] = gltf.scene;
if(lod==0) {
mountainlist[index]['ref_lod'+lod].traverse(child => {
if (child.isMesh && child.material) {
child.castShadow = true; // prep for shadows later
child.material = createCustomMountainMaterial(child.material);
}
});
} else {
mountainlist[index]['ref_lod'+lod].traverse(child => {
//if (child.isMesh && child.material) {
if (child.isMesh) {
//child.material = child.material.clone(); // separate material
//--child.material.transparent = true;
//--child.material.opacity = 0; // start hidden
child.visible = false; // start hidden
child.castShadow = true; // prep for shadows later
child.material = createCustomMountainMaterial(child.material);
}
});
}
});
console.log("All models processed.(mountains)");
mountainReady=true;
initTiles();
loading=-1;
$('loadtxt').innerHTML="Loading...";
$('loading').style.opacity=0;
$('loading').style.pointerEvents="none";
pushNotice("'k': show keybinds toggle visible/hidden");
if(cloudQuality==4) {
clouds.visible=true;
}
infoDiv.style.display="block";
skyMaterial.uniforms.cloudsReady.value = 1;
setTimeout(function() { $('loading').style.display="none"; },1000);
})
.catch(error => {
console.error("An error occurred during loading:", error);
});
}
load_mountains();
const spacing=500.0; // clear the center path
// Function to add mountains to a tile (called on create and recycle)
function addMountainsToTile(tile) {
if (!mountainReady) return;
//const mountainGeo = new THREE.ConeGeometry(20, 40, 32); // Larger mountains
//const mountainMat = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.8, metalness: 0 });
const spawnC=rndMinMax(2,44);
for (let i = 0; i < spawnC; i++) { // Add a few per tile
const posX = rndMinMax(spacing, tileSize / 2) * (Math.random() > 0.5 ? 1 : -1);
const posZ = Math.random() * tileSize - tileSize / 2;
const height = getHeightAt(posX + tile.position.x, posZ + tile.position.z);
const pick = Math.floor(Math.random() * mountainlist.length);
const scale = rndMinMax(150, 250);
const mountain = deepCloneGeometry(mountainlist[pick]['ref_lod1']);
mountain.position.set(posX, height - 50, posZ); // Slightly below surface
mountain.rotation.y = Math.random() * Math.PI * 2; // Random rotate
//mountain.scale.set(scale * 0.01, scale * 0.01, scale * 0.01); // start tiny
mountain.scale.set(scale, scale, scale);
mountain.userData = {
pick: pick,
targetScale: scale,
lod0: mountainlist[pick].ref_lod0, // reference to high detail
lod1: mountainlist[pick].ref_lod1, // reference to low
currentLod: 1, // 1 = low (current)
switchDist: 2000, // switch to high below this
fadeDist: 6000, // fading dist for opacity (4000) - currently "pop" dist (6000)
visible: false // start hidden
};
// Start fully transparent
mountain.traverse(child => {
//if (child.isMesh && child.material) {
if (child.isMesh) {
//--child.material.transparent = true;
//--child.material.opacity = 0;
child.renderOrder = 50;
child.layers.set(3);
child.visible = false;
}
});
mountain.renderOrder = 50;
mountain.layers.set(3);
tile.add(mountain);
}
}
// Initialize tiles
const tiles = [];
const zInitOffset=4000.0;
function initTiles() {
for (let i = 0; i < numTiles; i++) {
const tile = createTile(i * tileSize - (numTiles / 2 * tileSize) - zInitOffset, lodLevels[0].segments);
//tile.renderOrder = 3;
addMountainsToTile(tile);
tile.layers.set(3);
tiles.push(tile);
terrainGroup.add(tile);
}
}
let fireworkTemplate = null; // ← global
let fireworkAnimations = []; // ← global array of clips
const mixers = []; // Array for per-instance AnimationMixers
const fwmaterials = {}; // Store refs
const fwOffsetY=-5.0;
function load_fireworks() {
const loader = new GLTFLoader();
loader.load('./models/fireworks2/fireworks3.gltf', (gltf) => {
fireworkTemplate = gltf.scene;
fireworkAnimations = gltf.animations || []; // save animations here
fireworkTemplate.scale.set(50.0,50.0,50.0);
// fix rotation in blender instead and re-export~
// if need rotation fixing, make empty non animated parent and try these
// THIS IS THE MAGIC LINE
//fireworkTemplate.rotation.y = Math.PI; // –90° instead of +90° (Blender Z-up → Three Y-up)
// AND THIS ONE — re-orients the animation tracks themselves
//fireworkTemplate.traverse((child) => {
// if (child.isBone || child.isObject3D) {
// child.quaternion.premultiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1,0,0), Math.PI));
// }
//});
fireworkTemplate.traverse((child) => {
if (child.isMesh) {
//if(!firstSca) {
child.scale.setScalar(1); // force initial scale to 1 so baked tracks start from visible size
//} else { // mp3 page
// child.scale.setScalar(64); // force initial scale to 1 so baked tracks start from visible size
//}
}
});
// Optional but helps with bounding boxes
fireworkTemplate.updateMatrixWorld(true);
// Traverse to log/access objects/materials for debugging
fireworkTemplate.traverse((child) => {
if (child.isMesh) {
console.log('Mesh:', child.name, 'Material:', child.material ? child.material.name : 'None');
}
});
fireworkAnimations.forEach((clip, idx) => {
console.log(`Clip ${idx} tracks:`, clip.tracks.map(t => t.name));
clip.tracks.forEach(track => {
if (track.name.includes('.scale') && track.values.length >= 3) {
track.values[0] = 1; // X
track.values[1] = 1; // Y
track.values[2] = 1; // Z first keyframe
console.log('Modified initial scale for', track.name);
}
});
});
console.log('Firework model loaded – ready to spawn!');
// optional: spawn one immediately for testing
// spawnFirework(somePos, someNorm);
}, undefined, (error) => console.error(error));
}
load_fireworks();
// shared material method does not work with joined meshes**
//--var sunclockMaterial=null; // shared, initial only
//--function createCustomSunclockMaterial(originalMat) {
//-- if(sunclockMaterial==null) {
//-- return new THREE.ShaderMaterial({
//-- //uniforms: {
//-- uniforms: THREE.UniformsUtils.merge([
//-- THREE.UniformsLib.lights,
//-- THREE.UniformsLib.fog,
//-- {
//-- map: { value: originalMat.map },
//-- normalMap: { value: originalMat.normalMap },
//-- roughnessMap: { value: originalMat.roughnessMap },
//-- // Add more if needed: metalnessMap, aoMap, etc.
//-- lightDir: { value: sunLight.position.normalize() },
//-- lightDir2: { value: sunLight.position.normalize().negate() },
//-- daytime: { value: 0.0 },
//-- cameraPosition: { value: camera.position.clone() },
//-- fogColor: { value: new THREE.Color(0xffeeff) }, // Horizon haze
//-- fogDensity: { value: fogDensity },
//-- sunShadowMap: { value: null }, // will set in animate
//-- sunShadowMatrix: { value: new THREE.Matrix4() }, // will set in animate
//-- shadowBias: { value: shadowBias },
//-- clockShadowBias: { value: clockShadowBias },
//-- shadowNormalBias: { value: shadowNormalBias }, // pass your const 0.1
//-- shadowRadius: { value: shadowRadius },
//-- shadowOn: { value: shadowOn },
//-- alphaThreshold: { value: 0.5 },
//-- }]),
//-- //},
//-- vertexShader: vertexShaderSunclock,
//-- fragmentShader: fragmentShaderSunclock,
//-- side: THREE.FrontSide,
//-- fog: true,
//-- lights: true,
//-- transparent: false,
//-- depthWrite: true,
//-- depthTest: true,
//-- });
//-- }
//--
//-- return sunclockMaterial;
//--}
let sunclock = null;
function load_sunclock() {
const loader = new GLTFLoader();
loader.load('./models/sunclock/sunclock.gltf', (gltf) => {
sunclock = gltf.scene;
sunclock.castShadow = true; // prep for shadows later
sunclock.receiveShadow = true;
//sunclock.scale.set(1.0, 1.0, 1.0);
//sunclock.scale.setScalar(10);
//const box = new THREE.Box3().setFromObject(sunclock);
//console.log("Sunclock bounding box:", box.min, box.max);
//console.log("Size:", box.getSize(new THREE.Vector3()));
//sunclock.position.clone(camera.position);
//sunclock.position.set(-22, 53, -170);
sunclock.position.set(0, 53, 0);
sunclock.updateMatrixWorld(true);
console.log("sunclock loaded");
sunclock.renderOrder = 100; // High renderOrder to draw above everything in its pass
sunclock.traverse(child => {
if (child.isMesh && child.material) {
child.castShadow = true; // Sunclock doesn't cast on main map (only self-shadows on clock map)
child.receiveShadow = true; // Still receives
child.scale.setScalar(1); // force initial scale to 1 so baked tracks start from visible size
console.log(child.name+" - "+child.material.name);
//child.material = createCustomSunclockMaterial(child.material); // or use single instead
//console.dir(child.material);
// Create a FRESH material per mesh (don't reuse shared sunclockMaterial)
const customMat = new THREE.ShaderMaterial({
uniforms: THREE.UniformsUtils.clone( // clone to avoid sharing
THREE.UniformsUtils.merge([
THREE.UniformsLib.lights,
THREE.UniformsLib.fog,
{
map: { value: child.material.map }, // per-child texture
normalMap: { value: child.material.normalMap },
roughnessMap: { value: child.material.roughnessMap },
lightDir: { value: sunLight.position.normalize() },
lightDir2: { value: sunLight.position.normalize().negate() },
daytime: { value: 0.0 },
cameraPosition: { value: camera.position.clone() },
fogColor: { value: new THREE.Color(0xffeeff) },
fogDensity: { value: fogDensity },
sunShadowMap: { value: null },
sunShadowMatrix: { value: new THREE.Matrix4() },
clockShadowMap: { value: null },
clockShadowMatrix: { value: new THREE.Matrix4() },
shadowBias: { value: shadowBias },
clockShadowBias: { value: clockShadowBias },
shadowNormalBias: { value: shadowNormalBias },
clockShadowNormalBias: { value: clockShadowNormalBias },
shadowRadius: { value: shadowRadius },
clockShadowRadius: { value: clockShadowRadius },
shadowOn: { value: shadowOn },
alphaThreshold: { value: 0.5 },
rimShineStrength: { value: 0.28 }, // shine rim
rimShineColor: { value: new THREE.Color(1.0, 0.98, 0.92) },
ambientMulti: { value: 1.0 },
normalStrength: { value: 0.4 },
}
])
),
vertexShader: vertexShaderSunclock,
fragmentShader: fragmentShaderSunclock,
side: THREE.DoubleSide, // safer for joined models and shadow precision~ (keep)
fog: true,
lights: true,
transparent: false, // or true if you need alpha
});
child.material = customMat;
console.log("Applied custom material to:", child.name);
}
child.layers.set(5);
//if (child.isMesh) child.geometry.computeBoundingBox();
});
sunclock.layers.set(5);
scene.add(sunclock);
if(!sunclockVisible) {
sunclock.visible=false;
}
//const boxHelper = new THREE.BoxHelper(sunclock, 0xff0000);
//scene.add(boxHelper);
//
// // or custom green bound box cube:
//
//const box = new THREE.Box3().setFromObject(sunclock);
//const size = new THREE.Vector3();
//box.getSize(size);
//
//const cubeGeometry = new THREE.BoxGeometry(size.x, size.y, size.z);
//const cubeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00, transparent: true, opacity: 0.2 });
//const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
///cube.position.copy(sunclock.position);
//cube.rotation.copy(sunclock.rotation);
//scene.add(cube);
//
//// In animate if sunclock moves
//cube.position.copy(sunclock.position);
//cube.rotation.copy(sunclock.rotation);
//--const sunDir = new THREE.Vector3();
//--sunLight.getWorldDirection(sunDir);
//--clockShadowLight.getWorldDirection(sunDir);
const sunDir = sunLight.position.clone().normalize();
clockShadowLight.position.copy(sunclock.position).add(sunDir.multiplyScalar(10));
clockShadowLight.target = sunclock; // point at sunclock
console.log("sunclock added to scene");
});
}
load_sunclock();
// shared material method does not work with joined meshes**
var benchesMaterial=null; // shared, initial only
function createCustomBenchesMaterial(originalMat) {
if(benchesMaterial==null) {
return new THREE.ShaderMaterial({
//uniforms: {
uniforms: THREE.UniformsUtils.merge([
THREE.UniformsLib.lights,
THREE.UniformsLib.fog,
{
map: { value: originalMat.map },
normalMap: { value: originalMat.normalMap },
roughnessMap: { value: originalMat.roughnessMap },
// Add more if needed: metalnessMap, aoMap, etc.
lightDir: { value: sunLight.position.normalize() },
lightDir2: { value: sunLight.position.normalize().negate() },
daytime: { value: 0.0 },
cameraPosition: { value: camera.position.clone() },
fogColor: { value: new THREE.Color(0xffeeff) }, // Horizon haze
fogDensity: { value: fogDensity },
sunShadowMap: { value: null }, // will set in animate
sunShadowMatrix: { value: new THREE.Matrix4() }, // will set in animate
shadowBias: { value: shadowBias },
clockShadowBias: { value: clockShadowBias },
shadowNormalBias: { value: shadowNormalBias }, // pass your const 0.1
shadowRadius: { value: shadowRadius },
shadowOn: { value: shadowOn },
alphaThreshold: { value: 0.5 },
rimShineStrength: { value: 0.28 }, // shine rim
rimShineColor: { value: new THREE.Color(1.0, 0.98, 0.92) },
ambientMulti: { value: 1.0 },
normalStrength: { value: 0.4 },
}]),
//},
vertexShader: vertexShaderSunclock,
fragmentShader: fragmentShaderSunclock,
side: THREE.FrontSide,
fog: true,
lights: true,
transparent: false,
depthWrite: true,
depthTest: true,
});
}
return benchesMaterial;
}
var benchlist=[
{ name: "bench1pink", ref_lod0: null, deg: 0 },
{ name: "bench1red", ref_lod0: null, deg: 0 },
{ name: "bench1wood", ref_lod0: null, deg: 0 },
{ name: "bench2yellow", ref_lod0: null, deg: 0 },
{ name: "bench2green", ref_lod0: null, deg: 0 },
{ name: "bench2wood", ref_lod0: null, deg: 0 },
{ name: "table1cyan", ref_lod0: null, deg: 0 },
{ name: "table1blue", ref_lod0: null, deg: 0 },
{ name: "table1wood", ref_lod0: null, deg: 0 },
];
var benchesReady=false;
function load_benches() {
const loader = new GLTFLoader();
const promises = [];
const step=360/benchlist.length; // trigonometry twily
for(let i=0;i<benchlist.length;i++) {
const url = './models/benches/' + benchlist[i]['name']+".gltf";
// Push a promise into the array that also carries the index 'i' in its resolved value
promises.push(
loader.loadAsync(url).then(gltf => ({ gltf, index: i, lod: 0 }))
);
benchlist[i]['deg']=step*i;
}
Promise.all(promises)
.then(results => {
// results is an array like: [{ gltf: ..., index: 0 }, { gltf: ..., index: 1 }, ...]
console.log("All models loaded (benches)");
results.forEach(({ gltf, index, lod }) => {
console.log("Processing index:", index);
console.log("Model name:", benchlist[index]['name']+" LOD-"+lod);
// Add your logic here using 'index' and 'gltf'
benchlist[index]['ref_lod'+lod] = gltf.scene;
benchlist[index]['ref_lod'+lod].castShadow = true; // prep for shadows later
benchlist[index]['ref_lod'+lod].receiveShadow = true;
//if(lod==0) {
benchlist[index]['ref_lod'+lod].traverse(child => {
if (child.isMesh && child.material) {
child.receiveShadow = true; // Still receives
child.castShadow = true; // prep for shadows later
child.layers.set(5);
child.material = createCustomBenchesMaterial(child.material);
if(!benchesVisible) {
child.visible=false; // start hidden
}
}
});
const orbit=5.2;
const deg=benchlist[index]['deg'];
const x=(Math.cos(deg*radian)*orbit);
const z=(Math.sin(deg*radian)*orbit);
benchlist[index]['ref_lod0'].position.set(x,53.37,z);
benchlist[index]['ref_lod0'].rotation.set(0,(360*radian)-((deg+90)*radian),0);
scene.add(benchlist[index]['ref_lod0']);
//} else {
// benchlist[index]['ref_lod'+lod].traverse(child => {
// //if (child.isMesh && child.material) {
// if (child.isMesh) {
// //child.material = child.material.clone(); // separate material
// //--child.material.transparent = true;
// //--child.material.opacity = 0; // start hidden
// child.visible = false; // start hidden
// child.castShadow = true; // prep for shadows later
// child.material = createCustomBenchesMaterial(child.material);
// }
// });
//}
});
console.log("All models processed.(benches)");
benchesReady=true;
})
.catch(error => {
console.error("An error occurred during loading:", error);
});
}
load_benches();
const twilyAnim = {
mixer: null,
current: null,
actions: {}
};
let twily = null;
function load_twily() {
const loader = new GLTFLoader();
loader.load('./models/twily/twily2.gltf', (gltf) => {
console.log("GLTF loaded successfully!");
console.log("Scene children:", gltf.scene.children.length);
console.log("Animations:", gltf.animations?.length || 0);
gltf.animations?.forEach(clip => {
console.log("Clip:", clip.name, "duration:", clip.duration);
});
twily = gltf.scene;
twily.castShadow = true; // prep for shadows later
twily.receiveShadow = true;
//twily.scale.set(1.0, 1.0, 1.0);
//twily.scale.setScalar(10);
//const box = new THREE.Box3().setFromObject(twily);
//console.log("twily bounding box:", box.min, box.max);
//console.log("Size:", box.getSize(new THREE.Vector3()));
//twily.position.clone(camera.position);
//twily.position.set(-22, 53, -170);
//twily.position.set(0, 53.43, 2); // front head on
twily.position.set(1.5, 53.43, 2); // side
twily.updateMatrixWorld(true);
console.log("twily loaded");
twily.renderOrder = 100; // High renderOrder to draw above everything in its pass
twily.traverse(child => {
if (child.isMesh && child.material) {
child.castShadow = true; // twily doesn't cast on main map (only self-shadows on clock map)
child.receiveShadow = true; // Still receives
child.scale.setScalar(1); // force initial scale to 1 so baked tracks start from visible size
console.log(child.name+" - "+child.material.name);
//child.material = createCustomtwilyMaterial(child.material); // or use single instead
//console.dir(child.material);
// Create a FRESH material per mesh (don't reuse shared twilyMaterial)
const customMat = new THREE.ShaderMaterial({
uniforms: THREE.UniformsUtils.clone( // clone to avoid sharing
THREE.UniformsUtils.merge([
THREE.UniformsLib.lights,
THREE.UniformsLib.fog,
{
map: { value: child.material.map }, // per-child texture
normalMap: { value: child.material.normalMap },
roughnessMap: { value: child.material.roughnessMap },
lightDir: { value: sunLight.position.normalize() },
lightDir2: { value: sunLight.position.normalize().negate() },
daytime: { value: 0.0 },
cameraPosition: { value: camera.position.clone() },
fogColor: { value: new THREE.Color(0xffeeff) },
fogDensity: { value: fogDensity },
sunShadowMap: { value: null },
sunShadowMatrix: { value: new THREE.Matrix4() },
clockShadowMap: { value: null },
clockShadowMatrix: { value: new THREE.Matrix4() },
shadowBias: { value: shadowBias },
clockShadowBias: { value: clockShadowBias },
shadowNormalBias: { value: shadowNormalBias },
clockShadowNormalBias: { value: clockShadowNormalBias },
shadowRadius: { value: shadowRadius },
clockShadowRadius: { value: clockShadowRadius },
shadowOn: { value: shadowOn },
alphaThreshold: { value: 0.5 },
rimShineStrength: { value: 0.28 }, // shine rim
rimShineColor: { value: new THREE.Color(1.0, 0.98, 0.92) },
ambientMulti: { value: 2.5 },
normalStrength: { value: 0.2 },
}
])
),
vertexShader: vertexShaderTwily,
fragmentShader: fragmentShaderSunclock,
side: THREE.DoubleSide, // safer for joined models and shadow precision~ (keep)
fog: true,
lights: true,
transparent: false, // or true if you need alpha
skinning: true,
morphTargets: false,
});
child.material = customMat;
console.log("Applied custom material to:", child.name);
}
child.layers.set(5);
//if (child.isMesh) child.geometry.computeBoundingBox();
});
twily.layers.set(5);
scene.add(twily);
if(!twilyVisible) {
twily.visible=false;
}
if (gltf.animations && gltf.animations.length > 0) {
twilyAnim.mixer = new THREE.AnimationMixer(twily);
function createAction(clipName, loopMode = THREE.LoopRepeat, clamp = false, weight = 1.0) {
const clip = THREE.AnimationClip.findByName(gltf.animations, clipName);
if (!clip) {
console.warn(`Animation clip "${clipName}" not found!`);
return null;
}
const action = twilyAnim.mixer.clipAction(clip);
action.setLoop(loopMode);
action.clampWhenFinished = clamp;
action.weight = weight;
return action;
}
// Assign them – adjust exact names based on your console output!
// Common Blender-exported names: "Idle", "Start", "Cycle", "End", or sometimes "Armature|Idle", "rig.Cycle"
const idleAction = createAction("rig.Idle", THREE.LoopRepeat, false);
const spreadStartAction = createAction("rig.SpreadStart", THREE.LoopOnce, true); // one-shot, freeze at end
const spreadIdleAction = createAction("rig.SpreadIdle", THREE.LoopRepeat, false); // or LoopOnce if you want exactly N cycles
const spreadEndAction = createAction("rig.SpreadEnd", THREE.LoopOnce, true);
const trampleAction = createAction("rig.Trample", THREE.LoopRepeat, true);
// Store for easy reference
twilyAnim.actions.idle = idleAction;
twilyAnim.actions.spreadstart = spreadStartAction;
twilyAnim.actions.spreadidle = spreadIdleAction;
twilyAnim.actions.spreadend = spreadEndAction;
twilyAnim.actions.trample = trampleAction;
// Start with Idle
if (idleAction) {
idleAction.play();
twilyAnim.current = idleAction;
console.log("Started Idle animation");
}
} else {
console.warn("twily.gltf has no animations");
}
twily.traverse(child => {
if (child.isSkinnedMesh) {
child.frustumCulled = false; // optional safety — prevents culling bugs on animated bounds
}
});
console.log("twily added to scene");
});
}
load_twily();
// transition helper functions
// Helper: crossfade from current → target action (with reset if one-shot)
function crossFadeTo(targetAction, fadeDuration = 0.6, warp = true) {
if (!twilyAnim.current || !targetAction || twilyAnim.current === targetAction) return;
if (targetAction.loop === THREE.LoopOnce || targetAction.loop === THREE.LoopPingPong) {
targetAction.reset(); // rewind to frame 0 + reset weight
}
targetAction
.reset()
.setEffectiveTimeScale(1.0)
.setEffectiveWeight(1.0)
.fadeIn(fadeDuration)
.play();
twilyAnim.current.crossFadeTo(targetAction, fadeDuration, warp);
twilyAnim.current = targetAction;
}
let stopSpread = null;
let nextSpread = rndMinMax(10,30); // rndAnim ~ spread=any anim
// Assumes all related actions are in twilyActions with consistent naming
function playSequence(startAction, loopAction, endAction, returnAction = twilyAnim.actions.idle, fadeIn = 0.6, fadeOut = 0.8) {
const start = startAction;
const cycle = loopAction; // or cycle, loop, etc.
const end = endAction;
if (!start || !cycle || !end) {
console.warn(`Incomplete sequence for animations~to early?`);
return;
}
crossFadeTo(start, fadeIn);
const onStartDone = (e) => {
if (e.action === start) {
twilyAnim.mixer.removeEventListener('finished', onStartDone);
crossFadeTo(cycle, fadeIn);
}
};
twilyAnim.mixer.addEventListener('finished', onStartDone);
// Return a stop function so caller can decide when to end
return () => {
crossFadeTo(end, fadeOut);
const onEndDone = (e) => {
if (e.action === end) {
twilyAnim.mixer.removeEventListener('finished', onEndDone);
crossFadeTo(returnAction, fadeOut);
}
};
twilyAnim.mixer.addEventListener('finished', onEndDone);
stopSpread = null;
};
}
const atlasTexture = new THREE.TextureLoader().load('./models/fireworks2/finalworks.png');
// atlasTexture.flipY = false; // Uncomment if UVs invert
atlasTexture.magFilter = THREE.LinearFilter; // Or Nearest for pixel art
let baseFireworkMaterial = new THREE.ShaderMaterial({
uniforms: {
diffuseMapFirework: { value: atlasTexture },
u_offset: { value: new THREE.Vector2(0, 0) },
u_repeat: { value: new THREE.Vector2(0.05, 0.05) }, // 1/20
u_opacity: { value: 1.0 }
},
vertexShader: vertexShaderFirework,
fragmentShader: fragmentShaderFirework,
//fragmentShader: fragmentShaderFireworkDEBUG,
transparent: true,
depthWrite: false, // Helps with blending in space
side: THREE.DoubleSide // If models need backface
});
baseFireworkMaterial.blending = THREE.AdditiveBlending; // glowy fireworks!
baseFireworkMaterial.depthWrite = false;
//--baseFireworkMaterial.dispose(); // Free old program
// Re-create from scratch (copy-paste your original definition)
//--baseFireworkMaterial = new THREE.ShaderMaterial({
//-- uniforms: {
//-- diffuseMapFirework: { value: atlasTexture },
//-- u_offset: { value: new THREE.Vector2(0, 0) },
//-- u_repeat: { value: new THREE.Vector2(0.05, 0.05) },
//-- u_opacity: { value: 1.0 }
//-- },
//-- vertexShader: vertexShaderFirework,
//-- fragmentShader: fragmentShaderFirework,
//-- //fragmentShader: fragmentShaderFireworkDEBUG,
//-- transparent: true,
//-- transparent: true,
//-- depthWrite: false,
//-- side: THREE.DoubleSide,
//-- blending: THREE.AdditiveBlending
//--});
// NEW: Distance-based brightness boost (makes far fireworks pop)
/*baseFireworkMaterial.onBeforeCompile = (shader) => {
shader.uniforms.cameraPos = { value: camera.position };
shader.uniforms.distanceScale = { value: 4.0 }; // Tweak 1.5–4.0
shader.fragmentShader = shader.fragmentShader.replace(
'#include <fog_fragment>',
`
#include <fog_fragment>
float dist = length(vViewPosition);
float boost = 1.0 + (dist / 50.0) * distanceScale; // Adjust 50.0 to your orbit distance
gl_FragColor.rgb *= boost;
`
);
};*/
const activeFireworks = [];
const clock = new THREE.Clock(); // Global
const fwFPS = 5; // Low as mentioned
const ANIM_COLS = 4;
const FRAMES_PER_ANIM = 5;
const TOTAL_FRAME_COLS = ANIM_COLS * FRAMES_PER_ANIM; // 20
function updateFireworkUV(fw, globalTime) {
const age = globalTime - fw.startTime;
const phase = age < fw.launchDuration ? 'ascent' :
age < fw.launchDuration + fw.explodeDuration ? 'explode' : 'fade';
let localFrame = 0;
if (phase === 'ascent') {
localFrame = Math.floor(age * fwFPS); // Normal for trail/ball
} else if (phase === 'explode') {
localFrame = Math.floor((age - fw.launchDuration) * fwFPS);
} else { // Fade: Slow to last frames or blank
localFrame = 23; // Freeze on last frame, or set to 0 for first/blank
}
const frame = localFrame % 24;
const frameCol = frame % FRAMES_PER_ANIM;
const frameRow = Math.floor(frame / FRAMES_PER_ANIM);
fw.instance.traverse((child) => {
if (child.isMesh && child.material && child.material.userData.animIndex !== undefined) {
let animIndex = child.material.userData.animIndex;
// Caps: Freeze/hide during ascent, blank on fade
if (animIndex >= 3 && animIndex <= 12) { // Stars/trails range
if (phase === 'ascent') {
animIndex = 15; // Switch to blank anim slot during ascent
} else if (phase === 'fade') {
animIndex = 15; // Blank for off
}
}
const animCol = animIndex % ANIM_COLS;
const animRow = Math.floor(animIndex / ANIM_COLS);
const globalCol = animCol * FRAMES_PER_ANIM + frameCol;
const globalRowFromTop = animRow * FRAMES_PER_ANIM + frameRow;
const offsetX = globalCol / TOTAL_FRAME_COLS;
const offsetY = (TOTAL_FRAME_COLS - 1 - globalRowFromTop) / TOTAL_FRAME_COLS;
child.material.uniforms.u_offset.value.set(offsetX, offsetY);
// Opacity fade (already in your loop, but sync here if needed)
if (phase === 'fade') {
const fadeProgress = (age - fw.launchDuration - fw.explodeDuration) / 5; // 5s fade
child.material.uniforms.u_opacity.value = Math.max(0, 1 - fadeProgress);
} else if (phase === 'ascent' && age < 0.5) {
child.material.uniforms.u_opacity.value = age / 0.5 * 0.7 + 0.3;
}
}
});
}
function spawnFirework(localPos, tile, starVariant = Math.floor(Math.random() * 5), trailVariant = Math.floor(Math.random() * 5)) {
if (!fireworkTemplate) {
console.warn("Firework model not loaded yet!");
return;
}
const instance = fireworkTemplate.clone();
instance.position.copy(localPos);
//instance.lookAt(instance.position.clone().add(localNorm));
instance.layers.set(4);
tile.add(instance);
// DEBUG LINE — shoots 10 units along normal so you can see the exact spot
//const lineMaterial = new THREE.LineBasicMaterial({ color: 0x00ffff, linewidth: 3 });
//const lineGeo = new THREE.BufferGeometry().setFromPoints([
// new THREE.Vector3(0,0,0),
// localNorm.clone().multiplyScalar(10) // 10 units long cyan line
//]);
//const line = new THREE.Line(lineGeo);
//earth.add(line); // attached to the firework root
// Mixer for launch anim (same as before)
const mixer = new THREE.AnimationMixer(instance);
if (fireworkAnimations.length > 0) {
let clip = THREE.AnimationClip.findByName(fireworkAnimations, 'launch') || fireworkAnimations[0];
if (clip) {
const action = mixer.clipAction(clip);
action.setLoop(THREE.LoopOnce);
action.clampWhenFinished = true;
action.play();
action.getClip().tracks.forEach(track => {
if (track.name.includes('.scale')) {
console.log('Scale track:', track.name, 'values:', track.values); // See if keys are non-1
}
});
}
}
// Material overrides (unchanged)
instance.traverse((child) => {
if (child.isMesh && child.material) {
child.renderOrder = 10; // Higher than clouds (assume clouds=0)
child.layers.set(4);
const matName = child.material.name.toLowerCase();
child.material = baseFireworkMaterial.clone();
if (matName.includes('fireball')) child.material.userData.animIndex = 0;
else if (matName.includes('firetrail')) child.material.userData.animIndex = 1;
else if (matName.includes('firestar')) {
child.material.userData.animIndex = 3 + starVariant;
} else if (matName.includes('startrail')) {
child.material.userData.animIndex = 8 + trailVariant;
} else {
child.material.userData.animIndex = 15; // Fallback to blank/empty anim slot
}
}
});
activeFireworks.push({
instance: instance,
mixer: mixer,
startTime: clock.getElapsedTime(),
launchDuration: 1.8, // Match your ascent time in seconds
explodeDuration: 5.0, // How long the full explosion lasts
totalDuration: 8.0, // Total visible time before fade
tileRef: tile,
});
}
var aSFT;
var currentTime=null;
var baseTime=0;
var timeOffset=0;
function autoSpawnFirework() {
clearTimeout(aSFT);
let seltile=null;
//console.log("auto fireworks initiated");
//let lastDist=-10000.0;
//for(let i=0;i<tiles.length;i++) {
// const tile=tiles[i];
// distance calculation
/*const dx = tile.position.x - camera.position.x;
const dy = tile.position.y - camera.position.y;
const dz = tile.position.z - camera.position.z;
const sum_squares = dx * dx + dy * dy + dz * dz; // Sum of squared differences
const distance = Math.sqrt(sum_squares); // Square root to get distance
console.log("tile z: "+tile.position.z+" tile dist: "+distance);
if(distance>lastDist) lastDist=distance;*/
//};
if(tiles.length>4) {
let sel=3;
//let add=0;
//if(terrainGroupZCount>0) add=1;
if(terrainGroupZ>tileSize*.5) {
sel=2;
}
seltile=tiles[(sel)];
//console.log("seltile="+(sel)+" terrainGroupZ="+terrainGroupZ);
}
if(!seltile) {
aSFT=setTimeout(function() { autoSpawnFirework(); },5000); // try again, not ready
return;
}
currentTime = new Date();
//const currentSimDate = new Date(baseTime + timeOffset);
const currentSimDate = new Date();
//console.log(currentSimDate.getHours());
//const isNYEPeriod = currentSimDate >= new Date(currentTime.getFullYear(), 11, 14, 18) && // Dec 14 18:00 UTC
// currentSimDate < new Date(currentTime.getFullYear(), 11, 15, 18); // Dec 15 18:00 UTC TEST
const isNYEPeriod = currentSimDate >= new Date(currentTime.getFullYear(), 11, 31, 18) || // Dec 31 18:00 UTC (start christmas island)
currentSimDate < new Date(currentTime.getFullYear(), 0, 1, 6); // Jan 1 14:00 UTC (end hawaii) // currentTime is currentSimDate, no need to +1 year
//console.log(currentTime.getMinutes());
//const NYEquiet=(currentTime.getMinutes()<=30)?1.0:0.5; // most active first 30 min past every hour
const NYEquiet=1.0; // full active whole hour
const spawnChance = isNYEPeriod ? 1.0 : 0.1; // 100% during NYE, 10% otherwise
if (Math.random() > spawnChance || Math.random() > NYEquiet || !fireworkTemplate) {
aSFT=setTimeout(function() { autoSpawnFirework(); },3000);
return;
}
//const lat=0;
//const lon=0;
let spawnNum=rndMinMax(1,2);
if(isNYEPeriod) {
if(NYEquiet==1.0) {
//if(!firstSca) {
spawnNum=rndMinMax(5,50);
//} else { // mp3 page
/// spawnNum=rndMinMax(2,15);
//}
//console.log("Is NYE now! (quiet time)");
} else {
spawnNum=rndMinMax(1,10);
//console.log("Is NYE now!");
}
}
const fwsizeZ = tileSize;
const fwsizeX = tileSize / 8;
let fspawn=0;
while(fspawn<spawnNum) {
//const posX = rndMinMax(spacing, tileSize / 2) * (Math.random() > 0.5 ? 1 : -1);
const selected = { x: Math.random() * fwsizeX - fwsizeX / 2, z: Math.random() * fwsizeZ - fwsizeZ / 2 };
//console.dir(selected);
let fwpos={x: 0.0, y: 0.0, z: 0.0 };
fwpos.x = selected.x + rndMinMax(-100,100); // Cluster around city
fwpos.z = selected.z + rndMinMax(-100,100);
fwpos.y = getHeightAt(fwpos.x + seltile.position.x, fwpos.z + seltile.position.z) + fwOffsetY;
const spawnWick=rndMinMax(10,600);
setTimeout(spawnFirework,spawnWick,fwpos,seltile); // fwnorm
fspawn++;
//console.log("new fireworks at x:"+fwpos.x+" y:"+fwpos.y+" z:"+fwpos.z);
}
let nextFwSpawn=rndMinMax(100,5000);
aSFT=setTimeout(function() { autoSpawnFirework(); },nextFwSpawn*1);
}
autoSpawnFirework();
const geometryPool = new Map(); // key = segments, value = PlaneGeometry
function deepCloneGeometry(original) {
//console.log(original);
const clone = original.clone();
// Force new buffers for all attributes that might be modified
for (const name in clone.attributes) {
clone.attributes[name] = clone.attributes[name].clone();
}
if (clone.index) {
clone.index = clone.index.clone();
}
return clone;
}
// In updateTileLODs() – use squared distance to avoid sqrt cost
function updateTileLODs() {
const camPos = camera.position;
tiles.forEach(tile => {
const tileCenter = new THREE.Vector3(0, 0, tile.position.z).applyMatrix4(terrainGroup.matrixWorld);
const distSq = camPos.distanceToSquared(tileCenter);
let targetSegments = lodLevels[lodLevels.length - 1].segments;
for (const level of lodLevels) {
if (distSq < level.maxDist * level.maxDist) {
targetSegments = level.segments;
break;
}
}
if (targetSegments !== tile.userData.segments) {
const oldGeo = tile.geometry;
// Create new geometry
const newGeo = new THREE.PlaneGeometry(tileSize, tileSize, targetSegments, targetSegments);
//const newGeo = deepCloneGeometry(geometryPool.get(segments));
newGeo.rotateX(-Math.PI / 2);
//console.log('Switching tile to segments:', targetSegments, 'clone position count:', newGeo.attributes.position.count);
tile.geometry = newGeo;
tile.userData.segments = targetSegments;
// Re-apply heights & normals
updateTileHeights(tile);
oldGeo.dispose(); // Free memory immediately
}
});
}
let mountainTimer = 0;
const MOUNTAIN_UPDATE_INTERVAL = 0.3; // every 300ms
function updateMountainLODsAndVisibility(dt) {
mountainTimer += dt;
if (mountainTimer < MOUNTAIN_UPDATE_INTERVAL) return;
mountainTimer = 0;
const camPos = camera.position;
terrainGroup.traverse(obj => {
if (!obj.userData.pick) return; // not a mountain
const worldPos = obj.getWorldPosition(new THREE.Vector3());
const dist = camPos.distanceTo(worldPos);
const ud = obj.userData;
// 1. Handle visibility (fade in from far)
const shouldBeVisible = dist < ud.fadeDist;
if (shouldBeVisible && !ud.visible) {
ud.visible = true;
// Trigger fade-in
obj.traverse(child => {
//if (child.isMesh && child.material) {
if (child.isMesh) {
//child.material.opacity = 0;
//--child.material.opacity = 1.0;
child.visible = true;
//child.material.needsUpdate = true;
}
});
}
// 2. Fade opacity
if (ud.visible) {
obj.traverse(child => {
//if (child.isMesh && child.material && child.material.transparent) {
if (child.isMesh) {
if (dist < ud.fadeDist - 200) {
//--child.material.opacity = 1.0;
child.visible = true;
} else {
// Smooth fade in over 800 units
//child.material.opacity = 1.0 - (dist - (ud.fadeDist - 800)) / 800;
//child.material.opacity = Math.max(0, Math.min(1, child.material.opacity));
//--child.material.opacity = 0.0;
child.visible = false;
}
}
});
}
// 3. LOD switch (only when close enough)
const targetLod = dist < ud.switchDist ? 0 : 1;
if (targetLod !== ud.currentLod) {
//console.log(dist);
const parent = obj.parent;
if (!parent) return;
const newModel = targetLod === 0
? ud.lod0.clone()
: ud.lod1.clone();
// Copy transform
newModel.position.copy(obj.position);
newModel.rotation.copy(obj.rotation);
newModel.scale.copy(obj.scale);
newModel.layers.set(3);
// Traverse and set layer on all children (important for GLTF clones with sub-meshes)
newModel.traverse(child => {
child.layers.set(3); // set on every descendant
});
// Copy visibility state
newModel.userData = { ...ud, currentLod: targetLod };
if (!shouldBeVisible) {
newModel.traverse(child => {
//if (child.isMesh && child.material) {
if (child.isMesh) {
//--child.material.transparent = true;
//--child.material.opacity = 0;
child.visible = false;
}
});
}
parent.remove(obj);
parent.add(newModel);
//console.log(newModel);
}
});
}
var initShadowMaps=false;
var initClockShadowMaps=false;
var firstFrameRendered=false;
function updateShadowCameras() {
const terrainZ = terrainGroup.position.z; // track terrain movement
const camZ = camera.position.z;
const centerZ = camZ - terrainZ; // world z-center of visible area
if (sunLight.shadow.map && 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
//renderer.shadowMap.texture.minFilter = THREE.LinearFilter; // Explicitly set
//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);
// Target: world center z, with orbit offset to center frustum during side angles
const orbitOffset = sunDir.clone().multiplyScalar(2000); // pull toward sun, tune 1000–3000
//const sunTarget = new THREE.Vector3(camera.position).add(orbitOffset);
const sunTarget = new THREE.Vector3(camera.position.x, camera.position.y, centerZ).add(orbitOffset);
// Position: behind sun along its dir
//sunLight.shadow.camera.position.copy(sunPos).addScaledVector(sunDir, -shadowCamFar / 2);
// Lock rotation to scene axes (no tilt/moire)
//sunLight.shadow.camera.rotation.set(0, 0, 0);
//sunLight.shadow.camera.up.set(0, 1, 0);
//sunLight.shadow.camera.updateProjectionMatrix();
sunLight.shadow.camera.updateMatrixWorld();
sunLight.shadow.matrix.copy(sunLight.shadow.camera.projectionMatrix).multiply(sunLight.shadow.camera.matrixWorldInverse.clone());
terrainMaterial.uniforms.sunShadowMatrix.value.copy(sunLight.shadow.matrix);
terrainMaterial.uniforms.sunShadowMap.value = sunLight.shadow.map.texture;
if(mountainMaterial!=null) {
mountainMaterial.uniforms.sunShadowMap.value = sunLight.shadow.map.texture;
mountainMaterial.uniforms.sunShadowMatrix.value.copy(sunLight.shadow.matrix);
}
if(clockShadowLight && sunclock) {
// Use normalized dir from main light (correct for orbit)
const sunDir = sunLight.position.clone().normalize();
// Position behind the clock, along light dir (200 is good — tune 100–400)
clockShadowLight.position.copy(sunclock.position).add(sunDir.multiplyScalar(10));
clockShadowLight.target.position.copy(sunclock.position);
clockShadowLight.target.updateMatrixWorld();
clockShadowLight.updateMatrixWorld();
//clockShadowLight.shadow.updateMatrices(clockShadowLight);
clockShadowLight.shadow.camera.updateMatrixWorld();
clockShadowLight.shadow.matrix.copy(clockShadowLight.shadow.camera.projectionMatrix).multiply(clockShadowLight.shadow.camera.matrixWorldInverse.clone());
clockShadowLight.shadow.map.needsUpdate = true;
sunclock.traverse(child => {
if (child.material && child.material.isShaderMaterial) {
child.material.uniforms.sunShadowMatrix.value.copy(sunLight.shadow.matrix);
child.material.uniforms.sunShadowMap.value = sunLight.shadow.map.texture;
child.material.uniforms.clockShadowMap.value = clockShadowLight.shadow.map.texture;
child.material.uniforms.clockShadowMatrix.value.copy(clockShadowLight.shadow.matrix);
child.material.needsUpdate = true;
}
});
}
if(benchesMaterial!=null) {
//benchesMaterial.uniforms.sunShadowMatrix.value.copy(sunLight.shadow.matrix);
//benchesMaterial.uniforms.sunShadowMap.value = sunLight.shadow.map.texture;
//benchesMaterial.uniforms.clockShadowMap.value = clockShadowLight.shadow.map.texture;
//benchesMaterial.uniforms.clockShadowMatrix.value.copy(clockShadowLight.shadow.matrix);
//benchesMaterial.needsUpdate = true;
for(let i=0;i<benchlist.length;i++) {
if (benchlist[i]['ref_lod0']) {
benchlist[i]['ref_lod0'].traverse(child => {
if (child.material && child.material.isShaderMaterial) {
child.material.uniforms.sunShadowMatrix.value.copy(sunLight.shadow.matrix);
child.material.uniforms.sunShadowMap.value = sunLight.shadow.map.texture;
child.material.uniforms.clockShadowMap.value = clockShadowLight.shadow.map.texture;
child.material.uniforms.clockShadowMatrix.value.copy(clockShadowLight.shadow.matrix);
child.material.needsUpdate = true;
}
});
}
}
}
if(twily) {
twily.traverse(child => {
if (child.material && child.material.isShaderMaterial) {
child.material.uniforms.sunShadowMatrix.value.copy(sunLight.shadow.matrix);
child.material.uniforms.sunShadowMap.value = sunLight.shadow.map.texture;
child.material.uniforms.clockShadowMap.value = clockShadowLight.shadow.map.texture;
child.material.uniforms.clockShadowMatrix.value.copy(clockShadowLight.shadow.matrix);
child.material.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();
}
sunLight.shadow.map.needsUpdate = true;
} else if(!firstFrameRendered) {
firstFrameRendered=true;
}
}
var dd=null;
var hh=0;
var mm=0;
var ss=0;
var tsp=0;
var ss_theta=0;
var ss_phi=0;
var testval=0;
var tmpColor={};
var testValOn=false;
var testValAuto=true;
var waitDensity=0.0;
var nextDensity=0.0;
var cloudDensityTarget=cDensity;
var windY=0.0; // negative/positive range? make dome full sphere to do windX and windZ as well
var waitWindY=0;
var nextWindY=0;
var windVelocityY=0.0;
let lodUpdateTimer = 0;
const LOD_UPDATE_INTERVAL = 0.25; // seconds
let loadtime=0.0;
let loaddelta=0.0;
let initloading=true;
// Animation loop
let lastTime = 0;
let terrainGroupZ=0;
let terrainGroupZCount=0;
let terrainMove=true;
let gradientBG="";
var lastPos=new THREE.Vector3(0.0,0.0,0.0);
var lastRot=new THREE.Vector3(0.0,0.0,0.0);
var infoOpa=300;
var nextNorthernLight = 0.0; // check if night && cDensity<
var waitNorthernLight = 0.0; // wait for next to rnd?
var northernLightOpa = 0.0; //
var northernLightFadeState=0; // 0 = next is fade in, 1 = next is fade out
function animate(time) {
requestAnimationFrame(animate);
const dt = (time - lastTime) / 1000;
lastTime = time;
loaddelta += dt;
nextSpread -= dt;
if(nextSpread <= 0) {
if(stopSpread == null) {
var rndAnim = rndMinMax(1,100);
if(rndAnim>50) {
// multi animation with return
stopSpread = playSequence(
twilyAnim.actions.spreadstart,
twilyAnim.actions.spreadidle,
twilyAnim.actions.spreadend
);
nextSpread=rndMinMax(10,20); // duration spread
} else {
// single animation with return
crossFadeTo(twilyAnim.actions.trample);
stopSpread = function() {
crossFadeTo(twilyAnim.actions.idle);
stopSpread = null
};
nextSpread=rndMinMax(5,10); // duration trample
}
} else {
stopSpread();
nextSpread=rndMinMax(10,30); // wait rndAnim
}
}
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>15.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;
}
}
const delta = clock.getDelta(); // For mixers
const globalTime = clock.getElapsedTime();
updateNotices(Date.now(), dt);
if(waitDensity>nextDensity) {
cloudDensityTarget = rndMinMax(10,90) * .01; // %
nextDensity = rndMinMax(5,50); // sec
waitDensity = 0;
//console.log("cloudDensityTarget="+cloudDensityTarget+", cDensity="+cDensity);
} else {
waitDensity+=dt;
}
if(cDensity<cloudDensityTarget) {
cDensity+=0.001 * dt;
if(cDensity>0.9) cDensity=0.9;
} else if(cDensity>cloudDensityTarget) {
cDensity-=0.001 * dt;
if(cDensity<0.1) cDensity=0.1;
}
skyMaterial.uniforms.cDensity.value=cDensity;
customDepthMat.uniforms.cDensity.value=cDensity;
//console.log("cDensity="+cDensity);
// only used for test clouds sphere simple cloud 1
const cloudsSpinY=windVelocityY * dt; // dt = 1.0 = sec
if(waitWindY>nextWindY) {
windY = rndMinMax(-1.0,1.0);
nextWindY = rndMinMax(5,50); // sec
waitWindY = 0;
} else {
waitWindY+=dt;
}
if(windVelocityY<windY*.001) {
windVelocityY+=0.001 * dt;
if(windVelocityY>1.0) windVelocityY=1.0;
} else if(windVelocityY>windY*.001) {
windVelocityY-=0.001 * dt;
if(windVelocityY>0.0) windVelocityY=0.0;
}
clouds.rotation.y -= cloudsSpinY;
if(terrainMove) {
// Move world toward camera
const terrainMoveZ=speed * dt;
terrainGroupZ += terrainMoveZ;
if(terrainGroupZ>=tileSize) { // keeping track and for fireworks
terrainGroupZ-=tileSize;
terrainGroupZCount++;
}
terrainGroup.position.z -= terrainMoveZ;
}
// Recycle tiles
const firstTile = tiles[0];
if(firstTile) {
if (firstTile.position.z + terrainGroup.position.z < -tileSize-zInitOffset) {
tiles.shift();
firstTile.position.z += numTiles * tileSize-zInitOffset;
// Update heights for new position
const vertices = firstTile.geometry.attributes.position.array;
for (let i = 0; i < vertices.length; i += 3) {
const worldX = vertices[i] + firstTile.position.x;
const worldZ = vertices[i + 2] + firstTile.position.z;
vertices[i + 1] = getHeightAt(worldX, worldZ);
}
firstTile.geometry.attributes.position.needsUpdate = true;
firstTile.geometry.computeVertexNormals();
// Clear old children
while (firstTile.children.length) firstTile.remove(firstTile.children[0]);
// Re-add new mountains
addMountainsToTile(firstTile);
tiles.push(firstTile);
}
}
// Update info box
const pos = camera.position;
const rot = camera.rotation;
infoDiv.innerHTML = `
Camera Position:<br>
X: ${pos.x.toFixed(2)}<br>
Y: ${pos.y.toFixed(2)}<br>
Z: ${pos.z.toFixed(2)}<br><br>
Camera Rotation:<br>
X: ${(rot.x * 180 / Math.PI).toFixed(2)}°<br>
Y: ${(rot.y * 180 / Math.PI).toFixed(2)}°<br>
Z: ${(rot.z * 180 / Math.PI).toFixed(2)}°
`;
if(lastRot.x!=rot.x || lastRot.y!=rot.y || lastRot.z !=rot.z ||
lastPos.x!=pos.x || lastPos.y!=pos.y || lastPos.z!=pos.z) {
infoOpa=300.0; // 100 is fade time + 2x 100 wait
lastRot.copy(rot);
lastPos.copy(pos);
//console.log("has moved");
}
//console.log("last rotation"+lastRot.x+" infoOpa: "+infoOpa);
if(infoOpa>0.0) {
infoOpa-=100.0 * dt; // 1 sec
$('info').style.opacity=(Math.min(infoOpa,100)*.01);
} else {
infoOpa=0.0;
$('info').style.opacity=infoOpa*.01;
}
lodUpdateTimer += dt;
if (lodUpdateTimer >= LOD_UPDATE_INTERVAL) {
updateTileLODs();
lodUpdateTimer = 0;
}
updateMountainLODsAndVisibility(dt);
var lapse=86400; // realtime
if(testValOn) {
lapse=2000; // debugtime testval uncomment
}
//sky.position.copy(camera.position).setY(0);
//sky.position.set(0.0, 0.0, camera.position.z);
// sun position calculations
dd=new Date();
hh=dd.getHours()*60*60;
mm=dd.getMinutes()*60;
ss=dd.getSeconds()+mm+hh;
//ss+=(6*60*60); // offset time, - 4-5 hours seem about accurate for sunset at 6pm front
ss-=(6*60*60);
if(ss>86400) ss-=86400;
if(ss<0) ss+=86400;
if(testValOn) {
// debugtime
if(testValAuto) {
testval++;
}
if(testval>2000) testval-=2000;
tsp=testval/1000; // debugtime
ss=testval;
} else {
// realtime
tsp=ss/43200; // realtime comment out for debugtime sunpos
}
ss_phi=((tsp)*180)+90;
if(ss_phi>360) ss_phi-=360;
//ss_theta=(-45);
var scal=0;
if(ss_phi<180) {
// make 86400 into 360 be 0 to 180 and 180 to 360 into 0 to 2 then 0 to 2 into -1 to 0 to 1, then math abs -1 0 1 into 1 0 1 then make 1 0 1 into 0 1 0 with 1- and 0 -1 0 with -1-
scal=Math.abs((ss_phi/90)-1); // 0 1 0
if(ss_phi<90) scal=-scal; //negative edge case
} else {
scal=1-Math.abs(1-((ss_phi-180)/90)-1); // 0 -1 0
}
//ss_theta=(scal*45);
//let smoothscal=SmoothStep(-1.0,1.0,scal);
let smoothscal=(easeInOut((scal+1.0)*.5)*2.0)-1.0; // curve the midnight/midday corner point up/down while arriving at center
ss_theta=(smoothscal*45);
let ssTun = ss - (25*lapse/100);
if(ssTun<0) ssTun+=lapse;
//console.log(Math.abs((ss/(lapse*.5))-1.0));
const daytime=Math.abs((ssTun/(lapse*.5))-1.0); // 1 0 1
//console.log("ss_phi: "+ss_phi+" ss_theta: "+ss_theta+" scal="+scal);
//console.log(" scal="+scal);
// phi and theta have been swapped here intentionally to tilt on a different axis than top/bottom~
sunTime=getDirection(ss_phi,ss_theta); // 90,170 = default front above ground, 180 phi = on ground
moonTime=getDirection(-ss_phi,-ss_theta); // 90,170 = default front above ground, 180 phi = on ground
//sunPhi should be 180 at 6am and 6pm, going to 300 at night and 60 on day
//sunTheta should be 90 at 6pm, -90 or 270 at 6am
if(daytime>=0.49) { // sun below horizon
sunLight.position.set(sunTime.x*8000,sunTime.y*8000,sunTime.z*8000);
} else {
sunLight.position.set(-moonTime.x*8000,moonTime.y*8000,moonTime.z*8000);
}
if(clockShadowLight && sunclock) {
clockShadowLight.position.copy(sunLight.position);
clockShadowLight.target = sunclock; // point at sunclock
}
skyMaterial.uniforms.sunDirection.value = sunTime.normalize();
cloudsMaterial.uniforms.sunDirection.value = sunTime.normalize();
customDepthMat.uniforms.sunDirection.value = sunTime.normalize();
//skyMaterial.uniforms.moonDirection.value = moonTime.normalize();
terrainMaterial.uniforms.lightDir.value.copy(sunLight.position.normalize());
terrainMaterial.uniforms.cameraPosition.value.copy(camera.position);
if(mountainMaterial!=null) {
mountainMaterial.uniforms.cameraPosition.value.copy(camera.position);
mountainMaterial.uniforms.lightDir.value.copy(sunLight.position).normalize();
mountainMaterial.uniforms.lightDir2.value.copy(sunLight.position).normalize().negate();
}
if (sunclock) {
sunclock.traverse(child => {
if (child.isMesh && child.material && child.material.isShaderMaterial) {
child.material.uniforms.cameraPosition.value.copy(camera.position);
child.material.uniforms.lightDir.value.copy(sunLight.position).normalize();
child.material.uniforms.lightDir2.value.copy(sunLight.position).normalize().negate();
//child.material.needsUpdate = true;
}
});
}
//if(benchesMaterial!=null) {
if(benchesReady!=null) {
//benchesMaterial.uniforms.cameraPosition.value.copy(camera.position);
//benchesMaterial.uniforms.lightDir.value.copy(sunLight.position).normalize();
//benchesMaterial.uniforms.lightDir2.value.copy(sunLight.position).normalize().negate();
for(let i=0;i<benchlist.length;i++) {
if (benchlist[i]['ref_lod0']) {
benchlist[i]['ref_lod0'].traverse(child => {
if (child.isMesh && child.material && child.material.isShaderMaterial) {
child.material.uniforms.cameraPosition.value.copy(camera.position);
child.material.uniforms.lightDir.value.copy(sunLight.position).normalize();
child.material.uniforms.lightDir2.value.copy(sunLight.position).normalize().negate();
//child.material.needsUpdate = true;
}
});
}
}
}
if (twily) {
twily.traverse(child => {
if (child.isMesh && child.material && child.material.isShaderMaterial) {
child.material.uniforms.cameraPosition.value.copy(camera.position);
child.material.uniforms.lightDir.value.copy(sunLight.position).normalize();
child.material.uniforms.lightDir2.value.copy(sunLight.position).normalize().negate();
//child.material.needsUpdate = true;
}
});
}
camera.updateMatrixWorld();
skyMaterial.uniforms.lightDir.value.copy(sunLight.position.normalize());
skyMaterial.uniforms.lightDir2.value.copy(sunLight.position.normalize().negate());
cloudsMaterial.uniforms.lightDir.value.copy(sunLight.position.normalize());
cloudsMaterial.uniforms.lightDir2.value.copy(sunLight.position.normalize().negate());
clouds.updateMatrixWorld();
normalMatrix.getNormalMatrix(clouds.matrixWorld);
skyMaterial.uniforms.nMatrix.value = normalMatrix;
skyMaterial.uniforms.vMatrix.value.copy(camera.matrixWorldInverse);
cloudsMaterial.uniforms.nMatrix.value = normalMatrix;
cloudsMaterial.uniforms.vMatrix.value.copy(camera.matrixWorldInverse);
customDepthMat.uniforms.nMatrix.value = normalMatrix;
customDepthMat.uniforms.vMatrix.value.copy(camera.matrixWorldInverse);
//cloudsMaterial.uniforms.invView.value.copy(camera.matrixWorldInverse).invert();
//cloudsMaterial.uniforms.invProjection = { value: new THREE.Matrix4() };
//cloudsMaterial.uniforms.invProjection.value.copy(camera.projectionMatrix).invert();
mvpMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse).multiply(clouds.matrixWorld);
cloudsMaterial.uniforms.mvpMatrix.value = mvpMatrix;
skyMaterial.uniforms.mvpMatrix.value = mvpMatrix;
customDepthMat.uniforms.mvpMatrix.value = mvpMatrix;
// sky color calculations
var l=0; // segment selection
var t=ss; // range current
//const hr_1=(4.2*lapse/100); // percentage
const hr_1=(lapse/24); // percentage
const hr_3=hr_1*3;
const hr_4=hr_3+hr_1;
const hr_5=hr_4+hr_1;
const hr_9=hr_5+hr_4;
const hr_12=hr_9+hr_3;
const hr_13=hr_12+hr_1;
const hr_16=hr_13+hr_3;
const hr_21=hr_16+hr_5;
//console.log("ss="+ss);
//console.log("hr_1="+hr_1+" ss>hr_1?:"+((ss>hr_1)?"true":"false"));
//console.log("hr_="+hr_3+" ss>hr_3?:"+((ss>hr_3)?"true":"false"));
//console.log("hr_="+hr_4+" ss>hr_4?:"+((ss>hr_4)?"true":"false"));
//console.log("hr_="+hr_5+" ss>hr_5?:"+((ss>hr_5)?"true":"false"));
//console.log("hr_="+hr_9+" ss>hr_9?:"+((ss>hr_9)?"true":"false"));
//console.log("hr_="+hr_12+" ss>hr_12?:"+((ss>hr_12)?"true":"false"));
//console.log("hr_="+hr_13+" ss>hr_13?:"+((ss>hr_13)?"true":"false"));
//console.log("hr_="+hr_16+" ss>hr_16?:"+((ss>hr_16)?"true":"false"));
//console.log("hr_="+hr_21+" ss>hr_21?:"+((ss>hr_21)?"true":"false"));
//ss-=(10*lapse/100);
//if(ss<0) ss+=lapse; // rollover
var m=hr_1; // range max
if(ss>hr_21) { // night end 3
l=7;
t=ss-hr_21;
m=hr_3;
} else if(ss>hr_16) { // midnight 5
l=6;
t=ss-hr_16;
m=hr_5;
} else if(ss>hr_13) { // night begin 3
l=5;
t=ss-hr_13;
m=hr_3;
} else if(ss>hr_12) { // evening 1
l=4;
t=ss-hr_12;
m=hr_1;
} else if(ss>hr_9) { // day end 3
l=3;
t=ss-hr_9;
m=hr_3;
} else if(ss>hr_4) { // midday 5
l=2;
t=ss-hr_4;
m=hr_5;
} else if(ss>hr_1) { // day begin 3
l=1;
t=ss-hr_1;
m=hr_3;
}
var l2=l+1;
//if(l2>3) l2=0;
if(l2>7) l2=0;
//console.log("l="+l+" l2="+l2+" t="+t+" m="+m);
tmpColor['r1']=skyColors['topColor'][l][0];
tmpColor['g1']=skyColors['topColor'][l][1];
tmpColor['b1']=skyColors['topColor'][l][2];
tmpColor['r2']=skyColors['topColor'][l2][0];
tmpColor['g2']=skyColors['topColor'][l2][1];
tmpColor['b2']=skyColors['topColor'][l2][2];
tmpColor['r3']=skyColors['bottomColor'][l][0];
tmpColor['g3']=skyColors['bottomColor'][l][1];
tmpColor['b3']=skyColors['bottomColor'][l][2];
tmpColor['r4']=skyColors['bottomColor'][l2][0];
tmpColor['g4']=skyColors['bottomColor'][l2][1];
tmpColor['b4']=skyColors['bottomColor'][l2][2];
let p = Math.min(t / m, 1); // progress
//const eP=p; // linear
const eP = easeInOut(p); // easedProgress
//const currentValue = lerp(startValue, endValue, eP);
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);
skyMaterial.uniforms.topColor.value=new THREE.Color(tmpColor['rlt']/255,tmpColor['glt']/255,tmpColor['blt']/255);
skyMaterial.uniforms.bottomColor.value=new THREE.Color(tmpColor['rlb']/255,tmpColor['glb']/255,tmpColor['blb']/255);
// sun and sky calculations end
const milliseconds=ss+(dd.getMilliseconds()/1000.0);
skyMaterial.uniforms.xtime.value=milliseconds;
cloudsMaterial.uniforms.xtime.value=milliseconds;
customDepthMat.uniforms.xtime.value=milliseconds;
northernMaterial.uniforms.time.value = milliseconds/50.0;
//skyMaterial.uniforms.daytime.value=Math.abs((ss/(86400.0*.5))-1.0);
skyMaterial.uniforms.daytime.value=daytime;
cloudsMaterial.uniforms.daytime.value=daytime;
customDepthMat.uniforms.daytime.value=daytime;
terrainMaterial.uniforms.daytime.value=daytime;
if(mountainMaterial!=null) {
mountainMaterial.uniforms.daytime.value=daytime;
}
if (sunclock) {
sunclock.traverse(child => {
if (child.isMesh && child.material && child.material.isShaderMaterial) {
child.material.uniforms.daytime.value=daytime;
//child.material.needsUpdate = true;
}
});
}
//if(benchesMaterial!=null) {
if(benchesReady!=null) {
//benchesMaterial.uniforms.daytime.value=daytime;
for(let i=0;i<benchlist.length;i++) {
if (benchlist[i]['ref_lod0']) {
benchlist[i]['ref_lod0'].traverse(child => {
if (child.isMesh && child.material && child.material.isShaderMaterial) {
child.material.uniforms.daytime.value=daytime;
//child.material.needsUpdate = true;
}
});
}
}
}
if (twily) {
twily.traverse(child => {
if (child.isMesh && child.material && child.material.isShaderMaterial) {
child.material.uniforms.daytime.value=daytime;
//child.material.needsUpdate = true;
}
});
if (twilyAnim.mixer) {
twilyAnim.mixer.update(delta);
}
}
// UPDATE ALL ACTIVE FIREWORKS
for (let i = activeFireworks.length - 1; i >= 0; i--) {
const fw = activeFireworks[i];
const tile = activeFireworks[i]['tileRef'];
// 1. launch keyframe animation
fw.mixer.update(delta);
// 2. sprite sheet animation
updateFireworkUV(fw, globalTime);
//--// 3. fade out + destroy after ~15 minutes (900 seconds)
const age = globalTime - fw.startTime;
const action = fw.mixer._actions[0]; // Assume first action=launch
if (action && action.time >= action.getClip().duration) { // Finished
// Start fade or destroy
if (age > action.getClip().duration + 0) { // Extra 5s linger
//scene.remove(fw.instance);
if(tile) {
tile.remove(fw.instance);
}
fw.instance.traverse((child) => {
if (child.isMesh) {
if (child.geometry) child.geometry.dispose();
if (child.material) {
if (Array.isArray(child.material)) {
child.material.forEach(mat => mat.dispose());
} else {
child.material.dispose();
}
}
}
});
if (fw.mixer) fw.mixer.uncacheRoot(fw.instance); // Clean mixer
activeFireworks.splice(i, 1);
} else if (age > fw.totalDuration - 4) { // Start fade 4 seconds before end (tune 4→6 for slower)
const fadeTime = fw.totalDuration - age; // Remaining seconds
const fade = fadeTime / 4; // 1 → 0 over 4 sec
// Slow opacity ramp + freeze UV on last frame (soft texture fade)
fw.instance.traverse((child) => {
if (child.isMesh && child.material?.uniforms?.u_opacity) {
//child.material.uniforms.u_opacity.value = fade; // Linear fade
child.material.uniforms.u_opacity.value = Math.pow(fade, 0.5);
// Optional ease-out: Math.pow(fade, 0.5) for slower start
}
});
} else {
const fade = 1 - (age - action.getClip().duration) / 5;
fw.instance.traverse(child => {
if (child.material) child.material.uniforms.u_opacity.value = fade;
});
}
}
}
if(waitNorthernLight>=nextNorthernLight) {
const nlfs=northernLightFadeState;
if(daytime<0.42) {
northernLightFadeState = rndMinMax(0,1);
} else {
northernLightFadeState = 0; // keep off daytime
}
if(northernLightAlwaysOn) {
northernLightFadeState = 1; // alwayson
}
if(northernLightFadeState==1 && nlfs==0) {
newNorthernPosition();
}
nextNorthernLight = rndMinMax(60,300); // sec
waitNorthernLight = 0;
} else {
waitNorthernLight+=dt;
}
if(northernLightFadeState==1 && northernLightOpa<100.0) {
northernLightOpa+=1.0 * dt;
if(northernLightOpa>100.0) northernLightOpa=100.0;
northernMaterial.uniforms.fadeOpa.value=northernLightOpa*.01;
//console.log("northernLight fade in opa="+northernLightOpa);
} else if(northernLightFadeState==0 && northernLightOpa>0.0) {
northernLightOpa-=1.0 * dt;
if(northernLightOpa<0.0) northernLightOpa=0.0;
northernMaterial.uniforms.fadeOpa.value=northernLightOpa*.01;
//console.log("northernLight fade out opa="+northernLightOpa);
}
controls.update();
//renderer.render(scene, camera);
// Force shadow map update BEFORE multi-pass, with all layers enabled
shadowCamera.copy(camera); // sync position/rotation
//scene.updateMatrixWorld(); // Ensure positions are up-to-date
// Temporarily disable expensive features for dummy render
//const oldFog = scene.fog;
///scene.fog = null; // disable fog
//const oldEnv = scene.environment;
//scene.environment = null; // disable env map if any
renderer.shadowMap.autoUpdate = true; // Temporarily enable for correct timing
renderer.shadowMap.needsUpdate = true; // Force if needed
updateShadowCameras();
// Hack: Render with a tiny viewport to minimize cost (shadow map is internal)
const oldViewport = renderer.getViewport(new THREE.Vector4());
renderer.setViewport(0, 0, 1, 1); // tiny 1x1 pixel
renderer.render(scene, shadowCamera); // dummy render to trigger shadow map
renderer.setViewport(oldViewport); // restore
// Restore
//scene.fog = oldFog;
//scene.environment = oldEnv;
renderer.shadowMap.autoUpdate = false;
renderer.clear(true, true, true);
// Pass 1: Sky only (Layer 1) - writes depth to clip distant terrain
camera.layers.set(1); // only render objects on layer 1
renderer.render(scene, camera);
// Pass 2: Northern lights (Layer 2) - no depth test/write, blends into sky
camera.layers.set(2); // only render layer 2
northernMaterial.depthTest = false;
northernMaterial.depthWrite = false;
renderer.render(scene, camera);
// Pass 0: Default objects, not used currently
camera.layers.set(0); // only render layer 0
renderer.render(scene, camera);
// Pass 3: Terrain + mountains (Layer 0) - normal depth to occlude lights
camera.layers.set(3); // only render layer 3
renderer.render(scene, camera);
// Pass 4: Fireworks~near transparents, 2 is far transparents~northern lights
camera.layers.set(4); // only render layer 4
renderer.render(scene, camera);
// Pass 5: UI/Front of all, sunclock etc
camera.layers.set(5); // only render layer 0
renderer.render(scene, camera);
// Reset camera layers for next frame (important!)
camera.layers.enableAll(); // or set back to default (usually 1)
}
animate(0);
var wireframeMode=false;
var cloudQuality=3; // start simple2
//var cloudQuality=2; // start off
var wrT;
function window_resize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// Resize handler
window.addEventListener('resize', () => {
clearTimeout(wrT);
wrT=setTimeout(function() { window_resize(); },100);
});
function extCloudOff() {
cloudQuality=2; // off
skyMaterial.uniforms.cloudsOn.value=0;
clouds.visible=false;
// clouds toggled off by parent, update parent with time of day for sunlight?
}
//parent.postMessage(['new_tim',timeString,testEarthAngle,testTheta],'*');
if(window.self!==window.parent) {
window.addEventListener('message', (event) => {
//if (event.origin === 'https://links.analiestar.com') {
console.log('Message from parent:', event.data[0]);
switch(event.data[0]) {
case "cloudsoff":
//extCloudOff(); // not active since not using ray clouds
sunclockVisible=false;
if(sunclock) {
sunclock.visible=false; // for use in twily.info/mp3 as scene
}
benchesVisible=false;
for(let i=0;i<benchlist.length;i++) {
if (benchlist[i]['ref_lod0']) {
benchlist[i]['ref_lod0'].traverse(child => {
child.visible=benchesVisible;
});
}
}
twilyVisible=false;
if(twily) {
twily.visible=false; // for use in twily.info/mp3 as scene
}
//resetCamera(camera,controls); // increase to pos 1 default mp3
break;
default:
}
//}
});
parent.postMessage(['cloudcheck',''],'*');
}
const canvas = renderer.domElement;
// Function to hide the cursor
function hideCursor() {
canvas.style.cursor = 'none';
// Prevent default behavior like text selection which interferes with drag
event.preventDefault();
}
function showCursor() {
// You can set it to default, auto, or any other cursor style you prefer
canvas.style.cursor = 'auto';
}
canvas.addEventListener('mousedown', hideCursor);
document.addEventListener('mousedown', hideCursor);
// Use 'mouseup' on the window/document to ensure the cursor reappears
// even if the mouse button is released outside the canvas area
document.addEventListener('mouseup', showCursor);
/* // alternative PointerLockControls library FPS intended, esc to 'unlock'
document.addEventListener( 'click', function () {
controls.lock();
}, false );
// The cursor is automatically hidden when the pointer is locked.
// You can listen for the 'unlock' event to perform other UI actions.
controls.addEventListener( 'unlock', function () {
console.log( 'Pointer unlocked, cursor visible.' );
});*/
const pressedKeys = new Set;
let cancelKey=false;
let cKT;
document.addEventListener('keydown', (e) => {
clearTimeout(cKT);
if (e.key === 'Control' || e.key=="Shift" || e.key=="Alt") { //
cancelKey=true;
//console.log("key cancelled1 "+e.key);
return;
}
//console.log("key add "+e.key);
pressedKeys.add(e.key);
//if (e.key === 'Shift') {
// controls.screenSpacePanning = false;
//}
});
document.addEventListener('keyup', (e) => {
pressedKeys.delete(e.key);
if(cancelKey) {
cancelKey=false;
//console.log("key cancelled2 "+event.key);
return;
}
if (event.key === 'w') { // Toggle with 'w' key
wireframeMode = !wireframeMode;
terrainMaterial.wireframe = wireframeMode;
skyMaterial.wireframe = wireframeMode;
cloudsMaterial.wireframe = wireframeMode;
northernMaterial.wireframe = wireframeMode;
if(mountainMaterial!=null) {
mountainMaterial.wireframe = wireframeMode;
}
if (sunclock) {
sunclock.traverse(child => {
if (child.isMesh && child.material && child.material.isShaderMaterial) {
child.material.wireframe = wireframeMode;
//child.material.needsUpdate = true;
}
});
}
//if(benchesMaterial!=null) {
if(benchesReady!=null) {
//benchesMaterial.wireframe = wireframeMode;
for(let i=0;i<benchlist.length;i++) {
if (benchlist[i]['ref_lod0']) {
benchlist[i]['ref_lod0'].traverse(child => {
if (child.isMesh && child.material && child.material.isShaderMaterial) {
child.material.wireframe = wireframeMode;
//child.material.needsUpdate = true;
}
});
}
}
}
if (twily) {
twily.traverse(child => {
if (child.isMesh && child.material && child.material.isShaderMaterial) {
child.material.wireframe = wireframeMode;
//child.material.needsUpdate = true;
}
});
}
if(wireframeMode) {
northernMaterial.uniforms.debug.value=1;
skyGroup.visible=false;
pushNotice("'w': toggle wireframe mode on");
} else {
northernMaterial.uniforms.debug.value=0;
skyGroup.visible=true;
pushNotice("'w': toggle wireframe mode off");
}
}
if (event.key === 's') { // shadow on - debug dither - debug clean - off
//shadowOn = (shadowOn==1)?0:1;
shadowOn++;
if(shadowOn>3) shadowOn=0;
terrainMaterial.uniforms.shadowOn.value = shadowOn;
if(mountainMaterial!=null) {
mountainMaterial.uniforms.shadowOn.value = shadowOn;
}
if (sunclock) {
sunclock.traverse(child => {
if (child.isMesh && child.material && child.material.isShaderMaterial) {
child.material.uniforms.shadowOn.value = shadowOn;
child.material.needsUpdate = true;
}
});
}
//if(benchesMaterial!=null) {
if(benchesReady!=null) {
//benchesMaterial.uniforms.shadowOn.value = shadowOn;
//benchesMaterial.needsUpdate = true;
for(let i=0;i<benchlist.length;i++) {
if (benchlist[i]['ref_lod0']) {
benchlist[i]['ref_lod0'].traverse(child => {
if (child.isMesh && child.material && child.material.isShaderMaterial) {
child.material.uniforms.shadowOn.value = shadowOn;
child.material.needsUpdate = true;
}
});
}
}
}
if (twily) {
twily.traverse(child => {
if (child.isMesh && child.material && child.material.isShaderMaterial) {
child.material.uniforms.shadowOn.value = shadowOn;
child.material.needsUpdate = true;
}
});
}
if(shadowOn==0) {
pushNotice("'s': toggle shadow mountain/cloud off");
} else if(shadowOn==1) {
pushNotice("'s': toggle shadow all on");
} else if(shadowOn==2) {
pushNotice("'s': toggle shadow + dither map");
} else {
pushNotice("'s': toggle shadow clean map");
}
}
if (event.key === 'd') { // depthwrite off - on
skyDepthOn=!skyDepthOn;
skyMaterial.depthWrite=skyDepthOn;
if(skyDepthOn) {
pushNotice("'d': toggle depthWrite on sky on");
} else {
pushNotice("'d': toggle depthWrite on sky off");
}
}
if (event.key === 't') { // testval debug toggle on - off
//--testValOn=!testValOn;
if(testValOn && testValAuto) { // true true
testValAuto=false;
pushNotice("'t': toggle testVal on with fixed");
} else if(testValOn && !testValAuto) { // true false
testValOn=false;
testValAuto=true;
pushNotice("'t': toggle testVal off (realtime)");
} else { // false true ?
testValOn=true;
pushNotice("'t': toggle testVal on with auto on");
}
}
if (event.key === 'c') { // cloud high - low - off
cloudQuality++;
if(cloudQuality>4) cloudQuality=0;
clouds.visible = false;
sky.castShadow=false;
customDepthMat.uniforms.cloudsOn.value=0;
if(cloudQuality==0) { // high
skyMaterial.uniforms.cloudsOn.value=1;
skyMaterial.uniforms.marchSize.value=40.0;
skyMaterial.uniforms.maxSteps.value=6;
pushNotice("'c': toggle cloud raymarched high");
} else if(cloudQuality==1) { // low
skyMaterial.uniforms.cloudsOn.value=1;
skyMaterial.uniforms.marchSize.value=80.0;
skyMaterial.uniforms.maxSteps.value=2;
pushNotice("'c': toggle cloud raymarched low");
} else if(cloudQuality==2) { // all off [default]
skyMaterial.uniforms.cloudsOn.value=0;
pushNotice("'c': toggle cloud all off");
} else if(cloudQuality==3) { // 2d / simple2 [default] - clouds2 now built into skymaterial (has customdepthmat)
skyMaterial.uniforms.cloudsOn.value=2;
customDepthMat.uniforms.cloudsOn.value=2;
sky.castShadow=true;
pushNotice("'c': toggle cloud simple2 [default]");
} else if(cloudQuality==4) { // 2d / simple1 (separate sphere)
skyMaterial.uniforms.cloudsOn.value=0;
clouds.visible = true;
cloudsMaterial.vertexShader=vertexShaderClouds;
cloudsMaterial.fragmentShader=fragmentShaderClouds;
//--cloudsMaterial.vertexShader=vertexShaderClouds2;
//--cloudsMaterial.fragmentShader=fragmentShaderClouds2;
//cloudsMaterial.vertexShader=vertexShaderSky;
//cloudsMaterial.fragmentShader=fragmentShaderShadow; // debug
//clouds.customDepthMaterial = customDepthMat;
cloudsMaterial.needsUpdate=true;
pushNotice("'c': toggle cloud simple1");
}
//skyMaterial.needsUpdate=true;
}
if (event.key === 'v') { // vortex on/off (dead effect)
vortexOn=!vortexOn;
if(vortexOn) {
skyMaterial.uniforms.vortexOn.value = 1;
customDepthMat.uniforms.vortexOn.value = 1;
effectGroup.visible=false;
fogDensity=0.001;
pushNotice("'v': toggle vortex on (dead)");
} else {
skyMaterial.uniforms.vortexOn.value = 0;
customDepthMat.uniforms.vortexOn.value = 0;
effectGroup.visible=true;
fogDensity=0.0005;
pushNotice("'v': toggle vortex off (alive)");
}
terrainMaterial.uniforms.fogDensity.value = fogDensity;
if(mountainMaterial) {
mountainMaterial.uniforms.fogDensity.value = fogDensity;
}
if (sunclock) {
sunclock.traverse(child => {
if (child.isMesh && child.material && child.material.isShaderMaterial) {
child.material.uniforms.fogDensity.value = fogDensity;
child.material.needsUpdate = true;
}
});
}
for(let i=0;i<benchlist.length;i++) {
if (benchlist[i]['ref_lod0']) {
benchlist[i]['ref_lod0'].traverse(child => {
if (child.isMesh && child.material && child.material.isShaderMaterial) {
child.material.uniforms.fogDensity.value = fogDensity;
child.material.needsUpdate = true;
}
});
}
}
if (twily) {
twily.traverse(child => {
if (child.isMesh && child.material && child.material.isShaderMaterial) {
child.material.uniforms.fogDensity.value = fogDensity;
child.material.needsUpdate = true;
}
});
}
}
if (event.key === 'r') { // trig northern light pos change
newNorthernPosition();
pushNotice("'r': repositioned northern lights");
}
if (event.key === 'n') { // trig northern light pos change
northernLightAlwaysOn=!northernLightAlwaysOn;
waitNorthernLight=nextNorthernLight;
if(northernLightAlwaysOn) {
pushNotice("'n': toggle northern light alwayson true");
} else {
pushNotice("'n': toggle northern light alwayson false");
}
}
if (event.key === 'h') { // trig helpers camera shadow
if(helperLoaded) {
unloadHelpers();
pushNotice("'h': toggle shadow helpers off");
} else {
loadHelpers();
pushNotice("'h': toggle shadow helpers on");
}
}
if (event.key === 'u') { // trig sundial hide
sunclockVisible=!sunclockVisible;
if(!sunclockVisible) {
sunclock.visible=false;
pushNotice("'u': toggle sundial visible false");
} else {
sunclock.visible=true;
pushNotice("'u': toggle sundial visible true");
}
}
if (event.key === 'b') { // trig bench hide
benchesVisible=!benchesVisible;
for(let i=0;i<benchlist.length;i++) {
if (benchlist[i]['ref_lod0']) {
benchlist[i]['ref_lod0'].traverse(child => {
child.visible=benchesVisible;
});
}
}
if(!benchesVisible) {
pushNotice("'b': toggle benches visible false");
} else {
pushNotice("'b': toggle benches visible true");
}
}
if (event.key === 'm') { // trig twily hide
twilyVisible=!twilyVisible;
if(!twilyVisible) {
twily.visible=false;
pushNotice("'m': toggle twily visible false");
} else {
twily.visible=true;
pushNotice("'m': toggle twily visible true");
}
}
if (event.key === 'k') { // trig keybinds list
keybindsVisible=!keybindsVisible;
if(keybindsVisible) {
//$('keysWrap').style.visibility="visible";
$('keysWrap').style.display="flex";
pushNotice("'k': toggle keybinds visible true");
} else {
//$('keysWrap').style.visibility="hidden";
$('keysWrap').style.display="none";
pushNotice("'k': toggle keybinds visible false");
}
}
if (event.key === 'p') { // toggle camera position
resetCamera(camera,controls);
pushNotice("'p': toggle reset camera pos "+cameraSet);
}
if (event.code === 'Space') { //
terrainMove=!terrainMove;
if(terrainMove) {
pushNotice("'Space': toggle terrainMove moving");
} else {
pushNotice("'Space': toggle terrainMove stopped");
}
}
//alert(event.code+" - "+event.key);
pressedKeys.clear();
});
document.addEventListener("visibilitychange", function() {
if (document.visibilityState === "visible") {
//console.log("Tab is active (visible) again!");
// Your code to run when the tab becomes active
cancelKey=true;
cKT=setTimeout(function() { cancelKey=false; },500);
} else {
//console.log("Tab is inactive (hidden)");
// Your code to run when the tab becomes inactive
}
});
var keybindsVisible=false;
var keybinds=[
{k:"w", d:"*wireframe* toggle on/off {sky,terrain,mountains}"},
{k:"s", d:"*shadows* toggle on,debugdither,debugclean,off"},
{k:"h", d:"shadow camera *helpers* toggle on/off"},
{k:"c", d:"*clouds* toggle 2d2(default),2d1,ray[heavy],ray[medium],off"},
{k:"d", d:"*depthwrite* toggle skybox on/off"},
{k:"t", d:"*time* toggle realtime[run],testval[run],testval[pause]"},
{k:"v", d:"*vortex* toggle (dead) on/off"},
{k:"n", d:"*nothern lights* toggle alwayson true/false"},
{k:"r", d:"northern light *re-position*/orient"},
{k:"u", d:"hide *sundial* toggle visible/hidden"},
{k:"b", d:"hide *benches* toggle visible/hidden"},
{k:"m", d:"hide *twily* toggle visible/hidden"},
{k:"p", d:"cycle camera *position* {0,1,2,3,4,5}"},
{k:"k", d:"show *keybinds* toggle visible/hidden"},
{k:"space", d:"terrain toggle *moving* on/off"},
];
function populateKeybinds() {
$('keys').innerHTML="";
var html="";
html+="<div class=\"tbl\" style=\"width: auto; table-layout: fixed;\">";
html+="<div class=\"tr\">";
html+="<div class=\"td\" style=\"width: 55px;\">";
html+="Key:";
html+="</div>";
html+="<div class=\"td\">";
html+="Description:";
html+="</div>";
html+="</div>\n";
for(var i=0;i<keybinds.length;i++) {
var k=keybinds[i]['k'];
var d=keybinds[i]['d'];
// Regular expression explanation:
// /\*(.*?)\*/g
// /.../g -> Defines the start and end of the regex, 'g' ensures all occurrences are replaced.
// \* -> Matches a literal asterisk character (asterisk needs to be escaped with a backslash).
// (.*?) -> This is a capture group (the content we want to keep).
// . -> Matches any character.
// *? -> Matches zero or more times, but non-greedily (stops at the first closing asterisk).
d = d.replace(/\*(.*?)\*/g, "<strong class=\"pink\">$1</strong>");
html+="<div class=\"tr\">";
html+="<div class=\"td yellow\">";
html+="<span class=\"keypad\">";
html+=k;
html+="</span>";
html+="</div>";
html+="<div class=\"td grey\">";
html+=d;
html+="</div>";
html+="</div>\n";
//$('keys').innerHTML+=keybinds[i]['k'];
}
html+="</div>";
html+="</div>";
html+="</div>\n";
$('keys').insertAdjacentHTML('beforeend',html);
}
populateKeybinds();
var anaS=`%c
,/#(,
./%%%%(,
.*#%%%%#,
,/#%%%%%%*
./%%%%%%%%%%*
*%%%%%%%%%%%%/.
,#%%%%%%%%%%%%(.
./%%%%#*/%%%%%%/. /%(,*(
./#%%%%/. ./%%#/,,%%%%%%%%%%%/
*(%%##%%%%%%%%%%%%%%(,./%%%%%%#*.
*#%%#* ../%%%%#/. ./%%%%/.
.*(%%#*. ./%%%%#, *%%%%(,
.*#%%###%%%%%%#/, .*%%%%#, .*%%%%#, *
./#%%/.*(%%(*. *%%%%%%%%/.,#%%%%* *
.(%%%%%%%%%%/, ,*//*, ,
`
function printascii() { setTimeout(console.log.bind(console,anaS,'background: #2a1e27; color: #fbbb57;')); }
printascii();
</script>
</body>
</html>
Top