gaussian-splat-lite 1.4.2 → 1.5.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 +45 -1
- package/CONTRIBUTING.md +1 -1
- package/README.md +1 -1
- package/bin/build-lod.js +56 -5
- package/bin/decoder/cluster.wasm +0 -0
- package/bin/rad/cluster.js +756 -0
- package/bin/rad/parallel.js +84 -6
- package/bin/rad/rad.js +107 -123
- package/bin/rad/splats.js +27 -10
- package/bin/rad/storage.js +4 -1
- package/dist/gaussian-splat-lite.cjs +777 -609
- package/dist/gaussian-splat-lite.cjs.map +1 -1
- package/dist/gaussian-splat-lite.min.cjs +126 -126
- package/dist/gaussian-splat-lite.min.cjs.map +1 -1
- package/dist/gaussian-splat-lite.module.js +777 -609
- package/dist/gaussian-splat-lite.module.js.map +1 -1
- package/dist/gaussian-splat-lite.module.min.js +3391 -3254
- package/dist/gaussian-splat-lite.module.min.js.map +1 -1
- package/dist/types/data/IndexedSplats.d.cts +12 -4
- package/dist/types/data/IndexedSplats.d.ts +12 -4
- package/dist/types/data/RadPagedSplats.d.cts +3 -23
- package/dist/types/data/RadPagedSplats.d.ts +3 -23
- package/dist/types/data/Splats.d.cts +4 -6
- package/dist/types/data/Splats.d.ts +4 -6
- package/dist/types/data/defines.d.cts +5 -4
- package/dist/types/data/defines.d.ts +5 -4
- package/dist/types/data/pendingUploads.d.cts +14 -0
- package/dist/types/data/pendingUploads.d.ts +14 -0
- package/dist/types/data/splatData.d.cts +1 -1
- package/dist/types/data/splatData.d.ts +1 -1
- package/dist/types/data/splatRange.d.cts +1 -1
- package/dist/types/data/splatRange.d.ts +1 -1
- package/dist/types/index.d.cts +2 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/loaders/SplatLoader.d.cts +2 -0
- package/dist/types/loaders/SplatLoader.d.ts +2 -0
- package/dist/types/loaders/loadSplatData.d.cts +3 -0
- package/dist/types/loaders/loadSplatData.d.ts +3 -0
- package/dist/types/loaders/loadTypes.d.cts +3 -0
- package/dist/types/loaders/loadTypes.d.ts +3 -0
- package/dist/types/loaders/morton.d.cts +6 -3
- package/dist/types/loaders/morton.d.ts +6 -3
- package/dist/types/loaders/postDecode/protocol.d.cts +1 -0
- package/dist/types/loaders/postDecode/protocol.d.ts +1 -0
- package/dist/types/loaders/rad.d.cts +5 -5
- package/dist/types/loaders/rad.d.ts +5 -5
- package/dist/types/loaders/stream/rad-stream/RadSelectionState.d.cts +2 -2
- package/dist/types/loaders/stream/rad-stream/RadSelectionState.d.ts +2 -2
- package/dist/types/loaders/stream/rad-stream/RadStreamLoader.d.cts +42 -16
- package/dist/types/loaders/stream/rad-stream/RadStreamLoader.d.ts +42 -16
- package/dist/types/loaders/stream/rad-stream/RadStreamScheduler.d.cts +6 -5
- package/dist/types/loaders/stream/rad-stream/RadStreamScheduler.d.ts +6 -5
- package/dist/types/loaders/stream/rad-stream/radFade.d.cts +2 -2
- package/dist/types/loaders/stream/rad-stream/radFade.d.ts +2 -2
- package/dist/types/loaders/stream/rad-stream/radLod.d.cts +11 -3
- package/dist/types/loaders/stream/rad-stream/radLod.d.ts +11 -3
- package/dist/types/loaders/stream/rad-stream/workerHandlers.d.cts +9 -3
- package/dist/types/loaders/stream/rad-stream/workerHandlers.d.ts +9 -3
- package/dist/types/loaders/stream/sog-stream/SogStreamBatch.d.cts +0 -1
- package/dist/types/loaders/stream/sog-stream/SogStreamBatch.d.ts +0 -1
- package/dist/types/loaders/stream/sog-stream/SogStreamLoader.d.cts +21 -5
- package/dist/types/loaders/stream/sog-stream/SogStreamLoader.d.ts +21 -5
- package/dist/types/loaders/stream/sog-stream/SogStreamScheduler.d.cts +21 -4
- package/dist/types/loaders/stream/sog-stream/SogStreamScheduler.d.ts +21 -4
- package/dist/types/loaders/stream/sog-stream/sogLod.d.cts +13 -11
- package/dist/types/loaders/stream/sog-stream/sogLod.d.ts +13 -11
- package/dist/types/loaders/stream/sog-stream/sogVisibility.d.cts +13 -7
- package/dist/types/loaders/stream/sog-stream/sogVisibility.d.ts +13 -7
- package/dist/types/loaders/stream/sog-stream/workerHandlers.d.cts +8 -13
- package/dist/types/loaders/stream/sog-stream/workerHandlers.d.ts +8 -13
- package/dist/types/loaders/stream/streamOptions.d.cts +9 -3
- package/dist/types/loaders/stream/streamOptions.d.ts +9 -3
- package/dist/types/scene/SplatMesh.d.cts +5 -4
- package/dist/types/scene/SplatMesh.d.ts +5 -4
- package/docs/BuildRad.md +23 -1
- package/docs/RadStreamScheduler.md +34 -9
- package/docs/SogStreamScheduler.md +47 -11
- package/docs/SplatFileType.md +1 -1
- package/docs/SplatLoader.md +1 -0
- package/docs/SplatMesh.md +5 -4
- package/docs/Splats.md +2 -3
- package/package.json +2 -2
package/bin/rad/parallel.js
CHANGED
|
@@ -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";
|
|
@@ -60,6 +59,10 @@ const WORKER_BYTES = 40 * 1024 ** 2;
|
|
|
60
59
|
const ENCODER_BYTES = 52 * 1024 ** 2;
|
|
61
60
|
const MAIN_BYTES = 64 * 1024 ** 2;
|
|
62
61
|
const CHUNKS_BYTES = 24 * 1024 ** 2;
|
|
62
|
+
// What an SH clustering worker thread takes besides its kernel and jobs,
|
|
63
|
+
// and the cache that clustering keeps: it reads rows in order.
|
|
64
|
+
const CLUSTER_BYTES = 32 * 1024 ** 2;
|
|
65
|
+
const STREAM_BYTES = 64 * 1024 ** 2;
|
|
63
66
|
export const YOUNG_MB = 8;
|
|
64
67
|
|
|
65
68
|
// Worker threads with `bytes` each: at most 8, using at most `share` of the
|
|
@@ -88,6 +91,79 @@ export function encoders(workspace, chunks) {
|
|
|
88
91
|
return count;
|
|
89
92
|
}
|
|
90
93
|
|
|
94
|
+
// Makes room for SH clustering worker threads that take `bytes` each besides
|
|
95
|
+
// the thread itself, and `extra` bytes on this thread, and returns how many
|
|
96
|
+
// there are, at most `jobs`. They only compute, so they may use every other
|
|
97
|
+
// core and all the room but a small cache.
|
|
98
|
+
export function clusterers(workspace, bytes, extra, jobs) {
|
|
99
|
+
workspace.settle();
|
|
100
|
+
const each = CLUSTER_BYTES + bytes;
|
|
101
|
+
const count = Math.max(
|
|
102
|
+
0,
|
|
103
|
+
Math.min(
|
|
104
|
+
availableParallelism() - 1,
|
|
105
|
+
jobs,
|
|
106
|
+
Math.floor((workspace.room - extra - STREAM_BYTES) / each),
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
workspace.expect(count * each + extra);
|
|
110
|
+
return count;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Ordered request/reply workers for clustering and encoding. Their callers
|
|
114
|
+
// schedule the jobs and close the workers; without workers, requests use the
|
|
115
|
+
// local handler. Keep workers referenced so exit events can reject requests.
|
|
116
|
+
export class WorkerGroup {
|
|
117
|
+
constructor(url, count, workerData, local, name) {
|
|
118
|
+
this.local = local;
|
|
119
|
+
this.workers = Array.from({ length: count }, () => {
|
|
120
|
+
const worker = new Worker(url, {
|
|
121
|
+
workerData,
|
|
122
|
+
resourceLimits: {
|
|
123
|
+
maxOldGenerationSizeMb: 64,
|
|
124
|
+
maxYoungGenerationSizeMb: YOUNG_MB,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
worker.waiting = [];
|
|
128
|
+
worker.on("message", (result) => worker.waiting.shift().resolve(result));
|
|
129
|
+
// A failed worker fails its pending and later requests.
|
|
130
|
+
const fail = (error) => {
|
|
131
|
+
worker.failed ??= error;
|
|
132
|
+
for (const job of worker.waiting.splice(0)) job.reject(worker.failed);
|
|
133
|
+
};
|
|
134
|
+
worker.on("error", fail);
|
|
135
|
+
worker.on("exit", (code) =>
|
|
136
|
+
fail(new Error(`${name} worker exited (${code})`)),
|
|
137
|
+
);
|
|
138
|
+
return worker;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
get threads() {
|
|
143
|
+
return this.workers.length ? this.workers : [undefined];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
request(worker, message, transfer = []) {
|
|
147
|
+
if (!worker) return Promise.resolve(this.local.handle(message));
|
|
148
|
+
if (worker.failed) return Promise.reject(worker.failed);
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
worker.waiting.push({ resolve, reject });
|
|
151
|
+
worker.postMessage(message, transfer);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
broadcast(message) {
|
|
156
|
+
return Promise.all(
|
|
157
|
+
this.threads.map((worker) => this.request(worker, message)),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Resolves once the workers have stopped and their memory is free.
|
|
162
|
+
close() {
|
|
163
|
+
return Promise.all(this.workers.map((worker) => worker.terminate()));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
91
167
|
const shared = (Type, size) =>
|
|
92
168
|
new Type(new SharedArrayBuffer(size * Type.BYTES_PER_ELEMENT));
|
|
93
169
|
|
|
@@ -231,14 +307,15 @@ export class Pool {
|
|
|
231
307
|
this.run(COPY, count, 1024);
|
|
232
308
|
}
|
|
233
309
|
|
|
234
|
-
// Refreshes and
|
|
235
|
-
//
|
|
236
|
-
check(splats, base, count) {
|
|
310
|
+
// Refreshes, checks and optionally crops `count` splats from `base` in
|
|
311
|
+
// place, storing their filter states in `results`; see Splats.retain().
|
|
312
|
+
check(splats, base, count, withinDist) {
|
|
237
313
|
this.share({
|
|
238
314
|
splatRecords: splats.records.directory,
|
|
239
315
|
splatSh: splats.sh
|
|
240
316
|
.slice(0, splats.maxSh)
|
|
241
317
|
.map((column) => column.directory),
|
|
318
|
+
withinDist,
|
|
242
319
|
});
|
|
243
320
|
Atomics.store(this.control, BASE, base);
|
|
244
321
|
this.run(CHECK, count, 1024);
|
|
@@ -370,6 +447,7 @@ class Task {
|
|
|
370
447
|
if (state.column)
|
|
371
448
|
this.column = new SharedColumn(views, state.column, Uint16Array);
|
|
372
449
|
if (state.splatRecords) {
|
|
450
|
+
this.withinDist = state.withinDist;
|
|
373
451
|
this.splatRecords = new SharedColumn(
|
|
374
452
|
views,
|
|
375
453
|
state.splatRecords,
|
|
@@ -450,7 +528,7 @@ class Task {
|
|
|
450
528
|
for (let d = 0; d < dims; d++)
|
|
451
529
|
if (!Number.isFinite(HALF[row[at + d]])) return INVALID;
|
|
452
530
|
}
|
|
453
|
-
return record
|
|
531
|
+
return filterRecord(record, this.withinDist);
|
|
454
532
|
}
|
|
455
533
|
|
|
456
534
|
copy(from, to) {
|
package/bin/rad/rad.js
CHANGED
|
@@ -10,20 +10,22 @@ import {
|
|
|
10
10
|
statSync,
|
|
11
11
|
} from "node:fs";
|
|
12
12
|
import path from "node:path";
|
|
13
|
-
import {
|
|
14
|
-
Worker,
|
|
15
|
-
isMainThread,
|
|
16
|
-
parentPort,
|
|
17
|
-
workerData,
|
|
18
|
-
} from "node:worker_threads";
|
|
13
|
+
import { isMainThread, parentPort, workerData } from "node:worker_threads";
|
|
19
14
|
import { deflateRawSync } from "node:zlib";
|
|
20
15
|
import { HALF, half, halfBits, opacityRadius } from "./math.js";
|
|
21
|
-
import {
|
|
16
|
+
import { WorkerGroup, encoders } from "./parallel.js";
|
|
22
17
|
import { Progress } from "./progress.js";
|
|
23
18
|
import { SH_DIMS } from "./splats.js";
|
|
24
19
|
import { readAll, removePath, writeAll } from "./storage.js";
|
|
25
20
|
|
|
26
21
|
const CHUNK = 65536;
|
|
22
|
+
// Fixed s8 range with SPZ's 1/128 SH step; -1 clips to -127/128.
|
|
23
|
+
const SH_MAX = 127 / 128;
|
|
24
|
+
function encodeSh(value) {
|
|
25
|
+
const magnitude = Math.min(127, Math.round(Math.abs(value) * 128));
|
|
26
|
+
return (value < 0 ? -magnitude : magnitude) & 255;
|
|
27
|
+
}
|
|
28
|
+
const SH_CODES = Uint8Array.from(HALF, encodeSh);
|
|
27
29
|
function header(magic, meta) {
|
|
28
30
|
const json = Buffer.from(JSON.stringify(meta));
|
|
29
31
|
const result = Buffer.alloc(8 + Math.ceil(json.length / 8) * 8);
|
|
@@ -99,18 +101,15 @@ function alpha(value) {
|
|
|
99
101
|
? half(Math.min(2, 1 + 0.25 * (opacityRadius(value) - 1)))
|
|
100
102
|
: value;
|
|
101
103
|
}
|
|
102
|
-
//
|
|
104
|
+
// The base attributes that determine the encoding ranges.
|
|
103
105
|
class Counts {
|
|
104
106
|
constructor() {
|
|
105
107
|
this.colors = new Histogram();
|
|
106
108
|
this.scales = new Histogram();
|
|
107
|
-
this.sh = SH_DIMS.map(() => new Histogram());
|
|
108
109
|
this.alphaF16 = false;
|
|
109
110
|
}
|
|
110
|
-
add(
|
|
111
|
-
|
|
112
|
-
for (let i = 0; i < count; i++) {
|
|
113
|
-
const offset = i * 16;
|
|
111
|
+
add(records) {
|
|
112
|
+
for (let offset = 0; offset < records.length; offset += 16) {
|
|
114
113
|
this.alphaF16 ||= alpha(records[offset + 3]) > 1;
|
|
115
114
|
for (let d = 4; d <= 6; d++)
|
|
116
115
|
this.colors.add(halfBits(records[offset + d]));
|
|
@@ -122,19 +121,15 @@ class Counts {
|
|
|
122
121
|
);
|
|
123
122
|
this.scales.add(halfBits(Math.max(a, b, c)));
|
|
124
123
|
}
|
|
125
|
-
for (let band = 0; band < chunk.sh.length; band++)
|
|
126
|
-
for (const bits of chunk.sh[band]) this.sh[band].add(bits);
|
|
127
124
|
}
|
|
128
125
|
merge(other) {
|
|
129
126
|
this.colors.merge(other.colors);
|
|
130
127
|
this.scales.merge(other.scales);
|
|
131
|
-
for (let band = 0; band < SH_DIMS.length; band++)
|
|
132
|
-
this.sh[band].merge(other.sh[band]);
|
|
133
128
|
this.alphaF16 ||= other.alphaF16;
|
|
134
129
|
}
|
|
135
130
|
}
|
|
136
131
|
|
|
137
|
-
function encoding(counts
|
|
132
|
+
function encoding(counts) {
|
|
138
133
|
const { colors, scales } = counts;
|
|
139
134
|
const rgbMin = Math.min(0, colors.percentile(0.01));
|
|
140
135
|
const rgbMax = Math.max(1, colors.percentile(0.99));
|
|
@@ -143,69 +138,67 @@ function encoding(counts, maxSh) {
|
|
|
143
138
|
Math.max(Math.log(1e-30), scales.percentile(0.01)),
|
|
144
139
|
);
|
|
145
140
|
const lnScaleMax = Math.max(9, scales.percentile(0.99));
|
|
146
|
-
const limits = [1, 1, 1];
|
|
147
|
-
for (let band = 0; band < maxSh; band++) {
|
|
148
|
-
const histogram = counts.sh[band];
|
|
149
|
-
limits[band] = Math.max(
|
|
150
|
-
1,
|
|
151
|
-
Math.abs(histogram.percentile(0.05)),
|
|
152
|
-
Math.abs(histogram.percentile(0.95)),
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
// The s8 code of each half value, per band.
|
|
156
|
-
const sh = limits.map((limit) => {
|
|
157
|
-
const codes = new Uint8Array(65536);
|
|
158
|
-
for (let bits = 0; bits < 65536; bits++)
|
|
159
|
-
codes[bits] =
|
|
160
|
-
signedRound(Math.max(-127, Math.min(127, (HALF[bits] / limit) * 127))) &
|
|
161
|
-
255;
|
|
162
|
-
return codes;
|
|
163
|
-
});
|
|
164
141
|
return {
|
|
165
142
|
alphaF16: counts.alphaF16,
|
|
166
143
|
rgbF16: rgbMin < -1 || rgbMax > 2,
|
|
167
|
-
sh,
|
|
168
144
|
scaleF16: lnScaleMax - lnScaleMin > 25,
|
|
169
|
-
limits,
|
|
170
145
|
meta: {
|
|
171
146
|
rgbMin,
|
|
172
147
|
rgbMax,
|
|
173
148
|
lnScaleMin,
|
|
174
149
|
lnScaleMax,
|
|
175
|
-
sh1Max:
|
|
176
|
-
sh2Max:
|
|
177
|
-
sh3Max:
|
|
150
|
+
sh1Max: SH_MAX,
|
|
151
|
+
sh2Max: SH_MAX,
|
|
152
|
+
sh3Max: SH_MAX,
|
|
178
153
|
lodOpacity: true,
|
|
179
154
|
},
|
|
180
155
|
};
|
|
181
156
|
}
|
|
182
157
|
const unorm = (value, min, max) =>
|
|
183
158
|
Math.round(Math.max(0, Math.min(255, ((value - min) / (max - min)) * 255)));
|
|
184
|
-
const signedRound = (value) =>
|
|
185
|
-
value < 0 ? -Math.round(-value) : Math.round(value);
|
|
186
159
|
|
|
187
|
-
// The columns
|
|
188
|
-
|
|
189
|
-
function gather(splats, base, count, children = true) {
|
|
160
|
+
// The columns encodeChunk() reads. With a codebook, labels replace the SH.
|
|
161
|
+
function gather(splats, base, count, codebook) {
|
|
190
162
|
return {
|
|
191
163
|
base,
|
|
192
164
|
count,
|
|
165
|
+
maxSh: splats.maxSh,
|
|
193
166
|
records: splats.records.read(base * 16, new Float32Array(count * 16)),
|
|
194
|
-
sh:
|
|
195
|
-
|
|
196
|
-
.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
167
|
+
sh: codebook
|
|
168
|
+
? []
|
|
169
|
+
: splats.sh
|
|
170
|
+
.slice(0, splats.maxSh)
|
|
171
|
+
.map((column, band) =>
|
|
172
|
+
column.read(
|
|
173
|
+
base * SH_DIMS[band],
|
|
174
|
+
new Uint16Array(count * SH_DIMS[band]),
|
|
175
|
+
),
|
|
176
|
+
),
|
|
177
|
+
...(codebook && {
|
|
178
|
+
labels: codebook.labels.read(base, new Uint16Array(count)),
|
|
205
179
|
}),
|
|
180
|
+
counts: splats.childCount.read(base, new Uint32Array(count)),
|
|
181
|
+
starts: splats.childStart.read(base, new Float64Array(count)),
|
|
206
182
|
};
|
|
207
183
|
}
|
|
208
184
|
|
|
185
|
+
// The codebook's entries as s8 codes of each band, coefficient by coefficient.
|
|
186
|
+
function encodeCodes(codebook, maxSh) {
|
|
187
|
+
const { count, dims, codes } = codebook;
|
|
188
|
+
const bands = [];
|
|
189
|
+
let offset = 0;
|
|
190
|
+
for (let band = 0; band < maxSh; band++) {
|
|
191
|
+
const size = SH_DIMS[band];
|
|
192
|
+
const bytes = new Uint8Array(count * size);
|
|
193
|
+
for (let d = 0; d < size; d++)
|
|
194
|
+
for (let c = 0; c < count; c++)
|
|
195
|
+
bytes[d * count + c] = encodeSh(codes[c * dims + offset + d]);
|
|
196
|
+
bands.push(bytes);
|
|
197
|
+
offset += size;
|
|
198
|
+
}
|
|
199
|
+
return bands;
|
|
200
|
+
}
|
|
201
|
+
|
|
209
202
|
// Raw property bytes, reused: each property is compressed before the next.
|
|
210
203
|
const scratch = [Buffer.alloc(0), Buffer.alloc(0)];
|
|
211
204
|
function raw(size, slot = 0) {
|
|
@@ -326,18 +319,25 @@ function encodeChunk(chunk, encoding) {
|
|
|
326
319
|
bytes[i * 3 + 2] = unorm(theta, 0, Math.PI);
|
|
327
320
|
}
|
|
328
321
|
property("orientation", "oct88r8", bytes);
|
|
322
|
+
if (chunk.labels) {
|
|
323
|
+
// The first chunk holds the codebook for all.
|
|
324
|
+
for (let band = 0; chunk.codes && band < chunk.maxSh; band++)
|
|
325
|
+
property(`sh${band + 1}_code`, "s8", chunk.codes[band], [
|
|
326
|
+
-SH_MAX,
|
|
327
|
+
SH_MAX,
|
|
328
|
+
]);
|
|
329
|
+
bytes = raw(count * 2);
|
|
330
|
+
for (let i = 0; i < count; i++) bytes.writeUInt16LE(chunk.labels[i], i * 2);
|
|
331
|
+
property("sh_label", "u16", bytes);
|
|
332
|
+
}
|
|
329
333
|
for (let band = 0; band < chunk.sh.length; band++) {
|
|
330
334
|
const dims = SH_DIMS[band];
|
|
331
335
|
const values = chunk.sh[band];
|
|
332
|
-
const codes = encoding.sh[band];
|
|
333
336
|
bytes = raw(count * dims);
|
|
334
337
|
for (let d = 0; d < dims; d++)
|
|
335
338
|
for (let i = 0; i < count; i++)
|
|
336
|
-
bytes[d * count + i] =
|
|
337
|
-
property(`sh${band + 1}`, "s8", bytes, [
|
|
338
|
-
-encoding.limits[band],
|
|
339
|
-
encoding.limits[band],
|
|
340
|
-
]);
|
|
339
|
+
bytes[d * count + i] = SH_CODES[values[i * dims + d]];
|
|
340
|
+
property(`sh${band + 1}`, "s8", bytes, [-SH_MAX, SH_MAX]);
|
|
341
341
|
}
|
|
342
342
|
const counts = raw(count * 2);
|
|
343
343
|
const starts = raw(count * 4, 1);
|
|
@@ -352,7 +352,7 @@ function encodeChunk(chunk, encoding) {
|
|
|
352
352
|
version: 1,
|
|
353
353
|
base,
|
|
354
354
|
count,
|
|
355
|
-
maxSh: chunk.
|
|
355
|
+
maxSh: chunk.maxSh,
|
|
356
356
|
lodTree: true,
|
|
357
357
|
splatEncoding: encoding.meta,
|
|
358
358
|
payloadBytes,
|
|
@@ -369,8 +369,8 @@ class Coder {
|
|
|
369
369
|
this.counts = new Counts();
|
|
370
370
|
this.format = undefined;
|
|
371
371
|
}
|
|
372
|
-
handle({ type, chunk, format }) {
|
|
373
|
-
if (type === "count") this.counts.add(
|
|
372
|
+
handle({ type, records, chunk, format }) {
|
|
373
|
+
if (type === "count") this.counts.add(records);
|
|
374
374
|
else if (type === "counts") return this.counts;
|
|
375
375
|
else if (type === "format") this.format = format;
|
|
376
376
|
else return encodeChunk(chunk, this.format);
|
|
@@ -379,62 +379,33 @@ class Coder {
|
|
|
379
379
|
}
|
|
380
380
|
|
|
381
381
|
// Spreads counting and encoding over worker threads.
|
|
382
|
-
class Encoder {
|
|
382
|
+
class Encoder extends WorkerGroup {
|
|
383
383
|
// Encodes `chunks` with workers when there is more than one and the
|
|
384
384
|
// workspace has room for them.
|
|
385
385
|
constructor(workspace, chunks) {
|
|
386
|
+
const count = encoders(workspace, chunks);
|
|
387
|
+
super(
|
|
388
|
+
new URL(import.meta.url),
|
|
389
|
+
count,
|
|
390
|
+
{ coder: true },
|
|
391
|
+
count ? undefined : new Coder(),
|
|
392
|
+
"Encoding",
|
|
393
|
+
);
|
|
386
394
|
this.next = 0;
|
|
387
|
-
this.coder = new Coder();
|
|
388
|
-
this.workers = Array.from({ length: encoders(workspace, chunks) }, () => {
|
|
389
|
-
const worker = new Worker(new URL(import.meta.url), {
|
|
390
|
-
workerData: { coder: true },
|
|
391
|
-
resourceLimits: {
|
|
392
|
-
maxOldGenerationSizeMb: 64,
|
|
393
|
-
maxYoungGenerationSizeMb: YOUNG_MB,
|
|
394
|
-
},
|
|
395
|
-
});
|
|
396
|
-
worker.unref();
|
|
397
|
-
worker.waiting = [];
|
|
398
|
-
worker.on("message", (result) => worker.waiting.shift().resolve(result));
|
|
399
|
-
// A failed worker fails its pending and later requests.
|
|
400
|
-
const fail = (error) => {
|
|
401
|
-
worker.failed ??= error;
|
|
402
|
-
for (const job of worker.waiting.splice(0)) job.reject(worker.failed);
|
|
403
|
-
};
|
|
404
|
-
worker.on("error", fail);
|
|
405
|
-
worker.on("exit", (code) =>
|
|
406
|
-
fail(new Error(`Encoding worker exited (${code})`)),
|
|
407
|
-
);
|
|
408
|
-
return worker;
|
|
409
|
-
});
|
|
410
395
|
}
|
|
411
396
|
get size() {
|
|
412
397
|
return this.workers.length;
|
|
413
398
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (worker.failed) return Promise.reject(worker.failed);
|
|
417
|
-
return new Promise((resolve, reject) => {
|
|
418
|
-
worker.waiting.push({ resolve, reject });
|
|
419
|
-
worker.postMessage(message, transfer);
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
// Sends a gathered chunk to the next worker.
|
|
423
|
-
send(type, chunk) {
|
|
399
|
+
// Sends the next job to the next worker.
|
|
400
|
+
send(message, transfer) {
|
|
424
401
|
return this.request(
|
|
425
402
|
this.workers[this.next++ % this.workers.length],
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
chunk.records.buffer,
|
|
429
|
-
...chunk.sh.map((values) => values.buffer),
|
|
430
|
-
...(chunk.counts ? [chunk.counts.buffer, chunk.starts.buffer] : []),
|
|
431
|
-
],
|
|
403
|
+
message,
|
|
404
|
+
transfer,
|
|
432
405
|
);
|
|
433
406
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const all = this.workers.length ? this.workers : [undefined];
|
|
437
|
-
return Promise.all(all.map((worker) => this.request(worker, message)));
|
|
407
|
+
count(records) {
|
|
408
|
+
return this.send({ type: "count", records }, [records.buffer]);
|
|
438
409
|
}
|
|
439
410
|
async counts() {
|
|
440
411
|
const counts = new Counts();
|
|
@@ -446,13 +417,16 @@ class Encoder {
|
|
|
446
417
|
return this.broadcast({ type: "format", format });
|
|
447
418
|
}
|
|
448
419
|
async encode(chunk) {
|
|
449
|
-
const bytes = await this.send("encode", chunk
|
|
420
|
+
const bytes = await this.send({ type: "encode", chunk }, [
|
|
421
|
+
chunk.records.buffer,
|
|
422
|
+
...chunk.sh.map((values) => values.buffer),
|
|
423
|
+
...(chunk.labels ? [chunk.labels.buffer] : []),
|
|
424
|
+
...(chunk.codes ?? []).map((values) => values.buffer),
|
|
425
|
+
chunk.counts.buffer,
|
|
426
|
+
chunk.starts.buffer,
|
|
427
|
+
]);
|
|
450
428
|
return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.length);
|
|
451
429
|
}
|
|
452
|
-
// Resolves once the workers have stopped and their memory is free.
|
|
453
|
-
close() {
|
|
454
|
-
return Promise.all(this.workers.map((worker) => worker.terminate()));
|
|
455
|
-
}
|
|
456
430
|
}
|
|
457
431
|
|
|
458
432
|
if (!isMainThread && workerData?.coder) {
|
|
@@ -471,7 +445,8 @@ if (!isMainThread && workerData?.coder) {
|
|
|
471
445
|
});
|
|
472
446
|
}
|
|
473
447
|
|
|
474
|
-
|
|
448
|
+
// With a codebook from clusterSh(), splats store SH as its labels.
|
|
449
|
+
export async function writeRad(splats, output, chunked, lodBase, codebook) {
|
|
475
450
|
const chunks = [];
|
|
476
451
|
const stem = chunkStem(output);
|
|
477
452
|
const spool = chunked ? undefined : splats.workspace.file();
|
|
@@ -529,26 +504,34 @@ export async function writeRad(splats, output, chunked, lodBase) {
|
|
|
529
504
|
let countedChunks = 0;
|
|
530
505
|
for (let base = 0; base < splats.length; base += CHUNK) {
|
|
531
506
|
splats.workspace.check();
|
|
507
|
+
const count = Math.min(CHUNK, splats.length - base);
|
|
508
|
+
const records = splats.records.read(
|
|
509
|
+
base * 16,
|
|
510
|
+
new Float32Array(count * 16),
|
|
511
|
+
);
|
|
532
512
|
const counted = encoder
|
|
533
|
-
.
|
|
534
|
-
"count",
|
|
535
|
-
gather(splats, base, Math.min(CHUNK, splats.length - base), false),
|
|
536
|
-
)
|
|
513
|
+
.count(records)
|
|
537
514
|
.then(() => progress.update(++countedChunks));
|
|
538
515
|
counted.catch(() => {});
|
|
539
516
|
pending.push(counted);
|
|
540
517
|
if (pending.length > encoder.size + 1) await pending.shift();
|
|
541
518
|
}
|
|
542
519
|
while (pending.length) await pending.shift();
|
|
543
|
-
const format = encoding(await encoder.counts()
|
|
520
|
+
const format = encoding(await encoder.counts());
|
|
544
521
|
await encoder.use(format);
|
|
545
522
|
progress.finish();
|
|
546
523
|
progress = new Progress("Encoding RAD chunks", chunkCount);
|
|
547
524
|
for (let base = 0; base < splats.length; base += CHUNK) {
|
|
548
525
|
splats.workspace.check();
|
|
549
|
-
const
|
|
550
|
-
|
|
526
|
+
const chunk = gather(
|
|
527
|
+
splats,
|
|
528
|
+
base,
|
|
529
|
+
Math.min(CHUNK, splats.length - base),
|
|
530
|
+
codebook,
|
|
551
531
|
);
|
|
532
|
+
if (codebook && base === 0)
|
|
533
|
+
chunk.codes = encodeCodes(codebook, splats.maxSh);
|
|
534
|
+
const bytes = encoder.encode(chunk);
|
|
552
535
|
bytes.catch(() => {});
|
|
553
536
|
pending.push(bytes);
|
|
554
537
|
if (pending.length > encoder.size + 1) store(await pending.shift());
|
|
@@ -565,6 +548,7 @@ export async function writeRad(splats, output, chunked, lodBase) {
|
|
|
565
548
|
allChunkBytes: total,
|
|
566
549
|
chunks,
|
|
567
550
|
splatEncoding: format.meta,
|
|
551
|
+
...(codebook && { shCodeCount: codebook.count }),
|
|
568
552
|
comment: `Gaussian Splat Lite quality LOD (base ${lodBase})`,
|
|
569
553
|
});
|
|
570
554
|
const writing = new Progress(
|
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 =
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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]);
|
package/bin/rad/storage.js
CHANGED
|
@@ -129,7 +129,10 @@ export class Workspace {
|
|
|
129
129
|
shrink() {
|
|
130
130
|
if (this.slots.length * PAGE_BYTES <= this.capacity) return;
|
|
131
131
|
const slots = Math.floor(this.capacity / this.arenaBytes) * this.arenaPages;
|
|
132
|
-
|
|
132
|
+
// Without a copy of the slots: a copy kept the released pages alive
|
|
133
|
+
// through the collection below, and their memory returned only later.
|
|
134
|
+
for (let slot = slots; slot < this.slots.length; slot++)
|
|
135
|
+
if (this.slots[slot]) this.drop(this.slots[slot]);
|
|
133
136
|
this.slots.length = slots;
|
|
134
137
|
this.reuse.length = slots;
|
|
135
138
|
this.free = this.free.filter((slot) => slot < slots);
|