@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
@@ -0,0 +1,20 @@
1
+ import { SplatData } from '../../SplatData.js';
2
+ export interface FilterClusterOptions {
3
+ voxelResolution?: number;
4
+ opacityCutoff?: number;
5
+ minContribution?: number;
6
+ seed?: {
7
+ x: number;
8
+ y: number;
9
+ z: number;
10
+ };
11
+ }
12
+ export interface FilterClusterRuntimeOptions {
13
+ backend?: 'cpu' | 'gpu';
14
+ cpuWorkerCount?: number;
15
+ box?: {
16
+ minCorner: [number, number, number];
17
+ maxCorner: [number, number, number];
18
+ };
19
+ }
20
+ export declare function filterCluster(data: SplatData, options?: FilterClusterOptions, runtime?: FilterClusterRuntimeOptions): Promise<SplatData>;
@@ -0,0 +1,628 @@
1
+ import { ColIdx, SplatData } from '../../SplatData.js';
2
+ import { logger } from '../Logger.js';
3
+ import { alignGridBounds, BLOCK_EMPTY, BLOCK_MIXED, BLOCK_SOLID, BlockMaskBuffer, extentsFromQuatScale, readBlockType, SOLID_HI, SOLID_LO, SparseVoxelGrid, writeBlockType, } from './common.js';
4
+ import { cpuVoxelize, gpuVoxelize } from './voxelize.js';
5
+ const QUEUE_CAP_MAX = 1 << 30;
6
+ function makeFaceMask(axis, value) {
7
+ let lo = 0;
8
+ let hi = 0;
9
+ for (let z = 0; z < 4; z++) {
10
+ for (let y = 0; y < 4; y++) {
11
+ for (let x = 0; x < 4; x++) {
12
+ if ((axis === 0 && x !== value) || (axis === 1 && y !== value) || (axis === 2 && z !== value)) {
13
+ continue;
14
+ }
15
+ const bit = x + y * 4 + z * 16;
16
+ if (bit < 32) {
17
+ lo = (lo | (1 << bit)) >>> 0;
18
+ }
19
+ else {
20
+ hi = (hi | (1 << (bit - 32))) >>> 0;
21
+ }
22
+ }
23
+ }
24
+ }
25
+ return [lo >>> 0, hi >>> 0];
26
+ }
27
+ const FACE_MASKS = [
28
+ makeFaceMask(0, 0),
29
+ makeFaceMask(0, 3),
30
+ makeFaceMask(1, 0),
31
+ makeFaceMask(1, 3),
32
+ makeFaceMask(2, 0),
33
+ makeFaceMask(2, 3),
34
+ ];
35
+ function voxelMask(ix, iy, iz) {
36
+ const bit = (ix & 3) + ((iy & 3) << 2) + ((iz & 3) << 4);
37
+ return bit < 32 ? [(1 << bit) >>> 0, 0] : [0, (1 << (bit - 32)) >>> 0];
38
+ }
39
+ function buildBlockLookup(buffer) {
40
+ const solidSet = new Set();
41
+ const solid = buffer.getSolidBlocks();
42
+ for (let i = 0; i < solid.length; i++) {
43
+ solidSet.add(solid[i]);
44
+ }
45
+ const mixed = buffer.getMixedBlocks();
46
+ const mixedMap = new Map();
47
+ for (let i = 0; i < mixed.blockIdx.length; i++) {
48
+ mixedMap.set(mixed.blockIdx[i], i);
49
+ }
50
+ return { solidSet, mixedMap, masks: mixed.masks };
51
+ }
52
+ function isCenterInOccupiedVoxel(px, py, pz, grid, lookup) {
53
+ const bx = Math.floor((px - grid.gridMinX) / grid.blockSize);
54
+ const by = Math.floor((py - grid.gridMinY) / grid.blockSize);
55
+ const bz = Math.floor((pz - grid.gridMinZ) / grid.blockSize);
56
+ if (bx < 0 || bx >= grid.numBlocksX || by < 0 || by >= grid.numBlocksY || bz < 0 || bz >= grid.numBlocksZ) {
57
+ return false;
58
+ }
59
+ const blockIdx = bx + by * grid.strideY + bz * grid.strideZ;
60
+ if (lookup.solidSet.has(blockIdx)) {
61
+ return true;
62
+ }
63
+ const mixedIdx = lookup.mixedMap.get(blockIdx);
64
+ if (mixedIdx === undefined) {
65
+ return false;
66
+ }
67
+ const lx = Math.floor((px - grid.gridMinX - bx * grid.blockSize) / grid.voxelResolution) & 3;
68
+ const ly = Math.floor((py - grid.gridMinY - by * grid.blockSize) / grid.voxelResolution) & 3;
69
+ const lz = Math.floor((pz - grid.gridMinZ - bz * grid.blockSize) / grid.voxelResolution) & 3;
70
+ const bitIdx = lx + ly * 4 + lz * 16;
71
+ const word = bitIdx < 32 ? lookup.masks[mixedIdx * 2] : lookup.masks[mixedIdx * 2 + 1];
72
+ return ((word >>> (bitIdx & 31)) & 1) !== 0;
73
+ }
74
+ function gaussianAtVoxelCenter(px, py, pz, qx, qy, qz, qw, sx, sy, sz, opacity, vx, vy, vz) {
75
+ const dx = vx - px;
76
+ const dy = vy - py;
77
+ const dz = vz - pz;
78
+ const iqx = -qx;
79
+ const iqy = -qy;
80
+ const iqz = -qz;
81
+ const tx = 2 * (iqy * dz - iqz * dy);
82
+ const ty = 2 * (iqz * dx - iqx * dz);
83
+ const tz = 2 * (iqx * dy - iqy * dx);
84
+ const lx = dx + qw * tx + (iqy * tz - iqz * ty);
85
+ const ly = dy + qw * ty + (iqz * tx - iqx * tz);
86
+ const lz = dz + qw * tz + (iqx * ty - iqy * tx);
87
+ const isx = sx > 1e-8 ? 1 / sx : 1e8;
88
+ const isy = sy > 1e-8 ? 1 / sy : 1e8;
89
+ const isz = sz > 1e-8 ? 1 / sz : 1e8;
90
+ const d2 = (lx * isx) ** 2 + (ly * isy) ** 2 + (lz * isz) ** 2;
91
+ return opacity * Math.exp(-0.5 * d2);
92
+ }
93
+ function gaussianContributesToVoxels(i, data, extents, grid, lookup, minContribution, minHits = 1) {
94
+ const table = data.table;
95
+ const px = table[ColIdx.x][i];
96
+ const py = table[ColIdx.y][i];
97
+ const pz = table[ColIdx.z][i];
98
+ const ex = extents[i * 3];
99
+ const ey = extents[i * 3 + 1];
100
+ const ez = extents[i * 3 + 2];
101
+ const minBx = Math.max(0, Math.floor((px - ex - grid.gridMinX) / grid.blockSize));
102
+ const maxBx = Math.min(grid.numBlocksX - 1, Math.floor((px + ex - grid.gridMinX) / grid.blockSize));
103
+ const minBy = Math.max(0, Math.floor((py - ey - grid.gridMinY) / grid.blockSize));
104
+ const maxBy = Math.min(grid.numBlocksY - 1, Math.floor((py + ey - grid.gridMinY) / grid.blockSize));
105
+ const minBz = Math.max(0, Math.floor((pz - ez - grid.gridMinZ) / grid.blockSize));
106
+ const maxBz = Math.min(grid.numBlocksZ - 1, Math.floor((pz + ez - grid.gridMinZ) / grid.blockSize));
107
+ const qx = table[ColIdx.qx][i];
108
+ const qy = table[ColIdx.qy][i];
109
+ const qz = table[ColIdx.qz][i];
110
+ const qw = table[ColIdx.qw][i];
111
+ const sx = table[ColIdx.sx][i];
112
+ const sy = table[ColIdx.sy][i];
113
+ const sz = table[ColIdx.sz][i];
114
+ const opacity = table[ColIdx.a][i];
115
+ let hits = 0;
116
+ for (let bz = minBz; bz <= maxBz; bz++) {
117
+ for (let by = minBy; by <= maxBy; by++) {
118
+ for (let bx = minBx; bx <= maxBx; bx++) {
119
+ const blockIdx = bx + by * grid.strideY + bz * grid.strideZ;
120
+ const solid = lookup.solidSet.has(blockIdx);
121
+ const mixedIdx = solid ? undefined : lookup.mixedMap.get(blockIdx);
122
+ if (!solid && mixedIdx === undefined) {
123
+ continue;
124
+ }
125
+ const lo = solid ? 0xffff_ffff : lookup.masks[mixedIdx * 2];
126
+ const hi = solid ? 0xffff_ffff : lookup.masks[mixedIdx * 2 + 1];
127
+ const blockMinX = grid.gridMinX + bx * grid.blockSize;
128
+ const blockMinY = grid.gridMinY + by * grid.blockSize;
129
+ const blockMinZ = grid.gridMinZ + bz * grid.blockSize;
130
+ for (let lz = 0; lz < 4; lz++) {
131
+ const word = lz < 2 ? lo : hi;
132
+ const zBitBase = (lz & 1) * 16;
133
+ const vz = blockMinZ + (lz + 0.5) * grid.voxelResolution;
134
+ for (let ly = 0; ly < 4; ly++) {
135
+ const bitBase = zBitBase + ly * 4;
136
+ const vy = blockMinY + (ly + 0.5) * grid.voxelResolution;
137
+ for (let lx = 0; lx < 4; lx++) {
138
+ if (((word >>> (bitBase + lx)) & 1) === 0) {
139
+ continue;
140
+ }
141
+ const vx = blockMinX + (lx + 0.5) * grid.voxelResolution;
142
+ if (gaussianAtVoxelCenter(px, py, pz, qx, qy, qz, qw, sx, sy, sz, opacity, vx, vy, vz) >=
143
+ minContribution) {
144
+ if (++hits >= minHits) {
145
+ return true;
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
153
+ }
154
+ return false;
155
+ }
156
+ function twoLevelBFS(blocked, blockSeeds, voxelSeeds, nx, ny, nz) {
157
+ const visited = new SparseVoxelGrid(nx, ny, nz);
158
+ const nbx = nx >> 2;
159
+ const nby = ny >> 2;
160
+ const bStride = nbx * nby;
161
+ const blockedTypes = blocked.types;
162
+ const blockedMasks = blocked.masks;
163
+ const visitedTypes = visited.types;
164
+ const visitedMasks = visited.masks;
165
+ let bqCap = 1 << 14;
166
+ let bq = new Uint32Array(bqCap);
167
+ let bqMask = bqCap - 1;
168
+ let bqHead = 0;
169
+ let bqTail = 0;
170
+ let bqSize = 0;
171
+ let vqCap = 1 << 14;
172
+ let vqX = new Uint32Array(vqCap);
173
+ let vqY = new Uint32Array(vqCap);
174
+ let vqZ = new Uint32Array(vqCap);
175
+ let vqMask = vqCap - 1;
176
+ let vqHead = 0;
177
+ let vqTail = 0;
178
+ let vqSize = 0;
179
+ function growBlockQueue() {
180
+ if (bqCap >= QUEUE_CAP_MAX) {
181
+ throw new Error(`filterCluster: block queue exceeded ${QUEUE_CAP_MAX}; try a coarser filterCluster.voxelResolution`);
182
+ }
183
+ const grown = new Uint32Array(bqCap * 2);
184
+ for (let i = 0; i < bqSize; i++) {
185
+ grown[i] = bq[(bqHead + i) & bqMask];
186
+ }
187
+ bq = grown;
188
+ bqCap = grown.length;
189
+ bqMask = bqCap - 1;
190
+ bqHead = 0;
191
+ bqTail = bqSize;
192
+ }
193
+ function growVoxelQueue() {
194
+ if (vqCap >= QUEUE_CAP_MAX) {
195
+ throw new Error(`filterCluster: voxel queue exceeded ${QUEUE_CAP_MAX}; try a coarser filterCluster.voxelResolution`);
196
+ }
197
+ const grownX = new Uint32Array(vqCap * 2);
198
+ const grownY = new Uint32Array(vqCap * 2);
199
+ const grownZ = new Uint32Array(vqCap * 2);
200
+ for (let i = 0; i < vqSize; i++) {
201
+ const src = (vqHead + i) & vqMask;
202
+ grownX[i] = vqX[src];
203
+ grownY[i] = vqY[src];
204
+ grownZ[i] = vqZ[src];
205
+ }
206
+ vqX = grownX;
207
+ vqY = grownY;
208
+ vqZ = grownZ;
209
+ vqCap = grownX.length;
210
+ vqMask = vqCap - 1;
211
+ vqHead = 0;
212
+ vqTail = vqSize;
213
+ }
214
+ function enqueueBlock(blockIdx) {
215
+ if (bqSize >= bqCap) {
216
+ growBlockQueue();
217
+ }
218
+ bq[bqTail] = blockIdx;
219
+ bqTail = (bqTail + 1) & bqMask;
220
+ bqSize++;
221
+ }
222
+ function enqueueVoxel(ix, iy, iz) {
223
+ if (vqSize >= vqCap) {
224
+ growVoxelQueue();
225
+ }
226
+ vqX[vqTail] = ix;
227
+ vqY[vqTail] = iy;
228
+ vqZ[vqTail] = iz;
229
+ vqTail = (vqTail + 1) & vqMask;
230
+ vqSize++;
231
+ }
232
+ function tryFillBlock(blockIdx) {
233
+ if (readBlockType(blockedTypes, blockIdx) !== BLOCK_EMPTY) {
234
+ return false;
235
+ }
236
+ if (readBlockType(visitedTypes, blockIdx) !== BLOCK_EMPTY) {
237
+ return false;
238
+ }
239
+ writeBlockType(visitedTypes, blockIdx, BLOCK_SOLID);
240
+ enqueueBlock(blockIdx);
241
+ return true;
242
+ }
243
+ function enqueueVisitedMaskVoxels(blockIdx, bx, by, bz, lo, hi) {
244
+ const baseX = bx << 2;
245
+ const baseY = by << 2;
246
+ const baseZ = bz << 2;
247
+ let bt = readBlockType(visitedTypes, blockIdx);
248
+ let slot = -1;
249
+ let oldLo = 0;
250
+ let oldHi = 0;
251
+ if (bt === BLOCK_SOLID) {
252
+ return;
253
+ }
254
+ if (bt === BLOCK_MIXED) {
255
+ slot = visitedMasks.slot(blockIdx);
256
+ oldLo = visitedMasks.lo[slot];
257
+ oldHi = visitedMasks.hi[slot];
258
+ }
259
+ else {
260
+ writeBlockType(visitedTypes, blockIdx, BLOCK_MIXED);
261
+ visitedMasks.set(blockIdx, 0, 0);
262
+ slot = visitedMasks.slot(blockIdx);
263
+ bt = BLOCK_MIXED;
264
+ }
265
+ const newLo = (lo & ~oldLo) >>> 0;
266
+ const newHi = (hi & ~oldHi) >>> 0;
267
+ if ((newLo | newHi) === 0) {
268
+ return;
269
+ }
270
+ visitedMasks.lo[slot] = (oldLo | newLo) >>> 0;
271
+ visitedMasks.hi[slot] = (oldHi | newHi) >>> 0;
272
+ if (visitedMasks.lo[slot] === SOLID_LO && visitedMasks.hi[slot] === SOLID_HI) {
273
+ visitedMasks.removeAt(slot);
274
+ writeBlockType(visitedTypes, blockIdx, BLOCK_SOLID);
275
+ }
276
+ let bits = newLo;
277
+ while (bits) {
278
+ const bit = 31 - Math.clz32(bits & -bits);
279
+ enqueueVoxel(baseX + (bit & 3), baseY + ((bit >> 2) & 3), baseZ + (bit >> 4));
280
+ bits &= bits - 1;
281
+ }
282
+ bits = newHi;
283
+ while (bits) {
284
+ const bit = 31 - Math.clz32(bits & -bits);
285
+ const bi = bit + 32;
286
+ enqueueVoxel(baseX + (bi & 3), baseY + ((bi >> 2) & 3), baseZ + (bi >> 4));
287
+ bits &= bits - 1;
288
+ }
289
+ }
290
+ function enqueueFaceVoxels(blockIdx, face, bx, by, bz) {
291
+ if (readBlockType(visitedTypes, blockIdx) === BLOCK_SOLID) {
292
+ return;
293
+ }
294
+ const blockedSlot = blockedMasks.slot(blockIdx);
295
+ const visitedSlot = readBlockType(visitedTypes, blockIdx) === BLOCK_MIXED ? visitedMasks.slot(blockIdx) : -1;
296
+ const [faceLo, faceHi] = FACE_MASKS[face];
297
+ const freeLo = (faceLo & ~blockedMasks.lo[blockedSlot] & ~(visitedSlot >= 0 ? visitedMasks.lo[visitedSlot] : 0)) >>> 0;
298
+ const freeHi = (faceHi & ~blockedMasks.hi[blockedSlot] & ~(visitedSlot >= 0 ? visitedMasks.hi[visitedSlot] : 0)) >>> 0;
299
+ enqueueVisitedMaskVoxels(blockIdx, bx, by, bz, freeLo, freeHi);
300
+ }
301
+ function processBlock(blockIdx) {
302
+ const bx = blockIdx % nbx;
303
+ const byBz = (blockIdx / nbx) | 0;
304
+ const by = byBz % nby;
305
+ const bz = (byBz / nby) | 0;
306
+ if (bx > 0) {
307
+ const n = blockIdx - 1;
308
+ const bt = readBlockType(blockedTypes, n);
309
+ if (bt === BLOCK_EMPTY) {
310
+ tryFillBlock(n);
311
+ }
312
+ else if (bt === BLOCK_MIXED) {
313
+ enqueueFaceVoxels(n, 1, bx - 1, by, bz);
314
+ }
315
+ }
316
+ if (bx < nbx - 1) {
317
+ const n = blockIdx + 1;
318
+ const bt = readBlockType(blockedTypes, n);
319
+ if (bt === BLOCK_EMPTY) {
320
+ tryFillBlock(n);
321
+ }
322
+ else if (bt === BLOCK_MIXED) {
323
+ enqueueFaceVoxels(n, 0, bx + 1, by, bz);
324
+ }
325
+ }
326
+ if (by > 0) {
327
+ const n = blockIdx - nbx;
328
+ const bt = readBlockType(blockedTypes, n);
329
+ if (bt === BLOCK_EMPTY) {
330
+ tryFillBlock(n);
331
+ }
332
+ else if (bt === BLOCK_MIXED) {
333
+ enqueueFaceVoxels(n, 3, bx, by - 1, bz);
334
+ }
335
+ }
336
+ if (by < nby - 1) {
337
+ const n = blockIdx + nbx;
338
+ const bt = readBlockType(blockedTypes, n);
339
+ if (bt === BLOCK_EMPTY) {
340
+ tryFillBlock(n);
341
+ }
342
+ else if (bt === BLOCK_MIXED) {
343
+ enqueueFaceVoxels(n, 2, bx, by + 1, bz);
344
+ }
345
+ }
346
+ if (bz > 0) {
347
+ const n = blockIdx - bStride;
348
+ const bt = readBlockType(blockedTypes, n);
349
+ if (bt === BLOCK_EMPTY) {
350
+ tryFillBlock(n);
351
+ }
352
+ else if (bt === BLOCK_MIXED) {
353
+ enqueueFaceVoxels(n, 5, bx, by, bz - 1);
354
+ }
355
+ }
356
+ if (bz < (nz >> 2) - 1) {
357
+ const n = blockIdx + bStride;
358
+ const bt = readBlockType(blockedTypes, n);
359
+ if (bt === BLOCK_EMPTY) {
360
+ tryFillBlock(n);
361
+ }
362
+ else if (bt === BLOCK_MIXED) {
363
+ enqueueFaceVoxels(n, 4, bx, by, bz + 1);
364
+ }
365
+ }
366
+ }
367
+ function processVoxel(ix, iy, iz) {
368
+ function visit(x, y, z) {
369
+ if (x < 0 || x >= nx || y < 0 || y >= ny || z < 0 || z >= nz) {
370
+ return;
371
+ }
372
+ const blockIdx = (x >> 2) + (y >> 2) * nbx + (z >> 2) * bStride;
373
+ const bt = readBlockType(blockedTypes, blockIdx);
374
+ if (bt === BLOCK_SOLID) {
375
+ return;
376
+ }
377
+ if (bt === BLOCK_EMPTY) {
378
+ tryFillBlock(blockIdx);
379
+ return;
380
+ }
381
+ const blockedSlot = blockedMasks.slot(blockIdx);
382
+ const bit = (x & 3) + ((y & 3) << 2) + ((z & 3) << 4);
383
+ const blockedWord = bit < 32 ? blockedMasks.lo[blockedSlot] : blockedMasks.hi[blockedSlot];
384
+ if (((blockedWord >>> (bit & 31)) & 1) !== 0) {
385
+ return;
386
+ }
387
+ const lo = bit < 32 ? (1 << bit) >>> 0 : 0;
388
+ const hi = bit >= 32 ? (1 << (bit - 32)) >>> 0 : 0;
389
+ enqueueVisitedMaskVoxels(blockIdx, x >> 2, y >> 2, z >> 2, lo, hi);
390
+ }
391
+ visit(ix - 1, iy, iz);
392
+ visit(ix + 1, iy, iz);
393
+ visit(ix, iy - 1, iz);
394
+ visit(ix, iy + 1, iz);
395
+ visit(ix, iy, iz - 1);
396
+ visit(ix, iy, iz + 1);
397
+ }
398
+ for (const blockIdx of blockSeeds) {
399
+ tryFillBlock(blockIdx);
400
+ }
401
+ for (const seed of voxelSeeds) {
402
+ const [lo, hi] = voxelMask(seed.ix, seed.iy, seed.iz);
403
+ enqueueVisitedMaskVoxels((seed.ix >> 2) + (seed.iy >> 2) * nbx + (seed.iz >> 2) * bStride, seed.ix >> 2, seed.iy >> 2, seed.iz >> 2, lo, hi);
404
+ }
405
+ while (bqSize > 0 || vqSize > 0) {
406
+ if (bqSize > 0) {
407
+ const blockIdx = bq[bqHead];
408
+ bqHead = (bqHead + 1) & bqMask;
409
+ bqSize--;
410
+ processBlock(blockIdx);
411
+ }
412
+ else {
413
+ const ix = vqX[vqHead];
414
+ const iy = vqY[vqHead];
415
+ const iz = vqZ[vqHead];
416
+ vqHead = (vqHead + 1) & vqMask;
417
+ vqSize--;
418
+ processVoxel(ix, iy, iz);
419
+ }
420
+ }
421
+ return visited;
422
+ }
423
+ function findClusterVoxelFlood(buffer, nx, ny, nz, seedIx, seedIy, seedIz) {
424
+ const occupied = SparseVoxelGrid.fromBuffer(buffer, nx, ny, nz);
425
+ const blocked = occupied.cropToInverted(0, 0, 0, occupied.nbx, occupied.nby, occupied.nbz);
426
+ if (blocked.getVoxel(seedIx, seedIy, seedIz)) {
427
+ const nearest = SparseVoxelGrid.findNearestFreeCell(blocked, seedIx, seedIy, seedIz, Math.max(nx, ny, nz));
428
+ if (!nearest) {
429
+ return null;
430
+ }
431
+ seedIx = nearest.ix;
432
+ seedIy = nearest.iy;
433
+ seedIz = nearest.iz;
434
+ logger.warn(`filterCluster seed is empty; using nearest occupied voxel (${seedIx}, ${seedIy}, ${seedIz})`);
435
+ }
436
+ const seedBlockIdx = (seedIx >> 2) + (seedIy >> 2) * blocked.nbx + (seedIz >> 2) * blocked.bStride;
437
+ const seedBlockType = readBlockType(blocked.types, seedBlockIdx);
438
+ return twoLevelBFS(blocked, seedBlockType === BLOCK_EMPTY ? [seedBlockIdx] : [], seedBlockType === BLOCK_EMPTY ? [] : [{ ix: seedIx, iy: seedIy, iz: seedIz }], nx, ny, nz);
439
+ }
440
+ function cloneRows(data, rows) {
441
+ const out = new SplatData().init(rows.length, data.shDegree);
442
+ for (let c = 0; c < data.table.length; c++) {
443
+ const src = data.table[c];
444
+ const dst = out.table[c];
445
+ for (let i = 0; i < rows.length; i++) {
446
+ dst[i] = src[rows[i]];
447
+ }
448
+ }
449
+ return out;
450
+ }
451
+ export async function filterCluster(data, options = {}, runtime = {}) {
452
+ const backend = runtime.backend ?? 'gpu';
453
+ const voxelResolution = options.voxelResolution ?? 1.0;
454
+ const opacityCutoff = options.opacityCutoff ?? 0.999;
455
+ const minContribution = options.minContribution ?? 0.1;
456
+ const seed = options.seed ?? { x: 0, y: 0, z: 0 };
457
+ const box = runtime.box;
458
+ if (!Number.isFinite(voxelResolution) || voxelResolution <= 0) {
459
+ throw new Error(`filterCluster: voxelResolution must be > 0, got ${voxelResolution}`);
460
+ }
461
+ if (!Number.isFinite(opacityCutoff) || opacityCutoff < 0 || opacityCutoff > 1) {
462
+ throw new Error(`filterCluster: opacityCutoff must be in [0, 1], got ${opacityCutoff}`);
463
+ }
464
+ if (!Number.isFinite(minContribution) || minContribution < 0) {
465
+ throw new Error(`filterCluster: minContribution must be >= 0, got ${minContribution}`);
466
+ }
467
+ if (data.counts === 0) {
468
+ return data;
469
+ }
470
+ const table = data.table;
471
+ const xCol = table[ColIdx.x];
472
+ const yCol = table[ColIdx.y];
473
+ const zCol = table[ColIdx.z];
474
+ const sxCol = table[ColIdx.sx];
475
+ const syCol = table[ColIdx.sy];
476
+ const szCol = table[ColIdx.sz];
477
+ const qxCol = table[ColIdx.qx];
478
+ const qyCol = table[ColIdx.qy];
479
+ const qzCol = table[ColIdx.qz];
480
+ const qwCol = table[ColIdx.qw];
481
+ const aCol = table[ColIdx.a];
482
+ const extents = new Float32Array(data.counts * 3);
483
+ const sceneBounds = {
484
+ min: { x: Infinity, y: Infinity, z: Infinity },
485
+ max: { x: -Infinity, y: -Infinity, z: -Infinity },
486
+ };
487
+ for (let i = 0; i < data.counts; i++) {
488
+ const e = extentsFromQuatScale(sxCol[i], syCol[i], szCol[i], qxCol[i], qyCol[i], qzCol[i], qwCol[i], aCol[i]);
489
+ extents[i * 3] = e.ex;
490
+ extents[i * 3 + 1] = e.ey;
491
+ extents[i * 3 + 2] = e.ez;
492
+ sceneBounds.min.x = Math.min(sceneBounds.min.x, xCol[i] - e.ex);
493
+ sceneBounds.min.y = Math.min(sceneBounds.min.y, yCol[i] - e.ey);
494
+ sceneBounds.min.z = Math.min(sceneBounds.min.z, zCol[i] - e.ez);
495
+ sceneBounds.max.x = Math.max(sceneBounds.max.x, xCol[i] + e.ex);
496
+ sceneBounds.max.y = Math.max(sceneBounds.max.y, yCol[i] + e.ey);
497
+ sceneBounds.max.z = Math.max(sceneBounds.max.z, zCol[i] + e.ez);
498
+ }
499
+ let gridBounds = alignGridBounds(sceneBounds, voxelResolution);
500
+ if (box) {
501
+ gridBounds = alignGridBounds({
502
+ min: {
503
+ x: Math.max(gridBounds.min.x, box.minCorner[0]),
504
+ y: Math.max(gridBounds.min.y, box.minCorner[1]),
505
+ z: Math.max(gridBounds.min.z, box.minCorner[2]),
506
+ },
507
+ max: {
508
+ x: Math.min(gridBounds.max.x, box.maxCorner[0]),
509
+ y: Math.min(gridBounds.max.y, box.maxCorner[1]),
510
+ z: Math.min(gridBounds.max.z, box.maxCorner[2]),
511
+ },
512
+ }, voxelResolution);
513
+ if (gridBounds.min.x >= gridBounds.max.x ||
514
+ gridBounds.min.y >= gridBounds.max.y ||
515
+ gridBounds.min.z >= gridBounds.max.z) {
516
+ logger.warn('filterCluster: box does not overlap scene bounds, returning empty data');
517
+ return cloneRows(data, []);
518
+ }
519
+ }
520
+ const maxGridExtent = 4096 * voxelResolution;
521
+ function clampAxis(min, max, seedValue) {
522
+ if (max - min <= maxGridExtent) {
523
+ return { min, max };
524
+ }
525
+ const half = maxGridExtent * 0.5;
526
+ const center = Math.max(min + half, Math.min(seedValue, max - half));
527
+ return { min: center - half, max: center + half };
528
+ }
529
+ const cx = clampAxis(gridBounds.min.x, gridBounds.max.x, seed.x);
530
+ const cy = clampAxis(gridBounds.min.y, gridBounds.max.y, seed.y);
531
+ const cz = clampAxis(gridBounds.min.z, gridBounds.max.z, seed.z);
532
+ gridBounds = {
533
+ min: { x: cx.min, y: cy.min, z: cz.min },
534
+ max: { x: cx.max, y: cy.max, z: cz.max },
535
+ };
536
+ const blockSize = 4 * voxelResolution;
537
+ const nbx = Math.round((gridBounds.max.x - gridBounds.min.x) / blockSize);
538
+ const nby = Math.round((gridBounds.max.y - gridBounds.min.y) / blockSize);
539
+ const nbz = Math.round((gridBounds.max.z - gridBounds.min.z) / blockSize);
540
+ const nx = nbx * 4;
541
+ const ny = nby * 4;
542
+ const nz = nbz * 4;
543
+ const cpuOptions = { alphaThreshold: 0 };
544
+ if (runtime.cpuWorkerCount !== undefined) {
545
+ cpuOptions.workerCount = runtime.cpuWorkerCount;
546
+ }
547
+ logger.info(`filterCluster: backend=${backend}, resolution=${voxelResolution}, opacityCutoff=${opacityCutoff}, ` +
548
+ `minContribution=${minContribution}, grid=${nx}x${ny}x${nz}` +
549
+ (backend === 'cpu'
550
+ ? `, cpuWorkerCount=${cpuOptions.workerCount && cpuOptions.workerCount > 0 ? cpuOptions.workerCount : 'auto'}`
551
+ : ''));
552
+ let buffer;
553
+ if (backend === 'gpu') {
554
+ try {
555
+ buffer = await gpuVoxelize(xCol, yCol, zCol, sxCol, syCol, szCol, qxCol, qyCol, qzCol, qwCol, aCol, extents, gridBounds, voxelResolution, opacityCutoff);
556
+ }
557
+ catch (e) {
558
+ if (e instanceof Error) {
559
+ logger.error(`filterCluster GPU backend failed: ${e.message}`);
560
+ if (e.stack) {
561
+ logger.error(`filterCluster GPU stack: ${e.stack}`);
562
+ }
563
+ }
564
+ else {
565
+ logger.error(`filterCluster GPU backend failed: ${String(e)}`);
566
+ }
567
+ logger.error('filterCluster GPU backend failed, fallback to CPU.');
568
+ buffer = await cpuVoxelize(xCol, yCol, zCol, sxCol, syCol, szCol, qxCol, qyCol, qzCol, qwCol, aCol, extents, gridBounds, voxelResolution, opacityCutoff, cpuOptions);
569
+ }
570
+ }
571
+ else {
572
+ buffer = await cpuVoxelize(xCol, yCol, zCol, sxCol, syCol, szCol, qxCol, qyCol, qzCol, qwCol, aCol, extents, gridBounds, voxelResolution, opacityCutoff, cpuOptions);
573
+ }
574
+ if (buffer.count === 0) {
575
+ logger.warn('filterCluster: no occupied voxels, returning empty data');
576
+ return cloneRows(data, []);
577
+ }
578
+ const seedIx = Math.max(0, Math.min(Math.floor((seed.x - gridBounds.min.x) / voxelResolution), nx - 1));
579
+ const seedIy = Math.max(0, Math.min(Math.floor((seed.y - gridBounds.min.y) / voxelResolution), ny - 1));
580
+ const seedIz = Math.max(0, Math.min(Math.floor((seed.z - gridBounds.min.z) / voxelResolution), nz - 1));
581
+ const visited = findClusterVoxelFlood(buffer, nx, ny, nz, seedIx, seedIy, seedIz);
582
+ if (!visited) {
583
+ logger.warn('filterCluster: no occupied voxel near seed, returning empty data');
584
+ return cloneRows(data, []);
585
+ }
586
+ const visitedBuffer = visited.toBuffer(0, 0, 0, nbx, nby, nbz);
587
+ logger.info(`filterCluster: cluster blocks=${visitedBuffer.count}, occupied blocks=${buffer.count}`);
588
+ if (visitedBuffer.count === buffer.count) {
589
+ logger.info('filterCluster: all blocks are connected, no filtering needed');
590
+ return data;
591
+ }
592
+ const lookup = buildBlockLookup(visitedBuffer);
593
+ const grid = {
594
+ gridMinX: gridBounds.min.x,
595
+ gridMinY: gridBounds.min.y,
596
+ gridMinZ: gridBounds.min.z,
597
+ blockSize,
598
+ voxelResolution,
599
+ numBlocksX: nbx,
600
+ numBlocksY: nby,
601
+ numBlocksZ: nbz,
602
+ strideY: nbx,
603
+ strideZ: nbx * nby,
604
+ };
605
+ const keep = [];
606
+ const largeThreshold = 2 * voxelResolution;
607
+ const minOccupancyRatio = 0.1;
608
+ const invVoxel = 1 / voxelResolution;
609
+ for (let i = 0; i < data.counts; i++) {
610
+ if (isCenterInOccupiedVoxel(xCol[i], yCol[i], zCol[i], grid, lookup)) {
611
+ keep.push(i);
612
+ continue;
613
+ }
614
+ const ex = extents[i * 3];
615
+ const ey = extents[i * 3 + 1];
616
+ const ez = extents[i * 3 + 2];
617
+ let minHits = 1;
618
+ if (Math.max(ex, ey, ez) * 2 > largeThreshold) {
619
+ const aabbVoxels = 2 * ex * invVoxel * (2 * ey * invVoxel) * (2 * ez * invVoxel);
620
+ minHits = Math.max(1, Math.ceil(aabbVoxels * minOccupancyRatio));
621
+ }
622
+ if (gaussianContributesToVoxels(i, data, extents, grid, lookup, minContribution, minHits)) {
623
+ keep.push(i);
624
+ }
625
+ }
626
+ logger.info(`filterCluster: kept ${keep.length} / ${data.counts} gaussians`);
627
+ return keep.length === data.counts ? data : cloneRows(data, keep);
628
+ }
@@ -1,2 +1,2 @@
1
- import { SparseVoxelGrid } from './common.js';
2
- export declare const gpuDilate3: (src: SparseVoxelGrid, halfExtentXZ: number, halfExtentY: number) => Promise<SparseVoxelGrid>;
1
+ import { SparseVoxelGrid } from './common.js';
2
+ export declare function gpuDilate3(src: SparseVoxelGrid, halfExtentXZ: number, halfExtentY: number): Promise<SparseVoxelGrid>;