gaussian-splat-lite 1.3.2 → 1.4.1

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +36 -2
  2. package/CONTRIBUTING.md +14 -2
  3. package/NOTICE +11 -3
  4. package/README.md +5 -7
  5. package/THIRD_PARTY_LICENSES.md +64 -6
  6. package/bin/build-lod.js +241 -0
  7. package/bin/decoder/decoder.js +340 -0
  8. package/bin/decoder/decoder_bg.wasm +0 -0
  9. package/bin/rad/grid.js +620 -0
  10. package/bin/rad/heap.js +270 -0
  11. package/bin/rad/input.js +87 -0
  12. package/bin/rad/lod.js +657 -0
  13. package/bin/rad/math.js +166 -0
  14. package/bin/rad/parallel.js +578 -0
  15. package/bin/rad/progress.js +50 -0
  16. package/bin/rad/rad.js +612 -0
  17. package/bin/rad/splats.js +432 -0
  18. package/bin/rad/storage.js +776 -0
  19. package/dist/gaussian-splat-lite.cjs +392 -794
  20. package/dist/gaussian-splat-lite.cjs.map +1 -1
  21. package/dist/gaussian-splat-lite.min.cjs +246 -308
  22. package/dist/gaussian-splat-lite.min.cjs.map +1 -1
  23. package/dist/gaussian-splat-lite.module.js +394 -796
  24. package/dist/gaussian-splat-lite.module.js.map +1 -1
  25. package/dist/gaussian-splat-lite.module.min.js +2518 -2802
  26. package/dist/gaussian-splat-lite.module.min.js.map +1 -1
  27. package/dist/types/addons/NeuralDenoiseNode.d.cts +1 -2
  28. package/dist/types/addons/NeuralDenoiseNode.d.ts +1 -2
  29. package/dist/types/addons/NeuralDenoiseXR.d.cts +31 -0
  30. package/dist/types/addons/NeuralDenoiseXR.d.ts +31 -0
  31. package/dist/types/addons/neuralDenoiseState.d.cts +22 -0
  32. package/dist/types/addons/neuralDenoiseState.d.ts +22 -0
  33. package/dist/types/addons/neuralDenoiseWeights.d.cts +41 -2
  34. package/dist/types/addons/neuralDenoiseWeights.d.ts +41 -2
  35. package/dist/types/data/RadPagedSplats.d.cts +1 -1
  36. package/dist/types/data/RadPagedSplats.d.ts +1 -1
  37. package/dist/types/index.d.cts +1 -2
  38. package/dist/types/index.d.ts +1 -2
  39. package/dist/types/loaders/rad/radFormat.d.cts +2 -2
  40. package/dist/types/loaders/rad/radFormat.d.ts +2 -2
  41. package/docs/BuildRad.md +91 -0
  42. package/docs/GaussianSplatRenderer.md +2 -2
  43. package/docs/NeuralDenoiseNode.md +32 -13
  44. package/docs/RadStreamScheduler.md +3 -1
  45. package/docs/StochasticRendering.md +20 -42
  46. package/package.json +9 -3
  47. package/dist/types/addons/TAANode.d.cts +0 -49
  48. package/dist/types/addons/TAANode.d.ts +0 -49
  49. package/dist/types/addons/TAAPass.d.cts +0 -57
  50. package/dist/types/addons/TAAPass.d.ts +0 -57
  51. package/dist/types/addons/taaShared.d.cts +0 -74
  52. package/dist/types/addons/taaShared.d.ts +0 -74
  53. package/docs/TAAPass.md +0 -184
