<!DOCTYPE html>
<!--
Author: Twily 2025-2026
Landscape/skybox webgl demo
Keybinds:
w wireframe toggle on/off {sky,terrain,mountains}
s shadows toggle on,debugdither,debugclean,off
c clouds toggle 2d2(default),2d1,ray[heavy],ray[medium],off
d depthwrite toggle skybox on/off
t time toggle realtime[run],testval[run],testval[pause]
space toggle terrain moving on/off
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Three.js Infinite Landscape</title>
<style>
html,body { width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; user-select: none; background: #344C6C; color: #ccc; }
canvas { display: block; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 100; }
#notices {
position: absolute;
top: 10px;
left: 10px;
color: yellow;
font-family: monospace;
font-size: 12px;
pointer-events: none;
z-index: 1001;
display: flex;
flex-flow: column;
}
#notices > span {
display: inline-block;
background: rgba(0, 0, 0, 0.5);
padding: 10px;
}
#info {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.5);
color: white;
padding: 10px;
font-family: monospace;
font-size: 12px;
pointer-events: none;
z-index: 1000;
}
#loading {
position: fixed; top: 0; left: 0;
width: 100%; height: 100%;
background: transparent;
text-shadow: 0 0 6px #000;
color: #fff;
font-size: 22pt;
text-align: center;
opacity: 1;
transition: opacity 1s ease;
z-index: 1001;
}
#loadtxt {
//width: 500px; text-align: left;
display: inline-block;
}
.tbl { display: table; width: 100%; height: 100%; table-layout: fixed; }
.tr { display: table-row; }
.td { display: table-cell; vertical-align: middle;}
</style>
</head>
<body>
<div id="loading"><div class="tbl"><div class="tr"><div class="td"><span id="loadtxt">Loading...</span></div></div></div></div>
<div id="notices"></div>
<div id="info"></div>
<script type="importmap">
{
"imports": {
"three": "./build/three.module.js",
"three/addons/": "./jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
//import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'; // replaces orbitcontrols
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const tileSize = 4000; // Widened to hide edges easier
const segments = 512; // Increased for better detail/resolution
const heightScale = 50;
const noiseScale = 100;
const speed = 50; // Flying speed
const numTiles = 5; // Keep this many tiles visible
const vertexShaderSky = `
uniform mat4 mvpMatrix;
varying vec3 vWorldPosition;
varying vec3 vLocalPosition;
//varying vec2 vNormal;
varying vec2 vUv;
varying vec3 ourNormal;
void main() {
//ourNormal=vNormal;
ourNormal=normal;
vLocalPosition = position;
//uv = vec2(vNormal.x, vNormal.z);
vUv = uv * 1.0;
vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
//gl_Position = mvpMatrix * vec4(position, 1.0);
}
`;
const fragmentShaderSky = `
uniform vec3 topColor;
uniform vec3 bottomColor;
uniform vec3 moonColor;
uniform vec3 sunColor;
uniform vec3 sunDirection;
//uniform vec3 moonDirection;
uniform float sunSize;
uniform float moonSize;
varying vec3 vWorldPosition;
varying vec3 vLocalPosition;
varying vec3 ourNormal;
varying vec2 vUv;
uniform float xtime; // seconds~day
uniform float daytime; // 1 0 1
uniform vec3 lightDir;
uniform vec3 lightDir2;
uniform int cloudsReady;
uniform float cloudDensity;
uniform float cloudAbsorption;
uniform float cloudScale;
uniform float cloudSpeed;
uniform float cloudBottom;
uniform float cloudThickness;
uniform float scatteringAniso;
uniform int maxSteps;
uniform float marchSize;
uniform int lightSteps;
uniform float frame;
uniform int cloudsOn;
uniform int starsOn;
uniform float cDensity;
uniform vec3 horizonGlowColorDay;
uniform vec3 horizonGlowColorNight;
uniform float horizonGlowIntensity;
uniform float horizonGlowHeight;
uniform float horizonGlowSharpness;
uniform float horizonNoiseScale;
uniform float horizonNoiseStrength;
uniform int horizonGlowOn;
const float PI = 3.1415926535897932384626433832795;
// random2 noise2 fbm2 for clouds2 builtin~ simple clouds 2
// Noise functions
float random2(vec3 p) {
return fract(sin(dot(p, vec3(12.9898, 78.233, 45.5432))) * 43758.5453123);
}
float noise2(vec3 p) {
vec3 i = floor(p);
vec3 f = fract(p);
vec3 u = f * f * (3.0 - 2.0 * f);
return mix(mix(mix(random2(i), random2(i + vec3(1.0, 0.0, 0.0)), u.x),
mix(random2(i + vec3(0.0, 1.0, 0.0)), random2(i + vec3(1.0, 1.0, 0.0)), u.x), u.y),
mix(mix(random2(i + vec3(0.0, 0.0, 1.0)), random2(i + vec3(1.0, 0.0, 1.0)), u.x),
mix(random2(i + vec3(0.0, 1.0, 1.0)), random2(i + vec3(1.0, 1.0, 1.0)), u.x), u.y), u.z);
}
float fbm2(vec3 p) {
float v = 0.0;
float a = 0.5;
vec3 shift = vec3(100.0);
for (int i = 0; i < 2; ++i) { // default 6
v += a * noise2(p);
p = p * 2.0 + shift;
a *= 0.2; // gain ( https://thebookofshaders.com/13/ )
}
return v;
}
// Pseudo-random hash function based on 2D coordinates
float hash(vec2 p) {
return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}
// Generate four random values per grid cell
vec4 hash42(ivec2 p) {
vec2 p2 = vec2(p);
return vec4(
hash(p2),
hash(p2 + vec2(1.0, 0.0)),
hash(p2 + vec2(0.0, 1.0)),
hash(p2 + vec2(1.0, 1.0))
);
}
// indent removed for section raymarched clouds
// standard GLSL 3D Perlin for clouds
float mod289(float x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 perm(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }
float noise3D(vec3 p) {
vec3 a = floor(p);
vec3 d = p - a;
d = d * d * (3.0 - 2.0 * d);
vec4 b = a.xxyy + vec4(0.0, 1.0, 0.0, 1.0);
vec4 k1 = perm(b.xyxy);
vec4 k2 = perm(k1.xyxy + b.zzww);
vec4 c = k2 + a.zzzz;
vec4 k3 = perm(c);
vec4 k4 = perm(c + 1.0);
vec4 o1 = fract(k3 * (1.0 / 41.0));
vec4 o2 = fract(k4 * (1.0 / 41.0));
vec4 o3 = o2 * d.z + o1 * (1.0 - d.z);
vec2 o4 = o3.yw * d.x + o3.xz * (1.0 - d.x);
return o4.y * d.y + o4.x * (1.0 - d.y);
}
float fbm(vec3 p) {
float f = 0.0;
float amp = 0.5;
float freq = 1.0;
mat3 rot = mat3(0.36, 0.93, 0.0, -0.93, 0.36, 0.0, 0.0, 0.0, 1.0); // Rotation to break artifacts
for (int i = 0; i < 5; i++) {
f += amp * noise3D(p * freq);
p *= rot * 2.01;
amp *= 0.45; // Slightly less persistence for fluff
freq *= 2.0;
}
return f;
}
// Scattering and absorption
float hg(float g, float mu) {
float gg = g * g;
return (1.0 - gg) / (4.0 * PI * pow(1.0 + gg - 2.0 * g * mu, 1.5));
}
float beers(float dist, float abs) {
return exp(-dist * abs);
}
// Cloud march (returns scattered light and transmittance for blending)
struct CloudResult {
vec3 scattered;
float transmittance;
};
// Updated raymarchClouds
CloudResult raymarchClouds(vec3 ro, vec3 rd) {
vec3 scattered = vec3(0.0);
float transmittance = 1.0;
float depth = 0.0;
vec2 frag = gl_FragCoord.xy + frame;
float dither = (hash(frag.xy) - 0.5) * marchSize * 0.5;
if (rd.y <= 0.0) return CloudResult(vec3(0.0), 1.0); // Skip below horizon
float cloudTop = cloudBottom + cloudThickness;
float tBottom = (cloudBottom - ro.y) / rd.y;
float tTop = (cloudTop - ro.y) / rd.y;
float tStart = max(0.0, min(tBottom, tTop));
float tEnd = min(max(tBottom, tTop), 10000.0); // Cap max dist for perf
if (tStart >= tEnd) return CloudResult(vec3(0.0), 1.0);
depth = tStart + dither;
int i = 0;
while (i < maxSteps && depth < tEnd && transmittance > 0.01) {
vec3 p = ro + depth * rd;
vec3 noisePos = p * cloudScale + vec3(xtime * cloudSpeed * 0.3, 0.0, xtime * cloudSpeed);
float heightFrac = (p.y - cloudBottom) / cloudThickness;
float baseDensity = smoothstep(0.0, 0.1, heightFrac) * smoothstep(1.0, 0.6, heightFrac) * cloudDensity; // Softer fade, puffier top
float noise = fbm(noisePos);
float detail = fbm(noisePos * 4.0); // High-freq for erosion
float density = max(0.0, baseDensity * (noise * 1.2 - 0.2) - detail * 0.3); // Gaps + fluff
if (density > 0.001) {
// Light march (short)
float lightDepth = 0.0;
float lightTrans = 1.0;
vec3 lightDir = sunDirection;
int j = 0;
while (j < lightSteps) {
vec3 lp = p + lightDepth * lightDir;
float lheightFrac = (lp.y - cloudBottom) / cloudThickness;
float lbase = smoothstep(0.0, 0.1, lheightFrac) * smoothstep(1.0, 0.6, lheightFrac);
float lnoise = fbm(lp * cloudScale + vec3(xtime * cloudSpeed * 0.3, 0.0, xtime * cloudSpeed));
float ldetail = fbm(lp * cloudScale * 4.0 + vec3(xtime * cloudSpeed * 0.3, 0.0, xtime * cloudSpeed));
float ldens = max(0.0, lbase * (lnoise * 1.2 - 0.2) - ldetail * 0.3);
lightTrans *= beers(marchSize, ldens * cloudAbsorption);
lightDepth += marchSize;
if (lightTrans < 0.01) break;
j++;
}
float su = dot(rd, sunDirection);
float mu = dot(rd, -sunDirection);
float phasem = hg(scatteringAniso, su) + hg(-0.2, mu) * 0.1; // Multi-lobe for soft scatter
float phases = hg(scatteringAniso, mu) + hg(-0.2, mu) * 0.3; // Multi-lobe for soft scatter
float ds = marchSize; // Step size for integral
float powder = 1.0 - exp(-2.0 * density * ds * cloudAbsorption); // Silver lining
float luminance1 = density * phases * lightTrans * powder;
float luminance2 = density * phasem * lightTrans * powder;
vec3 cloudTint = mix(bottomColor, topColor, heightFrac * 0.8 + 0.2); // Subtle time-of-day blend
scattered += ds * luminance1 * sunColor * cloudTint * transmittance; // Add ds, no arbitrary boost
scattered += ds * luminance2 * moonColor * cloudTint * transmittance; // Add ds, no arbitrary boost
transmittance *= beers(ds, density * cloudAbsorption);
}
depth += marchSize;
i++;
}
return CloudResult(scattered, transmittance);
} // end ray clouds
//--float getDaytimeFactor(float daytime) {
//-- float t;
//--
//-- if (daytime <= 0.5) {
//-- // Stay very low most of the time → only rise quickly near 0.5
//-- float u = daytime / 0.5;
//-- t = pow(u, 5.0); // try 4.0 → 7.0
//-- } else {
//-- // Fast initial brightening after noon → then slow creep to full sun
//-- float u = (daytime - 0.5) / 0.5;
//-- t = 1.0 - pow(1.0 - u, 0.25); // 0.2–0.4 feels nice
//-- }
//--
//-- return t;
//--}
float getDaytimeFactor(float daytime) {
float t;
float mid = 0.5; // Adjust if "0.5" isn't exactly sunrise/noon; e.g., 0.3 for earlier sunrise
float y_mid = 0.75; // Value at mid-point (low = linger moon; high = quick to sun)
float ny_mid = 0.25; // Value at mid-point (low = linger moon; high = quick to sun)
float gamma = 3.0; // Higher = slower majority (linger moon), sharper rise near mid
float delta = 3.0; // Lower = faster jump after mid, slower tail to full sun
if (daytime <= mid) {
// Very slow majority → sharp rise near mid (exclude most night, quick fade-in)
float u = daytime / mid;
t = y_mid * pow(u, gamma);
} else {
// Fast jump after mid → slow majority to full (quick start, exclude most day by stabilizing high)
float u = (daytime - mid) / (1.0 - mid);
t = y_mid + (1.0 - y_mid) * pow(u, delta);
}
return t;
}
void main() {
vec3 viewDir = normalize(vWorldPosition - cameraPosition);
float height = normalize(vWorldPosition).y - 0.15; // -.15 pulls up the gradient
vec2 uv = vUv;
float rad=PI/180.0;
float phase=xtime/86400.0; // realtime
//phase=xtime/2000.0; // debugtime testval
//--float daytime=abs((xtime/(86400.0*.5))-1.0); // do on cpu instead
float twinkleSpeed = 86400.0 / 50.0;
//-- float twinkle = sin(uTime * twinkleSpeed);
//float time = sin(phase * twinkleSpeed);
float time = phase / (2.0 * 3.14159); // Normalizes phase from 0-2π to 0-1
time *= twinkleSpeed;
// Configuration parameters (adjustable)
float numCells = 43.0; // Number of grid cells across UV space (20x20 grid)
float maxBrightness = 2.0 * (1.0-((daytime*.5)+.5)); // Maximum star brightness
//float twinkleSpeed = 2.0; // Speed of twinkling
//float sigma = 0.0000002; // Glow size (in UV space, tweak based on resolution)
float sigma = 0.0000002; // Glow size (in UV space, tweak based on resolution)
float starDensity = 0.5; // Fraction of cells with stars (0.0 to 1.0)
float angle = PI / 2.0; // Or use a fixed value like PI / 2.0 for 90 deg
// Center the UVs
vec2 centered_uv = uv - 0.5;
// Calculate rotation factors
float cos_angle = cos(angle);
float sin_angle = sin(angle);
// Apply rotation matrix (mat2)
// [ cos(a) -sin(a) ] [x] [x*cos(a) - y*sin(a)]
// [ sin(a) cos(a) ] [y] = [x*sin(a) + y*cos(a)]
vec2 rotated_uv = vec2(
centered_uv.x * cos_angle - centered_uv.y * sin_angle,
centered_uv.x * sin_angle + centered_uv.y * cos_angle
);
// Translate back to original space
vec2 final_uv = rotated_uv + 0.5;
vec2 moving_uv = final_uv + vec2(0.0, time / (2.0 * PI));
ivec2 cell = ivec2(floor(moving_uv * numCells));
vec3 totalColor = vec3(0.0); // Accumulate star contributions
float starHeight=0.0; // stops at pulled up horizon
if(height>starHeight) {
// Check the current cell and its 8 neighbors for star contributions
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
ivec2 neighbor = cell + ivec2(i, j);
vec4 h = hash42(neighbor);
// Only create a star if the random value meets density threshold
if (h.w < starDensity) {
// Random position offset within the cell
vec2 starOffset = h.xy;
vec2 starPos = (vec2(neighbor) + starOffset) / numCells;
// Distance from fragment to star
float d = length(moving_uv - starPos);
// Star brightness with twinkling
float baseBrightness = h.z * maxBrightness;
float twinklePhase = h.w * 6.28318; // 2π for phase
float twinkle = 0.5 + 0.5 * sin(phase * twinkleSpeed + twinklePhase);
float totalBrightness = baseBrightness * twinkle;
// Glow effect using Gaussian falloff
float glow = exp(-d * d / sigma);
totalColor += vec3(totalBrightness * glow);
}
}
}
}
float t = max(height, 0.0);
// Apply smoothstep to remap the linear 't' value to a curved one
//float curved_t = smoothstep(0.0, 0.1, t);
float curved_t = pow(t, .666);
//float curved_t = t * t * (3.0 - 2.0 * t);
vec3 gradient = mix(bottomColor, topColor, curved_t); // Gradient based on y
float sunDot = dot(viewDir, sunDirection);
float moonDot = dot(viewDir, -sunDirection);
float moonGlow = pow(smoothstep(1.0 - moonSize, 1.0, moonDot),.8); // Blended sun disc
float moonRim = smoothstep(1.0 - moonSize * 1.05, 1.0, moonDot); // Blended moon disc
float moonAura = smoothstep(1.0 - moonSize * 420.0, 1.0, moonDot); // Blended moon disc
float moonAura2 = smoothstep(1.0 - moonSize * 20.0, 1.0, moonDot); // Blended moon disc
float sunGlow = pow(smoothstep(1.0 - sunSize, 1.0, sunDot),.8); // Blended sun disc
float sunRim = smoothstep(1.0 - sunSize * 1.05, 1.0, sunDot); // Blended sun disc
float sunAura = smoothstep(1.0 - sunSize * 1200.0, 1.0, sunDot); // Blended sun disc
float sunAura2 = smoothstep(1.0 - sunSize * 150.0, 1.0, sunDot); // Blended sun disc
float sunAura3 = smoothstep(1.0 - sunSize * 30.0, 1.0, sunDot); // Blended sun disc
//float sunHalo = smoothstep(0.95, 1.0, pow(sunDot, 150.0));
vec3 sunColor2=sunColor;
vec3 moonColor2=moonColor;
//sunColor2.b+=(height * .25);
//sunColor2.g+=(height * .55);
//sunColor2.r=1.0;
//sunColor2.g=0.0;
//sunColor2.b=0.0;
//sunColor2.g-=1.0-((daytime * .25) + .75);
//sunColor2.b-=1.0-((daytime * .25) + .75);
//sunColor2.r+=1.0-((daytime * .25) + .75);
//float moonBlend = ((moonAura * 0.05) + (moonAura2 * 0.05) + (moonRim) + (moonGlow * 5.0)) * (1.0-getDaytimeFactor(daytime));
//float sunBlend = ((sunAura3 * .2) + (sunAura2 * 0.3) + (sunAura * (daytime * 0.5)) + (sunRim) + (sunGlow * 15.0)) * getDaytimeFactor(daytime);
float moonBlend=((moonAura * .03) + (moonAura2 * .05) + (moonRim) + (moonGlow * 2.0)) * max((1.0-((daytime*2.0)-1.0)),0.0);
float sunBlend=((sunAura3 * .1) + (sunAura2 * .15) + (sunAura * (daytime * .5)) + (sunRim) + (sunGlow * 5.0)) * max(((daytime*.5)+0.5),0.0);
vec3 skyColor=vec3(1.0,0.0,1.0);
if(starsOn==1 && height>starHeight) {
skyColor=mix(mix(gradient+(totalColor * max((1.0-((daytime*.5)+.0)),0.0)),moonColor2,moonBlend),sunColor2,sunBlend) * 1.02;
} else {
skyColor=mix(mix(gradient,moonColor2,moonBlend),sunColor2,sunBlend) * 1.02;
}
skyColor+=sunGlow * sunColor2;
skyColor+=moonGlow * moonColor2;
// -------------------
// Horizon Glow Gradient (bottom bleed-up)
// -------------------
float horizonFactor = pow(1.0 - height-.15, horizonGlowSharpness); // sharp falloff upward
horizonFactor = smoothstep(0.0, 1.0, horizonFactor); // soften base
// Optional: stronger near actual horizon, weaker high up
horizonFactor *= (1.0 - smoothstep(0.0, horizonGlowHeight * 2.0, height));
// Mild noise perturbation (using your existing fbm2 / noise2)
vec3 noisePos = vec3(vUv * horizonNoiseScale, xtime * 0.05); // slow animate
float noise = fbm2(noisePos * 2.0) * 2.0 - 1.0; // -1..1 range
noise *= horizonNoiseStrength;
// Modulate glow with noise + height falloff
float glowAmount = horizonFactor * (1.0 + noise * 0.5); // subtle variation
glowAmount = clamp(glowAmount, 0.0, 1.5); // prevent overbright
// Final additive glow — use your moon/sun color or a dedicated one
vec3 horizonGlow = mix(horizonGlowColorNight,horizonGlowColorDay,daytime) * glowAmount * horizonGlowIntensity * (1.0-((cDensity*.5)+.5));
// recommend mix intensity here with cloud density or other weather in future
// (1.0-(max((daytime*2.0),0.0)))=mixn
vec3 finalColor = skyColor;
if(cloudsReady==1) {
if(cloudsOn==1) {
// Clouds: Assume ro ≈ vec3(0), but pass cameraPosition if offset
CloudResult clouds = raymarchClouds(cameraPosition, viewDir);
// Blend: Scattered + sky through clouds (stars dim behind)
finalColor = clouds.scattered + clouds.transmittance * skyColor;
finalColor += (1.0 - clouds.transmittance) * 0.1 * gradient; // Subtle night glow
if(starsOn==1) {
finalColor += (1.0 - clouds.transmittance) * 0.1 * totalColor; // Subtle night glow
}
} else if(cloudsOn==2) {
vec3 pos = normalize(vLocalPosition);
//vec3 npos = vec3(pos.y, -pos.x, pos.z); // random flip to fit
vec3 cloudColor = vec3(1.0,1.0,1.0);
//vec3 finalColor = vec3(0.0,0.0,0.0);
float alpha=0.0;
vec3 lightColor = vec3(0.0,0.0,0.0);
if(height>-0.15) { // avoid calc for bottom half
//float cloudtime = abs((xtime / 43200.0)-1.0);
//float cloudtime = abs((xtime / 10.0)-4320.0);
float phasetime = xtime / 5.0; // match in shadow shader if using
float phaselen = (86400.0 / 5.0) * .5; // realtime
//float phaselen = (2000.0 / 5.0) * .5; // debug
float cloudtime = abs(phasetime-phaselen);
float scale = 5.0;
vec3 flow1 = pos * scale + vec3(cloudtime * 0.10);
vec3 flow2 = pos * scale - vec3(cloudtime * 0.06);
vec3 flow3 = pos * scale + vec3(cloudtime * 0.03);
float n1 = noise2(flow1);
float n2 = noise2(flow2);
float n3 = noise2(flow3);
float baseNoise = (n2 + n3 - n1) * cDensity; // Base detail
float detailNoise = fbm2(pos * scale * 4.2 + vec3(cloudtime * 0.1)); // Smudged variation
float _cloudDensity = smoothstep(0.3, .7, baseNoise + detailNoise * 0.2); // Increased coverage
cloudColor = vec3(.5, .5, .5);
alpha = _cloudDensity;
// taken from https://twily.info/plainC/terrain/data/shaders/frag_shader_clouds.glsl#view
vec3 norm=normalize(ourNormal);
vec3 sunColor = vec3(1.0, 0.45, 0.0);
vec3 moonColor = vec3(0.0, 0.8, 1.0);
float mixn=min((daytime*2.0),1.0); // 0 - 0.5 // night half
float mixt=max((daytime*2.0)-1.0,0.0); // 0.5 - 1 // day half
//float sunStrength=(0.5-mixt)+.5;
//float moonStrength=1.0-(mixn);
float sunStrength=0.5-mixt;
float moonStrength=0.5-mixn;
vec3 alterSunDirection=vec3(sunDirection.x,-sunDirection.y,sunDirection.z); // flipped side y
vec3 fullColor=mix(moonColor,sunColor,getDaytimeFactor(daytime));
float diff=max(dot(norm, alterSunDirection), 0.0);
vec3 diffuse=diff*sunColor*sunStrength;
float diff2=max(dot(norm, -alterSunDirection), 0.0);
vec3 diffuse2=diff2*moonColor*moonStrength;
lightColor = ((diffuse+diffuse2+fullColor)/3.0);
//vec3 lightColor = vec3(1.0,1.0,1.0);
// Edge tint based on alpha and nightFactor (approximated from light)
float edgeFactor = smoothstep(0.2, 0.8, 1.0 - (alpha * 1.0)); // Higher alpha = less edge
vec3 colorNight=vec3(0.2,0.3,0.5);
vec3 colorDay=vec3(0.6,0.3,0.1);
vec3 colorTwilight=vec3(0.8,0.1,0.2);
float moonCloudBrightness=(daytime*.25)+.25;
float sunCloudBrightness=(daytime*.25)+1.25-(mixt*.5);
//float cloudBrightness=mix(moonCloudBrightness,sunCloudBrightness,mixn);
float cloudBrightness=mix(moonCloudBrightness,sunCloudBrightness,getDaytimeFactor(daytime) * 2.0);
vec3 edgeTint = mix(
colorNight, // 0
mix(
colorTwilight, // 0.5
colorDay, // 1
mixt),
mixn) * 2.0;
lightColor *= edgeTint * 5.0 * (1.0-(cDensity*.5)) * (1.0-min(abs(((daytime*2.0)-1.0)*1.0),0.5)) * (((mixn*.5)+.45));
// * (((mixn*.5)+.25)) // tone down at night
// * (1.00-min(abs(((daytime*2.0)-1.0)*1.0),0.5)) // tone down midday
//lightColor *= edgeTint * 2.0 * (1.0-(cDensity*.5)); //
cloudColor *= (daytime*.5)+.25;
//cloudColor *= max(abs(((daytime*2.0)-1.0)*1.0),0.5);
//edgeTint = edgeTint * mix(diff,diff2,max(1.0-((daytime*.5)+.25),0.0));
edgeTint = edgeTint * mix(diff2,diff,getDaytimeFactor(daytime)) * (1.0-(cDensity*.5));
cloudColor *= mix(cloudColor, edgeTint, edgeFactor) + cloudBrightness; // Apply tint
//cloudColor = colorNight;
// used for subtracting clouds at horizon currently, no glow, and not in shadow
alpha *= 1.0-min(glowAmount * horizonGlowIntensity,1.0);
//vec3 black=vec3(0.0);
//finalColor = mix(black,cloudColor.rgb * ((lightColor * .2)+.8),alpha);
finalColor = mix(finalColor, cloudColor.rgb + lightColor, alpha);
} // if vUv>0.5
}
} // clouds ready
if(horizonGlowOn==1) {
// Blend modes to try (additive is most glow-y / natural for atmosphere)
finalColor += horizonGlow; // pure additive glow (recommended)
//finalColor = mix(finalColor, horizonGlow, 0.4); // softer tint blend (alternative)
//finalColor = finalColor * (1.0 + horizonGlow); // multiplicative boost (stronger)
}
//finalColor=mix(finalColor,finalClouds,cloudAlpha);
gl_FragColor=vec4(finalColor,1.0);
//gl_FragColor = vec4(vec3(clouds.transmittance), 1.0);
//gl_FragColor = vec4(clouds.scattered, 1.0);
// ------------- old below
//gl_FragColor = vec4(mix(gradient+totalColor, sunColor2, (sunAura3 * .1) + (sunAura2 * .1) + (sunAura * .1) + (sunRim * 1.0) + (sunGlow * 1.0)), 1.0);
// final with sun
//gl_FragColor = vec4(mix(gradient, sunColor2, ((sunAura3 * .01) + (sunAura2 * .02) + (sunAura * .1) + (sunRim * 0.5) + (sunGlow * 0.5)) * daytime * 5.0) * 1.00, 1.0); // * 5.0 harder edges between glow and rim but overexposure color
//vec3 color=mix(mix(gradient,moonColor2,0.5),sunColor2,0.5);
// final with sun and moon
//gl_FragColor = vec4(mix(mix(gradient, moonColor2, ((moonAura * .002) + (moonAura2 * .02) + (moonRim * 1.0) + (moonGlow * 1.0)) * max(2.0-daytime,0.5) * 2.0), sunColor2, ((sunAura3 * .01) + (sunAura2 * .02) + (sunAura * .1) + (sunRim * 1.0) + (sunGlow * 1.0)) * daytime * 5.0) * 1.02, 1.0); // * 5.0 harder edges between glow and rim but overexposure color
//gl_FragColor = vec4(mix(gradient, moonColor2, moonGlow), 1.0); // moon test
//gl_FragColor = vec4(daytime,daytime,daytime,1.0);
}
`;
//. custom shadow for 2d slouds simple2 [default]
// replicate procedural clouds here for custom packing
// does not work currently~
const fragmentShaderShadow = `
#include <packing>
uniform sampler2D uAlphaMap;
uniform float uAlphaThreshold;
varying vec3 vWorldPosition;
uniform float xtime;
varying vec3 vPosition;
varying vec3 vLocalPosition;
varying vec3 vViewPosition;
varying vec2 TexCoord;
varying vec3 v_Normal;
uniform vec3 sunDirection;
//uniform vec3 moonDirection;
uniform float sunSize;
uniform float daytime;
uniform vec3 lightDir;
uniform vec3 lightDir2;
varying vec2 vUv;
uniform float cDensity;
uniform int cloudsOn;
const float PI = 3.1415926535897932384626433832795;
// Noise functions
float random(vec3 p) {
return fract(sin(dot(p, vec3(12.9898, 78.233, 45.5432))) * 43758.5453123);
}
float noise(vec3 p) {
vec3 i = floor(p);
vec3 f = fract(p);
vec3 u = f * f * (3.0 - 2.0 * f);
return mix(mix(mix(random(i), random(i + vec3(1.0, 0.0, 0.0)), u.x),
mix(random(i + vec3(0.0, 1.0, 0.0)), random(i + vec3(1.0, 1.0, 0.0)), u.x), u.y),
mix(mix(random(i + vec3(0.0, 0.0, 1.0)), random(i + vec3(1.0, 0.0, 1.0)), u.x),
mix(random(i + vec3(0.0, 1.0, 1.0)), random(i + vec3(1.0, 1.0, 1.0)), u.x), u.y), u.z);
}
float fbm(vec3 p) {
float v = 0.0;
float a = 0.5;
vec3 shift = vec3(100.0);
for (int i = 0; i < 2; ++i) { // default 6
v += a * noise(p);
p = p * 2.0 + shift;
a *= 0.2;
}
return v;
}
void main() {
//--vec4 texColor = texture2D(uAlphaMap, vUv);
//--if (texColor.a < uAlphaThreshold) discard;
if(cloudsOn==2) {
float height = normalize(vWorldPosition).y - 0.15; // -.15 pulls up the gradient
vec2 uv = vUv;
vec3 pos = normalize(vLocalPosition);
// Center the UVs
vec2 centered_uv = uv - 0.5;
float angle = PI / 2.0; // Or use a fixed value like PI / 2.0 for 90 deg
// Calculate rotation factors
float cos_angle = cos(angle);
float sin_angle = sin(angle);
// Apply rotation matrix (mat2)
// [ cos(a) -sin(a) ] [x] [x*cos(a) - y*sin(a)]
// [ sin(a) cos(a) ] [y] = [x*sin(a) + y*cos(a)]
vec2 rotated_uv = vec2(
centered_uv.x * cos_angle - centered_uv.y * sin_angle,
centered_uv.x * sin_angle + centered_uv.y * cos_angle
);
// Translate back to original space
vec2 final_uv = rotated_uv + 0.5;
vec3 cloudColor = vec3(1.0,1.0,1.0);
vec3 finalColor = vec3(0.0,0.0,0.0);
float alpha=0.0;
//if(rotated_uv.x>=0.5) { // avoid calc for bottom half
if(height>-0.15) { // avoid calc for bottom half
//float time = abs((xtime / 10.0)-4320.0);
float phasetime = xtime / 5.0;
float phaselen = (86400.0 / 5.0) * .5; // realtime
//float phaselen = (2000.0 / 5.0) * .5; // debug
float time = abs(phasetime-phaselen);
float scale = 5.0;
vec3 flow1 = pos * scale + vec3(time * 0.10);
vec3 flow2 = pos * scale - vec3(time * 0.06);
vec3 flow3 = pos * scale + vec3(time * 0.03);
float n1 = noise(flow1);
float n2 = noise(flow2);
float n3 = noise(flow3);
float baseNoise = (n2 + n3 - n1) * cDensity; // Base detail
float detailNoise = fbm(pos * scale * 4.2 + vec3(time * 0.01)); // Smudged variation
float cloudDensity = smoothstep(0.3, .7, baseNoise + detailNoise * 0.3); // Increased coverage
cloudColor = vec3(.5, .5, .5);
alpha = cloudDensity;
// Edge tint based on alpha and nightFactor (approximated from light)
float edgeFactor = smoothstep(0.2, 0.8, 1.0 - alpha); // Higher alpha = less edge
float nightFactor = daytime;
nightFactor = smoothstep(0.2, 0.8, nightFactor);
vec3 edgeTint = mix(
vec3(0.2, 0.2, 0.2), // Grey
mix(
vec3(0.5, 0.6, 0.7),
vec3(0.8, 0.5, 0.5),
nightFactor * 5.0), // Blue to red
0.5 - (nightFactor * .25)); // Darker at edges
cloudColor = mix(cloudColor, edgeTint, edgeFactor) * 2.0; // Apply tint
alpha = mix(alpha,0.0,edgeFactor) * 2.0;
finalColor = cloudColor.rgb;
}
//uniform float alphaThreshold;
//float alphaThreshold = 0.3;
if (alpha < uAlphaThreshold) discard;
//gl_FragColor = vec4(finalColor, alpha);
//gl_FragColor = vec4(vec3(alpha,0.0,0.0), alpha); // debug
// This line is essential for depth packing
gl_FragColor = packDepthToRGBA(gl_FragCoord.z);
} else {
gl_FragColor = packDepthToRGBA(gl_FragCoord.z);
}
}
`;
const vertexShaderClouds = `
uniform mat4 mvpMatrix;
varying vec3 ourNormal;
varying vec2 vUv;
varying float ypos;
void main()
{
ourNormal=normal;
vUv = uv;
gl_Position = mvpMatrix * vec4(position, 1.0);
ypos=position.x*0.001f; // -1000 to 1000 = -1 to 1
}
`;
const fragmentShaderClouds = `
#define PI 3.14159265359
uniform float xtime; // seconds~day
//uniform vec3 data; // data.x time, data.y freq, data.z amp
uniform float daytime; // -1-0-1
uniform vec3 sunDirection;
//uniform vec3 moonDirection;
uniform float sunSize;
varying vec3 ourNormal;
varying vec2 vUv; // UV coordinates from the vertex shader
// Pseudo-random hash function based on 2D coordinates
float hash(vec2 p) {
return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}
// 2D value noise with smooth interpolation
float noise(vec2 p) {
vec2 i = floor(p); // Integer part
vec2 f = fract(p); // Fractional part
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f); // Cubic interpolation
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}
// Fractal Brownian motion for detailed cloud patterns
float fbm(vec2 p) {
float value = 0.0;
//float amplitude = 0.5;
float frequency = 0.222;
float amplitude = 0.222;
//float frequency = data.y;
for (int i = 0; i < 4; i++) { // 4 octaves for detail
value += amplitude * noise(p * frequency);
frequency *= 2.0; // Double frequency each octave
amplitude *= 0.5; // Halve amplitude each octave
}
return value;
}
// clouds by grok - butthole above
void main() {
//float time = xtime;
float rad=PI/180.0;
//float time=(xtime * 1.0 / 86400) * 25000; // 50 breaks
float phase = xtime;
vec2 uv = vUv;
vec3 finalColor=vec3(0.0,0.0,0.0);
float alpha=0.0;
if(vUv.y>=0.5) { // avoid calc for bottom half
float twinkleSpeed = 86400.0 / 3.0;
//-- float twinkle = sin(uTime * twinkleSpeed);
//float time = sin(phase * twinkleSpeed);
float time = phase / (2.0 * 3.14159) * twinkleSpeed;
///vec2 flame_uv = uv + vec2(0.0, offset_y);
float speed = 0.1; // Speed of cloud movement
float cloudScale = 100.0; // Scale of cloud patterns
// Circular offset for looping animation
//vec2 offset = vec2(sin(time * speed), cos(time * speed)) * 0.5;
vec2 offset = vec2(sin(time), cos(time)) * 0.5;
// Scale UV coordinates and apply offset
vec2 scaledUV = uv * cloudScale;
scaledUV.x*=2.0; //
float cloudValue = fbm(scaledUV + offset);
// Map noise to alpha for cloud density with soft edges
alpha = smoothstep(0.0, 0.7, cloudValue);
//float alpha=0.5;
// Purplish cloud color (stylized)
//vec3 cloudColor = vec3(0.6, 0.6, 0.66) * (1.0 - daytime);
// color blendings
float methodA=1.0-daytime;
float methodD=(methodA*2.0);
float methodE=(methodA*2.0)-1.0;
// day/night cloud colors blend
vec3 color1 = vec3(0.5,0.3,0.5); // day end
vec3 color2 = vec3(0.6,0.6,0.7);
vec3 color3 = vec3(0.7,0.6,0.7); // <<day begin
//
vec3 color4 = vec3(0.63,0.43,0.43); // night end
vec3 color5 = vec3(0.43,0.36,0.43);
vec3 color6 = vec3(0.15,0.16,0.43); // night begin
vec3 ourColor = vec3(mix(color3,color2,methodD));
ourColor = mix(ourColor,color1,methodE);
vec3 ourColor2 = vec3(mix(color5,color4,methodD));
ourColor2 = mix(ourColor2,color6,methodE);
vec3 cloudColor = vec3(mix(ourColor,ourColor2,daytime));
vec3 norm=normalize(ourNormal);
vec3 sunColor = vec3(1.0f, 0.93f, 0.74f); // sky sphere
vec3 moonColor = vec3(0.7f, 1.0f, 0.8f); // lumination
float sunStrength=10.0f;
float moonStrength=3.0f;
float diff=max(dot(norm, sunDirection), 0.0);
vec3 diffuse=diff*sunColor*sunStrength;
float diff2=max(dot(norm, -sunDirection), 0.0);
vec3 diffuse2=diff2*moonColor*moonStrength;
finalColor = (cloudColor + ((diffuse + diffuse2) * 0.1)) * cloudColor;
}
// Output color with transparency
//gl_FragColor = vec4(cloudColor, alpha);
gl_FragColor = vec4(finalColor, alpha);
//gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`;
// clouds2 moved to skymaterial not using clouds geometry
const vertexShaderClouds2 = `
uniform mat4 mvpMatrix;
varying vec3 vPosition;
varying vec3 vLocalPosition;
uniform mat3 nMatrix; // CPU-computed normal matrix
uniform mat4 vMatrix; // View matrix (for view-space lighting)
varying vec3 vViewPosition; // View-space position
varying vec2 TexCoord;
varying vec3 v_Normal;
varying vec3 ourNormal;
varying vec2 vUv;
//varying float ypos;
void main() {
//ourNormal=vNormal;
ourNormal=normal;
vUv = uv;
vLocalPosition = position;
vPosition = (modelMatrix * vec4(position, 1.0)).xyz;
vViewPosition = (modelViewMatrix * vec4(position, 1.0)).xyz; // View-space position
TexCoord = vec2(vPosition.x, vPosition.z);
vec3 worldNormal = normalize(nMatrix * normal);
v_Normal = normalize((vMatrix * vec4(worldNormal, 0.0)).xyz);
//ypos=position.y*0.0005f; // -1000 to 1000 = -1 to 1
gl_Position = mvpMatrix * vec4(position, 1.0);
}
`;
const fragmentShaderClouds2 = `
uniform float xtime;
//uniform mat4 invView; // Inverse view matrix
//uniform mat4 invProjection; // Inverse view matrix
varying vec3 vPosition;
varying vec3 vLocalPosition;
varying vec3 vViewPosition;
varying vec2 TexCoord;
varying vec3 v_Normal;
//varying vec3 v_vertToLight;
//varying vec4 viewSunPos;
uniform vec3 sunDirection;
//uniform vec3 moonDirection;
uniform float sunSize;
uniform float daytime;
uniform vec3 lightDir;
uniform vec3 lightDir2;
varying vec3 ourNormal;
varying vec2 vUv;
uniform float cDensity;
//varying float ypos;
const float PI = 3.1415926535897932384626433832795;
// Noise functions
float random(vec3 p) {
return fract(sin(dot(p, vec3(12.9898, 78.233, 45.5432))) * 43758.5453123);
}
float noise(vec3 p) {
vec3 i = floor(p);
vec3 f = fract(p);
vec3 u = f * f * (3.0 - 2.0 * f);
return mix(mix(mix(random(i), random(i + vec3(1.0, 0.0, 0.0)), u.x),
mix(random(i + vec3(0.0, 1.0, 0.0)), random(i + vec3(1.0, 1.0, 0.0)), u.x), u.y),
mix(mix(random(i + vec3(0.0, 0.0, 1.0)), random(i + vec3(1.0, 0.0, 1.0)), u.x),
mix(random(i + vec3(0.0, 1.0, 1.0)), random(i + vec3(1.0, 1.0, 1.0)), u.x), u.y), u.z);
}
float fbm(vec3 p) {
float v = 0.0;
float a = 0.5;
vec3 shift = vec3(100.0);
for (int i = 0; i < 2; ++i) { // default 6
v += a * noise(p);
p = p * 2.0 + shift;
a *= 0.2;
}
return v;
}
//float linearizeDepth(float depth) {
// float near = 0.1;
// float far = 500.0; // Adjust to match your camera
// float z = depth * 2.0 - 1.0; // NDC
// return 2.0 * near * far / (far + near - z * (far - near));
//}
//
//vec3 getWorldPosition(float depth, vec2 uv) {
// float z = linearizeDepth(depth);
// vec4 clipSpace = vec4(uv * 2.0 - 1.0, z, 1.0);
// vec4 viewSpace = invProjection * clipSpace;
// viewSpace /= viewSpace.w;
// vec4 worldSpace = invView * viewSpace;
// return worldSpace.xyz;
//}
void main() {
// Noise for cloud pattern
vec3 pos = normalize(vLocalPosition);
vec3 cloudColor = vec3(1.0,1.0,1.0);
vec3 finalColor = vec3(0.0,0.0,0.0);
float alpha=0.0;
if(vUv.y>=0.5) { // avoid calc for bottom half
//float time = abs((xtime / 43200.0)-1.0);
//float time = abs((xtime / 10.0)-4320.0);
float phasetime = xtime / 5.0; // match in shadow shader if using
float phaselen = (86400.0 / 5.0) * .5; // realtime
//float phaselen = (2000.0 / 5.0) * .5; // debug
float time = abs(phasetime-phaselen);
//float loopedRealtime = mod(time, 365.0 * 24.0 * 3600.0); // 365 days
//float globalAngle = (loopedRealtime / (365.0 * 24.0 * 3600.0)) * 2.0 * PI;
//float localAngle = globalAngle; // Optional latitude factor if desired
//-- float cosR = cos(localAngle);
//-- float sinR = sin(localAngle);
//-- vec3 pos2 = vec3(
//-- cosR * pos.x + sinR * pos.z,
//-- pos.y,
//-- -sinR * pos.x + cosR * pos.z
//-- );
float scale = 5.0;
vec3 flow1 = pos * scale + vec3(time * 0.10);
vec3 flow2 = pos * scale - vec3(time * 0.06);
vec3 flow3 = pos * scale + vec3(time * 0.03);
float n1 = noise(flow1);
float n2 = noise(flow2);
float n3 = noise(flow3);
float baseNoise = (n2 + n3 - n1) * cDensity; // Base detail
float detailNoise = fbm(pos * scale * 4.2 + vec3(time * 0.01)); // Smudged variation
float cloudDensity = smoothstep(0.3, .7, baseNoise + detailNoise * 0.2); // Increased coverage
cloudColor = vec3(.5, .5, .5);
alpha = cloudDensity;
// taken from https://twily.info/plainC/terrain/data/shaders/frag_shader_clouds.glsl#view
vec3 norm=normalize(ourNormal);
vec3 sunColor = vec3(1.0, 0.93, 0.74);
vec3 moonColor = vec3(0.7, 0.5, 0.9);
float sunStrength=1.0;
float moonStrength=0.2;
float diff=max(dot(norm, sunDirection), 0.0);
vec3 diffuse=diff*sunColor*sunStrength;
float diff2=max(dot(norm, -sunDirection), 0.0);
vec3 diffuse2=diff2*moonColor*moonStrength;
vec3 lightColor = mix(diffuse,diffuse2,1.0-daytime);
// Edge tint based on alpha and nightFactor (approximated from light)
float edgeFactor = smoothstep(0.2, 0.8, 1.0 - (alpha * 1.5)); // Higher alpha = less edge
vec3 colorNight=vec3(0.2,0.3,0.5);
vec3 colorDay=vec3(0.6,0.3,0.1);
vec3 colorTwilight=vec3(0.8,0.1,0.2);
float mixn=min((daytime*2.0),1.0); // 0 - 0.5
float mixt=max((daytime*2.0)-1.0,0.0); // 0.5 - 1
float cloudBrightness=max((max(daytime,0.25)*2.0),0.0); // *half+half = ambient
vec3 edgeTint = mix(
colorNight, // 0
mix(
colorTwilight, // 0.5
colorDay, // 1
mixt),
mixn);
edgeTint = edgeTint * mix(diff,diff2,1.0-daytime);
cloudColor = cloudColor * ((mix(diff,diff2,1.0-daytime)*.2)+.8);
cloudColor = mix(cloudColor, edgeTint, edgeFactor) * cloudBrightness * 2.0; // Apply tint
//cloudColor = colorNight;
//vec3 P = getWorldPosition(vPosition.z, TexCoord);
//finalColor = cloudColor.rgb * ((lightColor * .8)+.8);
//vec3 black=vec3(0.0);
//finalColor = mix(black,cloudColor.rgb * ((lightColor * .2)+.8),alpha);
finalColor = mix(finalColor, cloudColor.rgb * ((lightColor * .2)+.8), alpha);
}
//vec3 testColor = vec3(0,.5,0);
// Apply lighting to color
//testColor *= lightIntensity;
//uniform float alphaThreshold;
float alphaThreshold = 0.3;
if (alpha < alphaThreshold) discard;
gl_FragColor = vec4(finalColor, alpha);
//gl_FragColor = vec4(vec3(cloudColor), alpha);
//gl_FragColor = vec4(testColor, 1);
//gl_FragColor = vec4(P, alpha);
}
`;
const vertexShaderTerrain = `
varying vec2 vUv;
varying float vHeight;
varying vec3 vNormal;
varying vec3 vWorldPos;
uniform float repeatScale;
//varying float vFogDepth;
varying vec3 vWorldPosition;
// Manual shadow coord
varying vec4 vSunShadowCoord;
uniform float shadowNormalBias;
uniform mat4 sunShadowMatrix; // from uniform
uniform int shadowOn;
${THREE.ShaderChunk['common']}
${THREE.ShaderChunk['fog_pars_vertex']}
void main() {
vUv = uv * repeatScale; // Scale UVs in vertex for repeating
vHeight = position.y / ${heightScale}.0; // Normalized height for splatting
vNormal = normal;
vec4 worldPos = modelMatrix * vec4(position, 1.0);
vWorldPos = worldPos.xyz;
vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
//vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
vFogDepth = -mvPosition.z;
//gl_Position = projectionMatrix * mvPosition;
// Offset for sun
vec3 offset = normal * shadowNormalBias; // object-space offset (assumes uniform scale; if not, use worldNormal below)
// For non-uniform scale: vec3 offset = vWorldNormal * shadowNormalBias;
vec4 offsetWorldPos = modelMatrix * vec4(position + offset, 1.0);
if(shadowOn >= 1) {
vSunShadowCoord = sunShadowMatrix * offsetWorldPos;
// Repeat for moon with another offsetWorldPos if separate bias, but same for now
}
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
${THREE.ShaderChunk['fog_vertex']}
}
`;
const fragmentShaderTerrain = `
precision highp float;
${THREE.ShaderChunk['common']}
${THREE.ShaderChunk['packing']}
${THREE.ShaderChunk['fog_pars_fragment']}
uniform sampler2D grassTex;
uniform sampler2D rockTex;
uniform sampler2D snowTex;
uniform sampler2D normalGrass;
uniform sampler2D normalRock;
uniform sampler2D normalSnow;
uniform sampler2D roughGrass;
uniform sampler2D roughRock;
uniform sampler2D roughSnow;
uniform float repeatScale;
uniform vec3 lightDir;
uniform vec3 lightDir2;
uniform float daytime; // 1 0 1
// Manual shadow uniforms
uniform float shadowBias;
uniform float shadowRadius;
uniform sampler2D sunShadowMap;
uniform mat4 sunShadowMatrix;
varying vec4 vSunShadowCoord;
uniform int shadowOn;
varying vec2 vUv;
varying float vHeight;
varying vec3 vNormal;
varying vec3 vWorldPos;
varying vec3 vWorldPosition;
void main() {
//vec2 uv=vUv * repeatScale;
vec2 uv = vUv;
// Splat based on height (grass low, rock mid, snow high)
vec3 albedo = mix(texture2D(grassTex, uv).rgb, texture2D(rockTex, uv).rgb, smoothstep(0.2, 0.5, vHeight));
albedo = mix(albedo, texture2D(snowTex, uv).rgb, smoothstep(0.6, 0.8, vHeight));
// Splat normals
vec3 norm = mix(texture2D(normalGrass, uv).rgb, texture2D(normalRock, uv).rgb, smoothstep(0.2, 0.5, vHeight));
norm = mix(norm, texture2D(normalSnow, uv).rgb, smoothstep(0.6, 0.8, vHeight));
norm = norm * 2.0 - 1.0; // Unpack
// Hardcoded ORM: AO=1.0 (no occlusion), rough=0.5, metal=0.0
float ao = 1.0;
//float rough = 0.5;
float rough=mix(texture2D(roughGrass, uv).r, texture2D(roughRock, uv).r, smoothstep(0.2, 0.5, vHeight));
rough = mix(rough, texture2D(roughSnow, uv).r, smoothstep(0.6, 0.8, vHeight));
float metal = 0.0;
// Simple PBR lighting (diffuse + specular approximation)
vec3 viewDir = normalize(cameraPosition - vWorldPosition);
vec3 finalNormal = normalize(vNormal + norm * 0.5); // Reduced normal strength if too bumpy
// View-space normal + tangent-space bump
vec3 lightDir2Mod=vec3(-lightDir2.x,-lightDir2.y,-lightDir2.z);
float diff = max(dot(lightDir, finalNormal), 0.0) * ao * (((daytime*.5)+.5));
vec3 halfway = normalize(lightDir + viewDir);
float spec = pow(max(dot(finalNormal, halfway), 0.0), 32.0 * (rough)) * (0.04 + metal) * (((daytime*.5)+.5));
float diffMoon = max(dot(lightDir2Mod, finalNormal), 0.0) * ao * max((1.0-((daytime*2.0)-1.0)),0.0) * .4;
vec3 halfwayMoon = normalize(lightDir2Mod + viewDir);
float specMoon = pow(max(dot(finalNormal, halfwayMoon), 0.0), 32.0 * (rough)) * (0.04 + metal) * (1.0-((daytime*.5)+.5));
vec3 color = albedo * max(diff + diffMoon,.3) + vec3(spec + specMoon); // Increased ambient contribution for brightness
float shadow = 1.0; // no shadow
if(shadowOn>=1) {
// Manual shadow sampling for sun
shadow=abs((daytime*2.0)-1.0); // 1 0 1 0 = 0.5 0 0.5 1 ~;
vec4 shadowCoord = vSunShadowCoord / vSunShadowCoord.w;
shadowCoord = shadowCoord * 0.5 + 0.5; // NDC to [0,1]
if (shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 &&
shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0 &&
shadowCoord.z >= 0.0 && shadowCoord.z <= 1.0) {
// Circle check: distance from center (0.5,0.5) <= radius 0.5 (fits [0,1])
//vec2 centerDist = shadowCoord.xy - vec2(0.5, 0.5); // avoid circular in orthographic?
//if (length(centerDist) <= 0.5 &&
// shadowCoord.z >= 0.0 &&
// shadowCoord.z <= 1.0) {
float shadowDepth=0.0;
float bias=0.0;
shadowDepth = unpackRGBAToDepth(texture(sunShadowMap, shadowCoord.xy));
//bias = shadowBias / max(0.01, dot(finalNormal, lightDir)); // dynamic bias
//bias = shadowBias * max(0.05, dot(finalNormal, lightDir)); // dynamic bias
float ndotl=max(dot(finalNormal, lightDir), 0.01);
float slopeFactor = sqrt(1.0 - ndotl * ndotl) / ndotl; // tan(acos(ndotl))
bias = shadowBias + 0.0001 * slopeFactor; // tune 0.0001 as slope bias; small to avoid leaks
bias = clamp(bias, shadowBias, 0.001); // cap to prevent excessive
//float bias = shadowBias;
shadow = shadowCoord.z > shadowDepth + bias ? 0.0 : 1.0;
if(shadowOn<=2) {
vec2 texelSize = 1.0 / vec2(4096.0, 4096.0); // match mapSize
// Add to fragmentShader uniforms or defines
const int numSamples = 16;
vec2 poissonDisk[16] = vec2[](
vec2(-0.94201624, -0.39906216), vec2(0.94558609, -0.76890725),
vec2(-0.094184101, -0.92938870), vec2(0.34495938, 0.29387760),
vec2(-0.91588581, 0.45771432), vec2(-0.81544232, -0.87912464),
vec2(-0.38277543, 0.27676845), vec2(0.97484398, 0.75648379),
vec2(0.44323325, -0.97511554), vec2(0.53742981, -0.47373420),
vec2(-0.26496911, -0.41893023), vec2(0.79197514, 0.19090188),
vec2(-0.24188840, 0.99706507), vec2(-0.81409955, 0.91437590),
vec2(0.19984103, 0.78641367), vec2(0.14383161, -0.14100790)
);
// In the loop:
shadow = 0.0; // Reset to accumulate lit
for (int i = 0; i < numSamples; i++) {
vec2 offset = poissonDisk[i] * shadowRadius * texelSize;
// Optional: Rotate disk for variety (reduces repetition)
float angle = fract(sin(dot(shadowCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) * 6.2832;
offset = vec2(cos(angle) * offset.x - sin(angle) * offset.y, sin(angle) * offset.x + cos(angle) * offset.y);
float d = unpackRGBAToDepth(texture(sunShadowMap, shadowCoord.xy + offset));
shadow += (shadowCoord.z > d + bias) ? 0.0 : 1.0;
}
shadow /= float(numSamples);
} // dithering shadow
}
// Real shadow receive
//float shadow = getShadow(); // from shadowmask chunk
if(daytime>0.5) {
color *= (shadow * 0.6 + 0.6); // shadow factor + ambient day
} else {
color *= (shadow * 0.3 + 0.9); // shadow factor + ambient night
}
} // shadowOn>=1
// Fog application (using your vFogDepth)
float fogFactor = 1.0 - exp(-fogDensity * fogDensity * vFogDepth * vFogDepth);
color *= max(fogColor,.2) * 2.0;
color = mix(color, fogColor, fogFactor);
//#include <fog_fragment>
if(shadowOn>=2) {
gl_FragColor = vec4(vec3(shadow), 1.0);
} else {
gl_FragColor = vec4(color, 1.0);
}
//gl_FragColor = vec4(rough, rough, rough, 1.0);
}
`;
const vertexShaderMountain = `
varying vec2 vUv;
varying vec3 vWorldNormal; // world-space normal (transformed)
varying vec3 vWorldPos; // world-space position
//varying float vFogDepth;
// Manual shadow coord
varying vec4 vSunShadowCoord;
uniform float shadowNormalBias;
uniform mat4 sunShadowMatrix; // from uniform
uniform int shadowOn;
${THREE.ShaderChunk['common']}
${THREE.ShaderChunk['fog_pars_vertex']}
void main() {
vUv = uv;
//vNormal = normal;
// World-space normal (correct for random rotation)
mat3 normalMat3 = mat3(transpose(inverse(modelMatrix))); // proper normal transform
vWorldNormal = normalize(normalMat3 * normal);
vec4 worldPos = modelMatrix * vec4(position, 1.0);
vWorldPos = worldPos.xyz;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
vFogDepth = -mvPosition.z;
// Offset for sun
vec3 offset = normal * shadowNormalBias; // object-space offset (assumes uniform scale; if not, use worldNormal below)
// For non-uniform scale: vec3 offset = vWorldNormal * shadowNormalBias;
vec4 offsetWorldPos = modelMatrix * vec4(position + offset, 1.0);
if(shadowOn >= 1) {
vSunShadowCoord = sunShadowMatrix * offsetWorldPos;
// Repeat for moon with another offsetWorldPos if separate bias, but same for now
}
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
${THREE.ShaderChunk['fog_vertex']}
}
`;
const fragmentShaderMountain = `
precision highp float;
${THREE.ShaderChunk['common']}
${THREE.ShaderChunk['packing']}
${THREE.ShaderChunk['fog_pars_fragment']}
uniform sampler2D map;
uniform sampler2D normalMap;
uniform sampler2D roughnessMap;
uniform vec3 lightDir;
uniform vec3 lightDir2;
uniform float daytime; // 1 0 1
// Manual shadow uniforms
uniform float shadowBias;
uniform float shadowRadius;
uniform sampler2D sunShadowMap;
uniform mat4 sunShadowMatrix;
varying vec4 vSunShadowCoord;
uniform int shadowOn;
varying vec2 vUv;
varying vec3 vWorldNormal;
varying vec3 vWorldPos;
//varying float vFogDepth;
void main() {
vec4 albedo = texture(map, vUv);
float rough = texture2D(roughnessMap, vUv).r;
// Object-space normal map perturbation
vec3 normalTex = texture(normalMap, vUv).rgb * 2.0 - 1.0;
// Add bump to world-space normal
vec3 finalNormal = normalize(vWorldNormal + normalTex * 0.8); // strength 0.8, tune as needed
vec3 viewDir = normalize(cameraPosition - vWorldPos);
float ao=1.0;
float metal=0.0;
vec3 lightDir2Mod=vec3(-lightDir2.x,-lightDir2.y,-lightDir2.z);
float diff = max(dot(finalNormal, lightDir), 0.0) * ao * (((daytime*.5)+.5));
vec3 halfway = normalize(lightDir + viewDir);
float spec = pow(max(dot(finalNormal, halfway), 0.0), 32.0 * (rough)) * (0.04 + metal) * (((daytime*.5)+.5));
float diffMoon = max(dot(lightDir2Mod, finalNormal), 0.0) * ao * max((1.0-((daytime*2.0)-1.0)),0.0) * .6;
// -1 0 1 x 2 = -2 0 2
// -2 0 2 - 1 = -3 0 1
// 1 - -3 0 1 =
vec3 halfwayMoon = normalize(lightDir2Mod + viewDir);
float specMoon = pow(max(dot(finalNormal, halfwayMoon), 0.0), 32.0 * (rough)) * (0.04 + metal) * (1.0-((daytime*.5)+.5));
vec3 color = albedo.rgb * max(diff + diffMoon,.3) * 3.0 + vec3(spec + specMoon); // Increased ambient contribution for brightness
float shadow = 1.0; // no shadow
if(shadowOn>=1) {
// Manual shadow sampling for sun
shadow=abs((daytime*2.0)-1.0); // 1 0 1 0 = 0.5 0 0.5 1 ~;
vec4 shadowCoord = vSunShadowCoord / vSunShadowCoord.w;
shadowCoord = shadowCoord * 0.5 + 0.5; // NDC to [0,1]
if (shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 &&
shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0 &&
shadowCoord.z >= 0.0 && shadowCoord.z <= 1.0) {
// Circle check: distance from center (0.5,0.5) <= radius 0.5 (fits [0,1])
//vec2 centerDist = shadowCoord.xy - vec2(0.5, 0.5); // avoid circular in orthographic?
//if (length(centerDist) <= 0.5 &&
// shadowCoord.z >= 0.0 &&
// shadowCoord.z <= 1.0) {
float shadowDepth=0.0;
float bias=0.0;
shadowDepth = unpackRGBAToDepth(texture(sunShadowMap, shadowCoord.xy));
//bias = shadowBias / max(0.01, dot(finalNormal, lightDir)); // dynamic bias
//bias = shadowBias * max(0.05, dot(finalNormal, lightDir)); // dynamic bias
float ndotl=max(dot(finalNormal, lightDir), 0.01);
float slopeFactor = sqrt(1.0 - ndotl * ndotl) / ndotl; // tan(acos(ndotl))
bias = shadowBias + 0.0001 * slopeFactor; // tune 0.0001 as slope bias; small to avoid leaks
bias = clamp(bias, shadowBias, 0.001); // cap to prevent excessive
//float bias = shadowBias;
shadow = shadowCoord.z > shadowDepth + bias ? 0.0 : 1.0;
if(shadowOn<=2) {
vec2 texelSize = 1.0 / vec2(4096.0, 4096.0); // match mapSize
// Add to fragmentShader uniforms or defines
const int numSamples = 16;
vec2 poissonDisk[16] = vec2[](
vec2(-0.94201624, -0.39906216), vec2(0.94558609, -0.76890725),
vec2(-0.094184101, -0.92938870), vec2(0.34495938, 0.29387760),
vec2(-0.91588581, 0.45771432), vec2(-0.81544232, -0.87912464),
vec2(-0.38277543, 0.27676845), vec2(0.97484398, 0.75648379),
vec2(0.44323325, -0.97511554), vec2(0.53742981, -0.47373420),
vec2(-0.26496911, -0.41893023), vec2(0.79197514, 0.19090188),
vec2(-0.24188840, 0.99706507), vec2(-0.81409955, 0.91437590),
vec2(0.19984103, 0.78641367), vec2(0.14383161, -0.14100790)
);
// In the loop:
shadow = 0.0; // Reset to accumulate lit
for (int i = 0; i < numSamples; i++) {
vec2 offset = poissonDisk[i] * shadowRadius * texelSize;
// Optional: Rotate disk for variety (reduces repetition)
float angle = fract(sin(dot(shadowCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) * 6.2832;
offset = vec2(cos(angle) * offset.x - sin(angle) * offset.y, sin(angle) * offset.x + cos(angle) * offset.y);
float d = unpackRGBAToDepth(texture(sunShadowMap, shadowCoord.xy + offset));
shadow += (shadowCoord.z > d + bias) ? 0.0 : 1.0;
}
shadow /= float(numSamples);
} // dithering shadows
}
// Real shadow receive
//float shadow = getShadow(); // from shadowmask chunk
if(daytime>0.5) {
color *= (shadow * 0.6 + 0.6); // shadow factor + ambient day
} else {
color *= (shadow * 0.3 + 0.9); // shadow factor + ambient night
}
} // shadowOn>=1
// Fog application (using your vFogDepth)
float fogFactor = 1.0 - exp(-fogDensity * fogDensity * vFogDepth * vFogDepth);
color *= max(fogColor,.2) * 2.0;
color = mix(color, fogColor, fogFactor);
//#include <fog_fragment>
if(shadowOn>=2) {
gl_FragColor = vec4(vec3(shadow), albedo.a);
} else {
gl_FragColor = vec4(color, albedo.a);
}
//gl_FragColor = vec4(rough, rough, rough, 1.0);
//gl_FragColor = vec4(vViewNormal, 1.0);
}
`;
const vertexShaderFirework = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShaderFirework = `
uniform sampler2D diffuseMapFirework;
uniform vec2 u_offset;
uniform vec2 u_repeat;
uniform float u_opacity;
varying vec2 vUv;
void main() {
vec2 uv = vUv * u_repeat + u_offset;
vec4 diffuseColor = texture2D(diffuseMapFirework, uv);
vec3 color = diffuseColor.rgb * 2.0;
float alpha = diffuseColor.a * u_opacity;
if (alpha < 0.3) discard;
gl_FragColor = vec4(color, alpha);
}
`;
const fragmentShaderFireworkDEBUG = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 1.0, 0.8); // solid bright magenta
}
`;
// Classic Perlin Noise implementation (3D for terrain variation)
class ClassicalNoise {
constructor(r = Math) {
this.grad3 = [[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],
[1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],
[0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]];
this.p = [];
for (let i = 0; i < 256; i++) {
this.p[i] = Math.floor(r.random() * 256);
}
this.perm = [];
for (let i = 0; i < 512; i++) {
this.perm[i] = this.p[i & 255];
}
}
dot(g, x, y, z) {
return g[0] * x + g[1] * y + g[2] * z;
}
mix(a, b, t) {
return (1.0 - t) * a + t * b;
}
fade(t) {
return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}
noise(x, y, z) {
let X = Math.floor(x), Y = Math.floor(y), Z = Math.floor(z);
x -= X; y -= Y; z -= Z;
X &= 255; Y &= 255; Z &= 255;
let gi000 = this.perm[X + this.perm[Y + this.perm[Z]]] % 12;
let gi001 = this.perm[X + this.perm[Y + this.perm[Z + 1]]] % 12;
let gi010 = this.perm[X + this.perm[Y + 1 + this.perm[Z]]] % 12;
let gi011 = this.perm[X + this.perm[Y + 1 + this.perm[Z + 1]]] % 12;
let gi100 = this.perm[X + 1 + this.perm[Y + this.perm[Z]]] % 12;
let gi101 = this.perm[X + 1 + this.perm[Y + this.perm[Z + 1]]] % 12;
let gi110 = this.perm[X + 1 + this.perm[Y + 1 + this.perm[Z]]] % 12;
let gi111 = this.perm[X + 1 + this.perm[Y + 1 + this.perm[Z + 1]]] % 12;
let n000 = this.dot(this.grad3[gi000], x, y, z);
let n100 = this.dot(this.grad3[gi100], x - 1, y, z);
let n010 = this.dot(this.grad3[gi010], x, y - 1, z);
let n110 = this.dot(this.grad3[gi110], x - 1, y - 1, z);
let n001 = this.dot(this.grad3[gi001], x, y, z - 1);
let n101 = this.dot(this.grad3[gi101], x - 1, y, z - 1);
let n011 = this.dot(this.grad3[gi011], x, y - 1, z - 1);
let n111 = this.dot(this.grad3[gi111], x - 1, y - 1, z - 1);
let u = this.fade(x), v = this.fade(y), w = this.fade(z);
let nx00 = this.mix(n000, n100, u);
let nx01 = this.mix(n001, n101, u);
let nx10 = this.mix(n010, n110, u);
let nx11 = this.mix(n011, n111, u);
let nxy0 = this.mix(nx00, nx10, v);
let nxy1 = this.mix(nx01, nx11, v);
return this.mix(nxy0, nxy1, w);
}
}
// linear(k): Returns k unchanged (constant speed)
function linear(k) {
return k;
}
// easeIn(k): Starts slow and speeds up (e.g., using a power function).
function easeIn(k) {
return Math.pow(k, 2); // Quadratic ease-in
}
// easeOut(k): Starts fast and slows down (a common natural effect, similar to the continuous lerp approach).
function easeOut(k) {
return 1 - Math.pow(1 - k, 2); // Quadratic ease-out
}
// easeInOut(k): Starts slow, speeds up in the middle, and slows down at the end.
function easeInOut(k) {
return k < 0.5 ? 2 * Math.pow(k, 2) : 1 - Math.pow(1 - k, 2) * 2; // Quadratic ease-in-out
// A common alternative using Math.sin for a smoother curve:
// return 0.5 * (Math.sin((k - 0.5) * Math.PI) + 1);
}
// standard lerp t=0-1
function lerp(start,end,t) {
return start * (1 - t) + end * t;
}
// To implement a smoothstep version of your custom lerp in C# for 2026, you can use the standard cubic Hermite interpolation formula. This function creates an "S-curve" that starts and ends slowly, making transitions feel more natural
function SmoothStep(start,end,t) {
// Clamp t between 0 and 1 to prevent overshoot
if(t>1.0) t=1.0;
else if(t<0.0) t=0.0;
// The smoothstep cubic formula: 3t^2 - 2t^3
t = t * t * (3.0 - 2.0 * t);
// Standard interpolation using the smoothed 't'
//return start * (1f - t) + end * t;
return lerp(start,end,t);
}
const radian=Math.PI / 180.0;
const $=function(id) { return document.getElementById(id); }
const rndMinMax=function(min,max) { return Math.floor(Math.random()*(max-min+1)+min); }
const perlin = new ClassicalNoise();
// Scene setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
camera.position.set(-21.15, 60.31, -195.56); // Flying height
//camera.position.set(-46.15, 1908.31, -2053.56); // far view debug
//camera.lookAt(0, 0, -100); // Look forward along -z
//camera.rotation.set(-6.0*radian, 180.0*radian, 0.0); // (if not using OrbitControls)
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
//renderer.sortObjects = false;
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(.0+camera.position.x, .5+camera.position.y, 5.+camera.position.z);
// alternative -- see bottom controls.lock~ cursor lock
//const controls = new PointerLockControls( camera, renderer.domElement );
camera.updateProjectionMatrix();
function getDirection(theta, phi) { // dir = return
let theta_left = theta + 90.0;
let phi_up = phi - 90.0;
if(theta_left>180.0) theta_left -=360.0;
if(phi_up<-180.0) phi_up += 360.0;
//const x=Math.sin(90.0 * radian) * Math.cos(theta_left * radian);
//const y=Math.cos(90.0 * radian);
//const z=Math.sin(90.0 * radian) * Math.sin(theta_left * radian);
const x=Math.sin(phi_up * radian) * Math.cos(theta * radian);
const y=Math.cos(phi_up * radian);
const z=Math.sin(phi_up * radian) * Math.sin(theta * radian);
//const x=Math.sin(phi * radian) * Math.cos(theta * radian);
//const y=Math.cos(phi);
//const z=Math.sin(phi * radian) * Math.sin(theta * radian);
//Vec3 XZonly={ // for player full directional speed not Y
// cos(theta * radian),
// 0,
// sin(theta * radian)
//};
return new THREE.Vector3(x,y,z);
}
var sunTime=getDirection(170,90); // theta orient around horizontally, phi vertical/altitude
var moonTime=getDirection(-170,-90); // theta orient around horizontally, phi vertical/altitude
// sunrise/sunset = phi at 180, above ground at 170, below ground at 190
// front facing with camera at 90, back facing at -90 or 270
// still need the light position itself
// Lights (PBR-friendly)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8); // Increased ambient for better brightness
scene.add(ambientLight);
const sunLight = new THREE.DirectionalLight(0xffffff, 1.5); // Increased intensity
//sunLight.position.set(100, 100, 100); // Sun position for shadows/lighting (match sunDirection in skyMaterial)
sunLight.position.set(sunTime.x*8000,sunTime.y*8000,sunTime.z*8000); // Sun position from sphere coords
scene.add(sunLight);
sunLight.target = new THREE.Object3D();
sunLight.target.position.set(0, 0, 0); // world origin or camera ground
sunLight.target.updateMatrixWorld();
//const helper=new THREE.CameraHelper(sunLight.shadow.camera);
//scene.add(helper);
// shadow maps
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // nicer
const shadowBias = -0.0001; // slight negative for acne
const shadowNormalBias = 0.1; // for bumpy terrain/mountains
const shadowRadius = 4; // softens edges (if using BasicShadowMap, ignore for PCF)
const shadowMapWidth = 4096;
const shadowMapHeight = 4096;
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = shadowMapWidth;
sunLight.shadow.mapSize.height = shadowMapHeight;
sunLight.shadow.bias = shadowBias;
sunLight.shadow.normalBias = shadowNormalBias;
sunLight.shadow.radius = shadowRadius;
// Base frustum (will update dynamically in animate)
const shadowCamWidth = 12000; // covers ~1.5 tiles wide (tune if too small/large)
const shadowCamHeight = 12000; // covers height variation
const shadowCamFar = 6000; // light-to-ground distance + margin
const shadowCamNear = -6000; // close to light, avoids near-clip issues
sunLight.shadow.camera = new THREE.OrthographicCamera(
-shadowCamWidth / 2,
shadowCamWidth / 2,
shadowCamHeight / 2,
-shadowCamHeight / 2,
shadowCamNear,
shadowCamFar
);
//sunLight.shadow.camera.updateProjectionMatrix();
//helper.update();
// Sun plane (red)
/*const sunShadowPlane = new THREE.Mesh(
new THREE.PlaneGeometry(6000, 6000),
new THREE.MeshBasicMaterial({ color: 0xff0000, transparent: true, opacity: 0.3, side: THREE.DoubleSide })
);
scene.add(sunShadowPlane);*/
// Moon shadow plane (green)
scene.fog = new THREE.FogExp2(0xaaccff, 0.0008); // color ≈ sky horizon, very low density
// Tune density: 0.00005 – 0.00015 depending on how fast you want fade
// Color should roughly match your sky bottomColor or horizon
const skyColors={
topColor: [
[100,94,160], // morning 1
[100,106,190], // day begin 3
[101,115,185], // midday 5
[100,105,170], // day end 3
[55,35,140], // evening 1
[14,7,60], // night begin 3
[1,0,35], // midnight 5
[8,10,56], // night end 3
],
bottomColor: [
[165,145,175],// morning
[164,178,160],// day begin
[165,174,204],// midday
[178,170,180],// day end
[215,87,137], // evening
[130,30,45], // night begin
[20,10,30], // midnight
[25,40,75], // night end
],
};
// calculate the 86400 seconds to 4 segments time,
// but not equally sized? day/night = 16hr~, morning/evening=8hr~
// 86400/4 = 21600
// 16/2 = 8 * 60 * 60 = 28800 / 2 = 14400 (4hr)
//
// morning= 1 to 14400 (+14400)
// day= 14401 to 43200 (+28800)
// evening= 43201 to 57600 (+14400)
// night= 57601 to 86400 (+28800)
//
// cont in animate
var cDensity=0.5;
var skyDepthOn=true;
// Sky sphere (WoW-like gradient with sun)
const skyGeometry = new THREE.SphereGeometry(3000, 32, 24); // 3000 with depth write to make 'growing in' muntains
const skyMaterial = new THREE.ShaderMaterial({
uniforms: {
//topColor: { value: new THREE.Color(0x0077ff) }, // Blue sky
//bottomColor: { value: new THREE.Color(0xffffff) }, // Horizon haze
topColor: { value: new THREE.Color(0x6677ff) }, // Blue sky
bottomColor: { value: new THREE.Color(0xffeeff) }, // Horizon haze
moonColor: { value: new THREE.Color(0xeeffff) }, // Sun glow
sunColor: { value: new THREE.Color(0xfffeee) }, // Sun glow
//sunColor: { value: new THREE.Color(0xffa600) }, // Sun glow
//sunDirection: { value: new THREE.Vector3(0.2, 0.2, 1).normalize() }, // Sun pos (normalized)
sunDirection: { value: sunTime.normalize() }, // Sun pos (normalized) from sphere coords
//moonDirection: { value: moonTime.normalize() }, // Sun pos (normalized) from sphere coords
sunSize: { value: 0.00125 }, // Sun disc size
moonSize: { value: 0.0025 }, // Sun disc size
xtime: { value: 0.0 },
daytime: { value: 0.0 },
cloudDensity: { value: 0.5 }, // Lower base fluff
cloudAbsorption: { value: 0.01 }, // Much lower = less darkening, trans >0
cloudScale: { value: 0.0015 }, // Finer detail
cloudSpeed: { value: 0.0003 }, // Slower drift
cloudBottom: { value: 200.0 }, // Closer for testing visibility
cloudThickness: { value: 50.0 }, // Thinner layer = less accum up
scatteringAniso: { value: 0.7 }, // Stronger rims
maxSteps: { value: 6 }, // Higher for horizon detail
lightSteps: { value: 2 }, // Short shadows
marchSize: { value: 40.0 }, // Smaller = finer, but balance perf
frame: { value: 0.0 },
cloudsOn: { value: 2 }, // match with cloudquality startup
starsOn: { value: 1 },
lightDir: { value: sunLight.position.normalize() },
lightDir2: { value: sunLight.position.normalize().negate() },
nMatrix: { value: new THREE.Matrix3() }, // Initialize empty mat3
vMatrix: { value: new THREE.Matrix4() }, // View matrix
mvpMatrix: { value: new THREE.Matrix4() },
cloudsReady: { value: (loading==-1)?1:0 }, // wait render clouds for loading screen
cDensity: { value: cDensity }, // multiplier 0.0-1.0
// Example colors — warm sunrise/sunset orange → soft yellow, or cool moon blue
horizonGlowColorDay: { value: new THREE.Color(1.0, 0.6, 0.3) }, // warm sun-like
horizonGlowColorNight: { value: new THREE.Color(0.4, 0.6, 1.0) }, // alternative: cool moon
horizonGlowIntensity: { value: 1.0 }, // 0.3–1.2 range, subtle to strong
horizonGlowHeight: { value: 0.25 }, // Where sharpness peak ~ middle (0.3–0.6)
horizonGlowSharpness: { value: 6.0 }, // 2.0 soft → 8.0+ sharp edge
horizonNoiseScale: { value: 12.0 }, // Noise frequency (higher = finer)
horizonNoiseStrength: { value: 0.12 }, // 0.05–0.25 subtle variation
horizonGlowOn: { value: 1 },
},
vertexShader: vertexShaderSky,
fragmentShader: fragmentShaderSky,
side: THREE.BackSide, // Inside out
depthWrite: skyDepthOn,
fog: false,
});
const sky = new THREE.Mesh(skyGeometry, skyMaterial);
scene.add(sky);
sky.rotation.set(0.0,0.0,90.0*radian);
const normalMatrix = new THREE.Matrix3(); // create once and reuse
const mvpMatrix = new THREE.Matrix4();
const customDepthMat = new THREE.ShaderMaterial({ // used with sky
uniforms: {
//uAlphaMap: { value: myAlphaTexture },
uAlphaThreshold: { value: 0.3 },
sunDirection: { value: sunTime.normalize() }, // Sun pos (normalized) from sphere coords
//moonDirection: { value: moonTime.normalize() }, // Sun pos (normalized) from sphere coords
sunSize: { value: 0.00125 }, // Sun disc size
moonSize: { value: 0.0025 }, // Sun disc size
xtime: { value: 0.0 },
daytime: { value: 0.0 },
lightDir: { value: sunLight.position.normalize() },
lightDir2: { value: sunLight.position.normalize().negate() },
nMatrix: { value: new THREE.Matrix3() }, // Initialize empty mat3
vMatrix: { value: new THREE.Matrix4() }, // View matrix
mvpMatrix: { value: new THREE.Matrix4() },
cDensity: { value: cDensity }, // multiplier 0.0-1.0
cloudsOn: { value: 2 }, // match with cloudquality startup
},
//vertexShader: vertexShaderClouds2, // Re-use your main vertex shader for consistency
vertexShader: vertexShaderSky,
fragmentShader: fragmentShaderShadow,
side: THREE.DoubleSide, // Inside out?
transparent: false, // clip instead
alphaTest: 0.3, // same as threshold
depthWrite: true,
});
// simple 2d clouds
//const cloudsGeometry = new THREE.SphereGeometry(3100, 32, 24); // more than skybox needs renderorder and depthtest false
const cloudsGeometry = new THREE.SphereGeometry(2950, 32, 24); // 3000 with depth write to make 'growing in' muntains
const cloudsMaterial = new THREE.ShaderMaterial({
uniforms: {
sunDirection: { value: sunTime.normalize() }, // Sun pos (normalized) from sphere coords
//moonDirection: { value: moonTime.normalize() }, // Sun pos (normalized) from sphere coords
sunSize: { value: 0.00125 }, // Sun disc size
moonSize: { value: 0.0025 }, // S1un disc size
xtime: { value: 0.0 },
daytime: { value: 0.0 },
lightDir: { value: sunLight.position.normalize() },
lightDir2: { value: sunLight.position.normalize().negate() },
nMatrix: { value: new THREE.Matrix3() }, // Initialize empty mat3
vMatrix: { value: new THREE.Matrix4() }, // View matrix
mvpMatrix: { value: new THREE.Matrix4() },
cDensity: { value: cDensity }, // multiplier 0.0-1.0
},
//vertexShader: vertexShaderClouds2,
//fragmentShader: fragmentShaderClouds2,
vertexShader: vertexShaderSky,
fragmentShader: fragmentShaderShadow, // debug
side: THREE.BackSide, // Inside out
depthWrite: false,
fog: false,
transparent: true,
alphaTest: 0.3,
});
const clouds = new THREE.Mesh(cloudsGeometry, cloudsMaterial);
clouds.visible=false;
//clouds.rotation.copy(sky.rotation);
clouds.renderOrder = 0;
scene.add(clouds); // builtin with skymaterial
//clouds.customDepthMaterial = customDepthMat;
//clouds.customDepthMaterial.shadowSide = THREE.BackSide;
sky.castShadow = true;
sky.customDepthMaterial = customDepthMat;
sky.customDepthMaterial.shadowSide = THREE.FrontSide;
clouds.receiveShadow = false;
sky.receiveShadow = false;
//sky.rotation.set(0.0,0.0,90.0*radian);
// Terrain setup
const terrainGroup = new THREE.Group();
scene.add(terrainGroup);
// LOD config – fine-tuned for your scale (4000 tile size, flying speed ~50)
const lodLevels = [
{ maxDist: 3000, segments: 256 }, // very close – max detail
{ maxDist: 8000, segments: 128 }, // medium distance
{ maxDist: Infinity, segments: 64 }, // everything else – good enough
// Optional extra low LOD for very far
// { maxDist: Infinity, segments: 32 }
];
var shadowOn=1; // start with shadow
//var shadowOn=0; // start no shadow
const textureLoader = new THREE.TextureLoader();
// Load textures (replace paths; ensure they exist)
const grassTex = textureLoader.load('./textures/grass1.png');
grassTex.wrapS = grassTex.wrapT = THREE.RepeatWrapping;
grassTex.repeat.set(80.0, 80.0); // Increased repeat for larger tiles/better scaling
const rockTex = textureLoader.load('./textures/rock1.png');
rockTex.wrapS = rockTex.wrapT = THREE.RepeatWrapping;
rockTex.repeat.set(80.0, 80.0);
const snowTex = textureLoader.load('./textures/snow.png');
snowTex.wrapS = snowTex.wrapT = THREE.RepeatWrapping;
snowTex.repeat.set(80.0, 80.0);
const normalGrass = textureLoader.load('./textures/grass1_normal.png');
normalGrass.wrapS = normalGrass.wrapT = THREE.RepeatWrapping;
normalGrass.repeat.set(80.0, 80.0);
const normalRock = textureLoader.load('./textures/rock1_normal.png');
normalRock.wrapS = normalRock.wrapT = THREE.RepeatWrapping;
normalRock.repeat.set(80.0, 80.0);
const normalSnow = textureLoader.load('./textures/snow_normal.png');
normalSnow.wrapS = normalSnow.wrapT = THREE.RepeatWrapping;
normalSnow.repeat.set(80.0, 80.0);
const roughGrass = textureLoader.load('./textures/grass1_rough.png');
roughGrass.wrapS = roughGrass.wrapT = THREE.RepeatWrapping;
roughGrass.repeat.set(80.0, 80.0); // Increased repeat for larger tiles/better scaling
const roughRock = textureLoader.load('./textures/rock1_rough.png');
roughRock.wrapS = roughRock.wrapT = THREE.RepeatWrapping;
roughRock.repeat.set(80.0, 80.0);
const roughSnow = textureLoader.load('./textures/snow_rough.png');
roughSnow.wrapS = roughSnow.wrapT = THREE.RepeatWrapping;
roughSnow.repeat.set(80.0, 80.0);
// Removed ORM for now; hardcoded values
const terrainMaterial = new THREE.ShaderMaterial({
//uniforms: {
uniforms: THREE.UniformsUtils.merge([
THREE.UniformsLib.lights,
THREE.UniformsLib.fog,
{
grassTex: { value: grassTex },
rockTex: { value: rockTex },
snowTex: { value: snowTex },
normalGrass: { value: normalGrass },
normalRock: { value: normalRock },
normalSnow: { value: normalSnow },
roughGrass: { value: roughGrass },
roughRock: { value: roughRock },
roughSnow: { value: roughSnow },
repeatScale: { value: 80.0 }, // Match repeat
lightDir: { value: sunLight.position.normalize() },
lightDir2: { value: sunLight.position.normalize().negate() },
daytime: { value: 0.0 },
cameraPosition: { value: camera.position.clone() }, // Match repeat
fogColor: { value: new THREE.Color(0xffeeff) }, // Horizon haze
fogDensity: { value: 0.0008 },
// Manual shadow map for sun (first directional light)
sunShadowMap: { value: null }, // will set in animate
sunShadowMatrix: { value: new THREE.Matrix4() }, // will set in animate
shadowBias: { value: shadowBias },
shadowNormalBias: { value: shadowNormalBias }, // pass your const 0.1
shadowRadius: { value: shadowRadius },
shadowOn: { value: shadowOn },
}]),
//},
vertexShader: vertexShaderTerrain,
fragmentShader: fragmentShaderTerrain,
//side: THREE.DoubleSide,
side: THREE.FrontSide,
fog: true,
lights: true,
});
//terrainMaterial.wireframe = true;
// Function to get height at world position (using same noise)
function getHeightAt(worldX, worldZ) {
return heightScale * (perlin.noise(worldX / noiseScale, worldZ / noiseScale, 0) + 0.5);
}
// Function to create/update a tile
function createTile(zOffset, segments) {
const geometry = new THREE.PlaneGeometry(tileSize, tileSize, segments, segments);
geometry.rotateX(-Math.PI / 2);
const mesh = new THREE.Mesh(geometry, terrainMaterial);
mesh.castShadow = true;
mesh.position.z = zOffset;
mesh.userData.segments = segments; // for debug/info if needed
updateTileHeights(mesh); // extract height update to separate function
return mesh;
}
function updateTileHeights(tile) {
const geometry = tile.geometry;
const vertices = geometry.attributes.position.array;
for (let i = 0; i < vertices.length; i += 3) {
const worldX = vertices[i] + tile.position.x;
const worldZ = vertices[i + 2] + tile.position.z;
vertices[i + 1] = getHeightAt(worldX, worldZ);
}
geometry.attributes.position.needsUpdate = true;
geometry.computeVertexNormals();
}
var mountainMaterial=null; // shared, initial only
function createCustomMountainMaterial(originalMat) {
if(mountainMaterial==null) {
//return new THREE.ShaderMaterial({
mountainMaterial=new THREE.ShaderMaterial({
//uniforms: {
uniforms: THREE.UniformsUtils.merge([
THREE.UniformsLib.lights,
THREE.UniformsLib.fog,
{
map: { value: originalMat.map },
normalMap: { value: originalMat.normalMap },
roughnessMap: { value: originalMat.roughnessMap },
// Add more if needed: metalnessMap, aoMap, etc.
lightDir: { value: sunLight.position.normalize() },
lightDir2: { value: sunLight.position.normalize().negate() },
daytime: { value: 0.0 },
cameraPosition: { value: camera.position.clone() },
fogColor: { value: new THREE.Color(0xffeeff) }, // Horizon haze
fogDensity: { value: 0.0008 },
sunShadowMap: { value: null }, // will set in animate
sunShadowMatrix: { value: new THREE.Matrix4() }, // will set in animate
shadowBias: { value: shadowBias },
shadowNormalBias: { value: shadowNormalBias }, // pass your const 0.1
shadowRadius: { value: shadowRadius },
shadowOn: { value: shadowOn },
}]),
//},
vertexShader: vertexShaderMountain,
fragmentShader: fragmentShaderMountain,
side: THREE.FrontSide,
fog: true,
lights: true,
});
}
return mountainMaterial;
}
var loading=0;
var mountainlist=[
{ name: "mountain_1", ref_lod0: null, ref_lod1: null },
{ name: "mountain_2", ref_lod0: null, ref_lod1: null },
{ name: "mountain_3", ref_lod0: null, ref_lod1: null },
{ name: "mountain_4", ref_lod0: null, ref_lod1: null },
{ name: "mountain_5", ref_lod0: null, ref_lod1: null },
{ name: "mountain_6", ref_lod0: null, ref_lod1: null },
{ name: "mountain_7", ref_lod0: null, ref_lod1: null },
{ name: "mountain_8", ref_lod0: null, ref_lod1: null },
{ name: "mountain_9", ref_lod0: null, ref_lod1: null },
{ name: "mountain_10", ref_lod0: null, ref_lod1: null },
{ name: "mountain_11", ref_lod0: null, ref_lod1: null },
{ name: "mountain_12", ref_lod0: null, ref_lod1: null },
{ name: "mountain_13", ref_lod0: null, ref_lod1: null },
{ name: "mountain_14", ref_lod0: null, ref_lod1: null },
{ name: "mountain_15", ref_lod0: null, ref_lod1: null },
{ name: "mountain_16", ref_lod0: null, ref_lod1: null },
];
var mountainReady=false;
function load_mountains() {
const loader = new GLTFLoader();
const promises = [];
for(let i=0;i<mountainlist.length;i++) {
const url = './models/mountainpack/' + mountainlist[i]['name']+".gltf";
const url_low = './models/mountainpack/' + mountainlist[i]['name']+"_lod1.gltf";
// Push a promise into the array that also carries the index 'i' in its resolved value
promises.push(
loader.loadAsync(url).then(gltf => ({ gltf, index: i, lod: 0 }))
);
promises.push(
loader.loadAsync(url_low).then(gltf => ({ gltf, index: i, lod: 1 }))
);
}
Promise.all(promises)
.then(results => {
// results is an array like: [{ gltf: ..., index: 0 }, { gltf: ..., index: 1 }, ...]
console.log("All models loaded (mountains)");
results.forEach(({ gltf, index, lod }) => {
console.log("Processing index:", index);
console.log("Model name:", mountainlist[index]['name']+" LOD-"+lod);
// Add your logic here using 'index' and 'gltf'
mountainlist[index]['ref_lod'+lod] = gltf.scene;
if(lod==0) {
mountainlist[index]['ref_lod'+lod].traverse(child => {
if (child.isMesh && child.material) {
child.castShadow = true; // prep for shadows later
child.material = createCustomMountainMaterial(child.material);
}
});
} else {
mountainlist[index]['ref_lod'+lod].traverse(child => {
//if (child.isMesh && child.material) {
if (child.isMesh) {
//child.material = child.material.clone(); // separate material
//--child.material.transparent = true;
//--child.material.opacity = 0; // start hidden
child.visible = false; // start hidden
child.castShadow = true; // prep for shadows later
child.material = createCustomMountainMaterial(child.material);
}
});
}
});
console.log("All models processed.(mountains)");
mountainReady=true;
initTiles();
loading=-1;
$('loadtxt').innerHTML="Loading...";
$('loading').style.opacity=0;
$('loading').style.pointerEvents="none";
if(cloudQuality==4) {
clouds.visible=true;
}
skyMaterial.uniforms.cloudsReady.value = 1;
setTimeout(function() { $('loading').style.display="none"; },1000);
})
.catch(error => {
console.error("An error occurred during loading:", error);
});
}
load_mountains();
const spacing=500.0; // clear the center path
// Function to add mountains to a tile (called on create and recycle)
function addMountainsToTile(tile) {
if (!mountainReady) return;
//const mountainGeo = new THREE.ConeGeometry(20, 40, 32); // Larger mountains
//const mountainMat = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.8, metalness: 0 });
const spawnC=rndMinMax(2,44);
for (let i = 0; i < spawnC; i++) { // Add a few per tile
const posX = rndMinMax(spacing, tileSize / 2) * (Math.random() > 0.5 ? 1 : -1);
const posZ = Math.random() * tileSize - tileSize / 2;
const height = getHeightAt(posX + tile.position.x, posZ + tile.position.z);
const pick = Math.floor(Math.random() * mountainlist.length);
const scale = rndMinMax(150, 250);
const mountain = deepCloneGeometry(mountainlist[pick]['ref_lod1']);
mountain.position.set(posX, height - 50, posZ); // Slightly below surface
mountain.rotation.y = Math.random() * Math.PI * 2; // Random rotate
//mountain.scale.set(scale * 0.01, scale * 0.01, scale * 0.01); // start tiny
mountain.scale.set(scale, scale, scale);
mountain.userData = {
pick: pick,
targetScale: scale,
lod0: mountainlist[pick].ref_lod0, // reference to high detail
lod1: mountainlist[pick].ref_lod1, // reference to low
currentLod: 1, // 1 = low (current)
switchDist: 2000, // switch to high below this
fadeDist: 6000, // fading dist for opacity (4000) - currently "pop" dist (6000)
visible: false // start hidden
};
// Start fully transparent
mountain.traverse(child => {
//if (child.isMesh && child.material) {
if (child.isMesh) {
//--child.material.transparent = true;
//--child.material.opacity = 0;
//child.renderOrder = 5;
child.visible = false;
}
});
//mountain.renderOrder = 5;
tile.add(mountain);
}
}
// Initialize tiles
const tiles = [];
const zInitOffset=4000.0;
function initTiles() {
for (let i = 0; i < numTiles; i++) {
const tile = createTile(i * tileSize - (numTiles / 2 * tileSize) - zInitOffset, lodLevels[0].segments);
//tile.renderOrder = 3;
addMountainsToTile(tile);
tiles.push(tile);
terrainGroup.add(tile);
}
}
let fireworkTemplate = null; // ← global
let fireworkAnimations = []; // ← global array of clips
const mixers = []; // Array for per-instance AnimationMixers
const fwmaterials = {}; // Store refs
const fwOffsetY=-5.0;
function load_fireworks() {
const loader = new GLTFLoader();
loader.load('./models/fireworks2/fireworks3.gltf', (gltf) => {
fireworkTemplate = gltf.scene;
fireworkAnimations = gltf.animations || []; // save animations here
fireworkTemplate.scale.set(50.0,50.0,50.0);
// fix rotation in blender instead and re-export~
// if need rotation fixing, make empty non animated parent and try these
// THIS IS THE MAGIC LINE
//fireworkTemplate.rotation.y = Math.PI; // –90° instead of +90° (Blender Z-up → Three Y-up)
// AND THIS ONE — re-orients the animation tracks themselves
//fireworkTemplate.traverse((child) => {
// if (child.isBone || child.isObject3D) {
// child.quaternion.premultiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1,0,0), Math.PI));
// }
//});
fireworkTemplate.traverse((child) => {
if (child.isMesh) {
//if(!firstSca) {
child.scale.setScalar(1); // force initial scale to 1 so baked tracks start from visible size
//} else { // mp3 page
// child.scale.setScalar(64); // force initial scale to 1 so baked tracks start from visible size
//}
}
});
// Optional but helps with bounding boxes
fireworkTemplate.updateMatrixWorld(true);
// Traverse to log/access objects/materials for debugging
fireworkTemplate.traverse((child) => {
if (child.isMesh) {
console.log('Mesh:', child.name, 'Material:', child.material ? child.material.name : 'None');
}
});
fireworkAnimations.forEach((clip, idx) => {
console.log(`Clip ${idx} tracks:`, clip.tracks.map(t => t.name));
clip.tracks.forEach(track => {
if (track.name.includes('.scale') && track.values.length >= 3) {
track.values[0] = 1; // X
track.values[1] = 1; // Y
track.values[2] = 1; // Z first keyframe
console.log('Modified initial scale for', track.name);
}
});
});
console.log('Firework model loaded – ready to spawn!');
// optional: spawn one immediately for testing
// spawnFirework(somePos, someNorm);
}, undefined, (error) => console.error(error));
}
load_fireworks();
const atlasTexture = new THREE.TextureLoader().load('./models/fireworks2/finalworks.png');
// atlasTexture.flipY = false; // Uncomment if UVs invert
atlasTexture.magFilter = THREE.LinearFilter; // Or Nearest for pixel art
let baseFireworkMaterial = new THREE.ShaderMaterial({
uniforms: {
diffuseMapFirework: { value: atlasTexture },
u_offset: { value: new THREE.Vector2(0, 0) },
u_repeat: { value: new THREE.Vector2(0.05, 0.05) }, // 1/20
u_opacity: { value: 1.0 }
},
vertexShader: vertexShaderFirework,
fragmentShader: fragmentShaderFirework,
//fragmentShader: fragmentShaderFireworkDEBUG,
transparent: true,
depthWrite: false, // Helps with blending in space
side: THREE.DoubleSide // If models need backface
});
baseFireworkMaterial.blending = THREE.AdditiveBlending; // glowy fireworks!
baseFireworkMaterial.depthWrite = false;
//--baseFireworkMaterial.dispose(); // Free old program
// Re-create from scratch (copy-paste your original definition)
//--baseFireworkMaterial = new THREE.ShaderMaterial({
//-- uniforms: {
//-- diffuseMapFirework: { value: atlasTexture },
//-- u_offset: { value: new THREE.Vector2(0, 0) },
//-- u_repeat: { value: new THREE.Vector2(0.05, 0.05) },
//-- u_opacity: { value: 1.0 }
//-- },
//-- vertexShader: vertexShaderFirework,
//-- fragmentShader: fragmentShaderFirework,
//-- //fragmentShader: fragmentShaderFireworkDEBUG,
//-- transparent: true,
//-- transparent: true,
//-- depthWrite: false,
//-- side: THREE.DoubleSide,
//-- blending: THREE.AdditiveBlending
//--});
// NEW: Distance-based brightness boost (makes far fireworks pop)
/*baseFireworkMaterial.onBeforeCompile = (shader) => {
shader.uniforms.cameraPos = { value: camera.position };
shader.uniforms.distanceScale = { value: 4.0 }; // Tweak 1.5–4.0
shader.fragmentShader = shader.fragmentShader.replace(
'#include <fog_fragment>',
`
#include <fog_fragment>
float dist = length(vViewPosition);
float boost = 1.0 + (dist / 50.0) * distanceScale; // Adjust 50.0 to your orbit distance
gl_FragColor.rgb *= boost;
`
);
};*/
const activeFireworks = [];
const clock = new THREE.Clock(); // Global
const fwFPS = 5; // Low as mentioned
const ANIM_COLS = 4;
const FRAMES_PER_ANIM = 5;
const TOTAL_FRAME_COLS = ANIM_COLS * FRAMES_PER_ANIM; // 20
function updateFireworkUV(fw, globalTime) {
const age = globalTime - fw.startTime;
const phase = age < fw.launchDuration ? 'ascent' :
age < fw.launchDuration + fw.explodeDuration ? 'explode' : 'fade';
let localFrame = 0;
if (phase === 'ascent') {
localFrame = Math.floor(age * fwFPS); // Normal for trail/ball
} else if (phase === 'explode') {
localFrame = Math.floor((age - fw.launchDuration) * fwFPS);
} else { // Fade: Slow to last frames or blank
localFrame = 23; // Freeze on last frame, or set to 0 for first/blank
}
const frame = localFrame % 24;
const frameCol = frame % FRAMES_PER_ANIM;
const frameRow = Math.floor(frame / FRAMES_PER_ANIM);
fw.instance.traverse((child) => {
if (child.isMesh && child.material && child.material.userData.animIndex !== undefined) {
let animIndex = child.material.userData.animIndex;
// Caps: Freeze/hide during ascent, blank on fade
if (animIndex >= 3 && animIndex <= 12) { // Stars/trails range
if (phase === 'ascent') {
animIndex = 15; // Switch to blank anim slot during ascent
} else if (phase === 'fade') {
animIndex = 15; // Blank for off
}
}
const animCol = animIndex % ANIM_COLS;
const animRow = Math.floor(animIndex / ANIM_COLS);
const globalCol = animCol * FRAMES_PER_ANIM + frameCol;
const globalRowFromTop = animRow * FRAMES_PER_ANIM + frameRow;
const offsetX = globalCol / TOTAL_FRAME_COLS;
const offsetY = (TOTAL_FRAME_COLS - 1 - globalRowFromTop) / TOTAL_FRAME_COLS;
child.material.uniforms.u_offset.value.set(offsetX, offsetY);
// Opacity fade (already in your loop, but sync here if needed)
if (phase === 'fade') {
const fadeProgress = (age - fw.launchDuration - fw.explodeDuration) / 5; // 5s fade
child.material.uniforms.u_opacity.value = Math.max(0, 1 - fadeProgress);
} else if (phase === 'ascent' && age < 0.5) {
child.material.uniforms.u_opacity.value = age / 0.5 * 0.7 + 0.3;
}
}
});
}
function spawnFirework(localPos, tile, starVariant = Math.floor(Math.random() * 5), trailVariant = Math.floor(Math.random() * 5)) {
if (!fireworkTemplate) {
console.warn("Firework model not loaded yet!");
return;
}
const instance = fireworkTemplate.clone();
instance.position.copy(localPos);
//instance.lookAt(instance.position.clone().add(localNorm));
tile.add(instance);
// DEBUG LINE — shoots 10 units along normal so you can see the exact spot
//const lineMaterial = new THREE.LineBasicMaterial({ color: 0x00ffff, linewidth: 3 });
//const lineGeo = new THREE.BufferGeometry().setFromPoints([
// new THREE.Vector3(0,0,0),
// localNorm.clone().multiplyScalar(10) // 10 units long cyan line
//]);
//const line = new THREE.Line(lineGeo);
//earth.add(line); // attached to the firework root
// Mixer for launch anim (same as before)
const mixer = new THREE.AnimationMixer(instance);
if (fireworkAnimations.length > 0) {
let clip = THREE.AnimationClip.findByName(fireworkAnimations, 'launch') || fireworkAnimations[0];
if (clip) {
const action = mixer.clipAction(clip);
action.setLoop(THREE.LoopOnce);
action.clampWhenFinished = true;
action.play();
action.getClip().tracks.forEach(track => {
if (track.name.includes('.scale')) {
console.log('Scale track:', track.name, 'values:', track.values); // See if keys are non-1
}
});
}
}
// Material overrides (unchanged)
instance.traverse((child) => {
if (child.isMesh && child.material) {
child.renderOrder = 10; // Higher than clouds (assume clouds=0)
const matName = child.material.name.toLowerCase();
child.material = baseFireworkMaterial.clone();
if (matName.includes('fireball')) child.material.userData.animIndex = 0;
else if (matName.includes('firetrail')) child.material.userData.animIndex = 1;
else if (matName.includes('firestar')) {
child.material.userData.animIndex = 3 + starVariant;
} else if (matName.includes('startrail')) {
child.material.userData.animIndex = 8 + trailVariant;
} else {
child.material.userData.animIndex = 15; // Fallback to blank/empty anim slot
}
}
});
activeFireworks.push({
instance: instance,
mixer: mixer,
startTime: clock.getElapsedTime(),
launchDuration: 1.8, // Match your ascent time in seconds
explodeDuration: 5.0, // How long the full explosion lasts
totalDuration: 8.0, // Total visible time before fade
tileRef: tile,
});
}
var aSFT;
var currentTime=null;
var baseTime=0;
var timeOffset=0;
function autoSpawnFirework() {
clearTimeout(aSFT);
let seltile=null;
//console.log("auto fireworks initiated");
//let lastDist=-10000.0;
//for(let i=0;i<tiles.length;i++) {
// const tile=tiles[i];
// distance calculation
/*const dx = tile.position.x - camera.position.x;
const dy = tile.position.y - camera.position.y;
const dz = tile.position.z - camera.position.z;
const sum_squares = dx * dx + dy * dy + dz * dz; // Sum of squared differences
const distance = Math.sqrt(sum_squares); // Square root to get distance
console.log("tile z: "+tile.position.z+" tile dist: "+distance);
if(distance>lastDist) lastDist=distance;*/
//};
if(tiles.length>4) {
let sel=3;
//let add=0;
//if(terrainGroupZCount>0) add=1;
if(terrainGroupZ>tileSize*.5) {
sel=2;
}
seltile=tiles[(sel)];
//console.log("seltile="+(sel)+" terrainGroupZ="+terrainGroupZ);
}
if(!seltile) {
aSFT=setTimeout(function() { autoSpawnFirework(); },5000); // try again, not ready
return;
}
currentTime = new Date();
//const currentSimDate = new Date(baseTime + timeOffset);
const currentSimDate = new Date();
//console.log(currentSimDate.getHours());
//const isNYEPeriod = currentSimDate >= new Date(currentTime.getFullYear(), 11, 14, 18) && // Dec 14 18:00 UTC
// currentSimDate < new Date(currentTime.getFullYear(), 11, 15, 18); // Dec 15 18:00 UTC TEST
const isNYEPeriod = currentSimDate >= new Date(currentTime.getFullYear(), 11, 31, 18) || // Dec 31 18:00 UTC (start christmas island)
currentSimDate < new Date(currentTime.getFullYear(), 0, 1, 6); // Jan 1 14:00 UTC (end hawaii) // currentTime is currentSimDate, no need to +1 year
//console.log(currentTime.getMinutes());
//const NYEquiet=(currentTime.getMinutes()<=30)?1.0:0.5; // most active first 30 min past every hour
const NYEquiet=1.0; // full active whole hour
const spawnChance = isNYEPeriod ? 1.0 : 0.1; // 100% during NYE, 10% otherwise
if (Math.random() > spawnChance || Math.random() > NYEquiet || !fireworkTemplate) {
aSFT=setTimeout(function() { autoSpawnFirework(); },3000);
return;
}
//const lat=0;
//const lon=0;
let spawnNum=rndMinMax(1,2);
if(isNYEPeriod) {
if(NYEquiet==1.0) {
//if(!firstSca) {
spawnNum=rndMinMax(5,50);
//} else { // mp3 page
/// spawnNum=rndMinMax(2,15);
//}
//console.log("Is NYE now! (quiet time)");
} else {
spawnNum=rndMinMax(1,10);
//console.log("Is NYE now!");
}
}
const fwsizeZ = tileSize;
const fwsizeX = tileSize / 8;
let fspawn=0;
while(fspawn<spawnNum) {
//const posX = rndMinMax(spacing, tileSize / 2) * (Math.random() > 0.5 ? 1 : -1);
const selected = { x: Math.random() * fwsizeX - fwsizeX / 2, z: Math.random() * fwsizeZ - fwsizeZ / 2 };
//console.dir(selected);
let fwpos={x: 0.0, y: 0.0, z: 0.0 };
fwpos.x = selected.x + rndMinMax(-100,100); // Cluster around city
fwpos.z = selected.z + rndMinMax(-100,100);
fwpos.y = getHeightAt(fwpos.x + seltile.position.x, fwpos.z + seltile.position.z) + fwOffsetY;
const spawnWick=rndMinMax(10,600);
setTimeout(spawnFirework,spawnWick,fwpos,seltile); // fwnorm
fspawn++;
//console.log("new fireworks at x:"+fwpos.x+" y:"+fwpos.y+" z:"+fwpos.z);
}
let nextFwSpawn=rndMinMax(100,5000);
aSFT=setTimeout(function() { autoSpawnFirework(); },nextFwSpawn*1);
}
autoSpawnFirework();
const geometryPool = new Map(); // key = segments, value = PlaneGeometry
function deepCloneGeometry(original) {
//console.log(original);
const clone = original.clone();
// Force new buffers for all attributes that might be modified
for (const name in clone.attributes) {
clone.attributes[name] = clone.attributes[name].clone();
}
if (clone.index) {
clone.index = clone.index.clone();
}
return clone;
}
// In updateTileLODs() – use squared distance to avoid sqrt cost
function updateTileLODs() {
const camPos = camera.position;
tiles.forEach(tile => {
const tileCenter = new THREE.Vector3(0, 0, tile.position.z).applyMatrix4(terrainGroup.matrixWorld);
const distSq = camPos.distanceToSquared(tileCenter);
let targetSegments = lodLevels[lodLevels.length - 1].segments;
for (const level of lodLevels) {
if (distSq < level.maxDist * level.maxDist) {
targetSegments = level.segments;
break;
}
}
if (targetSegments !== tile.userData.segments) {
const oldGeo = tile.geometry;
// Create new geometry
const newGeo = new THREE.PlaneGeometry(tileSize, tileSize, targetSegments, targetSegments);
//const newGeo = deepCloneGeometry(geometryPool.get(segments));
newGeo.rotateX(-Math.PI / 2);
//console.log('Switching tile to segments:', targetSegments, 'clone position count:', newGeo.attributes.position.count);
tile.geometry = newGeo;
tile.userData.segments = targetSegments;
// Re-apply heights & normals
updateTileHeights(tile);
oldGeo.dispose(); // Free memory immediately
}
});
}
let mountainTimer = 0;
const MOUNTAIN_UPDATE_INTERVAL = 0.3; // every 300ms
function updateMountainLODsAndVisibility(dt) {
mountainTimer += dt;
if (mountainTimer < MOUNTAIN_UPDATE_INTERVAL) return;
mountainTimer = 0;
const camPos = camera.position;
terrainGroup.traverse(obj => {
if (!obj.userData.pick) return; // not a mountain
const worldPos = obj.getWorldPosition(new THREE.Vector3());
const dist = camPos.distanceTo(worldPos);
const ud = obj.userData;
// 1. Handle visibility (fade in from far)
const shouldBeVisible = dist < ud.fadeDist;
if (shouldBeVisible && !ud.visible) {
ud.visible = true;
// Trigger fade-in
obj.traverse(child => {
//if (child.isMesh && child.material) {
if (child.isMesh) {
//child.material.opacity = 0;
//--child.material.opacity = 1.0;
child.visible = true;
//child.material.needsUpdate = true;
}
});
}
// 2. Fade opacity
if (ud.visible) {
obj.traverse(child => {
//if (child.isMesh && child.material && child.material.transparent) {
if (child.isMesh) {
if (dist < ud.fadeDist - 200) {
//--child.material.opacity = 1.0;
child.visible = true;
} else {
// Smooth fade in over 800 units
//child.material.opacity = 1.0 - (dist - (ud.fadeDist - 800)) / 800;
//child.material.opacity = Math.max(0, Math.min(1, child.material.opacity));
//--child.material.opacity = 0.0;
child.visible = false;
}
}
});
}
// 3. LOD switch (only when close enough)
const targetLod = dist < ud.switchDist ? 0 : 1;
if (targetLod !== ud.currentLod) {
const parent = obj.parent;
if (!parent) return;
const newModel = targetLod === 0
? ud.lod0.clone()
: ud.lod1.clone();
// Copy transform
newModel.position.copy(obj.position);
newModel.rotation.copy(obj.rotation);
newModel.scale.copy(obj.scale);
// Copy visibility state
newModel.userData = { ...ud, currentLod: targetLod };
if (!shouldBeVisible) {
newModel.traverse(child => {
//if (child.isMesh && child.material) {
if (child.isMesh) {
//--child.material.transparent = true;
//--child.material.opacity = 0;
child.visible = false;
}
});
}
parent.remove(obj);
parent.add(newModel);
}
});
}
var initShadowMaps=false;
function updateShadowCameras() {
if(!initShadowMaps) {
if (sunLight.shadow.map) {
sunLight.shadow.map.texture.wrapS = THREE.ClampToEdgeWrapping;
sunLight.shadow.map.texture.wrapT = THREE.ClampToEdgeWrapping;
sunLight.shadow.map.texture.borderColor = new THREE.Vector4(1, 1, 1, 1); // 1.0 = lit (white), hides excess shadows outside map
// Force update (optional, but good for test)
sunLight.shadow.map.needsUpdate = true;
initShadowMaps=true;
console.log("Shadow map init");
}
}
const terrainZ = terrainGroup.position.z; // track terrain movement
const camZ = camera.position.z;
const centerZ = camZ - terrainZ; // world z-center of visible area
if (sunLight.shadow.map) {
// Sun
const sunPos = sunLight.position.clone();
const sunDir = new THREE.Vector3();
sunLight.getWorldDirection(sunDir);
// Target: world center z, with orbit offset to center frustum during side angles
const orbitOffset = sunDir.clone().multiplyScalar(2000); // pull toward sun, tune 1000–3000
//const sunTarget = new THREE.Vector3(camera.position).add(orbitOffset);
const sunTarget = new THREE.Vector3(camera.position.x, camera.position.y, centerZ).add(orbitOffset);
// Position: behind sun along its dir
//sunLight.shadow.camera.position.copy(sunPos).addScaledVector(sunDir, -shadowCamFar / 2);
// Lock rotation to scene axes (no tilt/moire)
//sunLight.shadow.camera.rotation.set(0, 0, 0);
//sunLight.shadow.camera.up.set(0, 1, 0);
//sunLight.shadow.camera.updateProjectionMatrix();
sunLight.shadow.camera.updateMatrixWorld();
sunLight.shadow.matrix.copy(sunLight.shadow.camera.projectionMatrix).multiply(sunLight.shadow.camera.matrixWorldInverse.clone());
terrainMaterial.uniforms.sunShadowMatrix.value.copy(sunLight.shadow.matrix);
terrainMaterial.uniforms.sunShadowMap.value = sunLight.shadow.map.texture;
if(mountainMaterial!=null) {
mountainMaterial.uniforms.sunShadowMap.value = sunLight.shadow.map.texture;
mountainMaterial.uniforms.sunShadowMatrix.value.copy(sunLight.shadow.matrix);
}
if(typeof sunShadowPlane!=="undefined") {
// Update sun cover plane
sunShadowPlane.position.copy(sunTarget);
sunShadowPlane.rotation.set(-Math.PI / 2, 0, 0); // lay flat on XZ
sunShadowPlane.updateMatrixWorld();
}
}
}
var notices=[];
var nid=-1;
function pushNotice(txt="") {
let ts=new Date().getTime();
let dur=5*1000; // 5 sec
nid++;
let obj={
idx: nid,
ts: ts,
dur: dur,
opa: 100.0,
};
// create html here
let html="<span id=\"notice_"+nid+"\">"+txt+"</span>";
//$('notices').insertAdjacentHTML('beforeend',html);
$('notices').insertAdjacentHTML('afterbegin',html);
notices.push(obj);
}
function updateNotices(ts=0, dt=0) {
if(notices.length==0) return;
for(let i=0;i<notices.length;i++) {
if((notices[i]['ts']+notices[i]['dur'])<ts) {
if(notices[i]['opa']<=0) {
// delete
$('notice_'+notices[i]['idx']).parentNode.removeChild($('notice_'+notices[i]['idx']));
notices.splice(i,1);
i--;
} else {
// fade out
notices[i]['opa']-=100.0 * dt;
$('notice_'+notices[i]['idx']).style.opacity=notices[i]['opa']/100;
}
}
}
}
var dd=null;
var hh=0;
var mm=0;
var ss=0;
var tsp=0;
var ss_theta=0;
var ss_phi=0;
var testval=0;
var tmpColor={};
var testValOn=false;
var testValAuto=true;
var waitDensity=0.0;
var nextDensity=0.0;
var cloudDensityTarget=cDensity;
var windY=0.0; // negative/positive range? make dome full sphere to do windX and windZ as well
var waitWindY=0;
var nextWindY=0;
var windVelocityY=0.0;
let lodUpdateTimer = 0;
const LOD_UPDATE_INTERVAL = 0.25; // seconds
let loadtime=0.0;
let loaddelta=0.0;
let initloading=true;
// Animation loop
let lastTime = 0;
const infoDiv = document.getElementById('info');
let terrainGroupZ=0;
let terrainGroupZCount=0;
let terrainMove=true;
let gradientBG="";
var lastPos=new THREE.Vector3(0.0,0.0,0.0);
var lastRot=new THREE.Vector3(0.0,0.0,0.0);
var infoOpa=300;
function animate(time) {
requestAnimationFrame(animate);
const dt = (time - lastTime) / 1000;
lastTime = time;
loaddelta += dt;
if(loading!=-1 && loaddelta>0.3) {
loadtime += loaddelta;
loaddelta=0.0;
loading++;
if(loading>3) loading=0;
let loadtxt="Loading";
for(let i=0;i<loading;i++) loadtxt+=".";
if(loadtime>12.0) {
loadtxt+="<br /><span style=\"white-space: nowrap; break-word: none; font-size: 10pt;\">(Taking a long time to load models,<br />try refresh in case it got stuck again x))";
}
$('loadtxt').innerHTML=loadtxt;
if(initloading) {
const loadtxtwidth=$('loadtxt').clientWidth;
$('loadtxt').style.width=(loadtxtwidth+5)+"px"; // hardset
$('loadtxt').style.textAlign="left";
initloading=false;
}
}
updateNotices(Date.now(), dt);
if(waitDensity>nextDensity) {
cloudDensityTarget = rndMinMax(10,90) * .01; // %
nextDensity = rndMinMax(5,50); // sec
waitDensity = 0;
//console.log("cloudDensityTarget="+cloudDensityTarget+", cDensity="+cDensity);
} else {
waitDensity+=dt;
}
if(cDensity<cloudDensityTarget) {
cDensity+=0.001 * dt;
if(cDensity>0.9) cDensity=0.9;
} else if(cDensity>cloudDensityTarget) {
cDensity-=0.001 * dt;
if(cDensity<0.1) cDensity=0.1;
}
skyMaterial.uniforms.cDensity.value=cDensity;
customDepthMat.uniforms.cDensity.value=cDensity;
//console.log("cDensity="+cDensity);
// only used for test clouds sphere simple cloud 1
const cloudsSpinY=windVelocityY * dt; // dt = 1.0 = sec
if(waitWindY>nextWindY) {
windY = rndMinMax(-1.0,1.0);
nextWindY = rndMinMax(5,50); // sec
waitWindY = 0;
} else {
waitWindY+=dt;
}
if(windVelocityY<windY*.001) {
windVelocityY+=0.001 * dt;
if(windVelocityY>1.0) windVelocityY=1.0;
} else if(windVelocityY>windY*.001) {
windVelocityY-=0.001 * dt;
if(windVelocityY>0.0) windVelocityY=0.0;
}
clouds.rotation.y -= cloudsSpinY;
if(terrainMove) {
// Move world toward camera
const terrainMoveZ=speed * dt;
terrainGroupZ += terrainMoveZ;
if(terrainGroupZ>=tileSize) { // keeping track and for fireworks
terrainGroupZ-=tileSize;
terrainGroupZCount++;
}
terrainGroup.position.z -= terrainMoveZ;
}
// Recycle tiles
const firstTile = tiles[0];
if(firstTile) {
if (firstTile.position.z + terrainGroup.position.z < -tileSize-zInitOffset) {
tiles.shift();
firstTile.position.z += numTiles * tileSize-zInitOffset;
// Update heights for new position
const vertices = firstTile.geometry.attributes.position.array;
for (let i = 0; i < vertices.length; i += 3) {
const worldX = vertices[i] + firstTile.position.x;
const worldZ = vertices[i + 2] + firstTile.position.z;
vertices[i + 1] = getHeightAt(worldX, worldZ);
}
firstTile.geometry.attributes.position.needsUpdate = true;
firstTile.geometry.computeVertexNormals();
// Clear old children
while (firstTile.children.length) firstTile.remove(firstTile.children[0]);
// Re-add new mountains
addMountainsToTile(firstTile);
tiles.push(firstTile);
}
}
// Update info box
const pos = camera.position;
const rot = camera.rotation;
infoDiv.innerHTML = `
Camera Position:<br>
X: ${pos.x.toFixed(2)}<br>
Y: ${pos.y.toFixed(2)}<br>
Z: ${pos.z.toFixed(2)}<br><br>
Camera Rotation:<br>
X: ${(rot.x * 180 / Math.PI).toFixed(2)}°<br>
Y: ${(rot.y * 180 / Math.PI).toFixed(2)}°<br>
Z: ${(rot.z * 180 / Math.PI).toFixed(2)}°
`;
if(lastRot.x!=rot.x || lastRot.y!=rot.y || lastRot.z !=rot.z ||
lastPos.x!=pos.x || lastPos.y!=pos.y || lastPos.z!=pos.z) {
infoOpa=300.0; // 100 is fade time + 2x 100 wait
lastRot.copy(rot);
lastPos.copy(pos);
//console.log("has moved");
}
//console.log("last rotation"+lastRot.x+" infoOpa: "+infoOpa);
if(infoOpa>0.0) {
infoOpa-=100.0 * dt; // 1 sec
$('info').style.opacity=(Math.min(infoOpa,100)*.01);
} else {
infoOpa=0.0;
$('info').style.opacity=infoOpa*.01;
}
lodUpdateTimer += dt;
if (lodUpdateTimer >= LOD_UPDATE_INTERVAL) {
updateTileLODs();
lodUpdateTimer = 0;
}
updateMountainLODsAndVisibility(dt);
var lapse=86400; // realtime
if(testValOn) {
lapse=2000; // debugtime testval uncomment
}
//sky.position.copy(camera.position).setY(0);
//sky.position.set(0.0, 0.0, camera.position.z);
// sun position calculations
dd=new Date();
hh=dd.getHours()*60*60;
mm=dd.getMinutes()*60;
ss=dd.getSeconds()+mm+hh;
//ss+=(6*60*60); // offset time, - 4-5 hours seem about accurate for sunset at 6pm front
ss-=(6*60*60);
if(ss>86400) ss-=86400;
if(ss<0) ss+=86400;
if(testValOn) {
// debugtime
if(testValAuto) {
testval++;
}
if(testval>2000) testval-=2000;
tsp=testval/1000; // debugtime
ss=testval;
} else {
// realtime
tsp=ss/43200; // realtime comment out for debugtime sunpos
}
ss_phi=((tsp)*180)+90;
if(ss_phi>360) ss_phi-=360;
//ss_theta=(-45);
var scal=0;
if(ss_phi<180) {
// make 86400 into 360 be 0 to 180 and 180 to 360 into 0 to 2 then 0 to 2 into -1 to 0 to 1, then math abs -1 0 1 into 1 0 1 then make 1 0 1 into 0 1 0 with 1- and 0 -1 0 with -1-
scal=Math.abs((ss_phi/90)-1); // 0 1 0
if(ss_phi<90) scal=-scal; //negative edge case
} else {
scal=1-Math.abs(1-((ss_phi-180)/90)-1); // 0 -1 0
}
//ss_theta=(scal*45);
//let smoothscal=SmoothStep(-1.0,1.0,scal);
let smoothscal=(easeInOut((scal+1.0)*.5)*2.0)-1.0; // curve the midnight/midday corner point up/down while arriving at center
ss_theta=(smoothscal*45);
let ssTun = ss - (25*lapse/100);
if(ssTun<0) ssTun+=lapse;
//console.log(Math.abs((ss/(lapse*.5))-1.0));
const daytime=Math.abs((ssTun/(lapse*.5))-1.0); // 1 0 1
//console.log("ss_phi: "+ss_phi+" ss_theta: "+ss_theta+" scal="+scal);
//console.log(" scal="+scal);
// phi and theta have been swapped here intentionally to tilt on a different axis than top/bottom~
sunTime=getDirection(ss_phi,ss_theta); // 90,170 = default front above ground, 180 phi = on ground
moonTime=getDirection(-ss_phi,-ss_theta); // 90,170 = default front above ground, 180 phi = on ground
//sunPhi should be 180 at 6am and 6pm, going to 300 at night and 60 on day
//sunTheta should be 90 at 6pm, -90 or 270 at 6am
if(daytime>=0.5) {
sunLight.position.set(sunTime.x*8000,sunTime.y*8000,sunTime.z*8000);
} else {
sunLight.position.set(-moonTime.x*8000,moonTime.y*8000,moonTime.z*8000);
}
skyMaterial.uniforms.sunDirection.value = sunTime.normalize();
cloudsMaterial.uniforms.sunDirection.value = sunTime.normalize();
customDepthMat.uniforms.sunDirection.value = sunTime.normalize();
//skyMaterial.uniforms.moonDirection.value = moonTime.normalize();
terrainMaterial.uniforms.lightDir.value.copy(sunLight.position.normalize());
terrainMaterial.uniforms.cameraPosition.value.copy(camera.position);
if(mountainMaterial!=null) {
mountainMaterial.uniforms.cameraPosition.value.copy(camera.position);
mountainMaterial.uniforms.lightDir.value.copy(sunLight.position).normalize();
mountainMaterial.uniforms.lightDir2.value.copy(sunLight.position).normalize().negate();
}
camera.updateMatrixWorld();
skyMaterial.uniforms.lightDir.value.copy(sunLight.position.normalize());
skyMaterial.uniforms.lightDir2.value.copy(sunLight.position.normalize().negate());
cloudsMaterial.uniforms.lightDir.value.copy(sunLight.position.normalize());
cloudsMaterial.uniforms.lightDir2.value.copy(sunLight.position.normalize().negate());
clouds.updateMatrixWorld();
normalMatrix.getNormalMatrix(clouds.matrixWorld);
skyMaterial.uniforms.nMatrix.value = normalMatrix;
skyMaterial.uniforms.vMatrix.value.copy(camera.matrixWorldInverse);
cloudsMaterial.uniforms.nMatrix.value = normalMatrix;
cloudsMaterial.uniforms.vMatrix.value.copy(camera.matrixWorldInverse);
customDepthMat.uniforms.nMatrix.value = normalMatrix;
customDepthMat.uniforms.vMatrix.value.copy(camera.matrixWorldInverse);
//cloudsMaterial.uniforms.invView.value.copy(camera.matrixWorldInverse).invert();
//cloudsMaterial.uniforms.invProjection = { value: new THREE.Matrix4() };
//cloudsMaterial.uniforms.invProjection.value.copy(camera.projectionMatrix).invert();
mvpMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse).multiply(clouds.matrixWorld);
cloudsMaterial.uniforms.mvpMatrix.value = mvpMatrix;
skyMaterial.uniforms.mvpMatrix.value = mvpMatrix;
customDepthMat.uniforms.mvpMatrix.value = mvpMatrix;
// sky color calculations
var l=0; // segment selection
var t=ss; // range current
//const hr_1=(4.2*lapse/100); // percentage
const hr_1=(lapse/24); // percentage
const hr_3=hr_1*3;
const hr_4=hr_3+hr_1;
const hr_5=hr_4+hr_1;
const hr_9=hr_5+hr_4;
const hr_12=hr_9+hr_3;
const hr_13=hr_12+hr_1;
const hr_16=hr_13+hr_3;
const hr_21=hr_16+hr_5;
//console.log("ss="+ss);
//console.log("hr_1="+hr_1+" ss>hr_1?:"+((ss>hr_1)?"true":"false"));
//console.log("hr_="+hr_3+" ss>hr_3?:"+((ss>hr_3)?"true":"false"));
//console.log("hr_="+hr_4+" ss>hr_4?:"+((ss>hr_4)?"true":"false"));
//console.log("hr_="+hr_5+" ss>hr_5?:"+((ss>hr_5)?"true":"false"));
//console.log("hr_="+hr_9+" ss>hr_9?:"+((ss>hr_9)?"true":"false"));
//console.log("hr_="+hr_12+" ss>hr_12?:"+((ss>hr_12)?"true":"false"));
//console.log("hr_="+hr_13+" ss>hr_13?:"+((ss>hr_13)?"true":"false"));
//console.log("hr_="+hr_16+" ss>hr_16?:"+((ss>hr_16)?"true":"false"));
//console.log("hr_="+hr_21+" ss>hr_21?:"+((ss>hr_21)?"true":"false"));
//ss-=(10*lapse/100);
//if(ss<0) ss+=lapse; // rollover
var m=hr_1; // range max
if(ss>hr_21) { // night end 3
l=7;
t=ss-hr_21;
m=hr_3;
} else if(ss>hr_16) { // midnight 5
l=6;
t=ss-hr_16;
m=hr_5;
} else if(ss>hr_13) { // night begin 3
l=5;
t=ss-hr_13;
m=hr_3;
} else if(ss>hr_12) { // evening 1
l=4;
t=ss-hr_12;
m=hr_1;
} else if(ss>hr_9) { // day end 3
l=3;
t=ss-hr_9;
m=hr_3;
} else if(ss>hr_4) { // midday 5
l=2;
t=ss-hr_4;
m=hr_5;
} else if(ss>hr_1) { // day begin 3
l=1;
t=ss-hr_1;
m=hr_3;
}
var l2=l+1;
//if(l2>3) l2=0;
if(l2>7) l2=0;
//console.log("l="+l+" l2="+l2+" t="+t+" m="+m);
tmpColor['r1']=skyColors['topColor'][l][0];
tmpColor['g1']=skyColors['topColor'][l][1];
tmpColor['b1']=skyColors['topColor'][l][2];
tmpColor['r2']=skyColors['topColor'][l2][0];
tmpColor['g2']=skyColors['topColor'][l2][1];
tmpColor['b2']=skyColors['topColor'][l2][2];
tmpColor['r3']=skyColors['bottomColor'][l][0];
tmpColor['g3']=skyColors['bottomColor'][l][1];
tmpColor['b3']=skyColors['bottomColor'][l][2];
tmpColor['r4']=skyColors['bottomColor'][l2][0];
tmpColor['g4']=skyColors['bottomColor'][l2][1];
tmpColor['b4']=skyColors['bottomColor'][l2][2];
let p = Math.min(t / m, 1); // progress
//const eP=p; // linear
const eP = easeInOut(p); // easedProgress
//const currentValue = lerp(startValue, endValue, eP);
tmpColor['rlt']=lerp(tmpColor['r1'],tmpColor['r2'],eP);
tmpColor['glt']=lerp(tmpColor['g1'],tmpColor['g2'],eP);
tmpColor['blt']=lerp(tmpColor['b1'],tmpColor['b2'],eP);
tmpColor['rlb']=lerp(tmpColor['r3'],tmpColor['r4'],eP);
tmpColor['glb']=lerp(tmpColor['g3'],tmpColor['g4'],eP);
tmpColor['blb']=lerp(tmpColor['b3'],tmpColor['b4'],eP);
tmpColor['fr']=lerp(tmpColor['rlb'],tmpColor['rlt'],.66)*.66;
tmpColor['fg']=lerp(tmpColor['glb'],tmpColor['glt'],.66)*.46;
tmpColor['fb']=lerp(tmpColor['blb'],tmpColor['blt'],.66)*.16;
//if(document.body) {
// gradientBG="linear-gradient(90deg, rgb("+tmpColor['rlt']+","+tmpColor['glt']+","+tmpColor['blt']+"), rgb("+tmpColor['rlb']+","+tmpColor['glb']+","+tmpColor['blb']+"))";
// document.body.style.backgroundColor="rgb("+tmpColor['fr']+","+tmpColor['fg']+","+tmpColor['fb']+")";
// //document.body.style.background=gradientBG;
//}
scene.fog = new THREE.FogExp2(new THREE.Color(tmpColor['fr']/255,tmpColor['fg']/255,tmpColor['fb']/255), 0.0005);
//console.log("lerp r1->r2 t/m="+t+"/"+m+" rlt: "+tmpColor['rlt']+" p="+p+" eP="+eP+" l="+l+" l2="+l2);
skyMaterial.uniforms.topColor.value=new THREE.Color(tmpColor['rlt']/255,tmpColor['glt']/255,tmpColor['blt']/255);
skyMaterial.uniforms.bottomColor.value=new THREE.Color(tmpColor['rlb']/255,tmpColor['glb']/255,tmpColor['blb']/255);
// sun and sky calculations end
const milliseconds=ss+(dd.getMilliseconds()/1000.0);
skyMaterial.uniforms.xtime.value=milliseconds;
cloudsMaterial.uniforms.xtime.value=milliseconds;
customDepthMat.uniforms.xtime.value=milliseconds;
//skyMaterial.uniforms.daytime.value=Math.abs((ss/(86400.0*.5))-1.0);
skyMaterial.uniforms.daytime.value=daytime;
cloudsMaterial.uniforms.daytime.value=daytime;
customDepthMat.uniforms.daytime.value=daytime;
terrainMaterial.uniforms.daytime.value=daytime;
if(mountainMaterial!=null) {
mountainMaterial.uniforms.daytime.value=daytime;
}
updateShadowCameras();
const delta = clock.getDelta(); // For mixers
const globalTime = clock.getElapsedTime();
// UPDATE ALL ACTIVE FIREWORKS
for (let i = activeFireworks.length - 1; i >= 0; i--) {
const fw = activeFireworks[i];
const tile = activeFireworks[i]['tileRef'];
// 1. launch keyframe animation
fw.mixer.update(delta);
// 2. sprite sheet animation
updateFireworkUV(fw, globalTime);
//--// 3. fade out + destroy after ~15 minutes (900 seconds)
const age = globalTime - fw.startTime;
const action = fw.mixer._actions[0]; // Assume first action=launch
if (action && action.time >= action.getClip().duration) { // Finished
// Start fade or destroy
if (age > action.getClip().duration + 0) { // Extra 5s linger
//scene.remove(fw.instance);
if(tile) {
tile.remove(fw.instance);
}
fw.instance.traverse((child) => {
if (child.isMesh) {
if (child.geometry) child.geometry.dispose();
if (child.material) {
if (Array.isArray(child.material)) {
child.material.forEach(mat => mat.dispose());
} else {
child.material.dispose();
}
}
}
});
if (fw.mixer) fw.mixer.uncacheRoot(fw.instance); // Clean mixer
activeFireworks.splice(i, 1);
} else if (age > fw.totalDuration - 4) { // Start fade 4 seconds before end (tune 4→6 for slower)
const fadeTime = fw.totalDuration - age; // Remaining seconds
const fade = fadeTime / 4; // 1 → 0 over 4 sec
// Slow opacity ramp + freeze UV on last frame (soft texture fade)
fw.instance.traverse((child) => {
if (child.isMesh && child.material?.uniforms?.u_opacity) {
//child.material.uniforms.u_opacity.value = fade; // Linear fade
child.material.uniforms.u_opacity.value = Math.pow(fade, 0.5);
// Optional ease-out: Math.pow(fade, 0.5) for slower start
}
});
} else {
const fade = 1 - (age - action.getClip().duration) / 5;
fw.instance.traverse(child => {
if (child.material) child.material.uniforms.u_opacity.value = fade;
});
}
}
}
controls.update();
renderer.render(scene, camera);
}
animate(0);
var wireframeMode=false;
var cloudQuality=3; // start simple2
//var cloudQuality=2; // start off
var wrT;
function window_resize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// Resize handler
window.addEventListener('resize', () => {
clearTimeout(wrT);
wrT=setTimeout(function() { window_resize(); },100);
});
function extCloudOff() {
cloudQuality=2; // off
skyMaterial.uniforms.cloudsOn.value=0;
clouds.visible=false;
// clouds toggled off by parent, update parent with time of day for sunlight?
}
//parent.postMessage(['new_tim',timeString,testEarthAngle,testTheta],'*');
if(window.self!==window.parent) {
window.addEventListener('message', (event) => {
//if (event.origin === 'https://links.analiestar.com') {
console.log('Message from parent:', event.data[0]);
switch(event.data[0]) {
case "cloudsoff":
extCloudOff();
break;
default:
}
//}
});
parent.postMessage(['cloudcheck',''],'*');
}
const canvas = renderer.domElement;
// Function to hide the cursor
function hideCursor() {
canvas.style.cursor = 'none';
// Prevent default behavior like text selection which interferes with drag
event.preventDefault();
}
function showCursor() {
// You can set it to default, auto, or any other cursor style you prefer
canvas.style.cursor = 'auto';
}
canvas.addEventListener('mousedown', hideCursor);
document.addEventListener('mousedown', hideCursor);
// Use 'mouseup' on the window/document to ensure the cursor reappears
// even if the mouse button is released outside the canvas area
document.addEventListener('mouseup', showCursor);
/* // alternative PointerLockControls library FPS intended, esc to 'unlock'
document.addEventListener( 'click', function () {
controls.lock();
}, false );
// The cursor is automatically hidden when the pointer is locked.
// You can listen for the 'unlock' event to perform other UI actions.
controls.addEventListener( 'unlock', function () {
console.log( 'Pointer unlocked, cursor visible.' );
});*/
const pressedKeys = new Set;
document.addEventListener('keydown', (e) => {
pressedKeys.add(e.key);
//if (e.key === 'Shift') {
// controls.screenSpacePanning = false;
//}
});
document.addEventListener('keyup', (e) => {
pressedKeys.delete(e.key);
if (event.key === 'w') { // Toggle with 'w' key
wireframeMode = !wireframeMode;
terrainMaterial.wireframe = wireframeMode;
skyMaterial.wireframe = wireframeMode;
cloudsMaterial.wireframe = wireframeMode;
if(mountainMaterial!=null) {
mountainMaterial.wireframe = wireframeMode;
}
if(wireframeMode) {
pushNotice("'w': toggle wireframe mode on");
} else {
pushNotice("'w': toggle wireframe mode off");
}
}
if (event.key === 's') { // shadow on - debug dither - debug clean - off
//shadowOn = (shadowOn==1)?0:1;
shadowOn++;
if(shadowOn>3) shadowOn=0;
terrainMaterial.uniforms.shadowOn.value = shadowOn;
if(mountainMaterial!=null) {
mountainMaterial.uniforms.shadowOn.value = shadowOn;
}
if(shadowOn==0) {
pushNotice("'s': toggle shadow all off");
} else if(shadowOn==1) {
pushNotice("'s': toggle shadow all on");
} else if(shadowOn==2) {
pushNotice("'s': toggle shadow + dither map");
} else {
pushNotice("'s': toggle shadow clean map");
}
}
if (event.key === 'd') { // depthwrite off - on
skyDepthOn=!skyDepthOn;
skyMaterial.depthWrite=skyDepthOn;
if(skyDepthOn) {
pushNotice("'d': toggle depthWrite on sky on");
} else {
pushNotice("'d': toggle depthWrite on sky off");
}
}
if (event.key === 't') { // testval debug toggle on - off
//--testValOn=!testValOn;
if(testValOn && testValAuto) { // true true
testValAuto=false;
pushNotice("'t': toggle testVal on with fixed");
} else if(testValOn && !testValAuto) { // true false
testValOn=false;
testValAuto=true;
pushNotice("'t': toggle testVal off (realtime)");
} else { // false true ?
testValOn=true;
pushNotice("'t': toggle testVal on with auto on");
}
}
if (event.key === 'c') { // cloud high - low - off
cloudQuality++;
if(cloudQuality>4) cloudQuality=0;
clouds.visible = false;
sky.castShadow=false;
customDepthMat.uniforms.cloudsOn.value=0;
if(cloudQuality==0) { // high
skyMaterial.uniforms.cloudsOn.value=1;
skyMaterial.uniforms.marchSize.value=40.0;
skyMaterial.uniforms.maxSteps.value=6;
pushNotice("'c': toggle cloud raymarched high");
} else if(cloudQuality==1) { // low
skyMaterial.uniforms.cloudsOn.value=1;
skyMaterial.uniforms.marchSize.value=80.0;
skyMaterial.uniforms.maxSteps.value=2;
pushNotice("'c': toggle cloud raymarched low");
} else if(cloudQuality==2) { // all off [default]
skyMaterial.uniforms.cloudsOn.value=0;
pushNotice("'c': toggle cloud all off");
} else if(cloudQuality==3) { // 2d / simple2 [default] - clouds2 now built into skymaterial (has customdepthmat)
skyMaterial.uniforms.cloudsOn.value=2;
customDepthMat.uniforms.cloudsOn.value=2;
sky.castShadow=true;
pushNotice("'c': toggle cloud simple2 [default]");
} else if(cloudQuality==4) { // 2d / simple1 (separate sphere)
skyMaterial.uniforms.cloudsOn.value=0;
clouds.visible = true;
cloudsMaterial.vertexShader=vertexShaderClouds;
cloudsMaterial.fragmentShader=fragmentShaderClouds;
//--cloudsMaterial.vertexShader=vertexShaderClouds2;
//--cloudsMaterial.fragmentShader=fragmentShaderClouds2;
//cloudsMaterial.vertexShader=vertexShaderSky;
//cloudsMaterial.fragmentShader=fragmentShaderShadow; // debug
//clouds.customDepthMaterial = customDepthMat;
cloudsMaterial.needsUpdate=true;
pushNotice("'c': toggle cloud simple1");
}
//skyMaterial.needsUpdate=true;
} else if (event.code === 'Space') { //
terrainMove=!terrainMove;
if(terrainMove) {
pushNotice("'Space': toggle terrainMove moving");
} else {
pushNotice("'Space': toggle terrainMove stopped");
}
}
pressedKeys.clear();
});
var anaS=`%c
,/#(,
./%%%%(,
.*#%%%%#,
,/#%%%%%%*
./%%%%%%%%%%*
*%%%%%%%%%%%%/.
,#%%%%%%%%%%%%(.
./%%%%#*/%%%%%%/. /%(,*(
./#%%%%/. ./%%#/,,%%%%%%%%%%%/
*(%%##%%%%%%%%%%%%%%(,./%%%%%%#*.
*#%%#* ../%%%%#/. ./%%%%/.
.*(%%#*. ./%%%%#, *%%%%(,
.*#%%###%%%%%%#/, .*%%%%#, .*%%%%#, *
./#%%/.*(%%(*. *%%%%%%%%/.,#%%%%* *
.(%%%%%%%%%%/, ,*//*, ,
`
function printascii() { setTimeout(console.log.bind(console,anaS,'background: #2a1e27; color: #fbbb57;')); }
printascii();
</script>
</body>
</html>
Top