@manycore/aholo-splat-transform 1.2.8 → 1.2.9

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 (97) hide show
  1. package/CHANGELOG.md +120 -113
  2. package/README.md +39 -39
  3. package/THIRD_PARTY_LICENSES.txt +1373 -1373
  4. package/bin/cli.js +125 -118
  5. package/dist/SplatData.d.ts +67 -67
  6. package/dist/SplatData.js +167 -150
  7. package/dist/constant.d.ts +3 -3
  8. package/dist/constant.js +13 -13
  9. package/dist/file/IFile.d.ts +5 -5
  10. package/dist/file/IFile.js +1 -1
  11. package/dist/file/esz.d.ts +11 -11
  12. package/dist/file/esz.js +337 -322
  13. package/dist/file/index.d.ts +8 -8
  14. package/dist/file/index.js +7 -7
  15. package/dist/file/ksplat.d.ts +12 -12
  16. package/dist/file/ksplat.js +293 -231
  17. package/dist/file/lcc.d.ts +11 -11
  18. package/dist/file/lcc.js +161 -158
  19. package/dist/file/ply.d.ts +13 -13
  20. package/dist/file/ply.js +439 -390
  21. package/dist/file/sog.d.ts +80 -80
  22. package/dist/file/sog.js +525 -494
  23. package/dist/file/splat.d.ts +6 -6
  24. package/dist/file/splat.js +119 -99
  25. package/dist/file/spz.d.ts +11 -11
  26. package/dist/file/spz.js +597 -583
  27. package/dist/file/voxel.d.ts +43 -37
  28. package/dist/file/voxel.js +411 -280
  29. package/dist/index.d.ts +33 -33
  30. package/dist/index.js +54 -54
  31. package/dist/native/index.d.ts +54 -54
  32. package/dist/native/index.js +122 -129
  33. package/dist/native/utils.d.ts +1 -0
  34. package/dist/native/utils.js +54 -0
  35. package/dist/tasks/AutoChunkLodTask.d.ts +13 -13
  36. package/dist/tasks/AutoChunkLodTask.js +117 -117
  37. package/dist/tasks/AutoLodTask.d.ts +10 -10
  38. package/dist/tasks/AutoLodTask.js +20 -20
  39. package/dist/tasks/BaseTask.d.ts +15 -15
  40. package/dist/tasks/BaseTask.js +5 -5
  41. package/dist/tasks/FlexLodTask.d.ts +12 -12
  42. package/dist/tasks/FlexLodTask.js +54 -44
  43. package/dist/tasks/ModifyTask.d.ts +9 -9
  44. package/dist/tasks/ModifyTask.js +166 -156
  45. package/dist/tasks/ReadTask.d.ts +9 -9
  46. package/dist/tasks/ReadTask.js +29 -29
  47. package/dist/tasks/SkeletonLodTask.d.ts +10 -10
  48. package/dist/tasks/SkeletonLodTask.js +176 -156
  49. package/dist/tasks/VoxelTask.d.ts +35 -30
  50. package/dist/tasks/VoxelTask.js +40 -37
  51. package/dist/tasks/WriteTask.d.ts +12 -12
  52. package/dist/tasks/WriteTask.js +70 -70
  53. package/dist/utils/BufferReader.d.ts +12 -12
  54. package/dist/utils/BufferReader.js +45 -45
  55. package/dist/utils/Logger.d.ts +11 -11
  56. package/dist/utils/Logger.js +40 -40
  57. package/dist/utils/StreamChunkDecoder.d.ts +16 -16
  58. package/dist/utils/StreamChunkDecoder.js +31 -31
  59. package/dist/utils/index.d.ts +27 -27
  60. package/dist/utils/index.js +101 -101
  61. package/dist/utils/k-means.d.ts +4 -4
  62. package/dist/utils/k-means.js +340 -341
  63. package/dist/utils/math.d.ts +46 -46
  64. package/dist/utils/math.js +350 -346
  65. package/dist/utils/quantize-1d.d.ts +4 -4
  66. package/dist/utils/quantize-1d.js +164 -164
  67. package/dist/utils/sh-rotate.d.ts +2 -2
  68. package/dist/utils/sh-rotate.js +236 -175
  69. package/dist/utils/splat.d.ts +21 -21
  70. package/dist/utils/splat.js +397 -387
  71. package/dist/utils/voxel/binary.d.ts +8 -0
  72. package/dist/utils/voxel/binary.js +176 -0
  73. package/dist/utils/voxel/common.d.ts +178 -162
  74. package/dist/utils/voxel/common.js +1752 -1682
  75. package/dist/utils/voxel/coplanar-merge.d.ts +63 -63
  76. package/dist/utils/voxel/coplanar-merge.js +818 -819
  77. package/dist/utils/voxel/filter-cluster.d.ts +20 -0
  78. package/dist/utils/voxel/filter-cluster.js +628 -0
  79. package/dist/utils/voxel/gpu-dilation.d.ts +2 -2
  80. package/dist/utils/voxel/gpu-dilation.js +677 -656
  81. package/dist/utils/voxel/marching-cubes.d.ts +42 -42
  82. package/dist/utils/voxel/marching-cubes.js +1645 -1657
  83. package/dist/utils/voxel/mesh.d.ts +3 -3
  84. package/dist/utils/voxel/mesh.js +130 -130
  85. package/dist/utils/voxel/nav.d.ts +29 -29
  86. package/dist/utils/voxel/nav.js +1068 -1043
  87. package/dist/utils/voxel/postprocess.d.ts +23 -23
  88. package/dist/utils/voxel/postprocess.js +408 -375
  89. package/dist/utils/voxel/voxel-faces.d.ts +18 -18
  90. package/dist/utils/voxel/voxel-faces.js +662 -663
  91. package/dist/utils/voxel/voxelize.d.ts +34 -33
  92. package/dist/utils/voxel/voxelize.js +1208 -1193
  93. package/dist/utils/webgpu.d.ts +8 -8
  94. package/dist/utils/webgpu.js +122 -122
  95. package/package.json +37 -39
  96. package/dist/native/cpp/bin/linux/binding.node +0 -0
  97. package/dist/native/cpp/bin/windows/binding.node +0 -0
