@playcanvas/splat-transform 0.4.0 → 0.4.2

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/dist/index.mjs CHANGED
@@ -1881,7 +1881,7 @@ class Quat {
1881
1881
  }
1882
1882
  }
1883
1883
 
1884
- var version = "0.3.0";
1884
+ var version = "0.4.2";
1885
1885
 
1886
1886
  class Column {
1887
1887
  name;
@@ -2307,6 +2307,309 @@ const process = (dataTable, processActions) => {
2307
2307
  return result;
2308
2308
  };
2309
2309
 
2310
+ // Half-precision floating point decoder
2311
+ function decodeFloat16(encoded) {
2312
+ const signBit = (encoded >> 15) & 1;
2313
+ const exponent = (encoded >> 10) & 0x1f;
2314
+ const mantissa = encoded & 0x3ff;
2315
+ if (exponent === 0) {
2316
+ if (mantissa === 0) {
2317
+ return signBit ? -0 : 0.0;
2318
+ }
2319
+ // Denormalized number
2320
+ let m = mantissa;
2321
+ let exp = -14;
2322
+ while (!(m & 0x400)) {
2323
+ m <<= 1;
2324
+ exp--;
2325
+ }
2326
+ m &= 0x3ff;
2327
+ const finalExp = exp + 127;
2328
+ const finalMantissa = m << 13;
2329
+ const bits = (signBit << 31) | (finalExp << 23) | finalMantissa;
2330
+ return new Float32Array(new Uint32Array([bits]).buffer)[0];
2331
+ }
2332
+ if (exponent === 0x1f) {
2333
+ return mantissa === 0 ? (signBit ? -Infinity : Infinity) : NaN;
2334
+ }
2335
+ const finalExp = exponent - 15 + 127;
2336
+ const finalMantissa = mantissa << 13;
2337
+ const bits = (signBit << 31) | (finalExp << 23) | finalMantissa;
2338
+ return new Float32Array(new Uint32Array([bits]).buffer)[0];
2339
+ }
2340
+ const COMPRESSION_MODES = [
2341
+ {
2342
+ centerBytes: 12,
2343
+ scaleBytes: 12,
2344
+ rotationBytes: 16,
2345
+ colorBytes: 4,
2346
+ harmonicsBytes: 4,
2347
+ scaleStartByte: 12,
2348
+ rotationStartByte: 24,
2349
+ colorStartByte: 40,
2350
+ harmonicsStartByte: 44,
2351
+ scaleQuantRange: 1
2352
+ },
2353
+ {
2354
+ centerBytes: 6,
2355
+ scaleBytes: 6,
2356
+ rotationBytes: 8,
2357
+ colorBytes: 4,
2358
+ harmonicsBytes: 2,
2359
+ scaleStartByte: 6,
2360
+ rotationStartByte: 12,
2361
+ colorStartByte: 20,
2362
+ harmonicsStartByte: 24,
2363
+ scaleQuantRange: 32767
2364
+ },
2365
+ {
2366
+ centerBytes: 6,
2367
+ scaleBytes: 6,
2368
+ rotationBytes: 8,
2369
+ colorBytes: 4,
2370
+ harmonicsBytes: 1,
2371
+ scaleStartByte: 6,
2372
+ rotationStartByte: 12,
2373
+ colorStartByte: 20,
2374
+ harmonicsStartByte: 24,
2375
+ scaleQuantRange: 32767
2376
+ }
2377
+ ];
2378
+ const HARMONICS_COMPONENT_COUNT = [0, 9, 24, 45];
2379
+ const readKsplat = async (fileHandle) => {
2380
+ const stats = await fileHandle.stat();
2381
+ const totalSize = stats.size;
2382
+ // Load complete file
2383
+ const fileBuffer = Buffer$1.alloc(totalSize);
2384
+ await fileHandle.read(fileBuffer, 0, totalSize, 0);
2385
+ const MAIN_HEADER_SIZE = 4096;
2386
+ const SECTION_HEADER_SIZE = 1024;
2387
+ if (totalSize < MAIN_HEADER_SIZE) {
2388
+ throw new Error('File too small to be valid .ksplat format');
2389
+ }
2390
+ // Parse main header
2391
+ const mainHeader = new DataView(fileBuffer.buffer, fileBuffer.byteOffset, MAIN_HEADER_SIZE);
2392
+ const majorVersion = mainHeader.getUint8(0);
2393
+ const minorVersion = mainHeader.getUint8(1);
2394
+ if (majorVersion !== 0 || minorVersion < 1) {
2395
+ throw new Error(`Unsupported version ${majorVersion}.${minorVersion}`);
2396
+ }
2397
+ const maxSections = mainHeader.getUint32(4, true);
2398
+ const numSplats = mainHeader.getUint32(16, true);
2399
+ const compressionMode = mainHeader.getUint16(20, true);
2400
+ if (compressionMode > 2) {
2401
+ throw new Error(`Invalid compression mode: ${compressionMode}`);
2402
+ }
2403
+ const minHarmonicsValue = mainHeader.getFloat32(36, true) || -1.5;
2404
+ const maxHarmonicsValue = mainHeader.getFloat32(40, true) || 1.5;
2405
+ if (numSplats === 0) {
2406
+ throw new Error('Invalid .ksplat file: file is empty');
2407
+ }
2408
+ // First pass: scan all sections to find maximum harmonics degree
2409
+ let maxHarmonicsDegree = 0;
2410
+ for (let sectionIdx = 0; sectionIdx < maxSections; sectionIdx++) {
2411
+ const sectionHeaderOffset = MAIN_HEADER_SIZE + sectionIdx * SECTION_HEADER_SIZE;
2412
+ const sectionHeader = new DataView(fileBuffer.buffer, fileBuffer.byteOffset + sectionHeaderOffset, SECTION_HEADER_SIZE);
2413
+ const sectionSplatCount = sectionHeader.getUint32(0, true);
2414
+ if (sectionSplatCount === 0)
2415
+ continue; // Skip empty sections
2416
+ const harmonicsDegree = sectionHeader.getUint16(40, true);
2417
+ maxHarmonicsDegree = Math.max(maxHarmonicsDegree, harmonicsDegree);
2418
+ }
2419
+ // Initialize data storage with base columns
2420
+ const columns = [
2421
+ new Column('x', new Float32Array(numSplats)),
2422
+ new Column('y', new Float32Array(numSplats)),
2423
+ new Column('z', new Float32Array(numSplats)),
2424
+ new Column('scale_0', new Float32Array(numSplats)),
2425
+ new Column('scale_1', new Float32Array(numSplats)),
2426
+ new Column('scale_2', new Float32Array(numSplats)),
2427
+ new Column('f_dc_0', new Float32Array(numSplats)),
2428
+ new Column('f_dc_1', new Float32Array(numSplats)),
2429
+ new Column('f_dc_2', new Float32Array(numSplats)),
2430
+ new Column('opacity', new Float32Array(numSplats)),
2431
+ new Column('rot_0', new Float32Array(numSplats)),
2432
+ new Column('rot_1', new Float32Array(numSplats)),
2433
+ new Column('rot_2', new Float32Array(numSplats)),
2434
+ new Column('rot_3', new Float32Array(numSplats))
2435
+ ];
2436
+ // Add spherical harmonics columns based on maximum degree found
2437
+ const maxHarmonicsComponentCount = HARMONICS_COMPONENT_COUNT[maxHarmonicsDegree];
2438
+ for (let i = 0; i < maxHarmonicsComponentCount; i++) {
2439
+ columns.push(new Column(`f_rest_${i}`, new Float32Array(numSplats)));
2440
+ }
2441
+ const { centerBytes, scaleBytes, rotationBytes, colorBytes, harmonicsBytes, scaleStartByte, rotationStartByte, colorStartByte, harmonicsStartByte, scaleQuantRange } = COMPRESSION_MODES[compressionMode];
2442
+ let currentSectionDataOffset = MAIN_HEADER_SIZE + maxSections * SECTION_HEADER_SIZE;
2443
+ let splatIndex = 0;
2444
+ // Process each section
2445
+ for (let sectionIdx = 0; sectionIdx < maxSections; sectionIdx++) {
2446
+ const sectionHeaderOffset = MAIN_HEADER_SIZE + sectionIdx * SECTION_HEADER_SIZE;
2447
+ const sectionHeader = new DataView(fileBuffer.buffer, fileBuffer.byteOffset + sectionHeaderOffset, SECTION_HEADER_SIZE);
2448
+ const sectionSplatCount = sectionHeader.getUint32(0, true);
2449
+ const maxSectionSplats = sectionHeader.getUint32(4, true);
2450
+ const bucketCapacity = sectionHeader.getUint32(8, true);
2451
+ const bucketCount = sectionHeader.getUint32(12, true);
2452
+ const spatialBlockSize = sectionHeader.getFloat32(16, true);
2453
+ const bucketStorageSize = sectionHeader.getUint16(20, true);
2454
+ const quantizationRange = sectionHeader.getUint32(24, true) || scaleQuantRange;
2455
+ const fullBuckets = sectionHeader.getUint32(32, true);
2456
+ const partialBuckets = sectionHeader.getUint32(36, true);
2457
+ const harmonicsDegree = sectionHeader.getUint16(40, true);
2458
+ // Calculate layout
2459
+ const fullBucketSplats = fullBuckets * bucketCapacity;
2460
+ const partialBucketMetaSize = partialBuckets * 4;
2461
+ const totalBucketStorageSize = bucketStorageSize * bucketCount + partialBucketMetaSize;
2462
+ const harmonicsComponentCount = HARMONICS_COMPONENT_COUNT[harmonicsDegree];
2463
+ const bytesPerSplat = centerBytes + scaleBytes + rotationBytes +
2464
+ colorBytes + harmonicsComponentCount * harmonicsBytes;
2465
+ const sectionDataSize = bytesPerSplat * maxSectionSplats;
2466
+ // Calculate decompression parameters
2467
+ const positionScale = spatialBlockSize / 2.0 / quantizationRange;
2468
+ // Get bucket centers
2469
+ const bucketCentersOffset = currentSectionDataOffset + partialBucketMetaSize;
2470
+ const bucketCenters = new Float32Array(fileBuffer.buffer, fileBuffer.byteOffset + bucketCentersOffset, bucketCount * 3);
2471
+ // Get partial bucket sizes
2472
+ const partialBucketSizes = new Uint32Array(fileBuffer.buffer, fileBuffer.byteOffset + currentSectionDataOffset, partialBuckets);
2473
+ // Get splat data
2474
+ const splatDataOffset = currentSectionDataOffset + totalBucketStorageSize;
2475
+ const splatData = new DataView(fileBuffer.buffer, fileBuffer.byteOffset + splatDataOffset, sectionDataSize);
2476
+ // Harmonic value decoder
2477
+ const decodeHarmonics = (offset, component) => {
2478
+ switch (compressionMode) {
2479
+ case 0:
2480
+ return splatData.getFloat32(offset + harmonicsStartByte + component * 4, true);
2481
+ case 1:
2482
+ return decodeFloat16(splatData.getUint16(offset + harmonicsStartByte + component * 2, true));
2483
+ case 2: {
2484
+ const normalized = splatData.getUint8(offset + harmonicsStartByte + component) / 255;
2485
+ return minHarmonicsValue + normalized * (maxHarmonicsValue - minHarmonicsValue);
2486
+ }
2487
+ default:
2488
+ return 0;
2489
+ }
2490
+ };
2491
+ // Track partial bucket processing
2492
+ let currentPartialBucket = fullBuckets;
2493
+ let currentPartialBase = fullBucketSplats;
2494
+ // Process splats in this section
2495
+ for (let splatIdx = 0; splatIdx < sectionSplatCount; splatIdx++) {
2496
+ const splatByteOffset = splatIdx * bytesPerSplat;
2497
+ // Determine which bucket this splat belongs to
2498
+ let bucketIdx;
2499
+ if (splatIdx < fullBucketSplats) {
2500
+ bucketIdx = Math.floor(splatIdx / bucketCapacity);
2501
+ }
2502
+ else {
2503
+ const currentBucketSize = partialBucketSizes[currentPartialBucket - fullBuckets];
2504
+ if (splatIdx >= currentPartialBase + currentBucketSize) {
2505
+ currentPartialBucket++;
2506
+ currentPartialBase += currentBucketSize;
2507
+ }
2508
+ bucketIdx = currentPartialBucket;
2509
+ }
2510
+ // Decode position
2511
+ let x, y, z;
2512
+ if (compressionMode === 0) {
2513
+ x = splatData.getFloat32(splatByteOffset, true);
2514
+ y = splatData.getFloat32(splatByteOffset + 4, true);
2515
+ z = splatData.getFloat32(splatByteOffset + 8, true);
2516
+ }
2517
+ else {
2518
+ x = (splatData.getUint16(splatByteOffset, true) - quantizationRange) * positionScale + bucketCenters[bucketIdx * 3];
2519
+ y = (splatData.getUint16(splatByteOffset + 2, true) - quantizationRange) * positionScale + bucketCenters[bucketIdx * 3 + 1];
2520
+ z = (splatData.getUint16(splatByteOffset + 4, true) - quantizationRange) * positionScale + bucketCenters[bucketIdx * 3 + 2];
2521
+ }
2522
+ // Decode scales
2523
+ let scaleX, scaleY, scaleZ;
2524
+ if (compressionMode === 0) {
2525
+ scaleX = splatData.getFloat32(splatByteOffset + scaleStartByte, true);
2526
+ scaleY = splatData.getFloat32(splatByteOffset + scaleStartByte + 4, true);
2527
+ scaleZ = splatData.getFloat32(splatByteOffset + scaleStartByte + 8, true);
2528
+ }
2529
+ else {
2530
+ scaleX = decodeFloat16(splatData.getUint16(splatByteOffset + scaleStartByte, true));
2531
+ scaleY = decodeFloat16(splatData.getUint16(splatByteOffset + scaleStartByte + 2, true));
2532
+ scaleZ = decodeFloat16(splatData.getUint16(splatByteOffset + scaleStartByte + 4, true));
2533
+ }
2534
+ // Decode rotation quaternion
2535
+ let rot0, rot1, rot2, rot3;
2536
+ if (compressionMode === 0) {
2537
+ rot0 = splatData.getFloat32(splatByteOffset + rotationStartByte, true);
2538
+ rot1 = splatData.getFloat32(splatByteOffset + rotationStartByte + 4, true);
2539
+ rot2 = splatData.getFloat32(splatByteOffset + rotationStartByte + 8, true);
2540
+ rot3 = splatData.getFloat32(splatByteOffset + rotationStartByte + 12, true);
2541
+ }
2542
+ else {
2543
+ rot0 = decodeFloat16(splatData.getUint16(splatByteOffset + rotationStartByte, true));
2544
+ rot1 = decodeFloat16(splatData.getUint16(splatByteOffset + rotationStartByte + 2, true));
2545
+ rot2 = decodeFloat16(splatData.getUint16(splatByteOffset + rotationStartByte + 4, true));
2546
+ rot3 = decodeFloat16(splatData.getUint16(splatByteOffset + rotationStartByte + 6, true));
2547
+ }
2548
+ // Decode color and opacity
2549
+ const red = splatData.getUint8(splatByteOffset + colorStartByte);
2550
+ const green = splatData.getUint8(splatByteOffset + colorStartByte + 1);
2551
+ const blue = splatData.getUint8(splatByteOffset + colorStartByte + 2);
2552
+ const opacity = splatData.getUint8(splatByteOffset + colorStartByte + 3);
2553
+ // Store position
2554
+ columns[0].data[splatIndex] = x;
2555
+ columns[1].data[splatIndex] = y;
2556
+ columns[2].data[splatIndex] = z;
2557
+ // Store scale (convert from linear in .ksplat to log scale for internal use)
2558
+ columns[3].data[splatIndex] = scaleX > 0 ? Math.log(scaleX) : -10;
2559
+ columns[4].data[splatIndex] = scaleY > 0 ? Math.log(scaleY) : -10;
2560
+ columns[5].data[splatIndex] = scaleZ > 0 ? Math.log(scaleZ) : -10;
2561
+ // Store color (convert from uint8 back to spherical harmonics)
2562
+ const SH_C0 = 0.28209479177387814;
2563
+ columns[6].data[splatIndex] = (red / 255.0 - 0.5) / SH_C0;
2564
+ columns[7].data[splatIndex] = (green / 255.0 - 0.5) / SH_C0;
2565
+ columns[8].data[splatIndex] = (blue / 255.0 - 0.5) / SH_C0;
2566
+ // Store opacity (convert from uint8 to float and apply inverse sigmoid)
2567
+ const epsilon = 1e-6;
2568
+ const normalizedOpacity = Math.max(epsilon, Math.min(1.0 - epsilon, opacity / 255.0));
2569
+ columns[9].data[splatIndex] = Math.log(normalizedOpacity / (1.0 - normalizedOpacity));
2570
+ // Store quaternion
2571
+ columns[10].data[splatIndex] = rot0;
2572
+ columns[11].data[splatIndex] = rot1;
2573
+ columns[12].data[splatIndex] = rot2;
2574
+ columns[13].data[splatIndex] = rot3;
2575
+ // Store spherical harmonics
2576
+ for (let i = 0; i < harmonicsComponentCount; i++) {
2577
+ let channel;
2578
+ let coeff;
2579
+ // band 0 is packed together, then band 1, then band 2.
2580
+ if (i < 9) {
2581
+ channel = Math.floor(i / 3);
2582
+ coeff = i % 3;
2583
+ }
2584
+ else if (i < 24) {
2585
+ channel = Math.floor((i - 9) / 5);
2586
+ coeff = (i - 9) % 5 + 3;
2587
+ }
2588
+ else {
2589
+ // don't think 3 bands are supported, but here just in case
2590
+ channel = Math.floor((i - 24) / 7);
2591
+ coeff = (i - 24) % 7 + 8;
2592
+ }
2593
+ const col = channel * (harmonicsComponentCount / 3) + coeff;
2594
+ columns[14 + col].data[splatIndex] = decodeHarmonics(splatByteOffset, i);
2595
+ }
2596
+ splatIndex++;
2597
+ }
2598
+ currentSectionDataOffset += sectionDataSize + totalBucketStorageSize;
2599
+ }
2600
+ if (splatIndex !== numSplats) {
2601
+ throw new Error(`Splat count mismatch: expected ${numSplats}, processed ${splatIndex}`);
2602
+ }
2603
+ const resultTable = new DataTable(columns);
2604
+ return {
2605
+ comments: [],
2606
+ elements: [{
2607
+ name: 'vertex',
2608
+ dataTable: resultTable
2609
+ }]
2610
+ };
2611
+ };
2612
+
2310
2613
  const getDataType = (type) => {
2311
2614
  switch (type) {
2312
2615
  case 'char': return Int8Array;
@@ -2421,393 +2724,102 @@ const readPly = async (fileHandle) => {
2421
2724
  const chunkData = Buffer$1.alloc(chunkSize * rowSize);
2422
2725
  for (let c = 0; c < numChunks; ++c) {
2423
2726
  const numRows = Math.min(chunkSize, element.count - c * chunkSize);
2424
- await fileHandle.read(chunkData, 0, rowSize * numRows);
2425
- let offset = 0;
2426
- // read data row at a time
2427
- for (let r = 0; r < numRows; ++r) {
2428
- const rowOffset = c * chunkSize + r;
2429
- // copy into column data
2430
- for (let p = 0; p < columns.length; ++p) {
2431
- const s = sizes[p];
2432
- chunkData.copy(buffers[p], rowOffset * s, offset, offset + s);
2433
- offset += s;
2434
- }
2435
- }
2436
- }
2437
- elements.push({
2438
- name: element.name,
2439
- dataTable: new DataTable(columns)
2440
- });
2441
- }
2442
- return {
2443
- comments: header.comments,
2444
- elements
2445
- };
2446
- };
2447
-
2448
- const readSplat = async (fileHandle) => {
2449
- // Get file size to determine number of splats
2450
- const fileStats = await fileHandle.stat();
2451
- const fileSize = fileStats.size;
2452
- // Each splat is 32 bytes
2453
- const BYTES_PER_SPLAT = 32;
2454
- if (fileSize % BYTES_PER_SPLAT !== 0) {
2455
- throw new Error('Invalid .splat file: file size is not a multiple of 32 bytes');
2456
- }
2457
- const numSplats = fileSize / BYTES_PER_SPLAT;
2458
- if (numSplats === 0) {
2459
- throw new Error('Invalid .splat file: file is empty');
2460
- }
2461
- // Create columns for the standard Gaussian splat data
2462
- const columns = [
2463
- // Position
2464
- new Column('x', new Float32Array(numSplats)),
2465
- new Column('y', new Float32Array(numSplats)),
2466
- new Column('z', new Float32Array(numSplats)),
2467
- // Scale (stored as linear in .splat, convert to log for internal use)
2468
- new Column('scale_0', new Float32Array(numSplats)),
2469
- new Column('scale_1', new Float32Array(numSplats)),
2470
- new Column('scale_2', new Float32Array(numSplats)),
2471
- // Color/opacity
2472
- new Column('f_dc_0', new Float32Array(numSplats)), // Red
2473
- new Column('f_dc_1', new Float32Array(numSplats)), // Green
2474
- new Column('f_dc_2', new Float32Array(numSplats)), // Blue
2475
- new Column('opacity', new Float32Array(numSplats)),
2476
- // Rotation quaternion
2477
- new Column('rot_0', new Float32Array(numSplats)),
2478
- new Column('rot_1', new Float32Array(numSplats)),
2479
- new Column('rot_2', new Float32Array(numSplats)),
2480
- new Column('rot_3', new Float32Array(numSplats))
2481
- ];
2482
- // Read data in chunks
2483
- const chunkSize = 1024;
2484
- const numChunks = Math.ceil(numSplats / chunkSize);
2485
- const chunkData = Buffer$1.alloc(chunkSize * BYTES_PER_SPLAT);
2486
- for (let c = 0; c < numChunks; ++c) {
2487
- const numRows = Math.min(chunkSize, numSplats - c * chunkSize);
2488
- const bytesToRead = numRows * BYTES_PER_SPLAT;
2489
- const { bytesRead } = await fileHandle.read(chunkData, 0, bytesToRead);
2490
- if (bytesRead !== bytesToRead) {
2491
- throw new Error('Failed to read expected amount of data from .splat file');
2492
- }
2493
- // Parse each splat in the chunk
2494
- for (let r = 0; r < numRows; ++r) {
2495
- const splatIndex = c * chunkSize + r;
2496
- const offset = r * BYTES_PER_SPLAT;
2497
- // Read position (3 × float32)
2498
- const x = chunkData.readFloatLE(offset + 0);
2499
- const y = chunkData.readFloatLE(offset + 4);
2500
- const z = chunkData.readFloatLE(offset + 8);
2501
- // Read scale (3 × float32)
2502
- const scaleX = chunkData.readFloatLE(offset + 12);
2503
- const scaleY = chunkData.readFloatLE(offset + 16);
2504
- const scaleZ = chunkData.readFloatLE(offset + 20);
2505
- // Read color and opacity (4 × uint8)
2506
- const red = chunkData.readUInt8(offset + 24);
2507
- const green = chunkData.readUInt8(offset + 25);
2508
- const blue = chunkData.readUInt8(offset + 26);
2509
- const opacity = chunkData.readUInt8(offset + 27);
2510
- // Read rotation quaternion (4 × uint8)
2511
- const rot0 = chunkData.readUInt8(offset + 28);
2512
- const rot1 = chunkData.readUInt8(offset + 29);
2513
- const rot2 = chunkData.readUInt8(offset + 30);
2514
- const rot3 = chunkData.readUInt8(offset + 31);
2515
- // Store position
2516
- columns[0].data[splatIndex] = x;
2517
- columns[1].data[splatIndex] = y;
2518
- columns[2].data[splatIndex] = z;
2519
- // Store scale (convert from linear in .splat to log scale for internal use)
2520
- columns[3].data[splatIndex] = Math.log(scaleX);
2521
- columns[4].data[splatIndex] = Math.log(scaleY);
2522
- columns[5].data[splatIndex] = Math.log(scaleZ);
2523
- // Store color (convert from uint8 back to spherical harmonics)
2524
- const SH_C0 = 0.28209479177387814;
2525
- columns[6].data[splatIndex] = (red / 255.0 - 0.5) / SH_C0;
2526
- columns[7].data[splatIndex] = (green / 255.0 - 0.5) / SH_C0;
2527
- columns[8].data[splatIndex] = (blue / 255.0 - 0.5) / SH_C0;
2528
- // Store opacity (convert from uint8 to float and apply inverse sigmoid)
2529
- const epsilon = 1e-6;
2530
- const normalizedOpacity = Math.max(epsilon, Math.min(1.0 - epsilon, opacity / 255.0));
2531
- columns[9].data[splatIndex] = Math.log(normalizedOpacity / (1.0 - normalizedOpacity));
2532
- // Store rotation quaternion (convert from uint8 [0,255] to float [-1,1] and normalize)
2533
- const rot0Norm = (rot0 / 255.0) * 2.0 - 1.0;
2534
- const rot1Norm = (rot1 / 255.0) * 2.0 - 1.0;
2535
- const rot2Norm = (rot2 / 255.0) * 2.0 - 1.0;
2536
- const rot3Norm = (rot3 / 255.0) * 2.0 - 1.0;
2537
- // Normalize quaternion
2538
- const length = Math.sqrt(rot0Norm * rot0Norm + rot1Norm * rot1Norm + rot2Norm * rot2Norm + rot3Norm * rot3Norm);
2539
- if (length > 0) {
2540
- columns[10].data[splatIndex] = rot0Norm / length;
2541
- columns[11].data[splatIndex] = rot1Norm / length;
2542
- columns[12].data[splatIndex] = rot2Norm / length;
2543
- columns[13].data[splatIndex] = rot3Norm / length;
2544
- }
2545
- else {
2546
- // Default to identity quaternion if invalid
2547
- columns[10].data[splatIndex] = 0.0;
2548
- columns[11].data[splatIndex] = 0.0;
2549
- columns[12].data[splatIndex] = 0.0;
2550
- columns[13].data[splatIndex] = 1.0;
2551
- }
2552
- }
2553
- }
2554
- return {
2555
- comments: [],
2556
- elements: [{
2557
- name: 'vertex',
2558
- dataTable: new DataTable(columns)
2559
- }]
2560
- };
2561
- };
2562
-
2563
- // Half-precision floating point decoder
2564
- function decodeFloat16(encoded) {
2565
- const signBit = (encoded >> 15) & 1;
2566
- const exponent = (encoded >> 10) & 0x1f;
2567
- const mantissa = encoded & 0x3ff;
2568
- if (exponent === 0) {
2569
- if (mantissa === 0) {
2570
- return signBit ? -0 : 0.0;
2571
- }
2572
- // Denormalized number
2573
- let m = mantissa;
2574
- let exp = -14;
2575
- while (!(m & 0x400)) {
2576
- m <<= 1;
2577
- exp--;
2578
- }
2579
- m &= 0x3ff;
2580
- const finalExp = exp + 127;
2581
- const finalMantissa = m << 13;
2582
- const bits = (signBit << 31) | (finalExp << 23) | finalMantissa;
2583
- return new Float32Array(new Uint32Array([bits]).buffer)[0];
2584
- }
2585
- if (exponent === 0x1f) {
2586
- return mantissa === 0 ? (signBit ? -Infinity : Infinity) : NaN;
2587
- }
2588
- const finalExp = exponent - 15 + 127;
2589
- const finalMantissa = mantissa << 13;
2590
- const bits = (signBit << 31) | (finalExp << 23) | finalMantissa;
2591
- return new Float32Array(new Uint32Array([bits]).buffer)[0];
2592
- }
2593
- const COMPRESSION_MODES = [
2594
- {
2595
- centerBytes: 12,
2596
- scaleBytes: 12,
2597
- rotationBytes: 16,
2598
- colorBytes: 4,
2599
- harmonicsBytes: 4,
2600
- scaleStartByte: 12,
2601
- rotationStartByte: 24,
2602
- colorStartByte: 40,
2603
- harmonicsStartByte: 44,
2604
- scaleQuantRange: 1
2605
- },
2606
- {
2607
- centerBytes: 6,
2608
- scaleBytes: 6,
2609
- rotationBytes: 8,
2610
- colorBytes: 4,
2611
- harmonicsBytes: 2,
2612
- scaleStartByte: 6,
2613
- rotationStartByte: 12,
2614
- colorStartByte: 20,
2615
- harmonicsStartByte: 24,
2616
- scaleQuantRange: 32767
2617
- },
2618
- {
2619
- centerBytes: 6,
2620
- scaleBytes: 6,
2621
- rotationBytes: 8,
2622
- colorBytes: 4,
2623
- harmonicsBytes: 1,
2624
- scaleStartByte: 6,
2625
- rotationStartByte: 12,
2626
- colorStartByte: 20,
2627
- harmonicsStartByte: 24,
2628
- scaleQuantRange: 32767
2629
- }
2630
- ];
2631
- const HARMONICS_COMPONENT_COUNT = [0, 9, 24, 45];
2632
- const readKsplat = async (fileHandle) => {
2633
- const stats = await fileHandle.stat();
2634
- const totalSize = stats.size;
2635
- // Load complete file
2636
- const fileBuffer = Buffer$1.alloc(totalSize);
2637
- await fileHandle.read(fileBuffer, 0, totalSize, 0);
2638
- const MAIN_HEADER_SIZE = 4096;
2639
- const SECTION_HEADER_SIZE = 1024;
2640
- if (totalSize < MAIN_HEADER_SIZE) {
2641
- throw new Error('File too small to be valid .ksplat format');
2642
- }
2643
- // Parse main header
2644
- const mainHeader = new DataView(fileBuffer.buffer, fileBuffer.byteOffset, MAIN_HEADER_SIZE);
2645
- const majorVersion = mainHeader.getUint8(0);
2646
- const minorVersion = mainHeader.getUint8(1);
2647
- if (majorVersion !== 0 || minorVersion < 1) {
2648
- throw new Error(`Unsupported version ${majorVersion}.${minorVersion}`);
2727
+ await fileHandle.read(chunkData, 0, rowSize * numRows);
2728
+ let offset = 0;
2729
+ // read data row at a time
2730
+ for (let r = 0; r < numRows; ++r) {
2731
+ const rowOffset = c * chunkSize + r;
2732
+ // copy into column data
2733
+ for (let p = 0; p < columns.length; ++p) {
2734
+ const s = sizes[p];
2735
+ chunkData.copy(buffers[p], rowOffset * s, offset, offset + s);
2736
+ offset += s;
2737
+ }
2738
+ }
2739
+ }
2740
+ elements.push({
2741
+ name: element.name,
2742
+ dataTable: new DataTable(columns)
2743
+ });
2649
2744
  }
2650
- const maxSections = mainHeader.getUint32(4, true);
2651
- const numSplats = mainHeader.getUint32(16, true);
2652
- const compressionMode = mainHeader.getUint16(20, true);
2653
- if (compressionMode > 2) {
2654
- throw new Error(`Invalid compression mode: ${compressionMode}`);
2745
+ return {
2746
+ comments: header.comments,
2747
+ elements
2748
+ };
2749
+ };
2750
+
2751
+ const readSplat = async (fileHandle) => {
2752
+ // Get file size to determine number of splats
2753
+ const fileStats = await fileHandle.stat();
2754
+ const fileSize = fileStats.size;
2755
+ // Each splat is 32 bytes
2756
+ const BYTES_PER_SPLAT = 32;
2757
+ if (fileSize % BYTES_PER_SPLAT !== 0) {
2758
+ throw new Error('Invalid .splat file: file size is not a multiple of 32 bytes');
2655
2759
  }
2656
- const minHarmonicsValue = mainHeader.getFloat32(36, true) ?? -1.5;
2657
- const maxHarmonicsValue = mainHeader.getFloat32(40, true) ?? 1.5;
2760
+ const numSplats = fileSize / BYTES_PER_SPLAT;
2658
2761
  if (numSplats === 0) {
2659
- throw new Error('Invalid .ksplat file: file is empty');
2660
- }
2661
- // First pass: scan all sections to find maximum harmonics degree
2662
- let maxHarmonicsDegree = 0;
2663
- for (let sectionIdx = 0; sectionIdx < maxSections; sectionIdx++) {
2664
- const sectionHeaderOffset = MAIN_HEADER_SIZE + sectionIdx * SECTION_HEADER_SIZE;
2665
- const sectionHeader = new DataView(fileBuffer.buffer, fileBuffer.byteOffset + sectionHeaderOffset, SECTION_HEADER_SIZE);
2666
- const sectionSplatCount = sectionHeader.getUint32(0, true);
2667
- if (sectionSplatCount === 0)
2668
- continue; // Skip empty sections
2669
- const harmonicsDegree = sectionHeader.getUint16(40, true);
2670
- maxHarmonicsDegree = Math.max(maxHarmonicsDegree, harmonicsDegree);
2762
+ throw new Error('Invalid .splat file: file is empty');
2671
2763
  }
2672
- // Initialize data storage with base columns
2764
+ // Create columns for the standard Gaussian splat data
2673
2765
  const columns = [
2766
+ // Position
2674
2767
  new Column('x', new Float32Array(numSplats)),
2675
2768
  new Column('y', new Float32Array(numSplats)),
2676
2769
  new Column('z', new Float32Array(numSplats)),
2770
+ // Scale (stored as linear in .splat, convert to log for internal use)
2677
2771
  new Column('scale_0', new Float32Array(numSplats)),
2678
2772
  new Column('scale_1', new Float32Array(numSplats)),
2679
2773
  new Column('scale_2', new Float32Array(numSplats)),
2680
- new Column('f_dc_0', new Float32Array(numSplats)),
2681
- new Column('f_dc_1', new Float32Array(numSplats)),
2682
- new Column('f_dc_2', new Float32Array(numSplats)),
2774
+ // Color/opacity
2775
+ new Column('f_dc_0', new Float32Array(numSplats)), // Red
2776
+ new Column('f_dc_1', new Float32Array(numSplats)), // Green
2777
+ new Column('f_dc_2', new Float32Array(numSplats)), // Blue
2683
2778
  new Column('opacity', new Float32Array(numSplats)),
2779
+ // Rotation quaternion
2684
2780
  new Column('rot_0', new Float32Array(numSplats)),
2685
2781
  new Column('rot_1', new Float32Array(numSplats)),
2686
2782
  new Column('rot_2', new Float32Array(numSplats)),
2687
2783
  new Column('rot_3', new Float32Array(numSplats))
2688
2784
  ];
2689
- // Add spherical harmonics columns based on maximum degree found
2690
- const maxHarmonicsComponentCount = HARMONICS_COMPONENT_COUNT[maxHarmonicsDegree];
2691
- for (let i = 0; i < maxHarmonicsComponentCount; i++) {
2692
- columns.push(new Column(`f_rest_${i}`, new Float32Array(numSplats)));
2693
- }
2694
- const { centerBytes, scaleBytes, rotationBytes, colorBytes, harmonicsBytes, scaleStartByte, rotationStartByte, colorStartByte, harmonicsStartByte, scaleQuantRange } = COMPRESSION_MODES[compressionMode];
2695
- let currentSectionDataOffset = MAIN_HEADER_SIZE + maxSections * SECTION_HEADER_SIZE;
2696
- let splatIndex = 0;
2697
- // Process each section
2698
- for (let sectionIdx = 0; sectionIdx < maxSections; sectionIdx++) {
2699
- const sectionHeaderOffset = MAIN_HEADER_SIZE + sectionIdx * SECTION_HEADER_SIZE;
2700
- const sectionHeader = new DataView(fileBuffer.buffer, fileBuffer.byteOffset + sectionHeaderOffset, SECTION_HEADER_SIZE);
2701
- const sectionSplatCount = sectionHeader.getUint32(0, true);
2702
- const maxSectionSplats = sectionHeader.getUint32(4, true);
2703
- const bucketCapacity = sectionHeader.getUint32(8, true);
2704
- const bucketCount = sectionHeader.getUint32(12, true);
2705
- const spatialBlockSize = sectionHeader.getFloat32(16, true);
2706
- const bucketStorageSize = sectionHeader.getUint16(20, true);
2707
- const quantizationRange = sectionHeader.getUint32(24, true) || scaleQuantRange;
2708
- const fullBuckets = sectionHeader.getUint32(32, true);
2709
- const partialBuckets = sectionHeader.getUint32(36, true);
2710
- const harmonicsDegree = sectionHeader.getUint16(40, true);
2711
- // Calculate layout
2712
- const fullBucketSplats = fullBuckets * bucketCapacity;
2713
- const partialBucketMetaSize = partialBuckets * 4;
2714
- const totalBucketStorageSize = bucketStorageSize * bucketCount + partialBucketMetaSize;
2715
- const harmonicsComponentCount = HARMONICS_COMPONENT_COUNT[harmonicsDegree];
2716
- const bytesPerSplat = centerBytes + scaleBytes + rotationBytes +
2717
- colorBytes + harmonicsComponentCount * harmonicsBytes;
2718
- const sectionDataSize = bytesPerSplat * maxSectionSplats;
2719
- // Calculate decompression parameters
2720
- const positionScale = spatialBlockSize / 2.0 / quantizationRange;
2721
- // Get bucket centers
2722
- const bucketCentersOffset = currentSectionDataOffset + partialBucketMetaSize;
2723
- const bucketCenters = new Float32Array(fileBuffer.buffer, fileBuffer.byteOffset + bucketCentersOffset, bucketCount * 3);
2724
- // Get partial bucket sizes
2725
- const partialBucketSizes = new Uint32Array(fileBuffer.buffer, fileBuffer.byteOffset + currentSectionDataOffset, partialBuckets);
2726
- // Get splat data
2727
- const splatDataOffset = currentSectionDataOffset + totalBucketStorageSize;
2728
- const splatData = new DataView(fileBuffer.buffer, fileBuffer.byteOffset + splatDataOffset, sectionDataSize);
2729
- // Harmonic value decoder
2730
- const decodeHarmonics = (offset, component) => {
2731
- switch (compressionMode) {
2732
- case 0:
2733
- return splatData.getFloat32(offset + harmonicsStartByte + component * 4, true);
2734
- case 1:
2735
- return decodeFloat16(splatData.getUint16(offset + harmonicsStartByte + component * 2, true));
2736
- case 2: {
2737
- const normalized = splatData.getUint8(offset + harmonicsStartByte + component) / 255;
2738
- return minHarmonicsValue + normalized * (maxHarmonicsValue - minHarmonicsValue);
2739
- }
2740
- default:
2741
- return 0;
2742
- }
2743
- };
2744
- // Track partial bucket processing
2745
- let currentPartialBucket = fullBuckets;
2746
- let currentPartialBase = fullBucketSplats;
2747
- // Process splats in this section
2748
- for (let splatIdx = 0; splatIdx < sectionSplatCount; splatIdx++) {
2749
- const splatByteOffset = splatIdx * bytesPerSplat;
2750
- // Determine which bucket this splat belongs to
2751
- let bucketIdx;
2752
- if (splatIdx < fullBucketSplats) {
2753
- bucketIdx = Math.floor(splatIdx / bucketCapacity);
2754
- }
2755
- else {
2756
- const currentBucketSize = partialBucketSizes[currentPartialBucket - fullBuckets];
2757
- if (splatIdx >= currentPartialBase + currentBucketSize) {
2758
- currentPartialBucket++;
2759
- currentPartialBase += currentBucketSize;
2760
- }
2761
- bucketIdx = currentPartialBucket;
2762
- }
2763
- // Decode position
2764
- let x, y, z;
2765
- if (compressionMode === 0) {
2766
- x = splatData.getFloat32(splatByteOffset, true);
2767
- y = splatData.getFloat32(splatByteOffset + 4, true);
2768
- z = splatData.getFloat32(splatByteOffset + 8, true);
2769
- }
2770
- else {
2771
- x = (splatData.getUint16(splatByteOffset, true) - quantizationRange) * positionScale + bucketCenters[bucketIdx * 3];
2772
- y = (splatData.getUint16(splatByteOffset + 2, true) - quantizationRange) * positionScale + bucketCenters[bucketIdx * 3 + 1];
2773
- z = (splatData.getUint16(splatByteOffset + 4, true) - quantizationRange) * positionScale + bucketCenters[bucketIdx * 3 + 2];
2774
- }
2775
- // Decode scales
2776
- let scaleX, scaleY, scaleZ;
2777
- if (compressionMode === 0) {
2778
- scaleX = splatData.getFloat32(splatByteOffset + scaleStartByte, true);
2779
- scaleY = splatData.getFloat32(splatByteOffset + scaleStartByte + 4, true);
2780
- scaleZ = splatData.getFloat32(splatByteOffset + scaleStartByte + 8, true);
2781
- }
2782
- else {
2783
- scaleX = decodeFloat16(splatData.getUint16(splatByteOffset + scaleStartByte, true));
2784
- scaleY = decodeFloat16(splatData.getUint16(splatByteOffset + scaleStartByte + 2, true));
2785
- scaleZ = decodeFloat16(splatData.getUint16(splatByteOffset + scaleStartByte + 4, true));
2786
- }
2787
- // Decode rotation quaternion
2788
- let rot0, rot1, rot2, rot3;
2789
- if (compressionMode === 0) {
2790
- rot0 = splatData.getFloat32(splatByteOffset + rotationStartByte, true);
2791
- rot1 = splatData.getFloat32(splatByteOffset + rotationStartByte + 4, true);
2792
- rot2 = splatData.getFloat32(splatByteOffset + rotationStartByte + 8, true);
2793
- rot3 = splatData.getFloat32(splatByteOffset + rotationStartByte + 12, true);
2794
- }
2795
- else {
2796
- rot0 = decodeFloat16(splatData.getUint16(splatByteOffset + rotationStartByte, true));
2797
- rot1 = decodeFloat16(splatData.getUint16(splatByteOffset + rotationStartByte + 2, true));
2798
- rot2 = decodeFloat16(splatData.getUint16(splatByteOffset + rotationStartByte + 4, true));
2799
- rot3 = decodeFloat16(splatData.getUint16(splatByteOffset + rotationStartByte + 6, true));
2800
- }
2801
- // Decode color and opacity
2802
- const red = splatData.getUint8(splatByteOffset + colorStartByte);
2803
- const green = splatData.getUint8(splatByteOffset + colorStartByte + 1);
2804
- const blue = splatData.getUint8(splatByteOffset + colorStartByte + 2);
2805
- const opacity = splatData.getUint8(splatByteOffset + colorStartByte + 3);
2785
+ // Read data in chunks
2786
+ const chunkSize = 1024;
2787
+ const numChunks = Math.ceil(numSplats / chunkSize);
2788
+ const chunkData = Buffer$1.alloc(chunkSize * BYTES_PER_SPLAT);
2789
+ for (let c = 0; c < numChunks; ++c) {
2790
+ const numRows = Math.min(chunkSize, numSplats - c * chunkSize);
2791
+ const bytesToRead = numRows * BYTES_PER_SPLAT;
2792
+ const { bytesRead } = await fileHandle.read(chunkData, 0, bytesToRead);
2793
+ if (bytesRead !== bytesToRead) {
2794
+ throw new Error('Failed to read expected amount of data from .splat file');
2795
+ }
2796
+ // Parse each splat in the chunk
2797
+ for (let r = 0; r < numRows; ++r) {
2798
+ const splatIndex = c * chunkSize + r;
2799
+ const offset = r * BYTES_PER_SPLAT;
2800
+ // Read position (3 × float32)
2801
+ const x = chunkData.readFloatLE(offset + 0);
2802
+ const y = chunkData.readFloatLE(offset + 4);
2803
+ const z = chunkData.readFloatLE(offset + 8);
2804
+ // Read scale (3 × float32)
2805
+ const scaleX = chunkData.readFloatLE(offset + 12);
2806
+ const scaleY = chunkData.readFloatLE(offset + 16);
2807
+ const scaleZ = chunkData.readFloatLE(offset + 20);
2808
+ // Read color and opacity (4 × uint8)
2809
+ const red = chunkData.readUInt8(offset + 24);
2810
+ const green = chunkData.readUInt8(offset + 25);
2811
+ const blue = chunkData.readUInt8(offset + 26);
2812
+ const opacity = chunkData.readUInt8(offset + 27);
2813
+ // Read rotation quaternion (4 × uint8)
2814
+ const rot0 = chunkData.readUInt8(offset + 28);
2815
+ const rot1 = chunkData.readUInt8(offset + 29);
2816
+ const rot2 = chunkData.readUInt8(offset + 30);
2817
+ const rot3 = chunkData.readUInt8(offset + 31);
2806
2818
  // Store position
2807
2819
  columns[0].data[splatIndex] = x;
2808
2820
  columns[1].data[splatIndex] = y;
2809
2821
  columns[2].data[splatIndex] = z;
2810
- // Store scale (convert from linear in .ksplat to log scale for internal use)
2822
+ // Store scale (convert from linear in .splat to log scale for internal use)
2811
2823
  columns[3].data[splatIndex] = Math.log(scaleX);
2812
2824
  columns[4].data[splatIndex] = Math.log(scaleY);
2813
2825
  columns[5].data[splatIndex] = Math.log(scaleZ);
@@ -2820,28 +2832,33 @@ const readKsplat = async (fileHandle) => {
2820
2832
  const epsilon = 1e-6;
2821
2833
  const normalizedOpacity = Math.max(epsilon, Math.min(1.0 - epsilon, opacity / 255.0));
2822
2834
  columns[9].data[splatIndex] = Math.log(normalizedOpacity / (1.0 - normalizedOpacity));
2823
- // Store quaternion
2824
- columns[10].data[splatIndex] = rot0;
2825
- columns[11].data[splatIndex] = rot1;
2826
- columns[12].data[splatIndex] = rot2;
2827
- columns[13].data[splatIndex] = rot3;
2828
- // Store spherical harmonics
2829
- for (let i = 0; i < harmonicsComponentCount; i++) {
2830
- columns[14 + i].data[splatIndex] = decodeHarmonics(splatByteOffset, i);
2835
+ // Store rotation quaternion (convert from uint8 [0,255] to float [-1,1] and normalize)
2836
+ const rot0Norm = (rot0 / 255.0) * 2.0 - 1.0;
2837
+ const rot1Norm = (rot1 / 255.0) * 2.0 - 1.0;
2838
+ const rot2Norm = (rot2 / 255.0) * 2.0 - 1.0;
2839
+ const rot3Norm = (rot3 / 255.0) * 2.0 - 1.0;
2840
+ // Normalize quaternion
2841
+ const length = Math.sqrt(rot0Norm * rot0Norm + rot1Norm * rot1Norm + rot2Norm * rot2Norm + rot3Norm * rot3Norm);
2842
+ if (length > 0) {
2843
+ columns[10].data[splatIndex] = rot0Norm / length;
2844
+ columns[11].data[splatIndex] = rot1Norm / length;
2845
+ columns[12].data[splatIndex] = rot2Norm / length;
2846
+ columns[13].data[splatIndex] = rot3Norm / length;
2847
+ }
2848
+ else {
2849
+ // Default to identity quaternion if invalid
2850
+ columns[10].data[splatIndex] = 0.0;
2851
+ columns[11].data[splatIndex] = 0.0;
2852
+ columns[12].data[splatIndex] = 0.0;
2853
+ columns[13].data[splatIndex] = 1.0;
2831
2854
  }
2832
- splatIndex++;
2833
2855
  }
2834
- currentSectionDataOffset += sectionDataSize + totalBucketStorageSize;
2835
- }
2836
- if (splatIndex !== numSplats) {
2837
- throw new Error(`Splat count mismatch: expected ${numSplats}, processed ${splatIndex}`);
2838
2856
  }
2839
- const resultTable = new DataTable(columns);
2840
2857
  return {
2841
2858
  comments: [],
2842
2859
  elements: [{
2843
2860
  name: 'vertex',
2844
- dataTable: resultTable
2861
+ dataTable: new DataTable(columns)
2845
2862
  }]
2846
2863
  };
2847
2864
  };
@@ -3663,44 +3680,44 @@ const parseArguments = () => {
3663
3680
  }
3664
3681
  return { files, options };
3665
3682
  };
3666
- const usage = `
3667
- Apply geometric transforms & filters to Gaussian-splat point clouds
3668
- ===================================================================
3669
-
3670
- USAGE
3671
- splat-transform [GLOBAL] <input.{ply|splat|ksplat}> [ACTIONS] ... <output.{ply|compressed.ply|meta.json|csv}> [ACTIONS]
3672
-
3673
- • Every time an input file appears, it becomes the current working set; the following
3674
- ACTIONS are applied in the order listed.
3675
- • The last file on the command line is treated as the output; anything after it is
3676
- interpreted as actions that modify the final result.
3677
-
3678
- SUPPORTED INPUTS
3679
- .ply .splat .ksplat
3680
-
3681
- SUPPORTED OUTPUTS
3682
- .ply .compressed.ply meta.json (SOGS) .csv
3683
-
3684
- ACTIONS (can be repeated, in any order)
3685
- -t, --translate x,y,z Translate splats by (x, y, z)
3686
- -r, --rotate x,y,z Rotate splats by Euler angles (deg)
3687
- -s, --scale x Uniformly scale splats by factor x
3688
- -n, --filterNaN Remove any Gaussian containing NaN/Inf
3689
- -c, --filterByValue name,cmp,value Keep splats where <name> <cmp> <value>
3690
- cmp ∈ {lt,lte,gt,gte,eq,neq}
3691
- -h, --filterBands {0|1|2|3} Strip spherical-harmonic bands > N
3692
-
3693
- GLOBAL OPTIONS
3694
- -w, --overwrite Overwrite output file if it already exists
3695
- -h, --help Show this help and exit
3696
- -v, --version Show version and exit
3697
-
3698
- EXAMPLES
3699
- # Simple scale-then-translate
3700
- splat-transform bunny.ply -s 0.5 -t 0,0,10 bunny_scaled.ply
3701
-
3702
- # Chain two inputs and write compressed output, overwriting if necessary
3703
- splat-transform -w cloudA.ply -r 0,90,0 cloudB.ply -s 2 merged.compressed.ply
3683
+ const usage = `
3684
+ Apply geometric transforms & filters to Gaussian-splat point clouds
3685
+ ===================================================================
3686
+
3687
+ USAGE
3688
+ splat-transform [GLOBAL] <input.{ply|splat|ksplat}> [ACTIONS] ... <output.{ply|compressed.ply|meta.json|csv}> [ACTIONS]
3689
+
3690
+ • Every time an input file appears, it becomes the current working set; the following
3691
+ ACTIONS are applied in the order listed.
3692
+ • The last file on the command line is treated as the output; anything after it is
3693
+ interpreted as actions that modify the final result.
3694
+
3695
+ SUPPORTED INPUTS
3696
+ .ply .splat .ksplat
3697
+
3698
+ SUPPORTED OUTPUTS
3699
+ .ply .compressed.ply meta.json (SOGS) .csv
3700
+
3701
+ ACTIONS (can be repeated, in any order)
3702
+ -t, --translate x,y,z Translate splats by (x, y, z)
3703
+ -r, --rotate x,y,z Rotate splats by Euler angles (deg)
3704
+ -s, --scale x Uniformly scale splats by factor x
3705
+ -n, --filterNaN Remove any Gaussian containing NaN/Inf
3706
+ -c, --filterByValue name,cmp,value Keep splats where <name> <cmp> <value>
3707
+ cmp ∈ {lt,lte,gt,gte,eq,neq}
3708
+ -b, --filterBands {0|1|2|3} Strip spherical-harmonic bands > N
3709
+
3710
+ GLOBAL OPTIONS
3711
+ -w, --overwrite Overwrite output file if it already exists
3712
+ -h, --help Show this help and exit
3713
+ -v, --version Show version and exit
3714
+
3715
+ EXAMPLES
3716
+ # Simple scale-then-translate
3717
+ splat-transform bunny.ply -s 0.5 -t 0,0,10 bunny_scaled.ply
3718
+
3719
+ # Chain two inputs and write compressed output, overwriting if necessary
3720
+ splat-transform -w cloudA.ply -r 0,90,0 cloudB.ply -s 2 merged.compressed.ply
3704
3721
  `;
3705
3722
  const main = async () => {
3706
3723
  console.log(`splat-transform v${version}`);