@loaders.gl/splats 5.0.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 +20876 -0
- package/dist/dist.min.js +18 -0
- package/dist/index.cjs +2046 -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 +42 -0
- package/dist/ksplat-loader-types.d.ts.map +1 -0
- package/dist/ksplat-loader-types.js +26 -0
- package/dist/ksplat-loader-types.js.map +1 -0
- package/dist/ksplat-loader.d.ts +22 -0
- package/dist/ksplat-loader.d.ts.map +1 -0
- package/dist/ksplat-loader.js +13 -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 +120 -0
- package/dist/lib/parse-rad.d.ts.map +1 -0
- package/dist/lib/parse-rad.js +235 -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 +132 -0
- package/dist/lib/splats-arrow-table.js.map +1 -0
- package/dist/rad-loader-types.d.ts +36 -0
- package/dist/rad-loader-types.d.ts.map +1 -0
- package/dist/rad-loader-types.js +22 -0
- package/dist/rad-loader-types.js.map +1 -0
- package/dist/rad-loader.d.ts +21 -0
- package/dist/rad-loader.d.ts.map +1 -0
- package/dist/rad-loader.js +15 -0
- package/dist/rad-loader.js.map +1 -0
- package/dist/rad-source-loader.d.ts +115 -0
- package/dist/rad-source-loader.d.ts.map +1 -0
- package/dist/rad-source-loader.js +338 -0
- package/dist/rad-source-loader.js.map +1 -0
- package/dist/splat-loader-types.d.ts +42 -0
- package/dist/splat-loader-types.d.ts.map +1 -0
- package/dist/splat-loader-types.js +26 -0
- package/dist/splat-loader-types.js.map +1 -0
- package/dist/splat-loader.d.ts +22 -0
- package/dist/splat-loader.d.ts.map +1 -0
- package/dist/splat-loader.js +13 -0
- package/dist/splat-loader.js.map +1 -0
- package/dist/splats-format.d.ts +43 -0
- package/dist/splats-format.d.ts.map +1 -0
- package/dist/splats-format.js +46 -0
- package/dist/splats-format.js.map +1 -0
- package/dist/spz-loader-types.d.ts +44 -0
- package/dist/spz-loader-types.d.ts.map +1 -0
- package/dist/spz-loader-types.js +26 -0
- package/dist/spz-loader-types.js.map +1 -0
- package/dist/spz-loader.d.ts +23 -0
- package/dist/spz-loader.d.ts.map +1 -0
- package/dist/spz-loader.js +12 -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 +85 -0
- package/src/bundled.ts +5 -0
- package/src/index.ts +26 -0
- package/src/ksplat-loader-types.ts +32 -0
- package/src/ksplat-loader.ts +18 -0
- package/src/lib/parse-ksplat.ts +538 -0
- package/src/lib/parse-rad-chunk.ts +721 -0
- package/src/lib/parse-rad.ts +439 -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 +188 -0
- package/src/rad-loader-types.ts +27 -0
- package/src/rad-loader.ts +24 -0
- package/src/rad-source-loader.ts +504 -0
- package/src/splat-loader-types.ts +32 -0
- package/src/splat-loader.ts +18 -0
- package/src/splats-format.ts +51 -0
- package/src/spz-loader-types.ts +32 -0
- package/src/spz-loader.ts +18 -0
- package/src/types.ts +39 -0
- package/src/unbundled.ts +5 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
const RAD_MAGIC = 0x30444152;
|
|
6
|
+
const RAD_CHUNK_MAGIC = 0x43444152;
|
|
7
|
+
const RAD_HEADER_BYTE_LENGTH = 8;
|
|
8
|
+
const RAD_CHUNK_PAYLOAD_LENGTH_BYTE_LENGTH = 8;
|
|
9
|
+
const TEXT_DECODER = new TextDecoder();
|
|
10
|
+
|
|
11
|
+
/** Spark RAD splat quantization and shader decode range metadata. */
|
|
12
|
+
export type RADSplatEncoding = {
|
|
13
|
+
/** Minimum RGB value encoded into quantized color properties. */
|
|
14
|
+
rgbMin?: number;
|
|
15
|
+
/** Maximum RGB value encoded into quantized color properties. */
|
|
16
|
+
rgbMax?: number;
|
|
17
|
+
/** Minimum natural log scale encoded into quantized scale properties. */
|
|
18
|
+
lnScaleMin?: number;
|
|
19
|
+
/** Maximum natural log scale encoded into quantized scale properties. */
|
|
20
|
+
lnScaleMax?: number;
|
|
21
|
+
/** Maximum absolute SH degree-1 coefficient value. */
|
|
22
|
+
sh1Max?: number;
|
|
23
|
+
/** Maximum absolute SH degree-2 coefficient value. */
|
|
24
|
+
sh2Max?: number;
|
|
25
|
+
/** Maximum absolute SH degree-3 coefficient value. */
|
|
26
|
+
sh3Max?: number;
|
|
27
|
+
/** Whether opacity is encoded for LoD blending. */
|
|
28
|
+
lodOpacity?: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** One RAD chunk location in the top-level RAD chunk table. */
|
|
32
|
+
export type RADChunkRange = {
|
|
33
|
+
/** Chunk byte offset relative to the top-level RAD chunk payload area. */
|
|
34
|
+
offset: number;
|
|
35
|
+
/** Chunk byte length. */
|
|
36
|
+
bytes: number;
|
|
37
|
+
/** Optional first splat index represented by this chunk. */
|
|
38
|
+
base?: number;
|
|
39
|
+
/** Optional number of splats represented by this chunk. */
|
|
40
|
+
count?: number;
|
|
41
|
+
/** Optional sidecar `.radc` filename relative to the RAD file URL. */
|
|
42
|
+
filename?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Parsed Spark RAD top-level metadata with loader-derived byte offsets. */
|
|
46
|
+
export type RADMetadata = {
|
|
47
|
+
/** RAD container version. Version 1 is currently supported. */
|
|
48
|
+
version: number;
|
|
49
|
+
/** RAD payload type. Spark currently writes `gsplat`. */
|
|
50
|
+
type: string;
|
|
51
|
+
/** Total splat count represented by the RAD source. */
|
|
52
|
+
count: number;
|
|
53
|
+
/** Maximum spherical harmonics degree present in the chunks. */
|
|
54
|
+
maxSh?: number;
|
|
55
|
+
/** Whether the source includes LoD tree child-count and child-start properties. */
|
|
56
|
+
lodTree?: boolean;
|
|
57
|
+
/** Nominal number of splats per chunk. */
|
|
58
|
+
chunkSize?: number;
|
|
59
|
+
/** Total inline chunk byte length when present in the RAD metadata. */
|
|
60
|
+
allChunkBytes?: number;
|
|
61
|
+
/** RAD chunk table. */
|
|
62
|
+
chunks: RADChunkRange[];
|
|
63
|
+
/** Optional shared splat encoding metadata for chunk decoding. */
|
|
64
|
+
splatEncoding?: RADSplatEncoding;
|
|
65
|
+
/** Optional spherical harmonics codebook count. */
|
|
66
|
+
shCodeCount?: number;
|
|
67
|
+
/** Optional RAD writer comment. */
|
|
68
|
+
comment?: string;
|
|
69
|
+
/** Byte length of the JSON metadata block. */
|
|
70
|
+
headerJsonByteLength: number;
|
|
71
|
+
/** Byte offset where inline RAD chunks begin. */
|
|
72
|
+
chunksByteOffset: number;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Known RAD chunk property names. */
|
|
76
|
+
export type RADChunkPropertyName =
|
|
77
|
+
| 'center'
|
|
78
|
+
| 'alpha'
|
|
79
|
+
| 'rgb'
|
|
80
|
+
| 'scales'
|
|
81
|
+
| 'orientation'
|
|
82
|
+
| 'sh1'
|
|
83
|
+
| 'sh2'
|
|
84
|
+
| 'sh3'
|
|
85
|
+
| 'child_count'
|
|
86
|
+
| 'child_start'
|
|
87
|
+
| 'sh1_code'
|
|
88
|
+
| 'sh2_code'
|
|
89
|
+
| 'sh3_code'
|
|
90
|
+
| 'sh_label';
|
|
91
|
+
|
|
92
|
+
/** Known RAD chunk property encodings. */
|
|
93
|
+
export type RADChunkPropertyEncoding =
|
|
94
|
+
| 'f32'
|
|
95
|
+
| 'f16'
|
|
96
|
+
| 'f32_lebytes'
|
|
97
|
+
| 'f16_lebytes'
|
|
98
|
+
| 'r8'
|
|
99
|
+
| 'r8_delta'
|
|
100
|
+
| 's8'
|
|
101
|
+
| 's8_delta'
|
|
102
|
+
| 'ln_0r8'
|
|
103
|
+
| 'ln_f16'
|
|
104
|
+
| 'oct88r8'
|
|
105
|
+
| 'u16'
|
|
106
|
+
| 'u32';
|
|
107
|
+
|
|
108
|
+
/** Known RAD chunk property compression modes. */
|
|
109
|
+
export type RADChunkPropertyCompression = 'gz';
|
|
110
|
+
|
|
111
|
+
/** One property payload entry inside a RAD chunk. */
|
|
112
|
+
export type RADChunkProperty = {
|
|
113
|
+
/** Property byte offset relative to the chunk payload area. */
|
|
114
|
+
offset: number;
|
|
115
|
+
/** Property byte length before 8-byte payload padding. */
|
|
116
|
+
bytes: number;
|
|
117
|
+
/** Property semantic name. */
|
|
118
|
+
property: RADChunkPropertyName | string;
|
|
119
|
+
/** Property encoding name. */
|
|
120
|
+
encoding: RADChunkPropertyEncoding | string;
|
|
121
|
+
/** Optional property compression mode. */
|
|
122
|
+
compression?: RADChunkPropertyCompression | string;
|
|
123
|
+
/** Optional decode minimum for quantized properties. */
|
|
124
|
+
min?: number;
|
|
125
|
+
/** Optional decode maximum for quantized properties. */
|
|
126
|
+
max?: number;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** Parsed Spark RADC chunk metadata with loader-derived byte offsets. */
|
|
130
|
+
export type RADChunkMetadata = {
|
|
131
|
+
/** RAD chunk version. Version 1 is currently supported. */
|
|
132
|
+
version: number;
|
|
133
|
+
/** First global splat index represented by this chunk. */
|
|
134
|
+
base: number;
|
|
135
|
+
/** Number of splats represented by this chunk. */
|
|
136
|
+
count: number;
|
|
137
|
+
/** Byte length of the padded property payload. */
|
|
138
|
+
payloadBytes: number;
|
|
139
|
+
/** Maximum spherical harmonics degree present in this chunk. */
|
|
140
|
+
maxSh?: number;
|
|
141
|
+
/** Whether this chunk contains LoD tree properties. */
|
|
142
|
+
lodTree?: boolean;
|
|
143
|
+
/** Optional chunk-local splat encoding metadata. */
|
|
144
|
+
splatEncoding?: RADSplatEncoding;
|
|
145
|
+
/** Property table for the chunk payload. */
|
|
146
|
+
properties: RADChunkProperty[];
|
|
147
|
+
/** Byte length of the chunk JSON metadata block. */
|
|
148
|
+
headerJsonByteLength: number;
|
|
149
|
+
/** Byte offset where the chunk payload begins. */
|
|
150
|
+
payloadByteOffset: number;
|
|
151
|
+
/** Byte length of the RADC header and payload. */
|
|
152
|
+
chunkByteLength: number;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/** Returns true when the input begins with the Spark RAD magic bytes. */
|
|
156
|
+
export function isRAD(data: ArrayBuffer | ArrayBufferView): boolean {
|
|
157
|
+
const bytes = getUint8Array(data);
|
|
158
|
+
if (bytes.byteLength < 4) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
return (
|
|
162
|
+
new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0, true) === RAD_MAGIC
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Parses a complete or header-prefix Spark `.rad` buffer. */
|
|
167
|
+
export function parseRADHeader(data: ArrayBuffer | ArrayBufferView): RADMetadata {
|
|
168
|
+
const metadata = tryParseRADHeader(data);
|
|
169
|
+
if (!metadata) {
|
|
170
|
+
throw new Error('RADLoader: file must contain a complete RAD metadata header.');
|
|
171
|
+
}
|
|
172
|
+
return metadata;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Parses a Spark `.rad` header when enough bytes are available. */
|
|
176
|
+
export function tryParseRADHeader(data: ArrayBuffer | ArrayBufferView): RADMetadata | null {
|
|
177
|
+
const bytes = getUint8Array(data);
|
|
178
|
+
if (bytes.byteLength < RAD_HEADER_BYTE_LENGTH) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
183
|
+
const magic = dataView.getUint32(0, true);
|
|
184
|
+
if (magic !== RAD_MAGIC) {
|
|
185
|
+
throw new Error(`RADLoader: RAD0 magic header not found, received 0x${magic.toString(16)}.`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const headerJsonByteLength = dataView.getUint32(4, true);
|
|
189
|
+
const headerJsonEnd = RAD_HEADER_BYTE_LENGTH + headerJsonByteLength;
|
|
190
|
+
if (bytes.byteLength < headerJsonEnd) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const rawMetadata = parseJSONRecord(
|
|
195
|
+
bytes.subarray(RAD_HEADER_BYTE_LENGTH, headerJsonEnd),
|
|
196
|
+
'RADLoader: failed to parse RAD metadata JSON.'
|
|
197
|
+
);
|
|
198
|
+
return normalizeRADMetadata(rawMetadata, headerJsonByteLength);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Parses the metadata header from one Spark `.radc` chunk buffer. */
|
|
202
|
+
export function parseRADChunkHeader(data: ArrayBuffer | ArrayBufferView): RADChunkMetadata {
|
|
203
|
+
const bytes = getUint8Array(data);
|
|
204
|
+
if (bytes.byteLength < RAD_HEADER_BYTE_LENGTH) {
|
|
205
|
+
throw new Error('RADLoader: RADC chunk must contain an 8-byte metadata header.');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
209
|
+
const magic = dataView.getUint32(0, true);
|
|
210
|
+
if (magic !== RAD_CHUNK_MAGIC) {
|
|
211
|
+
throw new Error(`RADLoader: RADC magic header not found, received 0x${magic.toString(16)}.`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const headerJsonByteLength = dataView.getUint32(4, true);
|
|
215
|
+
const headerJsonEnd = RAD_HEADER_BYTE_LENGTH + headerJsonByteLength;
|
|
216
|
+
const payloadByteLengthOffset = RAD_HEADER_BYTE_LENGTH + roundUpToEight(headerJsonByteLength);
|
|
217
|
+
const payloadByteOffset = payloadByteLengthOffset + RAD_CHUNK_PAYLOAD_LENGTH_BYTE_LENGTH;
|
|
218
|
+
if (bytes.byteLength < payloadByteOffset) {
|
|
219
|
+
throw new Error('RADLoader: RADC chunk must contain a complete metadata header.');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const rawMetadata = parseJSONRecord(
|
|
223
|
+
bytes.subarray(RAD_HEADER_BYTE_LENGTH, headerJsonEnd),
|
|
224
|
+
'RADLoader: failed to parse RADC metadata JSON.'
|
|
225
|
+
);
|
|
226
|
+
const payloadBytes = readSafeUint64(
|
|
227
|
+
dataView,
|
|
228
|
+
payloadByteLengthOffset,
|
|
229
|
+
'RADC payload byte length'
|
|
230
|
+
);
|
|
231
|
+
return normalizeRADChunkMetadata(
|
|
232
|
+
rawMetadata,
|
|
233
|
+
headerJsonByteLength,
|
|
234
|
+
payloadByteOffset,
|
|
235
|
+
payloadBytes
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Rounds a byte length to Spark RAD's 8-byte alignment. */
|
|
240
|
+
export function roundUpToEight(byteLength: number): number {
|
|
241
|
+
return (byteLength + 7) & ~7;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function normalizeRADMetadata(
|
|
245
|
+
rawMetadata: Record<string, unknown>,
|
|
246
|
+
headerJsonByteLength: number
|
|
247
|
+
): RADMetadata {
|
|
248
|
+
const version = requireSafeInteger(rawMetadata.version, 'RAD version');
|
|
249
|
+
if (version !== 1) {
|
|
250
|
+
throw new Error(`RADLoader: version ${version} is not supported.`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const type = requireString(rawMetadata.type, 'RAD type');
|
|
254
|
+
if (type !== 'gsplat') {
|
|
255
|
+
throw new Error(`RADLoader: RAD type ${type} is not supported.`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const chunks = requireArray(rawMetadata.chunks, 'RAD chunks').map((chunk, chunkIndex) =>
|
|
259
|
+
normalizeRADChunkRange(chunk, chunkIndex)
|
|
260
|
+
);
|
|
261
|
+
const metadata: RADMetadata = {
|
|
262
|
+
version,
|
|
263
|
+
type,
|
|
264
|
+
count: requireSafeInteger(rawMetadata.count, 'RAD count'),
|
|
265
|
+
maxSh: optionalSafeInteger(rawMetadata.maxSh, 'RAD maxSh'),
|
|
266
|
+
lodTree: optionalBoolean(rawMetadata.lodTree, 'RAD lodTree'),
|
|
267
|
+
chunkSize: optionalSafeInteger(rawMetadata.chunkSize, 'RAD chunkSize'),
|
|
268
|
+
allChunkBytes: optionalSafeInteger(rawMetadata.allChunkBytes, 'RAD allChunkBytes'),
|
|
269
|
+
chunks,
|
|
270
|
+
splatEncoding: optionalSplatEncoding(rawMetadata.splatEncoding, 'RAD splatEncoding'),
|
|
271
|
+
shCodeCount: optionalSafeInteger(rawMetadata.shCodeCount, 'RAD shCodeCount'),
|
|
272
|
+
comment: optionalString(rawMetadata.comment, 'RAD comment'),
|
|
273
|
+
headerJsonByteLength,
|
|
274
|
+
chunksByteOffset: RAD_HEADER_BYTE_LENGTH + roundUpToEight(headerJsonByteLength)
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
return metadata;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function normalizeRADChunkRange(rawChunk: unknown, chunkIndex: number): RADChunkRange {
|
|
281
|
+
const chunk = requireRecord(rawChunk, `RAD chunk ${chunkIndex}`);
|
|
282
|
+
return {
|
|
283
|
+
offset: requireSafeInteger(chunk.offset, `RAD chunk ${chunkIndex} offset`),
|
|
284
|
+
bytes: requireSafeInteger(chunk.bytes, `RAD chunk ${chunkIndex} bytes`),
|
|
285
|
+
base: optionalSafeInteger(chunk.base, `RAD chunk ${chunkIndex} base`),
|
|
286
|
+
count: optionalSafeInteger(chunk.count, `RAD chunk ${chunkIndex} count`),
|
|
287
|
+
filename: optionalString(chunk.filename, `RAD chunk ${chunkIndex} filename`)
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function normalizeRADChunkMetadata(
|
|
292
|
+
rawMetadata: Record<string, unknown>,
|
|
293
|
+
headerJsonByteLength: number,
|
|
294
|
+
payloadByteOffset: number,
|
|
295
|
+
payloadBytes: number
|
|
296
|
+
): RADChunkMetadata {
|
|
297
|
+
const version = requireSafeInteger(rawMetadata.version, 'RADC version');
|
|
298
|
+
if (version !== 1) {
|
|
299
|
+
throw new Error(`RADLoader: RADC version ${version} is not supported.`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const metadataPayloadBytes = requireSafeInteger(rawMetadata.payloadBytes, 'RADC payloadBytes');
|
|
303
|
+
if (metadataPayloadBytes !== payloadBytes) {
|
|
304
|
+
throw new Error('RADLoader: RADC metadata payload byte length does not match binary header.');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
version,
|
|
309
|
+
base: requireSafeInteger(rawMetadata.base, 'RADC base'),
|
|
310
|
+
count: requireSafeInteger(rawMetadata.count, 'RADC count'),
|
|
311
|
+
payloadBytes,
|
|
312
|
+
maxSh: optionalSafeInteger(rawMetadata.maxSh, 'RADC maxSh'),
|
|
313
|
+
lodTree: optionalBoolean(rawMetadata.lodTree, 'RADC lodTree'),
|
|
314
|
+
splatEncoding: optionalSplatEncoding(rawMetadata.splatEncoding, 'RADC splatEncoding'),
|
|
315
|
+
properties: requireArray(rawMetadata.properties, 'RADC properties').map((property, index) =>
|
|
316
|
+
normalizeRADChunkProperty(property, index)
|
|
317
|
+
),
|
|
318
|
+
headerJsonByteLength,
|
|
319
|
+
payloadByteOffset,
|
|
320
|
+
chunkByteLength: payloadByteOffset + payloadBytes
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function normalizeRADChunkProperty(rawProperty: unknown, propertyIndex: number): RADChunkProperty {
|
|
325
|
+
const property = requireRecord(rawProperty, `RADC property ${propertyIndex}`);
|
|
326
|
+
return {
|
|
327
|
+
offset: requireSafeInteger(property.offset, `RADC property ${propertyIndex} offset`),
|
|
328
|
+
bytes: requireSafeInteger(property.bytes, `RADC property ${propertyIndex} bytes`),
|
|
329
|
+
property: requireString(property.property, `RADC property ${propertyIndex} name`),
|
|
330
|
+
encoding: requireString(property.encoding, `RADC property ${propertyIndex} encoding`),
|
|
331
|
+
compression: optionalString(property.compression, `RADC property ${propertyIndex} compression`),
|
|
332
|
+
min: optionalFiniteNumber(property.min, `RADC property ${propertyIndex} min`),
|
|
333
|
+
max: optionalFiniteNumber(property.max, `RADC property ${propertyIndex} max`)
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function optionalSplatEncoding(value: unknown, fieldName: string): RADSplatEncoding | undefined {
|
|
338
|
+
if (value === undefined) {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
const encoding = requireRecord(value, fieldName);
|
|
342
|
+
return {
|
|
343
|
+
rgbMin: optionalFiniteNumber(encoding.rgbMin, `${fieldName}.rgbMin`),
|
|
344
|
+
rgbMax: optionalFiniteNumber(encoding.rgbMax, `${fieldName}.rgbMax`),
|
|
345
|
+
lnScaleMin: optionalFiniteNumber(encoding.lnScaleMin, `${fieldName}.lnScaleMin`),
|
|
346
|
+
lnScaleMax: optionalFiniteNumber(encoding.lnScaleMax, `${fieldName}.lnScaleMax`),
|
|
347
|
+
sh1Max: optionalFiniteNumber(encoding.sh1Max, `${fieldName}.sh1Max`),
|
|
348
|
+
sh2Max: optionalFiniteNumber(encoding.sh2Max, `${fieldName}.sh2Max`),
|
|
349
|
+
sh3Max: optionalFiniteNumber(encoding.sh3Max, `${fieldName}.sh3Max`),
|
|
350
|
+
lodOpacity: optionalBoolean(encoding.lodOpacity, `${fieldName}.lodOpacity`)
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function parseJSONRecord(bytes: Uint8Array, message: string): Record<string, unknown> {
|
|
355
|
+
try {
|
|
356
|
+
return requireRecord(JSON.parse(TEXT_DECODER.decode(bytes)), message);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
if (error instanceof Error && error.message.startsWith('RADLoader:')) {
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
throw new Error(message);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function requireRecord(value: unknown, fieldName: string): Record<string, unknown> {
|
|
366
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
367
|
+
throw new Error(`RADLoader: ${fieldName} must be an object.`);
|
|
368
|
+
}
|
|
369
|
+
return value as Record<string, unknown>;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function requireArray(value: unknown, fieldName: string): unknown[] {
|
|
373
|
+
if (!Array.isArray(value)) {
|
|
374
|
+
throw new Error(`RADLoader: ${fieldName} must be an array.`);
|
|
375
|
+
}
|
|
376
|
+
return value;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function requireString(value: unknown, fieldName: string): string {
|
|
380
|
+
if (typeof value !== 'string') {
|
|
381
|
+
throw new Error(`RADLoader: ${fieldName} must be a string.`);
|
|
382
|
+
}
|
|
383
|
+
return value;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function optionalString(value: unknown, fieldName: string): string | undefined {
|
|
387
|
+
if (value === undefined) {
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
return requireString(value, fieldName);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function optionalBoolean(value: unknown, fieldName: string): boolean | undefined {
|
|
394
|
+
if (value === undefined) {
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
if (typeof value !== 'boolean') {
|
|
398
|
+
throw new Error(`RADLoader: ${fieldName} must be a boolean.`);
|
|
399
|
+
}
|
|
400
|
+
return value;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function requireSafeInteger(value: unknown, fieldName: string): number {
|
|
404
|
+
if (!Number.isSafeInteger(value) || Number(value) < 0) {
|
|
405
|
+
throw new Error(`RADLoader: ${fieldName} must be a non-negative safe integer.`);
|
|
406
|
+
}
|
|
407
|
+
return Number(value);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function optionalSafeInteger(value: unknown, fieldName: string): number | undefined {
|
|
411
|
+
if (value === undefined) {
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
return requireSafeInteger(value, fieldName);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function optionalFiniteNumber(value: unknown, fieldName: string): number | undefined {
|
|
418
|
+
if (value === undefined) {
|
|
419
|
+
return undefined;
|
|
420
|
+
}
|
|
421
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
422
|
+
throw new Error(`RADLoader: ${fieldName} must be a finite number.`);
|
|
423
|
+
}
|
|
424
|
+
return value;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function readSafeUint64(dataView: DataView, byteOffset: number, fieldName: string): number {
|
|
428
|
+
const value = dataView.getBigUint64(byteOffset, true);
|
|
429
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
430
|
+
throw new Error(`RADLoader: ${fieldName} exceeds Number.MAX_SAFE_INTEGER.`);
|
|
431
|
+
}
|
|
432
|
+
return Number(value);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function getUint8Array(data: ArrayBuffer | ArrayBufferView): Uint8Array {
|
|
436
|
+
return data instanceof ArrayBuffer
|
|
437
|
+
? new Uint8Array(data)
|
|
438
|
+
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
439
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {MeshArrowTable} from '@loaders.gl/schema';
|
|
6
|
+
import type {GaussianSplats} from '../types';
|
|
7
|
+
import {makeGaussianSplatsArrowTable} from './splats-arrow-table';
|
|
8
|
+
import {
|
|
9
|
+
convertAlphaByteToLinearOpacity,
|
|
10
|
+
decodeQuaternionByte,
|
|
11
|
+
normalizeQuaternion
|
|
12
|
+
} from './splat-utils';
|
|
13
|
+
|
|
14
|
+
const SPLAT_ROW_BYTE_LENGTH = 32;
|
|
15
|
+
|
|
16
|
+
/** Parses a raw `.splat` ArrayBuffer into a Mesh Arrow table. */
|
|
17
|
+
export function parseSPLAT(data: ArrayBuffer): MeshArrowTable {
|
|
18
|
+
return makeGaussianSplatsArrowTable(parseSPLATToGaussianSplats(data));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Parses a raw `.splat` ArrayBuffer into decoded Gaussian splat values. */
|
|
22
|
+
export function parseSPLATToGaussianSplats(data: ArrayBuffer): GaussianSplats {
|
|
23
|
+
if (data.byteLength % SPLAT_ROW_BYTE_LENGTH !== 0) {
|
|
24
|
+
throw new Error(`SPLATLoader: byte length must be a multiple of ${SPLAT_ROW_BYTE_LENGTH}.`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const splatCount = data.byteLength / SPLAT_ROW_BYTE_LENGTH;
|
|
28
|
+
const dataView = new DataView(data);
|
|
29
|
+
const positions = new Float32Array(splatCount * 3);
|
|
30
|
+
const scales = new Float32Array(splatCount * 3);
|
|
31
|
+
const rotations = new Float32Array(splatCount * 4);
|
|
32
|
+
const colors = new Uint8Array(splatCount * 3);
|
|
33
|
+
const opacities = new Float32Array(splatCount);
|
|
34
|
+
|
|
35
|
+
for (let splatIndex = 0; splatIndex < splatCount; splatIndex++) {
|
|
36
|
+
const byteOffset = splatIndex * SPLAT_ROW_BYTE_LENGTH;
|
|
37
|
+
const positionOffset = splatIndex * 3;
|
|
38
|
+
const rotationOffset = splatIndex * 4;
|
|
39
|
+
|
|
40
|
+
positions[positionOffset + 0] = dataView.getFloat32(byteOffset + 0, true);
|
|
41
|
+
positions[positionOffset + 1] = dataView.getFloat32(byteOffset + 4, true);
|
|
42
|
+
positions[positionOffset + 2] = dataView.getFloat32(byteOffset + 8, true);
|
|
43
|
+
scales[positionOffset + 0] = dataView.getFloat32(byteOffset + 12, true);
|
|
44
|
+
scales[positionOffset + 1] = dataView.getFloat32(byteOffset + 16, true);
|
|
45
|
+
scales[positionOffset + 2] = dataView.getFloat32(byteOffset + 20, true);
|
|
46
|
+
colors[positionOffset + 0] = dataView.getUint8(byteOffset + 24);
|
|
47
|
+
colors[positionOffset + 1] = dataView.getUint8(byteOffset + 25);
|
|
48
|
+
colors[positionOffset + 2] = dataView.getUint8(byteOffset + 26);
|
|
49
|
+
opacities[splatIndex] = convertAlphaByteToLinearOpacity(dataView.getUint8(byteOffset + 27));
|
|
50
|
+
|
|
51
|
+
const [w, x, y, z] = normalizeQuaternion(
|
|
52
|
+
decodeQuaternionByte(dataView.getUint8(byteOffset + 28)),
|
|
53
|
+
decodeQuaternionByte(dataView.getUint8(byteOffset + 29)),
|
|
54
|
+
decodeQuaternionByte(dataView.getUint8(byteOffset + 30)),
|
|
55
|
+
decodeQuaternionByte(dataView.getUint8(byteOffset + 31))
|
|
56
|
+
);
|
|
57
|
+
rotations[rotationOffset + 0] = w;
|
|
58
|
+
rotations[rotationOffset + 1] = x;
|
|
59
|
+
rotations[rotationOffset + 2] = y;
|
|
60
|
+
rotations[rotationOffset + 3] = z;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
format: 'splat',
|
|
65
|
+
splatCount,
|
|
66
|
+
positions,
|
|
67
|
+
scales,
|
|
68
|
+
rotations,
|
|
69
|
+
colors,
|
|
70
|
+
opacities,
|
|
71
|
+
loaderData: {
|
|
72
|
+
format: 'splat',
|
|
73
|
+
splatCount,
|
|
74
|
+
rowByteLength: SPLAT_ROW_BYTE_LENGTH
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|