@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.
Files changed (112) hide show
  1. package/LICENSE +41 -0
  2. package/README.md +3 -0
  3. package/dist/bundled.d.ts +2 -0
  4. package/dist/bundled.d.ts.map +1 -0
  5. package/dist/bundled.js +5 -0
  6. package/dist/bundled.js.map +1 -0
  7. package/dist/dist.dev.js +20876 -0
  8. package/dist/dist.min.js +18 -0
  9. package/dist/index.cjs +2046 -0
  10. package/dist/index.cjs.map +7 -0
  11. package/dist/index.d.ts +10 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +10 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/ksplat-loader-types.d.ts +42 -0
  16. package/dist/ksplat-loader-types.d.ts.map +1 -0
  17. package/dist/ksplat-loader-types.js +26 -0
  18. package/dist/ksplat-loader-types.js.map +1 -0
  19. package/dist/ksplat-loader.d.ts +22 -0
  20. package/dist/ksplat-loader.d.ts.map +1 -0
  21. package/dist/ksplat-loader.js +13 -0
  22. package/dist/ksplat-loader.js.map +1 -0
  23. package/dist/lib/parse-ksplat.d.ts +7 -0
  24. package/dist/lib/parse-ksplat.d.ts.map +1 -0
  25. package/dist/lib/parse-ksplat.js +312 -0
  26. package/dist/lib/parse-ksplat.js.map +1 -0
  27. package/dist/lib/parse-rad-chunk.d.ts +19 -0
  28. package/dist/lib/parse-rad-chunk.d.ts.map +1 -0
  29. package/dist/lib/parse-rad-chunk.js +514 -0
  30. package/dist/lib/parse-rad-chunk.js.map +1 -0
  31. package/dist/lib/parse-rad.d.ts +120 -0
  32. package/dist/lib/parse-rad.d.ts.map +1 -0
  33. package/dist/lib/parse-rad.js +235 -0
  34. package/dist/lib/parse-rad.js.map +1 -0
  35. package/dist/lib/parse-splat.d.ts +7 -0
  36. package/dist/lib/parse-splat.d.ts.map +1 -0
  37. package/dist/lib/parse-splat.js +58 -0
  38. package/dist/lib/parse-splat.js.map +1 -0
  39. package/dist/lib/parse-spz.d.ts +7 -0
  40. package/dist/lib/parse-spz.d.ts.map +1 -0
  41. package/dist/lib/parse-spz.js +268 -0
  42. package/dist/lib/parse-spz.js.map +1 -0
  43. package/dist/lib/splat-utils.d.ts +12 -0
  44. package/dist/lib/splat-utils.d.ts.map +1 -0
  45. package/dist/lib/splat-utils.js +38 -0
  46. package/dist/lib/splat-utils.js.map +1 -0
  47. package/dist/lib/splats-arrow-table.d.ts +5 -0
  48. package/dist/lib/splats-arrow-table.d.ts.map +1 -0
  49. package/dist/lib/splats-arrow-table.js +132 -0
  50. package/dist/lib/splats-arrow-table.js.map +1 -0
  51. package/dist/rad-loader-types.d.ts +36 -0
  52. package/dist/rad-loader-types.d.ts.map +1 -0
  53. package/dist/rad-loader-types.js +22 -0
  54. package/dist/rad-loader-types.js.map +1 -0
  55. package/dist/rad-loader.d.ts +21 -0
  56. package/dist/rad-loader.d.ts.map +1 -0
  57. package/dist/rad-loader.js +15 -0
  58. package/dist/rad-loader.js.map +1 -0
  59. package/dist/rad-source-loader.d.ts +115 -0
  60. package/dist/rad-source-loader.d.ts.map +1 -0
  61. package/dist/rad-source-loader.js +338 -0
  62. package/dist/rad-source-loader.js.map +1 -0
  63. package/dist/splat-loader-types.d.ts +42 -0
  64. package/dist/splat-loader-types.d.ts.map +1 -0
  65. package/dist/splat-loader-types.js +26 -0
  66. package/dist/splat-loader-types.js.map +1 -0
  67. package/dist/splat-loader.d.ts +22 -0
  68. package/dist/splat-loader.d.ts.map +1 -0
  69. package/dist/splat-loader.js +13 -0
  70. package/dist/splat-loader.js.map +1 -0
  71. package/dist/splats-format.d.ts +43 -0
  72. package/dist/splats-format.d.ts.map +1 -0
  73. package/dist/splats-format.js +46 -0
  74. package/dist/splats-format.js.map +1 -0
  75. package/dist/spz-loader-types.d.ts +44 -0
  76. package/dist/spz-loader-types.d.ts.map +1 -0
  77. package/dist/spz-loader-types.js +26 -0
  78. package/dist/spz-loader-types.js.map +1 -0
  79. package/dist/spz-loader.d.ts +23 -0
  80. package/dist/spz-loader.d.ts.map +1 -0
  81. package/dist/spz-loader.js +12 -0
  82. package/dist/spz-loader.js.map +1 -0
  83. package/dist/types.d.ts +34 -0
  84. package/dist/types.d.ts.map +1 -0
  85. package/dist/types.js +5 -0
  86. package/dist/types.js.map +1 -0
  87. package/dist/unbundled.d.ts +2 -0
  88. package/dist/unbundled.d.ts.map +1 -0
  89. package/dist/unbundled.js +5 -0
  90. package/dist/unbundled.js.map +1 -0
  91. package/package.json +85 -0
  92. package/src/bundled.ts +5 -0
  93. package/src/index.ts +26 -0
  94. package/src/ksplat-loader-types.ts +32 -0
  95. package/src/ksplat-loader.ts +18 -0
  96. package/src/lib/parse-ksplat.ts +538 -0
  97. package/src/lib/parse-rad-chunk.ts +721 -0
  98. package/src/lib/parse-rad.ts +439 -0
  99. package/src/lib/parse-splat.ts +77 -0
  100. package/src/lib/parse-spz.ts +368 -0
  101. package/src/lib/splat-utils.ts +49 -0
  102. package/src/lib/splats-arrow-table.ts +188 -0
  103. package/src/rad-loader-types.ts +27 -0
  104. package/src/rad-loader.ts +24 -0
  105. package/src/rad-source-loader.ts +504 -0
  106. package/src/splat-loader-types.ts +32 -0
  107. package/src/splat-loader.ts +18 -0
  108. package/src/splats-format.ts +51 -0
  109. package/src/spz-loader-types.ts +32 -0
  110. package/src/spz-loader.ts +18 -0
  111. package/src/types.ts +39 -0
  112. package/src/unbundled.ts +5 -0
