@mapcomponents/three 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/.babelrc +12 -0
  2. package/.storybook/main.ts +20 -0
  3. package/.storybook/preview.ts +0 -0
  4. package/README.md +54 -0
  5. package/cypress.config.ts +13 -0
  6. package/eslint.config.mjs +12 -0
  7. package/package.json +24 -0
  8. package/project.json +15 -0
  9. package/public/assets/3D/godzilla_simple.glb +0 -0
  10. package/public/assets/splats/output.splat +0 -0
  11. package/src/components/MlThreeModelLayer/MlThreeModelLayer.cy.tsx +63 -0
  12. package/src/components/MlThreeModelLayer/MlThreeModelLayer.meta.json +21 -0
  13. package/src/components/MlThreeModelLayer/MlThreeModelLayer.stories.tsx +161 -0
  14. package/src/components/MlThreeModelLayer/MlThreeModelLayer.tsx +153 -0
  15. package/src/components/MlThreeSplatLayer/MlThreeSplatLayer.cy.tsx +62 -0
  16. package/src/components/MlThreeSplatLayer/MlThreeSplatLayer.meta.json +21 -0
  17. package/src/components/MlThreeSplatLayer/MlThreeSplatLayer.stories.tsx +151 -0
  18. package/src/components/MlThreeSplatLayer/MlThreeSplatLayer.tsx +158 -0
  19. package/src/components/MlTransformControls.tsx +112 -0
  20. package/src/components/ThreeContext.tsx +26 -0
  21. package/src/components/ThreeObjectControls.tsx +197 -0
  22. package/src/components/ThreeProvider.tsx +149 -0
  23. package/src/cypress/support/commands.ts +1 -0
  24. package/src/cypress/support/component-index.html +13 -0
  25. package/src/cypress/support/component.ts +13 -0
  26. package/src/decorators/ThreejsContextDecorator.tsx +42 -0
  27. package/src/decorators/style.css +33 -0
  28. package/src/index.ts +7 -0
  29. package/src/lib/ThreejsSceneHelper.ts +250 -0
  30. package/src/lib/ThreejsSceneRenderer.ts +73 -0
  31. package/src/lib/ThreejsUtils.ts +62 -0
  32. package/src/lib/splats/GaussianSplattingMesh.ts +848 -0
  33. package/src/lib/splats/GaussianSplattingShaders.ts +266 -0
  34. package/src/lib/splats/loaders/PlySplatLoader.ts +537 -0
  35. package/src/lib/splats/loaders/SplatLoader.ts +52 -0
  36. package/src/lib/utils/coroutine.ts +121 -0
  37. package/tsconfig.json +21 -0
  38. package/tsconfig.lib.json +27 -0
  39. package/tsconfig.storybook.json +24 -0
  40. package/vite.config.ts +49 -0
