@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/README.md
CHANGED
|
@@ -9,8 +9,9 @@ Voxel grids for [`@ortho-earth/globe`](../../packages/globe). The plugin fills t
|
|
|
9
9
|
- They are **picked** by the unified picking: `handle.pick(x, y)`, `map.on("click", id, …)` and `queryRenderedFeatures({ layers: [id] })`. A hit carries the cell `index`, its `[i, j, k]` `cell`, the cell's `value` (scalar grids, via `handle.pick`) and your `properties`. Picking marches only the pixels under the cursor (scissor), so it stays cheap.
|
|
10
10
|
- **Preparation** runs in a worker (`voxel-reader.ts`): values are quantised to 8 bits (`R8`, or `RGBA8` for `components: 4`), the colormap is baked, the box is placed or the region shell is tessellated; the main thread only forwards the buffers.
|
|
11
11
|
- **Large grids** are split into **bricks**: a grid with an axis beyond 256 cells (the 3D texture size every WebGL2 device guarantees) becomes bricks of 126 cells per axis, one texture each with a 1-cell apron so trilinear sampling and iso gradients run across the seams. Bricks are drawn far to near, a brick whose cells are all below `threshold` (or empty) is skipped, and `clip`, `threshold`, picking (`index` / `cell` of the whole grid) and every style change work across them. `handle.bricks` says how many.
|
|
12
|
-
- **Time**: `times` (ms, ascending) makes `values` a run of frames, one per time. The layer follows `map.clock` by default (the last frame at or before the clock's time; scrub or play the clock and the frame changes without re-uploading), or `time: <ms>` pins one frame. One `range` covers every frame so colours compare across time.
|
|
13
|
-
-
|
|
12
|
+
- **Time**: `times` (ms, ascending) makes `values` a run of frames, one per time. The layer follows `map.clock` by default (the last frame at or before the clock's time; scrub or play the clock and the frame changes without re-uploading), or `time: <ms>` pins one frame. One `range` covers every frame so colours compare across time. Only the frame shown is on the GPU; the others wait in the overlay's memory and go up when the clock reaches them.
|
|
13
|
+
- **GPU budget**: `voxelPlugin({ budget })` (MB, default 512, 128 on touch devices) caps the voxel textures of a map. A grid bigger than half of it also gets coarser levels (1/2, 1/4… cells per axis, averaging the non-empty cells), per brick. When the layers in view would not fit, the farthest ones (by distance over cell size) are drawn from a coarser level, and the textures of layers out of view are let go. `handle.levels` says how many coarser levels were made.
|
|
14
|
+
- Stages 1–4 are done (box and region grids, blocks / volume / iso, clipping, WebGPU and WebGL2, 3D Tiles `EXT_primitive_voxels`); stage 5 has bricks, time, the GPU budget with levels of detail, and NetCDF / Zarr readers. See [PLAN.md](./PLAN.md).
|
|
14
15
|
|
|
15
16
|
```js
|
|
16
17
|
import { createGlobe } from "@ortho-earth/globe";
|
|
@@ -40,6 +41,15 @@ field.backend; // "webgpu" | "webgl2
|
|
|
40
41
|
map.on("click", "field", e => console.log(e.features[0].properties.cell));
|
|
41
42
|
```
|
|
42
43
|
|
|
44
|
+
## Files: NetCDF and Zarr
|
|
45
|
+
|
|
46
|
+
`map.addData(url, { kind: "voxel", variable?, frames?, heights? })` reads a grid file in the reader worker and draws it as a `region` grid:
|
|
47
|
+
|
|
48
|
+
- **NetCDF classic** (CDF-1, CDF-2, CDF-5): a `.nc` URL, or its bytes (`ArrayBuffer` / `Uint8Array`). NetCDF-4 (HDF5) is not read yet; `nccopy -k classic in.nc out.nc` converts it.
|
|
49
|
+
- **Zarr** v2 and v3: a `.zarr` URL (the store's root). Chunks may be uncompressed or gzip / zlib (v2) and `bytes` + `gzip` (v3); blosc, zstd and sharding are not read yet. With consolidated metadata (`.zmetadata`, or `consolidated_metadata` in `zarr.json`) the variable is found by itself; otherwise pass `variable`. Only the chunks of the frames asked for are fetched.
|
|
50
|
+
- The variable is `(time?, z?, y, x)`, with x the last (fastest) dimension. Coordinates follow CF: `degrees_east` / `degrees_north` (or `lon` / `lat`), a vertical coordinate in m / km (`positive: "down"` and depth go below zero) or Pa / hPa (the standard atmosphere, spacing taken as even), and `<unit> since <date>` times, which become `times` for `map.clock`. Descending latitude or height is flipped, cell centres become the region's edges, and `scale_factor`, `add_offset`, `_FillValue`, `missing_value` and `valid_*` are applied (missing cells are empty).
|
|
51
|
+
- `variable` picks the variable (default: the non-coordinate numeric variable with the most dimensions), `frames: [start, end)` reads part of the time dimension, and `heights: [h0, h1]` (m) overrides the vertical extent. `handle.source` holds what was read: `format`, `variable`, `units`, `longName`, `dims`, `attrs`.
|
|
52
|
+
|
|
43
53
|
## VoxelData
|
|
44
54
|
|
|
45
55
|
| field | meaning |
|
|
@@ -74,12 +84,12 @@ map.on("click", "field", e => console.log(e.features[0].properties.cell));
|
|
|
74
84
|
|
|
75
85
|
## Handle
|
|
76
86
|
|
|
77
|
-
`id`, `kind`, `shape`, `dims`, `n` (cells), `bricks` (0 = one texture), `times` (null without frames), `frame` (the frame shown), `anchor`, `range`, `backend` (`"webgpu"` | `"webgl2"`, null until the overlay has chosen), `reader` (`"worker"` | `"main"`), `error` (the renderer refused the layer, e.g. a grid beyond the 3D texture limit), `ready`, `setData(data)`, `setProps(opts)` (style-only changes do not re-upload the grid), `setVisible(on)`, `pick(x, y, r?)` (the value of the frame shown), `remove()`.
|
|
87
|
+
`id`, `kind`, `shape`, `dims`, `n` (cells), `bricks` (0 = one texture), `levels` (coarser levels for the budget, 0 = none), `source` (what a NetCDF / Zarr file was read as, null for VoxelData), `times` (null without frames), `frame` (the frame shown), `anchor`, `range`, `backend` (`"webgpu"` | `"webgl2"`, null until the overlay has chosen), `reader` (`"worker"` | `"main"`), `error` (the renderer refused the layer, e.g. a grid beyond the 3D texture limit), `ready`, `setData(data)`, `setProps(opts)` (style-only changes do not re-upload the grid), `setVisible(on)`, `pick(x, y, r?)` (the value of the frame shown), `remove()`.
|
|
78
88
|
|
|
79
89
|
## Notes
|
|
80
90
|
|
|
81
91
|
- Scalar values are quantised to 8 bits for the GPU (`range` → 1..255, 0 = empty). The colormap resolves 256 steps and an iso threshold lands on one of them; `handle.pick` reports the original value from the data kept on the main thread.
|
|
82
|
-
- A grid with every axis within 256 cells is one texture; a larger one is split into bricks (above). The bricks are kept in GPU memory as a whole (1 byte per cell, 4 for RGBA): a 1024 × 1024 × 128 grid needs 128 MB
|
|
92
|
+
- A grid with every axis within 256 cells is one texture; a larger one is split into bricks (above). The bricks are kept in GPU memory as a whole (1 byte per cell, 4 for RGBA): a 1024 × 1024 × 128 grid needs 128 MB; the GPU budget (above) draws far bricks from coarser levels when that does not fit.
|
|
83
93
|
- Region grids are marched in meters along a straight ray and each sample is converted back to longitude / latitude / height, so cell borders are the true meridians, parallels and height surfaces. Their blocks mode samples the nearest cell every quarter cell instead of a DDA (slightly soft edges). The shell is built on a sphere; under the ellipsoid display (`?ell=1`) a country-sized region can sit a few km off, which stage 3 will tighten. Very large regions (thousands of km) shade with the anchor's east / north / up.
|
|
84
94
|
- All grids of one map share one overlay canvas (the render worker's WebGL contexts are limited); the overlay goes away a moment (`OVERLAY_LINGER_MS`, 250 ms) after the last handle is removed, so `remove()` followed at once by `addData()` (switching scenes) keeps the same overlay instead of tearing it down and racing a new one with the same name.
|
|
85
95
|
- **3D Tiles**: a tileset whose glTF content carries `EXT_primitive_voxels` (with `KHR_implicit_shapes` boxes or `EXT_implicit_ellipsoid_region` regions), or whose `root.content` declares `3DTILES_content_voxels` (the grid fills the tile's bounding volume), becomes one layer per tile through `map.add3DTiles(url, { voxel: { render, colormap, threshold, property, … } })`; `property` picks the attribute (a class property name works too). Cylinders are not drawn yet; without this plugin the globe skips the grids and warns once.
|
package/dist/grid.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface PrepareOptions {
|
|
|
16
16
|
threshold?: number | null;
|
|
17
17
|
/** 楕円体表示の b/a(region の殻を本体と同じ形で作る・既定 1=球) */
|
|
18
18
|
rAx?: number;
|
|
19
|
+
/** LoD=一こまの GPU の大きさ(bytes)がこれを越える格子は粗い段(各軸 1/2・1/4…)も作る(既定=作らない) */
|
|
20
|
+
lodBytes?: number | null;
|
|
19
21
|
}
|
|
20
22
|
/** region(経緯度×高さの格子)の描き手向けの欄 */
|
|
21
23
|
export interface GeoGrid {
|
|
@@ -110,27 +112,35 @@ export declare function frameAt(times: ArrayLike<number>, ms: number): number;
|
|
|
110
112
|
export declare const thrOf: (threshold: number | null | undefined, range: [number, number]) => number;
|
|
111
113
|
/** VoxelData → 描き手へ渡す物(worker で。配列は transfer できる物)。値は閾で切らない(閾は描き手が見る=等値面は場の全体が要る) */
|
|
112
114
|
export declare function prepare(data: unknown, o?: PrepareOptions): Prepared;
|
|
115
|
+
/** 段(ブリック・粗い段)の配列=transfer する物 */
|
|
116
|
+
export declare const levelBuffers: (l: Level) => ArrayBuffer[];
|
|
113
117
|
export declare const transferables: (p: Prepared | PreparedGrid) => ArrayBuffer[];
|
|
114
118
|
/** 一枚のテクスチャで描く格子の上限(各軸) */
|
|
115
119
|
export declare const BRICK_LIMIT = 256;
|
|
116
120
|
/** ブリックの持ち分(各軸のセル数)=テクスチャは糊代を足して最大 BRICK + 2 */
|
|
117
121
|
export declare const BRICK = 126;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
/**
|
|
122
|
+
/** 一つの段=ブリック(s=1)か、その粗い段(s=2, 4…=セル 1 個が元の s 個ぶん)。座標はその段のセル */
|
|
123
|
+
export interface Level {
|
|
124
|
+
/** 縮めた倍率(1=元の格子) */ s: number;
|
|
125
|
+
/** テクスチャの 0 番のセルの、その段の親の格子での位置 */ off: [number, number, number];
|
|
126
|
+
/** 描く範囲(テクスチャのセル座標・粗い段は端数あり)[x0, y0, z0, x1, y1, z1] */ own: [number, number, number, number, number, number];
|
|
121
127
|
dims: [number, number, number];
|
|
122
|
-
n: number;
|
|
123
128
|
tex: Uint8Array; /** こまごとのテクスチャ(frames[0]=tex)・こまが無ければ null */
|
|
124
129
|
frames: Uint8Array[] | null; /** 最大値(R8 の 0〜255・RGBA は α・こまを通して) */
|
|
125
130
|
vmax: number;
|
|
126
|
-
anchor: [number, number, number];
|
|
131
|
+
/** box=親の錨と A×s(セル 1 個ぶん)・region=部分の region */ anchor: [number, number, number];
|
|
127
132
|
A: Float64Array;
|
|
128
133
|
radius: number;
|
|
129
134
|
geo: GeoGrid | null;
|
|
130
135
|
}
|
|
136
|
+
export interface Brick extends Level {
|
|
137
|
+
n: number; /** 粗い段(s=2, 4…)・作らなければ null */
|
|
138
|
+
lods: Level[] | null;
|
|
139
|
+
}
|
|
131
140
|
export interface PreparedGrid extends Prepared {
|
|
132
141
|
vmax: number; /** 分けた時のブリック(tex は空) */
|
|
133
|
-
bricks: Brick[] | null;
|
|
142
|
+
bricks: Brick[] | null; /** 一枚の格子の粗い段(ブリックは各ブリックの lods)・作らなければ null */
|
|
143
|
+
lods: Level[] | null;
|
|
134
144
|
}
|
|
135
145
|
/** 詰めた値の最大(R8 はそのまま・RGBA は α) */
|
|
136
146
|
export declare function maxOf(tex: Uint8Array, comps: 1 | 4): number;
|
|
@@ -145,7 +155,26 @@ export declare function parentIndex(index: number, dims: ArrayLike<number>, off:
|
|
|
145
155
|
index: number;
|
|
146
156
|
cell: [number, number, number];
|
|
147
157
|
};
|
|
148
|
-
/**
|
|
158
|
+
/** 粗い段の数の上限(1/16 まで) */
|
|
159
|
+
export declare const LOD_MAX = 4;
|
|
160
|
+
/** 段 s の格子の大きさ(各軸 ceil(d/s)) */
|
|
161
|
+
export declare const levelDims: (dims: ArrayLike<number>, s: number) => [number, number, number];
|
|
162
|
+
/** 作る粗い段の数=一こまが lodBytes を越えれば、一番粗い段が lodBytes/4 以下になるまで(LOD_MAX まで)。越えなければ 0 */
|
|
163
|
+
export declare function levelCount(dims: ArrayLike<number>, comps: number, lodBytes: number | null | undefined): number;
|
|
164
|
+
/** 各軸 1/2 に縮める(R8=空でない子の平均・RGBA=α > 0 の子の平均・全部空なら空) */
|
|
165
|
+
export declare function downsample(tex: Uint8Array, dims: [number, number, number], comps: 1 | 4): {
|
|
166
|
+
tex: Uint8Array;
|
|
167
|
+
dims: [number, number, number];
|
|
168
|
+
};
|
|
169
|
+
/** 段の並び(s=2, 4…)=こまごとに縮める */
|
|
170
|
+
export declare function pyramid(frames: Uint8Array[], dims: [number, number, number], comps: 1 | 4, L: number): Array<{
|
|
171
|
+
s: number;
|
|
172
|
+
dims: [number, number, number];
|
|
173
|
+
frames: Uint8Array[];
|
|
174
|
+
}>;
|
|
175
|
+
/** 段 s の格子(frames)から、元の格子の持ち分 [lo, hi)(元のセル)を糊代 1 セルつきで切り出す。s=1 はブリックそのもの */
|
|
176
|
+
export declare function makeLevel(frames: Uint8Array[], Dl: [number, number, number], comps: 1 | 4, s: number, lo: ArrayLike<number>, hi: ArrayLike<number>, parent: Placement, pgeo: GeoGrid | null, dims: [number, number, number], rAx?: number): Level;
|
|
177
|
+
/** prepare の大きな格子版=上限を越えればブリックに分ける(越えなければ prepare と同じ+vmax)。o.lodBytes を越える格子は粗い段(lods)も作る */
|
|
149
178
|
export declare function prepareGrid(data: unknown, o?: PrepareOptions, brick?: number, limit?: number): PreparedGrid;
|
|
150
179
|
export interface RayHit {
|
|
151
180
|
cell: [number, number, number];
|
package/dist/grid.js
CHANGED
|
@@ -304,8 +304,12 @@ export function prepare(data, o = {}) {
|
|
|
304
304
|
const cmap = buildColormap(o.colormap, o.alphaRamp ?? null);
|
|
305
305
|
return { shape: d.shape, dims: [dims[0], dims[1], dims[2]], comps, n, tex: frames[0], cmap, anchor, A, radius, range, thr, placed, geo, times: d.times ? Array.from(d.times) : null, frames: T > 1 ? frames : null };
|
|
306
306
|
}
|
|
307
|
-
|
|
308
|
-
|
|
307
|
+
/** 段(ブリック・粗い段)の配列=transfer する物 */
|
|
308
|
+
export const levelBuffers = (l) => [l.tex.buffer, l.A.buffer, ...(l.geo ? [l.geo.shell.buffer] : []), ...(l.frames ?? []).map(f => f.buffer)];
|
|
309
|
+
const uniq = (a) => a.filter((b, i) => a.indexOf(b) === i);
|
|
310
|
+
export const transferables = (p) => uniq([p.tex.buffer, p.cmap.buffer, p.A.buffer, ...(p.geo ? [p.geo.shell.buffer] : []), ...(p.frames ?? []).map(f => f.buffer),
|
|
311
|
+
...("lods" in p && p.lods ? p.lods.flatMap(levelBuffers) : []),
|
|
312
|
+
...("bricks" in p && p.bricks ? p.bricks.flatMap(b => [...levelBuffers(b), ...(b.lods ?? []).flatMap(levelBuffers)]) : [])]);
|
|
309
313
|
// ---- 大きな格子=ブリック(第 5 段・2026-10-24)----
|
|
310
314
|
// 一軸でも BRICK_LIMIT(WebGL2 の 3D テクスチャの最低保証)を越える格子は、持ち分 BRICK セル角のブリックに分けて別々のテクスチャにする。
|
|
311
315
|
// 各ブリックのテクスチャは持ち分の前後に 1 セルずつ糊代(隣のセル)を足す=三線形・等値面の勾配が継ぎ目で途切れない。描くのは持ち分(own)だけ=体積が継ぎ目で二重に重ならない。
|
|
@@ -344,15 +348,109 @@ export function parentIndex(index, dims, off, full) {
|
|
|
344
348
|
const cell = [i + off[0], j + off[1], k + off[2]];
|
|
345
349
|
return { index: cell[0] + full[0] * (cell[1] + full[1] * cell[2]), cell };
|
|
346
350
|
}
|
|
347
|
-
|
|
351
|
+
// ---- LoD(第 5 段 ③・2026-10-25)----
|
|
352
|
+
// GPU の予算(plugin の budget)を越えそうな格子は、各軸 1/2・1/4…に縮めた段も作る(ブリックはブリックごと・こまもこまごと)。どの段を GPU に置くかは描き手が決める(voxel-gl.ts planLevels=遠い層から粗く)。
|
|
353
|
+
// 縮め方=子の 2×2×2 のうち空でないセルの平均(全部空なら空)=薄い壁や疎な点も消えない。粗い段の箱は元の格子の s 倍のセル=端の奇数は半端な描く範囲(own の端数)で切る。
|
|
354
|
+
/** 粗い段の数の上限(1/16 まで) */
|
|
355
|
+
export const LOD_MAX = 4;
|
|
356
|
+
/** 段 s の格子の大きさ(各軸 ceil(d/s)) */
|
|
357
|
+
export const levelDims = (dims, s) => [Math.ceil(dims[0] / s), Math.ceil(dims[1] / s), Math.ceil(dims[2] / s)];
|
|
358
|
+
/** 作る粗い段の数=一こまが lodBytes を越えれば、一番粗い段が lodBytes/4 以下になるまで(LOD_MAX まで)。越えなければ 0 */
|
|
359
|
+
export function levelCount(dims, comps, lodBytes) {
|
|
360
|
+
const bytes = (s) => { const d = levelDims(dims, s); return d[0] * d[1] * d[2] * comps; };
|
|
361
|
+
if (lodBytes == null || !(bytes(1) > lodBytes))
|
|
362
|
+
return 0;
|
|
363
|
+
let L = 1;
|
|
364
|
+
while (L < LOD_MAX && bytes(2 ** L) > lodBytes / 4)
|
|
365
|
+
L++;
|
|
366
|
+
return L;
|
|
367
|
+
}
|
|
368
|
+
/** 各軸 1/2 に縮める(R8=空でない子の平均・RGBA=α > 0 の子の平均・全部空なら空) */
|
|
369
|
+
export function downsample(tex, dims, comps) {
|
|
370
|
+
const D = levelDims(dims, 2), out = new Uint8Array(D[0] * D[1] * D[2] * comps), acc = [0, 0, 0, 0];
|
|
371
|
+
for (let k = 0; k < D[2]; k++)
|
|
372
|
+
for (let j = 0; j < D[1]; j++)
|
|
373
|
+
for (let i = 0; i < D[0]; i++) {
|
|
374
|
+
let cnt = 0;
|
|
375
|
+
acc[0] = acc[1] = acc[2] = acc[3] = 0;
|
|
376
|
+
for (let dz = 0; dz < 2; dz++) {
|
|
377
|
+
const z = 2 * k + dz;
|
|
378
|
+
if (z >= dims[2])
|
|
379
|
+
break;
|
|
380
|
+
for (let dy = 0; dy < 2; dy++) {
|
|
381
|
+
const y = 2 * j + dy;
|
|
382
|
+
if (y >= dims[1])
|
|
383
|
+
break;
|
|
384
|
+
for (let dx = 0; dx < 2; dx++) {
|
|
385
|
+
const x = 2 * i + dx;
|
|
386
|
+
if (x >= dims[0])
|
|
387
|
+
break;
|
|
388
|
+
const c = (x + dims[0] * (y + dims[1] * z)) * comps;
|
|
389
|
+
if (comps === 1) {
|
|
390
|
+
if (tex[c]) {
|
|
391
|
+
acc[0] += tex[c];
|
|
392
|
+
cnt++;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
else if (tex[c + 3]) {
|
|
396
|
+
for (let a = 0; a < 4; a++)
|
|
397
|
+
acc[a] += tex[c + a];
|
|
398
|
+
cnt++;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (!cnt)
|
|
404
|
+
continue;
|
|
405
|
+
const o = (i + D[0] * (j + D[1] * k)) * comps;
|
|
406
|
+
for (let a = 0; a < comps; a++)
|
|
407
|
+
out[o + a] = Math.round(acc[a] / cnt);
|
|
408
|
+
}
|
|
409
|
+
return { tex: out, dims: D };
|
|
410
|
+
}
|
|
411
|
+
/** 段の並び(s=2, 4…)=こまごとに縮める */
|
|
412
|
+
export function pyramid(frames, dims, comps, L) {
|
|
413
|
+
const out = [];
|
|
414
|
+
let cur = frames, d = dims;
|
|
415
|
+
for (let l = 1; l <= L; l++) {
|
|
416
|
+
const r = cur.map(f => downsample(f, d, comps));
|
|
417
|
+
cur = r.map(x => x.tex);
|
|
418
|
+
d = r[0].dims;
|
|
419
|
+
out.push({ s: 2 ** l, dims: d, frames: cur });
|
|
420
|
+
}
|
|
421
|
+
return out;
|
|
422
|
+
}
|
|
423
|
+
/** 段 s の格子(frames)から、元の格子の持ち分 [lo, hi)(元のセル)を糊代 1 セルつきで切り出す。s=1 はブリックそのもの */
|
|
424
|
+
export function makeLevel(frames, Dl, comps, s, lo, hi, parent, pgeo, dims, rAx = 1) {
|
|
425
|
+
const clo = [0, 1, 2].map(i => lo[i] / s), chi = [0, 1, 2].map(i => hi[i] / s);
|
|
426
|
+
const off = [0, 1, 2].map(i => Math.max(0, Math.floor(clo[i]) - 1)), end = [0, 1, 2].map(i => Math.min(Dl[i], Math.ceil(chi[i]) + 1));
|
|
427
|
+
const bd = [0, 1, 2].map(i => end[i] - off[i]);
|
|
428
|
+
const fr = frames.map(f => extract(f, Dl, comps, off, bd)), tex = fr[0];
|
|
429
|
+
const own = [clo[0] - off[0], clo[1] - off[1], clo[2] - off[2], chi[0] - off[0], chi[1] - off[1], chi[2] - off[2]];
|
|
430
|
+
const vmax = Math.max(...fr.map(f => maxOf(f, comps)));
|
|
431
|
+
const base = { s, off, own, dims: bd, tex, frames: fr.length > 1 ? fr : null, vmax };
|
|
432
|
+
if (pgeo) {
|
|
433
|
+
const g = pgeo, f = (i, v) => v * s / dims[i]; // その段のセル → 元の割合(粗い段の端は元の外へ半端に出る=own で切る)
|
|
434
|
+
const r = placeRegion([g.west + g.spanLon * f(0, off[0]), g.south + g.spanLat * f(1, off[1]), g.west + g.spanLon * f(0, end[0]), g.south + g.spanLat * f(1, end[1]), g.h0 + (g.h1 - g.h0) * f(2, off[2]), g.h0 + (g.h1 - g.h0) * f(2, end[2])], bd, rAx);
|
|
435
|
+
return { ...base, anchor: r.anchor, A: r.A, radius: r.radius, geo: r.geo };
|
|
436
|
+
}
|
|
437
|
+
const A = Float64Array.from(parent.A, v => v * s); // セル 1 個=元の s 個ぶん(列ごとに同じ倍率)
|
|
438
|
+
return { ...base, anchor: parent.anchor, A, radius: parent.radius, geo: null };
|
|
439
|
+
}
|
|
440
|
+
/** 値 → 詰めた一こまずつ(R8/RGBA8) */
|
|
441
|
+
const packFrames = (d, range, n) => { const T = d.times?.length ?? 1, out = []; for (let t = 0; t < T; t++) {
|
|
442
|
+
const v = T > 1 ? frameValues(d.values, n, d.components, t) : d.values;
|
|
443
|
+
out.push(d.components === 4 ? toRgba8(v, d.noData) : quantize(v, range, d.noData, null));
|
|
444
|
+
} return out; };
|
|
445
|
+
/** prepare の大きな格子版=上限を越えればブリックに分ける(越えなければ prepare と同じ+vmax)。o.lodBytes を越える格子は粗い段(lods)も作る */
|
|
348
446
|
export function prepareGrid(data, o = {}, brick = BRICK, limit = BRICK_LIMIT) {
|
|
349
|
-
const d = checkVoxelData(data), dims = d.dims;
|
|
447
|
+
const d = checkVoxelData(data), dims = d.dims, comps = d.components, L = levelCount(dims, comps, o.lodBytes);
|
|
350
448
|
if (!needsBricks(dims, limit)) {
|
|
351
|
-
const p = prepare(d, o);
|
|
352
|
-
|
|
449
|
+
const p = prepare(d, o), fr = p.frames ?? [p.tex], vmax = Math.max(...fr.map(f => maxOf(f, p.comps)));
|
|
450
|
+
const lods = L ? pyramid(fr, dims, comps, L).map(lv => makeLevel(lv.frames, lv.dims, comps, lv.s, [0, 0, 0], dims, p, p.geo, dims, o.rAx ?? 1)) : null;
|
|
451
|
+
return { ...p, vmax, bricks: null, lods };
|
|
353
452
|
}
|
|
354
|
-
const
|
|
355
|
-
const comps = d.components;
|
|
453
|
+
const nAll = dims[0] * dims[1] * dims[2];
|
|
356
454
|
let range = [0, 1];
|
|
357
455
|
if (comps === 1) {
|
|
358
456
|
range = o.range ?? d.range ?? valueRange(d.values, d.noData);
|
|
@@ -373,35 +471,19 @@ export function prepareGrid(data, o = {}, brick = BRICK, limit = BRICK_LIMIT) {
|
|
|
373
471
|
parent = r;
|
|
374
472
|
pgeo = r.geo;
|
|
375
473
|
}
|
|
474
|
+
const full = packFrames(d, range, nAll), pyr = L ? pyramid(full, dims, comps, L) : [];
|
|
376
475
|
const sx = brickSpans(dims[0], brick), sy = brickSpans(dims[1], brick), sz = brickSpans(dims[2], brick), bricks = [];
|
|
377
476
|
let vmax = 0;
|
|
378
477
|
for (const [z0, z1] of sz)
|
|
379
478
|
for (const [y0, y1] of sy)
|
|
380
479
|
for (const [x0, x1] of sx) {
|
|
381
480
|
const lo = [x0, y0, z0], hi = [x1, y1, z1];
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
for (let t = 0; t < T; t++) {
|
|
386
|
-
const sub = extract(T > 1 ? frameValues(d.values, nAll, comps, t) : d.values, dims, comps, off, bd);
|
|
387
|
-
fr.push(comps === 4 ? toRgba8(sub, d.noData) : quantize(sub, range, d.noData, null));
|
|
388
|
-
}
|
|
389
|
-
const tex = fr[0], frames = T > 1 ? fr : null;
|
|
390
|
-
const vm = Math.max(...fr.map(f => maxOf(f, comps)));
|
|
391
|
-
vmax = Math.max(vmax, vm);
|
|
392
|
-
const own = [lo[0] - off[0], lo[1] - off[1], lo[2] - off[2], hi[0] - off[0], hi[1] - off[1], hi[2] - off[2]];
|
|
393
|
-
let b;
|
|
394
|
-
if (pgeo) {
|
|
395
|
-
const g = pgeo, f = (i, v) => v / dims[i];
|
|
396
|
-
const r = placeRegion([g.west + g.spanLon * f(0, off[0]), g.south + g.spanLat * f(1, off[1]), g.west + g.spanLon * f(0, end[0]), g.south + g.spanLat * f(1, end[1]), g.h0 + (g.h1 - g.h0) * f(2, off[2]), g.h0 + (g.h1 - g.h0) * f(2, end[2])], bd, o.rAx ?? 1);
|
|
397
|
-
b = { off, own, dims: bd, n: bd[0] * bd[1] * bd[2], tex, frames, vmax: vm, anchor: r.anchor, A: r.A, radius: r.radius, geo: r.geo };
|
|
398
|
-
}
|
|
399
|
-
else
|
|
400
|
-
b = { off, own, dims: bd, n: bd[0] * bd[1] * bd[2], tex, frames, vmax: vm, anchor: parent.anchor, A: Float64Array.from(parent.A), radius: parent.radius, geo: null };
|
|
401
|
-
bricks.push(b);
|
|
481
|
+
const b = makeLevel(full, dims, comps, 1, lo, hi, parent, pgeo, dims, o.rAx ?? 1);
|
|
482
|
+
vmax = Math.max(vmax, b.vmax);
|
|
483
|
+
bricks.push({ ...b, n: b.dims[0] * b.dims[1] * b.dims[2], lods: L ? pyr.map(lv => makeLevel(lv.frames, lv.dims, comps, lv.s, lo, hi, parent, pgeo, dims, o.rAx ?? 1)) : null });
|
|
402
484
|
}
|
|
403
485
|
const cmap = buildColormap(o.colormap, o.alphaRamp ?? null);
|
|
404
|
-
return { shape: d.shape, dims: [dims[0], dims[1], dims[2]], comps, n:
|
|
486
|
+
return { shape: d.shape, dims: [dims[0], dims[1], dims[2]], comps, n: nAll, tex: new Uint8Array(0), cmap, anchor: parent.anchor, A: parent.A, radius: parent.radius, range, thr: comps === 4 ? 0 : thrOf(o.threshold, range), placed: parent.placed, geo: null, times: d.times ? Array.from(d.times) : null, frames: null, vmax, bricks, lods: null };
|
|
405
487
|
}
|
|
406
488
|
/** 光線と箱 [0, dims] の交わり(t0, t1)。無ければ null */
|
|
407
489
|
export function rayBox(ro, rd, dims) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { GlobeMap, OrthoPlugin, OverlayHandle, OverlayPick, VoxelData } from "@ortho-earth/globe";
|
|
2
2
|
import type { ColormapSpec, RenderMode, Clip } from "./grid.js";
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
export type {
|
|
3
|
+
import type { SourceMeta } from "./sources.js";
|
|
4
|
+
export { prepare, checkVoxelData, checkClip, thrOf, buildColormap, colorStops, parseColor, quantize, toRgba8, valueRange, placeBox, placeRegion, unitAt, hprMat, enuBasis, ecefToLngLat, ecefEnuRows, inv3, mul3, rayBox, marchBlocks, COLORMAPS, COLORMAP_NAMES, SHELL_N, prepareGrid, maxOf, needsBricks, brickSpans, extract, parentIndex, frameValues, frameAt, BRICK, BRICK_LIMIT, LOD_MAX, levelDims, levelCount, downsample, pyramid, makeLevel, levelBuffers } from "./grid.js";
|
|
5
|
+
export type { PrepareOptions, Prepared, PreparedGrid, Brick, Level, ColormapSpec, ColorStop, RenderMode, Clip, Hpr, Placement, GeoGrid, RayHit, EnuBasis, Mat3 } from "./grid.js";
|
|
6
|
+
export type { VoxelMessage, VoxelLayerBody, LevelBody, VoxelStyle, VoxelPickResult, VoxelAltitude, OverlayHost, CamState, VoxelApi, PickContext } from "./voxel-gl.js";
|
|
6
7
|
export type { VoxelBackend, VoxelDrawOpts } from "./voxel-draw.js";
|
|
8
|
+
export { readSource, parseNetCDF, sniffNetCDF, openZarr, zarrArray, readZarr, toVoxelData, pickVariable, axisOf, cfTimes, heightsOf, pressureToHeight, edges, unpack, isZarrUrl } from "./sources.js";
|
|
9
|
+
export type { SourceOptions, SourceMeta, SourceData } from "./sources.js";
|
|
7
10
|
export type { VoxelGpuModule } from "./voxel-gpu.js";
|
|
8
11
|
import type { VoxelBackend } from "./voxel-draw.js";
|
|
9
|
-
export declare const VERSION = "0.
|
|
12
|
+
export declare const VERSION = "0.6.0";
|
|
10
13
|
export type VoxelSource = VoxelData | string | URL | ArrayBuffer | Uint8Array;
|
|
11
14
|
export interface VoxelOptions {
|
|
12
15
|
kind?: "voxel";
|
|
@@ -39,11 +42,22 @@ export interface VoxelOptions {
|
|
|
39
42
|
/** grids with `times` (a time dimension): "clock" (default) shows the frame of the map's clock (map.clock: the last frame at or before its time) and follows it as it plays;
|
|
40
43
|
* a number (ms) pins that time. Changes without re-uploading the grid */
|
|
41
44
|
time?: number | "clock";
|
|
45
|
+
/** NetCDF / Zarr sources: the variable to draw (default the non-coordinate numeric variable with the most dimensions) */
|
|
46
|
+
variable?: string;
|
|
47
|
+
/** NetCDF / Zarr sources: read only frames [start, end) of the time dimension */
|
|
48
|
+
frames?: [number, number];
|
|
49
|
+
/** NetCDF / Zarr sources: the height range [h0, h1] in meters (default from the vertical coordinate; pressure levels use the standard atmosphere) */
|
|
50
|
+
heights?: [number, number];
|
|
42
51
|
}
|
|
43
52
|
export interface VoxelPluginOptions {
|
|
44
53
|
/** "auto" (default): WebGPU when the map draws with WebGPU (shared device and depth texture), else WebGL2. "webgpu": WebGPU (own device under a WebGL2 map) or fail. "webgl2": always WebGL2 */
|
|
45
54
|
backend?: VoxelBackend | "auto";
|
|
55
|
+
/** GPU memory for the voxel textures of this map, in MB (default 512, 128 on touch devices). Only the frame shown is uploaded; a grid bigger than half of it also gets
|
|
56
|
+
* coarser levels, drawn far away when the layers in view would not fit. Infinity turns the budget off */
|
|
57
|
+
budget?: number;
|
|
46
58
|
}
|
|
59
|
+
/** 既定の GPU の予算(MB)=タッチ端末は小さく(packages/ortho-core/fallback-ladder.md の 160 MB の中) */
|
|
60
|
+
export declare const defaultBudgetMB: () => number;
|
|
47
61
|
/** handle.pick の 1 件=統一 picking の答え(layer=手綱の id・index=セルの番号・cell=[i, j, k]・value=セルの値) */
|
|
48
62
|
export interface VoxelHit extends OverlayPick {
|
|
49
63
|
id: number;
|
|
@@ -86,6 +100,10 @@ export interface VoxelHandle {
|
|
|
86
100
|
times: number[] | null;
|
|
87
101
|
/** the frame shown (0 without a time dimension) */
|
|
88
102
|
frame: number;
|
|
103
|
+
/** number of coarser levels made for the GPU budget (0 = the grid fits) */
|
|
104
|
+
levels: number;
|
|
105
|
+
/** what a NetCDF / Zarr source was read as (format, variable, units, long name, dimensions, attributes); null for VoxelData */
|
|
106
|
+
source: SourceMeta | null;
|
|
89
107
|
ready: Promise<VoxelHandle>;
|
|
90
108
|
setData(data: VoxelSource): Promise<VoxelHandle>;
|
|
91
109
|
setProps(p: Partial<VoxelOptions>): Promise<VoxelHandle>;
|
|
@@ -108,7 +126,7 @@ export declare const OVERLAY_LINGER_MS = 250;
|
|
|
108
126
|
/** 格子を上げ直さずに変えられる opts(setProps はこれだけなら "style" を送る) */
|
|
109
127
|
export declare const STYLE_KEYS: readonly (keyof VoxelOptions)[];
|
|
110
128
|
/** map.addData(data, { kind: "voxel", … }) の実体 */
|
|
111
|
-
export declare function createVoxel(map: VoxelMap, data: VoxelSource, opts?: VoxelOptions, { backend }?: VoxelPluginOptions): VoxelHandle;
|
|
129
|
+
export declare function createVoxel(map: VoxelMap, data: VoxelSource, opts?: VoxelOptions, { backend, budget }?: VoxelPluginOptions): VoxelHandle;
|
|
112
130
|
/** map.use(voxelPlugin()) → map.addData(data, { kind: "voxel", render?, colormap?, range?, threshold?, alphaRamp?, step?, opacity?, altitude?, depthTest?, above?, properties? }) */
|
|
113
|
-
export declare function voxelPlugin({ backend }?: VoxelPluginOptions): VoxelPlugin;
|
|
131
|
+
export declare function voxelPlugin({ backend, budget }?: VoxelPluginOptions): VoxelPlugin;
|
|
114
132
|
export default voxelPlugin;
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { transferables, levelBuffers, checkVoxelData, checkClip, buildColormap, thrOf, frameAt } from "./grid.js";
|
|
2
|
+
import { readAndPrepare } from "./voxel-reader.js";
|
|
3
|
+
export { prepare, checkVoxelData, checkClip, thrOf, buildColormap, colorStops, parseColor, quantize, toRgba8, valueRange, placeBox, placeRegion, unitAt, hprMat, enuBasis, ecefToLngLat, ecefEnuRows, inv3, mul3, rayBox, marchBlocks, COLORMAPS, COLORMAP_NAMES, SHELL_N, prepareGrid, maxOf, needsBricks, brickSpans, extract, parentIndex, frameValues, frameAt, BRICK, BRICK_LIMIT, LOD_MAX, levelDims, levelCount, downsample, pyramid, makeLevel, levelBuffers } from "./grid.js";
|
|
4
|
+
export { readSource, parseNetCDF, sniffNetCDF, openZarr, zarrArray, readZarr, toVoxelData, pickVariable, axisOf, cfTimes, heightsOf, pressureToHeight, edges, unpack, isZarrUrl } from "./sources.js";
|
|
3
5
|
// 描き手(worker)の URL=入口(voxel-draw.js が兄弟を import)。字面は .js のまま=npm の dist では実体・repo の中では Vite が .ts に読み替える
|
|
4
6
|
const VOXEL_URL = new URL("./voxel-draw.js", import.meta.url).href;
|
|
5
|
-
export const VERSION = "0.
|
|
7
|
+
export const VERSION = "0.6.0";
|
|
8
|
+
/** 既定の GPU の予算(MB)=タッチ端末は小さく(packages/ortho-core/fallback-ladder.md の 160 MB の中) */
|
|
9
|
+
export const defaultBudgetMB = () => { try {
|
|
10
|
+
return typeof matchMedia === "function" && matchMedia("(pointer: coarse)").matches ? 128 : 512;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return 512;
|
|
14
|
+
} };
|
|
15
|
+
const MB = 1 << 20;
|
|
6
16
|
/** 楕円体表示の b/a(region の殻を本体と同じ形で作る) */
|
|
7
17
|
const R_AX_ELL = 1 - 1 / 298.257223563;
|
|
8
18
|
const rAxOf = (map) => { try {
|
|
@@ -14,7 +24,7 @@ catch {
|
|
|
14
24
|
const shared = new WeakMap();
|
|
15
25
|
/** 手綱が全部外れてからオーバーレイを片付けるまでの猶予(ms)=場面の切り替え(remove → すぐ addData)で同じ名のオーバーレイを壊して作り直さない(render worker の片付けと新しい登録が競う) */
|
|
16
26
|
export const OVERLAY_LINGER_MS = 250;
|
|
17
|
-
function acquire(map, above, id, backend) {
|
|
27
|
+
function acquire(map, above, id, backend, budget) {
|
|
18
28
|
let m = shared.get(map);
|
|
19
29
|
if (!m)
|
|
20
30
|
shared.set(map, (m = { ids: new Set() }));
|
|
@@ -40,6 +50,7 @@ function acquire(map, above, id, backend) {
|
|
|
40
50
|
slot.errors.get(e.q)?.(e.error || "error");
|
|
41
51
|
}
|
|
42
52
|
};
|
|
53
|
+
ov.post({ type: "budget", bytes: Number.isFinite(budget) ? budget * MB : 0 }); // 0=予算なし
|
|
43
54
|
m[k] = slot;
|
|
44
55
|
}
|
|
45
56
|
m[k].refs++;
|
|
@@ -99,15 +110,14 @@ const readerFree = () => { if (--liveHandles > 0 || !reader)
|
|
|
99
110
|
return; readerIdle = setTimeout(() => { readerIdle = null; if (liveHandles || !reader)
|
|
100
111
|
return; reader.w.terminate(); reader = null; }, 5000); };
|
|
101
112
|
const isFileSource = (src) => typeof src === "string" || src instanceof URL || src instanceof ArrayBuffer || src instanceof Uint8Array;
|
|
102
|
-
async function load(src, o, rAx) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
const popts = { colormap: o.colormap, range: o.range ?? null, alphaRamp: o.alphaRamp ?? null, threshold: o.threshold ?? null, rAx };
|
|
113
|
+
async function load(src, o, rAx, budget) {
|
|
114
|
+
const file = isFileSource(src), d = file ? null : checkVoxelData(src);
|
|
115
|
+
const source = file ? { variable: o.variable, frames: o.frames ?? null, heights: o.heights ?? null } : undefined, fsrc = file ? (src instanceof URL ? src.href : src) : undefined;
|
|
116
|
+
const popts = { colormap: o.colormap, range: o.range ?? null, alphaRamp: o.alphaRamp ?? null, threshold: o.threshold ?? null, rAx, lodBytes: Number.isFinite(budget) ? budget * MB / 2 : null }; // 予算の半分を越える格子は粗い段も
|
|
107
117
|
const r = spawnReader();
|
|
108
118
|
if (r) {
|
|
109
119
|
try {
|
|
110
|
-
const seq = ++r.seq, req = { seq, data: d, opts: popts };
|
|
120
|
+
const seq = ++r.seq, req = file ? { seq, src: fsrc, source, opts: popts } : { seq, data: d, opts: popts };
|
|
111
121
|
const p = await new Promise((res, rej) => { r.pending.set(seq, { res, rej }); r.w.postMessage(req); }); // data は写し=渡した人の配列は手放さない
|
|
112
122
|
return { ...p, reader: "worker" };
|
|
113
123
|
}
|
|
@@ -116,7 +126,7 @@ async function load(src, o, rAx) {
|
|
|
116
126
|
throw e;
|
|
117
127
|
}
|
|
118
128
|
}
|
|
119
|
-
return { ...
|
|
129
|
+
return { ...(await readAndPrepare(file ? { src: fsrc, source, opts: popts } : { data: d, opts: popts })), reader: "main" };
|
|
120
130
|
}
|
|
121
131
|
const isVoxelHit = (id) => (t) => t.layer === id;
|
|
122
132
|
const checkEnum = (v, allowed, name) => { if (!allowed.includes(v))
|
|
@@ -138,15 +148,21 @@ function checkOpts(opts) {
|
|
|
138
148
|
checkClip(opts.clip);
|
|
139
149
|
if (opts.time != null && opts.time !== "clock" && !(typeof opts.time === "number" && Number.isFinite(opts.time)))
|
|
140
150
|
throw new TypeError("voxel: time must be \"clock\" or a time in ms");
|
|
151
|
+
if (opts.variable != null && typeof opts.variable !== "string")
|
|
152
|
+
throw new TypeError("voxel: variable must be a string");
|
|
153
|
+
if (opts.frames != null && !(Array.isArray(opts.frames) && opts.frames.length === 2 && opts.frames.every(Number.isInteger) && opts.frames[0] >= 0 && opts.frames[1] > opts.frames[0]))
|
|
154
|
+
throw new RangeError("voxel: frames must be [start, end) frame numbers");
|
|
155
|
+
if (opts.heights != null && !(Array.isArray(opts.heights) && opts.heights.length === 2 && opts.heights.every(Number.isFinite) && opts.heights[0] < opts.heights[1]))
|
|
156
|
+
throw new RangeError("voxel: heights must be [h0, h1] meters with h0 < h1");
|
|
141
157
|
}
|
|
142
158
|
let anonSeq = 0;
|
|
143
159
|
/** map.addData(data, { kind: "voxel", … }) の実体 */
|
|
144
|
-
export function createVoxel(map, data, opts = {}, { backend = "auto" } = {}) {
|
|
160
|
+
export function createVoxel(map, data, opts = {}, { backend = "auto", budget = defaultBudgetMB() } = {}) {
|
|
145
161
|
checkOpts(opts);
|
|
146
162
|
if (!isFileSource(data))
|
|
147
163
|
checkVoxelData(data); // 形の間違いは今・同期に投げる(worker へ送ってからでは遅い)
|
|
148
164
|
const id = opts.id ?? `voxel${++anonSeq}`;
|
|
149
|
-
const { ov, slot, release } = acquire(map, !!opts.above, id, backend);
|
|
165
|
+
const { ov, slot, release } = acquire(map, !!opts.above, id, backend, budget);
|
|
150
166
|
try {
|
|
151
167
|
ov.addLayerId?.(id);
|
|
152
168
|
}
|
|
@@ -172,7 +188,7 @@ export function createVoxel(map, data, opts = {}, { backend = "auto" } = {}) {
|
|
|
172
188
|
map.clock.off("tick", onClock).off("change", onClock); };
|
|
173
189
|
const run = async (src, o) => {
|
|
174
190
|
const my = ++seq;
|
|
175
|
-
const p = await load(src, o, rAxOf(map));
|
|
191
|
+
const p = await load(src, o, rAxOf(map), budget);
|
|
176
192
|
if (removed || my !== seq)
|
|
177
193
|
return h;
|
|
178
194
|
h.dims = p.dims;
|
|
@@ -183,11 +199,13 @@ export function createVoxel(map, data, opts = {}, { backend = "auto" } = {}) {
|
|
|
183
199
|
h.error = null;
|
|
184
200
|
h.shape = p.shape;
|
|
185
201
|
h.bricks = p.bricks?.length ?? 0;
|
|
202
|
+
h.levels = (p.lods ?? p.bricks?.[0]?.lods)?.length ?? 0;
|
|
186
203
|
times = h.times = p.times;
|
|
187
204
|
h.frame = frameNow();
|
|
188
205
|
follow(!!times && (o.time ?? "clock") === "clock");
|
|
189
206
|
cur = src;
|
|
190
|
-
values = src.values;
|
|
207
|
+
values = p.values ?? src.values;
|
|
208
|
+
h.source = p.meta ?? null;
|
|
191
209
|
comps = p.comps;
|
|
192
210
|
baked = true;
|
|
193
211
|
const style = { mode: MODE[o.render ?? "volume"], thr: p.thr, step: o.step ?? 0.5, opacity: o.opacity ?? 1, clip: checkClip(o.clip), depthTest: o.depthTest !== false, properties: o.properties ?? null, range: p.range, on: visible, frame: h.frame, alt: o.altitude ?? (p.placed ? "absolute" : "relative") };
|
|
@@ -201,9 +219,9 @@ export function createVoxel(map, data, opts = {}, { backend = "auto" } = {}) {
|
|
|
201
219
|
for (const q of qs)
|
|
202
220
|
slot.errors.set(q, e => { h.error = e; }); // 描き手の build の失敗(3D テクスチャの上限など)は非同期に届く=warn と手綱の error(ready は解決する)
|
|
203
221
|
if (!p.bricks)
|
|
204
|
-
ov.post({ type: "layer", q: id, shape: p.shape, dims: p.dims, comps: p.comps, tex: p.tex, frames: p.frames, cmap: p.cmap, anchor: p.anchor, A: p.A, radius: p.radius, geo: p.geo, vmax: p.vmax, ...style }, transferables(p));
|
|
222
|
+
ov.post({ type: "layer", q: id, shape: p.shape, dims: p.dims, comps: p.comps, tex: p.tex, frames: p.frames, lods: p.lods, cmap: p.cmap, anchor: p.anchor, A: p.A, radius: p.radius, geo: p.geo, vmax: p.vmax, ...style }, transferables(p));
|
|
205
223
|
else
|
|
206
|
-
p.bricks.forEach((b, i) => ov.post({ type: "layer", q: qs[i], layer: id, shape: p.shape, dims: b.dims, comps: p.comps, tex: b.tex, frames: b.frames, cmap: i ? p.cmap.slice() : p.cmap, anchor: b.anchor, A: b.A, radius: b.radius, geo: b.geo, vmax: b.vmax, off: b.off, own: b.own, full: p.dims, ...style }, [
|
|
224
|
+
p.bricks.forEach((b, i) => ov.post({ type: "layer", q: qs[i], layer: id, shape: p.shape, dims: b.dims, comps: p.comps, tex: b.tex, frames: b.frames, lods: b.lods, cmap: i ? p.cmap.slice() : p.cmap, anchor: b.anchor, A: b.A, radius: b.radius, geo: b.geo, vmax: b.vmax, off: b.off, own: b.own, full: p.dims, ...style }, [...levelBuffers(b), ...(b.lods ?? []).flatMap(levelBuffers)].filter((x, k, a) => a.indexOf(x) === k)));
|
|
207
225
|
return h;
|
|
208
226
|
};
|
|
209
227
|
// 見た目だけの変更=格子を上げ直さない(render・opacity・step・threshold・clip・depthTest・properties・colormap・alphaRamp)
|
|
@@ -243,7 +261,7 @@ export function createVoxel(map, data, opts = {}, { backend = "auto" } = {}) {
|
|
|
243
261
|
t.properties = { ...t.properties, value: v };
|
|
244
262
|
} return t; };
|
|
245
263
|
const h = {
|
|
246
|
-
id, kind: "voxel", overlay: ov, dims: null, n: 0, anchor: null, range: null, reader: null, error: null, shape: null, bricks: 0, times: null, frame: 0,
|
|
264
|
+
id, kind: "voxel", overlay: ov, dims: null, n: 0, anchor: null, range: null, reader: null, error: null, shape: null, bricks: 0, levels: 0, source: null, times: null, frame: 0,
|
|
247
265
|
get backend() { return slot.backend; },
|
|
248
266
|
ready: undefined,
|
|
249
267
|
setData: (d) => { if (!isFileSource(d))
|
|
@@ -271,14 +289,16 @@ export function createVoxel(map, data, opts = {}, { backend = "auto" } = {}) {
|
|
|
271
289
|
return h;
|
|
272
290
|
}
|
|
273
291
|
/** map.use(voxelPlugin()) → map.addData(data, { kind: "voxel", render?, colormap?, range?, threshold?, alphaRamp?, step?, opacity?, altitude?, depthTest?, above?, properties? }) */
|
|
274
|
-
export function voxelPlugin({ backend = "auto" } = {}) {
|
|
292
|
+
export function voxelPlugin({ backend = "auto", budget = defaultBudgetMB() } = {}) {
|
|
275
293
|
checkEnum(backend, ["auto", "webgpu", "webgl2"], "backend");
|
|
294
|
+
if (!(typeof budget === "number" && budget > 0))
|
|
295
|
+
throw new RangeError("voxel: budget must be a positive number of MB (Infinity = no budget)");
|
|
276
296
|
return {
|
|
277
297
|
name: "voxel",
|
|
278
298
|
version: VERSION,
|
|
279
299
|
provides: ["data.voxel"],
|
|
280
300
|
install(map) {
|
|
281
|
-
map.registry.dataKinds.add("voxel", { create: (m, data, o) => createVoxel(m, data, o, { backend }) });
|
|
301
|
+
map.registry.dataKinds.add("voxel", { create: (m, data, o) => createVoxel(m, data, o, { backend, budget }) });
|
|
282
302
|
return () => { try {
|
|
283
303
|
map.registry.dataKinds.remove("voxel");
|
|
284
304
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { VoxelData } from "@ortho-earth/globe";
|
|
2
|
+
export interface SourceOptions {
|
|
3
|
+
/** 描く変数の名前(省略=座標でない、次元の一番多い数値の変数) */
|
|
4
|
+
variable?: string;
|
|
5
|
+
/** 時間の次元のこまの範囲 [始め, 終わり)(番号・省略=全部) */
|
|
6
|
+
frames?: [number, number] | null;
|
|
7
|
+
/** 高さの範囲 [h0, h1](m)を上書き(高さの座標が無い・気圧面の近似を避けたい時) */
|
|
8
|
+
heights?: [number, number] | null;
|
|
9
|
+
/** fetch の代わり(検定・認証つきの取り方) */
|
|
10
|
+
fetch?: (url: string) => Promise<Response>;
|
|
11
|
+
}
|
|
12
|
+
/** 読んだ変数の説明(手綱の source に載る) */
|
|
13
|
+
export interface SourceMeta {
|
|
14
|
+
format: "netcdf" | "zarr";
|
|
15
|
+
variable: string;
|
|
16
|
+
units: string | null;
|
|
17
|
+
longName: string | null;
|
|
18
|
+
dims: string[];
|
|
19
|
+
attrs: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
export type SourceData = VoxelData & {
|
|
22
|
+
shape: "region";
|
|
23
|
+
region: [number, number, number, number, number, number];
|
|
24
|
+
values: Float32Array;
|
|
25
|
+
meta: SourceMeta;
|
|
26
|
+
};
|
|
27
|
+
type Attrs = Record<string, unknown>;
|
|
28
|
+
interface VarInfo {
|
|
29
|
+
name: string;
|
|
30
|
+
dims: string[];
|
|
31
|
+
shape: number[];
|
|
32
|
+
attrs: Attrs;
|
|
33
|
+
numeric: boolean;
|
|
34
|
+
}
|
|
35
|
+
interface Reader {
|
|
36
|
+
format: "netcdf" | "zarr";
|
|
37
|
+
vars: VarInfo[];
|
|
38
|
+
/** 変数の値(全体か、先頭の軸を [t0, t1) に絞った物)→ Float64 ではなく Float32(scale/offset・fill 済み) */
|
|
39
|
+
read(name: string, first?: [number, number] | null): Promise<Float32Array>;
|
|
40
|
+
/** 座標の 1 次元の値(そのまま) */
|
|
41
|
+
coord(name: string): Promise<Float64Array>;
|
|
42
|
+
}
|
|
43
|
+
type Axis = "x" | "y" | "z" | "t";
|
|
44
|
+
/** 座標の変数が何の軸か(CF:axis・units・standard_name・名前) */
|
|
45
|
+
export declare function axisOf(name: string, attrs: Attrs): Axis | null;
|
|
46
|
+
/** CF の時刻("hours since 1900-01-01 00:00:0.0" など)→ ms。読めなければ null */
|
|
47
|
+
export declare function cfTimes(values: ArrayLike<number>, units: string | null): number[] | null;
|
|
48
|
+
/** 気圧(Pa)→ 高さ(m・標準大気の対流圏の式) */
|
|
49
|
+
export declare const pressureToHeight: (pa: number) => number;
|
|
50
|
+
/** 高さの座標 → m(units と positive で) */
|
|
51
|
+
export declare function heightsOf(values: ArrayLike<number>, attrs: Attrs, name?: string): number[];
|
|
52
|
+
/** セルの中心の並び → 範囲(端のセルの半分だけ外へ)と昇順か。一つだけなら step で幅 */
|
|
53
|
+
export declare function edges(c: ArrayLike<number>, step1?: number): {
|
|
54
|
+
lo: number;
|
|
55
|
+
hi: number;
|
|
56
|
+
asc: boolean;
|
|
57
|
+
};
|
|
58
|
+
/** 選んだ変数(座標でない・数値・次元 2〜4・x と y がある)を探す */
|
|
59
|
+
export declare function pickVariable(r: Pick<Reader, "vars">, want?: string): VarInfo;
|
|
60
|
+
/** 読み手 → VoxelData(region・times) */
|
|
61
|
+
export declare function toVoxelData(r: Reader, o?: SourceOptions): Promise<SourceData>;
|
|
62
|
+
/** 値の整え(scale_factor/add_offset・fill・valid range → NaN) */
|
|
63
|
+
export declare function unpack(src: ArrayLike<number> & {
|
|
64
|
+
length: number;
|
|
65
|
+
}, attrs: Attrs, fill?: number | null): Float32Array;
|
|
66
|
+
/** NetCDF の形式("classic"=読める・"hdf5"=NetCDF-4・null=NetCDF でない) */
|
|
67
|
+
export declare function sniffNetCDF(b: Uint8Array): "classic" | "hdf5" | null;
|
|
68
|
+
export declare function parseNetCDF(buf: ArrayBuffer | Uint8Array): Reader;
|
|
69
|
+
interface ZArray {
|
|
70
|
+
shape: number[];
|
|
71
|
+
chunks: number[];
|
|
72
|
+
dtype: string;
|
|
73
|
+
fill: number | null;
|
|
74
|
+
codec: "none" | "gzip" | "zlib";
|
|
75
|
+
sep: string;
|
|
76
|
+
v3: boolean;
|
|
77
|
+
key: (c: number[]) => string;
|
|
78
|
+
attrs: Attrs;
|
|
79
|
+
dims: string[];
|
|
80
|
+
}
|
|
81
|
+
/** Zarr のメタ(v2 の .zarray+.zattrs、v3 の zarr.json)→ 読み方 */
|
|
82
|
+
export declare function zarrArray(meta: Attrs, attrs?: Attrs, v3?: boolean): ZArray;
|
|
83
|
+
/** Zarr の配列を読む(先頭の軸を [a, b) に絞れる・無いチャンクは fill) */
|
|
84
|
+
export declare function readZarr(get: (path: string) => Promise<Uint8Array | null>, path: string, z: ZArray, first?: [number, number] | null): Promise<Float64Array>;
|
|
85
|
+
/** Zarr の店(URL の根)→ 読み手。consolidated(.zmetadata/zarr.json の consolidated_metadata)があれば一覧、無ければ variable と座標を名前で取る */
|
|
86
|
+
export declare function openZarr(root: string, o?: SourceOptions): Promise<Reader>;
|
|
87
|
+
/** URL の種類(.zarr=Zarr の店・それ以外の .nc 等=一つのファイル) */
|
|
88
|
+
export declare const isZarrUrl: (u: string) => boolean;
|
|
89
|
+
/** VoxelSource(URL/bytes)→ VoxelData(region・times・meta) */
|
|
90
|
+
export declare function readSource(src: string | URL | ArrayBuffer | Uint8Array, o?: SourceOptions): Promise<SourceData>;
|
|
91
|
+
export {};
|