@loaders.gl/splats 4.5.0-alpha.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.
- package/LICENSE +41 -0
- package/README.md +3 -0
- package/dist/bundled.d.ts +2 -0
- package/dist/bundled.d.ts.map +1 -0
- package/dist/bundled.js +5 -0
- package/dist/bundled.js.map +1 -0
- package/dist/dist.dev.js +33741 -0
- package/dist/dist.min.js +82 -0
- package/dist/index.cjs +1786 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/ksplat-loader-types.d.ts +19 -0
- package/dist/ksplat-loader-types.d.ts.map +1 -0
- package/dist/ksplat-loader-types.js +20 -0
- package/dist/ksplat-loader-types.js.map +1 -0
- package/dist/ksplat-loader.d.ts +21 -0
- package/dist/ksplat-loader.d.ts.map +1 -0
- package/dist/ksplat-loader.js +12 -0
- package/dist/ksplat-loader.js.map +1 -0
- package/dist/lib/parse-ksplat.d.ts +7 -0
- package/dist/lib/parse-ksplat.d.ts.map +1 -0
- package/dist/lib/parse-ksplat.js +312 -0
- package/dist/lib/parse-ksplat.js.map +1 -0
- package/dist/lib/parse-rad-chunk.d.ts +19 -0
- package/dist/lib/parse-rad-chunk.d.ts.map +1 -0
- package/dist/lib/parse-rad-chunk.js +514 -0
- package/dist/lib/parse-rad-chunk.js.map +1 -0
- package/dist/lib/parse-rad.d.ts +136 -0
- package/dist/lib/parse-rad.d.ts.map +1 -0
- package/dist/lib/parse-rad.js +110 -0
- package/dist/lib/parse-rad.js.map +1 -0
- package/dist/lib/parse-splat.d.ts +7 -0
- package/dist/lib/parse-splat.d.ts.map +1 -0
- package/dist/lib/parse-splat.js +58 -0
- package/dist/lib/parse-splat.js.map +1 -0
- package/dist/lib/parse-spz.d.ts +7 -0
- package/dist/lib/parse-spz.d.ts.map +1 -0
- package/dist/lib/parse-spz.js +268 -0
- package/dist/lib/parse-spz.js.map +1 -0
- package/dist/lib/splat-utils.d.ts +12 -0
- package/dist/lib/splat-utils.d.ts.map +1 -0
- package/dist/lib/splat-utils.js +38 -0
- package/dist/lib/splat-utils.js.map +1 -0
- package/dist/lib/splats-arrow-table.d.ts +5 -0
- package/dist/lib/splats-arrow-table.d.ts.map +1 -0
- package/dist/lib/splats-arrow-table.js +133 -0
- package/dist/lib/splats-arrow-table.js.map +1 -0
- package/dist/rad-loader-types.d.ts +16 -0
- package/dist/rad-loader-types.d.ts.map +1 -0
- package/dist/rad-loader-types.js +16 -0
- package/dist/rad-loader-types.js.map +1 -0
- package/dist/rad-loader.d.ts +20 -0
- package/dist/rad-loader.d.ts.map +1 -0
- package/dist/rad-loader.js +14 -0
- package/dist/rad-loader.js.map +1 -0
- package/dist/rad-metadata.schema.json +133 -0
- package/dist/rad-source-loader.d.ts +112 -0
- package/dist/rad-source-loader.d.ts.map +1 -0
- package/dist/rad-source-loader.js +329 -0
- package/dist/rad-source-loader.js.map +1 -0
- package/dist/rad-zod-schema.cjs +81 -0
- package/dist/rad-zod-schema.cjs.map +7 -0
- package/dist/rad-zod-schema.d.ts +88 -0
- package/dist/rad-zod-schema.d.ts.map +1 -0
- package/dist/rad-zod-schema.js +70 -0
- package/dist/rad-zod-schema.js.map +1 -0
- package/dist/radc-metadata.schema.json +112 -0
- package/dist/splat-loader-types.d.ts +19 -0
- package/dist/splat-loader-types.d.ts.map +1 -0
- package/dist/splat-loader-types.js +20 -0
- package/dist/splat-loader-types.js.map +1 -0
- package/dist/splat-loader.d.ts +21 -0
- package/dist/splat-loader.d.ts.map +1 -0
- package/dist/splat-loader.js +12 -0
- package/dist/splat-loader.js.map +1 -0
- package/dist/splats-format.d.ts +39 -0
- package/dist/splats-format.d.ts.map +1 -0
- package/dist/splats-format.js +42 -0
- package/dist/splats-format.js.map +1 -0
- package/dist/spz-loader-types.d.ts +20 -0
- package/dist/spz-loader-types.d.ts.map +1 -0
- package/dist/spz-loader-types.js +20 -0
- package/dist/spz-loader-types.js.map +1 -0
- package/dist/spz-loader.d.ts +22 -0
- package/dist/spz-loader.d.ts.map +1 -0
- package/dist/spz-loader.js +11 -0
- package/dist/spz-loader.js.map +1 -0
- package/dist/types.d.ts +34 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/unbundled.d.ts +2 -0
- package/dist/unbundled.d.ts.map +1 -0
- package/dist/unbundled.js +5 -0
- package/dist/unbundled.js.map +1 -0
- package/package.json +94 -0
- package/src/bundled.ts +5 -0
- package/src/index.ts +28 -0
- package/src/ksplat-loader-types.ts +25 -0
- package/src/ksplat-loader.ts +16 -0
- package/src/lib/parse-ksplat.ts +540 -0
- package/src/lib/parse-rad-chunk.ts +721 -0
- package/src/lib/parse-rad.ts +318 -0
- package/src/lib/parse-splat.ts +77 -0
- package/src/lib/parse-spz.ts +368 -0
- package/src/lib/splat-utils.ts +49 -0
- package/src/lib/splats-arrow-table.ts +190 -0
- package/src/rad-loader-types.ts +20 -0
- package/src/rad-loader.ts +22 -0
- package/src/rad-source-loader.ts +491 -0
- package/src/rad-zod-schema.ts +83 -0
- package/src/splat-loader-types.ts +25 -0
- package/src/splat-loader.ts +16 -0
- package/src/splats-format.ts +47 -0
- package/src/spz-loader-types.ts +25 -0
- package/src/spz-loader.ts +16 -0
- package/src/types.ts +39 -0
- package/src/unbundled.ts +5 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {DataSourceOptions} from '@loaders.gl/loader-utils';
|
|
6
|
+
import {DataSource} from '@loaders.gl/loader-utils';
|
|
7
|
+
import type {MeshArrowTable} from '@loaders.gl/schema';
|
|
8
|
+
import {RADFormat} from './splats-format';
|
|
9
|
+
import {
|
|
10
|
+
parseRADChunkHeader,
|
|
11
|
+
tryParseRADHeader,
|
|
12
|
+
type RADChunkMetadata,
|
|
13
|
+
type RADChunkRange,
|
|
14
|
+
type RADMetadata
|
|
15
|
+
} from './lib/parse-rad';
|
|
16
|
+
import {
|
|
17
|
+
parseRADChunk,
|
|
18
|
+
parseRADChunkToGaussianSplats,
|
|
19
|
+
type RADChunkDecodeOptions
|
|
20
|
+
} from './lib/parse-rad-chunk';
|
|
21
|
+
import {makeGaussianSplatsArrowTable} from './lib/splats-arrow-table';
|
|
22
|
+
import type {GaussianSplats} from './types';
|
|
23
|
+
|
|
24
|
+
const DEFAULT_RAD_HEADER_BYTE_LENGTHS = [64 * 1024, 256 * 1024, 1024 * 1024];
|
|
25
|
+
const DEFAULT_RAD_MAX_CONCURRENT_CHUNK_REQUESTS = 4;
|
|
26
|
+
|
|
27
|
+
// __VERSION__ is injected by babel-plugin-version-inline
|
|
28
|
+
// @ts-ignore TS2304: Cannot find name '__VERSION__'.
|
|
29
|
+
const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'latest';
|
|
30
|
+
|
|
31
|
+
/** Options for creating a Spark RAD source. */
|
|
32
|
+
export type RADSourceLoaderOptions = DataSourceOptions & {
|
|
33
|
+
rad?: {
|
|
34
|
+
/** Optional request headers forwarded to RAD and RADC fetches. */
|
|
35
|
+
headers?: Record<string, string>;
|
|
36
|
+
/** Whether remote RAD and RADC fetches should include credentials. */
|
|
37
|
+
withCredentials?: boolean;
|
|
38
|
+
/** Header byte lengths to try while probing remote RAD metadata. */
|
|
39
|
+
headerByteLengths?: number[];
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Optional parameters for fetching RAD chunk bytes or metadata. */
|
|
44
|
+
export type RADChunkRequestOptions = {
|
|
45
|
+
/** Abort signal forwarded to the underlying fetch. */
|
|
46
|
+
signal?: AbortSignal;
|
|
47
|
+
} & RADChunkDecodeOptions;
|
|
48
|
+
|
|
49
|
+
/** Options for iterating decoded RAD chunk tables. */
|
|
50
|
+
export type RADChunkTableIteratorOptions = RADChunkRequestOptions & {
|
|
51
|
+
/** First chunk index to decode. */
|
|
52
|
+
startChunkIndex?: number;
|
|
53
|
+
/** Maximum number of chunks to decode. */
|
|
54
|
+
maxChunks?: number;
|
|
55
|
+
/** Maximum number of splats to decode before stopping. */
|
|
56
|
+
maxSplats?: number;
|
|
57
|
+
/** Maximum number of RAD chunks to fetch and decode at once when the iterator must inspect a chunk window before yielding. */
|
|
58
|
+
maxConcurrentChunkRequests?: number;
|
|
59
|
+
/** Whether loaded child splats should replace parent LoD splats in returned tables. */
|
|
60
|
+
pruneLoadedLoDParents?: boolean;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Source factory for Spark `.rad` paged LoD Gaussian splat containers. */
|
|
64
|
+
export const RADSourceLoader = {
|
|
65
|
+
dataType: null as unknown as RADSource,
|
|
66
|
+
batchType: null as never,
|
|
67
|
+
...RADFormat,
|
|
68
|
+
name: 'RADSourceLoader',
|
|
69
|
+
version: VERSION,
|
|
70
|
+
type: 'rad',
|
|
71
|
+
fromUrl: true,
|
|
72
|
+
fromBlob: true,
|
|
73
|
+
|
|
74
|
+
options: {
|
|
75
|
+
rad: {
|
|
76
|
+
headers: undefined!,
|
|
77
|
+
withCredentials: false,
|
|
78
|
+
headerByteLengths: undefined!
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
defaultOptions: {
|
|
83
|
+
rad: {
|
|
84
|
+
headers: undefined!,
|
|
85
|
+
withCredentials: false,
|
|
86
|
+
headerByteLengths: undefined!
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
testURL: (url: string): boolean => /\.rad(?:$|[?#])/i.test(url),
|
|
91
|
+
createDataSource: (data: string | Blob, options: RADSourceLoaderOptions): RADSource =>
|
|
92
|
+
new RADSource(data, options)
|
|
93
|
+
} as const;
|
|
94
|
+
|
|
95
|
+
/** Runtime source for RAD metadata and chunk-byte access. */
|
|
96
|
+
export class RADSource extends DataSource<string | Blob, RADSourceLoaderOptions> {
|
|
97
|
+
/** Shared metadata promise exposed for source-style integrations. */
|
|
98
|
+
metadata: Promise<RADMetadata>;
|
|
99
|
+
|
|
100
|
+
/** Lazily initialized RAD header promise. */
|
|
101
|
+
private _metadataPromise: Promise<RADMetadata> | null = null;
|
|
102
|
+
/** Lazily initialized per-chunk metadata promises. */
|
|
103
|
+
private _chunkMetadataPromises: Map<number, Promise<RADChunkMetadata>> = new Map();
|
|
104
|
+
|
|
105
|
+
constructor(data: string | Blob, options: RADSourceLoaderOptions) {
|
|
106
|
+
super(data, options, RADSourceLoader.defaultOptions);
|
|
107
|
+
this.metadata = this.getMetadata();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Ensures the RAD header metadata has been loaded. */
|
|
111
|
+
async initialize(): Promise<void> {
|
|
112
|
+
await this.getMetadata();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Returns parsed Spark RAD top-level metadata. */
|
|
116
|
+
async getMetadata(): Promise<RADMetadata> {
|
|
117
|
+
if (!this._metadataPromise) {
|
|
118
|
+
this._metadataPromise = this._loadMetadata();
|
|
119
|
+
}
|
|
120
|
+
return await this._metadataPromise;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Returns the number of chunks declared by the RAD metadata. */
|
|
124
|
+
async getChunkCount(): Promise<number> {
|
|
125
|
+
const metadata = await this.getMetadata();
|
|
126
|
+
return metadata.chunks.length;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Returns the URL used for a chunk, or `null` for Blob-backed inline chunks. */
|
|
130
|
+
async getChunkUrl(chunkIndex: number): Promise<string | null> {
|
|
131
|
+
const metadata = await this.getMetadata();
|
|
132
|
+
const chunk = this._getChunkRange(metadata, chunkIndex);
|
|
133
|
+
if (chunk.filename) {
|
|
134
|
+
return resolveRADChunkUrl(this.url, chunk.filename);
|
|
135
|
+
}
|
|
136
|
+
return typeof this.data === 'string' ? this.url : null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Fetches one raw RADC chunk by index. */
|
|
140
|
+
async getChunk(chunkIndex: number, options: RADChunkRequestOptions = {}): Promise<ArrayBuffer> {
|
|
141
|
+
const metadata = await this.getMetadata();
|
|
142
|
+
const chunk = this._getChunkRange(metadata, chunkIndex);
|
|
143
|
+
|
|
144
|
+
if (chunk.filename) {
|
|
145
|
+
if (typeof this.data !== 'string') {
|
|
146
|
+
throw new Error('RADSource: sidecar RADC chunks require a URL-backed RAD source.');
|
|
147
|
+
}
|
|
148
|
+
return await this._fetchArrayBuffer(resolveRADChunkUrl(this.url, chunk.filename), undefined, {
|
|
149
|
+
signal: options.signal
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (chunk.bytes === 0) {
|
|
154
|
+
return new ArrayBuffer(0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const byteOffset = metadata.chunksByteOffset + chunk.offset;
|
|
158
|
+
if (typeof this.data !== 'string') {
|
|
159
|
+
return await this.data.slice(byteOffset, byteOffset + chunk.bytes).arrayBuffer();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return await this._fetchArrayBuffer(
|
|
163
|
+
this.url,
|
|
164
|
+
{offset: byteOffset, bytes: chunk.bytes},
|
|
165
|
+
{signal: options.signal}
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Fetches and parses metadata for one RADC chunk. */
|
|
170
|
+
async getChunkMetadata(
|
|
171
|
+
chunkIndex: number,
|
|
172
|
+
options: RADChunkRequestOptions = {}
|
|
173
|
+
): Promise<RADChunkMetadata> {
|
|
174
|
+
let chunkMetadataPromise = this._chunkMetadataPromises.get(chunkIndex);
|
|
175
|
+
if (!chunkMetadataPromise) {
|
|
176
|
+
chunkMetadataPromise = this.getChunk(chunkIndex, options).then((chunk) =>
|
|
177
|
+
parseRADChunkHeader(chunk)
|
|
178
|
+
);
|
|
179
|
+
this._chunkMetadataPromises.set(chunkIndex, chunkMetadataPromise);
|
|
180
|
+
}
|
|
181
|
+
return await chunkMetadataPromise;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Fetches and decodes one RADC chunk into raw Gaussian splat arrays. */
|
|
185
|
+
async getChunkSplats(
|
|
186
|
+
chunkIndex: number,
|
|
187
|
+
options: RADChunkRequestOptions = {}
|
|
188
|
+
): Promise<GaussianSplats> {
|
|
189
|
+
const metadata = await this.getMetadata();
|
|
190
|
+
return parseRADChunkToGaussianSplats(
|
|
191
|
+
await this.getChunk(chunkIndex, options),
|
|
192
|
+
this._getChunkDecodeOptions(metadata, options)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Fetches and decodes one RADC chunk into a Mesh Arrow table. */
|
|
197
|
+
async getChunkTable(
|
|
198
|
+
chunkIndex: number,
|
|
199
|
+
options: RADChunkRequestOptions = {}
|
|
200
|
+
): Promise<MeshArrowTable> {
|
|
201
|
+
const metadata = await this.getMetadata();
|
|
202
|
+
return parseRADChunk(
|
|
203
|
+
await this.getChunk(chunkIndex, options),
|
|
204
|
+
this._getChunkDecodeOptions(metadata, options)
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Iterates decoded RADC chunks as Mesh Arrow tables. */
|
|
209
|
+
async *getChunkTables(options: RADChunkTableIteratorOptions = {}): AsyncIterable<MeshArrowTable> {
|
|
210
|
+
const metadata = await this.getMetadata();
|
|
211
|
+
const startChunkIndex = options.startChunkIndex ?? 0;
|
|
212
|
+
const maxChunks = options.maxChunks ?? Number.POSITIVE_INFINITY;
|
|
213
|
+
const maxSplats = options.maxSplats ?? Number.POSITIVE_INFINITY;
|
|
214
|
+
let chunkCount = 0;
|
|
215
|
+
let splatCount = 0;
|
|
216
|
+
|
|
217
|
+
if (options.pruneLoadedLoDParents) {
|
|
218
|
+
const chunkIndices = getRADChunkTableIteratorIndices(
|
|
219
|
+
metadata,
|
|
220
|
+
startChunkIndex,
|
|
221
|
+
maxChunks,
|
|
222
|
+
maxSplats
|
|
223
|
+
);
|
|
224
|
+
const splatChunks = await this._getChunkSplatsConcurrently(chunkIndices, options);
|
|
225
|
+
let loadedGlobalSplatEnd = 0;
|
|
226
|
+
for (const splats of splatChunks) {
|
|
227
|
+
chunkCount++;
|
|
228
|
+
splatCount += splats.splatCount;
|
|
229
|
+
const base = getRADChunkSplatBase(splats);
|
|
230
|
+
loadedGlobalSplatEnd = Math.max(loadedGlobalSplatEnd, base + splats.splatCount);
|
|
231
|
+
}
|
|
232
|
+
for (const splats of splatChunks) {
|
|
233
|
+
yield makeGaussianSplatsArrowTable(pruneLoadedLoDParents(splats, loadedGlobalSplatEnd));
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for (
|
|
239
|
+
let chunkIndex = startChunkIndex;
|
|
240
|
+
chunkIndex < metadata.chunks.length && chunkCount < maxChunks && splatCount < maxSplats;
|
|
241
|
+
chunkIndex++
|
|
242
|
+
) {
|
|
243
|
+
const table = await this.getChunkTable(chunkIndex, options);
|
|
244
|
+
chunkCount++;
|
|
245
|
+
splatCount += table.data.numRows;
|
|
246
|
+
yield table;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Fetches and decodes RAD chunks with bounded concurrency while preserving chunk order. */
|
|
251
|
+
private async _getChunkSplatsConcurrently(
|
|
252
|
+
chunkIndices: number[],
|
|
253
|
+
options: RADChunkTableIteratorOptions
|
|
254
|
+
): Promise<GaussianSplats[]> {
|
|
255
|
+
const splatChunks = new Array<GaussianSplats>(chunkIndices.length);
|
|
256
|
+
const maxConcurrentChunkRequests = getRADMaxConcurrentChunkRequests(
|
|
257
|
+
options.maxConcurrentChunkRequests
|
|
258
|
+
);
|
|
259
|
+
let nextResultIndex = 0;
|
|
260
|
+
|
|
261
|
+
async function loadNextChunk(source: RADSource): Promise<void> {
|
|
262
|
+
while (nextResultIndex < chunkIndices.length) {
|
|
263
|
+
const resultIndex = nextResultIndex++;
|
|
264
|
+
const chunkIndex = chunkIndices[resultIndex];
|
|
265
|
+
splatChunks[resultIndex] = await source.getChunkSplats(chunkIndex, options);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const workerCount = Math.min(maxConcurrentChunkRequests, chunkIndices.length);
|
|
270
|
+
await Promise.all(Array.from({length: workerCount}, () => loadNextChunk(this)));
|
|
271
|
+
return splatChunks;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Adds source-level RAD decode metadata to chunk decode options. */
|
|
275
|
+
private _getChunkDecodeOptions(
|
|
276
|
+
metadata: RADMetadata,
|
|
277
|
+
options: RADChunkRequestOptions
|
|
278
|
+
): RADChunkRequestOptions {
|
|
279
|
+
return {
|
|
280
|
+
...options,
|
|
281
|
+
radChunk: {
|
|
282
|
+
splatEncoding: metadata.splatEncoding,
|
|
283
|
+
...options.radChunk
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Loads and parses the top-level RAD metadata. */
|
|
289
|
+
private async _loadMetadata(): Promise<RADMetadata> {
|
|
290
|
+
const headerByteLengths = this.options.rad?.headerByteLengths?.length
|
|
291
|
+
? this.options.rad.headerByteLengths
|
|
292
|
+
: DEFAULT_RAD_HEADER_BYTE_LENGTHS;
|
|
293
|
+
|
|
294
|
+
if (typeof this.data !== 'string') {
|
|
295
|
+
const byteLength = Math.min(this.data.size, Math.max(...headerByteLengths));
|
|
296
|
+
const metadata = tryParseRADHeader(await this.data.slice(0, byteLength).arrayBuffer());
|
|
297
|
+
if (metadata) {
|
|
298
|
+
return metadata;
|
|
299
|
+
}
|
|
300
|
+
throw new Error('RADSource: Blob does not contain a complete RAD metadata header.');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
for (const byteLength of headerByteLengths) {
|
|
304
|
+
const metadata = tryParseRADHeader(
|
|
305
|
+
await this._fetchArrayBuffer(this.url, {offset: 0, bytes: byteLength})
|
|
306
|
+
);
|
|
307
|
+
if (metadata) {
|
|
308
|
+
return metadata;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
throw new Error('RADSource: failed to load a complete RAD metadata header.');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Returns a validated chunk table entry. */
|
|
316
|
+
private _getChunkRange(metadata: RADMetadata, chunkIndex: number): RADChunkRange {
|
|
317
|
+
if (!Number.isInteger(chunkIndex) || chunkIndex < 0 || chunkIndex >= metadata.chunks.length) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`RADSource: chunk index ${chunkIndex} is outside 0-${metadata.chunks.length - 1}.`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
return metadata.chunks[chunkIndex];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Fetches a full or ranged ArrayBuffer while tolerating non-206 range fallbacks. */
|
|
326
|
+
private async _fetchArrayBuffer(
|
|
327
|
+
url: string,
|
|
328
|
+
range?: {offset: number; bytes: number},
|
|
329
|
+
options: RADChunkRequestOptions = {}
|
|
330
|
+
): Promise<ArrayBuffer> {
|
|
331
|
+
const response = await this._fetchResponse(url, this._getRequestInit(range, options));
|
|
332
|
+
if (!response.ok) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`RADSource: failed to fetch ${url}: ${response.status} ${response.statusText}`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
339
|
+
if (range && response.status !== 206 && arrayBuffer.byteLength >= range.offset + range.bytes) {
|
|
340
|
+
return arrayBuffer.slice(range.offset, range.offset + range.bytes);
|
|
341
|
+
}
|
|
342
|
+
if (range && arrayBuffer.byteLength > range.bytes) {
|
|
343
|
+
return arrayBuffer.slice(0, range.bytes);
|
|
344
|
+
}
|
|
345
|
+
return arrayBuffer;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Fetches through custom source fetch hooks or the injected core fetchFile helper. */
|
|
349
|
+
private async _fetchResponse(url: string, requestInit: RequestInit): Promise<Response> {
|
|
350
|
+
return await this.fetch(url, requestInit);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Builds request options for RAD fetches. */
|
|
354
|
+
private _getRequestInit(
|
|
355
|
+
range?: {offset: number; bytes: number},
|
|
356
|
+
options: RADChunkRequestOptions = {}
|
|
357
|
+
): RequestInit {
|
|
358
|
+
const headers = new Headers(this.options.rad?.headers);
|
|
359
|
+
if (range && range.bytes > 0) {
|
|
360
|
+
headers.set('Range', `bytes=${range.offset}-${range.offset + range.bytes - 1}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
headers,
|
|
365
|
+
credentials: this.options.rad?.withCredentials ? 'include' : 'same-origin',
|
|
366
|
+
signal: options.signal
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Returns the RAD chunk indices requested by a chunk-table iterator. */
|
|
372
|
+
function getRADChunkTableIteratorIndices(
|
|
373
|
+
metadata: RADMetadata,
|
|
374
|
+
startChunkIndex: number,
|
|
375
|
+
maxChunks: number,
|
|
376
|
+
maxSplats: number
|
|
377
|
+
): number[] {
|
|
378
|
+
const chunkIndices: number[] = [];
|
|
379
|
+
let estimatedSplatCount = 0;
|
|
380
|
+
for (
|
|
381
|
+
let chunkIndex = startChunkIndex;
|
|
382
|
+
chunkIndex < metadata.chunks.length &&
|
|
383
|
+
chunkIndices.length < maxChunks &&
|
|
384
|
+
estimatedSplatCount < maxSplats;
|
|
385
|
+
chunkIndex++
|
|
386
|
+
) {
|
|
387
|
+
const chunk = metadata.chunks[chunkIndex];
|
|
388
|
+
chunkIndices.push(chunkIndex);
|
|
389
|
+
estimatedSplatCount += chunk?.count ?? metadata.chunkSize ?? 0;
|
|
390
|
+
}
|
|
391
|
+
return chunkIndices;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Returns a positive integer chunk request concurrency limit. */
|
|
395
|
+
function getRADMaxConcurrentChunkRequests(maxConcurrentChunkRequests?: number): number {
|
|
396
|
+
return Number.isFinite(maxConcurrentChunkRequests) && maxConcurrentChunkRequests! > 0
|
|
397
|
+
? Math.floor(maxConcurrentChunkRequests!)
|
|
398
|
+
: DEFAULT_RAD_MAX_CONCURRENT_CHUNK_REQUESTS;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Returns the first global splat index represented by decoded RAD chunk data. */
|
|
402
|
+
function getRADChunkSplatBase(splats: GaussianSplats): number {
|
|
403
|
+
return typeof splats.loaderData?.base === 'number' ? splats.loaderData.base : 0;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Remove parent LoD rows when their children are present in the loaded chunk range. */
|
|
407
|
+
function pruneLoadedLoDParents(
|
|
408
|
+
splats: GaussianSplats,
|
|
409
|
+
loadedGlobalSplatEnd: number
|
|
410
|
+
): GaussianSplats {
|
|
411
|
+
const childCounts = splats.loaderData?.childCounts;
|
|
412
|
+
const childStarts = splats.loaderData?.childStarts;
|
|
413
|
+
if (!(childCounts instanceof Uint16Array) || !(childStarts instanceof Uint32Array)) {
|
|
414
|
+
return splats;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const keepRows = new Uint32Array(splats.splatCount);
|
|
418
|
+
let keepCount = 0;
|
|
419
|
+
for (let rowIndex = 0; rowIndex < splats.splatCount; rowIndex++) {
|
|
420
|
+
const childCount = childCounts[rowIndex];
|
|
421
|
+
const childStart = childStarts[rowIndex];
|
|
422
|
+
if (childCount > 0 && childStart < loadedGlobalSplatEnd) {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
keepRows[keepCount++] = rowIndex;
|
|
426
|
+
}
|
|
427
|
+
if (keepCount === splats.splatCount) {
|
|
428
|
+
return splats;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const keptRows = keepRows.subarray(0, keepCount);
|
|
432
|
+
const sphericalHarmonicsComponentCount = splats.sphericalHarmonicsComponentCount ?? 0;
|
|
433
|
+
return {
|
|
434
|
+
...splats,
|
|
435
|
+
splatCount: keepCount,
|
|
436
|
+
positions: copyInterleavedRows(splats.positions, 3, keptRows),
|
|
437
|
+
scales: copyInterleavedRows(splats.scales, 3, keptRows),
|
|
438
|
+
rotations: copyInterleavedRows(splats.rotations, 4, keptRows),
|
|
439
|
+
colors: copyInterleavedRows(splats.colors, 3, keptRows),
|
|
440
|
+
sphericalHarmonicDcs: splats.sphericalHarmonicDcs
|
|
441
|
+
? copyInterleavedRows(splats.sphericalHarmonicDcs, 3, keptRows)
|
|
442
|
+
: undefined,
|
|
443
|
+
opacities: copyInterleavedRows(splats.opacities, 1, keptRows),
|
|
444
|
+
sphericalHarmonics:
|
|
445
|
+
splats.sphericalHarmonics && sphericalHarmonicsComponentCount
|
|
446
|
+
? copyInterleavedRows(splats.sphericalHarmonics, sphericalHarmonicsComponentCount, keptRows)
|
|
447
|
+
: undefined,
|
|
448
|
+
loaderData: {
|
|
449
|
+
...splats.loaderData,
|
|
450
|
+
count: keepCount,
|
|
451
|
+
childCounts: copyInterleavedRows(childCounts, 1, keptRows),
|
|
452
|
+
childStarts: copyInterleavedRows(childStarts, 1, keptRows)
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Copy selected rows from an interleaved typed array. */
|
|
458
|
+
function copyInterleavedRows<T extends Float32Array | Uint8Array | Uint16Array | Uint32Array>(
|
|
459
|
+
values: T,
|
|
460
|
+
itemSize: number,
|
|
461
|
+
rows: Uint32Array
|
|
462
|
+
): T {
|
|
463
|
+
const copiedValues = new (values.constructor as {new (length: number): T})(
|
|
464
|
+
rows.length * itemSize
|
|
465
|
+
);
|
|
466
|
+
for (let outputRowIndex = 0; outputRowIndex < rows.length; outputRowIndex++) {
|
|
467
|
+
const inputOffset = rows[outputRowIndex] * itemSize;
|
|
468
|
+
const outputOffset = outputRowIndex * itemSize;
|
|
469
|
+
copiedValues.set(values.subarray(inputOffset, inputOffset + itemSize), outputOffset);
|
|
470
|
+
}
|
|
471
|
+
return copiedValues;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Resolves a sidecar RADC chunk filename relative to its parent RAD URL. */
|
|
475
|
+
export function resolveRADChunkUrl(rootUrl: string, chunkFilename: string): string {
|
|
476
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(chunkFilename) || chunkFilename.startsWith('/')) {
|
|
477
|
+
return chunkFilename;
|
|
478
|
+
}
|
|
479
|
+
if (!rootUrl) {
|
|
480
|
+
throw new Error('RADSource: cannot resolve a sidecar RADC chunk without a RAD URL.');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
return new URL(chunkFilename, rootUrl).toString();
|
|
485
|
+
} catch {
|
|
486
|
+
const [rootPath, suffix = ''] = rootUrl.split(/([?#].*)/, 2);
|
|
487
|
+
const lastSlashIndex = Math.max(rootPath.lastIndexOf('/'), rootPath.lastIndexOf('\\'));
|
|
488
|
+
const rootDirectory = lastSlashIndex >= 0 ? rootPath.slice(0, lastSlashIndex + 1) : '';
|
|
489
|
+
return `${rootDirectory}${chunkFilename}${suffix}`;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {z} from 'zod';
|
|
6
|
+
import type {
|
|
7
|
+
RADChunkMetadataJSON,
|
|
8
|
+
RADChunkProperty,
|
|
9
|
+
RADChunkRange,
|
|
10
|
+
RADMetadataJSON,
|
|
11
|
+
RADSplatEncoding
|
|
12
|
+
} from './lib/parse-rad';
|
|
13
|
+
|
|
14
|
+
const nonNegativeSafeIntegerSchema = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
15
|
+
|
|
16
|
+
/** Zod schema for Spark RAD splat quantization and shader decode ranges. */
|
|
17
|
+
export const RADSplatEncodingSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
rgbMin: z.number().finite().optional(),
|
|
20
|
+
rgbMax: z.number().finite().optional(),
|
|
21
|
+
lnScaleMin: z.number().finite().optional(),
|
|
22
|
+
lnScaleMax: z.number().finite().optional(),
|
|
23
|
+
sh1Max: z.number().finite().optional(),
|
|
24
|
+
sh2Max: z.number().finite().optional(),
|
|
25
|
+
sh3Max: z.number().finite().optional(),
|
|
26
|
+
lodOpacity: z.boolean().optional()
|
|
27
|
+
})
|
|
28
|
+
.passthrough() satisfies z.ZodType<RADSplatEncoding>;
|
|
29
|
+
|
|
30
|
+
/** Zod schema for one chunk-table entry in top-level RAD metadata. */
|
|
31
|
+
export const RADChunkRangeSchema = z
|
|
32
|
+
.object({
|
|
33
|
+
offset: nonNegativeSafeIntegerSchema,
|
|
34
|
+
bytes: nonNegativeSafeIntegerSchema,
|
|
35
|
+
base: nonNegativeSafeIntegerSchema.optional(),
|
|
36
|
+
count: nonNegativeSafeIntegerSchema.optional(),
|
|
37
|
+
filename: z.string().optional()
|
|
38
|
+
})
|
|
39
|
+
.passthrough() satisfies z.ZodType<RADChunkRange>;
|
|
40
|
+
|
|
41
|
+
/** Zod schema for raw JSON metadata stored in a Spark RAD header. */
|
|
42
|
+
export const RADMetadataJSONSchema = z
|
|
43
|
+
.object({
|
|
44
|
+
version: z.literal(1),
|
|
45
|
+
type: z.literal('gsplat'),
|
|
46
|
+
count: nonNegativeSafeIntegerSchema,
|
|
47
|
+
maxSh: nonNegativeSafeIntegerSchema.optional(),
|
|
48
|
+
lodTree: z.boolean().optional(),
|
|
49
|
+
chunkSize: nonNegativeSafeIntegerSchema.optional(),
|
|
50
|
+
allChunkBytes: nonNegativeSafeIntegerSchema.optional(),
|
|
51
|
+
chunks: z.array(RADChunkRangeSchema),
|
|
52
|
+
splatEncoding: RADSplatEncodingSchema.optional(),
|
|
53
|
+
shCodeCount: nonNegativeSafeIntegerSchema.optional(),
|
|
54
|
+
comment: z.string().optional()
|
|
55
|
+
})
|
|
56
|
+
.passthrough() satisfies z.ZodType<RADMetadataJSON>;
|
|
57
|
+
|
|
58
|
+
/** Zod schema for one property-table entry in RADC metadata. */
|
|
59
|
+
export const RADChunkPropertySchema = z
|
|
60
|
+
.object({
|
|
61
|
+
offset: nonNegativeSafeIntegerSchema,
|
|
62
|
+
bytes: nonNegativeSafeIntegerSchema,
|
|
63
|
+
property: z.string(),
|
|
64
|
+
encoding: z.string(),
|
|
65
|
+
compression: z.string().optional(),
|
|
66
|
+
min: z.number().finite().optional(),
|
|
67
|
+
max: z.number().finite().optional()
|
|
68
|
+
})
|
|
69
|
+
.passthrough() satisfies z.ZodType<RADChunkProperty>;
|
|
70
|
+
|
|
71
|
+
/** Zod schema for raw JSON metadata stored in a Spark RADC chunk header. */
|
|
72
|
+
export const RADChunkMetadataJSONSchema = z
|
|
73
|
+
.object({
|
|
74
|
+
version: z.literal(1),
|
|
75
|
+
base: nonNegativeSafeIntegerSchema,
|
|
76
|
+
count: nonNegativeSafeIntegerSchema,
|
|
77
|
+
payloadBytes: nonNegativeSafeIntegerSchema,
|
|
78
|
+
maxSh: nonNegativeSafeIntegerSchema.optional(),
|
|
79
|
+
lodTree: z.boolean().optional(),
|
|
80
|
+
splatEncoding: RADSplatEncodingSchema.optional(),
|
|
81
|
+
properties: z.array(RADChunkPropertySchema)
|
|
82
|
+
})
|
|
83
|
+
.passthrough() satisfies z.ZodType<RADChunkMetadataJSON>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {Loader} from '@loaders.gl/loader-utils';
|
|
6
|
+
import type {MeshArrowTable} from '@loaders.gl/schema';
|
|
7
|
+
import type {SplatsLoaderOptions} from './types';
|
|
8
|
+
import {SPLATFormat} from './splats-format';
|
|
9
|
+
|
|
10
|
+
// __VERSION__ is injected by babel-plugin-version-inline
|
|
11
|
+
// @ts-ignore TS2304: Cannot find name '__VERSION__'.
|
|
12
|
+
const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'latest';
|
|
13
|
+
|
|
14
|
+
/** Loader metadata for raw `.splat` Gaussian splat files. */
|
|
15
|
+
export const SPLATLoader = {
|
|
16
|
+
dataType: null as unknown as MeshArrowTable,
|
|
17
|
+
batchType: null as never,
|
|
18
|
+
...SPLATFormat,
|
|
19
|
+
version: VERSION,
|
|
20
|
+
options: {
|
|
21
|
+
splats: {
|
|
22
|
+
shape: 'arrow-table'
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
} as const satisfies Loader<MeshArrowTable, never, SplatsLoaderOptions>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {LoaderWithParser} from '@loaders.gl/loader-utils';
|
|
6
|
+
import type {MeshArrowTable} from '@loaders.gl/schema';
|
|
7
|
+
import type {SplatsLoaderOptions} from './types';
|
|
8
|
+
import {parseSPLAT} from './lib/parse-splat';
|
|
9
|
+
import {SPLATLoader as SPLATLoaderMetadata} from './splat-loader-types';
|
|
10
|
+
|
|
11
|
+
/** Parser-bearing loader for raw `.splat` Gaussian splat files. */
|
|
12
|
+
export const SPLATLoaderWithParser = {
|
|
13
|
+
...SPLATLoaderMetadata,
|
|
14
|
+
parse: async (arrayBuffer: ArrayBuffer) => parseSPLAT(arrayBuffer),
|
|
15
|
+
parseSync: (arrayBuffer: ArrayBuffer) => parseSPLAT(arrayBuffer)
|
|
16
|
+
} as const satisfies LoaderWithParser<MeshArrowTable, never, SplatsLoaderOptions>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {Format} from '@loaders.gl/loader-utils';
|
|
6
|
+
|
|
7
|
+
/** SPLAT - raw binary Gaussian splat format. */
|
|
8
|
+
export const SPLATFormat = {
|
|
9
|
+
name: 'SPLAT',
|
|
10
|
+
id: 'splat',
|
|
11
|
+
module: 'splats',
|
|
12
|
+
extensions: ['splat'],
|
|
13
|
+
mimeTypes: ['application/octet-stream'],
|
|
14
|
+
binary: true
|
|
15
|
+
} as const satisfies Format;
|
|
16
|
+
|
|
17
|
+
/** KSPLAT - GaussianSplats3D optimized Gaussian splat format. */
|
|
18
|
+
export const KSPLATFormat = {
|
|
19
|
+
name: 'KSPLAT',
|
|
20
|
+
id: 'ksplat',
|
|
21
|
+
module: 'splats',
|
|
22
|
+
extensions: ['ksplat'],
|
|
23
|
+
mimeTypes: ['application/octet-stream'],
|
|
24
|
+
binary: true
|
|
25
|
+
} as const satisfies Format;
|
|
26
|
+
|
|
27
|
+
/** SPZ - Niantic Spatial compressed Gaussian splat format. */
|
|
28
|
+
export const SPZFormat = {
|
|
29
|
+
name: 'SPZ',
|
|
30
|
+
id: 'spz',
|
|
31
|
+
module: 'splats',
|
|
32
|
+
extensions: ['spz'],
|
|
33
|
+
mimeTypes: ['application/octet-stream'],
|
|
34
|
+
binary: true,
|
|
35
|
+
tests: [new Uint8Array([0x4e, 0x47, 0x53, 0x50]).buffer]
|
|
36
|
+
} as const satisfies Format;
|
|
37
|
+
|
|
38
|
+
/** RAD - Spark paged level-of-detail Gaussian splat container. */
|
|
39
|
+
export const RADFormat = {
|
|
40
|
+
name: 'RAD',
|
|
41
|
+
id: 'rad',
|
|
42
|
+
module: 'splats',
|
|
43
|
+
extensions: ['rad'],
|
|
44
|
+
mimeTypes: ['application/octet-stream'],
|
|
45
|
+
binary: true,
|
|
46
|
+
tests: [new Uint8Array([0x52, 0x41, 0x44, 0x30]).buffer]
|
|
47
|
+
} as const satisfies Format;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {Loader} from '@loaders.gl/loader-utils';
|
|
6
|
+
import type {MeshArrowTable} from '@loaders.gl/schema';
|
|
7
|
+
import type {SplatsLoaderOptions} from './types';
|
|
8
|
+
import {SPZFormat} from './splats-format';
|
|
9
|
+
|
|
10
|
+
// __VERSION__ is injected by babel-plugin-version-inline
|
|
11
|
+
// @ts-ignore TS2304: Cannot find name '__VERSION__'.
|
|
12
|
+
const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'latest';
|
|
13
|
+
|
|
14
|
+
/** Loader metadata for Niantic Spatial `.spz` Gaussian splat files. */
|
|
15
|
+
export const SPZLoader = {
|
|
16
|
+
dataType: null as unknown as MeshArrowTable,
|
|
17
|
+
batchType: null as never,
|
|
18
|
+
...SPZFormat,
|
|
19
|
+
version: VERSION,
|
|
20
|
+
options: {
|
|
21
|
+
splats: {
|
|
22
|
+
shape: 'arrow-table'
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
} as const satisfies Loader<MeshArrowTable, never, SplatsLoaderOptions>;
|