@babylonjs/loaders 9.26.1 → 9.26.2
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/SPLAT/gaussianSplattingStream.d.ts +79 -25
- package/SPLAT/gaussianSplattingStream.js +320 -95
- package/SPLAT/gaussianSplattingStream.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_interactivity/declarationMapper.d.ts +110 -3
- package/glTF/2.0/Extensions/KHR_interactivity/declarationMapper.js +468 -74
- package/glTF/2.0/Extensions/KHR_interactivity/declarationMapper.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_interactivity/flowGraphEventReferenceBlock.d.ts +47 -0
- package/glTF/2.0/Extensions/KHR_interactivity/flowGraphEventReferenceBlock.js +68 -0
- package/glTF/2.0/Extensions/KHR_interactivity/flowGraphEventReferenceBlock.js.map +1 -0
- package/glTF/2.0/Extensions/KHR_interactivity/flowGraphGLTFDataProvider.d.ts +2 -2
- package/glTF/2.0/Extensions/KHR_interactivity/flowGraphGLTFDataProvider.js +1 -1
- package/glTF/2.0/Extensions/KHR_interactivity/flowGraphGLTFDataProvider.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_interactivity/flowGraphObjectReferenceBlock.d.ts +24 -0
- package/glTF/2.0/Extensions/KHR_interactivity/flowGraphObjectReferenceBlock.js +38 -0
- package/glTF/2.0/Extensions/KHR_interactivity/flowGraphObjectReferenceBlock.js.map +1 -0
- package/glTF/2.0/Extensions/KHR_interactivity/flowGraphUnsupportedInteractivityBlock.d.ts +41 -0
- package/glTF/2.0/Extensions/KHR_interactivity/flowGraphUnsupportedInteractivityBlock.js +63 -0
- package/glTF/2.0/Extensions/KHR_interactivity/flowGraphUnsupportedInteractivityBlock.js.map +1 -0
- package/glTF/2.0/Extensions/KHR_interactivity/index.d.ts +5 -0
- package/glTF/2.0/Extensions/KHR_interactivity/index.js +5 -0
- package/glTF/2.0/Extensions/KHR_interactivity/index.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityGraphModel.d.ts +112 -0
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityGraphModel.js +773 -0
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityGraphModel.js.map +1 -0
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityGraphParser.d.ts +18 -10
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityGraphParser.js +299 -54
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityGraphParser.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityHostResolver.d.ts +2 -1
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityHostResolver.js +7 -2
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityHostResolver.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityNodeState.d.ts +24 -0
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityNodeState.js +89 -0
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityNodeState.js.map +1 -0
- package/glTF/2.0/Extensions/KHR_interactivity/pure.d.ts +5 -0
- package/glTF/2.0/Extensions/KHR_interactivity/pure.js +5 -0
- package/glTF/2.0/Extensions/KHR_interactivity/pure.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_interactivity.pure.d.ts +55 -1
- package/glTF/2.0/Extensions/KHR_interactivity.pure.js +124 -76
- package/glTF/2.0/Extensions/KHR_interactivity.pure.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_interactivity.types.d.ts +18 -1
- package/glTF/2.0/Extensions/KHR_interactivity.types.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_node_hoverability.pure.d.ts +2 -0
- package/glTF/2.0/Extensions/KHR_node_hoverability.pure.js +114 -56
- package/glTF/2.0/Extensions/KHR_node_hoverability.pure.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_node_selectability.pure.d.ts +2 -0
- package/glTF/2.0/Extensions/KHR_node_selectability.pure.js +80 -35
- package/glTF/2.0/Extensions/KHR_node_selectability.pure.js.map +1 -1
- package/glTF/2.0/Extensions/interactivityRefPathToObjectConverter.js +3 -2
- package/glTF/2.0/Extensions/interactivityRefPathToObjectConverter.js.map +1 -1
- package/glTF/2.0/Extensions/objectModelMapping.d.ts +4 -2
- package/glTF/2.0/Extensions/objectModelMapping.js +33 -4
- package/glTF/2.0/Extensions/objectModelMapping.js.map +1 -1
- package/glTF/2.0/glTFLoader.pure.d.ts +1 -1
- package/glTF/2.0/glTFLoader.pure.js +9 -4
- package/glTF/2.0/glTFLoader.pure.js.map +1 -1
- package/glTF/2.0/glTFLoaderExtension.d.ts +2 -1
- package/glTF/2.0/glTFLoaderExtension.js.map +1 -1
- package/package.json +3 -3
|
@@ -21,6 +21,11 @@ const EnvironmentFileId = -1;
|
|
|
21
21
|
// Core bytes per resident splat: the four work-buffer textures cost 16+16+16+4 = 52 bytes on the GPU, plus ~32
|
|
22
22
|
// bytes of CPU position/sort data. `_resolveResidentBudget` adds the SH and rotation/scale texture cost on top.
|
|
23
23
|
const BytesPerResidentSplat = 84;
|
|
24
|
+
const MaxSupportedShDegree = 4;
|
|
25
|
+
const MaxSupportedShTextureCount = 5;
|
|
26
|
+
const ProgressiveMetadataFileThreshold = 32;
|
|
27
|
+
const DesktopAutomaticResidentMemoryMb = 512;
|
|
28
|
+
const MobileAutomaticResidentMemoryMb = 256;
|
|
24
29
|
// Scratch objects reused by the per-frame optimal-LOD evaluation (avoids per-call allocations).
|
|
25
30
|
const TmpInvWorld = new Matrix();
|
|
26
31
|
const TmpLocalCamera = new Vector3();
|
|
@@ -94,6 +99,9 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
94
99
|
* @param options streaming options
|
|
95
100
|
*/
|
|
96
101
|
constructor(name, metadata, rootUrl, scene, options = {}) {
|
|
102
|
+
// Validate before super registers the mesh with the scene, so invalid options leave no partial entity.
|
|
103
|
+
const maxResidentSplats = GaussianSplattingStream._NormalizeResidentLimit(options.maxResidentSplats ?? 0, "maxResidentSplats", true);
|
|
104
|
+
const memoryBudgetMb = GaussianSplattingStream._NormalizeResidentLimit(options.memoryBudgetMb ?? 0, "memoryBudgetMb", false);
|
|
97
105
|
super(name, null, scene, false);
|
|
98
106
|
// Flat list of leaf nodes that carry renderable LOD entries (used by the LOD heuristic and debug).
|
|
99
107
|
this._leafNodes = [];
|
|
@@ -118,7 +126,7 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
118
126
|
this._hostBudgetAllocation = null;
|
|
119
127
|
// Frustum LOD bias: when enabled, nodes outside the camera frustum are rendered at their coarsest LOD.
|
|
120
128
|
this._frustumCulling = true;
|
|
121
|
-
// Reused
|
|
129
|
+
// Reused local-space frustum planes and local-to-clip scratch matrix (avoids per-frame allocation).
|
|
122
130
|
this._frustumPlanes = [
|
|
123
131
|
new Plane(0, 0, 0, 0),
|
|
124
132
|
new Plane(0, 0, 0, 0),
|
|
@@ -127,6 +135,7 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
127
135
|
new Plane(0, 0, 0, 0),
|
|
128
136
|
new Plane(0, 0, 0, 0),
|
|
129
137
|
];
|
|
138
|
+
this._cullCameraViewProj = new Matrix();
|
|
130
139
|
this._cullViewProj = new Matrix();
|
|
131
140
|
// Reused per-leaf "inside any active camera's frustum" accumulator for the union frustum test (avoids per-frame allocation).
|
|
132
141
|
this._frustumScratch = [];
|
|
@@ -150,6 +159,8 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
150
159
|
this._fileCounts = new Map();
|
|
151
160
|
// Cached SOG metadata per file so on-demand decodes don't refetch the meta.json.
|
|
152
161
|
this._fileMeta = new Map();
|
|
162
|
+
// Unique source files required by the coarsest entry of at least one leaf.
|
|
163
|
+
this._baseFileIds = new Set();
|
|
153
164
|
// Files whose splats have been fully GPU-decoded into the work buffer (render-safe).
|
|
154
165
|
this._decodedFiles = new Set();
|
|
155
166
|
// Files whose decode is currently in flight (dedupes concurrent requests).
|
|
@@ -161,9 +172,11 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
161
172
|
this._fileRefs = new Map();
|
|
162
173
|
// Files whose in-flight decode was cancelled; checked at decode checkpoints to bail out cooperatively.
|
|
163
174
|
this._cancelledDecodes = new Set();
|
|
164
|
-
// Eviction
|
|
175
|
+
// Eviction is enabled for progressive metadata or capacity below the complete source size.
|
|
165
176
|
this._evictionEnabled = false;
|
|
166
177
|
this._residentBudget = 0;
|
|
178
|
+
// Padding + environment + unique whole coarse source files. Zero until coarse metadata has resolved.
|
|
179
|
+
this._minimumResidentSplats = 0;
|
|
167
180
|
// Raw budget options; the final `_residentBudget` is resolved from these once the SH/rotation byte cost is known
|
|
168
181
|
// (after the metadata pre-pass), so the memory budget accounts for the extra baked SH and rotation textures.
|
|
169
182
|
this._maxResidentSplats = 0;
|
|
@@ -182,6 +195,7 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
182
195
|
// Per-frame LOD streaming loop; installed once the base layer is ready.
|
|
183
196
|
this._lodObserver = null;
|
|
184
197
|
this._baseLayerReady = false;
|
|
198
|
+
this._metadataReady = false;
|
|
185
199
|
// Throttling state for the per-frame LOD loop: each active camera's world position at the last LOD evaluation, so
|
|
186
200
|
// a re-eval is gated on the camera set changing or any camera translating past `_lodUpdateDistance`.
|
|
187
201
|
this._framesSinceLodUpdate = 0;
|
|
@@ -281,14 +295,9 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
281
295
|
if (options.evictionCooldownFrames !== undefined) {
|
|
282
296
|
this._evictionCooldownFrames = Math.max(0, Math.floor(options.evictionCooldownFrames));
|
|
283
297
|
}
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
this._maxResidentSplats = Math.floor(options.maxResidentSplats);
|
|
288
|
-
}
|
|
289
|
-
if (options.memoryBudgetMb !== undefined && options.memoryBudgetMb > 0) {
|
|
290
|
-
this._memoryBudgetMb = options.memoryBudgetMb;
|
|
291
|
-
}
|
|
298
|
+
// Resolve the capacity after coarse metadata and the fixed SH/rotation layout are known.
|
|
299
|
+
this._maxResidentSplats = maxResidentSplats;
|
|
300
|
+
this._memoryBudgetMb = memoryBudgetMb;
|
|
292
301
|
this._downloadManager = new GaussianSplattingDownloadManager({
|
|
293
302
|
maxConcurrent: options.maxConcurrentDownloads,
|
|
294
303
|
maxRetries: options.maxDownloadRetries,
|
|
@@ -335,7 +344,8 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
335
344
|
// deferred: _streamAllAsync resolves it once the base layer has decoded. If it finishes WITHOUT the part ever
|
|
336
345
|
// becoming ready (empty stream) or throws, dispose the controller so a hosted stream doesn't leave its work
|
|
337
346
|
// buffer and reserved region allocated — the synchronous AddGaussianSplattingStreamPart never awaits, so it
|
|
338
|
-
// can't clean up itself.
|
|
347
|
+
// can't clean up itself. Standalone startup failures also dispose their partial resources.
|
|
348
|
+
// `_partReadySettled` distinguishes a genuine success (leave it running) from a
|
|
339
349
|
// finished-but-never-ready result (dispose).
|
|
340
350
|
// eslint-disable-next-line github/no-then
|
|
341
351
|
void this._streamAllAsync().then(() => {
|
|
@@ -348,7 +358,7 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
348
358
|
this._lod0SplatCount = Object.freeze({ status: "unavailable" });
|
|
349
359
|
Logger.Error("GaussianSplattingStream: streaming failed: " + (e?.message ?? e));
|
|
350
360
|
this._rejectPartReady("GaussianSplattingStream: streaming failed: " + (e?.message ?? e));
|
|
351
|
-
if (
|
|
361
|
+
if (!this._disposed) {
|
|
352
362
|
this._disposeAndReclaim();
|
|
353
363
|
}
|
|
354
364
|
});
|
|
@@ -486,7 +496,7 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
486
496
|
* @returns true when no loading work remains
|
|
487
497
|
*/
|
|
488
498
|
_isLoadingIdle() {
|
|
489
|
-
return this._baseLayerReady && this._decodeQueue.length === 0 && this._loadingFiles.size === 0 && this._downloadManager.isIdle;
|
|
499
|
+
return this._baseLayerReady && this._metadataReady && this._decodeQueue.length === 0 && this._loadingFiles.size === 0 && this._downloadManager.isIdle;
|
|
490
500
|
}
|
|
491
501
|
/**
|
|
492
502
|
* Finest (most detailed) LOD level any node is allowed to render. `0` allows full detail (level 0);
|
|
@@ -538,12 +548,23 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
538
548
|
/**
|
|
539
549
|
* The resolved maximum number of splats kept resident in the work buffer. This combines
|
|
540
550
|
* {@link IGaussianSplattingStreamOptions.maxResidentSplats} and {@link IGaussianSplattingStreamOptions.memoryBudgetMb},
|
|
541
|
-
* taking the smaller limit
|
|
551
|
+
* taking the smaller positive limit and raising it to {@link minimumResidentSplats}. With neither limit set,
|
|
552
|
+
* the complete source size is retained when known; otherwise a bounded device-tiered default is used.
|
|
553
|
+
* The fixed capacity is capped at the device texture limit. `0` means initial capacity has not been resolved.
|
|
542
554
|
* @experimental
|
|
543
555
|
*/
|
|
544
556
|
get residentSplatBudget() {
|
|
545
557
|
return this._residentBudget;
|
|
546
558
|
}
|
|
559
|
+
/**
|
|
560
|
+
* Minimum fixed work-buffer capacity required for the complete coarse representation: one invisible padding
|
|
561
|
+
* splat, the included environment, and every unique whole coarse source file. It is `0` until the required
|
|
562
|
+
* coarse metadata has resolved. Initial residency options below this value are raised to it.
|
|
563
|
+
* @experimental
|
|
564
|
+
*/
|
|
565
|
+
get minimumResidentSplats() {
|
|
566
|
+
return this._minimumResidentSplats;
|
|
567
|
+
}
|
|
547
568
|
/**
|
|
548
569
|
* The total number of splats represented by valid level-0 leaf entries. This remains pending while source
|
|
549
570
|
* metadata is loading and is unavailable when no applicable level-0 entries exist or required metadata fails.
|
|
@@ -748,8 +769,8 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
748
769
|
super.dispose(doNotRecurse);
|
|
749
770
|
}
|
|
750
771
|
/**
|
|
751
|
-
* Disposes this stream
|
|
752
|
-
*
|
|
772
|
+
* Disposes this stream and, when hosted, compacts the host once to reclaim its tombstoned region's reserved
|
|
773
|
+
* rows. Used on a definitive load failure / empty result — a discrete, one-off reclaim, versus a bare
|
|
753
774
|
* {@link dispose} that only tombstones so tearing down several parts doesn't rebuild the atlas repeatedly.
|
|
754
775
|
*/
|
|
755
776
|
_disposeAndReclaim() {
|
|
@@ -762,7 +783,7 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
762
783
|
}
|
|
763
784
|
/**
|
|
764
785
|
* The world matrix that actually places this stream's splats, used to map the camera into the space the
|
|
765
|
-
* node bounds live in (for LOD distance) and to build
|
|
786
|
+
* node bounds live in (for LOD distance) and to build camera-local frusta (for frustum culling). Standalone:
|
|
766
787
|
* this controller mesh carries the transform. Hosted: this controller is a hidden, unplaced node — the splats
|
|
767
788
|
* are placed by the reserved part's proxy (SOG up-axis basis composed with the host's placement), so LOD and
|
|
768
789
|
* culling MUST use the proxy's world matrix or they compute distances/frustum tests in the wrong space
|
|
@@ -1118,51 +1139,80 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
1118
1139
|
node.activeLod = undefined;
|
|
1119
1140
|
node.lodCooldown = 0;
|
|
1120
1141
|
node.inFrustum = true;
|
|
1121
|
-
//
|
|
1142
|
+
// Static local bounds; each camera's frustum is transformed into this space for evaluation.
|
|
1122
1143
|
node.cullBounds = new BoundingInfo(Vector3.FromArray(bmin), Vector3.FromArray(bmax));
|
|
1123
1144
|
this._leafNodes.push(node);
|
|
1124
1145
|
}
|
|
1125
1146
|
/**
|
|
1126
|
-
* Streams the scene
|
|
1127
|
-
*
|
|
1128
|
-
*
|
|
1147
|
+
* Streams the scene. Large streams fetch only required coarse metadata before allocating and decoding the
|
|
1148
|
+
* complete coarse layer; finer metadata starts afterwards so it cannot occupy the download queue ahead of
|
|
1149
|
+
* visible geometry. Small streams and streams containing only coarse files retain an exact all-file capacity upper bound.
|
|
1129
1150
|
*/
|
|
1130
1151
|
async _streamAllAsync() {
|
|
1131
|
-
// Step 1: learn splat counts for the environment and every referenced LOD file (cheap meta only). This also
|
|
1132
|
-
// resolves the max SH degree, so the resident-splat budget can now be sized with the SH/rotation byte cost.
|
|
1133
1152
|
const fileIds = this._collectAllFileIds();
|
|
1134
|
-
const
|
|
1135
|
-
this.
|
|
1153
|
+
const baseFileIds = this._collectBaseFileIds();
|
|
1154
|
+
this._baseFileIds.clear();
|
|
1155
|
+
for (const fileId of baseFileIds) {
|
|
1156
|
+
this._baseFileIds.add(fileId);
|
|
1157
|
+
}
|
|
1158
|
+
if (this._leafNodes.length > 0 && baseFileIds.length === 0) {
|
|
1159
|
+
throw new Error("GaussianSplattingStream: no valid coarse source files were referenced.");
|
|
1160
|
+
}
|
|
1161
|
+
const progressiveMetadata = fileIds.length > ProgressiveMetadataFileThreshold && baseFileIds.length < fileIds.length;
|
|
1162
|
+
if (progressiveMetadata && this._decodeSh) {
|
|
1163
|
+
// Finer metadata is deliberately unknown during allocation. Reserve the renderer's complete supported
|
|
1164
|
+
// degree-4 layout so a later file can never require a fixed-atlas resize or silently lose its SH.
|
|
1165
|
+
this._streamShDegree = MaxSupportedShDegree;
|
|
1166
|
+
this._shTextureCount = MaxSupportedShTextureCount;
|
|
1167
|
+
}
|
|
1168
|
+
// The environment keeps its existing initial ordering. On large streams only coarse source metadata follows
|
|
1169
|
+
// it, leaving the FIFO download manager free for coarse textures before the hundreds of finer metadata files.
|
|
1170
|
+
const initialFileIds = progressiveMetadata ? baseFileIds : fileIds;
|
|
1171
|
+
const envCount = await this._gatherCountsAsync(initialFileIds);
|
|
1136
1172
|
if (this._disposed) {
|
|
1137
1173
|
return;
|
|
1138
1174
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1175
|
+
for (const fileId of baseFileIds) {
|
|
1176
|
+
const count = this._fileCounts.get(fileId);
|
|
1177
|
+
if (!this._fileMeta.has(fileId) || count === undefined || count <= 0) {
|
|
1178
|
+
throw new Error(`GaussianSplattingStream: required coarse metadata for file ${fileId} is unavailable or invalid.`);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
this._resolveMinimumResidentSplats(baseFileIds, envCount);
|
|
1182
|
+
if (!progressiveMetadata) {
|
|
1183
|
+
this._resolveLod0SplatCount();
|
|
1184
|
+
this._metadataReady = true;
|
|
1185
|
+
}
|
|
1142
1186
|
// Index 0 is reserved as a never-decoded padding splat: the sort worker and index buffer pad unused
|
|
1143
1187
|
// slots with index 0, and leaving that slot zeroed (center.w = 0 => zero covariance, alpha 0) makes
|
|
1144
1188
|
// the padding invisible instead of ghosting a copy of the first real splat.
|
|
1145
|
-
let fullCapacity =
|
|
1146
|
-
if (
|
|
1147
|
-
fullCapacity
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1189
|
+
let fullCapacity = null;
|
|
1190
|
+
if (!progressiveMetadata) {
|
|
1191
|
+
fullCapacity = 1 + envCount;
|
|
1192
|
+
for (const fileId of fileIds) {
|
|
1193
|
+
const count = this._fileCounts.get(fileId);
|
|
1194
|
+
if (count !== undefined && count > 0) {
|
|
1195
|
+
fullCapacity = GaussianSplattingStream._SafeAddCounts(fullCapacity, count, "stream capacity");
|
|
1196
|
+
}
|
|
1153
1197
|
}
|
|
1154
1198
|
}
|
|
1155
|
-
if (
|
|
1156
|
-
|
|
1199
|
+
if (this._minimumResidentSplats <= 1) {
|
|
1200
|
+
throw new Error("GaussianSplattingStream: stream produced no splats.");
|
|
1201
|
+
}
|
|
1202
|
+
let largestBaseFileCount = 0;
|
|
1203
|
+
for (const fileId of baseFileIds) {
|
|
1204
|
+
largestBaseFileCount = Math.max(largestBaseFileCount, this._fileCounts.get(fileId));
|
|
1157
1205
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1206
|
+
const capacity = this._resolveResidentBudget(fullCapacity, largestBaseFileCount);
|
|
1207
|
+
// Progressive metadata always implies that additional source files may need to replace one another. For an
|
|
1208
|
+
// exact small stream, eviction is necessary only when its chosen capacity is below the full source set.
|
|
1209
|
+
this._evictionEnabled = progressiveMetadata || (fullCapacity !== null && capacity < fullCapacity);
|
|
1161
1210
|
this._residency = new GaussianSplattingResidencyController(capacity, this._evictionCooldownFrames, (file) => this._onFileEvicted(file));
|
|
1162
|
-
//
|
|
1211
|
+
// Padding is always pinned. Reserve the environment now, but pin it only after a successful decode so
|
|
1212
|
+
// failed environment data can release its allocation before coarse/fine files need that space.
|
|
1163
1213
|
this._residency.pin(PaddingFileId, 1);
|
|
1164
1214
|
if (envCount > 0) {
|
|
1165
|
-
const envOffset = this._residency.
|
|
1215
|
+
const envOffset = this._residency.allocate(EnvironmentFileId, envCount);
|
|
1166
1216
|
if (envOffset !== null) {
|
|
1167
1217
|
this._environmentRange = { offset: envOffset, count: envCount };
|
|
1168
1218
|
}
|
|
@@ -1270,35 +1320,48 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
1270
1320
|
return;
|
|
1271
1321
|
}
|
|
1272
1322
|
}
|
|
1273
|
-
//
|
|
1323
|
+
// Decode the environment, then every unique coarse source. Each completed coarse file immediately
|
|
1324
|
+
// promotes its leaves and publishes their ranges; incomplete essential coarse data fails the stream.
|
|
1274
1325
|
if (this._environmentRange && this._environmentFiles) {
|
|
1275
1326
|
await this._decodeEnvironmentAsync();
|
|
1276
1327
|
}
|
|
1277
1328
|
this._environmentFiles = null;
|
|
1278
|
-
const
|
|
1279
|
-
for (const node of this._leafNodes) {
|
|
1280
|
-
const entry = node.lods[String(node.baseLod)];
|
|
1281
|
-
if (entry && this._fileCounts.has(entry.file)) {
|
|
1282
|
-
baseFiles.add(entry.file);
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
for (const fileId of Array.from(baseFiles)) {
|
|
1329
|
+
for (const fileId of baseFileIds) {
|
|
1286
1330
|
if (this._disposed) {
|
|
1287
1331
|
return;
|
|
1288
1332
|
}
|
|
1289
1333
|
// eslint-disable-next-line no-await-in-loop
|
|
1290
|
-
await this._decodeFileAsync(fileId);
|
|
1334
|
+
const decoded = await this._decodeFileAsync(fileId);
|
|
1335
|
+
if (!decoded) {
|
|
1336
|
+
throw new Error(`GaussianSplattingStream: required coarse file ${fileId} failed to decode.`);
|
|
1337
|
+
}
|
|
1291
1338
|
}
|
|
1292
1339
|
if (this._disposed) {
|
|
1293
1340
|
return;
|
|
1294
1341
|
}
|
|
1295
|
-
|
|
1342
|
+
for (const node of this._leafNodes) {
|
|
1343
|
+
if (node.activeLod !== node.baseLod || node.activeFile === undefined) {
|
|
1344
|
+
throw new Error("GaussianSplattingStream: the complete coarse layer could not be activated.");
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
// Coarse data is now complete and visible. Hosted callers can place/frame the real bounds immediately,
|
|
1348
|
+
// while large streams fetch finer metadata in the background.
|
|
1296
1349
|
this._baseLayerReady = true;
|
|
1350
|
+
this._resolvePartReady();
|
|
1351
|
+
if (progressiveMetadata) {
|
|
1352
|
+
const fineFileIds = fileIds.filter((fileId) => !this._baseFileIds.has(fileId));
|
|
1353
|
+
await this._gatherCountsAsync(fineFileIds, false);
|
|
1354
|
+
this._resolveLod0SplatCount();
|
|
1355
|
+
this._metadataReady = true;
|
|
1356
|
+
if (this._disposed) {
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
// Only after complete coarse coverage and the finer metadata phase may the per-frame loop request fine
|
|
1361
|
+
// geometry. The fixed work buffer continues to retain every pinned coarse source as fallback.
|
|
1297
1362
|
if (!this._lodObserver) {
|
|
1298
1363
|
this._lodObserver = this._scene.onBeforeRenderObservable.add(() => this._onLodFrame());
|
|
1299
1364
|
}
|
|
1300
|
-
// Hosted: the reserved part now exists with a decoded base layer and real bounds — release awaiters.
|
|
1301
|
-
this._resolvePartReady();
|
|
1302
1365
|
}
|
|
1303
1366
|
/**
|
|
1304
1367
|
* Waits (up to a frame cap) until the work buffer's backup/restore copy shaders are compiled, so a later
|
|
@@ -1321,23 +1384,67 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
1321
1384
|
}
|
|
1322
1385
|
}
|
|
1323
1386
|
/**
|
|
1324
|
-
* Resolves the
|
|
1325
|
-
*
|
|
1326
|
-
*
|
|
1327
|
-
*
|
|
1387
|
+
* Resolves the fixed initial work-buffer capacity. Explicit count/MB limits are combined by taking the smaller
|
|
1388
|
+
* and then raised to the complete coarse minimum. Without an explicit limit, a known complete source retains
|
|
1389
|
+
* legacy full residency. When finer metadata is deferred and the complete size is unknown, the device-tiered
|
|
1390
|
+
* memory estimate is given at least one largest-coarse-file of headroom so one replacement file can refine
|
|
1391
|
+
* while all coarse fallback files remain pinned.
|
|
1392
|
+
* @param fullCapacity exact complete source capacity, or null when unavailable
|
|
1393
|
+
* @param largestBaseFileCount largest whole coarse source file, used as modest replacement headroom
|
|
1394
|
+
* @returns the fixed initial work-buffer capacity
|
|
1328
1395
|
*/
|
|
1329
|
-
_resolveResidentBudget() {
|
|
1330
|
-
|
|
1396
|
+
_resolveResidentBudget(fullCapacity = null, largestBaseFileCount = 0) {
|
|
1397
|
+
const hasExplicitLimit = this._maxResidentSplats > 0 || this._memoryBudgetMb > 0;
|
|
1398
|
+
let budget = this._maxResidentSplats > 0 ? this._maxResidentSplats : Number.POSITIVE_INFINITY;
|
|
1399
|
+
const bytesPerSplat = this._bytesPerResidentSplat();
|
|
1331
1400
|
if (this._memoryBudgetMb > 0) {
|
|
1332
|
-
// Per resident splat: core 84 B, + 16 B per packed-u32 SH texture, + the 3 RGBA rotation textures. The
|
|
1333
|
-
// work buffer uses half-float rotation textures (8 B each = 24 B) when the engine can render to them,
|
|
1334
|
-
// else full float (16 B each = 48 B) — match that so fallback devices aren't under-budgeted.
|
|
1335
|
-
const rotBytes = this._scene.getEngine().getCaps().textureHalfFloatRender ? 24 : 48;
|
|
1336
|
-
const bytesPerSplat = BytesPerResidentSplat + this._shTextureCount * 16 + (this._needsRotationScale ? rotBytes : 0);
|
|
1337
1401
|
const fromMB = Math.floor((this._memoryBudgetMb * 1024 * 1024) / bytesPerSplat);
|
|
1338
|
-
budget =
|
|
1402
|
+
budget = Math.min(budget, fromMB);
|
|
1403
|
+
}
|
|
1404
|
+
if (!hasExplicitLimit && fullCapacity !== null) {
|
|
1405
|
+
budget = fullCapacity;
|
|
1406
|
+
}
|
|
1407
|
+
else if (!hasExplicitLimit) {
|
|
1408
|
+
const automaticMb = this._scene.getEngine().hostInformation?.isMobile ? MobileAutomaticResidentMemoryMb : DesktopAutomaticResidentMemoryMb;
|
|
1409
|
+
const automaticCount = Math.floor((automaticMb * 1024 * 1024) / bytesPerSplat);
|
|
1410
|
+
const refinementHeadroom = Math.max(0, Math.floor(largestBaseFileCount));
|
|
1411
|
+
const coarseWithHeadroom = GaussianSplattingStream._SafeAddCounts(this._minimumResidentSplats, refinementHeadroom, "automatic resident capacity");
|
|
1412
|
+
budget = Math.max(automaticCount, coarseWithHeadroom);
|
|
1413
|
+
}
|
|
1414
|
+
else if (budget < this._minimumResidentSplats) {
|
|
1415
|
+
Logger.Warn(`GaussianSplattingStream: the requested initial residency (${budget} splats) is below the complete coarse minimum (${this._minimumResidentSplats}); using the minimum.`);
|
|
1416
|
+
}
|
|
1417
|
+
budget = Math.max(this._minimumResidentSplats, Math.floor(budget));
|
|
1418
|
+
if (fullCapacity !== null) {
|
|
1419
|
+
budget = Math.min(budget, fullCapacity);
|
|
1420
|
+
}
|
|
1421
|
+
if (fullCapacity !== null || this._minimumResidentSplats > 0) {
|
|
1422
|
+
const maxTextureSize = Math.floor(this._scene.getEngine().getCaps().maxTextureSize);
|
|
1423
|
+
const maxCapacity = maxTextureSize * maxTextureSize;
|
|
1424
|
+
if (!Number.isSafeInteger(maxCapacity) || this._minimumResidentSplats > maxCapacity) {
|
|
1425
|
+
throw new Error(`GaussianSplattingStream: minimum resident splat count ${this._minimumResidentSplats} exceeds the device texture capacity ${maxCapacity}.`);
|
|
1426
|
+
}
|
|
1427
|
+
if (budget > maxCapacity) {
|
|
1428
|
+
budget = maxCapacity;
|
|
1429
|
+
}
|
|
1430
|
+
if (budget < this._minimumResidentSplats) {
|
|
1431
|
+
throw new Error(`GaussianSplattingStream: minimum resident splat count ${this._minimumResidentSplats} exceeds the device texture capacity ${maxCapacity}.`);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
if (!Number.isSafeInteger(budget) || budget < this._minimumResidentSplats || budget < 1) {
|
|
1435
|
+
throw new Error("GaussianSplattingStream: resolved resident capacity is invalid.");
|
|
1339
1436
|
}
|
|
1340
1437
|
this._residentBudget = budget;
|
|
1438
|
+
return budget;
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Returns the estimated combined GPU/CPU bytes occupied by one resident splat.
|
|
1442
|
+
* @returns estimated bytes per resident splat
|
|
1443
|
+
*/
|
|
1444
|
+
_bytesPerResidentSplat() {
|
|
1445
|
+
// The three rotation textures use half floats (8 B each) when supported, otherwise full floats (16 B each).
|
|
1446
|
+
const rotBytes = this._scene.getEngine().getCaps().textureHalfFloatRender ? 24 : 48;
|
|
1447
|
+
return BytesPerResidentSplat + this._shTextureCount * 16 + (this._needsRotationScale ? rotBytes : 0);
|
|
1341
1448
|
}
|
|
1342
1449
|
/**
|
|
1343
1450
|
* Collects the unique set of source file indices referenced by any LOD of any leaf, sorted ascending.
|
|
@@ -1355,6 +1462,36 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
1355
1462
|
}
|
|
1356
1463
|
return Array.from(ids).sort((a, b) => a - b);
|
|
1357
1464
|
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Collects unique whole source files required by the coarsest entry of every valid leaf.
|
|
1467
|
+
* @returns sorted unique coarse file indices
|
|
1468
|
+
*/
|
|
1469
|
+
_collectBaseFileIds() {
|
|
1470
|
+
const ids = new Set();
|
|
1471
|
+
for (const node of this._leafNodes) {
|
|
1472
|
+
const entry = node.lods?.[String(node.baseLod)];
|
|
1473
|
+
if (entry) {
|
|
1474
|
+
ids.add(entry.file);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
return Array.from(ids).sort((a, b) => a - b);
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Resolves the immutable minimum initial capacity from padding, environment, and unique whole coarse files.
|
|
1481
|
+
* @param baseFileIds unique coarse source file indices
|
|
1482
|
+
* @param environmentCount included environment splat count
|
|
1483
|
+
*/
|
|
1484
|
+
_resolveMinimumResidentSplats(baseFileIds, environmentCount) {
|
|
1485
|
+
let minimum = GaussianSplattingStream._SafeAddCounts(1, Math.max(0, Math.floor(environmentCount)), "minimum resident splats");
|
|
1486
|
+
for (const fileId of baseFileIds) {
|
|
1487
|
+
const count = this._fileCounts.get(fileId);
|
|
1488
|
+
if (count === undefined || !Number.isSafeInteger(count) || count <= 0) {
|
|
1489
|
+
throw new Error(`GaussianSplattingStream: required coarse metadata for file ${fileId} is unavailable or invalid.`);
|
|
1490
|
+
}
|
|
1491
|
+
minimum = GaussianSplattingStream._SafeAddCounts(minimum, count, "minimum resident splats");
|
|
1492
|
+
}
|
|
1493
|
+
this._minimumResidentSplats = minimum;
|
|
1494
|
+
}
|
|
1358
1495
|
/**
|
|
1359
1496
|
* Settles the level-0 diagnostic from normalized renderable leaf entries after their source metadata resolves.
|
|
1360
1497
|
* A file may back several leaf ranges, so its source count is deliberately not used in the total.
|
|
@@ -1385,12 +1522,13 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
1385
1522
|
this._lod0SplatCount = hasLod0Entry ? Object.freeze({ status: "available", count: total }) : Object.freeze({ status: "unavailable" });
|
|
1386
1523
|
}
|
|
1387
1524
|
/**
|
|
1388
|
-
* Fetches the environment bundle and
|
|
1389
|
-
* each file's parsed metadata for the later on-demand decode.
|
|
1525
|
+
* Fetches the environment bundle and the supplied referenced files' metadata to learn splat counts, caching
|
|
1526
|
+
* each file's parsed metadata for the later on-demand decode. File metadata fetches run in parallel.
|
|
1390
1527
|
* @param fileIds file indices to fetch metadata for
|
|
1528
|
+
* @param includeEnvironment whether to fetch the optional environment bundle in this phase
|
|
1391
1529
|
* @returns the environment splat count (0 when there is no environment)
|
|
1392
1530
|
*/
|
|
1393
|
-
async _gatherCountsAsync(fileIds) {
|
|
1531
|
+
async _gatherCountsAsync(fileIds, includeEnvironment = true) {
|
|
1394
1532
|
let envCount = 0;
|
|
1395
1533
|
// Track the max SH degree/coeffs across every streamed file (+ environment): the baked SH atlas is sized
|
|
1396
1534
|
// for the max once, up front, so no mid-stream resize — lower-degree files neutral-fill their higher bands.
|
|
@@ -1405,7 +1543,7 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
1405
1543
|
maxCoeffs = info.coeffs;
|
|
1406
1544
|
}
|
|
1407
1545
|
};
|
|
1408
|
-
if (this._metadata.environment) {
|
|
1546
|
+
if (includeEnvironment && this._metadata.environment) {
|
|
1409
1547
|
try {
|
|
1410
1548
|
const url = this._rootUrl + this._metadata.environment;
|
|
1411
1549
|
const buffer = await this._downloadManager.loadFileAsync(url);
|
|
@@ -1447,9 +1585,9 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
1447
1585
|
}
|
|
1448
1586
|
// Resolve the stream's baked-SH configuration: enabled only when requested AND the data carries shN.
|
|
1449
1587
|
if (this._decodeSh && maxShDegree > 0 && maxCoeffs > 0) {
|
|
1450
|
-
this._streamShDegree = maxShDegree;
|
|
1588
|
+
this._streamShDegree = Math.max(this._streamShDegree, maxShDegree);
|
|
1451
1589
|
// Packed-u32 SH textures: 16 SH scalar-bytes per texel, 3 channels per coefficient (matches ParseSogDatas).
|
|
1452
|
-
this._shTextureCount = Math.ceil((maxCoeffs * 3) / 16);
|
|
1590
|
+
this._shTextureCount = Math.max(this._shTextureCount, Math.ceil((maxCoeffs * 3) / 16));
|
|
1453
1591
|
}
|
|
1454
1592
|
return envCount;
|
|
1455
1593
|
}
|
|
@@ -1620,10 +1758,12 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
1620
1758
|
return;
|
|
1621
1759
|
}
|
|
1622
1760
|
const range = this._environmentRange;
|
|
1761
|
+
let decoded = false;
|
|
1623
1762
|
try {
|
|
1624
1763
|
const parsed = await ParseSogMetaAsTextures(this._environmentFiles, "", this._scene, !this._useGpuPositionReadback, this._downloadManager);
|
|
1625
1764
|
const pack = parsed.sogTextures;
|
|
1626
1765
|
if (!pack) {
|
|
1766
|
+
Logger.Warn("GaussianSplattingStream: environment decoding produced no texture data.");
|
|
1627
1767
|
return;
|
|
1628
1768
|
}
|
|
1629
1769
|
try {
|
|
@@ -1634,10 +1774,18 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
1634
1774
|
if (this._disposed) {
|
|
1635
1775
|
return;
|
|
1636
1776
|
}
|
|
1637
|
-
await this._applyDecodedPositionsAsync(pack, range.offset, range.count);
|
|
1777
|
+
const positionsApplied = await this._applyDecodedPositionsAsync(pack, range.offset, range.count);
|
|
1638
1778
|
if (this._disposed) {
|
|
1639
1779
|
return;
|
|
1640
1780
|
}
|
|
1781
|
+
if (!positionsApplied) {
|
|
1782
|
+
Logger.Warn("GaussianSplattingStream: decoded positions could not be applied for the environment.");
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
if (this._residency?.pin(EnvironmentFileId, range.count) !== range.offset) {
|
|
1786
|
+
throw new Error("GaussianSplattingStream: decoded environment could not be pinned.");
|
|
1787
|
+
}
|
|
1788
|
+
decoded = true;
|
|
1641
1789
|
this._refreshActiveRanges();
|
|
1642
1790
|
}
|
|
1643
1791
|
finally {
|
|
@@ -1648,6 +1796,12 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
1648
1796
|
catch (e) {
|
|
1649
1797
|
Logger.Warn("GaussianSplattingStream: failed to decode environment: " + (e?.message ?? e));
|
|
1650
1798
|
}
|
|
1799
|
+
finally {
|
|
1800
|
+
if (!decoded) {
|
|
1801
|
+
this._residency?.free(EnvironmentFileId);
|
|
1802
|
+
this._environmentRange = null;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1651
1805
|
}
|
|
1652
1806
|
/**
|
|
1653
1807
|
* Loads one LOD source file as GPU textures, decodes it into its fixed work-buffer block, records its
|
|
@@ -1655,15 +1809,19 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
1655
1809
|
* Concurrent or repeat requests for the same file are ignored. If the file is cancelled mid-flight
|
|
1656
1810
|
* (because every node that wanted it retargeted), the decode bails cooperatively at the next checkpoint.
|
|
1657
1811
|
* @param fileId file index to decode
|
|
1812
|
+
* @returns whether the file was decoded and published successfully
|
|
1658
1813
|
*/
|
|
1659
1814
|
async _decodeFileAsync(fileId) {
|
|
1660
|
-
if (this._decodedFiles.has(fileId)
|
|
1661
|
-
return;
|
|
1815
|
+
if (this._decodedFiles.has(fileId)) {
|
|
1816
|
+
return true;
|
|
1817
|
+
}
|
|
1818
|
+
if (this._loadingFiles.has(fileId) || !this._residency) {
|
|
1819
|
+
return false;
|
|
1662
1820
|
}
|
|
1663
1821
|
const meta = this._fileMeta.get(fileId);
|
|
1664
1822
|
const count = this._fileCounts.get(fileId);
|
|
1665
1823
|
if (!meta || count === undefined) {
|
|
1666
|
-
return;
|
|
1824
|
+
return false;
|
|
1667
1825
|
}
|
|
1668
1826
|
this._loadingFiles.add(fileId);
|
|
1669
1827
|
this._cancelledDecodes.delete(fileId);
|
|
@@ -1672,16 +1830,20 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
1672
1830
|
const parsed = await ParseSogMetaAsTextures(meta.sogData, meta.subRootUrl, this._scene, !this._useGpuPositionReadback, this._downloadManager, fileId);
|
|
1673
1831
|
const pack = parsed.sogTextures;
|
|
1674
1832
|
if (!pack) {
|
|
1675
|
-
return;
|
|
1833
|
+
return false;
|
|
1676
1834
|
}
|
|
1677
1835
|
// Serialize the allocate -> decode -> readback section: a relayout runs only inside it (see
|
|
1678
1836
|
// _relayoutAndAllocateAsync), so it never moves a file whose decode has not finished writing.
|
|
1679
1837
|
const release = await this._acquireDecodeGateAsync();
|
|
1680
1838
|
try {
|
|
1681
1839
|
if (this._disposed || !this._workBuffer || this._cancelledDecodes.has(fileId)) {
|
|
1682
|
-
return;
|
|
1840
|
+
return false;
|
|
1841
|
+
}
|
|
1842
|
+
const residency = this._residency;
|
|
1843
|
+
if (!residency) {
|
|
1844
|
+
return false;
|
|
1683
1845
|
}
|
|
1684
|
-
let base =
|
|
1846
|
+
let base = residency.allocate(fileId, count);
|
|
1685
1847
|
if (base === null) {
|
|
1686
1848
|
// Defragment the work buffer to reclaim fragmented free space, then retry.
|
|
1687
1849
|
base = await this._relayoutAndAllocateAsync(fileId, count);
|
|
@@ -1692,25 +1854,35 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
1692
1854
|
if (!this._cancelledDecodes.has(fileId)) {
|
|
1693
1855
|
Logger.Warn(`GaussianSplattingStream: resident memory budget full; skipping LOD file ${fileId}.`);
|
|
1694
1856
|
}
|
|
1695
|
-
return;
|
|
1857
|
+
return false;
|
|
1696
1858
|
}
|
|
1697
1859
|
allocated = true;
|
|
1698
1860
|
if (this._disposed || !this._workBuffer || this._cancelledDecodes.has(fileId)) {
|
|
1699
|
-
return;
|
|
1861
|
+
return false;
|
|
1700
1862
|
}
|
|
1701
1863
|
await this._workBuffer.decodeAsync(pack, base);
|
|
1702
1864
|
if (this._disposed || this._cancelledDecodes.has(fileId)) {
|
|
1703
|
-
return;
|
|
1865
|
+
return false;
|
|
1704
1866
|
}
|
|
1705
|
-
await this._applyDecodedPositionsAsync(pack, base, count);
|
|
1867
|
+
const positionsApplied = await this._applyDecodedPositionsAsync(pack, base, count);
|
|
1706
1868
|
if (this._disposed) {
|
|
1707
|
-
return;
|
|
1869
|
+
return false;
|
|
1870
|
+
}
|
|
1871
|
+
if (!positionsApplied) {
|
|
1872
|
+
Logger.Warn(`GaussianSplattingStream: decoded positions could not be applied for LOD file ${fileId}.`);
|
|
1873
|
+
return false;
|
|
1874
|
+
}
|
|
1875
|
+
if (this._baseFileIds.has(fileId)) {
|
|
1876
|
+
if (residency.pin(fileId, count) !== base) {
|
|
1877
|
+
throw new Error(`GaussianSplattingStream: required coarse file ${fileId} could not be pinned.`);
|
|
1878
|
+
}
|
|
1708
1879
|
}
|
|
1709
1880
|
this._decodedFiles.add(fileId);
|
|
1710
1881
|
// Promote any nodes that can now reach their desired LOD via this newly decoded file.
|
|
1711
1882
|
if (this._applyDesiredLods()) {
|
|
1712
1883
|
this._refreshActiveRanges();
|
|
1713
1884
|
}
|
|
1885
|
+
return true;
|
|
1714
1886
|
}
|
|
1715
1887
|
finally {
|
|
1716
1888
|
GaussianSplattingStream._DisposePack(pack);
|
|
@@ -1722,11 +1894,12 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
1722
1894
|
if (!this._cancelledDecodes.has(fileId)) {
|
|
1723
1895
|
throw e;
|
|
1724
1896
|
}
|
|
1897
|
+
return false;
|
|
1725
1898
|
}
|
|
1726
1899
|
finally {
|
|
1727
1900
|
// If a slot was allocated but the decode did not complete (cancelled/disposed), release it.
|
|
1728
1901
|
if (allocated && !this._decodedFiles.has(fileId)) {
|
|
1729
|
-
this._residency
|
|
1902
|
+
this._residency?.free(fileId);
|
|
1730
1903
|
}
|
|
1731
1904
|
this._loadingFiles.delete(fileId);
|
|
1732
1905
|
this._cancelledDecodes.delete(fileId);
|
|
@@ -2252,17 +2425,36 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
2252
2425
|
}
|
|
2253
2426
|
const nodes = this._leafNodes;
|
|
2254
2427
|
const inAny = this._frustumScratch;
|
|
2255
|
-
//
|
|
2256
|
-
//
|
|
2428
|
+
// Node bounds remain immutable in stream-local space. Compose that space to clip once per camera instead
|
|
2429
|
+
// of transforming every leaf's box and sphere into world space every frame.
|
|
2257
2430
|
const world = this._getEffectiveWorldMatrix(false);
|
|
2258
2431
|
for (let i = 0; i < nodes.length; i++) {
|
|
2259
|
-
nodes[i].cullBounds.update(world);
|
|
2260
2432
|
inAny[i] = false;
|
|
2261
2433
|
}
|
|
2262
2434
|
// A node is in-frustum if inside ANY active camera's frustum: OR each camera's test into the accumulator.
|
|
2263
2435
|
for (const cam of cameras) {
|
|
2264
|
-
cam.getViewMatrix().multiplyToRef(cam.getProjectionMatrix(), this.
|
|
2436
|
+
cam.getViewMatrix().multiplyToRef(cam.getProjectionMatrix(), this._cullCameraViewProj);
|
|
2437
|
+
world.multiplyToRef(this._cullCameraViewProj, this._cullViewProj);
|
|
2265
2438
|
Frustum.GetPlanesToRef(this._cullViewProj, this._frustumPlanes);
|
|
2439
|
+
let validFrustum = true;
|
|
2440
|
+
for (const plane of this._frustumPlanes) {
|
|
2441
|
+
if (!Number.isFinite(plane.normal.x) ||
|
|
2442
|
+
!Number.isFinite(plane.normal.y) ||
|
|
2443
|
+
!Number.isFinite(plane.normal.z) ||
|
|
2444
|
+
!Number.isFinite(plane.d) ||
|
|
2445
|
+
plane.normal.lengthSquared() === 0) {
|
|
2446
|
+
validFrustum = false;
|
|
2447
|
+
break;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
if (!validFrustum) {
|
|
2451
|
+
// Degenerate transforms/projections cannot safely reject local volumes. Treat this camera as seeing
|
|
2452
|
+
// every node, preserving conservative rendering rather than turning malformed planes into holes.
|
|
2453
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
2454
|
+
inAny[i] = true;
|
|
2455
|
+
}
|
|
2456
|
+
continue;
|
|
2457
|
+
}
|
|
2266
2458
|
for (let i = 0; i < nodes.length; i++) {
|
|
2267
2459
|
if (!inAny[i] && nodes[i].cullBounds.isInFrustum(this._frustumPlanes)) {
|
|
2268
2460
|
inAny[i] = true;
|
|
@@ -2277,6 +2469,40 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
2277
2469
|
}
|
|
2278
2470
|
return changed;
|
|
2279
2471
|
}
|
|
2472
|
+
/**
|
|
2473
|
+
* Normalizes an initial residency option, preserving finite non-positive values as an unset limit.
|
|
2474
|
+
* @param value option value
|
|
2475
|
+
* @param name option name used in errors
|
|
2476
|
+
* @param integer whether the normalized value must be an integer splat count
|
|
2477
|
+
* @returns normalized value
|
|
2478
|
+
*/
|
|
2479
|
+
static _NormalizeResidentLimit(value, name, integer) {
|
|
2480
|
+
if (!Number.isFinite(value)) {
|
|
2481
|
+
throw new RangeError(`GaussianSplattingStream: ${name} must be finite.`);
|
|
2482
|
+
}
|
|
2483
|
+
if (value <= 0) {
|
|
2484
|
+
return 0;
|
|
2485
|
+
}
|
|
2486
|
+
const normalized = integer ? Math.max(1, Math.floor(value)) : value;
|
|
2487
|
+
if (normalized > Number.MAX_SAFE_INTEGER) {
|
|
2488
|
+
throw new RangeError(`GaussianSplattingStream: ${name} is outside the supported range.`);
|
|
2489
|
+
}
|
|
2490
|
+
return normalized;
|
|
2491
|
+
}
|
|
2492
|
+
/**
|
|
2493
|
+
* Adds trusted non-negative integer counts without allowing unsafe capacity arithmetic.
|
|
2494
|
+
* @param left first count
|
|
2495
|
+
* @param right second count
|
|
2496
|
+
* @param label capacity name used in errors
|
|
2497
|
+
* @returns the safe integer sum
|
|
2498
|
+
*/
|
|
2499
|
+
static _SafeAddCounts(left, right, label) {
|
|
2500
|
+
const sum = left + right;
|
|
2501
|
+
if (!Number.isSafeInteger(left) || left < 0 || !Number.isSafeInteger(right) || right < 0 || !Number.isSafeInteger(sum)) {
|
|
2502
|
+
throw new Error(`GaussianSplattingStream: ${label} exceeds the supported integer range.`);
|
|
2503
|
+
}
|
|
2504
|
+
return sum;
|
|
2505
|
+
}
|
|
2280
2506
|
/**
|
|
2281
2507
|
* Reads the splat count from SOG metadata, coerced to a finite non-negative integer (metadata is untrusted, so
|
|
2282
2508
|
* `count` / `shape[0]` may be a string or malformed — a non-numeric value must not leak into count arithmetic).
|
|
@@ -2301,7 +2527,6 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
2301
2527
|
// Derive the SH degree from remote (untrusted) metadata, then validate/clamp it: the degree drives the SH
|
|
2302
2528
|
// render-target count and decode-pass count, so a bogus (huge / non-finite / negative) `bands` or `shape`
|
|
2303
2529
|
// must not be able to demand unbounded allocation. The draw path supports shTexture0..4, i.e. degree <= 4.
|
|
2304
|
-
const maxDegree = 4;
|
|
2305
2530
|
let degree = 0;
|
|
2306
2531
|
const bands = data.shN.bands;
|
|
2307
2532
|
if (typeof bands === "number" && Number.isFinite(bands) && bands > 0) {
|
|
@@ -2314,9 +2539,9 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
2314
2539
|
if (!(degree > 0)) {
|
|
2315
2540
|
return { degree: 0, coeffs: 0 };
|
|
2316
2541
|
}
|
|
2317
|
-
if (degree >
|
|
2318
|
-
Logger.Warn(`GaussianSplattingStream: SH degree ${degree} exceeds the maximum supported (${
|
|
2319
|
-
degree =
|
|
2542
|
+
if (degree > MaxSupportedShDegree) {
|
|
2543
|
+
Logger.Warn(`GaussianSplattingStream: SH degree ${degree} exceeds the maximum supported (${MaxSupportedShDegree}); clamping.`);
|
|
2544
|
+
degree = MaxSupportedShDegree;
|
|
2320
2545
|
}
|
|
2321
2546
|
return { degree, coeffs: (degree + 1) ** 2 - 1 };
|
|
2322
2547
|
}
|