@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.
Files changed (121) 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 +33741 -0
  8. package/dist/dist.min.js +82 -0
  9. package/dist/index.cjs +1786 -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 +19 -0
  16. package/dist/ksplat-loader-types.d.ts.map +1 -0
  17. package/dist/ksplat-loader-types.js +20 -0
  18. package/dist/ksplat-loader-types.js.map +1 -0
  19. package/dist/ksplat-loader.d.ts +21 -0
  20. package/dist/ksplat-loader.d.ts.map +1 -0
  21. package/dist/ksplat-loader.js +12 -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 +136 -0
  32. package/dist/lib/parse-rad.d.ts.map +1 -0
  33. package/dist/lib/parse-rad.js +110 -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 +133 -0
  50. package/dist/lib/splats-arrow-table.js.map +1 -0
  51. package/dist/rad-loader-types.d.ts +16 -0
  52. package/dist/rad-loader-types.d.ts.map +1 -0
  53. package/dist/rad-loader-types.js +16 -0
  54. package/dist/rad-loader-types.js.map +1 -0
  55. package/dist/rad-loader.d.ts +20 -0
  56. package/dist/rad-loader.d.ts.map +1 -0
  57. package/dist/rad-loader.js +14 -0
  58. package/dist/rad-loader.js.map +1 -0
  59. package/dist/rad-metadata.schema.json +133 -0
  60. package/dist/rad-source-loader.d.ts +112 -0
  61. package/dist/rad-source-loader.d.ts.map +1 -0
  62. package/dist/rad-source-loader.js +329 -0
  63. package/dist/rad-source-loader.js.map +1 -0
  64. package/dist/rad-zod-schema.cjs +81 -0
  65. package/dist/rad-zod-schema.cjs.map +7 -0
  66. package/dist/rad-zod-schema.d.ts +88 -0
  67. package/dist/rad-zod-schema.d.ts.map +1 -0
  68. package/dist/rad-zod-schema.js +70 -0
  69. package/dist/rad-zod-schema.js.map +1 -0
  70. package/dist/radc-metadata.schema.json +112 -0
  71. package/dist/splat-loader-types.d.ts +19 -0
  72. package/dist/splat-loader-types.d.ts.map +1 -0
  73. package/dist/splat-loader-types.js +20 -0
  74. package/dist/splat-loader-types.js.map +1 -0
  75. package/dist/splat-loader.d.ts +21 -0
  76. package/dist/splat-loader.d.ts.map +1 -0
  77. package/dist/splat-loader.js +12 -0
  78. package/dist/splat-loader.js.map +1 -0
  79. package/dist/splats-format.d.ts +39 -0
  80. package/dist/splats-format.d.ts.map +1 -0
  81. package/dist/splats-format.js +42 -0
  82. package/dist/splats-format.js.map +1 -0
  83. package/dist/spz-loader-types.d.ts +20 -0
  84. package/dist/spz-loader-types.d.ts.map +1 -0
  85. package/dist/spz-loader-types.js +20 -0
  86. package/dist/spz-loader-types.js.map +1 -0
  87. package/dist/spz-loader.d.ts +22 -0
  88. package/dist/spz-loader.d.ts.map +1 -0
  89. package/dist/spz-loader.js +11 -0
  90. package/dist/spz-loader.js.map +1 -0
  91. package/dist/types.d.ts +34 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +5 -0
  94. package/dist/types.js.map +1 -0
  95. package/dist/unbundled.d.ts +2 -0
  96. package/dist/unbundled.d.ts.map +1 -0
  97. package/dist/unbundled.js +5 -0
  98. package/dist/unbundled.js.map +1 -0
  99. package/package.json +94 -0
  100. package/src/bundled.ts +5 -0
  101. package/src/index.ts +28 -0
  102. package/src/ksplat-loader-types.ts +25 -0
  103. package/src/ksplat-loader.ts +16 -0
  104. package/src/lib/parse-ksplat.ts +540 -0
  105. package/src/lib/parse-rad-chunk.ts +721 -0
  106. package/src/lib/parse-rad.ts +318 -0
  107. package/src/lib/parse-splat.ts +77 -0
  108. package/src/lib/parse-spz.ts +368 -0
  109. package/src/lib/splat-utils.ts +49 -0
  110. package/src/lib/splats-arrow-table.ts +190 -0
  111. package/src/rad-loader-types.ts +20 -0
  112. package/src/rad-loader.ts +22 -0
  113. package/src/rad-source-loader.ts +491 -0
  114. package/src/rad-zod-schema.ts +83 -0
  115. package/src/splat-loader-types.ts +25 -0
  116. package/src/splat-loader.ts +16 -0
  117. package/src/splats-format.ts +47 -0
  118. package/src/spz-loader-types.ts +25 -0
  119. package/src/spz-loader.ts +16 -0
  120. package/src/types.ts +39 -0
  121. package/src/unbundled.ts +5 -0
