gaussian-splat-lite 1.4.2 → 1.4.3

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 (48) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/bin/build-lod.js +17 -2
  3. package/bin/rad/parallel.js +7 -6
  4. package/bin/rad/splats.js +27 -10
  5. package/dist/gaussian-splat-lite.cjs +104 -70
  6. package/dist/gaussian-splat-lite.cjs.map +1 -1
  7. package/dist/gaussian-splat-lite.min.cjs +51 -51
  8. package/dist/gaussian-splat-lite.min.cjs.map +1 -1
  9. package/dist/gaussian-splat-lite.module.js +104 -70
  10. package/dist/gaussian-splat-lite.module.js.map +1 -1
  11. package/dist/gaussian-splat-lite.module.min.js +563 -544
  12. package/dist/gaussian-splat-lite.module.min.js.map +1 -1
  13. package/dist/types/data/IndexedSplats.d.cts +1 -4
  14. package/dist/types/data/IndexedSplats.d.ts +1 -4
  15. package/dist/types/data/Splats.d.cts +2 -6
  16. package/dist/types/data/Splats.d.ts +2 -6
  17. package/dist/types/data/defines.d.cts +3 -4
  18. package/dist/types/data/defines.d.ts +3 -4
  19. package/dist/types/data/splatData.d.cts +1 -1
  20. package/dist/types/data/splatData.d.ts +1 -1
  21. package/dist/types/data/splatRange.d.cts +1 -1
  22. package/dist/types/data/splatRange.d.ts +1 -1
  23. package/dist/types/loaders/morton.d.cts +6 -3
  24. package/dist/types/loaders/morton.d.ts +6 -3
  25. package/dist/types/loaders/postDecode/protocol.d.cts +1 -0
  26. package/dist/types/loaders/postDecode/protocol.d.ts +1 -0
  27. package/dist/types/loaders/rad.d.cts +5 -5
  28. package/dist/types/loaders/rad.d.ts +5 -5
  29. package/dist/types/loaders/stream/rad-stream/RadStreamScheduler.d.cts +0 -2
  30. package/dist/types/loaders/stream/rad-stream/RadStreamScheduler.d.ts +0 -2
  31. package/dist/types/loaders/stream/sog-stream/SogStreamLoader.d.cts +1 -1
  32. package/dist/types/loaders/stream/sog-stream/SogStreamLoader.d.ts +1 -1
  33. package/dist/types/loaders/stream/sog-stream/SogStreamScheduler.d.cts +16 -2
  34. package/dist/types/loaders/stream/sog-stream/SogStreamScheduler.d.ts +16 -2
  35. package/dist/types/loaders/stream/sog-stream/sogLod.d.cts +8 -9
  36. package/dist/types/loaders/stream/sog-stream/sogLod.d.ts +8 -9
  37. package/dist/types/loaders/stream/sog-stream/sogVisibility.d.cts +13 -7
  38. package/dist/types/loaders/stream/sog-stream/sogVisibility.d.ts +13 -7
  39. package/dist/types/loaders/stream/streamOptions.d.cts +1 -1
  40. package/dist/types/loaders/stream/streamOptions.d.ts +1 -1
  41. package/dist/types/scene/SplatMesh.d.cts +0 -2
  42. package/dist/types/scene/SplatMesh.d.ts +0 -2
  43. package/docs/BuildRad.md +1 -0
  44. package/docs/RadStreamScheduler.md +4 -4
  45. package/docs/SogStreamScheduler.md +14 -4
  46. package/docs/SplatMesh.md +2 -2
  47. package/docs/Splats.md +1 -3
  48. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ and this project follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.4.3] - 2026-10-25
11
+
12
+ ### Added
13
+
14
+ - Added `--within-dist <x,y,z,radius>` to `gsl-build-lod` to keep splat centers inside or on a sphere in input coordinates before building LOD. See [Building RAD files](docs/BuildRad.md).
15
+
16
+ ### Changed
17
+
18
+ - Streamed SOG keeps surrounding regions in its detail budget, with less detail behind the camera and corrected distances for oversized regions. Added runtime-adjustable `lodBehindPenalty` (default `4`) and `lodDistanceShrink` (default `0.75`).
19
+
20
+ ### Removed
21
+
22
+ - Removed raycast hit `sourceIndex`, `Splats.getSourceIndex()`, and `RadStreamScheduler.getGlobalIndex()`. Use `hit.object.splats.getSplat(hit.index)` to read the current hit; streamed indices can change when the visible detail changes.
23
+
24
+ ### Fixed
25
+
26
+ - Streamed SOG waits for initial detail selection before rejecting `firstRenderable`, including when an empty environment finishes last.
27
+ - Streamed SOG rejects `firstRenderable` when all required loads fail permanently, even while the camera keeps moving.
28
+ - Streamed SOG retains displayed coverage when detail selection fails, stopping further loading while existing fades finish.
29
+
10
30
  ## [1.4.2] - 2026-10-24
11
31
 
12
32
  ### Changed
