mapspinner 0.1.34 → 0.1.36
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 +39 -18
- package/src/planet-orchestrator.js +1 -1
- package/src/shaders/terrain.glsl +13 -9
package/package.json
CHANGED
package/src/gl-render.js
CHANGED
|
@@ -49,7 +49,7 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
49
49
|
// (-52%) and tris/quad 1152->512 (-55%), so the per-vertex 14-oct broadShapeM VS (browser-9: 95% of
|
|
50
50
|
// the low-alt frame) runs on ~half the vertices. median scales ~24/16 -> ~3.6px, far closer to the
|
|
51
51
|
// band; the fine relief is carried per-pixel by the FS dFdx normal, not the mesh tessellation.
|
|
52
|
-
const GRID = opts.gridMeshSize ||
|
|
52
|
+
const GRID = opts.gridMeshSize || 8; // mesh quads per edge. FPS is TRIANGLE/VERTEX-throughput bound (not ALU). 16->11->8 (user 2026-06-14 144fps push): GRID 8 reclaimed once the biome bands were WARPED (warpN, no longer straight contours) and the relief-faceting (dFdx(vH)) was removed, so the earlier GRID-8 jaggedness is gone. Override via ?grid=N.
|
|
53
53
|
// Expose the LIVE mesh grid so screen-space-error diagnostics (planet.html __diag.pxPerPoly)
|
|
54
54
|
// divide by the real polys/tile instead of a stale literal. Any future GRID change self-corrects
|
|
55
55
|
// the metric (the 24->16 lever left pxPerPoly defaulting to 24 = 1.5x wrong band fraction).
|
|
@@ -569,6 +569,13 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
569
569
|
const vbo=gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER,vbo); gl.bufferData(gl.ARRAY_BUFFER,verts,gl.STATIC_DRAW);
|
|
570
570
|
const ibo=gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,ibo); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,indices,gl.STATIC_DRAW);
|
|
571
571
|
const instBuf=gl.createBuffer(); // per-instance [ox,oy,l,level,face] (filled per frame in render())
|
|
572
|
+
// DATA-CONTINUITY CACHE (2026-06-14): terrain + water get their OWN persistent instance buffers so
|
|
573
|
+
// neither clobbers the other (the shared-buffer clobber forced a re-upload every frame and was the
|
|
574
|
+
// root of the prior 'water drawn as terrain' regression). On a STATIC frame (same quads array object)
|
|
575
|
+
// the instance data is identical -> skip the Float32Array build + bufferData + water dedup Set-loop
|
|
576
|
+
// and just rebind+draw. Pure CPU/GC win (GPU is vertex-bound, the upload is off the critical path).
|
|
577
|
+
const instBufWater=gl.createBuffer();
|
|
578
|
+
let _instQuadsRef=null, _instWaterRef=null, _instWaterN=0;
|
|
572
579
|
|
|
573
580
|
// per-face local->world (cube face -> sphere local frame). Column-major mat3 packed
|
|
574
581
|
// into a Float32Array(9). Matches localToWorld3 convention:
|
|
@@ -892,14 +899,19 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
892
899
|
// h = composeHeight(dir) for each vertex.
|
|
893
900
|
const FLOATS = 5; // [ox,oy,l,level,face]
|
|
894
901
|
if (n > 0) {
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
inst[i*FLOATS+0] = q.ox; inst[i*FLOATS+1] = q.oy; inst[i*FLOATS+2] = q.l; inst[i*FLOATS+3] = q.level;
|
|
899
|
-
inst[i*FLOATS+4] = quads[i].face;
|
|
900
|
-
}
|
|
902
|
+
// STATIC-FRAME SKIP: only rebuild + re-upload the instance data when the quad SET changed (the
|
|
903
|
+
// orchestrator passes the SAME quads array object on a static camera, a fresh array on rebuild).
|
|
904
|
+
const _dirty = (quads !== _instQuadsRef);
|
|
901
905
|
gl.bindBuffer(gl.ARRAY_BUFFER, instBuf);
|
|
902
|
-
|
|
906
|
+
if (_dirty) {
|
|
907
|
+
const inst = new Float32Array(n * FLOATS);
|
|
908
|
+
for (let i = 0; i < n; i++) {
|
|
909
|
+
const q = quads[i].quad;
|
|
910
|
+
inst[i*FLOATS+0] = q.ox; inst[i*FLOATS+1] = q.oy; inst[i*FLOATS+2] = q.l; inst[i*FLOATS+3] = q.level;
|
|
911
|
+
inst[i*FLOATS+4] = quads[i].face;
|
|
912
|
+
}
|
|
913
|
+
gl.bufferData(gl.ARRAY_BUFFER, inst, gl.DYNAMIC_DRAW);
|
|
914
|
+
}
|
|
903
915
|
const STRIDE = FLOATS * 4;
|
|
904
916
|
gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 4, gl.FLOAT, false, STRIDE, 0); gl.vertexAttribDivisor(1, 1); // iOffset
|
|
905
917
|
gl.enableVertexAttribArray(2); gl.vertexAttribPointer(2, 1, gl.FLOAT, false, STRIDE, 4 * 4); gl.vertexAttribDivisor(2, 1); // iFace
|
|
@@ -926,17 +938,24 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
926
938
|
// (~1.6km) sag ~5cm, far under any visible bathymetry, still ~16-64x fewer water verts
|
|
927
939
|
// than the deep terrain leaves.
|
|
928
940
|
const WCAP = 9;
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
941
|
+
// OWN persistent buffer (instBufWater) + static-frame skip: on an unchanged quad set, reuse the
|
|
942
|
+
// cached water instances (skip the dedup Set-loop + Float32Array + bufferData). Separate buffer
|
|
943
|
+
// means the terrain pass never clobbers it (the prior water-as-terrain regression root).
|
|
944
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, instBufWater);
|
|
945
|
+
if (_dirty || quads !== _instWaterRef) {
|
|
946
|
+
const seen = new Set(); const wl = [];
|
|
947
|
+
for (let i = 0; i < n; i++) {
|
|
948
|
+
const q = quads[i].quad; let ox = q.ox, oy = q.oy, l = q.l, lv = q.level;
|
|
949
|
+
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; }
|
|
950
|
+
const key = quads[i].face + ':' + ox + ':' + oy + ':' + l;
|
|
951
|
+
if (seen.has(key)) continue; seen.add(key);
|
|
952
|
+
wl.push(ox, oy, l, lv, quads[i].face);
|
|
953
|
+
}
|
|
954
|
+
_instWaterN = wl.length / FLOATS;
|
|
955
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(wl), gl.DYNAMIC_DRAW);
|
|
956
|
+
_instWaterRef = quads;
|
|
936
957
|
}
|
|
937
|
-
const wn =
|
|
938
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, instBuf);
|
|
939
|
-
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(wl), gl.DYNAMIC_DRAW);
|
|
958
|
+
const wn = _instWaterN;
|
|
940
959
|
gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 4, gl.FLOAT, false, STRIDE, 0); gl.vertexAttribDivisor(1, 1);
|
|
941
960
|
gl.enableVertexAttribArray(2); gl.vertexAttribPointer(2, 1, gl.FLOAT, false, STRIDE, 4 * 4); gl.vertexAttribDivisor(2, 1);
|
|
942
961
|
if (_uw) {
|
|
@@ -954,6 +973,8 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
954
973
|
gl.disable(gl.BLEND);
|
|
955
974
|
if (typeof window !== 'undefined') window.__lastWaterQuads = wn;
|
|
956
975
|
}
|
|
976
|
+
_instQuadsRef = quads; // mark this quad set uploaded; next frame with the same array skips the rebuild
|
|
977
|
+
if (typeof window !== 'undefined') window.__instUploads = (window.__instUploads | 0) + (_dirty ? 1 : 0);
|
|
957
978
|
}
|
|
958
979
|
if (typeof window !== 'undefined') window.__lastDrawCalls = (n > 0) ? 2 : 0;
|
|
959
980
|
return 0; // glError is checked via checkGlError() once per frame after quadtree (CPU/GPU pipelining)
|
|
@@ -177,7 +177,7 @@ export async function initMapspinnerPlanet(gl, opts = {}) {
|
|
|
177
177
|
// count (orbit 860->20, lowalt 1272->328). 1.0 is the calibrated default; override live
|
|
178
178
|
// via window.__splitFactor.
|
|
179
179
|
const splitFactor = opts.splitFactor ?? 1.0;
|
|
180
|
-
const gridMeshSize = opts.gridMeshSize ||
|
|
180
|
+
const gridMeshSize = opts.gridMeshSize || 8; // 16->11 FPS lever (triangle-throughput bound, not ALU; GRID 8 jagged biome crossovers, see gl-render.js GRID)
|
|
181
181
|
|
|
182
182
|
gl.getExtension('EXT_color_buffer_float'); // RGBA32F atlas render targets
|
|
183
183
|
// OES_texture_float_linear lets the driver LINEAR-filter the RGBA32F HPF/elevation textures. When
|
package/src/shaders/terrain.glsl
CHANGED
|
@@ -964,20 +964,24 @@ void main() {
|
|
|
964
964
|
// so it never drops below the mesh cell (would re-alias) nor exceeds ~1/3 the tile.
|
|
965
965
|
highp float nStepM = (uNrmStepM > 0.0) ? uNrmStepM : 300.0;
|
|
966
966
|
highp float duP = clamp(nStepM / max(defOffset.z, 1.0), 1.0 / ((uGrid > 0.0) ? uGrid : 16.0), 0.34);
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
967
|
+
// 3-TAP FORWARD (FPS push 2026-06-14: target 144fps): center + 2 FORWARD offsets, NOT the 5-tap
|
|
968
|
+
// central. The central was added for jaggedness, but the actual jaggedness root was the dFdx(vH)
|
|
969
|
+
// relief term (now removed) -- at the LOW-PASS step (duP ~300m >> vertex spacing) the forward vs
|
|
970
|
+
// central asymmetry is negligible while it costs 3 composeHeight/vertex instead of 5 (-40% of the
|
|
971
|
+
// dominant VS cost). Still captures vtxDisplace (faceWarp shifts both dir+faceLocal per tap).
|
|
972
|
+
highp float hPU = 0.0, hPV = 0.0;
|
|
973
|
+
highp vec3 dPU = dir0, dPV = dir0;
|
|
974
|
+
int fdIters = (uGrid >= 0.0) ? 2 : 1; // 2 forward offset taps (+ center hN0) = 3-tap normal; runtime-bounded (FXC unroll-defeat)
|
|
970
975
|
for (int i = 0; i < fdIters; i++) {
|
|
971
|
-
highp vec2 off = (i == 0) ? vec2(duP, 0.0) :
|
|
976
|
+
highp vec2 off = (i == 0) ? vec2(duP, 0.0) : vec2(0.0, duP);
|
|
972
977
|
highp vec2 fl = faceWarp((vertex.xy + off) * defOffset.z + defOffset.xy);
|
|
973
978
|
highp vec3 dd = normalize(defLocalToWorld * vec3(fl, defRadius));
|
|
974
979
|
highp float hh = composeHeight(dd, fl, defOffset.z);
|
|
975
|
-
if (i == 0) { hPU = hh; dPU = dd; } else
|
|
976
|
-
else if (i == 2) { hPV = hh; dPV = dd; } else { hMV = hh; dMV = dd; }
|
|
980
|
+
if (i == 0) { hPU = hh; dPU = dd; } else { hPV = hh; dPV = dd; }
|
|
977
981
|
}
|
|
978
|
-
highp vec3
|
|
979
|
-
highp vec3
|
|
980
|
-
vN = normalize(cross(
|
|
982
|
+
highp vec3 wC = dir0 * (defRadius + hN0);
|
|
983
|
+
highp vec3 wU = dPU * (defRadius + hPU), wV = dPV * (defRadius + hPV);
|
|
984
|
+
vN = normalize(cross(wU - wC, wV - wC));
|
|
981
985
|
if (dot(vN, dir0) < 0.0) vN = -vN; // keep it outward (terrain has no overhangs)
|
|
982
986
|
}
|
|
983
987
|
highp float h = hN0;
|