mapspinner 0.1.3 → 0.1.5
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/AGENTS.md +3 -3
- package/CHANGELOG.md +1 -1
- package/package.json +1 -1
- package/planet.html +15 -20
- package/src/anchor-field.js +5 -6
- package/src/gl-render.js +1 -1
- package/src/planet-orchestrator.js +2 -2
- package/src/quadtree.js +1 -4
- package/src/shaders/atmosphere.glsl +1 -1
- package/src/shaders/terrain.glsl +9 -13
package/AGENTS.md
CHANGED
|
@@ -33,7 +33,7 @@ camera you can't aim. Use the DATA diagnostics — `__diag.pxPerPoly()` (on-scre
|
|
|
33
33
|
## The terrain pipeline in one page (read this before touching terrain)
|
|
34
34
|
|
|
35
35
|
Earth-scale terrain SDK, WebGL2, served at `http://localhost:8080/` (entry `planet.html`, `server.js`).
|
|
36
|
-
GPU one-fractal: no
|
|
36
|
+
GPU one-fractal: no tile producer. A finer
|
|
37
37
|
LOD is a denser sample of the SAME field. The procedural broadShapeM fractal is the LIVE render
|
|
38
38
|
path (hasAtlas==0, the default). The baked atlas is opt-in (planet-orchestrator.js opts.atlas===true;
|
|
39
39
|
live `window.__toggleAtlas(on)` / `__forceAtlas`); its bake is CLI-validated faithful (atlas-bake.mjs
|
|
@@ -58,7 +58,7 @@ probes, and `window.__diag.reloadShaders()` hot-reload through `/cmd` — no pag
|
|
|
58
58
|
|
|
59
59
|
Data flow, each stage names its one file:
|
|
60
60
|
1. QUADTREE picks which cube-sphere patches to draw per camera altitude — `src/quadtree.js`
|
|
61
|
-
(cube-sphere quadtree in JS
|
|
61
|
+
(cube-sphere quadtree in JS), driven per frame by
|
|
62
62
|
`src/planet-orchestrator.js`.
|
|
63
63
|
2. MESH per patch is a GRID+2 grid (`src/gl-render.js`) whose outer ring is a SKIRT (terrain.glsl
|
|
64
64
|
drops `vertex.z>0.5` verts radially below the surface) hiding LOD T-junction cracks. The outer
|
|
@@ -70,7 +70,7 @@ Data flow, each stage names its one file:
|
|
|
70
70
|
`inciseRidgeField`. Collision = a GPU `_PROBE_` variant of the SAME shader (1px readback,
|
|
71
71
|
`gl-render.sampleGroundM`), no CPU mirror. — full design: recall "TV8 GPU-TERRAIN ARCHITECTURE
|
|
72
72
|
DECISION" in rs-learn.
|
|
73
|
-
4. DEFORM: direct per-vertex sphere projection — `vWorld = dir0 * (R + h)`
|
|
73
|
+
4. DEFORM: direct per-vertex sphere projection — `vWorld = dir0 * (R + h)`
|
|
74
74
|
corner-blend deform; round at any tessellation, no flat patches at high GRID).
|
|
75
75
|
5. FS shades from height+slope+climate (`terrainAlbedoClimate`) + per-vertex seamless normal +
|
|
76
76
|
ocean/lake/river. No FS detail TEXTURE (a tiled image would moire + UV-scroll); closeup MACRO
|
package/CHANGELOG.md
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
## 2026-05-23
|
|
26
26
|
|
|
27
|
-
- `terrain-phase2.js` (commit `d87c51b`): Add `allocateTileSlot`, `computePipelines`, `computeBindGroups`
|
|
27
|
+
- `terrain-phase2.js` (commit `d87c51b`): Add `allocateTileSlot`, `computePipelines`, `computeBindGroups` — stopped TypeError crash every render frame when `terrain-phase3-integration.js` called these missing methods.
|
|
28
28
|
- `shader-loader.js` (commit `1e61b7b` session): Fix `#if` numeric literal handling — `#if 1` was treated as a flag lookup (always false), stripping always-true blocks and causing GPU validation errors and black canvas.
|
|
29
29
|
- `terrain-phase1.js`: Change `normTexArray` format from `rg32float` to `rgba8unorm` to match `normal_producer.wgsl` storageTexture output.
|
|
30
30
|
- `terrain-phase2.js`: Remove unused bind group entries for upsample (b5), normal producer (b0), ortho producer (b0,b1,b2,b6) — WebGPU `layout:'auto'` strips unreachable bindings, causing validation errors when they were supplied.
|
package/package.json
CHANGED
package/planet.html
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<html><head><meta charset="utf-8"><title>
|
|
2
|
+
<html><head><meta charset="utf-8"><title>mapspinner — WebGL2 Terrain SDK</title>
|
|
3
3
|
<style>
|
|
4
4
|
html,body{margin:0;height:100%;background:#000;color:#9f9;font:12px monospace;overflow:hidden}
|
|
5
5
|
canvas{display:block;width:100vw;height:100vh}
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
#panel .grp{border-top:1px solid #3f3;padding-top:4px;margin-top:6px;color:#cfc}
|
|
15
15
|
</style></head>
|
|
16
16
|
<body>
|
|
17
|
-
<!-- The
|
|
17
|
+
<!-- The mapspinner WebGL2 renderer renders here. -->
|
|
18
18
|
<canvas id="cwebgl" style="position:fixed;inset:0;width:100vw;height:100vh"></canvas>
|
|
19
|
-
<div id="hud"><b>
|
|
19
|
+
<div id="hud"><b>mapspinner — WebGL2 Terrain SDK</b><br>
|
|
20
20
|
Controls: <b>WASD</b>/drag orbit . <b>Q/E</b>/wheel zoom . <b>R</b> reset<br>
|
|
21
21
|
<span id="stats">init...</span></div>
|
|
22
22
|
<div id="panel">
|
|
@@ -33,18 +33,16 @@ Controls: <b>WASD</b>/drag orbit . <b>Q/E</b>/wheel zoom . <b>R</b> reset<br>
|
|
|
33
33
|
<div id="genPanel" style="position:fixed;left:8px;top:34px;z-index:30;display:none;max-height:88vh;overflow:auto;background:rgba(12,16,20,0.92);color:#cfe;font:10px/1.5 monospace;padding:6px 8px;border:1px solid #2a3a44;width:250px"></div>
|
|
34
34
|
<div id="err"></div>
|
|
35
35
|
<script type="module">
|
|
36
|
-
//
|
|
37
|
-
// on the WebGL2 path
|
|
38
|
-
// legacy WebGPU device/pipelines/ocean/atmosphere
|
|
39
|
-
// proland_terrain.wasm (which only drove that dead path) have been DELETED.
|
|
36
|
+
// The mapspinner terrain + atmosphere + ocean run entirely
|
|
37
|
+
// on the WebGL2 path, loaded lazily in frameLoop. The
|
|
38
|
+
// legacy WebGPU device/pipelines/ocean/atmosphere have been DELETED.
|
|
40
39
|
const errEl=document.getElementById('err');
|
|
41
40
|
const showErr=m=>{errEl.style.display='block';errEl.textContent=String(m);window.__pageErr=String(m);console.error(m)};
|
|
42
41
|
window.addEventListener('error',e=>showErr(e.message||e));
|
|
43
42
|
window.addEventListener('unhandledrejection',e=>showErr(e.reason?.message||e.reason||e));
|
|
44
43
|
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// (proland-planet-orchestrator) drawing to #cwebgl. `canvas` is the WebGL2 canvas;
|
|
44
|
+
// The legacy WebGPU render path has been DELETED. All terrain + atmosphere + ocean
|
|
45
|
+
// run on the WebGL2 path drawing to #cwebgl. `canvas` is the WebGL2 canvas;
|
|
48
46
|
// camera event handlers + sizing read it.
|
|
49
47
|
const canvas=document.getElementById('cwebgl');
|
|
50
48
|
|
|
@@ -124,8 +122,7 @@ const normalize=v=>{const r=Math.hypot(...v)||1;return [v[0]/r,v[1]/r,v[2]/r]};
|
|
|
124
122
|
// where the camera starts (cam.pos below), not by this scalar -- RADIUS just
|
|
125
123
|
// sets the units everything else is denominated in.
|
|
126
124
|
const RADIUS=6371.0;
|
|
127
|
-
// WebGL2
|
|
128
|
-
// elevation). Surface radius 6.36e6 m; the km-unit world maps to it 1 unit = this many m.
|
|
125
|
+
// WebGL2 terrain works in METERS. Surface radius 6.36e6 m; the km-unit world maps to it 1 unit = this many m.
|
|
129
126
|
const WEBGL2_TERRAIN_R_M = 6360000.0;
|
|
130
127
|
const WEBGL2_M_PER_UNIT = WEBGL2_TERRAIN_R_M / RADIUS;
|
|
131
128
|
// EXPOSE the world-scale constants as globals so a scripted browser-witness (page.evaluate runs
|
|
@@ -298,8 +295,7 @@ async function pollCmd() {
|
|
|
298
295
|
} catch (_) {} finally { __cmdBusy = false; }
|
|
299
296
|
}
|
|
300
297
|
setInterval(pollCmd, 500);
|
|
301
|
-
//
|
|
302
|
-
// renderer. Set window.__useWebGL2Terrain=false to disable (no fallback now).
|
|
298
|
+
// The mapspinner WebGL2 terrain is the renderer.
|
|
303
299
|
if (window.__useWebGL2Terrain === undefined) window.__useWebGL2Terrain = true;
|
|
304
300
|
const stats=document.getElementById('stats');
|
|
305
301
|
// SINGLE-FLIGHT FRAME SCHEDULER (2026-06-12): every frameLoop continuation routes through here so
|
|
@@ -314,8 +310,7 @@ function frameLoop(){
|
|
|
314
310
|
const frameMs = now - lastT; // true inter-frame interval (for FPS)
|
|
315
311
|
const dt=Math.min(0.05,(now-lastT)/1000); lastT=now;
|
|
316
312
|
|
|
317
|
-
// ----
|
|
318
|
-
// wasm + thin WebGL2 renderer (proland-planet-orchestrator) on the #cwebgl canvas.
|
|
313
|
+
// ---- mapspinner WebGL2 terrain path. Lazily inits the renderer on the #cwebgl canvas.
|
|
319
314
|
// The actual render + rAF happens AFTER the camera-update section below (so WASD/
|
|
320
315
|
// drag/zoom + autopilot move the camera before the frame is drawn).
|
|
321
316
|
if (window.__useWebGL2Terrain) {
|
|
@@ -378,7 +373,7 @@ function frameLoop(){
|
|
|
378
373
|
// We drive it in meter-space and scale the km-unit camera into meters via
|
|
379
374
|
// WEBGL2_M_PER_UNIT below.
|
|
380
375
|
import('./src/planet-orchestrator.js')
|
|
381
|
-
.then(m => m.
|
|
376
|
+
.then(m => m.initMapspinnerPlanet(glw, { radius: WEBGL2_TERRAIN_R_M, frustumCull: false }))
|
|
382
377
|
.then(orch => { window.__planetOrch = orch; window.__planetOrchGL = glw; window.__planetOrchStatus='ready';
|
|
383
378
|
import('./src/terrain-gen-controls.js').then(()=>{ try{ window.__gen && window.__gen.apply(); }catch(_){} }).catch(e=>console.warn('gen-controls',e)); })
|
|
384
379
|
.catch(e => { window.__planetOrchStatus='error:'+(e.message||e); console.error('orch init',e); });
|
|
@@ -546,7 +541,7 @@ function frameLoop(){
|
|
|
546
541
|
window.__altM = heightAboveGroundM;
|
|
547
542
|
|
|
548
543
|
// The camera (autopilot + free-fly + clamp) has now been fully updated this frame,
|
|
549
|
-
// so render the
|
|
544
|
+
// so render the planet with the CURRENT camera. This is
|
|
550
545
|
// the sole render path; it advances the sun, draws via the orchestrator, updates
|
|
551
546
|
// the HUD/title/diag, schedules the next frame, and returns.
|
|
552
547
|
if (window.__useWebGL2Terrain && window.__planetOrch) {
|
|
@@ -705,8 +700,8 @@ cam.oceanAmplitude = 1.0 * (0.4 + 5 / 12.0); // == old default (windSpeed 5)
|
|
|
705
700
|
cam.oceanChoppiness = 1.6 * (0.5 + 0.5); // == old default (seaState 0.5)
|
|
706
701
|
cam.oceanFoam = 0.5;
|
|
707
702
|
cam.oceanTimeScale = 1.0;
|
|
708
|
-
// Display-mode toggle
|
|
709
|
-
// by the one-cell overlap ring in the mesh
|
|
703
|
+
// Display-mode toggle. Skirts removed: LOD cracks are now hidden
|
|
704
|
+
// by the one-cell overlap ring in the mesh, not a dropped skirt curtain.
|
|
710
705
|
// User picking from the dropdown is an explicit action that must WIN over any stale diagnostic
|
|
711
706
|
// override (window.__displayMode left set by __dbg.setDisplayMode/census/aimDir). Clear the override
|
|
712
707
|
// so the menu value takes effect immediately -- otherwise the precedence pick in the render loop
|
package/src/anchor-field.js
CHANGED
|
@@ -12,10 +12,9 @@
|
|
|
12
12
|
// only that cell's neighbourhood -- never the whole planet.
|
|
13
13
|
//
|
|
14
14
|
// INTEGRATION: this is a pure-JS field (editable, serialisable). The orchestrator samples
|
|
15
|
-
// it per
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// quadtree partition at its own scale band). No wasm rebuild is needed to edit params.
|
|
15
|
+
// it per tile and pushes the result into the terrain VS via a uniform or texture.
|
|
16
|
+
// The field is C0-continuous across tile edges because every tile samples the SAME global
|
|
17
|
+
// quadtree partition at its own scale band.
|
|
19
18
|
//
|
|
20
19
|
// PERFORMANCE (all the most-performant choices):
|
|
21
20
|
// - Param storage is a flat Float32Array per band (cache-friendly, zero object churn),
|
|
@@ -26,7 +25,7 @@
|
|
|
26
25
|
// - Sampling is O(bands) -- a handful of band lookups + one bilinear per band -- with no
|
|
27
26
|
// allocation on the hot path (results written into a reused scratch object).
|
|
28
27
|
|
|
29
|
-
// ---- cube-face frame (mirrors
|
|
28
|
+
// ---- cube-face frame (mirrors planet-orchestrator FACE_FRAME / render localToWorld3).
|
|
30
29
|
// A face-local point (u,v in [-1,1], outward axis) maps to a world direction. We only need
|
|
31
30
|
// the inverse (world dir -> face + uv) for sampleDir, and the forward (face,uv -> dir) for
|
|
32
31
|
// the per-node procedural hash seed coordinate.
|
|
@@ -415,7 +414,7 @@ export function createAnchorField(opts = {}) {
|
|
|
415
414
|
return _scratch;
|
|
416
415
|
}
|
|
417
416
|
|
|
418
|
-
// PER-TILE sample for the orchestrator: a
|
|
417
|
+
// PER-TILE sample for the orchestrator: a tile (face, level, tx, ty) covers a uv
|
|
419
418
|
// square on its face; sample the field at the tile CENTRE (the bias is per-tile, and the
|
|
420
419
|
// field is C0 across tiles because every tile samples the same global band quadtrees).
|
|
421
420
|
function sampleTile(face, level, tx, ty) {
|
package/src/gl-render.js
CHANGED
|
@@ -561,7 +561,7 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
561
561
|
const instBuf=gl.createBuffer(); // per-instance [ox,oy,l,level,face] (filled per frame in render())
|
|
562
562
|
|
|
563
563
|
// per-face local->world (cube face -> sphere local frame). Column-major mat3 packed
|
|
564
|
-
// into a Float32Array(9). Matches
|
|
564
|
+
// into a Float32Array(9). Matches localToWorld3 convention:
|
|
565
565
|
// col0 = U/rs, col1 = faceCenter, col2 = V/rs. rootQuadSize=2 -> face spans [-1,1].
|
|
566
566
|
function localToWorld3(face) {
|
|
567
567
|
// face axes (cube): for face 3 (+Z) U=+X, V=+Y, center=+Z. Generic table:
|
|
@@ -202,7 +202,7 @@ export async function initMapspinnerPlanet(gl, opts = {}) {
|
|
|
202
202
|
|
|
203
203
|
// ---- HIERARCHICAL PARAMETER FIELD (HPF) ----------------------------------------
|
|
204
204
|
// The anchor field drives generation as a CONTINUOUS function of world direction. To keep
|
|
205
|
-
//
|
|
205
|
+
// For seamlessness we feed it to the terrain VS as a TEXTURE (a
|
|
206
206
|
// continuous C0 field via LINEAR filtering) rather than a per-tile constant (a per-tile
|
|
207
207
|
// step would reintroduce seams -- see the CLOD mean-drift / edge-equality lessons). We bake
|
|
208
208
|
// the field's continental band into a per-face HPF_RES^2 RGBA32F 2D-array (seaBias, elevAmp,
|
|
@@ -414,7 +414,7 @@ export async function initMapspinnerPlanet(gl, opts = {}) {
|
|
|
414
414
|
fallbackCount: c.fallbackCount, maxFallbackLevel: c.maxFallbackLevel, frontFallback: c.frontFallback, cached: true };
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
-
// configure the
|
|
417
|
+
// configure the quadtree once (meter units; root l=2R).
|
|
418
418
|
// splitFactor is LIVE-TUNABLE via window.__splitFactor so the px/poly target
|
|
419
419
|
// (4-50 px per triangle edge) can be calibrated in-browser with one dispatch
|
|
420
420
|
// (no wasm rebuild / reload) -- the efficiency analog of __diag.setGen. A SMALLER
|
package/src/quadtree.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
// quadtree.js -- the cube-face terrain quadtree LOD selection
|
|
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 --
|
|
1
|
+
// quadtree.js -- the cube-face terrain quadtree LOD selection.
|
|
5
2
|
// terrain shape is the single GPU fractal (broadShapeM in terrain.glsl), evaluated per-vertex.
|
|
6
3
|
//
|
|
7
4
|
// Pure JS, no wasm, no allocation per frame beyond the leaf array. One Quadtree instance per
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/shaders/atmosphere.glsl (#version 300 es is prepended by the JS loader)
|
|
2
2
|
// -----------------------------------------------------------------------------
|
|
3
|
-
// WebGL2 analytic Rayleigh/Mie single-scatter atmosphere for the
|
|
3
|
+
// WebGL2 analytic Rayleigh/Mie single-scatter atmosphere for the planet.
|
|
4
4
|
//
|
|
5
5
|
// PORT NOTE — what this is vs. the WebGPU path:
|
|
6
6
|
// planet.html runs the AUTHORITATIVE Bruneton model: it BAKES the transmittance/
|
package/src/shaders/terrain.glsl
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// Driven by the thin WebGL2 render layer (per-quad deformation uniforms + textureTile
|
|
6
|
-
// coords from the Proland wasm). No geometry/tess -> WebGL2-clean. The JS prepends
|
|
7
|
-
// #version + precision and compiles _VERTEX_ / _FRAGMENT_ separately.
|
|
1
|
+
// mapspinner WebGL2 terrain render shader.
|
|
2
|
+
// VS: spherical deformation + direct per-vertex sphere projection.
|
|
3
|
+
// FS: sample normal + albedo, sun-lit Lambert with ambient floor.
|
|
4
|
+
// The JS prepends #version + precision and compiles _VERTEX_ / _FRAGMENT_ separately.
|
|
8
5
|
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
// --- DIRECT per-vertex sphere-projection uniforms (replaces the Proland corner-blend CLOD) ------
|
|
6
|
+
// --- DIRECT per-vertex sphere-projection uniforms ------
|
|
12
7
|
// SINGLE INSTANCED DRAW: defOffset (ox,oy,l,level) + the face frame are PER-INSTANCE now (one
|
|
13
8
|
// gl.drawElementsInstanced over the whole visible leaf set, no per-quad uniform churn). In the VS
|
|
14
9
|
// they come from instance attributes (iOffset + iFace); defRadius/defViewProjRel stay uniforms
|
|
@@ -712,7 +707,7 @@ mat3 faceFrame(float f){
|
|
|
712
707
|
// function of world dir -> replaces the old hardcoded sin/cos lobe with the editable field.
|
|
713
708
|
highp float continentalBias(vec3 dir) { return hpfSample(dir).r; } // W7: R = seaBias (meters) -> highp
|
|
714
709
|
|
|
715
|
-
// ---- CONTINUOUS BROAD+MID SHAPE (LOD-uniformity fix
|
|
710
|
+
// ---- CONTINUOUS BROAD+MID SHAPE (LOD-uniformity fix). The
|
|
716
711
|
// per-level g_noiseAmp cascade adds a DIFFERENT noise draw at each LOD, so consecutive levels
|
|
717
712
|
// look different (1500km vs 1200km "way different"). The fix: source the broad+regional SHAPE
|
|
718
713
|
// from ONE continuous fBm of WORLD DIRECTION, sampled the same at every LOD -> a finer level is
|
|
@@ -966,7 +961,8 @@ void main() {
|
|
|
966
961
|
float dHdvN = float(hFD2 - h) / nStep;
|
|
967
962
|
vNrm = normalize(uxN * (-dHduN) + uyN * (-dHdvN) + uzN); // n = normalize([-dz/du,-dz/dv,1]) in the (ux,uy,uz) frame
|
|
968
963
|
}
|
|
969
|
-
// DIRECT per-vertex sphere projection (replaces the
|
|
964
|
+
// DIRECT per-vertex sphere projection (replaces the
|
|
965
|
+
deformedCorners*alphaPrime blend,
|
|
970
966
|
// which bilinearly interpolated 4 deformed corners -> FLAT quad interior -> faceted at high GRID).
|
|
971
967
|
// dir0 is THIS vertex's world direction (faceWarp'd, defLocalToWorld-mapped); place it on the
|
|
972
968
|
// sphere at radius R+h and project. Every vertex curves -> round at any tessellation.
|
|
@@ -2076,7 +2072,7 @@ void main() {
|
|
|
2076
2072
|
// ---- HEIGHT PROBE (collision): compute the EXACT rendered terrain height for one world dir,
|
|
2077
2073
|
// reusing the same hpfSample + broadShapeM the mesh VS uses, so the free-fly collision floor
|
|
2078
2074
|
// can never diverge from the rendered surface (user-chosen: read px from the GPU, not a CPU
|
|
2079
|
-
// mirror). Paired with a 1-point VS in
|
|
2075
|
+
// mirror). Paired with a 1-point VS in gl-render.js; writes height (metres) to R32F.
|
|
2080
2076
|
#ifdef _PROBE_
|
|
2081
2077
|
uniform vec3 probeDir; // world direction under the camera (normalized)
|
|
2082
2078
|
out vec4 probeOut;
|