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