3dtiles-inspector 0.2.13 → 0.2.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "3dtiles-inspector",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "Inspect, align, and save local 3D Tiles root transforms in an interactive browser session.",
5
5
  "author": "William Liu <lyz15972107087@gmail.com>",
6
6
  "license": "Apache-2.0",
@@ -55,8 +55,8 @@
55
55
  },
56
56
  "devDependencies": {
57
57
  "@sparkjsdev/spark": "2.1.0",
58
- "3d-tiles-renderer": "0.4.27",
59
- "3d-tiles-rendererjs-3dgs-plugin": "0.1.11",
58
+ "3d-tiles-renderer": "0.4.28",
59
+ "3d-tiles-rendererjs-3dgs-plugin": "0.1.14",
60
60
  "esbuild": "^0.25.11"
61
61
  },
62
62
  "dependencies": {
@@ -2,6 +2,23 @@ const { InspectorError } = require('../../errors');
2
2
  const { normalizeMatrix4Array } = require('./normalize');
3
3
  const { getNodeLocalMatrix } = require('./gltfResource');
4
4
 
5
+ const KHR_GAUSSIAN_SPLATTING_EXTENSION = 'KHR_gaussian_splatting';
6
+ const KHR_GAUSSIAN_SPLATTING_SPZ_EXTENSION =
7
+ 'KHR_gaussian_splatting_compression_spz_2';
8
+ const EXT_SPLAT_OPACITY_EXTENSION = 'EXT_splat_opacity';
9
+
10
+ function getGaussianSplattingExtensions(primitive) {
11
+ return primitive?.extensions?.[KHR_GAUSSIAN_SPLATTING_EXTENSION]?.extensions;
12
+ }
13
+
14
+ function getGaussianSplatOpacityExtension(primitive) {
15
+ const extensions = getGaussianSplattingExtensions(primitive);
16
+ return (
17
+ extensions?.[EXT_SPLAT_OPACITY_EXTENSION] ??
18
+ primitive?.extensions?.[EXT_SPLAT_OPACITY_EXTENSION]
19
+ );
20
+ }
21
+
5
22
  function getTileLocalTransform(THREE, tile) {
6
23
  const matrix = new THREE.Matrix4();
7
24
  if (Array.isArray(tile?.transform)) {
@@ -37,10 +54,22 @@ function getRootUpRotationMatrix(THREE, tileset) {
37
54
  }
38
55
 
39
56
  function primitiveHasGaussianSpzExtension(primitive) {
40
- return (
41
- primitive?.extensions?.KHR_gaussian_splatting?.extensions
42
- ?.KHR_gaussian_splatting_compression_spz_2 != null
43
- );
57
+ const extensions = getGaussianSplattingExtensions(primitive);
58
+ return extensions?.[KHR_GAUSSIAN_SPLATTING_SPZ_EXTENSION] != null;
59
+ }
60
+
61
+ function getGaussianSplatOpacityAccessorIndex(primitive) {
62
+ const opacityAccessor = getGaussianSplatOpacityExtension(primitive)
63
+ ?.opacityAccessor;
64
+ if (opacityAccessor == null) {
65
+ return null;
66
+ }
67
+ if (!Number.isInteger(opacityAccessor) || opacityAccessor < 0) {
68
+ throw new InspectorError(
69
+ 'EXT_splat_opacity opacityAccessor must be a non-negative integer.',
70
+ );
71
+ }
72
+ return opacityAccessor;
44
73
  }
45
74
 
46
75
  function collectGaussianPrimitiveDescriptors(
@@ -87,12 +116,12 @@ function collectGaussianPrimitiveDescriptors(
87
116
  const mesh = meshes[node.mesh];
88
117
  if (mesh && Array.isArray(mesh.primitives)) {
89
118
  mesh.primitives.forEach((primitive, primitiveIndex) => {
119
+ const extensions = getGaussianSplattingExtensions(primitive);
90
120
  const gaussianExtension =
91
- primitive?.extensions?.KHR_gaussian_splatting?.extensions
92
- ?.KHR_gaussian_splatting_compression_spz_2;
121
+ extensions?.[KHR_GAUSSIAN_SPLATTING_SPZ_EXTENSION];
93
122
  const bufferView = gaussianExtension?.bufferView;
94
123
  if (bufferView == null) {
95
- if (primitive?.extensions?.KHR_gaussian_splatting) {
124
+ if (primitive?.extensions?.[KHR_GAUSSIAN_SPLATTING_EXTENSION]) {
96
125
  throw new InspectorError(
97
126
  'Only KHR_gaussian_splatting_compression_spz_2 Gaussian primitives are supported for crop deletion.',
98
127
  );
@@ -288,6 +317,7 @@ function updateGaussianPrimitiveAccessorCounts(resource, descriptors, count) {
288
317
 
289
318
  module.exports = {
290
319
  collectGaussianPrimitiveDescriptors,
320
+ getGaussianSplatOpacityAccessorIndex,
291
321
  getRootUpRotationMatrix,
292
322
  getTileLocalTransform,
293
323
  getTileWorldTransform,
@@ -16,6 +16,7 @@ const {
16
16
  } = require('./gltfResource');
17
17
  const {
18
18
  collectGaussianPrimitiveDescriptors,
19
+ getGaussianSplatOpacityAccessorIndex,
19
20
  getTileLocalTransform,
20
21
  hasNonGaussianScenePrimitives,
21
22
  hasScenePrimitives,
@@ -24,6 +25,10 @@ const {
24
25
  } = require('./gaussianPrimitives');
25
26
  const { SPLAT_CROP_WORKER_COUNT } = require('./workerPool');
26
27
 
28
+ const ACCESSOR_COMPONENT_TYPE_FLOAT = 5126;
29
+ const ACCESSOR_TYPE_SCALAR = 'SCALAR';
30
+ const FLOAT32_BYTE_LENGTH = 4;
31
+
27
32
  class AsyncLimiter {
28
33
  constructor(limit) {
29
34
  this.active = 0;
@@ -618,6 +623,154 @@ function markSplatResourceProgress(context, resourcePath) {
618
623
  });
619
624
  }
620
625
 
626
+ function getPrimitiveForDescriptor(resource, descriptor) {
627
+ if (
628
+ !Number.isInteger(descriptor.meshIndex) ||
629
+ !Number.isInteger(descriptor.primitiveIndex)
630
+ ) {
631
+ return null;
632
+ }
633
+ return resource.json.meshes?.[descriptor.meshIndex]?.primitives?.[
634
+ descriptor.primitiveIndex
635
+ ];
636
+ }
637
+
638
+ function getAccessorDefinition(resource, accessorIndex, label) {
639
+ const accessor = resource.json.accessors?.[accessorIndex];
640
+ if (!accessor || typeof accessor !== 'object') {
641
+ throw new InspectorError(`${label} references missing accessor ${accessorIndex}.`);
642
+ }
643
+ if (accessor.componentType !== ACCESSOR_COMPONENT_TYPE_FLOAT) {
644
+ throw new InspectorError(`${label} must use FLOAT componentType.`);
645
+ }
646
+ if (accessor.type !== ACCESSOR_TYPE_SCALAR) {
647
+ throw new InspectorError(`${label} must use SCALAR type.`);
648
+ }
649
+ if (accessor.sparse) {
650
+ throw new InspectorError(`${label} sparse accessors are not supported.`);
651
+ }
652
+ if (!Number.isInteger(accessor.bufferView)) {
653
+ throw new InspectorError(`${label} must reference a bufferView.`);
654
+ }
655
+ if (!Number.isInteger(accessor.count) || accessor.count < 0) {
656
+ throw new InspectorError(`${label} count must be a non-negative integer.`);
657
+ }
658
+
659
+ const byteOffset = Number(accessor.byteOffset ?? 0);
660
+ if (!Number.isInteger(byteOffset) || byteOffset < 0) {
661
+ throw new InspectorError(`${label} byteOffset must be a non-negative integer.`);
662
+ }
663
+ const bufferView = resource.json.bufferViews?.[accessor.bufferView];
664
+ if (!bufferView || typeof bufferView !== 'object') {
665
+ throw new InspectorError(`${label} references missing bufferView.`);
666
+ }
667
+ const byteStride = Number(bufferView.byteStride ?? FLOAT32_BYTE_LENGTH);
668
+ if (!Number.isInteger(byteStride) || byteStride < FLOAT32_BYTE_LENGTH) {
669
+ throw new InspectorError(
670
+ `${label} byteStride must be at least ${FLOAT32_BYTE_LENGTH}.`,
671
+ );
672
+ }
673
+
674
+ return { accessor, bufferView, byteOffset, byteStride };
675
+ }
676
+
677
+ function makeFloat32ScalarAccessorSubsetReplacement(
678
+ resource,
679
+ accessorIndex,
680
+ survivors,
681
+ ) {
682
+ const label = `EXT_splat_opacity accessor ${accessorIndex}`;
683
+ const { accessor, bufferView, byteOffset, byteStride } = getAccessorDefinition(
684
+ resource,
685
+ accessorIndex,
686
+ label,
687
+ );
688
+ const slice = getBufferViewSlice(resource, accessor.bufferView);
689
+ const sourceLength =
690
+ accessor.count === 0
691
+ ? 0
692
+ : (accessor.count - 1) * byteStride + FLOAT32_BYTE_LENGTH;
693
+ const sourceEnd = byteOffset + sourceLength;
694
+
695
+ if (sourceEnd > slice.bytes.length) {
696
+ throw new InspectorError(`${label} exceeds its bufferView.`);
697
+ }
698
+
699
+ const out = Buffer.allocUnsafe(survivors.length * FLOAT32_BYTE_LENGTH);
700
+ let min = Infinity;
701
+ let max = -Infinity;
702
+
703
+ for (let index = 0; index < survivors.length; index++) {
704
+ const sourceIndex = survivors[index];
705
+ if (
706
+ !Number.isInteger(sourceIndex) ||
707
+ sourceIndex < 0 ||
708
+ sourceIndex >= accessor.count
709
+ ) {
710
+ throw new InspectorError(`${label} survivor index is out of range.`);
711
+ }
712
+
713
+ const value = slice.bytes.readFloatLE(
714
+ byteOffset + sourceIndex * byteStride,
715
+ );
716
+ out.writeFloatLE(value, index * FLOAT32_BYTE_LENGTH);
717
+ if (value < min) min = value;
718
+ if (value > max) max = value;
719
+ }
720
+
721
+ accessor.count = survivors.length;
722
+ delete accessor.byteOffset;
723
+ delete bufferView.byteStride;
724
+ if (survivors.length > 0) {
725
+ accessor.min = [min];
726
+ accessor.max = [max];
727
+ }
728
+
729
+ return {
730
+ bufferIndex: slice.bufferIndex,
731
+ bufferViewIndex: accessor.bufferView,
732
+ bytes: out,
733
+ end: slice.end,
734
+ start: slice.start,
735
+ };
736
+ }
737
+
738
+ function addGaussianSplatOpacityAccessorReplacements(
739
+ resource,
740
+ descriptors,
741
+ survivors,
742
+ replacementsByBuffer,
743
+ ) {
744
+ if (!(survivors instanceof Uint32Array) || survivors.length === 0) {
745
+ return 0;
746
+ }
747
+
748
+ const opacityAccessors = new Set();
749
+ descriptors.forEach((descriptor) => {
750
+ const primitive = getPrimitiveForDescriptor(resource, descriptor);
751
+ const accessorIndex = getGaussianSplatOpacityAccessorIndex(primitive);
752
+ if (accessorIndex !== null) {
753
+ opacityAccessors.add(accessorIndex);
754
+ }
755
+ });
756
+
757
+ let replacementCount = 0;
758
+ opacityAccessors.forEach((accessorIndex) => {
759
+ const replacement = makeFloat32ScalarAccessorSubsetReplacement(
760
+ resource,
761
+ accessorIndex,
762
+ survivors,
763
+ );
764
+ if (!replacementsByBuffer.has(replacement.bufferIndex)) {
765
+ replacementsByBuffer.set(replacement.bufferIndex, []);
766
+ }
767
+ addReplacement(replacementsByBuffer.get(replacement.bufferIndex), replacement);
768
+ replacementCount += 1;
769
+ });
770
+
771
+ return replacementCount;
772
+ }
773
+
621
774
  async function processGltfResource({
622
775
  THREE,
623
776
  filePath,
@@ -679,6 +832,15 @@ async function processGltfResource({
679
832
  continue;
680
833
  }
681
834
  if (Number.isInteger(rewrite.survivorCount)) {
835
+ if (rewrite.deleted > 0) {
836
+ modified =
837
+ addGaussianSplatOpacityAccessorReplacements(
838
+ resource,
839
+ viewDescriptors,
840
+ rewrite.survivors,
841
+ replacementsByBuffer,
842
+ ) > 0 || modified;
843
+ }
682
844
  modified =
683
845
  updateGaussianPrimitiveAccessorCounts(
684
846
  resource,
@@ -345,6 +345,7 @@ function rewriteSpzBytes({
345
345
  deleted,
346
346
  empty: false,
347
347
  splatCount: parsed.sourceCount,
348
+ survivors,
348
349
  survivorCount: survivors.length,
349
350
  };
350
351
  }
@@ -373,6 +374,11 @@ parentPort.on('message', async (message) => {
373
374
  bytes = getTransferableBytes(result.bytes);
374
375
  transferList.push(bytes.buffer);
375
376
  }
377
+ let survivors = null;
378
+ if (result.survivors) {
379
+ survivors = result.survivors;
380
+ transferList.push(survivors.buffer);
381
+ }
376
382
 
377
383
  parentPort.postMessage(
378
384
  {
@@ -383,6 +389,7 @@ parentPort.on('message', async (message) => {
383
389
  deleted: result.deleted,
384
390
  empty: result.empty,
385
391
  splatCount: result.splatCount,
392
+ survivors,
386
393
  survivorCount: result.survivorCount,
387
394
  },
388
395
  },
@@ -110,6 +110,8 @@ class SplatCropWorkerPool {
110
110
  deleted: Number(result.deleted || 0),
111
111
  empty: !!result.empty,
112
112
  splatCount: Number(result.splatCount || 0),
113
+ survivors:
114
+ result.survivors instanceof Uint32Array ? result.survivors : null,
113
115
  survivorCount: Number(result.survivorCount || 0),
114
116
  });
115
117
  }