@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/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
- function build(d) {
239
- const gl = glc;
240
- const [nx, ny, nz] = d.dims;
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 ${d.dims.join("×")} exceed MAX_3D_TEXTURE_SIZE ${max3d}`);
243
- const mk = (data) => {
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 = d.shape === "region" ? d.geo ?? null : null;
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
- return { shape: geo ? "region" : "box", dims: [nx, ny, nz], comps: d.comps, n: nx * ny * nz, anchor: d.anchor, alt: d.alt || "relative", A, Ainv, radius: d.radius || 0, 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, tex, cmap, on: d.on !== false, base: 0, dist: 0, nrm, gradM, geo, vao, vb, nVerts, layer: d.layer ?? null, off: d.off ?? null, own: d.own ?? null, full: d.full ?? null, vmax: d.vmax ?? 255, texs, frame };
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 t of L.texs)
280
- gl.deleteTexture(t); gl.deleteTexture(L.cmap); if (L.vb)
281
- gl.deleteBuffer(L.vb); if (L.vao)
282
- gl.deleteVertexArray(L.vao); };
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 && L.texs.length > 1) {
312
- L.frame = Math.min(L.texs.length - 1, Math.max(0, d.frame));
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 cell = [i + L.off[0], j + L.off[1], k + L.off[2]];
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 cell = [i + L.off[0], j + L.off[1], k + L.off[2]];
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 build(d, q) {
310
- const [nx, ny, nz] = d.dims, max3d = device.limits.maxTextureDimension3D || 256;
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 ${d.dims.join("×")} exceed maxTextureDimension3D ${max3d}`);
313
- const mk = (data, f) => {
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 = d.shape === "region" ? d.geo ?? null : null;
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
- return { q, shape: geo ? "region" : "box", dims: [nx, ny, nz], comps: d.comps, n: nx * ny * nz, anchor: d.anchor, alt: d.alt || "relative", A, Ainv, radius: d.radius || 0, 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, tex, cmap, vb, nVerts, ub, bg: null, bgDep: null, on: d.on !== false, base: 0, dist: 0, nrm, gradM, geo, layer: d.layer ?? null, off: d.off ?? null, own: d.own ?? null, full: d.full ?? null, vmax: d.vmax ?? 255, texs, frame };
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 t of L.texs)
338
- t.destroy(); L.cmap.destroy(); L.ub.destroy(); if (L.vb !== boxVb)
339
- L.vb.destroy(); };
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 && L.texs.length > 1) {
369
- L.frame = Math.min(L.texs.length - 1, Math.max(0, d.frame));
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" }] });
@@ -1,16 +1,26 @@
1
1
  import type { PrepareOptions, PreparedGrid } from "./grid.js";
2
- /** main → worker */
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: unknown;
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>;
@@ -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
- try {
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.4.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"