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 CHANGED
@@ -7,6 +7,20 @@ and this project follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.4.0] - 2026-10-21
11
+
12
+ ### Added
13
+
14
+ - 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).
15
+
16
+ ### Changed
17
+
18
+ - RAD files and gzip-compressed SPZ files (v1–v3) decode about 10% faster.
19
+
20
+ ### Fixed
21
+
22
+ - Fixed incorrect orientation and unstable widths of thin Splats on WebGL, WebGPU, and the WebGL2 fallback.
23
+
10
24
  ## [1.3.2] - 2026-10-18
11
25
 
12
26
  ### Added
@@ -621,7 +635,8 @@ and this project follows [Semantic Versioning](https://semver.org/).
621
635
 
622
636
  - Initial public release of the Three.js Gaussian Splatting renderer.
623
637
 
624
- [Unreleased]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.3.2...HEAD
638
+ [Unreleased]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.4.0...HEAD
639
+ [1.4.0]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.3.2...v1.4.0
625
640
  [1.3.2]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.3.1...v1.3.2
626
641
  [1.3.1]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.3.0...v1.3.1
627
642
  [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
@@ -175,6 +175,7 @@ const streaming = new SogStreamScheduler({
175
175
 
176
176
  **Large scenes**
177
177
 
178
+ - [Building RAD files](docs/BuildRad.md) — Convert models to streamable RAD with quality LOD.
178
179
  - [RadStreamScheduler](docs/RadStreamScheduler.md) — Large RAD scenes with adaptive detail.
179
180
  - [SogStreamScheduler](docs/SogStreamScheduler.md) — Large SOG scenes with adaptive detail.
180
181
 
@@ -194,7 +195,7 @@ See [Contributing](CONTRIBUTING.md#validation) for validation commands. `npm run
194
195
 
195
196
  ## Acknowledgements
196
197
 
197
- The overall architecture of Gaussian Splat Lite draws on [Spark](https://github.com/sparkjsdev/spark) and [SuperSplat](https://github.com/playcanvas/supersplat).
198
+ Gaussian Splat Lite uses [Spark](https://github.com/sparkjsdev/spark) and [SuperSplat](https://github.com/playcanvas/supersplat) as reference implementations.
198
199
 
199
200
  Example model adapted from [shehabmekky](https://superspl.at/scene/c1e6297e) ([CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)).
200
201
 
@@ -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
+ }