@manycore/aholo-splat-transform 1.2.8 → 1.2.10

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 (97) hide show
  1. package/CHANGELOG.md +124 -113
  2. package/README.md +39 -39
  3. package/THIRD_PARTY_LICENSES.txt +1373 -1373
  4. package/bin/cli.js +125 -118
  5. package/dist/SplatData.d.ts +67 -67
  6. package/dist/SplatData.js +167 -150
  7. package/dist/constant.d.ts +3 -3
  8. package/dist/constant.js +13 -13
  9. package/dist/file/IFile.d.ts +5 -5
  10. package/dist/file/IFile.js +1 -1
  11. package/dist/file/esz.d.ts +11 -11
  12. package/dist/file/esz.js +337 -322
  13. package/dist/file/index.d.ts +8 -8
  14. package/dist/file/index.js +7 -7
  15. package/dist/file/ksplat.d.ts +12 -12
  16. package/dist/file/ksplat.js +293 -231
  17. package/dist/file/lcc.d.ts +11 -11
  18. package/dist/file/lcc.js +161 -158
  19. package/dist/file/ply.d.ts +13 -13
  20. package/dist/file/ply.js +439 -390
  21. package/dist/file/sog.d.ts +80 -80
  22. package/dist/file/sog.js +525 -494
  23. package/dist/file/splat.d.ts +6 -6
  24. package/dist/file/splat.js +119 -99
  25. package/dist/file/spz.d.ts +11 -11
  26. package/dist/file/spz.js +597 -583
  27. package/dist/file/voxel.d.ts +43 -37
  28. package/dist/file/voxel.js +411 -280
  29. package/dist/index.d.ts +33 -33
  30. package/dist/index.js +54 -54
  31. package/dist/native/index.d.ts +54 -54
  32. package/dist/native/index.js +122 -129
  33. package/dist/native/utils.d.ts +1 -0
  34. package/dist/native/utils.js +54 -0
  35. package/dist/tasks/AutoChunkLodTask.d.ts +13 -13
  36. package/dist/tasks/AutoChunkLodTask.js +117 -117
  37. package/dist/tasks/AutoLodTask.d.ts +10 -10
  38. package/dist/tasks/AutoLodTask.js +20 -20
  39. package/dist/tasks/BaseTask.d.ts +15 -15
  40. package/dist/tasks/BaseTask.js +5 -5
  41. package/dist/tasks/FlexLodTask.d.ts +12 -12
  42. package/dist/tasks/FlexLodTask.js +54 -44
  43. package/dist/tasks/ModifyTask.d.ts +9 -9
  44. package/dist/tasks/ModifyTask.js +166 -156
  45. package/dist/tasks/ReadTask.d.ts +9 -9
  46. package/dist/tasks/ReadTask.js +29 -29
  47. package/dist/tasks/SkeletonLodTask.d.ts +10 -10
  48. package/dist/tasks/SkeletonLodTask.js +176 -156
  49. package/dist/tasks/VoxelTask.d.ts +35 -30
  50. package/dist/tasks/VoxelTask.js +40 -37
  51. package/dist/tasks/WriteTask.d.ts +12 -12
  52. package/dist/tasks/WriteTask.js +70 -70
  53. package/dist/utils/BufferReader.d.ts +12 -12
  54. package/dist/utils/BufferReader.js +45 -45
  55. package/dist/utils/Logger.d.ts +11 -11
  56. package/dist/utils/Logger.js +40 -40
  57. package/dist/utils/StreamChunkDecoder.d.ts +16 -16
  58. package/dist/utils/StreamChunkDecoder.js +31 -31
  59. package/dist/utils/index.d.ts +27 -27
  60. package/dist/utils/index.js +101 -101
  61. package/dist/utils/k-means.d.ts +4 -4
  62. package/dist/utils/k-means.js +340 -341
  63. package/dist/utils/math.d.ts +46 -46
  64. package/dist/utils/math.js +350 -346
  65. package/dist/utils/quantize-1d.d.ts +4 -4
  66. package/dist/utils/quantize-1d.js +164 -164
  67. package/dist/utils/sh-rotate.d.ts +2 -2
  68. package/dist/utils/sh-rotate.js +236 -175
  69. package/dist/utils/splat.d.ts +21 -21
  70. package/dist/utils/splat.js +397 -387
  71. package/dist/utils/voxel/binary.d.ts +8 -0
  72. package/dist/utils/voxel/binary.js +176 -0
  73. package/dist/utils/voxel/common.d.ts +178 -162
  74. package/dist/utils/voxel/common.js +1752 -1682
  75. package/dist/utils/voxel/coplanar-merge.d.ts +63 -63
  76. package/dist/utils/voxel/coplanar-merge.js +818 -819
  77. package/dist/utils/voxel/filter-cluster.d.ts +20 -0
  78. package/dist/utils/voxel/filter-cluster.js +628 -0
  79. package/dist/utils/voxel/gpu-dilation.d.ts +2 -2
  80. package/dist/utils/voxel/gpu-dilation.js +677 -656
  81. package/dist/utils/voxel/marching-cubes.d.ts +42 -42
  82. package/dist/utils/voxel/marching-cubes.js +1645 -1657
  83. package/dist/utils/voxel/mesh.d.ts +3 -3
  84. package/dist/utils/voxel/mesh.js +130 -130
  85. package/dist/utils/voxel/nav.d.ts +29 -29
  86. package/dist/utils/voxel/nav.js +1068 -1043
  87. package/dist/utils/voxel/postprocess.d.ts +23 -23
  88. package/dist/utils/voxel/postprocess.js +408 -375
  89. package/dist/utils/voxel/voxel-faces.d.ts +18 -18
  90. package/dist/utils/voxel/voxel-faces.js +662 -663
  91. package/dist/utils/voxel/voxelize.d.ts +34 -33
  92. package/dist/utils/voxel/voxelize.js +1208 -1193
  93. package/dist/utils/webgpu.d.ts +8 -8
  94. package/dist/utils/webgpu.js +122 -122
  95. package/package.json +38 -39
  96. package/dist/native/cpp/bin/linux/binding.node +0 -0
  97. package/dist/native/cpp/bin/windows/binding.node +0 -0
