@playcanvas/splat-transform 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1133 @@
1
+ import { open } from 'node:fs/promises';
2
+ import { Buffer } from 'node:buffer';
3
+
4
+ var version = "0.1.0";
5
+
6
+ const getDataType = (type) => {
7
+ switch (type) {
8
+ case 'char': return Int8Array;
9
+ case 'uchar': return Uint8Array;
10
+ case 'short': return Int16Array;
11
+ case 'ushort': return Uint16Array;
12
+ case 'int': return Int32Array;
13
+ case 'uint': return Uint32Array;
14
+ case 'float': return Float32Array;
15
+ case 'double': return Float64Array;
16
+ default: return null;
17
+ }
18
+ };
19
+ const calcDataSize = (plyFile) => {
20
+ let result = 0;
21
+ for (const element of plyFile.elements) {
22
+ for (const property of element.properties) {
23
+ result += getDataType(property.type).BYTES_PER_ELEMENT * element.count;
24
+ }
25
+ }
26
+ return result;
27
+ };
28
+ const shNames = new Array(45).fill('').map((_, i) => `f_rest_${i}`);
29
+
30
+ // parse the ply header text and return an array of Element structures and a
31
+ // string containing the ply format
32
+ const parsePlyHeader = (data) => {
33
+ // decode header and split into lines
34
+ const strings = new TextDecoder('ascii')
35
+ .decode(data)
36
+ .split('\n')
37
+ .filter(line => line);
38
+ const elements = [];
39
+ let element;
40
+ for (let i = 1; i < strings.length; ++i) {
41
+ const words = strings[i].split(' ');
42
+ switch (words[0]) {
43
+ case 'ply':
44
+ case 'format':
45
+ case 'comment':
46
+ case 'end_header':
47
+ // skip
48
+ break;
49
+ case 'element': {
50
+ if (words.length !== 3) {
51
+ throw new Error('invalid ply header');
52
+ }
53
+ element = {
54
+ name: words[1],
55
+ count: parseInt(words[2], 10),
56
+ properties: []
57
+ };
58
+ elements.push(element);
59
+ break;
60
+ }
61
+ case 'property': {
62
+ if (!element || words.length !== 3 || !getDataType(words[1])) {
63
+ throw new Error('invalid ply header');
64
+ }
65
+ element.properties.push({
66
+ name: words[2],
67
+ type: words[1]
68
+ });
69
+ break;
70
+ }
71
+ default: {
72
+ throw new Error(`unrecognized header value '${words[0]}' in ply header`);
73
+ }
74
+ }
75
+ }
76
+ return { strings, elements };
77
+ };
78
+ const cmp = (a, b, aOffset = 0) => {
79
+ for (let i = 0; i < b.length; ++i) {
80
+ if (a[aOffset + i] !== b[i]) {
81
+ return false;
82
+ }
83
+ }
84
+ return true;
85
+ };
86
+ const magicBytes = new Uint8Array([112, 108, 121, 10]); // ply\n
87
+ const endHeaderBytes = new Uint8Array([10, 101, 110, 100, 95, 104, 101, 97, 100, 101, 114, 10]); // \nend_header\n
88
+ const readPly = async (fileHandle) => {
89
+ // we don't support ply text header larger than 128k
90
+ const headerBuf = Buffer.alloc(128 * 1024);
91
+ // smallest possible header size
92
+ let headerSize = magicBytes.length + endHeaderBytes.length;
93
+ if ((await fileHandle.read(headerBuf, 0, headerSize)).bytesRead !== headerSize) {
94
+ throw new Error('failed to read file header');
95
+ }
96
+ if (!cmp(headerBuf, magicBytes)) {
97
+ throw new Error('invalid file header');
98
+ }
99
+ // read the rest of the header till we find end header byte pattern
100
+ while (true) {
101
+ // read the next character
102
+ if ((await fileHandle.read(headerBuf, headerSize++, 1)).bytesRead !== 1) {
103
+ throw new Error('failed to read file header');
104
+ }
105
+ // check if we've reached the end of the header
106
+ if (cmp(headerBuf, endHeaderBytes, headerSize - endHeaderBytes.length)) {
107
+ break;
108
+ }
109
+ }
110
+ // parse the header
111
+ const header = parsePlyHeader(headerBuf.subarray(0, headerSize));
112
+ const dataSize = calcDataSize(header);
113
+ const data = Buffer.alloc(dataSize);
114
+ if ((await fileHandle.read(data, 0, dataSize)).bytesRead !== dataSize) {
115
+ throw new Error('failed reading ply data');
116
+ }
117
+ return { header, data };
118
+ };
119
+
120
+ // wraps ply file data and adds helpers accessors
121
+ class Splat {
122
+ plyFile;
123
+ vertex;
124
+ properties = {};
125
+ constructor(plyFile) {
126
+ this.plyFile = plyFile;
127
+ // find vertex element and populate property offsets
128
+ let offset = 0;
129
+ for (let i = 0; i < plyFile.header.elements.length; ++i) {
130
+ const element = plyFile.header.elements[i];
131
+ if (element.name === 'vertex') {
132
+ this.vertex = element;
133
+ }
134
+ for (let j = 0; j < element.properties.length; ++j) {
135
+ const property = element.properties[j];
136
+ if (element === this.vertex) {
137
+ this.properties[property.name] = {
138
+ type: property.type,
139
+ offset
140
+ };
141
+ }
142
+ offset += getDataType(property.type).BYTES_PER_ELEMENT;
143
+ }
144
+ }
145
+ }
146
+ // return the total number of splats
147
+ get numSplats() {
148
+ return this.vertex?.count;
149
+ }
150
+ // return the number of spherical harmonic bands present in the data
151
+ get numSHBands() {
152
+ return { '9': 1, '24': 2, '-1': 3 }[shNames.findIndex(v => !this.properties.hasOwnProperty(v))] ?? 0;
153
+ }
154
+ // simple iterator that assumes input data is float32
155
+ createIterator(fields, result) {
156
+ const offsets = fields.map(f => this.properties[f].offset / 4);
157
+ const float32 = new Float32Array(this.plyFile.data.buffer);
158
+ return (index) => {
159
+ const base = index * this.vertex.properties.length;
160
+ for (let i = 0; i < fields.length; ++i) {
161
+ result[i] = float32[base + offsets[i]];
162
+ }
163
+ };
164
+ }
165
+ }
166
+
167
+ const math = {
168
+ DEG_TO_RAD: Math.PI / 180,
169
+ RAD_TO_DEG: 180 / Math.PI,
170
+ clamp(value, min, max) {
171
+ if (value >= max) return max;
172
+ if (value <= min) return min;
173
+ return value;
174
+ },
175
+ intToBytes24(i) {
176
+ const r = i >> 16 & 0xff;
177
+ const g = i >> 8 & 0xff;
178
+ const b = i & 0xff;
179
+ return [r, g, b];
180
+ },
181
+ intToBytes32(i) {
182
+ const r = i >> 24 & 0xff;
183
+ const g = i >> 16 & 0xff;
184
+ const b = i >> 8 & 0xff;
185
+ const a = i & 0xff;
186
+ return [r, g, b, a];
187
+ },
188
+ bytesToInt24(r, g, b) {
189
+ if (r.length) {
190
+ b = r[2];
191
+ g = r[1];
192
+ r = r[0];
193
+ }
194
+ return r << 16 | g << 8 | b;
195
+ },
196
+ bytesToInt32(r, g, b, a) {
197
+ if (r.length) {
198
+ a = r[3];
199
+ b = r[2];
200
+ g = r[1];
201
+ r = r[0];
202
+ }
203
+ return (r << 24 | g << 16 | b << 8 | a) >>> 0;
204
+ },
205
+ lerp(a, b, alpha) {
206
+ return a + (b - a) * math.clamp(alpha, 0, 1);
207
+ },
208
+ lerpAngle(a, b, alpha) {
209
+ if (b - a > 180) {
210
+ b -= 360;
211
+ }
212
+ if (b - a < -180) {
213
+ b += 360;
214
+ }
215
+ return math.lerp(a, b, math.clamp(alpha, 0, 1));
216
+ },
217
+ powerOfTwo(x) {
218
+ return x !== 0 && !(x & x - 1);
219
+ },
220
+ nextPowerOfTwo(val) {
221
+ val--;
222
+ val |= val >> 1;
223
+ val |= val >> 2;
224
+ val |= val >> 4;
225
+ val |= val >> 8;
226
+ val |= val >> 16;
227
+ val++;
228
+ return val;
229
+ },
230
+ nearestPowerOfTwo(val) {
231
+ return Math.pow(2, Math.round(Math.log(val) / Math.log(2)));
232
+ },
233
+ random(min, max) {
234
+ const diff = max - min;
235
+ return Math.random() * diff + min;
236
+ },
237
+ smoothstep(min, max, x) {
238
+ if (x <= min) return 0;
239
+ if (x >= max) return 1;
240
+ x = (x - min) / (max - min);
241
+ return x * x * (3 - 2 * x);
242
+ },
243
+ smootherstep(min, max, x) {
244
+ if (x <= min) return 0;
245
+ if (x >= max) return 1;
246
+ x = (x - min) / (max - min);
247
+ return x * x * x * (x * (x * 6 - 15) + 10);
248
+ },
249
+ roundUp(numToRound, multiple) {
250
+ if (multiple === 0) {
251
+ return numToRound;
252
+ }
253
+ return Math.ceil(numToRound / multiple) * multiple;
254
+ },
255
+ between(num, a, b, inclusive) {
256
+ const min = Math.min(a, b);
257
+ const max = Math.max(a, b);
258
+ return inclusive ? num >= min && num <= max : num > min && num < max;
259
+ }
260
+ };
261
+
262
+ var _Vec;
263
+ class Vec3 {
264
+ constructor(x = 0, y = 0, z = 0) {
265
+ this.x = void 0;
266
+ this.y = void 0;
267
+ this.z = void 0;
268
+ if (x.length === 3) {
269
+ this.x = x[0];
270
+ this.y = x[1];
271
+ this.z = x[2];
272
+ } else {
273
+ this.x = x;
274
+ this.y = y;
275
+ this.z = z;
276
+ }
277
+ }
278
+ add(rhs) {
279
+ this.x += rhs.x;
280
+ this.y += rhs.y;
281
+ this.z += rhs.z;
282
+ return this;
283
+ }
284
+ add2(lhs, rhs) {
285
+ this.x = lhs.x + rhs.x;
286
+ this.y = lhs.y + rhs.y;
287
+ this.z = lhs.z + rhs.z;
288
+ return this;
289
+ }
290
+ addScalar(scalar) {
291
+ this.x += scalar;
292
+ this.y += scalar;
293
+ this.z += scalar;
294
+ return this;
295
+ }
296
+ addScaled(rhs, scalar) {
297
+ this.x += rhs.x * scalar;
298
+ this.y += rhs.y * scalar;
299
+ this.z += rhs.z * scalar;
300
+ return this;
301
+ }
302
+ clone() {
303
+ const cstr = this.constructor;
304
+ return new cstr(this.x, this.y, this.z);
305
+ }
306
+ copy(rhs) {
307
+ this.x = rhs.x;
308
+ this.y = rhs.y;
309
+ this.z = rhs.z;
310
+ return this;
311
+ }
312
+ cross(lhs, rhs) {
313
+ const lx = lhs.x;
314
+ const ly = lhs.y;
315
+ const lz = lhs.z;
316
+ const rx = rhs.x;
317
+ const ry = rhs.y;
318
+ const rz = rhs.z;
319
+ this.x = ly * rz - ry * lz;
320
+ this.y = lz * rx - rz * lx;
321
+ this.z = lx * ry - rx * ly;
322
+ return this;
323
+ }
324
+ distance(rhs) {
325
+ const x = this.x - rhs.x;
326
+ const y = this.y - rhs.y;
327
+ const z = this.z - rhs.z;
328
+ return Math.sqrt(x * x + y * y + z * z);
329
+ }
330
+ div(rhs) {
331
+ this.x /= rhs.x;
332
+ this.y /= rhs.y;
333
+ this.z /= rhs.z;
334
+ return this;
335
+ }
336
+ div2(lhs, rhs) {
337
+ this.x = lhs.x / rhs.x;
338
+ this.y = lhs.y / rhs.y;
339
+ this.z = lhs.z / rhs.z;
340
+ return this;
341
+ }
342
+ divScalar(scalar) {
343
+ this.x /= scalar;
344
+ this.y /= scalar;
345
+ this.z /= scalar;
346
+ return this;
347
+ }
348
+ dot(rhs) {
349
+ return this.x * rhs.x + this.y * rhs.y + this.z * rhs.z;
350
+ }
351
+ equals(rhs) {
352
+ return this.x === rhs.x && this.y === rhs.y && this.z === rhs.z;
353
+ }
354
+ equalsApprox(rhs, epsilon = 1e-6) {
355
+ return Math.abs(this.x - rhs.x) < epsilon && Math.abs(this.y - rhs.y) < epsilon && Math.abs(this.z - rhs.z) < epsilon;
356
+ }
357
+ length() {
358
+ return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
359
+ }
360
+ lengthSq() {
361
+ return this.x * this.x + this.y * this.y + this.z * this.z;
362
+ }
363
+ lerp(lhs, rhs, alpha) {
364
+ this.x = lhs.x + alpha * (rhs.x - lhs.x);
365
+ this.y = lhs.y + alpha * (rhs.y - lhs.y);
366
+ this.z = lhs.z + alpha * (rhs.z - lhs.z);
367
+ return this;
368
+ }
369
+ mul(rhs) {
370
+ this.x *= rhs.x;
371
+ this.y *= rhs.y;
372
+ this.z *= rhs.z;
373
+ return this;
374
+ }
375
+ mul2(lhs, rhs) {
376
+ this.x = lhs.x * rhs.x;
377
+ this.y = lhs.y * rhs.y;
378
+ this.z = lhs.z * rhs.z;
379
+ return this;
380
+ }
381
+ mulScalar(scalar) {
382
+ this.x *= scalar;
383
+ this.y *= scalar;
384
+ this.z *= scalar;
385
+ return this;
386
+ }
387
+ normalize(src = this) {
388
+ const lengthSq = src.x * src.x + src.y * src.y + src.z * src.z;
389
+ if (lengthSq > 0) {
390
+ const invLength = 1 / Math.sqrt(lengthSq);
391
+ this.x = src.x * invLength;
392
+ this.y = src.y * invLength;
393
+ this.z = src.z * invLength;
394
+ }
395
+ return this;
396
+ }
397
+ floor(src = this) {
398
+ this.x = Math.floor(src.x);
399
+ this.y = Math.floor(src.y);
400
+ this.z = Math.floor(src.z);
401
+ return this;
402
+ }
403
+ ceil(src = this) {
404
+ this.x = Math.ceil(src.x);
405
+ this.y = Math.ceil(src.y);
406
+ this.z = Math.ceil(src.z);
407
+ return this;
408
+ }
409
+ round(src = this) {
410
+ this.x = Math.round(src.x);
411
+ this.y = Math.round(src.y);
412
+ this.z = Math.round(src.z);
413
+ return this;
414
+ }
415
+ min(rhs) {
416
+ if (rhs.x < this.x) this.x = rhs.x;
417
+ if (rhs.y < this.y) this.y = rhs.y;
418
+ if (rhs.z < this.z) this.z = rhs.z;
419
+ return this;
420
+ }
421
+ max(rhs) {
422
+ if (rhs.x > this.x) this.x = rhs.x;
423
+ if (rhs.y > this.y) this.y = rhs.y;
424
+ if (rhs.z > this.z) this.z = rhs.z;
425
+ return this;
426
+ }
427
+ project(rhs) {
428
+ const a_dot_b = this.x * rhs.x + this.y * rhs.y + this.z * rhs.z;
429
+ const b_dot_b = rhs.x * rhs.x + rhs.y * rhs.y + rhs.z * rhs.z;
430
+ const s = a_dot_b / b_dot_b;
431
+ this.x = rhs.x * s;
432
+ this.y = rhs.y * s;
433
+ this.z = rhs.z * s;
434
+ return this;
435
+ }
436
+ set(x, y, z) {
437
+ this.x = x;
438
+ this.y = y;
439
+ this.z = z;
440
+ return this;
441
+ }
442
+ sub(rhs) {
443
+ this.x -= rhs.x;
444
+ this.y -= rhs.y;
445
+ this.z -= rhs.z;
446
+ return this;
447
+ }
448
+ sub2(lhs, rhs) {
449
+ this.x = lhs.x - rhs.x;
450
+ this.y = lhs.y - rhs.y;
451
+ this.z = lhs.z - rhs.z;
452
+ return this;
453
+ }
454
+ subScalar(scalar) {
455
+ this.x -= scalar;
456
+ this.y -= scalar;
457
+ this.z -= scalar;
458
+ return this;
459
+ }
460
+ toString() {
461
+ return `[${this.x}, ${this.y}, ${this.z}]`;
462
+ }
463
+ }
464
+ _Vec = Vec3;
465
+ Vec3.ZERO = Object.freeze(new _Vec(0, 0, 0));
466
+ Vec3.HALF = Object.freeze(new _Vec(0.5, 0.5, 0.5));
467
+ Vec3.ONE = Object.freeze(new _Vec(1, 1, 1));
468
+ Vec3.UP = Object.freeze(new _Vec(0, 1, 0));
469
+ Vec3.DOWN = Object.freeze(new _Vec(0, -1, 0));
470
+ Vec3.RIGHT = Object.freeze(new _Vec(1, 0, 0));
471
+ Vec3.LEFT = Object.freeze(new _Vec(-1, 0, 0));
472
+ Vec3.FORWARD = Object.freeze(new _Vec(0, 0, -1));
473
+ Vec3.BACK = Object.freeze(new _Vec(0, 0, 1));
474
+
475
+ var _Quat;
476
+ class Quat {
477
+ constructor(x = 0, y = 0, z = 0, w = 1) {
478
+ this.x = void 0;
479
+ this.y = void 0;
480
+ this.z = void 0;
481
+ this.w = void 0;
482
+ if (x.length === 4) {
483
+ this.x = x[0];
484
+ this.y = x[1];
485
+ this.z = x[2];
486
+ this.w = x[3];
487
+ } else {
488
+ this.x = x;
489
+ this.y = y;
490
+ this.z = z;
491
+ this.w = w;
492
+ }
493
+ }
494
+ clone() {
495
+ const cstr = this.constructor;
496
+ return new cstr(this.x, this.y, this.z, this.w);
497
+ }
498
+ conjugate(src = this) {
499
+ this.x = src.x * -1;
500
+ this.y = src.y * -1;
501
+ this.z = src.z * -1;
502
+ this.w = src.w;
503
+ return this;
504
+ }
505
+ copy(rhs) {
506
+ this.x = rhs.x;
507
+ this.y = rhs.y;
508
+ this.z = rhs.z;
509
+ this.w = rhs.w;
510
+ return this;
511
+ }
512
+ equals(rhs) {
513
+ return this.x === rhs.x && this.y === rhs.y && this.z === rhs.z && this.w === rhs.w;
514
+ }
515
+ equalsApprox(rhs, epsilon = 1e-6) {
516
+ return Math.abs(this.x - rhs.x) < epsilon && Math.abs(this.y - rhs.y) < epsilon && Math.abs(this.z - rhs.z) < epsilon && Math.abs(this.w - rhs.w) < epsilon;
517
+ }
518
+ getAxisAngle(axis) {
519
+ let rad = Math.acos(this.w) * 2;
520
+ const s = Math.sin(rad / 2);
521
+ if (s !== 0) {
522
+ axis.x = this.x / s;
523
+ axis.y = this.y / s;
524
+ axis.z = this.z / s;
525
+ if (axis.x < 0 || axis.y < 0 || axis.z < 0) {
526
+ axis.x *= -1;
527
+ axis.y *= -1;
528
+ axis.z *= -1;
529
+ rad *= -1;
530
+ }
531
+ } else {
532
+ axis.x = 1;
533
+ axis.y = 0;
534
+ axis.z = 0;
535
+ }
536
+ return rad * math.RAD_TO_DEG;
537
+ }
538
+ getEulerAngles(eulers = new Vec3()) {
539
+ let x, y, z;
540
+ const qx = this.x;
541
+ const qy = this.y;
542
+ const qz = this.z;
543
+ const qw = this.w;
544
+ const a2 = 2 * (qw * qy - qx * qz);
545
+ if (a2 <= -0.99999) {
546
+ x = 2 * Math.atan2(qx, qw);
547
+ y = -Math.PI / 2;
548
+ z = 0;
549
+ } else if (a2 >= 0.99999) {
550
+ x = 2 * Math.atan2(qx, qw);
551
+ y = Math.PI / 2;
552
+ z = 0;
553
+ } else {
554
+ x = Math.atan2(2 * (qw * qx + qy * qz), 1 - 2 * (qx * qx + qy * qy));
555
+ y = Math.asin(a2);
556
+ z = Math.atan2(2 * (qw * qz + qx * qy), 1 - 2 * (qy * qy + qz * qz));
557
+ }
558
+ return eulers.set(x, y, z).mulScalar(math.RAD_TO_DEG);
559
+ }
560
+ invert(src = this) {
561
+ return this.conjugate(src).normalize();
562
+ }
563
+ length() {
564
+ return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w);
565
+ }
566
+ lengthSq() {
567
+ return this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w;
568
+ }
569
+ mul(rhs) {
570
+ const q1x = this.x;
571
+ const q1y = this.y;
572
+ const q1z = this.z;
573
+ const q1w = this.w;
574
+ const q2x = rhs.x;
575
+ const q2y = rhs.y;
576
+ const q2z = rhs.z;
577
+ const q2w = rhs.w;
578
+ this.x = q1w * q2x + q1x * q2w + q1y * q2z - q1z * q2y;
579
+ this.y = q1w * q2y + q1y * q2w + q1z * q2x - q1x * q2z;
580
+ this.z = q1w * q2z + q1z * q2w + q1x * q2y - q1y * q2x;
581
+ this.w = q1w * q2w - q1x * q2x - q1y * q2y - q1z * q2z;
582
+ return this;
583
+ }
584
+ mulScalar(scalar, src = this) {
585
+ this.x = src.x * scalar;
586
+ this.y = src.y * scalar;
587
+ this.z = src.z * scalar;
588
+ this.w = src.w * scalar;
589
+ return this;
590
+ }
591
+ mul2(lhs, rhs) {
592
+ const q1x = lhs.x;
593
+ const q1y = lhs.y;
594
+ const q1z = lhs.z;
595
+ const q1w = lhs.w;
596
+ const q2x = rhs.x;
597
+ const q2y = rhs.y;
598
+ const q2z = rhs.z;
599
+ const q2w = rhs.w;
600
+ this.x = q1w * q2x + q1x * q2w + q1y * q2z - q1z * q2y;
601
+ this.y = q1w * q2y + q1y * q2w + q1z * q2x - q1x * q2z;
602
+ this.z = q1w * q2z + q1z * q2w + q1x * q2y - q1y * q2x;
603
+ this.w = q1w * q2w - q1x * q2x - q1y * q2y - q1z * q2z;
604
+ return this;
605
+ }
606
+ normalize(src = this) {
607
+ let len = src.length();
608
+ if (len === 0) {
609
+ this.x = this.y = this.z = 0;
610
+ this.w = 1;
611
+ } else {
612
+ len = 1 / len;
613
+ this.x = src.x * len;
614
+ this.y = src.y * len;
615
+ this.z = src.z * len;
616
+ this.w = src.w * len;
617
+ }
618
+ return this;
619
+ }
620
+ set(x, y, z, w) {
621
+ this.x = x;
622
+ this.y = y;
623
+ this.z = z;
624
+ this.w = w;
625
+ return this;
626
+ }
627
+ setFromAxisAngle(axis, angle) {
628
+ angle *= 0.5 * math.DEG_TO_RAD;
629
+ const sa = Math.sin(angle);
630
+ const ca = Math.cos(angle);
631
+ this.x = sa * axis.x;
632
+ this.y = sa * axis.y;
633
+ this.z = sa * axis.z;
634
+ this.w = ca;
635
+ return this;
636
+ }
637
+ setFromEulerAngles(ex, ey, ez) {
638
+ if (ex instanceof Vec3) {
639
+ const vec = ex;
640
+ ex = vec.x;
641
+ ey = vec.y;
642
+ ez = vec.z;
643
+ }
644
+ const halfToRad = 0.5 * math.DEG_TO_RAD;
645
+ ex *= halfToRad;
646
+ ey *= halfToRad;
647
+ ez *= halfToRad;
648
+ const sx = Math.sin(ex);
649
+ const cx = Math.cos(ex);
650
+ const sy = Math.sin(ey);
651
+ const cy = Math.cos(ey);
652
+ const sz = Math.sin(ez);
653
+ const cz = Math.cos(ez);
654
+ this.x = sx * cy * cz - cx * sy * sz;
655
+ this.y = cx * sy * cz + sx * cy * sz;
656
+ this.z = cx * cy * sz - sx * sy * cz;
657
+ this.w = cx * cy * cz + sx * sy * sz;
658
+ return this;
659
+ }
660
+ setFromMat4(m) {
661
+ const d = m.data;
662
+ let m00 = d[0];
663
+ let m01 = d[1];
664
+ let m02 = d[2];
665
+ let m10 = d[4];
666
+ let m11 = d[5];
667
+ let m12 = d[6];
668
+ let m20 = d[8];
669
+ let m21 = d[9];
670
+ let m22 = d[10];
671
+ let l;
672
+ l = m00 * m00 + m01 * m01 + m02 * m02;
673
+ if (l === 0) return this.set(0, 0, 0, 1);
674
+ l = 1 / Math.sqrt(l);
675
+ m00 *= l;
676
+ m01 *= l;
677
+ m02 *= l;
678
+ l = m10 * m10 + m11 * m11 + m12 * m12;
679
+ if (l === 0) return this.set(0, 0, 0, 1);
680
+ l = 1 / Math.sqrt(l);
681
+ m10 *= l;
682
+ m11 *= l;
683
+ m12 *= l;
684
+ l = m20 * m20 + m21 * m21 + m22 * m22;
685
+ if (l === 0) return this.set(0, 0, 0, 1);
686
+ l = 1 / Math.sqrt(l);
687
+ m20 *= l;
688
+ m21 *= l;
689
+ m22 *= l;
690
+ if (m22 < 0) {
691
+ if (m00 > m11) {
692
+ this.set(1 + m00 - m11 - m22, m01 + m10, m20 + m02, m12 - m21);
693
+ } else {
694
+ this.set(m01 + m10, 1 - m00 + m11 - m22, m12 + m21, m20 - m02);
695
+ }
696
+ } else {
697
+ if (m00 < -m11) {
698
+ this.set(m20 + m02, m12 + m21, 1 - m00 - m11 + m22, m01 - m10);
699
+ } else {
700
+ this.set(m12 - m21, m20 - m02, m01 - m10, 1 + m00 + m11 + m22);
701
+ }
702
+ }
703
+ return this.mulScalar(1.0 / this.length());
704
+ }
705
+ setFromDirections(from, to) {
706
+ const dotProduct = 1 + from.dot(to);
707
+ if (dotProduct < Number.EPSILON) {
708
+ if (Math.abs(from.x) > Math.abs(from.y)) {
709
+ this.x = -from.z;
710
+ this.y = 0;
711
+ this.z = from.x;
712
+ this.w = 0;
713
+ } else {
714
+ this.x = 0;
715
+ this.y = -from.z;
716
+ this.z = from.y;
717
+ this.w = 0;
718
+ }
719
+ } else {
720
+ this.x = from.y * to.z - from.z * to.y;
721
+ this.y = from.z * to.x - from.x * to.z;
722
+ this.z = from.x * to.y - from.y * to.x;
723
+ this.w = dotProduct;
724
+ }
725
+ return this.normalize();
726
+ }
727
+ slerp(lhs, rhs, alpha) {
728
+ const lx = lhs.x;
729
+ const ly = lhs.y;
730
+ const lz = lhs.z;
731
+ const lw = lhs.w;
732
+ let rx = rhs.x;
733
+ let ry = rhs.y;
734
+ let rz = rhs.z;
735
+ let rw = rhs.w;
736
+ let cosHalfTheta = lw * rw + lx * rx + ly * ry + lz * rz;
737
+ if (cosHalfTheta < 0) {
738
+ rw = -rw;
739
+ rx = -rx;
740
+ ry = -ry;
741
+ rz = -rz;
742
+ cosHalfTheta = -cosHalfTheta;
743
+ }
744
+ if (Math.abs(cosHalfTheta) >= 1) {
745
+ this.w = lw;
746
+ this.x = lx;
747
+ this.y = ly;
748
+ this.z = lz;
749
+ return this;
750
+ }
751
+ const halfTheta = Math.acos(cosHalfTheta);
752
+ const sinHalfTheta = Math.sqrt(1 - cosHalfTheta * cosHalfTheta);
753
+ if (Math.abs(sinHalfTheta) < 0.001) {
754
+ this.w = lw * 0.5 + rw * 0.5;
755
+ this.x = lx * 0.5 + rx * 0.5;
756
+ this.y = ly * 0.5 + ry * 0.5;
757
+ this.z = lz * 0.5 + rz * 0.5;
758
+ return this;
759
+ }
760
+ const ratioA = Math.sin((1 - alpha) * halfTheta) / sinHalfTheta;
761
+ const ratioB = Math.sin(alpha * halfTheta) / sinHalfTheta;
762
+ this.w = lw * ratioA + rw * ratioB;
763
+ this.x = lx * ratioA + rx * ratioB;
764
+ this.y = ly * ratioA + ry * ratioB;
765
+ this.z = lz * ratioA + rz * ratioB;
766
+ return this;
767
+ }
768
+ transformVector(vec, res = new Vec3()) {
769
+ const x = vec.x,
770
+ y = vec.y,
771
+ z = vec.z;
772
+ const qx = this.x,
773
+ qy = this.y,
774
+ qz = this.z,
775
+ qw = this.w;
776
+ const ix = qw * x + qy * z - qz * y;
777
+ const iy = qw * y + qz * x - qx * z;
778
+ const iz = qw * z + qx * y - qy * x;
779
+ const iw = -qx * x - qy * y - qz * z;
780
+ res.x = ix * qw + iw * -qx + iy * -qz - iz * -qy;
781
+ res.y = iy * qw + iw * -qy + iz * -qx - ix * -qz;
782
+ res.z = iz * qw + iw * -qz + ix * -qy - iy * -qx;
783
+ return res;
784
+ }
785
+ toString() {
786
+ return `[${this.x}, ${this.y}, ${this.z}, ${this.w}]`;
787
+ }
788
+ }
789
+ _Quat = Quat;
790
+ Quat.IDENTITY = Object.freeze(new _Quat(0, 0, 0, 1));
791
+ Quat.ZERO = Object.freeze(new _Quat(0, 0, 0, 0));
792
+
793
+ const generatedByString = `Generated by splat-transform ${version}`;
794
+ const shBandCoeffs = [0, 3, 8, 15];
795
+ const q = new Quat();
796
+ // process and compress a chunk of 256 splats
797
+ class Chunk {
798
+ static members = [
799
+ 'x', 'y', 'z',
800
+ 'scale_0', 'scale_1', 'scale_2',
801
+ 'f_dc_0', 'f_dc_1', 'f_dc_2', 'opacity',
802
+ 'rot_0', 'rot_1', 'rot_2', 'rot_3'
803
+ ];
804
+ size;
805
+ data = {};
806
+ // compressed data
807
+ chunkData;
808
+ position;
809
+ rotation;
810
+ scale;
811
+ color;
812
+ constructor(size = 256) {
813
+ this.size = size;
814
+ Chunk.members.forEach((m) => {
815
+ this.data[m] = new Float32Array(size);
816
+ });
817
+ this.chunkData = new Float32Array(18);
818
+ this.position = new Uint32Array(size);
819
+ this.rotation = new Uint32Array(size);
820
+ this.scale = new Uint32Array(size);
821
+ this.color = new Uint32Array(size);
822
+ }
823
+ set(index, singleSplat) {
824
+ for (let i = 0; i < Chunk.members.length; ++i) {
825
+ this.data[Chunk.members[i]][index] = singleSplat[i];
826
+ }
827
+ }
828
+ pack() {
829
+ const calcMinMax = (data) => {
830
+ let min;
831
+ let max;
832
+ min = max = data[0];
833
+ for (let i = 1; i < data.length; ++i) {
834
+ const v = data[i];
835
+ min = Math.min(min, v);
836
+ max = Math.max(max, v);
837
+ }
838
+ return { min, max };
839
+ };
840
+ const normalize = (x, min, max) => {
841
+ if (x <= min)
842
+ return 0;
843
+ if (x >= max)
844
+ return 1;
845
+ return (max - min < 0.00001) ? 0 : (x - min) / (max - min);
846
+ };
847
+ const data = this.data;
848
+ const x = data.x;
849
+ const y = data.y;
850
+ const z = data.z;
851
+ const scale_0 = data.scale_0;
852
+ const scale_1 = data.scale_1;
853
+ const scale_2 = data.scale_2;
854
+ const rot_0 = data.rot_0;
855
+ const rot_1 = data.rot_1;
856
+ const rot_2 = data.rot_2;
857
+ const rot_3 = data.rot_3;
858
+ const f_dc_0 = data.f_dc_0;
859
+ const f_dc_1 = data.f_dc_1;
860
+ const f_dc_2 = data.f_dc_2;
861
+ const opacity = data.opacity;
862
+ const px = calcMinMax(x);
863
+ const py = calcMinMax(y);
864
+ const pz = calcMinMax(z);
865
+ const sx = calcMinMax(scale_0);
866
+ const sy = calcMinMax(scale_1);
867
+ const sz = calcMinMax(scale_2);
868
+ // clamp scale because sometimes values are at infinity
869
+ const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
870
+ sx.min = clamp(sx.min, -20, 20);
871
+ sx.max = clamp(sx.max, -20, 20);
872
+ sy.min = clamp(sy.min, -20, 20);
873
+ sy.max = clamp(sy.max, -20, 20);
874
+ sz.min = clamp(sz.min, -20, 20);
875
+ sz.max = clamp(sz.max, -20, 20);
876
+ // convert f_dc_ to colors before calculating min/max and packaging
877
+ const SH_C0 = 0.28209479177387814;
878
+ for (let i = 0; i < f_dc_0.length; ++i) {
879
+ f_dc_0[i] = f_dc_0[i] * SH_C0 + 0.5;
880
+ f_dc_1[i] = f_dc_1[i] * SH_C0 + 0.5;
881
+ f_dc_2[i] = f_dc_2[i] * SH_C0 + 0.5;
882
+ }
883
+ const cr = calcMinMax(f_dc_0);
884
+ const cg = calcMinMax(f_dc_1);
885
+ const cb = calcMinMax(f_dc_2);
886
+ const packUnorm = (value, bits) => {
887
+ const t = (1 << bits) - 1;
888
+ return Math.max(0, Math.min(t, Math.floor(value * t + 0.5)));
889
+ };
890
+ const pack111011 = (x, y, z) => {
891
+ return packUnorm(x, 11) << 21 |
892
+ packUnorm(y, 10) << 11 |
893
+ packUnorm(z, 11);
894
+ };
895
+ const pack8888 = (x, y, z, w) => {
896
+ return packUnorm(x, 8) << 24 |
897
+ packUnorm(y, 8) << 16 |
898
+ packUnorm(z, 8) << 8 |
899
+ packUnorm(w, 8);
900
+ };
901
+ // pack quaternion into 2,10,10,10
902
+ const packRot = (x, y, z, w) => {
903
+ q.set(x, y, z, w).normalize();
904
+ const a = [q.x, q.y, q.z, q.w];
905
+ const largest = a.reduce((curr, v, i) => (Math.abs(v) > Math.abs(a[curr]) ? i : curr), 0);
906
+ if (a[largest] < 0) {
907
+ a[0] = -a[0];
908
+ a[1] = -a[1];
909
+ a[2] = -a[2];
910
+ a[3] = -a[3];
911
+ }
912
+ const norm = Math.sqrt(2) * 0.5;
913
+ let result = largest;
914
+ for (let i = 0; i < 4; ++i) {
915
+ if (i !== largest) {
916
+ result = (result << 10) | packUnorm(a[i] * norm + 0.5, 10);
917
+ }
918
+ }
919
+ return result;
920
+ };
921
+ // pack
922
+ for (let i = 0; i < this.size; ++i) {
923
+ this.position[i] = pack111011(normalize(x[i], px.min, px.max), normalize(y[i], py.min, py.max), normalize(z[i], pz.min, pz.max));
924
+ this.rotation[i] = packRot(rot_0[i], rot_1[i], rot_2[i], rot_3[i]);
925
+ this.scale[i] = pack111011(normalize(scale_0[i], sx.min, sx.max), normalize(scale_1[i], sy.min, sy.max), normalize(scale_2[i], sz.min, sz.max));
926
+ this.color[i] = pack8888(normalize(f_dc_0[i], cr.min, cr.max), normalize(f_dc_1[i], cg.min, cg.max), normalize(f_dc_2[i], cb.min, cb.max), 1 / (1 + Math.exp(-opacity[i])));
927
+ }
928
+ this.chunkData.set([
929
+ px.min, py.min, pz.min, px.max, py.max, pz.max,
930
+ sx.min, sy.min, sz.min, sx.max, sy.max, sz.max,
931
+ cr.min, cg.min, cb.min, cr.max, cg.max, cb.max
932
+ ], 0);
933
+ }
934
+ }
935
+ // sort the compressed indices into morton order
936
+ const sortSplats = (splat, indices) => {
937
+ // https://fgiesen.wordpress.com/2009/12/13/decoding-morton-codes/
938
+ const encodeMorton3 = (x, y, z) => {
939
+ const Part1By2 = (x) => {
940
+ x &= 0x000003ff;
941
+ x = (x ^ (x << 16)) & 0xff0000ff;
942
+ x = (x ^ (x << 8)) & 0x0300f00f;
943
+ x = (x ^ (x << 4)) & 0x030c30c3;
944
+ x = (x ^ (x << 2)) & 0x09249249;
945
+ return x;
946
+ };
947
+ return (Part1By2(z) << 2) + (Part1By2(y) << 1) + Part1By2(x);
948
+ };
949
+ let minx;
950
+ let miny;
951
+ let minz;
952
+ let maxx;
953
+ let maxy;
954
+ let maxz;
955
+ const vertex = [0, 0, 0];
956
+ const it = splat.createIterator(['x', 'y', 'z'], vertex);
957
+ // calculate scene extents across all splats (using sort centers, because they're in world space)
958
+ for (let i = 0; i < splat.numSplats; ++i) {
959
+ it(i);
960
+ const x = vertex[0];
961
+ const y = vertex[1];
962
+ const z = vertex[2];
963
+ if (minx === undefined) {
964
+ minx = maxx = x;
965
+ miny = maxy = y;
966
+ minz = maxz = z;
967
+ }
968
+ else {
969
+ if (x < minx)
970
+ minx = x;
971
+ else if (x > maxx)
972
+ maxx = x;
973
+ if (y < miny)
974
+ miny = y;
975
+ else if (y > maxy)
976
+ maxy = y;
977
+ if (z < minz)
978
+ minz = z;
979
+ else if (z > maxz)
980
+ maxz = z;
981
+ }
982
+ }
983
+ const xlen = maxx - minx;
984
+ const ylen = maxy - miny;
985
+ const zlen = maxz - minz;
986
+ const morton = new Uint32Array(indices.length);
987
+ let idx = 0;
988
+ for (let i = 0; i < splat.numSplats; ++i) {
989
+ it(i);
990
+ const x = vertex[0];
991
+ const y = vertex[1];
992
+ const z = vertex[2];
993
+ const ix = Math.floor(1024 * (x - minx) / xlen);
994
+ const iy = Math.floor(1024 * (y - miny) / ylen);
995
+ const iz = Math.floor(1024 * (z - minz) / zlen);
996
+ morton[idx++] = encodeMorton3(ix, iy, iz);
997
+ }
998
+ // order splats by morton code
999
+ indices.sort((a, b) => morton[a.globalIndex] - morton[b.globalIndex]);
1000
+ };
1001
+ const chunkProps = [
1002
+ 'min_x', 'min_y', 'min_z',
1003
+ 'max_x', 'max_y', 'max_z',
1004
+ 'min_scale_x', 'min_scale_y', 'min_scale_z',
1005
+ 'max_scale_x', 'max_scale_y', 'max_scale_z',
1006
+ 'min_r', 'min_g', 'min_b',
1007
+ 'max_r', 'max_g', 'max_b'
1008
+ ];
1009
+ const vertexProps = [
1010
+ 'packed_position',
1011
+ 'packed_rotation',
1012
+ 'packed_scale',
1013
+ 'packed_color'
1014
+ ];
1015
+ const writeCompressedPly = async (fileHandle, splat) => {
1016
+ // make a list of indices spanning all splats (so we can sort them together)
1017
+ const indices = [];
1018
+ for (let i = 0; i < splat.numSplats; ++i) {
1019
+ indices.push({ splatIndex: 0, i, globalIndex: indices.length });
1020
+ }
1021
+ if (indices.length === 0) {
1022
+ throw new Error('no splats to write');
1023
+ }
1024
+ const numSplats = indices.length;
1025
+ const numChunks = Math.ceil(numSplats / 256);
1026
+ const outputSHBands = splat.numSHBands;
1027
+ const outputSHCoeffs = shBandCoeffs[outputSHBands];
1028
+ const shHeader = outputSHBands ? [
1029
+ `element sh ${numSplats}`,
1030
+ new Array(outputSHCoeffs * 3).fill('').map((_, i) => `property uchar f_rest_${i}`)
1031
+ ].flat() : [];
1032
+ const headerText = [
1033
+ 'ply',
1034
+ 'format binary_little_endian 1.0',
1035
+ `comment ${generatedByString}`,
1036
+ `element chunk ${numChunks}`,
1037
+ chunkProps.map(p => `property float ${p}`),
1038
+ `element vertex ${numSplats}`,
1039
+ vertexProps.map(p => `property uint ${p}`),
1040
+ shHeader,
1041
+ 'end_header\n'
1042
+ ].flat().join('\n');
1043
+ const header = (new TextEncoder()).encode(headerText);
1044
+ const chunkData = new Float32Array(numChunks * chunkProps.length);
1045
+ const splatData = new Uint32Array(numSplats * vertexProps.length);
1046
+ const shData = new Uint8Array(numSplats * outputSHCoeffs * 3);
1047
+ // sort splats into some kind of order (morton order rn)
1048
+ sortSplats(splat, indices);
1049
+ const singleSplat = Chunk.members.map(_ => 0);
1050
+ const it = splat.createIterator(Chunk.members, singleSplat);
1051
+ const shMembers = shNames.slice(0, outputSHCoeffs * 3);
1052
+ const singleSH = shMembers.map(_ => 0);
1053
+ const shIt = splat.createIterator(shMembers, singleSH);
1054
+ const chunk = new Chunk();
1055
+ for (let i = 0; i < numChunks; ++i) {
1056
+ const num = Math.min(numSplats, (i + 1) * 256) - i * 256;
1057
+ for (let j = 0; j < num; ++j) {
1058
+ const index = indices[i * 256 + j];
1059
+ // read splat data
1060
+ it(index.i);
1061
+ // update chunk
1062
+ chunk.set(j, singleSplat);
1063
+ shIt(index.i);
1064
+ // quantize and write sh data
1065
+ let off = (i * 256 + j) * outputSHCoeffs * 3;
1066
+ for (let k = 0; k < outputSHCoeffs * 3; ++k) {
1067
+ const nvalue = singleSH[k] / 8 + 0.5;
1068
+ shData[off++] = Math.max(0, Math.min(255, Math.trunc(nvalue * 256)));
1069
+ }
1070
+ }
1071
+ // pack the chunk
1072
+ chunk.pack();
1073
+ // store the float data
1074
+ chunkData.set(chunk.chunkData, i * 18);
1075
+ // write packed bits
1076
+ const offset = i * 256 * 4;
1077
+ for (let j = 0; j < num; ++j) {
1078
+ splatData[offset + j * 4 + 0] = chunk.position[j];
1079
+ splatData[offset + j * 4 + 1] = chunk.rotation[j];
1080
+ splatData[offset + j * 4 + 2] = chunk.scale[j];
1081
+ splatData[offset + j * 4 + 3] = chunk.color[j];
1082
+ }
1083
+ }
1084
+ await fileHandle.write(header);
1085
+ await fileHandle.write(new Uint8Array(chunkData.buffer));
1086
+ await fileHandle.write(new Uint8Array(splatData.buffer));
1087
+ await fileHandle.write(shData);
1088
+ };
1089
+
1090
+ const readData = async (filename) => {
1091
+ // open input
1092
+ console.log(`loading '${filename}'...`);
1093
+ const inputFile = await open(filename, 'r');
1094
+ // read contents
1095
+ console.log(`reading contents...`);
1096
+ const plyFile = await readPly(inputFile);
1097
+ // close file
1098
+ await inputFile.close();
1099
+ return plyFile;
1100
+ };
1101
+ const processData = (plyFile) => {
1102
+ // check we have the necessary elements for processing
1103
+ };
1104
+ const writeData = async (filename, plyFile) => {
1105
+ const outputFile = await open(filename, 'w');
1106
+ await writeCompressedPly(outputFile, new Splat(plyFile));
1107
+ await outputFile.close();
1108
+ };
1109
+ const main = async () => {
1110
+ console.log(`splat-transform v${version}`);
1111
+ if (process.argv.length < 3) {
1112
+ console.error('Usage: splat-transform <input-file> <output-file>');
1113
+ process.exit(1);
1114
+ }
1115
+ const inputFilename = process.argv[2];
1116
+ const outputFilename = process.argv[3];
1117
+ try {
1118
+ // open input
1119
+ const plyFile = await readData(inputFilename);
1120
+ // process
1121
+ processData(plyFile);
1122
+ // write
1123
+ await writeData(outputFilename, plyFile);
1124
+ }
1125
+ catch (err) {
1126
+ // handle errors
1127
+ console.error(`error: ${err.message}`);
1128
+ process.exit(1);
1129
+ }
1130
+ console.log('done');
1131
+ };
1132
+ await main();
1133
+ //# sourceMappingURL=index.mjs.map