gaussian-splat-lite 1.3.2 → 1.4.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/CHANGELOG.md +16 -1
- package/CONTRIBUTING.md +14 -2
- package/NOTICE +11 -3
- package/README.md +2 -1
- package/THIRD_PARTY_LICENSES.md +64 -6
- package/bin/build-lod.js +241 -0
- package/bin/decoder/decoder.js +340 -0
- package/bin/decoder/decoder_bg.wasm +0 -0
- package/bin/rad/grid.js +620 -0
- package/bin/rad/heap.js +270 -0
- package/bin/rad/input.js +87 -0
- package/bin/rad/lod.js +657 -0
- package/bin/rad/math.js +166 -0
- package/bin/rad/parallel.js +578 -0
- package/bin/rad/progress.js +50 -0
- package/bin/rad/rad.js +612 -0
- package/bin/rad/splats.js +432 -0
- package/bin/rad/storage.js +776 -0
- package/dist/gaussian-splat-lite.cjs +16 -10
- package/dist/gaussian-splat-lite.cjs.map +1 -1
- package/dist/gaussian-splat-lite.min.cjs +106 -98
- package/dist/gaussian-splat-lite.min.cjs.map +1 -1
- package/dist/gaussian-splat-lite.module.js +16 -10
- package/dist/gaussian-splat-lite.module.js.map +1 -1
- package/dist/gaussian-splat-lite.module.min.js +1541 -1533
- package/dist/gaussian-splat-lite.module.min.js.map +1 -1
- package/dist/types/data/RadPagedSplats.d.cts +1 -1
- package/dist/types/data/RadPagedSplats.d.ts +1 -1
- package/dist/types/loaders/rad/radFormat.d.cts +2 -2
- package/dist/types/loaders/rad/radFormat.d.ts +2 -2
- package/docs/BuildRad.md +91 -0
- package/docs/RadStreamScheduler.md +3 -1
- package/package.json +9 -3
package/bin/rad/lod.js
ADDED
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
// LOD construction adapted from MIT-licensed code. See THIRD_PARTY_LICENSES.md.
|
|
2
|
+
import { Cells, Grid, NONE, cellHash } from "./grid.js";
|
|
3
|
+
import { Heap, Queue, sortSplats } from "./heap.js";
|
|
4
|
+
import { SCALE, halfBits, rotation } from "./math.js";
|
|
5
|
+
import { MISS, ROUND, hint } from "./parallel.js";
|
|
6
|
+
import { Progress } from "./progress.js";
|
|
7
|
+
import { SH_DIMS, Similarities } from "./splats.js";
|
|
8
|
+
import { IndexArray, PagedArray, Permutation } from "./storage.js";
|
|
9
|
+
|
|
10
|
+
export function buildLod(splats, lodBase, pool) {
|
|
11
|
+
const initial = splats.length;
|
|
12
|
+
const workspace = splats.workspace;
|
|
13
|
+
sortSplats(splats, pool);
|
|
14
|
+
mergeLevels(splats, pool);
|
|
15
|
+
splats.mergeSh(initial, pool);
|
|
16
|
+
const root = splats.length - 1;
|
|
17
|
+
|
|
18
|
+
// Children always precede their binary parent: the two of merged splat
|
|
19
|
+
// `index` are entries 2 * (index - initial) and the next of `children`. A
|
|
20
|
+
// merged splat is kept if its weight is at least lodBase times its
|
|
21
|
+
// children's largest feature; one that is not passes that feature on, and
|
|
22
|
+
// its kept descendants become children of its kept ancestor.
|
|
23
|
+
const features = new PagedArray(workspace, Float64Array);
|
|
24
|
+
const selection = new Progress("Selecting LOD nodes", splats.length);
|
|
25
|
+
let retainedCount = initial;
|
|
26
|
+
for (let index = 0; index < initial; index++) {
|
|
27
|
+
features.set(index, splats.weight.get(index));
|
|
28
|
+
if ((index & 4095) === 0) selection.update(index);
|
|
29
|
+
}
|
|
30
|
+
// A bit for each merged splat.
|
|
31
|
+
const kept = new IndexArray(workspace);
|
|
32
|
+
for (let index = initial; index < splats.length; index++) {
|
|
33
|
+
let maximum = Number.NEGATIVE_INFINITY;
|
|
34
|
+
for (let c = 0; c < 2; c++) {
|
|
35
|
+
const child = splats.children.get((index - initial) * 2 + c);
|
|
36
|
+
maximum = Math.max(maximum, features.get(child));
|
|
37
|
+
}
|
|
38
|
+
const feature = splats.weight.get(index);
|
|
39
|
+
const retained = index === root || feature >= maximum * lodBase;
|
|
40
|
+
features.set(index, retained ? feature : maximum);
|
|
41
|
+
if (retained) {
|
|
42
|
+
retainedCount++;
|
|
43
|
+
const bit = index - initial;
|
|
44
|
+
kept.set(bit >>> 5, kept.get(bit >>> 5) | (1 << (bit & 31)));
|
|
45
|
+
}
|
|
46
|
+
if ((index & 4095) === 0) selection.update(index);
|
|
47
|
+
}
|
|
48
|
+
features.dispose();
|
|
49
|
+
splats.weight.dispose();
|
|
50
|
+
selection.finish(`${retainedCount} nodes retained`);
|
|
51
|
+
|
|
52
|
+
// Ordering reads sizes at random; a copy without the rest of each record
|
|
53
|
+
// stays cached.
|
|
54
|
+
const sizes = new PagedArray(workspace, Float32Array);
|
|
55
|
+
const sizing = new Progress("Reading LOD sizes", splats.length);
|
|
56
|
+
for (let index = 0; index < splats.length; index++) {
|
|
57
|
+
sizes.set(index, splats.feature(index));
|
|
58
|
+
if ((index & 4095) === 0) sizing.update(index);
|
|
59
|
+
}
|
|
60
|
+
sizing.finish();
|
|
61
|
+
// A splat's logical index is its position in `order`. Its children get
|
|
62
|
+
// consecutive logical indices: `first` and the `counts` - 1 after it.
|
|
63
|
+
const order = new IndexArray(workspace);
|
|
64
|
+
const first = new IndexArray(workspace);
|
|
65
|
+
const counts = new IndexArray(workspace);
|
|
66
|
+
order.push(root);
|
|
67
|
+
// The splats to visit, by logical index, splat and size: the frontier of a
|
|
68
|
+
// pass as triples of words, and a stack within it.
|
|
69
|
+
let frontier = new IndexArray(workspace);
|
|
70
|
+
frontier.push(0);
|
|
71
|
+
frontier.push(root);
|
|
72
|
+
frontier.push(bitsOf(sizes.get(root)));
|
|
73
|
+
let logicals = new Uint32Array(64);
|
|
74
|
+
let indices = new Uint32Array(64);
|
|
75
|
+
let lengths = new Float32Array(64);
|
|
76
|
+
let list = new Uint32Array(64);
|
|
77
|
+
let walk = new Uint32Array(64);
|
|
78
|
+
const ordering = new Progress("Ordering LOD tree", retainedCount);
|
|
79
|
+
let visited = 0;
|
|
80
|
+
for (let limit = sizes.get(root); ; limit /= 4) {
|
|
81
|
+
const next = new IndexArray(workspace);
|
|
82
|
+
for (let f = 0; f < frontier.length; f += 3) {
|
|
83
|
+
logicals[0] = frontier.get(f);
|
|
84
|
+
indices[0] = frontier.get(f + 1);
|
|
85
|
+
lengths[0] = floatOf(frontier.get(f + 2));
|
|
86
|
+
let top = 1;
|
|
87
|
+
while (top) {
|
|
88
|
+
if ((++visited & 4095) === 0) ordering.update(order.length);
|
|
89
|
+
top--;
|
|
90
|
+
const logical = logicals[top];
|
|
91
|
+
const index = indices[top];
|
|
92
|
+
if (lengths[top] < limit) {
|
|
93
|
+
next.push(logical);
|
|
94
|
+
next.push(index);
|
|
95
|
+
next.push(bitsOf(lengths[top]));
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (index < initial) continue;
|
|
99
|
+
// Its children: the kept splats below it, through those not kept.
|
|
100
|
+
let count = 0;
|
|
101
|
+
let depth = 0;
|
|
102
|
+
walk[depth++] = index;
|
|
103
|
+
while (depth) {
|
|
104
|
+
const pair = (walk[--depth] - initial) * 2;
|
|
105
|
+
for (let c = 0; c < 2; c++) {
|
|
106
|
+
const child = splats.children.get(pair + c);
|
|
107
|
+
const bit = child - initial;
|
|
108
|
+
if (child < initial || (kept.get(bit >>> 5) >>> (bit & 31)) & 1) {
|
|
109
|
+
if (count === list.length) list = grow(list, count);
|
|
110
|
+
list[count++] = child;
|
|
111
|
+
} else {
|
|
112
|
+
if (depth === walk.length) walk = grow(walk, depth);
|
|
113
|
+
walk[depth++] = child;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (count > 65535)
|
|
118
|
+
throw new Error("LOD child count exceeds RAD u16 limit");
|
|
119
|
+
sort(list, count);
|
|
120
|
+
const start = order.length;
|
|
121
|
+
first.set(logical, start);
|
|
122
|
+
counts.set(logical, count);
|
|
123
|
+
for (let c = 0; c < count; c++) order.push(list[c]);
|
|
124
|
+
if (top + count > logicals.length) {
|
|
125
|
+
logicals = grow(logicals, top, top + count);
|
|
126
|
+
indices = grow(indices, top, top + count);
|
|
127
|
+
lengths = grow(lengths, top, top + count);
|
|
128
|
+
}
|
|
129
|
+
for (let c = count - 1; c >= 0; c--) {
|
|
130
|
+
logicals[top] = start + c;
|
|
131
|
+
indices[top] = list[c];
|
|
132
|
+
lengths[top++] = sizes.get(list[c]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
frontier.dispose();
|
|
137
|
+
if (!next.length) {
|
|
138
|
+
next.dispose();
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
frontier = next;
|
|
142
|
+
}
|
|
143
|
+
sizes.dispose();
|
|
144
|
+
kept.dispose();
|
|
145
|
+
ordering.finish();
|
|
146
|
+
chunkTree(splats, order, first, counts, pool);
|
|
147
|
+
order.dispose();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Merges the splats level by level into a binary tree, adding the merged
|
|
151
|
+
// splats after the leaves.
|
|
152
|
+
function mergeLevels(splats, pool) {
|
|
153
|
+
const initial = splats.length;
|
|
154
|
+
const workspace = splats.workspace;
|
|
155
|
+
// Merged splats lie within the leaves' bounds. merge() reads the leaves'
|
|
156
|
+
// weights.
|
|
157
|
+
const bounds = {
|
|
158
|
+
min: [0, 1, 2].map(() => Number.POSITIVE_INFINITY),
|
|
159
|
+
max: [0, 1, 2].map(() => Number.NEGATIVE_INFINITY),
|
|
160
|
+
};
|
|
161
|
+
const record = new Float64Array(16);
|
|
162
|
+
const preparing = new Progress("Preparing LOD bounds", initial);
|
|
163
|
+
for (let i = 0; i < initial; i++) {
|
|
164
|
+
splats.read(i, record);
|
|
165
|
+
for (let d = 0; d < 3; d++) {
|
|
166
|
+
bounds.min[d] = Math.min(bounds.min[d], record[d]);
|
|
167
|
+
bounds.max[d] = Math.max(bounds.max[d], record[d]);
|
|
168
|
+
}
|
|
169
|
+
splats.weight.set(i, record[15] * record[3]);
|
|
170
|
+
if ((i & 4095) === 0) preparing.update(i);
|
|
171
|
+
}
|
|
172
|
+
preparing.finish();
|
|
173
|
+
const removed = new PagedArray(workspace, Uint8Array);
|
|
174
|
+
const similarities = new Similarities(splats);
|
|
175
|
+
let active = new Heap(workspace);
|
|
176
|
+
let frontier = 0;
|
|
177
|
+
// The similarity terms and weight of a popped splat and of the splat it
|
|
178
|
+
// merges with.
|
|
179
|
+
const query = new Float64Array(14);
|
|
180
|
+
const candidate = new Float64Array(14);
|
|
181
|
+
const keys = new Uint32Array(ROUND);
|
|
182
|
+
// A worker's merged record and its terms.
|
|
183
|
+
const merge = new Float32Array(16);
|
|
184
|
+
const terms13 = new Float32Array(13);
|
|
185
|
+
// Cells that merges of the round have added entries to, and the blocks of
|
|
186
|
+
// 4 cells per axis next to them: queries in other blocks skip the check.
|
|
187
|
+
const added = new Cells(18);
|
|
188
|
+
const near = new Cells(20);
|
|
189
|
+
for (
|
|
190
|
+
let level = Math.ceil(Math.log2(Math.max(splats.feature(0), 0.000001)));
|
|
191
|
+
;
|
|
192
|
+
level++
|
|
193
|
+
) {
|
|
194
|
+
const step = 2 ** level;
|
|
195
|
+
if (!Number.isFinite(step)) throw new Error("LOD scale overflow");
|
|
196
|
+
// The leaves are sorted by feature, so find this level's range without
|
|
197
|
+
// an extra linear pass just to count its work.
|
|
198
|
+
let low = frontier;
|
|
199
|
+
let high = initial;
|
|
200
|
+
while (low < high) {
|
|
201
|
+
const middle = Math.floor((low + high) / 2);
|
|
202
|
+
if (splats.feature(middle) <= step) low = middle + 1;
|
|
203
|
+
else high = middle;
|
|
204
|
+
}
|
|
205
|
+
const { grid, queue } = Progress.inline(() => {
|
|
206
|
+
const adding = new Progress(`Preparing level ${level}`, low - frontier);
|
|
207
|
+
const previous = frontier;
|
|
208
|
+
while (frontier < low) {
|
|
209
|
+
active.push(frontier, -splats.feature(frontier));
|
|
210
|
+
frontier++;
|
|
211
|
+
if ((frontier & 4095) === 0) adding.update(frontier - previous);
|
|
212
|
+
}
|
|
213
|
+
adding.finish();
|
|
214
|
+
const grid = new Grid(similarities, step, bounds, pool);
|
|
215
|
+
grid.build(active, removed, `Level ${level}`);
|
|
216
|
+
const queue = new Queue(
|
|
217
|
+
active,
|
|
218
|
+
workspace,
|
|
219
|
+
`Sorting level ${level} queue`,
|
|
220
|
+
);
|
|
221
|
+
return { grid, queue };
|
|
222
|
+
});
|
|
223
|
+
active.dispose();
|
|
224
|
+
const next = new Heap(workspace);
|
|
225
|
+
const detail = () =>
|
|
226
|
+
`${frontier}/${initial} leaves, ${queue.length} pending, ${splats.length} splats`;
|
|
227
|
+
const progress = new Progress(`Level ${level}`, queue.length, detail());
|
|
228
|
+
// Rounds of merges in heap order. Workers search for the candidates of
|
|
229
|
+
// the next popped splats against the grid at the start of the round.
|
|
230
|
+
// Committing them in order, a search still holds unless its candidate is
|
|
231
|
+
// gone or a merge of the round added a candidate at least as good, and
|
|
232
|
+
// splats that merges add pop as they would one by one.
|
|
233
|
+
while (queue.length) {
|
|
234
|
+
grid.tidy();
|
|
235
|
+
const limit = Math.min(
|
|
236
|
+
ROUND,
|
|
237
|
+
Math.max(256, Math.floor(grid.count / 100)),
|
|
238
|
+
);
|
|
239
|
+
let count = 0;
|
|
240
|
+
while (count < limit && queue.length) {
|
|
241
|
+
const index = queue.pop();
|
|
242
|
+
if (removed.get(index)) continue;
|
|
243
|
+
pool.nodes[count] = index;
|
|
244
|
+
pool.entries[count] = grid.entry(index);
|
|
245
|
+
keys[count] = queue.key;
|
|
246
|
+
count++;
|
|
247
|
+
}
|
|
248
|
+
// Entries from here on are added by the round; slots stay put while
|
|
249
|
+
// the layout does.
|
|
250
|
+
const start = grid.length;
|
|
251
|
+
const layout = grid.layout;
|
|
252
|
+
if (count < 64) pool.results.fill(MISS, 0, count);
|
|
253
|
+
else pool.search(grid, count);
|
|
254
|
+
added.clear();
|
|
255
|
+
near.clear();
|
|
256
|
+
for (let t = 0; t < count; ) {
|
|
257
|
+
let index;
|
|
258
|
+
let self;
|
|
259
|
+
let key;
|
|
260
|
+
let best = MISS;
|
|
261
|
+
let task = -1;
|
|
262
|
+
if (queue.length && queue.before(keys[t], pool.nodes[t])) {
|
|
263
|
+
index = queue.pop();
|
|
264
|
+
key = queue.key;
|
|
265
|
+
if (removed.get(index)) continue;
|
|
266
|
+
self = grid.entry(index);
|
|
267
|
+
} else {
|
|
268
|
+
index = pool.nodes[t];
|
|
269
|
+
self = pool.entries[t];
|
|
270
|
+
key = keys[t];
|
|
271
|
+
best = pool.results[t];
|
|
272
|
+
task = t++;
|
|
273
|
+
if (removed.get(index)) continue;
|
|
274
|
+
}
|
|
275
|
+
// A search that the main thread need not do passes on the terms.
|
|
276
|
+
if (best === MISS) grid.read(self, query);
|
|
277
|
+
else
|
|
278
|
+
for (let d = 0; d < 14; d++) query[d] = pool.queries[task * 14 + d];
|
|
279
|
+
if (best !== MISS && best !== NONE && grid.isRemoved(best)) best = MISS;
|
|
280
|
+
const inverse = grid.inverse;
|
|
281
|
+
if (
|
|
282
|
+
best !== MISS &&
|
|
283
|
+
near.has(
|
|
284
|
+
cellHash(
|
|
285
|
+
Math.floor(Math.floor(query[0] * inverse) / 4),
|
|
286
|
+
Math.floor(Math.floor(query[1] * inverse) / 4),
|
|
287
|
+
Math.floor(Math.floor(query[2] * inverse) / 4),
|
|
288
|
+
),
|
|
289
|
+
)
|
|
290
|
+
) {
|
|
291
|
+
const better = grid.find(query, index, start, added);
|
|
292
|
+
if (better !== NONE) {
|
|
293
|
+
const metric = pool.metrics[task];
|
|
294
|
+
if (best === NONE || grid.metric > metric) {
|
|
295
|
+
best = better;
|
|
296
|
+
task = -1;
|
|
297
|
+
} else if (grid.metric === metric) best = MISS;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (best === MISS) {
|
|
301
|
+
best = grid.find(query, index);
|
|
302
|
+
task = -1;
|
|
303
|
+
}
|
|
304
|
+
if (best === NONE) {
|
|
305
|
+
// Its key in the queue is its feature's.
|
|
306
|
+
next.pushKey(index, key);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const other =
|
|
310
|
+
task < 0 ? grid.read(best, candidate) : pool.partners[task];
|
|
311
|
+
// A search that still holds was merged with the same inputs.
|
|
312
|
+
const merged =
|
|
313
|
+
task < 0
|
|
314
|
+
? splats.merge(index, other, query, candidate)
|
|
315
|
+
: splats.append(
|
|
316
|
+
index,
|
|
317
|
+
other,
|
|
318
|
+
copy(pool.records, task * 16, merge, 16),
|
|
319
|
+
pool.blends[task * 2],
|
|
320
|
+
pool.blends[task * 2 + 1],
|
|
321
|
+
);
|
|
322
|
+
removed.set(index, 1);
|
|
323
|
+
removed.set(other, 1);
|
|
324
|
+
grid.remove(self);
|
|
325
|
+
grid.remove(best);
|
|
326
|
+
const feature = splats.feature(merged);
|
|
327
|
+
if (feature > step) next.push(merged, -feature);
|
|
328
|
+
else {
|
|
329
|
+
const terms =
|
|
330
|
+
task < 0 ? undefined : copy(pool.terms, task * 13, terms13, 13);
|
|
331
|
+
// The slot of its cell where the search found it.
|
|
332
|
+
const slot =
|
|
333
|
+
task < 0
|
|
334
|
+
? hint(grid, splats.merged, query, candidate)
|
|
335
|
+
: layout === grid.layout
|
|
336
|
+
? pool.hints[task]
|
|
337
|
+
: NONE;
|
|
338
|
+
added.add(
|
|
339
|
+
grid.insert(merged, splats.weight.get(merged), terms, slot),
|
|
340
|
+
);
|
|
341
|
+
const { lastX: x, lastY: y, lastZ: z } = grid;
|
|
342
|
+
const x1 = Math.floor((x + 1) / 4);
|
|
343
|
+
const y1 = Math.floor((y + 1) / 4);
|
|
344
|
+
const z1 = Math.floor((z + 1) / 4);
|
|
345
|
+
for (let bz = Math.floor((z - 1) / 4); bz <= z1; bz++)
|
|
346
|
+
for (let by = Math.floor((y - 1) / 4); by <= y1; by++)
|
|
347
|
+
for (let bx = Math.floor((x - 1) / 4); bx <= x1; bx++)
|
|
348
|
+
near.add(cellHash(bx, by, bz));
|
|
349
|
+
queue.push(merged, -feature);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// At round boundaries the queue only shrinks: each new merged item
|
|
353
|
+
// replaces at least one popped item. Include those items until drained.
|
|
354
|
+
progress.update(progress.total - queue.length, detail());
|
|
355
|
+
}
|
|
356
|
+
grid.dispose();
|
|
357
|
+
queue.dispose();
|
|
358
|
+
progress.finish(detail());
|
|
359
|
+
active = next;
|
|
360
|
+
if (frontier === initial && active.length <= 1) break;
|
|
361
|
+
}
|
|
362
|
+
active.dispose();
|
|
363
|
+
removed.dispose();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// A copy of the first `count` values of `array` with room for `size`.
|
|
367
|
+
function grow(array, count, size = count + 1) {
|
|
368
|
+
const larger = new array.constructor(size * 2);
|
|
369
|
+
larger.set(array.subarray(0, count));
|
|
370
|
+
return larger;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Sizes in words of an IndexArray.
|
|
374
|
+
const float = new Float32Array(1);
|
|
375
|
+
const word = new Uint32Array(float.buffer);
|
|
376
|
+
function bitsOf(value) {
|
|
377
|
+
float[0] = value;
|
|
378
|
+
return word[0];
|
|
379
|
+
}
|
|
380
|
+
function floatOf(bits) {
|
|
381
|
+
word[0] = bits;
|
|
382
|
+
return float[0];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Copies `count` values of `source` from `from` into `out`, and returns it.
|
|
386
|
+
function copy(source, from, out, count) {
|
|
387
|
+
for (let i = 0; i < count; i++) out[i] = source[from + i];
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Sorts the first `count` values ascending.
|
|
392
|
+
function sort(values, count) {
|
|
393
|
+
if (count > 32) {
|
|
394
|
+
values.subarray(0, count).sort();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
for (let i = 1; i < count; i++) {
|
|
398
|
+
const value = values[i];
|
|
399
|
+
let j = i;
|
|
400
|
+
for (; j > 0 && values[j - 1] > value; j--) values[j] = values[j - 1];
|
|
401
|
+
values[j] = value;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Lays out the tree in chunks of 65536 splats and moves every column into
|
|
406
|
+
// that order. Children become consecutive rows: childStart is then the row of
|
|
407
|
+
// a splat's first child.
|
|
408
|
+
function chunkTree(splats, order, first, counts, pool) {
|
|
409
|
+
const workspace = splats.workspace;
|
|
410
|
+
const nodes = splats.length;
|
|
411
|
+
for (const column of [splats.childStart, splats.childCount, splats.children])
|
|
412
|
+
column.dispose();
|
|
413
|
+
// Chunking reads records at random by logical index, so it reads them in
|
|
414
|
+
// logical order, where splats of a subtree lie close together.
|
|
415
|
+
const logical = new Permutation(
|
|
416
|
+
workspace,
|
|
417
|
+
order,
|
|
418
|
+
nodes,
|
|
419
|
+
"Preparing LOD order",
|
|
420
|
+
);
|
|
421
|
+
splats.records = logical.apply(
|
|
422
|
+
splats.records,
|
|
423
|
+
16,
|
|
424
|
+
pool,
|
|
425
|
+
false,
|
|
426
|
+
"Reordering LOD nodes",
|
|
427
|
+
);
|
|
428
|
+
logical.dispose();
|
|
429
|
+
const sizes = new PagedArray(workspace, Float32Array);
|
|
430
|
+
const sizing = new Progress("Reading chunk sizes", order.length);
|
|
431
|
+
for (let index = 0; index < order.length; index++) {
|
|
432
|
+
sizes.set(index, splats.feature(index));
|
|
433
|
+
if ((index & 4095) === 0) sizing.update(index);
|
|
434
|
+
}
|
|
435
|
+
sizing.finish();
|
|
436
|
+
// The row of each splat's first child.
|
|
437
|
+
const starts = new PagedArray(workspace, Float64Array);
|
|
438
|
+
const indices = new IndexArray(workspace);
|
|
439
|
+
indices.push(0);
|
|
440
|
+
const top = new IndexArray(workspace);
|
|
441
|
+
top.push(0);
|
|
442
|
+
const batches = [top];
|
|
443
|
+
const record = new Float64Array(16);
|
|
444
|
+
const r = new Float64Array(9);
|
|
445
|
+
// Splats without children add no rows when they pop, so they wait outside
|
|
446
|
+
// the heap with the number of pops before them: one pops unless all later
|
|
447
|
+
// pops rank above it.
|
|
448
|
+
let leaves = new Uint32Array(1024);
|
|
449
|
+
let leafSizes = new Float32Array(1024);
|
|
450
|
+
let since = new Uint32Array(1024);
|
|
451
|
+
let waiting = 0;
|
|
452
|
+
// The splats that pop from the heap, and their sizes.
|
|
453
|
+
let popped = new Uint32Array(1024);
|
|
454
|
+
let poppedSizes = new Float32Array(1024);
|
|
455
|
+
let pops = 0;
|
|
456
|
+
const add = (heap, index) => {
|
|
457
|
+
const size = sizes.get(index);
|
|
458
|
+
if (counts.get(index)) heap.push(index, size);
|
|
459
|
+
else {
|
|
460
|
+
if (waiting === leaves.length) {
|
|
461
|
+
leaves = grow(leaves, waiting);
|
|
462
|
+
leafSizes = grow(leafSizes, waiting);
|
|
463
|
+
since = grow(since, waiting);
|
|
464
|
+
}
|
|
465
|
+
leaves[waiting] = index;
|
|
466
|
+
leafSizes[waiting] = size;
|
|
467
|
+
since[waiting++] = pops;
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
const chunking = new Progress("Arranging LOD chunks", order.length);
|
|
471
|
+
for (let b = 0; b < batches.length; b++) {
|
|
472
|
+
const batch = batches[b];
|
|
473
|
+
batches[b] = undefined;
|
|
474
|
+
const heap = new Heap(workspace, true);
|
|
475
|
+
waiting = 0;
|
|
476
|
+
pops = 0;
|
|
477
|
+
for (let i = 0; i < batch.length; i++) add(heap, batch.get(i));
|
|
478
|
+
batch.dispose();
|
|
479
|
+
const start = indices.length;
|
|
480
|
+
let end = Math.ceil((start + 8192) / 65536) * 65536;
|
|
481
|
+
// The splats without children that stay: the last `stayed` of `leaves`,
|
|
482
|
+
// from `stay` on.
|
|
483
|
+
let stay = 0;
|
|
484
|
+
let stayed = 0;
|
|
485
|
+
while (heap.length) {
|
|
486
|
+
const parent = heap.pop();
|
|
487
|
+
const count = counts.get(parent);
|
|
488
|
+
if (pops === popped.length) {
|
|
489
|
+
popped = grow(popped, pops);
|
|
490
|
+
poppedSizes = grow(poppedSizes, pops);
|
|
491
|
+
}
|
|
492
|
+
popped[pops] = parent;
|
|
493
|
+
poppedSizes[pops++] = sizes.get(parent);
|
|
494
|
+
if (indices.length === start)
|
|
495
|
+
end = Math.max(end, Math.ceil((start + count) / 65536) * 65536);
|
|
496
|
+
if (indices.length + count > end) {
|
|
497
|
+
heap.push(parent, sizes.get(parent));
|
|
498
|
+
// A waiting splat stays if it ranks below the lowest of the pops
|
|
499
|
+
// after it. From the last to wait, which follows the fewest pops.
|
|
500
|
+
let k = pops;
|
|
501
|
+
let low = Number.POSITIVE_INFINITY;
|
|
502
|
+
let lowIndex = 0;
|
|
503
|
+
stay = waiting;
|
|
504
|
+
for (let i = waiting - 1; i >= 0; i--) {
|
|
505
|
+
for (; k > since[i]; k--) {
|
|
506
|
+
const size = poppedSizes[k - 1];
|
|
507
|
+
if (size < low || (size === low && popped[k - 1] < lowIndex)) {
|
|
508
|
+
low = size;
|
|
509
|
+
lowIndex = popped[k - 1];
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (
|
|
513
|
+
leafSizes[i] < low ||
|
|
514
|
+
(leafSizes[i] === low && leaves[i] < lowIndex)
|
|
515
|
+
)
|
|
516
|
+
leaves[--stay] = leaves[i];
|
|
517
|
+
}
|
|
518
|
+
stayed = waiting - stay;
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
starts.set(parent, indices.length);
|
|
522
|
+
const firstChild = first.get(parent);
|
|
523
|
+
for (let child = firstChild; child < firstChild + count; child++) {
|
|
524
|
+
indices.push(child);
|
|
525
|
+
add(heap, child);
|
|
526
|
+
}
|
|
527
|
+
if ((pops & 4095) === 0) chunking.update(indices.length);
|
|
528
|
+
}
|
|
529
|
+
// What stays: the heap, then the splats without children.
|
|
530
|
+
const rest = heap.length + stayed;
|
|
531
|
+
const item = (i) =>
|
|
532
|
+
i < heap.length ? heap.index(i) : leaves[stay + i - heap.length];
|
|
533
|
+
if (rest) {
|
|
534
|
+
const min = [
|
|
535
|
+
Number.POSITIVE_INFINITY,
|
|
536
|
+
Number.POSITIVE_INFINITY,
|
|
537
|
+
Number.POSITIVE_INFINITY,
|
|
538
|
+
];
|
|
539
|
+
const max = [
|
|
540
|
+
Number.NEGATIVE_INFINITY,
|
|
541
|
+
Number.NEGATIVE_INFINITY,
|
|
542
|
+
Number.NEGATIVE_INFINITY,
|
|
543
|
+
];
|
|
544
|
+
// The centers decide the groups below; keep them for that.
|
|
545
|
+
const centers = new PagedArray(workspace, Float32Array);
|
|
546
|
+
for (let i = 0; i < rest; i++) {
|
|
547
|
+
if ((i & 4095) === 0) chunking.update(indices.length);
|
|
548
|
+
splats.read(item(i), record);
|
|
549
|
+
centers.write(i * 3, record, 3);
|
|
550
|
+
rotation(record, r);
|
|
551
|
+
const sx = Math.max(0.001, SCALE[halfBits(record[7])]) * 1.5;
|
|
552
|
+
const sy = Math.max(0.001, SCALE[halfBits(record[8])]) * 1.5;
|
|
553
|
+
const sz = Math.max(0.001, SCALE[halfBits(record[9])]) * 1.5;
|
|
554
|
+
for (let d = 0; d < 3; d++) {
|
|
555
|
+
const half =
|
|
556
|
+
Math.abs(r[d]) * sx +
|
|
557
|
+
Math.abs(r[3 + d]) * sy +
|
|
558
|
+
Math.abs(r[6 + d]) * sz;
|
|
559
|
+
min[d] = Math.min(min[d], record[d] - half);
|
|
560
|
+
max[d] = Math.max(max[d], record[d] + half);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const extents = min.map((value, d) => max[d] - value);
|
|
564
|
+
const split = min.map((value, d) => (value + max[d]) * 0.5);
|
|
565
|
+
const axis =
|
|
566
|
+
extents[0] >= extents[1] && extents[0] >= extents[2]
|
|
567
|
+
? 0
|
|
568
|
+
: extents[1] >= extents[2]
|
|
569
|
+
? 1
|
|
570
|
+
: 2;
|
|
571
|
+
const binary = extents[axis] >= 3 * Math.min(...extents);
|
|
572
|
+
const groups = Array.from(
|
|
573
|
+
{ length: binary ? 2 : 8 },
|
|
574
|
+
() => new IndexArray(workspace),
|
|
575
|
+
);
|
|
576
|
+
for (let i = 0; i < rest; i++) {
|
|
577
|
+
if ((i & 4095) === 0) chunking.update(indices.length);
|
|
578
|
+
const center = centers.read(i * 3, record, 3);
|
|
579
|
+
const group = binary
|
|
580
|
+
? Number(center[axis] >= split[axis])
|
|
581
|
+
: Number(center[0] >= split[0]) +
|
|
582
|
+
2 * Number(center[1] >= split[1]) +
|
|
583
|
+
4 * Number(center[2] >= split[2]);
|
|
584
|
+
groups[group].push(item(i));
|
|
585
|
+
}
|
|
586
|
+
centers.dispose();
|
|
587
|
+
const sequence = binary
|
|
588
|
+
? groups[0].length >= groups[1].length
|
|
589
|
+
? [0, 1]
|
|
590
|
+
: [1, 0]
|
|
591
|
+
: [0, 1, 3, 2, 6, 7, 5, 4];
|
|
592
|
+
for (const index of sequence) {
|
|
593
|
+
if (groups[index].length) batches.push(groups[index]);
|
|
594
|
+
else groups[index].dispose();
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
heap.dispose();
|
|
598
|
+
chunking.update(indices.length);
|
|
599
|
+
}
|
|
600
|
+
sizes.dispose();
|
|
601
|
+
if (indices.length !== order.length)
|
|
602
|
+
throw new Error("Incomplete LOD tree ordering");
|
|
603
|
+
chunking.finish();
|
|
604
|
+
const chunked = new Permutation(
|
|
605
|
+
workspace,
|
|
606
|
+
indices,
|
|
607
|
+
order.length,
|
|
608
|
+
"Preparing chunk order",
|
|
609
|
+
);
|
|
610
|
+
splats.records = chunked.apply(
|
|
611
|
+
splats.records,
|
|
612
|
+
16,
|
|
613
|
+
pool,
|
|
614
|
+
false,
|
|
615
|
+
"Reordering chunk splats",
|
|
616
|
+
);
|
|
617
|
+
splats.childCount = chunked.apply(
|
|
618
|
+
counts,
|
|
619
|
+
1,
|
|
620
|
+
pool,
|
|
621
|
+
false,
|
|
622
|
+
"Reordering child counts",
|
|
623
|
+
);
|
|
624
|
+
splats.childStart = chunked.apply(
|
|
625
|
+
starts,
|
|
626
|
+
1,
|
|
627
|
+
pool,
|
|
628
|
+
false,
|
|
629
|
+
"Reordering child offsets",
|
|
630
|
+
);
|
|
631
|
+
chunked.dispose();
|
|
632
|
+
// The SH coefficients are still in node order.
|
|
633
|
+
const shOrder = new Progress("Collecting LOD SH order", indices.length);
|
|
634
|
+
for (let i = 0; i < indices.length; i++) {
|
|
635
|
+
indices.set(i, order.get(indices.get(i)));
|
|
636
|
+
if ((i & 4095) === 0) shOrder.update(i);
|
|
637
|
+
}
|
|
638
|
+
shOrder.finish();
|
|
639
|
+
const nodeOrder = new Permutation(
|
|
640
|
+
workspace,
|
|
641
|
+
indices,
|
|
642
|
+
nodes,
|
|
643
|
+
"Preparing LOD SH order",
|
|
644
|
+
);
|
|
645
|
+
for (let band = 0; band < splats.maxSh; band++)
|
|
646
|
+
splats.sh[band] = nodeOrder.apply(
|
|
647
|
+
splats.sh[band],
|
|
648
|
+
SH_DIMS[band],
|
|
649
|
+
pool,
|
|
650
|
+
false,
|
|
651
|
+
`Reordering LOD SH${band + 1}`,
|
|
652
|
+
);
|
|
653
|
+
nodeOrder.dispose();
|
|
654
|
+
first.dispose();
|
|
655
|
+
splats.length = indices.length;
|
|
656
|
+
indices.dispose();
|
|
657
|
+
}
|