@babylonjs/core 9.12.0 → 9.12.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.
Files changed (90) hide show
  1. package/Behaviors/Cameras/interpolatingBehavior.d.ts +17 -2
  2. package/Behaviors/Cameras/interpolatingBehavior.js +53 -23
  3. package/Behaviors/Cameras/interpolatingBehavior.js.map +1 -1
  4. package/Cameras/Inputs/flyCameraKeyboardInput.js +9 -1
  5. package/Cameras/Inputs/flyCameraKeyboardInput.js.map +1 -1
  6. package/Cameras/Inputs/flyCameraMouseInput.d.ts +2 -0
  7. package/Cameras/Inputs/flyCameraMouseInput.js +14 -2
  8. package/Cameras/Inputs/flyCameraMouseInput.js.map +1 -1
  9. package/Cameras/Inputs/freeCameraKeyboardMoveInput.js +25 -5
  10. package/Cameras/Inputs/freeCameraKeyboardMoveInput.js.map +1 -1
  11. package/Cameras/Inputs/freeCameraMouseInput.d.ts +14 -0
  12. package/Cameras/Inputs/freeCameraMouseInput.js +26 -4
  13. package/Cameras/Inputs/freeCameraMouseInput.js.map +1 -1
  14. package/Cameras/Inputs/freeCameraTouchInput.js +16 -5
  15. package/Cameras/Inputs/freeCameraTouchInput.js.map +1 -1
  16. package/Cameras/Inputs/geospatialCameraKeyboardInput.js +8 -1
  17. package/Cameras/Inputs/geospatialCameraKeyboardInput.js.map +1 -1
  18. package/Cameras/arcRotateCamera.pure.js.map +1 -1
  19. package/Cameras/flyCamera.pure.d.ts +8 -0
  20. package/Cameras/flyCamera.pure.js.map +1 -1
  21. package/Cameras/freeCamera.pure.d.ts +8 -0
  22. package/Cameras/freeCamera.pure.js.map +1 -1
  23. package/Cameras/geospatialCamera.pure.d.ts +30 -7
  24. package/Cameras/geospatialCamera.pure.js +39 -14
  25. package/Cameras/geospatialCamera.pure.js.map +1 -1
  26. package/Cameras/geospatialCameraMovement.js +2 -2
  27. package/Cameras/geospatialCameraMovement.js.map +1 -1
  28. package/Cameras/index.d.ts +1 -0
  29. package/Cameras/index.js +1 -0
  30. package/Cameras/index.js.map +1 -1
  31. package/Cameras/pure.d.ts +1 -0
  32. package/Cameras/pure.js +1 -0
  33. package/Cameras/pure.js.map +1 -1
  34. package/Cameras/targetCamera.pure.d.ts +25 -0
  35. package/Cameras/targetCamera.pure.js +83 -25
  36. package/Cameras/targetCamera.pure.js.map +1 -1
  37. package/Cameras/targetCameraMovement.d.ts +56 -0
  38. package/Cameras/targetCameraMovement.js +63 -0
  39. package/Cameras/targetCameraMovement.js.map +1 -0
  40. package/Engines/AbstractEngine/abstractEngine.scissor.d.ts +18 -0
  41. package/Engines/AbstractEngine/abstractEngine.scissor.js +2 -0
  42. package/Engines/AbstractEngine/abstractEngine.scissor.js.map +1 -0
  43. package/Engines/abstractEngine.pure.js +2 -2
  44. package/Engines/abstractEngine.pure.js.map +1 -1
  45. package/Engines/engine.d.ts +1 -0
  46. package/Engines/engine.js +1 -0
  47. package/Engines/engine.js.map +1 -1
  48. package/Engines/engine.pure.d.ts +0 -12
  49. package/Engines/engine.pure.js +0 -20
  50. package/Engines/engine.pure.js.map +1 -1
  51. package/Engines/nullEngine.pure.d.ts +66 -0
  52. package/Engines/nullEngine.pure.js +93 -0
  53. package/Engines/nullEngine.pure.js.map +1 -1
  54. package/Engines/pure.d.ts +1 -0
  55. package/Engines/pure.js +1 -0
  56. package/Engines/pure.js.map +1 -1
  57. package/Engines/thinEngine.scissor.d.ts +6 -0
  58. package/Engines/thinEngine.scissor.js +9 -0
  59. package/Engines/thinEngine.scissor.js.map +1 -0
  60. package/Engines/thinEngine.scissor.pure.d.ts +6 -0
  61. package/Engines/thinEngine.scissor.pure.js +24 -0
  62. package/Engines/thinEngine.scissor.pure.js.map +1 -0
  63. package/Engines/thinNativeEngine.d.ts +0 -1
  64. package/Engines/thinNativeEngine.js +0 -1
  65. package/Engines/thinNativeEngine.js.map +1 -1
  66. package/Engines/thinNativeEngine.pure.js +7 -0
  67. package/Engines/thinNativeEngine.pure.js.map +1 -1
  68. package/Engines/webgpuEngine.pure.js.map +1 -1
  69. package/Materials/GreasedLine/greasedLinePluginMaterialShadersGLSL.js +10 -3
  70. package/Materials/GreasedLine/greasedLinePluginMaterialShadersGLSL.js.map +1 -1
  71. package/Meshes/GaussianSplatting/gaussianSplattingMesh.pure.js +18 -8
  72. package/Meshes/GaussianSplatting/gaussianSplattingMesh.pure.js.map +1 -1
  73. package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.pure.d.ts +50 -1
  74. package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.pure.js +199 -104
  75. package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.pure.js.map +1 -1
  76. package/Meshes/GaussianSplatting/gaussianSplattingSortWorker.d.ts +37 -0
  77. package/Meshes/GaussianSplatting/gaussianSplattingSortWorker.js +160 -0
  78. package/Meshes/GaussianSplatting/gaussianSplattingSortWorker.js.map +1 -0
  79. package/Meshes/geometry.js +30 -15
  80. package/Meshes/geometry.js.map +1 -1
  81. package/Meshes/mesh.pure.js +26 -14
  82. package/Meshes/mesh.pure.js.map +1 -1
  83. package/Misc/tools.pure.js +1 -1
  84. package/Misc/tools.pure.js.map +1 -1
  85. package/Shaders/greasedLine.vertex.js +19 -3
  86. package/Shaders/greasedLine.vertex.js.map +1 -1
  87. package/package.json +3 -2
  88. package/scene.pure.d.ts +12 -0
  89. package/scene.pure.js +36 -0
  90. package/scene.pure.js.map +1 -1
