gaussian-splat-lite 1.5.0 → 1.5.2
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 +29 -9
- package/bin/build-lod.js +14 -3
- package/bin/decoder/cluster.wasm +0 -0
- package/bin/decoder/decoder.js +5 -4
- package/bin/decoder/decoder_bg.wasm +0 -0
- package/bin/rad/cluster.js +47 -14
- package/bin/rad/heap.js +1 -2
- package/bin/rad/lod.js +48 -67
- package/bin/rad/rad.js +50 -28
- package/bin/rad/splats.js +129 -37
- package/bin/rad/storage.js +147 -34
- package/bin/rad/writeback.js +100 -0
- package/dist/gaussian-splat-lite.cjs +37 -3
- package/dist/gaussian-splat-lite.cjs.map +1 -1
- package/dist/gaussian-splat-lite.min.cjs +127 -93
- package/dist/gaussian-splat-lite.min.cjs.map +1 -1
- package/dist/gaussian-splat-lite.module.js +37 -3
- package/dist/gaussian-splat-lite.module.js.map +1 -1
- package/dist/gaussian-splat-lite.module.min.js +972 -938
- package/dist/gaussian-splat-lite.module.min.js.map +1 -1
- package/docs/BuildRad.md +2 -2
- package/docs/RadStreamScheduler.md +3 -3
- package/docs/SogStreamScheduler.md +1 -1
- package/docs/SplatLoader.md +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,28 +7,45 @@ and this project follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.5.2] - 2026-10-29
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- `gsl-build-lod` reduces temporary-file reads for models larger than `--memory-budget`.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Neural denoising no longer lets a still view turn grainy again when it keeps rendering for many seconds.
|
|
19
|
+
- `gsl-build-lod` limits pending disk writes within `--memory-budget` to prevent excessive memory use on slow disks.
|
|
20
|
+
|
|
21
|
+
## [1.5.1] - 2026-10-27
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- `gsl-build-lod` SH codebooks now match the original view-dependent color of large models more closely. Conversion takes slightly longer.
|
|
26
|
+
|
|
10
27
|
## [1.5.0] - 2026-10-27
|
|
11
28
|
|
|
12
29
|
### Added
|
|
13
30
|
|
|
14
|
-
- Added `sources` to `RadStreamScheduler` and `SogStreamScheduler` to stream several inputs with one detail selection. The inputs share the splat budget and loading concurrency; RAD inputs also share both memory budgets
|
|
31
|
+
- Added `sources` to `RadStreamScheduler` and `SogStreamScheduler` to stream several inputs with one detail selection. The inputs share the splat budget and loading concurrency; RAD inputs also share both memory budgets; inputs with different SH degrees all use the memory of the highest one, up to `maxSh`. See [RAD streaming](docs/RadStreamScheduler.md#multiple-inputs) and [SOG streaming](docs/SogStreamScheduler.md#multiple-inputs).
|
|
15
32
|
- Added an `environment` option to `SogStreamScheduler` and each of its `sources`. Set it to `false` to skip that input's environment.
|
|
16
|
-
- Added `maxSh` to `RadStreamScheduler` and `SogStreamScheduler` to limit SH decoding, storage, and rendering. Set it at creation to `0`–`3` (default `3`) to reduce
|
|
17
|
-
- Added a loading-time `maxSh` option to `SplatMesh` and `Splats`, and a `SplatLoader.maxSh` setting (`0`–`4`, default `4`).
|
|
18
|
-
- Added
|
|
33
|
+
- Added `maxSh` to `RadStreamScheduler` and `SogStreamScheduler` to limit SH decoding, storage, and rendering. Set it at creation to `0`–`3` (default `3`) to reduce memory use; `0` keeps only the base color, and streamed SOG then also skips downloading SH data.
|
|
34
|
+
- Added a loading-time `maxSh` option to `SplatMesh` and `Splats`, and a `SplatLoader.maxSh` setting (`0`–`4`, default `4`). Lower values reduce memory use.
|
|
35
|
+
- Added SH codebook compression to `gsl-build-lod`, on by default: splats share up to 65,536 SH entries. This can make large files much smaller at the cost of some view-dependent detail; small models may grow. Use `--cluster-sh=<n>` to change the default 10 refinement iterations or `--no-cluster-sh` to store SH without a codebook. Clustering stays within `--memory-budget`. See [SH codebook](docs/BuildRad.md#sh-codebook).
|
|
19
36
|
|
|
20
37
|
### Changed
|
|
21
38
|
|
|
22
39
|
- Raised the minimum Node.js version to 22.12 for the package and `gsl-build-lod`.
|
|
23
40
|
- `SplatMesh.maxSh` is now read-only and set through its constructor options, matching the streaming schedulers. Assigning `mesh.maxSh` now throws a `TypeError` in strict-mode code, including ES modules. Create a new mesh to change the limit. The viewer's Max SH control has been removed.
|
|
24
|
-
- RAD streaming
|
|
41
|
+
- RAD streaming no longer accepts files with chunks of more than 65,536 splats. Files from `gsl-build-lod` are unaffected.
|
|
25
42
|
- RAD and SOG schedulers now create their own `THREE.Group`; the `group` constructor option has been removed. Add and transform `streaming.group` instead.
|
|
26
|
-
- RAD and SOG
|
|
27
|
-
- `gsl-build-lod` now
|
|
43
|
+
- RAD and SOG streamed data now reaches the GPU only when you render. Keep rendering, not just calling `update()`, while waiting for `firstRenderable`.
|
|
44
|
+
- With `--no-cluster-sh`, `gsl-build-lod` now keeps the view-dependent color of SPZ inputs almost exactly.
|
|
28
45
|
|
|
29
46
|
### Fixed
|
|
30
47
|
|
|
31
|
-
- Fixed
|
|
48
|
+
- Fixed `gsl-build-lod` briefly using more memory than `--memory-budget` when starting worker threads.
|
|
32
49
|
|
|
33
50
|
## [1.4.3] - 2026-10-25
|
|
34
51
|
|
|
@@ -703,7 +720,10 @@ and this project follows [Semantic Versioning](https://semver.org/).
|
|
|
703
720
|
|
|
704
721
|
- Initial public release of the Three.js Gaussian Splatting renderer.
|
|
705
722
|
|
|
706
|
-
[Unreleased]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.
|
|
723
|
+
[Unreleased]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.5.2...HEAD
|
|
724
|
+
[1.5.2]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.5.1...v1.5.2
|
|
725
|
+
[1.5.1]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.5.0...v1.5.1
|
|
726
|
+
[1.5.0]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.4.3...v1.5.0
|
|
707
727
|
[1.4.3]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.4.2...v1.4.3
|
|
708
728
|
[1.4.2]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.4.1...v1.4.2
|
|
709
729
|
[1.4.1]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.4.0...v1.4.1
|
package/bin/build-lod.js
CHANGED
|
@@ -13,6 +13,7 @@ import { buildLod } from "./rad/lod.js";
|
|
|
13
13
|
import { Pool } from "./rad/parallel.js";
|
|
14
14
|
import { checkOutput, writeRad } from "./rad/rad.js";
|
|
15
15
|
import { GIB, Workspace } from "./rad/storage.js";
|
|
16
|
+
import { syncFiles } from "./rad/writeback.js";
|
|
16
17
|
|
|
17
18
|
const help = `Convert PLY or SPZ files to RAD with levels of detail (LOD).
|
|
18
19
|
|
|
@@ -37,7 +38,7 @@ Options:
|
|
|
37
38
|
-h, --help Show this help
|
|
38
39
|
|
|
39
40
|
SH4 inputs are accepted; coefficients above --max-sh are discarded.
|
|
40
|
-
The memory target covers the
|
|
41
|
+
The memory target covers the conversion, worker threads and pending disk writes included.
|
|
41
42
|
Requires Node.js 22.12 or later. No compilation is required.`;
|
|
42
43
|
|
|
43
44
|
function parse(args) {
|
|
@@ -139,7 +140,13 @@ async function convert(input, options, kernel, cancelled) {
|
|
|
139
140
|
`${path.basename(input, path.extname(input))}-lod.rad`,
|
|
140
141
|
);
|
|
141
142
|
checkOutput(output, options.chunked);
|
|
142
|
-
const workspace = new Workspace(
|
|
143
|
+
const workspace = new Workspace(
|
|
144
|
+
options.budget,
|
|
145
|
+
(files, state) =>
|
|
146
|
+
parentPort.postMessage({ method: "writeback", args: [files, state] }),
|
|
147
|
+
options.tempDir,
|
|
148
|
+
cancelled,
|
|
149
|
+
);
|
|
143
150
|
const start = performance.now();
|
|
144
151
|
console.log(
|
|
145
152
|
`${input} -> ${output}\nMemory target: ${(options.budget / GIB).toFixed(3)} GiB`,
|
|
@@ -222,6 +229,7 @@ async function run(options, cancelled) {
|
|
|
222
229
|
|
|
223
230
|
// Keep signal handling responsive while the conversion runs synchronous code.
|
|
224
231
|
// The worker observes cancellation and unwinds its existing catch/finally blocks.
|
|
232
|
+
// Cleanup finishes submitted writeback before closing files and exiting.
|
|
225
233
|
async function start(options) {
|
|
226
234
|
const cancelled = new Int32Array(new SharedArrayBuffer(4));
|
|
227
235
|
let progressLine = "";
|
|
@@ -230,6 +238,7 @@ async function start(options) {
|
|
|
230
238
|
progressLine = "";
|
|
231
239
|
};
|
|
232
240
|
const print = ({ method, args }) => {
|
|
241
|
+
if (method === "writeback") return syncFiles(...args, cancelled);
|
|
233
242
|
if (method === "progress") {
|
|
234
243
|
const [line, done] = args;
|
|
235
244
|
if (process.stdout.isTTY) {
|
|
@@ -249,7 +258,9 @@ async function start(options) {
|
|
|
249
258
|
const code = signal === "SIGINT" ? 130 : 143;
|
|
250
259
|
if (Atomics.compareExchange(cancelled, 0, 0, code) === 0) {
|
|
251
260
|
endProgress();
|
|
252
|
-
console.error(
|
|
261
|
+
console.error(
|
|
262
|
+
`${signal}: cancelling conversion; waiting for active disk I/O before cleanup...`,
|
|
263
|
+
);
|
|
253
264
|
}
|
|
254
265
|
};
|
|
255
266
|
process.on("SIGINT", interrupt);
|
package/bin/decoder/cluster.wasm
CHANGED
|
Binary file
|
package/bin/decoder/decoder.js
CHANGED
|
@@ -78,10 +78,6 @@ function __wbg_get_imports() {
|
|
|
78
78
|
const ret = new Error(getStringFromWasm0(arg0, arg1));
|
|
79
79
|
return ret;
|
|
80
80
|
},
|
|
81
|
-
__wbg_new_from_slice_d85ad974cf8f6f35: function(arg0, arg1) {
|
|
82
|
-
const ret = new Float32Array(getArrayF32FromWasm0(arg0, arg1));
|
|
83
|
-
return ret;
|
|
84
|
-
},
|
|
85
81
|
__wbg_stack_3b0d974bbf31e44f: function(arg0, arg1) {
|
|
86
82
|
const ret = arg1.stack;
|
|
87
83
|
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
@@ -95,6 +91,11 @@ function __wbg_get_imports() {
|
|
|
95
91
|
return ret;
|
|
96
92
|
},
|
|
97
93
|
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
|
|
94
|
+
// Cast intrinsic for `Ref(Slice(F32)) -> NamedExternref("Float32Array")`.
|
|
95
|
+
const ret = getArrayF32FromWasm0(arg0, arg1);
|
|
96
|
+
return ret;
|
|
97
|
+
},
|
|
98
|
+
__wbindgen_cast_0000000000000003: function(arg0, arg1) {
|
|
98
99
|
// Cast intrinsic for `Ref(String) -> Externref`.
|
|
99
100
|
const ret = getStringFromWasm0(arg0, arg1);
|
|
100
101
|
return ret;
|
|
Binary file
|
package/bin/rad/cluster.js
CHANGED
|
@@ -6,18 +6,21 @@ import { HALF } from "./math.js";
|
|
|
6
6
|
import { WorkerGroup, clusterers } from "./parallel.js";
|
|
7
7
|
import { Progress } from "./progress.js";
|
|
8
8
|
import { SH_DIMS } from "./splats.js";
|
|
9
|
-
import { PagedArray } from "./storage.js";
|
|
9
|
+
import { PAGE_BYTES, PagedArray } from "./storage.js";
|
|
10
10
|
|
|
11
11
|
// Codebook entries at most: RAD labels are 16-bit.
|
|
12
12
|
const MAX_CODES = 65536;
|
|
13
13
|
// Training reads SAMPLE_SHARE of the splats, and at least SAMPLES_PER_CODE
|
|
14
14
|
// per entry; larger models train on a sample by weight, and only the final
|
|
15
|
-
// assignment reads all. Entries start at evenly spaced training splats
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
// assignment reads all. Entries start at evenly spaced training splats.
|
|
16
|
+
// With codebooks shrunk to keep 16 per entry at 5%, renders of Elevator and
|
|
17
|
+
// JunkShop came 5–14% closer to the uncompressed SH at 10% than at 5%; on
|
|
18
|
+
// C-NorthSouth (111M splats after LOD) clustering took about 185 s, not 170.
|
|
19
|
+
// On Elevator (3.9M splats after LOD), 64 per entry, here all splats, came
|
|
20
|
+
// 5.8% closer than 16 at full detail, with 41% fewer pixels off by more than
|
|
21
|
+
// 32 levels in LOD views, in 18 s instead of 6 s.
|
|
22
|
+
const SAMPLES_PER_CODE = 64;
|
|
23
|
+
const SAMPLE_SHARE = 0.1;
|
|
21
24
|
// Splats per job of a thread, and per call of the kernel.
|
|
22
25
|
const JOB = 4096;
|
|
23
26
|
const BATCH = 1024;
|
|
@@ -320,6 +323,13 @@ class Assigner extends WorkerGroup {
|
|
|
320
323
|
}
|
|
321
324
|
}
|
|
322
325
|
|
|
326
|
+
// Rows that fit in the cache, allowing a partial page per column. Repeated
|
|
327
|
+
// scans cache this prefix and bypass the rest to avoid evicting it.
|
|
328
|
+
function fitting(bytes, row, columns) {
|
|
329
|
+
const pages = Math.floor(bytes / PAGE_BYTES) - columns;
|
|
330
|
+
return Math.max(0, Math.floor((pages * PAGE_BYTES) / row));
|
|
331
|
+
}
|
|
332
|
+
|
|
323
333
|
// Reads splats' SH as rows of half bits, the bands one after another, with
|
|
324
334
|
// their weights, area × opacity as merging weighs them.
|
|
325
335
|
class Rows {
|
|
@@ -327,16 +337,21 @@ class Rows {
|
|
|
327
337
|
this.splats = splats;
|
|
328
338
|
this.dims = dims;
|
|
329
339
|
this.length = splats.length;
|
|
340
|
+
this.kept = Number.POSITIVE_INFINITY;
|
|
330
341
|
this.column = new Uint16Array(JOB * SH_DIMS[SH_DIMS.length - 1]);
|
|
331
342
|
this.records = new Float32Array(JOB * 16);
|
|
332
343
|
}
|
|
333
344
|
|
|
345
|
+
keep(bytes) {
|
|
346
|
+
this.kept = fitting(bytes, this.dims * 2 + 64, this.splats.maxSh + 1);
|
|
347
|
+
}
|
|
348
|
+
|
|
334
349
|
read(base, count, bits, weights) {
|
|
335
|
-
const { splats, dims, column } = this;
|
|
350
|
+
const { splats, dims, column, kept } = this;
|
|
336
351
|
let offset = 0;
|
|
337
352
|
for (let band = 0; band < splats.maxSh; band++) {
|
|
338
353
|
const size = SH_DIMS[band];
|
|
339
|
-
splats.sh[band].
|
|
354
|
+
splats.sh[band].scan(base * size, column, count * size, kept * size);
|
|
340
355
|
for (let i = 0; i < count; i++)
|
|
341
356
|
for (let d = 0; d < size; d++)
|
|
342
357
|
bits[i * dims + offset + d] = column[i * size + d];
|
|
@@ -346,10 +361,11 @@ class Rows {
|
|
|
346
361
|
}
|
|
347
362
|
|
|
348
363
|
weigh(base, count, weights) {
|
|
349
|
-
const records = this.splats.records.
|
|
364
|
+
const records = this.splats.records.scan(
|
|
350
365
|
base * 16,
|
|
351
366
|
this.records,
|
|
352
367
|
count * 16,
|
|
368
|
+
this.kept * 16,
|
|
353
369
|
);
|
|
354
370
|
for (let i = 0; i < count; i++)
|
|
355
371
|
weights[i] = records[i * 16 + 15] * records[i * 16 + 3];
|
|
@@ -361,13 +377,19 @@ class Sample {
|
|
|
361
377
|
constructor(workspace, dims) {
|
|
362
378
|
this.dims = dims;
|
|
363
379
|
this.length = 0;
|
|
380
|
+
this.kept = Number.POSITIVE_INFINITY;
|
|
364
381
|
this.bits = new PagedArray(workspace, Uint16Array);
|
|
365
382
|
this.weights = new PagedArray(workspace, Float64Array);
|
|
366
383
|
}
|
|
367
384
|
|
|
385
|
+
keep(bytes) {
|
|
386
|
+
this.kept = fitting(bytes, this.dims * 2 + 8, 2);
|
|
387
|
+
}
|
|
388
|
+
|
|
368
389
|
read(base, count, bits, weights) {
|
|
369
|
-
|
|
370
|
-
this.
|
|
390
|
+
const { dims, kept } = this;
|
|
391
|
+
this.bits.scan(base * dims, bits, count * dims, kept * dims);
|
|
392
|
+
this.weights.scan(base, weights, count, kept);
|
|
371
393
|
}
|
|
372
394
|
|
|
373
395
|
dispose() {
|
|
@@ -587,8 +609,8 @@ class Grouper {
|
|
|
587
609
|
// weighted k-means as Spark does: entries start at evenly spaced splats,
|
|
588
610
|
// each of `iterations` moves them to the weighted mean of their splats, an
|
|
589
611
|
// entry without any stays, and a final pass labels each splat with its
|
|
590
|
-
// nearest entry. Weights are area × opacity. Training uses
|
|
591
|
-
// splats, at least
|
|
612
|
+
// nearest entry. Weights are area × opacity. Training uses 10% of the
|
|
613
|
+
// splats, at least 64 per entry, sampled in proportion to their weights;
|
|
592
614
|
// with many entries, passes search only groups of them. Returns the
|
|
593
615
|
// entries, `count` rows of `dims` SH coefficients, and the labels.
|
|
594
616
|
export async function clusterSh(splats, iterations, module) {
|
|
@@ -650,11 +672,21 @@ export async function clusterSh(splats, iterations, module) {
|
|
|
650
672
|
extra,
|
|
651
673
|
Math.ceil(training.length / size),
|
|
652
674
|
);
|
|
675
|
+
// Measure worker growth between passes: bypass reads do not allocate
|
|
676
|
+
// pages or trigger measurements. Leave room for labels when the final
|
|
677
|
+
// pass shares the training cache.
|
|
678
|
+
const labelPages = sampled ? 0 : Math.ceil((length * 2) / PAGE_BYTES);
|
|
679
|
+
const keep = () => {
|
|
680
|
+
workspace.adjust();
|
|
681
|
+
training.keep(workspace.capacity - labelPages * PAGE_BYTES);
|
|
682
|
+
};
|
|
683
|
+
keep();
|
|
653
684
|
const codes = new Float64Array(count * dims);
|
|
654
685
|
const sums = new Float64Array(count * dims);
|
|
655
686
|
const totals = new Float64Array(count);
|
|
656
687
|
const bits = new Uint16Array(dims);
|
|
657
688
|
const weight = new Float64Array(1);
|
|
689
|
+
// Read seeds past the cached prefix directly, preserving training rows.
|
|
658
690
|
for (let c = 0; c < count; c++) {
|
|
659
691
|
// Spark computes the seed rows in single precision.
|
|
660
692
|
const row = Math.floor(
|
|
@@ -702,6 +734,7 @@ export async function clusterSh(splats, iterations, module) {
|
|
|
702
734
|
layout(codes, order, dims, center, blocked, norms);
|
|
703
735
|
await assigner.use("codes", { codes: blocked, norms, center });
|
|
704
736
|
}
|
|
737
|
+
keep();
|
|
705
738
|
if (!last) {
|
|
706
739
|
sums.fill(0);
|
|
707
740
|
totals.fill(0);
|
package/bin/rad/heap.js
CHANGED
|
@@ -247,7 +247,7 @@ export function sortSplats(splats, pool) {
|
|
|
247
247
|
// rank alone keeps splats of equal size in index order.
|
|
248
248
|
const items = new IndexArray(workspace);
|
|
249
249
|
for (let i = 0; i < count; i++) {
|
|
250
|
-
const key = rank(-splats.
|
|
250
|
+
const key = rank(-splats.sizes.get(i));
|
|
251
251
|
const words = items.page(i * 2, true);
|
|
252
252
|
const offset = (i * 2) & items.mask;
|
|
253
253
|
words[offset] = key;
|
|
@@ -266,5 +266,4 @@ export function sortSplats(splats, pool) {
|
|
|
266
266
|
sorted.dispose();
|
|
267
267
|
ordering.finish();
|
|
268
268
|
splats.reorder(order, pool);
|
|
269
|
-
order.dispose();
|
|
270
269
|
}
|
package/bin/rad/lod.js
CHANGED
|
@@ -49,21 +49,17 @@ export function buildLod(splats, lodBase, pool) {
|
|
|
49
49
|
splats.weight.dispose();
|
|
50
50
|
selection.finish(`${retainedCount} nodes retained`);
|
|
51
51
|
|
|
52
|
-
// Ordering reads sizes at random
|
|
53
|
-
//
|
|
54
|
-
const sizes =
|
|
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();
|
|
52
|
+
// Ordering reads sizes at random, from a column without the rest of each
|
|
53
|
+
// record, and lists them in logical order for chunkTree().
|
|
54
|
+
const sizes = splats.sizes;
|
|
61
55
|
// A splat's logical index is its position in `order`. Its children get
|
|
62
56
|
// consecutive logical indices: `first` and the `counts` - 1 after it.
|
|
63
57
|
const order = new IndexArray(workspace);
|
|
64
58
|
const first = new IndexArray(workspace);
|
|
65
59
|
const counts = new IndexArray(workspace);
|
|
60
|
+
const ordered = new PagedArray(workspace, Float32Array);
|
|
66
61
|
order.push(root);
|
|
62
|
+
ordered.push(sizes.get(root));
|
|
67
63
|
// The splats to visit, by logical index, splat and size: the frontier of a
|
|
68
64
|
// pass as triples of words, and a stack within it.
|
|
69
65
|
let frontier = new IndexArray(workspace);
|
|
@@ -120,17 +116,22 @@ export function buildLod(splats, lodBase, pool) {
|
|
|
120
116
|
const start = order.length;
|
|
121
117
|
first.set(logical, start);
|
|
122
118
|
counts.set(logical, count);
|
|
123
|
-
for (let c = 0; c < count; c++) order.push(list[c]);
|
|
124
119
|
if (top + count > logicals.length) {
|
|
125
120
|
logicals = grow(logicals, top, top + count);
|
|
126
121
|
indices = grow(indices, top, top + count);
|
|
127
122
|
lengths = grow(lengths, top, top + count);
|
|
128
123
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
124
|
+
// Each child's size is read once, at random, for both uses.
|
|
125
|
+
for (let c = 0; c < count; c++) {
|
|
126
|
+
const length = sizes.get(list[c]);
|
|
127
|
+
order.push(list[c]);
|
|
128
|
+
ordered.push(length);
|
|
129
|
+
const at = top + count - 1 - c;
|
|
130
|
+
logicals[at] = start + c;
|
|
131
|
+
indices[at] = list[c];
|
|
132
|
+
lengths[at] = length;
|
|
133
133
|
}
|
|
134
|
+
top += count;
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
137
|
frontier.dispose();
|
|
@@ -143,7 +144,7 @@ export function buildLod(splats, lodBase, pool) {
|
|
|
143
144
|
sizes.dispose();
|
|
144
145
|
kept.dispose();
|
|
145
146
|
ordering.finish();
|
|
146
|
-
chunkTree(splats, order, first, counts, pool);
|
|
147
|
+
chunkTree(splats, order, first, counts, ordered, pool);
|
|
147
148
|
order.dispose();
|
|
148
149
|
}
|
|
149
150
|
|
|
@@ -152,24 +153,8 @@ export function buildLod(splats, lodBase, pool) {
|
|
|
152
153
|
function mergeLevels(splats, pool) {
|
|
153
154
|
const initial = splats.length;
|
|
154
155
|
const workspace = splats.workspace;
|
|
155
|
-
// Merged splats lie within the leaves' bounds.
|
|
156
|
-
|
|
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();
|
|
156
|
+
// Merged splats lie within the leaves' bounds.
|
|
157
|
+
const { bounds, sizes } = splats;
|
|
173
158
|
const removed = new PagedArray(workspace, Uint8Array);
|
|
174
159
|
const similarities = new Similarities(splats);
|
|
175
160
|
let active = new Heap(workspace);
|
|
@@ -187,7 +172,7 @@ function mergeLevels(splats, pool) {
|
|
|
187
172
|
const added = new Cells(18);
|
|
188
173
|
const near = new Cells(20);
|
|
189
174
|
for (
|
|
190
|
-
let level = Math.ceil(Math.log2(Math.max(
|
|
175
|
+
let level = Math.ceil(Math.log2(Math.max(sizes.get(0), 0.000001)));
|
|
191
176
|
;
|
|
192
177
|
level++
|
|
193
178
|
) {
|
|
@@ -199,14 +184,14 @@ function mergeLevels(splats, pool) {
|
|
|
199
184
|
let high = initial;
|
|
200
185
|
while (low < high) {
|
|
201
186
|
const middle = Math.floor((low + high) / 2);
|
|
202
|
-
if (
|
|
187
|
+
if (sizes.get(middle) <= step) low = middle + 1;
|
|
203
188
|
else high = middle;
|
|
204
189
|
}
|
|
205
190
|
const { grid, queue } = Progress.inline(() => {
|
|
206
191
|
const adding = new Progress(`Preparing level ${level}`, low - frontier);
|
|
207
192
|
const previous = frontier;
|
|
208
193
|
while (frontier < low) {
|
|
209
|
-
active.push(frontier, -
|
|
194
|
+
active.push(frontier, -sizes.get(frontier));
|
|
210
195
|
frontier++;
|
|
211
196
|
if ((frontier & 4095) === 0) adding.update(frontier - previous);
|
|
212
197
|
}
|
|
@@ -323,7 +308,7 @@ function mergeLevels(splats, pool) {
|
|
|
323
308
|
removed.set(other, 1);
|
|
324
309
|
grid.remove(self);
|
|
325
310
|
grid.remove(best);
|
|
326
|
-
const feature =
|
|
311
|
+
const feature = sizes.get(merged);
|
|
327
312
|
if (feature > step) next.push(merged, -feature);
|
|
328
313
|
else {
|
|
329
314
|
const terms =
|
|
@@ -404,8 +389,8 @@ function sort(values, count) {
|
|
|
404
389
|
|
|
405
390
|
// Lays out the tree in chunks of 65536 splats and moves every column into
|
|
406
391
|
// 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) {
|
|
392
|
+
// a splat's first child. `sizes` are the splats' sizes in logical order.
|
|
393
|
+
function chunkTree(splats, order, first, counts, sizes, pool) {
|
|
409
394
|
const workspace = splats.workspace;
|
|
410
395
|
const nodes = splats.length;
|
|
411
396
|
for (const column of [splats.childStart, splats.childCount, splats.children])
|
|
@@ -426,13 +411,6 @@ function chunkTree(splats, order, first, counts, pool) {
|
|
|
426
411
|
"Reordering LOD nodes",
|
|
427
412
|
);
|
|
428
413
|
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
414
|
// The row of each splat's first child.
|
|
437
415
|
const starts = new PagedArray(workspace, Float64Array);
|
|
438
416
|
const indices = new IndexArray(workspace);
|
|
@@ -629,28 +607,31 @@ function chunkTree(splats, order, first, counts, pool) {
|
|
|
629
607
|
"Reordering child offsets",
|
|
630
608
|
);
|
|
631
609
|
chunked.dispose();
|
|
632
|
-
// The SH coefficients are still in
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
splats.sh[band] = nodeOrder.apply(
|
|
647
|
-
splats.sh[band],
|
|
648
|
-
SH_DIMS[band],
|
|
649
|
-
pool,
|
|
650
|
-
false,
|
|
651
|
-
`Reordering LOD SH${band + 1}`,
|
|
610
|
+
// The SH coefficients are still in the order of mergeSh().
|
|
611
|
+
if (splats.maxSh) {
|
|
612
|
+
const shOrder = new Progress("Collecting LOD SH order", indices.length);
|
|
613
|
+
for (let i = 0; i < indices.length; i++) {
|
|
614
|
+
indices.set(i, splats.shRow(order.get(indices.get(i))));
|
|
615
|
+
if ((i & 4095) === 0) shOrder.update(i);
|
|
616
|
+
}
|
|
617
|
+
shOrder.finish();
|
|
618
|
+
splats.shOrder.dispose();
|
|
619
|
+
const nodeOrder = new Permutation(
|
|
620
|
+
workspace,
|
|
621
|
+
indices,
|
|
622
|
+
nodes,
|
|
623
|
+
"Preparing LOD SH order",
|
|
652
624
|
);
|
|
653
|
-
|
|
625
|
+
for (let band = 0; band < splats.maxSh; band++)
|
|
626
|
+
splats.sh[band] = nodeOrder.apply(
|
|
627
|
+
splats.sh[band],
|
|
628
|
+
SH_DIMS[band],
|
|
629
|
+
pool,
|
|
630
|
+
false,
|
|
631
|
+
`Reordering LOD SH${band + 1}`,
|
|
632
|
+
);
|
|
633
|
+
nodeOrder.dispose();
|
|
634
|
+
}
|
|
654
635
|
first.dispose();
|
|
655
636
|
splats.length = indices.length;
|
|
656
637
|
indices.dispose();
|