package/dist/file/spz.js CHANGED
@@ -1,583 +1,597 @@
1
- import { constants as zlibConstants, zstdCompressSync, zstdDecompressSync } from 'node:zlib';
2
- import { SH_C0, SH_MAPS } from '../constant.js';
3
- import { BufferReader, fromHalf, clamp, StreamChunkDecoder, mortonSort } from '../utils/index.js';
4
- const SPZ_MAGIC = 0x5053474e; // NGSP = Niantic gaussian splat
5
- const SPZ_VERSION = 3;
6
- const ZSTD_COMPRESSION_LEVEL = 12;
7
- const FLAG_ANTIALIASED = 0x1;
8
- const COLOR_SCALE = SH_C0 / 0.15;
9
- const rotation = new Array(4);
10
- const SH_SCALE1 = 1 << 3;
11
- const SH_SCALE2 = 1 << 4;
12
- export class SpzFile {
13
- constructor(compressLevel, spzVersion = SPZ_VERSION) {
14
- if (spzVersion !== 3 && spzVersion !== 4) {
15
- throw new Error(`Unsupported SPZ version: ${spzVersion}`);
16
- }
17
- this.compressLevel = compressLevel;
18
- this.spzVersion = spzVersion;
19
- }
20
- async read(stream, _contentLength, data) {
21
- const setCenter = data.setCenter.bind(data);
22
- const setAlpha = data.setAlpha.bind(data);
23
- const setColor = data.setColor.bind(data);
24
- const setScale = data.setScale.bind(data);
25
- const setQuat = data.setQuat.bind(data);
26
- const setShN = data.setShN.bind(data);
27
- const SCALE_LUT = new Float32Array(256);
28
- for (let i = 0; i < 256; i++) {
29
- SCALE_LUT[i] = Math.exp(i / 16 - 10);
30
- }
31
- const COLOR_LUT = new Float32Array(256);
32
- for (let i = 0; i < 256; i++) {
33
- COLOR_LUT[i] = (i / 255 - 0.5) * COLOR_SCALE + 0.5;
34
- }
35
- let version = SPZ_VERSION;
36
- let counts = 0;
37
- let shDegree = 0;
38
- let fractionalBits = 12;
39
- let flags = FLAG_ANTIALIASED;
40
- let reserved = 0;
41
- let isF16 = false;
42
- let useSmallestThreeQuat = true;
43
- let fraction = 1;
44
- let fractionInv = 1;
45
- let shCounts = 0;
46
- let BlockOffset = 0;
47
- const shN = [];
48
- const reader = new BufferReader();
49
- const decoder = new StreamChunkDecoder(reader);
50
- decoder.setDecoders([
51
- {
52
- init: () => [1, 16],
53
- decode: async (_offset, _counts, buf) => {
54
- const header = new DataView(buf.buffer);
55
- if (header.getUint32(0, true) !== SPZ_MAGIC) {
56
- throw new Error('Invalid SPZ file');
57
- }
58
- ({ version, counts, shDegree, fractionalBits, flags, extra: reserved } = readSpzHeader(header));
59
- if (version < 1 || version > 3) {
60
- throw new Error(`Unsupported SPZ version: ${version}`);
61
- }
62
- isF16 = version < 2;
63
- useSmallestThreeQuat = version >= 3;
64
- fraction = 1 << fractionalBits;
65
- fractionInv = 1 / fraction;
66
- shCounts = SH_MAPS[shDegree];
67
- BlockOffset = await data.initBlock(counts, shDegree);
68
- if (flags || reserved) {
69
- //
70
- }
71
- },
72
- },
73
- {
74
- init: () => [counts, isF16 ? 6 : 9],
75
- decode: (offset, counts, buf) => {
76
- offset += BlockOffset;
77
- let x, y, z;
78
- for (let i = 0; i < counts; i++) {
79
- if (isF16) {
80
- const o = i * 6;
81
- x = fromHalf((buf[o + 1] << 8) | buf[o]);
82
- y = fromHalf((buf[o + 3] << 8) | buf[o + 2]);
83
- z = fromHalf((buf[o + 5] << 8) | buf[o + 4]);
84
- }
85
- else {
86
- const o = i * 9;
87
- x = (((buf[o + 2] << 24) | (buf[o + 1] << 16) | (buf[o] << 8)) >> 8) * fractionInv;
88
- y = (((buf[o + 5] << 24) | (buf[o + 4] << 16) | (buf[o + 3] << 8)) >> 8) * fractionInv;
89
- z = (((buf[o + 8] << 24) | (buf[o + 7] << 16) | (buf[o + 6] << 8)) >> 8) * fractionInv;
90
- }
91
- setCenter(offset + i, x, y, z);
92
- }
93
- },
94
- },
95
- {
96
- init: () => [counts, 1],
97
- decode: (offset, counts, buf) => {
98
- offset += BlockOffset;
99
- for (let i = 0; i < counts; i++) {
100
- setAlpha(offset + i, buf[i] / 255);
101
- }
102
- },
103
- },
104
- {
105
- init: () => [counts, 3],
106
- decode: (offset, counts, buf) => {
107
- offset += BlockOffset;
108
- for (let i = 0; i < counts; i++) {
109
- const o = i * 3;
110
- setColor(offset + i, COLOR_LUT[buf[o]], COLOR_LUT[buf[o + 1]], COLOR_LUT[buf[o + 2]]);
111
- }
112
- },
113
- },
114
- {
115
- init: () => [counts, 3],
116
- decode: (offset, counts, buf) => {
117
- offset += BlockOffset;
118
- for (let i = 0; i < counts; i++) {
119
- const o = i * 3;
120
- setScale(offset + i, SCALE_LUT[buf[o]], SCALE_LUT[buf[o + 1]], SCALE_LUT[buf[o + 2]]);
121
- }
122
- },
123
- },
124
- {
125
- init: () => [counts, useSmallestThreeQuat ? 4 : 3],
126
- decode: (offset, counts, buf) => {
127
- offset += BlockOffset;
128
- let qx, qy, qz, qw;
129
- for (let i = 0; i < counts; i++) {
130
- if (!useSmallestThreeQuat) {
131
- const o = i * 3;
132
- qx = buf[o] / 127.5 - 1;
133
- qy = buf[o + 1] / 127.5 - 1;
134
- qz = buf[o + 2] / 127.5 - 1;
135
- qw = Math.sqrt(Math.max(0, 1 - qx * qx - qy * qy - qz * qz));
136
- }
137
- else {
138
- const o = i * 4;
139
- const packed = buf[o] | (buf[o + 1] << 8) | (buf[o + 2] << 16) | (buf[o + 3] << 24);
140
- const largest = packed >>> 30;
141
- let temp = packed;
142
- let sum = 0;
143
- for (let j = 3; j >= 0; j--) {
144
- if (j === largest) {
145
- continue;
146
- }
147
- const mag = temp & 0x1FF;
148
- const sign = (temp >>> 9) & 1;
149
- temp >>>= 10;
150
- const v = Math.SQRT1_2 * (mag / 0x1FF) * (sign ? -1 : 1);
151
- rotation[j] = v;
152
- sum += v * v;
153
- }
154
- rotation[largest] = Math.sqrt(1 - sum);
155
- qx = rotation[0];
156
- qy = rotation[1];
157
- qz = rotation[2];
158
- qw = rotation[3];
159
- }
160
- setQuat(offset + i, qx, qy, qz, qw);
161
- }
162
- },
163
- },
164
- {
165
- init: () => [counts, shCounts],
166
- decode: (offset, counts, buf) => {
167
- offset += BlockOffset;
168
- for (let i = 0; i < counts; i++) {
169
- const o = i * shCounts;
170
- for (let j = 0; j < shCounts; j++) {
171
- shN[j] = (buf[o + j] - 128) / 128;
172
- }
173
- setShN(offset + i, shN);
174
- }
175
- },
176
- },
177
- ]);
178
- const peeked = await peekStream(stream, 8);
179
- stream = peeked.stream;
180
- if (isSpzV4(peeked.prefix)) {
181
- await readSpzV4Stream(stream, reader, decoder);
182
- data.finishBlock();
183
- return;
184
- }
185
- let source;
186
- if (this.compressLevel === -1) {
187
- source = stream.getReader();
188
- }
189
- else {
190
- source = stream.pipeThrough(new DecompressionStream('gzip')).getReader();
191
- }
192
- while (true) {
193
- const { done, value } = await source.read();
194
- if (done) {
195
- break;
196
- }
197
- reader.write(value);
198
- decoder.flush();
199
- }
200
- data.finishBlock();
201
- }
202
- async write(writeStream, data, indices = mortonSort(data)) {
203
- if (this.spzVersion === 4) {
204
- await this.writeV4(writeStream, data, indices);
205
- }
206
- else {
207
- await this.writeV3(writeStream, data, indices);
208
- }
209
- }
210
- async writeV3(writeStream, data, indices) {
211
- let writer;
212
- let pipePromise;
213
- if (this.compressLevel === -1) {
214
- writer = writeStream.getWriter();
215
- pipePromise = Promise.resolve();
216
- }
217
- else {
218
- const compressStream = new CompressionStream('gzip');
219
- pipePromise = compressStream.readable.pipeTo(writeStream);
220
- writer = compressStream.writable.getWriter();
221
- }
222
- const version = SPZ_VERSION;
223
- const counts = data.counts;
224
- const shDegree = data.shDegree;
225
- const fractionalBits = 12;
226
- const flags = FLAG_ANTIALIASED;
227
- const shCounts = getShCounts(shDegree);
228
- const context = createSpzEncodeContext(data, indices, fractionalBits, shCounts);
229
- // header
230
- writer.write(createSpzHeader(version, counts, shDegree, fractionalBits, flags, 0));
231
- for (const attribute of getSpzAttributes(shDegree)) {
232
- await writeSpzAttribute(writer, context, attribute);
233
- }
234
- await writer.close();
235
- await pipePromise;
236
- }
237
- async writeV4(writeStream, data, indices) {
238
- const version = 4;
239
- const counts = data.counts;
240
- const shDegree = data.shDegree;
241
- const fractionalBits = 12;
242
- const flags = FLAG_ANTIALIASED;
243
- const shCounts = getShCounts(shDegree);
244
- const context = createSpzEncodeContext(data, indices, fractionalBits, shCounts);
245
- const compressed = [];
246
- const uncompressedSizes = [];
247
- for (const attribute of getSpzAttributes(shDegree)) {
248
- const chunk = createSpzAttributeChunk(context, attribute, 0, counts);
249
- uncompressedSizes.push(chunk.byteLength);
250
- compressed.push(zstdCompressSync(chunk, {
251
- params: {
252
- [zlibConstants.ZSTD_c_compressionLevel]: ZSTD_COMPRESSION_LEVEL,
253
- },
254
- }));
255
- }
256
- const tocByteOffset = 32;
257
- const tocSize = compressed.length * 16;
258
- const header = createSpzHeader(version, counts, shDegree, fractionalBits, flags, compressed.length, 32);
259
- new DataView(header.buffer).setUint32(16, tocByteOffset, true);
260
- const toc = new Uint8Array(tocSize);
261
- const tocView = new DataView(toc.buffer);
262
- for (let i = 0; i < compressed.length; i++) {
263
- const entryOffset = i * 16;
264
- writeUint64(tocView, entryOffset, compressed[i].byteLength);
265
- writeUint64(tocView, entryOffset + 8, uncompressedSizes[i]);
266
- }
267
- const writer = writeStream.getWriter();
268
- await writer.write(header);
269
- await writer.write(toc);
270
- for (const chunk of compressed) {
271
- await writer.write(chunk);
272
- }
273
- await writer.close();
274
- }
275
- }
276
- function getShCounts(shDegree) {
277
- const shCounts = SH_MAPS[shDegree];
278
- if (shCounts === undefined) {
279
- throw new Error(`Unsupported SPZ SH degree: ${shDegree}`);
280
- }
281
- return shCounts;
282
- }
283
- function createSpzEncodeContext(data, indices, fractionalBits, shCounts) {
284
- return {
285
- data,
286
- indices,
287
- fractionalBits,
288
- fraction: 1 << fractionalBits,
289
- shCounts,
290
- single: {
291
- x: 0, y: 0, z: 0,
292
- sx: 0, sy: 0, sz: 0,
293
- qx: 0, qy: 0, qz: 0, qw: 0,
294
- r: 0, g: 0, b: 0, a: 0,
295
- shN: new Array(shCounts),
296
- },
297
- };
298
- }
299
- function getSpzAttributes(shDegree) {
300
- return shDegree > 0 ? ['position', 'alpha', 'color', 'scale', 'quat', 'sh'] : ['position', 'alpha', 'color', 'scale', 'quat'];
301
- }
302
- function getSpzAttributeInfo(attribute, shCounts) {
303
- switch (attribute) {
304
- case 'position':
305
- return { itemSize: 9, chunkSize: 4096 };
306
- case 'alpha':
307
- return { itemSize: 1, chunkSize: 65536 };
308
- case 'color':
309
- case 'scale':
310
- return { itemSize: 3, chunkSize: 16384 };
311
- case 'quat':
312
- return { itemSize: 4, chunkSize: 16384 };
313
- case 'sh':
314
- return { itemSize: shCounts, chunkSize: 1024 };
315
- }
316
- }
317
- function createSpzAttributeChunk(context, attribute, offset, counts) {
318
- const { data, indices, single, shCounts } = context;
319
- const { itemSize } = getSpzAttributeInfo(attribute, shCounts);
320
- const chunk = new Uint8Array(counts * itemSize);
321
- for (let i = 0; i < counts; i++) {
322
- const index = indices[offset + i];
323
- switch (attribute) {
324
- case 'position': {
325
- data.getCenter(index, single);
326
- const o = i * itemSize;
327
- const ix = clamp(single.x * context.fraction, -0x7fffff, 0x7fffff);
328
- chunk[o + 0] = ix & 0xff;
329
- chunk[o + 1] = (ix >> 8) & 0xff;
330
- chunk[o + 2] = (ix >> 16) & 0xff;
331
- const iy = clamp(single.y * context.fraction, -0x7fffff, 0x7fffff);
332
- chunk[o + 3] = iy & 0xff;
333
- chunk[o + 4] = (iy >> 8) & 0xff;
334
- chunk[o + 5] = (iy >> 16) & 0xff;
335
- const iz = clamp(single.z * context.fraction, -0x7fffff, 0x7fffff);
336
- chunk[o + 6] = iz & 0xff;
337
- chunk[o + 7] = (iz >> 8) & 0xff;
338
- chunk[o + 8] = (iz >> 16) & 0xff;
339
- break;
340
- }
341
- case 'alpha':
342
- data.getAlpha(index, single);
343
- chunk[i] = clamp(Math.round(single.a * 255), 0, 255);
344
- break;
345
- case 'color': {
346
- data.getColor(index, single);
347
- const o = i * itemSize;
348
- chunk[o + 0] = clamp(Math.round(((single.r - 0.5) / COLOR_SCALE + 0.5) * 255), 0, 255);
349
- chunk[o + 1] = clamp(Math.round(((single.g - 0.5) / COLOR_SCALE + 0.5) * 255), 0, 255);
350
- chunk[o + 2] = clamp(Math.round(((single.b - 0.5) / COLOR_SCALE + 0.5) * 255), 0, 255);
351
- break;
352
- }
353
- case 'scale': {
354
- data.getScale(index, single);
355
- const o = i * itemSize;
356
- chunk[o + 0] = clamp(Math.round((Math.log(single.sx) + 10) * 16), 0, 255);
357
- chunk[o + 1] = clamp(Math.round((Math.log(single.sy) + 10) * 16), 0, 255);
358
- chunk[o + 2] = clamp(Math.round((Math.log(single.sz) + 10) * 16), 0, 255);
359
- break;
360
- }
361
- case 'quat': {
362
- data.getQuat(index, single);
363
- const o = i * itemSize;
364
- rotation[0] = single.qx;
365
- rotation[1] = single.qy;
366
- rotation[2] = single.qz;
367
- rotation[3] = single.qw;
368
- let iLargest = 0;
369
- for (let j = 1; j < 4; ++j) {
370
- if (Math.abs(rotation[j]) > Math.abs(rotation[iLargest])) {
371
- iLargest = j;
372
- }
373
- }
374
- const negate = rotation[iLargest] < 0 ? 1 : 0;
375
- let comp = iLargest;
376
- for (let j = 0; j < 4; ++j) {
377
- if (j !== iLargest) {
378
- const negbit = (rotation[j] < 0 ? 1 : 0) ^ negate;
379
- const mag = Math.floor(((1 << 9) - 1) * (Math.abs(rotation[j]) / Math.SQRT1_2) + 0.5);
380
- comp = (comp << 10) | (negbit << 9) | mag;
381
- }
382
- }
383
- chunk[o + 0] = comp & 0xff;
384
- chunk[o + 1] = (comp >> 8) & 0xff;
385
- chunk[o + 2] = (comp >> 16) & 0xff;
386
- chunk[o + 3] = (comp >> 24) & 0xff;
387
- break;
388
- }
389
- case 'sh': {
390
- data.getShN(index, single.shN);
391
- const o = i * itemSize;
392
- for (let j = 0; j < itemSize; j++) {
393
- if (j < 9) {
394
- chunk[o + j] = clamp(Math.floor((Math.round(single.shN[j] * 128) + 128 + SH_SCALE1 / 2) / SH_SCALE1) * SH_SCALE1, 0, 255);
395
- continue;
396
- }
397
- chunk[o + j] = clamp(Math.floor((Math.round(single.shN[j] * 128) + 128 + SH_SCALE2 / 2) / SH_SCALE2) * SH_SCALE2, 0, 255);
398
- }
399
- break;
400
- }
401
- }
402
- }
403
- return chunk;
404
- }
405
- async function writeSpzAttribute(writer, context, attribute) {
406
- const { chunkSize } = getSpzAttributeInfo(attribute, context.shCounts);
407
- const chunkCounts = Math.ceil(context.data.counts / chunkSize);
408
- for (let i = 0; i < chunkCounts; i++) {
409
- if (writer.desiredSize <= 0) {
410
- await writer.ready;
411
- }
412
- const offset = i * chunkSize;
413
- const counts = Math.min(chunkSize, context.data.counts - offset);
414
- writer.write(createSpzAttributeChunk(context, attribute, offset, counts));
415
- }
416
- }
417
- function readUint64(view, offset) {
418
- const low = view.getUint32(offset, true);
419
- const high = view.getUint32(offset + 4, true);
420
- const value = high * 0x100000000 + low;
421
- if (!Number.isSafeInteger(value)) {
422
- throw new Error(`SPZ stream size is too large: ${value}`);
423
- }
424
- return value;
425
- }
426
- function writeUint64(view, offset, value) {
427
- if (!Number.isSafeInteger(value) || value < 0) {
428
- throw new Error(`Invalid SPZ stream size: ${value}`);
429
- }
430
- view.setUint32(offset, value >>> 0, true);
431
- view.setUint32(offset + 4, Math.floor(value / 0x100000000), true);
432
- }
433
- function createSpzHeader(version, counts, shDegree, fractionalBits, flags, extra, byteLength = 16) {
434
- const header = new DataView(new ArrayBuffer(byteLength));
435
- header.setUint32(0, SPZ_MAGIC, true);
436
- header.setUint32(4, version, true);
437
- header.setUint32(8, counts, true);
438
- header.setUint8(12, shDegree);
439
- header.setUint8(13, fractionalBits);
440
- header.setUint8(14, flags);
441
- header.setUint8(15, extra);
442
- return new Uint8Array(header.buffer);
443
- }
444
- function readSpzHeader(view) {
445
- return {
446
- version: view.getUint32(4, true),
447
- counts: view.getUint32(8, true),
448
- shDegree: view.getUint8(12),
449
- fractionalBits: view.getUint8(13),
450
- flags: view.getUint8(14),
451
- extra: view.getUint8(15),
452
- };
453
- }
454
- function getSpzV4AttributeSizes(counts, shDegree) {
455
- const shCounts = getShCounts(shDegree);
456
- const sizes = [
457
- counts * 9, // position
458
- counts, // alpha
459
- counts * 3, // color
460
- counts * 3, // scale
461
- counts * 4, // quat
462
- ];
463
- if (shDegree > 0) {
464
- sizes.push(counts * shCounts); // sh
465
- }
466
- return sizes;
467
- }
468
- function isSpzV4(buffer) {
469
- if (buffer.byteLength < 8) {
470
- return false;
471
- }
472
- const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
473
- return view.getUint32(0, true) === SPZ_MAGIC && view.getUint32(4, true) === 4;
474
- }
475
- async function readSpzV4Stream(stream, reader, decoder) {
476
- const read = createExactReader(stream);
477
- const header = await read(32);
478
- const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
479
- const { counts, shDegree, fractionalBits, flags, extra: numStreams } = readSpzHeader(view);
480
- const tocByteOffset = view.getUint32(16, true);
481
- const expectedSizes = getSpzV4AttributeSizes(counts, shDegree);
482
- if (numStreams !== expectedSizes.length) {
483
- throw new Error(`Invalid SPZ v4 stream count: ${numStreams}`);
484
- }
485
- if (tocByteOffset < 32) {
486
- throw new Error(`Invalid SPZ v4 TOC offset: ${tocByteOffset}`);
487
- }
488
- if (tocByteOffset > 32) {
489
- await read(tocByteOffset - 32);
490
- }
491
- const toc = await read(numStreams * 16);
492
- const tocView = new DataView(toc.buffer, toc.byteOffset, toc.byteLength);
493
- // Reuse the legacy v3 attribute decoder after parsing the v4 container.
494
- reader.write(createSpzHeader(SPZ_VERSION, counts, shDegree, fractionalBits, flags & FLAG_ANTIALIASED, 0));
495
- decoder.flush();
496
- for (let i = 0; i < numStreams; i++) {
497
- const entryOffset = i * 16;
498
- const compressedSize = readUint64(tocView, entryOffset);
499
- const uncompressedSize = readUint64(tocView, entryOffset + 8);
500
- if (uncompressedSize !== expectedSizes[i]) {
501
- throw new Error(`Invalid SPZ v4 stream size at index ${i}`);
502
- }
503
- const compressed = await read(compressedSize);
504
- const decompressed = zstdDecompressSync(compressed, {
505
- maxOutputLength: uncompressedSize,
506
- });
507
- if (decompressed.byteLength !== uncompressedSize) {
508
- throw new Error(`Invalid SPZ v4 decompressed size at index ${i}`);
509
- }
510
- reader.write(new Uint8Array(decompressed.buffer, decompressed.byteOffset, decompressed.byteLength));
511
- decoder.flush();
512
- }
513
- }
514
- // Return a reader that resolves exactly byteLength bytes and keeps leftover bytes for the next read.
515
- function createExactReader(stream) {
516
- const reader = stream.getReader();
517
- let chunk;
518
- let chunkOffset = 0;
519
- return async (byteLength) => {
520
- const result = new Uint8Array(byteLength);
521
- let offset = 0;
522
- while (offset < byteLength) {
523
- if (!chunk || chunkOffset >= chunk.byteLength) {
524
- const { done, value } = await reader.read();
525
- if (done || !value) {
526
- throw new Error('Invalid SPZ v4 file: stream ended unexpectedly');
527
- }
528
- chunk = value;
529
- chunkOffset = 0;
530
- }
531
- const copyLength = Math.min(byteLength - offset, chunk.byteLength - chunkOffset);
532
- result.set(chunk.subarray(chunkOffset, chunkOffset + copyLength), offset);
533
- chunkOffset += copyLength;
534
- offset += copyLength;
535
- }
536
- return result;
537
- };
538
- }
539
- // Peek leading bytes for format detection, then replay the consumed chunks through a replacement stream.
540
- async function peekStream(stream, byteLength) {
541
- const reader = stream.getReader();
542
- const chunks = [];
543
- let size = 0;
544
- while (size < byteLength) {
545
- const { done, value } = await reader.read();
546
- if (done || !value) {
547
- break;
548
- }
549
- chunks.push(value);
550
- size += value.byteLength;
551
- }
552
- const prefix = new Uint8Array(Math.min(size, byteLength));
553
- let offset = 0;
554
- for (const chunk of chunks) {
555
- const copyLength = Math.min(chunk.byteLength, prefix.byteLength - offset);
556
- prefix.set(chunk.subarray(0, copyLength), offset);
557
- offset += copyLength;
558
- if (offset === prefix.byteLength) {
559
- break;
560
- }
561
- }
562
- return {
563
- prefix,
564
- stream: new ReadableStream({
565
- start(controller) {
566
- for (const chunk of chunks) {
567
- controller.enqueue(chunk);
568
- }
569
- },
570
- async pull(controller) {
571
- const { done, value } = await reader.read();
572
- if (done) {
573
- controller.close();
574
- return;
575
- }
576
- controller.enqueue(value);
577
- },
578
- cancel(reason) {
579
- return reader.cancel(reason);
580
- },
581
- }),
582
- };
583
- }
1
+ import { constants as zlibConstants, zstdCompressSync, zstdDecompressSync } from 'node:zlib';
2
+ import { SH_C0, SH_MAPS } from '../constant.js';
3
+ import { BufferReader, fromHalf, clamp, StreamChunkDecoder, mortonSort } from '../utils/index.js';
4
+ const SPZ_MAGIC = 0x5053474e; // NGSP = Niantic gaussian splat
5
+ const SPZ_VERSION = 3;
6
+ const ZSTD_COMPRESSION_LEVEL = 12;
7
+ const FLAG_ANTIALIASED = 0x1;
8
+ const COLOR_SCALE = SH_C0 / 0.15;
9
+ const rotation = new Array(4);
10
+ const SH_SCALE1 = 1 << 3;
11
+ const SH_SCALE2 = 1 << 4;
12
+ export class SpzFile {
13
+ constructor(compressLevel, spzVersion = SPZ_VERSION) {
14
+ if (spzVersion !== 3 && spzVersion !== 4) {
15
+ throw new Error(`Unsupported SPZ version: ${spzVersion}`);
16
+ }
17
+ this.compressLevel = compressLevel;
18
+ this.spzVersion = spzVersion;
19
+ }
20
+ async read(stream, _contentLength, data) {
21
+ const setCenter = data.setCenter.bind(data);
22
+ const setAlpha = data.setAlpha.bind(data);
23
+ const setColor = data.setColor.bind(data);
24
+ const setScale = data.setScale.bind(data);
25
+ const setQuat = data.setQuat.bind(data);
26
+ const setShN = data.setShN.bind(data);
27
+ const SCALE_LUT = new Float32Array(256);
28
+ for (let i = 0; i < 256; i++) {
29
+ SCALE_LUT[i] = Math.exp(i / 16 - 10);
30
+ }
31
+ const COLOR_LUT = new Float32Array(256);
32
+ for (let i = 0; i < 256; i++) {
33
+ COLOR_LUT[i] = (i / 255 - 0.5) * COLOR_SCALE + 0.5;
34
+ }
35
+ let version = SPZ_VERSION;
36
+ let counts = 0;
37
+ let shDegree = 0;
38
+ let fractionalBits = 12;
39
+ let flags = FLAG_ANTIALIASED;
40
+ let reserved = 0;
41
+ let isF16 = false;
42
+ let useSmallestThreeQuat = true;
43
+ let fraction = 1;
44
+ let fractionInv = 1;
45
+ let shCounts = 0;
46
+ let BlockOffset = 0;
47
+ const shN = [];
48
+ const reader = new BufferReader();
49
+ const decoder = new StreamChunkDecoder(reader);
50
+ decoder.setDecoders([
51
+ {
52
+ init: () => [1, 16],
53
+ decode: async (_offset, _counts, buf) => {
54
+ const header = new DataView(buf.buffer);
55
+ if (header.getUint32(0, true) !== SPZ_MAGIC) {
56
+ throw new Error('Invalid SPZ file');
57
+ }
58
+ ({ version, counts, shDegree, fractionalBits, flags, extra: reserved } = readSpzHeader(header));
59
+ if (version < 1 || version > 3) {
60
+ throw new Error(`Unsupported SPZ version: ${version}`);
61
+ }
62
+ isF16 = version < 2;
63
+ useSmallestThreeQuat = version >= 3;
64
+ fraction = 1 << fractionalBits;
65
+ fractionInv = 1 / fraction;
66
+ shCounts = SH_MAPS[shDegree];
67
+ BlockOffset = await data.initBlock(counts, shDegree);
68
+ if (flags || reserved) {
69
+ //
70
+ }
71
+ },
72
+ },
73
+ {
74
+ init: () => [counts, isF16 ? 6 : 9],
75
+ decode: (offset, counts, buf) => {
76
+ offset += BlockOffset;
77
+ let x, y, z;
78
+ for (let i = 0; i < counts; i++) {
79
+ if (isF16) {
80
+ const o = i * 6;
81
+ x = fromHalf((buf[o + 1] << 8) | buf[o]);
82
+ y = fromHalf((buf[o + 3] << 8) | buf[o + 2]);
83
+ z = fromHalf((buf[o + 5] << 8) | buf[o + 4]);
84
+ }
85
+ else {
86
+ const o = i * 9;
87
+ x = (((buf[o + 2] << 24) | (buf[o + 1] << 16) | (buf[o] << 8)) >> 8) * fractionInv;
88
+ y = (((buf[o + 5] << 24) | (buf[o + 4] << 16) | (buf[o + 3] << 8)) >> 8) * fractionInv;
89
+ z = (((buf[o + 8] << 24) | (buf[o + 7] << 16) | (buf[o + 6] << 8)) >> 8) * fractionInv;
90
+ }
91
+ setCenter(offset + i, x, y, z);
92
+ }
93
+ },
94
+ },
95
+ {
96
+ init: () => [counts, 1],
97
+ decode: (offset, counts, buf) => {
98
+ offset += BlockOffset;
99
+ for (let i = 0; i < counts; i++) {
100
+ setAlpha(offset + i, buf[i] / 255);
101
+ }
102
+ },
103
+ },
104
+ {
105
+ init: () => [counts, 3],
106
+ decode: (offset, counts, buf) => {
107
+ offset += BlockOffset;
108
+ for (let i = 0; i < counts; i++) {
109
+ const o = i * 3;
110
+ setColor(offset + i, COLOR_LUT[buf[o]], COLOR_LUT[buf[o + 1]], COLOR_LUT[buf[o + 2]]);
111
+ }
112
+ },
113
+ },
114
+ {
115
+ init: () => [counts, 3],
116
+ decode: (offset, counts, buf) => {
117
+ offset += BlockOffset;
118
+ for (let i = 0; i < counts; i++) {
119
+ const o = i * 3;
120
+ setScale(offset + i, SCALE_LUT[buf[o]], SCALE_LUT[buf[o + 1]], SCALE_LUT[buf[o + 2]]);
121
+ }
122
+ },
123
+ },
124
+ {
125
+ init: () => [counts, useSmallestThreeQuat ? 4 : 3],
126
+ decode: (offset, counts, buf) => {
127
+ offset += BlockOffset;
128
+ let qx, qy, qz, qw;
129
+ for (let i = 0; i < counts; i++) {
130
+ if (!useSmallestThreeQuat) {
131
+ const o = i * 3;
132
+ qx = buf[o] / 127.5 - 1;
133
+ qy = buf[o + 1] / 127.5 - 1;
134
+ qz = buf[o + 2] / 127.5 - 1;
135
+ qw = Math.sqrt(Math.max(0, 1 - qx * qx - qy * qy - qz * qz));
136
+ }
137
+ else {
138
+ const o = i * 4;
139
+ const packed = buf[o] | (buf[o + 1] << 8) | (buf[o + 2] << 16) | (buf[o + 3] << 24);
140
+ const largest = packed >>> 30;
141
+ let temp = packed;
142
+ let sum = 0;
143
+ for (let j = 3; j >= 0; j--) {
144
+ if (j === largest) {
145
+ continue;
146
+ }
147
+ const mag = temp & 0x1ff;
148
+ const sign = (temp >>> 9) & 1;
149
+ temp >>>= 10;
150
+ const v = Math.SQRT1_2 * (mag / 0x1ff) * (sign ? -1 : 1);
151
+ rotation[j] = v;
152
+ sum += v * v;
153
+ }
154
+ rotation[largest] = Math.sqrt(1 - sum);
155
+ qx = rotation[0];
156
+ qy = rotation[1];
157
+ qz = rotation[2];
158
+ qw = rotation[3];
159
+ }
160
+ setQuat(offset + i, qx, qy, qz, qw);
161
+ }
162
+ },
163
+ },
164
+ {
165
+ init: () => [counts, shCounts],
166
+ decode: (offset, counts, buf) => {
167
+ offset += BlockOffset;
168
+ for (let i = 0; i < counts; i++) {
169
+ const o = i * shCounts;
170
+ for (let j = 0; j < shCounts; j++) {
171
+ shN[j] = (buf[o + j] - 128) / 128;
172
+ }
173
+ setShN(offset + i, shN);
174
+ }
175
+ },
176
+ },
177
+ ]);
178
+ const peeked = await peekStream(stream, 8);
179
+ stream = peeked.stream;
180
+ if (isSpzV4(peeked.prefix)) {
181
+ await readSpzV4Stream(stream, reader, decoder);
182
+ data.finishBlock();
183
+ return;
184
+ }
185
+ let source;
186
+ if (this.compressLevel === -1) {
187
+ source = stream.getReader();
188
+ }
189
+ else {
190
+ source = stream
191
+ .pipeThrough(new DecompressionStream('gzip'))
192
+ .getReader();
193
+ }
194
+ while (true) {
195
+ const { done, value } = await source.read();
196
+ if (done) {
197
+ break;
198
+ }
199
+ reader.write(value);
200
+ decoder.flush();
201
+ }
202
+ data.finishBlock();
203
+ }
204
+ async write(writeStream, data, indices = mortonSort(data)) {
205
+ if (this.spzVersion === 4) {
206
+ await this.writeV4(writeStream, data, indices);
207
+ }
208
+ else {
209
+ await this.writeV3(writeStream, data, indices);
210
+ }
211
+ }
212
+ async writeV3(writeStream, data, indices) {
213
+ let writer;
214
+ let pipePromise;
215
+ if (this.compressLevel === -1) {
216
+ writer = writeStream.getWriter();
217
+ pipePromise = Promise.resolve();
218
+ }
219
+ else {
220
+ const compressStream = new CompressionStream('gzip');
221
+ pipePromise = compressStream.readable.pipeTo(writeStream);
222
+ writer = compressStream.writable.getWriter();
223
+ }
224
+ const version = SPZ_VERSION;
225
+ const counts = data.counts;
226
+ const shDegree = data.shDegree;
227
+ const fractionalBits = 12;
228
+ const flags = FLAG_ANTIALIASED;
229
+ const shCounts = getShCounts(shDegree);
230
+ const context = createSpzEncodeContext(data, indices, fractionalBits, shCounts);
231
+ // header
232
+ writer.write(createSpzHeader(version, counts, shDegree, fractionalBits, flags, 0));
233
+ for (const attribute of getSpzAttributes(shDegree)) {
234
+ await writeSpzAttribute(writer, context, attribute);
235
+ }
236
+ await writer.close();
237
+ await pipePromise;
238
+ }
239
+ async writeV4(writeStream, data, indices) {
240
+ const version = 4;
241
+ const counts = data.counts;
242
+ const shDegree = data.shDegree;
243
+ const fractionalBits = 12;
244
+ const flags = FLAG_ANTIALIASED;
245
+ const shCounts = getShCounts(shDegree);
246
+ const context = createSpzEncodeContext(data, indices, fractionalBits, shCounts);
247
+ const compressed = [];
248
+ const uncompressedSizes = [];
249
+ for (const attribute of getSpzAttributes(shDegree)) {
250
+ const chunk = createSpzAttributeChunk(context, attribute, 0, counts);
251
+ uncompressedSizes.push(chunk.byteLength);
252
+ compressed.push(zstdCompressSync(chunk, {
253
+ params: {
254
+ [zlibConstants.ZSTD_c_compressionLevel]: ZSTD_COMPRESSION_LEVEL,
255
+ },
256
+ }));
257
+ }
258
+ const tocByteOffset = 32;
259
+ const tocSize = compressed.length * 16;
260
+ const header = createSpzHeader(version, counts, shDegree, fractionalBits, flags, compressed.length, 32);
261
+ new DataView(header.buffer).setUint32(16, tocByteOffset, true);
262
+ const toc = new Uint8Array(tocSize);
263
+ const tocView = new DataView(toc.buffer);
264
+ for (let i = 0; i < compressed.length; i++) {
265
+ const entryOffset = i * 16;
266
+ writeUint64(tocView, entryOffset, compressed[i].byteLength);
267
+ writeUint64(tocView, entryOffset + 8, uncompressedSizes[i]);
268
+ }
269
+ const writer = writeStream.getWriter();
270
+ await writer.write(header);
271
+ await writer.write(toc);
272
+ for (const chunk of compressed) {
273
+ await writer.write(chunk);
274
+ }
275
+ await writer.close();
276
+ }
277
+ }
278
+ function getShCounts(shDegree) {
279
+ const shCounts = SH_MAPS[shDegree];
280
+ if (shCounts === undefined) {
281
+ throw new Error(`Unsupported SPZ SH degree: ${shDegree}`);
282
+ }
283
+ return shCounts;
284
+ }
285
+ function createSpzEncodeContext(data, indices, fractionalBits, shCounts) {
286
+ return {
287
+ data,
288
+ indices,
289
+ fractionalBits,
290
+ fraction: 1 << fractionalBits,
291
+ shCounts,
292
+ single: {
293
+ x: 0,
294
+ y: 0,
295
+ z: 0,
296
+ sx: 0,
297
+ sy: 0,
298
+ sz: 0,
299
+ qx: 0,
300
+ qy: 0,
301
+ qz: 0,
302
+ qw: 0,
303
+ r: 0,
304
+ g: 0,
305
+ b: 0,
306
+ a: 0,
307
+ shN: new Array(shCounts),
308
+ },
309
+ };
310
+ }
311
+ function getSpzAttributes(shDegree) {
312
+ return shDegree > 0
313
+ ? ['position', 'alpha', 'color', 'scale', 'quat', 'sh']
314
+ : ['position', 'alpha', 'color', 'scale', 'quat'];
315
+ }
316
+ function getSpzAttributeInfo(attribute, shCounts) {
317
+ switch (attribute) {
318
+ case 'position':
319
+ return { itemSize: 9, chunkSize: 4096 };
320
+ case 'alpha':
321
+ return { itemSize: 1, chunkSize: 65536 };
322
+ case 'color':
323
+ case 'scale':
324
+ return { itemSize: 3, chunkSize: 16384 };
325
+ case 'quat':
326
+ return { itemSize: 4, chunkSize: 16384 };
327
+ case 'sh':
328
+ return { itemSize: shCounts, chunkSize: 1024 };
329
+ }
330
+ }
331
+ function createSpzAttributeChunk(context, attribute, offset, counts) {
332
+ const { data, indices, single, shCounts } = context;
333
+ const { itemSize } = getSpzAttributeInfo(attribute, shCounts);
334
+ const chunk = new Uint8Array(counts * itemSize);
335
+ for (let i = 0; i < counts; i++) {
336
+ const index = indices[offset + i];
337
+ switch (attribute) {
338
+ case 'position': {
339
+ data.getCenter(index, single);
340
+ const o = i * itemSize;
341
+ const ix = clamp(single.x * context.fraction, -0x7fffff, 0x7fffff);
342
+ chunk[o + 0] = ix & 0xff;
343
+ chunk[o + 1] = (ix >> 8) & 0xff;
344
+ chunk[o + 2] = (ix >> 16) & 0xff;
345
+ const iy = clamp(single.y * context.fraction, -0x7fffff, 0x7fffff);
346
+ chunk[o + 3] = iy & 0xff;
347
+ chunk[o + 4] = (iy >> 8) & 0xff;
348
+ chunk[o + 5] = (iy >> 16) & 0xff;
349
+ const iz = clamp(single.z * context.fraction, -0x7fffff, 0x7fffff);
350
+ chunk[o + 6] = iz & 0xff;
351
+ chunk[o + 7] = (iz >> 8) & 0xff;
352
+ chunk[o + 8] = (iz >> 16) & 0xff;
353
+ break;
354
+ }
355
+ case 'alpha':
356
+ data.getAlpha(index, single);
357
+ chunk[i] = clamp(Math.round(single.a * 255), 0, 255);
358
+ break;
359
+ case 'color': {
360
+ data.getColor(index, single);
361
+ const o = i * itemSize;
362
+ chunk[o + 0] = clamp(Math.round(((single.r - 0.5) / COLOR_SCALE + 0.5) * 255), 0, 255);
363
+ chunk[o + 1] = clamp(Math.round(((single.g - 0.5) / COLOR_SCALE + 0.5) * 255), 0, 255);
364
+ chunk[o + 2] = clamp(Math.round(((single.b - 0.5) / COLOR_SCALE + 0.5) * 255), 0, 255);
365
+ break;
366
+ }
367
+ case 'scale': {
368
+ data.getScale(index, single);
369
+ const o = i * itemSize;
370
+ chunk[o + 0] = clamp(Math.round((Math.log(single.sx) + 10) * 16), 0, 255);
371
+ chunk[o + 1] = clamp(Math.round((Math.log(single.sy) + 10) * 16), 0, 255);
372
+ chunk[o + 2] = clamp(Math.round((Math.log(single.sz) + 10) * 16), 0, 255);
373
+ break;
374
+ }
375
+ case 'quat': {
376
+ data.getQuat(index, single);
377
+ const o = i * itemSize;
378
+ rotation[0] = single.qx;
379
+ rotation[1] = single.qy;
380
+ rotation[2] = single.qz;
381
+ rotation[3] = single.qw;
382
+ let iLargest = 0;
383
+ for (let j = 1; j < 4; ++j) {
384
+ if (Math.abs(rotation[j]) > Math.abs(rotation[iLargest])) {
385
+ iLargest = j;
386
+ }
387
+ }
388
+ const negate = rotation[iLargest] < 0 ? 1 : 0;
389
+ let comp = iLargest;
390
+ for (let j = 0; j < 4; ++j) {
391
+ if (j !== iLargest) {
392
+ const negbit = (rotation[j] < 0 ? 1 : 0) ^ negate;
393
+ const mag = Math.floor(((1 << 9) - 1) * (Math.abs(rotation[j]) / Math.SQRT1_2) + 0.5);
394
+ comp = (comp << 10) | (negbit << 9) | mag;
395
+ }
396
+ }
397
+ chunk[o + 0] = comp & 0xff;
398
+ chunk[o + 1] = (comp >> 8) & 0xff;
399
+ chunk[o + 2] = (comp >> 16) & 0xff;
400
+ chunk[o + 3] = (comp >> 24) & 0xff;
401
+ break;
402
+ }
403
+ case 'sh': {
404
+ data.getShN(index, single.shN);
405
+ const o = i * itemSize;
406
+ for (let j = 0; j < itemSize; j++) {
407
+ if (j < 9) {
408
+ chunk[o + j] = clamp(Math.floor((Math.round(single.shN[j] * 128) + 128 + SH_SCALE1 / 2) / SH_SCALE1) * SH_SCALE1, 0, 255);
409
+ continue;
410
+ }
411
+ chunk[o + j] = clamp(Math.floor((Math.round(single.shN[j] * 128) + 128 + SH_SCALE2 / 2) / SH_SCALE2) * SH_SCALE2, 0, 255);
412
+ }
413
+ break;
414
+ }
415
+ }
416
+ }
417
+ return chunk;
418
+ }
419
+ async function writeSpzAttribute(writer, context, attribute) {
420
+ const { chunkSize } = getSpzAttributeInfo(attribute, context.shCounts);
421
+ const chunkCounts = Math.ceil(context.data.counts / chunkSize);
422
+ for (let i = 0; i < chunkCounts; i++) {
423
+ if (writer.desiredSize <= 0) {
424
+ await writer.ready;
425
+ }
426
+ const offset = i * chunkSize;
427
+ const counts = Math.min(chunkSize, context.data.counts - offset);
428
+ writer.write(createSpzAttributeChunk(context, attribute, offset, counts));
429
+ }
430
+ }
431
+ function readUint64(view, offset) {
432
+ const low = view.getUint32(offset, true);
433
+ const high = view.getUint32(offset + 4, true);
434
+ const value = high * 0x100000000 + low;
435
+ if (!Number.isSafeInteger(value)) {
436
+ throw new Error(`SPZ stream size is too large: ${value}`);
437
+ }
438
+ return value;
439
+ }
440
+ function writeUint64(view, offset, value) {
441
+ if (!Number.isSafeInteger(value) || value < 0) {
442
+ throw new Error(`Invalid SPZ stream size: ${value}`);
443
+ }
444
+ view.setUint32(offset, value >>> 0, true);
445
+ view.setUint32(offset + 4, Math.floor(value / 0x100000000), true);
446
+ }
447
+ function createSpzHeader(version, counts, shDegree, fractionalBits, flags, extra, byteLength = 16) {
448
+ const header = new DataView(new ArrayBuffer(byteLength));
449
+ header.setUint32(0, SPZ_MAGIC, true);
450
+ header.setUint32(4, version, true);
451
+ header.setUint32(8, counts, true);
452
+ header.setUint8(12, shDegree);
453
+ header.setUint8(13, fractionalBits);
454
+ header.setUint8(14, flags);
455
+ header.setUint8(15, extra);
456
+ return new Uint8Array(header.buffer);
457
+ }
458
+ function readSpzHeader(view) {
459
+ return {
460
+ version: view.getUint32(4, true),
461
+ counts: view.getUint32(8, true),
462
+ shDegree: view.getUint8(12),
463
+ fractionalBits: view.getUint8(13),
464
+ flags: view.getUint8(14),
465
+ extra: view.getUint8(15),
466
+ };
467
+ }
468
+ function getSpzV4AttributeSizes(counts, shDegree) {
469
+ const shCounts = getShCounts(shDegree);
470
+ const sizes = [
471
+ counts * 9, // position
472
+ counts, // alpha
473
+ counts * 3, // color
474
+ counts * 3, // scale
475
+ counts * 4, // quat
476
+ ];
477
+ if (shDegree > 0) {
478
+ sizes.push(counts * shCounts); // sh
479
+ }
480
+ return sizes;
481
+ }
482
+ function isSpzV4(buffer) {
483
+ if (buffer.byteLength < 8) {
484
+ return false;
485
+ }
486
+ const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
487
+ return view.getUint32(0, true) === SPZ_MAGIC && view.getUint32(4, true) === 4;
488
+ }
489
+ async function readSpzV4Stream(stream, reader, decoder) {
490
+ const read = createExactReader(stream);
491
+ const header = await read(32);
492
+ const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
493
+ const { counts, shDegree, fractionalBits, flags, extra: numStreams } = readSpzHeader(view);
494
+ const tocByteOffset = view.getUint32(16, true);
495
+ const expectedSizes = getSpzV4AttributeSizes(counts, shDegree);
496
+ if (numStreams !== expectedSizes.length) {
497
+ throw new Error(`Invalid SPZ v4 stream count: ${numStreams}`);
498
+ }
499
+ if (tocByteOffset < 32) {
500
+ throw new Error(`Invalid SPZ v4 TOC offset: ${tocByteOffset}`);
501
+ }
502
+ if (tocByteOffset > 32) {
503
+ await read(tocByteOffset - 32);
504
+ }
505
+ const toc = await read(numStreams * 16);
506
+ const tocView = new DataView(toc.buffer, toc.byteOffset, toc.byteLength);
507
+ // Reuse the legacy v3 attribute decoder after parsing the v4 container.
508
+ reader.write(createSpzHeader(SPZ_VERSION, counts, shDegree, fractionalBits, flags & FLAG_ANTIALIASED, 0));
509
+ decoder.flush();
510
+ for (let i = 0; i < numStreams; i++) {
511
+ const entryOffset = i * 16;
512
+ const compressedSize = readUint64(tocView, entryOffset);
513
+ const uncompressedSize = readUint64(tocView, entryOffset + 8);
514
+ if (uncompressedSize !== expectedSizes[i]) {
515
+ throw new Error(`Invalid SPZ v4 stream size at index ${i}`);
516
+ }
517
+ const compressed = await read(compressedSize);
518
+ const decompressed = zstdDecompressSync(compressed, {
519
+ maxOutputLength: uncompressedSize,
520
+ });
521
+ if (decompressed.byteLength !== uncompressedSize) {
522
+ throw new Error(`Invalid SPZ v4 decompressed size at index ${i}`);
523
+ }
524
+ reader.write(new Uint8Array(decompressed.buffer, decompressed.byteOffset, decompressed.byteLength));
525
+ decoder.flush();
526
+ }
527
+ }
528
+ // Return a reader that resolves exactly byteLength bytes and keeps leftover bytes for the next read.
529
+ function createExactReader(stream) {
530
+ const reader = stream.getReader();
531
+ let chunk;
532
+ let chunkOffset = 0;
533
+ return async (byteLength) => {
534
+ const result = new Uint8Array(byteLength);
535
+ let offset = 0;
536
+ while (offset < byteLength) {
537
+ if (!chunk || chunkOffset >= chunk.byteLength) {
538
+ const { done, value } = await reader.read();
539
+ if (done || !value) {
540
+ throw new Error('Invalid SPZ v4 file: stream ended unexpectedly');
541
+ }
542
+ chunk = value;
543
+ chunkOffset = 0;
544
+ }
545
+ const copyLength = Math.min(byteLength - offset, chunk.byteLength - chunkOffset);
546
+ result.set(chunk.subarray(chunkOffset, chunkOffset + copyLength), offset);
547
+ chunkOffset += copyLength;
548
+ offset += copyLength;
549
+ }
550
+ return result;
551
+ };
552
+ }
553
+ // Peek leading bytes for format detection, then replay the consumed chunks through a replacement stream.
554
+ async function peekStream(stream, byteLength) {
555
+ const reader = stream.getReader();
556
+ const chunks = [];
557
+ let size = 0;
558
+ while (size < byteLength) {
559
+ const { done, value } = await reader.read();
560
+ if (done || !value) {
561
+ break;
562
+ }
563
+ chunks.push(value);
564
+ size += value.byteLength;
565
+ }
566
+ const prefix = new Uint8Array(Math.min(size, byteLength));
567
+ let offset = 0;
568
+ for (const chunk of chunks) {
569
+ const copyLength = Math.min(chunk.byteLength, prefix.byteLength - offset);
570
+ prefix.set(chunk.subarray(0, copyLength), offset);
571
+ offset += copyLength;
572
+ if (offset === prefix.byteLength) {
573
+ break;
574
+ }
575
+ }
576
+ return {
577
+ prefix,
578
+ stream: new ReadableStream({
579
+ start(controller) {
580
+ for (const chunk of chunks) {
581
+ controller.enqueue(chunk);
582
+ }
583
+ },
584
+ async pull(controller) {
585
+ const { done, value } = await reader.read();
586
+ if (done) {
587
+ controller.close();
588
+ return;
589
+ }
590
+ controller.enqueue(value);
591
+ },
592
+ cancel(reason) {
593
+ return reader.cancel(reason);
594
+ },
595
+ }),
596
+ };
597
+ }