@@ -40,6 +40,19 @@ interface IUpdateOptions {
40
40
  /** @internal When set, skips reprocessing splats [0, previousVertexCount) and copies from cached arrays instead. */
41
41
  previousVertexCount?: number;
42
42
  }
43
+ /**
44
+ * Defines a contiguous source-splat range to render from a GaussianSplattingMesh.
45
+ */
46
+ export interface IGaussianSplattingSplatRange {
47
+ /**
48
+ * First source splat index to render.
49
+ */
50
+ offset: number;
51
+ /**
52
+ * Number of source splats to render.
53
+ */
54
+ count: number;
55
+ }
43
56
  interface ITextureDataUpdateCapableEngine {
44
57
  updateTextureData(texture: InternalTexture, imageData: ArrayBufferView, xOffset: number, yOffset: number, width: number, height: number, faceIndex?: number, lod?: number, generateMipMaps?: boolean): void;
45
58
  updateRawTexture(texture: Nullable<InternalTexture>, data: Nullable<ArrayBufferView>, format: number, invertY: boolean, compression?: Nullable<string>, type?: number, useSRGBBuffer?: boolean): void;
@@ -254,6 +267,7 @@ export declare class GaussianSplattingMeshBase extends Mesh {
254
267
  protected _canPostToWorker: boolean;
255
268
  private _readyToDisplay;
256
269
  private _sortRequestId;
270
+ private _activeRangeVersion;
257
271
  private _hasRenderedOnce;
258
272
  protected _covariancesATexture: Nullable<BaseTexture>;
259
273
  protected _covariancesBTexture: Nullable<BaseTexture>;
@@ -286,6 +300,9 @@ export declare class GaussianSplattingMeshBase extends Mesh {
286
300
  private _splatSizeMin;
287
301
  private _splatSizeMax;
288
302
  private _sortIsDirty;
303
+ private _activeSplatRanges;
304
+ private _activeSplatRangeKey;
305
+ private _activeSplatRenderCount;
289
306
  protected _cachedBoundingMin: Nullable<Vector3>;
290
307
  protected _cachedBoundingMax: Nullable<Vector3>;
291
308
  private static _RowOutputLength;
@@ -317,6 +334,7 @@ export declare class GaussianSplattingMeshBase extends Mesh {
317
334
  * @internal
318
335
  */
319
336
  protected static _GetSplatDataFloats(data: ArrayBuffer | ArrayBufferView): Float32Array;
337
+ private static _BuildSplatRangeData;
320
338
  /**
321
339
  * Cosine value of the angle threshold to update view dependent splat sorting. Default is 0.0001.
322
340
  */
@@ -346,6 +364,38 @@ export declare class GaussianSplattingMeshBase extends Mesh {
346
364
  * Number of splats in the mesh
347
365
  */
348
366
  get splatCount(): number | undefined;
367
+ /**
368
+ * Number of source splats currently selected for rendering.
369
+ * When no range filter is active, this is the mesh's full source splat count.
370
+ */
371
+ get renderedSplatCount(): number;
372
+ /**
373
+ * Restricts rendering to the provided source splat ranges.
374
+ * Passing `null` clears the range filter and renders the full source splat set.
375
+ * @param ranges contiguous source ranges to render, or null to render all splats
376
+ */
377
+ setSplatIndexRanges(ranges: Nullable<readonly IGaussianSplattingSplatRange[]>): void;
378
+ private _setSplatIndexRanges;
379
+ private _ensureDepthMixSize;
380
+ /**
381
+ * Sends the active source-splat intervals to the sort worker. When no range filter is active,
382
+ * a single interval covering all source splats is sent so the worker never assumes the full set.
383
+ */
384
+ private _postIntervalsToWorker;
385
+ /**
386
+ * Initializes this mesh to render from an externally-provided, GPU-decoded work buffer, bypassing the
387
+ * CPU `updateData` path. The four textures must hold the standard decoded GS layout addressed by linear
388
+ * splat index over a square texture: centers (x,y,z,1), covariance A (Sigma00,01,02,11) and B
389
+ * (Sigma12,22,*,*) as full covariance (so `center.w` is 1), and RGBA color. `splatPositions` are the
390
+ * stride-4 CPU centers consumed by the depth-sort worker.
391
+ * @param centers centers texture
392
+ * @param covariancesA covariance A texture
393
+ * @param covariancesB covariance B texture
394
+ * @param colors color texture
395
+ * @param splatPositions stride-4 CPU centers for depth sorting (length vertexCount*4)
396
+ * @param vertexCount number of splats addressable in the work buffer
397
+ */
398
+ protected _setExternalWorkBuffer(centers: BaseTexture, covariancesA: BaseTexture, covariancesB: BaseTexture, colors: BaseTexture, splatPositions: Float32Array, vertexCount: number): void;
349
399
  /**
350
400
  * returns the splats data array buffer that contains in order : postions (3 floats), size (3 floats), color (4 bytes), orientation quaternion (4 bytes)
351
401
  * Only available if the mesh was created with keepInRam: true
@@ -559,7 +609,6 @@ export declare class GaussianSplattingMeshBase extends Mesh {
559
609
  * @returns a new Gaussian Splatting Mesh
560
610
  */
561
611
  clone(name?: string): GaussianSplattingMeshBase;
562
- private static _CreateWorker;
563
612
  protected _makeEmptySplat(index: number, covA: Uint16Array, covB: Uint16Array, colorArray: Uint8Array): void;
564
613
  /**
565
614
  * Processes a single splat from the source buffer (at srcIndex) and writes the result into
@@ -13,6 +13,7 @@ import { Scalar } from "../../Maths/math.scalar.js";
13
13
  import { runCoroutineSync, runCoroutineAsync, createYieldingScheduler } from "../../Misc/coroutine.js";
14
14
  import { EngineStore } from "../../Engines/engineStore.js";
15
15
  import { ImportMeshAsync } from "../../Loading/sceneLoader.js";
16
+ import { GaussianSplattingSortWorker, GaussianSplattingSortWorkerCommand } from "./gaussianSplattingSortWorker.js";
16
17
  const IsNative = typeof _native !== "undefined";
17
18
  const Native = IsNative ? _native : null;
18
19
  // @internal
@@ -273,6 +274,27 @@ export class GaussianSplattingMeshBase extends Mesh {
273
274
  }
274
275
  return new Float32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / floatSize);
275
276
  }
277
+ static _BuildSplatRangeData(ranges, vertexCount) {
278
+ if (ranges === null) {
279
+ return { ranges: null, count: vertexCount, key: "" };
280
+ }
281
+ const rangePairs = [];
282
+ let totalCount = 0;
283
+ let key = "";
284
+ for (const range of ranges) {
285
+ const start = Math.max(0, Math.floor(range.offset));
286
+ const count = Math.max(0, Math.floor(range.count));
287
+ const end = Math.min(vertexCount, start + count);
288
+ const rangeCount = Math.max(0, end - start);
289
+ if (rangeCount === 0) {
290
+ continue;
291
+ }
292
+ rangePairs.push(start, rangeCount);
293
+ totalCount += rangeCount;
294
+ key += `${start}:${rangeCount};`;
295
+ }
296
+ return { ranges: new Uint32Array(rangePairs), count: totalCount, key };
297
+ }
276
298
  /**
277
299
  * If true, disables depth sorting of the splats (default: false)
278
300
  */
@@ -326,6 +348,100 @@ export class GaussianSplattingMeshBase extends Mesh {
326
348
  get splatCount() {
327
349
  return this._splatIndex?.length;
328
350
  }
351
+ /**
352
+ * Number of source splats currently selected for rendering.
353
+ * When no range filter is active, this is the mesh's full source splat count.
354
+ */
355
+ get renderedSplatCount() {
356
+ return this._activeSplatRanges ? this._activeSplatRenderCount : this._vertexCount;
357
+ }
358
+ /**
359
+ * Restricts rendering to the provided source splat ranges.
360
+ * Passing `null` clears the range filter and renders the full source splat set.
361
+ * @param ranges contiguous source ranges to render, or null to render all splats
362
+ */
363
+ setSplatIndexRanges(ranges) {
364
+ this._setSplatIndexRanges(ranges);
365
+ }
366
+ _setSplatIndexRanges(ranges) {
367
+ const rangeData = GaussianSplattingMeshBase._BuildSplatRangeData(ranges, this._vertexCount);
368
+ const isSameRangeSet = this._activeSplatRanges === null ? rangeData.ranges === null : rangeData.ranges !== null && this._activeSplatRangeKey === rangeData.key;
369
+ if (isSameRangeSet) {
370
+ return;
371
+ }
372
+ this._activeSplatRanges = rangeData.ranges;
373
+ this._activeSplatRangeKey = rangeData.key;
374
+ this._activeSplatRenderCount = rangeData.count;
375
+ this._activeRangeVersion++;
376
+ this._postIntervalsToWorker();
377
+ if (this._worker) {
378
+ // Defer swapping the rendered index buffer until the worker returns a depth sort computed for
379
+ // this exact range set (see the worker `onmessage` handler). Until then we keep rendering the
380
+ // previous fully-sorted state, so a range/LOD change never shows an unsorted flash nor a stale
381
+ // index buffer (rendering indices that belong to a different active set, which caused the
382
+ // disappear/reappear flicker). Only the worker's depth buffer is resized here so the next sort
383
+ // produces the right number of entries.
384
+ this._ensureDepthMixSize(Math.max((this._activeSplatRenderCount + 15) & ~0xf, 16));
385
+ this._sortIsDirty = true;
386
+ this._postToWorker(true);
387
+ }
388
+ else {
389
+ // Synchronous sort paths (disabled depth sort / Native): swap the index buffer immediately.
390
+ this._updateSplatIndexBuffer(this._vertexCount);
391
+ this._postToWorker(true);
392
+ }
393
+ }
394
+ // (Re)allocates the worker depth buffer to the given padded size. A fresh array is allocated when the
395
+ // size differs or the current buffer is detached (in-flight in the worker), so a queued sort can be
396
+ // re-posted with a correctly-sized buffer without disturbing the in-flight one.
397
+ _ensureDepthMixSize(paddedCount) {
398
+ if (IsNative) {
399
+ return;
400
+ }
401
+ if (!this._depthMix || this._depthMix.length !== paddedCount || this._depthMix.buffer.byteLength === 0) {
402
+ this._depthMix = new BigInt64Array(paddedCount);
403
+ }
404
+ }
405
+ /**
406
+ * Sends the active source-splat intervals to the sort worker. When no range filter is active,
407
+ * a single interval covering all source splats is sent so the worker never assumes the full set.
408
+ */
409
+ _postIntervalsToWorker() {
410
+ if (!this._worker) {
411
+ return;
412
+ }
413
+ const intervals = this._activeSplatRanges ? new Uint32Array(this._activeSplatRanges) : new Uint32Array([0, this._vertexCount]);
414
+ this._worker.postMessage({ command: GaussianSplattingSortWorkerCommand.INTERVALS, intervals }, [intervals.buffer]);
415
+ }
416
+ /**
417
+ * Initializes this mesh to render from an externally-provided, GPU-decoded work buffer, bypassing the
418
+ * CPU `updateData` path. The four textures must hold the standard decoded GS layout addressed by linear
419
+ * splat index over a square texture: centers (x,y,z,1), covariance A (Sigma00,01,02,11) and B
420
+ * (Sigma12,22,*,*) as full covariance (so `center.w` is 1), and RGBA color. `splatPositions` are the
421
+ * stride-4 CPU centers consumed by the depth-sort worker.
422
+ * @param centers centers texture
423
+ * @param covariancesA covariance A texture
424
+ * @param covariancesB covariance B texture
425
+ * @param colors color texture
426
+ * @param splatPositions stride-4 CPU centers for depth sorting (length vertexCount*4)
427
+ * @param vertexCount number of splats addressable in the work buffer
428
+ */
429
+ _setExternalWorkBuffer(centers, covariancesA, covariancesB, colors, splatPositions, vertexCount) {
430
+ this._covariancesATexture = covariancesA;
431
+ this._covariancesBTexture = covariancesB;
432
+ this._centersTexture = centers;
433
+ this._colorsTexture = colors;
434
+ this._splatPositions = splatPositions;
435
+ this._vertexCount = vertexCount;
436
+ this._activeSplatRanges = null;
437
+ this._activeSplatRangeKey = "";
438
+ this._activeSplatRenderCount = 0;
439
+ this._readyToDisplay = false;
440
+ // Sizes _splatIndex/_depthMix, starts the sort worker, and posts positions + intervals.
441
+ this._instantiateWorker();
442
+ // updateData (bypassed here) normally enables the mesh; do it explicitly for the work-buffer path.
443
+ this.setEnabled(true);
444
+ }
329
445
  /**
330
446
  * returns the splats data array buffer that contains in order : postions (3 floats), size (3 floats), color (4 bytes), orientation quaternion (4 bytes)
331
447
  * Only available if the mesh was created with keepInRam: true
@@ -577,6 +693,9 @@ export class GaussianSplattingMeshBase extends Mesh {
577
693
  this._canPostToWorker = true;
578
694
  this._readyToDisplay = false;
579
695
  this._sortRequestId = 0;
696
+ // Incremented on every active-range change. A worker sort result is only applied when it was computed
697
+ // for the current range version (and size), so a sort for stale ranges is never rendered.
698
+ this._activeRangeVersion = 0;
580
699
  this._hasRenderedOnce = false;
581
700
  this._covariancesATexture = null;
582
701
  this._covariancesBTexture = null;
@@ -609,6 +728,9 @@ export class GaussianSplattingMeshBase extends Mesh {
609
728
  this._splatSizeMin = Infinity;
610
729
  this._splatSizeMax = -Infinity;
611
730
  this._sortIsDirty = false;
731
+ this._activeSplatRanges = null;
732
+ this._activeSplatRangeKey = "";
733
+ this._activeSplatRenderCount = 0;
612
734
  // Cached bounding box for incremental addPart updates (O(1) vs O(N) scan of positions)
613
735
  this._cachedBoundingMin = null;
614
736
  this._cachedBoundingMax = null;
@@ -910,16 +1032,24 @@ export class GaussianSplattingMeshBase extends Mesh {
910
1032
  this._canPostToWorker = false;
911
1033
  if (this._worker) {
912
1034
  this._worker.postMessage({
1035
+ command: GaussianSplattingSortWorkerCommand.SORT,
913
1036
  worldMatrix: worldMatrix.m,
914
1037
  cameraForward: [cameraViewMatrix.m[2], cameraViewMatrix.m[6], cameraViewMatrix.m[10]],
915
1038
  cameraPosition: [camera.globalPosition.x, camera.globalPosition.y, camera.globalPosition.z],
916
1039
  depthMix: this._depthMix,
917
1040
  cameraId: camera.uniqueId,
918
1041
  sortRequestId: cameraViewInfos.sortRequestId,
1042
+ rangeVersion: this._activeRangeVersion,
919
1043
  }, [this._depthMix.buffer]);
920
1044
  }
921
1045
  else if (Native?.sortSplats) {
922
- Native.sortSplats(this._modelViewProjectionMatrix, this._splatPositions, this._splatIndex, this._scene.useRightHandedSystem);
1046
+ if (this._activeSplatRanges) {
1047
+ // Native sort can't filter ranges: fall back to the (already range-filtered) identity index buffer.
1048
+ this._updateSplatIndexBuffer(this._vertexCount);
1049
+ }
1050
+ else {
1051
+ Native.sortSplats(this._modelViewProjectionMatrix, this._splatPositions, this._splatIndex, this._scene.useRightHandedSystem);
1052
+ }
923
1053
  if (cameraViewInfos.splatIndexBufferSet) {
924
1054
  cameraViewInfos.mesh.thinInstanceBufferUpdated("splatIndex");
925
1055
  }
@@ -1960,7 +2090,9 @@ export class GaussianSplattingMeshBase extends Mesh {
1960
2090
  const positions = Float32Array.from(this._splatPositions);
1961
2091
  const vertexCount = this._vertexCount;
1962
2092
  if (this._worker) {
1963
- this._worker.postMessage({ positions, vertexCount }, [positions.buffer]);
2093
+ this._worker.postMessage({ command: GaussianSplattingSortWorkerCommand.POSITIONS, positions, vertexCount }, [positions.buffer]);
2094
+ // Re-sync the active interval set in case the source splat count changed.
2095
+ this._postIntervalsToWorker();
1964
2096
  }
1965
2097
  // Handle SH textures in update path - create if they don't exist
1966
2098
  if (sh && !this._shTextures) {
@@ -2038,7 +2170,9 @@ export class GaussianSplattingMeshBase extends Mesh {
2038
2170
  if (this._worker) {
2039
2171
  const positions = Float32Array.from(this._splatPositions);
2040
2172
  const vertexCount = this._vertexCount;
2041
- this._worker.postMessage({ positions, vertexCount }, [positions.buffer]);
2173
+ this._worker.postMessage({ command: GaussianSplattingSortWorkerCommand.POSITIONS, positions, vertexCount }, [positions.buffer]);
2174
+ // Re-sync the active interval set in case the source splat count changed.
2175
+ this._postIntervalsToWorker();
2042
2176
  }
2043
2177
  this._postToWorker(true);
2044
2178
  }
@@ -2075,7 +2209,10 @@ export class GaussianSplattingMeshBase extends Mesh {
2075
2209
  if (this._worker) {
2076
2210
  const positions = Float32Array.from(this._splatPositions);
2077
2211
  const vertexCount = this._vertexCount;
2078
- this._worker.postMessage({ positions, vertexCount }, [positions.buffer]);
2212
+ this._worker.postMessage({ command: GaussianSplattingSortWorkerCommand.POSITIONS, positions, vertexCount }, [positions.buffer]);
2213
+ // The source splat count may have changed (e.g. addPart): re-sync the worker's active
2214
+ // interval set so it covers the current splats instead of a stale (smaller) range.
2215
+ this._postIntervalsToWorker();
2079
2216
  }
2080
2217
  this._sortIsDirty = true;
2081
2218
  }
@@ -2100,6 +2237,11 @@ export class GaussianSplattingMeshBase extends Mesh {
2100
2237
  }
2101
2238
  const vertexCount = uBuffer.length / GaussianSplattingMeshBase._RowOutputLength;
2102
2239
  if (vertexCount != this._vertexCount) {
2240
+ // The source set changed: drop any active range filter so the buffers resize to the full
2241
+ // set. Callers (e.g. streaming LOD) re-apply their ranges after the data is committed.
2242
+ this._activeSplatRanges = null;
2243
+ this._activeSplatRangeKey = "";
2244
+ this._activeSplatRenderCount = 0;
2103
2245
  this._updateSplatIndexBuffer(vertexCount);
2104
2246
  }
2105
2247
  this._vertexCount = vertexCount;
@@ -2241,19 +2383,40 @@ export class GaussianSplattingMeshBase extends Mesh {
2241
2383
  }
2242
2384
  // in case size is different
2243
2385
  _updateSplatIndexBuffer(vertexCount) {
2244
- const paddedVertexCount = (vertexCount + 15) & ~0xf;
2245
- if (!this._splatIndex || vertexCount != this._splatIndex.length) {
2386
+ const renderedSplatCount = this._activeSplatRanges ? this._activeSplatRenderCount : vertexCount;
2387
+ const paddedVertexCount = Math.max((renderedSplatCount + 15) & ~0xf, 16);
2388
+ const previousIndex = this._splatIndex;
2389
+ if (!previousIndex || paddedVertexCount !== previousIndex.length) {
2246
2390
  this._splatIndex = new Float32Array(paddedVertexCount);
2247
- for (let i = 0; i < paddedVertexCount; i++) {
2248
- this._splatIndex[i] = i;
2249
- }
2250
2391
  // update meshes for knowns cameras
2251
2392
  this._cameraViewInfos.forEach((cameraViewInfos) => {
2252
2393
  cameraViewInfos.mesh.thinInstanceSetBuffer("splatIndex", this._splatIndex, 16, false);
2253
2394
  });
2254
2395
  }
2396
+ const splatIndex = this._splatIndex;
2397
+ // Populate the buffer with the current active source indices (identity when unfiltered) so every
2398
+ // rendered slot points at a valid splat. This is the synchronous (no async sort worker) path; the
2399
+ // worker path instead defers the index-buffer swap until a matching sort lands (see _onWorkerCreated).
2400
+ if (this._activeSplatRanges) {
2401
+ let index = 0;
2402
+ for (let rangeIndex = 0; rangeIndex < this._activeSplatRanges.length; rangeIndex += 2) {
2403
+ const start = this._activeSplatRanges[rangeIndex];
2404
+ const count = this._activeSplatRanges[rangeIndex + 1];
2405
+ for (let sourceIndex = start; sourceIndex < start + count; sourceIndex++) {
2406
+ splatIndex[index++] = sourceIndex;
2407
+ }
2408
+ }
2409
+ for (; index < paddedVertexCount; index++) {
2410
+ splatIndex[index] = 0;
2411
+ }
2412
+ }
2413
+ else {
2414
+ for (let i = 0; i < paddedVertexCount; i++) {
2415
+ splatIndex[i] = i;
2416
+ }
2417
+ }
2255
2418
  // Update depthMix
2256
- if ((!this._depthMix || vertexCount != this._depthMix.length) && !IsNative) {
2419
+ if ((!this._depthMix || paddedVertexCount !== this._depthMix.length) && !IsNative) {
2257
2420
  this._depthMix = new BigInt64Array(paddedVertexCount);
2258
2421
  }
2259
2422
  this.forcedInstanceCount = Math.max(paddedVertexCount >> 4, 1);
@@ -2349,21 +2512,28 @@ export class GaussianSplattingMeshBase extends Mesh {
2349
2512
  // If the previous worker was terminated mid-sort it would never have set _canPostToWorker
2350
2513
  // back to true, leaving the sort permanently frozen on the new worker.
2351
2514
  this._canPostToWorker = true;
2352
- this._worker = new Worker(URL.createObjectURL(new Blob(["(", GaussianSplattingMeshBase._CreateWorker.toString(), ")(self)"], {
2515
+ this._worker = new Worker(URL.createObjectURL(new Blob(["(", GaussianSplattingSortWorker.toString(), ")(self)"], {
2353
2516
  type: "application/javascript",
2354
2517
  })));
2355
2518
  const positions = Float32Array.from(this._splatPositions);
2356
- this._worker.postMessage({ positions }, [positions.buffer]);
2519
+ this._worker.postMessage({ command: GaussianSplattingSortWorkerCommand.POSITIONS, positions }, [positions.buffer]);
2520
+ // The main thread owns the active interval set: send it explicitly (covering all indices when
2521
+ // no LOD filter is active) rather than letting the worker assume the full source set.
2522
+ this._postIntervalsToWorker();
2357
2523
  this._onWorkerCreated(this._worker);
2358
2524
  this._worker.onerror = () => {
2359
2525
  // If the worker throws an unhandled error, unlock the posting gate so the next frame can retry the sort.
2360
2526
  this._canPostToWorker = true;
2361
2527
  };
2362
2528
  this._worker.onmessage = (e) => {
2363
- // Recompute vertexCountPadded in case _vertexCount has changed since the last update
2364
- const vertexCountPadded = (this._vertexCount + 15) & ~0xf;
2365
- // If the vertex count changed, we discard this result and trigger a new sort
2366
- if (e.data.depthMix.length != vertexCountPadded) {
2529
+ // Size the result against the active (rendered) splat count, which may be a subset of the
2530
+ // full source set when an interval/LOD filter is active.
2531
+ const renderedPadded = Math.max((this.renderedSplatCount + 15) & ~0xf, 16);
2532
+ // Discard the result and trigger a fresh sort if it no longer matches the current active set:
2533
+ // either the active count changed since it was posted, or it was computed for a stale range
2534
+ // version (the active ranges changed while it was in flight). Applying it would render indices
2535
+ // that belong to a different active set.
2536
+ if (e.data.depthMix.length != renderedPadded || e.data.rangeVersion !== this._activeRangeVersion) {
2367
2537
  // Only re-enable posting and trigger a re-sort if the buffer is available.
2368
2538
  // If byteLength === 0 the buffer is already in-flight for a newer sort;
2369
2539
  // that sort's onmessage will handle things when it returns.
@@ -2377,13 +2547,25 @@ export class GaussianSplattingMeshBase extends Mesh {
2377
2547
  this._depthMix = e.data.depthMix;
2378
2548
  const cameraId = e.data.cameraId;
2379
2549
  const sortRequestId = e.data.sortRequestId;
2550
+ // The index buffer swap is deferred to here so the previous fully-sorted state stays on screen
2551
+ // until this matching sort lands. Resize it now (the active count may have grown/shrunk since the
2552
+ // last applied sort) and rebind it for every known camera.
2553
+ if (!this._splatIndex || this._splatIndex.length !== renderedPadded) {
2554
+ this._splatIndex = new Float32Array(renderedPadded);
2555
+ this._cameraViewInfos.forEach((info) => {
2556
+ info.mesh.thinInstanceSetBuffer("splatIndex", this._splatIndex, 16, false);
2557
+ info.splatIndexBufferSet = true;
2558
+ });
2559
+ }
2380
2560
  const indexMix = new Uint32Array(e.data.depthMix.buffer);
2381
2561
  if (this._splatIndex) {
2382
- for (let j = 0; j < vertexCountPadded; j++) {
2562
+ for (let j = 0; j < renderedPadded; j++) {
2383
2563
  this._splatIndex[j] = indexMix[2 * j];
2384
2564
  }
2385
2565
  }
2566
+ this.forcedInstanceCount = Math.max(renderedPadded >> 4, 1);
2386
2567
  if (this._delayedTextureUpdate) {
2568
+ const vertexCountPadded = (this._vertexCount + 15) & ~0xf;
2387
2569
  const textureSize = this._getTextureSize(vertexCountPadded);
2388
2570
  this._updateSubTextures(this._delayedTextureUpdate.centers, this._delayedTextureUpdate.covA, this._delayedTextureUpdate.covB, this._delayedTextureUpdate.colors, 0, textureSize.y, this._delayedTextureUpdate.sh);
2389
2571
  this._delayedTextureUpdate = null;
@@ -2559,93 +2741,6 @@ GaussianSplattingMeshBase._DefaultViewUpdateThreshold = 1e-4;
2559
2741
  * A value of 0 (default) means display update will not happens before splat is ready.
2560
2742
  */
2561
2743
  GaussianSplattingMeshBase.ProgressiveUpdateAmount = 0;
2562
- GaussianSplattingMeshBase._CreateWorker = function (self) {
2563
- let positions;
2564
- let depthMix;
2565
- let indices;
2566
- let floatMix;
2567
- let partIndices;
2568
- let partMatrices;
2569
- self.onmessage = (e) => {
2570
- // updated on init
2571
- if (e.data.positions) {
2572
- positions = e.data.positions;
2573
- }
2574
- // update on rig node changed
2575
- else if (e.data.partMatrices) {
2576
- partMatrices = e.data.partMatrices;
2577
- }
2578
- // update on rig node indices changed
2579
- else if (e.data.partIndices !== undefined) {
2580
- partIndices = e.data.partIndices;
2581
- }
2582
- // update on view changed
2583
- else {
2584
- const cameraId = e.data.cameraId;
2585
- const sortRequestId = e.data.sortRequestId;
2586
- const globalWorldMatrix = e.data.worldMatrix;
2587
- const cameraForward = e.data.cameraForward;
2588
- const cameraPosition = e.data.cameraPosition;
2589
- depthMix = e.data.depthMix;
2590
- if (!positions || !cameraForward) {
2591
- // Sort request arrived before positions were initialized — return the buffer unchanged so the main thread can unlock _canPostToWorker.
2592
- self.postMessage({ depthMix, cameraId, sortRequestId }, [depthMix.buffer]);
2593
- return;
2594
- }
2595
- const vertexCountPadded = (positions.length / 4 + 15) & ~0xf;
2596
- indices = new Uint32Array(depthMix.buffer);
2597
- floatMix = new Float32Array(depthMix.buffer);
2598
- // Sort
2599
- for (let j = 0; j < vertexCountPadded; j++) {
2600
- indices[2 * j] = j;
2601
- }
2602
- // depth = dot(cameraForward, worldPos - cameraPos)
2603
- const camDot = cameraForward[0] * cameraPosition[0] + cameraForward[1] * cameraPosition[1] + cameraForward[2] * cameraPosition[2];
2604
- const computeDepthCoeffs = (m) => {
2605
- return [
2606
- cameraForward[0] * m[0] + cameraForward[1] * m[1] + cameraForward[2] * m[2],
2607
- cameraForward[0] * m[4] + cameraForward[1] * m[5] + cameraForward[2] * m[6],
2608
- cameraForward[0] * m[8] + cameraForward[1] * m[9] + cameraForward[2] * m[10],
2609
- cameraForward[0] * m[12] + cameraForward[1] * m[13] + cameraForward[2] * m[14] - camDot,
2610
- ];
2611
- };
2612
- try {
2613
- if (partMatrices && partIndices) {
2614
- // Precompute depth coefficients for each rig node
2615
- const depthCoeffs = partMatrices.map((m) => computeDepthCoeffs(m));
2616
- // NB: For performance reasons, we assume that part indices are valid
2617
- const length = partIndices.length;
2618
- for (let j = 0; j < vertexCountPadded; j++) {
2619
- // NB: We need this 'min' because vertex array is padded, not partIndices
2620
- const partIndex = partIndices[Math.min(j, length - 1)];
2621
- const coeff = depthCoeffs[partIndex];
2622
- floatMix[2 * j + 1] = coeff[0] * positions[4 * j + 0] + coeff[1] * positions[4 * j + 1] + coeff[2] * positions[4 * j + 2] + coeff[3];
2623
- // instead of using minus to sort back to front, we use bitwise not operator to invert the order of indices
2624
- // might not be faster but a minus sign implies a reference value that may not be enough and will decrease floatting precision
2625
- indices[2 * j + 1] = ~indices[2 * j + 1];
2626
- }
2627
- }
2628
- else {
2629
- // Compute depth coefficients from global world matrix
2630
- const [a, b, c, d] = computeDepthCoeffs(globalWorldMatrix);
2631
- for (let j = 0; j < vertexCountPadded; j++) {
2632
- floatMix[2 * j + 1] = a * positions[4 * j + 0] + b * positions[4 * j + 1] + c * positions[4 * j + 2] + d;
2633
- indices[2 * j + 1] = ~indices[2 * j + 1];
2634
- }
2635
- }
2636
- depthMix.sort();
2637
- }
2638
- catch (sortError) {
2639
- // Transient data inconsistency (e.g. partIndices/partMatrices mismatch during addPart/removePart rebuild).
2640
- // Return the buffer unsorted so the main thread can unlock _canPostToWorker and retry next frame.
2641
- // Logger is unavailable inside the worker — console is the only option.
2642
- // eslint-disable-next-line no-console
2643
- console.error("Gaussian splat sort worker encountered an error (will retry next frame):", sortError);
2644
- }
2645
- self.postMessage({ depthMix, cameraId, sortRequestId }, [depthMix.buffer]);
2646
- }
2647
- };
2648
- };
2649
2744
  /**
2650
2745
  * Allocates SH texture buffers pre-filled with 128 (the neutral encoding of ~0.0 in the
2651
2746
  * shader's decompose() function). Padding bytes beyond the actual coefficients in the last