@manycore/aholo-splat-transform 1.2.7 → 1.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/COPYRIGHT.md +17 -0
- package/README.md +5 -5
- package/THIRD_PARTY_LICENSES.txt +1373 -0
- package/bin/cli.js +3 -3
- package/dist/SplatData.js +6 -12
- package/dist/file/esz.d.ts +11 -0
- package/dist/file/esz.js +322 -0
- package/dist/file/index.d.ts +1 -0
- package/dist/file/index.js +1 -0
- package/dist/file/ksplat.js +4 -5
- package/dist/file/lcc.js +5 -4
- package/dist/file/ply.js +8 -6
- package/dist/file/sog.js +8 -18
- package/dist/file/spz.d.ts +4 -1
- package/dist/file/spz.js +358 -175
- package/dist/native/cpp/bin/linux/binding.node +0 -0
- package/dist/native/cpp/bin/windows/binding.node +0 -0
- package/dist/native/index.js +4 -3
- package/dist/tasks/WriteTask.d.ts +1 -0
- package/dist/tasks/WriteTask.js +6 -6
- package/dist/utils/BufferReader.js +2 -4
- package/dist/utils/Logger.js +4 -2
- package/dist/utils/StreamChunkDecoder.js +1 -6
- package/dist/utils/k-means.js +0 -9
- package/dist/utils/math.js +7 -12
- package/dist/utils/splat.d.ts +3 -2
- package/dist/utils/splat.js +12 -3
- package/dist/utils/voxel/common.js +10 -28
- package/dist/utils/voxel/gpu-dilation.js +3 -12
- package/package.json +13 -4
package/dist/file/spz.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
+
import { constants as zlibConstants, zstdCompressSync, zstdDecompressSync } from 'node:zlib';
|
|
1
2
|
import { SH_C0, SH_MAPS } from '../constant.js';
|
|
2
3
|
import { BufferReader, fromHalf, clamp, StreamChunkDecoder, mortonSort } from '../utils/index.js';
|
|
3
4
|
const SPZ_MAGIC = 0x5053474e; // NGSP = Niantic gaussian splat
|
|
4
5
|
const SPZ_VERSION = 3;
|
|
6
|
+
const ZSTD_COMPRESSION_LEVEL = 12;
|
|
5
7
|
const FLAG_ANTIALIASED = 0x1;
|
|
6
8
|
const COLOR_SCALE = SH_C0 / 0.15;
|
|
7
9
|
const rotation = new Array(4);
|
|
8
10
|
const SH_SCALE1 = 1 << 3;
|
|
9
11
|
const SH_SCALE2 = 1 << 4;
|
|
10
12
|
export class SpzFile {
|
|
11
|
-
compressLevel
|
|
12
|
-
|
|
13
|
+
constructor(compressLevel, spzVersion = SPZ_VERSION) {
|
|
14
|
+
if (spzVersion !== 3 && spzVersion !== 4) {
|
|
15
|
+
throw new Error(`Unsupported SPZ version: ${spzVersion}`);
|
|
16
|
+
}
|
|
13
17
|
this.compressLevel = compressLevel;
|
|
18
|
+
this.spzVersion = spzVersion;
|
|
14
19
|
}
|
|
15
20
|
async read(stream, _contentLength, data) {
|
|
16
21
|
const setCenter = data.setCenter.bind(data);
|
|
@@ -50,15 +55,10 @@ export class SpzFile {
|
|
|
50
55
|
if (header.getUint32(0, true) !== SPZ_MAGIC) {
|
|
51
56
|
throw new Error('Invalid SPZ file');
|
|
52
57
|
}
|
|
53
|
-
version = header
|
|
58
|
+
({ version, counts, shDegree, fractionalBits, flags, extra: reserved } = readSpzHeader(header));
|
|
54
59
|
if (version < 1 || version > 3) {
|
|
55
60
|
throw new Error(`Unsupported SPZ version: ${version}`);
|
|
56
61
|
}
|
|
57
|
-
counts = header.getUint32(8, true);
|
|
58
|
-
shDegree = header.getUint8(12);
|
|
59
|
-
fractionalBits = header.getUint8(13);
|
|
60
|
-
flags = header.getUint8(14);
|
|
61
|
-
reserved = header.getUint8(15);
|
|
62
62
|
isF16 = version < 2;
|
|
63
63
|
useSmallestThreeQuat = version >= 3;
|
|
64
64
|
fraction = 1 << fractionalBits;
|
|
@@ -175,6 +175,13 @@ export class SpzFile {
|
|
|
175
175
|
},
|
|
176
176
|
},
|
|
177
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
|
+
}
|
|
178
185
|
let source;
|
|
179
186
|
if (this.compressLevel === -1) {
|
|
180
187
|
source = stream.getReader();
|
|
@@ -193,6 +200,14 @@ export class SpzFile {
|
|
|
193
200
|
data.finishBlock();
|
|
194
201
|
}
|
|
195
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) {
|
|
196
211
|
let writer;
|
|
197
212
|
let pipePromise;
|
|
198
213
|
if (this.compressLevel === -1) {
|
|
@@ -209,192 +224,360 @@ export class SpzFile {
|
|
|
209
224
|
const shDegree = data.shDegree;
|
|
210
225
|
const fractionalBits = 12;
|
|
211
226
|
const flags = FLAG_ANTIALIASED;
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
const shCounts = SH_MAPS[shDegree];
|
|
227
|
+
const shCounts = getShCounts(shDegree);
|
|
228
|
+
const context = createSpzEncodeContext(data, indices, fractionalBits, shCounts);
|
|
215
229
|
// header
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
header.setUint32(0, SPZ_MAGIC, true);
|
|
220
|
-
header.setUint32(4, version, true);
|
|
221
|
-
header.setUint32(8, counts, true);
|
|
222
|
-
header.setUint8(12, shDegree);
|
|
223
|
-
header.setUint8(13, fractionalBits);
|
|
224
|
-
header.setUint8(14, flags);
|
|
225
|
-
header.setUint8(15, reserved);
|
|
226
|
-
writer.write(buffer);
|
|
230
|
+
writer.write(createSpzHeader(version, counts, shDegree, fractionalBits, flags, 0));
|
|
231
|
+
for (const attribute of getSpzAttributes(shDegree)) {
|
|
232
|
+
await writeSpzAttribute(writer, context, attribute);
|
|
227
233
|
}
|
|
228
|
-
|
|
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: {
|
|
229
291
|
x: 0, y: 0, z: 0,
|
|
230
292
|
sx: 0, sy: 0, sz: 0,
|
|
231
293
|
qx: 0, qy: 0, qz: 0, qw: 0,
|
|
232
294
|
r: 0, g: 0, b: 0, a: 0,
|
|
233
295
|
shN: new Array(shCounts),
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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;
|
|
264
340
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const offset = i * chunkSize;
|
|
277
|
-
for (let j = 0; j < currentChunkSize; j++) {
|
|
278
|
-
data.getAlpha(indices[offset + j], single);
|
|
279
|
-
chunk[j] = clamp(Math.round(single.a * 255), 0, 255);
|
|
280
|
-
}
|
|
281
|
-
writer.write(chunk);
|
|
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;
|
|
282
352
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (writer.desiredSize <= 0) {
|
|
291
|
-
await writer.ready;
|
|
292
|
-
}
|
|
293
|
-
const currentChunkSize = Math.min(chunkSize, data.counts - i * chunkSize);
|
|
294
|
-
const chunk = new Uint8Array(currentChunkSize * ItemSize);
|
|
295
|
-
const offset = i * chunkSize;
|
|
296
|
-
for (let j = 0; j < currentChunkSize; j++) {
|
|
297
|
-
data.getColor(indices[offset + j], single);
|
|
298
|
-
const o = j * ItemSize;
|
|
299
|
-
chunk[o + 0] = clamp(Math.round(((single.r - 0.5) / COLOR_SCALE + 0.5) * 255), 0, 255);
|
|
300
|
-
chunk[o + 1] = clamp(Math.round(((single.g - 0.5) / COLOR_SCALE + 0.5) * 255), 0, 255);
|
|
301
|
-
chunk[o + 2] = clamp(Math.round(((single.b - 0.5) / COLOR_SCALE + 0.5) * 255), 0, 255);
|
|
302
|
-
}
|
|
303
|
-
writer.write(chunk);
|
|
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;
|
|
304
360
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
+
}
|
|
314
373
|
}
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
chunk[o + 2] = clamp(Math.round((Math.log(single.sz) + 10) * 16), 0, 255);
|
|
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
|
+
}
|
|
324
382
|
}
|
|
325
|
-
|
|
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;
|
|
326
388
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (writer.desiredSize <= 0) {
|
|
335
|
-
await writer.ready;
|
|
336
|
-
}
|
|
337
|
-
const currentChunkSize = Math.min(chunkSize, data.counts - i * chunkSize);
|
|
338
|
-
const chunk = new Uint8Array(currentChunkSize * ItemSize);
|
|
339
|
-
const offset = i * chunkSize;
|
|
340
|
-
for (let j = 0; j < currentChunkSize; j++) {
|
|
341
|
-
data.getQuat(indices[offset + j], single);
|
|
342
|
-
const o = j * ItemSize;
|
|
343
|
-
rotation[0] = single.qx;
|
|
344
|
-
rotation[1] = single.qy;
|
|
345
|
-
rotation[2] = single.qz;
|
|
346
|
-
rotation[3] = single.qw;
|
|
347
|
-
let iLargest = 0;
|
|
348
|
-
for (let i = 1; i < 4; ++i) {
|
|
349
|
-
if (Math.abs(rotation[i]) > Math.abs(rotation[iLargest])) {
|
|
350
|
-
iLargest = i;
|
|
351
|
-
}
|
|
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;
|
|
352
396
|
}
|
|
353
|
-
|
|
354
|
-
let comp = iLargest;
|
|
355
|
-
for (let i = 0; i < 4; ++i) {
|
|
356
|
-
if (i !== iLargest) {
|
|
357
|
-
const negbit = (rotation[i] < 0 ? 1 : 0) ^ negate;
|
|
358
|
-
const mag = Math.floor(((1 << 9) - 1) * (Math.abs(rotation[i]) / Math.SQRT1_2) + 0.5);
|
|
359
|
-
comp = (comp << 10) | (negbit << 9) | mag;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
chunk[o + 0] = comp & 0xff;
|
|
363
|
-
chunk[o + 1] = (comp >> 8) & 0xff;
|
|
364
|
-
chunk[o + 2] = (comp >> 16) & 0xff;
|
|
365
|
-
chunk[o + 3] = (comp >> 24) & 0xff;
|
|
397
|
+
chunk[o + j] = clamp(Math.floor((Math.round(single.shN[j] * 128) + 128 + SH_SCALE2 / 2) / SH_SCALE2) * SH_SCALE2, 0, 255);
|
|
366
398
|
}
|
|
367
|
-
|
|
399
|
+
break;
|
|
368
400
|
}
|
|
369
401
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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');
|
|
393
527
|
}
|
|
394
|
-
|
|
528
|
+
chunk = value;
|
|
529
|
+
chunkOffset = 0;
|
|
395
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;
|
|
396
560
|
}
|
|
397
|
-
await writer.close();
|
|
398
|
-
await pipePromise;
|
|
399
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
|
+
};
|
|
400
583
|
}
|
|
Binary file
|
|
Binary file
|
package/dist/native/index.js
CHANGED
|
@@ -86,13 +86,14 @@ export function generateLod(splat, levelParameters, blockPrecision, minSize, max
|
|
|
86
86
|
return { splats, blocks };
|
|
87
87
|
}
|
|
88
88
|
export class WebPLosslessProfile {
|
|
89
|
-
|
|
89
|
+
constructor() {
|
|
90
|
+
this.lossless = true;
|
|
91
|
+
}
|
|
90
92
|
}
|
|
91
93
|
export class WebPQualityProfile {
|
|
92
|
-
quality;
|
|
93
|
-
lossless = false;
|
|
94
94
|
constructor(quality) {
|
|
95
95
|
this.quality = quality;
|
|
96
|
+
this.lossless = false;
|
|
96
97
|
}
|
|
97
98
|
;
|
|
98
99
|
}
|