@@ -0,0 +1,848 @@
1
+ /**
2
+ * Derived from mapbox-3d-tiles by Jianshun Yang (MIT License)
3
+ * https://github.com/yangjs6/mapbox-3d-tiles
4
+ */
5
+
6
+ import {
7
+ Box3,
8
+ BufferAttribute,
9
+ BufferGeometry,
10
+ Camera,
11
+ ClampToEdgeWrapping,
12
+ DataTexture,
13
+ DataUtils,
14
+ DoubleSide,
15
+ DynamicDrawUsage,
16
+ FloatType,
17
+ Group,
18
+ HalfFloatType,
19
+ InstancedBufferAttribute,
20
+ InstancedBufferGeometry,
21
+ LinearFilter,
22
+ Material,
23
+ Matrix4,
24
+ Mesh,
25
+ NearestFilter,
26
+ NormalBlending,
27
+ PixelFormat,
28
+ Quaternion,
29
+ RGBAFormat,
30
+ RGBAIntegerFormat,
31
+ RGFormat,
32
+ Scene,
33
+ ShaderMaterial,
34
+ Sphere,
35
+ UnsignedByteType,
36
+ UnsignedIntType,
37
+ UVMapping,
38
+ Vector2,
39
+ Vector3,
40
+ WebGLRenderer,
41
+ } from 'three';
42
+ import {
43
+ Coroutine,
44
+ createYieldingScheduler,
45
+ runCoroutineAsync,
46
+ runCoroutineSync,
47
+ } from '../utils/coroutine';
48
+ import { fragmentShaderSource, vertexShaderSource } from './GaussianSplattingShaders';
49
+
50
+ export type Nullable<T> = T | null;
51
+
52
+ const toHalfFloat = (val: number) => DataUtils.toHalfFloat(val);
53
+
54
+ /**
55
+ * Geometry for Gaussian Splatting
56
+ */
57
+ export class GaussianSplattingGeometry {
58
+ static build(maxSplatCount = 1): InstancedBufferGeometry {
59
+ const baseGeometry = new BufferGeometry();
60
+
61
+ // Triangle vertices (slightly faster than quad due to fewer shader invocations)
62
+ baseGeometry.setIndex([0, 1, 2]);
63
+ const positions = new BufferAttribute(
64
+ new Float32Array([-3.0, -2.0, 0.0, 3.0, -2.0, 0.0, 0.0, 4.0, 0.0]),
65
+ 3
66
+ );
67
+ baseGeometry.setAttribute('position', positions);
68
+
69
+ const geometry = new InstancedBufferGeometry();
70
+ geometry.setIndex(baseGeometry.getIndex());
71
+ geometry.setAttribute('position', baseGeometry.getAttribute('position'));
72
+
73
+ const splatIndexes = new InstancedBufferAttribute(new Float32Array(maxSplatCount), 1, false);
74
+ splatIndexes.setUsage(DynamicDrawUsage);
75
+ geometry.setAttribute('splatIndex', splatIndexes);
76
+ geometry.instanceCount = 0;
77
+
78
+ return geometry;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Radix sort-based depth sorter for Gaussian Splatting
84
+ */
85
+ export class GaussianSplattingSorter {
86
+ private static readonly BATCH_SIZE = 327680;
87
+ private static workCount = 0;
88
+
89
+ vertexCount = 0;
90
+ positions: Float32Array | null = null;
91
+ hasInit = false;
92
+
93
+ splatIndex: Uint32Array | null = null;
94
+ depthValues: Int32Array | null = null;
95
+ tempDepths: Int32Array | null = null;
96
+ tempIndices: Uint32Array | null = null;
97
+ abortController: AbortController | null = null;
98
+
99
+ onmessage: ((splatIndex: Uint32Array) => void) | null = null;
100
+
101
+ terminate(): void {
102
+ this.abortController?.abort();
103
+ this.abortController = null;
104
+ this.vertexCount = 0;
105
+ this.positions = null;
106
+ this.splatIndex = null;
107
+ this.onmessage = null;
108
+ }
109
+
110
+ private initSortData(): void {
111
+ if (this.hasInit || this.vertexCount < 0) return;
112
+
113
+ const count = this.vertexCount;
114
+ this.depthValues = new Int32Array(count);
115
+ this.splatIndex = new Uint32Array(count);
116
+ this.tempDepths = new Int32Array(count);
117
+ this.tempIndices = new Uint32Array(count);
118
+ this.hasInit = true;
119
+ }
120
+
121
+ private *sortData(viewProj: number[], isAsync: boolean): Coroutine<void> {
122
+ if (!this.hasInit) this.initSortData();
123
+
124
+ const { positions, vertexCount, depthValues, splatIndex, tempDepths, tempIndices } = this;
125
+ if (!positions || !depthValues || !splatIndex || !tempDepths || !tempIndices) return;
126
+
127
+ let maxDepth = -Infinity;
128
+ let minDepth = Infinity;
129
+
130
+ for (let i = 0; i < vertexCount; i++) {
131
+ splatIndex[i] = i;
132
+ const depth =
133
+ viewProj[2] * positions[4 * i] +
134
+ viewProj[6] * positions[4 * i + 1] +
135
+ viewProj[10] * positions[4 * i + 2];
136
+ const depthInt = Math.floor(depth * 4096);
137
+ depthValues[i] = depthInt;
138
+ maxDepth = Math.max(maxDepth, depthInt);
139
+ minDepth = Math.min(minDepth, depthInt);
140
+ }
141
+
142
+ if (isAsync) {
143
+ GaussianSplattingSorter.workCount += vertexCount;
144
+ if (GaussianSplattingSorter.workCount > GaussianSplattingSorter.BATCH_SIZE) {
145
+ GaussianSplattingSorter.workCount = 0;
146
+ yield;
147
+ }
148
+ }
149
+
150
+ const depthOffset = -minDepth;
151
+ for (let i = 0; i < vertexCount; i++) {
152
+ depthValues[i] += depthOffset;
153
+ }
154
+
155
+ const counts = new Uint32Array(256);
156
+
157
+ for (let shift = 0; shift < 32; shift += 8) {
158
+ counts.fill(0);
159
+
160
+ for (let i = 0; i < vertexCount; i++) {
161
+ counts[(depthValues[i] >> shift) & 0xff]++;
162
+ }
163
+
164
+ let total = 0;
165
+ for (let i = 0; i < counts.length; i++) {
166
+ const current = counts[i];
167
+ counts[i] = total;
168
+ total += current;
169
+ }
170
+
171
+ for (let i = 0; i < vertexCount; i++) {
172
+ const byte = (depthValues[i] >> shift) & 0xff;
173
+ const pos = counts[byte]++;
174
+ tempDepths[pos] = depthValues[i];
175
+ tempIndices[pos] = splatIndex[i];
176
+ }
177
+
178
+ depthValues.set(tempDepths);
179
+ splatIndex.set(tempIndices);
180
+
181
+ if (isAsync) {
182
+ GaussianSplattingSorter.workCount += vertexCount;
183
+ if (GaussianSplattingSorter.workCount > GaussianSplattingSorter.BATCH_SIZE) {
184
+ GaussianSplattingSorter.workCount = 0;
185
+ yield;
186
+ }
187
+ }
188
+ }
189
+ }
190
+
191
+ init(positions: Float32Array, vertexCount: number): void {
192
+ this.positions = positions;
193
+ this.vertexCount = vertexCount;
194
+ this.initSortData();
195
+ }
196
+
197
+ async sortDataAsync(viewProj: number[]): Promise<void> {
198
+ this.abortController?.abort();
199
+ this.abortController = new AbortController();
200
+ const signal = this.abortController.signal;
201
+
202
+ try {
203
+ await runCoroutineAsync(this.sortData(viewProj, true), createYieldingScheduler(), signal);
204
+ if (this.onmessage && this.splatIndex) {
205
+ this.onmessage(this.splatIndex);
206
+ }
207
+ } catch (error: unknown) {
208
+ if (error instanceof Error && error.name !== 'AbortError') {
209
+ console.error('Splat sort error:', error);
210
+ }
211
+ } finally {
212
+ this.abortController = null;
213
+ }
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Material for Gaussian Splatting
219
+ */
220
+ export class GaussianSplattingMaterial {
221
+ static build(shDegree = 0): ShaderMaterial {
222
+ return new ShaderMaterial({
223
+ uniforms: {
224
+ invViewport: { value: new Vector2() },
225
+ dataTextureSize: { value: new Vector2() },
226
+ focal: { value: new Vector2() },
227
+ covariancesATexture: { value: null },
228
+ covariancesBTexture: { value: null },
229
+ centersTexture: { value: null },
230
+ colorsTexture: { value: null },
231
+ shTexture0: { value: null },
232
+ shTexture1: { value: null },
233
+ shTexture2: { value: null },
234
+ },
235
+ defines: { SH_DEGREE: shDegree },
236
+ vertexShader: vertexShaderSource,
237
+ fragmentShader: fragmentShaderSource,
238
+ transparent: true,
239
+ alphaTest: 1.0,
240
+ blending: NormalBlending,
241
+ depthTest: true,
242
+ depthWrite: true,
243
+ side: DoubleSide,
244
+ });
245
+ }
246
+
247
+ static updateUniforms(
248
+ renderer: WebGLRenderer,
249
+ camera: Camera,
250
+ mesh: GaussianSplattingMesh
251
+ ): void {
252
+ const material = mesh.material as ShaderMaterial;
253
+ if (!material?.uniforms) return;
254
+
255
+ const { uniforms } = material;
256
+ const renderSize = renderer.getSize(new Vector2());
257
+
258
+ uniforms.invViewport.value.set(1 / renderSize.x, 1 / renderSize.y);
259
+
260
+ if (camera) {
261
+ const cleanMatrix = (camera as any)._cleanProjectionMatrix;
262
+ const elements = cleanMatrix?.elements ?? cleanMatrix ?? camera.projectionMatrix.elements;
263
+
264
+ uniforms.focal.value.set(elements[0] * 0.5 * renderSize.x, elements[5] * 0.5 * renderSize.y);
265
+ }
266
+
267
+ if (mesh.covariancesATexture) {
268
+ const { width, height } = mesh.covariancesATexture.image;
269
+ uniforms.dataTextureSize.value.set(width, height);
270
+ uniforms.covariancesATexture.value = mesh.covariancesATexture;
271
+ uniforms.covariancesBTexture.value = mesh.covariancesBTexture;
272
+ uniforms.centersTexture.value = mesh.centersTexture;
273
+ uniforms.colorsTexture.value = mesh.colorsTexture;
274
+
275
+ mesh.shTextures?.forEach((tex, i) => {
276
+ uniforms[`shTexture${i}`].value = tex;
277
+ });
278
+ }
279
+
280
+ material.uniformsNeedUpdate = true;
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Gaussian Splatting mesh renderer
286
+ */
287
+ export class GaussianSplattingMesh extends Mesh {
288
+ private static readonly ROW_OUTPUT_LENGTH = 3 * 4 + 3 * 4 + 4 + 4;
289
+ private static readonly SPLAT_BATCH_SIZE = 327680;
290
+
291
+ private vertexCount = 0;
292
+ private worker: Nullable<GaussianSplattingSorter> = null;
293
+ private frameIdLastUpdate = -1;
294
+ private frameIdThisUpdate = 0;
295
+ private cameraMatrix: Matrix4 | null = null;
296
+ private internalModelViewMatrix: Matrix4 | null = null;
297
+ private canPostToWorker = false;
298
+ private covariancesATextureInternal: Nullable<DataTexture> = null;
299
+ private covariancesBTextureInternal: Nullable<DataTexture> = null;
300
+ private centersTextureInternal: Nullable<DataTexture> = null;
301
+ private colorsTextureInternal: Nullable<DataTexture> = null;
302
+ private splatPositions: Nullable<Float32Array> = null;
303
+ private splatPositions2: Nullable<Float32Array> = null;
304
+ private splatIndex: Nullable<Float32Array> = null;
305
+ private shTexturesInternal: Nullable<DataTexture[]> = null;
306
+ private splatsDataInternal: Nullable<ArrayBuffer> = null;
307
+ private readonly keepInRam: boolean = false;
308
+
309
+ private oldDirection = new Vector3();
310
+ private useRGBACovariants = true;
311
+ private tmpCovariances = [0, 0, 0, 0, 0, 0];
312
+ private sortIsDirty = false;
313
+ private lastSortTime = 0;
314
+ private sortThrottleMs = 200;
315
+ private shDegreeValue = 0;
316
+
317
+ private tempQuaternion = new Quaternion();
318
+ private tempPosition = new Vector3();
319
+ private tempScale = new Vector3();
320
+ private tempColor = new Uint8Array(4);
321
+ private tempMatrix = new Matrix4();
322
+
323
+ declare boundingBox: Box3 | null;
324
+ declare boundingSphere: Sphere | null;
325
+ readonly isGaussianSplattingMesh = true as const;
326
+ readyToDisplay = false;
327
+ override readonly type = 'GaussianSplattingMesh' as const;
328
+
329
+ get shDegree() {
330
+ return this.shDegreeValue;
331
+ }
332
+ get splatsData() {
333
+ return this.splatsDataInternal;
334
+ }
335
+ get covariancesATexture() {
336
+ return this.covariancesATextureInternal;
337
+ }
338
+ get covariancesBTexture() {
339
+ return this.covariancesBTextureInternal;
340
+ }
341
+ get centersTexture() {
342
+ return this.centersTextureInternal;
343
+ }
344
+ get colorsTexture() {
345
+ return this.colorsTextureInternal;
346
+ }
347
+ get shTextures() {
348
+ return this.shTexturesInternal;
349
+ }
350
+
351
+ constructor() {
352
+ super();
353
+ this.geometry = GaussianSplattingGeometry.build();
354
+ this.material = GaussianSplattingMaterial.build();
355
+ this.setEnabled(false);
356
+ }
357
+
358
+ setEnabled(enabled: boolean): void {
359
+ this.visible = enabled;
360
+ }
361
+
362
+ postToWorker(forced = false): Promise<void> | undefined {
363
+ const frameId = this.frameIdThisUpdate;
364
+ if (
365
+ (forced || frameId !== this.frameIdLastUpdate) &&
366
+ this.worker &&
367
+ this.cameraMatrix &&
368
+ this.canPostToWorker
369
+ ) {
370
+ this.internalModelViewMatrix = new Matrix4().multiplyMatrices(
371
+ this.cameraMatrix,
372
+ this.matrixWorld
373
+ );
374
+
375
+ const invCamera = this.cameraMatrix.clone().invert();
376
+ const modelView = new Matrix4().multiplyMatrices(invCamera, this.matrixWorld);
377
+ const newDirection = new Vector3(0, 0, 1).transformDirection(modelView);
378
+ const dot = newDirection.dot(this.oldDirection);
379
+
380
+ if (forced || Math.abs(dot - 1) >= 0.01) {
381
+ this.oldDirection.copy(newDirection);
382
+ this.frameIdLastUpdate = frameId;
383
+ this.canPostToWorker = false;
384
+ return this.worker.sortDataAsync(this.internalModelViewMatrix.elements);
385
+ }
386
+ }
387
+ return undefined;
388
+ }
389
+
390
+ override onBeforeRender(
391
+ renderer: WebGLRenderer,
392
+ scene: Scene,
393
+ camera: Camera,
394
+ geometry: BufferGeometry,
395
+ material: Material,
396
+ group: Group
397
+ ): void {
398
+ this.frameIdThisUpdate = renderer.info.render.frame;
399
+
400
+ const now = performance.now();
401
+ if (now - this.lastSortTime > this.sortThrottleMs) {
402
+ this.lastSortTime = now;
403
+ this.sortDataAsync(camera).catch((err) => {
404
+ if (err.name !== 'AbortError') {
405
+ console.warn('Splat sorting error:', err);
406
+ }
407
+ });
408
+ }
409
+
410
+ GaussianSplattingMaterial.updateUniforms(renderer, camera, this);
411
+ super.onBeforeRender(renderer, scene, camera, geometry, material, group);
412
+ }
413
+
414
+ loadDataAsync(data: ArrayBuffer): Promise<void> {
415
+ return this.updateDataAsync(data);
416
+ }
417
+
418
+ dispose(): void {
419
+ this.covariancesATextureInternal?.dispose();
420
+ this.covariancesBTextureInternal?.dispose();
421
+ this.centersTextureInternal?.dispose();
422
+ this.colorsTextureInternal?.dispose();
423
+ this.shTexturesInternal?.forEach((tex) => tex.dispose());
424
+
425
+ this.covariancesATextureInternal = null;
426
+ this.covariancesBTextureInternal = null;
427
+ this.centersTextureInternal = null;
428
+ this.colorsTextureInternal = null;
429
+ this.shTexturesInternal = null;
430
+
431
+ this.worker?.terminate();
432
+ this.worker = null;
433
+ }
434
+
435
+ private copyTextures(source: GaussianSplattingMesh): void {
436
+ this.covariancesATextureInternal = source.covariancesATexture?.clone() ?? null;
437
+ this.covariancesBTextureInternal = source.covariancesBTexture?.clone() ?? null;
438
+ this.centersTextureInternal = source.centersTexture?.clone() ?? null;
439
+ this.colorsTextureInternal = source.colorsTexture?.clone() ?? null;
440
+ if (source.shTexturesInternal) {
441
+ this.shTexturesInternal = source.shTexturesInternal.map((tex) => tex.clone());
442
+ }
443
+ }
444
+
445
+ // @ts-expect-error - Return type differs from base class
446
+ override clone(): GaussianSplattingMesh {
447
+ const cloned = new GaussianSplattingMesh();
448
+ cloned.geometry = this.geometry.clone();
449
+ cloned.material = (this.material as Material).clone();
450
+ cloned.vertexCount = this.vertexCount;
451
+ cloned.copyTextures(this);
452
+ cloned.splatPositions = this.splatPositions;
453
+ cloned.readyToDisplay = false;
454
+ cloned.instantiateWorker();
455
+ return cloned;
456
+ }
457
+
458
+ private makeSplatFromComponents(
459
+ sourceIndex: number,
460
+ destinationIndex: number,
461
+ position: Vector3,
462
+ scale: Vector3,
463
+ quaternion: Quaternion,
464
+ color: Uint8Array,
465
+ covA: Uint16Array,
466
+ covB: Uint16Array,
467
+ colorArray: Uint8Array,
468
+ minimum: Vector3,
469
+ maximum: Vector3
470
+ ): void {
471
+ quaternion.w = -quaternion.w;
472
+ scale = scale.multiplyScalar(2);
473
+
474
+ const te = this.tempMatrix.elements;
475
+ const covBSItemSize = this.useRGBACovariants ? 4 : 2;
476
+
477
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
478
+ this.splatPositions![4 * sourceIndex + 0] = position.x;
479
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
480
+ this.splatPositions![4 * sourceIndex + 1] = position.y;
481
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
482
+ this.splatPositions![4 * sourceIndex + 2] = position.z;
483
+
484
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
485
+ this.splatPositions2![4 * sourceIndex + 0] = position.x;
486
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
487
+ this.splatPositions2![4 * sourceIndex + 1] = position.y;
488
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
489
+ this.splatPositions2![4 * sourceIndex + 2] = position.z;
490
+
491
+ minimum.min(position);
492
+ maximum.max(position);
493
+
494
+ const { x, y, z, w } = quaternion;
495
+ const x2 = x + x,
496
+ y2 = y + y,
497
+ z2 = z + z;
498
+ const xx = x * x2,
499
+ xy = x * y2,
500
+ xz = x * z2;
501
+ const yy = y * y2,
502
+ yz = y * z2,
503
+ zz = z * z2;
504
+ const wx = w * x2,
505
+ wy = w * y2,
506
+ wz = w * z2;
507
+ const { x: sx, y: sy, z: sz } = scale;
508
+
509
+ te[0] = (1 - (yy + zz)) * sx;
510
+ te[1] = (xy + wz) * sy;
511
+ te[2] = (xz - wy) * sz;
512
+ te[4] = (xy - wz) * sx;
513
+ te[5] = (1 - (xx + zz)) * sy;
514
+ te[6] = (yz + wx) * sz;
515
+ te[8] = (xz + wy) * sx;
516
+ te[9] = (yz - wx) * sy;
517
+ te[10] = (1 - (xx + yy)) * sz;
518
+
519
+ const covariances = this.tmpCovariances;
520
+ covariances[0] = te[0] * te[0] + te[1] * te[1] + te[2] * te[2];
521
+ covariances[1] = te[0] * te[4] + te[1] * te[5] + te[2] * te[6];
522
+ covariances[2] = te[0] * te[8] + te[1] * te[9] + te[2] * te[10];
523
+ covariances[3] = te[4] * te[4] + te[5] * te[5] + te[6] * te[6];
524
+ covariances[4] = te[4] * te[8] + te[5] * te[9] + te[6] * te[10];
525
+ covariances[5] = te[8] * te[8] + te[9] * te[9] + te[10] * te[10];
526
+
527
+ let factor = -10000;
528
+ for (let i = 0; i < 6; i++) {
529
+ factor = Math.max(factor, Math.abs(covariances[i]));
530
+ }
531
+
532
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
533
+ this.splatPositions![4 * sourceIndex + 3] = factor;
534
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
535
+ this.splatPositions2![4 * sourceIndex + 3] = factor;
536
+
537
+ covA[destinationIndex * 4 + 0] = toHalfFloat(covariances[0] / factor);
538
+ covA[destinationIndex * 4 + 1] = toHalfFloat(covariances[1] / factor);
539
+ covA[destinationIndex * 4 + 2] = toHalfFloat(covariances[2] / factor);
540
+ covA[destinationIndex * 4 + 3] = toHalfFloat(covariances[3] / factor);
541
+ covB[destinationIndex * covBSItemSize + 0] = toHalfFloat(covariances[4] / factor);
542
+ covB[destinationIndex * covBSItemSize + 1] = toHalfFloat(covariances[5] / factor);
543
+
544
+ colorArray[destinationIndex * 4 + 0] = color[0];
545
+ colorArray[destinationIndex * 4 + 1] = color[1];
546
+ colorArray[destinationIndex * 4 + 2] = color[2];
547
+ colorArray[destinationIndex * 4 + 3] = color[3];
548
+ }
549
+
550
+ private makeSplatFromBuffer(
551
+ sourceIndex: number,
552
+ destinationIndex: number,
553
+ fBuffer: Float32Array,
554
+ uBuffer: Uint8Array,
555
+ covA: Uint16Array,
556
+ covB: Uint16Array,
557
+ colorArray: Uint8Array,
558
+ minimum: Vector3,
559
+ maximum: Vector3
560
+ ): void {
561
+ const baseF = 8 * sourceIndex;
562
+ const baseU = 32 * sourceIndex;
563
+
564
+ this.tempPosition.set(fBuffer[baseF], fBuffer[baseF + 1], fBuffer[baseF + 2]);
565
+ this.tempScale.set(fBuffer[baseF + 3], fBuffer[baseF + 4], fBuffer[baseF + 5]);
566
+
567
+ this.tempQuaternion
568
+ .set(
569
+ (uBuffer[baseU + 29] - 127.5) / 127.5,
570
+ (uBuffer[baseU + 30] - 127.5) / 127.5,
571
+ (uBuffer[baseU + 31] - 127.5) / 127.5,
572
+ (uBuffer[baseU + 28] - 127.5) / 127.5
573
+ )
574
+ .normalize();
575
+
576
+ this.tempColor[0] = uBuffer[baseU + 24];
577
+ this.tempColor[1] = uBuffer[baseU + 25];
578
+ this.tempColor[2] = uBuffer[baseU + 26];
579
+ this.tempColor[3] = uBuffer[baseU + 27];
580
+
581
+ this.makeSplatFromComponents(
582
+ sourceIndex,
583
+ destinationIndex,
584
+ this.tempPosition,
585
+ this.tempScale,
586
+ this.tempQuaternion,
587
+ this.tempColor,
588
+ covA,
589
+ covB,
590
+ colorArray,
591
+ minimum,
592
+ maximum
593
+ );
594
+ }
595
+
596
+ private updateTextures(
597
+ covA: Uint16Array,
598
+ covB: Uint16Array,
599
+ colorArray: Uint8Array,
600
+ sh?: Uint8Array[]
601
+ ): void {
602
+ const textureSize = this.getTextureSize(this.vertexCount);
603
+
604
+ const createF32Texture = (data: Float32Array, w: number, h: number, format: PixelFormat) => {
605
+ const tex = new DataTexture(
606
+ data,
607
+ w,
608
+ h,
609
+ format,
610
+ FloatType,
611
+ UVMapping,
612
+ ClampToEdgeWrapping,
613
+ ClampToEdgeWrapping,
614
+ NearestFilter,
615
+ NearestFilter
616
+ );
617
+ tex.generateMipmaps = false;
618
+ tex.needsUpdate = true;
619
+ return tex;
620
+ };
621
+
622
+ const createU8Texture = (data: Uint8Array, w: number, h: number, format: PixelFormat) => {
623
+ const tex = new DataTexture(
624
+ data,
625
+ w,
626
+ h,
627
+ format,
628
+ UnsignedByteType,
629
+ UVMapping,
630
+ ClampToEdgeWrapping,
631
+ ClampToEdgeWrapping,
632
+ NearestFilter,
633
+ NearestFilter
634
+ );
635
+ tex.generateMipmaps = false;
636
+ tex.needsUpdate = true;
637
+ return tex;
638
+ };
639
+
640
+ const createU32Texture = (data: Uint32Array, w: number, h: number, format: PixelFormat) => {
641
+ const tex = new DataTexture(
642
+ data,
643
+ w,
644
+ h,
645
+ format,
646
+ UnsignedIntType,
647
+ UVMapping,
648
+ ClampToEdgeWrapping,
649
+ ClampToEdgeWrapping,
650
+ NearestFilter,
651
+ NearestFilter
652
+ );
653
+ tex.generateMipmaps = false;
654
+ tex.needsUpdate = true;
655
+ return tex;
656
+ };
657
+
658
+ const createF16Texture = (data: Uint16Array, w: number, h: number, format: PixelFormat) => {
659
+ const tex = new DataTexture(
660
+ data,
661
+ w,
662
+ h,
663
+ format,
664
+ HalfFloatType,
665
+ UVMapping,
666
+ ClampToEdgeWrapping,
667
+ ClampToEdgeWrapping,
668
+ NearestFilter,
669
+ NearestFilter
670
+ );
671
+ tex.generateMipmaps = false;
672
+ tex.needsUpdate = true;
673
+ return tex;
674
+ };
675
+
676
+ this.covariancesATextureInternal = createF16Texture(
677
+ covA,
678
+ textureSize.x,
679
+ textureSize.y,
680
+ RGBAFormat
681
+ );
682
+ this.covariancesBTextureInternal = createF16Texture(
683
+ covB,
684
+ textureSize.x,
685
+ textureSize.y,
686
+ this.useRGBACovariants ? RGBAFormat : RGFormat
687
+ );
688
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
689
+ this.centersTextureInternal = createF32Texture(
690
+ this.splatPositions!,
691
+ textureSize.x,
692
+ textureSize.y,
693
+ RGBAFormat
694
+ );
695
+ this.colorsTextureInternal = createU8Texture(
696
+ colorArray,
697
+ textureSize.x,
698
+ textureSize.y,
699
+ RGBAFormat
700
+ );
701
+
702
+ if (sh) {
703
+ this.shTexturesInternal = sh.map((shData) => {
704
+ const buffer = new Uint32Array(shData.buffer);
705
+ const shTexture = createU32Texture(buffer, textureSize.x, textureSize.y, RGBAIntegerFormat);
706
+ shTexture.wrapS = ClampToEdgeWrapping;
707
+ shTexture.wrapT = ClampToEdgeWrapping;
708
+ return shTexture;
709
+ });
710
+ }
711
+
712
+ this.instantiateWorker();
713
+ }
714
+
715
+ private updateBoundingInfo(minimum: Vector3, maximum: Vector3): void {
716
+ this.boundingBox = new Box3(minimum, maximum);
717
+ this.boundingSphere = this.boundingBox.getBoundingSphere(new Sphere());
718
+ }
719
+
720
+ private *updateDataCoroutine(
721
+ data: ArrayBuffer,
722
+ isAsync: boolean,
723
+ sh?: Uint8Array[]
724
+ ): Coroutine<void> {
725
+ if (!this.covariancesATextureInternal) {
726
+ this.readyToDisplay = false;
727
+ }
728
+
729
+ const uBuffer = new Uint8Array(data);
730
+ const fBuffer = new Float32Array(uBuffer.buffer);
731
+
732
+ if (this.keepInRam) {
733
+ this.splatsDataInternal = data;
734
+ }
735
+
736
+ this.shDegreeValue = sh ? sh.length : 0;
737
+ const vertexCount = uBuffer.length / GaussianSplattingMesh.ROW_OUTPUT_LENGTH;
738
+
739
+ if (vertexCount !== this.vertexCount) {
740
+ this.vertexCount = vertexCount;
741
+ this.geometry = GaussianSplattingGeometry.build(this.vertexCount);
742
+ this.material = GaussianSplattingMaterial.build(this.shDegreeValue);
743
+ this.updateSplatIndexBuffer(this.vertexCount);
744
+ }
745
+
746
+ const textureSize = this.getTextureSize(vertexCount);
747
+ const textureLength = textureSize.x * textureSize.y;
748
+
749
+ this.splatPositions = new Float32Array(4 * textureLength);
750
+ this.splatPositions2 = new Float32Array(4 * vertexCount);
751
+ const covA = new Uint16Array(textureLength * 4);
752
+ const covB = new Uint16Array((this.useRGBACovariants ? 4 : 2) * textureLength);
753
+ const colorArray = new Uint8Array(textureLength * 4);
754
+
755
+ const minimum = new Vector3(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
756
+ const maximum = new Vector3(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE);
757
+
758
+ for (let i = 0; i < vertexCount; i++) {
759
+ this.makeSplatFromBuffer(i, i, fBuffer, uBuffer, covA, covB, colorArray, minimum, maximum);
760
+ if (isAsync && i % GaussianSplattingMesh.SPLAT_BATCH_SIZE === 0) {
761
+ yield;
762
+ }
763
+ }
764
+
765
+ this.updateTextures(covA, covB, colorArray, sh);
766
+ this.updateBoundingInfo(minimum, maximum);
767
+ this.setEnabled(true);
768
+ this.postToWorker(true);
769
+ }
770
+
771
+ async updateDataAsync(data: ArrayBuffer, sh?: Uint8Array[]): Promise<void> {
772
+ return runCoroutineAsync(this.updateDataCoroutine(data, true, sh), createYieldingScheduler());
773
+ }
774
+
775
+ updateData(data: ArrayBuffer, sh?: Uint8Array[]): void {
776
+ runCoroutineSync(this.updateDataCoroutine(data, false, sh));
777
+ }
778
+
779
+ sortDataAsync(camera: Camera, forced = false): Promise<void> {
780
+ if (!this.worker || !camera) {
781
+ return Promise.resolve();
782
+ }
783
+
784
+ this.cameraMatrix = camera.matrixWorldInverse;
785
+ return this.postToWorker(forced) ?? Promise.resolve();
786
+ }
787
+
788
+ private updateSplatIndexBuffer(vertexCount: number): void {
789
+ if (!this.splatIndex || vertexCount > this.splatIndex.length) {
790
+ this.splatIndex = new Float32Array(vertexCount);
791
+ for (let j = 0; j < vertexCount; j++) {
792
+ this.splatIndex[j] = j;
793
+ }
794
+ (this.geometry.attributes.splatIndex as BufferAttribute).set(this.splatIndex);
795
+ this.geometry.attributes.splatIndex.needsUpdate = true;
796
+ }
797
+ (this.geometry as InstancedBufferGeometry).instanceCount = vertexCount;
798
+ }
799
+
800
+ private instantiateWorker(): void {
801
+ if (!this.vertexCount) return;
802
+
803
+ this.updateSplatIndexBuffer(this.vertexCount);
804
+ this.worker?.terminate();
805
+ this.worker = new GaussianSplattingSorter();
806
+
807
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
808
+ this.worker.init(this.splatPositions2!, this.vertexCount);
809
+ this.canPostToWorker = true;
810
+
811
+ this.worker.onmessage = (splatIndex) => {
812
+ if (this.splatIndex && splatIndex) {
813
+ for (let j = 0; j < this.vertexCount; j++) {
814
+ this.splatIndex[j] = splatIndex[j];
815
+ }
816
+ (this.geometry.attributes.splatIndex as BufferAttribute).set(this.splatIndex);
817
+ }
818
+
819
+ this.geometry.attributes.splatIndex.needsUpdate = true;
820
+ this.canPostToWorker = true;
821
+ this.readyToDisplay = true;
822
+
823
+ if (this.sortIsDirty) {
824
+ this.postToWorker(true);
825
+ this.sortIsDirty = false;
826
+ }
827
+ };
828
+ }
829
+
830
+ private getTextureSize(length: number): Vector2 {
831
+ const maxTextureSize = 4096;
832
+ const width = maxTextureSize;
833
+ let height = 1;
834
+
835
+ while (width * height < length) {
836
+ height *= 2;
837
+ }
838
+
839
+ if (height > width) {
840
+ console.error(
841
+ `GaussianSplatting texture size: (${width}, ${height}), maxTextureSize: ${width}`
842
+ );
843
+ height = width;
844
+ }
845
+
846
+ return new Vector2(width, height);
847
+ }
848
+ }