mapspinner 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/workflows/fps-perf.js +147 -0
- package/.claude/workflows/optimize-dna.js +201 -0
- package/.claude/workflows/shader-bottleneck-dna.js +168 -0
- package/.claude/workflows/speed-dna.js +209 -0
- package/.claude/workflows/startup-perf.js +117 -0
- package/.github/workflows/publish.yml +43 -0
- package/AGENTS.md +265 -0
- package/CHANGELOG.md +31 -0
- package/CLAUDE.md +1 -0
- package/README.md +82 -0
- package/examples/basic-sdk-usage.html +114 -0
- package/package.json +28 -0
- package/planet.html +2181 -0
- package/planet.zip +0 -0
- package/scripts/backend-ab.mjs +88 -0
- package/scripts/dev-chrome.cmd +22 -0
- package/scripts/verify.mjs +69 -0
- package/server.js +127 -0
- package/src/anchor-field.js +559 -0
- package/src/gl-render.js +944 -0
- package/src/index.js +41 -0
- package/src/planet-orchestrator.js +790 -0
- package/src/quadtree.js +160 -0
- package/src/shaders/atmosphere.glsl +215 -0
- package/src/shaders/terrain.glsl +2109 -0
- package/src/terrain-gen-controls.js +122 -0
- package/tests/run.js +58 -0
- package/textures/grass-color.jpg +0 -0
- package/textures/grass-displacement.jpg +0 -0
- package/textures/rock-color.jpg +0 -0
- package/textures/rock-displacement.jpg +0 -0
- package/textures/sand-color.jpg +0 -0
- package/textures/sand-displacement.jpg +0 -0
- package/textures/snow-color.jpg +0 -0
- package/textures/snow-displacement.jpg +0 -0
package/src/quadtree.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// quadtree.js -- the cube-face terrain quadtree LOD selection, ported from the deleted
|
|
2
|
+
// proland_terrain.cpp (TerrainNode/TerrainQuad subdivision + SphericalDeformation + the
|
|
3
|
+
// pixel-calibrated split distance). This is the ONE piece of the old C++/wasm the render
|
|
4
|
+
// needed; everything else (the elevation/normal/ortho atlas producer + cascade) is gone --
|
|
5
|
+
// terrain shape is the single GPU fractal (broadShapeM in terrain.glsl), evaluated per-vertex.
|
|
6
|
+
//
|
|
7
|
+
// Pure JS, no wasm, no allocation per frame beyond the leaf array. One Quadtree instance per
|
|
8
|
+
// run; call setConfig once, computeSplitDist when viewport/fov change, updateQuadtree per frame
|
|
9
|
+
// with the camera in LOCAL (undeformed cube-face) space -> returns leaf quads [{level,tx,ty,ox,oy,l}].
|
|
10
|
+
|
|
11
|
+
export class Quadtree {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.size = 6360000.0; // root half-extent / planet radius (m)
|
|
14
|
+
this.maxLevel = 20;
|
|
15
|
+
this.splitDist = 1.1; // computed from splitFactor+viewport+fov
|
|
16
|
+
this.distFactor = 2.0; // altitude weighting
|
|
17
|
+
this._cam = [0, 0, 0]; // camera in LOCAL (undeformed) space (aim-shifted LOD reference)
|
|
18
|
+
this._camAlt = 0.0; // true altitude above the sphere (|localCam|-R), same on every face
|
|
19
|
+
this._nadir = [0, 0]; // TRUE camera nadir (face-local x,y) for the far-LOD foreground protect
|
|
20
|
+
this._aim = null; // AIM ground point (face-local x,y), 2nd foreground-protect center (or null)
|
|
21
|
+
this._leaves = [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setConfig(size, maxLevel, distFactor) {
|
|
25
|
+
this.size = size; this.maxLevel = maxLevel; this.distFactor = distFactor;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// splitDist = splitFactor * viewportH/1024 * tan(40deg)/tan(fov/2), clamped >= 1.1.
|
|
29
|
+
computeSplitDist(splitFactor, viewportH, fovRad) {
|
|
30
|
+
let sd = splitFactor * viewportH / 1024.0 * Math.tan(40.0 / 180.0 * Math.PI) / Math.tan(fovRad / 2.0);
|
|
31
|
+
if (!(sd >= 1.1) || !isFinite(sd)) sd = 1.1;
|
|
32
|
+
this.splitDist = sd;
|
|
33
|
+
return sd;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// TerrainNode::getCameraDist: local-space max-distance metric. The altitude term uses the TRUE
|
|
37
|
+
// camera altitude above the sphere (same on every face -> adjacent faces subdivide by real
|
|
38
|
+
// proximity, the fix for the turn-around-unpatched bug).
|
|
39
|
+
_cameraDist(ox, oy, l) {
|
|
40
|
+
const dz = Math.max(this._camAlt / this.distFactor, 0.0);
|
|
41
|
+
const dx = Math.min(Math.abs(this._cam[0] - ox), Math.abs(this._cam[0] - (ox + l)));
|
|
42
|
+
const dy = Math.min(Math.abs(this._cam[1] - oy), Math.abs(this._cam[1] - (oy + l)));
|
|
43
|
+
return Math.max(dz, Math.max(dx, dy));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// TerrainQuad subdivision: subdivide while dist < l*splitDist AND level < maxLevel; emit leaves.
|
|
47
|
+
// DISTANCE FALLOFF (user 2026-06-01j: 'when close to terrain, far-away quads should LOD better').
|
|
48
|
+
// The plain dist<l*splitDist gives constant screen-space leaf size (leaf ~ dist/splitDist), so when
|
|
49
|
+
// the camera is LOW the horizon-distant terrain still subdivides fine = wasted quads. Tighten the
|
|
50
|
+
// effective splitDist with the quad's LATERAL distance from the camera: far quads need to be much
|
|
51
|
+
// bigger to keep splitting, so distant terrain stays coarse while the near/under-camera stays fine.
|
|
52
|
+
// Falloff scaled by altitude so high views (where 'far' is the whole visible disc) are unaffected.
|
|
53
|
+
_recurse(level, tx, ty, ox, oy, l) {
|
|
54
|
+
const dist = this._cameraDist(ox, oy, l);
|
|
55
|
+
// DISTANCE FALLOFF: coarsen far quads when low. Use the lateral distance to the quad CENTER
|
|
56
|
+
// (NOT its nearest edge -- a big near quad has a far edge, which must NOT penalize it, or the
|
|
57
|
+
// root never splits). Only quads whose CENTRE sits well beyond the near footprint coarsen.
|
|
58
|
+
const cxv = ox + l * 0.5, cyv = oy + l * 0.5;
|
|
59
|
+
// latC = lateral distance from the TRUE camera NADIR (this._nadir), NOT the aim-shifted LOD ref.
|
|
60
|
+
// This decouples foreground-protection from the aim-shift: the foreground (near the true nadir)
|
|
61
|
+
// always has latC~0 -> fall=1.0 -> full LOD, no matter how low the floor. So a LOW floor can
|
|
62
|
+
// reduce the far horizon hard WITHOUT collapsing nearby terrain (user: 'nearby reduced all the
|
|
63
|
+
// way when going to land' was the aim-shifted ref penalizing the foreground).
|
|
64
|
+
// latC = lateral distance from the nadir to the quad's NEAREST POINT (clamp nadir into the quad's
|
|
65
|
+
// [ox,ox+l]x[oy,oy+l] extent), NOT its CENTER. THE CENTER WAS THE BUG (user 2026-06-02: 'dense LOD
|
|
66
|
+
// off to the side, not under the camera; moving toward a face edge it wraps'): a LARGE quad that
|
|
67
|
+
// CONTAINS the camera has its center offset by up to l/2, so a center-distance falloff penalized
|
|
68
|
+
// the camera's own containing quads and they never subdivided -- the only fine quads landed where a
|
|
69
|
+
// quad CENTER happened to align with nadir (off toward the face centre). Nearest-point distance is
|
|
70
|
+
// ~0 for any quad containing the nadir, so the footprint refines UNDER the camera at any face pos.
|
|
71
|
+
const nx0 = Math.max(ox, Math.min(this._nadir[0], ox + l));
|
|
72
|
+
const ny0 = Math.max(oy, Math.min(this._nadir[1], oy + l));
|
|
73
|
+
let latC = Math.max(Math.abs(this._nadir[0] - nx0), Math.abs(this._nadir[1] - ny0));
|
|
74
|
+
// Protect the foreground around the AIM ground point too (the pitched-down bottom-of-screen band):
|
|
75
|
+
// nearest-point distance to the aim box as well; take the NEARER so a quad close to EITHER is fine.
|
|
76
|
+
if (this._aim !== null) {
|
|
77
|
+
const ax0 = Math.max(ox, Math.min(this._aim[0], ox + l));
|
|
78
|
+
const ay0 = Math.max(oy, Math.min(this._aim[1], oy + l));
|
|
79
|
+
const latA = Math.max(Math.abs(this._aim[0] - ax0), Math.abs(this._aim[1] - ay0));
|
|
80
|
+
if (latA < latC) latC = latA;
|
|
81
|
+
}
|
|
82
|
+
// penalty-free near radius. At very low alt the ON-SCREEN foreground extends to the HORIZON
|
|
83
|
+
// (~sqrt(2*R*alt): 1km->113km), so a fixed 20km near coarsened the visible field on sea-level
|
|
84
|
+
// landing (user 2026-06-02: 'at sea level LOD reduces when landing'). Scale near to the horizon
|
|
85
|
+
// distance so the whole visible field out to ~0.6*horizon stays full-LOD at any landing altitude;
|
|
86
|
+
// camAlt*6 dominates at higher alt where the horizon is far. size == R.
|
|
87
|
+
// near = penalty-free radius around the foreground. It must cover the WHOLE on-screen field so
|
|
88
|
+
// detail keeps INCREASING all the way to the ground (user 2026-06-02: 'LOD not increasing under
|
|
89
|
+
// 500km'). The visible field reaches the HORIZON (~sqrt(2*R*alt)); protect the FULL horizon (was
|
|
90
|
+
// 0.6) so nothing on-screen is coarsened by the far-LOD falloff -- the falloff then only trims
|
|
91
|
+
// terrain BEYOND the horizon (off-screen waste), never visible detail.
|
|
92
|
+
const horizon = Math.sqrt(2.0 * this.size * Math.max(this._camAlt, 0.0));
|
|
93
|
+
// W2 SINGLE-VERSION near-radius tighten (mob-w2, unconditional): max(camAlt*6, horizon*0.9,
|
|
94
|
+
// 20km). Shrinking the penalty-free near radius lets the far-LOD falloff coarsen MORE of the
|
|
95
|
+
// off-screen / past-horizon field, cutting peak visible leaves toward ~600-900 (precondition
|
|
96
|
+
// for the 512 layer cap). These are THE only values -- no device tier.
|
|
97
|
+
const near = Math.max(this._camAlt * 6.0, horizon * 0.9, 20000.0);
|
|
98
|
+
// floor 0.5 (was 0.18): past the horizon the split may HALVE, not crush to ~1/5 (which was
|
|
99
|
+
// collapsing near-field detail on descent). Foreground (latC<near) stays fall=1 -> full LOD.
|
|
100
|
+
const fall = 1.0 / (1.0 + Math.max(0.0, latC - near) / (near * 2.0));
|
|
101
|
+
// ALTITUDE-DETAIL-GRADIENT-SWAP (user: 'above fps height, swap more near detail for far detail so the
|
|
102
|
+
// higher we get the less of a detail gradient there is to the land center'). The far-coarsening floor
|
|
103
|
+
// is normally 0.5 (far quads split at half the near rate = a radial detail gradient around the
|
|
104
|
+
// camera). Above the fps height (~5km AGL = first-person ground play) raise that floor toward 1.0 as
|
|
105
|
+
// altitude climbs, so far and near refine at the same rate = the gradient FLATTENS the higher we get
|
|
106
|
+
// (detail budget spreads from the near foreground out to the far field). Below fps height the gradient
|
|
107
|
+
// stays sharp (floor 0.5) so the deck keeps its near-field detail. fpsAltM live via this.fpsAltM.
|
|
108
|
+
const fpsAltM = this.fpsAltM || 5000.0;
|
|
109
|
+
const aboveFps = Math.min(1.0, Math.max(0.0, (this._camAlt - fpsAltM) / (fpsAltM * 8.0))); // 0 at fps height -> 1 by ~45km
|
|
110
|
+
const floor = 0.5 + 0.5 * (aboveFps * aboveFps * (3 - 2 * aboveFps)); // 0.5 (fps deck) -> 1.0 (high alt, flat gradient)
|
|
111
|
+
const effSplit = this.splitDist * Math.max(floor, fall);
|
|
112
|
+
if (dist < l * effSplit && level < this.maxLevel) {
|
|
113
|
+
const hl = l / 2.0;
|
|
114
|
+
this._recurse(level + 1, 2 * tx, 2 * ty, ox, oy, hl);
|
|
115
|
+
this._recurse(level + 1, 2 * tx + 1, 2 * ty, ox + hl, oy, hl);
|
|
116
|
+
this._recurse(level + 1, 2 * tx, 2 * ty + 1, ox, oy + hl, hl);
|
|
117
|
+
this._recurse(level + 1, 2 * tx + 1, 2 * ty + 1, ox + hl, oy + hl, hl);
|
|
118
|
+
} else {
|
|
119
|
+
this._leaves.push({ level, tx, ty, ox, oy, l });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Run the quadtree for the current camera (LOCAL cube-face coords); returns the leaf-quad array.
|
|
124
|
+
// The root quad spans [-size, size] in x and y (one cube face).
|
|
125
|
+
// camX/Y/Z = the (aim-shifted) LOD reference; nadirX/nadirY (optional) = the TRUE camera nadir in
|
|
126
|
+
// face-local coords, used by the far-LOD falloff to protect the real foreground (decoupled from the
|
|
127
|
+
// aim-shift). Defaults to the LOD ref's x,y when not supplied (back-compat). aimX/aimY (optional) =
|
|
128
|
+
// the look ray's ground-hit in face-local coords; a SECOND foreground-protect center so the
|
|
129
|
+
// pitched-down bottom-of-screen band stays full-LOD in every azimuth (null/omitted -> nadir only).
|
|
130
|
+
updateQuadtree(camX, camY, camZ, nadirX, nadirY, aimX, aimY, camAlt) {
|
|
131
|
+
this._cam[0] = camX; this._cam[1] = camY; this._cam[2] = camZ;
|
|
132
|
+
// TRUE ALTITUDE (fix off-center LOD stall, user 2026-06-03 'LOD dense only at the start point').
|
|
133
|
+
// camX,camY are the atan-WARPED face-local lateral coords (from worldToFaceLocal); off the face
|
|
134
|
+
// centre they are large (up to ~R near the edge), so sqrt(camX^2+camY^2+camZ^2) hugely
|
|
135
|
+
// OVERESTIMATES altitude -> the altitude term dominates the split metric -> LOD stalls everywhere
|
|
136
|
+
// but the face centre (where camX=camY=0 makes the hypot correct). Use the caller's TRUE altitude
|
|
137
|
+
// (|camWorld|-R) when supplied; fall back to the old hypot only for back-compat.
|
|
138
|
+
this._camAlt = (camAlt !== undefined && camAlt !== null)
|
|
139
|
+
? camAlt
|
|
140
|
+
: Math.sqrt(camX * camX + camY * camY + camZ * camZ) - this.size;
|
|
141
|
+
this._nadir[0] = (nadirX !== undefined) ? nadirX : camX;
|
|
142
|
+
this._nadir[1] = (nadirY !== undefined) ? nadirY : camY;
|
|
143
|
+
this._aim = (aimX !== undefined && aimY !== undefined) ? [aimX, aimY] : null;
|
|
144
|
+
this._leaves.length = 0;
|
|
145
|
+
this._recurse(0, 0, 0, -this.size, -this.size, 2.0 * this.size);
|
|
146
|
+
return this._leaves;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// SphericalDeformation::localToDeformed with the tangent-adjusted (equal-area-ish) cube->sphere
|
|
151
|
+
// remap: warp the normalized face coord s=x/R by tan(s*pi/4) before the radial projection so cell
|
|
152
|
+
// area is near-uniform (seam-safe: identity at s=+-1). MUST match terrain.glsl faceWarp + the VS.
|
|
153
|
+
// q = (R+z) * normalize(R*tan(x/R*pi/4), R*tan(y/R*pi/4), R).
|
|
154
|
+
export function localToDeformed(x, y, z, R) {
|
|
155
|
+
const k = Math.PI / 4.0;
|
|
156
|
+
const wx = R * Math.tan((x / R) * k);
|
|
157
|
+
const wy = R * Math.tan((y / R) * k);
|
|
158
|
+
const inv = (z + R) / Math.sqrt(wx * wx + wy * wy + R * R);
|
|
159
|
+
return [wx * inv, wy * inv, R * inv];
|
|
160
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// src/shaders/atmosphere.glsl (#version 300 es is prepended by the JS loader)
|
|
2
|
+
// -----------------------------------------------------------------------------
|
|
3
|
+
// WebGL2 analytic Rayleigh/Mie single-scatter atmosphere for the Proland planet.
|
|
4
|
+
//
|
|
5
|
+
// PORT NOTE — what this is vs. the WebGPU path:
|
|
6
|
+
// planet.html runs the AUTHORITATIVE Bruneton model: it BAKES the transmittance/
|
|
7
|
+
// scattering/irradiance LUTs at runtime with WebGPU COMPUTE shaders (3D textures,
|
|
8
|
+
// multiple-scattering orders 2..4) and then samples them with a_GetSkyRadiance().
|
|
9
|
+
// WebGL2 has NO compute shaders, and the baked LUTs live in a *separate* WebGPU
|
|
10
|
+
// GPUDevice/context that cannot be shared into the WebGL2 context. Re-baking the
|
|
11
|
+
// full 3D multiple-scattering LUTs with WebGL2 fragment passes is a large, fragile
|
|
12
|
+
// undertaking. Per the task's explicit fallback, this file implements a FAITHFUL
|
|
13
|
+
// analytic single-scatter atmosphere instead: same atmosphere geometry (bottom
|
|
14
|
+
// 6360 km, top 6420 km), the same Rayleigh/Mie scattering coefficients and phase
|
|
15
|
+
// functions and solar irradiance Bruneton uses, and an analytic optical-depth
|
|
16
|
+
// transmittance. It produces a real limb/halo, sky color gradient, a sun disc,
|
|
17
|
+
// and sun+sky irradiance on the terrain. SIMPLIFIED vs. baked Bruneton:
|
|
18
|
+
// - single scattering only (no orders 2..4 multiple scattering),
|
|
19
|
+
// - analytic exponential optical depth (chapman-ish) instead of LUT transmittance,
|
|
20
|
+
// - sky ambient from a hemispheric Rayleigh approximation instead of the irradiance LUT.
|
|
21
|
+
//
|
|
22
|
+
// Units: positions passed in are ATMOSPHERE-SPACE kilometres (surface at ATM_BOTTOM
|
|
23
|
+
// = 6360). atmPos() maps the meter-scale render world (R = 6360000 m) into this space.
|
|
24
|
+
// -----------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
const float ATM_PI = 3.14159265358979;
|
|
27
|
+
|
|
28
|
+
// Atmosphere geometry (km) — matches ATMO_DEF used to bake the WebGPU LUTs.
|
|
29
|
+
const float ATM_BOTTOM = 6360.0;
|
|
30
|
+
// Visible shell thickened to ~140 km (vs Bruneton's 60 km) so the limb/halo reads
|
|
31
|
+
// as a real atmosphere ring from orbit instead of a sub-pixel sliver. SIMPLIFICATION
|
|
32
|
+
// vs. the baked Bruneton LUTs (which use 6420). Scale heights raised to match so the
|
|
33
|
+
// density still falls off smoothly across the thicker shell.
|
|
34
|
+
const float ATM_TOP = 6500.0;
|
|
35
|
+
const float ATM_RAYLEIGH_H = 18.0; // Rayleigh scale height (km), inflated for the halo
|
|
36
|
+
const float ATM_MIE_H = 4.0; // Mie scale height (km)
|
|
37
|
+
const float ATM_MIE_G = 0.8;
|
|
38
|
+
|
|
39
|
+
// Bruneton scattering coefficients (per-km) and solar irradiance (W/m^2/nm-ish).
|
|
40
|
+
const vec3 ATM_RAYLEIGH = vec3(0.005802, 0.013558, 0.0331);
|
|
41
|
+
const vec3 ATM_MIE = vec3(0.003996, 0.003996, 0.003996);
|
|
42
|
+
const vec3 ATM_MIE_EXT = vec3(0.003996) * (1.0 / 0.9); // Mie extinction = scat/ssa, ssa~0.9
|
|
43
|
+
const vec3 ATM_SOLAR_IRRADIANCE = vec3(1.474, 1.8504, 1.91198);
|
|
44
|
+
const float ATM_SUN_ANGULAR_RADIUS = 0.004675;
|
|
45
|
+
|
|
46
|
+
// Map a render-space world position (meters, surface radius R_m) into atmosphere
|
|
47
|
+
// space (km, surface at ATM_BOTTOM). Preserves altitude fractions across the shell.
|
|
48
|
+
// W7: worldMeters ~6.4e6 m + R_m ~6.4e6 m both overflow fp16 -> highp (terrain.glsl injects this file
|
|
49
|
+
// into the FS, whose global default precision is now mediump). The km-scale result narrows naturally.
|
|
50
|
+
vec3 atmPos(highp vec3 worldMeters, highp float R_m) {
|
|
51
|
+
return worldMeters * (ATM_BOTTOM / R_m);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
float atm_rayleighPhase(float nu) { return (3.0/(16.0*ATM_PI)) * (1.0 + nu*nu); }
|
|
55
|
+
float atm_miePhase(float g, float nu) {
|
|
56
|
+
float k = 3.0/(8.0*ATM_PI) * (1.0-g*g)/(2.0+g*g);
|
|
57
|
+
return k * (1.0+nu*nu) / pow(max(1.0 + g*g - 2.0*g*nu, 1e-4), 1.5);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Distance from a point at radius r along direction with cosine mu to the top shell.
|
|
61
|
+
// Returns -1 if the ray escapes without entering the atmosphere from outside.
|
|
62
|
+
// W7: r ~6360 km so r*r ~4e7 OVERFLOWS fp16 (max 65504) -> r + the disc quadratic MUST be highp.
|
|
63
|
+
float atm_distToTop(highp float r, float mu) {
|
|
64
|
+
highp float disc = r*r*(mu*mu - 1.0) + ATM_TOP*ATM_TOP;
|
|
65
|
+
if (disc < 0.0) return -1.0;
|
|
66
|
+
return max(-r*mu + sqrt(disc), 0.0);
|
|
67
|
+
}
|
|
68
|
+
// Distance to ground sphere (ATM_BOTTOM), or -1 if no hit ahead.
|
|
69
|
+
float atm_distToGround(highp float r, float mu) {
|
|
70
|
+
highp float disc = r*r*(mu*mu - 1.0) + ATM_BOTTOM*ATM_BOTTOM;
|
|
71
|
+
if (disc < 0.0) return -1.0;
|
|
72
|
+
highp float d = -r*mu - sqrt(disc);
|
|
73
|
+
return d >= 0.0 ? d : -1.0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Analytic densities at radius r (altitude above ATM_BOTTOM).
|
|
77
|
+
// W7: r ~6360 (km-scale planet radius) and the (r - ATM_BOTTOM) altitude cancellation are highp -- at
|
|
78
|
+
// mediump fp16 the ~6360 radius resolves to ~4 km steps and the density profile (scale-height tens of
|
|
79
|
+
// km) would band/collapse. The exp() RESULT narrows to the mediump default.
|
|
80
|
+
float atm_rayleighDensity(highp float r) { return exp(-(r - ATM_BOTTOM)/ATM_RAYLEIGH_H); }
|
|
81
|
+
float atm_mieDensity(highp float r) { return exp(-(r - ATM_BOTTOM)/ATM_MIE_H); }
|
|
82
|
+
|
|
83
|
+
// Optical depth (Rayleigh,Mie line integral) from point p0 along dir for distance d.
|
|
84
|
+
// Cheap fixed-step trapezoid; cheap enough for a fullscreen pass.
|
|
85
|
+
void atm_opticalDepth(highp vec3 p0, vec3 dir, float d, out float odR, out float odM) { // W7: km-scale p0 -> highp
|
|
86
|
+
const int N = 8;
|
|
87
|
+
float dt = d / float(N);
|
|
88
|
+
odR = 0.0; odM = 0.0;
|
|
89
|
+
for (int i = 0; i < N; i++) {
|
|
90
|
+
highp vec3 p = p0 + dir * (dt * (float(i) + 0.5)); // W7: km-scale march point
|
|
91
|
+
highp float r = length(p);
|
|
92
|
+
odR += atm_rayleighDensity(r) * dt;
|
|
93
|
+
odM += atm_mieDensity(r) * dt;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Transmittance over a segment of length d from p0 along dir.
|
|
98
|
+
vec3 atm_transmittanceSeg(highp vec3 p0, vec3 dir, float d) { // W7: km-scale p0 -> highp
|
|
99
|
+
float odR, odM;
|
|
100
|
+
atm_opticalDepth(p0, dir, d, odR, odM);
|
|
101
|
+
return exp(-(ATM_RAYLEIGH * odR + ATM_MIE_EXT * odM));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Transmittance from point p to the top of the atmosphere along sun direction.
|
|
105
|
+
vec3 atm_transmittanceToSun(highp vec3 p, vec3 sun) { // W7: km-scale p -> highp
|
|
106
|
+
highp float r = length(p);
|
|
107
|
+
float mu = dot(p, sun) / r;
|
|
108
|
+
float d = atm_distToTop(r, mu);
|
|
109
|
+
if (d < 0.0) return vec3(1.0);
|
|
110
|
+
// SOFT sea-level horizon (user 2026-06-11 'shading indicates depth not slope' -- THE math bug):
|
|
111
|
+
// the old branch returned a BINARY vec3(0) whenever the sun ray intersected the ATM_BOTTOM
|
|
112
|
+
// sphere. ATM_BOTTOM is SEA LEVEL, so at low sun the direct light became a hard step function
|
|
113
|
+
// of ELEVATION (every point below a sun-dependent altitude got zero sun, every point above got
|
|
114
|
+
// full sun) -- slope-blind, elevation-keyed dark patches that read as 'depth shading' + dark
|
|
115
|
+
// rocky blobs. Replace with a smooth attenuation over ~2 deg of sun dip below the sea horizon:
|
|
116
|
+
// night stays dark, the elevation step is gone, slopes drive the shading again.
|
|
117
|
+
float muHoriz = -sqrt(max(0.0, 1.0 - (ATM_BOTTOM * ATM_BOTTOM) / (r * r)));
|
|
118
|
+
float soft = smoothstep(muHoriz - 0.035, muHoriz + 0.005, mu);
|
|
119
|
+
if (soft <= 0.0) return vec3(0.0);
|
|
120
|
+
return atm_transmittanceSeg(p, sun, d) * soft;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Sky in-scatter radiance + view transmittance along a view ray from cameraIn.
|
|
124
|
+
// camera/view in atmosphere-space km. Single-scatter Rayleigh+Mie integration.
|
|
125
|
+
vec3 atm_skyRadiance(highp vec3 cameraIn, vec3 viewRay, vec3 sun, out vec3 transmittance) { // W7: km-scale camera -> highp
|
|
126
|
+
highp vec3 camera = cameraIn;
|
|
127
|
+
highp float r = length(camera);
|
|
128
|
+
float mu = dot(camera, viewRay) / r;
|
|
129
|
+
|
|
130
|
+
// March-start: if outside the atmosphere, advance to the top shell.
|
|
131
|
+
if (r > ATM_TOP) {
|
|
132
|
+
float dt = atm_distToTop(r, mu);
|
|
133
|
+
if (dt < 0.0) { transmittance = vec3(1.0); return vec3(0.0); } // misses atmosphere
|
|
134
|
+
camera = camera + viewRay * dt;
|
|
135
|
+
r = length(camera);
|
|
136
|
+
mu = dot(camera, viewRay) / r;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// March-end: ground hit, else exit at top shell.
|
|
140
|
+
float dGround = atm_distToGround(r, mu);
|
|
141
|
+
float dTop = atm_distToTop(r, mu);
|
|
142
|
+
bool ground = dGround > 0.0;
|
|
143
|
+
float dEnd = ground ? dGround : max(dTop, 0.0);
|
|
144
|
+
if (dEnd <= 0.0) { transmittance = vec3(1.0); return vec3(0.0); }
|
|
145
|
+
|
|
146
|
+
const int N = 16;
|
|
147
|
+
float dt = dEnd / float(N);
|
|
148
|
+
vec3 inscatR = vec3(0.0);
|
|
149
|
+
vec3 inscatM = vec3(0.0);
|
|
150
|
+
float odR = 0.0, odM = 0.0; // accumulated optical depth from camera to sample
|
|
151
|
+
|
|
152
|
+
for (int i = 0; i < N; i++) {
|
|
153
|
+
highp vec3 p = camera + viewRay * (dt * (float(i) + 0.5)); // W7: km-scale march point -> highp
|
|
154
|
+
highp float pr = length(p);
|
|
155
|
+
float dR = atm_rayleighDensity(pr) * dt;
|
|
156
|
+
float dM = atm_mieDensity(pr) * dt;
|
|
157
|
+
odR += dR; odM += dM;
|
|
158
|
+
// transmittance camera->sample
|
|
159
|
+
vec3 tView = exp(-(ATM_RAYLEIGH * odR + ATM_MIE_EXT * odM));
|
|
160
|
+
// transmittance sample->sun
|
|
161
|
+
vec3 tSun = atm_transmittanceToSun(p, sun);
|
|
162
|
+
vec3 t = tView * tSun;
|
|
163
|
+
inscatR += t * dR;
|
|
164
|
+
inscatM += t * dM;
|
|
165
|
+
}
|
|
166
|
+
float nu = dot(viewRay, sun);
|
|
167
|
+
transmittance = exp(-(ATM_RAYLEIGH * odR + ATM_MIE_EXT * odM));
|
|
168
|
+
if (ground) transmittance = vec3(0.0);
|
|
169
|
+
|
|
170
|
+
vec3 radiance = ATM_SOLAR_IRRADIANCE * (
|
|
171
|
+
inscatR * ATM_RAYLEIGH * atm_rayleighPhase(nu) +
|
|
172
|
+
inscatM * ATM_MIE * atm_miePhase(ATM_MIE_G, nu));
|
|
173
|
+
return radiance;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Sun + sky irradiance reaching a surface point with given normal.
|
|
177
|
+
// Returns DIRECT sun irradiance (already * N.L); sky_irradiance is the ambient term.
|
|
178
|
+
// DIFFUSE WRAP (user 2026-06-10 'looking down the horizon at grass it gets darker'): pure Lambert
|
|
179
|
+
// kills slopes tilted a few degrees off the sun, so a downward view (which preferentially shows
|
|
180
|
+
// ripple backsides) reads darker than the grazing view of the same field. Wrap lifts away-facing
|
|
181
|
+
// gentle slopes (N.L -> (N.L+w)/(1+w)) -- soft-knee terrain diffuse, view-angle gradient gone.
|
|
182
|
+
// Unset by programs that do not bind it (GL zero-default) -> plain Lambert there (sky pass).
|
|
183
|
+
uniform float uDiffWrap; // 0 = pure Lambert; ~0.35 = soft wrap (terrain render program)
|
|
184
|
+
vec3 atm_sunSkyIrradiance(highp vec3 point, vec3 normal, vec3 sun, out vec3 sky_irradiance) { // W7: km-scale point -> highp
|
|
185
|
+
highp float r = length(point);
|
|
186
|
+
vec3 up = point / r;
|
|
187
|
+
float muS = dot(up, sun);
|
|
188
|
+
// ELEVATION-INDEPENDENT direct sun (user 2026-06-11 'normals appear elevation based again' --
|
|
189
|
+
// the recurring class, previously narrowed by 1263c51 but not killed): BOTH the sea-horizon cut
|
|
190
|
+
// (muHoriz is a function of r) and the sun-path optical depth key on the POINT'S RADIUS, so at
|
|
191
|
+
// low sun the direct light is a smooth function of terrain ELEVATION -- slope-blind brightness
|
|
192
|
+
// gradients across whole regions that read as 'height-based normals'. Evaluate the sun
|
|
193
|
+
// transmittance on a FIXED shell (sea level + 0.5 km): day/night and sunset reddening still
|
|
194
|
+
// follow the sun's local altitude (mu via `up`), but terrain elevation can no longer modulate
|
|
195
|
+
// direct light -- slope (N.sun) owns the shading. The sky/AP marches keep their per-point
|
|
196
|
+
// physics (airborne sample points genuinely differ); only the SURFACE direct term is pinned.
|
|
197
|
+
vec3 tSun = atm_transmittanceToSun(up * (ATM_BOTTOM + 0.5), sun);
|
|
198
|
+
vec3 direct = ATM_SOLAR_IRRADIANCE * tSun * clamp((dot(normal, sun) + uDiffWrap) / (1.0 + uDiffWrap), 0.0, 1.0);
|
|
199
|
+
// Sky (ambient) dome: Rayleigh-tinted, scaled by how much sky the surface sees
|
|
200
|
+
// and by daylight (smoothstep over the terminator). Hemispheric weight (1+N.up)/2.
|
|
201
|
+
float day = smoothstep(-0.10, 0.25, muS);
|
|
202
|
+
// Desaturate the sky-ambient tint toward white: raw Rayleigh (b~4x r) washed lit land
|
|
203
|
+
// fully blue. mostly-white ambient lights terrain without recoloring it blue.
|
|
204
|
+
vec3 rayTint = ATM_RAYLEIGH / (ATM_RAYLEIGH.x);
|
|
205
|
+
vec3 skyTint = mix(vec3(1.0), rayTint, 0.4);
|
|
206
|
+
// SUN-vs-SKY BALANCE (user 2026-06-02: 'normals arent affecting the lit view properly'). The
|
|
207
|
+
// sky ambient is weighted by N.up (near-directionless), so a too-large ambient washes out the
|
|
208
|
+
// directional N.sun sun term and opposing slopes barely differ -> normals look like they dont
|
|
209
|
+
// shade. Cut the ambient coefficient (0.14 -> 0.075) so the DIRECT sun (which carries the normal
|
|
210
|
+
// via N.sun) dominates and slopes self-shade; the (1+N.up)/2 dome weight stays so it still fills
|
|
211
|
+
// shadowed faces without flattening them. An ambient floor downstream keeps shadows non-black.
|
|
212
|
+
sky_irradiance = ATM_SOLAR_IRRADIANCE * 0.075 * day * skyTint
|
|
213
|
+
* (0.5 * (1.0 + dot(normal, up)));
|
|
214
|
+
return direct;
|
|
215
|
+
}
|