mapspinner 0.1.38 → 0.1.40
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 +162 -10
- package/src/shaders/terrain.glsl +59 -2
package/package.json
CHANGED
package/src/gl-render.js
CHANGED
|
@@ -272,6 +272,140 @@ 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
|
+
|
|
341
|
+
// ===== THC HEIGHT POOL + LRU (the VS-sample consumer; the FPS win) =====
|
|
342
|
+
// The VS samples a baked per-tile height (O(1) texture fetch) instead of composeHeight 5x/vertex,
|
|
343
|
+
// when window.__thc is on. A 2D-array pool holds one BAKE_RES^2 R32F layer per live tile; a leaf
|
|
344
|
+
// gets a layer (baked once) on first sight, LRU-evicted when the pool is full. Default OFF -> the
|
|
345
|
+
// live render is unchanged (composeHeight), so this is safe to ship behind the toggle.
|
|
346
|
+
const THC_POOL_LAYERS = 512;
|
|
347
|
+
let heightPool=null, poolFbo=null;
|
|
348
|
+
const _tcMap = new Map(); // tileKey -> layer
|
|
349
|
+
const _tcLayerKey = new Array(THC_POOL_LAYERS).fill(null); // layer -> tileKey (evict bookkeeping)
|
|
350
|
+
const _tcUsed = new Int32Array(THC_POOL_LAYERS); // layer -> last-used frame
|
|
351
|
+
let _tcFrame = 0, _tcNextFree = 0, _tcBakesThisFrame = 0;
|
|
352
|
+
// BAKE-ON-EDIT: terraform/HPF changes make every baked layer stale -> drop the whole map so each
|
|
353
|
+
// visible tile re-bakes on next sight (synchronously, before its draw -> no black/stale frame).
|
|
354
|
+
function invalidatePool(){ _tcMap.clear(); _tcLayerKey.fill(null); _tcNextFree = 0; }
|
|
355
|
+
function ensurePool(){
|
|
356
|
+
if (heightPool) return;
|
|
357
|
+
heightPool = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D_ARRAY, heightPool);
|
|
358
|
+
gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.R32F, THC_BAKE_RES, THC_BAKE_RES, THC_POOL_LAYERS);
|
|
359
|
+
const lin = _halfFloatLinearOK ? gl.LINEAR : gl.NEAREST; // R32F LINEAR needs OES_texture_float_linear; else VS does manual bilinear
|
|
360
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, lin); gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, lin);
|
|
361
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
362
|
+
poolFbo = gl.createFramebuffer();
|
|
363
|
+
}
|
|
364
|
+
function bakeTileToLayer(face,ox,oy,l,level,layer){
|
|
365
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, poolFbo);
|
|
366
|
+
gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, heightPool, 0, layer);
|
|
367
|
+
gl.viewport(0,0,THC_BAKE_RES,THC_BAKE_RES);
|
|
368
|
+
gl.useProgram(bakeProg); gl.bindVertexArray(bakeVao);
|
|
369
|
+
if (_hpfTex){ gl.activeTexture(gl.TEXTURE3); gl.bindTexture(gl.TEXTURE_2D_ARRAY,_hpfTex); gl.uniform1i(BU('hpfPool'),3); }
|
|
370
|
+
if (_hpfTex2){ gl.activeTexture(gl.TEXTURE5); gl.bindTexture(gl.TEXTURE_2D_ARRAY,_hpfTex2); gl.uniform1i(BU('hpfPool2'),5); }
|
|
371
|
+
gl.uniform1i(BU('hasHpf'), _hpfTex?1:0);
|
|
372
|
+
setComposeHeightUniforms(BU);
|
|
373
|
+
gl.uniform1f(BU('defRadius'), R);
|
|
374
|
+
gl.uniformMatrix3fv(BU('uBakeFrame'), false, new Float32Array(_faceFrames[face|0]));
|
|
375
|
+
gl.uniform4f(BU('uBakeOffset'), ox, oy, l, level);
|
|
376
|
+
gl.uniform1f(BU('uBakeRes'), THC_BAKE_RES);
|
|
377
|
+
gl.disable(gl.DEPTH_TEST);
|
|
378
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
379
|
+
_tcBakesThisFrame++;
|
|
380
|
+
}
|
|
381
|
+
// pool layer for a tile, baked on first sight; LRU-evicts when full. Returns -1 if not yet bakeable.
|
|
382
|
+
function ensureTileLayer(face,ox,oy,l,level){
|
|
383
|
+
const key = face+':'+ox+':'+oy+':'+l;
|
|
384
|
+
let layer = _tcMap.get(key);
|
|
385
|
+
if (layer === undefined){
|
|
386
|
+
if (_tcNextFree < THC_POOL_LAYERS){ layer = _tcNextFree++; }
|
|
387
|
+
else { let lru=0, lruF=_tcUsed[0]; for(let k=1;k<THC_POOL_LAYERS;k++) if(_tcUsed[k]<lruF){lruF=_tcUsed[k];lru=k;} layer=lru; const old=_tcLayerKey[lru]; if(old!=null) _tcMap.delete(old); }
|
|
388
|
+
_tcMap.set(key, layer); _tcLayerKey[layer]=key;
|
|
389
|
+
bakeTileToLayer(face,ox,oy,l,level,layer);
|
|
390
|
+
}
|
|
391
|
+
_tcUsed[layer]=_tcFrame;
|
|
392
|
+
return layer;
|
|
393
|
+
}
|
|
394
|
+
// THC active = toggle on AND both programs/pool ready. Builds them lazily; returns false until ready
|
|
395
|
+
// so the first frames fall back to composeHeight (uThc=0) with no garbage.
|
|
396
|
+
let _tcInvSeen = 0;
|
|
397
|
+
function thcActive(){
|
|
398
|
+
if (typeof window==='undefined' || !window.__thc) return false;
|
|
399
|
+
if (!bakeProg){ ensureBake(); return false; }
|
|
400
|
+
ensurePool();
|
|
401
|
+
// live re-bake hook: window.__thcInvalidate() bumps __thcInval; any composeHeight-shaping edit
|
|
402
|
+
// (e.g. __gen biome/relief dials) should call it so the baked pool refreshes.
|
|
403
|
+
const inv = (window.__thcInval|0);
|
|
404
|
+
if (inv !== _tcInvSeen){ _tcInvSeen = inv; invalidatePool(); }
|
|
405
|
+
return !!heightPool;
|
|
406
|
+
}
|
|
407
|
+
if (typeof window !== 'undefined') window.__thcInvalidate = () => { window.__thcInval = (window.__thcInval|0) + 1; };
|
|
408
|
+
|
|
275
409
|
// FLOAT-LINEAR FORMAT PROBE (NOT a quality tier): OES_texture_float_linear lets the HPF atlas pools
|
|
276
410
|
// filter LINEAR in hardware -> hpfSample collapses to one texture() call. 0 = manual 4-tap fallback.
|
|
277
411
|
const _halfFloatLinearOK = !!gl.getExtension('OES_texture_float_linear') || !!gl.getExtension('OES_texture_half_float_linear');
|
|
@@ -576,7 +710,7 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
576
710
|
// the instance data is identical -> skip the Float32Array build + bufferData + water dedup Set-loop
|
|
577
711
|
// and just rebind+draw. Pure CPU/GC win (GPU is vertex-bound, the upload is off the critical path).
|
|
578
712
|
const instBufWater=gl.createBuffer();
|
|
579
|
-
let _instQuadsRef=null, _instWaterRef=null, _instWaterN=0;
|
|
713
|
+
let _instQuadsRef=null, _instWaterRef=null, _instWaterN=0, _lastThc=false;
|
|
580
714
|
|
|
581
715
|
// per-face local->world (cube face -> sphere local frame). Column-major mat3 packed
|
|
582
716
|
// into a Float32Array(9). Matches localToWorld3 convention:
|
|
@@ -896,14 +1030,27 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
896
1030
|
gl.uniformMatrix4fv(U('defViewProjRel'), false, viewProjRel);
|
|
897
1031
|
gl.uniform1f(U('defRadius'), R);
|
|
898
1032
|
const n = quads.length;
|
|
899
|
-
//
|
|
900
|
-
//
|
|
901
|
-
|
|
902
|
-
const FLOATS = 5; // [ox,oy,l,level,face]
|
|
1033
|
+
// Instance buffer: [ox,oy,l,level,face, iLayer] (6 floats). iLayer = the THC pool layer for this
|
|
1034
|
+
// tile (when __thc on); the VS samples the baked height there instead of composeHeight.
|
|
1035
|
+
const FLOATS = 6; // [ox,oy,l,level,face, iLayer]
|
|
903
1036
|
if (n > 0) {
|
|
904
|
-
//
|
|
905
|
-
//
|
|
906
|
-
const
|
|
1037
|
+
// THC: when active, ensure every visible tile has a baked pool layer (bake on first sight). The
|
|
1038
|
+
// bakes clobber the FBO/program/viewport -> restore the canvas render state afterward.
|
|
1039
|
+
const _thc = thcActive();
|
|
1040
|
+
let _layers = null;
|
|
1041
|
+
if (_thc) {
|
|
1042
|
+
_tcFrame++; _tcBakesThisFrame = 0;
|
|
1043
|
+
_layers = new Float32Array(n);
|
|
1044
|
+
for (let i = 0; i < n; i++) { const q = quads[i].quad; _layers[i] = ensureTileLayer(quads[i].face, q.ox, q.oy, q.l, q.level); }
|
|
1045
|
+
gl.bindVertexArray(null); // bakeTileToLayer left bakeVao bound; the main path uses the default VAO
|
|
1046
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
1047
|
+
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
|
1048
|
+
gl.enable(gl.DEPTH_TEST); // bakeTileToLayer disabled depth
|
|
1049
|
+
gl.useProgram(_activeProg);
|
|
1050
|
+
if (typeof window !== 'undefined') window.__thcBakes = _tcBakesThisFrame;
|
|
1051
|
+
}
|
|
1052
|
+
// STATIC-FRAME SKIP: rebuild only when the quad set changed OR the toggle flipped (iLayer needs writing).
|
|
1053
|
+
const _dirty = (quads !== _instQuadsRef) || (_thc !== _lastThc);
|
|
907
1054
|
gl.bindBuffer(gl.ARRAY_BUFFER, instBuf);
|
|
908
1055
|
if (_dirty) {
|
|
909
1056
|
const inst = new Float32Array(n * FLOATS);
|
|
@@ -911,12 +1058,17 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
911
1058
|
const q = quads[i].quad;
|
|
912
1059
|
inst[i*FLOATS+0] = q.ox; inst[i*FLOATS+1] = q.oy; inst[i*FLOATS+2] = q.l; inst[i*FLOATS+3] = q.level;
|
|
913
1060
|
inst[i*FLOATS+4] = quads[i].face;
|
|
1061
|
+
inst[i*FLOATS+5] = _layers ? _layers[i] : 0.0;
|
|
914
1062
|
}
|
|
915
1063
|
gl.bufferData(gl.ARRAY_BUFFER, inst, gl.DYNAMIC_DRAW);
|
|
916
1064
|
}
|
|
1065
|
+
_lastThc = _thc;
|
|
917
1066
|
const STRIDE = FLOATS * 4;
|
|
918
1067
|
gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 4, gl.FLOAT, false, STRIDE, 0); gl.vertexAttribDivisor(1, 1); // iOffset
|
|
919
1068
|
gl.enableVertexAttribArray(2); gl.vertexAttribPointer(2, 1, gl.FLOAT, false, STRIDE, 4 * 4); gl.vertexAttribDivisor(2, 1); // iFace
|
|
1069
|
+
gl.enableVertexAttribArray(3); gl.vertexAttribPointer(3, 1, gl.FLOAT, false, STRIDE, 5 * 4); gl.vertexAttribDivisor(3, 1); // iLayer (THC pool layer)
|
|
1070
|
+
gl.uniform1f(U('uThc'), _thc ? 1.0 : 0.0);
|
|
1071
|
+
if (_thc) { gl.activeTexture(gl.TEXTURE8); gl.bindTexture(gl.TEXTURE_2D_ARRAY, heightPool); gl.uniform1i(U('uHeightPool'), 8); gl.uniform1f(U('uPoolRes'), THC_BAKE_RES); gl.uniform1f(U('uPoolLinear'), _halfFloatLinearOK ? 1.0 : 0.0); }
|
|
920
1072
|
gl.uniform1f(U('uIsWater'), 0.0);
|
|
921
1073
|
// UNDERWATER DETECTION: camera below sea level enables underwater shading + water surface
|
|
922
1074
|
// rendering from below. Set before the terrain draw so the FS can apply underwater fog.
|
|
@@ -951,7 +1103,7 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
951
1103
|
if (lv > WCAP) { const A = l * Math.pow(2, lv - WCAP); ox = Math.floor(ox / A) * A; oy = Math.floor(oy / A) * A; l = A; lv = WCAP; }
|
|
952
1104
|
const key = quads[i].face + ':' + ox + ':' + oy + ':' + l;
|
|
953
1105
|
if (seen.has(key)) continue; seen.add(key);
|
|
954
|
-
wl.push(ox, oy, l, lv, quads[i].face);
|
|
1106
|
+
wl.push(ox, oy, l, lv, quads[i].face, 0); // iLayer unused for water (VS pins sea level)
|
|
955
1107
|
}
|
|
956
1108
|
_instWaterN = wl.length / FLOATS;
|
|
957
1109
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(wl), gl.DYNAMIC_DRAW);
|
|
@@ -1014,6 +1166,6 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
1014
1166
|
}
|
|
1015
1167
|
return out;
|
|
1016
1168
|
}
|
|
1017
|
-
function setHpf(tex, res, tex2) { _hpfTex = tex; _hpfRes = res|0; _hpfTex2 = tex2 || null; } // tex2 = RG8(temp,humid) pack (W12)
|
|
1169
|
+
function setHpf(tex, res, tex2) { _hpfTex = tex; _hpfRes = res|0; _hpfTex2 = tex2 || null; invalidatePool(); } // tex2 = RG8(temp,humid) pack (W12); HPF change -> re-bake THC tiles
|
|
1018
1170
|
return { get prog(){ return prog; }, render, checkGlError, probe, sampleGroundM, cullMatrix, recompile, setHpf, GRID, indexCount: indices.length, M4 };
|
|
1019
1171
|
}
|
package/src/shaders/terrain.glsl
CHANGED
|
@@ -390,7 +390,7 @@ highp vec2 faceWarp(highp vec2 p){ return defRadius * tan((p / defRadius) * 0.78
|
|
|
390
390
|
uniform float uDetailOverlay; // amplitude lever (user-tuned 6; 0 = off; __detailOverlay)
|
|
391
391
|
uniform float uGrid; // interior cells per tile edge (GRID=16); for mesh-based vertex normals
|
|
392
392
|
uniform float uNrmStepM; // lit-normal FD step in metres (150); uniform-fed to defeat FXC constant folding
|
|
393
|
-
#if defined(_VERTEX_) || defined(_PROBE_)
|
|
393
|
+
#if defined(_VERTEX_) || defined(_PROBE_) || defined(_HEIGHTBAKE_)
|
|
394
394
|
highp float broadShapeM(vec3 dir, float reliefMul, float ridgeMul){ // W7: returns metres (~13000) -> highp
|
|
395
395
|
if (hasHpf == 0) return 0.0;
|
|
396
396
|
float mtnAmp = 1.0; // mountain-amplitude (was a window.__mtnAmp uniform; inlined at neutral 1.0)
|
|
@@ -693,6 +693,25 @@ highp float composeHeight(vec3 dir0, highp vec2 faceLocal, float tileM){ // W7
|
|
|
693
693
|
layout(location=0) in vec3 vertex; // vertex.xy in [0,1] parametric quad coord
|
|
694
694
|
layout(location=1) in highp vec4 iOffset; // W7: PER-INSTANCE (ox, oy, l, level) face-local metres ~6.4e6 -> highp
|
|
695
695
|
layout(location=2) in float iFace; // PER-INSTANCE cube face index 0..5
|
|
696
|
+
layout(location=3) in float iLayer; // THC: PER-INSTANCE height-pool array layer for this tile (uThc on)
|
|
697
|
+
// THC (Tile Heightmap Cache): when uThc>0.5 the VS reads the baked composeHeight from uHeightPool
|
|
698
|
+
// (a 2D-array, one layer per visible tile, parametric uv = vertex.xy) instead of re-evaluating
|
|
699
|
+
// composeHeight 5x/vertex. uPoolLinear=0 -> manual bilinear (R32F not linear-filterable).
|
|
700
|
+
uniform float uThc;
|
|
701
|
+
uniform sampler2DArray uHeightPool;
|
|
702
|
+
uniform float uPoolRes;
|
|
703
|
+
uniform float uPoolLinear;
|
|
704
|
+
highp float thcSample(highp vec2 uv, float layer){
|
|
705
|
+
highp vec2 t = clamp(uv, 0.0, 1.0) * (uPoolRes - 1.0);
|
|
706
|
+
if (uPoolLinear > 0.5) return texture(uHeightPool, vec3((t + 0.5) / uPoolRes, layer)).r;
|
|
707
|
+
highp vec2 f = floor(t); highp vec2 fr = t - f;
|
|
708
|
+
highp vec2 b0 = (f + 0.5) / uPoolRes, b1 = (f + 1.5) / uPoolRes;
|
|
709
|
+
highp float h00 = texture(uHeightPool, vec3(b0.x, b0.y, layer)).r;
|
|
710
|
+
highp float h10 = texture(uHeightPool, vec3(b1.x, b0.y, layer)).r;
|
|
711
|
+
highp float h01 = texture(uHeightPool, vec3(b0.x, b1.y, layer)).r;
|
|
712
|
+
highp float h11 = texture(uHeightPool, vec3(b1.x, b1.y, layer)).r;
|
|
713
|
+
return mix(mix(h00, h10, fr.x), mix(h01, h11, fr.x), fr.y);
|
|
714
|
+
}
|
|
696
715
|
out highp vec3 vWorld; // W7 highp ISLAND: absolute world pos ~6.4e6 m (FS lighting/atmosphere) -- fp16 would jitter it
|
|
697
716
|
out highp float vH; // W7: signed elevation (metres, ~13000) -- highp for the material ramp / strata / ocean depth
|
|
698
717
|
out highp vec3 vNrm; // W8: world-space per-vertex analytic normal (fixed-step central diff of the FULL composeHeight). The SOLE FS lit normal -- replaces the jittery cross(dFdx,dFdy). highp to match vWorld on the ~6.4e6 m planet.
|
|
@@ -949,7 +968,26 @@ void main() {
|
|
|
949
968
|
// sides of a seam share the same dir0/tangent frame -> consistent normals, no row artifact.
|
|
950
969
|
highp float hN0 = 0.0;
|
|
951
970
|
highp vec3 vN = dir0;
|
|
952
|
-
if (uIsWater < 0.5) {
|
|
971
|
+
if (uIsWater < 0.5 && uThc > 0.5) {
|
|
972
|
+
// THC FAST PATH: height + symmetric central-diff normal from the baked pool. 5 bilinear texture
|
|
973
|
+
// taps replace 5 full composeHeight evals (the VS-bound cost). The 4 normal taps use the SAME
|
|
974
|
+
// parametric step as the composeHeight path; the dir at each tap is a cheap faceWarp+normalize
|
|
975
|
+
// (no field eval). uNrmStepM-scaled step => the same smoothed (low-pass) normal as the slow path.
|
|
976
|
+
hN0 = thcSample(vertex.xy, iLayer);
|
|
977
|
+
highp float nStepM = (uNrmStepM > 0.0) ? uNrmStepM : 300.0;
|
|
978
|
+
highp float duP = clamp(nStepM / max(defOffset.z, 1.0), 1.0 / ((uGrid > 0.0) ? uGrid : 16.0), 0.34);
|
|
979
|
+
highp float hPU = thcSample(vertex.xy + vec2(duP, 0.0), iLayer);
|
|
980
|
+
highp float hMU = thcSample(vertex.xy + vec2(-duP, 0.0), iLayer);
|
|
981
|
+
highp float hPV = thcSample(vertex.xy + vec2(0.0, duP), iLayer);
|
|
982
|
+
highp float hMV = thcSample(vertex.xy + vec2(0.0, -duP), iLayer);
|
|
983
|
+
highp vec3 dPU = normalize(defLocalToWorld * vec3(faceWarp((vertex.xy + vec2(duP,0.0)) * defOffset.z + defOffset.xy), defRadius));
|
|
984
|
+
highp vec3 dMU = normalize(defLocalToWorld * vec3(faceWarp((vertex.xy + vec2(-duP,0.0)) * defOffset.z + defOffset.xy), defRadius));
|
|
985
|
+
highp vec3 dPV = normalize(defLocalToWorld * vec3(faceWarp((vertex.xy + vec2(0.0,duP)) * defOffset.z + defOffset.xy), defRadius));
|
|
986
|
+
highp vec3 dMV = normalize(defLocalToWorld * vec3(faceWarp((vertex.xy + vec2(0.0,-duP)) * defOffset.z + defOffset.xy), defRadius));
|
|
987
|
+
vN = normalize(cross(dPU * (defRadius + hPU) - dMU * (defRadius + hMU),
|
|
988
|
+
dPV * (defRadius + hPV) - dMV * (defRadius + hMV)));
|
|
989
|
+
if (dot(vN, dir0) < 0.0) vN = -vN;
|
|
990
|
+
} else if (uIsWater < 0.5) {
|
|
953
991
|
hN0 = composeHeight(dir0, faceLocal, defOffset.z); // center = the geometry height h
|
|
954
992
|
// VERTEX NORMAL = CENTRAL DIFFERENCE in PARAMETRIC MESH SPACE over the FULL composeHeight (2026-06-14
|
|
955
993
|
// jagged-normal fix). Two earlier methods both jagged: (a) interior FORWARD mesh-cell cross product
|
|
@@ -2229,3 +2267,22 @@ void main(){
|
|
|
2229
2267
|
probeOut = vec4(h, 0.0, 0.0, 1.0);
|
|
2230
2268
|
}
|
|
2231
2269
|
#endif
|
|
2270
|
+
|
|
2271
|
+
#ifdef _HEIGHTBAKE_
|
|
2272
|
+
// THC HEIGHT-CACHE BAKE (2026-06-14): render ONE tile's composeHeight into a height-pool texel grid.
|
|
2273
|
+
// Each fragment = one parametric (u,v) of the tile; the dir + faceLocal mapping is IDENTICAL to the
|
|
2274
|
+
// mesh VS (faceWarp(vertex.xy*defOffset.z+defOffset.xy), normalize(frame*vec3(faceLocal,defRadius)))
|
|
2275
|
+
// so the baked height matches the procedural geometry exactly. NON-DESTRUCTIVE: this is a separate
|
|
2276
|
+
// program; the live VS still computes composeHeight until thc-vs-sample switches it to a pool sample.
|
|
2277
|
+
uniform mat3 uBakeFrame; // face-local -> world for this tile's face (== faceFrame(face))
|
|
2278
|
+
uniform vec4 uBakeOffset; // tile (ox, oy, l, level) face-local metres (== iOffset)
|
|
2279
|
+
uniform float uBakeRes; // bake grid resolution (texels per edge)
|
|
2280
|
+
out vec4 bakeOut;
|
|
2281
|
+
void main(){
|
|
2282
|
+
highp vec2 uv = (gl_FragCoord.xy - 0.5) / max(uBakeRes - 1.0, 1.0); // texel -> parametric [0,1]
|
|
2283
|
+
highp vec2 faceLocal = faceWarp(uv * uBakeOffset.z + uBakeOffset.xy);
|
|
2284
|
+
highp vec3 dir0 = normalize(uBakeFrame * vec3(faceLocal, defRadius));
|
|
2285
|
+
highp float h = composeHeight(dir0, faceLocal, uBakeOffset.z);
|
|
2286
|
+
bakeOut = vec4(h, 0.0, 0.0, 1.0);
|
|
2287
|
+
}
|
|
2288
|
+
#endif
|