@babylonjs/loaders 9.19.1 → 9.20.1
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 +170 -1
- package/SPLAT/gaussianSplattingStream.js +516 -40
- package/SPLAT/gaussianSplattingStream.js.map +1 -1
- package/SPLAT/gaussianSplattingWorkBuffer.d.ts +163 -4
- package/SPLAT/gaussianSplattingWorkBuffer.js +683 -35
- package/SPLAT/gaussianSplattingWorkBuffer.js.map +1 -1
- package/SPLAT/gaussianSplattingWorkBufferShaders.d.ts +70 -2
- package/SPLAT/gaussianSplattingWorkBufferShaders.js +465 -4
- package/SPLAT/gaussianSplattingWorkBufferShaders.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_interactivity/declarationMapper.d.ts +16 -1
- package/glTF/2.0/Extensions/KHR_interactivity/declarationMapper.js +232 -25
- package/glTF/2.0/Extensions/KHR_interactivity/declarationMapper.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityGraphParser.d.ts +52 -2
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityGraphParser.js +108 -7
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityGraphParser.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityHostResolver.d.ts +37 -0
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityHostResolver.js +69 -0
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityHostResolver.js.map +1 -0
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityReferences.d.ts +44 -0
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityReferences.js +51 -0
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityReferences.js.map +1 -0
- package/glTF/2.0/Extensions/KHR_interactivity.pure.d.ts +1 -0
- package/glTF/2.0/Extensions/KHR_interactivity.pure.js +143 -12
- package/glTF/2.0/Extensions/KHR_interactivity.pure.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_node_hoverability.pure.js +6 -0
- package/glTF/2.0/Extensions/KHR_node_hoverability.pure.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_node_selectability.pure.js +4 -0
- package/glTF/2.0/Extensions/KHR_node_selectability.pure.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_node_visibility.pure.js +11 -0
- package/glTF/2.0/Extensions/KHR_node_visibility.pure.js.map +1 -1
- package/glTF/2.0/Extensions/babylonScenePathToObjectConverter.d.ts +126 -0
- package/glTF/2.0/Extensions/babylonScenePathToObjectConverter.js +251 -0
- package/glTF/2.0/Extensions/babylonScenePathToObjectConverter.js.map +1 -0
- package/glTF/2.0/Extensions/compositePathToObjectConverter.d.ts +51 -0
- package/glTF/2.0/Extensions/compositePathToObjectConverter.js +51 -0
- package/glTF/2.0/Extensions/compositePathToObjectConverter.js.map +1 -0
- package/glTF/2.0/Extensions/gltfPathToObjectConverter.js +69 -7
- package/glTF/2.0/Extensions/gltfPathToObjectConverter.js.map +1 -1
- package/glTF/2.0/Extensions/interactivityAssetPathToObjectConverter.d.ts +41 -0
- package/glTF/2.0/Extensions/interactivityAssetPathToObjectConverter.js +108 -0
- package/glTF/2.0/Extensions/interactivityAssetPathToObjectConverter.js.map +1 -0
- package/glTF/2.0/Extensions/interactivityRefPathToObjectConverter.d.ts +28 -0
- package/glTF/2.0/Extensions/interactivityRefPathToObjectConverter.js +71 -0
- package/glTF/2.0/Extensions/interactivityRefPathToObjectConverter.js.map +1 -0
- package/glTF/2.0/Extensions/objectModelMapping.d.ts +118 -30
- package/glTF/2.0/Extensions/objectModelMapping.js +472 -70
- package/glTF/2.0/Extensions/objectModelMapping.js.map +1 -1
- package/glTF/2.0/Extensions/transmissionHelper.js +4 -0
- package/glTF/2.0/Extensions/transmissionHelper.js.map +1 -1
- package/glTF/2.0/glTFLoader.pure.js +2 -1
- package/glTF/2.0/glTFLoader.pure.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { GaussianSplattingMesh } from "@babylonjs/core/Meshes/GaussianSplatting/gaussianSplattingMesh.js";
|
|
2
2
|
import { Logger } from "@babylonjs/core/Misc/logger.js";
|
|
3
3
|
import { Tools } from "@babylonjs/core/Misc/tools.js";
|
|
4
|
-
import { Vector3, Matrix } from "@babylonjs/core/Maths/math.vector.js";
|
|
4
|
+
import { Vector3, Matrix, Quaternion } from "@babylonjs/core/Maths/math.vector.js";
|
|
5
5
|
import { Color4 } from "@babylonjs/core/Maths/math.color.js";
|
|
6
6
|
import { Frustum } from "@babylonjs/core/Maths/math.frustum.js";
|
|
7
7
|
import { Plane } from "@babylonjs/core/Maths/math.plane.js";
|
|
@@ -18,8 +18,8 @@ const RefTanHalfFov = Math.tan((22.5 * Math.PI) / 180);
|
|
|
18
18
|
// Sentinel "file" ids for the residency controller's pinned (never-evicted) allocations.
|
|
19
19
|
const PaddingFileId = -2;
|
|
20
20
|
const EnvironmentFileId = -1;
|
|
21
|
-
//
|
|
22
|
-
//
|
|
21
|
+
// Core bytes per resident splat: the four work-buffer textures cost 16+16+16+4 = 52 bytes on the GPU, plus ~32
|
|
22
|
+
// bytes of CPU position/sort data. `_resolveResidentBudget` adds the SH and rotation/scale texture cost on top.
|
|
23
23
|
const BytesPerResidentSplat = 84;
|
|
24
24
|
// Scratch objects reused by the per-frame optimal-LOD evaluation (avoids per-call allocations).
|
|
25
25
|
const TmpInvWorld = new Matrix();
|
|
@@ -122,6 +122,10 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
122
122
|
this._cullViewProj = new Matrix();
|
|
123
123
|
// GPU work buffer holding all decoded splats; created once the total capacity is known.
|
|
124
124
|
this._workBuffer = null;
|
|
125
|
+
this._streamShDegree = 0;
|
|
126
|
+
this._shTextureCount = 0;
|
|
127
|
+
// Rotation/scale for voxel-IBL shadows. Enabled via options.needsRotationScale.
|
|
128
|
+
this._needsRotationScale = false;
|
|
125
129
|
// True once GPU position readback has been validated against a CPU decode (see _probeReadbackAsync). While
|
|
126
130
|
// false, positions are decoded on the CPU from the means images; once validated, every SOG image uses the
|
|
127
131
|
// fast direct upload and positions are read back from the work buffer (non-blocking).
|
|
@@ -150,6 +154,10 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
150
154
|
// Eviction streaming config: enabled only when a budget smaller than the full dataset is configured.
|
|
151
155
|
this._evictionEnabled = false;
|
|
152
156
|
this._residentBudget = 0;
|
|
157
|
+
// Raw budget options; the final `_residentBudget` is resolved from these once the SH/rotation byte cost is known
|
|
158
|
+
// (after the metadata pre-pass), so the memory budget accounts for the extra baked SH and rotation textures.
|
|
159
|
+
this._maxResidentSplats = 0;
|
|
160
|
+
this._memoryBudgetMb = 0;
|
|
153
161
|
this._evictionCooldownFrames = 100;
|
|
154
162
|
// Serializes the allocate -> decode -> readback critical section so a defrag relayout (which runs inside it)
|
|
155
163
|
// never overlaps another file's decode writing the work buffer, which would corrupt the moved data.
|
|
@@ -182,9 +190,39 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
182
190
|
// Signature of the per-leaf displayed LOD levels, used to skip rebuilding unchanged debug geometry.
|
|
183
191
|
this._debugSignature = 0;
|
|
184
192
|
this._disposed = false;
|
|
193
|
+
// Hosted mode: when set, the stream decodes/sorts into a reserved region of a compound mesh instead of
|
|
194
|
+
// rendering itself. `_host` is the reserved-part handle (resolved once the total capacity is known),
|
|
195
|
+
// `_positionBase` is the region's first splat index in the compound's shared position buffer.
|
|
196
|
+
this._hostCompound = null;
|
|
197
|
+
this._host = null;
|
|
198
|
+
this._positionBase = 0;
|
|
199
|
+
// Unsubscribe functions for the host's atlas-rebuild hooks (backup/restore the region across a grow).
|
|
200
|
+
this._unsubBeforeRebuild = null;
|
|
201
|
+
this._unsubAfterRebuild = null;
|
|
202
|
+
// Unsubscribe functions binding this controller's lifetime to its host compound: removing the part or disposing
|
|
203
|
+
// the compound disposes this stream, even mid-load. Registered at reservation so the window is never open.
|
|
204
|
+
this._hostUnsubRemove = null;
|
|
205
|
+
this._hostUnsubDispose = null;
|
|
206
|
+
// True once the host has released this stream's part (removePart, or the compound is being disposed), so dispose()
|
|
207
|
+
// must NOT call back into the compound to remove the part again.
|
|
208
|
+
this._partReleasedByHost = false;
|
|
209
|
+
// CPU snapshot of this region's shared `_splatPositions` taken before an atlas grow and restored after it —
|
|
210
|
+
// the grow rebuilds `_splatPositions` from CPU part sources, and a streamed region has none, so without this
|
|
211
|
+
// its sort-worker positions would be zeroed (the streamed splats would collapse to the origin).
|
|
212
|
+
this._positionSnapshot = null;
|
|
213
|
+
// Hosted mode: resolves once the reserved part exists AND its base layer has decoded (proxy bounds are
|
|
214
|
+
// real); rejects if streaming fails/disposes before that. Lets AddGaussianSplattingStreamPartAsync hand
|
|
215
|
+
// back a ready part proxy, replacing the standalone waitForEnabled/waitForStreamedBounds handshake.
|
|
216
|
+
this._partReadyPromise = null;
|
|
217
|
+
this._partReadyResolve = null;
|
|
218
|
+
this._partReadyReject = null;
|
|
219
|
+
this._partReadySettled = false;
|
|
185
220
|
this._metadata = metadata;
|
|
186
221
|
this._rootUrl = rootUrl;
|
|
187
222
|
this._streamOptions = options;
|
|
223
|
+
this._hostCompound = options.hostCompound ?? null;
|
|
224
|
+
this._decodeSh = options.decodeSh ?? true;
|
|
225
|
+
this._needsRotationScale = options.needsRotationScale ?? false;
|
|
188
226
|
// LOD heuristic parameters: take the provided values, otherwise keep the PlayCanvas-aligned defaults.
|
|
189
227
|
const maxLod = Math.max(0, metadata.lodLevels - 1);
|
|
190
228
|
this._lodRangeMax = maxLod;
|
|
@@ -227,37 +265,130 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
227
265
|
if (options.evictionCooldownFrames !== undefined) {
|
|
228
266
|
this._evictionCooldownFrames = Math.max(0, Math.floor(options.evictionCooldownFrames));
|
|
229
267
|
}
|
|
230
|
-
//
|
|
231
|
-
|
|
268
|
+
// Capture the raw budget options; `_residentBudget` is resolved in _streamAllAsync once the SH/rotation
|
|
269
|
+
// per-splat cost is known (a memory budget must count the extra baked SH and rotation textures, not just core).
|
|
232
270
|
if (options.maxResidentSplats !== undefined && options.maxResidentSplats > 0) {
|
|
233
|
-
|
|
271
|
+
this._maxResidentSplats = Math.floor(options.maxResidentSplats);
|
|
234
272
|
}
|
|
235
273
|
if (options.memoryBudgetMb !== undefined && options.memoryBudgetMb > 0) {
|
|
236
|
-
|
|
237
|
-
budget = budget > 0 ? Math.min(budget, fromMB) : fromMB;
|
|
274
|
+
this._memoryBudgetMb = options.memoryBudgetMb;
|
|
238
275
|
}
|
|
239
|
-
this._residentBudget = budget;
|
|
240
276
|
this._downloadManager = new GaussianSplattingDownloadManager({
|
|
241
277
|
maxConcurrent: options.maxConcurrentDownloads,
|
|
242
278
|
maxRetries: options.maxDownloadRetries,
|
|
243
279
|
});
|
|
244
|
-
// PlayCanvas SOG data is authored with a flipped Y
|
|
245
|
-
|
|
246
|
-
//
|
|
247
|
-
this.
|
|
280
|
+
// PlayCanvas SOG data is authored with a flipped Y and Z-up. Standalone: bake the orientation into this
|
|
281
|
+
// mesh's transform. Hosted: this mesh does not render — the orientation is applied to the reserved part's
|
|
282
|
+
// proxy transform instead (see _streamAllAsync), so it composes with the compound's per-part world matrix.
|
|
283
|
+
if (!this._hostCompound) {
|
|
284
|
+
this.scaling.y *= -1;
|
|
285
|
+
this.rotation.x = -Math.PI / 2;
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
// Hidden controller: never rendered/picked/serialized; the compound renders the streamed splats.
|
|
289
|
+
this.setEnabled(false);
|
|
290
|
+
this.isPickable = false;
|
|
291
|
+
this.doNotSerialize = true;
|
|
292
|
+
// Created before _streamAllAsync is kicked off (below) so there is no resolve-before-await race.
|
|
293
|
+
this._partReadyPromise = new Promise((resolve, reject) => {
|
|
294
|
+
this._partReadyResolve = resolve;
|
|
295
|
+
this._partReadyReject = reject;
|
|
296
|
+
});
|
|
297
|
+
// Attach a no-op rejection handler so a caller that never awaits whenPartReadyAsync() (e.g. the synchronous
|
|
298
|
+
// AddGaussianSplattingStreamPart) does not produce an unhandled promise rejection on failure; real
|
|
299
|
+
// consumers still observe the rejection through their own await.
|
|
300
|
+
// eslint-disable-next-line github/no-then
|
|
301
|
+
this._partReadyPromise.catch(() => { });
|
|
302
|
+
// Bind to the host's disposal FROM CONSTRUCTION (not just from reservation): the metadata pre-pass in
|
|
303
|
+
// _streamAllAsync runs before the region is reserved, so a compound disposed during that download would
|
|
304
|
+
// otherwise be missed and the controller would reserve into a disposed host. dispose() -> _disposed, so
|
|
305
|
+
// _streamAllAsync's post-download check bails before reserving.
|
|
306
|
+
const disposeObserver = this._hostCompound.onDisposeObservable.add(() => {
|
|
307
|
+
if (!this._disposed) {
|
|
308
|
+
this._partReleasedByHost = true;
|
|
309
|
+
this.dispose();
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
this._hostUnsubDispose = () => this._hostCompound.onDisposeObservable.remove(disposeObserver);
|
|
313
|
+
}
|
|
248
314
|
this._collectLodEntries(metadata.tree);
|
|
249
315
|
if (options.debugDisplay) {
|
|
250
316
|
this.debugDisplay = true;
|
|
251
317
|
}
|
|
252
|
-
// Kick off streaming without blocking the caller or the render loop.
|
|
253
|
-
//
|
|
254
|
-
|
|
318
|
+
// Kick off streaming without blocking the caller or the render loop. In hosted mode settle the part-ready
|
|
319
|
+
// deferred: _streamAllAsync resolves it once the base layer has decoded. If it finishes WITHOUT the part ever
|
|
320
|
+
// becoming ready (empty stream) or throws, dispose the controller so a hosted stream doesn't leave its work
|
|
321
|
+
// buffer and reserved region allocated — the synchronous AddGaussianSplattingStreamPart never awaits, so it
|
|
322
|
+
// can't clean up itself. `_partReadySettled` distinguishes a genuine success (leave it running) from a
|
|
323
|
+
// finished-but-never-ready result (dispose).
|
|
324
|
+
// eslint-disable-next-line github/no-then
|
|
325
|
+
void this._streamAllAsync().then(() => {
|
|
326
|
+
const becameReady = this._partReadySettled;
|
|
327
|
+
this._rejectPartReady("GaussianSplattingStream: stream produced no splats.");
|
|
328
|
+
if (!becameReady && this._hostCompound && !this._disposed) {
|
|
329
|
+
this._disposeAndReclaim();
|
|
330
|
+
}
|
|
331
|
+
}, (e) => {
|
|
255
332
|
Logger.Error("GaussianSplattingStream: streaming failed: " + (e?.message ?? e));
|
|
333
|
+
this._rejectPartReady("GaussianSplattingStream: streaming failed: " + (e?.message ?? e));
|
|
334
|
+
if (this._hostCompound && !this._disposed) {
|
|
335
|
+
this._disposeAndReclaim();
|
|
336
|
+
}
|
|
256
337
|
});
|
|
257
338
|
}
|
|
258
339
|
getClassName() {
|
|
259
340
|
return "GaussianSplattingStream";
|
|
260
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* When `_hostCompound` is set (i.e. this stream was created via {@link AddGaussianSplattingStreamPart}
|
|
344
|
+
* to drive a reserved region of another compound mesh, rather than rendering itself), this instance is
|
|
345
|
+
* disabled and never drawn — so it never runs its own depth-sort worker and the base class's readiness
|
|
346
|
+
* check (which waits for one) would never pass. Report ready unconditionally in that case; the host
|
|
347
|
+
* compound is the one actually rendering, and its own `isReady()` already covers real sort completion.
|
|
348
|
+
* @param completeCheck defines if a complete check (including materials and lights) has to be done (false by default)
|
|
349
|
+
* @returns true when ready
|
|
350
|
+
*/
|
|
351
|
+
isReady(completeCheck = false) {
|
|
352
|
+
if (this._hostCompound) {
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
return super.isReady(completeCheck);
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Hosted mode only: the compound part proxy this stream drives (world transform + visibility of the
|
|
359
|
+
* reserved region), or null before the part has been reserved (or when running standalone).
|
|
360
|
+
*/
|
|
361
|
+
get streamingPartProxy() {
|
|
362
|
+
return this._host?.proxy ?? null;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Hosted mode only: resolves once the reserved part exists and its base layer has decoded (so the proxy's
|
|
366
|
+
* bounds are real and the part is ready to be placed/framed), or rejects if streaming fails/disposes first.
|
|
367
|
+
* Resolves immediately for a standalone stream. Used by {@link AddGaussianSplattingStreamPartAsync}.
|
|
368
|
+
* @returns a promise that settles when the hosted part is ready to use
|
|
369
|
+
*/
|
|
370
|
+
async whenPartReadyAsync() {
|
|
371
|
+
await (this._partReadyPromise ?? Promise.resolve());
|
|
372
|
+
}
|
|
373
|
+
/** Resolves the part-ready deferred (hosted mode); no-op if already settled or standalone. */
|
|
374
|
+
_resolvePartReady() {
|
|
375
|
+
if (this._partReadySettled) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
this._partReadySettled = true;
|
|
379
|
+
this._partReadyResolve?.();
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Rejects the part-ready deferred (hosted mode); no-op if already settled or standalone.
|
|
383
|
+
* @param message failure reason surfaced to the awaiter
|
|
384
|
+
*/
|
|
385
|
+
_rejectPartReady(message) {
|
|
386
|
+
if (this._partReadySettled) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
this._partReadySettled = true;
|
|
390
|
+
this._partReadyReject?.(new Error(message));
|
|
391
|
+
}
|
|
261
392
|
/**
|
|
262
393
|
* Resolves once the scene is fully streamed and displayed for the current camera: a LOD re-evaluation has
|
|
263
394
|
* run for the current point of view, every reachable LOD file has finished downloading and decoding (no
|
|
@@ -284,7 +415,7 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
284
415
|
const scene = this._scene;
|
|
285
416
|
let stable = 0;
|
|
286
417
|
const isSettled = () => {
|
|
287
|
-
if (this._isLoadingIdle() && this.
|
|
418
|
+
if (this._isLoadingIdle() && this._sinkIsDepthSortSettled) {
|
|
288
419
|
return ++stable >= required;
|
|
289
420
|
}
|
|
290
421
|
stable = 0;
|
|
@@ -416,7 +547,29 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
416
547
|
}
|
|
417
548
|
}
|
|
418
549
|
dispose(doNotRecurse) {
|
|
550
|
+
if (this._disposed) {
|
|
551
|
+
// Idempotent: a failed load disposes from its own _streamAllAsync handler, and the awaiter's catch may
|
|
552
|
+
// dispose again — don't re-fire cleanup/observables (and super.dispose) a second time.
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
419
555
|
this._disposed = true;
|
|
556
|
+
this._rejectPartReady("GaussianSplattingStream: disposed before the part was ready.");
|
|
557
|
+
this._unsubBeforeRebuild?.();
|
|
558
|
+
this._unsubAfterRebuild?.();
|
|
559
|
+
this._unsubBeforeRebuild = null;
|
|
560
|
+
this._unsubAfterRebuild = null;
|
|
561
|
+
this._hostUnsubRemove?.();
|
|
562
|
+
this._hostUnsubDispose?.();
|
|
563
|
+
this._hostUnsubRemove = null;
|
|
564
|
+
this._hostUnsubDispose = null;
|
|
565
|
+
// If this stream disposes on its own rather than because the host removed its part, release the reserved
|
|
566
|
+
// region (tombstone). Reclaiming the rows is a separate compaction — cheap disposal here so tearing down N
|
|
567
|
+
// parts doesn't trigger N atlas rebuilds; the caller/host reclaims when appropriate (a failed load compacts
|
|
568
|
+
// once, see the _streamAllAsync handler). Skipped when the host removed the part (it owns that policy).
|
|
569
|
+
if (this._host && this._hostCompound && !this._partReleasedByHost && !this._hostCompound.isDisposed()) {
|
|
570
|
+
this._hostCompound.removePart(this._host.partIndex);
|
|
571
|
+
}
|
|
572
|
+
this._host = null;
|
|
420
573
|
if (this._lodObserver) {
|
|
421
574
|
this._scene.onBeforeRenderObservable.remove(this._lodObserver);
|
|
422
575
|
this._lodObserver = null;
|
|
@@ -429,6 +582,35 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
429
582
|
this._workBuffer = null;
|
|
430
583
|
super.dispose(doNotRecurse);
|
|
431
584
|
}
|
|
585
|
+
/**
|
|
586
|
+
* Disposes this stream (which tombstones its region) and then compacts the host once to actually reclaim the
|
|
587
|
+
* reserved rows. Used on a definitive load failure / empty result — a discrete, one-off reclaim, versus a bare
|
|
588
|
+
* {@link dispose} that only tombstones so tearing down several parts doesn't rebuild the atlas repeatedly.
|
|
589
|
+
*/
|
|
590
|
+
_disposeAndReclaim() {
|
|
591
|
+
const compound = this._hostCompound;
|
|
592
|
+
const hadPart = !!this._host && !this._partReleasedByHost;
|
|
593
|
+
this.dispose();
|
|
594
|
+
if (hadPart && compound && !compound.isDisposed()) {
|
|
595
|
+
compound.compactAtlas();
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* The world matrix that actually places this stream's splats, used to map the camera into the space the
|
|
600
|
+
* node bounds live in (for LOD distance) and to build per-node world AABBs (for frustum culling). Standalone:
|
|
601
|
+
* this controller mesh carries the transform. Hosted: this controller is a hidden, unplaced node — the splats
|
|
602
|
+
* are placed by the reserved part's proxy (SOG up-axis basis composed with the host's placement), so LOD and
|
|
603
|
+
* culling MUST use the proxy's world matrix or they compute distances/frustum tests in the wrong space
|
|
604
|
+
* (producing wrong per-chunk LODs, i.e. holes, whenever the host applies a non-identity transform).
|
|
605
|
+
* @param force when true, forces a full world-matrix recompute (else uses the renderId/sync fast-path)
|
|
606
|
+
* @returns the effective world matrix for LOD/culling
|
|
607
|
+
*/
|
|
608
|
+
_getEffectiveWorldMatrix(force) {
|
|
609
|
+
if (this._host) {
|
|
610
|
+
return this._host.proxy.computeWorldMatrix(force);
|
|
611
|
+
}
|
|
612
|
+
return this.computeWorldMatrix(force);
|
|
613
|
+
}
|
|
432
614
|
/**
|
|
433
615
|
* Re-evaluates the optimal LOD for every node based on the camera position. The result is stored in
|
|
434
616
|
* each node's `optimalLod`. Rendering is unaffected; this currently drives only diagnostics and the
|
|
@@ -454,7 +636,7 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
454
636
|
const tanHalfH = tanHalfV * aspect;
|
|
455
637
|
const fovScale = Math.min(tanHalfV, tanHalfH) / RefTanHalfFov;
|
|
456
638
|
// Transform the camera into the mesh's local space (where the node bounds live).
|
|
457
|
-
this.
|
|
639
|
+
this._getEffectiveWorldMatrix(false).invertToRef(TmpInvWorld);
|
|
458
640
|
const localCamera = Vector3.TransformCoordinatesToRef(camera.globalPosition, TmpInvWorld, TmpLocalCamera);
|
|
459
641
|
const px = localCamera.x;
|
|
460
642
|
const py = localCamera.y;
|
|
@@ -695,13 +877,15 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
695
877
|
* base layer, then installs the per-frame loop that streams finer LODs on demand.
|
|
696
878
|
*/
|
|
697
879
|
async _streamAllAsync() {
|
|
698
|
-
//
|
|
880
|
+
// Step 1: learn splat counts for the environment and every referenced LOD file (cheap meta only). This also
|
|
881
|
+
// resolves the max SH degree, so the resident-splat budget can now be sized with the SH/rotation byte cost.
|
|
699
882
|
const fileIds = this._collectAllFileIds();
|
|
700
883
|
const envCount = await this._gatherCountsAsync(fileIds);
|
|
701
884
|
if (this._disposed) {
|
|
702
885
|
return;
|
|
703
886
|
}
|
|
704
|
-
|
|
887
|
+
this._resolveResidentBudget();
|
|
888
|
+
// Step 2: learn the full dataset size (padding + environment + every LOD file). The work buffer is
|
|
705
889
|
// sized to this unless a smaller budget enables eviction-based streaming.
|
|
706
890
|
// Index 0 is reserved as a never-decoded padding splat: the sort worker and index buffer pad unused
|
|
707
891
|
// slots with index 0, and leaving that slot zeroed (center.w = 0 => zero covariance, alpha 0) makes
|
|
@@ -735,17 +919,103 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
735
919
|
this._environmentFiles = null;
|
|
736
920
|
}
|
|
737
921
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
922
|
+
if (this._hostCompound) {
|
|
923
|
+
// Hosted: reserve a region of the compound sized to the work buffer, orient the part's proxy for the
|
|
924
|
+
// SOG up-axis, and decode straight into the compound's shared atlas so the streamed splats sort/draw
|
|
925
|
+
// in one pass with the compound's other parts. The compound owns the worker/render; this mesh stays
|
|
926
|
+
// a hidden controller.
|
|
927
|
+
const sogWorld = Matrix.Compose(new Vector3(1, -1, 1), Quaternion.RotationYawPitchRoll(0, -Math.PI / 2, 0), Vector3.ZeroReadOnly);
|
|
928
|
+
// Reserve with SH so the compound converts its SH textures to shared render-targetable integer MRTs and
|
|
929
|
+
// sets its SH degree; the hosted work buffer bakes into those shared targets at the region base offset.
|
|
930
|
+
const host = this._hostCompound.reserveStreamingPart(capacity, sogWorld, this.name + "_part", this._shTextureCount, this._streamShDegree, this._needsRotationScale);
|
|
931
|
+
this._host = host;
|
|
932
|
+
this._positionBase = host.base;
|
|
933
|
+
// Bind this controller's lifetime to its part FROM RESERVATION (not after readiness): removing the part or
|
|
934
|
+
// disposing the compound — even while still downloading/decoding — disposes this stream so it stops writing
|
|
935
|
+
// into the compound's borrowed textures. `_partReleasedByHost` stops dispose() from removing the part again.
|
|
936
|
+
const compound = this._hostCompound;
|
|
937
|
+
// The remove observer needs the assigned part index, so it is registered here (at reservation); the
|
|
938
|
+
// compound-disposal observer was already registered at construction (see the ctor) to cover the pre-pass.
|
|
939
|
+
const removeObserver = compound.onPartRemovedObservable.add((removedIndex) => {
|
|
940
|
+
if (!this._disposed && this._host && removedIndex === this._host.partIndex) {
|
|
941
|
+
this._partReleasedByHost = true;
|
|
942
|
+
this.dispose();
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
this._hostUnsubRemove = () => compound.onPartRemovedObservable.remove(removeObserver);
|
|
946
|
+
const shExternal = this._shTextureCount > 0 && host.shMrtAtlas ? { textureCount: this._shTextureCount, externalMrts: host.shMrtAtlas } : undefined;
|
|
947
|
+
const rotExternal = this._needsRotationScale && host.rotMrtAtlas ? { externalMrt: host.rotMrtAtlas } : undefined;
|
|
948
|
+
// Use the region's ROW-ALIGNED capacity (host.capacity), not the raw stream capacity: backup/restore/
|
|
949
|
+
// relayout scope to whole atlas rows, so an unaligned capacity would drop the region's partial final row.
|
|
950
|
+
this._workBuffer = new GaussianSplattingWorkBuffer(this._scene, host.capacity, {
|
|
951
|
+
mrt: host.mrtAtlas,
|
|
952
|
+
width: host.atlasWidth,
|
|
953
|
+
baseOffset: host.base,
|
|
954
|
+
}, shExternal, rotExternal);
|
|
955
|
+
this._readbackCandidate = this._workBuffer.supportsAsyncCentersReadback;
|
|
956
|
+
// Write decoded centers directly into the compound's shared position buffer (offset by the region base).
|
|
957
|
+
this._splatPositions = host.splatPositions;
|
|
958
|
+
this._vertexCount = capacity;
|
|
959
|
+
// Preserve this region's GPU-only data when the compound grows its atlas (adding a part / another
|
|
960
|
+
// stream): back it up before the old atlas is disposed, then rebind + restore into the new atlas.
|
|
961
|
+
const wb = this._workBuffer;
|
|
962
|
+
this._unsubBeforeRebuild = host.onBeforeAtlasRebuild(() => {
|
|
963
|
+
// Back up the region's atlas texels, and snapshot its CPU positions: the grow reallocates the shared
|
|
964
|
+
// `_splatPositions` and rebuilds it from CPU part sources, but this region has none, so its positions
|
|
965
|
+
// would be lost. `this._splatPositions` is still the pre-grow array and holds the real positions.
|
|
966
|
+
wb.backupRegion();
|
|
967
|
+
this._positionSnapshot = this._splatPositions ? this._splatPositions.slice(this._positionBase * 4, (this._positionBase + this._vertexCount) * 4) : null;
|
|
968
|
+
});
|
|
969
|
+
this._unsubAfterRebuild = host.onAfterAtlasRebuild(() => {
|
|
970
|
+
if (host.mrtAtlas) {
|
|
971
|
+
wb.rebindAtlas(host.mrtAtlas);
|
|
972
|
+
}
|
|
973
|
+
// Rebind to the recreated shared SH and rotation atlases; restoreRegion() writes the backups into them.
|
|
974
|
+
wb.rebindShAtlas(host.shMrtAtlas);
|
|
975
|
+
wb.rebindRotAtlas(host.rotMrtAtlas);
|
|
976
|
+
// A plain grow keeps `host.base`; a compaction relocates the region to a new base. Update the base
|
|
977
|
+
// before restoring so the region's texels and positions land there.
|
|
978
|
+
this._positionBase = host.base;
|
|
979
|
+
wb.setBaseOffset(host.base);
|
|
980
|
+
wb.restoreRegion();
|
|
981
|
+
// Re-cache the reallocated shared array and restore the region's CPU positions at the (new) base.
|
|
982
|
+
this._splatPositions = host.splatPositions;
|
|
983
|
+
if (this._positionSnapshot && this._splatPositions) {
|
|
984
|
+
this._splatPositions.set(this._positionSnapshot, this._positionBase * 4);
|
|
985
|
+
this._positionSnapshot = null;
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
// Nothing active until a resource is decoded (as a range on the reserved part).
|
|
989
|
+
host.setActiveRanges([]);
|
|
990
|
+
}
|
|
991
|
+
else {
|
|
992
|
+
// Bake higher-order SH when requested and present: the work buffer owns `_shTextureCount` integer SH
|
|
993
|
+
// targets and the draw path lights the decoded splats with them (SH degree = max across files).
|
|
994
|
+
const sh = this._shTextureCount > 0 ? { textureCount: this._shTextureCount } : undefined;
|
|
995
|
+
// Decode rotation/scale into an owned 3-attachment half-float target when voxel-IBL shadows are requested.
|
|
996
|
+
const rot = this._needsRotationScale ? {} : undefined;
|
|
997
|
+
this._workBuffer = new GaussianSplattingWorkBuffer(this._scene, capacity, undefined, sh, rot);
|
|
998
|
+
// GPU readback is only enabled after it is validated against a CPU decode on the first file (see
|
|
999
|
+
// _probeReadbackAsync); until then positions are decoded on the CPU so there is always a correct result.
|
|
1000
|
+
this._readbackCandidate = this._workBuffer.supportsAsyncCentersReadback;
|
|
1001
|
+
const splatPositions = new Float32Array(capacity * 4);
|
|
1002
|
+
const textures = this._workBuffer.textures;
|
|
1003
|
+
const shTextures = sh ? this._workBuffer.shTextures : undefined;
|
|
1004
|
+
const rotTextures = rot ? this._workBuffer.rotationTextures : undefined;
|
|
1005
|
+
this._setExternalWorkBuffer(textures[0], textures[1], textures[2], textures[3], splatPositions, capacity, shTextures, this._streamShDegree, rotTextures);
|
|
1006
|
+
// Nothing is active until at least one resource has been decoded.
|
|
1007
|
+
this.setSplatIndexRanges([]);
|
|
1008
|
+
this.setEnabled(true);
|
|
1009
|
+
}
|
|
1010
|
+
// Hosted only: compile the region's backup/restore copy shaders BEFORE decoding any data, so a later
|
|
1011
|
+
// grow/compaction (which synchronously backs this region up) can never race shader compilation and lose it.
|
|
1012
|
+
if (this._host && this._workBuffer) {
|
|
1013
|
+
await this._waitForCanBackupAsync(this._workBuffer);
|
|
1014
|
+
if (this._disposed) {
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
// Step 3: decode the environment, then every node's coarsest LOD as the permanent base layer.
|
|
749
1019
|
if (this._environmentRange && this._environmentFiles) {
|
|
750
1020
|
await this._decodeEnvironmentAsync();
|
|
751
1021
|
}
|
|
@@ -767,11 +1037,52 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
767
1037
|
if (this._disposed) {
|
|
768
1038
|
return;
|
|
769
1039
|
}
|
|
770
|
-
//
|
|
1040
|
+
// Step 4: hand off to the per-frame LOD streaming loop.
|
|
771
1041
|
this._baseLayerReady = true;
|
|
772
1042
|
if (!this._lodObserver) {
|
|
773
1043
|
this._lodObserver = this._scene.onBeforeRenderObservable.add(() => this._onLodFrame());
|
|
774
1044
|
}
|
|
1045
|
+
// Hosted: the reserved part now exists with a decoded base layer and real bounds — release awaiters.
|
|
1046
|
+
this._resolvePartReady();
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Waits (up to a frame cap) until the work buffer's backup/restore copy shaders are compiled, so a later
|
|
1050
|
+
* grow/compaction can preserve this hosted region (see {@link GaussianSplattingWorkBuffer.backupRegion}).
|
|
1051
|
+
* Polls per rendered frame: shader readiness here depends on the render loop (and the shared atlas can be
|
|
1052
|
+
* rebuilt concurrently), so this stays synchronized with the render-driven decode and always makes progress.
|
|
1053
|
+
* On timeout it proceeds best-effort — a subsequent grow/compaction then warns rather than blocking decode.
|
|
1054
|
+
* @param wb the hosted work buffer to wait on
|
|
1055
|
+
*/
|
|
1056
|
+
async _waitForCanBackupAsync(wb) {
|
|
1057
|
+
for (let frame = 0; frame < 600 && !this._disposed; frame++) {
|
|
1058
|
+
if (wb.canBackup) {
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1062
|
+
await new Promise((resolve) => this._scene.onBeforeRenderObservable.addOnce(() => resolve()));
|
|
1063
|
+
}
|
|
1064
|
+
if (!this._disposed && !wb.canBackup) {
|
|
1065
|
+
Logger.Warn("GaussianSplattingStream: backup/restore copy shaders did not compile in time; a grow/compaction before they are ready may drop streamed data.");
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Resolves the resident-splat budget from the raw options, sizing a memory (MB) budget with the actual per-splat
|
|
1070
|
+
* GPU+CPU cost — core data plus the baked SH textures and rotation/scale textures when enabled — so SH/rotation
|
|
1071
|
+
* assets don't silently consume up to double the configured budget. Requires the SH degree (from the metadata
|
|
1072
|
+
* pre-pass) to be known. The smaller of the splat-count and memory budgets wins.
|
|
1073
|
+
*/
|
|
1074
|
+
_resolveResidentBudget() {
|
|
1075
|
+
let budget = this._maxResidentSplats;
|
|
1076
|
+
if (this._memoryBudgetMb > 0) {
|
|
1077
|
+
// Per resident splat: core 84 B, + 16 B per packed-u32 SH texture, + the 3 RGBA rotation textures. The
|
|
1078
|
+
// work buffer uses half-float rotation textures (8 B each = 24 B) when the engine can render to them,
|
|
1079
|
+
// else full float (16 B each = 48 B) — match that so fallback devices aren't under-budgeted.
|
|
1080
|
+
const rotBytes = this._scene.getEngine().getCaps().textureHalfFloatRender ? 24 : 48;
|
|
1081
|
+
const bytesPerSplat = BytesPerResidentSplat + this._shTextureCount * 16 + (this._needsRotationScale ? rotBytes : 0);
|
|
1082
|
+
const fromMB = Math.floor((this._memoryBudgetMb * 1024 * 1024) / bytesPerSplat);
|
|
1083
|
+
budget = budget > 0 ? Math.min(budget, fromMB) : fromMB;
|
|
1084
|
+
}
|
|
1085
|
+
this._residentBudget = budget;
|
|
775
1086
|
}
|
|
776
1087
|
/**
|
|
777
1088
|
* Collects the unique set of source file indices referenced by any LOD of any leaf, sorted ascending.
|
|
@@ -797,6 +1108,19 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
797
1108
|
*/
|
|
798
1109
|
async _gatherCountsAsync(fileIds) {
|
|
799
1110
|
let envCount = 0;
|
|
1111
|
+
// Track the max SH degree/coeffs across every streamed file (+ environment): the baked SH atlas is sized
|
|
1112
|
+
// for the max once, up front, so no mid-stream resize — lower-degree files neutral-fill their higher bands.
|
|
1113
|
+
let maxShDegree = 0;
|
|
1114
|
+
let maxCoeffs = 0;
|
|
1115
|
+
const foldSh = (data) => {
|
|
1116
|
+
const info = GaussianSplattingStream._GetShInfo(data);
|
|
1117
|
+
if (info.degree > maxShDegree) {
|
|
1118
|
+
maxShDegree = info.degree;
|
|
1119
|
+
}
|
|
1120
|
+
if (info.coeffs > maxCoeffs) {
|
|
1121
|
+
maxCoeffs = info.coeffs;
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
800
1124
|
if (this._metadata.environment) {
|
|
801
1125
|
try {
|
|
802
1126
|
const url = this._rootUrl + this._metadata.environment;
|
|
@@ -806,6 +1130,7 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
806
1130
|
if (metaBytes) {
|
|
807
1131
|
const meta = JSON.parse(new TextDecoder().decode(metaBytes));
|
|
808
1132
|
envCount = GaussianSplattingStream._GetSplatCount(meta);
|
|
1133
|
+
foldSh(meta);
|
|
809
1134
|
this._environmentFiles = files;
|
|
810
1135
|
}
|
|
811
1136
|
}
|
|
@@ -832,6 +1157,16 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
832
1157
|
Logger.Warn(`GaussianSplattingStream: failed to load metadata for ${relativePath}: ${e?.message ?? e}`);
|
|
833
1158
|
}
|
|
834
1159
|
}));
|
|
1160
|
+
// Fold in every file's SH (done after the parallel fetch so _fileMeta is fully populated).
|
|
1161
|
+
for (const { sogData } of this._fileMeta.values()) {
|
|
1162
|
+
foldSh(sogData);
|
|
1163
|
+
}
|
|
1164
|
+
// Resolve the stream's baked-SH configuration: enabled only when requested AND the data carries shN.
|
|
1165
|
+
if (this._decodeSh && maxShDegree > 0 && maxCoeffs > 0) {
|
|
1166
|
+
this._streamShDegree = maxShDegree;
|
|
1167
|
+
// Packed-u32 SH textures: 16 SH scalar-bytes per texel, 3 channels per coefficient (matches ParseSogDatas).
|
|
1168
|
+
this._shTextureCount = Math.ceil((maxCoeffs * 3) / 16);
|
|
1169
|
+
}
|
|
835
1170
|
return envCount;
|
|
836
1171
|
}
|
|
837
1172
|
/**
|
|
@@ -872,10 +1207,50 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
872
1207
|
* @param count number of splats in the range
|
|
873
1208
|
*/
|
|
874
1209
|
_applyPositions(positions, base, count) {
|
|
875
|
-
|
|
1210
|
+
// In hosted mode _splatPositions is the compound's shared buffer; the region starts at _positionBase.
|
|
1211
|
+
this._splatPositions.set(positions, (this._positionBase + base) * 4);
|
|
876
1212
|
this._updateBounds(positions, count);
|
|
877
1213
|
// Incrementally patch only this range in the sort worker (avoids the full position-buffer re-copy).
|
|
878
|
-
this.
|
|
1214
|
+
this._sinkPostPositionsRange(base, count);
|
|
1215
|
+
}
|
|
1216
|
+
// ---- Sink routing: standalone drives this mesh; hosted drives the compound's reserved-part handle. ----
|
|
1217
|
+
/**
|
|
1218
|
+
* Sets the active source ranges (local to the stream's buffer) on the render sink.
|
|
1219
|
+
* @param localRanges active ranges in the stream's local index space
|
|
1220
|
+
*/
|
|
1221
|
+
_sinkSetActiveRanges(localRanges) {
|
|
1222
|
+
if (this._host) {
|
|
1223
|
+
this._host.setActiveRanges(localRanges);
|
|
1224
|
+
}
|
|
1225
|
+
else {
|
|
1226
|
+
this.setSplatIndexRanges(localRanges);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Patches a decoded position range (local offset) into the render sink's sort worker.
|
|
1231
|
+
* @param base first splat index of the range, local to the stream's buffer
|
|
1232
|
+
* @param count number of splats in the range
|
|
1233
|
+
*/
|
|
1234
|
+
_sinkPostPositionsRange(base, count) {
|
|
1235
|
+
if (this._host) {
|
|
1236
|
+
this._host.postPositionsRange(base, count);
|
|
1237
|
+
}
|
|
1238
|
+
else {
|
|
1239
|
+
this._postWorkerPositionsRange(base, count);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
/** Re-posts the full position/part set to the render sink's worker (after a relayout moved the region). */
|
|
1243
|
+
_sinkNotifyDataChanged() {
|
|
1244
|
+
if (this._host) {
|
|
1245
|
+
this._host.notifyDataChanged();
|
|
1246
|
+
}
|
|
1247
|
+
else {
|
|
1248
|
+
this._notifyWorkerNewData();
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
/** Whether the render sink's depth sort is settled. */
|
|
1252
|
+
get _sinkIsDepthSortSettled() {
|
|
1253
|
+
return this._host ? this._host.isDepthSortSettled : this._isDepthSortSettled;
|
|
879
1254
|
}
|
|
880
1255
|
/**
|
|
881
1256
|
* One-time validation of GPU position readback: reads a sample of the just-decoded range back from the work
|
|
@@ -1157,13 +1532,15 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
1157
1532
|
this._workBuffer.relayoutSync(srcIndexByDst);
|
|
1158
1533
|
// CPU positions: compaction only ever moves a block to a lower offset, so copying in place in ascending
|
|
1159
1534
|
// new-offset order is safe (a block's source is never overwritten by an earlier move). This avoids a
|
|
1160
|
-
// full capacity*4 scratch buffer.
|
|
1535
|
+
// full capacity*4 scratch buffer. Block offsets are region-local; in hosted mode `_splatPositions` is the
|
|
1536
|
+
// compound-wide buffer, so shift both source and destination by the region base (`_positionBase`, 0 standalone).
|
|
1161
1537
|
const positions = this._splatPositions;
|
|
1538
|
+
const base = this._positionBase;
|
|
1162
1539
|
resident.sort((a, b) => a.offset - b.offset);
|
|
1163
1540
|
for (const block of resident) {
|
|
1164
1541
|
const oldOffset = oldOffsets.get(block.file);
|
|
1165
1542
|
if (oldOffset !== block.offset) {
|
|
1166
|
-
positions.copyWithin(block.offset * 4, oldOffset * 4, (oldOffset + block.count) * 4);
|
|
1543
|
+
positions.copyWithin((base + block.offset) * 4, (base + oldOffset) * 4, (base + oldOffset + block.count) * 4);
|
|
1167
1544
|
}
|
|
1168
1545
|
}
|
|
1169
1546
|
// Update the environment offset (it may have moved), re-post to the sort worker, and refresh ranges.
|
|
@@ -1173,7 +1550,7 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
1173
1550
|
this._environmentRange.offset = envOffset;
|
|
1174
1551
|
}
|
|
1175
1552
|
}
|
|
1176
|
-
this.
|
|
1553
|
+
this._sinkNotifyDataChanged();
|
|
1177
1554
|
this._refreshActiveRanges();
|
|
1178
1555
|
}
|
|
1179
1556
|
/**
|
|
@@ -1414,7 +1791,7 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
1414
1791
|
// World-space frustum planes from the current view-projection, tested against each node's world AABB.
|
|
1415
1792
|
// force=false uses the renderId/sync fast-path (still recomputes when the transform actually changed),
|
|
1416
1793
|
// avoiding a full world-matrix recompute every frame for the per-node frustum test.
|
|
1417
|
-
const world = this.
|
|
1794
|
+
const world = this._getEffectiveWorldMatrix(false);
|
|
1418
1795
|
camera.getViewMatrix().multiplyToRef(camera.getProjectionMatrix(), this._cullViewProj);
|
|
1419
1796
|
Frustum.GetPlanesToRef(this._cullViewProj, this._frustumPlanes);
|
|
1420
1797
|
for (const node of this._leafNodes) {
|
|
@@ -1435,6 +1812,38 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
1435
1812
|
static _GetSplatCount(data) {
|
|
1436
1813
|
return data.count ?? (Array.isArray(data.means.shape) ? data.means.shape[0] : 0);
|
|
1437
1814
|
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Reads a SOG file's higher-order SH degree and coefficient count from its metadata, mirroring
|
|
1817
|
+
* {@link ParseSogDatas}'s `coeffs`/`shDegree` derivation. Returns zeros when the file carries no `shN`.
|
|
1818
|
+
* @param data parsed SOG root metadata
|
|
1819
|
+
* @returns the SH degree and higher-order coefficient count (excludes the DC/SH0 term)
|
|
1820
|
+
*/
|
|
1821
|
+
static _GetShInfo(data) {
|
|
1822
|
+
if (!data.shN) {
|
|
1823
|
+
return { degree: 0, coeffs: 0 };
|
|
1824
|
+
}
|
|
1825
|
+
// Derive the SH degree from remote (untrusted) metadata, then validate/clamp it: the degree drives the SH
|
|
1826
|
+
// render-target count and decode-pass count, so a bogus (huge / non-finite / negative) `bands` or `shape`
|
|
1827
|
+
// must not be able to demand unbounded allocation. The draw path supports shTexture0..4, i.e. degree <= 4.
|
|
1828
|
+
const maxDegree = 4;
|
|
1829
|
+
let degree = 0;
|
|
1830
|
+
const bands = data.shN.bands;
|
|
1831
|
+
if (typeof bands === "number" && Number.isFinite(bands) && bands > 0) {
|
|
1832
|
+
degree = Math.floor(bands);
|
|
1833
|
+
}
|
|
1834
|
+
else if (Array.isArray(data.shN.shape) && Number.isFinite(data.shN.shape[1]) && data.shN.shape[1] > 0) {
|
|
1835
|
+
const shapeCoeffs = Math.floor(data.shN.shape[1] / 3);
|
|
1836
|
+
degree = shapeCoeffs > 0 ? Math.round(Math.sqrt(shapeCoeffs + 1) - 1) : 0;
|
|
1837
|
+
}
|
|
1838
|
+
if (!(degree > 0)) {
|
|
1839
|
+
return { degree: 0, coeffs: 0 };
|
|
1840
|
+
}
|
|
1841
|
+
if (degree > maxDegree) {
|
|
1842
|
+
Logger.Warn(`GaussianSplattingStream: SH degree ${degree} exceeds the maximum supported (${maxDegree}); clamping.`);
|
|
1843
|
+
degree = maxDegree;
|
|
1844
|
+
}
|
|
1845
|
+
return { degree, coeffs: (degree + 1) ** 2 - 1 };
|
|
1846
|
+
}
|
|
1438
1847
|
/**
|
|
1439
1848
|
* Disposes all GPU source textures of a SOG pack (they are only needed for the one decode pass).
|
|
1440
1849
|
* @param pack the SOG texture pack
|
|
@@ -1465,7 +1874,13 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
1465
1874
|
min.minimizeInPlaceFromFloats(x, y, z);
|
|
1466
1875
|
max.maximizeInPlaceFromFloats(x, y, z);
|
|
1467
1876
|
}
|
|
1468
|
-
|
|
1877
|
+
// Hosted: grow the reserved part's (and compound's) bounds. Standalone: set this mesh's bounds.
|
|
1878
|
+
if (this._host) {
|
|
1879
|
+
this._host.expandBounds(min, max);
|
|
1880
|
+
}
|
|
1881
|
+
else {
|
|
1882
|
+
this.setBoundingInfo(new BoundingInfo(min, max));
|
|
1883
|
+
}
|
|
1469
1884
|
}
|
|
1470
1885
|
/**
|
|
1471
1886
|
* Rebuilds the active interval set from the environment plus each node's currently-selected LOD entry,
|
|
@@ -1490,7 +1905,8 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
1490
1905
|
}
|
|
1491
1906
|
ranges.push({ offset: base + entry.offset, count: entry.count });
|
|
1492
1907
|
}
|
|
1493
|
-
|
|
1908
|
+
// Ranges are local to the stream's buffer; the sink (compound handle) offsets them by the region base.
|
|
1909
|
+
this._sinkSetActiveRanges(GaussianSplattingStream._CoalesceRanges(ranges));
|
|
1494
1910
|
}
|
|
1495
1911
|
/**
|
|
1496
1912
|
* Sorts and merges adjacent/overlapping ranges to keep the interval list compact.
|
|
@@ -1538,4 +1954,64 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
|
|
|
1538
1954
|
return files;
|
|
1539
1955
|
}
|
|
1540
1956
|
}
|
|
1957
|
+
/**
|
|
1958
|
+
* Adds a PlayCanvas-style SOG LOD stream as a part of a compound Gaussian Splatting mesh, so the streamed
|
|
1959
|
+
* splats are depth-sorted and rendered in ONE pass together with the compound's other (static) parts.
|
|
1960
|
+
*
|
|
1961
|
+
* The returned mesh is a hidden controller: it streams SOG LOD files, GPU-decodes them into a reserved region
|
|
1962
|
+
* of the compound's shared atlas, and drives which of its splats are active (LOD) — the compound owns the sort
|
|
1963
|
+
* and the single instanced draw. The SOG up-axis orientation is applied to the reserved part's proxy transform;
|
|
1964
|
+
* move/hide the part via the proxy (`streamController` exposes it once streaming has started).
|
|
1965
|
+
* @param compound the compound mesh to add the streamed part to
|
|
1966
|
+
* @param name name for the streaming controller / part
|
|
1967
|
+
* @param metadata parsed `lod-meta.json`
|
|
1968
|
+
* @param rootUrl base URL the metadata's relative paths resolve against
|
|
1969
|
+
* @param options streaming options
|
|
1970
|
+
* @returns the streaming controller mesh (hidden; drives the reserved compound part)
|
|
1971
|
+
* @experimental
|
|
1972
|
+
*/
|
|
1973
|
+
export function AddGaussianSplattingStreamPart(compound, name, metadata, rootUrl, options = {}) {
|
|
1974
|
+
return new GaussianSplattingStream(name, metadata, rootUrl, compound.getScene(), { ...options, hostCompound: compound });
|
|
1975
|
+
}
|
|
1976
|
+
/**
|
|
1977
|
+
* Adds a PlayCanvas-style SOG LOD stream as a part of a compound Gaussian Splatting mesh and resolves once the
|
|
1978
|
+
* part is ready to use, returning its {@link GaussianSplattingPartProxyMesh} — the same handle
|
|
1979
|
+
* `GaussianSplattingCompoundMesh.addPart` returns for a static part. This lets a host application treat a
|
|
1980
|
+
* streamed splat exactly like any other compound part (place/frame/gizmo via the proxy, remove via
|
|
1981
|
+
* `compound.removePart(proxy.partIndex)`); the streaming controller lives behind the proxy and is disposed
|
|
1982
|
+
* automatically when the part is removed.
|
|
1983
|
+
*
|
|
1984
|
+
* Resolves after the reserved region exists and its base layer has decoded (so the proxy's bounds are real),
|
|
1985
|
+
* and rejects if streaming fails before that (the partially-constructed stream is disposed on rejection).
|
|
1986
|
+
*
|
|
1987
|
+
* NOTE: the base-layer decode runs on the GPU inside the scene's render loop, so this promise only resolves once
|
|
1988
|
+
* the scene is rendering. Do not `await` it before the render loop has started (it would never resolve) — start
|
|
1989
|
+
* rendering (e.g. `engine.runRenderLoop`) first, or `await` it concurrently with the first frames.
|
|
1990
|
+
* @param compound the compound mesh to add the streamed part to
|
|
1991
|
+
* @param name name for the streaming controller / part
|
|
1992
|
+
* @param metadata parsed `lod-meta.json`
|
|
1993
|
+
* @param rootUrl base URL the metadata's relative paths resolve against
|
|
1994
|
+
* @param options streaming options
|
|
1995
|
+
* @returns the part proxy driving the streamed region, ready to place/frame
|
|
1996
|
+
* @experimental
|
|
1997
|
+
*/
|
|
1998
|
+
export async function AddGaussianSplattingStreamPartAsync(compound, name, metadata, rootUrl, options = {}) {
|
|
1999
|
+
const stream = new GaussianSplattingStream(name, metadata, rootUrl, compound.getScene(), { ...options, hostCompound: compound });
|
|
2000
|
+
try {
|
|
2001
|
+
await stream.whenPartReadyAsync();
|
|
2002
|
+
}
|
|
2003
|
+
catch (e) {
|
|
2004
|
+
stream.dispose();
|
|
2005
|
+
throw e;
|
|
2006
|
+
}
|
|
2007
|
+
const proxy = stream.streamingPartProxy;
|
|
2008
|
+
if (!proxy) {
|
|
2009
|
+
stream.dispose();
|
|
2010
|
+
throw new Error("GaussianSplattingStream: streaming part was not reserved.");
|
|
2011
|
+
}
|
|
2012
|
+
// The controller already binds its own lifetime to the part (removePart / compound disposal dispose the stream,
|
|
2013
|
+
// and stream disposal releases the reserved part) — wired at reservation inside the stream, so it also covers the
|
|
2014
|
+
// download/decode window before this promise resolves. Nothing to register here.
|
|
2015
|
+
return proxy;
|
|
2016
|
+
}
|
|
1541
2017
|
//# sourceMappingURL=gaussianSplattingStream.js.map
|