@ortho-earth/voxel 0.0.0-stage → 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/LICENSE +29 -0
- package/README.md +95 -2
- package/dist/grid.d.ts +189 -0
- package/dist/grid.js +537 -0
- package/dist/index.d.ts +132 -0
- package/dist/index.js +309 -0
- package/dist/sources.d.ts +91 -0
- package/dist/sources.js +514 -0
- package/dist/voxel-draw.d.ts +16 -0
- package/dist/voxel-draw.js +77 -0
- package/dist/voxel-gl.d.ts +200 -0
- package/dist/voxel-gl.js +737 -0
- package/dist/voxel-gpu.d.ts +22 -0
- package/dist/voxel-gpu.js +775 -0
- package/dist/voxel-reader.d.ts +26 -0
- package/dist/voxel-reader.js +19 -0
- package/package.json +42 -5
package/dist/grid.js
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
const D2R = Math.PI / 180, R_SPHERE = 6371000, R_ELL = 6378137, WGS_A = 6378137, WGS_E2 = 6.69437999014e-3;
|
|
2
|
+
// ---- 色表 ----
|
|
3
|
+
/** 名前のある色表(matplotlib/turbo の要約=9 点を等間隔に。厳密な表ではない) */
|
|
4
|
+
export const COLORMAPS = Object.freeze({
|
|
5
|
+
viridis: ["#440154", "#482878", "#3e4a89", "#31688e", "#26828e", "#1f9e89", "#35b779", "#6ece58", "#b5de2b", "#fde725"],
|
|
6
|
+
magma: ["#000004", "#1c1044", "#4f127b", "#812581", "#b5367a", "#e55c30", "#fb8761", "#fec287", "#fcfdbf"],
|
|
7
|
+
inferno: ["#000004", "#1b0c41", "#4a0c6b", "#781c6d", "#a52c60", "#cf4446", "#ed6925", "#fb9b06", "#f7d13d", "#fcffa4"],
|
|
8
|
+
plasma: ["#0d0887", "#46039f", "#7201a8", "#9c179e", "#bd3786", "#d8576b", "#ed7953", "#fb9f3a", "#fdca26", "#f0f921"],
|
|
9
|
+
turbo: ["#30123b", "#4662d7", "#36aaf9", "#1ae4b6", "#72fe5e", "#c8ef34", "#faba39", "#f66b19", "#ca2a04", "#7a0403"],
|
|
10
|
+
grey: ["#000000", "#ffffff"],
|
|
11
|
+
heat: ["#000000", "#8b0000", "#ff4500", "#ffd700", "#ffffff"],
|
|
12
|
+
cool: ["#00ffff", "#ff00ff"],
|
|
13
|
+
blues: ["#f7fbff", "#c6dbef", "#6baed6", "#2171b5", "#08306b"],
|
|
14
|
+
reds: ["#fff5f0", "#fcbba1", "#fb6a4a", "#cb181d", "#67000d"],
|
|
15
|
+
});
|
|
16
|
+
export const COLORMAP_NAMES = Object.freeze(Object.keys(COLORMAPS));
|
|
17
|
+
export function parseColor(c) {
|
|
18
|
+
if (typeof c === "string") {
|
|
19
|
+
const m = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(c.trim());
|
|
20
|
+
if (!m)
|
|
21
|
+
throw new TypeError(`voxel: color must be #rrggbb or #rrggbbaa (got ${JSON.stringify(c)})`);
|
|
22
|
+
const v = parseInt(m[1], 16);
|
|
23
|
+
return [(v >> 16) & 255, (v >> 8) & 255, v & 255, m[2] ? parseInt(m[2], 16) : null];
|
|
24
|
+
}
|
|
25
|
+
if (!Array.isArray(c) || c.length < 3 || c.length > 4 || !c.every(Number.isFinite))
|
|
26
|
+
throw new TypeError("voxel: color must be [r, g, b, a?] (0..255)");
|
|
27
|
+
return [c[0], c[1], c[2], c.length === 4 ? c[3] : null];
|
|
28
|
+
}
|
|
29
|
+
/** 色表の指定 → [位置, 色] の並び(位置は昇順・0 と 1 を含む) */
|
|
30
|
+
export function colorStops(spec) {
|
|
31
|
+
if (spec == null)
|
|
32
|
+
spec = "viridis";
|
|
33
|
+
let list;
|
|
34
|
+
if (typeof spec === "string") {
|
|
35
|
+
const m = COLORMAPS[spec];
|
|
36
|
+
if (!m)
|
|
37
|
+
throw new RangeError(`voxel: unknown colormap "${spec}" (${COLORMAP_NAMES.join(" | ")} or stops)`);
|
|
38
|
+
list = m.map((c, i) => [i / (m.length - 1), c]);
|
|
39
|
+
}
|
|
40
|
+
else if (!Array.isArray(spec) || !spec.length)
|
|
41
|
+
throw new TypeError("voxel: colormap must be a name, a list of colors or a list of [t, color]");
|
|
42
|
+
else if (Array.isArray(spec[0]) && typeof spec[0][0] === "number" && spec[0].length === 2 && (typeof spec[0][1] === "string" || Array.isArray(spec[0][1])))
|
|
43
|
+
list = spec.map(([t, c]) => [+t, c]);
|
|
44
|
+
else
|
|
45
|
+
list = spec.map((c, i, a) => [a.length === 1 ? 0 : i / (a.length - 1), c]);
|
|
46
|
+
const out = list.map(([t, c]) => { if (!(t >= 0 && t <= 1))
|
|
47
|
+
throw new RangeError("voxel: colormap stop positions must be within 0..1"); return [t, parseColor(c)]; }).sort((a, b) => a[0] - b[0]);
|
|
48
|
+
if (out[0][0] > 0)
|
|
49
|
+
out.unshift([0, out[0][1]]);
|
|
50
|
+
if (out[out.length - 1][0] < 1)
|
|
51
|
+
out.push([1, out[out.length - 1][1]]);
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/** 色表=256 段の RGBA8(値 0〜1 → 色・α は stop に無ければ alphaRamp=[a0, a1] の線形) */
|
|
55
|
+
export function buildColormap(spec, alphaRamp = null) {
|
|
56
|
+
const st = colorStops(spec), out = new Uint8Array(256 * 4), [a0, a1] = alphaRamp ?? [0, 1];
|
|
57
|
+
let k = 0;
|
|
58
|
+
for (let i = 0; i < 256; i++) {
|
|
59
|
+
const t = i / 255;
|
|
60
|
+
while (k < st.length - 2 && t > st[k + 1][0])
|
|
61
|
+
k++;
|
|
62
|
+
const [t0, c0] = st[k], [t1, c1] = st[k + 1], f = t1 > t0 ? Math.min(1, Math.max(0, (t - t0) / (t1 - t0))) : 0;
|
|
63
|
+
const aa = c0[3] != null && c1[3] != null ? c0[3] + (c1[3] - c0[3]) * f : (a0 + (a1 - a0) * t) * 255;
|
|
64
|
+
out[i * 4] = Math.round(c0[0] + (c1[0] - c0[0]) * f);
|
|
65
|
+
out[i * 4 + 1] = Math.round(c0[1] + (c1[1] - c0[1]) * f);
|
|
66
|
+
out[i * 4 + 2] = Math.round(c0[2] + (c1[2] - c0[2]) * f);
|
|
67
|
+
out[i * 4 + 3] = Math.round(Math.min(255, Math.max(0, aa)));
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
// ---- 値の詰め(R8/RGBA8)----
|
|
72
|
+
/** 値の min/max(noData を除く・1 成分) */
|
|
73
|
+
export function valueRange(values, noData) {
|
|
74
|
+
let lo = Infinity, hi = -Infinity;
|
|
75
|
+
for (let i = 0; i < values.length; i++) {
|
|
76
|
+
const v = values[i];
|
|
77
|
+
if (v === noData || v !== v)
|
|
78
|
+
continue;
|
|
79
|
+
if (v < lo)
|
|
80
|
+
lo = v;
|
|
81
|
+
if (v > hi)
|
|
82
|
+
hi = v;
|
|
83
|
+
}
|
|
84
|
+
if (!(lo <= hi))
|
|
85
|
+
return [0, 1];
|
|
86
|
+
return lo < hi ? [lo, hi] : [lo, lo + 1];
|
|
87
|
+
}
|
|
88
|
+
/** 1 成分の値 → R8(0=空・1〜255=range を [0,1] に写した物。range の外は端に寄せる・threshold 未満は空) */
|
|
89
|
+
export function quantize(values, range, noData, threshold) {
|
|
90
|
+
const out = new Uint8Array(values.length), [lo, hi] = range, sc = 254 / (hi - lo), th = threshold ?? -Infinity;
|
|
91
|
+
for (let i = 0; i < values.length; i++) {
|
|
92
|
+
const v = values[i];
|
|
93
|
+
if (v === noData || v !== v || v < th)
|
|
94
|
+
continue;
|
|
95
|
+
const t = (v - lo) * sc;
|
|
96
|
+
out[i] = 1 + (t <= 0 ? 0 : t >= 254 ? 254 : Math.round(t));
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
/** 4 成分(RGBA)→ RGBA8(Uint8 はそのまま・Uint16 は 1/257・Float32 は 0〜1 を 255 倍。noData は α 0) */
|
|
101
|
+
export function toRgba8(values, noData) {
|
|
102
|
+
if (values instanceof Uint8Array && noData == null)
|
|
103
|
+
return values.slice(); // 写し=transfer しても渡した人の配列は手放さない
|
|
104
|
+
const out = new Uint8Array(values.length), sc = values instanceof Uint8Array ? 1 : values instanceof Uint16Array ? 1 / 257 : 255;
|
|
105
|
+
for (let i = 0; i < values.length; i += 4) {
|
|
106
|
+
if (noData != null && values[i + 3] === noData)
|
|
107
|
+
continue;
|
|
108
|
+
for (let c = 0; c < 4; c++) {
|
|
109
|
+
const t = values[i + c] * sc;
|
|
110
|
+
out[i + c] = t <= 0 ? 0 : t >= 255 ? 255 : Math.round(t);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
// ---- 置き方 ----
|
|
116
|
+
const unit = (lon, lat, rAx) => { const a = lon * D2R; let b = lat * D2R; if (rAx !== 1)
|
|
117
|
+
b = Math.atan(rAx * Math.tan(b)); const cb = Math.cos(b); return [cb * Math.cos(a), Math.sin(b), cb * Math.sin(a)]; };
|
|
118
|
+
/** 錨の東北上 m → 単位球(列=東・北・上×1/R)=本体の単位球(y が極)。splat の splat-gl.ts と同じ */
|
|
119
|
+
export function enuBasis(lon, lat, rAx = 1) {
|
|
120
|
+
const u = unit(lon, lat, rAx), cl = Math.hypot(u[0], u[2]), k = 1 / (rAx === 1 ? R_SPHERE : R_ELL);
|
|
121
|
+
const E = cl < 1e-12 ? [1, 0, 0] : [-u[2] / cl, 0, u[0] / cl], N = [E[1] * u[2] - E[2] * u[1], E[2] * u[0] - E[0] * u[2], E[0] * u[1] - E[1] * u[0]];
|
|
122
|
+
return { E: E.map(v => v * k), N: N.map(v => v * k), U: u.map(v => v * k), u };
|
|
123
|
+
}
|
|
124
|
+
/** ECEF(WGS84・z が極)→ 経緯度・楕円体高 */
|
|
125
|
+
export function ecefToLngLat(x, y, z) {
|
|
126
|
+
const lon = Math.atan2(y, x), p = Math.hypot(x, y);
|
|
127
|
+
let lat = Math.atan2(z, p * (1 - WGS_E2)), h = 0;
|
|
128
|
+
for (let i = 0; i < 6; i++) {
|
|
129
|
+
const s = Math.sin(lat), N = WGS_A / Math.sqrt(1 - WGS_E2 * s * s);
|
|
130
|
+
h = p / Math.cos(lat) - N;
|
|
131
|
+
lat = Math.atan2(z, p * (1 - WGS_E2 * N / (N + h)));
|
|
132
|
+
}
|
|
133
|
+
return [lon / D2R, lat / D2R, h];
|
|
134
|
+
}
|
|
135
|
+
/** ECEF の東北上の基底(行=東・北・上の単位ベクトル・ECEF m → ENU m は行優先 3×3) */
|
|
136
|
+
export function ecefEnuRows(lon, lat) {
|
|
137
|
+
const a = lon * D2R, b = lat * D2R, sa = Math.sin(a), ca = Math.cos(a), sb = Math.sin(b), cb = Math.cos(b);
|
|
138
|
+
return [-sa, ca, 0, -sb * ca, -sb * sa, cb, cb * ca, cb * sa, sb];
|
|
139
|
+
}
|
|
140
|
+
export const mul3 = (A, B) => { const C = new Array(9); for (let r = 0; r < 3; r++)
|
|
141
|
+
for (let c = 0; c < 3; c++)
|
|
142
|
+
C[r * 3 + c] = A[r * 3] * B[c] + A[r * 3 + 1] * B[3 + c] + A[r * 3 + 2] * B[6 + c]; return C; };
|
|
143
|
+
/** 行優先 3×3 の逆行列(特異なら投げる) */
|
|
144
|
+
export function inv3(m) {
|
|
145
|
+
const [a, b, c, d, e, f, g, h, i] = Array.from(m, Number);
|
|
146
|
+
const A = e * i - f * h, B = -(d * i - f * g), C = d * h - e * g, det = a * A + b * B + c * C;
|
|
147
|
+
if (!(Math.abs(det) > 1e-30))
|
|
148
|
+
throw new Error("voxel: the box is degenerate (singular placement)");
|
|
149
|
+
const k = 1 / det;
|
|
150
|
+
return [A * k, -(b * i - c * h) * k, (b * f - c * e) * k, B * k, (a * i - c * g) * k, -(a * f - c * d) * k, C * k, -(a * h - b * g) * k, (a * e - b * d) * k];
|
|
151
|
+
}
|
|
152
|
+
/** heading(北から時計回り)・pitch・roll(度)=東北上の中の向き(行優先 3×3・splat の placeMat と同じ約束) */
|
|
153
|
+
export function hprMat([hd = 0, pt = 0, rl = 0] = []) {
|
|
154
|
+
const h = -hd * D2R, p = pt * D2R, r = rl * D2R;
|
|
155
|
+
const Rz = [Math.cos(h), -Math.sin(h), 0, Math.sin(h), Math.cos(h), 0, 0, 0, 1], Rx = [1, 0, 0, 0, Math.cos(p), -Math.sin(p), 0, Math.sin(p), Math.cos(p)], Ry = [Math.cos(r), 0, Math.sin(r), 0, 1, 0, -Math.sin(r), 0, Math.cos(r)];
|
|
156
|
+
return mul3(mul3(Rz, Rx), Ry);
|
|
157
|
+
}
|
|
158
|
+
/** VoxelData の box → 錨+A(セル → 東北上 m)。transform=局所の単位立方体 [0,1]³ → ECEF m */
|
|
159
|
+
export function placeBox(box, dims) {
|
|
160
|
+
let anchor, T, placed = false;
|
|
161
|
+
if ("transform" in box && box.transform != null) {
|
|
162
|
+
const M = Array.from(box.transform, Number);
|
|
163
|
+
if (M.length !== 16 || !M.every(Number.isFinite))
|
|
164
|
+
throw new TypeError("voxel: box.transform must be a column-major 4×4 (16 finite numbers)");
|
|
165
|
+
anchor = ecefToLngLat(M[12], M[13], M[14]);
|
|
166
|
+
placed = true;
|
|
167
|
+
// 列優先 → 行優先の 3×3・列 i を dims[i] で割る=セル 1 個ぶんの ECEF ベクトル → 東北上 m
|
|
168
|
+
const lin = [M[0] / dims[0], M[4] / dims[1], M[8] / dims[2], M[1] / dims[0], M[5] / dims[1], M[9] / dims[2], M[2] / dims[0], M[6] / dims[1], M[10] / dims[2]];
|
|
169
|
+
T = mul3(ecefEnuRows(anchor[0], anchor[1]), lin);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const b = box;
|
|
173
|
+
anchor = [b.origin[0], b.origin[1], b.origin[2] || 0];
|
|
174
|
+
const R = hprMat(b.orientation ?? []);
|
|
175
|
+
T = [R[0] * b.size[0] / dims[0], R[1] * b.size[1] / dims[1], R[2] * b.size[2] / dims[2], R[3] * b.size[0] / dims[0], R[4] * b.size[1] / dims[1], R[5] * b.size[2] / dims[2], R[6] * b.size[0] / dims[0], R[7] * b.size[1] / dims[1], R[8] * b.size[2] / dims[2]];
|
|
176
|
+
}
|
|
177
|
+
// 錨から箱の最遠点まで=各辺の長さの和(上界・地平線の向こうの判定に使うだけ)
|
|
178
|
+
const radius = Math.hypot(T[0] * dims[0], T[3] * dims[0], T[6] * dims[0]) + Math.hypot(T[1] * dims[1], T[4] * dims[1], T[7] * dims[1]) + Math.hypot(T[2] * dims[2], T[5] * dims[2], T[8] * dims[2]);
|
|
179
|
+
return { anchor, A: Float64Array.from(T), radius, placed };
|
|
180
|
+
}
|
|
181
|
+
/** 経緯度・高さ → 単位球(本体の単位球=y が極・rAx=楕円体の b/a で測地緯度を更成緯度に) */
|
|
182
|
+
export const unitAt = (lon, lat, h, rAx = 1) => { const u = unit(lon, lat, rAx), r = 1 + h / (rAx === 1 ? R_SPHERE : R_ELL); return [u[0] * r, u[1] * r, u[2] * r]; };
|
|
183
|
+
export const SHELL_N = 16;
|
|
184
|
+
/** VoxelData の region → 錨(中心の経緯度・h0)+殻+セルの大きさ。region=[w, s, e, n(度), h0, h1(m)]・e < w は日付変更線を跨ぐ */
|
|
185
|
+
export function placeRegion(region, dims, rAx = 1) {
|
|
186
|
+
const [w, s, e, n, h0, h1] = Array.from(region, Number);
|
|
187
|
+
const spanLon = ((e - w) % 360 + 360) % 360 || 360, spanLat = n - s;
|
|
188
|
+
const lonC = w + spanLon / 2, latC = (s + n) / 2, R = rAx === 1 ? R_SPHERE : R_ELL;
|
|
189
|
+
const anchor = [((lonC + 180) % 360 + 360) % 360 - 180, latC, h0];
|
|
190
|
+
const B = enuBasis(anchor[0], anchor[1], rAx), o = unitAt(anchor[0], anchor[1], h0, rAx), k = 1 / R;
|
|
191
|
+
const Eu = B.E.map(v => v / k), Nu = B.N.map(v => v / k), Uu = B.U.map(v => v / k); // 単位ベクトル
|
|
192
|
+
const enu = (lon, lat, h) => { const p = unitAt(lon, lat, h, rAx), d = [p[0] - o[0], p[1] - o[1], p[2] - o[2]]; return [(d[0] * Eu[0] + d[1] * Eu[1] + d[2] * Eu[2]) * R, (d[0] * Nu[0] + d[1] * Nu[1] + d[2] * Nu[2]) * R, (d[0] * Uu[0] + d[1] * Uu[1] + d[2] * Uu[2]) * R]; };
|
|
193
|
+
// 6 面を SHELL_N×SHELL_N に分け、面ごとの外向き(錨の東北上で 下・上・南・北・西・東)に合う向きで三角形に(丸みで面が傾くので「中心から外」では決めない)
|
|
194
|
+
const out = [], N = SHELL_N;
|
|
195
|
+
const quad = (p, o) => {
|
|
196
|
+
const nx = (p[1][1] - p[0][1]) * (p[3][2] - p[0][2]) - (p[1][2] - p[0][2]) * (p[3][1] - p[0][1]), ny = (p[1][2] - p[0][2]) * (p[3][0] - p[0][0]) - (p[1][0] - p[0][0]) * (p[3][2] - p[0][2]), nz = (p[1][0] - p[0][0]) * (p[3][1] - p[0][1]) - (p[1][1] - p[0][1]) * (p[3][0] - p[0][0]);
|
|
197
|
+
const order = nx * o[0] + ny * o[1] + nz * o[2] >= 0 ? [0, 1, 2, 0, 2, 3] : [0, 2, 1, 0, 3, 2];
|
|
198
|
+
for (const i of order)
|
|
199
|
+
out.push(p[i][0], p[i][1], p[i][2]);
|
|
200
|
+
};
|
|
201
|
+
const face = (f, o) => { for (let j = 0; j < N; j++)
|
|
202
|
+
for (let i = 0; i < N; i++)
|
|
203
|
+
quad([f(i / N, j / N), f((i + 1) / N, j / N), f((i + 1) / N, (j + 1) / N), f(i / N, (j + 1) / N)], o); };
|
|
204
|
+
const lonAt = (a) => w + spanLon * a, latAt = (b) => s + spanLat * b, hAt = (c) => h0 + (h1 - h0) * c;
|
|
205
|
+
face((a, b) => enu(lonAt(a), latAt(b), h0), [0, 0, -1]);
|
|
206
|
+
face((a, b) => enu(lonAt(a), latAt(b), h1), [0, 0, 1]); // 下・上
|
|
207
|
+
face((a, c) => enu(lonAt(a), s, hAt(c)), [0, -1, 0]);
|
|
208
|
+
face((a, c) => enu(lonAt(a), n, hAt(c)), [0, 1, 0]); // 南・北
|
|
209
|
+
face((b, c) => enu(w, latAt(b), hAt(c)), [-1, 0, 0]);
|
|
210
|
+
face((b, c) => enu(w + spanLon, latAt(b), hAt(c)), [1, 0, 0]); // 西・東
|
|
211
|
+
const shell = Float32Array.from(out);
|
|
212
|
+
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity, z0 = Infinity, z1 = -Infinity;
|
|
213
|
+
for (let i = 0; i < out.length; i += 3) {
|
|
214
|
+
x0 = Math.min(x0, out[i]);
|
|
215
|
+
x1 = Math.max(x1, out[i]);
|
|
216
|
+
y0 = Math.min(y0, out[i + 1]);
|
|
217
|
+
y1 = Math.max(y1, out[i + 1]);
|
|
218
|
+
z0 = Math.min(z0, out[i + 2]);
|
|
219
|
+
z1 = Math.max(z1, out[i + 2]);
|
|
220
|
+
}
|
|
221
|
+
const bc = [(x0 + x1) / 2, (y0 + y1) / 2, (z0 + z1) / 2];
|
|
222
|
+
let rb = 0;
|
|
223
|
+
for (let i = 0; i < out.length; i += 3)
|
|
224
|
+
rb = Math.max(rb, Math.hypot(out[i] - bc[0], out[i + 1] - bc[1], out[i + 2] - bc[2]));
|
|
225
|
+
const cellM = [spanLon * D2R * R * Math.cos(latC * D2R) / dims[0], spanLat * D2R * R / dims[1], (h1 - h0) / dims[2]];
|
|
226
|
+
const A = Float64Array.from([cellM[0], 0, 0, 0, cellM[1], 0, 0, 0, cellM[2]]);
|
|
227
|
+
return { anchor, A, radius: Math.hypot(...bc) + rb, placed: true, geo: { west: w, south: s, spanLon, spanLat, h0, h1, shell, bs: [bc[0], bc[1], bc[2], rb], cellM } };
|
|
228
|
+
}
|
|
229
|
+
const nums = (v, name, len) => {
|
|
230
|
+
const a = v != null && typeof v === "object" && typeof v.length === "number" ? Array.from(v, Number) : null;
|
|
231
|
+
if (!a || a.length !== len || !a.every(Number.isFinite))
|
|
232
|
+
throw new TypeError(`voxel: ${name} must be ${len} finite numbers`);
|
|
233
|
+
return a;
|
|
234
|
+
};
|
|
235
|
+
/** 断面の指定を検める(各軸 [lo, hi]・0〜1・lo < hi) */
|
|
236
|
+
export function checkClip(c) {
|
|
237
|
+
if (c == null)
|
|
238
|
+
return null;
|
|
239
|
+
const a = nums(c, "clip", 6);
|
|
240
|
+
for (let i = 0; i < 3; i++)
|
|
241
|
+
if (!(a[i * 2] >= 0 && a[i * 2 + 1] <= 1 && a[i * 2] < a[i * 2 + 1]))
|
|
242
|
+
throw new RangeError("voxel: clip must be [x0, x1, y0, y1, z0, z1] fractions with 0 <= lo < hi <= 1");
|
|
243
|
+
return a;
|
|
244
|
+
}
|
|
245
|
+
export function checkVoxelData(d) {
|
|
246
|
+
const v = d;
|
|
247
|
+
if (!v || typeof v !== "object" || !Array.isArray(v.dims) || v.dims.length !== 3 || !(v.values instanceof Float32Array || v.values instanceof Uint16Array || v.values instanceof Uint8Array))
|
|
248
|
+
throw new TypeError("voxel: data must be VoxelData (dims, values, box | region)");
|
|
249
|
+
const comps = (v.components ?? 1);
|
|
250
|
+
if (comps !== 1 && comps !== 4)
|
|
251
|
+
throw new RangeError("voxel: components must be 1 | 4");
|
|
252
|
+
const T = v.times == null ? 1 : v.times.length;
|
|
253
|
+
if (v.times != null && (!(T >= 1) || !Array.from(v.times).every((x, i, a) => Number.isFinite(x) && (i === 0 || x > a[i - 1]))))
|
|
254
|
+
throw new RangeError("voxel: times must be finite and strictly ascending (ms)");
|
|
255
|
+
if (v.values.length !== v.dims[0] * v.dims[1] * v.dims[2] * comps * T)
|
|
256
|
+
throw new RangeError(`voxel: values length ${v.values.length} != ${v.dims[0] * v.dims[1] * v.dims[2] * comps * T}`);
|
|
257
|
+
const shape = v.shape ?? (v.box ? "box" : v.region ? "region" : undefined);
|
|
258
|
+
if (shape !== "box" && shape !== "region")
|
|
259
|
+
throw new RangeError("voxel: shape must be box | region");
|
|
260
|
+
return { ...v, shape, components: comps };
|
|
261
|
+
}
|
|
262
|
+
/** こま t の値(times が無ければ全部)=values の部分(写さない) */
|
|
263
|
+
export function frameValues(values, n, comps, t) { const k = n * comps; return values.subarray(t * k, (t + 1) * k); }
|
|
264
|
+
/** 時刻 ms → 見せるこま=その時刻までで最後のこま(最初のこまより前は 0・最後より後は最後) */
|
|
265
|
+
export function frameAt(times, ms) { let lo = 0, hi = times.length - 1; if (!(ms >= times[0]))
|
|
266
|
+
return 0; while (lo < hi) {
|
|
267
|
+
const m = (lo + hi + 1) >> 1;
|
|
268
|
+
if (times[m] <= ms)
|
|
269
|
+
lo = m;
|
|
270
|
+
else
|
|
271
|
+
hi = m - 1;
|
|
272
|
+
} return lo; }
|
|
273
|
+
/** 閾(データの単位)→ R8 の 0〜1 */
|
|
274
|
+
export const thrOf = (threshold, range) => threshold == null ? 0 : Math.min(1, Math.max(0, (threshold - range[0]) / (range[1] - range[0])));
|
|
275
|
+
/** VoxelData → 描き手へ渡す物(worker で。配列は transfer できる物)。値は閾で切らない(閾は描き手が見る=等値面は場の全体が要る) */
|
|
276
|
+
export function prepare(data, o = {}) {
|
|
277
|
+
const d = checkVoxelData(data), dims = d.dims, comps = d.components, n = dims[0] * dims[1] * dims[2];
|
|
278
|
+
let place, geo = null;
|
|
279
|
+
if (d.shape === "box") {
|
|
280
|
+
if (!d.box)
|
|
281
|
+
throw new TypeError("voxel: shape \"box\" needs box");
|
|
282
|
+
place = placeBox(d.box, dims);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
if (!d.region)
|
|
286
|
+
throw new TypeError("voxel: shape \"region\" needs region");
|
|
287
|
+
const r = placeRegion(d.region, dims, o.rAx ?? 1);
|
|
288
|
+
place = r;
|
|
289
|
+
geo = r.geo;
|
|
290
|
+
}
|
|
291
|
+
const { anchor, A, radius, placed } = place;
|
|
292
|
+
let range = [0, 1], thr = 0;
|
|
293
|
+
if (comps === 1) { // 色表の幅はこまを通して一つ(こまで色が変わらない)
|
|
294
|
+
range = o.range ?? d.range ?? valueRange(d.values, d.noData);
|
|
295
|
+
if (!(range[0] < range[1]))
|
|
296
|
+
throw new RangeError("voxel: range must be [min, max] with min < max");
|
|
297
|
+
thr = thrOf(o.threshold, range);
|
|
298
|
+
}
|
|
299
|
+
const T = d.times?.length ?? 1, frames = [];
|
|
300
|
+
for (let t = 0; t < T; t++) {
|
|
301
|
+
const v = T > 1 ? frameValues(d.values, n, comps, t) : d.values;
|
|
302
|
+
frames.push(comps === 4 ? toRgba8(v, d.noData) : quantize(v, range, d.noData, null));
|
|
303
|
+
}
|
|
304
|
+
const cmap = buildColormap(o.colormap, o.alphaRamp ?? null);
|
|
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
|
+
}
|
|
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)]) : [])]);
|
|
313
|
+
// ---- 大きな格子=ブリック(第 5 段・2026-10-24)----
|
|
314
|
+
// 一軸でも BRICK_LIMIT(WebGL2 の 3D テクスチャの最低保証)を越える格子は、持ち分 BRICK セル角のブリックに分けて別々のテクスチャにする。
|
|
315
|
+
// 各ブリックのテクスチャは持ち分の前後に 1 セルずつ糊代(隣のセル)を足す=三線形・等値面の勾配が継ぎ目で途切れない。描くのは持ち分(own)だけ=体積が継ぎ目で二重に重ならない。
|
|
316
|
+
// box のブリック=親と同じ錨と A(セル → 東北上 m)+セルの位置ずれ off(描き手が A·off だけ動かす=相対高さでも地形でブリックごとにずれない)。
|
|
317
|
+
// region のブリック=経緯度高の部分の region(殻もそれぞれ)。当たり=ブリックの番号 + off → 親の番号(描き手が full と off で戻す)。
|
|
318
|
+
// vmax=ブリックの最大値(R8・RGBA は α)=閾より下のブリックは描かない(空白飛ばしのブリック単位)。
|
|
319
|
+
/** 一枚のテクスチャで描く格子の上限(各軸) */
|
|
320
|
+
export const BRICK_LIMIT = 256;
|
|
321
|
+
/** ブリックの持ち分(各軸のセル数)=テクスチャは糊代を足して最大 BRICK + 2 */
|
|
322
|
+
export const BRICK = 126;
|
|
323
|
+
/** 詰めた値の最大(R8 はそのまま・RGBA は α) */
|
|
324
|
+
export function maxOf(tex, comps) { let m = 0; for (let i = comps === 4 ? 3 : 0; i < tex.length; i += comps)
|
|
325
|
+
if (tex[i] > m) {
|
|
326
|
+
m = tex[i];
|
|
327
|
+
if (m === 255)
|
|
328
|
+
break;
|
|
329
|
+
} return m; }
|
|
330
|
+
/** 分けるか(一軸でも上限を越える) */
|
|
331
|
+
export const needsBricks = (dims, limit = BRICK_LIMIT) => dims[0] > limit || dims[1] > limit || dims[2] > limit;
|
|
332
|
+
/** 各軸の持ち分の区切り([始め, 終わり) の並び) */
|
|
333
|
+
export function brickSpans(n, brick = BRICK) { const out = []; for (let a = 0; a < n; a += brick)
|
|
334
|
+
out.push([a, Math.min(n, a + brick)]); return out; }
|
|
335
|
+
/** 格子の部分(off から bd セル角)を写す(x が最速・成分は comps 個ずつ) */
|
|
336
|
+
export function extract(values, dims, comps, off, bd) {
|
|
337
|
+
const out = new values.constructor(bd[0] * bd[1] * bd[2] * comps), row = bd[0] * comps;
|
|
338
|
+
for (let k = 0; k < bd[2]; k++)
|
|
339
|
+
for (let j = 0; j < bd[1]; j++) {
|
|
340
|
+
const src = (off[0] + dims[0] * (off[1] + j + dims[1] * (off[2] + k))) * comps;
|
|
341
|
+
out.set(values.subarray(src, src + row), (bd[0] * (j + bd[1] * k)) * comps);
|
|
342
|
+
}
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
/** ブリックの番号(テクスチャの中)→ 親の格子の番号 */
|
|
346
|
+
export function parentIndex(index, dims, off, full) {
|
|
347
|
+
const i = index % dims[0], j = Math.floor(index / dims[0]) % dims[1], k = Math.floor(index / (dims[0] * dims[1]));
|
|
348
|
+
const cell = [i + off[0], j + off[1], k + off[2]];
|
|
349
|
+
return { index: cell[0] + full[0] * (cell[1] + full[1] * cell[2]), cell };
|
|
350
|
+
}
|
|
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)も作る */
|
|
446
|
+
export function prepareGrid(data, o = {}, brick = BRICK, limit = BRICK_LIMIT) {
|
|
447
|
+
const d = checkVoxelData(data), dims = d.dims, comps = d.components, L = levelCount(dims, comps, o.lodBytes);
|
|
448
|
+
if (!needsBricks(dims, limit)) {
|
|
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 };
|
|
452
|
+
}
|
|
453
|
+
const nAll = dims[0] * dims[1] * dims[2];
|
|
454
|
+
let range = [0, 1];
|
|
455
|
+
if (comps === 1) {
|
|
456
|
+
range = o.range ?? d.range ?? valueRange(d.values, d.noData);
|
|
457
|
+
if (!(range[0] < range[1]))
|
|
458
|
+
throw new RangeError("voxel: range must be [min, max] with min < max");
|
|
459
|
+
}
|
|
460
|
+
// 親の置き方(錨・A・半径)=box はブリックも同じ物を使う。region は部分の region を作り直す
|
|
461
|
+
let parent, pgeo = null;
|
|
462
|
+
if (d.shape === "box") {
|
|
463
|
+
if (!d.box)
|
|
464
|
+
throw new TypeError("voxel: shape \"box\" needs box");
|
|
465
|
+
parent = placeBox(d.box, dims);
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
if (!d.region)
|
|
469
|
+
throw new TypeError("voxel: shape \"region\" needs region");
|
|
470
|
+
const r = placeRegion(d.region, dims, o.rAx ?? 1);
|
|
471
|
+
parent = r;
|
|
472
|
+
pgeo = r.geo;
|
|
473
|
+
}
|
|
474
|
+
const full = packFrames(d, range, nAll), pyr = L ? pyramid(full, dims, comps, L) : [];
|
|
475
|
+
const sx = brickSpans(dims[0], brick), sy = brickSpans(dims[1], brick), sz = brickSpans(dims[2], brick), bricks = [];
|
|
476
|
+
let vmax = 0;
|
|
477
|
+
for (const [z0, z1] of sz)
|
|
478
|
+
for (const [y0, y1] of sy)
|
|
479
|
+
for (const [x0, x1] of sx) {
|
|
480
|
+
const lo = [x0, y0, z0], hi = [x1, y1, z1];
|
|
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 });
|
|
484
|
+
}
|
|
485
|
+
const cmap = buildColormap(o.colormap, o.alphaRamp ?? null);
|
|
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 };
|
|
487
|
+
}
|
|
488
|
+
/** 光線と箱 [0, dims] の交わり(t0, t1)。無ければ null */
|
|
489
|
+
export function rayBox(ro, rd, dims) {
|
|
490
|
+
let t0 = -Infinity, t1 = Infinity, axis = -1;
|
|
491
|
+
for (let i = 0; i < 3; i++) {
|
|
492
|
+
if (Math.abs(rd[i]) < 1e-12) {
|
|
493
|
+
if (ro[i] < 0 || ro[i] > dims[i])
|
|
494
|
+
return null;
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
let a = (0 - ro[i]) / rd[i], b = (dims[i] - ro[i]) / rd[i];
|
|
498
|
+
if (a > b)
|
|
499
|
+
[a, b] = [b, a];
|
|
500
|
+
if (a > t0) {
|
|
501
|
+
t0 = a;
|
|
502
|
+
axis = i;
|
|
503
|
+
}
|
|
504
|
+
if (b < t1)
|
|
505
|
+
t1 = b;
|
|
506
|
+
}
|
|
507
|
+
return t1 > Math.max(t0, 0) ? [t0, t1, axis] : null;
|
|
508
|
+
}
|
|
509
|
+
/** ブロックの DDA:値(R8・0=空)が thr 以上の最初のセル。tMax=ここまで(シーンの深度)。cell 空間の光線 */
|
|
510
|
+
export function marchBlocks(tex, dims, ro, rd, thr = 0, tMax = Infinity, comps = 1) {
|
|
511
|
+
const hit = rayBox(ro, rd, dims);
|
|
512
|
+
if (!hit)
|
|
513
|
+
return null;
|
|
514
|
+
const t0 = Math.max(hit[0], 0), t1 = Math.min(hit[1], tMax);
|
|
515
|
+
if (t1 <= t0)
|
|
516
|
+
return null;
|
|
517
|
+
const p = [ro[0] + rd[0] * (t0 + 1e-6), ro[1] + rd[1] * (t0 + 1e-6), ro[2] + rd[2] * (t0 + 1e-6)];
|
|
518
|
+
const c = p.map((v, i) => Math.min(dims[i] - 1, Math.max(0, Math.floor(v)))), st = [0, 1, 2].map(i => rd[i] > 0 ? 1 : rd[i] < 0 ? -1 : 0);
|
|
519
|
+
const tMaxA = [0, 1, 2].map(i => st[i] === 0 ? Infinity : ((st[i] > 0 ? c[i] + 1 : c[i]) - ro[i]) / rd[i]), tDelta = [0, 1, 2].map(i => st[i] === 0 ? Infinity : Math.abs(1 / rd[i]));
|
|
520
|
+
let axis = hit[0] >= 0 ? hit[2] : -1, t = t0;
|
|
521
|
+
const solid = (i) => comps === 4 ? tex[i * 4 + 3] > 0 : tex[i] > 0 && (tex[i] - 1) / 254 >= thr;
|
|
522
|
+
for (let steps = 0; steps < dims[0] + dims[1] + dims[2] + 2; steps++) {
|
|
523
|
+
const idx = c[0] + dims[0] * (c[1] + dims[1] * c[2]);
|
|
524
|
+
if (solid(idx))
|
|
525
|
+
return { cell: [c[0], c[1], c[2]], t, axis, steps };
|
|
526
|
+
const ax = tMaxA[0] < tMaxA[1] ? (tMaxA[0] < tMaxA[2] ? 0 : 2) : (tMaxA[1] < tMaxA[2] ? 1 : 2);
|
|
527
|
+
t = tMaxA[ax];
|
|
528
|
+
if (t > t1)
|
|
529
|
+
return null;
|
|
530
|
+
c[ax] += st[ax];
|
|
531
|
+
if (c[ax] < 0 || c[ax] >= dims[ax])
|
|
532
|
+
return null;
|
|
533
|
+
tMaxA[ax] += tDelta[ax];
|
|
534
|
+
axis = ax;
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
}
|