@onerjs/core 8.46.1 → 8.46.3

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 (54) hide show
  1. package/Collisions/gpuPicker.js +9 -1
  2. package/Collisions/gpuPicker.js.map +1 -1
  3. package/Engines/WebGPU/webgpuCacheRenderPipeline.d.ts +20 -0
  4. package/Engines/WebGPU/webgpuCacheRenderPipeline.js +58 -13
  5. package/Engines/WebGPU/webgpuCacheRenderPipeline.js.map +1 -1
  6. package/Engines/WebGPU/webgpuConstants.d.ts +5 -2
  7. package/Engines/WebGPU/webgpuConstants.js +3 -0
  8. package/Engines/WebGPU/webgpuConstants.js.map +1 -1
  9. package/Engines/abstractEngine.js +2 -2
  10. package/Engines/abstractEngine.js.map +1 -1
  11. package/Engines/engine.d.ts +45 -41
  12. package/Engines/shaderStore.js +2 -2
  13. package/Engines/shaderStore.js.map +1 -1
  14. package/Engines/webgpuEngine.d.ts +84 -0
  15. package/Engines/webgpuEngine.js +80 -1
  16. package/Engines/webgpuEngine.js.map +1 -1
  17. package/Layers/thinHighlightLayer.js.map +1 -1
  18. package/Layers/thinSelectionOutlineLayer.js +20 -5
  19. package/Layers/thinSelectionOutlineLayer.js.map +1 -1
  20. package/Materials/GaussianSplatting/gaussianSplattingGpuPickingMaterialPlugin.d.ts +7 -0
  21. package/Materials/GaussianSplatting/gaussianSplattingGpuPickingMaterialPlugin.js +22 -0
  22. package/Materials/GaussianSplatting/gaussianSplattingGpuPickingMaterialPlugin.js.map +1 -1
  23. package/Materials/GaussianSplatting/gaussianSplattingMaterial.js +2 -28
  24. package/Materials/GaussianSplatting/gaussianSplattingMaterial.js.map +1 -1
  25. package/Materials/Textures/baseTexture.js +3 -0
  26. package/Materials/Textures/baseTexture.js.map +1 -1
  27. package/Materials/Textures/texture.js +1 -0
  28. package/Materials/Textures/texture.js.map +1 -1
  29. package/Meshes/GaussianSplatting/gaussianSplattingCompoundMesh.d.ts +46 -0
  30. package/Meshes/GaussianSplatting/gaussianSplattingCompoundMesh.js +56 -0
  31. package/Meshes/GaussianSplatting/gaussianSplattingCompoundMesh.js.map +1 -0
  32. package/Meshes/GaussianSplatting/gaussianSplattingMesh.d.ts +104 -463
  33. package/Meshes/GaussianSplatting/gaussianSplattingMesh.js +553 -2018
  34. package/Meshes/GaussianSplatting/gaussianSplattingMesh.js.map +1 -1
  35. package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.d.ts +554 -0
  36. package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.js +2017 -0
  37. package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.js.map +1 -0
  38. package/Meshes/index.d.ts +2 -0
  39. package/Meshes/index.js +2 -0
  40. package/Meshes/index.js.map +1 -1
  41. package/Misc/tools.js +1 -1
  42. package/Misc/tools.js.map +1 -1
  43. package/Rendering/depthRenderer.js +2 -1
  44. package/Rendering/depthRenderer.js.map +1 -1
  45. package/node.d.ts +7 -0
  46. package/node.js +17 -0
  47. package/node.js.map +1 -1
  48. package/package.json +2 -2
  49. package/Shaders/ShadersInclude/openpbrBlockAmbientOcclusion.d.ts +0 -5
  50. package/Shaders/ShadersInclude/openpbrBlockAmbientOcclusion.js +0 -35
  51. package/Shaders/ShadersInclude/openpbrBlockAmbientOcclusion.js.map +0 -1
  52. package/ShadersWGSL/ShadersInclude/openpbrBlockAmbientOcclusion.d.ts +0 -5
  53. package/ShadersWGSL/ShadersInclude/openpbrBlockAmbientOcclusion.js +0 -36
  54. package/ShadersWGSL/ShadersInclude/openpbrBlockAmbientOcclusion.js.map +0 -1
@@ -1,2155 +1,690 @@
1
- import { SubMesh } from "../subMesh.js";
2
- import { Mesh } from "../mesh.js";
3
- import { VertexData } from "../mesh.vertexData.js";
4
- import { Matrix, TmpVectors, Vector2, Vector3, Quaternion } from "../../Maths/math.vector.js";
5
- import { Logger } from "../../Misc/logger.js";
6
- import { GaussianSplattingMaterial, GetGaussianSplattingMaxPartCount } from "../../Materials/GaussianSplatting/gaussianSplattingMaterial.js";
1
+ import { Quaternion, Vector3 } from "../../Maths/math.vector.js";
2
+ import { GetGaussianSplattingMaxPartCount } from "../../Materials/GaussianSplatting/gaussianSplattingMaterial.js";
3
+ import { GaussianSplattingMeshBase } from "./gaussianSplattingMeshBase.js";
7
4
  import { RawTexture } from "../../Materials/Textures/rawTexture.js";
8
5
 
9
6
  import "../thinInstanceMesh.js";
10
- import { ToHalfFloat } from "../../Misc/textureTools.js";
11
- import { Scalar } from "../../Maths/math.scalar.js";
12
- import { runCoroutineSync, runCoroutineAsync, createYieldingScheduler } from "../../Misc/coroutine.js";
13
- import { EngineStore } from "../../Engines/engineStore.js";
14
- import { ImportMeshAsync } from "../../Loading/sceneLoader.js";
15
7
  import { GaussianSplattingPartProxyMesh } from "./gaussianSplattingPartProxyMesh.js";
16
- const IsNative = typeof _native !== "undefined";
17
- const Native = IsNative ? _native : null;
18
- // @internal
19
- const UnpackUnorm = (value, bits) => {
20
- const t = (1 << bits) - 1;
21
- return (value & t) / t;
22
- };
23
- // @internal
24
- const Unpack111011 = (value, result) => {
25
- result.x = UnpackUnorm(value >>> 21, 11);
26
- result.y = UnpackUnorm(value >>> 11, 10);
27
- result.z = UnpackUnorm(value, 11);
28
- };
29
- // @internal
30
- const Unpack8888 = (value, result) => {
31
- result[0] = UnpackUnorm(value >>> 24, 8) * 255;
32
- result[1] = UnpackUnorm(value >>> 16, 8) * 255;
33
- result[2] = UnpackUnorm(value >>> 8, 8) * 255;
34
- result[3] = UnpackUnorm(value, 8) * 255;
35
- };
36
- // @internal
37
- // unpack quaternion with 2,10,10,10 format (largest element, 3x10bit element)
38
- const UnpackRot = (value, result) => {
39
- const norm = 1.0 / (Math.sqrt(2) * 0.5);
40
- const a = (UnpackUnorm(value >>> 20, 10) - 0.5) * norm;
41
- const b = (UnpackUnorm(value >>> 10, 10) - 0.5) * norm;
42
- const c = (UnpackUnorm(value, 10) - 0.5) * norm;
43
- const m = Math.sqrt(1.0 - (a * a + b * b + c * c));
44
- switch (value >>> 30) {
45
- case 0:
46
- result.set(m, a, b, c);
47
- break;
48
- case 1:
49
- result.set(a, m, b, c);
50
- break;
51
- case 2:
52
- result.set(a, b, m, c);
53
- break;
54
- case 3:
55
- result.set(a, b, c, m);
56
- break;
57
- }
58
- };
59
- /**
60
- * Representation of the types
61
- */
62
- var PLYType;
63
- (function (PLYType) {
64
- PLYType[PLYType["FLOAT"] = 0] = "FLOAT";
65
- PLYType[PLYType["INT"] = 1] = "INT";
66
- PLYType[PLYType["UINT"] = 2] = "UINT";
67
- PLYType[PLYType["DOUBLE"] = 3] = "DOUBLE";
68
- PLYType[PLYType["UCHAR"] = 4] = "UCHAR";
69
- PLYType[PLYType["UNDEFINED"] = 5] = "UNDEFINED";
70
- })(PLYType || (PLYType = {}));
71
- /**
72
- * Usage types of the PLY values
73
- */
74
- var PLYValue;
75
- (function (PLYValue) {
76
- PLYValue[PLYValue["MIN_X"] = 0] = "MIN_X";
77
- PLYValue[PLYValue["MIN_Y"] = 1] = "MIN_Y";
78
- PLYValue[PLYValue["MIN_Z"] = 2] = "MIN_Z";
79
- PLYValue[PLYValue["MAX_X"] = 3] = "MAX_X";
80
- PLYValue[PLYValue["MAX_Y"] = 4] = "MAX_Y";
81
- PLYValue[PLYValue["MAX_Z"] = 5] = "MAX_Z";
82
- PLYValue[PLYValue["MIN_SCALE_X"] = 6] = "MIN_SCALE_X";
83
- PLYValue[PLYValue["MIN_SCALE_Y"] = 7] = "MIN_SCALE_Y";
84
- PLYValue[PLYValue["MIN_SCALE_Z"] = 8] = "MIN_SCALE_Z";
85
- PLYValue[PLYValue["MAX_SCALE_X"] = 9] = "MAX_SCALE_X";
86
- PLYValue[PLYValue["MAX_SCALE_Y"] = 10] = "MAX_SCALE_Y";
87
- PLYValue[PLYValue["MAX_SCALE_Z"] = 11] = "MAX_SCALE_Z";
88
- PLYValue[PLYValue["PACKED_POSITION"] = 12] = "PACKED_POSITION";
89
- PLYValue[PLYValue["PACKED_ROTATION"] = 13] = "PACKED_ROTATION";
90
- PLYValue[PLYValue["PACKED_SCALE"] = 14] = "PACKED_SCALE";
91
- PLYValue[PLYValue["PACKED_COLOR"] = 15] = "PACKED_COLOR";
92
- PLYValue[PLYValue["X"] = 16] = "X";
93
- PLYValue[PLYValue["Y"] = 17] = "Y";
94
- PLYValue[PLYValue["Z"] = 18] = "Z";
95
- PLYValue[PLYValue["SCALE_0"] = 19] = "SCALE_0";
96
- PLYValue[PLYValue["SCALE_1"] = 20] = "SCALE_1";
97
- PLYValue[PLYValue["SCALE_2"] = 21] = "SCALE_2";
98
- PLYValue[PLYValue["DIFFUSE_RED"] = 22] = "DIFFUSE_RED";
99
- PLYValue[PLYValue["DIFFUSE_GREEN"] = 23] = "DIFFUSE_GREEN";
100
- PLYValue[PLYValue["DIFFUSE_BLUE"] = 24] = "DIFFUSE_BLUE";
101
- PLYValue[PLYValue["OPACITY"] = 25] = "OPACITY";
102
- PLYValue[PLYValue["F_DC_0"] = 26] = "F_DC_0";
103
- PLYValue[PLYValue["F_DC_1"] = 27] = "F_DC_1";
104
- PLYValue[PLYValue["F_DC_2"] = 28] = "F_DC_2";
105
- PLYValue[PLYValue["F_DC_3"] = 29] = "F_DC_3";
106
- PLYValue[PLYValue["ROT_0"] = 30] = "ROT_0";
107
- PLYValue[PLYValue["ROT_1"] = 31] = "ROT_1";
108
- PLYValue[PLYValue["ROT_2"] = 32] = "ROT_2";
109
- PLYValue[PLYValue["ROT_3"] = 33] = "ROT_3";
110
- PLYValue[PLYValue["MIN_COLOR_R"] = 34] = "MIN_COLOR_R";
111
- PLYValue[PLYValue["MIN_COLOR_G"] = 35] = "MIN_COLOR_G";
112
- PLYValue[PLYValue["MIN_COLOR_B"] = 36] = "MIN_COLOR_B";
113
- PLYValue[PLYValue["MAX_COLOR_R"] = 37] = "MAX_COLOR_R";
114
- PLYValue[PLYValue["MAX_COLOR_G"] = 38] = "MAX_COLOR_G";
115
- PLYValue[PLYValue["MAX_COLOR_B"] = 39] = "MAX_COLOR_B";
116
- PLYValue[PLYValue["SH_0"] = 40] = "SH_0";
117
- PLYValue[PLYValue["SH_1"] = 41] = "SH_1";
118
- PLYValue[PLYValue["SH_2"] = 42] = "SH_2";
119
- PLYValue[PLYValue["SH_3"] = 43] = "SH_3";
120
- PLYValue[PLYValue["SH_4"] = 44] = "SH_4";
121
- PLYValue[PLYValue["SH_5"] = 45] = "SH_5";
122
- PLYValue[PLYValue["SH_6"] = 46] = "SH_6";
123
- PLYValue[PLYValue["SH_7"] = 47] = "SH_7";
124
- PLYValue[PLYValue["SH_8"] = 48] = "SH_8";
125
- PLYValue[PLYValue["SH_9"] = 49] = "SH_9";
126
- PLYValue[PLYValue["SH_10"] = 50] = "SH_10";
127
- PLYValue[PLYValue["SH_11"] = 51] = "SH_11";
128
- PLYValue[PLYValue["SH_12"] = 52] = "SH_12";
129
- PLYValue[PLYValue["SH_13"] = 53] = "SH_13";
130
- PLYValue[PLYValue["SH_14"] = 54] = "SH_14";
131
- PLYValue[PLYValue["SH_15"] = 55] = "SH_15";
132
- PLYValue[PLYValue["SH_16"] = 56] = "SH_16";
133
- PLYValue[PLYValue["SH_17"] = 57] = "SH_17";
134
- PLYValue[PLYValue["SH_18"] = 58] = "SH_18";
135
- PLYValue[PLYValue["SH_19"] = 59] = "SH_19";
136
- PLYValue[PLYValue["SH_20"] = 60] = "SH_20";
137
- PLYValue[PLYValue["SH_21"] = 61] = "SH_21";
138
- PLYValue[PLYValue["SH_22"] = 62] = "SH_22";
139
- PLYValue[PLYValue["SH_23"] = 63] = "SH_23";
140
- PLYValue[PLYValue["SH_24"] = 64] = "SH_24";
141
- PLYValue[PLYValue["SH_25"] = 65] = "SH_25";
142
- PLYValue[PLYValue["SH_26"] = 66] = "SH_26";
143
- PLYValue[PLYValue["SH_27"] = 67] = "SH_27";
144
- PLYValue[PLYValue["SH_28"] = 68] = "SH_28";
145
- PLYValue[PLYValue["SH_29"] = 69] = "SH_29";
146
- PLYValue[PLYValue["SH_30"] = 70] = "SH_30";
147
- PLYValue[PLYValue["SH_31"] = 71] = "SH_31";
148
- PLYValue[PLYValue["SH_32"] = 72] = "SH_32";
149
- PLYValue[PLYValue["SH_33"] = 73] = "SH_33";
150
- PLYValue[PLYValue["SH_34"] = 74] = "SH_34";
151
- PLYValue[PLYValue["SH_35"] = 75] = "SH_35";
152
- PLYValue[PLYValue["SH_36"] = 76] = "SH_36";
153
- PLYValue[PLYValue["SH_37"] = 77] = "SH_37";
154
- PLYValue[PLYValue["SH_38"] = 78] = "SH_38";
155
- PLYValue[PLYValue["SH_39"] = 79] = "SH_39";
156
- PLYValue[PLYValue["SH_40"] = 80] = "SH_40";
157
- PLYValue[PLYValue["SH_41"] = 81] = "SH_41";
158
- PLYValue[PLYValue["SH_42"] = 82] = "SH_42";
159
- PLYValue[PLYValue["SH_43"] = 83] = "SH_43";
160
- PLYValue[PLYValue["SH_44"] = 84] = "SH_44";
161
- PLYValue[PLYValue["UNDEFINED"] = 85] = "UNDEFINED";
162
- })(PLYValue || (PLYValue = {}));
163
8
  /**
164
- * Class used to render a gaussian splatting mesh
9
+ * Class used to render a Gaussian Splatting mesh. Supports both single-cloud and compound
10
+ * (multi-part) rendering. In compound mode, multiple Gaussian Splatting source meshes are
11
+ * merged into one draw call while retaining per-part world-matrix control via
12
+ * addPart/addParts and removePart.
165
13
  */