@@ -660,7 +680,8 @@ and this project follows [Semantic Versioning](https://semver.org/).
660
680
 
661
681
  - Initial public release of the Three.js Gaussian Splatting renderer.
662
682
 
663
- [Unreleased]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.4.2...HEAD
683
+ [Unreleased]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.4.3...HEAD
684
+ [1.4.3]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.4.2...v1.4.3
664
685
  [1.4.2]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.4.1...v1.4.2
665
686
  [1.4.1]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.4.0...v1.4.1
666
687
  [1.4.0]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.3.2...v1.4.0
package/bin/build-lod.js CHANGED
@@ -24,6 +24,8 @@ Output: <input-stem>-lod.rad. Existing files are not overwritten.
24
24
  Options:
25
25
  --lod-base <base> LOD spacing (default: 1.75, range: 1.1..16)
26
26
  --max-sh <0..3> Maximum SH degree (default: 3)
27
+ --within-dist <x,y,z,radius>
28
+ Keep splat centers within this sphere (input coordinates)
27
29
  --rad-chunked Write a .rad header and separate .radc files
28
30
  --memory-budget <GiB> Target memory in GiB (default: 4, minimum: 1)
29
31
  --temp-dir <path> Temporary file location (default: system temp)
@@ -62,6 +64,7 @@ function parse(args) {
62
64
  const key = {
63
65
  "--lod-base": "lodBase",
64
66
  "--max-sh": "maxSh",
67
+ "--within-dist": "withinDist",
65
68
  "--memory-budget": "budget",
66
69
  "--temp-dir": "tempDir",
67
70
  "-o": "output",
@@ -86,6 +89,16 @@ function parse(args) {
86
89
  options.maxSh > 3
87
90
  )
88
91
  throw new Error("--max-sh must be an integer between 0 and 3");
92
+ if (options.withinDist !== undefined) {
93
+ const values = options.withinDist
94
+ .split(",")
95
+ .map((value) => (value.trim() ? Number(value) : Number.NaN));
96
+ if (values.length !== 4 || !values.every(Number.isFinite) || values[3] < 0)
97
+ throw new Error(
98
+ "--within-dist must be x,y,z,radius with finite numbers and a nonnegative radius",
99
+ );
100
+ options.withinDist = values;
101
+ }
89
102
  if (
90
103
  !Number.isFinite(options.budget) ||
91
104
  options.budget < GIB ||
@@ -120,7 +133,7 @@ async function convert(input, options, cancelled) {
120
133
  const splats = await readInput(input, options.maxSh, workspace);
121
134
  const count = splats.length;
122
135
  pool = new Pool(workspace, count);
123
- splats.retain(pool);
136
+ splats.retain(pool, options.withinDist);
124
137
  console.log(
125
138
  `Read ${count} splats, SH${splats.inputSh}, in ${((performance.now() - start) / 1000).toFixed(2)}s`,
126
139
  );
@@ -132,8 +145,10 @@ async function convert(input, options, cancelled) {
132
145
  console.log(
133
146
  `Removed ${splats.invalid} splats with NaN or infinite values or a zero rotation`,
134
147
  );
135
- const empty = count - splats.invalid - splats.length;
148
+ const empty = count - splats.invalid - splats.cropped - splats.length;
136
149
  if (empty) console.log(`Removed ${empty} empty splats`);
150
+ if (splats.cropped)
151
+ console.log(`Removed ${splats.cropped} splats outside --within-dist`);
137
152
  if (!splats.length) throw new Error("Input contains no usable splats");
138
153
  const leaves = splats.length;
139
154
  const lodStart = performance.now();
@@ -9,13 +9,12 @@ import {
9
9
  import { Lookup, NONE } from "./grid.js";
10
10
  import { HALF, halfBits } from "./math.js";
11
11
  import {
12
- EMPTY,
13
12
  INVALID,
14
- KEEP,
15
13
  LATER,
16
14
  Merger,
17
15
  SH_DIMS,
18
16
  Similarities,
17
+ filterRecord,
19
18
  refresh,
20
19
  validRecord,
21
20
  } from "./splats.js";
@@ -231,14 +230,15 @@ export class Pool {
231
230
  this.run(COPY, count, 1024);
232
231
  }
233
232
 
234
- // Refreshes and checks `count` splats from `base` in place, setting KEEP,
235
- // INVALID, EMPTY or LATER in `results`; see Splats.retain().
236
- check(splats, base, count) {
233
+ // Refreshes, checks and optionally crops `count` splats from `base` in
234
+ // place, storing their filter states in `results`; see Splats.retain().
235
+ check(splats, base, count, withinDist) {
237
236
  this.share({
238
237
  splatRecords: splats.records.directory,
239
238
  splatSh: splats.sh
240
239
  .slice(0, splats.maxSh)
241
240
  .map((column) => column.directory),
241
+ withinDist,
242
242
  });
243
243
  Atomics.store(this.control, BASE, base);
244
244
  this.run(CHECK, count, 1024);
@@ -370,6 +370,7 @@ class Task {
370
370
  if (state.column)
371
371
  this.column = new SharedColumn(views, state.column, Uint16Array);
372
372
  if (state.splatRecords) {
373
+ this.withinDist = state.withinDist;
373
374
  this.splatRecords = new SharedColumn(
374
375
  views,
375
376
  state.splatRecords,
@@ -450,7 +451,7 @@ class Task {
450
451
  for (let d = 0; d < dims; d++)
451
452
  if (!Number.isFinite(HALF[row[at + d]])) return INVALID;
452
453
  }
453
- return record[3] > 0 && record[14] > 0 ? KEEP : EMPTY;
454
+ return filterRecord(record, this.withinDist);
454
455
  }
455
456
 
456
457
  copy(from, to) {
package/bin/rad/splats.js CHANGED
@@ -20,8 +20,9 @@ export const SH_DIMS = [9, 15, 21];
20
20
  export const KEEP = 0;
21
21
  export const INVALID = 1;
22
22
  export const EMPTY = 2;
23
+ export const CROPPED = 3;
23
24
  // A worker found a page that is not cached.
24
- export const LATER = 3;
25
+ export const LATER = 4;
25
26
 
26
27
  // Sets a record's feature size and area from its scales and opacity.
27
28
  export function refresh(record) {
@@ -45,6 +46,16 @@ export function validRecord(record) {
45
46
  return true;
46
47
  }
47
48
 
49
+ // Classifies a valid record after refresh(), including optional cropping.
50
+ export function filterRecord(record, withinDist) {
51
+ if (record[3] <= 0 || record[14] <= 0) return EMPTY;
52
+ if (!withinDist) return KEEP;
53
+ const [x, y, z, radius] = withinDist;
54
+ return Math.hypot(record[0] - x, record[1] - y, record[2] - z) > radius
55
+ ? CROPPED
56
+ : KEEP;
57
+ }
58
+
48
59
  export class Splats {
49
60
  constructor(workspace, maxSh) {
50
61
  this.workspace = workspace;
@@ -52,6 +63,7 @@ export class Splats {
52
63
  this.inputSh = 0;
53
64
  this.length = 0;
54
65
  this.invalid = 0;
66
+ this.cropped = 0;
55
67
  this.records = new PagedArray(workspace, Float32Array);
56
68
  this.sh = SH_DIMS.map(() => new PagedArray(workspace, Uint16Array));
57
69
  // The two children of the i-th merged splat are entries 2i and 2i + 1 of
@@ -127,9 +139,11 @@ export class Splats {
127
139
 
128
140
  // Skips empty splats and counts invalid ones in `invalid`: NaN or infinite
129
141
  // values, including a zero rotation, which the decoder normalizes to NaN.
130
- // Workers refresh and check blocks of splats in place; the main thread
142
+ // Optionally keeps only centers within [x, y, z, radius], counting the
143
+ // otherwise usable splats outside this sphere in `cropped`.
144
+ // Workers refresh, check and crop blocks of splats; the main thread
131
145
  // checks those they leave, and moves kept splats after skipped ones.
132
- retain(pool) {
146
+ retain(pool, withinDist) {
133
147
  const progress = new Progress("Filtering splats", this.length);
134
148
  const { results } = pool;
135
149
  const block = results.length;
@@ -143,6 +157,7 @@ export class Splats {
143
157
  ];
144
158
  let count = 0;
145
159
  this.invalid = 0;
160
+ this.cropped = 0;
146
161
  for (let base = 0; base < this.length; base += block) {
147
162
  const size = Math.min(block, this.length - base);
148
163
  for (const [column, stride] of columns)
@@ -151,24 +166,26 @@ export class Splats {
151
166
  (base + size) * stride,
152
167
  column === this.records,
153
168
  );
154
- pool.check(this, base, size);
169
+ pool.check(this, base, size, withinDist);
155
170
  for (let i = 0; i < size; i++) {
156
171
  const index = base + i;
157
172
  let state = results[i];
158
- if (state === LATER) {
173
+ const later = state === LATER;
174
+ if (later) {
159
175
  this.read(index, record);
160
176
  refresh(record);
161
177
  state = !this.valid(index, record, rows)
162
178
  ? INVALID
163
- : record[3] > 0 && record[14] > 0
164
- ? KEEP
165
- : EMPTY;
166
- if (state === KEEP) this.records.write(index * 16, record);
179
+ : filterRecord(record, withinDist);
167
180
  }
168
181
  if (state === INVALID) this.invalid++;
182
+ if (state === CROPPED) this.cropped++;
169
183
  if (state !== KEEP) continue;
184
+ if (count !== index || later) {
185
+ if (!later) this.read(index, record);
186
+ this.records.write(count * 16, record);
187
+ }
170
188
  if (count !== index) {
171
- this.records.write(count * 16, this.read(index, record));
172
189
  for (let band = 0; band < this.maxSh; band++) {
173
190
  const dims = SH_DIMS[band];
174
191
  this.sh[band].read(index * dims, rows[band]);