package/CHANGELOG.md CHANGED
@@ -7,6 +7,38 @@ and this project follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.4.1] - 2026-10-24
11
+
12
+ ### Added
13
+
14
+ - Added experimental `NeuralDenoiseXR` for per-eye neural denoising in WebXR with `WebGLRenderer` and `WebGPURenderer`, including its WebGL2 fallback. See [Neural denoising](docs/NeuralDenoiseNode.md#webxr).
15
+
16
+ ### Changed
17
+
18
+ - Neural denoising reduces noise and flicker on moving objects, including moving Splats. Both modes also use less GPU time while the camera moves. During camera movement, `"quality"` adds smoothing with slightly softer detail, while `"performance"` preserves more fine detail at slow speeds.
19
+
20
+ ### Removed
21
+
22
+ - Removed `TAANode`, `TAAPass`, and the viewer's TAA option. Use `NeuralDenoiseNode` with `WebGPURenderer`, `NeuralDenoisePass` with `WebGLRenderer`, or `NeuralDenoiseXR` for WebXR. See [Neural denoising](docs/NeuralDenoiseNode.md).
23
+
24
+ ### Fixed
25
+
26
+ - On `WebGPURenderer`'s WebGL2 backend, the neural denoiser's `depthTexture` stopped updating after a resize.
27
+
28
+ ## [1.4.0] - 2026-10-21
29
+
30
+ ### Added
31
+
32
+ - Added `gsl-build-lod` to convert PLY and SPZ models into streamable RAD files with LOD, without compilation. Supports batch conversion, split files, up to SH3, and configurable memory and temporary storage. See [Building RAD files](docs/BuildRad.md).
33
+
34
+ ### Changed
35
+
36
+ - RAD files and gzip-compressed SPZ files (v1–v3) decode about 10% faster.
37
+
38
+ ### Fixed
39
+
40
+ - Fixed incorrect orientation and unstable widths of thin Splats on WebGL, WebGPU, and the WebGL2 fallback.
41
+
10
42
  ## [1.3.2] - 2026-10-18
11
43
 
12
44
  ### Added
@@ -105,7 +137,7 @@ and this project follows [Semantic Versioning](https://semver.org/).
105
137
  ### Changed
106
138
 
107
139
  - The default `minAlpha` is now `1 / 255` instead of `0.5 / 255`. Set `minAlpha: 0.5 / 255` to keep the previous look.
108
- - `TAAPass` is now used through `EffectComposer` with a final Three.js `OutputPass`; calling `taa.render(renderer)` on its own is no longer supported. Bloom and other effects go between the two passes without extra settings. Splat colors still match direct canvas rendering. `OutputPass` applies the renderer's tone mapping to the whole image, Splats included. See [TAAPass and TAANode](docs/TAAPass.md).
140
+ - `TAAPass` is now used through `EffectComposer` with a final Three.js `OutputPass`; calling `taa.render(renderer)` on its own is no longer supported. Bloom and other effects go between the two passes without extra settings. Splat colors still match direct canvas rendering. `OutputPass` applies the renderer's tone mapping to the whole image, Splats included.
109
141
 
110
142
  ### Removed
111
143
 
@@ -621,7 +653,9 @@ and this project follows [Semantic Versioning](https://semver.org/).
621
653
 
622
654
  - Initial public release of the Three.js Gaussian Splatting renderer.
623
655
 
624
- [Unreleased]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.3.2...HEAD
656
+ [Unreleased]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.4.1...HEAD
657
+ [1.4.1]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.4.0...v1.4.1
658
+ [1.4.0]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.3.2...v1.4.0
625
659
  [1.3.2]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.3.1...v1.3.2
626
660
  [1.3.1]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.3.0...v1.3.1
627
661
  [1.3.0]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.2.2...v1.3.0
package/CONTRIBUTING.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Development setup
4
4
 
5
- Requires Node.js 20.19+ or 22.12+, Rust via `rustup`, and the `wasm32-unknown-unknown` target.
5
+ Requires Node.js 20.19+ or 22.12+ and Rust via `rustup`. `npm run build:wasm` installs the `wasm32-unknown-unknown` target if needed.
6
6
 
7
7
  ```bash
8
8
  npm ci
@@ -14,6 +14,18 @@ Check WebGPU, WebGL2, and WebGPU · WebGL2 in the viewer. For rendering changes,
14
14
 
15
15
  Implementations and build scripts use JavaScript. Keep TypeScript declarations in adjacent `.d.ts` files and update them whenever the corresponding API changes. `src/index.js` and `src/index.d.ts` define the package's runtime and type exports.
16
16
 
17
+ ### RAD command-line tool
18
+
19
+ See [Building RAD files](docs/BuildRad.md) for installation, conversion commands, and options.
20
+
21
+ Before running the CLI from a checkout, and after changing the decoder, its dependencies, or its build settings, rebuild the decoder:
22
+
23
+ ```bash
24
+ npm run build:wasm:lod
25
+ ```
26
+
27
+ `npm run build:wasm` also builds the CLI decoder. The generated `bin/decoder/` files are ignored by Git and included in the npm package, so npm users can run the command without compiling anything. The release workflow builds them before packaging.
28
+
17
29
  ## Validation
18
30
 
19
31
  Before opening a pull request:
@@ -35,4 +47,4 @@ Review new or changed dependency install scripts and update `allowScripts` with
35
47
  - Add or update tests for behavior that can be exercised without WebGL.
36
48
  - Include a small reproduction and before/after images for rendering changes where possible.
37
49
  - Add user-visible changes to the `Unreleased` section of `CHANGELOG.md`; leave published version sections unchanged.
38
- - Do not commit generated `dist/`, `site-dist/`, Rust `target/`, or wasm-pack `pkg/` directories.
50
+ - Do not commit generated `dist/`, `site-dist/`, `bin/decoder/`, Rust `target/`, or wasm-pack `pkg/` directories.
package/NOTICE CHANGED
@@ -1,14 +1,22 @@
1
1
  Gaussian Splat Lite
2
2
  Copyright 2026 William Liu <lyz15972107087@gmail.com>
3
3
 
4
- This product includes software derived from SparkJS v2.1.0,
4
+ This product includes software derived from SparkJS,
5
5
  Copyright © 2025 WORLD LABS TECHNOLOGIES, INC., licensed under the MIT
6
6
  License. See THIRD_PARTY_LICENSES.md for the complete license notice.
7
7
 
8
- This product includes software adapted from SuperSplat 3.0.0-alpha,
8
+ This product includes software adapted from SuperSplat,
9
9
  Copyright (c) 2011-2026 PlayCanvas Ltd., licensed under the MIT License.
10
10
  See THIRD_PARTY_LICENSES.md for the complete license notice.
11
11
 
12
- This product includes ruzstd v0.7.3,
12
+ This product includes ruzstd,
13
13
  Copyright © 2019 Moritz Borcherding, licensed under the MIT License.
14
14
  See THIRD_PARTY_LICENSES.md for the complete license notice.
15
+
16
+ This product includes flate2,
17
+ Copyright (c) 2014-2026 Alex Crichton, licensed under the MIT License.
18
+ See THIRD_PARTY_LICENSES.md for the complete license notice.
19
+
20
+ This product includes zlib-rs,
21
+ (C) 2024 Trifecta Tech Foundation, licensed under the zlib License.
22
+ See THIRD_PARTY_LICENSES.md for the complete license notice.
package/README.md CHANGED
@@ -30,7 +30,7 @@ High performance 3D Gaussian Splatting (3DGS) renderer for **Three.js** with **W
30
30
  | --- | --- |
31
31
  | **WebGPU / WebGL2** | Use the same Three.js scene API with either renderer |
32
32
  | **Large-scene streaming** | Load RAD and SOG detail as the camera moves, with smooth transitions |
33
- | **Stochastic rendering** | Optional transparency mode, with built-in temporal anti-aliasing or an experimental neural denoiser to smooth it |
33
+ | **Stochastic rendering** | Optional transparency mode, with an experimental neural denoiser to smooth it |
34
34
  | **Offscreen capture** | Render Splats to images, cube maps, and environment maps for reflections |
35
35
  | **SDF edits** | Recolor or hide parts of a model without moving Splats |
36
36
  | **Data and precision** | Load URLs, files, or bytes; place local models in large GIS/ECEF scenes |
@@ -108,7 +108,7 @@ Sorted alpha blending is the default. To enable stochastic rendering:
108
108
  splatRenderer.stochastic = true;
109
109
  ```
110
110
 
111
- This mode produces visible noise. Smooth it with [temporal anti-aliasing](docs/TAAPass.md), or use the [experimental neural denoiser](docs/NeuralDenoiseNode.md) to help reduce trails behind moving objects. Both support WebGPU and WebGL2. See [Stochastic rendering](docs/StochasticRendering.md) for setup and requirements.
111
+ This mode produces visible noise. Smooth it with the [experimental neural denoiser](docs/NeuralDenoiseNode.md), which supports WebGPU and WebGL2, including WebXR. See [Stochastic rendering](docs/StochasticRendering.md) for setup and requirements.
112
112
 
113
113
  ## Streaming large scenes
114
114
 
@@ -160,8 +160,7 @@ const streaming = new SogStreamScheduler({
160
160
 
161
161
  - [GaussianSplatRenderer](docs/GaussianSplatRenderer.md) — Rendering options, sorting, and on-demand rendering.
162
162
  - [Stochastic rendering](docs/StochasticRendering.md) — The optional transparency mode and how to smooth its noise.
163
- - [TAAPass and TAANode](docs/TAAPass.md) — Temporal anti-aliasing for WebGLRenderer and WebGPURenderer.
164
- - [NeuralDenoiseNode and NeuralDenoisePass (Experimental)](docs/NeuralDenoiseNode.md) — Neural denoising on WebGPU and WebGL2, for scenes with moving objects.
163
+ - [Neural denoising (Experimental)](docs/NeuralDenoiseNode.md) — Neural denoising on WebGPU and WebGL2, including per-eye WebXR rendering with `NeuralDenoiseXR`.
165
164
  - [SplatCapture](docs/SplatCapture.md) — Offscreen images, pixel readback, cube maps, and environment maps.
166
165
 
167
166
  **Models**
@@ -175,6 +174,7 @@ const streaming = new SogStreamScheduler({
175
174
 
176
175
  **Large scenes**
177
176
 
177
+ - [Building RAD files](docs/BuildRad.md) — Convert models to streamable RAD with quality LOD.
178
178
  - [RadStreamScheduler](docs/RadStreamScheduler.md) — Large RAD scenes with adaptive detail.
179
179
  - [SogStreamScheduler](docs/SogStreamScheduler.md) — Large SOG scenes with adaptive detail.
180
180
 
@@ -194,9 +194,7 @@ See [Contributing](CONTRIBUTING.md#validation) for validation commands. `npm run
194
194
 
195
195
  ## Acknowledgements
196
196
 
197
- The overall architecture of Gaussian Splat Lite draws on [Spark](https://github.com/sparkjsdev/spark) and [SuperSplat](https://github.com/playcanvas/supersplat).
198
-
199
- Example model adapted from [shehabmekky](https://superspl.at/scene/c1e6297e) ([CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)).
197
+ Gaussian Splat Lite uses [Spark](https://github.com/sparkjsdev/spark) and [SuperSplat](https://github.com/playcanvas/supersplat) as reference implementations.
200
198
 
201
199
  ## License
202
200
 
@@ -1,8 +1,8 @@
1
1
  # Third-party licenses
2
2
 
3
- ## SparkJS v2.1.0
3
+ ## SparkJS
4
4
 
5
- Gaussian Splat Lite includes software derived from SparkJS v2.1.0.
5
+ Gaussian Splat Lite includes code derived from SparkJS.
6
6
 
7
7
  The MIT License
8
8
 
@@ -26,9 +26,9 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
26
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27
27
  THE SOFTWARE.
28
28
 
29
- ## SuperSplat 3.0.0-alpha
29
+ ## SuperSplat
30
30
 
31
- Gaussian Splat Lite includes software adapted from SuperSplat 3.0.0-alpha.
31
+ Gaussian Splat Lite includes software adapted from SuperSplat.
32
32
 
33
33
  MIT License
34
34
 
@@ -52,9 +52,9 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
52
52
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
53
53
  THE SOFTWARE.
54
54
 
55
- ## ruzstd 0.9.0
55
+ ## ruzstd
56
56
 
57
- Gaussian Splat Lite includes ruzstd 0.9.0 in its WebAssembly build.
57
+ Gaussian Splat Lite includes ruzstd in its WebAssembly build.
58
58
 
59
59
  MIT License
60
60
 
@@ -110,3 +110,61 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
110
110
  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
111
111
  IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
112
112
  DEALINGS IN THE SOFTWARE.
113
+
114
+ ## flate2
115
+
116
+ Gaussian Splat Lite includes flate2 in its WebAssembly build.
117
+
118
+ MIT License
119
+
120
+ Copyright (c) 2014-2026 Alex Crichton
121
+
122
+ Permission is hereby granted, free of charge, to any
123
+ person obtaining a copy of this software and associated
124
+ documentation files (the "Software"), to deal in the
125
+ Software without restriction, including without
126
+ limitation the rights to use, copy, modify, merge,
127
+ publish, distribute, sublicense, and/or sell copies of
128
+ the Software, and to permit persons to whom the Software
129
+ is furnished to do so, subject to the following
130
+ conditions:
131
+
132
+ The above copyright notice and this permission notice
133
+ shall be included in all copies or substantial portions
134
+ of the Software.
135
+
136
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
137
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
138
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
139
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
140
+ SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
141
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
142
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
143
+ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
144
+ DEALINGS IN THE SOFTWARE.
145
+
146
+ ## zlib-rs
147
+
148
+ Gaussian Splat Lite includes zlib-rs in its WebAssembly build.
149
+
150
+ zlib License
151
+
152
+ (C) 2024 Trifecta Tech Foundation
153
+
154
+ This software is provided 'as-is', without any express or implied
155
+ warranty. In no event will the authors be held liable for any damages
156
+ arising from the use of this software.
157
+
158
+ Permission is granted to anyone to use this software for any purpose,
159
+ including commercial applications, and to alter it and redistribute it
160
+ freely, subject to the following restrictions:
161
+
162
+ 1. The origin of this software must not be misrepresented; you must not
163
+ claim that you wrote the original software. If you use this software
164
+ in a product, an acknowledgment in the product documentation would be
165
+ appreciated but is not required.
166
+
167
+ 2. Altered source versions must be plainly marked as such, and must not be
168
+ misrepresented as being the original software.
169
+
170
+ 3. This notice may not be removed or altered from any source distribution.
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "node:path";
4
+ import {
5
+ Worker,
6
+ isMainThread,
7
+ parentPort,
8
+ workerData,
9
+ } from "node:worker_threads";
10
+ import { readInput } from "./rad/input.js";
11
+ import { buildLod } from "./rad/lod.js";
12
+ import { Pool } from "./rad/parallel.js";
13
+ import { checkOutput, writeRad } from "./rad/rad.js";
14
+ import { GIB, Workspace } from "./rad/storage.js";
15
+
16
+ const help = `Convert PLY or SPZ files to RAD with levels of detail (LOD).
17
+
18
+ Usage: gsl-build-lod <input> [more inputs...] [options]
19
+ npm run build-lod -- <input> [more inputs...] [options]
20
+
21
+ Inputs: PLY, SPZ.
22
+ Output: <input-stem>-lod.rad. Existing files are not overwritten.
23
+
24
+ Options:
25
+ --lod-base <base> LOD spacing (default: 1.75, range: 1.1..16)
26
+ --max-sh <0..3> Maximum SH degree (default: 3)
27
+ --rad-chunked Write a .rad header and separate .radc files
28
+ --memory-budget <GiB> Target memory in GiB (default: 4, minimum: 1)
29
+ --temp-dir <path> Temporary file location (default: system temp)
30
+ -o, --output <path> Output .rad path (one input only)
31
+ -h, --help Show this help
32
+
33
+ SH4 inputs are accepted; coefficients above --max-sh are discarded.
34
+ The memory target covers the whole conversion, worker threads included.
35
+ No compilation is required.`;
36
+
37
+ function parse(args) {
38
+ const options = {
39
+ inputs: [],
40
+ lodBase: 1.75,
41
+ maxSh: 3,
42
+ budget: 4 * GIB,
43
+ chunked: false,
44
+ };
45
+ for (let i = 0; i < args.length; i++) {
46
+ const arg = args[i];
47
+ if (arg === "--help" || arg === "-h") return;
48
+ if (arg === "--") {
49
+ options.inputs.push(...args.slice(i + 1));
50
+ break;
51
+ }
52
+ if (arg === "--rad-chunked") {
53
+ options.chunked = true;
54
+ continue;
55
+ }
56
+ if (!arg.startsWith("-")) {
57
+ options.inputs.push(arg);
58
+ continue;
59
+ }
60
+ const equal = arg.indexOf("=");
61
+ const name = equal < 0 ? arg : arg.slice(0, equal);
62
+ const key = {
63
+ "--lod-base": "lodBase",
64
+ "--max-sh": "maxSh",
65
+ "--memory-budget": "budget",
66
+ "--temp-dir": "tempDir",
67
+ "-o": "output",
68
+ "--output": "output",
69
+ }[name];
70
+ if (!key) throw new Error(`Unknown option: ${arg}`);
71
+ const value = equal < 0 ? args[++i] : arg.slice(equal + 1);
72
+ if (!value) throw new Error(`${name} requires a value`);
73
+ options[key] = ["lodBase", "maxSh", "budget"].includes(key)
74
+ ? Number(value) * (key === "budget" ? GIB : 1)
75
+ : value;
76
+ }
77
+ if (
78
+ !Number.isFinite(options.lodBase) ||
79
+ options.lodBase < 1.1 ||
80
+ options.lodBase > 16
81
+ )
82
+ throw new Error("--lod-base must be between 1.1 and 16");
83
+ if (
84
+ !Number.isInteger(options.maxSh) ||
85
+ options.maxSh < 0 ||
86
+ options.maxSh > 3
87
+ )
88
+ throw new Error("--max-sh must be an integer between 0 and 3");
89
+ if (
90
+ !Number.isFinite(options.budget) ||
91
+ options.budget < GIB ||
92
+ options.budget > Number.MAX_SAFE_INTEGER
93
+ )
94
+ throw new Error(
95
+ "--memory-budget must be a finite GiB number of at least 1 within the platform limit",
96
+ );
97
+ if (!options.inputs.length) throw new Error(help);
98
+ if (options.output && options.inputs.length !== 1)
99
+ throw new Error("--output requires exactly one input");
100
+ return options;
101
+ }
102
+
103
+ async function convert(input, options, cancelled) {
104
+ const output =
105
+ options.output ??
106
+ path.join(
107
+ path.dirname(input),
108
+ `${path.basename(input, path.extname(input))}-lod.rad`,
109
+ );
110
+ checkOutput(output, options.chunked);
111
+ const workspace = new Workspace(options.budget, options.tempDir, cancelled);
112
+ const start = performance.now();
113
+ console.log(
114
+ `${input} -> ${output}\nMemory target: ${(options.budget / GIB).toFixed(3)} GiB`,
115
+ );
116
+ // Worker threads for the LOD, closed before writing starts its own.
117
+ let pool;
118
+ try {
119
+ workspace.check();
120
+ const splats = await readInput(input, options.maxSh, workspace);
121
+ const count = splats.length;
122
+ pool = new Pool(workspace, count);
123
+ splats.retain(pool);
124
+ console.log(
125
+ `Read ${count} splats, SH${splats.inputSh}, in ${((performance.now() - start) / 1000).toFixed(2)}s`,
126
+ );
127
+ if (splats.inputSh > splats.maxSh)
128
+ console.log(
129
+ `Keeping SH0–SH${splats.maxSh}; higher coefficients are discarded for RAD output.`,
130
+ );
131
+ if (splats.invalid)
132
+ console.log(
133
+ `Removed ${splats.invalid} splats with NaN or infinite values or a zero rotation`,
134
+ );
135
+ const empty = count - splats.invalid - splats.length;
136
+ if (empty) console.log(`Removed ${empty} empty splats`);
137
+ if (!splats.length) throw new Error("Input contains no usable splats");
138
+ const leaves = splats.length;
139
+ const lodStart = performance.now();
140
+ buildLod(splats, options.lodBase, pool);
141
+ await pool.close();
142
+ console.log(
143
+ `Built ${splats.length} LOD splats from ${leaves} leaves in ${((performance.now() - lodStart) / 1000).toFixed(2)}s`,
144
+ );
145
+ const bytes = await writeRad(
146
+ splats,
147
+ output,
148
+ options.chunked,
149
+ options.lodBase,
150
+ );
151
+ console.log(
152
+ `Wrote ${(bytes / 1048576).toFixed(2)} MiB in ${((performance.now() - start) / 1000).toFixed(2)}s total`,
153
+ );
154
+ } finally {
155
+ await pool?.close();
156
+ workspace.dispose();
157
+ }
158
+ }
159
+
160
+ async function run(options, cancelled) {
161
+ let failed = 0;
162
+ for (const input of options.inputs) {
163
+ if (Atomics.load(cancelled, 0)) return;
164
+ try {
165
+ await convert(input, options, cancelled);
166
+ } catch (error) {
167
+ if (Atomics.load(cancelled, 0)) return;
168
+ console.error(`${input}: ${error.message ?? error}`);
169
+ failed++;
170
+ }
171
+ }
172
+ if (failed) throw new Error(`${failed} conversion(s) failed`);
173
+ }
174
+
175
+ // Keep signal handling responsive while the conversion runs synchronous code.
176
+ // The worker observes cancellation and unwinds its existing catch/finally blocks.
177
+ async function start(options) {
178
+ const cancelled = new Int32Array(new SharedArrayBuffer(4));
179
+ let progressLine = "";
180
+ const endProgress = () => {
181
+ if (progressLine) process.stdout.write("\n");
182
+ progressLine = "";
183
+ };
184
+ const print = ({ method, args }) => {
185
+ if (method === "progress") {
186
+ const [line, done] = args;
187
+ if (process.stdout.isTTY) {
188
+ const width = process.stdout.columns;
189
+ const text = !done && width ? line.slice(0, width - 1) : line;
190
+ process.stdout.write(`\r\x1b[2K${text}${done ? "\n" : ""}`);
191
+ progressLine = done ? "" : text;
192
+ } else console.log(line);
193
+ } else {
194
+ // Insert logs above the live progress instead of leaving a stale row.
195
+ if (progressLine) process.stdout.write("\r\x1b[2K");
196
+ console[method](...args);
197
+ if (progressLine) process.stdout.write(progressLine);
198
+ }
199
+ };
200
+ const interrupt = (signal) => {
201
+ const code = signal === "SIGINT" ? 130 : 143;
202
+ if (Atomics.compareExchange(cancelled, 0, 0, code) === 0) {
203
+ endProgress();
204
+ console.error(`${signal}: cancelling conversion and cleaning up...`);
205
+ }
206
+ };
207
+ process.on("SIGINT", interrupt);
208
+ process.on("SIGTERM", interrupt);
209
+ try {
210
+ const code = await new Promise((resolve, reject) => {
211
+ const worker = new Worker(new URL(import.meta.url), {
212
+ workerData: { options, cancelled },
213
+ });
214
+ worker.on("message", print);
215
+ worker.once("error", reject);
216
+ worker.once("exit", resolve);
217
+ });
218
+ process.exitCode = Atomics.load(cancelled, 0) || code;
219
+ } finally {
220
+ endProgress();
221
+ process.off("SIGINT", interrupt);
222
+ process.off("SIGTERM", interrupt);
223
+ }
224
+ }
225
+
226
+ try {
227
+ if (!isMainThread) {
228
+ // Worker stdout buffers later writes until its event loop yields.
229
+ // Forward logs directly so synchronous conversion keeps showing progress.
230
+ for (const method of ["log", "warn", "error"])
231
+ console[method] = (...args) => parentPort.postMessage({ method, args });
232
+ await run(workerData.options, workerData.cancelled);
233
+ } else {
234
+ const options = parse(process.argv.slice(2));
235
+ if (!options) console.log(help);
236
+ else await start(options);
237
+ }
238
+ } catch (error) {
239
+ console.error(error.message);
240
+ process.exitCode = 1;
241
+ }