@playcanvas/splat-transform 2.0.5 → 2.0.6

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
@@ -6275,11 +6275,11 @@ class CompressedChunk {
6275
6275
  /**
6276
6276
  * The splat-transform version (semver MAJOR.MINOR.PATCH).
6277
6277
  */
6278
- const version = '2.0.5';
6278
+ const version = '2.0.6';
6279
6279
  /**
6280
6280
  * The splat-transform revision (short Git hash of HEAD at build time).
6281
6281
  */
6282
- const revision = '1fc5209';
6282
+ const revision = 'aa3dbae';
6283
6283
 
6284
6284
  const generatedByString = `Generated by splat-transform ${version}`;
6285
6285
  const chunkProps = [
@@ -8267,13 +8267,17 @@ class GpuDilation {
8267
8267
  // Extract: clear bufferA, dispatch extract from sparse src into bufferA.
8268
8268
  this.dispatchClear(slot, slot.bufferA, numWords);
8269
8269
  this.dispatchExtract(slot, minBx, minBy, minBz, outerBx, outerBy, outerBz, numXWords);
8270
- // Tiny throwaway readback forces a queue submit between extract
8271
- // and dilate. WITHOUT this, atomicOr writes from the extract pass
8272
- // aren't reliably visible to the next dilate pass (apparent
8273
- // missing memory barrier in PlayCanvas/Dawn for cross-pass atomic
8274
- // writes). Promise is intentionally not awaitedonly the
8275
- // implicit submit+barrier matters. 16 bytes negligible PCIe.
8276
- slot.bufferA.read(0, 16, null, true).catch(() => { });
8270
+ // Force a queue submission boundary between the extract pass (which
8271
+ // writes bufferA via atomicOr bound as `storage, read_write` with
8272
+ // `array<atomic<u32>>`) and the dilate-X pass (which reads bufferA
8273
+ // bound as `storage, read` with `array<u32>`). Without this, the
8274
+ // atomic writes are not reliably visible to the next pass dilation
8275
+ // produces empty output. Other inter-pass transitions in this
8276
+ // pipeline (clear→extract, dilateX→Z→Y, Y→compact) rely on automatic
8277
+ // intra-encoder synchronization without issue. Verified raw Dawn
8278
+ // synchronizes this case correctly, so the bug is somewhere in
8279
+ // PlayCanvas's compute dispatch / bind-group path.
8280
+ this.device.submit();
8277
8281
  // X-pass: A → B
8278
8282
  this.dispatchX(slot, slot.bufferA, slot.bufferB, numXWords, outerNy, outerNz, halfExtentXZ);
8279
8283
  // Z-pass: B → A
@@ -9688,8 +9692,16 @@ const binIndices = (parent, lod) => {
9688
9692
  */
9689
9693
  const writeLod = async (options, fs) => {
9690
9694
  const { filename, iterations, createDevice, chunkCount, chunkExtent } = options;
9691
- const dataTable = convertToSpace(options.dataTable, Transform.IDENTITY, true);
9692
- const envDataTable = options.envDataTable ? convertToSpace(options.envDataTable, Transform.IDENTITY, true) : null;
9695
+ // Operate in PLY space so per-leaf bounds in tree.bound are in the same
9696
+ // coordinate frame as the SOG chunk data emitted by writeSog (which also
9697
+ // converts to Transform.PLY). Without this, view-dependent streaming
9698
+ // built on tree.bound picks the wrong chunks because the bounds are
9699
+ // 180°-Z-rotated relative to the splat positions inside them.
9700
+ // This intentionally mutates the input tables to avoid doubling peak
9701
+ // memory during LOD export. Callers should treat writeLod as consuming its
9702
+ // DataTable inputs.
9703
+ const dataTable = convertToSpace(options.dataTable, Transform.PLY, true);
9704
+ const envDataTable = options.envDataTable ? convertToSpace(options.envDataTable, Transform.PLY, true) : null;
9693
9705
  const outputDir = dirname(filename);
9694
9706
  // ensure top-level output folder exists
9695
9707
  await fs.mkdir(outputDir);
@@ -15201,8 +15213,14 @@ const voxelizeToBuffer = async (bvh, gpuVoxelization, gridBounds, voxelResolutio
15201
15213
  const buffer = new BlockMaskBuffer();
15202
15214
  const bStride = numBlocksX * numBlocksY;
15203
15215
  const batchSize = 16;
15204
- const MEGA_MAX_BATCHES = 512;
15205
- const MEGA_MAX_INDICES = 4 * 1024 * 1024;
15216
+ // Caps for one mega-flush. Sized to keep a single compute dispatch's
15217
+ // wall-clock comfortably under Windows D3D12 TDR (~2s watchdog) on slower
15218
+ // GPUs without materially affecting throughput on fast GPUs (the
15219
+ // double-buffered slots keep the GPU fed across the extra flushes).
15220
+ // Output is bit-identical regardless: each batch's full Gaussian list rides
15221
+ // with it, and slot buffers grow on demand to accommodate any oversize batch.
15222
+ const MEGA_MAX_BATCHES = 256;
15223
+ const MEGA_MAX_INDICES = 2 * 1024 * 1024;
15206
15224
  const maxBlocks = GpuVoxelization.MAX_BLOCKS_PER_BATCH;
15207
15225
  const numSlots = GpuVoxelization.NUM_SLOTS;
15208
15226
  const slotIndexArrays = [];
@@ -17008,21 +17026,32 @@ const cropToNavigable = (grid, gridBounds, voxelResolution) => {
17008
17026
  return { grid, gridBounds };
17009
17027
  }
17010
17028
  const { minBx, minBy, minBz, maxBx, maxBy, maxBz } = navBounds;
17011
- const cropMaxBx = maxBx + 1;
17012
- const cropMaxBy = maxBy + 1;
17013
- const cropMaxBz = maxBz + 1;
17014
- if (minBx === 0 && minBy === 0 && minBz === 0 &&
17029
+ // Pad by 1 block on each side so the cropped grid retains the solid wall
17030
+ // blocks immediately surrounding the navigable cavity. Matches the
17031
+ // MARGIN = 1 pattern used by carve() before this re-crop strips it. The
17032
+ // collision-mesh extractors treat out-of-grid as empty, so without this
17033
+ // padding the mesh has holes wherever the cavity reaches the cropped
17034
+ // boundary; with it, the mesh extractor sees a real SOLID→EMPTY
17035
+ // transition at the cavity edge and emits a sealed wall there.
17036
+ const MARGIN = 1;
17037
+ const cropMinBx = Math.max(0, minBx - MARGIN);
17038
+ const cropMinBy = Math.max(0, minBy - MARGIN);
17039
+ const cropMinBz = Math.max(0, minBz - MARGIN);
17040
+ const cropMaxBx = Math.min(nbx, maxBx + 1 + MARGIN);
17041
+ const cropMaxBy = Math.min(nby, maxBy + 1 + MARGIN);
17042
+ const cropMaxBz = Math.min(nbz, maxBz + 1 + MARGIN);
17043
+ if (cropMinBx === 0 && cropMinBy === 0 && cropMinBz === 0 &&
17015
17044
  cropMaxBx === nbx && cropMaxBy === nby && cropMaxBz === nbz) {
17016
17045
  return { grid, gridBounds };
17017
17046
  }
17018
17047
  const cropBar = logger.bar('Cropping grid', grid.types.length);
17019
- const croppedGrid = grid.cropTo(minBx, minBy, minBz, cropMaxBx, cropMaxBy, cropMaxBz, done => cropBar.update(done));
17048
+ const croppedGrid = grid.cropTo(cropMinBx, cropMinBy, cropMinBz, cropMaxBx, cropMaxBy, cropMaxBz, done => cropBar.update(done));
17020
17049
  cropBar.end();
17021
17050
  const blockSize = 4 * voxelResolution;
17022
- const croppedMin = new Vec3(gridBounds.min.x + minBx * blockSize, gridBounds.min.y + minBy * blockSize, gridBounds.min.z + minBz * blockSize);
17051
+ const croppedMin = new Vec3(gridBounds.min.x + cropMinBx * blockSize, gridBounds.min.y + cropMinBy * blockSize, gridBounds.min.z + cropMinBz * blockSize);
17023
17052
  const croppedBounds = {
17024
17053
  min: croppedMin,
17025
- max: new Vec3(croppedMin.x + (cropMaxBx - minBx) * blockSize, croppedMin.y + (cropMaxBy - minBy) * blockSize, croppedMin.z + (cropMaxBz - minBz) * blockSize)
17054
+ max: new Vec3(croppedMin.x + (cropMaxBx - cropMinBx) * blockSize, croppedMin.y + (cropMaxBy - cropMinBy) * blockSize, croppedMin.z + (cropMaxBz - cropMinBz) * blockSize)
17026
17055
  };
17027
17056
  return { grid: croppedGrid, gridBounds: croppedBounds };
17028
17057
  };
@@ -17149,13 +17178,27 @@ const writeVoxel = async (options, fs) => {
17149
17178
  gpuVoxelization = new GpuVoxelization(device);
17150
17179
  gpuVoxelization.uploadAllGaussians(pcDataTable, extentsResult.extents);
17151
17180
  // Align grid bounds to block boundaries BEFORE voxelization so the
17152
- // block coordinates used during voxelization match what the reader expects.
17153
- // When fillExterior runs, pad by halfExtent + 1 voxels per side so the
17154
- // boundary-face flood seeds survive the dilation (notably below the floor).
17181
+ // block coordinates used during voxelization match what the reader
17182
+ // expects. fillExterior and fillFloor both need a margin of empty
17183
+ // voxels outside the splat's tight 3-sigma extents to do their job:
17184
+ // fillExterior so the boundary-face flood seeds survive its dilation
17185
+ // (notably below the floor), fillFloor so its column walk has empty
17186
+ // XZ columns to convert into wall pillars and the dilation halo to
17187
+ // extend the floor footprint outward.
17188
+ //
17189
+ // Lateral pad combines both as `dilation_radius + 1` voxels per side.
17190
+ // Vertical pad is only contributed by exteriorPad — fillFloor's
17191
+ // dilation is XZ-only, and Y padding would extend the wall pillars
17192
+ // above the splat's natural ceiling and below its floor.
17155
17193
  const exteriorPad = hasFillExterior ?
17156
17194
  (Math.ceil(navExteriorRadius / voxelResolution) + 1) * voxelResolution :
17157
17195
  0;
17158
- let gridBounds = alignGridBounds(bounds.min.x - exteriorPad, bounds.min.y - exteriorPad, bounds.min.z - exteriorPad, bounds.max.x + exteriorPad, bounds.max.y + exteriorPad, bounds.max.z + exteriorPad, voxelResolution);
17196
+ const floorPad = hasFloorFill ?
17197
+ (Math.ceil(floorFillDilation / voxelResolution) + 1) * voxelResolution :
17198
+ 0;
17199
+ const padXZ = Math.max(exteriorPad, floorPad);
17200
+ const padY = exteriorPad;
17201
+ let gridBounds = alignGridBounds(bounds.min.x - padXZ, bounds.min.y - padY, bounds.min.z - padXZ, bounds.max.x + padXZ, bounds.max.y + padY, bounds.max.z + padXZ, voxelResolution);
17159
17202
  const buffer = await voxelizeToBuffer(bvh, gpuVoxelization, gridBounds, voxelResolution, opacityCutoff);
17160
17203
  bvh = null;
17161
17204
  pcDataTable = null;