mapspinner 0.1.37 → 0.1.39
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/package.json +1 -1
- package/src/gl-render.js +68 -0
- package/src/shaders/terrain.glsl +25 -1
package/package.json
CHANGED
package/src/gl-render.js
CHANGED
|
@@ -272,6 +272,72 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
272
272
|
return out[0];
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
// ===== THC HEIGHT-CACHE BAKE (2026-06-14, NON-DESTRUCTIVE) =====
|
|
276
|
+
// A separate program renders composeHeight for one tile into an R32F grid (a fullscreen tri; each
|
|
277
|
+
// fragment = one tile parametric texel). Used FIRST as a readback witness (bake vs procedural
|
|
278
|
+
// sampleGroundM) to prove the bake matches the geometry; the pool/LRU + the VS-sample switch are
|
|
279
|
+
// later DAG nodes. _faceFrames mirror terrain.glsl faceFrame() columns (column-major mat3).
|
|
280
|
+
const THC_BAKE_RES = 130;
|
|
281
|
+
const _faceFrames = [
|
|
282
|
+
[0,0,-1, 0,1,0, 1,0,0], [0,0,1, 0,1,0, -1,0,0],
|
|
283
|
+
[1,0,0, 0,0,-1, 0,1,0], [1,0,0, 0,0,1, 0,-1,0],
|
|
284
|
+
[1,0,0, 0,1,0, 0,0,1], [-1,0,0, 0,1,0, 0,0,-1],
|
|
285
|
+
];
|
|
286
|
+
let bakeProg=null, bakeTex=null, bakeFbo=null, _bakeBuilding=null; const _bakeUloc=new Map();
|
|
287
|
+
const BU = n => { let l=_bakeUloc.get(n); if(l===undefined){ l=gl.getUniformLocation(bakeProg,n); _bakeUloc.set(n,l);} return l; };
|
|
288
|
+
function ensureBake(){
|
|
289
|
+
if (bakeProg || _bakeBuilding) return;
|
|
290
|
+
_bakeBuilding = (async () => {
|
|
291
|
+
try {
|
|
292
|
+
const bvs = hdr + 'void main(){ vec2 p=vec2((gl_VertexID==1)?3.0:-1.0,(gl_VertexID==2)?3.0:-1.0); gl_Position=vec4(p,0.0,1.0); }';
|
|
293
|
+
const bfs = hdr + '\n#define _HEIGHTBAKE_\n' + src;
|
|
294
|
+
const bv=gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(bv,bvs); gl.compileShader(bv);
|
|
295
|
+
const bf=gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(bf,bfs); gl.compileShader(bf);
|
|
296
|
+
const bp=gl.createProgram(); gl.attachShader(bp,bv); gl.attachShader(bp,bf); gl.linkProgram(bp);
|
|
297
|
+
await awaitProgramLink(bp, bv, bf, 'bake');
|
|
298
|
+
const tex=gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
299
|
+
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.R32F, THC_BAKE_RES, THC_BAKE_RES);
|
|
300
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
301
|
+
const fbo=gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
302
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
|
|
303
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
304
|
+
_bakeUloc.clear(); bakeTex=tex; bakeFbo=fbo; bakeProg=bp;
|
|
305
|
+
} catch(e){ bakeProg=null; try{ if(typeof window!=='undefined') window.__bakeErr=String(e.message||e); }catch(_){} }
|
|
306
|
+
finally { _bakeBuilding=null; }
|
|
307
|
+
})();
|
|
308
|
+
}
|
|
309
|
+
const bakeVao = gl.createVertexArray();
|
|
310
|
+
// bake ONE tile into bakeTex + read it back (Float32Array of THC_BAKE_RES^2 heights). Returns null
|
|
311
|
+
// until the program is built (lazy). NON-DESTRUCTIVE: does not touch the live render path.
|
|
312
|
+
function bakeTileReadback(face, ox, oy, l, level){
|
|
313
|
+
if (!bakeProg){ ensureBake(); return null; }
|
|
314
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, bakeFbo);
|
|
315
|
+
gl.viewport(0,0,THC_BAKE_RES,THC_BAKE_RES);
|
|
316
|
+
gl.useProgram(bakeProg);
|
|
317
|
+
gl.bindVertexArray(bakeVao);
|
|
318
|
+
if (_hpfTex){ gl.activeTexture(gl.TEXTURE3); gl.bindTexture(gl.TEXTURE_2D_ARRAY,_hpfTex); gl.uniform1i(BU('hpfPool'),3); }
|
|
319
|
+
if (_hpfTex2){ gl.activeTexture(gl.TEXTURE5); gl.bindTexture(gl.TEXTURE_2D_ARRAY,_hpfTex2); gl.uniform1i(BU('hpfPool2'),5); }
|
|
320
|
+
gl.uniform1i(BU('hasHpf'), _hpfTex?1:0);
|
|
321
|
+
setComposeHeightUniforms(BU);
|
|
322
|
+
gl.uniform1f(BU('defRadius'), R);
|
|
323
|
+
gl.uniformMatrix3fv(BU('uBakeFrame'), false, new Float32Array(_faceFrames[face|0]));
|
|
324
|
+
gl.uniform4f(BU('uBakeOffset'), ox, oy, l, level);
|
|
325
|
+
gl.uniform1f(BU('uBakeRes'), THC_BAKE_RES);
|
|
326
|
+
gl.disable(gl.DEPTH_TEST);
|
|
327
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
328
|
+
const buf = new Float32Array(THC_BAKE_RES*THC_BAKE_RES*4);
|
|
329
|
+
gl.readPixels(0,0,THC_BAKE_RES,THC_BAKE_RES, gl.RGBA, gl.FLOAT, buf);
|
|
330
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
331
|
+
gl.bindVertexArray(null);
|
|
332
|
+
const out = new Float32Array(THC_BAKE_RES*THC_BAKE_RES);
|
|
333
|
+
for (let i=0;i<out.length;i++) out[i]=buf[i*4];
|
|
334
|
+
let dbg=null; try{ dbg={ offLoc: BU('uBakeOffset')!=null, resLoc: BU('uBakeRes')!=null, frameLoc: BU('uBakeFrame')!=null,
|
|
335
|
+
offRead: BU('uBakeOffset')?Array.from(gl.getUniform(bakeProg, BU('uBakeOffset'))):null,
|
|
336
|
+
resRead: BU('uBakeRes')?gl.getUniform(bakeProg, BU('uBakeRes')):null }; }catch(e){ dbg={err:String(e)}; }
|
|
337
|
+
return { heights: out, res: THC_BAKE_RES, dbg };
|
|
338
|
+
}
|
|
339
|
+
if (typeof window !== 'undefined') { window.__thcBakeReadback = bakeTileReadback; window.__thcEnsureBake = ensureBake; }
|
|
340
|
+
|
|
275
341
|
// FLOAT-LINEAR FORMAT PROBE (NOT a quality tier): OES_texture_float_linear lets the HPF atlas pools
|
|
276
342
|
// filter LINEAR in hardware -> hpfSample collapses to one texture() call. 0 = manual 4-tap fallback.
|
|
277
343
|
const _halfFloatLinearOK = !!gl.getExtension('OES_texture_float_linear') || !!gl.getExtension('OES_texture_half_float_linear');
|
|
@@ -286,6 +352,7 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
286
352
|
gl.uniform1f(loc('uDetailOverlay'), g('detailOverlay', 6.0)); // perlin-everywhere ELEVATION term in composeHeight -- probe must match the VS or collision diverges
|
|
287
353
|
gl.uniform1f(loc('vtxDetail'), g('vtxDetail', 1.0)); // DECISIVE: vtxDisplace strength (early-return on 0)
|
|
288
354
|
gl.uniform1f(loc('canyonDepthMul'), g('canyonDepth', 1.0));
|
|
355
|
+
gl.uniform1f(loc('uVsCheap'), (typeof window!=='undefined' && window.__vsCheap) ? 1.0 : 0.0); // VS carve-cost profiling A/B
|
|
289
356
|
gl.uniform1f(loc('uBeachShelfM'), g('beachShelf', 2400.0)); // land coastal shelf (geometry); probe MUST match render
|
|
290
357
|
gl.uniform1f(loc('cliffAmt'), g('cliffAmt', 1.0));
|
|
291
358
|
gl.uniform1i(loc('uFloatLinearOK'), _halfFloatLinearOK ? 1 : 0);
|
|
@@ -806,6 +873,7 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
806
873
|
// literals so the look is unchanged until the user dials a window global.
|
|
807
874
|
const _g = (n,d)=> (typeof window!=='undefined' && window['__'+n]!=null) ? +window['__'+n] : d;
|
|
808
875
|
gl.uniform1f(U('canyonDepthMul'), _g('canyonDepth', 1.0));
|
|
876
|
+
gl.uniform1f(U('uVsCheap'), (typeof window!=='undefined' && window.__vsCheap) ? 1.0 : 0.0); // VS carve-cost profiling A/B
|
|
809
877
|
gl.uniform1f(U('uBeachShelfM'), _g('beachShelf', 2400.0)); // land coastal shelf (geometry): h<S eased h*h/S = wide beach
|
|
810
878
|
gl.uniform1f(U('uHiFreqCut'), _g('hiFreqCut', 0.25)); // 0.5->0.25 (2026-06-10 'blotchy' -- see setComposeHeightUniforms)
|
|
811
879
|
gl.uniform1f(U('uVertexAO'), _g('vertexAO', 1.0)); // per-vertex shading/AO strength (DEFECT 2, 2026-06-06)
|
package/src/shaders/terrain.glsl
CHANGED
|
@@ -198,6 +198,7 @@ float riverCarveM(vec3 dir, out float wet){
|
|
|
198
198
|
float riverCarveM(vec3 dir){ float w; return riverCarveM(dir, w); }
|
|
199
199
|
const float CANYON_INCISE_DEPTH = 1400.0; // metres at the gorge floor (user 2026-06-02: deepen+widen so canyons visibly sculpt the elevation; was 480m = invisible vs multi-km relief)
|
|
200
200
|
uniform float canyonDepthMul; // LIVE canyon-depth lever (window.__canyonDepth; 1.0 default)
|
|
201
|
+
uniform float uVsCheap; // VS profiling: >0.5 skips all carves in composeHeight (window.__vsCheap; gpuTimer carve-cost A/B)
|
|
201
202
|
// W5: uNrmGain deleted (only fed the removed VS slopeGain).
|
|
202
203
|
uniform float uVertexAO; // per-vertex shading/AO strength lever (window.__vertexAO; 1.0 default)
|
|
203
204
|
// SEPARATE WATER SURFACE (user 2026-06-11 'the ocean should be a separate surface'): the same
|
|
@@ -389,7 +390,7 @@ highp vec2 faceWarp(highp vec2 p){ return defRadius * tan((p / defRadius) * 0.78
|
|
|
389
390
|
uniform float uDetailOverlay; // amplitude lever (user-tuned 6; 0 = off; __detailOverlay)
|
|
390
391
|
uniform float uGrid; // interior cells per tile edge (GRID=16); for mesh-based vertex normals
|
|
391
392
|
uniform float uNrmStepM; // lit-normal FD step in metres (150); uniform-fed to defeat FXC constant folding
|
|
392
|
-
#if defined(_VERTEX_) || defined(_PROBE_)
|
|
393
|
+
#if defined(_VERTEX_) || defined(_PROBE_) || defined(_HEIGHTBAKE_)
|
|
393
394
|
highp float broadShapeM(vec3 dir, float reliefMul, float ridgeMul){ // W7: returns metres (~13000) -> highp
|
|
394
395
|
if (hasHpf == 0) return 0.0;
|
|
395
396
|
float mtnAmp = 1.0; // mountain-amplitude (was a window.__mtnAmp uniform; inlined at neutral 1.0)
|
|
@@ -643,6 +644,10 @@ highp float composeHeight(vec3 dir0, highp vec2 faceLocal, float tileM){ // W7
|
|
|
643
644
|
// Shore-gated (fades in over the first 250m of land) so the coastline and the flat water planes are
|
|
644
645
|
// untouched and no noise islets pop offshore. The VS FD lit-normal picks it up automatically.
|
|
645
646
|
h += detailFbm(dir0) * uDetailOverlay * 30.0 * step(0.0, h);
|
|
647
|
+
// VS-PROFILING GATE (2026-06-14): uVsCheap>0.5 returns BEFORE all the carves (valley/lake/river/
|
|
648
|
+
// canyon/cliff/dune) so gpuTimer can A/B the per-vertex CARVE cost (the doctrine's suspected
|
|
649
|
+
// dominant term). Transient profiling toggle (window.__vsCheap); 0 = full path (default).
|
|
650
|
+
if (uVsCheap > 0.5) return h;
|
|
646
651
|
// FLAT-AREA VALLEY NETWORKS + LAKES (user 2026-06-13): incised valley systems in low-relief
|
|
647
652
|
// plains. Replaces the old noise bumps with a ridge-field valley network for connected
|
|
648
653
|
// linear depressions and lakes that fill the valley bottoms. Fades to zero by reliefMul ~0.5.
|
|
@@ -2224,3 +2229,22 @@ void main(){
|
|
|
2224
2229
|
probeOut = vec4(h, 0.0, 0.0, 1.0);
|
|
2225
2230
|
}
|
|
2226
2231
|
#endif
|
|
2232
|
+
|
|
2233
|
+
#ifdef _HEIGHTBAKE_
|
|
2234
|
+
// THC HEIGHT-CACHE BAKE (2026-06-14): render ONE tile's composeHeight into a height-pool texel grid.
|
|
2235
|
+
// Each fragment = one parametric (u,v) of the tile; the dir + faceLocal mapping is IDENTICAL to the
|
|
2236
|
+
// mesh VS (faceWarp(vertex.xy*defOffset.z+defOffset.xy), normalize(frame*vec3(faceLocal,defRadius)))
|
|
2237
|
+
// so the baked height matches the procedural geometry exactly. NON-DESTRUCTIVE: this is a separate
|
|
2238
|
+
// program; the live VS still computes composeHeight until thc-vs-sample switches it to a pool sample.
|
|
2239
|
+
uniform mat3 uBakeFrame; // face-local -> world for this tile's face (== faceFrame(face))
|
|
2240
|
+
uniform vec4 uBakeOffset; // tile (ox, oy, l, level) face-local metres (== iOffset)
|
|
2241
|
+
uniform float uBakeRes; // bake grid resolution (texels per edge)
|
|
2242
|
+
out vec4 bakeOut;
|
|
2243
|
+
void main(){
|
|
2244
|
+
highp vec2 uv = (gl_FragCoord.xy - 0.5) / max(uBakeRes - 1.0, 1.0); // texel -> parametric [0,1]
|
|
2245
|
+
highp vec2 faceLocal = faceWarp(uv * uBakeOffset.z + uBakeOffset.xy);
|
|
2246
|
+
highp vec3 dir0 = normalize(uBakeFrame * vec3(faceLocal, defRadius));
|
|
2247
|
+
highp float h = composeHeight(dir0, faceLocal, uBakeOffset.z);
|
|
2248
|
+
bakeOut = vec4(h, 0.0, 0.0, 1.0);
|
|
2249
|
+
}
|
|
2250
|
+
#endif
|