@@ -0,0 +1,540 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ /* eslint-disable max-params */
6
+
7
+ import type {MeshArrowTable} from '@loaders.gl/schema';
8
+ import type {GaussianSplats} from '../types';
9
+ import {makeGaussianSplatsArrowTable} from './splats-arrow-table';
10
+ import {convertAlphaByteToLinearOpacity, decodeFloat16, normalizeQuaternion} from './splat-utils';
11
+
12
+ const CURRENT_MAJOR_VERSION = 0;
13
+ const CURRENT_MINOR_VERSION = 1;
14
+ const HEADER_BYTE_LENGTH = 4096;
15
+ const SECTION_HEADER_BYTE_LENGTH = 1024;
16
+ const DEFAULT_SH_8BIT_COMPRESSION_RANGE = 3;
17
+ const DEFAULT_SH_8BIT_COMPRESSION_HALF_RANGE = DEFAULT_SH_8BIT_COMPRESSION_RANGE / 2;
18
+
19
+ type KSPLATHeader = {
20
+ versionMajor: number;
21
+ versionMinor: number;
22
+ maxSectionCount: number;
23
+ sectionCount: number;
24
+ maxSplatCount: number;
25
+ splatCount: number;
26
+ compressionLevel: 0 | 1 | 2;
27
+ minSphericalHarmonicsCoeff: number;
28
+ maxSphericalHarmonicsCoeff: number;
29
+ };
30
+
31
+ type KSPLATSection = {
32
+ bytesPerSplat: number;
33
+ splatCountOffset: number;
34
+ splatCount: number;
35
+ maxSplatCount: number;
36
+ bucketSize: number;
37
+ bucketCount: number;
38
+ bucketStorageSizeBytes: number;
39
+ bucketsStorageSizeBytes: number;
40
+ compressionScaleRange: number;
41
+ compressionScaleFactor: number;
42
+ base: number;
43
+ bucketsBase: number;
44
+ dataBase: number;
45
+ fullBucketCount: number;
46
+ partiallyFilledBucketCount: number;
47
+ sphericalHarmonicsDegree: number;
48
+ };
49
+
50
+ type CompressionLayout = {
51
+ bytesPerCenter: number;
52
+ bytesPerScale: number;
53
+ bytesPerRotation: number;
54
+ bytesPerColor: number;
55
+ scaleOffsetBytes: number;
56
+ colorOffsetBytes: number;
57
+ sphericalHarmonicsOffsetBytes: number;
58
+ scaleRange: number;
59
+ bytesPerSphericalHarmonicsComponent: number;
60
+ };
61
+
62
+ const COMPRESSION_LAYOUTS: Record<0 | 1 | 2, CompressionLayout> = {
63
+ 0: {
64
+ bytesPerCenter: 12,
65
+ bytesPerScale: 12,
66
+ bytesPerRotation: 16,
67
+ bytesPerColor: 4,
68
+ scaleOffsetBytes: 12,
69
+ colorOffsetBytes: 40,
70
+ sphericalHarmonicsOffsetBytes: 44,
71
+ scaleRange: 1,
72
+ bytesPerSphericalHarmonicsComponent: 4
73
+ },
74
+ 1: {
75
+ bytesPerCenter: 6,
76
+ bytesPerScale: 6,
77
+ bytesPerRotation: 8,
78
+ bytesPerColor: 4,
79
+ scaleOffsetBytes: 6,
80
+ colorOffsetBytes: 20,
81
+ sphericalHarmonicsOffsetBytes: 24,
82
+ scaleRange: 32767,
83
+ bytesPerSphericalHarmonicsComponent: 2
84
+ },
85
+ 2: {
86
+ bytesPerCenter: 6,
87
+ bytesPerScale: 6,
88
+ bytesPerRotation: 8,
89
+ bytesPerColor: 4,
90
+ scaleOffsetBytes: 6,
91
+ colorOffsetBytes: 20,
92
+ sphericalHarmonicsOffsetBytes: 24,
93
+ scaleRange: 32767,
94
+ bytesPerSphericalHarmonicsComponent: 1
95
+ }
96
+ };
97
+
98
+ /** Parses a `.ksplat` ArrayBuffer into a Mesh Arrow table. */
99
+ export function parseKSPLAT(data: ArrayBuffer): MeshArrowTable {
100
+ return makeGaussianSplatsArrowTable(parseKSPLATToGaussianSplats(data));
101
+ }
102
+
103
+ /** Parses a `.ksplat` ArrayBuffer into decoded Gaussian splat values. */
104
+ export function parseKSPLATToGaussianSplats(data: ArrayBuffer): GaussianSplats {
105
+ if (data.byteLength < HEADER_BYTE_LENGTH) {
106
+ throw new Error(`KSPLATLoader: file must contain a ${HEADER_BYTE_LENGTH}-byte header.`);
107
+ }
108
+
109
+ const header = parseKSPLATHeader(data);
110
+ checkKSPLATVersion(header);
111
+ const sections = parseKSPLATSections(data, header);
112
+ const splatCount = getTotalSplatCount(header, sections);
113
+ const sphericalHarmonicsComponentCount = Math.max(
114
+ 0,
115
+ ...sections
116
+ .slice(0, getActiveSectionCount(header))
117
+ .map((section) => getSphericalHarmonicsComponentCount(section.sphericalHarmonicsDegree))
118
+ );
119
+
120
+ const positions = new Float32Array(splatCount * 3);
121
+ const scales = new Float32Array(splatCount * 3);
122
+ const rotations = new Float32Array(splatCount * 4);
123
+ const colors = new Uint8Array(splatCount * 3);
124
+ const opacities = new Float32Array(splatCount);
125
+ const sphericalHarmonics = sphericalHarmonicsComponentCount
126
+ ? new Float32Array(splatCount * sphericalHarmonicsComponentCount)
127
+ : undefined;
128
+
129
+ let outputSplatIndex = 0;
130
+ for (const section of sections.slice(0, getActiveSectionCount(header))) {
131
+ validateSectionBounds(data, section);
132
+ for (
133
+ let localSplatIndex = 0;
134
+ localSplatIndex < section.splatCount && outputSplatIndex < splatCount;
135
+ localSplatIndex++
136
+ ) {
137
+ decodeKSPLATSplat(
138
+ data,
139
+ header,
140
+ section,
141
+ localSplatIndex,
142
+ outputSplatIndex,
143
+ positions,
144
+ scales,
145
+ rotations,
146
+ colors,
147
+ opacities,
148
+ sphericalHarmonics,
149
+ sphericalHarmonicsComponentCount
150
+ );
151
+ outputSplatIndex++;
152
+ }
153
+ }
154
+
155
+ return {
156
+ format: 'ksplat',
157
+ splatCount,
158
+ positions,
159
+ scales,
160
+ rotations,
161
+ colors,
162
+ opacities,
163
+ sphericalHarmonics,
164
+ sphericalHarmonicsComponentCount,
165
+ loaderData: {
166
+ format: 'ksplat',
167
+ ...header,
168
+ sections: sections.slice(0, getActiveSectionCount(header)).map((section) => ({
169
+ splatCount: section.splatCount,
170
+ maxSplatCount: section.maxSplatCount,
171
+ sphericalHarmonicsDegree: section.sphericalHarmonicsDegree
172
+ }))
173
+ }
174
+ };
175
+ }
176
+
177
+ /** Decodes one KSPLAT record into the shared Gaussian splat arrays. */
178
+ function decodeKSPLATSplat(
179
+ data: ArrayBuffer,
180
+ header: KSPLATHeader,
181
+ section: KSPLATSection,
182
+ localSplatIndex: number,
183
+ outputSplatIndex: number,
184
+ positions: Float32Array,
185
+ scales: Float32Array,
186
+ rotations: Float32Array,
187
+ colors: Uint8Array,
188
+ opacities: Float32Array,
189
+ sphericalHarmonics: Float32Array | undefined,
190
+ sphericalHarmonicsComponentCount: number
191
+ ): void {
192
+ const layout = COMPRESSION_LAYOUTS[header.compressionLevel];
193
+ const splatBase = section.dataBase + section.bytesPerSplat * localSplatIndex;
194
+ const positionOffset = outputSplatIndex * 3;
195
+ const rotationOffset = outputSplatIndex * 4;
196
+ const bucketCenter = getBucketCenter(data, section, localSplatIndex);
197
+
198
+ positions[positionOffset + 0] = decodeKSPLATCenterComponent(
199
+ data,
200
+ splatBase,
201
+ 0,
202
+ header,
203
+ section,
204
+ bucketCenter[0]
205
+ );
206
+ positions[positionOffset + 1] = decodeKSPLATCenterComponent(
207
+ data,
208
+ splatBase,
209
+ 1,
210
+ header,
211
+ section,
212
+ bucketCenter[1]
213
+ );
214
+ positions[positionOffset + 2] = decodeKSPLATCenterComponent(
215
+ data,
216
+ splatBase,
217
+ 2,
218
+ header,
219
+ section,
220
+ bucketCenter[2]
221
+ );
222
+
223
+ scales[positionOffset + 0] = readCompressedFloat(
224
+ data,
225
+ splatBase + layout.scaleOffsetBytes,
226
+ 0,
227
+ header.compressionLevel
228
+ );
229
+ scales[positionOffset + 1] = readCompressedFloat(
230
+ data,
231
+ splatBase + layout.scaleOffsetBytes,
232
+ 1,
233
+ header.compressionLevel
234
+ );
235
+ scales[positionOffset + 2] = readCompressedFloat(
236
+ data,
237
+ splatBase + layout.scaleOffsetBytes,
238
+ 2,
239
+ header.compressionLevel
240
+ );
241
+
242
+ const [w, x, y, z] = normalizeQuaternion(
243
+ readCompressedFloat(data, splatBase + layout.scaleOffsetBytes, 3, header.compressionLevel),
244
+ readCompressedFloat(data, splatBase + layout.scaleOffsetBytes, 4, header.compressionLevel),
245
+ readCompressedFloat(data, splatBase + layout.scaleOffsetBytes, 5, header.compressionLevel),
246
+ readCompressedFloat(data, splatBase + layout.scaleOffsetBytes, 6, header.compressionLevel)
247
+ );
248
+ rotations[rotationOffset + 0] = w;
249
+ rotations[rotationOffset + 1] = x;
250
+ rotations[rotationOffset + 2] = y;
251
+ rotations[rotationOffset + 3] = z;
252
+
253
+ const colorBase = splatBase + layout.colorOffsetBytes;
254
+ const colorView = new Uint8Array(data, colorBase, 4);
255
+ colors[positionOffset + 0] = colorView[0];
256
+ colors[positionOffset + 1] = colorView[1];
257
+ colors[positionOffset + 2] = colorView[2];
258
+ opacities[outputSplatIndex] = convertAlphaByteToLinearOpacity(colorView[3]);
259
+
260
+ if (sphericalHarmonics && sphericalHarmonicsComponentCount) {
261
+ decodeSphericalHarmonics(
262
+ data,
263
+ splatBase + layout.sphericalHarmonicsOffsetBytes,
264
+ header,
265
+ section,
266
+ outputSplatIndex,
267
+ sphericalHarmonics,
268
+ sphericalHarmonicsComponentCount
269
+ );
270
+ }
271
+ }
272
+
273
+ /** Reads and validates the fixed-size KSPLAT global header. */
274
+ function parseKSPLATHeader(data: ArrayBuffer): KSPLATHeader {
275
+ const dataView = new DataView(data, 0, HEADER_BYTE_LENGTH);
276
+ const compressionLevel = dataView.getUint16(20, true);
277
+ if (compressionLevel !== 0 && compressionLevel !== 1 && compressionLevel !== 2) {
278
+ throw new Error(`KSPLATLoader: unsupported compression level ${compressionLevel}.`);
279
+ }
280
+ return {
281
+ versionMajor: dataView.getUint8(0),
282
+ versionMinor: dataView.getUint8(1),
283
+ maxSectionCount: dataView.getUint32(4, true),
284
+ sectionCount: dataView.getUint32(8, true),
285
+ maxSplatCount: dataView.getUint32(12, true),
286
+ splatCount: dataView.getUint32(16, true),
287
+ compressionLevel,
288
+ minSphericalHarmonicsCoeff:
289
+ dataView.getFloat32(36, true) || -DEFAULT_SH_8BIT_COMPRESSION_HALF_RANGE,
290
+ maxSphericalHarmonicsCoeff:
291
+ dataView.getFloat32(40, true) || DEFAULT_SH_8BIT_COMPRESSION_HALF_RANGE
292
+ };
293
+ }
294
+
295
+ /** Throws when the KSPLAT version is older than the supported SplatBuffer version. */
296
+ function checkKSPLATVersion(header: KSPLATHeader): void {
297
+ if (
298
+ header.versionMajor === CURRENT_MAJOR_VERSION
299
+ ? header.versionMinor >= CURRENT_MINOR_VERSION
300
+ : header.versionMajor > CURRENT_MAJOR_VERSION
301
+ ) {
302
+ return;
303
+ }
304
+ throw new Error(
305
+ `KSPLATLoader: version ${header.versionMajor}.${header.versionMinor} is not supported.`
306
+ );
307
+ }
308
+
309
+ /** Reads all fixed-size KSPLAT section headers and derives their byte ranges. */
310
+ function parseKSPLATSections(data: ArrayBuffer, header: KSPLATHeader): KSPLATSection[] {
311
+ if (data.byteLength < HEADER_BYTE_LENGTH + header.maxSectionCount * SECTION_HEADER_BYTE_LENGTH) {
312
+ throw new Error('KSPLATLoader: file does not contain all section headers.');
313
+ }
314
+
315
+ const sections: KSPLATSection[] = [];
316
+ let sectionBase = HEADER_BYTE_LENGTH + header.maxSectionCount * SECTION_HEADER_BYTE_LENGTH;
317
+ let splatCountOffset = 0;
318
+
319
+ for (let sectionIndex = 0; sectionIndex < header.maxSectionCount; sectionIndex++) {
320
+ const sectionHeaderBase = HEADER_BYTE_LENGTH + sectionIndex * SECTION_HEADER_BYTE_LENGTH;
321
+ const dataView = new DataView(data, sectionHeaderBase, SECTION_HEADER_BYTE_LENGTH);
322
+ const splatCount = dataView.getUint32(0, true);
323
+ const maxSplatCount = dataView.getUint32(4, true);
324
+ const bucketSize = dataView.getUint32(8, true);
325
+ const bucketCount = dataView.getUint32(12, true);
326
+ const bucketBlockSize = dataView.getFloat32(16, true);
327
+ const bucketStorageSizeBytes = dataView.getUint16(20, true);
328
+ const compressionScaleRange =
329
+ dataView.getUint32(24, true) || COMPRESSION_LAYOUTS[header.compressionLevel].scaleRange;
330
+ const fullBucketCount = dataView.getUint32(32, true);
331
+ const partiallyFilledBucketCount = dataView.getUint32(36, true);
332
+ const sphericalHarmonicsDegree = dataView.getUint16(40, true);
333
+ const bytesPerSplat = getBytesPerSplat(header.compressionLevel, sphericalHarmonicsDegree);
334
+ const bucketsMetaDataSizeBytes = partiallyFilledBucketCount * 4;
335
+ const bucketsStorageSizeBytes = bucketStorageSizeBytes * bucketCount + bucketsMetaDataSizeBytes;
336
+ const storageSizeBytes = bytesPerSplat * maxSplatCount + bucketsStorageSizeBytes;
337
+
338
+ sections.push({
339
+ bytesPerSplat,
340
+ splatCountOffset,
341
+ splatCount: splatCount || maxSplatCount,
342
+ maxSplatCount,
343
+ bucketSize,
344
+ bucketCount,
345
+ bucketStorageSizeBytes,
346
+ bucketsStorageSizeBytes,
347
+ compressionScaleRange,
348
+ compressionScaleFactor: bucketBlockSize / 2 / compressionScaleRange,
349
+ base: sectionBase,
350
+ bucketsBase: sectionBase + bucketsMetaDataSizeBytes,
351
+ dataBase: sectionBase + bucketsStorageSizeBytes,
352
+ fullBucketCount,
353
+ partiallyFilledBucketCount,
354
+ sphericalHarmonicsDegree
355
+ });
356
+
357
+ sectionBase += storageSizeBytes;
358
+ splatCountOffset += maxSplatCount;
359
+ }
360
+
361
+ return sections;
362
+ }
363
+
364
+ /** Decodes one center component, including bucket-relative decompression. */
365
+ function decodeKSPLATCenterComponent(
366
+ data: ArrayBuffer,
367
+ splatBase: number,
368
+ component: number,
369
+ header: KSPLATHeader,
370
+ section: KSPLATSection,
371
+ bucketCenter: number
372
+ ): number {
373
+ const value = readCompressedCenterValue(data, splatBase, component, header.compressionLevel);
374
+ return header.compressionLevel >= 1
375
+ ? (value - section.compressionScaleRange) * section.compressionScaleFactor + bucketCenter
376
+ : value;
377
+ }
378
+
379
+ /** Reads one center component, preserving quantized uint16 values for compressed sections. */
380
+ function readCompressedCenterValue(
381
+ data: ArrayBuffer,
382
+ byteOffset: number,
383
+ component: number,
384
+ compressionLevel: 0 | 1 | 2
385
+ ): number {
386
+ const dataView = new DataView(data);
387
+ return compressionLevel === 0
388
+ ? dataView.getFloat32(byteOffset + component * 4, true)
389
+ : dataView.getUint16(byteOffset + component * 2, true);
390
+ }
391
+
392
+ /** Returns the bucket center for a compressed splat section. */
393
+ function getBucketCenter(
394
+ data: ArrayBuffer,
395
+ section: KSPLATSection,
396
+ localSplatIndex: number
397
+ ): [number, number, number] {
398
+ if (!section.bucketCount) {
399
+ return [0, 0, 0];
400
+ }
401
+ const bucketIndex = getBucketIndex(data, section, localSplatIndex);
402
+ const bucketBase = section.bucketsBase + bucketIndex * section.bucketStorageSizeBytes;
403
+ const dataView = new DataView(data, bucketBase, 12);
404
+ return [dataView.getFloat32(0, true), dataView.getFloat32(4, true), dataView.getFloat32(8, true)];
405
+ }
406
+
407
+ /** Returns the bucket index containing a local splat index. */
408
+ function getBucketIndex(
409
+ data: ArrayBuffer,
410
+ section: KSPLATSection,
411
+ localSplatIndex: number
412
+ ): number {
413
+ const maxSplatIndexInFullBuckets = section.fullBucketCount * section.bucketSize;
414
+ if (localSplatIndex < maxSplatIndexInFullBuckets) {
415
+ return Math.floor(localSplatIndex / section.bucketSize);
416
+ }
417
+
418
+ let bucketSplatIndex = maxSplatIndexInFullBuckets;
419
+ let bucketIndex = section.fullBucketCount;
420
+ for (
421
+ let partiallyFilledBucketIndex = 0;
422
+ partiallyFilledBucketIndex < section.partiallyFilledBucketCount;
423
+ partiallyFilledBucketIndex++
424
+ ) {
425
+ const bucketLength = new DataView(
426
+ data,
427
+ section.base + partiallyFilledBucketIndex * 4,
428
+ 4
429
+ ).getUint32(0, true);
430
+ if (localSplatIndex < bucketSplatIndex + bucketLength) {
431
+ return bucketIndex;
432
+ }
433
+ bucketSplatIndex += bucketLength;
434
+ bucketIndex++;
435
+ }
436
+ return Math.max(bucketIndex - 1, 0);
437
+ }
438
+
439
+ /** Decodes all SH rest coefficients for one splat into the output array. */
440
+ function decodeSphericalHarmonics(
441
+ data: ArrayBuffer,
442
+ byteOffset: number,
443
+ header: KSPLATHeader,
444
+ section: KSPLATSection,
445
+ outputSplatIndex: number,
446
+ output: Float32Array,
447
+ outputComponentCount: number
448
+ ): void {
449
+ const componentCount = getSphericalHarmonicsComponentCount(section.sphericalHarmonicsDegree);
450
+ const componentBase = outputSplatIndex * outputComponentCount;
451
+ for (let component = 0; component < componentCount; component++) {
452
+ output[componentBase + component] = readCompressedFloat(
453
+ data,
454
+ byteOffset,
455
+ component,
456
+ header.compressionLevel,
457
+ true,
458
+ header.minSphericalHarmonicsCoeff,
459
+ header.maxSphericalHarmonicsCoeff
460
+ );
461
+ }
462
+ }
463
+
464
+ /** Reads one compressed float or spherical harmonic coefficient. */
465
+ function readCompressedFloat(
466
+ data: ArrayBuffer,
467
+ byteOffset: number,
468
+ component: number,
469
+ compressionLevel: 0 | 1 | 2,
470
+ sphericalHarmonic: boolean = false,
471
+ sphericalHarmonicRangeMin: number = 0,
472
+ sphericalHarmonicRangeMax: number = 0
473
+ ): number {
474
+ const dataView = new DataView(data);
475
+ if (compressionLevel === 0) {
476
+ return dataView.getFloat32(byteOffset + component * 4, true);
477
+ }
478
+ if (compressionLevel === 1 || !sphericalHarmonic) {
479
+ return decodeFloat16(dataView.getUint16(byteOffset + component * 2, true));
480
+ }
481
+ const value = dataView.getUint8(byteOffset + component);
482
+ return (
483
+ (value / 255) * (sphericalHarmonicRangeMax - sphericalHarmonicRangeMin) +
484
+ sphericalHarmonicRangeMin
485
+ );
486
+ }
487
+
488
+ /** Returns the byte length of one splat record for a compression level and SH degree. */
489
+ function getBytesPerSplat(compressionLevel: 0 | 1 | 2, sphericalHarmonicsDegree: number): number {
490
+ const layout = COMPRESSION_LAYOUTS[compressionLevel];
491
+ return (
492
+ layout.bytesPerCenter +
493
+ layout.bytesPerScale +
494
+ layout.bytesPerRotation +
495
+ layout.bytesPerColor +
496
+ layout.bytesPerSphericalHarmonicsComponent *
497
+ getSphericalHarmonicsComponentCount(sphericalHarmonicsDegree)
498
+ );
499
+ }
500
+
501
+ /** Returns the number of SH rest coefficients for a GaussianSplats3D SH degree. */
502
+ function getSphericalHarmonicsComponentCount(sphericalHarmonicsDegree: number): number {
503
+ switch (sphericalHarmonicsDegree) {
504
+ case 0:
505
+ return 0;
506
+ case 1:
507
+ return 9;
508
+ case 2:
509
+ return 24;
510
+ case 3:
511
+ return 45;
512
+ default:
513
+ throw new Error(
514
+ `KSPLATLoader: unsupported spherical harmonics degree ${sphericalHarmonicsDegree}.`
515
+ );
516
+ }
517
+ }
518
+
519
+ /** Returns the active section count in a complete KSPLAT file. */
520
+ function getActiveSectionCount(header: KSPLATHeader): number {
521
+ return header.sectionCount || header.maxSectionCount;
522
+ }
523
+
524
+ /** Returns the total number of splats that will be decoded. */
525
+ function getTotalSplatCount(header: KSPLATHeader, sections: KSPLATSection[]): number {
526
+ const sectionSplatCount = sections
527
+ .slice(0, getActiveSectionCount(header))
528
+ .reduce((splatCount, section) => splatCount + section.splatCount, 0);
529
+ return header.splatCount
530
+ ? Math.min(header.splatCount, sectionSplatCount || header.splatCount)
531
+ : sectionSplatCount;
532
+ }
533
+
534
+ /** Throws when a section record range extends beyond the file buffer. */
535
+ function validateSectionBounds(data: ArrayBuffer, section: KSPLATSection): void {
536
+ const requiredByteLength = section.dataBase + section.bytesPerSplat * section.splatCount;
537
+ if (requiredByteLength > data.byteLength) {
538
+ throw new Error('KSPLATLoader: section data extends beyond the file length.');
539
+ }
540
+ }