package/dist/file/sog.js CHANGED
@@ -1,494 +1,525 @@
1
- import { unzipSync, zipSync } from 'fflate';
2
- import { Buffer } from 'node:buffer';
3
- import { decodeWebP, encodeWebP, WebPLosslessProfile } from '../native/index.js';
4
- import { SH_C0, SH_MAPS, NUM_F_REST_TO_SH_DEGREE } from '../constant.js';
5
- import { getOrCreateDevice, kmeans, logger, mortonSort, quantize1d, isUrl, extractFromRootDir, clamp } from '../utils/index.js';
6
- const ZIP_MAGIC = 0x04034b50;
7
- const PERM_TABLE = [
8
- [0, 1, 2, 3],
9
- [3, 1, 2, 0],
10
- [1, 3, 2, 0],
11
- [1, 2, 3, 0],
12
- ];
13
- const TEMP_ROT = new Float32Array(4);
14
- function logTransform(value) {
15
- return Math.sign(value) * Math.log(Math.abs(value) + 1);
16
- }
17
- ;
18
- function writeTableData(table, indices, width, height, channels = 4) {
19
- const data = new Uint8Array(width * height * channels);
20
- const numColumns = table.length;
21
- for (let i = 0; i < indices.length; ++i) {
22
- const idx = indices[i];
23
- data[i * channels + 0] = table[0][idx];
24
- data[i * channels + 1] = numColumns > 1 ? table[1][idx] : 0;
25
- data[i * channels + 2] = numColumns > 2 ? table[2][idx] : 0;
26
- data[i * channels + 3] = numColumns > 3 ? table[3][idx] : 255;
27
- }
28
- return data;
29
- }
30
- function buildSHTableMap(shCoeffs) {
31
- const result = [];
32
- for (let i = 0; i < 3; i++) {
33
- for (let j = 0; j < shCoeffs; j++) {
34
- result.push(j * 3 + i);
35
- }
36
- }
37
- return result;
38
- }
39
- export class SogFile {
40
- constructor(iterations = 10) {
41
- this.iterations = iterations;
42
- this.counts = 0;
43
- this.shDegree = 0;
44
- /**
45
- * @internal
46
- */
47
- this.refs = {};
48
- }
49
- async load(stream, contentLength) {
50
- const buffer = new Uint8Array(contentLength);
51
- const reader = stream.getReader();
52
- let offset = 0;
53
- while (true) {
54
- const { done, value } = await reader.read();
55
- if (done) {
56
- break;
57
- }
58
- buffer.set(value, offset);
59
- offset += value.length;
60
- }
61
- let metaBuffer = buffer;
62
- const view = new DataView(buffer.buffer);
63
- if (view.getUint32(0, true) === ZIP_MAGIC) {
64
- this.refs = extractFromRootDir(unzipSync(buffer));
65
- metaBuffer = this.refs['meta.json'];
66
- if (!metaBuffer) {
67
- throw new Error('SOG meta.json not found in the zip archive.');
68
- }
69
- }
70
- this.meta = JSON.parse(new TextDecoder().decode(metaBuffer));
71
- if (this.meta.version === undefined) {
72
- const { means, quats, shN } = this.meta;
73
- if (quats.encoding !== 'quaternion_packed') {
74
- throw new Error('Unsupported quaternion encoding');
75
- }
76
- this.counts = means.shape[0];
77
- this.shDegree = shN ? NUM_F_REST_TO_SH_DEGREE[shN.shape[1]] : 0;
78
- this.version = 1;
79
- }
80
- else {
81
- const { version, count, shN } = this.meta;
82
- if (version !== 2) {
83
- throw new Error(`Unsupported SOGS version: ${version}`);
84
- }
85
- this.counts = count;
86
- this.shDegree = shN?.bands ?? 0;
87
- this.version = version;
88
- }
89
- }
90
- parse_v1(data, offset) {
91
- const setFn = data.set.bind(data);
92
- const setShFn = data.setShN.bind(data);
93
- const { meta, counts, shDegree, cached } = this;
94
- const [mean0, mean1, scale0, quat0, color0, centroids, labels] = cached.map(v => v.data);
95
- const { means: { mins: [centerMinX, centerMinY, centerMinZ], maxs: [centerMaxX, centerMaxY, centerMaxZ] }, scales: { mins: [scaleMinX, scaleMinY, scaleMinZ], maxs: [scaleMaxX, scaleMaxY, scaleMaxZ] }, sh0: { mins: [colorMinR, colorMinG, colorMinB, colorMinA], maxs: [colorMaxR, colorMaxG, colorMaxB, colorMaxA], }, shN, } = meta;
96
- const rangeX = (centerMaxX - centerMinX) / 65535;
97
- const rangeY = (centerMaxY - centerMinY) / 65535;
98
- const rangeZ = (centerMaxZ - centerMinZ) / 65535;
99
- const SX_LUT = new Float32Array(256);
100
- const SY_LUT = new Float32Array(256);
101
- const SZ_LUT = new Float32Array(256);
102
- const scaleRangeX = (scaleMaxX - scaleMinX) / 255;
103
- const scaleRangeY = (scaleMaxY - scaleMinY) / 255;
104
- const scaleRangeZ = (scaleMaxZ - scaleMinZ) / 255;
105
- for (let i = 0; i < 256; i++) {
106
- SX_LUT[i] = Math.exp(scaleMinX + scaleRangeX * i);
107
- SY_LUT[i] = Math.exp(scaleMinY + scaleRangeY * i);
108
- SZ_LUT[i] = Math.exp(scaleMinZ + scaleRangeZ * i);
109
- }
110
- const A_LUT = new Float32Array(256);
111
- const colorRangeR = (colorMaxR - colorMinR) / 255;
112
- const colorRangeG = (colorMaxG - colorMinG) / 255;
113
- const colorRangeB = (colorMaxB - colorMinB) / 255;
114
- const colorRangeA = (colorMaxA - colorMinA) / 255;
115
- for (let i = 0; i < 256; i++) {
116
- A_LUT[i] = 1.0 / (1.0 + Math.exp(-(colorMinA + colorRangeA * i)));
117
- }
118
- const single = {
119
- x: 0, y: 0, z: 0,
120
- sx: 0, sy: 0, sz: 0,
121
- qx: 0, qy: 0, qz: 0, qw: 0,
122
- r: 0, g: 0, b: 0, a: 0,
123
- shN: [],
124
- };
125
- for (let i = 0; i < counts; i++) {
126
- const i4 = i * 4;
127
- const x = centerMinX + rangeX * (mean0[i4 + 0] + (mean1[i4 + 0] << 8));
128
- const y = centerMinY + rangeY * (mean0[i4 + 1] + (mean1[i4 + 1] << 8));
129
- const z = centerMinZ + rangeZ * (mean0[i4 + 2] + (mean1[i4 + 2] << 8));
130
- single.x = Math.sign(x) * (Math.exp(Math.abs(x)) - 1);
131
- single.y = Math.sign(y) * (Math.exp(Math.abs(y)) - 1);
132
- single.z = Math.sign(z) * (Math.exp(Math.abs(z)) - 1);
133
- single.sx = SX_LUT[scale0[i4 + 0]];
134
- single.sy = SY_LUT[scale0[i4 + 1]];
135
- single.sz = SZ_LUT[scale0[i4 + 2]];
136
- TEMP_ROT[0] = (quat0[i4 + 0] / 255 - 0.5) * Math.SQRT2;
137
- TEMP_ROT[1] = (quat0[i4 + 1] / 255 - 0.5) * Math.SQRT2;
138
- TEMP_ROT[2] = (quat0[i4 + 2] / 255 - 0.5) * Math.SQRT2;
139
- TEMP_ROT[3] = Math.sqrt(Math.max(0, 1.0 - TEMP_ROT[0] * TEMP_ROT[0] - TEMP_ROT[1] * TEMP_ROT[1] - TEMP_ROT[2] * TEMP_ROT[2]));
140
- const PERM = PERM_TABLE[quat0[i4 + 3] - 252];
141
- single.qx = TEMP_ROT[PERM[0]];
142
- single.qy = TEMP_ROT[PERM[1]];
143
- single.qz = TEMP_ROT[PERM[2]];
144
- single.qw = TEMP_ROT[PERM[3]];
145
- single.r = SH_C0 * (colorMinR + colorRangeR * color0[i4 + 0]) + 0.5;
146
- single.g = SH_C0 * (colorMinG + colorRangeG * color0[i4 + 1]) + 0.5;
147
- single.b = SH_C0 * (colorMinB + colorRangeB * color0[i4 + 2]) + 0.5;
148
- single.a = A_LUT[color0[i4 + 3]];
149
- setFn(offset + i, single);
150
- }
151
- if (shN) {
152
- const centroidTexWidth = cached[5].width;
153
- const { mins: min, maxs: max } = shN;
154
- const range = (max - min) / 255;
155
- const shCounts = SH_MAPS[shDegree];
156
- const sh = new Array(shCounts);
157
- const shCoeffs = shCounts / 3;
158
- for (let i = 0; i < counts; i++) {
159
- const i4 = i * 4;
160
- const label = labels[i4] + (labels[i4 + 1] << 8);
161
- const o = ((label >>> 6) * centroidTexWidth + (label & 63) * 15) * 4;
162
- for (let j = 0; j < shCoeffs; j++) {
163
- sh[j * 3 + 0] = min + range * centroids[o + j * 4 + 0];
164
- sh[j * 3 + 1] = min + range * centroids[o + j * 4 + 1];
165
- sh[j * 3 + 2] = min + range * centroids[o + j * 4 + 2];
166
- }
167
- setShFn(offset + i, sh);
168
- }
169
- }
170
- }
171
- parse_v2(data, offset) {
172
- const setFn = data.set.bind(data);
173
- const setShFn = data.setShN.bind(data);
174
- const { meta, counts, shDegree, cached } = this;
175
- const { means, scales, sh0, shN } = meta;
176
- const { mins: [centerMinX, centerMinY, centerMinZ], maxs: [centerMaxX, centerMaxY, centerMaxZ], } = means;
177
- const { codebook: scaleCodebook } = scales;
178
- const { codebook: sh0Codebook } = sh0;
179
- const [mean0, mean1, scale0, quat0, color0, centroids, labels] = cached.map(img => img.data);
180
- const rangeX = (centerMaxX - centerMinX) / 65535;
181
- const rangeY = (centerMaxY - centerMinY) / 65535;
182
- const rangeZ = (centerMaxZ - centerMinZ) / 65535;
183
- const SCALE_LUT = scaleCodebook.map(v => Math.exp(v));
184
- const single = {
185
- x: 0, y: 0, z: 0,
186
- sx: 0, sy: 0, sz: 0,
187
- qx: 0, qy: 0, qz: 0, qw: 0,
188
- r: 0, g: 0, b: 0, a: 0,
189
- shN: [],
190
- };
191
- for (let i = 0; i < counts; i++) {
192
- const i4 = i * 4;
193
- const x = centerMinX + rangeX * (mean0[i4 + 0] + (mean1[i4 + 0] << 8));
194
- const y = centerMinY + rangeY * (mean0[i4 + 1] + (mean1[i4 + 1] << 8));
195
- const z = centerMinZ + rangeZ * (mean0[i4 + 2] + (mean1[i4 + 2] << 8));
196
- single.x = Math.sign(x) * (Math.exp(Math.abs(x)) - 1);
197
- single.y = Math.sign(y) * (Math.exp(Math.abs(y)) - 1);
198
- single.z = Math.sign(z) * (Math.exp(Math.abs(z)) - 1);
199
- single.sx = SCALE_LUT[scale0[i4 + 0]];
200
- single.sy = SCALE_LUT[scale0[i4 + 1]];
201
- single.sz = SCALE_LUT[scale0[i4 + 2]];
202
- TEMP_ROT[0] = (quat0[i4 + 0] / 255 - 0.5) * Math.SQRT2;
203
- TEMP_ROT[1] = (quat0[i4 + 1] / 255 - 0.5) * Math.SQRT2;
204
- TEMP_ROT[2] = (quat0[i4 + 2] / 255 - 0.5) * Math.SQRT2;
205
- TEMP_ROT[3] = Math.sqrt(Math.max(0, 1.0 - TEMP_ROT[0] * TEMP_ROT[0] - TEMP_ROT[1] * TEMP_ROT[1] - TEMP_ROT[2] * TEMP_ROT[2]));
206
- const PERM = PERM_TABLE[quat0[i4 + 3] - 252];
207
- single.qx = TEMP_ROT[PERM[0]];
208
- single.qy = TEMP_ROT[PERM[1]];
209
- single.qz = TEMP_ROT[PERM[2]];
210
- single.qw = TEMP_ROT[PERM[3]];
211
- single.r = SH_C0 * sh0Codebook[color0[i4 + 0]] + 0.5;
212
- single.g = SH_C0 * sh0Codebook[color0[i4 + 1]] + 0.5;
213
- single.b = SH_C0 * sh0Codebook[color0[i4 + 2]] + 0.5;
214
- single.a = color0[i4 + 3] / 255;
215
- setFn(offset + i, single);
216
- }
217
- if (shN) {
218
- const { codebook } = shN;
219
- const shCounts = SH_MAPS[shDegree];
220
- const shCoeffs = shCounts / 3;
221
- const offsetItemSize = shCoeffs * 4;
222
- const sh = new Array(shCounts);
223
- for (let i = 0; i < counts; i++) {
224
- const i4 = i * 4;
225
- const o = (labels[i4 + 0] + (labels[i4 + 1] << 8)) * offsetItemSize;
226
- for (let j = 0; j < shCoeffs; j++) {
227
- sh[j * 3] = codebook[centroids[o + j * 4 + 0]];
228
- sh[j * 3 + 1] = codebook[centroids[o + j * 4 + 1]];
229
- sh[j * 3 + 2] = codebook[centroids[o + j * 4 + 2]];
230
- }
231
- setShFn(offset + i, sh);
232
- }
233
- }
234
- }
235
- async loadTexture(path) {
236
- let buffer = this.refs[path];
237
- if (!buffer) {
238
- if (isUrl(path)) {
239
- buffer = await fetch(path)
240
- .then(res => res.arrayBuffer())
241
- .then(buf => new Uint8Array(buf));
242
- }
243
- }
244
- if (!buffer) {
245
- throw new Error(`Cannot load texture: ${path}`);
246
- }
247
- const { data, width, height } = decodeWebP(buffer);
248
- return {
249
- data: new Uint8Array(data),
250
- width,
251
- height,
252
- };
253
- }
254
- async read(stream, contentLength, data) {
255
- await this.load(stream, contentLength);
256
- const BlockOffset = await data.initBlock(this.counts, this.shDegree);
257
- const { means, scales, quats, sh0, shN } = this.meta;
258
- this.cached = await Promise.all([
259
- means.files[0], means.files[1],
260
- scales.files[0], quats.files[0],
261
- sh0.files[0], shN?.files[0], shN?.files[1],
262
- ].filter(path => !!path).map(path => this.loadTexture(path)));
263
- if (this.version === 1) {
264
- this.parse_v1(data, BlockOffset);
265
- }
266
- else if (this.version === 2) {
267
- this.parse_v2(data, BlockOffset);
268
- }
269
- else {
270
- throw new Error(`Unsupported SOG version: ${this.version}`);
271
- }
272
- data.finishBlock();
273
- }
274
- async write(stream, data, indices = mortonSort(data)) {
275
- const { counts, shDegree, shCounts, table } = data;
276
- const width = Math.ceil(Math.sqrt(counts) / 4) * 4;
277
- const height = Math.ceil(counts / width / 4) * 4;
278
- const channels = 4;
279
- const single = {
280
- x: 0, y: 0, z: 0,
281
- sx: 0, sy: 0, sz: 0,
282
- qx: 0, qy: 0, qz: 0, qw: 0,
283
- r: 0, g: 0, b: 0, a: 0,
284
- shN: new Array(shCounts),
285
- };
286
- const webPProfile = new WebPLosslessProfile();
287
- const output = {};
288
- const meta = {
289
- version: 2,
290
- count: counts,
291
- means: {
292
- mins: [],
293
- maxs: [],
294
- files: [
295
- 'means_l.webp',
296
- 'means_u.webp'
297
- ]
298
- },
299
- scales: {
300
- codebook: [],
301
- files: ['scales.webp']
302
- },
303
- quats: {
304
- files: ['quats.webp']
305
- },
306
- sh0: {
307
- codebook: [],
308
- files: ['sh0.webp']
309
- }
310
- };
311
- // means
312
- {
313
- logger.time('SOG encoding means');
314
- const xCol = table[0 /* ColIdx.x */];
315
- const yCol = table[1 /* ColIdx.y */];
316
- const zCol = table[2 /* ColIdx.z */];
317
- // calculate minmax & transform
318
- let minX = Infinity;
319
- let minY = Infinity;
320
- let minZ = Infinity;
321
- let maxX = -Infinity;
322
- let maxY = -Infinity;
323
- let maxZ = -Infinity;
324
- for (let i = 0; i < counts; i++) {
325
- const idx = indices[i];
326
- const x = xCol[idx];
327
- const y = yCol[idx];
328
- const z = zCol[idx];
329
- if (x < minX) {
330
- minX = x;
331
- }
332
- if (x > maxX) {
333
- maxX = x;
334
- }
335
- if (y < minY) {
336
- minY = y;
337
- }
338
- if (y > maxY) {
339
- maxY = y;
340
- }
341
- if (z < minZ) {
342
- minZ = z;
343
- }
344
- if (z > maxZ) {
345
- maxZ = z;
346
- }
347
- }
348
- minX = logTransform(minX);
349
- minY = logTransform(minY);
350
- minZ = logTransform(minZ);
351
- maxX = logTransform(maxX);
352
- maxY = logTransform(maxY);
353
- maxZ = logTransform(maxZ);
354
- const scaleX = 65535 / Math.max(maxX - minX, 1e-9);
355
- const scaleY = 65535 / Math.max(maxY - minY, 1e-9);
356
- const scaleZ = 65535 / Math.max(maxZ - minZ, 1e-9);
357
- // encode means
358
- const meansL = new Uint8Array(width * height * channels).fill(0xff);
359
- const meansU = new Uint8Array(width * height * channels).fill(0xff);
360
- for (let i = 0; i < indices.length; i++) {
361
- const idx = indices[i];
362
- const x = (logTransform(xCol[idx]) - minX) * scaleX;
363
- const y = (logTransform(yCol[idx]) - minY) * scaleY;
364
- const z = (logTransform(zCol[idx]) - minZ) * scaleZ;
365
- meansL[i * 4 + 0] = x & 0xff;
366
- meansL[i * 4 + 1] = y & 0xff;
367
- meansL[i * 4 + 2] = z & 0xff;
368
- meansU[i * 4 + 0] = (x >> 8) & 0xff;
369
- meansU[i * 4 + 1] = (y >> 8) & 0xff;
370
- meansU[i * 4 + 2] = (z >> 8) & 0xff;
371
- }
372
- output['means_l.webp'] = encodeWebP(meansL, width, height, webPProfile);
373
- output['means_u.webp'] = encodeWebP(meansU, width, height, webPProfile);
374
- meta.means.mins = [minX, minY, minZ];
375
- meta.means.maxs = [maxX, maxY, maxZ];
376
- logger.timeEnd('SOG encoding means');
377
- }
378
- // quaternions
379
- {
380
- logger.time('SOG encoding quaternions');
381
- const quats = new Uint8Array(width * height * channels);
382
- const q = [0, 0, 0, 0];
383
- for (let i = 0; i < indices.length; ++i) {
384
- data.getQuat(indices[i], single);
385
- q[0] = single.qw;
386
- q[1] = single.qx;
387
- q[2] = single.qy;
388
- q[3] = single.qz;
389
- const l = Math.sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]);
390
- // normalize
391
- q.forEach((v, j) => {
392
- q[j] = v / l;
393
- });
394
- // find max component
395
- const maxComp = q.reduce((v, _, i) => (Math.abs(q[i]) > Math.abs(q[v]) ? i : v), 0);
396
- // invert if max component is negative
397
- if (q[maxComp] < 0) {
398
- q.forEach((_, j) => {
399
- q[j] *= -1;
400
- });
401
- }
402
- // scale by sqrt(2) to fit in [-1, 1] range
403
- q.forEach((_, j) => q[j] *= Math.SQRT2);
404
- const idx = [
405
- [1, 2, 3],
406
- [0, 2, 3],
407
- [0, 1, 3],
408
- [0, 1, 2]
409
- ][maxComp];
410
- quats[i * 4] = (q[idx[0]] * 0.5 + 0.5) * 255;
411
- quats[i * 4 + 1] = (q[idx[1]] * 0.5 + 0.5) * 255;
412
- quats[i * 4 + 2] = (q[idx[2]] * 0.5 + 0.5) * 255;
413
- quats[i * 4 + 3] = 252 + maxComp;
414
- }
415
- output['quats.webp'] = encodeWebP(quats, width, height, webPProfile);
416
- logger.timeEnd('SOG encoding quaternions');
417
- }
418
- // scales
419
- {
420
- logger.time('SOG encoding scales');
421
- const scaleData = quantize1d([table[3 /* ColIdx.sx */], table[4 /* ColIdx.sy */], table[5 /* ColIdx.sz */]], undefined, undefined, Math.log);
422
- const tableData = writeTableData(scaleData.labels, indices, width, height, channels);
423
- output['scales.webp'] = encodeWebP(tableData, width, height, webPProfile);
424
- meta.scales.codebook = Array.from(scaleData.centroids);
425
- logger.timeEnd('SOG encoding scales');
426
- }
427
- // colors
428
- {
429
- logger.time('SOG encoding colors');
430
- const colorData = quantize1d([table[10 /* ColIdx.r */], table[11 /* ColIdx.g */], table[12 /* ColIdx.b */]], undefined, undefined, v => (v - 0.5) / SH_C0);
431
- const aCol = table[13 /* ColIdx.a */];
432
- const opacityData = new Uint8Array(aCol.length);
433
- for (let i = 0; i < counts; ++i) {
434
- opacityData[i] = clamp(aCol[i] * 255, 0, 255);
435
- }
436
- colorData.labels.push(opacityData);
437
- const tableData = writeTableData(colorData.labels, indices, width, height, channels);
438
- output['sh0.webp'] = encodeWebP(tableData, width, height, webPProfile);
439
- meta.sh0.codebook = Array.from(colorData.centroids);
440
- logger.timeEnd('SOG encoding colors');
441
- }
442
- // SH
443
- if (shDegree > 0) {
444
- logger.time(`SOG encoding SH${shDegree}`);
445
- const shCoeffs = shCounts / 3;
446
- const shDataTable = [];
447
- for (const i of buildSHTableMap(shCoeffs)) {
448
- shDataTable.push(table[14 /* ColIdx.shOffset */ + i]);
449
- }
450
- const paletteSize = Math.min(64, 2 ** Math.floor(Math.log2(indices.length / 1024))) * 1024;
451
- const device = await getOrCreateDevice();
452
- logger.info(`SOG SH${shDegree} k-means with clusters=${paletteSize} iterations=${this.iterations}`);
453
- logger.time(`SOG SH${shDegree} k-means`);
454
- const { centroids, labels } = await kmeans(shDataTable, paletteSize, this.iterations, device);
455
- logger.timeEnd(`SOG SH${shDegree} k-means`);
456
- const codebook = quantize1d(centroids);
457
- // write centroids
458
- const centroidsBuf = new Uint8Array(64 * shCoeffs * Math.ceil(centroids[0].length / 64) * channels).fill(0xff);
459
- const centroidsRow = [];
460
- for (let i = 0; i < centroids[0].length; ++i) {
461
- codebook.labels.forEach((column, index) => {
462
- centroidsRow[index] = column[i];
463
- });
464
- for (let j = 0; j < shCoeffs; ++j) {
465
- centroidsBuf[i * shCoeffs * 4 + j * 4 + 0] = centroidsRow[shCoeffs * 0 + j];
466
- centroidsBuf[i * shCoeffs * 4 + j * 4 + 1] = centroidsRow[shCoeffs * 1 + j];
467
- centroidsBuf[i * shCoeffs * 4 + j * 4 + 2] = centroidsRow[shCoeffs * 2 + j];
468
- }
469
- }
470
- output['shN_centroids.webp'] = encodeWebP(centroidsBuf, 64 * shCoeffs, Math.ceil(centroids[0].length / 64), webPProfile);
471
- // write labels
472
- const labelsBuf = new Uint8Array(width * height * channels).fill(0xff);
473
- for (let i = 0; i < indices.length; ++i) {
474
- const label = labels[indices[i]];
475
- labelsBuf[i * 4 + 0] = label & 0xff;
476
- labelsBuf[i * 4 + 1] = (label >> 8) & 0xff;
477
- }
478
- output['shN_labels.webp'] = encodeWebP(labelsBuf, width, height, webPProfile);
479
- meta.shN = {
480
- count: paletteSize,
481
- bands: shDegree,
482
- codebook: Array.from(codebook.centroids),
483
- files: [
484
- 'shN_centroids.webp',
485
- 'shN_labels.webp'
486
- ]
487
- };
488
- logger.timeEnd(`SOG encoding SH${shDegree}`);
489
- }
490
- output['meta.json'] = Buffer.from(JSON.stringify(meta), 'utf-8');
491
- const result = zipSync(output);
492
- await stream.getWriter().write(result);
493
- }
494
- }
1
+ import { unzipSync, zipSync } from 'fflate';
2
+ import { Buffer } from 'node:buffer';
3
+ import { decodeWebP, encodeWebP, WebPLosslessProfile } from '../native/index.js';
4
+ import { ColIdx } from '../SplatData.js';
5
+ import { SH_C0, SH_MAPS, NUM_F_REST_TO_SH_DEGREE } from '../constant.js';
6
+ import { getOrCreateDevice, kmeans, logger, mortonSort, quantize1d, isUrl, extractFromRootDir, clamp, } from '../utils/index.js';
7
+ const ZIP_MAGIC = 0x04034b50;
8
+ const PERM_TABLE = [
9
+ // original quat idx ---> actual storage idx
10
+ [0, 1, 2, 3],
11
+ [3, 1, 2, 0],
12
+ [1, 3, 2, 0],
13
+ [1, 2, 3, 0],
14
+ ];
15
+ const TEMP_ROT = new Float32Array(4);
16
+ function logTransform(value) {
17
+ return Math.sign(value) * Math.log(Math.abs(value) + 1);
18
+ }
19
+ function writeTableData(table, indices, width, height, channels = 4) {
20
+ const data = new Uint8Array(width * height * channels);
21
+ const numColumns = table.length;
22
+ for (let i = 0; i < indices.length; ++i) {
23
+ const idx = indices[i];
24
+ data[i * channels + 0] = table[0][idx];
25
+ data[i * channels + 1] = numColumns > 1 ? table[1][idx] : 0;
26
+ data[i * channels + 2] = numColumns > 2 ? table[2][idx] : 0;
27
+ data[i * channels + 3] = numColumns > 3 ? table[3][idx] : 255;
28
+ }
29
+ return data;
30
+ }
31
+ function buildSHTableMap(shCoeffs) {
32
+ const result = [];
33
+ for (let i = 0; i < 3; i++) {
34
+ for (let j = 0; j < shCoeffs; j++) {
35
+ result.push(j * 3 + i);
36
+ }
37
+ }
38
+ return result;
39
+ }
40
+ export class SogFile {
41
+ constructor(iterations = 10) {
42
+ this.iterations = iterations;
43
+ this.counts = 0;
44
+ this.shDegree = 0;
45
+ /**
46
+ * @internal
47
+ */
48
+ this.refs = {};
49
+ }
50
+ async load(stream, contentLength) {
51
+ const buffer = new Uint8Array(contentLength);
52
+ const reader = stream.getReader();
53
+ let offset = 0;
54
+ while (true) {
55
+ const { done, value } = await reader.read();
56
+ if (done) {
57
+ break;
58
+ }
59
+ buffer.set(value, offset);
60
+ offset += value.length;
61
+ }
62
+ let metaBuffer = buffer;
63
+ const view = new DataView(buffer.buffer);
64
+ if (view.getUint32(0, true) === ZIP_MAGIC) {
65
+ this.refs = extractFromRootDir(unzipSync(buffer));
66
+ metaBuffer = this.refs['meta.json'];
67
+ if (!metaBuffer) {
68
+ throw new Error('SOG meta.json not found in the zip archive.');
69
+ }
70
+ }
71
+ this.meta = JSON.parse(new TextDecoder().decode(metaBuffer));
72
+ if (this.meta.version === undefined) {
73
+ const { means, quats, shN } = this.meta;
74
+ if (quats.encoding !== 'quaternion_packed') {
75
+ throw new Error('Unsupported quaternion encoding');
76
+ }
77
+ this.counts = means.shape[0];
78
+ this.shDegree = shN ? NUM_F_REST_TO_SH_DEGREE[shN.shape[1]] : 0;
79
+ this.version = 1;
80
+ }
81
+ else {
82
+ const { version, count, shN } = this.meta;
83
+ if (version !== 2) {
84
+ throw new Error(`Unsupported SOGS version: ${version}`);
85
+ }
86
+ this.counts = count;
87
+ this.shDegree = shN?.bands ?? 0;
88
+ this.version = version;
89
+ }
90
+ }
91
+ parse_v1(data, offset) {
92
+ const setFn = data.set.bind(data);
93
+ const setShFn = data.setShN.bind(data);
94
+ const { meta, counts, shDegree, cached } = this;
95
+ const [mean0, mean1, scale0, quat0, color0, centroids, labels] = cached.map(v => v.data);
96
+ const { means: { mins: [centerMinX, centerMinY, centerMinZ], maxs: [centerMaxX, centerMaxY, centerMaxZ], }, scales: { mins: [scaleMinX, scaleMinY, scaleMinZ], maxs: [scaleMaxX, scaleMaxY, scaleMaxZ], }, sh0: { mins: [colorMinR, colorMinG, colorMinB, colorMinA], maxs: [colorMaxR, colorMaxG, colorMaxB, colorMaxA], }, shN, } = meta;
97
+ const rangeX = (centerMaxX - centerMinX) / 65535;
98
+ const rangeY = (centerMaxY - centerMinY) / 65535;
99
+ const rangeZ = (centerMaxZ - centerMinZ) / 65535;
100
+ const SX_LUT = new Float32Array(256);
101
+ const SY_LUT = new Float32Array(256);
102
+ const SZ_LUT = new Float32Array(256);
103
+ const scaleRangeX = (scaleMaxX - scaleMinX) / 255;
104
+ const scaleRangeY = (scaleMaxY - scaleMinY) / 255;
105
+ const scaleRangeZ = (scaleMaxZ - scaleMinZ) / 255;
106
+ for (let i = 0; i < 256; i++) {
107
+ SX_LUT[i] = Math.exp(scaleMinX + scaleRangeX * i);
108
+ SY_LUT[i] = Math.exp(scaleMinY + scaleRangeY * i);
109
+ SZ_LUT[i] = Math.exp(scaleMinZ + scaleRangeZ * i);
110
+ }
111
+ const A_LUT = new Float32Array(256);
112
+ const colorRangeR = (colorMaxR - colorMinR) / 255;
113
+ const colorRangeG = (colorMaxG - colorMinG) / 255;
114
+ const colorRangeB = (colorMaxB - colorMinB) / 255;
115
+ const colorRangeA = (colorMaxA - colorMinA) / 255;
116
+ for (let i = 0; i < 256; i++) {
117
+ A_LUT[i] = 1.0 / (1.0 + Math.exp(-(colorMinA + colorRangeA * i)));
118
+ }
119
+ const single = {
120
+ x: 0,
121
+ y: 0,
122
+ z: 0,
123
+ sx: 0,
124
+ sy: 0,
125
+ sz: 0,
126
+ qx: 0,
127
+ qy: 0,
128
+ qz: 0,
129
+ qw: 0,
130
+ r: 0,
131
+ g: 0,
132
+ b: 0,
133
+ a: 0,
134
+ shN: [],
135
+ };
136
+ for (let i = 0; i < counts; i++) {
137
+ const i4 = i * 4;
138
+ const x = centerMinX + rangeX * (mean0[i4 + 0] + (mean1[i4 + 0] << 8));
139
+ const y = centerMinY + rangeY * (mean0[i4 + 1] + (mean1[i4 + 1] << 8));
140
+ const z = centerMinZ + rangeZ * (mean0[i4 + 2] + (mean1[i4 + 2] << 8));
141
+ single.x = Math.sign(x) * (Math.exp(Math.abs(x)) - 1);
142
+ single.y = Math.sign(y) * (Math.exp(Math.abs(y)) - 1);
143
+ single.z = Math.sign(z) * (Math.exp(Math.abs(z)) - 1);
144
+ single.sx = SX_LUT[scale0[i4 + 0]];
145
+ single.sy = SY_LUT[scale0[i4 + 1]];
146
+ single.sz = SZ_LUT[scale0[i4 + 2]];
147
+ TEMP_ROT[0] = (quat0[i4 + 0] / 255 - 0.5) * Math.SQRT2;
148
+ TEMP_ROT[1] = (quat0[i4 + 1] / 255 - 0.5) * Math.SQRT2;
149
+ TEMP_ROT[2] = (quat0[i4 + 2] / 255 - 0.5) * Math.SQRT2;
150
+ TEMP_ROT[3] = Math.sqrt(Math.max(0, 1.0 - TEMP_ROT[0] * TEMP_ROT[0] - TEMP_ROT[1] * TEMP_ROT[1] - TEMP_ROT[2] * TEMP_ROT[2]));
151
+ const PERM = PERM_TABLE[quat0[i4 + 3] - 252];
152
+ single.qx = TEMP_ROT[PERM[0]];
153
+ single.qy = TEMP_ROT[PERM[1]];
154
+ single.qz = TEMP_ROT[PERM[2]];
155
+ single.qw = TEMP_ROT[PERM[3]];
156
+ single.r = SH_C0 * (colorMinR + colorRangeR * color0[i4 + 0]) + 0.5;
157
+ single.g = SH_C0 * (colorMinG + colorRangeG * color0[i4 + 1]) + 0.5;
158
+ single.b = SH_C0 * (colorMinB + colorRangeB * color0[i4 + 2]) + 0.5;
159
+ single.a = A_LUT[color0[i4 + 3]];
160
+ setFn(offset + i, single);
161
+ }
162
+ if (shN) {
163
+ const centroidTexWidth = cached[5].width;
164
+ const { mins: min, maxs: max } = shN;
165
+ const range = (max - min) / 255;
166
+ const shCounts = SH_MAPS[shDegree];
167
+ const sh = new Array(shCounts);
168
+ const shCoeffs = shCounts / 3;
169
+ for (let i = 0; i < counts; i++) {
170
+ const i4 = i * 4;
171
+ const label = labels[i4] + (labels[i4 + 1] << 8);
172
+ const o = ((label >>> 6) * centroidTexWidth + (label & 63) * 15) * 4;
173
+ for (let j = 0; j < shCoeffs; j++) {
174
+ sh[j * 3 + 0] = min + range * centroids[o + j * 4 + 0];
175
+ sh[j * 3 + 1] = min + range * centroids[o + j * 4 + 1];
176
+ sh[j * 3 + 2] = min + range * centroids[o + j * 4 + 2];
177
+ }
178
+ setShFn(offset + i, sh);
179
+ }
180
+ }
181
+ }
182
+ parse_v2(data, offset) {
183
+ const setFn = data.set.bind(data);
184
+ const setShFn = data.setShN.bind(data);
185
+ const { meta, counts, shDegree, cached } = this;
186
+ const { means, scales, sh0, shN } = meta;
187
+ const { mins: [centerMinX, centerMinY, centerMinZ], maxs: [centerMaxX, centerMaxY, centerMaxZ], } = means;
188
+ const { codebook: scaleCodebook } = scales;
189
+ const { codebook: sh0Codebook } = sh0;
190
+ const [mean0, mean1, scale0, quat0, color0, centroids, labels] = cached.map(img => img.data);
191
+ const rangeX = (centerMaxX - centerMinX) / 65535;
192
+ const rangeY = (centerMaxY - centerMinY) / 65535;
193
+ const rangeZ = (centerMaxZ - centerMinZ) / 65535;
194
+ const SCALE_LUT = scaleCodebook.map(v => Math.exp(v));
195
+ const single = {
196
+ x: 0,
197
+ y: 0,
198
+ z: 0,
199
+ sx: 0,
200
+ sy: 0,
201
+ sz: 0,
202
+ qx: 0,
203
+ qy: 0,
204
+ qz: 0,
205
+ qw: 0,
206
+ r: 0,
207
+ g: 0,
208
+ b: 0,
209
+ a: 0,
210
+ shN: [],
211
+ };
212
+ for (let i = 0; i < counts; i++) {
213
+ const i4 = i * 4;
214
+ const x = centerMinX + rangeX * (mean0[i4 + 0] + (mean1[i4 + 0] << 8));
215
+ const y = centerMinY + rangeY * (mean0[i4 + 1] + (mean1[i4 + 1] << 8));
216
+ const z = centerMinZ + rangeZ * (mean0[i4 + 2] + (mean1[i4 + 2] << 8));
217
+ single.x = Math.sign(x) * (Math.exp(Math.abs(x)) - 1);
218
+ single.y = Math.sign(y) * (Math.exp(Math.abs(y)) - 1);
219
+ single.z = Math.sign(z) * (Math.exp(Math.abs(z)) - 1);
220
+ single.sx = SCALE_LUT[scale0[i4 + 0]];
221
+ single.sy = SCALE_LUT[scale0[i4 + 1]];
222
+ single.sz = SCALE_LUT[scale0[i4 + 2]];
223
+ TEMP_ROT[0] = (quat0[i4 + 0] / 255 - 0.5) * Math.SQRT2;
224
+ TEMP_ROT[1] = (quat0[i4 + 1] / 255 - 0.5) * Math.SQRT2;
225
+ TEMP_ROT[2] = (quat0[i4 + 2] / 255 - 0.5) * Math.SQRT2;
226
+ TEMP_ROT[3] = Math.sqrt(Math.max(0, 1.0 - TEMP_ROT[0] * TEMP_ROT[0] - TEMP_ROT[1] * TEMP_ROT[1] - TEMP_ROT[2] * TEMP_ROT[2]));
227
+ const PERM = PERM_TABLE[quat0[i4 + 3] - 252];
228
+ single.qx = TEMP_ROT[PERM[0]];
229
+ single.qy = TEMP_ROT[PERM[1]];
230
+ single.qz = TEMP_ROT[PERM[2]];
231
+ single.qw = TEMP_ROT[PERM[3]];
232
+ single.r = SH_C0 * sh0Codebook[color0[i4 + 0]] + 0.5;
233
+ single.g = SH_C0 * sh0Codebook[color0[i4 + 1]] + 0.5;
234
+ single.b = SH_C0 * sh0Codebook[color0[i4 + 2]] + 0.5;
235
+ single.a = color0[i4 + 3] / 255;
236
+ setFn(offset + i, single);
237
+ }
238
+ if (shN) {
239
+ const { codebook } = shN;
240
+ const shCounts = SH_MAPS[shDegree];
241
+ const shCoeffs = shCounts / 3;
242
+ const offsetItemSize = shCoeffs * 4;
243
+ const sh = new Array(shCounts);
244
+ for (let i = 0; i < counts; i++) {
245
+ const i4 = i * 4;
246
+ const o = (labels[i4 + 0] + (labels[i4 + 1] << 8)) * offsetItemSize;
247
+ for (let j = 0; j < shCoeffs; j++) {
248
+ sh[j * 3] = codebook[centroids[o + j * 4 + 0]];
249
+ sh[j * 3 + 1] = codebook[centroids[o + j * 4 + 1]];
250
+ sh[j * 3 + 2] = codebook[centroids[o + j * 4 + 2]];
251
+ }
252
+ setShFn(offset + i, sh);
253
+ }
254
+ }
255
+ }
256
+ async loadTexture(path) {
257
+ let buffer = this.refs[path];
258
+ if (!buffer) {
259
+ if (isUrl(path)) {
260
+ buffer = await fetch(path)
261
+ .then(res => res.arrayBuffer())
262
+ .then(buf => new Uint8Array(buf));
263
+ }
264
+ }
265
+ if (!buffer) {
266
+ throw new Error(`Cannot load texture: ${path}`);
267
+ }
268
+ const { data, width, height } = decodeWebP(buffer);
269
+ return {
270
+ data: new Uint8Array(data),
271
+ width,
272
+ height,
273
+ };
274
+ }
275
+ async read(stream, contentLength, data) {
276
+ await this.load(stream, contentLength);
277
+ const BlockOffset = await data.initBlock(this.counts, this.shDegree);
278
+ const { means, scales, quats, sh0, shN } = this.meta;
279
+ this.cached = await Promise.all([
280
+ means.files[0],
281
+ means.files[1],
282
+ scales.files[0],
283
+ quats.files[0],
284
+ sh0.files[0],
285
+ shN?.files[0],
286
+ shN?.files[1],
287
+ ]
288
+ .filter(path => !!path)
289
+ .map(path => this.loadTexture(path)));
290
+ if (this.version === 1) {
291
+ this.parse_v1(data, BlockOffset);
292
+ }
293
+ else if (this.version === 2) {
294
+ this.parse_v2(data, BlockOffset);
295
+ }
296
+ else {
297
+ throw new Error(`Unsupported SOG version: ${this.version}`);
298
+ }
299
+ data.finishBlock();
300
+ }
301
+ async write(stream, data, indices = mortonSort(data)) {
302
+ const { counts, shDegree, shCounts, table } = data;
303
+ const width = Math.ceil(Math.sqrt(counts) / 4) * 4;
304
+ const height = Math.ceil(counts / width / 4) * 4;
305
+ const channels = 4;
306
+ const single = {
307
+ x: 0,
308
+ y: 0,
309
+ z: 0,
310
+ sx: 0,
311
+ sy: 0,
312
+ sz: 0,
313
+ qx: 0,
314
+ qy: 0,
315
+ qz: 0,
316
+ qw: 0,
317
+ r: 0,
318
+ g: 0,
319
+ b: 0,
320
+ a: 0,
321
+ shN: new Array(shCounts),
322
+ };
323
+ const webPProfile = new WebPLosslessProfile();
324
+ const output = {};
325
+ const meta = {
326
+ version: 2,
327
+ count: counts,
328
+ means: {
329
+ mins: [],
330
+ maxs: [],
331
+ files: ['means_l.webp', 'means_u.webp'],
332
+ },
333
+ scales: {
334
+ codebook: [],
335
+ files: ['scales.webp'],
336
+ },
337
+ quats: {
338
+ files: ['quats.webp'],
339
+ },
340
+ sh0: {
341
+ codebook: [],
342
+ files: ['sh0.webp'],
343
+ },
344
+ };
345
+ // means
346
+ {
347
+ logger.time('SOG encoding means');
348
+ const xCol = table[ColIdx.x];
349
+ const yCol = table[ColIdx.y];
350
+ const zCol = table[ColIdx.z];
351
+ // calculate minmax & transform
352
+ let minX = Infinity;
353
+ let minY = Infinity;
354
+ let minZ = Infinity;
355
+ let maxX = -Infinity;
356
+ let maxY = -Infinity;
357
+ let maxZ = -Infinity;
358
+ for (let i = 0; i < counts; i++) {
359
+ const idx = indices[i];
360
+ const x = xCol[idx];
361
+ const y = yCol[idx];
362
+ const z = zCol[idx];
363
+ if (x < minX) {
364
+ minX = x;
365
+ }
366
+ if (x > maxX) {
367
+ maxX = x;
368
+ }
369
+ if (y < minY) {
370
+ minY = y;
371
+ }
372
+ if (y > maxY) {
373
+ maxY = y;
374
+ }
375
+ if (z < minZ) {
376
+ minZ = z;
377
+ }
378
+ if (z > maxZ) {
379
+ maxZ = z;
380
+ }
381
+ }
382
+ minX = logTransform(minX);
383
+ minY = logTransform(minY);
384
+ minZ = logTransform(minZ);
385
+ maxX = logTransform(maxX);
386
+ maxY = logTransform(maxY);
387
+ maxZ = logTransform(maxZ);
388
+ const scaleX = 65535 / Math.max(maxX - minX, 1e-9);
389
+ const scaleY = 65535 / Math.max(maxY - minY, 1e-9);
390
+ const scaleZ = 65535 / Math.max(maxZ - minZ, 1e-9);
391
+ // encode means
392
+ const meansL = new Uint8Array(width * height * channels).fill(0xff);
393
+ const meansU = new Uint8Array(width * height * channels).fill(0xff);
394
+ for (let i = 0; i < indices.length; i++) {
395
+ const idx = indices[i];
396
+ const x = (logTransform(xCol[idx]) - minX) * scaleX;
397
+ const y = (logTransform(yCol[idx]) - minY) * scaleY;
398
+ const z = (logTransform(zCol[idx]) - minZ) * scaleZ;
399
+ meansL[i * 4 + 0] = x & 0xff;
400
+ meansL[i * 4 + 1] = y & 0xff;
401
+ meansL[i * 4 + 2] = z & 0xff;
402
+ meansU[i * 4 + 0] = (x >> 8) & 0xff;
403
+ meansU[i * 4 + 1] = (y >> 8) & 0xff;
404
+ meansU[i * 4 + 2] = (z >> 8) & 0xff;
405
+ }
406
+ output['means_l.webp'] = encodeWebP(meansL, width, height, webPProfile);
407
+ output['means_u.webp'] = encodeWebP(meansU, width, height, webPProfile);
408
+ meta.means.mins = [minX, minY, minZ];
409
+ meta.means.maxs = [maxX, maxY, maxZ];
410
+ logger.timeEnd('SOG encoding means');
411
+ }
412
+ // quaternions
413
+ {
414
+ logger.time('SOG encoding quaternions');
415
+ const quats = new Uint8Array(width * height * channels);
416
+ const q = [0, 0, 0, 0];
417
+ for (let i = 0; i < indices.length; ++i) {
418
+ data.getQuat(indices[i], single);
419
+ q[0] = single.qw;
420
+ q[1] = single.qx;
421
+ q[2] = single.qy;
422
+ q[3] = single.qz;
423
+ const l = Math.sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]);
424
+ // normalize
425
+ q.forEach((v, j) => {
426
+ q[j] = v / l;
427
+ });
428
+ // find max component
429
+ const maxComp = q.reduce((v, _, i) => (Math.abs(q[i]) > Math.abs(q[v]) ? i : v), 0);
430
+ // invert if max component is negative
431
+ if (q[maxComp] < 0) {
432
+ q.forEach((_, j) => {
433
+ q[j] *= -1;
434
+ });
435
+ }
436
+ // scale by sqrt(2) to fit in [-1, 1] range
437
+ q.forEach((_, j) => (q[j] *= Math.SQRT2));
438
+ const idx = [
439
+ [1, 2, 3],
440
+ [0, 2, 3],
441
+ [0, 1, 3],
442
+ [0, 1, 2],
443
+ ][maxComp];
444
+ quats[i * 4] = (q[idx[0]] * 0.5 + 0.5) * 255;
445
+ quats[i * 4 + 1] = (q[idx[1]] * 0.5 + 0.5) * 255;
446
+ quats[i * 4 + 2] = (q[idx[2]] * 0.5 + 0.5) * 255;
447
+ quats[i * 4 + 3] = 252 + maxComp;
448
+ }
449
+ output['quats.webp'] = encodeWebP(quats, width, height, webPProfile);
450
+ logger.timeEnd('SOG encoding quaternions');
451
+ }
452
+ // scales
453
+ {
454
+ logger.time('SOG encoding scales');
455
+ const scaleData = quantize1d([table[ColIdx.sx], table[ColIdx.sy], table[ColIdx.sz]], undefined, undefined, Math.log);
456
+ const tableData = writeTableData(scaleData.labels, indices, width, height, channels);
457
+ output['scales.webp'] = encodeWebP(tableData, width, height, webPProfile);
458
+ meta.scales.codebook = Array.from(scaleData.centroids);
459
+ logger.timeEnd('SOG encoding scales');
460
+ }
461
+ // colors
462
+ {
463
+ logger.time('SOG encoding colors');
464
+ const colorData = quantize1d([table[ColIdx.r], table[ColIdx.g], table[ColIdx.b]], undefined, undefined, v => (v - 0.5) / SH_C0);
465
+ const aCol = table[ColIdx.a];
466
+ const opacityData = new Uint8Array(aCol.length);
467
+ for (let i = 0; i < counts; ++i) {
468
+ opacityData[i] = clamp(aCol[i] * 255, 0, 255);
469
+ }
470
+ colorData.labels.push(opacityData);
471
+ const tableData = writeTableData(colorData.labels, indices, width, height, channels);
472
+ output['sh0.webp'] = encodeWebP(tableData, width, height, webPProfile);
473
+ meta.sh0.codebook = Array.from(colorData.centroids);
474
+ logger.timeEnd('SOG encoding colors');
475
+ }
476
+ // SH
477
+ if (shDegree > 0) {
478
+ logger.time(`SOG encoding SH${shDegree}`);
479
+ const shCoeffs = shCounts / 3;
480
+ const shDataTable = [];
481
+ for (const i of buildSHTableMap(shCoeffs)) {
482
+ shDataTable.push(table[ColIdx.shOffset + i]);
483
+ }
484
+ const paletteSize = Math.min(64, 2 ** Math.floor(Math.log2(indices.length / 1024))) * 1024;
485
+ const device = await getOrCreateDevice();
486
+ logger.info(`SOG SH${shDegree} k-means with clusters=${paletteSize} iterations=${this.iterations}`);
487
+ logger.time(`SOG SH${shDegree} k-means`);
488
+ const { centroids, labels } = await kmeans(shDataTable, paletteSize, this.iterations, device);
489
+ logger.timeEnd(`SOG SH${shDegree} k-means`);
490
+ const codebook = quantize1d(centroids);
491
+ // write centroids
492
+ const centroidsBuf = new Uint8Array(64 * shCoeffs * Math.ceil(centroids[0].length / 64) * channels).fill(0xff);
493
+ const centroidsRow = [];
494
+ for (let i = 0; i < centroids[0].length; ++i) {
495
+ codebook.labels.forEach((column, index) => {
496
+ centroidsRow[index] = column[i];
497
+ });
498
+ for (let j = 0; j < shCoeffs; ++j) {
499
+ centroidsBuf[i * shCoeffs * 4 + j * 4 + 0] = centroidsRow[shCoeffs * 0 + j];
500
+ centroidsBuf[i * shCoeffs * 4 + j * 4 + 1] = centroidsRow[shCoeffs * 1 + j];
501
+ centroidsBuf[i * shCoeffs * 4 + j * 4 + 2] = centroidsRow[shCoeffs * 2 + j];
502
+ }
503
+ }
504
+ output['shN_centroids.webp'] = encodeWebP(centroidsBuf, 64 * shCoeffs, Math.ceil(centroids[0].length / 64), webPProfile);
505
+ // write labels
506
+ const labelsBuf = new Uint8Array(width * height * channels).fill(0xff);
507
+ for (let i = 0; i < indices.length; ++i) {
508
+ const label = labels[indices[i]];
509
+ labelsBuf[i * 4 + 0] = label & 0xff;
510
+ labelsBuf[i * 4 + 1] = (label >> 8) & 0xff;
511
+ }
512
+ output['shN_labels.webp'] = encodeWebP(labelsBuf, width, height, webPProfile);
513
+ meta.shN = {
514
+ count: paletteSize,
515
+ bands: shDegree,
516
+ codebook: Array.from(codebook.centroids),
517
+ files: ['shN_centroids.webp', 'shN_labels.webp'],
518
+ };
519
+ logger.timeEnd(`SOG encoding SH${shDegree}`);
520
+ }
521
+ output['meta.json'] = Buffer.from(JSON.stringify(meta), 'utf-8');
522
+ const result = zipSync(output);
523
+ await stream.getWriter().write(result);
524
+ }
525
+ }