166
- export class GaussianSplattingMesh extends Mesh {
14
+ export class GaussianSplattingMesh extends GaussianSplattingMeshBase {
167
15
  /**
168
- * If true, disables depth sorting of the splats (default: false)
16
+ * Creates a new GaussianSplattingMesh
17
+ * @param name the name of the mesh
18
+ * @param url optional URL to load a Gaussian Splatting file from
19
+ * @param scene the hosting scene
20
+ * @param keepInRam whether to keep the raw splat data in RAM after uploading to GPU
169
21
  */
170
- get disableDepthSort() {
171
- return this._disableDepthSort;
172
- }
173
- set disableDepthSort(value) {
174
- if (!this._disableDepthSort && value) {
175
- this._worker?.terminate();
176
- this._worker = null;
177
- this._disableDepthSort = true;
178
- }
179
- else if (this._disableDepthSort && !value) {
180
- this._disableDepthSort = false;
181
- this._sortIsDirty = true;
182
- this._instanciateWorker();
183
- }
22
+ constructor(name, url = null, scene = null, keepInRam = false) {
23
+ super(name, url, scene, keepInRam);
24
+ /**
25
+ * Proxy meshes indexed by part index. Maintained in sync with _partMatrices.
26
+ */
27
+ this._partProxies = [];
28
+ /**
29
+ * World matrices for each part, indexed by part index.
30
+ */
31
+ this._partMatrices = [];
32
+ /** When true, suppresses the sort trigger inside setWorldMatrixForPart during batch rebuilds. */
33
+ this._rebuilding = false;
34
+ /**
35
+ * Visibility values for each part (0.0 to 1.0), indexed by part index.
36
+ */
37
+ this._partVisibility = [];
38
+ this._partIndicesTexture = null;
39
+ this._partIndices = null;
40
+ // Ensure _splatsData is retained once compound mode is entered — addPart/addParts need
41
+ // the source data for full-texture rebuilds. Set after super() so it is visible to
42
+ // _updateData when the async load completes.
43
+ this._alwaysRetainSplatsData = true;
184
44
  }
185
45
  /**
186
- * View direction factor used to compute the SH view direction in the shader.
187
- * @deprecated Not used anymore for SH rendering
46
+ * Returns the class name
47
+ * @returns "GaussianSplattingMesh"
188
48
  */
189
- get viewDirectionFactor() {
190
- return Vector3.OneReadOnly;
49
+ getClassName() {
50
+ return "GaussianSplattingMesh";
191
51
  }
192
52
  /**
193
- * SH degree. 0 = no sh (default). 1 = 3 parameters. 2 = 8 parameters. 3 = 15 parameters.
194
- * Value is clamped between 0 and the maximum degree available from loaded data.
53
+ * Disposes proxy meshes and clears part data in addition to the base class GPU resources.
54
+ * @param doNotRecurse Set to true to not recurse into each children
195
55
  */
196
- get shDegree() {
197
- return this._shDegree;
198
- }
199
- set shDegree(value) {
200
- const maxDegree = this._shTextures?.length ?? 0;
201
- const clamped = Math.max(0, Math.min(Math.round(value), maxDegree));
202
- if (this._shDegree === clamped) {
203
- return;
56
+ dispose(doNotRecurse) {
57
+ for (const proxy of this._partProxies) {
58
+ proxy.dispose();
204
59
  }
205
- this._shDegree = clamped;
206
- this.material?.resetDrawCache();
207
- }
208
- /**
209
- * Maximum SH degree available from the loaded data.
210
- */
211
- get maxShDegree() {
212
- return this._shTextures?.length ?? 0;
213
- }
214
- /**
215
- * Number of splats in the mesh
216
- */
217
- get splatCount() {
218
- return this._splatIndex?.length;
60
+ if (this._partIndicesTexture) {
61
+ this._partIndicesTexture.dispose();
62
+ }
63
+ this._partProxies = [];
64
+ this._partMatrices = [];
65
+ this._partVisibility = [];
66
+ this._partIndicesTexture = null;
67
+ super.dispose(doNotRecurse);
219
68
  }
69
+ // ---------------------------------------------------------------------------
70
+ // Worker and material hooks
71
+ // ---------------------------------------------------------------------------
220
72
  /**
221
- * returns the splats data array buffer that contains in order : postions (3 floats), size (3 floats), color (4 bytes), orientation quaternion (4 bytes)
73
+ * Posts the initial per-part data to the sort worker after it has been created.
74
+ * Sends the current part matrices and group index array so the worker can correctly
75
+ * weight depth values per part.
76
+ * @param worker the newly created sort worker
222
77
  */
223
- get splatsData() {
224
- return this._splatsData;
78
+ _onWorkerCreated(worker) {
79
+ worker.postMessage({ partMatrices: this._partMatrices.map((matrix) => new Float32Array(matrix.m)) });
80
+ worker.postMessage({ partIndices: this._partIndices ? new Uint8Array(this._partIndices) : null });
225
81
  }
226
82
  /**
227
- * returns the SH data arrays
83
+ * Stores the raw part index array, padded to texture length, so the worker and GPU texture
84
+ * creation step have access to it.
85
+ * @param partIndices - the raw part indices array received during a data load
86
+ * @param textureLength - the padded texture length to allocate into
228
87
  */
229
- get shData() {
230
- return this._shData;
88
+ _onIndexDataReceived(partIndices, textureLength) {
89
+ this._partIndices = new Uint8Array(textureLength);
90
+ this._partIndices.set(partIndices);
231
91
  }
232
92
  /**
233
- * True when this mesh is a compound that regroups multiple Gaussian splatting parts.
93
+ * Returns `true` when at least one part has been added to this compound mesh.
94
+ * Returns `false` before any parts are added, so the mesh renders in normal
95
+ * (non-compound) mode until the first addPart/addParts call. This matches the
96
+ * old base-class behavior of `this._partMatrices.length > 0` and avoids
97
+ * binding unset partWorld uniforms (which would cause division-by-zero in the
98
+ * Gaussian projection Jacobian and produce huge distorted splats).
99
+ * @internal
234
100
  */
235
101
  get isCompound() {
236
102
  return this._partMatrices.length > 0;
237
103
  }
238
104
  /**
239
- * returns the part indices array
105
+ * During a removePart rebuild, keep the existing sort worker alive rather than
106
+ * tearing it down and spinning up a new one. This avoids startup latency and the
107
+ * transient state window where a stale sort could fire against an incomplete
108
+ * partMatrices array.
109
+ * Outside of a rebuild the base-class behaviour is used unchanged.
240
110
  */
241
- get partIndices() {
242
- return this._partIndices;
243
- }
244
- /**
245
- * Gets the part indices texture, if the mesh is a compound
246
- */
247
- get partIndicesTexture() {
248
- return this._partIndicesTexture;
249
- }
250
- /**
251
- * Gets the part visibility array, if the mesh is a compound
252
- */
253
- get partVisibility() {
254
- return this._partVisibility;
255
- }
256
- /**
257
- * Gets the covariancesA texture
258
- */
259
- get covariancesATexture() {
260
- return this._covariancesATexture;
261
- }
262
- /**
263
- * Gets the covariancesB texture
264
- */
265
- get covariancesBTexture() {
266
- return this._covariancesBTexture;
267
- }
268
- /**
269
- * Gets the centers texture
270
- */
271
- get centersTexture() {
272
- return this._centersTexture;
273
- }
274
- /**
275
- * Gets the colors texture
276
- */
277
- get colorsTexture() {
278
- return this._colorsTexture;
279
- }
280
- /**
281
- * Gets the SH textures
282
- */
283
- get shTextures() {
284
- return this._shTextures;
285
- }
286
- /**
287
- * Gets the kernel size
288
- * Documentation and mathematical explanations here:
289
- * https://github.com/graphdeco-inria/gaussian-splatting/issues/294#issuecomment-1772688093
290
- * https://github.com/autonomousvision/mip-splatting/issues/18#issuecomment-1929388931
291
- */
292
- get kernelSize() {
293
- return this._material instanceof GaussianSplattingMaterial ? this._material.kernelSize : 0;
294
- }
295
- /**
296
- * Get the compensation state
297
- */
298
- get compensation() {
299
- return this._material instanceof GaussianSplattingMaterial ? this._material.compensation : false;
300
- }
301
- /**
302
- * set rendering material
303
- */
304
- set material(value) {
305
- this._material = value;
306
- this._material.backFaceCulling = false;
307
- this._material.cullBackFaces = false;
308
- value.resetDrawCache();
309
- }
310
- /**
311
- * get rendering material
312
- */
313
- get material() {
314
- return this._material;
315
- }
316
- static _MakeSplatGeometryForMesh(mesh) {
317
- const vertexData = new VertexData();
318
- const originPositions = [-2, -2, 0, 2, -2, 0, 2, 2, 0, -2, 2, 0];
319
- const originIndices = [0, 1, 2, 0, 2, 3];
320
- const positions = [];
321
- const indices = [];
322
- for (let i = 0; i < GaussianSplattingMesh._BatchSize; i++) {
323
- for (let j = 0; j < 12; j++) {
324
- if (j == 2 || j == 5 || j == 8 || j == 11) {
325
- positions.push(i); // local splat index
326
- }
327
- else {
328
- positions.push(originPositions[j]);
329
- }
330
- }
331
- indices.push(originIndices.map((v) => v + i * 4));
332
- }
333
- vertexData.positions = positions;
334
- vertexData.indices = indices.flat();
335
- vertexData.applyToMesh(mesh);
336
- }
337
- /**
338
- * Creates a new gaussian splatting mesh
339
- * @param name defines the name of the mesh
340
- * @param url defines the url to load from (optional)
341
- * @param scene defines the hosting scene (optional)
342
- * @param keepInRam keep datas in ram for editing purpose
343
- */
344
- constructor(name, url = null, scene = null, keepInRam = false) {
345
- super(name, scene);
346
- this._vertexCount = 0;
347
- this._worker = null;
348
- this._modelViewProjectionMatrix = Matrix.Identity();
349
- this._canPostToWorker = true;
350
- this._readyToDisplay = false;
351
- this._covariancesATexture = null;
352
- this._covariancesBTexture = null;
353
- this._centersTexture = null;
354
- this._colorsTexture = null;
355
- this._splatPositions = null;
356
- this._splatIndex = null;
357
- this._shTextures = null;
358
- this._splatsData = null;
359
- this._shData = null;
360
- this._partIndicesTexture = null;
361
- this._partIndices = null;
362
- this._partMatrices = [];
363
- this._partVisibility = [];
364
- this._partProxies = new Map();
365
- this._textureSize = new Vector2(0, 0);
366
- this._keepInRam = false;
367
- this._delayedTextureUpdate = null;
368
- this._useRGBACovariants = false;
369
- this._material = null;
370
- this._tmpCovariances = [0, 0, 0, 0, 0, 0];
371
- this._sortIsDirty = false;
372
- this._shDegree = 0;
373
- this._cameraViewInfos = new Map();
374
- /**
375
- * Cosine value of the angle threshold to update view dependent splat sorting. Default is 0.0001.
376
- */
377
- this.viewUpdateThreshold = GaussianSplattingMesh._DefaultViewUpdateThreshold;
378
- this._disableDepthSort = false;
379
- this._loadingPromise = null;
380
- this.subMeshes = [];
381
- new SubMesh(0, 0, 4 * GaussianSplattingMesh._BatchSize, 0, 6 * GaussianSplattingMesh._BatchSize, this);
382
- this.setEnabled(false);
383
- // webGL2 and webGPU support for RG texture with float16 is fine. not webGL1
384
- this._useRGBACovariants = !this.getEngine().isWebGPU && this.getEngine().version === 1.0;
385
- this._keepInRam = keepInRam;
386
- if (url) {
387
- this._loadingPromise = this.loadFileAsync(url);
111
+ _instantiateWorker() {
112
+ if (this._rebuilding && this._worker) {
113
+ // Worker already exists and is kept alive; just resize the splat-index buffer.
114
+ this._updateSplatIndexBuffer(this._vertexCount);
115
+ return;
388
116
  }
389
- const gaussianSplattingMaterial = new GaussianSplattingMaterial(this.name + "_material", this._scene);
390
- gaussianSplattingMaterial.setSourceMesh(this);
391
- this._material = gaussianSplattingMaterial;
392
- // delete meshes created for cameras on camera removal
393
- this._scene.onCameraRemovedObservable.add((camera) => {
394
- const cameraId = camera.uniqueId;
395
- // delete mesh for this camera
396
- if (this._cameraViewInfos.has(cameraId)) {
397
- const cameraViewInfos = this._cameraViewInfos.get(cameraId);
398
- cameraViewInfos?.mesh.dispose();
399
- this._cameraViewInfos.delete(cameraId);
400
- }
401
- });
117
+ super._instantiateWorker();
402
118
  }
403
119
  /**
404
- * Get the loading promise when loading the mesh from a URL in the constructor
405
- * @returns constructor loading promise or null if no URL was provided
120
+ * Ensures the part-index GPU texture exists at the start of an incremental update.
121
+ * Called before the sub-texture upload so the correct texture is available for the first batch.
122
+ * @param textureSize - current texture dimensions
406
123
  */
407
- getLoadingPromise() {
408
- return this._loadingPromise;
124
+ _onIncrementalUpdateStart(textureSize) {
125
+ this._ensurePartIndicesTexture(textureSize, this._partIndices ?? undefined);
409
126
  }
410
127
  /**
411
- * Returns the class name
412
- * @returns "GaussianSplattingMesh"
128
+ * Posts positions (via super) and then additionally posts the current part-index array
129
+ * to the sort worker so it can associate each splat with its part.
413
130
  */
414
- getClassName() {
415
- return "GaussianSplattingMesh";
416
- }
417
- /**
418
- * Returns the total number of vertices (splats) within the mesh
419
- * @returns the total number of vertices
420
- */
421
- getTotalVertices() {
422
- return this._vertexCount;
423
- }
424
- /**
425
- * Is this node ready to be used/rendered
426
- * @param completeCheck defines if a complete check (including materials and lights) has to be done (false by default)
427
- * @returns true when ready
428
- */
429
- isReady(completeCheck = false) {
430
- if (!super.isReady(completeCheck, true)) {
431
- return false;
432
- }
433
- if (!this._readyToDisplay) {
434
- // mesh is ready when worker has done at least 1 sorting
435
- this._postToWorker(true);
436
- return false;
437
- }
438
- return true;
439
- }
440
- _getCameraDirection(camera) {
441
- const cameraViewMatrix = camera.getViewMatrix();
442
- const cameraProjectionMatrix = camera.getProjectionMatrix();
443
- const cameraViewProjectionMatrix = TmpVectors.Matrix[0];
444
- cameraViewMatrix.multiplyToRef(cameraProjectionMatrix, cameraViewProjectionMatrix);
445
- const modelMatrix = this.getWorldMatrix();
446
- const modelViewMatrix = TmpVectors.Matrix[1];
447
- modelMatrix.multiplyToRef(cameraViewMatrix, modelViewMatrix);
448
- modelMatrix.multiplyToRef(cameraViewProjectionMatrix, this._modelViewProjectionMatrix);
449
- // return vector used to compute distance to camera
450
- const localDirection = TmpVectors.Vector3[1];
451
- localDirection.set(modelViewMatrix.m[2], modelViewMatrix.m[6], modelViewMatrix.m[10]);
452
- localDirection.normalize();
453
- return localDirection;
454
- }
455
- /** @internal */
456
- _postToWorker(forced = false) {
457
- const scene = this._scene;
458
- const frameId = scene.getFrameId();
459
- // force update or at least frame update for camera is outdated
460
- let outdated = false;
461
- this._cameraViewInfos.forEach((cameraViewInfos) => {
462
- if (cameraViewInfos.frameIdLastUpdate !== frameId) {
463
- outdated = true;
464
- }
465
- });
466
- // array of cameras used for rendering
467
- const cameras = this._scene.activeCameras?.length ? this._scene.activeCameras : [this._scene.activeCamera];
468
- // list view infos for active cameras
469
- const activeViewInfos = [];
470
- cameras.forEach((camera) => {
471
- if (!camera) {
472
- return;
473
- }
474
- const cameraId = camera.uniqueId;
475
- const cameraViewInfos = this._cameraViewInfos.get(cameraId);
476
- if (cameraViewInfos) {
477
- activeViewInfos.push(cameraViewInfos);
478
- }
479
- else {
480
- // mesh doesn't exist yet for this camera
481
- const cameraMesh = new Mesh(this.name + "_cameraMesh_" + cameraId, this._scene);
482
- // not visible with inspector or the scene graph
483
- cameraMesh.reservedDataStore = { hidden: true };
484
- cameraMesh.setEnabled(false);
485
- cameraMesh.material = this.material;
486
- if (cameraMesh.material && cameraMesh.material instanceof GaussianSplattingMaterial) {
487
- const gsMaterial = cameraMesh.material;
488
- // GaussianSplattingMaterial source mesh may not have been set yet.
489
- // This happens for cloned resources from asset containers for instance,
490
- // where material is cloned before mesh.
491
- if (!gsMaterial.getSourceMesh()) {
492
- gsMaterial.setSourceMesh(this);
493
- }
494
- }
495
- GaussianSplattingMesh._MakeSplatGeometryForMesh(cameraMesh);
496
- const newViewInfos = {
497
- camera: camera,
498
- cameraDirection: new Vector3(0, 0, 0),
499
- mesh: cameraMesh,
500
- frameIdLastUpdate: frameId,
501
- splatIndexBufferSet: false,
502
- };
503
- activeViewInfos.push(newViewInfos);
504
- this._cameraViewInfos.set(cameraId, newViewInfos);
505
- }
506
- });
507
- // sort view infos by last updated frame id: first item is the least recently updated
508
- activeViewInfos.sort((a, b) => a.frameIdLastUpdate - b.frameIdLastUpdate);
509
- const hasSortFunction = this._worker || Native?.sortSplats || this._disableDepthSort;
510
- if ((forced || outdated) && hasSortFunction && (this._scene.activeCameras?.length || this._scene.activeCamera) && this._canPostToWorker) {
511
- // view infos sorted by least recent updated frame id
512
- activeViewInfos.forEach((cameraViewInfos) => {
513
- const camera = cameraViewInfos.camera;
514
- const cameraDirection = this._getCameraDirection(camera);
515
- const previousCameraDirection = cameraViewInfos.cameraDirection;
516
- const dot = Vector3.Dot(cameraDirection, previousCameraDirection);
517
- if ((forced || Math.abs(dot - 1) >= this.viewUpdateThreshold) && this._canPostToWorker) {
518
- cameraViewInfos.cameraDirection.copyFrom(cameraDirection);
519
- cameraViewInfos.frameIdLastUpdate = frameId;
520
- this._canPostToWorker = false;
521
- if (this._worker) {
522
- const cameraViewMatrix = camera.getViewMatrix();
523
- this._worker.postMessage({
524
- worldMatrix: this.getWorldMatrix().m,
525
- cameraForward: [cameraViewMatrix.m[2], cameraViewMatrix.m[6], cameraViewMatrix.m[10]],
526
- cameraPosition: [camera.globalPosition.x, camera.globalPosition.y, camera.globalPosition.z],
527
- depthMix: this._depthMix,
528
- cameraId: camera.uniqueId,
529
- }, [this._depthMix.buffer]);
530
- }
531
- else if (Native?.sortSplats) {
532
- Native.sortSplats(this._modelViewProjectionMatrix, this._splatPositions, this._splatIndex, this._scene.useRightHandedSystem);
533
- if (cameraViewInfos.splatIndexBufferSet) {
534
- cameraViewInfos.mesh.thinInstanceBufferUpdated("splatIndex");
535
- }
536
- else {
537
- cameraViewInfos.mesh.thinInstanceSetBuffer("splatIndex", this._splatIndex, 16, false);
538
- cameraViewInfos.splatIndexBufferSet = true;
539
- }
540
- this._canPostToWorker = true;
541
- this._readyToDisplay = true;
542
- }
543
- }
544
- });
545
- }
546
- else if (this._disableDepthSort) {
547
- activeViewInfos.forEach((cameraViewInfos) => {
548
- if (!cameraViewInfos.splatIndexBufferSet) {
549
- cameraViewInfos.mesh.thinInstanceSetBuffer("splatIndex", this._splatIndex, 16, false);
550
- cameraViewInfos.splatIndexBufferSet = true;
551
- }
552
- });
553
- this._canPostToWorker = true;
554
- this._readyToDisplay = true;
131
+ _notifyWorkerNewData() {
132
+ super._notifyWorkerNewData();
133
+ if (this._worker) {
134
+ this._worker.postMessage({ partIndices: this._partIndices ?? null });
555
135
  }
556
136
  }
557
137
  /**
558
- * Triggers the draw call for the mesh. Usually, you don't need to call this method by your own because the mesh rendering is handled by the scene rendering manager
559
- * @param subMesh defines the subMesh to render
560
- * @param enableAlphaMode defines if alpha mode can be changed
561
- * @param effectiveMeshReplacement defines an optional mesh used to provide info for the rendering
562
- * @returns the current mesh
138
+ * Binds all compound-specific shader uniforms: the group index texture, per-part world
139
+ * matrices, and per-part visibility values.
140
+ * @param effect the shader effect that is being bound
141
+ * @internal
563
142
  */
564
- render(subMesh, enableAlphaMode, effectiveMeshReplacement) {
565
- this._postToWorker();
566
- // geometry used for shadows, bind the first found in the camera view infos
567
- if (!this._geometry && this._cameraViewInfos.size) {
568
- this._geometry = this._cameraViewInfos.values().next().value.mesh.geometry;
569
- }
570
- const cameraId = this._scene.activeCamera.uniqueId;
571
- const cameraViewInfos = this._cameraViewInfos.get(cameraId);
572
- if (!cameraViewInfos || !cameraViewInfos.splatIndexBufferSet) {
573
- return this;
574
- }
575
- if (this.onBeforeRenderObservable) {
576
- this.onBeforeRenderObservable.notifyObservers(this);
577
- }
578
- const mesh = cameraViewInfos.mesh;
579
- mesh.getWorldMatrix().copyFrom(this.getWorldMatrix());
580
- // Propagate render pass material overrides (e.g., GPU picking) to the inner camera mesh.
581
- // When this mesh is rendered into a RenderTargetTexture with a material override (via setMaterialForRendering),
582
- // the override is set on this proxy mesh but needs to be applied to the actual camera mesh that does the rendering.
583
- const engine = this._scene.getEngine();
584
- const renderPassId = engine.currentRenderPassId;
585
- const renderPassMaterial = this.getMaterialForRenderPass(renderPassId);
586
- if (renderPassMaterial) {
587
- mesh.setMaterialForRenderPass(renderPassId, renderPassMaterial);
588
- }
589
- const ret = mesh.render(subMesh, enableAlphaMode, effectiveMeshReplacement);
590
- // Clean up the temporary override to avoid affecting other render passes
591
- if (renderPassMaterial) {
592
- mesh.setMaterialForRenderPass(renderPassId, undefined);
593
- }
594
- if (this.onAfterRenderObservable) {
595
- this.onAfterRenderObservable.notifyObservers(this);
143
+ bindExtraEffectUniforms(effect) {
144
+ if (!this._partIndicesTexture) {
145
+ return;
596
146
  }
597
- return ret;
598
- }
599
- static _TypeNameToEnum(name) {
600
- switch (name) {
601
- case "float":
602
- return 0 /* PLYType.FLOAT */;
603
- case "int":
604
- return 1 /* PLYType.INT */;
605
- case "uint":
606
- return 2 /* PLYType.UINT */;
607
- case "double":
608
- return 3 /* PLYType.DOUBLE */;
609
- case "uchar":
610
- return 4 /* PLYType.UCHAR */;
147
+ effect.setTexture("partIndicesTexture", this._partIndicesTexture);
148
+ const partWorldData = new Float32Array(this.partCount * 16);
149
+ for (let i = 0; i < this.partCount; i++) {
150
+ this._partMatrices[i].toArray(partWorldData, i * 16);
611
151
  }
612
- return 5 /* PLYType.UNDEFINED */;
613
- }
614
- static _ValueNameToEnum(name) {
615
- switch (name) {
616
- case "min_x":
617
- return 0 /* PLYValue.MIN_X */;
618
- case "min_y":
619
- return 1 /* PLYValue.MIN_Y */;
620
- case "min_z":
621
- return 2 /* PLYValue.MIN_Z */;
622
- case "max_x":
623
- return 3 /* PLYValue.MAX_X */;
624
- case "max_y":
625
- return 4 /* PLYValue.MAX_Y */;
626
- case "max_z":
627
- return 5 /* PLYValue.MAX_Z */;
628
- case "min_scale_x":
629
- return 6 /* PLYValue.MIN_SCALE_X */;
630
- case "min_scale_y":
631
- return 7 /* PLYValue.MIN_SCALE_Y */;
632
- case "min_scale_z":
633
- return 8 /* PLYValue.MIN_SCALE_Z */;
634
- case "max_scale_x":
635
- return 9 /* PLYValue.MAX_SCALE_X */;
636
- case "max_scale_y":
637
- return 10 /* PLYValue.MAX_SCALE_Y */;
638
- case "max_scale_z":
639
- return 11 /* PLYValue.MAX_SCALE_Z */;
640
- case "packed_position":
641
- return 12 /* PLYValue.PACKED_POSITION */;
642
- case "packed_rotation":
643
- return 13 /* PLYValue.PACKED_ROTATION */;
644
- case "packed_scale":
645
- return 14 /* PLYValue.PACKED_SCALE */;
646
- case "packed_color":
647
- return 15 /* PLYValue.PACKED_COLOR */;
648
- case "x":
649
- return 16 /* PLYValue.X */;
650
- case "y":
651
- return 17 /* PLYValue.Y */;
652
- case "z":
653
- return 18 /* PLYValue.Z */;
654
- case "scale_0":
655
- return 19 /* PLYValue.SCALE_0 */;
656
- case "scale_1":
657
- return 20 /* PLYValue.SCALE_1 */;
658
- case "scale_2":
659
- return 21 /* PLYValue.SCALE_2 */;
660
- case "diffuse_red":
661
- case "red":
662
- return 22 /* PLYValue.DIFFUSE_RED */;
663
- case "diffuse_green":
664
- case "green":
665
- return 23 /* PLYValue.DIFFUSE_GREEN */;
666
- case "diffuse_blue":
667
- case "blue":
668
- return 24 /* PLYValue.DIFFUSE_BLUE */;
669
- case "f_dc_0":
670
- return 26 /* PLYValue.F_DC_0 */;
671
- case "f_dc_1":
672
- return 27 /* PLYValue.F_DC_1 */;
673
- case "f_dc_2":
674
- return 28 /* PLYValue.F_DC_2 */;
675
- case "f_dc_3":
676
- return 29 /* PLYValue.F_DC_3 */;
677
- case "opacity":
678
- return 25 /* PLYValue.OPACITY */;
679
- case "rot_0":
680
- return 30 /* PLYValue.ROT_0 */;
681
- case "rot_1":
682
- return 31 /* PLYValue.ROT_1 */;
683
- case "rot_2":
684
- return 32 /* PLYValue.ROT_2 */;
685
- case "rot_3":
686
- return 33 /* PLYValue.ROT_3 */;
687
- case "min_r":
688
- return 34 /* PLYValue.MIN_COLOR_R */;
689
- case "min_g":
690
- return 35 /* PLYValue.MIN_COLOR_G */;
691
- case "min_b":
692
- return 36 /* PLYValue.MIN_COLOR_B */;
693
- case "max_r":
694
- return 37 /* PLYValue.MAX_COLOR_R */;
695
- case "max_g":
696
- return 38 /* PLYValue.MAX_COLOR_G */;
697
- case "max_b":
698
- return 39 /* PLYValue.MAX_COLOR_B */;
699
- case "f_rest_0":
700
- return 40 /* PLYValue.SH_0 */;
701
- case "f_rest_1":
702
- return 41 /* PLYValue.SH_1 */;
703
- case "f_rest_2":
704
- return 42 /* PLYValue.SH_2 */;
705
- case "f_rest_3":
706
- return 43 /* PLYValue.SH_3 */;
707
- case "f_rest_4":
708
- return 44 /* PLYValue.SH_4 */;
709
- case "f_rest_5":
710
- return 45 /* PLYValue.SH_5 */;
711
- case "f_rest_6":
712
- return 46 /* PLYValue.SH_6 */;
713
- case "f_rest_7":
714
- return 47 /* PLYValue.SH_7 */;
715
- case "f_rest_8":
716
- return 48 /* PLYValue.SH_8 */;
717
- case "f_rest_9":
718
- return 49 /* PLYValue.SH_9 */;
719
- case "f_rest_10":
720
- return 50 /* PLYValue.SH_10 */;
721
- case "f_rest_11":
722
- return 51 /* PLYValue.SH_11 */;
723
- case "f_rest_12":
724
- return 52 /* PLYValue.SH_12 */;
725
- case "f_rest_13":
726
- return 53 /* PLYValue.SH_13 */;
727
- case "f_rest_14":
728
- return 54 /* PLYValue.SH_14 */;
729
- case "f_rest_15":
730
- return 55 /* PLYValue.SH_15 */;
731
- case "f_rest_16":
732
- return 56 /* PLYValue.SH_16 */;
733
- case "f_rest_17":
734
- return 57 /* PLYValue.SH_17 */;
735
- case "f_rest_18":
736
- return 58 /* PLYValue.SH_18 */;
737
- case "f_rest_19":
738
- return 59 /* PLYValue.SH_19 */;
739
- case "f_rest_20":
740
- return 60 /* PLYValue.SH_20 */;
741
- case "f_rest_21":
742
- return 61 /* PLYValue.SH_21 */;
743
- case "f_rest_22":
744
- return 62 /* PLYValue.SH_22 */;
745
- case "f_rest_23":
746
- return 63 /* PLYValue.SH_23 */;
747
- case "f_rest_24":
748
- return 64 /* PLYValue.SH_24 */;
749
- case "f_rest_25":
750
- return 65 /* PLYValue.SH_25 */;
751
- case "f_rest_26":
752
- return 66 /* PLYValue.SH_26 */;
753
- case "f_rest_27":
754
- return 67 /* PLYValue.SH_27 */;
755
- case "f_rest_28":
756
- return 68 /* PLYValue.SH_28 */;
757
- case "f_rest_29":
758
- return 69 /* PLYValue.SH_29 */;
759
- case "f_rest_30":
760
- return 70 /* PLYValue.SH_30 */;
761
- case "f_rest_31":
762
- return 71 /* PLYValue.SH_31 */;
763
- case "f_rest_32":
764
- return 72 /* PLYValue.SH_32 */;
765
- case "f_rest_33":
766
- return 73 /* PLYValue.SH_33 */;
767
- case "f_rest_34":
768
- return 74 /* PLYValue.SH_34 */;
769
- case "f_rest_35":
770
- return 75 /* PLYValue.SH_35 */;
771
- case "f_rest_36":
772
- return 76 /* PLYValue.SH_36 */;
773
- case "f_rest_37":
774
- return 77 /* PLYValue.SH_37 */;
775
- case "f_rest_38":
776
- return 78 /* PLYValue.SH_38 */;
777
- case "f_rest_39":
778
- return 79 /* PLYValue.SH_39 */;
779
- case "f_rest_40":
780
- return 80 /* PLYValue.SH_40 */;
781
- case "f_rest_41":
782
- return 81 /* PLYValue.SH_41 */;
783
- case "f_rest_42":
784
- return 82 /* PLYValue.SH_42 */;
785
- case "f_rest_43":
786
- return 83 /* PLYValue.SH_43 */;
787
- case "f_rest_44":
788
- return 84 /* PLYValue.SH_44 */;
152
+ effect.setMatrices("partWorld", partWorldData);
153
+ const partVisibilityData = [];
154
+ for (let i = 0; i < this.partCount; i++) {
155
+ partVisibilityData.push(this._partVisibility[i] ?? 1.0);
789
156
  }
790
- return 85 /* PLYValue.UNDEFINED */;
157
+ effect.setArray("partVisibility", partVisibilityData);
791
158
  }
159
+ // ---------------------------------------------------------------------------
160
+ // Part matrix / visibility management
161
+ // ---------------------------------------------------------------------------
792
162
  /**
793
- * Parse a PLY file header and returns metas infos on splats and chunks
794
- * @param data the loaded buffer
795
- * @returns a PLYHeader
163
+ * Gets the number of parts in the compound.
796
164
  */
797
- static ParseHeader(data) {
798
- const ubuf = new Uint8Array(data);
799
- const header = new TextDecoder().decode(ubuf.slice(0, 1024 * 10));
800
- const headerEnd = "end_header\n";
801
- const headerEndIndex = header.indexOf(headerEnd);
802
- if (headerEndIndex < 0 || !header) {
803
- // standard splat
804
- return null;
805
- }
806
- const vertexCount = parseInt(/element vertex (\d+)\n/.exec(header)[1]);
807
- const chunkElement = /element chunk (\d+)\n/.exec(header);
808
- let chunkCount = 0;
809
- if (chunkElement) {
810
- chunkCount = parseInt(chunkElement[1]);
811
- }
812
- let rowVertexOffset = 0;
813
- let rowChunkOffset = 0;
814
- const offsets = {
815
- double: 8,
816
- int: 4,
817
- uint: 4,
818
- float: 4,
819
- short: 2,
820
- ushort: 2,
821
- uchar: 1,
822
- list: 0,
823
- };
824
- let ElementMode;
825
- (function (ElementMode) {
826
- ElementMode[ElementMode["Vertex"] = 0] = "Vertex";
827
- ElementMode[ElementMode["Chunk"] = 1] = "Chunk";
828
- ElementMode[ElementMode["SH"] = 2] = "SH";
829
- ElementMode[ElementMode["Unused"] = 3] = "Unused";
830
- })(ElementMode || (ElementMode = {}));
831
- let chunkMode = 1 /* ElementMode.Chunk */;
832
- const vertexProperties = [];
833
- const chunkProperties = [];
834
- const filtered = header.slice(0, headerEndIndex).split("\n");
835
- let shDegree = 0;
836
- for (const prop of filtered) {
837
- if (prop.startsWith("property ")) {
838
- const [, typeName, name] = prop.split(" ");
839
- const value = GaussianSplattingMesh._ValueNameToEnum(name);
840
- if (value != 85 /* PLYValue.UNDEFINED */) {
841
- // SH degree 1,2 or 3 for 9, 24 or 45 values
842
- if (value >= 84 /* PLYValue.SH_44 */) {
843
- shDegree = 3;
844
- }
845
- else if (value >= 64 /* PLYValue.SH_24 */) {
846
- shDegree = Math.max(shDegree, 2);
847
- }
848
- else if (value >= 48 /* PLYValue.SH_8 */) {
849
- shDegree = Math.max(shDegree, 1);
850
- }
851
- }
852
- const type = GaussianSplattingMesh._TypeNameToEnum(typeName);
853
- if (chunkMode == 1 /* ElementMode.Chunk */) {
854
- chunkProperties.push({ value, type, offset: rowChunkOffset });
855
- rowChunkOffset += offsets[typeName];
856
- }
857
- else if (chunkMode == 0 /* ElementMode.Vertex */) {
858
- vertexProperties.push({ value, type, offset: rowVertexOffset });
859
- rowVertexOffset += offsets[typeName];
860
- }
861
- else if (chunkMode == 2 /* ElementMode.SH */) {
862
- // SH doesn't count for vertex row size but its properties are used to retrieve SH
863
- vertexProperties.push({ value, type, offset: rowVertexOffset });
864
- }
865
- if (!offsets[typeName]) {
866
- Logger.Warn(`Unsupported property type: ${typeName}.`);
867
- }
868
- }
869
- else if (prop.startsWith("element ")) {
870
- const [, type] = prop.split(" ");
871
- if (type == "chunk") {
872
- chunkMode = 1 /* ElementMode.Chunk */;
873
- }
874
- else if (type == "vertex") {
875
- chunkMode = 0 /* ElementMode.Vertex */;
876
- }
877
- else if (type == "sh") {
878
- chunkMode = 2 /* ElementMode.SH */;
879
- }
880
- else {
881
- chunkMode = 3 /* ElementMode.Unused */;
882
- }
883
- }
884
- }
885
- const dataView = new DataView(data, headerEndIndex + headerEnd.length);
886
- const buffer = new ArrayBuffer(GaussianSplattingMesh._RowOutputLength * vertexCount);
887
- let shBuffer = null;
888
- let shCoefficientCount = 0;
889
- if (shDegree) {
890
- const shVectorCount = (shDegree + 1) * (shDegree + 1) - 1;
891
- shCoefficientCount = shVectorCount * 3;
892
- shBuffer = new ArrayBuffer(shCoefficientCount * vertexCount);
893
- }
894
- return {
895
- vertexCount: vertexCount,
896
- chunkCount: chunkCount,
897
- rowVertexLength: rowVertexOffset,
898
- rowChunkLength: rowChunkOffset,
899
- vertexProperties: vertexProperties,
900
- chunkProperties: chunkProperties,
901
- dataView: dataView,
902
- buffer: buffer,
903
- shDegree: shDegree,
904
- shCoefficientCount: shCoefficientCount,
905
- shBuffer: shBuffer,
906
- };
907
- }
908
- static _GetCompressedChunks(header, offset) {
909
- if (!header.chunkCount) {
910
- return null;
911
- }
912
- const dataView = header.dataView;
913
- const compressedChunks = new Array(header.chunkCount);
914
- for (let i = 0; i < header.chunkCount; i++) {
915
- const currentChunk = {
916
- min: new Vector3(),
917
- max: new Vector3(),
918
- minScale: new Vector3(),
919
- maxScale: new Vector3(),
920
- minColor: new Vector3(0, 0, 0),
921
- maxColor: new Vector3(1, 1, 1),
922
- };
923
- compressedChunks[i] = currentChunk;
924
- for (let propertyIndex = 0; propertyIndex < header.chunkProperties.length; propertyIndex++) {
925
- const property = header.chunkProperties[propertyIndex];
926
- let value;
927
- switch (property.type) {
928
- case 0 /* PLYType.FLOAT */:
929
- value = dataView.getFloat32(property.offset + offset.value, true);
930
- break;
931
- default:
932
- continue;
933
- }
934
- switch (property.value) {
935
- case 0 /* PLYValue.MIN_X */:
936
- currentChunk.min.x = value;
937
- break;
938
- case 1 /* PLYValue.MIN_Y */:
939
- currentChunk.min.y = value;
940
- break;
941
- case 2 /* PLYValue.MIN_Z */:
942
- currentChunk.min.z = value;
943
- break;
944
- case 3 /* PLYValue.MAX_X */:
945
- currentChunk.max.x = value;
946
- break;
947
- case 4 /* PLYValue.MAX_Y */:
948
- currentChunk.max.y = value;
949
- break;
950
- case 5 /* PLYValue.MAX_Z */:
951
- currentChunk.max.z = value;
952
- break;
953
- case 6 /* PLYValue.MIN_SCALE_X */:
954
- currentChunk.minScale.x = value;
955
- break;
956
- case 7 /* PLYValue.MIN_SCALE_Y */:
957
- currentChunk.minScale.y = value;
958
- break;
959
- case 8 /* PLYValue.MIN_SCALE_Z */:
960
- currentChunk.minScale.z = value;
961
- break;
962
- case 9 /* PLYValue.MAX_SCALE_X */:
963
- currentChunk.maxScale.x = value;
964
- break;
965
- case 10 /* PLYValue.MAX_SCALE_Y */:
966
- currentChunk.maxScale.y = value;
967
- break;
968
- case 11 /* PLYValue.MAX_SCALE_Z */:
969
- currentChunk.maxScale.z = value;
970
- break;
971
- case 34 /* PLYValue.MIN_COLOR_R */:
972
- currentChunk.minColor.x = value;
973
- break;
974
- case 35 /* PLYValue.MIN_COLOR_G */:
975
- currentChunk.minColor.y = value;
976
- break;
977
- case 36 /* PLYValue.MIN_COLOR_B */:
978
- currentChunk.minColor.z = value;
979
- break;
980
- case 37 /* PLYValue.MAX_COLOR_R */:
981
- currentChunk.maxColor.x = value;
982
- break;
983
- case 38 /* PLYValue.MAX_COLOR_G */:
984
- currentChunk.maxColor.y = value;
985
- break;
986
- case 39 /* PLYValue.MAX_COLOR_B */:
987
- currentChunk.maxColor.z = value;
988
- break;
989
- }
990
- }
991
- offset.value += header.rowChunkLength;
992
- }
993
- return compressedChunks;
994
- }
995
- static _GetSplat(header, index, compressedChunks, offset) {
996
- const q = TmpVectors.Quaternion[0];
997
- const temp3 = TmpVectors.Vector3[0];
998
- const rowOutputLength = GaussianSplattingMesh._RowOutputLength;
999
- const buffer = header.buffer;
1000
- const dataView = header.dataView;
1001
- const position = new Float32Array(buffer, index * rowOutputLength, 3);
1002
- const scale = new Float32Array(buffer, index * rowOutputLength + 12, 3);
1003
- const rgba = new Uint8ClampedArray(buffer, index * rowOutputLength + 24, 4);
1004
- const rot = new Uint8ClampedArray(buffer, index * rowOutputLength + 28, 4);
1005
- let sh = null;
1006
- if (header.shBuffer) {
1007
- sh = new Uint8ClampedArray(header.shBuffer, index * header.shCoefficientCount, header.shCoefficientCount);
1008
- }
1009
- const chunkIndex = index >> 8;
1010
- let r0 = 255;
1011
- let r1 = 0;
1012
- let r2 = 0;
1013
- let r3 = 0;
1014
- const plySH = [];
1015
- for (let propertyIndex = 0; propertyIndex < header.vertexProperties.length; propertyIndex++) {
1016
- const property = header.vertexProperties[propertyIndex];
1017
- let value;
1018
- switch (property.type) {
1019
- case 0 /* PLYType.FLOAT */:
1020
- value = dataView.getFloat32(offset.value + property.offset, true);
1021
- break;
1022
- case 1 /* PLYType.INT */:
1023
- value = dataView.getInt32(offset.value + property.offset, true);
1024
- break;
1025
- case 2 /* PLYType.UINT */:
1026
- value = dataView.getUint32(offset.value + property.offset, true);
1027
- break;
1028
- case 3 /* PLYType.DOUBLE */:
1029
- value = dataView.getFloat64(offset.value + property.offset, true);
1030
- break;
1031
- case 4 /* PLYType.UCHAR */:
1032
- value = dataView.getUint8(offset.value + property.offset);
1033
- break;
1034
- default:
1035
- continue;
1036
- }
1037
- switch (property.value) {
1038
- case 12 /* PLYValue.PACKED_POSITION */:
1039
- {
1040
- const compressedChunk = compressedChunks[chunkIndex];
1041
- Unpack111011(value, temp3);
1042
- position[0] = Scalar.Lerp(compressedChunk.min.x, compressedChunk.max.x, temp3.x);
1043
- position[1] = Scalar.Lerp(compressedChunk.min.y, compressedChunk.max.y, temp3.y);
1044
- position[2] = Scalar.Lerp(compressedChunk.min.z, compressedChunk.max.z, temp3.z);
1045
- }
1046
- break;
1047
- case 13 /* PLYValue.PACKED_ROTATION */:
1048
- {
1049
- UnpackRot(value, q);
1050
- r0 = q.x;
1051
- r1 = q.y;
1052
- r2 = q.z;
1053
- r3 = q.w;
1054
- }
1055
- break;
1056
- case 14 /* PLYValue.PACKED_SCALE */:
1057
- {
1058
- const compressedChunk = compressedChunks[chunkIndex];
1059
- Unpack111011(value, temp3);
1060
- scale[0] = Math.exp(Scalar.Lerp(compressedChunk.minScale.x, compressedChunk.maxScale.x, temp3.x));
1061
- scale[1] = Math.exp(Scalar.Lerp(compressedChunk.minScale.y, compressedChunk.maxScale.y, temp3.y));
1062
- scale[2] = Math.exp(Scalar.Lerp(compressedChunk.minScale.z, compressedChunk.maxScale.z, temp3.z));
1063
- }
1064
- break;
1065
- case 15 /* PLYValue.PACKED_COLOR */:
1066
- {
1067
- const compressedChunk = compressedChunks[chunkIndex];
1068
- Unpack8888(value, rgba);
1069
- rgba[0] = Scalar.Lerp(compressedChunk.minColor.x, compressedChunk.maxColor.x, rgba[0] / 255) * 255;
1070
- rgba[1] = Scalar.Lerp(compressedChunk.minColor.y, compressedChunk.maxColor.y, rgba[1] / 255) * 255;
1071
- rgba[2] = Scalar.Lerp(compressedChunk.minColor.z, compressedChunk.maxColor.z, rgba[2] / 255) * 255;
1072
- }
1073
- break;
1074
- case 16 /* PLYValue.X */:
1075
- position[0] = value;
1076
- break;
1077
- case 17 /* PLYValue.Y */:
1078
- position[1] = value;
1079
- break;
1080
- case 18 /* PLYValue.Z */:
1081
- position[2] = value;
1082
- break;
1083
- case 19 /* PLYValue.SCALE_0 */:
1084
- scale[0] = Math.exp(value);
1085
- break;
1086
- case 20 /* PLYValue.SCALE_1 */:
1087
- scale[1] = Math.exp(value);
1088
- break;
1089
- case 21 /* PLYValue.SCALE_2 */:
1090
- scale[2] = Math.exp(value);
1091
- break;
1092
- case 22 /* PLYValue.DIFFUSE_RED */:
1093
- rgba[0] = value;
1094
- break;
1095
- case 23 /* PLYValue.DIFFUSE_GREEN */:
1096
- rgba[1] = value;
1097
- break;
1098
- case 24 /* PLYValue.DIFFUSE_BLUE */:
1099
- rgba[2] = value;
1100
- break;
1101
- case 26 /* PLYValue.F_DC_0 */:
1102
- rgba[0] = (0.5 + GaussianSplattingMesh._SH_C0 * value) * 255;
1103
- break;
1104
- case 27 /* PLYValue.F_DC_1 */:
1105
- rgba[1] = (0.5 + GaussianSplattingMesh._SH_C0 * value) * 255;
1106
- break;
1107
- case 28 /* PLYValue.F_DC_2 */:
1108
- rgba[2] = (0.5 + GaussianSplattingMesh._SH_C0 * value) * 255;
1109
- break;
1110
- case 29 /* PLYValue.F_DC_3 */:
1111
- rgba[3] = (0.5 + GaussianSplattingMesh._SH_C0 * value) * 255;
1112
- break;
1113
- case 25 /* PLYValue.OPACITY */:
1114
- rgba[3] = (1 / (1 + Math.exp(-value))) * 255;
1115
- break;
1116
- case 30 /* PLYValue.ROT_0 */:
1117
- r0 = value;
1118
- break;
1119
- case 31 /* PLYValue.ROT_1 */:
1120
- r1 = value;
1121
- break;
1122
- case 32 /* PLYValue.ROT_2 */:
1123
- r2 = value;
1124
- break;
1125
- case 33 /* PLYValue.ROT_3 */:
1126
- r3 = value;
1127
- break;
1128
- }
1129
- if (sh && property.value >= 40 /* PLYValue.SH_0 */ && property.value <= 84 /* PLYValue.SH_44 */) {
1130
- const shIndex = property.value - 40 /* PLYValue.SH_0 */;
1131
- if (property.type == 4 /* PLYType.UCHAR */ && header.chunkCount) {
1132
- // compressed ply. dataView points to beginning of vertex
1133
- // could be improved with a direct copy instead of a per SH index computation + copy
1134
- const compressedValue = dataView.getUint8(header.rowChunkLength * header.chunkCount + header.vertexCount * header.rowVertexLength + index * header.shCoefficientCount + shIndex);
1135
- // compressed .ply SH import : https://github.com/playcanvas/engine/blob/fda3f0368b45d7381f0b5a1722bd2056128eaebe/src/scene/gsplat/gsplat-compressed-data.js#L88C81-L88C98
1136
- plySH[shIndex] = (compressedValue * (8 / 255) - 4) * 127.5 + 127.5;
1137
- }
1138
- else {
1139
- const clampedValue = Scalar.Clamp(value * 127.5 + 127.5, 0, 255);
1140
- plySH[shIndex] = clampedValue;
1141
- }
1142
- }
1143
- }
1144
- if (sh) {
1145
- const shDim = header.shDegree == 1 ? 3 : header.shDegree == 2 ? 8 : 15;
1146
- for (let j = 0; j < shDim; j++) {
1147
- sh[j * 3 + 0] = plySH[j];
1148
- sh[j * 3 + 1] = plySH[j + shDim];
1149
- sh[j * 3 + 2] = plySH[j + shDim * 2];
1150
- }
1151
- }
1152
- q.set(r1, r2, r3, r0);
1153
- q.normalize();
1154
- rot[0] = q.w * 127.5 + 127.5;
1155
- rot[1] = q.x * 127.5 + 127.5;
1156
- rot[2] = q.y * 127.5 + 127.5;
1157
- rot[3] = q.z * 127.5 + 127.5;
1158
- offset.value += header.rowVertexLength;
165
+ get partCount() {
166
+ return this._partMatrices.length;
1159
167
  }
1160
168
  /**
1161
- * Converts a .ply data with SH coefficients splat
1162
- * if data array buffer is not ply, returns the original buffer
1163
- * @param data the .ply data to load
1164
- * @param useCoroutine use coroutine and yield
1165
- * @returns the loaded splat buffer and optional array of sh coefficients
169
+ * Gets the part visibility array.
1166
170
  */
1167
- static *ConvertPLYWithSHToSplat(data, useCoroutine = false) {
1168
- const header = GaussianSplattingMesh.ParseHeader(data);
1169
- if (!header) {
1170
- return { buffer: data };
1171
- }
1172
- const offset = { value: 0 };
1173
- const compressedChunks = GaussianSplattingMesh._GetCompressedChunks(header, offset);
1174
- for (let i = 0; i < header.vertexCount; i++) {
1175
- GaussianSplattingMesh._GetSplat(header, i, compressedChunks, offset);
1176
- if (i % GaussianSplattingMesh._PlyConversionBatchSize === 0 && useCoroutine) {
1177
- yield;
1178
- }
1179
- }
1180
- let sh = null;
1181
- // make SH texture buffers
1182
- if (header.shDegree && header.shBuffer) {
1183
- const textureCount = Math.ceil(header.shCoefficientCount / 16); // 4 components can be stored per texture, 4 sh per component
1184
- let shIndexRead = 0;
1185
- const ubuf = new Uint8Array(header.shBuffer);
1186
- // sh is an array of uint8array that will be used to create sh textures
1187
- sh = [];
1188
- const splatCount = header.vertexCount;
1189
- const engine = EngineStore.LastCreatedEngine;
1190
- if (engine) {
1191
- const width = engine.getCaps().maxTextureSize;
1192
- const height = Math.ceil(splatCount / width);
1193
- // create array for the number of textures needed.
1194
- for (let textureIndex = 0; textureIndex < textureCount; textureIndex++) {
1195
- const texture = new Uint8Array(height * width * 4 * 4); // 4 components per texture, 4 sh per component
1196
- sh.push(texture);
1197
- }
1198
- for (let i = 0; i < splatCount; i++) {
1199
- for (let shIndexWrite = 0; shIndexWrite < header.shCoefficientCount; shIndexWrite++) {
1200
- const shValue = ubuf[shIndexRead++];
1201
- const textureIndex = Math.floor(shIndexWrite / 16);
1202
- const shArray = sh[textureIndex];
1203
- const byteIndexInTexture = shIndexWrite % 16; // [0..15]
1204
- const offsetPerSplat = i * 16; // 16 sh values per texture per splat.
1205
- shArray[byteIndexInTexture + offsetPerSplat] = shValue;
1206
- }
1207
- }
1208
- }
1209
- }
1210
- return { buffer: header.buffer, sh: sh };
171
+ get partVisibility() {
172
+ return this._partVisibility;
1211
173
  }
1212
174
  /**
1213
- * Converts a .ply data array buffer to splat
1214
- * if data array buffer is not ply, returns the original buffer
1215
- * @param data the .ply data to load
1216
- * @param useCoroutine use coroutine and yield
1217
- * @returns the loaded splat buffer without SH coefficient, whether ply contains or not SH.
175
+ * Sets the world matrix for a specific part of the compound.
176
+ * This will trigger a re-sort of the mesh.
177
+ * The `_partMatrices` array is automatically extended when `partIndex >= partCount`.
178
+ * @param partIndex index of the part
179
+ * @param worldMatrix the world matrix to set
1218
180
  */
1219
- static *ConvertPLYToSplat(data, useCoroutine = false) {
1220
- const header = GaussianSplattingMesh.ParseHeader(data);
1221
- if (!header) {
1222
- return data;
181
+ setWorldMatrixForPart(partIndex, worldMatrix) {
182
+ if (this._partMatrices.length <= partIndex) {
183
+ this.computeWorldMatrix(true);
184
+ const defaultMatrix = this.getWorldMatrix();
185
+ while (this._partMatrices.length <= partIndex) {
186
+ this._partMatrices.push(defaultMatrix.clone());
187
+ this._partVisibility.push(1.0);
188
+ }
1223
189
  }
1224
- const offset = { value: 0 };
1225
- const compressedChunks = GaussianSplattingMesh._GetCompressedChunks(header, offset);
1226
- for (let i = 0; i < header.vertexCount; i++) {
1227
- GaussianSplattingMesh._GetSplat(header, i, compressedChunks, offset);
1228
- if (i % GaussianSplattingMesh._PlyConversionBatchSize === 0 && useCoroutine) {
1229
- yield;
190
+ this._partMatrices[partIndex].copyFrom(worldMatrix);
191
+ // During a batch rebuild suppress intermediate posts — the final correct set is posted
192
+ // once the full rebuild completes (at the end of removePart).
193
+ if (!this._rebuilding) {
194
+ if (this._worker) {
195
+ this._worker.postMessage({ partMatrices: this._partMatrices.map((matrix) => new Float32Array(matrix.m)) });
1230
196
  }
197
+ this._postToWorker(true);
1231
198
  }
1232
- return header.buffer;
1233
- }
1234
- /**
1235
- * Converts a .ply data array buffer to splat
1236
- * if data array buffer is not ply, returns the original buffer
1237
- * @param data the .ply data to load
1238
- * @returns the loaded splat buffer
1239
- */
1240
- static async ConvertPLYToSplatAsync(data) {
1241
- return await runCoroutineAsync(GaussianSplattingMesh.ConvertPLYToSplat(data, true), createYieldingScheduler());
1242
- }
1243
- /**
1244
- * Converts a .ply with SH data array buffer to splat
1245
- * if data array buffer is not ply, returns the original buffer
1246
- * @param data the .ply data to load
1247
- * @returns the loaded splat buffer with SH
1248
- */
1249
- static async ConvertPLYWithSHToSplatAsync(data) {
1250
- return await runCoroutineAsync(GaussianSplattingMesh.ConvertPLYWithSHToSplat(data, true), createYieldingScheduler());
1251
199
  }
1252
200
  /**
1253
- * Loads a .splat Gaussian Splatting array buffer asynchronously
1254
- * @param data arraybuffer containing splat file
1255
- * @returns a promise that resolves when the operation is complete
201
+ * Gets the world matrix for a specific part of the compound.
202
+ * @param partIndex index of the part, that must be between 0 and partCount - 1
203
+ * @returns the world matrix for the part, or the current world matrix of the mesh if the part is not found
1256
204
  */
1257
- async loadDataAsync(data) {
1258
- return await this.updateDataAsync(data);
205
+ getWorldMatrixForPart(partIndex) {
206
+ return this._partMatrices[partIndex] ?? this.getWorldMatrix();
1259
207
  }
1260
208
  /**
1261
- * Loads a Gaussian or Splatting file asynchronously
1262
- * @param url path to the splat file to load
1263
- * @param scene optional scene it belongs to
1264
- * @returns a promise that resolves when the operation is complete
1265
- * @deprecated Please use SceneLoader.ImportMeshAsync instead
209
+ * Gets the visibility for a specific part of the compound.
210
+ * @param partIndex index of the part, that must be between 0 and partCount - 1
211
+ * @returns the visibility value (0.0 to 1.0) for the part
1266
212
  */
1267
- async loadFileAsync(url, scene) {
1268
- await ImportMeshAsync(url, (scene || EngineStore.LastCreatedScene), { pluginOptions: { splat: { gaussianSplattingMesh: this } } });
213
+ getPartVisibility(partIndex) {
214
+ return this._partVisibility[partIndex] ?? 1.0;
1269
215
  }
1270
216
  /**
1271
- * Releases resources associated with this mesh.
1272
- * @param doNotRecurse Set to true to not recurse into each children (recurse into each children by default)
217
+ * Sets the visibility for a specific part of the compound.
218
+ * @param partIndex index of the part, that must be between 0 and partCount - 1
219
+ * @param value the visibility value (0.0 to 1.0) to set
1273
220
  */
1274
- dispose(doNotRecurse) {
1275
- this._covariancesATexture?.dispose();
1276
- this._covariancesBTexture?.dispose();
1277
- this._centersTexture?.dispose();
1278
- this._colorsTexture?.dispose();
1279
- if (this._shTextures) {
1280
- for (const shTexture of this._shTextures) {
1281
- shTexture.dispose();
1282
- }
1283
- }
1284
- if (this._partIndicesTexture) {
1285
- this._partIndicesTexture.dispose();
1286
- }
1287
- this._covariancesATexture = null;
1288
- this._covariancesBTexture = null;
1289
- this._centersTexture = null;
1290
- this._colorsTexture = null;
1291
- this._shTextures = null;
1292
- this._partIndicesTexture = null;
1293
- this._partMatrices = [];
1294
- this._worker?.terminate();
1295
- this._worker = null;
1296
- // delete meshes created for each camera
1297
- this._cameraViewInfos.forEach((cameraViewInfo) => {
1298
- cameraViewInfo.mesh.dispose();
1299
- });
1300
- // dispose all proxy meshes
1301
- this._partProxies.forEach((proxy) => {
1302
- proxy.dispose();
1303
- });
1304
- this._partProxies.clear();
1305
- super.dispose(doNotRecurse, true);
221
+ setPartVisibility(partIndex, value) {
222
+ this._partVisibility[partIndex] = Math.max(0.0, Math.min(1.0, value));
1306
223
  }
1307
224
  _copyTextures(source) {
1308
- this._covariancesATexture = source.covariancesATexture?.clone();
1309
- this._covariancesBTexture = source.covariancesBTexture?.clone();
1310
- this._centersTexture = source.centersTexture?.clone();
1311
- this._colorsTexture = source.colorsTexture?.clone();
225
+ super._copyTextures(source);
1312
226
  this._partIndicesTexture = source._partIndicesTexture?.clone();
1313
- if (source._shTextures) {
1314
- this._shTextures = [];
1315
- for (const shTexture of source._shTextures) {
1316
- this._shTextures?.push(shTexture.clone());
1317
- }
1318
- }
1319
- }
1320
- /**
1321
- * Returns a new Mesh object generated from the current mesh properties.
1322
- * @param name is a string, the name given to the new mesh
1323
- * @returns a new Gaussian Splatting Mesh
1324
- */
1325
- clone(name = "") {
1326
- const newGS = new GaussianSplattingMesh(name, undefined, this.getScene());
1327
- newGS._copySource(this);
1328
- newGS.makeGeometryUnique();
1329
- newGS._vertexCount = this._vertexCount;
1330
- newGS._copyTextures(this);
1331
- newGS._modelViewProjectionMatrix = Matrix.Identity();
1332
- newGS._splatPositions = this._splatPositions;
1333
- newGS._readyToDisplay = false;
1334
- newGS._disableDepthSort = this._disableDepthSort;
1335
- newGS._partMatrices = this._partMatrices.map((m) => m.clone());
1336
- newGS._instanciateWorker();
1337
- const binfo = this.getBoundingInfo();
1338
- newGS.getBoundingInfo().reConstruct(binfo.minimum, binfo.maximum, this.getWorldMatrix());
1339
- newGS.forcedInstanceCount = this.forcedInstanceCount;
1340
- newGS.setEnabled(true);
1341
- return newGS;
1342
- }
1343
- _makeEmptySplat(index, covA, covB, colorArray) {
1344
- const covBSItemSize = this._useRGBACovariants ? 4 : 2;
1345
- this._splatPositions[4 * index + 0] = 0;
1346
- this._splatPositions[4 * index + 1] = 0;
1347
- this._splatPositions[4 * index + 2] = 0;
1348
- covA[index * 4 + 0] = ToHalfFloat(0);
1349
- covA[index * 4 + 1] = ToHalfFloat(0);
1350
- covA[index * 4 + 2] = ToHalfFloat(0);
1351
- covA[index * 4 + 3] = ToHalfFloat(0);
1352
- covB[index * covBSItemSize + 0] = ToHalfFloat(0);
1353
- covB[index * covBSItemSize + 1] = ToHalfFloat(0);
1354
- colorArray[index * 4 + 3] = 0;
1355
- }
1356
- _makeSplat(index, fBuffer, uBuffer, covA, covB, colorArray, minimum, maximum, options) {
1357
- const matrixRotation = TmpVectors.Matrix[0];
1358
- const matrixScale = TmpVectors.Matrix[1];
1359
- const quaternion = TmpVectors.Quaternion[0];
1360
- const covBSItemSize = this._useRGBACovariants ? 4 : 2;
1361
- const x = fBuffer[8 * index + 0];
1362
- const y = fBuffer[8 * index + 1] * (options.flipY ? -1 : 1);
1363
- const z = fBuffer[8 * index + 2];
1364
- this._splatPositions[4 * index + 0] = x;
1365
- this._splatPositions[4 * index + 1] = y;
1366
- this._splatPositions[4 * index + 2] = z;
1367
- minimum.minimizeInPlaceFromFloats(x, y, z);
1368
- maximum.maximizeInPlaceFromFloats(x, y, z);
1369
- quaternion.set((uBuffer[32 * index + 28 + 1] - 127.5) / 127.5, (uBuffer[32 * index + 28 + 2] - 127.5) / 127.5, (uBuffer[32 * index + 28 + 3] - 127.5) / 127.5, -(uBuffer[32 * index + 28 + 0] - 127.5) / 127.5);
1370
- quaternion.normalize();
1371
- quaternion.toRotationMatrix(matrixRotation);
1372
- Matrix.ScalingToRef(fBuffer[8 * index + 3 + 0] * 2, fBuffer[8 * index + 3 + 1] * 2, fBuffer[8 * index + 3 + 2] * 2, matrixScale);
1373
- const m = matrixRotation.multiplyToRef(matrixScale, TmpVectors.Matrix[0]).m;
1374
- const covariances = this._tmpCovariances;
1375
- covariances[0] = m[0] * m[0] + m[1] * m[1] + m[2] * m[2];
1376
- covariances[1] = m[0] * m[4] + m[1] * m[5] + m[2] * m[6];
1377
- covariances[2] = m[0] * m[8] + m[1] * m[9] + m[2] * m[10];
1378
- covariances[3] = m[4] * m[4] + m[5] * m[5] + m[6] * m[6];
1379
- covariances[4] = m[4] * m[8] + m[5] * m[9] + m[6] * m[10];
1380
- covariances[5] = m[8] * m[8] + m[9] * m[9] + m[10] * m[10];
1381
- // normalize covA, covB
1382
- let factor = -10000;
1383
- for (let covIndex = 0; covIndex < 6; covIndex++) {
1384
- factor = Math.max(factor, Math.abs(covariances[covIndex]));
1385
- }
1386
- this._splatPositions[4 * index + 3] = factor;
1387
- const transform = factor;
1388
- covA[index * 4 + 0] = ToHalfFloat(covariances[0] / transform);
1389
- covA[index * 4 + 1] = ToHalfFloat(covariances[1] / transform);
1390
- covA[index * 4 + 2] = ToHalfFloat(covariances[2] / transform);
1391
- covA[index * 4 + 3] = ToHalfFloat(covariances[3] / transform);
1392
- covB[index * covBSItemSize + 0] = ToHalfFloat(covariances[4] / transform);
1393
- covB[index * covBSItemSize + 1] = ToHalfFloat(covariances[5] / transform);
1394
- // colors
1395
- colorArray[index * 4 + 0] = uBuffer[32 * index + 24 + 0];
1396
- colorArray[index * 4 + 1] = uBuffer[32 * index + 24 + 1];
1397
- colorArray[index * 4 + 2] = uBuffer[32 * index + 24 + 2];
1398
- colorArray[index * 4 + 3] = uBuffer[32 * index + 24 + 3];
1399
227
  }
1400
- // NB: partIndices is assumed to be padded to a round texture size
1401
- _updateTextures(covA, covB, colorArray, sh, partIndices) {
1402
- const textureSize = this._getTextureSize(this._vertexCount);
1403
- // Update the textures
1404
- const createTextureFromData = (data, width, height, format) => {
1405
- return new RawTexture(data, width, height, format, this._scene, false, false, 2, 1);
1406
- };
228
+ _onUpdateTextures(textureSize) {
1407
229
  const createTextureFromDataU8 = (data, width, height, format) => {
1408
230
  return new RawTexture(data, width, height, format, this._scene, false, false, 2, 0);
1409
231
  };
1410
- const createTextureFromDataU32 = (data, width, height, format) => {
1411
- return new RawTexture(data, width, height, format, this._scene, false, false, 1, 7);
1412
- };
1413
- const createTextureFromDataF16 = (data, width, height, format) => {
1414
- return new RawTexture(data, width, height, format, this._scene, false, false, 2, 2);
1415
- };
1416
- const firstTime = this._covariancesATexture === null;
1417
- const textureSizeChanged = this._textureSize.y != textureSize.y;
1418
- if (!firstTime && !textureSizeChanged) {
1419
- this._delayedTextureUpdate = { covA, covB, colors: colorArray, centers: this._splatPositions, sh, partIndices };
1420
- const positions = Float32Array.from(this._splatPositions);
1421
- const vertexCount = this._vertexCount;
1422
- if (this._worker) {
1423
- this._worker.postMessage({ positions, vertexCount }, [positions.buffer]);
1424
- }
1425
- // Handle SH textures in update path - create if they don't exist
1426
- if (sh && !this._shTextures) {
1427
- this._shTextures = [];
1428
- for (const shData of sh) {
1429
- const buffer = new Uint32Array(shData.buffer);
1430
- const shTexture = createTextureFromDataU32(buffer, textureSize.x, textureSize.y, 11);
1431
- shTexture.wrapU = 0;
1432
- shTexture.wrapV = 0;
1433
- this._shTextures.push(shTexture);
1434
- }
1435
- }
1436
- // Handle compound data, if any
1437
- if (partIndices && !this._partIndicesTexture) {
1438
- const buffer = new Uint8Array(partIndices);
232
+ // Keep the part indices texture in sync with _partIndices whenever textures are rebuilt.
233
+ // The old "only create if absent" logic left the texture stale after a second addPart/addParts
234
+ // call that doesn't change the texture dimensions: all new splats kept reading partIndex=0
235
+ // (the first part), causing wrong positions, broken GPU picking, and shared movement.
236
+ if (this._partIndices) {
237
+ const buffer = new Uint8Array(this._partIndices);
238
+ if (!this._partIndicesTexture) {
1439
239
  this._partIndicesTexture = createTextureFromDataU8(buffer, textureSize.x, textureSize.y, 6);
1440
240
  this._partIndicesTexture.wrapU = 0;
1441
241
  this._partIndicesTexture.wrapV = 0;
1442
242
  }
1443
- if (this._worker) {
1444
- this._worker.postMessage({ partIndices: partIndices ?? null });
1445
- }
1446
- this._postToWorker(true);
1447
- }
1448
- else {
1449
- this._textureSize = textureSize;
1450
- this._covariancesATexture = createTextureFromDataF16(covA, textureSize.x, textureSize.y, 5);
1451
- this._covariancesBTexture = createTextureFromDataF16(covB, textureSize.x, textureSize.y, this._useRGBACovariants ? 5 : 7);
1452
- this._centersTexture = createTextureFromData(this._splatPositions, textureSize.x, textureSize.y, 5);
1453
- this._colorsTexture = createTextureFromDataU8(colorArray, textureSize.x, textureSize.y, 5);
1454
- if (sh) {
1455
- this._shTextures = [];
1456
- for (const shData of sh) {
1457
- const buffer = new Uint32Array(shData.buffer);
1458
- const shTexture = createTextureFromDataU32(buffer, textureSize.x, textureSize.y, 11);
1459
- shTexture.wrapU = 0;
1460
- shTexture.wrapV = 0;
1461
- this._shTextures.push(shTexture);
1462
- }
1463
- }
1464
- if (partIndices) {
1465
- const buffer = new Uint8Array(partIndices);
1466
- this._partIndicesTexture = createTextureFromDataU8(buffer, textureSize.x, textureSize.y, 6);
1467
- this._partIndicesTexture.wrapU = 0;
1468
- this._partIndicesTexture.wrapV = 0;
1469
- }
1470
- if (firstTime) {
1471
- this._instanciateWorker();
1472
- }
1473
243
  else {
1474
- if (this._worker) {
1475
- const positions = Float32Array.from(this._splatPositions);
1476
- const vertexCount = this._vertexCount;
1477
- this._worker.postMessage({ positions, vertexCount }, [positions.buffer]);
1478
- this._worker.postMessage({ partIndices: partIndices ?? null });
244
+ const existingSize = this._partIndicesTexture.getSize();
245
+ if (existingSize.width !== textureSize.x || existingSize.height !== textureSize.y) {
246
+ // Dimensions changed — dispose and recreate at the new size.
247
+ this._partIndicesTexture.dispose();
248
+ this._partIndicesTexture = createTextureFromDataU8(buffer, textureSize.x, textureSize.y, 6);
249
+ this._partIndicesTexture.wrapU = 0;
250
+ this._partIndicesTexture.wrapV = 0;
251
+ }
252
+ else {
253
+ // Same size — update data in-place (e.g. second addParts fitting in existing dims).
254
+ this._updateTextureFromData(this._partIndicesTexture, buffer, textureSize.x, 0, textureSize.y);
1479
255
  }
1480
- this._postToWorker(true);
1481
256
  }
1482
257
  }
1483
258
  }
1484
- *_updateData(data, isAsync, sh, partIndices, options = { flipY: false }) {
1485
- // if a covariance texture is present, then it's not a creation but an update
1486
- if (!this._covariancesATexture) {
1487
- this._readyToDisplay = false;
1488
- }
1489
- // Parse the data
1490
- const uBuffer = new Uint8Array(data);
1491
- const fBuffer = new Float32Array(uBuffer.buffer);
1492
- if (this._keepInRam) {
1493
- this._splatsData = data;
1494
- this._shData = sh ? sh.map((arr) => new Uint8Array(arr)) : null;
1495
- }
1496
- const vertexCount = uBuffer.length / GaussianSplattingMesh._RowOutputLength;
1497
- if (vertexCount != this._vertexCount) {
1498
- this._updateSplatIndexBuffer(vertexCount);
1499
- }
1500
- this._vertexCount = vertexCount;
1501
- // degree == 1 for 1 texture (3 terms), 2 for 2 textures(8 terms) and 3 for 3 textures (15 terms)
1502
- this._shDegree = sh ? sh.length : 0;
1503
- const textureSize = this._getTextureSize(vertexCount);
1504
- const textureLength = textureSize.x * textureSize.y;
1505
- const lineCountUpdate = GaussianSplattingMesh.ProgressiveUpdateAmount ?? textureSize.y;
1506
- const textureLengthPerUpdate = textureSize.x * lineCountUpdate;
1507
- this._splatPositions = new Float32Array(4 * textureLength);
1508
- const covA = new Uint16Array(textureLength * 4);
1509
- const covB = new Uint16Array((this._useRGBACovariants ? 4 : 2) * textureLength);
1510
- const colorArray = new Uint8Array(textureLength * 4);
1511
- // Ensure that partMatrices.length is at least the maximum part index + 1
1512
- if (partIndices) {
1513
- // We always keep part indices in RAM because they are needed for sorting
1514
- this._partIndices = new Uint8Array(textureLength);
1515
- this._partIndices.set(partIndices);
1516
- let maxPartIndex = -1;
1517
- for (let i = 0; i < partIndices.length; i++) {
1518
- maxPartIndex = Math.max(maxPartIndex, partIndices[i]);
1519
- }
1520
- this._ensureMinimumPartMatricesLength(maxPartIndex + 1);
1521
- }
1522
- const minimum = new Vector3(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
1523
- const maximum = new Vector3(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE);
1524
- if (GaussianSplattingMesh.ProgressiveUpdateAmount) {
1525
- // create textures with not filled-yet array, then update directly portions of it
1526
- this._updateTextures(covA, covB, colorArray, sh, this._partIndices ? this._partIndices : undefined);
1527
- this.setEnabled(true);
1528
- const partCount = Math.ceil(textureSize.y / lineCountUpdate);
1529
- for (let partIndex = 0; partIndex < partCount; partIndex++) {
1530
- const updateLine = partIndex * lineCountUpdate;
1531
- const splatIndexBase = updateLine * textureSize.x;
1532
- for (let i = 0; i < textureLengthPerUpdate; i++) {
1533
- this._makeSplat(splatIndexBase + i, fBuffer, uBuffer, covA, covB, colorArray, minimum, maximum, options);
1534
- }
1535
- this._updateSubTextures(this._splatPositions, covA, covB, colorArray, updateLine, Math.min(lineCountUpdate, textureSize.y - updateLine));
1536
- // Update the binfo
1537
- this.getBoundingInfo().reConstruct(minimum, maximum, this.getWorldMatrix());
1538
- if (isAsync) {
1539
- yield;
1540
- }
1541
- }
1542
- // sort will be dirty here as just finished filled positions will not be sorted
1543
- const positions = Float32Array.from(this._splatPositions);
1544
- const vertexCount = this._vertexCount;
259
+ _updateSubTextures(splatPositions, covA, covB, colorArray, lineStart, lineCount, sh, partIndices) {
260
+ super._updateSubTextures(splatPositions, covA, covB, colorArray, lineStart, lineCount, sh);
261
+ if (partIndices && this._partIndicesTexture) {
262
+ const textureSize = this._getTextureSize(this._vertexCount);
263
+ const texelStart = lineStart * textureSize.x;
264
+ const texelCount = lineCount * textureSize.x;
265
+ const partIndicesView = new Uint8Array(partIndices.buffer, texelStart, texelCount);
266
+ this._updateTextureFromData(this._partIndicesTexture, partIndicesView, textureSize.x, lineStart, lineCount);
1545
267
  if (this._worker) {
1546
- this._worker.postMessage({ positions, vertexCount }, [positions.buffer]);
1547
- this._worker.postMessage({ partIndices });
1548
- }
1549
- this._sortIsDirty = true;
1550
- }
1551
- else {
1552
- const paddedVertexCount = (vertexCount + 15) & ~0xf;
1553
- for (let i = 0; i < vertexCount; i++) {
1554
- this._makeSplat(i, fBuffer, uBuffer, covA, covB, colorArray, minimum, maximum, options);
1555
- if (isAsync && i % GaussianSplattingMesh._SplatBatchSize === 0) {
1556
- yield;
1557
- }
268
+ this._worker.postMessage({ partIndices: partIndices });
1558
269
  }
1559
- // pad the rest
1560
- for (let i = vertexCount; i < paddedVertexCount; i++) {
1561
- this._makeEmptySplat(i, covA, covB, colorArray);
1562
- }
1563
- // textures
1564
- this._updateTextures(covA, covB, colorArray, sh, this._partIndices ? this._partIndices : undefined);
1565
- // Update the binfo
1566
- this.getBoundingInfo().reConstruct(minimum, maximum, this.getWorldMatrix());
1567
- this.setEnabled(true);
1568
- this._sortIsDirty = true;
1569
270
  }
1570
- this._postToWorker(true);
1571
- }
1572
- /**
1573
- * Update asynchronously the buffer
1574
- * @param data array buffer containing center, color, orientation and scale of splats
1575
- * @param sh optional array of uint8 array for SH data
1576
- * @param partIndices optional array of uint8 for rig node indices
1577
- * @returns a promise
1578
- */
1579
- async updateDataAsync(data, sh, partIndices) {
1580
- return await runCoroutineAsync(this._updateData(data, true, sh, partIndices), createYieldingScheduler());
1581
271
  }
272
+ // ---------------------------------------------------------------------------
273
+ // Private helpers
274
+ // ---------------------------------------------------------------------------
1582
275
  /**
1583
- * @experimental
1584
- * Update data from GS (position, orientation, color, scaling)
1585
- * @param data array that contain all the datas
1586
- * @param sh optional array of uint8 array for SH data
1587
- * @param options optional informations on how to treat data (needs to be 3rd for backward compatibility)
1588
- * @param partIndices optional array of uint8 for rig node indices
276
+ * Creates the part indices GPU texture the first time an incremental addPart introduces
277
+ * compound data. Has no effect if the texture already exists or no partIndices are provided.
278
+ * @param textureSize - Current texture dimensions
279
+ * @param partIndices - Part index data; if undefined the method is a no-op
1589
280
  */
1590
- updateData(data, sh, options = { flipY: true }, partIndices) {
1591
- runCoroutineSync(this._updateData(data, false, sh, partIndices, options));
281
+ _ensurePartIndicesTexture(textureSize, partIndices) {
282
+ if (!partIndices || this._partIndicesTexture) {
283
+ return;
284
+ }
285
+ const buffer = new Uint8Array(this._partIndices);
286
+ this._partIndicesTexture = new RawTexture(buffer, textureSize.x, textureSize.y, 6, this._scene, false, false, 2, 0);
287
+ this._partIndicesTexture.wrapU = 0;
288
+ this._partIndicesTexture.wrapV = 0;
289
+ if (this._worker) {
290
+ this._worker.postMessage({ partIndices: partIndices ?? null });
291
+ }
1592
292
  }
1593
293
  /**
1594
- * Refreshes the bounding info, taking into account all the thin instances defined
1595
- * @returns the current Gaussian Splatting
294
+ * Core implementation for adding one or more external GaussianSplattingMesh objects as new
295
+ * parts. Writes directly into texture-sized CPU arrays and uploads in one pass — no merged
296
+ * CPU splat buffer is ever constructed.
297
+ *
298
+ * @param others - Source meshes to append (must each be non-compound and fully loaded)
299
+ * @param disposeOthers - Dispose source meshes after appending
300
+ * @returns Proxy meshes and their assigned part indices
1596
301
  */
1597
- refreshBoundingInfo() {
1598
- this.thinInstanceRefreshBoundingInfo(false);
1599
- return this;
1600
- }
1601
- // in case size is different
1602
- _updateSplatIndexBuffer(vertexCount) {
1603
- const paddedVertexCount = (vertexCount + 15) & ~0xf;
1604
- if (!this._splatIndex || vertexCount != this._splatIndex.length) {
1605
- this._splatIndex = new Float32Array(paddedVertexCount);
1606
- for (let i = 0; i < paddedVertexCount; i++) {
1607
- this._splatIndex[i] = i;
1608
- }
1609
- // update meshes for knowns cameras
1610
- this._cameraViewInfos.forEach((cameraViewInfos) => {
1611
- cameraViewInfos.mesh.thinInstanceSetBuffer("splatIndex", this._splatIndex, 16, false);
1612
- });
302
+ _addPartsInternal(others, disposeOthers) {
303
+ if (others.length === 0) {
304
+ return { proxyMeshes: [], assignedPartIndices: [] };
1613
305
  }
1614
- // Update depthMix
1615
- if ((!this._depthMix || vertexCount != this._depthMix.length) && !IsNative) {
1616
- this._depthMix = new BigInt64Array(paddedVertexCount);
306
+ // Validate
307
+ for (const other of others) {
308
+ if (!other._splatsData) {
309
+ throw new Error(`To call addPart()/addParts(), each source mesh must be fully loaded`);
310
+ }
311
+ if (other.isCompound) {
312
+ throw new Error(`To call addPart()/addParts(), each source mesh must not be a compound`);
313
+ }
1617
314
  }
1618
- this.forcedInstanceCount = Math.max(paddedVertexCount >> 4, 1);
1619
- }
1620
- _updateSubTextures(centers, covA, covB, colors, lineStart, lineCount, sh, partIndices) {
1621
- const updateTextureFromData = (texture, data, width, lineStart, lineCount) => {
1622
- this.getEngine().updateTextureData(texture.getInternalTexture(), data, 0, lineStart, width, lineCount, 0, 0, false);
1623
- };
1624
- const textureSize = this._getTextureSize(this._vertexCount);
315
+ const splatCountA = this._vertexCount;
316
+ const totalOtherCount = others.reduce((s, o) => s + o._vertexCount, 0);
317
+ const totalCount = splatCountA + totalOtherCount;
318
+ const textureSize = this._getTextureSize(totalCount);
319
+ const textureLength = textureSize.x * textureSize.y;
1625
320
  const covBSItemSize = this._useRGBACovariants ? 4 : 2;
1626
- const texelStart = lineStart * textureSize.x;
1627
- const texelCount = lineCount * textureSize.x;
1628
- const covAView = new Uint16Array(covA.buffer, texelStart * 4 * Uint16Array.BYTES_PER_ELEMENT, texelCount * 4);
1629
- const covBView = new Uint16Array(covB.buffer, texelStart * covBSItemSize * Uint16Array.BYTES_PER_ELEMENT, texelCount * covBSItemSize);
1630
- const colorsView = new Uint8Array(colors.buffer, texelStart * 4, texelCount * 4);
1631
- const centersView = new Float32Array(centers.buffer, texelStart * 4 * Float32Array.BYTES_PER_ELEMENT, texelCount * 4);
1632
- updateTextureFromData(this._covariancesATexture, covAView, textureSize.x, lineStart, lineCount);
1633
- updateTextureFromData(this._covariancesBTexture, covBView, textureSize.x, lineStart, lineCount);
1634
- updateTextureFromData(this._centersTexture, centersView, textureSize.x, lineStart, lineCount);
1635
- updateTextureFromData(this._colorsTexture, colorsView, textureSize.x, lineStart, lineCount);
1636
- if (sh) {
1637
- for (let i = 0; i < sh.length; i++) {
1638
- const componentCount = 4;
1639
- const shView = new Uint32Array(sh[i].buffer, texelStart * componentCount * 4, texelCount * componentCount);
1640
- updateTextureFromData(this._shTextures[i], shView, textureSize.x, lineStart, lineCount);
321
+ // Allocate destination arrays for the full new texture
322
+ const covA = new Uint16Array(textureLength * 4);
323
+ const covB = new Uint16Array(covBSItemSize * textureLength);
324
+ const colorArray = new Uint8Array(textureLength * 4);
325
+ // Determine merged SH degree
326
+ const hasSH = this._shData !== null && others.every((o) => o._shData !== null);
327
+ const shDegreeNew = hasSH ? Math.max(this._shDegree, ...others.map((o) => o._shDegree)) : 0;
328
+ let sh = undefined;
329
+ if (hasSH && shDegreeNew > 0) {
330
+ const bytesPerTexel = 16;
331
+ sh = [];
332
+ for (let i = 0; i < shDegreeNew; i++) {
333
+ sh.push(new Uint8Array(textureLength * bytesPerTexel));
1641
334
  }
1642
335
  }
1643
- if (partIndices && this._partIndicesTexture) {
1644
- const partIndicesView = new Uint8Array(partIndices.buffer, texelStart, texelCount);
1645
- updateTextureFromData(this._partIndicesTexture, partIndicesView, textureSize.x, lineStart, lineCount);
1646
- }
1647
- }
1648
- _instanciateWorker() {
1649
- if (!this._vertexCount) {
1650
- return;
1651
- }
1652
- if (this._disableDepthSort) {
1653
- return;
336
+ // --- Incremental path: can we reuse the already-committed GPU region? ---
337
+ const incremental = this._canReuseCachedData(splatCountA, totalCount);
338
+ const firstNewLine = incremental ? Math.floor(splatCountA / textureSize.x) : 0;
339
+ const minimum = incremental ? this._cachedBoundingMin.clone() : new Vector3(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
340
+ const maximum = incremental ? this._cachedBoundingMax.clone() : new Vector3(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE);
341
+ // Preserve existing processed positions in the new array
342
+ const oldPositions = this._splatPositions;
343
+ this._splatPositions = new Float32Array(4 * textureLength);
344
+ if (incremental && oldPositions) {
345
+ this._splatPositions.set(oldPositions.subarray(0, splatCountA * 4));
1654
346
  }
1655
- this._updateSplatIndexBuffer(this._vertexCount);
1656
- // no worker in native
1657
- if (IsNative) {
1658
- return;
347
+ // --- Build part indices ---
348
+ let nextPartIndex = this.partCount;
349
+ let partIndicesA = this._partIndices;
350
+ if (!partIndicesA) {
351
+ // First addPart on a plain mesh: assign its splats to part 0
352
+ partIndicesA = new Uint8Array(splatCountA);
353
+ nextPartIndex = splatCountA > 0 ? 1 : 0;
1659
354
  }
1660
- // Start the worker thread
1661
- this._worker?.terminate();
1662
- this._worker = new Worker(URL.createObjectURL(new Blob(["(", GaussianSplattingMesh._CreateWorker.toString(), ")(self)"], {
1663
- type: "application/javascript",
1664
- })));
1665
- const positions = Float32Array.from(this._splatPositions);
1666
- const partIndices = this._partIndices ? new Uint8Array(this._partIndices) : null;
1667
- const partMatrices = this._partMatrices.map((matrix) => new Float32Array(matrix.m));
1668
- this._worker.postMessage({ positions }, [positions.buffer]);
1669
- this._worker.postMessage({ partIndices });
1670
- this._worker.postMessage({ partMatrices });
1671
- this._worker.onmessage = (e) => {
1672
- // Recompute vertexCountPadded in case _vertexCount has changed since the last update
1673
- const vertexCountPadded = (this._vertexCount + 15) & ~0xf;
1674
- // If the vertex count changed, we discard this result and trigger a new sort
1675
- if (e.data.depthMix.length != vertexCountPadded) {
1676
- this._canPostToWorker = true;
1677
- this._postToWorker(true);
1678
- this._sortIsDirty = false;
1679
- return;
1680
- }
1681
- this._depthMix = e.data.depthMix;
1682
- const cameraId = e.data.cameraId;
1683
- const indexMix = new Uint32Array(e.data.depthMix.buffer);
1684
- if (this._splatIndex) {
1685
- for (let j = 0; j < vertexCountPadded; j++) {
1686
- this._splatIndex[j] = indexMix[2 * j];
355
+ this._partIndices = new Uint8Array(textureLength);
356
+ this._partIndices.set(partIndicesA.subarray(0, splatCountA));
357
+ const assignedPartIndices = [];
358
+ let dstOffset = splatCountA;
359
+ const maxPartCount = GetGaussianSplattingMaxPartCount(this._scene.getEngine());
360
+ for (const other of others) {
361
+ if (nextPartIndex >= maxPartCount) {
362
+ throw new Error(`Cannot add part, as the maximum part count (${maxPartCount}) has been reached`);
363
+ }
364
+ const newPartIndex = nextPartIndex++;
365
+ assignedPartIndices.push(newPartIndex);
366
+ this._partIndices.fill(newPartIndex, dstOffset, dstOffset + other._vertexCount);
367
+ dstOffset += other._vertexCount;
368
+ }
369
+ // --- Process source data ---
370
+ if (!incremental) {
371
+ // Full rebuild path — only reached when the GPU texture must be reallocated
372
+ // (either the texture height needs to grow to fit the new total, or this is
373
+ // the very first addPart onto a mesh with no GPU textures yet). In the common
374
+ // case where the texture height is unchanged, `incremental` is true and this
375
+ // entire block is skipped. The `splatCountA > 0` guard avoids redundant work
376
+ // on the first-ever addPart when the compound mesh starts empty.
377
+ if (splatCountA > 0) {
378
+ if (this._partProxies.length > 0) {
379
+ // Already compound: rebuild every existing part from its stored source data.
380
+ //
381
+ // DESIGN NOTE: The intended use of GaussianSplattingMesh / GaussianSplattingCompoundMesh
382
+ // in compound mode is to start EMPTY and compose parts exclusively via addPart/addParts.
383
+ // In a future major version this will be the only supported path and the "own data"
384
+ // legacy branch below will be removed.
385
+ //
386
+ // Until then, two layouts are possible:
387
+ // A) LEGACY — compound loaded its own splat data (via URL or updateData) before
388
+ // any addPart call. _partProxies[0] is undefined; the mesh's own splat data
389
+ // is treated as an implicit "part 0" in this._splatsData. Proxied parts occupy
390
+ // indices 1+. This layout will be deprecated in the next major version.
391
+ // B) PREFERRED — compound started empty; first addPart assigned partIndex=0.
392
+ // _partProxies[0] is set; this._splatsData is null; all parts are proxied.
393
+ let rebuildOffset = 0;
394
+ // Rebuild the compound's legacy "own" data at part 0 (scenario A only).
395
+ // Skipped in the preferred empty-composer path (scenario B).
396
+ if (!this._partProxies[0] && this._splatsData) {
397
+ const proxyVertexCount = this._partProxies.reduce((sum, proxy) => sum + (proxy ? proxy.proxiedMesh._vertexCount : 0), 0);
398
+ const part0Count = splatCountA - proxyVertexCount;
399
+ if (part0Count > 0) {
400
+ const uBufA = new Uint8Array(this._splatsData);
401
+ const fBufA = new Float32Array(this._splatsData);
402
+ for (let i = 0; i < part0Count; i++) {
403
+ this._makeSplat(i, fBufA, uBufA, covA, covB, colorArray, minimum, maximum, false);
404
+ }
405
+ if (sh && this._shData) {
406
+ const bytesPerTexel = 16;
407
+ for (let texIdx = 0; texIdx < sh.length; texIdx++) {
408
+ if (texIdx < this._shData.length) {
409
+ sh[texIdx].set(this._shData[texIdx].subarray(0, part0Count * bytesPerTexel), 0);
410
+ }
411
+ }
412
+ }
413
+ rebuildOffset += part0Count;
414
+ }
415
+ }
416
+ // Rebuild all proxied parts. Loop from index 0 because in the preferred
417
+ // scenario B, part 0 is itself a proxied part with no implicit "own" data.
418
+ for (let partIndex = 0; partIndex < this._partProxies.length; partIndex++) {
419
+ const proxy = this._partProxies[partIndex];
420
+ if (!proxy || !proxy.proxiedMesh) {
421
+ continue;
422
+ }
423
+ this._appendSourceToArrays(proxy.proxiedMesh, rebuildOffset, covA, covB, colorArray, sh, minimum, maximum);
424
+ rebuildOffset += proxy.proxiedMesh._vertexCount;
425
+ }
426
+ }
427
+ else {
428
+ // No proxies yet: this is the very first addPart call on a mesh that loaded
429
+ // its own splat data (scenario A legacy path). Re-process that own data so
430
+ // it occupies the start of the new texture before the incoming part is appended.
431
+ // In the preferred scenario B (empty composer) splatCountA is 0 and this
432
+ // entire branch is skipped by the outer `if (splatCountA > 0)` guard.
433
+ if (this._splatsData) {
434
+ const uBufA = new Uint8Array(this._splatsData);
435
+ const fBufA = new Float32Array(this._splatsData);
436
+ for (let i = 0; i < splatCountA; i++) {
437
+ this._makeSplat(i, fBufA, uBufA, covA, covB, colorArray, minimum, maximum, false);
438
+ }
439
+ if (sh && this._shData) {
440
+ const bytesPerTexel = 16;
441
+ for (let texIdx = 0; texIdx < sh.length; texIdx++) {
442
+ if (texIdx < this._shData.length) {
443
+ sh[texIdx].set(this._shData[texIdx].subarray(0, splatCountA * bytesPerTexel), 0);
444
+ }
445
+ }
446
+ }
447
+ }
1687
448
  }
1688
449
  }
1689
- if (this._delayedTextureUpdate) {
1690
- const textureSize = this._getTextureSize(vertexCountPadded);
1691
- this._updateSubTextures(this._delayedTextureUpdate.centers, this._delayedTextureUpdate.covA, this._delayedTextureUpdate.covB, this._delayedTextureUpdate.colors, 0, textureSize.y, this._delayedTextureUpdate.sh, this._delayedTextureUpdate.partIndices);
1692
- this._delayedTextureUpdate = null;
1693
- }
1694
- // get mesh for camera and update its instance buffer
1695
- const cameraViewInfos = this._cameraViewInfos.get(cameraId);
1696
- if (cameraViewInfos) {
1697
- if (cameraViewInfos.splatIndexBufferSet) {
1698
- cameraViewInfos.mesh.thinInstanceBufferUpdated("splatIndex");
450
+ }
451
+ // Incremental path: rebuild the partial first row (indices firstNewTexel to splatCountA-1)
452
+ // so _updateSubTextures does not upload stale zeros over those already-committed texels.
453
+ // The base-class _updateData always re-processes from firstNewTexel for the same reason;
454
+ // the compound path must do the same.
455
+ if (incremental) {
456
+ const firstNewTexel = firstNewLine * textureSize.x;
457
+ if (firstNewTexel < splatCountA) {
458
+ if (this._partProxies.length === 0) {
459
+ // No proxies: the mesh loaded its own splat data and this is the first
460
+ // addPart call (scenario A legacy path). Re-process the partial boundary
461
+ // row so it is not clobbered by stale zeros during the sub-texture upload.
462
+ if (this._splatsData) {
463
+ const uBufA = new Uint8Array(this._splatsData);
464
+ const fBufA = new Float32Array(this._splatsData);
465
+ for (let i = firstNewTexel; i < splatCountA; i++) {
466
+ this._makeSplat(i, fBufA, uBufA, covA, covB, colorArray, minimum, maximum, false, i);
467
+ }
468
+ }
1699
469
  }
1700
470
  else {
1701
- cameraViewInfos.mesh.thinInstanceSetBuffer("splatIndex", this._splatIndex, 16, false);
1702
- cameraViewInfos.splatIndexBufferSet = true;
471
+ // Already compound: build a per-partIndex source lookup so each splat in the
472
+ // partial boundary row can be re-processed from its original source buffer.
473
+ //
474
+ // Handles both layouts (see full-rebuild comment above):
475
+ // A) LEGACY: _partProxies[0] absent → seed lookup[0] with this._splatsData
476
+ // B) PREFERRED: _partProxies[0] present → all entries filled from proxies
477
+ const proxyTotal = this._partProxies.reduce((s, p) => s + (p ? p.proxiedMesh._vertexCount : 0), 0);
478
+ const part0Count = splatCountA - proxyTotal; // > 0 only in legacy scenario A
479
+ const srcUBufs = new Array(this._partProxies.length).fill(null);
480
+ const srcFBufs = new Array(this._partProxies.length).fill(null);
481
+ const partStarts = new Array(this._partProxies.length).fill(0);
482
+ // Legacy scenario A: part 0 is the mesh's own loaded data.
483
+ if (!this._partProxies[0] && this._splatsData && part0Count > 0) {
484
+ srcUBufs[0] = new Uint8Array(this._splatsData);
485
+ srcFBufs[0] = new Float32Array(this._splatsData);
486
+ partStarts[0] = 0;
487
+ }
488
+ // All proxied parts — start from pi=0 to cover preferred scenario B.
489
+ let cumOffset = part0Count;
490
+ for (let pi = 0; pi < this._partProxies.length; pi++) {
491
+ const proxy = this._partProxies[pi];
492
+ if (!proxy?.proxiedMesh) {
493
+ continue;
494
+ }
495
+ const srcData = proxy.proxiedMesh._splatsData ?? null;
496
+ srcUBufs[pi] = srcData ? new Uint8Array(srcData) : null;
497
+ srcFBufs[pi] = srcData ? new Float32Array(srcData) : null;
498
+ partStarts[pi] = cumOffset;
499
+ cumOffset += proxy.proxiedMesh._vertexCount;
500
+ }
501
+ for (let splatIdx = firstNewTexel; splatIdx < splatCountA; splatIdx++) {
502
+ const partIdx = this._partIndices ? this._partIndices[splatIdx] : 0;
503
+ const uBuf = partIdx < srcUBufs.length ? srcUBufs[partIdx] : null;
504
+ const fBuf = partIdx < srcFBufs.length ? srcFBufs[partIdx] : null;
505
+ if (uBuf && fBuf) {
506
+ this._makeSplat(splatIdx, fBuf, uBuf, covA, covB, colorArray, minimum, maximum, false, splatIdx - (partStarts[partIdx] ?? 0));
507
+ }
508
+ }
1703
509
  }
1704
510
  }
1705
- this._canPostToWorker = true;
1706
- this._readyToDisplay = true;
1707
- // sort is dirty when GS is visible for progressive update with a this message arriving but positions were partially filled
1708
- // another update needs to be kicked. The kick can't happen just when the position buffer is ready because _canPostToWorker might be false.
1709
- if (this._sortIsDirty) {
1710
- this._postToWorker(true);
1711
- this._sortIsDirty = false;
1712
- }
1713
- };
1714
- }
1715
- _getTextureSize(length) {
1716
- const engine = this._scene.getEngine();
1717
- const width = engine.getCaps().maxTextureSize;
1718
- let height = 1;
1719
- if (engine.version === 1 && !engine.isWebGPU) {
1720
- while (width * height < length) {
1721
- height *= 2;
1722
- }
1723
511
  }
1724
- else {
1725
- height = Math.ceil(length / width);
1726
- }
1727
- if (height > width) {
1728
- Logger.Error("GaussianSplatting texture size: (" + width + ", " + height + "), maxTextureSize: " + width);
1729
- height = width;
1730
- }
1731
- return new Vector2(width, height);
1732
- }
1733
- /**
1734
- * Gets the number of parts in the compound
1735
- * @returns the number of parts in the compound, or 0 if the mesh is not a compound
1736
- */
1737
- get partCount() {
1738
- return this._partMatrices.length;
1739
- }
1740
- /**
1741
- * Sets the world matrix for a specific part of the compound (if this mesh is a compound).
1742
- * This will trigger a re-sort of the mesh.
1743
- * @param partIndex index of the part, that must be between 0 and partCount - 1
1744
- * @param worldMatrix the world matrix to set
1745
- */
1746
- setWorldMatrixForPart(partIndex, worldMatrix) {
1747
- this._partMatrices[partIndex].copyFrom(worldMatrix);
1748
- if (this._worker) {
1749
- this._worker.postMessage({ partMatrices: this._partMatrices.map((matrix) => new Float32Array(matrix.m)) });
512
+ // Append each new source
513
+ dstOffset = splatCountA;
514
+ for (const other of others) {
515
+ this._appendSourceToArrays(other, dstOffset, covA, covB, colorArray, sh, minimum, maximum);
516
+ dstOffset += other._vertexCount;
1750
517
  }
1751
- this._postToWorker(true);
1752
- }
1753
- /**
1754
- * Gets the world matrix for a specific part of the compound (if this mesh is a compound).
1755
- * @param partIndex index of the part, that must be between 0 and partCount - 1
1756
- * @returns the world matrix for the part, or the current world matrix of the mesh if the mesh is not a compound
1757
- */
1758
- getWorldMatrixForPart(partIndex) {
1759
- return this._partMatrices[partIndex] ?? this.getWorldMatrix();
1760
- }
1761
- /**
1762
- * Gets the visibility for a specific part of the compound (if this mesh is a compound).
1763
- * @param partIndex index of the part, that must be between 0 and partCount - 1
1764
- * @returns the visibility value (0.0 to 1.0) for the part
1765
- */
1766
- getPartVisibility(partIndex) {
1767
- return this._partVisibility[partIndex] ?? 1.0;
1768
- }
1769
- /**
1770
- * Sets the visibility for a specific part of the compound (if this mesh is a compound).
1771
- * @param partIndex index of the part, that must be between 0 and partCount - 1
1772
- * @param value the visibility value (0.0 to 1.0) to set
1773
- */
1774
- setPartVisibility(partIndex, value) {
1775
- this._partVisibility[partIndex] = Math.max(0.0, Math.min(1.0, value));
1776
- }
1777
- /**
1778
- * Ensure that the part world matrix array is at least the given length.
1779
- * NB: This length is used as reference for the number of parts in the compound.
1780
- * Newly inserted parts are initialized with the current world matrix of the mesh.
1781
- * @param length - The minimum length to ensure
1782
- */
1783
- _ensureMinimumPartMatricesLength(length) {
1784
- if (this._partMatrices.length < length) {
1785
- this._resizePartMatrices(length);
518
+ // Pad empty splats to texture boundary
519
+ const paddedEnd = (totalCount + 15) & ~0xf;
520
+ for (let i = totalCount; i < paddedEnd; i++) {
521
+ this._makeEmptySplat(i, covA, covB, colorArray);
1786
522
  }
1787
- }
1788
- /**
1789
- * This sets the number of parts in the compound.
1790
- * Warning: This must be consistent with the indices used in the partIndices texture.
1791
- * Newly inserted parts are initialized with the current world matrix of the mesh.
1792
- * @param length - The length to resize to
1793
- */
1794
- _resizePartMatrices(length) {
1795
- if (this._partMatrices.length == length) {
1796
- return;
523
+ // --- Update vertex count / index buffer ---
524
+ if (totalCount !== this._vertexCount) {
525
+ this._updateSplatIndexBuffer(totalCount);
1797
526
  }
1798
- else if (this._partMatrices.length > length) {
1799
- this._partMatrices = this._partMatrices.slice(0, length);
1800
- this._partVisibility = this._partVisibility.slice(0, length);
527
+ this._vertexCount = totalCount;
528
+ this._shDegree = shDegreeNew;
529
+ // --- Upload to GPU ---
530
+ if (incremental) {
531
+ // Update the part-indices texture (handles both create and update-in-place).
532
+ // _ensurePartIndicesTexture is a no-op when the texture already exists, so on the
533
+ // second+ addPart the partIndices would be stale without this call.
534
+ this._onUpdateTextures(textureSize);
535
+ this._updateSubTextures(this._splatPositions, covA, covB, colorArray, firstNewLine, textureSize.y - firstNewLine, sh);
1801
536
  }
1802
537
  else {
1803
- this.computeWorldMatrix(true);
1804
- const defaultMatrix = this.getWorldMatrix();
1805
- while (this._partMatrices.length < length) {
1806
- this._partMatrices.push(defaultMatrix.clone());
1807
- this._partVisibility.push(1.0);
1808
- }
1809
- }
1810
- if (this._worker) {
1811
- this._worker.postMessage({ partMatrices: this._partMatrices.map((matrix) => new Float32Array(matrix.m)) });
1812
- }
1813
- this._postToWorker(true);
1814
- }
538
+ this._updateTextures(covA, covB, colorArray, sh);
539
+ }
540
+ this.getBoundingInfo().reConstruct(minimum, maximum, this.getWorldMatrix());
541
+ this.setEnabled(true);
542
+ this._cachedBoundingMin = minimum.clone();
543
+ this._cachedBoundingMax = maximum.clone();
544
+ this._notifyWorkerNewData();
545
+ // --- Create proxy meshes ---
546
+ const proxyMeshes = [];
547
+ for (let i = 0; i < others.length; i++) {
548
+ const other = others[i];
549
+ const newPartIndex = assignedPartIndices[i];
550
+ const partWorldMatrix = other.getWorldMatrix();
551
+ this.setWorldMatrixForPart(newPartIndex, partWorldMatrix);
552
+ const proxyMesh = new GaussianSplattingPartProxyMesh(other.name, this.getScene(), this, other, newPartIndex);
553
+ if (disposeOthers) {
554
+ other.dispose();
555
+ }
556
+ const quaternion = new Quaternion();
557
+ partWorldMatrix.decompose(proxyMesh.scaling, quaternion, proxyMesh.position);
558
+ proxyMesh.rotationQuaternion = quaternion;
559
+ proxyMesh.computeWorldMatrix(true);
560
+ this._partProxies[newPartIndex] = proxyMesh;
561
+ proxyMeshes.push(proxyMesh);
562
+ }
563
+ return { proxyMeshes, assignedPartIndices };
564
+ }
565
+ // ---------------------------------------------------------------------------
566
+ // Public compound API
567
+ // ---------------------------------------------------------------------------
1815
568
  /**
1816
569
  * Add another mesh to this mesh, as a new part. This makes the current mesh a compound, if not already.
1817
- * NB: The current mesh needs to be loaded with keepInRam: true.
1818
- * @param other - The other mesh to add. This must be loaded with keepInRam: true.
570
+ * The source mesh's splat data is read directly no merged CPU buffer is constructed.
571
+ * @param other - The other mesh to add. Must be fully loaded before calling this method.
1819
572
  * @param disposeOther - Whether to dispose the other mesh after adding it to the current mesh.
1820
573
  * @returns a placeholder mesh that can be used to manipulate the part transform
574
+ * @deprecated Use {@link GaussianSplattingCompoundMesh.addPart} instead.
1821
575
  */
1822
576
  addPart(other, disposeOther = true) {
1823
- const maxPartCount = GetGaussianSplattingMaxPartCount(this._scene.getEngine());
1824
- if (this.partCount >= maxPartCount) {
1825
- throw new Error(`Cannot add part, as the maximum part count (${maxPartCount}) has been reached`);
1826
- }
1827
- const splatCountA = this._vertexCount;
1828
- const splatsDataA = splatCountA == 0 ? new ArrayBuffer(0) : this.splatsData;
1829
- const shDataA = this.shData;
1830
- const splatCountB = other._vertexCount;
1831
- const splatsDataB = other.splatsData;
1832
- const shDataB = other.shData;
1833
- const mergedShDataLength = Math.max(shDataA?.length || 0, shDataB?.length || 0);
1834
- const hasMergedShData = shDataA !== null && shDataB !== null;
1835
- // Sanity checks
1836
- if (!splatsDataA) {
1837
- throw new Error(`To call addPart(), the current mesh must be loaded with keepInRam: true`);
1838
- }
1839
- const expectedSplatsDataSizeA = splatCountA * GaussianSplattingMesh._RowOutputLength;
1840
- if (splatsDataA.byteLength !== expectedSplatsDataSizeA) {
1841
- throw new Error(`splatsDataA size (${splatsDataA.byteLength}) does not match expected size (${expectedSplatsDataSizeA})`);
1842
- }
1843
- if (!splatsDataB) {
1844
- throw new Error(`To call addPart(), the other mesh must be loaded with keepInRam: true`);
1845
- }
1846
- const expectedSplatsDataSizeB = splatCountB * GaussianSplattingMesh._RowOutputLength;
1847
- if (splatsDataB.byteLength !== expectedSplatsDataSizeB) {
1848
- throw new Error(`splatsDataB size (${splatsDataB.byteLength}) does not match expected size (${expectedSplatsDataSizeB})`);
1849
- }
1850
- if (other.partIndices) {
1851
- throw new Error(`To call addPart(), the other mesh must not be a compound`);
1852
- }
1853
- // Concatenate splatsData (ArrayBuffer)
1854
- const mergedSplatsData = new Uint8Array(splatsDataA.byteLength + splatsDataB.byteLength);
1855
- mergedSplatsData.set(new Uint8Array(splatsDataA), 0);
1856
- mergedSplatsData.set(new Uint8Array(splatsDataB), splatsDataA.byteLength);
1857
- let mergedShData = undefined;
1858
- if (hasMergedShData) {
1859
- // Note: We need to calculate the texture size and pad accordingly
1860
- // Each SH texture texel stores 16 bytes (4 RGBA uint32 components)
1861
- const bytesPerTexel = 16;
1862
- const totalSplatCount = splatCountA + splatCountB;
1863
- mergedShData = [];
1864
- for (let i = 0; i < mergedShDataLength; i++) {
1865
- const mergedShDataItem = new Uint8Array(totalSplatCount * bytesPerTexel);
1866
- if (i < (shDataA?.length ?? 0)) {
1867
- mergedShDataItem.set(shDataA[i], 0);
1868
- }
1869
- if (i < (shDataB?.length ?? 0)) {
1870
- const byteOffset = bytesPerTexel * splatCountA;
1871
- mergedShDataItem.set(shDataB[i], byteOffset);
1872
- }
1873
- mergedShData.push(mergedShDataItem);
1874
- }
1875
- }
1876
- // Concatenate partIndices (Uint8Array)
1877
- let newPartIndex = this.partCount;
1878
- let partIndicesA = this.partIndices;
1879
- if (!partIndicesA) {
1880
- partIndicesA = new Uint8Array(splatCountA);
1881
- newPartIndex = splatCountA > 0 ? 1 : 0;
1882
- //newPartIndex = 1;
1883
- }
1884
- if (partIndicesA.length < splatCountA) {
1885
- throw new Error(`partIndices length (${partIndicesA.length}) should be at least vertexCount (${splatCountA}) in the current mesh`);
1886
- }
1887
- const partIndicesB = new Uint8Array(splatCountB).fill(newPartIndex);
1888
- const mergedPartIndices = new Uint8Array(splatCountA + splatCountB);
1889
- mergedPartIndices.set(partIndicesA.slice(0, splatCountA), 0);
1890
- mergedPartIndices.set(partIndicesB, splatCountA);
1891
- this.updateData(mergedSplatsData.buffer, mergedShData, { flipY: false }, mergedPartIndices);
1892
- // Merge part matrices (TODO)
1893
- const partWorldMatrix = other.getWorldMatrix();
1894
- this.setWorldMatrixForPart(newPartIndex, partWorldMatrix);
1895
- // Create a proxy mesh to manipulate the part transform
1896
- const proxyMesh = new GaussianSplattingPartProxyMesh(other.name, this.getScene(), this, other, newPartIndex);
1897
- if (disposeOther) {
1898
- other.dispose();
1899
- }
1900
- // Set the initial world matrix
1901
- const quaternion = new Quaternion();
1902
- partWorldMatrix.decompose(proxyMesh.scaling, quaternion, proxyMesh.position);
1903
- proxyMesh.rotationQuaternion = quaternion;
1904
- proxyMesh.computeWorldMatrix(true);
1905
- // Store the proxy in the map
1906
- this._partProxies.set(newPartIndex, proxyMesh);
1907
- return proxyMesh;
577
+ const { proxyMeshes } = this._addPartsInternal([other], disposeOther);
578
+ return proxyMeshes[0];
1908
579
  }
1909
580
  /**
1910
581
  * Remove a part from this compound mesh.
582
+ * The remaining parts are rebuilt directly from their stored source mesh references —
583
+ * no merged CPU splat buffer is read back. The current mesh is reset to a plain (single-part)
584
+ * state and then each remaining source is re-added via addParts.
1911
585
  * @param index - The index of the part to remove
586
+ * @deprecated Use {@link GaussianSplattingCompoundMesh.removePart} instead.
1912
587
  */
1913
588
  removePart(index) {
1914
589
  if (index < 0 || index >= this.partCount) {
1915
590
  throw new Error(`Part index ${index} is out of range [0, ${this.partCount})`);
1916
591
  }
1917
- // Get the current data
1918
- const splatsData = this.splatsData;
1919
- const shData = this.shData;
1920
- const partIndices = this.partIndices;
1921
- if (!splatsData || !partIndices) {
1922
- throw new Error("Cannot remove part from a non-compound mesh or mesh without keepInRam");
1923
- }
1924
- const splatCount = this._vertexCount;
1925
- const rowLength = GaussianSplattingMesh._RowOutputLength;
1926
- // Count splats that will remain (not in the removed part)
1927
- let newSplatCount = 0;
1928
- for (let i = 0; i < splatCount; i++) {
1929
- if (partIndices[i] !== index) {
1930
- newSplatCount++;
592
+ // Collect surviving proxy objects (sorted by current part index so part 0 is added first)
593
+ const survivors = [];
594
+ for (let proxyIndex = 0; proxyIndex < this._partProxies.length; proxyIndex++) {
595
+ const proxy = this._partProxies[proxyIndex];
596
+ if (proxy && proxyIndex !== index) {
597
+ survivors.push({ proxyMesh: proxy, oldIndex: proxyIndex, worldMatrix: proxy.getWorldMatrix().clone(), visibility: this._partVisibility[proxyIndex] ?? 1.0 });
1931
598
  }
1932
599
  }
1933
- // Build new splats data excluding the removed part
1934
- const newSplatsData = new Uint8Array(newSplatCount * rowLength);
1935
- const newPartIndices = new Uint8Array(newSplatCount);
1936
- let newShData = undefined;
1937
- if (shData) {
1938
- const bytesPerTexel = 16;
1939
- newShData = [];
1940
- for (let i = 0; i < shData.length; i++) {
1941
- newShData.push(new Uint8Array(newSplatCount * bytesPerTexel));
600
+ survivors.sort((a, b) => a.oldIndex - b.oldIndex);
601
+ // Validate every survivor still has its source data. If even one is missing we cannot rebuild.
602
+ for (const { proxyMesh } of survivors) {
603
+ if (!proxyMesh.proxiedMesh._splatsData) {
604
+ throw new Error(`Cannot remove part: the source mesh for part "${proxyMesh.name}" no longer has its splat data available.`);
1942
605
  }
1943
606
  }
1944
- let writeIndex = 0;
1945
- for (let readIndex = 0; readIndex < splatCount; readIndex++) {
1946
- const currentPartIndex = partIndices[readIndex];
1947
- if (currentPartIndex === index) {
1948
- // Skip splats from the removed part
1949
- continue;
1950
- }
1951
- // Copy splat data
1952
- const srcOffset = readIndex * rowLength;
1953
- const dstOffset = writeIndex * rowLength;
1954
- newSplatsData.set(new Uint8Array(splatsData, srcOffset, rowLength), dstOffset);
1955
- // Renumber part indices: indices > removed index get decremented
1956
- newPartIndices[writeIndex] = currentPartIndex > index ? currentPartIndex - 1 : currentPartIndex;
1957
- // Copy SH data if present
1958
- if (shData && newShData) {
1959
- const bytesPerTexel = 16;
1960
- for (let shIndex = 0; shIndex < shData.length; shIndex++) {
1961
- const srcShOffset = readIndex * bytesPerTexel;
1962
- const dstShOffset = writeIndex * bytesPerTexel;
1963
- newShData[shIndex].set(new Uint8Array(shData[shIndex].buffer, srcShOffset, bytesPerTexel), dstShOffset);
1964
- }
1965
- }
1966
- writeIndex++;
1967
- }
1968
- // Remove the part matrix and visibility
1969
- this._partMatrices.splice(index, 1);
1970
- this._partVisibility.splice(index, 1);
1971
- // Update worker with new part matrices
607
+ // --- Reset this mesh to an empty state ---
608
+ // Terminate the sort worker before zeroing _vertexCount. The worker's onmessage handler
609
+ // compares depthMix.length against (_vertexCount + 15) & ~0xf; with _vertexCount = 0 that
610
+ // becomes 16, which causes a forced re-sort loop on stale data and resets _canPostToWorker
611
+ // to true, defeating the gate below. The worker will be re-instantiated naturally after
612
+ // the rebuild via the first _postToWorker call.
1972
613
  if (this._worker) {
1973
- this._worker.postMessage({ partMatrices: this._partMatrices.map((matrix) => new Float32Array(matrix.m)) });
1974
- }
1975
- // Update the mesh with the new data
1976
- this.updateData(newSplatsData.buffer, newShData, { flipY: false }, newPartIndices);
1977
- // Dispose and remove the proxy for the removed part
1978
- const proxyToRemove = this._partProxies.get(index);
1979
- if (proxyToRemove) {
1980
- proxyToRemove.dispose();
1981
- this._partProxies.delete(index);
614
+ this._worker.terminate();
615
+ this._worker = null;
1982
616
  }
1983
- // Update the proxy map: renumber proxies with index > removed index
1984
- const proxiesToUpdate = [];
1985
- this._partProxies.forEach((proxy, proxyIndex) => {
1986
- if (proxyIndex > index) {
1987
- proxiesToUpdate.push([proxyIndex, proxy]);
617
+ // Dispose and null GPU textures so _updateTextures sees firstTime=true and creates
618
+ // fresh GPU textures.
619
+ this._covariancesATexture?.dispose();
620
+ this._covariancesBTexture?.dispose();
621
+ this._centersTexture?.dispose();
622
+ this._colorsTexture?.dispose();
623
+ this._covariancesATexture = null;
624
+ this._covariancesBTexture = null;
625
+ this._centersTexture = null;
626
+ this._colorsTexture = null;
627
+ if (this._shTextures) {
628
+ for (const t of this._shTextures) {
629
+ t.dispose();
1988
630
  }
1989
- });
1990
- // Remove and re-add with updated indices
1991
- for (const [oldIndex, proxy] of proxiesToUpdate) {
1992
- this._partProxies.delete(oldIndex);
1993
- // Update the proxy's internal partIndex
1994
- proxy.updatePartIndex(oldIndex - 1);
1995
- this._partProxies.set(oldIndex - 1, proxy);
631
+ this._shTextures = null;
1996
632
  }
1997
- }
1998
- /**
1999
- * Modifies the splats according to the passed transformation matrix.
2000
- * @param transform defines the transform matrix to use
2001
- * @returns the current mesh
2002
- */
2003
- bakeTransformIntoVertices(transform) {
2004
- const arrayBuffer = this.splatsData;
2005
- if (!arrayBuffer) {
2006
- Logger.Error("Cannot bake transform into vertices if splatsData is not kept in RAM");
2007
- return this;
633
+ if (this._partIndicesTexture) {
634
+ this._partIndicesTexture.dispose();
635
+ this._partIndicesTexture = null;
2008
636
  }
2009
- // Check for uniform scaling
2010
- const m = transform.m;
2011
- const scaleX = Math.sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]);
2012
- const scaleY = Math.sqrt(m[4] * m[4] + m[5] * m[5] + m[6] * m[6]);
2013
- const scaleZ = Math.sqrt(m[8] * m[8] + m[9] * m[9] + m[10] * m[10]);
2014
- const epsilon = 0.001;
2015
- if (Math.abs(scaleX - scaleY) > epsilon || Math.abs(scaleX - scaleZ) > epsilon) {
2016
- Logger.Error("Gaussian Splatting bakeTransformIntoVertices does not support non-uniform scaling");
2017
- return this;
637
+ this._vertexCount = 0;
638
+ this._splatPositions = null;
639
+ this._partIndices = null;
640
+ this._partMatrices = [];
641
+ this._partVisibility = [];
642
+ this._cachedBoundingMin = null;
643
+ this._cachedBoundingMax = null;
644
+ // Remove the proxy for the removed part and dispose it
645
+ const proxyToRemove = this._partProxies[index];
646
+ if (proxyToRemove) {
647
+ proxyToRemove.dispose();
2018
648
  }
2019
- const uBuffer = new Uint8Array(arrayBuffer);
2020
- const fBuffer = new Float32Array(arrayBuffer);
2021
- const temp = TmpVectors.Vector3[0];
2022
- let index;
2023
- const quaternion = TmpVectors.Quaternion[0];
2024
- const transformedQuaternion = TmpVectors.Quaternion[1];
2025
- transform.decompose(temp, transformedQuaternion, temp);
2026
- for (index = 0; index < this._vertexCount; index++) {
2027
- const floatIndex = index * 8; // 8 floats per splat (center.x, center.y, center.z, scale.x, scale.y, scale.z, ...)
2028
- Vector3.TransformCoordinatesFromFloatsToRef(fBuffer[floatIndex], fBuffer[floatIndex + 1], fBuffer[floatIndex + 2], transform, temp);
2029
- fBuffer[floatIndex] = temp.x;
2030
- fBuffer[floatIndex + 1] = temp.y;
2031
- fBuffer[floatIndex + 2] = temp.z;
2032
- // Apply uniform scaling to splat scales
2033
- fBuffer[floatIndex + 3] *= scaleX;
2034
- fBuffer[floatIndex + 4] *= scaleX;
2035
- fBuffer[floatIndex + 5] *= scaleX;
2036
- // Unpack quaternion from uint8array (matching _GetSplat packing convention)
2037
- quaternion.set((uBuffer[32 * index + 28 + 1] - 127.5) / 127.5, (uBuffer[32 * index + 28 + 2] - 127.5) / 127.5, (uBuffer[32 * index + 28 + 3] - 127.5) / 127.5, (uBuffer[32 * index + 28 + 0] - 127.5) / 127.5);
2038
- quaternion.normalize();
2039
- // If there is a negative scaling, we need to flip the quaternion to keep the correct handedness
2040
- if (this.scaling.x < 0) {
2041
- quaternion.x = -quaternion.x;
2042
- quaternion.w = -quaternion.w;
2043
- }
2044
- if (this.scaling.y < 0) {
2045
- quaternion.y = -quaternion.y;
2046
- quaternion.w = -quaternion.w;
2047
- }
2048
- if (this.scaling.z < 0) {
2049
- quaternion.z = -quaternion.z;
2050
- quaternion.w = -quaternion.w;
2051
- }
2052
- // Transform the quaternion
2053
- transformedQuaternion.multiplyToRef(quaternion, quaternion);
2054
- quaternion.normalize();
2055
- // Pack quaternion back to uint8array (matching _GetSplat packing convention)
2056
- uBuffer[32 * index + 28 + 0] = Math.round(quaternion.w * 127.5 + 127.5);
2057
- uBuffer[32 * index + 28 + 1] = Math.round(quaternion.x * 127.5 + 127.5);
2058
- uBuffer[32 * index + 28 + 2] = Math.round(quaternion.y * 127.5 + 127.5);
2059
- uBuffer[32 * index + 28 + 3] = Math.round(quaternion.z * 127.5 + 127.5);
649
+ this._partProxies = [];
650
+ // Rebuild from surviving sources. _addPartsInternal assigns part indices in order 0, 1, 2, …
651
+ // so the new index for each survivor is simply its position in the survivors array.
652
+ if (survivors.length === 0) {
653
+ // Nothing left — leave the mesh empty.
654
+ this.setEnabled(false);
655
+ return;
2060
656
  }
2061
- this.updateData(arrayBuffer, this.shData ?? undefined, { flipY: false });
2062
- return this;
657
+ // Gate the sort worker: suppress any sort request until the full rebuild is committed.
658
+ this._rebuilding = true;
659
+ this._canPostToWorker = false;
660
+ const sources = survivors.map((s) => s.proxyMesh.proxiedMesh);
661
+ const { proxyMeshes: newProxies } = this._addPartsInternal(sources, false);
662
+ // Restore world matrices and re-map proxies
663
+ for (let i = 0; i < survivors.length; i++) {
664
+ const oldProxy = survivors[i].proxyMesh;
665
+ const newProxy = newProxies[i];
666
+ const newPartIndex = newProxy.partIndex;
667
+ // Restore the world matrix and visibility the user had set on the old proxy
668
+ this.setWorldMatrixForPart(newPartIndex, survivors[i].worldMatrix);
669
+ this.setPartVisibility(newPartIndex, survivors[i].visibility);
670
+ const quaternion = new Quaternion();
671
+ survivors[i].worldMatrix.decompose(newProxy.scaling, quaternion, newProxy.position);
672
+ newProxy.rotationQuaternion = quaternion;
673
+ newProxy.computeWorldMatrix(true);
674
+ // Update the old proxy's index so any existing user references still work
675
+ oldProxy.updatePartIndex(newPartIndex);
676
+ this._partProxies[newPartIndex] = oldProxy;
677
+ // newProxy is redundant — it was created inside _addPartsInternal; dispose it
678
+ newProxy.dispose();
679
+ }
680
+ // Rebuild is complete: all partMatrices are now set correctly.
681
+ // Post the final complete set and fire one sort.
682
+ this._rebuilding = false;
683
+ // Break TypeScript's flow narrowing — _addPartsInternal may have reinstantiated _worker.
684
+ const workerAfterRebuild = this._worker;
685
+ workerAfterRebuild?.postMessage({ partMatrices: this._partMatrices.map((matrix) => new Float32Array(matrix.m)) });
686
+ this._canPostToWorker = true;
687
+ this._postToWorker(true);
2063
688
  }
2064
689
  }
2065
- GaussianSplattingMesh._RowOutputLength = 3 * 4 + 3 * 4 + 4 + 4; // Vector3 position, Vector3 scale, 1 u8 quaternion, 1 color with alpha
2066
- GaussianSplattingMesh._SH_C0 = 0.28209479177387814;
2067
- // batch size between 2 yield calls. This value is a tradeoff between updates overhead and framerate hiccups
2068
- // This step is faster the PLY conversion. So batch size can be bigger
2069
- GaussianSplattingMesh._SplatBatchSize = 327680;
2070
- // batch size between 2 yield calls during the PLY to splat conversion.
2071
- GaussianSplattingMesh._PlyConversionBatchSize = 32768;
2072
- GaussianSplattingMesh._BatchSize = 16; // 16 splats per instance
2073
- GaussianSplattingMesh._DefaultViewUpdateThreshold = 1e-4;
2074
- /**
2075
- * Set the number of batch (a batch is 16384 splats) after which a display update is performed
2076
- * A value of 0 (default) means display update will not happens before splat is ready.
2077
- */
2078
- GaussianSplattingMesh.ProgressiveUpdateAmount = 0;
2079
- GaussianSplattingMesh._CreateWorker = function (self) {
2080
- let positions;
2081
- let depthMix;
2082
- let indices;
2083
- let floatMix;
2084
- let partIndices;
2085
- let partMatrices;
2086
- self.onmessage = (e) => {
2087
- // updated on init
2088
- if (e.data.positions) {
2089
- positions = e.data.positions;
2090
- }
2091
- // update on rig node changed
2092
- else if (e.data.partMatrices) {
2093
- partMatrices = e.data.partMatrices;
2094
- }
2095
- // update on rig node indices changed
2096
- else if (e.data.partIndices !== undefined) {
2097
- partIndices = e.data.partIndices;
2098
- }
2099
- // update on view changed
2100
- else {
2101
- const cameraId = e.data.cameraId;
2102
- const globalWorldMatrix = e.data.worldMatrix;
2103
- const cameraForward = e.data.cameraForward;
2104
- const cameraPosition = e.data.cameraPosition;
2105
- const vertexCountPadded = (positions.length / 4 + 15) & ~0xf;
2106
- if (!positions || !cameraForward) {
2107
- // Sanity check, it shouldn't happen!
2108
- throw new Error("positions or camera info is not defined!");
2109
- }
2110
- depthMix = e.data.depthMix;
2111
- indices = new Uint32Array(depthMix.buffer);
2112
- floatMix = new Float32Array(depthMix.buffer);
2113
- // Sort
2114
- for (let j = 0; j < vertexCountPadded; j++) {
2115
- indices[2 * j] = j;
2116
- }
2117
- // depth = dot(cameraForward, worldPos - cameraPos)
2118
- const camDot = cameraForward[0] * cameraPosition[0] + cameraForward[1] * cameraPosition[1] + cameraForward[2] * cameraPosition[2];
2119
- const computeDepthCoeffs = (m) => {
2120
- return [
2121
- cameraForward[0] * m[0] + cameraForward[1] * m[1] + cameraForward[2] * m[2],
2122
- cameraForward[0] * m[4] + cameraForward[1] * m[5] + cameraForward[2] * m[6],
2123
- cameraForward[0] * m[8] + cameraForward[1] * m[9] + cameraForward[2] * m[10],
2124
- cameraForward[0] * m[12] + cameraForward[1] * m[13] + cameraForward[2] * m[14] - camDot,
2125
- ];
2126
- };
2127
- if (partMatrices && partIndices) {
2128
- // Precompute depth coefficients for each rig node
2129
- const depthCoeffs = partMatrices.map((m) => computeDepthCoeffs(m));
2130
- // NB: For performance reasons, we assume that part indices are valid
2131
- const length = partIndices.length;
2132
- for (let j = 0; j < vertexCountPadded; j++) {
2133
- // NB: We need this 'min' because vertex array is padded, not partIndices
2134
- const partIndex = partIndices[Math.min(j, length - 1)];
2135
- const coeff = depthCoeffs[partIndex];
2136
- floatMix[2 * j + 1] = coeff[0] * positions[4 * j + 0] + coeff[1] * positions[4 * j + 1] + coeff[2] * positions[4 * j + 2] + coeff[3];
2137
- // instead of using minus to sort back to front, we use bitwise not operator to invert the order of indices
2138
- // might not be faster but a minus sign implies a reference value that may not be enough and will decrease floatting precision
2139
- indices[2 * j + 1] = ~indices[2 * j + 1];
2140
- }
2141
- }
2142
- else {
2143
- // Compute depth coefficients from global world matrix
2144
- const [a, b, c, d] = computeDepthCoeffs(globalWorldMatrix);
2145
- for (let j = 0; j < vertexCountPadded; j++) {
2146
- floatMix[2 * j + 1] = a * positions[4 * j + 0] + b * positions[4 * j + 1] + c * positions[4 * j + 2] + d;
2147
- indices[2 * j + 1] = ~indices[2 * j + 1];
2148
- }
2149
- }
2150
- depthMix.sort();
2151
- self.postMessage({ depthMix, cameraId }, [depthMix.buffer]);
2152
- }
2153
- };
2154
- };
2155
690
  //# sourceMappingURL=gaussianSplattingMesh.js.map