@ortho-earth/voxel 0.4.0 → 0.6.0
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/README.md +14 -4
- package/dist/grid.d.ts +36 -7
- package/dist/grid.js +111 -29
- package/dist/index.d.ts +24 -6
- package/dist/index.js +40 -20
- package/dist/sources.d.ts +91 -0
- package/dist/sources.js +514 -0
- package/dist/voxel-gl.d.ts +21 -5
- package/dist/voxel-gl.js +148 -40
- package/dist/voxel-gpu.js +128 -27
- package/dist/voxel-reader.d.ts +13 -3
- package/dist/voxel-reader.js +10 -7
- package/package.json +2 -2
package/dist/voxel-gl.js
CHANGED
|
@@ -142,7 +142,7 @@ void main() {
|
|
|
142
142
|
}
|
|
143
143
|
${pick ? `discard;` : `if (u_mode != 1 || acc.a < 1.0 / 255.0) discard; o = acc;`}
|
|
144
144
|
}`;
|
|
145
|
-
let glc = null, host = null, prog = null, pickProg = null, boxVao = null, wantDepth = false, dummyTex = null, max3d = 256;
|
|
145
|
+
let budget = Infinity, glc = null, host = null, prog = null, pickProg = null, boxVao = null, wantDepth = false, dummyTex = null, max3d = 256;
|
|
146
146
|
let pickFbo = null, pickTex = null, pickW = 0, pickH = 0;
|
|
147
147
|
const layers = new Map(), probes = [];
|
|
148
148
|
const R_SPHERE = 6371000, R_ELL = 6378137, D2R = Math.PI / 180;
|
|
@@ -235,33 +235,15 @@ export function normalMats(A, Ainv) {
|
|
|
235
235
|
return { nrm, gradM };
|
|
236
236
|
}
|
|
237
237
|
const upCmap = (t, cmap) => { const gl = glc; gl.bindTexture(gl.TEXTURE_2D, t); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, 256, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, cmap); gl.bindTexture(gl.TEXTURE_2D, null); };
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
238
|
+
/** 元のセルの大きさ(m)=box は A の列の短い方・region は cellM の短い方 */
|
|
239
|
+
export const cellOf = (A, geo) => geo ? Math.min(...geo.cellM) : Math.min(...[0, 1, 2].map(c => Math.hypot(A[c], A[3 + c], A[6 + c])));
|
|
240
|
+
function mkLvl(b, comps, region, single) {
|
|
241
|
+
const gl = glc, [nx, ny, nz] = b.dims;
|
|
241
242
|
if (nx > max3d || ny > max3d || nz > max3d)
|
|
242
|
-
throw new Error(`voxel: dims ${
|
|
243
|
-
const
|
|
244
|
-
const tex = gl.createTexture();
|
|
245
|
-
gl.bindTexture(gl.TEXTURE_3D, tex);
|
|
246
|
-
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
247
|
-
if (d.comps === 4)
|
|
248
|
-
gl.texImage3D(gl.TEXTURE_3D, 0, gl.RGBA8, nx, ny, nz, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
|
249
|
-
else
|
|
250
|
-
gl.texImage3D(gl.TEXTURE_3D, 0, gl.R8, nx, ny, nz, 0, gl.RED, gl.UNSIGNED_BYTE, data);
|
|
251
|
-
for (const [p, v] of [[gl.TEXTURE_MIN_FILTER, gl.LINEAR], [gl.TEXTURE_MAG_FILTER, gl.LINEAR], [gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE], [gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE], [gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE]])
|
|
252
|
-
gl.texParameteri(gl.TEXTURE_3D, p, v);
|
|
253
|
-
gl.bindTexture(gl.TEXTURE_3D, null);
|
|
254
|
-
return tex;
|
|
255
|
-
};
|
|
256
|
-
const texs = (d.frames?.length ? d.frames : [d.tex]).map(mk), frame = Math.min(texs.length - 1, Math.max(0, d.frame ?? 0)), tex = texs[frame];
|
|
257
|
-
const cmap = gl.createTexture();
|
|
258
|
-
gl.bindTexture(gl.TEXTURE_2D, cmap);
|
|
259
|
-
for (const [p, v] of [[gl.TEXTURE_MIN_FILTER, gl.LINEAR], [gl.TEXTURE_MAG_FILTER, gl.LINEAR], [gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE], [gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE]])
|
|
260
|
-
gl.texParameteri(gl.TEXTURE_2D, p, v);
|
|
261
|
-
upCmap(cmap, d.cmap);
|
|
262
|
-
const A = Float64Array.from(d.A), Ainv = inv3(A), { nrm, gradM } = normalMats(A, Ainv);
|
|
243
|
+
throw new Error(`voxel: dims ${b.dims.join("×")} exceed MAX_3D_TEXTURE_SIZE ${max3d}`);
|
|
244
|
+
const A = Float64Array.from(b.A), Ainv = inv3(A), { nrm, gradM } = normalMats(A, Ainv);
|
|
263
245
|
let vao = null, vb = null, nVerts = 36;
|
|
264
|
-
const geo =
|
|
246
|
+
const geo = region ? b.geo ?? null : null;
|
|
265
247
|
if (geo) { // region=殻(東北上 m)
|
|
266
248
|
vao = gl.createVertexArray();
|
|
267
249
|
gl.bindVertexArray(vao);
|
|
@@ -273,13 +255,131 @@ function build(d) {
|
|
|
273
255
|
gl.bindVertexArray(null);
|
|
274
256
|
nVerts = geo.shell.length / 3;
|
|
275
257
|
}
|
|
276
|
-
|
|
258
|
+
const frames = b.frames?.length ? b.frames : [b.tex];
|
|
259
|
+
return { s: b.s, dims: [nx, ny, nz], off: single ? null : b.off, own: single ? null : b.own, anchor: b.anchor, A, Ainv, radius: b.radius || 0, nrm, gradM, geo, vao, vb, nVerts, frames, tex: null, up: -1, bytes: nx * ny * nz * comps };
|
|
260
|
+
}
|
|
261
|
+
/** 段 i を層の「いま」にする(置き方・範囲・殻) */
|
|
262
|
+
function useLevel(L, i) {
|
|
263
|
+
const V = L.levels[i];
|
|
264
|
+
L.lv = i;
|
|
265
|
+
L.s = V.s;
|
|
266
|
+
L.dims = V.dims;
|
|
267
|
+
L.off = V.off;
|
|
268
|
+
L.own = V.own;
|
|
269
|
+
L.anchor = V.anchor;
|
|
270
|
+
L.A = V.A;
|
|
271
|
+
L.Ainv = V.Ainv;
|
|
272
|
+
L.radius = V.radius;
|
|
273
|
+
L.nrm = V.nrm;
|
|
274
|
+
L.gradM = V.gradM;
|
|
275
|
+
L.geo = V.geo;
|
|
276
|
+
L.vao = V.vao;
|
|
277
|
+
L.vb = V.vb;
|
|
278
|
+
L.nVerts = V.nVerts;
|
|
279
|
+
L.full = V.off ? L.fullFine : null;
|
|
280
|
+
}
|
|
281
|
+
/** いまの段の、見せるこまを GPU に(無ければ作る・こまが違えば上げ直す) */
|
|
282
|
+
function ensure(L) {
|
|
283
|
+
const gl = glc, V = L.levels[L.lv], f = Math.min(V.frames.length - 1, Math.max(0, L.frame)), [nx, ny, nz] = V.dims, data = V.frames[f];
|
|
284
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
285
|
+
if (!V.tex) {
|
|
286
|
+
V.tex = gl.createTexture();
|
|
287
|
+
gl.bindTexture(gl.TEXTURE_3D, V.tex);
|
|
288
|
+
if (L.comps === 4)
|
|
289
|
+
gl.texImage3D(gl.TEXTURE_3D, 0, gl.RGBA8, nx, ny, nz, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
|
290
|
+
else
|
|
291
|
+
gl.texImage3D(gl.TEXTURE_3D, 0, gl.R8, nx, ny, nz, 0, gl.RED, gl.UNSIGNED_BYTE, data);
|
|
292
|
+
for (const [p, v] of [[gl.TEXTURE_MIN_FILTER, gl.LINEAR], [gl.TEXTURE_MAG_FILTER, gl.LINEAR], [gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE], [gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE], [gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE]])
|
|
293
|
+
gl.texParameteri(gl.TEXTURE_3D, p, v);
|
|
294
|
+
gl.bindTexture(gl.TEXTURE_3D, null);
|
|
295
|
+
V.up = f;
|
|
296
|
+
}
|
|
297
|
+
else if (V.up !== f) {
|
|
298
|
+
gl.bindTexture(gl.TEXTURE_3D, V.tex);
|
|
299
|
+
gl.texSubImage3D(gl.TEXTURE_3D, 0, 0, 0, 0, nx, ny, nz, L.comps === 4 ? gl.RGBA : gl.RED, gl.UNSIGNED_BYTE, data);
|
|
300
|
+
gl.bindTexture(gl.TEXTURE_3D, null);
|
|
301
|
+
V.up = f;
|
|
302
|
+
}
|
|
303
|
+
L.tex = V.tex;
|
|
304
|
+
}
|
|
305
|
+
const freeTex = (V) => { if (V.tex) {
|
|
306
|
+
glc.deleteTexture(V.tex);
|
|
307
|
+
V.tex = null;
|
|
308
|
+
V.up = -1;
|
|
309
|
+
} };
|
|
310
|
+
function build(d) {
|
|
311
|
+
const gl = glc, region = d.shape === "region", single = !d.off;
|
|
312
|
+
const levels = [mkLvl({ s: 1, off: d.off ?? [0, 0, 0], own: d.own ?? [0, 0, 0, ...d.dims], dims: d.dims, tex: d.tex, frames: d.frames ?? null, vmax: d.vmax ?? 255, anchor: d.anchor, A: d.A, radius: d.radius, geo: d.geo ?? null }, d.comps, region, single), ...(d.lods ?? []).map(b => mkLvl(b, d.comps, region, false))];
|
|
313
|
+
const cmap = gl.createTexture();
|
|
314
|
+
gl.bindTexture(gl.TEXTURE_2D, cmap);
|
|
315
|
+
for (const [p, v] of [[gl.TEXTURE_MIN_FILTER, gl.LINEAR], [gl.TEXTURE_MAG_FILTER, gl.LINEAR], [gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE], [gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE]])
|
|
316
|
+
gl.texParameteri(gl.TEXTURE_2D, p, v);
|
|
317
|
+
upCmap(cmap, d.cmap);
|
|
318
|
+
const V = levels[0], fullFine = d.full ?? d.dims;
|
|
319
|
+
const L = { shape: region ? "region" : "box", dims: V.dims, comps: d.comps, n: V.dims[0] * V.dims[1] * V.dims[2], anchor: V.anchor, alt: d.alt || "relative", A: V.A, Ainv: V.Ainv, radius: V.radius, mode: d.mode, thr: d.thr || 0, step: d.step || 0.5, opacity: d.opacity ?? 1, occl: d.depthTest !== false, clip: d.clip ?? null, properties: d.properties ?? null, range: d.range,
|
|
320
|
+
tex: null, cmap, on: d.on !== false, base: 0, dist: 0, nrm: V.nrm, gradM: V.gradM, geo: V.geo, vao: V.vao, vb: V.vb, nVerts: V.nVerts, layer: d.layer ?? null, off: V.off, own: V.own, full: d.full ?? null, vmax: d.vmax ?? 255,
|
|
321
|
+
frame: Math.max(0, d.frame ?? 0), lv: 0, s: 1, levels, cell: cellOf(V.A, V.geo), fullFine: [fullFine[0], fullFine[1], fullFine[2]] };
|
|
322
|
+
useLevel(L, 0);
|
|
323
|
+
return L;
|
|
277
324
|
}
|
|
278
325
|
const drop = (L) => { if (!L)
|
|
279
|
-
return; const gl = glc; for (const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
326
|
+
return; const gl = glc; for (const V of L.levels) {
|
|
327
|
+
freeTex(V);
|
|
328
|
+
if (V.vb)
|
|
329
|
+
gl.deleteBuffer(V.vb);
|
|
330
|
+
if (V.vao)
|
|
331
|
+
gl.deleteVertexArray(V.vao);
|
|
332
|
+
} gl.deleteTexture(L.cmap); };
|
|
333
|
+
/** LoD の割り振り(第 5 段 ③)=予算(bytes)に収まる中で一番細かく。遠い(元のセルが小さく見える)層から粗く:
|
|
334
|
+
* lv_i = clamp(floor(x + log2(dist_i / cell_i)), 0, 段の数 − 1) の x を二分で(x が大きいほど粗い・合計は x に単調)。一番粗くしても越えるなら全部一番粗く */
|
|
335
|
+
export function planLevels(items, budget) {
|
|
336
|
+
if (!items.length)
|
|
337
|
+
return [];
|
|
338
|
+
const g = items.map(it => Math.log2(Math.max(1e-9, it.dist) / Math.max(1e-9, it.cell)));
|
|
339
|
+
const at = (x) => items.map((it, i) => Math.min(it.bytes.length - 1, Math.max(0, Math.floor(x + g[i]))));
|
|
340
|
+
const total = (lv) => lv.reduce((a, l, i) => a + items[i].bytes[l], 0);
|
|
341
|
+
let lo = -Math.max(...g) - 1, hi = -Math.min(...g) + 9; // lo=全部 0・hi=全部一番粗い
|
|
342
|
+
const fine = at(lo);
|
|
343
|
+
if (total(fine) <= budget)
|
|
344
|
+
return fine;
|
|
345
|
+
const coarse = at(hi);
|
|
346
|
+
if (total(coarse) > budget)
|
|
347
|
+
return coarse;
|
|
348
|
+
for (let k = 0; k < 40; k++) {
|
|
349
|
+
const m = (lo + hi) / 2;
|
|
350
|
+
if (total(at(m)) <= budget)
|
|
351
|
+
hi = m;
|
|
352
|
+
else
|
|
353
|
+
lo = m;
|
|
354
|
+
}
|
|
355
|
+
return at(hi);
|
|
356
|
+
}
|
|
357
|
+
/** 描く層の段を決めてテクスチャを揃え、予算を越えれば描かない層のテクスチャを捨てる */
|
|
358
|
+
function settle(vis) {
|
|
359
|
+
const lv = planLevels(vis.map(L => ({ dist: L.dist, cell: L.cell, bytes: L.levels.map(V => V.bytes) })), budget);
|
|
360
|
+
vis.forEach((L, i) => { if (L.lv !== lv[i])
|
|
361
|
+
useLevel(L, lv[i]); L.levels.forEach((V, k) => { if (k !== L.lv)
|
|
362
|
+
freeTex(V); }); ensure(L); });
|
|
363
|
+
let used = 0;
|
|
364
|
+
for (const L of layers.values())
|
|
365
|
+
for (const V of L.levels)
|
|
366
|
+
if (V.tex)
|
|
367
|
+
used += V.bytes;
|
|
368
|
+
if (used <= budget)
|
|
369
|
+
return;
|
|
370
|
+
const on = new Set(vis);
|
|
371
|
+
for (const L of layers.values()) {
|
|
372
|
+
if (on.has(L))
|
|
373
|
+
continue;
|
|
374
|
+
for (const V of L.levels)
|
|
375
|
+
if (V.tex) {
|
|
376
|
+
used -= V.bytes;
|
|
377
|
+
freeTex(V);
|
|
378
|
+
}
|
|
379
|
+
if (used <= budget)
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
283
383
|
function rebase() { let b = 1; for (const L of layers.values()) {
|
|
284
384
|
L.base = b;
|
|
285
385
|
b += L.n;
|
|
@@ -290,6 +390,10 @@ export function message(d) {
|
|
|
290
390
|
host?.requestDraw?.();
|
|
291
391
|
return;
|
|
292
392
|
}
|
|
393
|
+
if (d.type === "budget" && !glc) {
|
|
394
|
+
budget = d.bytes > 0 ? d.bytes : Infinity;
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
293
397
|
if (!glc)
|
|
294
398
|
return;
|
|
295
399
|
if (d.type === "layer") {
|
|
@@ -308,10 +412,8 @@ export function message(d) {
|
|
|
308
412
|
const L = layers.get(d.q);
|
|
309
413
|
if (!L)
|
|
310
414
|
return;
|
|
311
|
-
if (d.frame != null
|
|
312
|
-
L.frame = Math.
|
|
313
|
-
L.tex = L.texs[L.frame];
|
|
314
|
-
}
|
|
415
|
+
if (d.frame != null)
|
|
416
|
+
L.frame = Math.max(0, d.frame); // 次に描く時に上げ直す(ensure)
|
|
315
417
|
if (d.mode != null)
|
|
316
418
|
L.mode = d.mode;
|
|
317
419
|
if (d.thr != null)
|
|
@@ -348,6 +450,10 @@ export function message(d) {
|
|
|
348
450
|
rebase();
|
|
349
451
|
host?.requestDraw?.();
|
|
350
452
|
}
|
|
453
|
+
else if (d.type === "budget") {
|
|
454
|
+
budget = d.bytes > 0 ? d.bytes : Infinity;
|
|
455
|
+
host?.requestDraw?.();
|
|
456
|
+
}
|
|
351
457
|
}
|
|
352
458
|
// 層の mvp(box=セル空間 → clip=mvp × [E N U o] × A・region=東北上 m → clip)と、目の位置(同じ空間)。倍精度で掛けてから f32
|
|
353
459
|
const mvpL = new Float64Array(16), mvpC = new Float32Array(16), eyeC = new Float32Array(3), geoU = { o: new Float32Array(3), E: new Float32Array(3), N: new Float32Array(3), U: new Float32Array(3), R: 0, rAx: 1 };
|
|
@@ -436,11 +542,11 @@ export function stepsOf(L) {
|
|
|
436
542
|
}
|
|
437
543
|
/** 断面 → セル空間の範囲 */
|
|
438
544
|
export const clipBounds = (dims, clip) => ({ lo: [0, 1, 2].map(i => (clip ? clip[i * 2] : 0) * dims[i]), hi: [0, 1, 2].map(i => (clip ? clip[i * 2 + 1] : 1) * dims[i]) });
|
|
439
|
-
/** 描く範囲(テクスチャのセル座標)=断面(親の割合)と持ち分の交わり。空なら null
|
|
545
|
+
/** 描く範囲(テクスチャのセル座標)=断面(親の割合)と持ち分の交わり。空なら null(そのブリックは描かない)。粗い段(s)は親もその段のセルで測る */
|
|
440
546
|
export function drawBounds(L) {
|
|
441
547
|
if (!L.full || !L.off || !L.own)
|
|
442
548
|
return clipBounds(L.dims, L.clip);
|
|
443
|
-
const c = clipBounds(L.full, L.clip), lo = [0, 1, 2].map(i => Math.max(L.own[i], c.lo[i] - L.off[i])), hi = [0, 1, 2].map(i => Math.min(L.own[3 + i], c.hi[i] - L.off[i]));
|
|
549
|
+
const s = L.s ?? 1, c = clipBounds([L.full[0] / s, L.full[1] / s, L.full[2] / s], L.clip), lo = [0, 1, 2].map(i => Math.max(L.own[i], c.lo[i] - L.off[i])), hi = [0, 1, 2].map(i => Math.min(L.own[3 + i], c.hi[i] - L.off[i]));
|
|
444
550
|
return lo.every((v, i) => v < hi[i]) ? { lo, hi } : null;
|
|
445
551
|
}
|
|
446
552
|
/** 閾より下しか無い層(ブリック)=描かない(vmax=R8 の 0〜255。RGBA は α 0 の時だけ) */
|
|
@@ -456,12 +562,13 @@ export function centerDist(L, enu) {
|
|
|
456
562
|
}
|
|
457
563
|
return Math.hypot(enu[0] - c[0], enu[1] - c[1], enu[2] - c[2]);
|
|
458
564
|
}
|
|
459
|
-
/** 当たり(層の中の番号)→ 親の層の id・番号・セル(ブリックは off と full
|
|
565
|
+
/** 当たり(層の中の番号)→ 親の層の id・番号・セル(ブリックは off と full で戻す・粗い段は元のセルの真ん中へ) */
|
|
460
566
|
export function pickedCell(q, L, index) {
|
|
461
567
|
const [nx, ny] = L.dims, i = index % nx, j = Math.floor(index / nx) % ny, k = Math.floor(index / (nx * ny));
|
|
462
568
|
if (!L.off || !L.full)
|
|
463
569
|
return { layer: L.layer ?? q, index, cell: [i, j, k] };
|
|
464
|
-
const
|
|
570
|
+
const s = L.s ?? 1, f = (v, a) => s === 1 ? v : Math.min(L.full[a] - 1, Math.floor((v + 0.5) * s));
|
|
571
|
+
const cell = [f(i + L.off[0], 0), f(j + L.off[1], 1), f(k + L.off[2], 2)];
|
|
465
572
|
return { layer: L.layer ?? q, index: cell[0] + L.full[0] * (cell[1] + L.full[1] * cell[2]), cell };
|
|
466
573
|
}
|
|
467
574
|
function drawAll(pick, s, api, w, h) {
|
|
@@ -486,6 +593,7 @@ function drawAll(pick, s, api, w, h) {
|
|
|
486
593
|
for (const L of layers.values())
|
|
487
594
|
if (L.on && L.n && !isEmpty(L) && drawBounds(L) && place(L, s, api))
|
|
488
595
|
vis.push(L);
|
|
596
|
+
settle(vis);
|
|
489
597
|
vis.sort((a, b) => b.dist - a.dist); // 層は奥から
|
|
490
598
|
for (const L of vis) {
|
|
491
599
|
place(L, s, api);
|
|
@@ -533,7 +641,7 @@ function answerProbes(w, h) {
|
|
|
533
641
|
return;
|
|
534
642
|
const gl = glc;
|
|
535
643
|
const ids = probes.splice(0);
|
|
536
|
-
const ls = [...layers].map(([q, L]) => ({ q, on: L.on, n: L.n, dims: L.dims, mode: L.mode, shape: L.shape, dist: +L.dist.toFixed(4), anchor: L.anchor, alt: L.alt }));
|
|
644
|
+
const ls = [...layers].map(([q, L]) => ({ q, on: L.on, n: L.n, dims: L.dims, mode: L.mode, shape: L.shape, dist: +L.dist.toFixed(4), anchor: L.anchor, alt: L.alt, lv: L.lv, s: L.s, gpu: L.levels.reduce((a, V) => a + (V.tex ? V.bytes : 0), 0) }));
|
|
537
645
|
try {
|
|
538
646
|
const data = new Uint8Array(w * h * 4);
|
|
539
647
|
gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
package/dist/voxel-gpu.js
CHANGED
|
@@ -209,7 +209,7 @@ const stepsOf = (L) => {
|
|
|
209
209
|
};
|
|
210
210
|
const align256 = (v) => Math.ceil(v / 256) * 256;
|
|
211
211
|
function drawBounds(L) {
|
|
212
|
-
const dims = L.full && L.off && L.own ? L.full : L.dims, c = { lo: [0, 1, 2].map(i => (L.clip ? L.clip[i * 2] : 0) * dims[i]), hi: [0, 1, 2].map(i => (L.clip ? L.clip[i * 2 + 1] : 1) * dims[i]) };
|
|
212
|
+
const s = L.s ?? 1, dims = L.full && L.off && L.own ? [L.full[0] / s, L.full[1] / s, L.full[2] / s] : L.dims, c = { lo: [0, 1, 2].map(i => (L.clip ? L.clip[i * 2] : 0) * dims[i]), hi: [0, 1, 2].map(i => (L.clip ? L.clip[i * 2 + 1] : 1) * dims[i]) };
|
|
213
213
|
if (!L.full || !L.off || !L.own)
|
|
214
214
|
return c;
|
|
215
215
|
const lo = [0, 1, 2].map(i => Math.max(L.own[i], c.lo[i] - L.off[i])), hi = [0, 1, 2].map(i => Math.min(L.own[3 + i], c.hi[i] - L.off[i]));
|
|
@@ -230,12 +230,36 @@ function pickedCell(q, L, index) {
|
|
|
230
230
|
const [nx, ny] = L.dims, i = index % nx, j = Math.floor(index / nx) % ny, k = Math.floor(index / (nx * ny));
|
|
231
231
|
if (!L.off || !L.full)
|
|
232
232
|
return { layer: L.layer ?? q, index, cell: [i, j, k] };
|
|
233
|
-
const
|
|
233
|
+
const s = L.s ?? 1, f = (v, a) => s === 1 ? v : Math.min(L.full[a] - 1, Math.floor((v + 0.5) * s));
|
|
234
|
+
const cell = [f(i + L.off[0], 0), f(j + L.off[1], 1), f(k + L.off[2], 2)];
|
|
234
235
|
return { layer: L.layer ?? q, index: cell[0] + L.full[0] * (cell[1] + L.full[1] * cell[2]), cell };
|
|
235
236
|
}
|
|
237
|
+
const cellOf = (A, geo) => geo ? Math.min(...geo.cellM) : Math.min(...[0, 1, 2].map(c => Math.hypot(A[c], A[3 + c], A[6 + c])));
|
|
238
|
+
function planLevels(items, budget) {
|
|
239
|
+
if (!items.length)
|
|
240
|
+
return [];
|
|
241
|
+
const g = items.map(it => Math.log2(Math.max(1e-9, it.dist) / Math.max(1e-9, it.cell)));
|
|
242
|
+
const at = (x) => items.map((it, i) => Math.min(it.bytes.length - 1, Math.max(0, Math.floor(x + g[i]))));
|
|
243
|
+
const total = (lv) => lv.reduce((a, l, i) => a + items[i].bytes[l], 0);
|
|
244
|
+
let lo = -Math.max(...g) - 1, hi = -Math.min(...g) + 9;
|
|
245
|
+
const fine = at(lo);
|
|
246
|
+
if (total(fine) <= budget)
|
|
247
|
+
return fine;
|
|
248
|
+
const coarse = at(hi);
|
|
249
|
+
if (total(coarse) > budget)
|
|
250
|
+
return coarse;
|
|
251
|
+
for (let k = 0; k < 40; k++) {
|
|
252
|
+
const m = (lo + hi) / 2;
|
|
253
|
+
if (total(at(m)) <= budget)
|
|
254
|
+
hi = m;
|
|
255
|
+
else
|
|
256
|
+
lo = m;
|
|
257
|
+
}
|
|
258
|
+
return at(hi);
|
|
259
|
+
}
|
|
236
260
|
export function create() {
|
|
237
261
|
let device = null, ctx = null, host = null, format = "rgba8unorm";
|
|
238
|
-
let shared = false, wantDepth = false, dead = false;
|
|
262
|
+
let shared = false, wantDepth = false, dead = false, budget = Infinity;
|
|
239
263
|
let drawPipe = null, pickPipe = null, layout = null, sampler = null, dummyDep = null, boxVb = null;
|
|
240
264
|
let pickTex = null, pickW = 0, pickH = 0;
|
|
241
265
|
const layers = new Map(), probes = [];
|
|
@@ -306,19 +330,11 @@ export function create() {
|
|
|
306
330
|
ctx = c;
|
|
307
331
|
}
|
|
308
332
|
const upCmap = (t, cmap) => { device.queue.writeTexture({ texture: t }, cmap, { bytesPerRow: 1024 }, [256, 1]); };
|
|
309
|
-
function
|
|
310
|
-
const [nx, ny, nz] =
|
|
333
|
+
function mkLvl(b, q, region, single, comps) {
|
|
334
|
+
const [nx, ny, nz] = b.dims, max3d = device.limits.maxTextureDimension3D || 256;
|
|
311
335
|
if (nx > max3d || ny > max3d || nz > max3d)
|
|
312
|
-
throw new Error(`voxel: dims ${
|
|
313
|
-
const
|
|
314
|
-
const tex = device.createTexture({ label: `voxel-${q}${f ? `@${f}` : ""}`, size: [nx, ny, nz], dimension: "3d", format: d.comps === 4 ? "rgba8unorm" : "r8unorm", usage: TEX.TEXTURE_BINDING | TEX.COPY_DST });
|
|
315
|
-
device.queue.writeTexture({ texture: tex }, data, { bytesPerRow: nx * d.comps, rowsPerImage: ny }, [nx, ny, nz]);
|
|
316
|
-
return tex;
|
|
317
|
-
};
|
|
318
|
-
const texs = (d.frames?.length ? d.frames : [d.tex]).map(mk), frame = Math.min(texs.length - 1, Math.max(0, d.frame ?? 0)), tex = texs[frame];
|
|
319
|
-
const cmap = device.createTexture({ label: `voxel-cmap-${q}`, size: [256, 1], format: "rgba8unorm", usage: TEX.TEXTURE_BINDING | TEX.COPY_DST });
|
|
320
|
-
upCmap(cmap, d.cmap);
|
|
321
|
-
const A = Float64Array.from(d.A), Ainv = inv3(A), nrm = new Float32Array(9), gradM = new Float32Array(9);
|
|
336
|
+
throw new Error(`voxel: dims ${b.dims.join("×")} exceed maxTextureDimension3D ${max3d}`);
|
|
337
|
+
const A = Float64Array.from(b.A), Ainv = inv3(A), nrm = new Float32Array(9), gradM = new Float32Array(9);
|
|
322
338
|
for (let c = 0; c < 3; c++) {
|
|
323
339
|
const l = Math.hypot(A[c], A[3 + c], A[6 + c]) || 1;
|
|
324
340
|
nrm[c * 3] = A[c] / l;
|
|
@@ -328,15 +344,96 @@ export function create() {
|
|
|
328
344
|
for (let c = 0; c < 3; c++)
|
|
329
345
|
for (let r = 0; r < 3; r++)
|
|
330
346
|
gradM[c * 3 + r] = Ainv[c * 3 + r];
|
|
331
|
-
const geo =
|
|
332
|
-
const vb = geo ? mkBuf(geo.shell, BUF.VERTEX, `voxel-shell-${q}`) : boxVb, nVerts = geo ? geo.shell.length / 3 : 36;
|
|
347
|
+
const geo = region ? b.geo ?? null : null;
|
|
348
|
+
const vb = geo ? mkBuf(geo.shell, BUF.VERTEX, `voxel-shell-${q}@${b.s}`) : boxVb, nVerts = geo ? geo.shell.length / 3 : 36;
|
|
349
|
+
return { s: b.s, dims: [nx, ny, nz], off: single ? null : b.off, own: single ? null : b.own, anchor: b.anchor, A, Ainv, radius: b.radius || 0, nrm, gradM, geo, vb, nVerts, frames: b.frames?.length ? b.frames : [b.tex], tex: null, up: -1, bytes: nx * ny * nz * comps };
|
|
350
|
+
}
|
|
351
|
+
function useLevel(L, i) {
|
|
352
|
+
const V = L.levels[i];
|
|
353
|
+
L.lv = i;
|
|
354
|
+
L.s = V.s;
|
|
355
|
+
L.dims = V.dims;
|
|
356
|
+
L.off = V.off;
|
|
357
|
+
L.own = V.own;
|
|
358
|
+
L.anchor = V.anchor;
|
|
359
|
+
L.A = V.A;
|
|
360
|
+
L.Ainv = V.Ainv;
|
|
361
|
+
L.radius = V.radius;
|
|
362
|
+
L.nrm = V.nrm;
|
|
363
|
+
L.gradM = V.gradM;
|
|
364
|
+
L.geo = V.geo;
|
|
365
|
+
L.vb = V.vb;
|
|
366
|
+
L.nVerts = V.nVerts;
|
|
367
|
+
L.full = V.off ? L.fullFine : null;
|
|
368
|
+
}
|
|
369
|
+
/** いまの段の見せるこまを GPU に(無ければ作る・こまが違えば上げ直す)。tex が変われば bind group を作り直す */
|
|
370
|
+
function ensure(L) {
|
|
371
|
+
const V = L.levels[L.lv], f = Math.min(V.frames.length - 1, Math.max(0, L.frame)), [nx, ny, nz] = V.dims;
|
|
372
|
+
if (!V.tex)
|
|
373
|
+
V.tex = device.createTexture({ label: `voxel-${L.q}@${V.s}`, size: [nx, ny, nz], dimension: "3d", format: L.comps === 4 ? "rgba8unorm" : "r8unorm", usage: TEX.TEXTURE_BINDING | TEX.COPY_DST });
|
|
374
|
+
if (V.up !== f) {
|
|
375
|
+
device.queue.writeTexture({ texture: V.tex }, V.frames[f], { bytesPerRow: nx * L.comps, rowsPerImage: ny }, [nx, ny, nz]);
|
|
376
|
+
V.up = f;
|
|
377
|
+
}
|
|
378
|
+
if (L.tex !== V.tex) {
|
|
379
|
+
L.tex = V.tex;
|
|
380
|
+
L.bg = null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const freeTex = (V) => { if (V.tex) {
|
|
384
|
+
V.tex.destroy();
|
|
385
|
+
V.tex = null;
|
|
386
|
+
V.up = -1;
|
|
387
|
+
} };
|
|
388
|
+
function build(d, q) {
|
|
389
|
+
const region = d.shape === "region", single = !d.off;
|
|
390
|
+
const levels = [mkLvl({ s: 1, off: d.off ?? [0, 0, 0], own: d.own ?? [0, 0, 0, ...d.dims], dims: d.dims, tex: d.tex, frames: d.frames ?? null, vmax: d.vmax ?? 255, anchor: d.anchor, A: d.A, radius: d.radius, geo: d.geo ?? null }, q, region, single, d.comps), ...(d.lods ?? []).map(b => mkLvl(b, q, region, false, d.comps))];
|
|
391
|
+
const cmap = device.createTexture({ label: `voxel-cmap-${q}`, size: [256, 1], format: "rgba8unorm", usage: TEX.TEXTURE_BINDING | TEX.COPY_DST });
|
|
392
|
+
upCmap(cmap, d.cmap);
|
|
333
393
|
const ub = device.createBuffer({ label: `voxel-u-${q}`, size: UB_SIZE, usage: BUF.UNIFORM | BUF.COPY_DST });
|
|
334
|
-
|
|
394
|
+
const V = levels[0], fullFine = d.full ?? d.dims;
|
|
395
|
+
const L = { q, shape: region ? "region" : "box", dims: V.dims, comps: d.comps, n: V.dims[0] * V.dims[1] * V.dims[2], anchor: V.anchor, alt: d.alt || "relative", A: V.A, Ainv: V.Ainv, radius: V.radius, mode: d.mode, thr: d.thr || 0, step: d.step || 0.5, opacity: d.opacity ?? 1, occl: d.depthTest !== false, clip: d.clip ?? null, properties: d.properties ?? null, range: d.range,
|
|
396
|
+
tex: null, cmap, vb: V.vb, nVerts: V.nVerts, ub, bg: null, bgDep: null, on: d.on !== false, base: 0, dist: 0, nrm: V.nrm, gradM: V.gradM, geo: V.geo, layer: d.layer ?? null, off: V.off, own: V.own, full: d.full ?? null, vmax: d.vmax ?? 255,
|
|
397
|
+
frame: Math.max(0, d.frame ?? 0), lv: 0, s: 1, levels, cell: cellOf(V.A, V.geo), fullFine: [fullFine[0], fullFine[1], fullFine[2]] };
|
|
398
|
+
useLevel(L, 0);
|
|
399
|
+
return L;
|
|
335
400
|
}
|
|
336
401
|
const drop = (L) => { if (!L)
|
|
337
|
-
return; for (const
|
|
338
|
-
|
|
339
|
-
|
|
402
|
+
return; for (const V of L.levels) {
|
|
403
|
+
freeTex(V);
|
|
404
|
+
if (V.vb !== boxVb)
|
|
405
|
+
V.vb.destroy();
|
|
406
|
+
} L.cmap.destroy(); L.ub.destroy(); };
|
|
407
|
+
/** 描く層の段を決めてテクスチャを揃え、予算を越えれば描かない層のテクスチャを捨てる(voxel-gl.ts settle と同じ) */
|
|
408
|
+
function settle(vis) {
|
|
409
|
+
const lv = planLevels(vis.map(L => ({ dist: L.dist, cell: L.cell, bytes: L.levels.map(V => V.bytes) })), budget);
|
|
410
|
+
vis.forEach((L, i) => { if (L.lv !== lv[i])
|
|
411
|
+
useLevel(L, lv[i]); L.levels.forEach((V, k) => { if (k !== L.lv)
|
|
412
|
+
freeTex(V); }); ensure(L); });
|
|
413
|
+
let used = 0;
|
|
414
|
+
for (const L of layers.values())
|
|
415
|
+
for (const V of L.levels)
|
|
416
|
+
if (V.tex)
|
|
417
|
+
used += V.bytes;
|
|
418
|
+
if (used <= budget)
|
|
419
|
+
return;
|
|
420
|
+
const on = new Set(vis);
|
|
421
|
+
for (const L of layers.values()) {
|
|
422
|
+
if (on.has(L))
|
|
423
|
+
continue;
|
|
424
|
+
for (const V of L.levels)
|
|
425
|
+
if (V.tex) {
|
|
426
|
+
used -= V.bytes;
|
|
427
|
+
freeTex(V);
|
|
428
|
+
}
|
|
429
|
+
if (L.tex && !L.levels.some(V => V.tex === L.tex)) {
|
|
430
|
+
L.tex = null;
|
|
431
|
+
L.bg = null;
|
|
432
|
+
}
|
|
433
|
+
if (used <= budget)
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
340
437
|
function rebase() { let b = 1; for (const L of layers.values()) {
|
|
341
438
|
L.base = b;
|
|
342
439
|
b += L.n;
|
|
@@ -347,6 +444,11 @@ export function create() {
|
|
|
347
444
|
host?.requestDraw?.();
|
|
348
445
|
return;
|
|
349
446
|
}
|
|
447
|
+
if (d.type === "budget") {
|
|
448
|
+
budget = d.bytes > 0 ? d.bytes : Infinity;
|
|
449
|
+
host?.requestDraw?.();
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
350
452
|
if (!device)
|
|
351
453
|
return;
|
|
352
454
|
if (d.type === "layer") {
|
|
@@ -365,11 +467,8 @@ export function create() {
|
|
|
365
467
|
const L = layers.get(d.q);
|
|
366
468
|
if (!L)
|
|
367
469
|
return;
|
|
368
|
-
if (d.frame != null
|
|
369
|
-
L.frame = Math.
|
|
370
|
-
L.tex = L.texs[L.frame];
|
|
371
|
-
L.bg = null;
|
|
372
|
-
} // bind group は tex を抱える=作り直す
|
|
470
|
+
if (d.frame != null)
|
|
471
|
+
L.frame = Math.max(0, d.frame); // 次に描く時に上げ直す(ensure)
|
|
373
472
|
if (d.mode != null)
|
|
374
473
|
L.mode = d.mode;
|
|
375
474
|
if (d.thr != null)
|
|
@@ -545,7 +644,7 @@ export function create() {
|
|
|
545
644
|
function answerProbes(w, h) {
|
|
546
645
|
if (!probes.length)
|
|
547
646
|
return;
|
|
548
|
-
const ids = probes.splice(0), ls = [...layers].map(([q, L]) => ({ q, on: L.on, n: L.n, dims: L.dims, mode: L.mode, shape: L.shape, dist: +L.dist.toFixed(4), anchor: L.anchor, alt: L.alt }));
|
|
647
|
+
const ids = probes.splice(0), ls = [...layers].map(([q, L]) => ({ q, on: L.on, n: L.n, dims: L.dims, mode: L.mode, shape: L.shape, dist: +L.dist.toFixed(4), anchor: L.anchor, alt: L.alt, lv: L.lv, s: L.s, gpu: L.levels.reduce((a, V) => a + (V.tex ? V.bytes : 0), 0) }));
|
|
549
648
|
readCanvas(w, h).then(data => { for (const id of ids)
|
|
550
649
|
host?.post?.({ type: "pixels", id, w, h, flipY: false, layers: ls, data: id === ids[ids.length - 1] ? data : data.slice() }); })
|
|
551
650
|
.catch(e => { for (const id of ids)
|
|
@@ -561,6 +660,7 @@ export function create() {
|
|
|
561
660
|
continue;
|
|
562
661
|
vis.push(L);
|
|
563
662
|
}
|
|
663
|
+
settle(vis);
|
|
564
664
|
vis.sort((a, b) => b.dist - a.dist);
|
|
565
665
|
const enc = device.createCommandEncoder({ label: "voxel-frame" });
|
|
566
666
|
const pass = enc.beginRenderPass({ label: "voxel-draw", colorAttachments: [{ view: ctx.getCurrentTexture().createView(), loadOp: "clear", clearValue: { r: 0, g: 0, b: 0, a: 0 }, storeOp: "store" }] });
|
|
@@ -599,6 +699,7 @@ export function create() {
|
|
|
599
699
|
continue;
|
|
600
700
|
vis.push(L);
|
|
601
701
|
}
|
|
702
|
+
settle(vis);
|
|
602
703
|
vis.sort((a, b) => b.dist - a.dist);
|
|
603
704
|
const enc = device.createCommandEncoder({ label: "voxel-pick" });
|
|
604
705
|
const pass = enc.beginRenderPass({ colorAttachments: [{ view: pickTex.createView(), loadOp: "clear", clearValue: { r: 0, g: 0, b: 0, a: 0 }, storeOp: "store" }] });
|
package/dist/voxel-reader.d.ts
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import type { PrepareOptions, PreparedGrid } from "./grid.js";
|
|
2
|
-
|
|
2
|
+
import type { SourceOptions, SourceMeta } from "./sources.js";
|
|
3
|
+
/** main → worker(data=VoxelData か、src=URL/bytes と読み方) */
|
|
3
4
|
export interface ReaderRequest {
|
|
4
5
|
seq: number;
|
|
5
|
-
data
|
|
6
|
+
data?: unknown;
|
|
7
|
+
src?: string | ArrayBuffer | Uint8Array;
|
|
8
|
+
source?: Omit<SourceOptions, "fetch">;
|
|
6
9
|
opts: PrepareOptions;
|
|
7
10
|
}
|
|
11
|
+
/** ファイルから読んだ時に足す物 */
|
|
12
|
+
export interface ReadExtra {
|
|
13
|
+
values?: Float32Array;
|
|
14
|
+
meta?: SourceMeta;
|
|
15
|
+
}
|
|
8
16
|
/** worker → main */
|
|
9
17
|
export type ReaderReply = ({
|
|
10
18
|
seq: number;
|
|
11
19
|
ok: true;
|
|
12
|
-
} & PreparedGrid) | {
|
|
20
|
+
} & PreparedGrid & ReadExtra) | {
|
|
13
21
|
seq: number;
|
|
14
22
|
ok: false;
|
|
15
23
|
error: string;
|
|
16
24
|
};
|
|
25
|
+
/** 読んで下ごしらえ(worker でも main でも同じ) */
|
|
26
|
+
export declare function readAndPrepare(m: Omit<ReaderRequest, "seq">): Promise<PreparedGrid & ReadExtra>;
|
package/dist/voxel-reader.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
// voxel の読み手の worker(@ortho-earth/voxel・2026-10-23)=値の詰め(R8/RGBA8)・色表・置き方を main の外で。
|
|
2
2
|
// main(index.ts)が new Worker(new URL("./voxel-reader.js", import.meta.url), { type: "module" }) で一つ立て、層ごとに { seq, data, opts } を送る。
|
|
3
3
|
// 答え={ seq, ok: true, …PreparedGrid(大きな格子はブリックに分けた物)}(配列は transfer)か { seq, ok: false, error }。worker が立たない器では index.ts が同じ prepareGrid を main で呼ぶ=答えは同じ。
|
|
4
|
+
// ファイル(NetCDF/Zarr=sources.ts)は src で送る=取りに行くのも読むのも worker。答えに読んだ値(values・main が当たりの値に使う)と meta を足す。
|
|
4
5
|
import { prepareGrid, transferables } from "./grid.js";
|
|
6
|
+
import { readSource } from "./sources.js";
|
|
7
|
+
/** 読んで下ごしらえ(worker でも main でも同じ) */
|
|
8
|
+
export async function readAndPrepare(m) {
|
|
9
|
+
if (m.src == null)
|
|
10
|
+
return prepareGrid(m.data, m.opts);
|
|
11
|
+
const d = await readSource(m.src, m.source ?? {});
|
|
12
|
+
return { ...prepareGrid(d, m.opts), values: d.values, meta: d.meta };
|
|
13
|
+
}
|
|
5
14
|
const w = globalThis;
|
|
6
15
|
if (typeof w.postMessage === "function" && typeof w.document === "undefined" && /Worker/.test(w.constructor?.name || "")) {
|
|
7
16
|
w.onmessage = ({ data: m }) => {
|
|
8
|
-
|
|
9
|
-
const p = prepareGrid(m.data, m.opts);
|
|
10
|
-
w.postMessage({ seq: m.seq, ok: true, ...p }, transferables(p));
|
|
11
|
-
}
|
|
12
|
-
catch (e) {
|
|
13
|
-
w.postMessage({ seq: m.seq, ok: false, error: String(e?.message || e) });
|
|
14
|
-
}
|
|
17
|
+
readAndPrepare(m).then(p => w.postMessage({ seq: m.seq, ok: true, ...p }, [...transferables(p), ...(p.values ? [p.values.buffer] : [])]), e => w.postMessage({ seq: m.seq, ok: false, error: String(e?.message || e) }));
|
|
15
18
|
};
|
|
16
19
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ortho-earth/voxel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"license": "GPL-3.0-or-later",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Voxel grids for @ortho-earth/globe: fills the reserved map.addData kind \"voxel\" (VoxelData: a box or longitude/latitude/height region grid of scalar / RGBA cells), drawn as a same-frame overlay by ray marching (blocks: first solid cell, Minecraft-like; volume: trilinear, colormap, front-to-back; iso: the surface at a threshold) with clipping, scene depth, unified picking and a worker reader. WebGPU (shared device and depth) or WebGL2.",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"build": "tsc -p .",
|
|
14
14
|
"prepack": "node ../../scripts/pack-ts.mjs pre",
|
|
15
15
|
"postpack": "node ../../scripts/pack-ts.mjs post",
|
|
16
|
-
"test": "node tests/t-voxel.mjs"
|
|
16
|
+
"test": "node tests/t-voxel.mjs && node tests/t-sources.mjs"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
19
|
"@ortho-earth/globe": "^2.2.0"
|