@@ -0,0 +1,721 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {DeflateCompression} from '@loaders.gl/compression';
6
+ import type {MeshArrowTable} from '@loaders.gl/schema';
7
+ import type {GaussianSplats, SplatsLoaderOptions} from '../types';
8
+ import {
9
+ parseRADChunkHeader,
10
+ roundUpToEight,
11
+ type RADChunkMetadata,
12
+ type RADChunkProperty,
13
+ type RADSplatEncoding
14
+ } from './parse-rad';
15
+ import {makeGaussianSplatsArrowTable} from './splats-arrow-table';
16
+ import {decodeFloat16, normalizeQuaternion, SH_C0} from './splat-utils';
17
+
18
+ const RAD_PROPERTY_DEFLATE = new DeflateCompression();
19
+ const RAD_PROPERTY_RAW_DEFLATE = new DeflateCompression({raw: true});
20
+
21
+ /** Options for decoding one Spark RADC chunk payload. */
22
+ export type RADChunkDecodeOptions = SplatsLoaderOptions & {
23
+ radChunk?: {
24
+ /** Optional source-level splat encoding metadata used as a fallback for chunk-local metadata. */
25
+ splatEncoding?: RADSplatEncoding;
26
+ /** Whether decoded LoD child metadata is retained in `loaderData`. */
27
+ includeLoDTree?: boolean;
28
+ /** Whether decoded SH rest coefficients are included in Arrow table columns. */
29
+ includeSphericalHarmonics?: boolean;
30
+ };
31
+ };
32
+
33
+ /** Parses a Spark `.radc` chunk into a Mesh Arrow table. */
34
+ export function parseRADChunk(
35
+ data: ArrayBuffer | ArrayBufferView,
36
+ options?: RADChunkDecodeOptions
37
+ ): MeshArrowTable {
38
+ return makeGaussianSplatsArrowTable(parseRADChunkToGaussianSplats(data, options));
39
+ }
40
+
41
+ /** Parses a Spark `.radc` chunk into decoded Gaussian splat values. */
42
+ export function parseRADChunkToGaussianSplats(
43
+ data: ArrayBuffer | ArrayBufferView,
44
+ options?: RADChunkDecodeOptions
45
+ ): GaussianSplats {
46
+ const bytes = getUint8Array(data);
47
+ const metadata = getRADChunkDecodeMetadata(parseRADChunkHeader(bytes), options);
48
+ if (bytes.byteLength < metadata.chunkByteLength) {
49
+ throw new Error('RADLoader: RADC chunk payload is incomplete.');
50
+ }
51
+
52
+ const splatCount = metadata.count;
53
+ const positions = decodeRADFloatProperty(
54
+ bytes,
55
+ metadata,
56
+ requireRADProperty(metadata, 'center'),
57
+ 3
58
+ );
59
+ const opacities = decodeRADAlpha(bytes, metadata, splatCount);
60
+ const rgb = decodeRADRgb(bytes, metadata, splatCount);
61
+ const scales = decodeRADScales(bytes, metadata, splatCount);
62
+ const rotations = decodeRADRotations(bytes, metadata, splatCount);
63
+ const {colors, sphericalHarmonicDcs} = convertRgbToColorColumns(rgb);
64
+ const includeLoDTree = options?.radChunk?.includeLoDTree ?? true;
65
+ const childCounts = includeLoDTree
66
+ ? decodeOptionalRADUint16(bytes, metadata, 'child_count')
67
+ : null;
68
+ const childStarts = includeLoDTree
69
+ ? decodeOptionalRADUint32(bytes, metadata, 'child_start')
70
+ : null;
71
+ const includeSphericalHarmonics = options?.radChunk?.includeSphericalHarmonics ?? false;
72
+ const sphericalHarmonics = includeSphericalHarmonics
73
+ ? decodeRADSphericalHarmonics(bytes, metadata)
74
+ : null;
75
+
76
+ return {
77
+ format: 'rad',
78
+ splatCount,
79
+ positions,
80
+ scales,
81
+ rotations,
82
+ colors,
83
+ sphericalHarmonicDcs,
84
+ opacities,
85
+ sphericalHarmonics: sphericalHarmonics?.values,
86
+ sphericalHarmonicsComponentCount: sphericalHarmonics?.componentCount,
87
+ loaderData: {
88
+ format: 'rad',
89
+ base: metadata.base,
90
+ count: metadata.count,
91
+ maxSh: metadata.maxSh ?? 0,
92
+ lodTree: metadata.lodTree ?? false,
93
+ splatEncoding: metadata.splatEncoding,
94
+ childCounts: childCounts ?? undefined,
95
+ childStarts: childStarts ?? undefined
96
+ }
97
+ };
98
+ }
99
+
100
+ /** Decodes the chunk alpha property, falling back to fully opaque splats. */
101
+ function decodeRADAlpha(
102
+ bytes: Uint8Array,
103
+ metadata: RADChunkMetadata,
104
+ splatCount: number
105
+ ): Float32Array {
106
+ const property = findRADProperty(metadata, 'alpha');
107
+ if (!property) {
108
+ return new Float32Array(splatCount).fill(1);
109
+ }
110
+ const opacities = decodeRADFloatProperty(bytes, metadata, property, 1);
111
+ for (let splatIndex = 0; splatIndex < opacities.length; splatIndex++) {
112
+ opacities[splatIndex] = decodeRADOpacity(opacities[splatIndex], metadata, property);
113
+ }
114
+ return opacities;
115
+ }
116
+
117
+ /** Decodes normal or Spark LoD opacity into the renderer opacity domain. */
118
+ function decodeRADOpacity(
119
+ opacity: number,
120
+ metadata: RADChunkMetadata,
121
+ property: RADChunkProperty
122
+ ): number {
123
+ const normalizedOpacity = Math.max(opacity, 0);
124
+ if (
125
+ !metadata.splatEncoding?.lodOpacity ||
126
+ (property.encoding !== 'r8' && property.encoding !== 'r8_delta')
127
+ ) {
128
+ return normalizedOpacity;
129
+ }
130
+
131
+ return normalizedOpacity * 2;
132
+ }
133
+
134
+ /** Decodes the chunk RGB property, falling back to white splats. */
135
+ function decodeRADRgb(
136
+ bytes: Uint8Array,
137
+ metadata: RADChunkMetadata,
138
+ splatCount: number
139
+ ): Float32Array {
140
+ const property = findRADProperty(metadata, 'rgb');
141
+ if (!property) {
142
+ return new Float32Array(splatCount * 3).fill(1);
143
+ }
144
+ return decodeRADFloatProperty(bytes, metadata, property, 3);
145
+ }
146
+
147
+ /** Decodes the chunk scale property, falling back to unit splats. */
148
+ function decodeRADScales(
149
+ bytes: Uint8Array,
150
+ metadata: RADChunkMetadata,
151
+ splatCount: number
152
+ ): Float32Array {
153
+ const property = findRADProperty(metadata, 'scales');
154
+ if (!property) {
155
+ return new Float32Array(splatCount * 3).fill(1);
156
+ }
157
+ return decodeRADFloatProperty(bytes, metadata, property, 3);
158
+ }
159
+
160
+ /** Decodes the chunk orientation property, falling back to identity rotations. */
161
+ function decodeRADRotations(
162
+ bytes: Uint8Array,
163
+ metadata: RADChunkMetadata,
164
+ splatCount: number
165
+ ): Float32Array {
166
+ const property = findRADProperty(metadata, 'orientation');
167
+ if (!property) {
168
+ return makeIdentityRotations(splatCount);
169
+ }
170
+
171
+ const propertyBytes = getRADPropertyBytes(bytes, metadata, property);
172
+ switch (property.encoding) {
173
+ case 'oct88r8':
174
+ return decodeRADOct88R8Rotations(propertyBytes, splatCount);
175
+ case 'f32':
176
+ case 'f16': {
177
+ const xyz = decodeRADFloatProperty(bytes, metadata, property, 3);
178
+ const rotations = new Float32Array(splatCount * 4);
179
+ for (let splatIndex = 0; splatIndex < splatCount; splatIndex++) {
180
+ const xyzOffset = splatIndex * 3;
181
+ const rotationOffset = splatIndex * 4;
182
+ const x = xyz[xyzOffset + 0];
183
+ const y = xyz[xyzOffset + 1];
184
+ const z = xyz[xyzOffset + 2];
185
+ const w = Math.sqrt(Math.max(0, 1 - x * x - y * y - z * z));
186
+ const normalized = normalizeQuaternion(w, x, y, z);
187
+ rotations.set(normalized, rotationOffset);
188
+ }
189
+ return rotations;
190
+ }
191
+ default:
192
+ throw new Error(`RADLoader: unsupported orientation encoding ${property.encoding}.`);
193
+ }
194
+ }
195
+
196
+ /** Decodes optional spherical harmonic rest properties from a chunk. */
197
+ function decodeRADSphericalHarmonics(
198
+ bytes: Uint8Array,
199
+ metadata: RADChunkMetadata
200
+ ): {values: Float32Array; componentCount: number} | null {
201
+ const shProperties = [
202
+ {property: findRADProperty(metadata, 'sh1'), dimensions: 9},
203
+ {property: findRADProperty(metadata, 'sh2'), dimensions: 15},
204
+ {property: findRADProperty(metadata, 'sh3'), dimensions: 21}
205
+ ].filter(
206
+ (entry): entry is {property: RADChunkProperty; dimensions: number} =>
207
+ entry.property !== undefined
208
+ );
209
+ if (shProperties.length === 0) {
210
+ return null;
211
+ }
212
+
213
+ const decodedProperties = shProperties.map(entry => ({
214
+ values: decodeRADFloatProperty(bytes, metadata, entry.property, entry.dimensions),
215
+ dimensions: entry.dimensions
216
+ }));
217
+ const componentCount = decodedProperties.reduce((total, entry) => total + entry.dimensions, 0);
218
+ const values = new Float32Array(metadata.count * componentCount);
219
+ for (let splatIndex = 0; splatIndex < metadata.count; splatIndex++) {
220
+ let outputOffset = splatIndex * componentCount;
221
+ for (const decodedProperty of decodedProperties) {
222
+ const inputOffset = splatIndex * decodedProperty.dimensions;
223
+ values.set(
224
+ decodedProperty.values.subarray(inputOffset, inputOffset + decodedProperty.dimensions),
225
+ outputOffset
226
+ );
227
+ outputOffset += decodedProperty.dimensions;
228
+ }
229
+ }
230
+ return {values, componentCount};
231
+ }
232
+
233
+ /** Decodes one numeric RADC property into row-major Float32 values. */
234
+ function decodeRADFloatProperty(
235
+ bytes: Uint8Array,
236
+ metadata: RADChunkMetadata,
237
+ property: RADChunkProperty,
238
+ dimensions: number
239
+ ): Float32Array {
240
+ const propertyBytes = getRADPropertyBytes(bytes, metadata, property);
241
+ switch (property.encoding) {
242
+ case 'f32':
243
+ return decodePlanarFloat32(propertyBytes, dimensions, metadata.count);
244
+ case 'f16':
245
+ return decodePlanarFloat16(propertyBytes, dimensions, metadata.count);
246
+ case 'f32_lebytes':
247
+ return decodeLeBytesFloat32(propertyBytes, dimensions, metadata.count);
248
+ case 'f16_lebytes':
249
+ return decodeLeBytesFloat16(propertyBytes, dimensions, metadata.count);
250
+ case 'r8':
251
+ return decodeR8(
252
+ propertyBytes,
253
+ dimensions,
254
+ metadata.count,
255
+ getPropertyMin(metadata, property),
256
+ getPropertyMax(metadata, property)
257
+ );
258
+ case 'r8_delta':
259
+ return decodeR8Delta(
260
+ propertyBytes,
261
+ dimensions,
262
+ metadata.count,
263
+ getPropertyMin(metadata, property),
264
+ getPropertyMax(metadata, property)
265
+ );
266
+ case 's8':
267
+ return decodeS8(
268
+ propertyBytes,
269
+ dimensions,
270
+ metadata.count,
271
+ getPropertyMax(metadata, property)
272
+ );
273
+ case 's8_delta':
274
+ return decodeS8Delta(
275
+ propertyBytes,
276
+ dimensions,
277
+ metadata.count,
278
+ getPropertyMax(metadata, property)
279
+ );
280
+ case 'ln_0r8':
281
+ return decodeScaleR8(
282
+ propertyBytes,
283
+ dimensions,
284
+ metadata.count,
285
+ getPropertyMin(metadata, property),
286
+ getPropertyMax(metadata, property)
287
+ );
288
+ case 'ln_f16':
289
+ return decodeLogFloat16(propertyBytes, dimensions, metadata.count);
290
+ default:
291
+ throw new Error(`RADLoader: unsupported ${property.property} encoding ${property.encoding}.`);
292
+ }
293
+ }
294
+
295
+ /** Decodes one optional unsigned 16-bit RADC property. */
296
+ function decodeOptionalRADUint16(
297
+ bytes: Uint8Array,
298
+ metadata: RADChunkMetadata,
299
+ propertyName: string
300
+ ): Uint16Array | null {
301
+ const property = findRADProperty(metadata, propertyName);
302
+ if (!property) {
303
+ return null;
304
+ }
305
+ if (property.encoding !== 'u16') {
306
+ throw new Error(`RADLoader: unsupported ${propertyName} encoding ${property.encoding}.`);
307
+ }
308
+ const propertyBytes = getRADPropertyBytes(bytes, metadata, property);
309
+ const values = new Uint16Array(metadata.count);
310
+ for (let splatIndex = 0; splatIndex < metadata.count; splatIndex++) {
311
+ values[splatIndex] = propertyBytes[splatIndex * 2] | (propertyBytes[splatIndex * 2 + 1] << 8);
312
+ }
313
+ return values;
314
+ }
315
+
316
+ /** Decodes one optional unsigned 32-bit RADC property. */
317
+ function decodeOptionalRADUint32(
318
+ bytes: Uint8Array,
319
+ metadata: RADChunkMetadata,
320
+ propertyName: string
321
+ ): Uint32Array | null {
322
+ const property = findRADProperty(metadata, propertyName);
323
+ if (!property) {
324
+ return null;
325
+ }
326
+ if (property.encoding !== 'u32') {
327
+ throw new Error(`RADLoader: unsupported ${propertyName} encoding ${property.encoding}.`);
328
+ }
329
+ const propertyBytes = getRADPropertyBytes(bytes, metadata, property);
330
+ const dataView = new DataView(
331
+ propertyBytes.buffer,
332
+ propertyBytes.byteOffset,
333
+ propertyBytes.byteLength
334
+ );
335
+ const values = new Uint32Array(metadata.count);
336
+ for (let splatIndex = 0; splatIndex < metadata.count; splatIndex++) {
337
+ values[splatIndex] = dataView.getUint32(splatIndex * 4, true);
338
+ }
339
+ return values;
340
+ }
341
+
342
+ /** Returns decompressed bytes for one RADC property. */
343
+ function getRADPropertyBytes(
344
+ bytes: Uint8Array,
345
+ metadata: RADChunkMetadata,
346
+ property: RADChunkProperty
347
+ ): Uint8Array {
348
+ const paddedByteLength = roundUpToEight(property.bytes);
349
+ const byteOffset = metadata.payloadByteOffset + property.offset;
350
+ const byteEnd = byteOffset + paddedByteLength;
351
+ if (byteEnd > bytes.byteLength || property.offset + paddedByteLength > metadata.payloadBytes) {
352
+ throw new Error(`RADLoader: property ${property.property} exceeds RADC payload byte length.`);
353
+ }
354
+
355
+ const propertyBytes = bytes.subarray(byteOffset, byteOffset + property.bytes);
356
+ if (!property.compression) {
357
+ return propertyBytes;
358
+ }
359
+ if (property.compression !== 'gz') {
360
+ throw new Error(`RADLoader: unsupported property compression ${property.compression}.`);
361
+ }
362
+ return decompressRADProperty(propertyBytes, property.property);
363
+ }
364
+
365
+ /** Inflates one Spark RADC property payload. */
366
+ function decompressRADProperty(bytes: Uint8Array, propertyName: string): Uint8Array {
367
+ const arrayBuffer = copyToArrayBuffer(bytes);
368
+ try {
369
+ return new Uint8Array(RAD_PROPERTY_DEFLATE.decompressSync(arrayBuffer));
370
+ } catch {
371
+ try {
372
+ return new Uint8Array(RAD_PROPERTY_RAW_DEFLATE.decompressSync(arrayBuffer));
373
+ } catch {
374
+ throw new Error(`RADLoader: failed to decompress RADC property ${propertyName}.`);
375
+ }
376
+ }
377
+ }
378
+
379
+ /** Decodes planar Float32 values emitted by Spark RADC. */
380
+ function decodePlanarFloat32(bytes: Uint8Array, dimensions: number, count: number): Float32Array {
381
+ const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
382
+ const values = new Float32Array(count * dimensions);
383
+ for (let rowIndex = 0; rowIndex < count; rowIndex++) {
384
+ for (let dimension = 0; dimension < dimensions; dimension++) {
385
+ values[rowIndex * dimensions + dimension] = dataView.getFloat32(
386
+ (dimension * count + rowIndex) * 4,
387
+ true
388
+ );
389
+ }
390
+ }
391
+ return values;
392
+ }
393
+
394
+ /** Decodes planar Float16 values emitted by Spark RADC. */
395
+ function decodePlanarFloat16(bytes: Uint8Array, dimensions: number, count: number): Float32Array {
396
+ const values = new Float32Array(count * dimensions);
397
+ for (let rowIndex = 0; rowIndex < count; rowIndex++) {
398
+ for (let dimension = 0; dimension < dimensions; dimension++) {
399
+ const byteOffset = (dimension * count + rowIndex) * 2;
400
+ values[rowIndex * dimensions + dimension] = decodeFloat16(
401
+ bytes[byteOffset] | (bytes[byteOffset + 1] << 8)
402
+ );
403
+ }
404
+ }
405
+ return values;
406
+ }
407
+
408
+ /** Decodes Spark's byte-plane reordered Float32 values. */
409
+ function decodeLeBytesFloat32(bytes: Uint8Array, dimensions: number, count: number): Float32Array {
410
+ const values = new Float32Array(count * dimensions);
411
+ const stride = count * dimensions;
412
+ const scratch = new Uint8Array(4);
413
+ const scratchView = new DataView(scratch.buffer);
414
+ for (let rowIndex = 0; rowIndex < count; rowIndex++) {
415
+ for (let dimension = 0; dimension < dimensions; dimension++) {
416
+ const byteOffset = dimension * count + rowIndex;
417
+ for (let byteIndex = 0; byteIndex < 4; byteIndex++) {
418
+ scratch[byteIndex] = bytes[byteOffset + stride * byteIndex];
419
+ }
420
+ values[rowIndex * dimensions + dimension] = scratchView.getFloat32(0, true);
421
+ }
422
+ }
423
+ return values;
424
+ }
425
+
426
+ /** Decodes Spark's byte-plane reordered Float16 values. */
427
+ function decodeLeBytesFloat16(bytes: Uint8Array, dimensions: number, count: number): Float32Array {
428
+ const values = new Float32Array(count * dimensions);
429
+ const stride = count * dimensions;
430
+ for (let rowIndex = 0; rowIndex < count; rowIndex++) {
431
+ for (let dimension = 0; dimension < dimensions; dimension++) {
432
+ const byteOffset = dimension * count + rowIndex;
433
+ values[rowIndex * dimensions + dimension] = decodeFloat16(
434
+ bytes[byteOffset] | (bytes[byteOffset + stride] << 8)
435
+ );
436
+ }
437
+ }
438
+ return values;
439
+ }
440
+
441
+ /** Decodes R8 quantized planar float values. */
442
+ function decodeR8(
443
+ bytes: Uint8Array,
444
+ dimensions: number,
445
+ count: number,
446
+ min: number,
447
+ max: number
448
+ ): Float32Array {
449
+ const values = new Float32Array(count * dimensions);
450
+ const range = max - min;
451
+ for (let rowIndex = 0; rowIndex < count; rowIndex++) {
452
+ for (let dimension = 0; dimension < dimensions; dimension++) {
453
+ values[rowIndex * dimensions + dimension] =
454
+ (bytes[dimension * count + rowIndex] / 255) * range + min;
455
+ }
456
+ }
457
+ return values;
458
+ }
459
+
460
+ /** Decodes delta-compressed R8 quantized planar float values. */
461
+ function decodeR8Delta(
462
+ bytes: Uint8Array,
463
+ dimensions: number,
464
+ count: number,
465
+ min: number,
466
+ max: number
467
+ ): Float32Array {
468
+ const values = new Float32Array(count * dimensions);
469
+ const range = max - min;
470
+ const last = new Uint8Array(dimensions);
471
+ for (let rowIndex = 0; rowIndex < count; rowIndex++) {
472
+ for (let dimension = 0; dimension < dimensions; dimension++) {
473
+ const byteOffset = dimension * count + rowIndex;
474
+ const value = (last[dimension] + bytes[byteOffset]) & 0xff;
475
+ last[dimension] = value;
476
+ values[rowIndex * dimensions + dimension] = (value / 255) * range + min;
477
+ }
478
+ }
479
+ return values;
480
+ }
481
+
482
+ /** Decodes S8 quantized planar float values. */
483
+ function decodeS8(bytes: Uint8Array, dimensions: number, count: number, max: number): Float32Array {
484
+ const values = new Float32Array(count * dimensions);
485
+ for (let rowIndex = 0; rowIndex < count; rowIndex++) {
486
+ for (let dimension = 0; dimension < dimensions; dimension++) {
487
+ const byte = bytes[dimension * count + rowIndex];
488
+ values[rowIndex * dimensions + dimension] = (((byte << 24) >> 24) / 127) * max;
489
+ }
490
+ }
491
+ return values;
492
+ }
493
+
494
+ /** Decodes delta-compressed S8 quantized planar float values. */
495
+ function decodeS8Delta(
496
+ bytes: Uint8Array,
497
+ dimensions: number,
498
+ count: number,
499
+ max: number
500
+ ): Float32Array {
501
+ const values = new Float32Array(count * dimensions);
502
+ const last = new Uint8Array(dimensions);
503
+ for (let rowIndex = 0; rowIndex < count; rowIndex++) {
504
+ for (let dimension = 0; dimension < dimensions; dimension++) {
505
+ const byteOffset = dimension * count + rowIndex;
506
+ const value = (last[dimension] + bytes[byteOffset]) & 0xff;
507
+ last[dimension] = value;
508
+ values[rowIndex * dimensions + dimension] = (((value << 24) >> 24) / 127) * max;
509
+ }
510
+ }
511
+ return values;
512
+ }
513
+
514
+ /** Decodes Spark scale8 values to linear Gaussian standard deviations. */
515
+ function decodeScaleR8(
516
+ bytes: Uint8Array,
517
+ dimensions: number,
518
+ count: number,
519
+ min: number,
520
+ max: number
521
+ ): Float32Array {
522
+ const values = new Float32Array(count * dimensions);
523
+ for (let rowIndex = 0; rowIndex < count; rowIndex++) {
524
+ for (let dimension = 0; dimension < dimensions; dimension++) {
525
+ values[rowIndex * dimensions + dimension] = decodeScale8(
526
+ bytes[dimension * count + rowIndex],
527
+ min,
528
+ max
529
+ );
530
+ }
531
+ }
532
+ return values;
533
+ }
534
+
535
+ /** Decodes Spark log-Float16 scale values to linear Gaussian standard deviations. */
536
+ function decodeLogFloat16(bytes: Uint8Array, dimensions: number, count: number): Float32Array {
537
+ const logValues = decodePlanarFloat16(bytes, dimensions, count);
538
+ for (let valueIndex = 0; valueIndex < logValues.length; valueIndex++) {
539
+ logValues[valueIndex] = Math.exp(logValues[valueIndex]);
540
+ }
541
+ return logValues;
542
+ }
543
+
544
+ /** Decodes Spark oct88r8 quaternion values into `[w, x, y, z]` rotations. */
545
+ function decodeRADOct88R8Rotations(bytes: Uint8Array, count: number): Float32Array {
546
+ const rotations = new Float32Array(count * 4);
547
+ for (let splatIndex = 0; splatIndex < count; splatIndex++) {
548
+ const byteOffset = splatIndex * 3;
549
+ const [x, y, z, w] = decodeQuatOct888(
550
+ bytes[byteOffset],
551
+ bytes[byteOffset + 1],
552
+ bytes[byteOffset + 2]
553
+ );
554
+ rotations.set(normalizeQuaternion(w, x, y, z), splatIndex * 4);
555
+ }
556
+ return rotations;
557
+ }
558
+
559
+ /** Converts decoded RGB floats to loaders.gl color and SH DC columns. */
560
+ function convertRgbToColorColumns(rgb: Float32Array): {
561
+ colors: Uint8Array;
562
+ sphericalHarmonicDcs: Float32Array;
563
+ } {
564
+ const colors = new Uint8Array(rgb.length);
565
+ const sphericalHarmonicDcs = new Float32Array(rgb.length);
566
+ for (let componentIndex = 0; componentIndex < rgb.length; componentIndex++) {
567
+ const component = rgb[componentIndex];
568
+ colors[componentIndex] = normalizeColorByte(component);
569
+ sphericalHarmonicDcs[componentIndex] = (component - 0.5) / SH_C0;
570
+ }
571
+ return {colors, sphericalHarmonicDcs};
572
+ }
573
+
574
+ /** Returns an identity rotation array. */
575
+ function makeIdentityRotations(count: number): Float32Array {
576
+ const rotations = new Float32Array(count * 4);
577
+ for (let splatIndex = 0; splatIndex < count; splatIndex++) {
578
+ rotations[splatIndex * 4] = 1;
579
+ }
580
+ return rotations;
581
+ }
582
+
583
+ /** Decodes Spark's scale8 byte encoding. */
584
+ function decodeScale8(scale: number, min: number, max: number): number {
585
+ if (scale === 0) {
586
+ return 0;
587
+ }
588
+ const logScaleStep = (max - min) / 254;
589
+ return Math.exp(min + (scale - 1) * logScaleStep);
590
+ }
591
+
592
+ /** Decodes Spark's oct888 quaternion encoding into `[x, y, z, w]`. */
593
+ function decodeQuatOct888(u: number, v: number, r: number): [number, number, number, number] {
594
+ let x = (u / 255) * 2 - 1;
595
+ let y = (v / 255) * 2 - 1;
596
+ const z = 1 - Math.abs(x) - Math.abs(y);
597
+ const t = Math.max(-z, 0);
598
+ x = x >= 0 ? x - t : x + t;
599
+ y = y >= 0 ? y - t : y + t;
600
+ const length = Math.hypot(x, y, z);
601
+ const axisX = length > Number.EPSILON ? x / length : 1;
602
+ const axisY = length > Number.EPSILON ? y / length : 0;
603
+ const axisZ = length > Number.EPSILON ? z / length : 0;
604
+ const halfTheta = (r / 255) * 0.5 * Math.PI;
605
+ const sinHalfTheta = Math.sin(halfTheta);
606
+ return [axisX * sinHalfTheta, axisY * sinHalfTheta, axisZ * sinHalfTheta, Math.cos(halfTheta)];
607
+ }
608
+
609
+ /** Returns a named property or throws. */
610
+ function requireRADProperty(metadata: RADChunkMetadata, propertyName: string): RADChunkProperty {
611
+ const property = findRADProperty(metadata, propertyName);
612
+ if (!property) {
613
+ throw new Error(`RADLoader: RADC chunk is missing ${propertyName} property.`);
614
+ }
615
+ return property;
616
+ }
617
+
618
+ /** Returns a named property when present. */
619
+ function findRADProperty(
620
+ metadata: RADChunkMetadata,
621
+ propertyName: string
622
+ ): RADChunkProperty | undefined {
623
+ return metadata.properties.find(property => property.property === propertyName);
624
+ }
625
+
626
+ /** Merges source-level splat encoding with chunk-local encoding. */
627
+ function getRADChunkDecodeMetadata(
628
+ metadata: RADChunkMetadata,
629
+ options?: RADChunkDecodeOptions
630
+ ): RADChunkMetadata {
631
+ const splatEncoding = options?.radChunk?.splatEncoding;
632
+ if (!splatEncoding) {
633
+ return metadata;
634
+ }
635
+ return {
636
+ ...metadata,
637
+ splatEncoding: {
638
+ ...splatEncoding,
639
+ ...metadata.splatEncoding
640
+ }
641
+ };
642
+ }
643
+
644
+ /** Returns a required quantization minimum. */
645
+ function getPropertyMin(metadata: RADChunkMetadata, property: RADChunkProperty): number {
646
+ if (property.min === undefined) {
647
+ const fallback = getSplatEncodingMinimum(metadata.splatEncoding, property.property);
648
+ if (fallback !== undefined) {
649
+ return fallback;
650
+ }
651
+ throw new Error(`RADLoader: RADC property ${property.property} is missing min.`);
652
+ }
653
+ return property.min;
654
+ }
655
+
656
+ /** Returns a required quantization maximum. */
657
+ function getPropertyMax(metadata: RADChunkMetadata, property: RADChunkProperty): number {
658
+ if (property.max === undefined) {
659
+ const fallback = getSplatEncodingMaximum(metadata.splatEncoding, property.property);
660
+ if (fallback !== undefined) {
661
+ return fallback;
662
+ }
663
+ throw new Error(`RADLoader: RADC property ${property.property} is missing max.`);
664
+ }
665
+ return property.max;
666
+ }
667
+
668
+ /** Returns a source-level minimum value for a quantized RAD property. */
669
+ function getSplatEncodingMinimum(
670
+ splatEncoding: RADSplatEncoding | undefined,
671
+ propertyName: string
672
+ ): number | undefined {
673
+ switch (propertyName) {
674
+ case 'rgb':
675
+ return splatEncoding?.rgbMin;
676
+ case 'scales':
677
+ return splatEncoding?.lnScaleMin;
678
+ default:
679
+ return undefined;
680
+ }
681
+ }
682
+
683
+ /** Returns a source-level maximum value for a quantized RAD property. */
684
+ function getSplatEncodingMaximum(
685
+ splatEncoding: RADSplatEncoding | undefined,
686
+ propertyName: string
687
+ ): number | undefined {
688
+ switch (propertyName) {
689
+ case 'rgb':
690
+ return splatEncoding?.rgbMax;
691
+ case 'scales':
692
+ return splatEncoding?.lnScaleMax;
693
+ case 'sh1':
694
+ return splatEncoding?.sh1Max;
695
+ case 'sh2':
696
+ return splatEncoding?.sh2Max;
697
+ case 'sh3':
698
+ return splatEncoding?.sh3Max;
699
+ default:
700
+ return undefined;
701
+ }
702
+ }
703
+
704
+ /** Clamps a normalized color component into an unorm8 byte. */
705
+ function normalizeColorByte(value: number): number {
706
+ return Math.round(Math.min(Math.max(value, 0), 1) * 255);
707
+ }
708
+
709
+ /** Copies a byte view into a standalone ArrayBuffer. */
710
+ function copyToArrayBuffer(bytes: Uint8Array): ArrayBuffer {
711
+ const copy = new Uint8Array(bytes.byteLength);
712
+ copy.set(bytes);
713
+ return copy.buffer;
714
+ }
715
+
716
+ /** Converts an ArrayBuffer-like value into a Uint8Array view. */
717
+ function getUint8Array(data: ArrayBuffer | ArrayBufferView): Uint8Array {
718
+ return data instanceof ArrayBuffer
719
+ ? new Uint8Array(data)
720
+ : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
721
+ }