@onerjs/core 8.54.4 → 8.54.5
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/Engines/WebGPU/Extensions/engine.cubeTexture.pure.js +1 -1
- package/Engines/WebGPU/Extensions/engine.cubeTexture.pure.js.map +1 -1
- package/Engines/WebGPU/Extensions/engine.rawTexture.pure.js +2 -2
- package/Engines/WebGPU/Extensions/engine.rawTexture.pure.js.map +1 -1
- package/Engines/WebGPU/webgpuCacheRenderPipeline.d.ts +1 -0
- package/Engines/WebGPU/webgpuCacheRenderPipeline.js +18 -4
- package/Engines/WebGPU/webgpuCacheRenderPipeline.js.map +1 -1
- package/Engines/WebGPU/webgpuCacheTextureView.d.ts +13 -0
- package/Engines/WebGPU/webgpuCacheTextureView.js +32 -0
- package/Engines/WebGPU/webgpuCacheTextureView.js.map +1 -0
- package/Engines/WebGPU/webgpuRenderTargetWrapper.d.ts +10 -0
- package/Engines/WebGPU/webgpuRenderTargetWrapper.js +10 -0
- package/Engines/WebGPU/webgpuRenderTargetWrapper.js.map +1 -1
- package/Engines/WebGPU/webgpuShaderProcessorsWGSL.pure.js +57 -4
- package/Engines/WebGPU/webgpuShaderProcessorsWGSL.pure.js.map +1 -1
- package/Engines/WebGPU/webgpuTextureManager.d.ts +1 -1
- package/Engines/WebGPU/webgpuTextureManager.js +4 -2
- package/Engines/WebGPU/webgpuTextureManager.js.map +1 -1
- package/Engines/abstractEngine.pure.js +2 -2
- package/Engines/abstractEngine.pure.js.map +1 -1
- package/Engines/thinNativeEngine.pure.d.ts +5 -0
- package/Engines/thinNativeEngine.pure.js +29 -6
- package/Engines/thinNativeEngine.pure.js.map +1 -1
- package/Engines/webgpuEngine.pure.d.ts +1 -0
- package/Engines/webgpuEngine.pure.js +21 -8
- package/Engines/webgpuEngine.pure.js.map +1 -1
- package/FlowGraph/Blocks/Data/Math/flowGraphMathBlocks.pure.d.ts +95 -1
- package/FlowGraph/Blocks/Data/Math/flowGraphMathBlocks.pure.js +169 -4
- package/FlowGraph/Blocks/Data/Math/flowGraphMathBlocks.pure.js.map +1 -1
- package/FlowGraph/Blocks/Data/Math/flowGraphMathCombineExtractBlocks.pure.d.ts +21 -6
- package/FlowGraph/Blocks/Data/Math/flowGraphMathCombineExtractBlocks.pure.js +28 -26
- package/FlowGraph/Blocks/Data/Math/flowGraphMathCombineExtractBlocks.pure.js.map +1 -1
- package/FlowGraph/Blocks/Data/Math/flowGraphMatrixMathBlocks.pure.d.ts +30 -2
- package/FlowGraph/Blocks/Data/Math/flowGraphMatrixMathBlocks.pure.js +111 -29
- package/FlowGraph/Blocks/Data/Math/flowGraphMatrixMathBlocks.pure.js.map +1 -1
- package/FlowGraph/Blocks/Data/Math/flowGraphVectorMathBlocks.pure.d.ts +56 -2
- package/FlowGraph/Blocks/Data/Math/flowGraphVectorMathBlocks.pure.js +99 -14
- package/FlowGraph/Blocks/Data/Math/flowGraphVectorMathBlocks.pure.js.map +1 -1
- package/FlowGraph/Blocks/Data/Transformers/flowGraphJsonPointerParserBlock.pure.js +10 -2
- package/FlowGraph/Blocks/Data/Transformers/flowGraphJsonPointerParserBlock.pure.js.map +1 -1
- package/FlowGraph/Blocks/Data/Utils/flowGraphArrayIndexBlock.pure.js +21 -1
- package/FlowGraph/Blocks/Data/Utils/flowGraphArrayIndexBlock.pure.js.map +1 -1
- package/FlowGraph/Blocks/Data/flowGraphCachedOperationBlock.d.ts +18 -0
- package/FlowGraph/Blocks/Data/flowGraphCachedOperationBlock.js +29 -2
- package/FlowGraph/Blocks/Data/flowGraphCachedOperationBlock.js.map +1 -1
- package/FlowGraph/Blocks/Event/flowGraphReceiveCustomEventBlock.pure.d.ts +15 -0
- package/FlowGraph/Blocks/Event/flowGraphReceiveCustomEventBlock.pure.js +44 -7
- package/FlowGraph/Blocks/Event/flowGraphReceiveCustomEventBlock.pure.js.map +1 -1
- package/FlowGraph/Blocks/Event/flowGraphSceneReadyEventBlock.pure.d.ts +9 -0
- package/FlowGraph/Blocks/Event/flowGraphSceneReadyEventBlock.pure.js +9 -1
- package/FlowGraph/Blocks/Event/flowGraphSceneReadyEventBlock.pure.js.map +1 -1
- package/FlowGraph/Blocks/Event/flowGraphSceneTickEventBlock.pure.d.ts +7 -0
- package/FlowGraph/Blocks/Event/flowGraphSceneTickEventBlock.pure.js +8 -1
- package/FlowGraph/Blocks/Event/flowGraphSceneTickEventBlock.pure.js.map +1 -1
- package/FlowGraph/Blocks/Event/flowGraphStopEventPropagationBlock.d.ts +5 -0
- package/FlowGraph/Blocks/Event/flowGraphStopEventPropagationBlock.js +8 -0
- package/FlowGraph/Blocks/Event/flowGraphStopEventPropagationBlock.js.map +1 -0
- package/FlowGraph/Blocks/Event/flowGraphStopEventPropagationBlock.pure.d.ts +37 -0
- package/FlowGraph/Blocks/Event/flowGraphStopEventPropagationBlock.pure.js +44 -0
- package/FlowGraph/Blocks/Event/flowGraphStopEventPropagationBlock.pure.js.map +1 -0
- package/FlowGraph/Blocks/Event/index.d.ts +1 -0
- package/FlowGraph/Blocks/Event/index.js +1 -0
- package/FlowGraph/Blocks/Event/index.js.map +1 -1
- package/FlowGraph/Blocks/Event/pure.d.ts +1 -0
- package/FlowGraph/Blocks/Event/pure.js +1 -0
- package/FlowGraph/Blocks/Event/pure.js.map +1 -1
- package/FlowGraph/Blocks/Execution/Animation/flowGraphBezierCurveEasingBlock.pure.d.ts +4 -1
- package/FlowGraph/Blocks/Execution/Animation/flowGraphBezierCurveEasingBlock.pure.js +67 -2
- package/FlowGraph/Blocks/Execution/Animation/flowGraphBezierCurveEasingBlock.pure.js.map +1 -1
- package/FlowGraph/Blocks/Execution/Animation/flowGraphPlayAnimationBlock.pure.d.ts +13 -0
- package/FlowGraph/Blocks/Execution/Animation/flowGraphPlayAnimationBlock.pure.js +102 -7
- package/FlowGraph/Blocks/Execution/Animation/flowGraphPlayAnimationBlock.pure.js.map +1 -1
- package/FlowGraph/Blocks/Execution/Animation/flowGraphStopAnimationBlock.pure.js +5 -1
- package/FlowGraph/Blocks/Execution/Animation/flowGraphStopAnimationBlock.pure.js.map +1 -1
- package/FlowGraph/Blocks/Execution/ControlFlow/flowGraphCancelDelayBlock.pure.js +5 -1
- package/FlowGraph/Blocks/Execution/ControlFlow/flowGraphCancelDelayBlock.pure.js.map +1 -1
- package/FlowGraph/Blocks/Execution/ControlFlow/flowGraphForLoopBlock.pure.d.ts +11 -2
- package/FlowGraph/Blocks/Execution/ControlFlow/flowGraphForLoopBlock.pure.js +21 -3
- package/FlowGraph/Blocks/Execution/ControlFlow/flowGraphForLoopBlock.pure.js.map +1 -1
- package/FlowGraph/Blocks/Execution/ControlFlow/flowGraphSetDelayBlock.pure.js +25 -4
- package/FlowGraph/Blocks/Execution/ControlFlow/flowGraphSetDelayBlock.pure.js.map +1 -1
- package/FlowGraph/Blocks/flowGraphBlockFactory.js +18 -0
- package/FlowGraph/Blocks/flowGraphBlockFactory.js.map +1 -1
- package/FlowGraph/Blocks/flowGraphBlockNames.d.ts +9 -0
- package/FlowGraph/Blocks/flowGraphBlockNames.js +9 -0
- package/FlowGraph/Blocks/flowGraphBlockNames.js.map +1 -1
- package/FlowGraph/CustomTypes/flowGraphMatrix.d.ts +17 -3
- package/FlowGraph/CustomTypes/flowGraphMatrix.js +27 -21
- package/FlowGraph/CustomTypes/flowGraphMatrix.js.map +1 -1
- package/FlowGraph/flowGraphContext.d.ts +23 -0
- package/FlowGraph/flowGraphContext.js +29 -0
- package/FlowGraph/flowGraphContext.js.map +1 -1
- package/FlowGraph/flowGraphCoordinator.d.ts +58 -1
- package/FlowGraph/flowGraphCoordinator.js +72 -0
- package/FlowGraph/flowGraphCoordinator.js.map +1 -1
- package/FlowGraph/flowGraphDelayRegistry.d.ts +31 -0
- package/FlowGraph/flowGraphDelayRegistry.js +50 -0
- package/FlowGraph/flowGraphDelayRegistry.js.map +1 -0
- package/FlowGraph/flowGraphHostResolver.d.ts +69 -0
- package/FlowGraph/flowGraphHostResolver.js +25 -0
- package/FlowGraph/flowGraphHostResolver.js.map +1 -0
- package/FlowGraph/flowGraphMath.d.ts +48 -1
- package/FlowGraph/flowGraphMath.js +169 -4
- package/FlowGraph/flowGraphMath.js.map +1 -1
- package/FlowGraph/flowGraphPathConverterComponent.d.ts +14 -3
- package/FlowGraph/flowGraphPathConverterComponent.js +110 -18
- package/FlowGraph/flowGraphPathConverterComponent.js.map +1 -1
- package/FlowGraph/flowGraphSignalConnection.pure.js +7 -1
- package/FlowGraph/flowGraphSignalConnection.pure.js.map +1 -1
- package/FlowGraph/index.d.ts +2 -0
- package/FlowGraph/index.js +2 -0
- package/FlowGraph/index.js.map +1 -1
- package/FlowGraph/pure.d.ts +2 -0
- package/FlowGraph/pure.js +2 -0
- package/FlowGraph/pure.js.map +1 -1
- package/Maths/math.vector.pure.d.ts +10 -0
- package/Maths/math.vector.pure.js +13 -0
- package/Maths/math.vector.pure.js.map +1 -1
- package/Meshes/GaussianSplatting/gaussianSplattingMesh.pure.d.ts +216 -1
- package/Meshes/GaussianSplatting/gaussianSplattingMesh.pure.js +714 -67
- package/Meshes/GaussianSplatting/gaussianSplattingMesh.pure.js.map +1 -1
- package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.pure.d.ts +81 -4
- package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.pure.js +466 -52
- package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.pure.js.map +1 -1
- package/Meshes/GaussianSplatting/gaussianSplattingPartProxyMesh.pure.d.ts +19 -0
- package/Meshes/GaussianSplatting/gaussianSplattingPartProxyMesh.pure.js +24 -0
- package/Meshes/GaussianSplatting/gaussianSplattingPartProxyMesh.pure.js.map +1 -1
- package/Meshes/GaussianSplatting/gaussianSplattingSortWorker.js +31 -6
- package/Meshes/GaussianSplatting/gaussianSplattingSortWorker.js.map +1 -1
- package/Meshes/GreasedLine/greasedLineRibbonMesh.pure.js +5 -1
- package/Meshes/GreasedLine/greasedLineRibbonMesh.pure.js.map +1 -1
- package/Misc/greasedLineTools.js +1 -1
- package/Misc/greasedLineTools.js.map +1 -1
- package/Misc/tools.pure.js +14 -2
- package/Misc/tools.pure.js.map +1 -1
- package/Particles/gpuParticleSystem.pure.d.ts +66 -1
- package/Particles/gpuParticleSystem.pure.js +189 -49
- package/Particles/gpuParticleSystem.pure.js.map +1 -1
- package/Physics/v2/characterController.js +69 -17
- package/Physics/v2/characterController.js.map +1 -1
- package/XR/features/Layers/WebXRWebGPUCompositionLayer.d.ts +3 -9
- package/XR/features/Layers/WebXRWebGPUCompositionLayer.js +4 -11
- package/XR/features/Layers/WebXRWebGPUCompositionLayer.js.map +1 -1
- package/XR/features/WebXRAbstractFeature.d.ts +9 -0
- package/XR/features/WebXRAbstractFeature.js +16 -0
- package/XR/features/WebXRAbstractFeature.js.map +1 -1
- package/XR/features/WebXRDepthSensing.pure.d.ts +7 -0
- package/XR/features/WebXRDepthSensing.pure.js +350 -39
- package/XR/features/WebXRDepthSensing.pure.js.map +1 -1
- package/XR/features/WebXRImageTracking.pure.js.map +1 -1
- package/XR/features/WebXRLayers.pure.d.ts +6 -5
- package/XR/features/WebXRLayers.pure.js +65 -8
- package/XR/features/WebXRLayers.pure.js.map +1 -1
- package/XR/features/WebXRRawCameraAccess.pure.js +9 -2
- package/XR/features/WebXRRawCameraAccess.pure.js.map +1 -1
- package/XR/features/WebXRSpaceWarp.pure.d.ts +0 -1
- package/XR/features/WebXRSpaceWarp.pure.js +13 -4
- package/XR/features/WebXRSpaceWarp.pure.js.map +1 -1
- package/XR/features/WebXRWalkingLocomotion.pure.js.map +1 -1
- package/XR/webXRCamera.js +27 -0
- package/XR/webXRCamera.js.map +1 -1
- package/XR/webXRFeatureWarningRegistry.d.ts +14 -0
- package/XR/webXRFeatureWarningRegistry.js +19 -0
- package/XR/webXRFeatureWarningRegistry.js.map +1 -0
- package/XR/webXRFeaturesManager.d.ts +5 -0
- package/XR/webXRFeaturesManager.js +23 -4
- package/XR/webXRFeaturesManager.js.map +1 -1
- package/XR/webXRGraphicsBinding.d.ts +17 -10
- package/XR/webXRGraphicsBinding.js +8 -5
- package/XR/webXRGraphicsBinding.js.map +1 -1
- package/XR/webXRSessionManager.d.ts +3 -5
- package/XR/webXRSessionManager.js +1 -3
- package/XR/webXRSessionManager.js.map +1 -1
- package/XR/webXRWebGPURenderTargetTextureProvider.d.ts +21 -5
- package/XR/webXRWebGPURenderTargetTextureProvider.js +38 -10
- package/XR/webXRWebGPURenderTargetTextureProvider.js.map +1 -1
- package/package.json +1 -1
|
@@ -93,6 +93,31 @@ export class GaussianSplattingMesh extends GaussianSplattingMeshBase {
|
|
|
93
93
|
* Visibility values for each part (0.0 to 1.0), indexed by part index.
|
|
94
94
|
*/
|
|
95
95
|
this._partVisibility = [];
|
|
96
|
+
/**
|
|
97
|
+
* Per-part active source-splat range overrides, indexed by part index, in GLOBAL source-splat
|
|
98
|
+
* coordinates (offsets into the merged atlas). A part with no entry (undefined) renders its full
|
|
99
|
+
* range; a part with an entry renders only those ranges. Used by streaming parts to render just
|
|
100
|
+
* their currently-active LOD splats while static parts render fully. No overrides at all means no
|
|
101
|
+
* filter (the base renders every splat, preserving the non-streaming fast path).
|
|
102
|
+
*/
|
|
103
|
+
this._partSplatRanges = [];
|
|
104
|
+
/**
|
|
105
|
+
* True once a streaming part has been reserved. A streamed region's data lives only on the GPU (no retained CPU
|
|
106
|
+
* source), so atlas rebuilds and part add/remove take the streaming-aware paths that back up and restore each
|
|
107
|
+
* region rather than regenerating it from a CPU source.
|
|
108
|
+
* @internal
|
|
109
|
+
*/
|
|
110
|
+
this._hasStreamingPart = false;
|
|
111
|
+
/** Mutable bookkeeping for each reserved streaming region, so {@link compactAtlas} can relocate them. */
|
|
112
|
+
this._streamingStates = [];
|
|
113
|
+
/**
|
|
114
|
+
* Max SH degree contributed by the live (non-tombstoned) streaming parts. Recomputed by
|
|
115
|
+
* {@link _refreshStreamingShState} whenever parts change, so removing the last SH stream turns the shared SH
|
|
116
|
+
* atlas off instead of leaving SH_DEGREE high over texels nothing refills.
|
|
117
|
+
*/
|
|
118
|
+
this._streamingShDegree = 0;
|
|
119
|
+
/** Part indices tombstoned by {@link removePart} while streaming — excluded from render, reclaimed by {@link compactAtlas}. */
|
|
120
|
+
this._tombstonedPartIndices = new Set();
|
|
96
121
|
this._partIndicesTexture = null;
|
|
97
122
|
this._partIndices = null;
|
|
98
123
|
// Ensure _splatsData is retained once compound mode is entered — addPart/addParts need
|
|
@@ -140,7 +165,9 @@ export class GaussianSplattingMesh extends GaussianSplattingMeshBase {
|
|
|
140
165
|
const localMax = this._part0LocalMax ? this._part0LocalMax.clone() : new Vector3(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE);
|
|
141
166
|
const corner = new Vector3();
|
|
142
167
|
for (const proxy of this._partProxies) {
|
|
143
|
-
|
|
168
|
+
// Skip removed parts: a tombstoned proxy is kept (for compaction) but renders nothing, so it must not
|
|
169
|
+
// inflate the compound's bounds (and thus its frustum culling) until its rows are actually reclaimed.
|
|
170
|
+
if (!proxy || this._tombstonedPartIndices.has(proxy.partIndex)) {
|
|
144
171
|
continue;
|
|
145
172
|
}
|
|
146
173
|
// Proxies have no geometry — getHierarchyBoundingVectors returns sentinels. Use boundingBox directly.
|
|
@@ -404,6 +431,91 @@ export class GaussianSplattingMesh extends GaussianSplattingMeshBase {
|
|
|
404
431
|
setPartVisibility(partIndex, value) {
|
|
405
432
|
this._partVisibility[partIndex] = Math.max(0.0, Math.min(1.0, value));
|
|
406
433
|
}
|
|
434
|
+
/**
|
|
435
|
+
* Restricts which source splats of a single part are sorted and rendered, in GLOBAL source-splat
|
|
436
|
+
* coordinates (offsets into the merged atlas). Static parts render fully by default; a streaming
|
|
437
|
+
* part uses this to render only its currently-active LOD splats. The compound recomputes the union
|
|
438
|
+
* of every part's active ranges and drives the single shared depth sort / draw with it, so all
|
|
439
|
+
* parts still sort together in one pass.
|
|
440
|
+
*
|
|
441
|
+
* Passing `null` clears the override for that part (it reverts to rendering its full range). When no
|
|
442
|
+
* part has an override, the compound clears the range filter entirely (renders every splat), which
|
|
443
|
+
* preserves the non-streaming fast path.
|
|
444
|
+
* @param partIndex index of the part to constrain
|
|
445
|
+
* @param ranges active global source-splat ranges for the part, or `null` to render the whole part
|
|
446
|
+
*/
|
|
447
|
+
setPartSplatRanges(partIndex, ranges) {
|
|
448
|
+
if (partIndex < 0) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
this._partSplatRanges[partIndex] = ranges ? ranges.map((r) => ({ offset: r.offset, count: r.count })) : null;
|
|
452
|
+
this._refreshPartRangeUnion();
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Recomputes the global active-range union across all parts and pushes it to the shared sort/draw
|
|
456
|
+
* via the base {@link setSplatIndexRanges}. When no part carries an override the filter is cleared
|
|
457
|
+
* (render everything). A part without an override contributes its full `[offset, count)` derived
|
|
458
|
+
* from its proxy; a part with an override contributes exactly those ranges.
|
|
459
|
+
*/
|
|
460
|
+
_refreshPartRangeUnion() {
|
|
461
|
+
let hasOverride = false;
|
|
462
|
+
for (let i = 0; i < this._partSplatRanges.length; i++) {
|
|
463
|
+
if (this._partSplatRanges[i]) {
|
|
464
|
+
hasOverride = true;
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (!hasOverride) {
|
|
469
|
+
// No part constrains its splats: render the whole atlas (base fast path).
|
|
470
|
+
this.setSplatIndexRanges(null);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const collected = [];
|
|
474
|
+
const partCount = this.partCount;
|
|
475
|
+
for (let partIndex = 0; partIndex < partCount; partIndex++) {
|
|
476
|
+
const override = this._partSplatRanges[partIndex];
|
|
477
|
+
if (override) {
|
|
478
|
+
for (const range of override) {
|
|
479
|
+
if (range.count > 0) {
|
|
480
|
+
collected.push({ offset: range.offset, count: range.count });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
// No override: render the part's full extent, derived from its proxy.
|
|
486
|
+
const proxy = this._partProxies[partIndex];
|
|
487
|
+
if (proxy && proxy._vertexCount > 0) {
|
|
488
|
+
collected.push({ offset: proxy._splatsDataOffset, count: proxy._vertexCount });
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
this.setSplatIndexRanges(GaussianSplattingMesh._CoalesceSplatRanges(collected));
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Sorts and merges adjacent/overlapping source-splat ranges so the interval list handed to the sort
|
|
495
|
+
* worker stays compact (parts occupy disjoint regions, so this mainly coalesces contiguous parts).
|
|
496
|
+
* @param ranges raw ranges
|
|
497
|
+
* @returns coalesced ranges sorted by offset
|
|
498
|
+
*/
|
|
499
|
+
static _CoalesceSplatRanges(ranges) {
|
|
500
|
+
if (ranges.length <= 1) {
|
|
501
|
+
return ranges;
|
|
502
|
+
}
|
|
503
|
+
const sorted = ranges.slice().sort((a, b) => a.offset - b.offset);
|
|
504
|
+
const merged = [{ offset: sorted[0].offset, count: sorted[0].count }];
|
|
505
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
506
|
+
const last = merged[merged.length - 1];
|
|
507
|
+
const range = sorted[i];
|
|
508
|
+
const lastEnd = last.offset + last.count;
|
|
509
|
+
if (range.offset <= lastEnd) {
|
|
510
|
+
const end = Math.max(lastEnd, range.offset + range.count);
|
|
511
|
+
last.count = end - last.offset;
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
merged.push({ offset: range.offset, count: range.count });
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return merged;
|
|
518
|
+
}
|
|
407
519
|
_copyTextures(source) {
|
|
408
520
|
super._copyTextures(source);
|
|
409
521
|
this._partIndicesTexture = source._partIndicesTexture?.clone();
|
|
@@ -565,7 +677,9 @@ export class GaussianSplattingMesh extends GaussianSplattingMeshBase {
|
|
|
565
677
|
}
|
|
566
678
|
// Validate
|
|
567
679
|
for (const other of others) {
|
|
568
|
-
|
|
680
|
+
// Reserved-empty placeholders (reserveStreamingPart) intentionally carry no splat data —
|
|
681
|
+
// their atlas region is left zeroed (invisible) for a streaming engine to decode into later.
|
|
682
|
+
if (!other._splatsData && !other._isReservedEmpty) {
|
|
569
683
|
throw new Error(`To call addPart()/addParts(), each source mesh must be fully loaded`);
|
|
570
684
|
}
|
|
571
685
|
if (other.isCompound) {
|
|
@@ -582,17 +696,18 @@ export class GaussianSplattingMesh extends GaussianSplattingMeshBase {
|
|
|
582
696
|
const covA = new Uint16Array(textureLength * 4);
|
|
583
697
|
const covB = new Uint16Array(covBSItemSize * textureLength);
|
|
584
698
|
const colorArray = new Uint8Array(textureLength * 4);
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
//
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
const hasSH = this._shDegree > 0 || others.some((o) => o._shData !== null);
|
|
594
|
-
const shDegreeNew = hasSH ? Math.max(this._shDegree, ...others.map((o) => o._shDegree)) : 0;
|
|
699
|
+
// Merged SH degree. The compound carries SH if any existing or new part has it, or a hosted stream bakes it.
|
|
700
|
+
// Parts without SH get neutral (128) fill. `_streamingShDegree` is the degree of the live SH streams (recomputed
|
|
701
|
+
// from surviving parts, so it shrinks when one is removed); folding it in keeps `_shDegree` correct across a
|
|
702
|
+
// rebuild (which resets it) and drops it to 0 when no SH stream survives.
|
|
703
|
+
const streamingShDegree = this._streamingShDegree;
|
|
704
|
+
const staticHasSH = this._shDegree > 0 || others.some((o) => o._shData !== null);
|
|
705
|
+
const hasSH = staticHasSH || streamingShDegree > 0;
|
|
706
|
+
const shDegreeNew = hasSH ? Math.max(this._shDegree, streamingShDegree, ...others.map((o) => o._shDegree)) : 0;
|
|
595
707
|
let sh = undefined;
|
|
708
|
+
// Allocate the neutral (128 == 0 after decode) CPU SH base whenever the atlas carries SH, and upload it over
|
|
709
|
+
// the whole atlas before any SH stream restores its region, so non-SH regions read neutral instead of raw 0
|
|
710
|
+
// (which decodes to -1).
|
|
596
711
|
if (hasSH && shDegreeNew > 0) {
|
|
597
712
|
// Each SH texture holds one texel per splat; each texel is _GaussianSplattingBytesPerShTexel
|
|
598
713
|
// bytes with one byte per scalar, so it carries that many scalars. Degree d has
|
|
@@ -601,7 +716,7 @@ export class GaussianSplattingMesh extends GaussianSplattingMeshBase {
|
|
|
601
716
|
sh = AllocateShBuffers(shTextureCount, textureLength * _GaussianSplattingBytesPerShTexel);
|
|
602
717
|
}
|
|
603
718
|
// --- Incremental path: can we reuse the already-committed GPU region? ---
|
|
604
|
-
const incremental = this._canReuseCachedData(splatCountA, totalCount);
|
|
719
|
+
const incremental = this._canReuseCachedData(splatCountA, totalCount, sh?.length ?? 0);
|
|
605
720
|
const firstNewLine = incremental ? Math.floor(splatCountA / textureSize.x) : 0;
|
|
606
721
|
const minimum = incremental ? this._cachedBoundingMin.clone() : new Vector3(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
|
|
607
722
|
const maximum = incremental ? this._cachedBoundingMax.clone() : new Vector3(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE);
|
|
@@ -688,6 +803,16 @@ export class GaussianSplattingMesh extends GaussianSplattingMeshBase {
|
|
|
688
803
|
if (!proxy) {
|
|
689
804
|
continue;
|
|
690
805
|
}
|
|
806
|
+
// Streaming parts have no retained CPU source — their atlas rows (core + SH) are
|
|
807
|
+
// GPU-authoritative and preserved across the rebuild by the backup/restore hooks. Leave their
|
|
808
|
+
// rows at the fresh-array defaults (zero core, neutral SH) for the restore to overwrite; just
|
|
809
|
+
// advance the offset. (With a static part present _splatsData is non-null and these would
|
|
810
|
+
// otherwise reconstruct as a harmless zeros-slice; with only streaming parts it is null and
|
|
811
|
+
// _createRetainedPartSource returns null — so this skip is required, not just an optimization.)
|
|
812
|
+
if (this._streamingStates.some((s) => s.proxy === proxy)) {
|
|
813
|
+
rebuildOffset += proxy._vertexCount;
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
691
816
|
const source = this._createRetainedPartSource(proxy);
|
|
692
817
|
if (!source) {
|
|
693
818
|
throw new Error(`Cannot rebuild compound part "${proxy.name}": the retained compound source data is not available.`);
|
|
@@ -764,6 +889,14 @@ export class GaussianSplattingMesh extends GaussianSplattingMeshBase {
|
|
|
764
889
|
if (!proxy) {
|
|
765
890
|
continue;
|
|
766
891
|
}
|
|
892
|
+
// Streaming parts are GPU-authoritative (no CPU source) — skip reconstruction (their rows are
|
|
893
|
+
// restored via the atlas hooks). Row-aligned regions mean a streamed part's splats never fall
|
|
894
|
+
// in the partial boundary row anyway; leave its source buffers null and just track the offset.
|
|
895
|
+
if (this._streamingStates.some((s) => s.proxy === proxy)) {
|
|
896
|
+
partStarts[pi] = cumOffset;
|
|
897
|
+
cumOffset += proxy._vertexCount;
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
767
900
|
const source = this._createRetainedPartSource(proxy);
|
|
768
901
|
if (!source || !source._splatsData) {
|
|
769
902
|
throw new Error(`Cannot rebuild compound part "${proxy.name}": the retained compound source data is not available.`);
|
|
@@ -799,9 +932,14 @@ export class GaussianSplattingMesh extends GaussianSplattingMeshBase {
|
|
|
799
932
|
if (totalCount !== this._vertexCount) {
|
|
800
933
|
this._updateSplatIndexBuffer(totalCount);
|
|
801
934
|
}
|
|
802
|
-
|
|
935
|
+
// Retain CPU SH only for the STATIC contribution — a stream-only SH atlas has no CPU SH to reconstruct on a
|
|
936
|
+
// later compaction (the stream re-decodes/backup-restores its own region), so don't retain neutral buffers.
|
|
937
|
+
this._retainMergedPartData(splatCountA, totalCount, others, staticHasSH ? shDegreeNew : 0);
|
|
803
938
|
this._vertexCount = totalCount;
|
|
804
939
|
this._shDegree = shDegreeNew;
|
|
940
|
+
// Keep the max in sync so the public `shDegree` setter's clamp tracks the current data (and shrinks when a
|
|
941
|
+
// higher-degree part is removed). The compound's SH degree is fully described by the live parts.
|
|
942
|
+
this._maxShDegree = shDegreeNew;
|
|
805
943
|
// Gate the sort worker for the duration of this operation. _updateTextures (below) may create the worker and fire an
|
|
806
944
|
// immediate sort via _postToWorker. At that point partMatrices has not yet been updated for the incoming parts, so the
|
|
807
945
|
// worker would compute depthCoeffs for fewer parts than partIndices references — crashing with
|
|
@@ -906,6 +1044,13 @@ export class GaussianSplattingMesh extends GaussianSplattingMeshBase {
|
|
|
906
1044
|
});
|
|
907
1045
|
}
|
|
908
1046
|
this._canPostToWorker = true;
|
|
1047
|
+
// A streaming compound renders the active-range UNION (a part carries an override), so a newly
|
|
1048
|
+
// added static part must be folded into that union now — otherwise it stays out of the sorted
|
|
1049
|
+
// set and isn't drawn until the stream's next LOD re-evaluation happens to refresh the union.
|
|
1050
|
+
// (No-op for non-streaming compounds: with no override the union clears to the render-all path.)
|
|
1051
|
+
if (this._hasStreamingPart) {
|
|
1052
|
+
this._refreshPartRangeUnion();
|
|
1053
|
+
}
|
|
909
1054
|
this._postToWorker(true);
|
|
910
1055
|
}
|
|
911
1056
|
this.onPartCountChangedObservable.notifyObservers(this.partCount);
|
|
@@ -932,73 +1077,131 @@ export class GaussianSplattingMesh extends GaussianSplattingMeshBase {
|
|
|
932
1077
|
* @deprecated Use {@link GaussianSplattingCompoundMesh.addPart} instead.
|
|
933
1078
|
*/
|
|
934
1079
|
addPart(other, disposeOther = true) {
|
|
1080
|
+
// Adding parts while a streaming part exists is supported: the grow rebuild preserves streamed regions
|
|
1081
|
+
// (their GPU data is backed up/restored around the atlas recreation). Only removal is still restricted,
|
|
1082
|
+
// because it shifts existing part offsets (which streamed regions' work buffers depend on).
|
|
935
1083
|
const { proxyMeshes } = this._addPartsInternal([other], disposeOther);
|
|
936
1084
|
return proxyMeshes[0];
|
|
937
1085
|
}
|
|
938
1086
|
/**
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
942
|
-
*
|
|
943
|
-
* @param index - The index of the part to remove
|
|
944
|
-
* @deprecated Use {@link GaussianSplattingCompoundMesh.removePart} instead.
|
|
1087
|
+
* Recomputes the shared SH-atlas state ({@link _useShMrtAtlas}, {@link _shMrtAtlasTextureCount},
|
|
1088
|
+
* {@link _streamingShDegree}) from the currently-live (non-tombstoned) streaming parts. Call before an atlas
|
|
1089
|
+
* rebuild that follows a removal so a stale SH degree/count from a removed part can't keep the SH atlas active
|
|
1090
|
+
* (and SH_DEGREE high) with nothing refilling the SH texels.
|
|
945
1091
|
*/
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
for (let proxyIndex = 0; proxyIndex < this._partProxies.length; proxyIndex++) {
|
|
953
|
-
const proxy = this._partProxies[proxyIndex];
|
|
954
|
-
if (!proxy || proxyIndex === index) {
|
|
1092
|
+
_refreshStreamingShState() {
|
|
1093
|
+
let count = 0;
|
|
1094
|
+
let degree = 0;
|
|
1095
|
+
let needsRot = false;
|
|
1096
|
+
for (const state of this._streamingStates) {
|
|
1097
|
+
if (this._tombstonedPartIndices.has(state.partIndex)) {
|
|
955
1098
|
continue;
|
|
956
1099
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
throw new Error(`Cannot remove part: the retained compound source data is not available for part "${proxy.name}".`);
|
|
1100
|
+
if (state.shTextureCount > count) {
|
|
1101
|
+
count = state.shTextureCount;
|
|
960
1102
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
survivors.sort((a, b) => a.oldIndex - b.oldIndex);
|
|
964
|
-
// Validate every survivor still has its source data. If even one is missing we cannot rebuild.
|
|
965
|
-
for (const { proxyMesh, source } of survivors) {
|
|
966
|
-
if (!source._splatsData) {
|
|
967
|
-
throw new Error(`Cannot remove part: the source data for part "${proxyMesh.name}" is not available.`);
|
|
1103
|
+
if (state.shDegree > degree) {
|
|
1104
|
+
degree = state.shDegree;
|
|
968
1105
|
}
|
|
969
|
-
if (
|
|
970
|
-
|
|
1106
|
+
if (state.needsRotationScale) {
|
|
1107
|
+
needsRot = true;
|
|
971
1108
|
}
|
|
972
1109
|
}
|
|
973
|
-
|
|
1110
|
+
this._shMrtAtlasTextureCount = count;
|
|
1111
|
+
this._useShMrtAtlas = count > 0;
|
|
1112
|
+
this._streamingShDegree = degree;
|
|
1113
|
+
// Render-backed rotation atlas is active iff a live streaming part decodes rotation/scale. (`_needsRotationScaleTextures`
|
|
1114
|
+
// is left as-is — removing a stream doesn't disable IBL for surviving static parts, which keep CPU RawTextures.)
|
|
1115
|
+
this._useRotMrtAtlas = needsRot;
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Tombstones a part of a streaming compound: excludes it from the render union permanently and hides its
|
|
1119
|
+
* proxy, leaving its atlas rows idle. Used instead of the compacting {@link removePart} rebuild whenever a
|
|
1120
|
+
* streaming part is reserved — a streamed region has no retained CPU source and decodes at a FIXED base
|
|
1121
|
+
* offset, so the rebuild can neither reconstruct it nor shift any part without desynchronizing the streaming
|
|
1122
|
+
* engine. Every other part keeps its exact offset (no shift), so resident streams keep decoding at their base.
|
|
1123
|
+
* The empty `[]` override survives future add-driven rebuilds, so the region stays invisible even though its
|
|
1124
|
+
* texels get rebuilt; memory is only reclaimed by disposing/recreating the whole compound.
|
|
1125
|
+
* @param index the part index to tombstone
|
|
1126
|
+
*/
|
|
1127
|
+
_tombstonePart(index) {
|
|
1128
|
+
// Fire before mutation so the stream driving this part (which subscribes to this observable at reservation)
|
|
1129
|
+
// can dispose itself: stop its LOD loop, free its work buffer, and drop its atlas-rebuild hooks.
|
|
974
1130
|
this.onPartRemovedObservable.notifyObservers(index);
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1131
|
+
const proxy = this._partProxies[index];
|
|
1132
|
+
if (proxy) {
|
|
1133
|
+
proxy.setEnabled(false);
|
|
1134
|
+
proxy.visibility = 0;
|
|
1135
|
+
}
|
|
1136
|
+
this._partVisibility[index] = 0;
|
|
1137
|
+
// Invalidate this region's streaming handle so a caller that retained it can't later mutate a surviving part
|
|
1138
|
+
// that inherits this index/base after compaction.
|
|
1139
|
+
const removedState = this._streamingStates.find((s) => s.partIndex === index);
|
|
1140
|
+
if (removedState) {
|
|
1141
|
+
removedState.removed = true;
|
|
1142
|
+
}
|
|
1143
|
+
// Empty (non-null) override = "this part contributes no splats"; keeps the union filter engaged so the
|
|
1144
|
+
// idle region is never rendered, even after another part's addition rebuilds the atlas.
|
|
1145
|
+
this._partSplatRanges[index] = [];
|
|
1146
|
+
// Record the tombstone so compactAtlas() can drop this part's atlas rows and reclaim the memory.
|
|
1147
|
+
this._tombstonedPartIndices.add(index);
|
|
1148
|
+
this._refreshPartRangeUnion();
|
|
1149
|
+
this._updateBoundingInfoFromProxies();
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Tears the compound down to an empty state so a subsequent {@link _addPartsInternal} recreates fresh GPU
|
|
1153
|
+
* textures. Shared by {@link removePart} (compacting rebuild) and {@link compactAtlas}. Does NOT dispose the
|
|
1154
|
+
* part proxies — callers dispose only the proxies being removed and reuse the survivors' proxy objects.
|
|
1155
|
+
* @internal
|
|
1156
|
+
*/
|
|
1157
|
+
_resetForRebuild() {
|
|
1158
|
+
// Terminate the sort worker before zeroing _vertexCount. The worker's onmessage handler compares
|
|
1159
|
+
// depthMix.length against (_vertexCount + 15) & ~0xf; with _vertexCount = 0 that becomes 16, forcing a
|
|
1160
|
+
// re-sort loop on stale data and resetting _canPostToWorker to true, defeating the rebuild gate. The
|
|
1161
|
+
// worker is re-instantiated after the rebuild via the first _postToWorker call.
|
|
981
1162
|
if (this._worker) {
|
|
982
1163
|
this._worker.terminate();
|
|
983
1164
|
this._worker = null;
|
|
984
1165
|
}
|
|
985
|
-
// Dispose and null
|
|
986
|
-
//
|
|
987
|
-
this.
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1166
|
+
// Dispose GPU textures and null them so _updateTextures sees firstTime=true and allocates fresh ones.
|
|
1167
|
+
// For a streaming atlas the four data textures are attachments of _mrtAtlas — dispose the MRT, not each.
|
|
1168
|
+
if (this._mrtAtlas) {
|
|
1169
|
+
this._mrtAtlas.dispose();
|
|
1170
|
+
this._mrtAtlas = null;
|
|
1171
|
+
this._covariancesATexture = this._covariancesBTexture = this._centersTexture = this._colorsTexture = null;
|
|
1172
|
+
}
|
|
1173
|
+
else {
|
|
1174
|
+
this._covariancesATexture?.dispose();
|
|
1175
|
+
this._covariancesBTexture?.dispose();
|
|
1176
|
+
this._centersTexture?.dispose();
|
|
1177
|
+
this._colorsTexture?.dispose();
|
|
1178
|
+
this._covariancesATexture = null;
|
|
1179
|
+
this._covariancesBTexture = null;
|
|
1180
|
+
this._centersTexture = null;
|
|
1181
|
+
this._colorsTexture = null;
|
|
1182
|
+
}
|
|
1183
|
+
if (this._rotMrtAtlas) {
|
|
1184
|
+
// The three rotation textures are attachments of this MRT — dispose the MRT, not each attachment.
|
|
1185
|
+
this._rotMrtAtlas.dispose();
|
|
1186
|
+
this._rotMrtAtlas = null;
|
|
1187
|
+
}
|
|
1188
|
+
else {
|
|
1189
|
+
this._rotationsATexture?.dispose();
|
|
1190
|
+
this._rotationsBTexture?.dispose();
|
|
1191
|
+
this._rotationScaleTexture?.dispose();
|
|
1192
|
+
}
|
|
998
1193
|
this._rotationsATexture = null;
|
|
999
1194
|
this._rotationsBTexture = null;
|
|
1000
1195
|
this._rotationScaleTexture = null;
|
|
1001
|
-
if (this.
|
|
1196
|
+
if (this._shMrtAtlas) {
|
|
1197
|
+
// SH attachments belong to these MRTs — dispose the MRTs, not each attachment (mirrors _mrtAtlas).
|
|
1198
|
+
for (const mrt of this._shMrtAtlas) {
|
|
1199
|
+
mrt.dispose();
|
|
1200
|
+
}
|
|
1201
|
+
this._shMrtAtlas = null;
|
|
1202
|
+
this._shTextures = null;
|
|
1203
|
+
}
|
|
1204
|
+
else if (this._shTextures) {
|
|
1002
1205
|
for (const t of this._shTextures) {
|
|
1003
1206
|
t.dispose();
|
|
1004
1207
|
}
|
|
@@ -1020,12 +1223,213 @@ export class GaussianSplattingMesh extends GaussianSplattingMeshBase {
|
|
|
1020
1223
|
this._splatsData = null;
|
|
1021
1224
|
this._shData = null;
|
|
1022
1225
|
this._shDegree = 0;
|
|
1023
|
-
// Remove the proxy for the removed part and dispose it
|
|
1024
|
-
const proxyToRemove = this._partProxies[index];
|
|
1025
|
-
if (proxyToRemove) {
|
|
1026
|
-
proxyToRemove.dispose();
|
|
1027
|
-
}
|
|
1028
1226
|
this._partProxies = [];
|
|
1227
|
+
// Drop per-part range overrides too: a rebuild reassigns part indices from 0, so a leftover entry at a
|
|
1228
|
+
// now-unused high index would otherwise be inherited by a later-added part (rendering it wrong/nothing).
|
|
1229
|
+
// The rebuild re-sets the surviving parts' entries.
|
|
1230
|
+
this._partSplatRanges = [];
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Reclaims the atlas rows of parts removed (tombstoned) while a streaming part was resident. `removePart`
|
|
1234
|
+
* on a streaming compound only tombstones — it excludes the part from the render union and hides its proxy,
|
|
1235
|
+
* but leaves its rows allocated, since compacting them would relocate the still-resident streaming regions.
|
|
1236
|
+
* This method performs that compaction: it rebuilds the shared atlas from the LIVE parts at new, contiguous
|
|
1237
|
+
* (row-aligned for streaming) offsets, physically relocating each surviving streaming region's GPU texels,
|
|
1238
|
+
* CPU sort positions, and decode/render base offset, and drops the tombstoned rows — shrinking the atlas.
|
|
1239
|
+
*
|
|
1240
|
+
* Call it after removing one or more models to actually free the GPU/CPU memory (e.g. on idle, or once a
|
|
1241
|
+
* batch of removals settles). No-op when nothing is tombstoned. Safe while streams are actively decoding:
|
|
1242
|
+
* each surviving region is backed up before the old atlas is disposed and restored at its new base after.
|
|
1243
|
+
*/
|
|
1244
|
+
compactAtlas() {
|
|
1245
|
+
if (this._tombstonedPartIndices.size === 0) {
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
const atlasWidth = this._getTextureSize(1).x;
|
|
1249
|
+
const specs = [];
|
|
1250
|
+
let cumOffset = 0;
|
|
1251
|
+
for (let i = 0; i < this._partProxies.length; i++) {
|
|
1252
|
+
const proxy = this._partProxies[i];
|
|
1253
|
+
if (!proxy || this._tombstonedPartIndices.has(i)) {
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
const state = this._streamingStates.find((s) => s.proxy === proxy) ?? null;
|
|
1257
|
+
const worldMatrix = proxy.getWorldMatrix().clone();
|
|
1258
|
+
const visibility = this._partVisibility[i] ?? 1.0;
|
|
1259
|
+
if (state) {
|
|
1260
|
+
// Streaming: reserve an empty, row-aligned region (front pad + capacity) at the compacted offset.
|
|
1261
|
+
const alignedBase = Math.ceil(cumOffset / atlasWidth) * atlasWidth;
|
|
1262
|
+
const frontPad = alignedBase - cumOffset;
|
|
1263
|
+
const regionSplats = frontPad + state.capacity;
|
|
1264
|
+
const source = {
|
|
1265
|
+
name: proxy.name,
|
|
1266
|
+
_vertexCount: regionSplats,
|
|
1267
|
+
_splatsData: null,
|
|
1268
|
+
_shData: null,
|
|
1269
|
+
_shDegree: 0,
|
|
1270
|
+
isCompound: false,
|
|
1271
|
+
getWorldMatrix: () => worldMatrix,
|
|
1272
|
+
getBoundingInfo: () => new BoundingInfo(Vector3.ZeroReadOnly, Vector3.ZeroReadOnly),
|
|
1273
|
+
dispose: () => { },
|
|
1274
|
+
_isReservedEmpty: true,
|
|
1275
|
+
};
|
|
1276
|
+
specs.push({ oldProxy: proxy, source, worldMatrix, visibility, state, newBase: alignedBase, frontPad });
|
|
1277
|
+
cumOffset += regionSplats;
|
|
1278
|
+
}
|
|
1279
|
+
else {
|
|
1280
|
+
const source = this._createRetainedPartSource(proxy);
|
|
1281
|
+
if (!source || !source._splatsData) {
|
|
1282
|
+
throw new Error(`compactAtlas: the retained source data for static part "${proxy.name}" is not available.`);
|
|
1283
|
+
}
|
|
1284
|
+
specs.push({ oldProxy: proxy, source, worldMatrix, visibility, state: null, newBase: 0, frontPad: 0 });
|
|
1285
|
+
cumOffset += source._vertexCount;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
// Back up every surviving streaming region's GPU texels + CPU positions before the atlas is torn down.
|
|
1289
|
+
if (this._mrtAtlas) {
|
|
1290
|
+
this._onBeforeAtlasRebuildObservable.notifyObservers(this._mrtAtlas);
|
|
1291
|
+
}
|
|
1292
|
+
// Dispose the tombstoned parts' proxies (their streams were already disposed when tombstoned).
|
|
1293
|
+
for (const i of this._tombstonedPartIndices) {
|
|
1294
|
+
this._partProxies[i]?.dispose();
|
|
1295
|
+
}
|
|
1296
|
+
// Tear the compound down to empty so _addPartsInternal recreates fresh (smaller) GPU textures.
|
|
1297
|
+
this._resetForRebuild();
|
|
1298
|
+
if (specs.length === 0) {
|
|
1299
|
+
// Everything was tombstoned — nothing to rebuild. Clear the streaming/atlas capability flags so that
|
|
1300
|
+
// reusing this now-empty compound for plain (non-streaming) content doesn't recreate render-target
|
|
1301
|
+
// atlases or restore a stale SH/rotation configuration. _refreshStreamingShState turns off SH/rotation
|
|
1302
|
+
// from the (empty) live states; the render-backed core atlas + RGBA covariants reset here.
|
|
1303
|
+
this._streamingStates.length = 0;
|
|
1304
|
+
this._hasStreamingPart = false;
|
|
1305
|
+
this._tombstonedPartIndices.clear();
|
|
1306
|
+
this._refreshStreamingShState();
|
|
1307
|
+
this._useMrtAtlas = false;
|
|
1308
|
+
// Restore the engine-derived default rather than a hardcoded `false`: WebGL 1 (no WebGPU) cannot render
|
|
1309
|
+
// the RG half-float covariance-B path and requires RGBA, so a reused compound must keep that default.
|
|
1310
|
+
this._useRGBACovariants = !this.getEngine().isWebGPU && this.getEngine().version === 1.0;
|
|
1311
|
+
this.setEnabled(false);
|
|
1312
|
+
this.onPartCountChangedObservable.notifyObservers(0);
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
// Recompute the SH-atlas state from the SURVIVING streaming parts BEFORE the rebuild: if the removed part
|
|
1316
|
+
// was the only SH stream, this turns the SH atlas off (degree 0) so the rebuild neither recreates a stale
|
|
1317
|
+
// SH atlas nor keeps SH_DEGREE high over now-unfilled (raw-0) texels; a lower surviving degree shrinks too.
|
|
1318
|
+
this._refreshStreamingShState();
|
|
1319
|
+
this._rebuilding = true;
|
|
1320
|
+
this._canPostToWorker = false;
|
|
1321
|
+
try {
|
|
1322
|
+
const { proxyMeshes: newProxies } = this._addPartsInternal(specs.map((s) => s.source), false);
|
|
1323
|
+
// Re-map the surviving proxies onto their new indices/offsets and relocate streaming state.
|
|
1324
|
+
for (let i = 0; i < specs.length; i++) {
|
|
1325
|
+
const spec = specs[i];
|
|
1326
|
+
const newProxy = newProxies[i];
|
|
1327
|
+
const newPartIndex = newProxy.partIndex;
|
|
1328
|
+
this.setWorldMatrixForPart(newPartIndex, spec.worldMatrix);
|
|
1329
|
+
this.setPartVisibility(newPartIndex, spec.visibility);
|
|
1330
|
+
const quaternion = new Quaternion();
|
|
1331
|
+
spec.worldMatrix.decompose(newProxy.scaling, quaternion, newProxy.position);
|
|
1332
|
+
newProxy.rotationQuaternion = quaternion;
|
|
1333
|
+
newProxy.computeWorldMatrix(true);
|
|
1334
|
+
spec.oldProxy.updatePartIndex(newPartIndex);
|
|
1335
|
+
spec.oldProxy.updatePartMetadata(newProxy._vertexCount, newProxy._splatsDataOffset, newProxy._shDataOffset);
|
|
1336
|
+
this._partProxies[newPartIndex] = spec.oldProxy;
|
|
1337
|
+
newProxy.dispose();
|
|
1338
|
+
if (spec.state) {
|
|
1339
|
+
// Relocate: the usable base is the (aligned) source start + front pad. Update BEFORE the
|
|
1340
|
+
// onAfter hook fires so the stream reads the new base from the handle.
|
|
1341
|
+
spec.state.partIndex = newPartIndex;
|
|
1342
|
+
spec.state.base = newProxy._splatsDataOffset + spec.frontPad;
|
|
1343
|
+
// Re-post the region's active ranges at the new base so the union stays correct with no flicker.
|
|
1344
|
+
this._partSplatRanges[newPartIndex] = spec.state.localRanges
|
|
1345
|
+
? spec.state.localRanges.map((r) => ({ offset: spec.state.base + r.offset, count: r.count }))
|
|
1346
|
+
: null;
|
|
1347
|
+
}
|
|
1348
|
+
else {
|
|
1349
|
+
this._partSplatRanges[newPartIndex] = null; // static parts render fully
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
// Rebind + restore every surviving streaming region into the new (smaller) atlas at its new base.
|
|
1353
|
+
if (this._mrtAtlas) {
|
|
1354
|
+
this._onAfterAtlasRebuildObservable.notifyObservers(this._mrtAtlas);
|
|
1355
|
+
}
|
|
1356
|
+
// Drop the states of tombstoned streaming parts; keep only surviving ones.
|
|
1357
|
+
this._streamingStates = specs.filter((s) => s.state).map((s) => s.state);
|
|
1358
|
+
this._hasStreamingPart = this._streamingStates.length > 0;
|
|
1359
|
+
this._tombstonedPartIndices.clear();
|
|
1360
|
+
// Recompute bounds from the restored proxies (_addPartsInternal above used the zero-box placeholders),
|
|
1361
|
+
// else a static-transform compound keeps stale bounds and isInFrustum can cull it. Must run after the
|
|
1362
|
+
// tombstone set is cleared so a survivor reusing a tombstoned index isn't skipped by the filter.
|
|
1363
|
+
this._updateBoundingInfoFromProxies();
|
|
1364
|
+
this._rebuilding = false;
|
|
1365
|
+
this._canPostToWorker = true;
|
|
1366
|
+
// The streaming regions' CPU positions were restored into _splatPositions by the onAfter hook (after
|
|
1367
|
+
// _addPartsInternal had already posted the rebuild's zeros), so re-post the full merged set: positions
|
|
1368
|
+
// + part indices via _notifyWorkerNewData, then the per-part matrices (suppressed during _rebuilding),
|
|
1369
|
+
// refresh the union, and fire one sort — so the worker's positions/partIndices/partMatrices all agree.
|
|
1370
|
+
this._refreshPartRangeUnion();
|
|
1371
|
+
this._notifyWorkerNewData();
|
|
1372
|
+
const workerAfterRebuild = this._worker;
|
|
1373
|
+
workerAfterRebuild?.postMessage({
|
|
1374
|
+
command: GaussianSplattingSortWorkerCommand.PART_MATRICES,
|
|
1375
|
+
partMatrices: this._partMatrices.map((matrix) => new Float32Array(matrix.m)),
|
|
1376
|
+
});
|
|
1377
|
+
this._postToWorker(true);
|
|
1378
|
+
this.onPartCountChangedObservable.notifyObservers(this.partCount);
|
|
1379
|
+
}
|
|
1380
|
+
catch (e) {
|
|
1381
|
+
this._rebuilding = false;
|
|
1382
|
+
this._canPostToWorker = true;
|
|
1383
|
+
throw e;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Remove a part from this compound mesh.
|
|
1388
|
+
* The remaining parts are rebuilt directly from the compound mesh's retained source buffers.
|
|
1389
|
+
* The current mesh is reset to a plain (single-part) state and then each remaining source is
|
|
1390
|
+
* re-added via addParts.
|
|
1391
|
+
* When a streaming part is reserved the part is tombstoned instead of compacted (see {@link _tombstonePart}).
|
|
1392
|
+
* @param index - The index of the part to remove
|
|
1393
|
+
* @deprecated Use {@link GaussianSplattingCompoundMesh.removePart} instead.
|
|
1394
|
+
*/
|
|
1395
|
+
removePart(index) {
|
|
1396
|
+
if (index < 0 || index >= this.partCount) {
|
|
1397
|
+
throw new Error(`Part index ${index} is out of range [0, ${this.partCount})`);
|
|
1398
|
+
}
|
|
1399
|
+
// Streaming compounds can't compact (a reserved region has no CPU source and a fixed base offset).
|
|
1400
|
+
if (this._hasStreamingPart) {
|
|
1401
|
+
this._tombstonePart(index);
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
// Collect surviving proxy objects (sorted by current part index so part 0 is added first)
|
|
1405
|
+
const survivors = [];
|
|
1406
|
+
for (let proxyIndex = 0; proxyIndex < this._partProxies.length; proxyIndex++) {
|
|
1407
|
+
const proxy = this._partProxies[proxyIndex];
|
|
1408
|
+
if (!proxy || proxyIndex === index) {
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
const source = this._createRetainedPartSource(proxy);
|
|
1412
|
+
if (!source) {
|
|
1413
|
+
throw new Error(`Cannot remove part: the retained compound source data is not available for part "${proxy.name}".`);
|
|
1414
|
+
}
|
|
1415
|
+
survivors.push({ proxyMesh: proxy, source, oldIndex: proxyIndex, worldMatrix: proxy.getWorldMatrix().clone(), visibility: this._partVisibility[proxyIndex] ?? 1.0 });
|
|
1416
|
+
}
|
|
1417
|
+
survivors.sort((a, b) => a.oldIndex - b.oldIndex);
|
|
1418
|
+
// Validate every survivor still has its source data. If even one is missing we cannot rebuild.
|
|
1419
|
+
for (const { proxyMesh, source } of survivors) {
|
|
1420
|
+
if (!source._splatsData) {
|
|
1421
|
+
throw new Error(`Cannot remove part: the source data for part "${proxyMesh.name}" is not available.`);
|
|
1422
|
+
}
|
|
1423
|
+
if (source._shDegree > 0 && !source._shData) {
|
|
1424
|
+
throw new Error(`Cannot remove part: the SH data for part "${proxyMesh.name}" is not available.`);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
// Notify listeners before mutation so they can record state keyed on the original index.
|
|
1428
|
+
this.onPartRemovedObservable.notifyObservers(index);
|
|
1429
|
+
// Dispose the removed part's proxy, then reset to an empty state (survivors' proxy objects are held in
|
|
1430
|
+
// `survivors` and reused after the rebuild).
|
|
1431
|
+
this._partProxies[index]?.dispose();
|
|
1432
|
+
this._resetForRebuild();
|
|
1029
1433
|
// Rebuild from surviving sources. _addPartsInternal assigns part indices in order 0, 1, 2, …
|
|
1030
1434
|
// so the new index for each survivor is simply its position in the survivors array.
|
|
1031
1435
|
if (survivors.length === 0) {
|
|
@@ -1078,6 +1482,249 @@ export class GaussianSplattingMesh extends GaussianSplattingMeshBase {
|
|
|
1078
1482
|
throw e;
|
|
1079
1483
|
}
|
|
1080
1484
|
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Reserves a contiguous region of `capacity` splats in the compound as a new part for dynamic (streamed)
|
|
1487
|
+
* content. Unlike {@link addPart}, no source data is copied: the region is created as invisible padding
|
|
1488
|
+
* (zeroed) for a streaming engine to populate over time via the returned handle. The reserved part
|
|
1489
|
+
* participates in the compound's single shared depth sort and draw exactly like a static part — it has a
|
|
1490
|
+
* `partIndex`, a per-part world matrix (via its proxy), and a per-part visibility — so streamed splats are
|
|
1491
|
+
* sorted and rendered together in one pass with the static parts.
|
|
1492
|
+
*
|
|
1493
|
+
* Add the streaming part LAST (after all static parts). The returned handle drives which of the region's
|
|
1494
|
+
* splats render (LOD) and writes their data.
|
|
1495
|
+
* @param capacity number of splats to reserve
|
|
1496
|
+
* @param worldMatrix initial world matrix for the region's proxy (e.g. carrying a source's up-axis
|
|
1497
|
+
* convention); defaults to identity
|
|
1498
|
+
* @param name name for the region's proxy mesh
|
|
1499
|
+
* @param shTextureCount number of packed-u32 higher-order SH textures to allocate as a shared render-target SH
|
|
1500
|
+
* atlas the streaming engine bakes into (`ceil(coeffs*3/16)` for the stream's max SH degree); 0 = no SH
|
|
1501
|
+
* @param shDegree SH degree of the streamed content (drives the compound's `SH_DEGREE`); ignored when
|
|
1502
|
+
* `shTextureCount` is 0. The compound keeps the MAX SH degree across its parts.
|
|
1503
|
+
* @param needsRotationScale when true, converts the compound's rotation/scale textures to a shared render-target
|
|
1504
|
+
* half-float atlas the streaming engine decodes into, so the streamed splats participate in voxel-IBL shadows.
|
|
1505
|
+
* @returns a handle used to populate and control the reserved region
|
|
1506
|
+
*/
|
|
1507
|
+
reserveStreamingPart(capacity, worldMatrix = Matrix.Identity(), name = this.name + "_streamingPart", shTextureCount = 0, shDegree = 0, needsRotationScale = false) {
|
|
1508
|
+
// Require a positive integer (reject e.g. 0.5, which would floor to a 0-capacity region), and cap at the
|
|
1509
|
+
// largest atlas the backend can allocate (a square texture at the max width) so an absurd value can't drive
|
|
1510
|
+
// an out-of-memory typed-array / MRT allocation.
|
|
1511
|
+
if (!Number.isSafeInteger(capacity) || capacity < 1) {
|
|
1512
|
+
throw new Error("reserveStreamingPart: capacity must be a positive integer");
|
|
1513
|
+
}
|
|
1514
|
+
// Validate the SH sizing parameters — they drive the SH_DEGREE define and the SH texture allocation, so a
|
|
1515
|
+
// fractional/negative/huge value would give an invalid layout or an unbounded allocation. The draw path
|
|
1516
|
+
// supports degree 0..4 (shTexture0..4); SH is all-or-nothing; the texture count must match the degree.
|
|
1517
|
+
const maxSupportedShDegree = 4;
|
|
1518
|
+
if (!Number.isSafeInteger(shDegree) || shDegree < 0 || shDegree > maxSupportedShDegree) {
|
|
1519
|
+
throw new Error(`reserveStreamingPart: shDegree must be an integer in [0, ${maxSupportedShDegree}]`);
|
|
1520
|
+
}
|
|
1521
|
+
if (!Number.isSafeInteger(shTextureCount) || shTextureCount < 0) {
|
|
1522
|
+
throw new Error("reserveStreamingPart: shTextureCount must be a non-negative integer");
|
|
1523
|
+
}
|
|
1524
|
+
if (shDegree > 0 !== shTextureCount > 0) {
|
|
1525
|
+
throw new Error("reserveStreamingPart: shDegree and shTextureCount must both be positive (SH) or both zero (no SH)");
|
|
1526
|
+
}
|
|
1527
|
+
if (shDegree > 0) {
|
|
1528
|
+
const expectedShTextureCount = Math.ceil((((shDegree + 1) * (shDegree + 1) - 1) * 3) / _GaussianSplattingBytesPerShTexel);
|
|
1529
|
+
if (shTextureCount !== expectedShTextureCount) {
|
|
1530
|
+
throw new Error(`reserveStreamingPart: shTextureCount ${shTextureCount} does not match shDegree ${shDegree} (expected ${expectedShTextureCount})`);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
const maxTextureSize = this._scene.getEngine().getCaps().maxTextureSize;
|
|
1534
|
+
const maxCapacity = maxTextureSize * maxTextureSize;
|
|
1535
|
+
// Row-align the region so a later GPU relayout (defrag under a memory budget) can be scoped to whole
|
|
1536
|
+
// atlas rows via scissor without ever touching a preceding part that shares a row.
|
|
1537
|
+
// - Front alignment: start the usable region on the next row boundary. This only consumes the
|
|
1538
|
+
// preceding parts' already-allocated last-row tail padding, so it costs no extra memory.
|
|
1539
|
+
// - Capacity alignment: pad the region up to a whole number of rows so its end is a row boundary too
|
|
1540
|
+
// (needed when another part follows, e.g. multiple streaming parts).
|
|
1541
|
+
const atlasWidth = this._getTextureSize(1).x;
|
|
1542
|
+
const startOffset = this._vertexCount; // first atlas index the reserved part occupies
|
|
1543
|
+
const alignedBase = Math.ceil(startOffset / atlasWidth) * atlasWidth;
|
|
1544
|
+
const frontPad = alignedBase - startOffset; // invisible padding that fills the preceding row
|
|
1545
|
+
const alignedCapacity = Math.ceil(capacity / atlasWidth) * atlasWidth;
|
|
1546
|
+
const regionSplats = frontPad + alignedCapacity;
|
|
1547
|
+
// Validate the RESULTING atlas, not just `capacity`: existing splats, row-alignment padding, and the trailing
|
|
1548
|
+
// empty sentinel slot (`+ 1`, see _getTextureSize) all count against the device's square-texture limit.
|
|
1549
|
+
// _getTextureSize would silently clamp and drop the sentinel past the limit, so reject here — before any
|
|
1550
|
+
// state is mutated below, so a rejected reservation leaves the compound intact.
|
|
1551
|
+
const projectedSplats = startOffset + regionSplats;
|
|
1552
|
+
if (projectedSplats + 1 > maxCapacity) {
|
|
1553
|
+
throw new Error(`reserveStreamingPart: capacity ${capacity} would grow the atlas to ${projectedSplats} splats (plus a sentinel), exceeding the maximum atlas capacity ${maxCapacity}`);
|
|
1554
|
+
}
|
|
1555
|
+
// Back the atlas with a render-targetable MRT so a streaming engine can GPU-decode into the reserved
|
|
1556
|
+
// region. Must be set before _addPartsInternal so the (forced) full rebuild builds MRT attachments and
|
|
1557
|
+
// allocates covariance B as RGBA. Harmless/no-op if a streaming part was already reserved.
|
|
1558
|
+
this._useMrtAtlas = true;
|
|
1559
|
+
this._useRGBACovariants = true;
|
|
1560
|
+
// Higher-order SH: convert the SH textures to render-targetable integer MRTs so the stream can bake SH into
|
|
1561
|
+
// its region, and set the compound's SH degree so the draw path lights the decoded splats. Sized for the
|
|
1562
|
+
// MAX SH texture count across parts (a later higher-degree part grows it; lower-degree parts neutral-fill).
|
|
1563
|
+
if (shTextureCount > 0 && shDegree > 0) {
|
|
1564
|
+
this._useShMrtAtlas = true;
|
|
1565
|
+
this._shMrtAtlasTextureCount = Math.max(this._shMrtAtlasTextureCount, shTextureCount);
|
|
1566
|
+
// Streaming SH degree is tracked separately from _maxShDegree (which folds in static parts too) and is
|
|
1567
|
+
// recomputed from the surviving states on removal — so it shrinks correctly. The _addPartsInternal
|
|
1568
|
+
// rebuild below folds it into _shDegree/_maxShDegree.
|
|
1569
|
+
this._streamingShDegree = Math.max(this._streamingShDegree, shDegree);
|
|
1570
|
+
}
|
|
1571
|
+
// Rotation/scale: convert the rotation textures to a render-targetable half-float atlas so the stream can
|
|
1572
|
+
// decode rotation/scale into its region for voxel-IBL shadows. `_needsRotationScaleTextures` makes _makeSplat
|
|
1573
|
+
// fill static parts' rotation data (CPU-uploaded into the shared atlas by the rebuild) too.
|
|
1574
|
+
if (needsRotationScale) {
|
|
1575
|
+
this._useRotMrtAtlas = true;
|
|
1576
|
+
this._needsRotationScaleTextures = true;
|
|
1577
|
+
}
|
|
1578
|
+
// Running local-space bounds of the region's written centers; grown as splats are populated.
|
|
1579
|
+
const boundsMin = new Vector3(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
|
|
1580
|
+
const boundsMax = new Vector3(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE);
|
|
1581
|
+
// Synthetic placeholder source: no data, so _addPartsInternal leaves the region zeroed (invisible)
|
|
1582
|
+
// while still assigning a part index, filling _partIndices, growing the atlas + _splatPositions,
|
|
1583
|
+
// creating the proxy, and posting the (larger) position/interval set to the sort worker.
|
|
1584
|
+
const reservedSource = {
|
|
1585
|
+
name,
|
|
1586
|
+
_vertexCount: regionSplats,
|
|
1587
|
+
_splatsData: null,
|
|
1588
|
+
_shData: null,
|
|
1589
|
+
_shDegree: 0,
|
|
1590
|
+
isCompound: false,
|
|
1591
|
+
getWorldMatrix: () => worldMatrix,
|
|
1592
|
+
getBoundingInfo: () => new BoundingInfo(Vector3.ZeroReadOnly, Vector3.ZeroReadOnly),
|
|
1593
|
+
dispose: () => { },
|
|
1594
|
+
_isReservedEmpty: true,
|
|
1595
|
+
};
|
|
1596
|
+
const { proxyMeshes, assignedPartIndices } = this._addPartsInternal([reservedSource], false);
|
|
1597
|
+
const proxy = proxyMeshes[0];
|
|
1598
|
+
// Usable region starts at the row-aligned base (past the front padding) and spans the aligned capacity.
|
|
1599
|
+
// (proxy._splatsDataOffset === startOffset; the usable base is startOffset + frontPad === alignedBase.)
|
|
1600
|
+
// `base`/`partIndex` live in mutable state so compactAtlas can RELOCATE this region (new base/index) in
|
|
1601
|
+
// place; the handle's getters and closures read the state, so the streaming engine's handle stays valid.
|
|
1602
|
+
const state = {
|
|
1603
|
+
proxy,
|
|
1604
|
+
base: alignedBase,
|
|
1605
|
+
partIndex: assignedPartIndices[0],
|
|
1606
|
+
capacity: alignedCapacity,
|
|
1607
|
+
boundsMin,
|
|
1608
|
+
boundsMax,
|
|
1609
|
+
localRanges: null,
|
|
1610
|
+
shTextureCount: shTextureCount > 0 && shDegree > 0 ? shTextureCount : 0,
|
|
1611
|
+
shDegree: shTextureCount > 0 && shDegree > 0 ? shDegree : 0,
|
|
1612
|
+
needsRotationScale,
|
|
1613
|
+
removed: false,
|
|
1614
|
+
};
|
|
1615
|
+
this._streamingStates.push(state);
|
|
1616
|
+
capacity = alignedCapacity;
|
|
1617
|
+
// The handle's live getters need a stable reference to this compound (its atlas textures can be
|
|
1618
|
+
// recreated on a later rebuild), so alias it for the closures below.
|
|
1619
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
1620
|
+
const compound = this;
|
|
1621
|
+
const applyBounds = () => {
|
|
1622
|
+
if (boundsMin.x <= boundsMax.x) {
|
|
1623
|
+
proxy.setBoundingInfo(new BoundingInfo(boundsMin.clone(), boundsMax.clone()));
|
|
1624
|
+
compound._updateBoundingInfoFromProxies();
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
// Rejects any mutating call after the part was removed, so a retained stale handle can't touch a surviving
|
|
1628
|
+
// part that inherited this region's (now-reused) part index/base.
|
|
1629
|
+
const assertLive = (method) => {
|
|
1630
|
+
if (state.removed) {
|
|
1631
|
+
throw new Error(`${method}: this streaming part has been removed`);
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
// Enforces the documented local part boundary [0, capacity) so a handle call can never address another
|
|
1635
|
+
// part's atlas region (the region base is added to these local coordinates before use).
|
|
1636
|
+
const assertLocalRange = (method, offset, count) => {
|
|
1637
|
+
if (!Number.isInteger(offset) || !Number.isInteger(count) || offset < 0 || count < 0 || offset + count > state.capacity) {
|
|
1638
|
+
throw new Error(`${method}: local range [${offset}, ${offset + count}) is outside the reserved region [0, ${state.capacity})`);
|
|
1639
|
+
}
|
|
1640
|
+
};
|
|
1641
|
+
const handle = {
|
|
1642
|
+
proxy,
|
|
1643
|
+
capacity,
|
|
1644
|
+
get partIndex() {
|
|
1645
|
+
return state.partIndex;
|
|
1646
|
+
},
|
|
1647
|
+
get base() {
|
|
1648
|
+
return state.base;
|
|
1649
|
+
},
|
|
1650
|
+
get centersTexture() {
|
|
1651
|
+
return compound.centersTexture;
|
|
1652
|
+
},
|
|
1653
|
+
get covariancesATexture() {
|
|
1654
|
+
return compound.covariancesATexture;
|
|
1655
|
+
},
|
|
1656
|
+
get covariancesBTexture() {
|
|
1657
|
+
return compound.covariancesBTexture;
|
|
1658
|
+
},
|
|
1659
|
+
get colorsTexture() {
|
|
1660
|
+
return compound.colorsTexture;
|
|
1661
|
+
},
|
|
1662
|
+
get splatPositions() {
|
|
1663
|
+
return compound._splatPositions;
|
|
1664
|
+
},
|
|
1665
|
+
get mrtAtlas() {
|
|
1666
|
+
return compound._mrtAtlas;
|
|
1667
|
+
},
|
|
1668
|
+
get shMrtAtlas() {
|
|
1669
|
+
return compound._shMrtAtlas;
|
|
1670
|
+
},
|
|
1671
|
+
get rotMrtAtlas() {
|
|
1672
|
+
return compound._rotMrtAtlas;
|
|
1673
|
+
},
|
|
1674
|
+
get atlasWidth() {
|
|
1675
|
+
return compound._getTextureSize(compound._vertexCount).x;
|
|
1676
|
+
},
|
|
1677
|
+
get isDepthSortSettled() {
|
|
1678
|
+
return compound._isDepthSortSettled;
|
|
1679
|
+
},
|
|
1680
|
+
setActiveRanges: (localRanges) => {
|
|
1681
|
+
assertLive("setActiveRanges");
|
|
1682
|
+
// Keep every range inside this part's region so it can never activate another part's splats.
|
|
1683
|
+
if (localRanges) {
|
|
1684
|
+
for (const r of localRanges) {
|
|
1685
|
+
assertLocalRange("setActiveRanges", r.offset, r.count);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
// Remember the LOCAL ranges so compactAtlas can re-post them at the region's new base.
|
|
1689
|
+
state.localRanges = localRanges ? localRanges.map((r) => ({ offset: r.offset, count: r.count })) : null;
|
|
1690
|
+
const globalRanges = localRanges ? localRanges.map((r) => ({ offset: state.base + r.offset, count: r.count })) : null;
|
|
1691
|
+
compound.setPartSplatRanges(state.partIndex, globalRanges);
|
|
1692
|
+
},
|
|
1693
|
+
writeSplats: (localOffset, count, splatsData) => {
|
|
1694
|
+
assertLive("writeSplats");
|
|
1695
|
+
// Keep the write inside this part's region so it can never touch another part's atlas texels.
|
|
1696
|
+
assertLocalRange("writeSplats", localOffset, count);
|
|
1697
|
+
compound._writeStreamingSplats(state.base + localOffset, count, splatsData, boundsMin, boundsMax);
|
|
1698
|
+
applyBounds();
|
|
1699
|
+
},
|
|
1700
|
+
postPositionsRange: (localOffset, count) => {
|
|
1701
|
+
assertLive("postPositionsRange");
|
|
1702
|
+
assertLocalRange("postPositionsRange", localOffset, count);
|
|
1703
|
+
compound._postWorkerPositionsRange(state.base + localOffset, count);
|
|
1704
|
+
},
|
|
1705
|
+
expandBounds: (min, max) => {
|
|
1706
|
+
assertLive("expandBounds");
|
|
1707
|
+
boundsMin.minimizeInPlace(min);
|
|
1708
|
+
boundsMax.maximizeInPlace(max);
|
|
1709
|
+
applyBounds();
|
|
1710
|
+
},
|
|
1711
|
+
notifyDataChanged: () => {
|
|
1712
|
+
assertLive("notifyDataChanged");
|
|
1713
|
+
compound._notifyWorkerNewData();
|
|
1714
|
+
},
|
|
1715
|
+
onBeforeAtlasRebuild: (callback) => {
|
|
1716
|
+
const observer = compound._onBeforeAtlasRebuildObservable.add(callback);
|
|
1717
|
+
return () => compound._onBeforeAtlasRebuildObservable.remove(observer);
|
|
1718
|
+
},
|
|
1719
|
+
onAfterAtlasRebuild: (callback) => {
|
|
1720
|
+
const observer = compound._onAfterAtlasRebuildObservable.add(callback);
|
|
1721
|
+
return () => compound._onAfterAtlasRebuildObservable.remove(observer);
|
|
1722
|
+
},
|
|
1723
|
+
};
|
|
1724
|
+
// Mark AFTER the successful internal _addPartsInternal so its own call isn't blocked by the guard.
|
|
1725
|
+
this._hasStreamingPart = true;
|
|
1726
|
+
return handle;
|
|
1727
|
+
}
|
|
1081
1728
|
/**
|
|
1082
1729
|
* Serialize current GaussianSplattingMesh
|
|
1083
1730
|
* @param serializationObject defines the object which will receive the serialization data
|