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/CHANGELOG.md +11 -0
- package/dist/inspector-assets/viewer/app.js +3988 -764
- package/package.json +3 -3
- package/src/server/splatCrop/gaussianPrimitives.js +37 -7
- package/src/server/splatCrop/traversal.js +162 -0
- package/src/server/splatCrop/worker.js +7 -0
- package/src/server/splatCrop/workerPool.js +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "3dtiles-inspector",
|
|
3
|
-
"version": "0.2.
|
|
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.
|
|
59
|
-
"3d-tiles-rendererjs-3dgs-plugin": "0.1.
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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?.
|
|
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
|
}
|