@speridlabs/visus 1.0.1 → 1.0.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.
@@ -0,0 +1,1307 @@
1
+ var lt = Object.defineProperty;
2
+ var ht = (A, t, s) => t in A ? lt(A, t, { enumerable: !0, configurable: !0, writable: !0, value: s }) : A[t] = s;
3
+ var c = (A, t, s) => ht(A, typeof t != "symbol" ? t + "" : t, s);
4
+ import * as n from "three";
5
+ class et {
6
+ constructor() {
7
+ c(this, "min", new n.Vector3(1 / 0, 1 / 0, 1 / 0));
8
+ c(this, "max", new n.Vector3(-1 / 0, -1 / 0, -1 / 0));
9
+ c(this, "center", new n.Vector3());
10
+ /** Half extents (size/2) */
11
+ c(this, "halfExtents", new n.Vector3());
12
+ }
13
+ /**
14
+ * Reset the bounding box to its initial state
15
+ */
16
+ reset() {
17
+ this.min.set(1 / 0, 1 / 0, 1 / 0), this.max.set(-1 / 0, -1 / 0, -1 / 0), this.center.set(0, 0, 0), this.halfExtents.set(0, 0, 0);
18
+ }
19
+ /**
20
+ * Expand the bounding box to include the given point
21
+ * @param point Point to include in the bounding box
22
+ */
23
+ expandByPoint(t) {
24
+ this.min.x = Math.min(this.min.x, t.x), this.min.y = Math.min(this.min.y, t.y), this.min.z = Math.min(this.min.z, t.z), this.max.x = Math.max(this.max.x, t.x), this.max.y = Math.max(this.max.y, t.y), this.max.z = Math.max(this.max.z, t.z), this.updateDerived();
25
+ }
26
+ /**
27
+ * Expand this bounding box to include another bounding box
28
+ * @param box Bounding box to include
29
+ */
30
+ expandByBox(t) {
31
+ this.min.x = Math.min(this.min.x, t.min.x), this.min.y = Math.min(this.min.y, t.min.y), this.min.z = Math.min(this.min.z, t.min.z), this.max.x = Math.max(this.max.x, t.max.x), this.max.y = Math.max(this.max.y, t.max.y), this.max.z = Math.max(this.max.z, t.max.z), this.updateDerived();
32
+ }
33
+ /**
34
+ * Update the center and half extents based on min/max
35
+ */
36
+ updateDerived() {
37
+ this.center.addVectors(this.min, this.max).multiplyScalar(0.5), this.halfExtents.subVectors(this.max, this.min).multiplyScalar(0.5);
38
+ }
39
+ /**
40
+ * Check if this box contains a point
41
+ * @param point Point to check
42
+ * @returns True if the point is inside the box
43
+ */
44
+ containsPoint(t) {
45
+ return t.x >= this.min.x && t.x <= this.max.x && t.y >= this.min.y && t.y <= this.max.y && t.z >= this.min.z && t.z <= this.max.z;
46
+ }
47
+ /**
48
+ * Create a Three.js Box3 from this bounding box
49
+ * @returns THREE.Box3 representation
50
+ */
51
+ toBox3() {
52
+ return new n.Box3().set(this.min, this.max);
53
+ }
54
+ /**
55
+ * Create a clone of this bounding box
56
+ * @returns New bounding box with the same values
57
+ */
58
+ clone() {
59
+ const t = new et();
60
+ return t.min.copy(this.min), t.max.copy(this.max), t.center.copy(this.center), t.halfExtents.copy(this.halfExtents), t;
61
+ }
62
+ }
63
+ class ut {
64
+ // TODO: there is no sh spherical harmonics
65
+ constructor(t = 0) {
66
+ c(this, "numSplats", 0);
67
+ c(this, "positions");
68
+ c(this, "rotations");
69
+ c(this, "scales");
70
+ c(this, "colors");
71
+ c(this, "opacities");
72
+ c(this, "centers");
73
+ c(this, "boundingBox", new et());
74
+ this.numSplats = t, this.allocateBuffers(t);
75
+ }
76
+ allocateBuffers(t) {
77
+ this.positions = new Float32Array(t * 3), this.rotations = new Float32Array(t * 4), this.scales = new Float32Array(t * 3), this.colors = new Float32Array(t * 3), this.opacities = new Float32Array(t), this.centers = new Float32Array(t * 3);
78
+ }
79
+ /**
80
+ * Set data for a specific splat
81
+ * @param index Splat index
82
+ * @param position Position vector
83
+ * @param rotation Quaternion
84
+ * @param scale Scale vector (expects LOG SCALE)
85
+ * @param color Color
86
+ * @param opacity Opacity value
87
+ */
88
+ setSplat(t, s, e, r, o, i) {
89
+ if (t >= this.numSplats)
90
+ throw new Error(
91
+ `Splat index out of bounds: ${t} >= ${this.numSplats}`
92
+ );
93
+ const a = t * 3, d = t * 4, f = t * 3, v = t * 3;
94
+ this.positions[a] = s.x, this.positions[a + 1] = s.y, this.positions[a + 2] = s.z, this.rotations[d] = e.x, this.rotations[d + 1] = e.y, this.rotations[d + 2] = e.z, this.rotations[d + 3] = e.w, this.scales[f] = r.x, this.scales[f + 1] = r.y, this.scales[f + 2] = r.z, this.colors[v] = o.r, this.colors[v + 1] = o.g, this.colors[v + 2] = o.b, this.opacities[t] = i, this.centers[a] = s.x, this.centers[a + 1] = s.y, this.centers[a + 2] = s.z;
95
+ }
96
+ /**
97
+ * Get a splat's data
98
+ * @param index Splat index
99
+ * @returns Object containing splat data (linear scale)
100
+ */
101
+ getSplat(t) {
102
+ if (t >= this.numSplats)
103
+ throw new Error(
104
+ `Splat index out of bounds: ${t} >= ${this.numSplats}`
105
+ );
106
+ const s = t * 3, e = t * 4, r = t * 3, o = t * 3;
107
+ return {
108
+ position: new n.Vector3(
109
+ this.positions[s],
110
+ this.positions[s + 1],
111
+ this.positions[s + 2]
112
+ ),
113
+ rotation: new n.Quaternion(
114
+ this.rotations[e],
115
+ this.rotations[e + 1],
116
+ this.rotations[e + 2],
117
+ this.rotations[e + 3]
118
+ ),
119
+ // Convert log scale back to linear scale for external use
120
+ scale: new n.Vector3(
121
+ Math.exp(this.scales[r]),
122
+ Math.exp(this.scales[r + 1]),
123
+ Math.exp(this.scales[r + 2])
124
+ ),
125
+ color: new n.Color(
126
+ this.colors[o],
127
+ this.colors[o + 1],
128
+ this.colors[o + 2]
129
+ ),
130
+ opacity: this.opacities[t]
131
+ };
132
+ }
133
+ /**
134
+ * Calculate the bounding box for all splats
135
+ * NOTE: Currently it only uses positions, not the actual splat bounds
136
+ */
137
+ calculateBoundingBox() {
138
+ this.boundingBox.reset();
139
+ const t = new n.Vector3();
140
+ for (let s = 0; s < this.numSplats; s++) {
141
+ const e = s * 3;
142
+ t.set(
143
+ this.positions[e],
144
+ this.positions[e + 1],
145
+ this.positions[e + 2]
146
+ ), this.boundingBox.expandByPoint(t);
147
+ }
148
+ return this.boundingBox;
149
+ }
150
+ /**
151
+ * Create a Three.js BufferGeometry from this splat data
152
+ * This is for visualization/debugging purposes only, not for rendering
153
+ */
154
+ createDebugGeometry() {
155
+ const t = new n.BufferGeometry();
156
+ t.setAttribute(
157
+ "position",
158
+ new n.BufferAttribute(this.positions, 3)
159
+ );
160
+ const s = new Float32Array(this.numSplats * 3), e = new n.Quaternion(), r = new n.Euler();
161
+ for (let o = 0; o < this.numSplats; o++) {
162
+ const i = o * 4, a = o * 3;
163
+ e.set(
164
+ this.rotations[i],
165
+ this.rotations[i + 1],
166
+ this.rotations[i + 2],
167
+ this.rotations[i + 3]
168
+ ), r.setFromQuaternion(e), s[a] = r.x, s[a + 1] = r.y, s[a + 2] = r.z;
169
+ }
170
+ return t.setAttribute(
171
+ "rotation",
172
+ new n.BufferAttribute(s, 3)
173
+ ), t.setAttribute(
174
+ "scale",
175
+ new n.BufferAttribute(this.scales, 3)
176
+ ), t.setAttribute(
177
+ "color",
178
+ new n.BufferAttribute(this.colors, 3)
179
+ ), t.setAttribute(
180
+ "opacity",
181
+ new n.BufferAttribute(this.opacities, 1)
182
+ ), t;
183
+ }
184
+ }
185
+ class dt extends n.EventDispatcher {
186
+ constructor() {
187
+ super();
188
+ c(this, "worker");
189
+ c(this, "centers", null);
190
+ c(this, "orderTexture", null);
191
+ /** Bounding box data for optimization */
192
+ c(this, "chunks", null);
193
+ c(this, "lastCameraPosition", new n.Vector3());
194
+ c(this, "lastCameraDirection", new n.Vector3());
195
+ const s = this.createWorkerCode(), e = new Blob([s], { type: "application/javascript" });
196
+ this.worker = new Worker(URL.createObjectURL(e)), this.worker.onmessage = this.onWorkerMessage.bind(this);
197
+ }
198
+ /**
199
+ * Handles messages received from the sorting worker.
200
+ * @param event The message event from the worker.
201
+ */
202
+ onWorkerMessage(s) {
203
+ if (!this.orderTexture || !this.orderTexture.image)
204
+ return console.error("SplatSorter: Order texture not initialized!");
205
+ const { order: e, count: r } = s.data, o = this.orderTexture.image.data;
206
+ if (!(o instanceof Uint32Array))
207
+ return console.error(
208
+ "SplatSorter: Order texture data is not a Uint32Array!"
209
+ );
210
+ o.set(new Uint32Array(e)), this.orderTexture.needsUpdate = !0;
211
+ const i = o.buffer.slice(0), a = { order: i };
212
+ this.worker.postMessage(a, [i]), this.dispatchEvent({ type: "updated", count: r });
213
+ }
214
+ /**
215
+ * Initializes the sorter with necessary data and textures.
216
+ * @param orderTexture The THREE.DataTexture (R32UI) to store the sorted splat indices.
217
+ * @param centers A Float32Array containing the center position (x, y, z) for each splat.
218
+ * @param chunks Optional: A Float32Array containing bounding box chunk data [minX, minY, minZ, maxX, maxY, maxZ, ...] for optimization.
219
+ */
220
+ init(s, e, r) {
221
+ if (!s || !(s.image.data instanceof Uint32Array))
222
+ throw new Error("SplatSorter: Invalid orderTexture provided. Must be DataTexture with Uint32Array data.");
223
+ if (!e || e.length % 3 !== 0)
224
+ throw new Error("SplatSorter: Invalid centers array provided. Length must be multiple of 3.");
225
+ if (s.image.data.length < e.length / 3)
226
+ throw new Error("SplatSorter: orderTexture data buffer is smaller than the number of splats.");
227
+ const o = e.length / 3;
228
+ this.orderTexture = s, this.centers = e.slice();
229
+ const i = this.orderTexture.image.data;
230
+ for (let g = 0; g < o; g++) i[g] = g;
231
+ this.orderTexture.needsUpdate = !0;
232
+ const a = i.buffer.slice(0), d = this.centers.buffer.slice(0), f = {
233
+ order: a,
234
+ centers: d
235
+ }, v = [
236
+ a,
237
+ d
238
+ ];
239
+ if (r) {
240
+ this.chunks = r.slice();
241
+ const g = this.chunks.buffer.slice(0);
242
+ f.chunks = g, v.push(g);
243
+ }
244
+ this.worker.postMessage(f, v), queueMicrotask(() => {
245
+ this.dispatchEvent({
246
+ type: "updated",
247
+ count: o
248
+ });
249
+ });
250
+ }
251
+ /**
252
+ * Applies an optional mapping to filter or reorder splats before sorting.
253
+ * The sorter will only consider splats whose original indices are present in the mapping.
254
+ * @param mapping A Uint32Array where each element is the *original* index of a splat to include, or null to reset mapping.
255
+ */
256
+ setMapping(s) {
257
+ if (!this.centers)
258
+ return console.warn(
259
+ "SplatSorter: Cannot set mapping before initialization."
260
+ );
261
+ let e;
262
+ const r = [];
263
+ if (!s) {
264
+ const d = this.centers.buffer.slice(0);
265
+ return e = {
266
+ centers: d,
267
+ mapping: null
268
+ }, r.push(d), this.worker.postMessage(e, r);
269
+ }
270
+ const o = new Float32Array(s.length * 3);
271
+ for (let d = 0; d < s.length; d++) {
272
+ const f = s[d];
273
+ if (f * 3 + 2 >= this.centers.length) {
274
+ console.warn(
275
+ `SplatSorter: Mapping index ${f} out of bounds.`
276
+ );
277
+ continue;
278
+ }
279
+ const v = f * 3, g = d * 3;
280
+ o[g + 0] = this.centers[v + 0], o[g + 1] = this.centers[v + 1], o[g + 2] = this.centers[v + 2];
281
+ }
282
+ const i = o.buffer.slice(0), a = s.buffer.slice(0);
283
+ e = {
284
+ centers: i,
285
+ mapping: a
286
+ }, r.push(i, a), this.worker.postMessage(e, r);
287
+ }
288
+ /**
289
+ * Updates the camera parameters used for sorting.
290
+ * @param position The camera's position in the sorter's local coordinate space.
291
+ * @param direction The camera's forward direction in the sorter's local coordinate space.
292
+ */
293
+ setCamera(s, e) {
294
+ const r = this.lastCameraPosition.distanceToSquared(s) > 1e-7, o = this.lastCameraDirection.dot(e) < 0.9999;
295
+ if (!r && !o)
296
+ return;
297
+ this.lastCameraPosition.copy(s), this.lastCameraDirection.copy(e);
298
+ const i = {
299
+ cameraPosition: { x: s.x, y: s.y, z: s.z },
300
+ cameraDirection: { x: e.x, y: e.y, z: e.z }
301
+ };
302
+ this.worker.postMessage(i);
303
+ }
304
+ /**
305
+ * Terminates the Web Worker and cleans up resources.
306
+ */
307
+ dispose() {
308
+ this.worker && this.worker.terminate(), this.orderTexture = null, this.centers = null, this.chunks = null;
309
+ }
310
+ /**
311
+ * Creates the self-contained code for the sorting Web Worker.
312
+ * @returns A string containing the JavaScript code for the worker.
313
+ */
314
+ createWorkerCode() {
315
+ return `(${(function() {
316
+ let e = null, r = null, o = null, i = null, a = null, d = null, f = !1;
317
+ const v = { x: 0, y: 0, z: 0 }, g = { x: 0, y: 0, z: 0 }, p = { x: 0, y: 0, z: 0 }, h = { x: 0, y: 0, z: 0 };
318
+ let l = null, b = null;
319
+ const T = 32, O = new Array(T).fill(0), q = new Array(T).fill(0), E = new Array(T).fill(0), R = (k, C, F) => {
320
+ for (; k <= C; ) {
321
+ const M = C + k >> 1, m = F(M);
322
+ if (m > 0) k = M + 1;
323
+ else if (m < 0) C = M - 1;
324
+ else return M;
325
+ }
326
+ return ~k;
327
+ }, W = () => {
328
+ if (!e || !r || !a || !d)
329
+ return;
330
+ if (r.length === 0) {
331
+ const y = {
332
+ order: e.buffer,
333
+ count: 0
334
+ };
335
+ self.postMessage(y, [e.buffer]), e = null;
336
+ return;
337
+ }
338
+ const k = a.x, C = a.y, F = a.z, M = d.x, m = d.y, B = d.z, V = 1e-4, P = Math.abs(k - v.x) > V || Math.abs(C - v.y) > V || Math.abs(F - v.z) > V, x = Math.abs(M - g.x) > V || Math.abs(m - g.y) > V || Math.abs(B - g.z) > V;
339
+ if (!f && !P && !x)
340
+ return;
341
+ f = !1, v.x = k, v.y = C, v.z = F, g.x = M, g.y = m, g.z = B;
342
+ let u = 1 / 0, S = -1 / 0;
343
+ for (let y = 0; y < 8; ++y) {
344
+ const _ = y & 1 ? p.x : h.x, U = y & 2 ? p.y : h.y, w = y & 4 ? p.z : h.z, D = _ * M + U * m + w * B;
345
+ u = Math.min(u, D), S = Math.max(S, D);
346
+ }
347
+ const I = r.length / 3, L = S - u, z = (1 << Math.max(
348
+ 10,
349
+ Math.min(20, Math.ceil(Math.log2(I / 4)))
350
+ )) + 1;
351
+ if ((!l || l.length !== I) && (l = new Uint32Array(I)), !b || b.length !== z ? b = new Uint32Array(z) : b.fill(0), L < 1e-7) {
352
+ for (let y = 0; y < I; ++y) l[y] = 0;
353
+ b[0] = I;
354
+ } else if (o && o.length > 0) {
355
+ const y = o.length / 6;
356
+ O.fill(0);
357
+ for (let w = 0; w < y; ++w) {
358
+ const D = w * 6, X = o[D + 0], Z = o[D + 1], $ = o[D + 2], H = o[D + 3], Y = X * M + Z * m + $ * B - u, J = Y - H, G = Y + H, Q = Math.max(
359
+ 0,
360
+ Math.floor(J * T / L)
361
+ ), tt = Math.min(
362
+ T,
363
+ Math.ceil(G * T / L)
364
+ );
365
+ for (let N = Q; N < tt; ++N)
366
+ O[N]++;
367
+ }
368
+ let _ = 0;
369
+ for (let w = 0; w < T; ++w) _ += O[w];
370
+ E[0] = 0, q[0] = 0;
371
+ for (let w = 1; w < T; ++w)
372
+ E[w - 1] = O[w - 1] / _ * z >>> 0, q[w] = q[w - 1] + E[w - 1];
373
+ E[T - 1] = O[T - 1] / _ * z >>> 0;
374
+ const U = L / T;
375
+ for (let w = 0; w < I; ++w) {
376
+ const D = w * 3, X = r[D + 0], Z = r[D + 1], $ = r[D + 2], H = X * M + Z * m + $ * B, J = (S - H) / U, G = Math.max(
377
+ 0,
378
+ Math.min(
379
+ T - 1,
380
+ Math.floor(J)
381
+ )
382
+ ), Q = J - G, tt = q[G] + E[G] * Q >>> 0, N = Math.min(tt, z - 1);
383
+ l[w] = N, b[N]++;
384
+ }
385
+ } else {
386
+ const y = (z - 1) / L;
387
+ for (let _ = 0; _ < I; ++_) {
388
+ const U = _ * 3, w = r[U + 0], D = r[U + 1], X = r[U + 2], Z = w * M + D * m + X * B, H = (S - Z) * y >>> 0, Y = Math.min(H, z - 1);
389
+ l[_] = Y, b[Y]++;
390
+ }
391
+ }
392
+ for (let y = 1; y < z; y++)
393
+ b[y] += b[y - 1];
394
+ for (let y = I - 1; y >= 0; y--) {
395
+ const _ = l[y], U = --b[_];
396
+ e[U] = i ? i[y] : y;
397
+ }
398
+ const at = k * M + C * m + F * B, rt = (y) => {
399
+ if (!e) return -1 / 0;
400
+ const _ = e[y], U = _;
401
+ if (!r || U * 3 + 2 >= r.length)
402
+ return -1 / 0;
403
+ const w = U * 3;
404
+ return r[w] * M + r[w + 1] * m + r[w + 2] * B - at;
405
+ };
406
+ let st = I;
407
+ if (I > 0 && rt(I - 1) < 0) {
408
+ const y = R(
409
+ 0,
410
+ I - 1,
411
+ rt
412
+ );
413
+ st = y < 0 ? ~y : y;
414
+ }
415
+ const ct = {
416
+ order: e.buffer,
417
+ count: st
418
+ };
419
+ self.postMessage(ct, [e.buffer]), e = null;
420
+ };
421
+ self.onmessage = (k) => {
422
+ const C = k.data;
423
+ C.order && (e = new Uint32Array(C.order));
424
+ let F = !1;
425
+ if (C.centers && (r = new Float32Array(C.centers), F = !0, f = !0), Object.prototype.hasOwnProperty.call(C, "mapping") && (i = C.mapping ? new Uint32Array(C.mapping) : null, f = !0), C.chunks) {
426
+ if (o = new Float32Array(C.chunks), o.length > 0 && o[3] > 0)
427
+ for (let M = 0; M < o.length / 6; ++M) {
428
+ const m = M * 6, B = o[m + 0], V = o[m + 1], P = o[m + 2], x = o[m + 3], u = o[m + 4], S = o[m + 5];
429
+ o[m + 0] = (B + x) * 0.5, o[m + 1] = (V + u) * 0.5, o[m + 2] = (P + S) * 0.5, o[m + 3] = Math.sqrt(
430
+ (x - B) ** 2 + (u - V) ** 2 + (S - P) ** 2
431
+ ) * 0.5;
432
+ }
433
+ f = !0;
434
+ }
435
+ if (F && r && r.length > 0) {
436
+ p.x = h.x = r[0], p.y = h.y = r[1], p.z = h.z = r[2];
437
+ for (let M = 1; M < r.length / 3; M++) {
438
+ const m = M * 3;
439
+ p.x = Math.min(p.x, r[m + 0]), h.x = Math.max(h.x, r[m + 0]), p.y = Math.min(p.y, r[m + 1]), h.y = Math.max(h.y, r[m + 1]), p.z = Math.min(p.z, r[m + 2]), h.z = Math.max(h.z, r[m + 2]);
440
+ }
441
+ } else F && r && r.length === 0 && (p.x = h.x = p.y = h.y = p.z = h.z = 0);
442
+ C.cameraPosition && (a = C.cameraPosition), C.cameraDirection && (d = C.cameraDirection), W();
443
+ };
444
+ }).toString()})();`;
445
+ }
446
+ }
447
+ const pt = (A, t) => {
448
+ const s = ot(A), e = ot(t);
449
+ return s | e << 16;
450
+ };
451
+ function ot(A) {
452
+ const t = new Float32Array([A]), e = new Int32Array(t.buffer)[0];
453
+ let r = e >> 16 & 32768, o = e >> 12 & 2047;
454
+ const i = e >> 23 & 255;
455
+ return i < 103 ? r : i > 142 ? (r |= 31744, r |= (i === 255 ? 0 : 1) && e & 8388607, r) : i < 113 ? (o |= 2048, r |= (o >> 114 - i) + (o >> 113 - i & 1), r) : (r |= i - 112 << 10 | o >> 1, r += o & 1, r);
456
+ }
457
+ const ft = new ArrayBuffer(4), nt = new DataView(ft), mt = (A) => (nt.setUint32(0, A, !0), nt.getFloat32(0, !0));
458
+ class xt {
459
+ /**
460
+ * Create a new TextureManager for a set of splats
461
+ * @param splatData The splat data to manage textures for
462
+ */
463
+ constructor(t) {
464
+ c(this, "transformA");
465
+ // position xyz and rotation quaternion y,z components
466
+ c(this, "transformB");
467
+ // rotation quaternion x and scale xyz
468
+ c(this, "colorTexture");
469
+ // color an opacity
470
+ c(this, "orderTexture");
471
+ c(this, "textureWidth");
472
+ c(this, "textureHeight");
473
+ const s = t.numSplats;
474
+ this.textureWidth = Math.ceil(Math.sqrt(s)), this.textureHeight = Math.ceil(s / this.textureWidth), this.transformA = this.createTransformATexture(t), this.transformB = this.createTransformBTexture(t), this.colorTexture = this.createColorTexture(t), this.orderTexture = this.createOrderTexture(s);
475
+ }
476
+ /**
477
+ * Create the transform A texture (positions and quaternion w component)
478
+ * @param splatData The splat data
479
+ * @returns DataTexture containing position data
480
+ */
481
+ createTransformATexture(t) {
482
+ const s = t.numSplats, e = new Float32Array(
483
+ this.textureWidth * this.textureHeight * 4
484
+ );
485
+ for (let o = 0; o < s; o++) {
486
+ const i = o * 4, a = o * 3, d = o * 4;
487
+ e[i] = t.positions[a], e[i + 1] = t.positions[a + 1], e[i + 2] = t.positions[a + 2];
488
+ const f = t.rotations[d + 0], v = t.rotations[d + 1], g = pt(f, v);
489
+ e[i + 3] = mt(g);
490
+ }
491
+ const r = new n.DataTexture(
492
+ e,
493
+ this.textureWidth,
494
+ this.textureHeight,
495
+ n.RGBAFormat,
496
+ n.FloatType
497
+ // Store as Float32, shader will reinterpret bits
498
+ );
499
+ return r.needsUpdate = !0, r.magFilter = n.NearestFilter, r.minFilter = n.NearestFilter, r;
500
+ }
501
+ /**
502
+ * Create the transform B texture (scale and rotation xyz)
503
+ * @param splatData The splat data
504
+ * @returns DataTexture containing scale and rotation data
505
+ */
506
+ createTransformBTexture(t) {
507
+ const s = t.numSplats, e = new Float32Array(
508
+ this.textureWidth * this.textureHeight * 4
509
+ );
510
+ for (let o = 0; o < s; o++) {
511
+ const i = o * 4, a = o * 3, d = o * 4;
512
+ e[i] = t.scales[a], e[i + 1] = t.scales[a + 1], e[i + 2] = t.scales[a + 2], e[i + 3] = t.rotations[d + 2];
513
+ }
514
+ const r = new n.DataTexture(
515
+ e,
516
+ this.textureWidth,
517
+ this.textureHeight,
518
+ n.RGBAFormat,
519
+ n.FloatType
520
+ );
521
+ return r.needsUpdate = !0, r.magFilter = n.NearestFilter, r.minFilter = n.NearestFilter, r;
522
+ }
523
+ /**
524
+ * Create the color texture (RGB and opacity)
525
+ * @param splatData The splat data
526
+ * @returns DataTexture containing color data
527
+ */
528
+ createColorTexture(t) {
529
+ const s = t.numSplats, e = new Float32Array(
530
+ this.textureWidth * this.textureHeight * 4
531
+ );
532
+ for (let o = 0; o < s; o++) {
533
+ const i = o * 4, a = o * 3;
534
+ e[i] = t.colors[a], e[i + 1] = t.colors[a + 1], e[i + 2] = t.colors[a + 2], e[i + 3] = t.opacities[o];
535
+ }
536
+ const r = new n.DataTexture(
537
+ e,
538
+ this.textureWidth,
539
+ this.textureHeight,
540
+ n.RGBAFormat,
541
+ n.FloatType
542
+ );
543
+ return r.needsUpdate = !0, r;
544
+ }
545
+ /**
546
+ * Create the order texture for sorting
547
+ * @param numSplats Number of splats
548
+ * @returns DataTexture for storing order indices
549
+ */
550
+ createOrderTexture(t) {
551
+ const s = new Uint32Array(this.textureWidth * this.textureHeight);
552
+ for (let r = 0; r < t; r++)
553
+ s[r] = r;
554
+ const e = new n.DataTexture(
555
+ s,
556
+ this.textureWidth,
557
+ this.textureHeight,
558
+ n.RedIntegerFormat,
559
+ n.UnsignedIntType
560
+ );
561
+ return e.needsUpdate = !0, e;
562
+ }
563
+ dispose() {
564
+ this.transformA.dispose(), this.transformB.dispose(), this.colorTexture.dispose(), this.orderTexture.dispose();
565
+ }
566
+ }
567
+ const yt = (
568
+ /* glsl */
569
+ `
570
+ precision highp float;
571
+ precision highp int;
572
+ precision highp usampler2D;
573
+ precision highp sampler2D;
574
+
575
+ // --- Uniforms (Provided by Three.js) ---
576
+ // uniform mat4 modelViewMatrix; // Don't redeclare
577
+ // uniform mat4 projectionMatrix; // Don't redeclare
578
+ uniform vec2 viewport; // Viewport dimensions (width, height)
579
+ uniform uint numSplats; // Total number of splats potentially renderable by sorter
580
+
581
+ // Textures
582
+ uniform highp sampler2D transformA; // vec4: pos.xyz, packed_rot.yz
583
+ uniform highp sampler2D transformB; // vec4: log_scale.xyz, rot.x
584
+ uniform highp sampler2D splatColor; // vec4: color.rgb, opacity
585
+ uniform highp usampler2D splatOrder; // uint: original splat index
586
+
587
+ // --- Attributes ---
588
+ // Instancing attribute: base index for this block of splats
589
+ attribute uint splatInstanceIndex;
590
+
591
+ // Per-vertex attribute of the quad (Built-in, don't redeclare 'position')
592
+ // attribute vec3 position; // Use the built-in 'position'
593
+ // position.xy: corner coords (-1..1)
594
+ // position.z: index within the instance block (0..INSTANCE_BUFFER_SIZE-1)
595
+
596
+ // --- Varyings ---
597
+ varying vec4 vColor;
598
+ varying vec2 vUv; // For gaussian calculation in fragment shader
599
+
600
+ // --- Constants ---
601
+ #define INSTANCE_BUFFER_SIZE 128.0 // Must match the size in SplatMesh
602
+
603
+ // --- Helper Functions ---
604
+
605
+ // Use GLSL built-in unpackHalf2x16 for proper half-float unpacking
606
+
607
+ // Reconstruct quaternion from packed components (xy_packed, z)
608
+ // Returns (w,x,y,z) format for quatToMat3 compatibility
609
+ vec4 unpackRotation(float xy_packed, float z) {
610
+ vec2 xy = unpackHalf2x16(floatBitsToUint(xy_packed));
611
+ float x = xy.x;
612
+ float y = xy.y;
613
+ float w_squared = 1.0 - x*x - y*y - z*z;
614
+ float w = (w_squared < 0.0) ? 0.0 : sqrt(w_squared);
615
+ return vec4(w, x, y, z); // Return (w,x,y,z)
616
+ }
617
+
618
+ // Convert quaternion to 3x3 rotation matrix
619
+ mat3 quatToMat3(vec4 R) {
620
+ vec4 R2 = R + R;
621
+ float X = R2.x * R.w;
622
+ vec4 Y = R2.y * R;
623
+ vec4 Z = R2.z * R;
624
+ float W = R2.w * R.w;
625
+
626
+ return mat3(
627
+ 1.0 - Z.z - W,
628
+ Y.z + X,
629
+ Y.w - Z.x,
630
+ Y.z - X,
631
+ 1.0 - Y.y - W,
632
+ Z.w + Y.x,
633
+ Y.w + Z.x,
634
+ Z.w - Y.x,
635
+ 1.0 - Y.y - Z.z
636
+ );
637
+ }
638
+
639
+ // Calculate model-space covariance components
640
+ void getModelCovariance(vec3 scale, vec4 rotation, out vec3 covA, out vec3 covB) {
641
+ mat3 rot = quatToMat3(rotation);
642
+
643
+ // M = S * R (scale COLUMNS, then transpose)
644
+ mat3 M = transpose(mat3(
645
+ scale.x * rot[0], // scale.x * column 0
646
+ scale.y * rot[1], // scale.y * column 1
647
+ scale.z * rot[2] // scale.z * column 2
648
+ ));
649
+
650
+ covA = vec3(dot(M[0], M[0]), dot(M[0], M[1]), dot(M[0], M[2]));
651
+ covB = vec3(dot(M[1], M[1]), dot(M[1], M[2]), dot(M[2], M[2]));
652
+ }
653
+
654
+ // --- Main Function ---
655
+ void main(void) {
656
+ // Calculate the final splat index for this vertex
657
+ uint instanceOffset = uint(position.z); // Read index within instance from Z
658
+ uint orderedSplatIndex = splatInstanceIndex + instanceOffset;
659
+
660
+ // Discard if this splat index is beyond the sorted count
661
+ if (orderedSplatIndex >= numSplats) {
662
+ gl_Position = vec4(2.0, 2.0, 2.0, 1.0); // Off-screen
663
+ return;
664
+ }
665
+
666
+ // --- Source Data Lookup ---
667
+ uint texWidth = uint(textureSize(splatOrder, 0).x);
668
+ ivec2 orderUV = ivec2(orderedSplatIndex % texWidth, orderedSplatIndex / texWidth);
669
+ uint originalSplatIndex = texelFetch(splatOrder, orderUV, 0).r;
670
+
671
+ ivec2 dataUV = ivec2(originalSplatIndex % texWidth, originalSplatIndex / texWidth);
672
+
673
+ // Fetch data from textures
674
+ vec4 texTransformA = texelFetch(transformA, dataUV, 0);
675
+ vec4 texTransformB = texelFetch(transformB, dataUV, 0);
676
+ vec4 texColor = texelFetch(splatColor, dataUV, 0);
677
+
678
+ vec3 splatPosition = texTransformA.xyz;
679
+ vec3 splatLogScale = texTransformB.xyz; // Assume stored as log scale
680
+ vec4 splatRotation = unpackRotation(texTransformA.w, texTransformB.w); // Unpack rot xy, z
681
+ vec3 splatColor = texColor.rgb;
682
+ float splatOpacity = texColor.a;
683
+
684
+ vec3 splatScale = exp(splatLogScale); // Convert log scale to actual scale
685
+
686
+ // --- Center Projection ---
687
+
688
+ // Compute normalized view-space center
689
+ vec4 centerClipRaw = modelViewMatrix * vec4(splatPosition, 1.0);
690
+ vec3 centerView = centerClipRaw.xyz / centerClipRaw.w;
691
+
692
+ // Early out if splat is behind the camera
693
+ if (centerView.z > -0.2) { // Use small epsilon
694
+ gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
695
+ return;
696
+ }
697
+
698
+ // Project to clip space
699
+ vec4 centerClip = projectionMatrix * centerClipRaw;
700
+
701
+ // --- Covariance Calculation & Projection ---
702
+
703
+ // Calculating model space covariance (Vrk)
704
+ vec3 covA, covB;
705
+ getModelCovariance(splatScale, splatRotation, covA, covB);
706
+ mat3 Vrk = mat3(
707
+ covA.x, covA.y, covA.z,
708
+ covA.y, covB.x, covB.y, // covB.x is Vrk[1][1]
709
+ covA.z, covB.y, covB.z // covB.y is Vrk[1][2], covB.z is Vrk[2][2]
710
+ );
711
+
712
+ // Calculate Jacobian J (screen space change wrt view space change)
713
+ // Requres focal lenghts and view-space center depth (tz)
714
+ float focalX = viewport.x * projectionMatrix[0][0];
715
+ float focalY = viewport.y * projectionMatrix[1][1];
716
+ float tz = centerView.z;
717
+ float tz2 = tz * tz;
718
+
719
+ // Jacobian J = [fx/tz, 0, -fx*tx/tz^2]
720
+ // [0, fy/tz, -fy*ty/tz^2]
721
+ mat3 J = mat3(
722
+ focalX / tz, 0.0, -focalX * centerView.x / tz2,
723
+ 0.0, focalY / tz, -focalY * centerView.y / tz2,
724
+ 0.0, 0.0, 0.0
725
+ );
726
+
727
+ // Calculate W
728
+ mat3 W = transpose(mat3(modelViewMatrix));
729
+ // Calculate T = W * J
730
+ mat3 T = W * J;
731
+ // Calculate Projected 2D Covariance: SigmaProj = transpose(T) * Vrk * T
732
+ mat3 SigmaProj = transpose(T) * Vrk * T;
733
+
734
+ // Extract 2D components and add bias
735
+ float cov2D_11 = SigmaProj[0][0] + 0.3;
736
+ float cov2D_12 = SigmaProj[0][1];
737
+ float cov2D_22 = SigmaProj[1][1] + 0.3;
738
+
739
+ // --- Calculate 2D Screen Space Rotation & Scale ---
740
+
741
+ float det = cov2D_11 * cov2D_22 - cov2D_12 * cov2D_12;
742
+ // Ensure determinant is non-negative before sqrt
743
+ if (det <= 0.0) {
744
+ gl_Position = vec4(2.0, 2.0, 2.0, 1.0); // Discard degenerated
745
+ return;
746
+ }
747
+
748
+ float trace = cov2D_11 + cov2D_22;
749
+ float traceOver2 = 0.5 * trace;
750
+ float discriminantSqrt = sqrt(max(traceOver2 * traceOver2 - det, 0.0)); // Avoid sqrt(negative)
751
+
752
+ float lambda1 = traceOver2 + discriminantSqrt; // Larger eigenvalue
753
+ float lambda2 = max(0.1, traceOver2 - discriminantSqrt); // Smaller eigenvalue, clamped
754
+
755
+ // Compute eigenvectors
756
+
757
+ vec2 v1_eigen, v2_eigen;
758
+
759
+ // Handle diagonal case
760
+ if(abs(cov2D_12) < 1e-16) {
761
+ v1_eigen = vec2(1.0, 0.0);
762
+ } else {
763
+ v1_eigen = normalize(vec2(cov2D_12, lambda1 - cov2D_11)); // diagonal choice
764
+ }
765
+
766
+ v2_eigen = vec2(-v1_eigen.y, v1_eigen.x); // Perpendicular eigenvector
767
+
768
+ // Calculate SCALED vectors (l1, l2, incorparate factor)
769
+ float scaleFactor = 2.0 * sqrt(2.0); // ~2.8284
770
+ float l1 = sqrt(lambda1) * scaleFactor; // scaleX
771
+ float l2 = sqrt(lambda2) * scaleFactor; // scaleY
772
+
773
+ float vmin = min(512.0, min(viewport.x, viewport.y));
774
+ l1 = min(l1, 2.0 * vmin);
775
+ l2 = min(l2, 2.0 * vmin);
776
+
777
+ // Early out tiny splats
778
+ if (l1 < 2.0 && l2 < 2.0) { // Check if smaller than ~2 pixel
779
+ gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
780
+ return;
781
+ }
782
+
783
+ // Scaled eigenvectors for offset calculation
784
+ vec2 v1_scaled = l1 * v1_eigen;
785
+ vec2 v2_scaled = l2 * v2_eigen;
786
+
787
+ // --- FRUSTUM CHECK (laxo, estilo PlayCanvas) ---
788
+
789
+ vec2 c = centerClip.ww / viewport; // pixel to clip
790
+ float r = max(l1, l2); // radius in pixels
791
+
792
+ // Remove if the center - radius is already out of -w..w
793
+ if (any(greaterThan(abs(centerClip.xy) - vec2(r) * c, centerClip.ww))) {
794
+ gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
795
+ return;
796
+ }
797
+
798
+ // --- END FRUSTUM CHECK ---
799
+
800
+ // --- Final Vertex Position ---
801
+
802
+ vec2 cornerOffset = position.xy; // (-1,-1) to (1,1)
803
+
804
+ // Clip the quad so only high-alpha core is visible
805
+ float alpha = max(splatOpacity, 1e-6); // avoid log(0)
806
+ float clip = min(1.0, sqrt(-log(1.0 / 255.0 / alpha)) / 2.0);
807
+
808
+ // Apply clip to the corner offset *before* calculating screen space offset
809
+ vec2 clippedCornerOffset = cornerOffset * clip;
810
+ vec2 screenOffsetPixels = clippedCornerOffset.x * v1_scaled + clippedCornerOffset.y * v2_scaled;
811
+
812
+ // Convert pixel offset to clip space offset
813
+ vec2 clipOffset = screenOffsetPixels * (centerClip.w / viewport);
814
+
815
+
816
+ // Apply offset to center clip position
817
+ gl_Position = centerClip + vec4(clipOffset, 0.0, 0.0);
818
+
819
+ // --- Pass data to Fragment Shader ---
820
+
821
+ vColor = vec4(splatColor, splatOpacity);
822
+ vUv = clippedCornerOffset;
823
+ }
824
+ `
825
+ ), gt = (
826
+ /* glsl */
827
+ `
828
+ precision highp float;
829
+
830
+ varying vec4 vColor; // Color and opacity passed from vertex shader
831
+ varying vec2 vUv; // Quad UV coordinates (-1 to 1) passed from vertex shader
832
+
833
+ // Fast approximate e^x based on https://nic.schraudolph.org/pubs/Schraudolph99.pdf
834
+ const float EXP_A = 12102203.0; // ≈ 2^23 / ln(2)
835
+ const int EXP_BC_RMS = 1064866808; // (127 << 23) - 60801 * 8
836
+ float fastExp(float x) {
837
+ int i = int(EXP_A * x) + EXP_BC_RMS;
838
+ return intBitsToFloat(i);
839
+ }
840
+
841
+ void main(void) {
842
+
843
+ float distSq = dot(vUv, vUv); // Calculate squared distance from center (in the quad's coordinate system)
844
+ if (distSq > 1.0) discard; // Discard fragments outside the circle inscribed in the quad
845
+
846
+ // Calculate Gaussian function: alpha = opacity * exp(-distSq * factor)
847
+ // The factor 4.0 corresponds to the original implementation's scaling.
848
+ // factor = 1 / (2 * sigma^2), where sigma controls the spread.
849
+ // Using 4.0 implies sigma^2 = 1/8.
850
+ float alpha = fastExp(-distSq * 4.0) * vColor.a;
851
+
852
+ if (alpha < 1.0 / 255.0) discard; // Discard fragments with very low alpha
853
+
854
+ // Premultiply color by alpha (required for correct blending)
855
+ gl_FragColor = vec4(vColor.rgb * alpha, alpha);
856
+ }
857
+ `
858
+ );
859
+ class vt extends n.ShaderMaterial {
860
+ constructor(t = {}) {
861
+ const s = {
862
+ // Textures (values set via methods)
863
+ transformA: { value: null },
864
+ transformB: { value: null },
865
+ splatColor: { value: null },
866
+ splatOrder: { value: null },
867
+ // Other uniforms
868
+ viewport: { value: new n.Vector2(1, 1) },
869
+ // Will be updated
870
+ numSplats: { value: 0 }
871
+ // Max splats to render (updated by sorter)
872
+ };
873
+ super({
874
+ vertexShader: yt,
875
+ fragmentShader: gt,
876
+ uniforms: s,
877
+ transparent: !0,
878
+ blending: n.CustomBlending,
879
+ // Premultiplied alpha blending:
880
+ // color = src_color * src_alpha + dst_color * (1 - src_alpha)
881
+ // alpha = src_alpha * 1 + dst_alpha * (1 - src_alpha)
882
+ blendSrc: n.OneFactor,
883
+ // Using ONE because shader outputs premultiplied color (color * alpha)
884
+ blendDst: n.OneMinusSrcAlphaFactor,
885
+ blendSrcAlpha: n.OneFactor,
886
+ // source alpha comes from shader
887
+ blendDstAlpha: n.OneMinusSrcAlphaFactor,
888
+ blendEquation: n.AddEquation,
889
+ blendEquationAlpha: n.AddEquation,
890
+ depthTest: !0,
891
+ depthWrite: !1,
892
+ // Disable depth write for transparency
893
+ side: n.DoubleSide,
894
+ // Render both sides (or CULLFACE_NONE equivalent)
895
+ // Optional settings from constructor
896
+ alphaTest: t.alphaTest !== void 0 ? t.alphaTest : 0,
897
+ // Typically 0 for blending
898
+ toneMapped: t.toneMapped !== void 0 ? t.toneMapped : !1
899
+ // prettier-ignore
900
+ }), t.alphaHash && (this.alphaHash = !0, this.depthWrite = !0, this.blending = n.NoBlending);
901
+ }
902
+ /**
903
+ * Update the viewport size
904
+ * @param width Viewport width
905
+ * @param height Viewport height
906
+ */
907
+ updateViewport(t, s) {
908
+ this.uniforms.viewport.value.set(t, s);
909
+ }
910
+ /**
911
+ * Set transform texture A (positions)
912
+ * @param texture Texture containing positions
913
+ */
914
+ setTransformA(t) {
915
+ this.uniforms.transformA.value = t;
916
+ }
917
+ /**
918
+ * Set transform texture B (rotation, scale)
919
+ * @param texture Texture containing rotation and scale data
920
+ */
921
+ setTransformB(t) {
922
+ this.uniforms.transformB.value = t;
923
+ }
924
+ /**
925
+ * Set color texture
926
+ * @param texture Texture containing colors
927
+ */
928
+ setColorTexture(t) {
929
+ this.uniforms.splatColor.value = t;
930
+ }
931
+ /**
932
+ * Set order texture
933
+ * @param texture Texture containing sort order
934
+ */
935
+ setOrderTexture(t) {
936
+ this.uniforms.splatOrder.value = t;
937
+ }
938
+ /**
939
+ * Set number of splats to render
940
+ * @param count Number of splats
941
+ */
942
+ setNumSplats(t) {
943
+ this.uniforms.numSplats.value = t;
944
+ }
945
+ }
946
+ const j = class j extends n.Mesh {
947
+ // Match shader constant
948
+ /**
949
+ * Create a new SplatMesh for rendering Gaussian splats
950
+ * @param splatData The splat data to render
951
+ * @param options Rendering options
952
+ */
953
+ constructor(s, e = {}) {
954
+ const r = new vt(e), o = j.createInstancedGeometry(s.numSplats, j.INSTANCE_SIZE);
955
+ super(o, r);
956
+ c(this, "sorter");
957
+ c(this, "splatData");
958
+ c(this, "options");
959
+ c(this, "textureManager");
960
+ c(this, "material");
961
+ c(this, "geometry");
962
+ c(this, "lastCameraPositionLocal", new n.Vector3());
963
+ c(this, "lastCameraDirectionLocal", new n.Vector3());
964
+ c(this, "invModelMatrix", new n.Matrix4());
965
+ // Cached inverse matrix
966
+ c(this, "_vpW", -1);
967
+ c(this, "_vpH", -1);
968
+ c(this, "_size", new n.Vector2());
969
+ c(this, "_camPosW", new n.Vector3());
970
+ c(this, "_camDirW", new n.Vector3());
971
+ c(this, "_camPosL", new n.Vector3());
972
+ c(this, "_camDirL", new n.Vector3());
973
+ c(this, "onSorterUpdated", (s) => {
974
+ const e = s.count;
975
+ this.geometry.instanceCount = Math.ceil(e / j.INSTANCE_SIZE), this.material.setNumSplats(e);
976
+ });
977
+ this.geometry = o, this.material = r, this.splatData = s, this.frustumCulled = !1, this.options = {
978
+ autoSort: !0,
979
+ ...e
980
+ }, this.textureManager = new xt(s), this.sorter = new dt();
981
+ let i = this.createChunks() || void 0;
982
+ i === null && console.warn("Visus: Could not create sorter chunks, bounding box might be invalid."), i = void 0, this.sorter.addEventListener("updated", this.onSorterUpdated), this.sorter.init(
983
+ this.textureManager.orderTexture,
984
+ this.splatData.centers,
985
+ i ?? void 0
986
+ ), this.material.setTransformA(this.textureManager.transformA), this.material.setTransformB(this.textureManager.transformB), this.material.setColorTexture(this.textureManager.colorTexture), this.material.setOrderTexture(this.textureManager.orderTexture), this.material.setNumSplats(0), this.geometry.boundingBox = s.boundingBox.toBox3(), this.geometry.boundingSphere = new n.Sphere(), this.geometry.boundingBox.getBoundingSphere(this.geometry.boundingSphere);
987
+ }
988
+ /**
989
+ * Creates the instanced geometry for rendering splats.
990
+ * @param totalSplats Total number of splats in the data.
991
+ * @param instanceSize Number of splats per instance.
992
+ * @returns InstancedBufferGeometry
993
+ */
994
+ static createInstancedGeometry(s, e) {
995
+ const r = Math.ceil(s / e), o = new n.BufferGeometry(), i = new Float32Array([
996
+ // x, y, splat_index_in_instance
997
+ -1,
998
+ -1,
999
+ 0,
1000
+ 1,
1001
+ -1,
1002
+ 0,
1003
+ 1,
1004
+ 1,
1005
+ 0,
1006
+ -1,
1007
+ 1,
1008
+ 0
1009
+ ]), a = new Uint16Array([0, 1, 2, 0, 2, 3]), d = new Float32Array(4 * 3 * e);
1010
+ for (let p = 0; p < e; p++) {
1011
+ const h = p * 4 * 3;
1012
+ for (let l = 0; l < 4; l++)
1013
+ d[h + l * 3 + 0] = i[l * 3 + 0], d[h + l * 3 + 1] = i[l * 3 + 1], d[h + l * 3 + 2] = p;
1014
+ }
1015
+ const f = new Uint32Array(6 * e);
1016
+ for (let p = 0; p < e; p++) {
1017
+ const h = p * 6, l = p * 4;
1018
+ f[h + 0] = a[0] + l, f[h + 1] = a[1] + l, f[h + 2] = a[2] + l, f[h + 3] = a[3] + l, f[h + 4] = a[4] + l, f[h + 5] = a[5] + l;
1019
+ }
1020
+ o.setAttribute("position", new n.BufferAttribute(d, 3)), o.setIndex(new n.BufferAttribute(f, 1));
1021
+ const v = new n.InstancedBufferGeometry();
1022
+ v.index = o.index, v.setAttribute("position", o.getAttribute("position"));
1023
+ const g = new Uint32Array(r);
1024
+ for (let p = 0; p < r; p++)
1025
+ g[p] = p * e;
1026
+ return v.setAttribute("splatInstanceIndex", new n.InstancedBufferAttribute(g, 1, !1)), v.instanceCount = 0, v;
1027
+ }
1028
+ /**
1029
+ * Create chunks data (bounding box min/max) for the sorter.
1030
+ * @returns Float32Array containing chunk data [minX, minY, minZ, maxX, maxY, maxZ] or null.
1031
+ */
1032
+ createChunks() {
1033
+ const s = this.splatData.boundingBox;
1034
+ return s.min.x === 1 / 0 || s.max.x === -1 / 0 ? null : new Float32Array([
1035
+ s.min.x,
1036
+ s.min.y,
1037
+ s.min.z,
1038
+ s.max.x,
1039
+ s.max.y,
1040
+ s.max.z
1041
+ ]);
1042
+ }
1043
+ /**
1044
+ * Update the viewport size
1045
+ * @param width Viewport width
1046
+ * @param height Viewport height
1047
+ */
1048
+ updateViewport(s, e) {
1049
+ s === this._vpW && e === this._vpH || (this._vpW = s, this._vpH = e, this.material.updateViewport(s, e));
1050
+ }
1051
+ /**
1052
+ * Sorts splats based on camera position and direction.
1053
+ * @param camera The camera to sort against.
1054
+ */
1055
+ sort(s) {
1056
+ s.getWorldPosition(this._camPosW), s.getWorldDirection(this._camDirW), this.invModelMatrix.copy(this.matrixWorld).invert(), this._camPosL.copy(this._camPosW).applyMatrix4(this.invModelMatrix), this._camDirL.copy(this._camDirW).transformDirection(this.invModelMatrix);
1057
+ const e = this.lastCameraPositionLocal.distanceToSquared(this._camPosL) > 1e-6, r = this.lastCameraDirectionLocal.dot(this._camDirL) < 0.999;
1058
+ this.options.autoSort && (e || r) && (this.lastCameraPositionLocal.copy(this._camPosL), this.lastCameraDirectionLocal.copy(this._camDirL), this.sorter.setCamera(this._camPosL, this._camDirL));
1059
+ }
1060
+ /**
1061
+ * THREE.js hook called before rendering the object.
1062
+ * Used here to trigger sorting and update viewport.
1063
+ * @param renderer The renderer
1064
+ * @param scene The scene
1065
+ * @param camera The camera
1066
+ */
1067
+ // prettier-ignore
1068
+ // @ts-expect-error scene is not used
1069
+ onBeforeRender(s, e, r) {
1070
+ this.sort(r), s.getSize(this._size), this.updateViewport(this._size.x, this._size.y);
1071
+ }
1072
+ /**
1073
+ * Dispose of resources
1074
+ */
1075
+ dispose() {
1076
+ this.sorter.removeEventListener("updated", this.onSorterUpdated), this.sorter.dispose(), this.geometry.dispose(), this.material.dispose(), this.textureManager.dispose();
1077
+ }
1078
+ };
1079
+ /** Number of splats combined into a single instanced draw call. */
1080
+ c(j, "INSTANCE_SIZE", 128);
1081
+ let it = j;
1082
+ class bt extends n.Loader {
1083
+ /**
1084
+ * Load a PLY file with Gaussian Splat data
1085
+ * @param url URL of the PLY file
1086
+ * @param onLoad Optional callback when loading is complete
1087
+ * @param onProgress Optional progress callback
1088
+ * @param onError Optional error callback
1089
+ */
1090
+ load(t, s, e, r) {
1091
+ const o = new n.FileLoader(this.manager);
1092
+ o.setResponseType("arraybuffer"), o.setRequestHeader(this.requestHeader), o.setPath(this.path), o.setWithCredentials(this.withCredentials), o.load(
1093
+ t,
1094
+ (i) => {
1095
+ try {
1096
+ if (s) {
1097
+ const a = this.parse(i);
1098
+ s(a);
1099
+ }
1100
+ } catch (a) {
1101
+ r ? r(a) : console.error(a), this.manager.itemError(t);
1102
+ }
1103
+ },
1104
+ e,
1105
+ r
1106
+ );
1107
+ }
1108
+ /**
1109
+ * Load a PLY file asynchronously and return a Promise
1110
+ * @param url URL of the PLY file
1111
+ * @param onProgress Optional progress callback
1112
+ * @returns A Promise that resolves with the parsed SplatData
1113
+ */
1114
+ loadAsync(t, s) {
1115
+ return new Promise((e, r) => {
1116
+ const o = new n.FileLoader(this.manager);
1117
+ o.setResponseType("arraybuffer"), o.setRequestHeader(this.requestHeader), o.setPath(this.path), o.setWithCredentials(this.withCredentials), o.load(
1118
+ t,
1119
+ (i) => {
1120
+ try {
1121
+ const a = this.parse(i);
1122
+ e(a);
1123
+ } catch (a) {
1124
+ r(a), this.manager.itemError(t);
1125
+ }
1126
+ },
1127
+ s,
1128
+ (i) => {
1129
+ r(i), this.manager.itemError(t);
1130
+ }
1131
+ );
1132
+ });
1133
+ }
1134
+ /**
1135
+ * Parse PLY buffer data into SplatData
1136
+ * @param buffer ArrayBuffer containing PLY data
1137
+ * @returns Parsed SplatData
1138
+ */
1139
+ parse(t) {
1140
+ const s = new TextDecoder(), e = new Uint8Array(t), r = [112, 108, 121, 10], o = `
1141
+ end_header
1142
+ `;
1143
+ for (let x = 0; x < r.length; x++)
1144
+ if (e[x] !== r[x])
1145
+ throw new Error("Invalid PLY file: Missing magic bytes");
1146
+ let i = 0;
1147
+ for (let x = 0; x < e.length - o.length; x++) {
1148
+ let u = !0;
1149
+ for (let S = 0; S < o.length; S++)
1150
+ if (e[x + S] !== o.charCodeAt(S)) {
1151
+ u = !1;
1152
+ break;
1153
+ }
1154
+ if (u) {
1155
+ i = x + o.length;
1156
+ break;
1157
+ }
1158
+ }
1159
+ if (i === 0)
1160
+ throw new Error("Invalid PLY file: Could not find end of header");
1161
+ const d = s.decode(e.subarray(0, i)).split(`
1162
+ `), f = [];
1163
+ let v = null;
1164
+ for (let x = 1; x < d.length; x++) {
1165
+ const u = d[x].trim();
1166
+ if (u === "" || u === "end_header") continue;
1167
+ const S = u.split(" ");
1168
+ switch (S[0]) {
1169
+ case "format":
1170
+ v = S[1];
1171
+ break;
1172
+ case "element":
1173
+ f.push({
1174
+ name: S[1],
1175
+ count: parseInt(S[2], 10),
1176
+ properties: []
1177
+ });
1178
+ break;
1179
+ case "property":
1180
+ if (f.length === 0)
1181
+ throw new Error(
1182
+ "Invalid PLY file: Property without element"
1183
+ );
1184
+ f[f.length - 1].properties.push({
1185
+ type: S[1],
1186
+ name: S[2],
1187
+ storage: null
1188
+ });
1189
+ break;
1190
+ }
1191
+ }
1192
+ if (v !== "binary_little_endian")
1193
+ throw new Error(`Unsupported PLY format: ${v}`);
1194
+ const g = f.find((x) => x.name === "vertex");
1195
+ if (!g)
1196
+ throw new Error("Invalid PLY file: No vertex element found");
1197
+ const p = new ut(g.count), h = new DataView(t);
1198
+ let l = i;
1199
+ const b = (x) => g.properties.findIndex((u) => u.name === x), T = b("x"), O = b("y"), q = b("z"), E = [
1200
+ b("rot_0"),
1201
+ b("rot_1"),
1202
+ b("rot_2"),
1203
+ b("rot_3")
1204
+ ], R = [
1205
+ b("scale_0"),
1206
+ b("scale_1"),
1207
+ b("scale_2")
1208
+ ], W = [
1209
+ b("f_dc_0"),
1210
+ b("f_dc_1"),
1211
+ b("f_dc_2")
1212
+ ], k = b("opacity");
1213
+ if ([
1214
+ T,
1215
+ O,
1216
+ q,
1217
+ ...E,
1218
+ ...R,
1219
+ ...W,
1220
+ k
1221
+ ].some((x) => x === -1))
1222
+ throw new Error("Invalid PLY file: Missing required properties");
1223
+ const F = 0.28209479177387814, M = (x) => {
1224
+ if (x > 0) return 1 / (1 + Math.exp(-x));
1225
+ const u = Math.exp(x);
1226
+ return u / (1 + u);
1227
+ }, m = new n.Vector3(), B = new n.Quaternion(), V = new n.Vector3(), P = new n.Color();
1228
+ for (let x = 0; x < g.count; x++) {
1229
+ const u = [];
1230
+ for (let I = 0; I < g.properties.length; I++) {
1231
+ const K = g.properties[I].type;
1232
+ let z;
1233
+ switch (K) {
1234
+ case "char":
1235
+ z = h.getInt8(l), l += 1;
1236
+ break;
1237
+ case "uchar":
1238
+ z = h.getUint8(l), l += 1;
1239
+ break;
1240
+ case "short":
1241
+ z = h.getInt16(l, !0), l += 2;
1242
+ break;
1243
+ case "ushort":
1244
+ z = h.getUint16(l, !0), l += 2;
1245
+ break;
1246
+ case "int":
1247
+ z = h.getInt32(l, !0), l += 4;
1248
+ break;
1249
+ case "uint":
1250
+ z = h.getUint32(l, !0), l += 4;
1251
+ break;
1252
+ case "float":
1253
+ z = h.getFloat32(l, !0), l += 4;
1254
+ break;
1255
+ case "double":
1256
+ z = h.getFloat64(l, !0), l += 8;
1257
+ break;
1258
+ default:
1259
+ throw new Error(`Unsupported property type: ${K}`);
1260
+ }
1261
+ u.push(z);
1262
+ }
1263
+ m.set(
1264
+ u[T],
1265
+ u[O],
1266
+ u[q]
1267
+ ), B.set(
1268
+ u[E[1]],
1269
+ // PLY stores rot 1,2,3,0 (x,y,z,w)
1270
+ u[E[2]],
1271
+ u[E[3]],
1272
+ u[E[0]]
1273
+ ).normalize(), V.set(
1274
+ u[R[0]],
1275
+ // Read directly assuming it's log scale
1276
+ u[R[1]],
1277
+ u[R[2]]
1278
+ ), P.set(
1279
+ 0.5 + u[W[0]] * F,
1280
+ 0.5 + u[W[1]] * F,
1281
+ 0.5 + u[W[2]] * F
1282
+ ), P.r = Math.max(0, Math.min(1, P.r)), P.g = Math.max(0, Math.min(1, P.g)), P.b = Math.max(0, Math.min(1, P.b));
1283
+ const S = M(u[k]);
1284
+ p.setSplat(
1285
+ x,
1286
+ m,
1287
+ B,
1288
+ V,
1289
+ // Pass log scale directly
1290
+ P,
1291
+ S
1292
+ );
1293
+ }
1294
+ return p.calculateBoundingBox(), p;
1295
+ }
1296
+ }
1297
+ const Mt = "0.3.0";
1298
+ export {
1299
+ et as BoundingBox,
1300
+ bt as PlyLoader,
1301
+ ut as SplatData,
1302
+ vt as SplatMaterial,
1303
+ it as SplatMesh,
1304
+ dt as SplatSorter,
1305
+ xt as TextureManager,
1306
+ Mt as VERSION
1307
+ };