@manycore/aholo-splat-transform 1.2.6

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 (88) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +33 -0
  3. package/bin/cli.js +118 -0
  4. package/dist/SplatData.d.ts +67 -0
  5. package/dist/SplatData.js +156 -0
  6. package/dist/constant.d.ts +3 -0
  7. package/dist/constant.js +13 -0
  8. package/dist/file/IFile.d.ts +5 -0
  9. package/dist/file/IFile.js +1 -0
  10. package/dist/file/index.d.ts +7 -0
  11. package/dist/file/index.js +6 -0
  12. package/dist/file/ksplat.d.ts +12 -0
  13. package/dist/file/ksplat.js +232 -0
  14. package/dist/file/lcc.d.ts +11 -0
  15. package/dist/file/lcc.js +157 -0
  16. package/dist/file/ply.d.ts +13 -0
  17. package/dist/file/ply.js +388 -0
  18. package/dist/file/sog.d.ts +80 -0
  19. package/dist/file/sog.js +504 -0
  20. package/dist/file/splat.d.ts +6 -0
  21. package/dist/file/splat.js +99 -0
  22. package/dist/file/spz.d.ts +8 -0
  23. package/dist/file/spz.js +400 -0
  24. package/dist/file/voxel.d.ts +37 -0
  25. package/dist/file/voxel.js +280 -0
  26. package/dist/index.d.ts +33 -0
  27. package/dist/index.js +54 -0
  28. package/dist/native/cpp/bin/linux/binding.node +0 -0
  29. package/dist/native/cpp/bin/windows/binding.node +0 -0
  30. package/dist/native/index.d.ts +54 -0
  31. package/dist/native/index.js +128 -0
  32. package/dist/tasks/AutoChunkLodTask.d.ts +13 -0
  33. package/dist/tasks/AutoChunkLodTask.js +117 -0
  34. package/dist/tasks/AutoLodTask.d.ts +10 -0
  35. package/dist/tasks/AutoLodTask.js +20 -0
  36. package/dist/tasks/BaseTask.d.ts +15 -0
  37. package/dist/tasks/BaseTask.js +5 -0
  38. package/dist/tasks/FlexLodTask.d.ts +12 -0
  39. package/dist/tasks/FlexLodTask.js +44 -0
  40. package/dist/tasks/ModifyTask.d.ts +9 -0
  41. package/dist/tasks/ModifyTask.js +156 -0
  42. package/dist/tasks/ReadTask.d.ts +8 -0
  43. package/dist/tasks/ReadTask.js +29 -0
  44. package/dist/tasks/SkeletonLodTask.d.ts +10 -0
  45. package/dist/tasks/SkeletonLodTask.js +156 -0
  46. package/dist/tasks/VoxelTask.d.ts +30 -0
  47. package/dist/tasks/VoxelTask.js +37 -0
  48. package/dist/tasks/WriteTask.d.ts +11 -0
  49. package/dist/tasks/WriteTask.js +70 -0
  50. package/dist/utils/BufferReader.d.ts +12 -0
  51. package/dist/utils/BufferReader.js +47 -0
  52. package/dist/utils/Logger.d.ts +11 -0
  53. package/dist/utils/Logger.js +38 -0
  54. package/dist/utils/StreamChunkDecoder.d.ts +16 -0
  55. package/dist/utils/StreamChunkDecoder.js +36 -0
  56. package/dist/utils/index.d.ts +27 -0
  57. package/dist/utils/index.js +101 -0
  58. package/dist/utils/k-means.d.ts +4 -0
  59. package/dist/utils/k-means.js +350 -0
  60. package/dist/utils/math.d.ts +46 -0
  61. package/dist/utils/math.js +351 -0
  62. package/dist/utils/quantize-1d.d.ts +4 -0
  63. package/dist/utils/quantize-1d.js +164 -0
  64. package/dist/utils/sh-rotate.d.ts +2 -0
  65. package/dist/utils/sh-rotate.js +175 -0
  66. package/dist/utils/splat.d.ts +20 -0
  67. package/dist/utils/splat.js +378 -0
  68. package/dist/utils/voxel/common.d.ts +162 -0
  69. package/dist/utils/voxel/common.js +1700 -0
  70. package/dist/utils/voxel/coplanar-merge.d.ts +63 -0
  71. package/dist/utils/voxel/coplanar-merge.js +819 -0
  72. package/dist/utils/voxel/gpu-dilation.d.ts +2 -0
  73. package/dist/utils/voxel/gpu-dilation.js +665 -0
  74. package/dist/utils/voxel/marching-cubes.d.ts +42 -0
  75. package/dist/utils/voxel/marching-cubes.js +1657 -0
  76. package/dist/utils/voxel/mesh.d.ts +3 -0
  77. package/dist/utils/voxel/mesh.js +130 -0
  78. package/dist/utils/voxel/nav.d.ts +29 -0
  79. package/dist/utils/voxel/nav.js +1043 -0
  80. package/dist/utils/voxel/postprocess.d.ts +23 -0
  81. package/dist/utils/voxel/postprocess.js +375 -0
  82. package/dist/utils/voxel/voxel-faces.d.ts +18 -0
  83. package/dist/utils/voxel/voxel-faces.js +663 -0
  84. package/dist/utils/voxel/voxelize.d.ts +33 -0
  85. package/dist/utils/voxel/voxelize.js +1193 -0
  86. package/dist/utils/webgpu.d.ts +8 -0
  87. package/dist/utils/webgpu.js +122 -0
  88. package/package.json +32 -0
@@ -0,0 +1,117 @@
1
+ import { combineSplatData, computeDenseBox } from '../utils/index.js';
2
+ import { BaseTask } from './BaseTask.js';
3
+ import { generateLod } from '../native/index.js';
4
+ const DefaultLevels = [
5
+ { precision: 1.0, scaleBoost: 1 },
6
+ { precision: 0.5, scaleBoost: 1 },
7
+ { precision: 0.25, scaleBoost: 1 },
8
+ { precision: 0.05, scaleBoost: 1.01 },
9
+ { precision: 0.01, scaleBoost: 1.02 },
10
+ ];
11
+ export class AutoChunkLodTask extends BaseTask {
12
+ async exec(config, { logger, resources }) {
13
+ const { input, output, type, maxChunkCounts = 400000, levels = DefaultLevels } = config;
14
+ const splat = resources.get(input);
15
+ logger.info(`loaded -> "${input}"`);
16
+ const forwardBox = computeDenseBox(splat, 0.8);
17
+ const outputs = [];
18
+ const outputBlocks = [];
19
+ const permanentFiles = [];
20
+ {
21
+ logger.info('generate lod');
22
+ logger.time('generate elapsed');
23
+ const { blocks, splats } = generateLod(splat, levels, Math.min(1, maxChunkCounts / splat.counts), 2000, 20);
24
+ logger.timeEnd('generate elapsed');
25
+ const chunkL3Idx = [];
26
+ const chunkL4Idx = [];
27
+ for (let i = 0; i < blocks.length; i++) {
28
+ const block = blocks[i];
29
+ chunkL4Idx.push(block.refs[4]);
30
+ if (block.refs[3] !== block.refs[4]) {
31
+ chunkL3Idx.push(block.refs[3]);
32
+ }
33
+ }
34
+ const layout = new Map();
35
+ {
36
+ const chunkL4 = combineSplatData(chunkL4Idx.map(idx => splats[idx]));
37
+ outputs.push({ name: `chunk_0.${type}`, content: chunkL4, preserveOrder: true });
38
+ permanentFiles.push(0);
39
+ let offset = 0;
40
+ for (let i = 0; i < chunkL4Idx.length; i++) {
41
+ const idx = chunkL4Idx[i];
42
+ const counts = splats[idx].counts;
43
+ layout.set(idx, { idx: 0, offset, counts });
44
+ offset += counts;
45
+ }
46
+ }
47
+ if (chunkL3Idx.length > 0) {
48
+ const chunkL3 = combineSplatData(chunkL3Idx.map(idx => splats[idx]));
49
+ outputs.push({ name: `chunk_1.${type}`, content: chunkL3, preserveOrder: true });
50
+ permanentFiles.push(1);
51
+ let offset = 0;
52
+ for (let i = 0; i < chunkL3Idx.length; i++) {
53
+ const idx = chunkL3Idx[i];
54
+ const counts = splats[idx].counts;
55
+ layout.set(idx, { idx: 1, offset, counts });
56
+ offset += counts;
57
+ }
58
+ }
59
+ for (let i = 0; i < splats.length; i++) {
60
+ if (chunkL3Idx.includes(i) || chunkL4Idx.includes(i)) {
61
+ continue;
62
+ }
63
+ const idx = outputs.length;
64
+ const splat = splats[i];
65
+ outputs.push({
66
+ name: `chunk_${idx}.${type}`,
67
+ content: splat,
68
+ });
69
+ layout.set(i, { idx, offset: 0, counts: splat.counts });
70
+ }
71
+ for (const block of blocks) {
72
+ outputBlocks.push({
73
+ bound: block.box,
74
+ lods: block.refs.map(ref => {
75
+ const v = layout.get(ref);
76
+ return {
77
+ file: v.idx,
78
+ offset: v.offset,
79
+ count: v.counts,
80
+ };
81
+ })
82
+ });
83
+ }
84
+ }
85
+ logger.info(`Total blocks: ${outputBlocks.length}, files: ${outputs.length}`);
86
+ logger.info(`Gaussian per level: `);
87
+ let maxLength = 0;
88
+ for (let i = 0; i < levels.length; i++) {
89
+ const level = levels[i];
90
+ const levelCount = outputBlocks.map(block => block.lods[i].count).reduce((acc, i) => acc + i, 0);
91
+ const levelStr = levelCount.toString().padStart(maxLength, ' ');
92
+ maxLength = levelStr.length;
93
+ logger.info(`\tLevel ${i}${`(${(level.precision * 100).toFixed(2)}%)`.padStart(9, ' ')}: ${levelStr}${`(${(levelCount / splat.counts * 100).toFixed(2)}%)`.padStart(9, ' ')}`);
94
+ }
95
+ resources.set(output, [
96
+ {
97
+ name: 'lod-meta.json',
98
+ content: JSON.stringify({
99
+ magicCode: 0x262834,
100
+ type: 'lod-splat',
101
+ version: '1.0',
102
+ counts: splat.counts,
103
+ shDegree: splat.shDegree,
104
+ levels: levels.length,
105
+ forwardBox,
106
+ files: outputs.map(f => f.name),
107
+ permanentFiles,
108
+ tree: outputBlocks,
109
+ }),
110
+ },
111
+ ...outputs,
112
+ ]);
113
+ }
114
+ requiresGPU(config) {
115
+ return config.type === 'sog';
116
+ }
117
+ }
@@ -0,0 +1,10 @@
1
+ import { Context, BaseTask } from './BaseTask.js';
2
+ export interface Config {
3
+ input: string;
4
+ output: string;
5
+ counts?: number;
6
+ ratio?: number;
7
+ }
8
+ export declare class AutoLodTask extends BaseTask<Config> {
9
+ exec(config: Config, { logger, resources }: Context): Promise<void>;
10
+ }
@@ -0,0 +1,20 @@
1
+ import { combineSplatData } from '../utils/index.js';
2
+ import { generateLod } from '../native/index.js';
3
+ import { BaseTask } from './BaseTask.js';
4
+ export class AutoLodTask extends BaseTask {
5
+ async exec(config, { logger, resources }) {
6
+ const { input, output, counts = Infinity, ratio = 0.3 } = config;
7
+ const splat = resources.get(input);
8
+ logger.info(`loaded -> "${input}"`);
9
+ const target = Math.min(Math.ceil(splat.counts * ratio), counts);
10
+ logger.info(`expected -> ${target}(${((target / splat.counts) * 100).toFixed(2)}%) | ratio=${ratio} counts=${counts}`);
11
+ const { blocks, splats } = generateLod(splat, [
12
+ { precision: 1.0, scaleBoost: 1.0 },
13
+ { precision: target / splat.counts, scaleBoost: 1.0 },
14
+ ], 0.2, 2000, 20);
15
+ const raw = combineSplatData(blocks.map(item => splats[item.refs[1]]));
16
+ logger.info(`result -> ${raw.counts}(${(raw.counts / target * 100).toFixed(2)}%)`);
17
+ resources.set(output, raw);
18
+ logger.info(`stored -> key="${output}"`);
19
+ }
20
+ }
@@ -0,0 +1,15 @@
1
+ import { SplatData } from '../SplatData.js';
2
+ import { Logger } from '../utils/Logger.js';
3
+ export interface SingleFile {
4
+ name: string;
5
+ content: SplatData | string;
6
+ preserveOrder?: boolean;
7
+ }
8
+ export interface Context {
9
+ logger: Logger;
10
+ resources: Map<string, SplatData | SingleFile[]>;
11
+ }
12
+ export declare abstract class BaseTask<T> {
13
+ abstract exec(config: T, ctx: Context): Promise<void>;
14
+ requiresGPU(_config: T): boolean;
15
+ }
@@ -0,0 +1,5 @@
1
+ export class BaseTask {
2
+ requiresGPU(_config) {
3
+ return false;
4
+ }
5
+ }
@@ -0,0 +1,12 @@
1
+ import { Context, BaseTask } from './BaseTask.js';
2
+ export interface Config {
3
+ input: string;
4
+ output: string;
5
+ scorePath: string;
6
+ counts?: number;
7
+ ratio?: number;
8
+ originalIndices?: string;
9
+ }
10
+ export declare class FlexLodTask extends BaseTask<Config> {
11
+ exec(config: Config, { logger, resources }: Context): Promise<void>;
12
+ }
@@ -0,0 +1,44 @@
1
+ import fs from 'node:fs';
2
+ import { SplatData } from '../SplatData.js';
3
+ import { BaseTask } from './BaseTask.js';
4
+ export class FlexLodTask extends BaseTask {
5
+ async exec(config, { logger, resources }) {
6
+ const { input, output, scorePath, counts = Infinity, ratio = 0.3, originalIndices } = config;
7
+ const splat = resources.get(input);
8
+ logger.info(`loaded -> "${input}"`);
9
+ const target = Math.min(counts, Math.ceil(splat.counts * ratio));
10
+ logger.info(`expected -> ${target}(${((target / splat.counts) * 100).toFixed(2)}%) | ratio=${ratio} counts=${counts}`);
11
+ const scores = new Float32Array(fs.readFileSync(scorePath).buffer);
12
+ let sorted = new Uint32Array(splat.counts);
13
+ for (let i = 0; i < sorted.length; i++) {
14
+ sorted[i] = i;
15
+ }
16
+ sorted.sort((a, b) => scores[b] - scores[a]);
17
+ sorted = sorted.subarray(0, target).sort((a, b) => a - b);
18
+ const raw = new SplatData().init(target, splat.shDegree);
19
+ const single = {
20
+ x: 0, y: 0, z: 0,
21
+ sx: 0, sy: 0, sz: 0,
22
+ qx: 0, qy: 0, qz: 0, qw: 0,
23
+ r: 0, g: 0, b: 0, a: 0,
24
+ shN: new Array(splat.shCounts),
25
+ };
26
+ const shN = single.shN;
27
+ for (let i = 0; i < target; i++) {
28
+ splat.get(sorted[i], single);
29
+ splat.getShN(sorted[i], shN);
30
+ raw.set(i, single);
31
+ raw.setShN(i, shN);
32
+ }
33
+ if (originalIndices) {
34
+ const originIndices = new Uint32Array(target);
35
+ for (let i = 0; i < target; i++) {
36
+ originIndices[i] = sorted[i];
37
+ }
38
+ fs.writeFileSync(originalIndices, originIndices);
39
+ logger.info(`original indices saved -> "${originalIndices}"`);
40
+ }
41
+ resources.set(output, raw);
42
+ logger.info(`stored -> key="${output}"`);
43
+ }
44
+ }
@@ -0,0 +1,9 @@
1
+ import { Context, BaseTask } from './BaseTask.js';
2
+ export interface Config {
3
+ input: string;
4
+ output: string;
5
+ modifyPaths?: string[];
6
+ }
7
+ export declare class ModifyTask extends BaseTask<Config> {
8
+ exec(config: Config, { logger, resources }: Context): Promise<void>;
9
+ }
@@ -0,0 +1,156 @@
1
+ import fs from 'node:fs';
2
+ import { createSHRotateFn, fastDeleteSplat, Matrix3, Matrix4, Quaternion, Vector3 } from '../utils/index.js';
3
+ import { BaseTask } from './BaseTask.js';
4
+ async function createSplatModify(path, counts) {
5
+ if (!path) {
6
+ return undefined;
7
+ }
8
+ const { isRowMatrix = true, transform, deletedIndices: deletedIndicesBitMap = [], indicesTransform = [], } = JSON.parse(fs.readFileSync(path, 'utf-8'));
9
+ const used = new Uint8Array(counts);
10
+ let usedCounts = 0;
11
+ const deletedIndices = [];
12
+ for (let i = 0; i < deletedIndicesBitMap.length; i++) {
13
+ const v = deletedIndicesBitMap[i];
14
+ for (let j = 0; j < 8; j++) {
15
+ if (v & (1 << j)) {
16
+ const idx = i * 8 + j;
17
+ deletedIndices.push(idx);
18
+ used[idx] = 1;
19
+ usedCounts++;
20
+ }
21
+ }
22
+ }
23
+ const groupIndices = [];
24
+ const groupTransforms = [];
25
+ const modelMatrix = new Matrix4(transform, isRowMatrix);
26
+ const transforms = indicesTransform.map(v => new Matrix4(v.transform, isRowMatrix).multiply(modelMatrix));
27
+ for (let i = 0; i < transforms.length; i++) {
28
+ const { indices } = indicesTransform[i];
29
+ for (let j = 0; j < indices.length; j++) {
30
+ used[indices[j]] = 1;
31
+ }
32
+ usedCounts += indices.length;
33
+ const matrix = transforms[i];
34
+ if (matrix.equals(Matrix4.ONE)) {
35
+ continue;
36
+ }
37
+ const scale = new Vector3(1, 1, 1);
38
+ const quat = new Quaternion(0, 0, 0, 1);
39
+ matrix.decompose(new Vector3(1, 1, 1), quat, scale);
40
+ groupIndices.push(indices);
41
+ groupTransforms.push({
42
+ isScale: !scale.equals(Vector3.ONE),
43
+ isRotate: !quat.equals(Quaternion.ONE),
44
+ matrix,
45
+ scale,
46
+ quat,
47
+ shRotateFn: createSHRotateFn(new Matrix3().setFromMatrix4(new Matrix4().compose(new Vector3(0, 0, 0), quat, new Vector3(1, 1, 1)))),
48
+ });
49
+ }
50
+ if (!modelMatrix.equals(Matrix4.ONE)) {
51
+ const indices = new Array(counts - usedCounts);
52
+ for (let i = 0; i < used.length; i++) {
53
+ if (used[i]) {
54
+ continue;
55
+ }
56
+ indices.push(i);
57
+ }
58
+ const matrix = modelMatrix;
59
+ const scale = new Vector3(1, 1, 1);
60
+ const quat = new Quaternion(0, 0, 0, 1);
61
+ matrix.decompose(new Vector3(1, 1, 1), quat, scale);
62
+ groupIndices.unshift(indices);
63
+ groupTransforms.unshift({
64
+ isScale: !scale.equals(Vector3.ONE),
65
+ isRotate: !quat.equals(Quaternion.ONE),
66
+ matrix,
67
+ scale,
68
+ quat,
69
+ shRotateFn: createSHRotateFn(new Matrix3().setFromMatrix4(new Matrix4().compose(new Vector3(0, 0, 0), quat, new Vector3(1, 1, 1)))),
70
+ });
71
+ }
72
+ return {
73
+ deletedIndices,
74
+ groupIndices,
75
+ groupTransforms,
76
+ };
77
+ }
78
+ export class ModifyTask extends BaseTask {
79
+ async exec(config, { logger, resources }) {
80
+ const { input, modifyPaths = [], output } = config;
81
+ const splat = resources.get(input);
82
+ logger.info(`loaded -> "${input}"`);
83
+ const modifies = await Promise.all(modifyPaths.map((p, i) => createSplatModify(p, splat.blockContentCounts[i])));
84
+ const tempVec = new Vector3(0, 0, 0);
85
+ const tempQuat = new Quaternion(0, 0, 0, 1);
86
+ const single = {
87
+ x: 0, y: 0, z: 0,
88
+ sx: 0, sy: 0, sz: 0,
89
+ qx: 0, qy: 0, qz: 0, qw: 0,
90
+ r: 0, g: 0, b: 0, a: 0,
91
+ shN: new Array(splat.shCounts),
92
+ };
93
+ const shN = single.shN;
94
+ const shCoeffs = new Array(splat.shCounts / 3).fill(0);
95
+ const deletedTotalIndices = [];
96
+ for (let i = 0; i < modifies.length; i++) {
97
+ const modify = modifies[i];
98
+ if (!modify) {
99
+ logger.info(`modify[${i}] is null, skip`);
100
+ continue;
101
+ }
102
+ const offset = splat.blockOffsets[i];
103
+ const { deletedIndices, groupIndices, groupTransforms } = modify;
104
+ logger.info(`modify[${i}] offset=${offset} groups=${groupIndices.length} delete=${deletedIndices.length}`);
105
+ for (let j = 0; j < groupIndices.length; j++) {
106
+ const indices = groupIndices[j];
107
+ const { isScale, isRotate, matrix, scale, quat, shRotateFn } = groupTransforms[j];
108
+ logger.info(`group[${i}:${j}] size=${indices.length} scale=${isScale} rotate=${isRotate}`);
109
+ for (let k = 0; k < indices.length; k++) {
110
+ const idx = offset + indices[k];
111
+ splat.get(idx, single);
112
+ tempVec.set(single.x, single.y, single.z).applyMatrix4(matrix);
113
+ single.x = tempVec.x;
114
+ single.y = tempVec.y;
115
+ single.z = tempVec.z;
116
+ if (isScale) {
117
+ tempVec.set(single.sx, single.sy, single.sz).mul(scale);
118
+ single.sx = tempVec.x;
119
+ single.sy = tempVec.y;
120
+ single.sz = tempVec.z;
121
+ }
122
+ if (isRotate) {
123
+ tempQuat.set(single.qx, single.qy, single.qz, single.qw).premultiply(quat);
124
+ single.qx = tempQuat.x;
125
+ single.qy = tempQuat.y;
126
+ single.qz = tempQuat.z;
127
+ single.qw = tempQuat.w;
128
+ }
129
+ splat.set(idx, single);
130
+ if (isRotate) {
131
+ splat.getShN(idx, shN);
132
+ for (let m = 0; m < 3; m++) {
133
+ for (let n = 0; n < shCoeffs.length; n++) {
134
+ shCoeffs[n] = shN[n * 3 + m];
135
+ }
136
+ shRotateFn(shCoeffs);
137
+ for (let n = 0; n < shCoeffs.length; n++) {
138
+ shN[n * 3 + m] = shCoeffs[n];
139
+ }
140
+ }
141
+ splat.setShN(idx, shN);
142
+ }
143
+ }
144
+ }
145
+ for (let j = 0; j < deletedIndices.length; j++) {
146
+ deletedTotalIndices.push(offset + deletedIndices[j]);
147
+ }
148
+ }
149
+ if (deletedTotalIndices.length > 0) {
150
+ fastDeleteSplat(splat, deletedTotalIndices);
151
+ logger.info(`delete ${deletedTotalIndices.length} splat`);
152
+ }
153
+ resources.set(output, splat);
154
+ logger.info(`stored -> key="${output}"`);
155
+ }
156
+ }
@@ -0,0 +1,8 @@
1
+ import { Context, BaseTask } from './BaseTask.js';
2
+ export interface Config {
3
+ inputs: string[];
4
+ output: string;
5
+ }
6
+ export declare class ReadTask extends BaseTask<Config> {
7
+ exec(config: Config, { logger, resources }: Context): Promise<void>;
8
+ }
@@ -0,0 +1,29 @@
1
+ import fs from 'node:fs';
2
+ import { Readable } from 'node:stream';
3
+ import { SplatData } from '../SplatData.js';
4
+ import { createSplatFile } from '../utils/index.js';
5
+ import { BaseTask } from './BaseTask.js';
6
+ export class ReadTask extends BaseTask {
7
+ async exec(config, { logger, resources }) {
8
+ const { inputs, output } = config;
9
+ const splat = new SplatData(inputs.length);
10
+ const promises = [];
11
+ let totalBytes = 0;
12
+ for (let i = 0; i < inputs.length; i++) {
13
+ const path = inputs[i];
14
+ const { size } = fs.statSync(path);
15
+ totalBytes += size;
16
+ const stream = Readable.toWeb(fs.createReadStream(path));
17
+ const promise = createSplatFile(path).read(stream, size, splat);
18
+ promises.push(promise);
19
+ }
20
+ await Promise.all(promises);
21
+ logger.info(`load: ${inputs.length} files | sizes=${(totalBytes / 1024 / 1024).toFixed(2)}MB`);
22
+ for (let i = 0; i < inputs.length; i++) {
23
+ logger.info(` - ${inputs[i]}`);
24
+ }
25
+ logger.info(`counts: ${splat.counts}, SH: ${splat.shDegree}`);
26
+ resources.set(output, splat);
27
+ logger.info(`stored -> "${output}"`);
28
+ }
29
+ }
@@ -0,0 +1,10 @@
1
+ import { Context, BaseTask } from './BaseTask.js';
2
+ export interface Config {
3
+ input: string;
4
+ output: string;
5
+ counts?: number;
6
+ ratio?: number;
7
+ }
8
+ export declare class SkeletonLodTask extends BaseTask<Config> {
9
+ exec(config: Config, { logger, resources }: Context): Promise<void>;
10
+ }
@@ -0,0 +1,156 @@
1
+ import { SplatData } from '../SplatData.js';
2
+ import { BaseTask } from './BaseTask.js';
3
+ const VOXEL_CHUNK_SIZE = 0.02;
4
+ const VOXEL_CHUNK_SCALE = 1.3;
5
+ export class SkeletonLodTask extends BaseTask {
6
+ async exec(config, { logger, resources }) {
7
+ const { input, output, counts = 85000, ratio = 0.1 } = config;
8
+ const splat = resources.get(input);
9
+ logger.info(`loaded -> "${input}"`);
10
+ const target = Math.min(Math.ceil(splat.counts * ratio), counts);
11
+ logger.info(`expected -> ${target}(${((target / splat.counts) * 100).toFixed(2)}%) | ratio=${ratio} counts=${counts}`);
12
+ const xCol = splat.table[0 /* ColIdx.x */];
13
+ const yCol = splat.table[1 /* ColIdx.y */];
14
+ const zCol = splat.table[2 /* ColIdx.z */];
15
+ let minX = Infinity;
16
+ let minY = Infinity;
17
+ let minZ = Infinity;
18
+ for (let i = 0; i < splat.counts; i++) {
19
+ const x = xCol[i];
20
+ const y = yCol[i];
21
+ const z = zCol[i];
22
+ if (x < minX) {
23
+ minX = x;
24
+ }
25
+ if (y < minY) {
26
+ minY = y;
27
+ }
28
+ if (z < minZ) {
29
+ minZ = z;
30
+ }
31
+ }
32
+ const chunkMap = new Map();
33
+ for (let i = 0; i < splat.counts; i++) {
34
+ const x = ((xCol[i] - minX) / VOXEL_CHUNK_SIZE) | 0;
35
+ const y = ((yCol[i] - minY) / VOXEL_CHUNK_SIZE) | 0;
36
+ const z = ((zCol[i] - minZ) / VOXEL_CHUNK_SIZE) | 0;
37
+ const key = `${x},${y},${z}`;
38
+ let arr = chunkMap.get(key);
39
+ if (!arr) {
40
+ arr = [];
41
+ chunkMap.set(key, arr);
42
+ }
43
+ arr.push(i);
44
+ }
45
+ const chunks = Array.from(chunkMap.values());
46
+ const CHUNK_RATIO = chunks.reduce((p, c) => p + c.length ** VOXEL_CHUNK_SCALE, 0) / (target * 0.1);
47
+ const mergeChucks = [];
48
+ for (let i = 0; i < chunks.length; i++) {
49
+ const chunk = chunks[i];
50
+ const size = Math.max(1, Math.ceil((chunk.length ** VOXEL_CHUNK_SCALE) / CHUNK_RATIO));
51
+ if (size === 1) {
52
+ mergeChucks.push(chunk);
53
+ continue;
54
+ }
55
+ let minX = Infinity;
56
+ let minY = Infinity;
57
+ let minZ = Infinity;
58
+ let maxX = -Infinity;
59
+ let maxY = -Infinity;
60
+ let maxZ = -Infinity;
61
+ for (let j = 0; j < chunk.length; j++) {
62
+ const offset = chunk[j];
63
+ const x = xCol[offset];
64
+ const y = yCol[offset];
65
+ const z = zCol[offset];
66
+ if (x < minX) {
67
+ minX = x;
68
+ }
69
+ if (y < minY) {
70
+ minY = y;
71
+ }
72
+ if (z < minZ) {
73
+ minZ = z;
74
+ }
75
+ if (x > maxX) {
76
+ maxX = x;
77
+ }
78
+ if (y > maxY) {
79
+ maxY = y;
80
+ }
81
+ if (z > maxZ) {
82
+ maxZ = z;
83
+ }
84
+ }
85
+ const subChuckSize = Math.ceil(Math.cbrt(size));
86
+ const scaleX = subChuckSize / Math.max(maxX - minX, 1e-9);
87
+ const scaleY = subChuckSize / Math.max(maxY - minY, 1e-9);
88
+ const scaleZ = subChuckSize / Math.max(maxZ - minZ, 1e-9);
89
+ const subChunkMap = new Map();
90
+ for (let j = 0; j < chunk.length; j++) {
91
+ const idx = chunk[j];
92
+ const x = ((xCol[idx] - minX) * scaleX) | 0;
93
+ const y = ((yCol[idx] - minY) * scaleY) | 0;
94
+ const z = ((zCol[idx] - minZ) * scaleZ) | 0;
95
+ const key = `${x},${y},${z}`;
96
+ let arr = subChunkMap.get(key);
97
+ if (!arr) {
98
+ arr = [];
99
+ subChunkMap.set(key, arr);
100
+ }
101
+ arr.push(idx);
102
+ }
103
+ const subChunks = Array.from(subChunkMap.values());
104
+ if (subChunks.length > size) {
105
+ subChunks.sort((a, b) => b.length - a.length);
106
+ subChunks.length = size;
107
+ }
108
+ for (let j = 0; j < subChunks.length; j++) {
109
+ mergeChucks.push(subChunks[j]);
110
+ }
111
+ }
112
+ if (mergeChucks.length > target) {
113
+ mergeChucks.sort((a, b) => b.length - a.length);
114
+ mergeChucks.length = target;
115
+ }
116
+ const raw = new SplatData().init(mergeChucks.length, 0);
117
+ const result = {
118
+ x: 0, y: 0, z: 0,
119
+ sx: 0.005, sy: 0.005, sz: 0.005,
120
+ qx: 0, qy: 0, qz: 0, qw: 1,
121
+ r: 0, g: 0, b: 0, a: 0,
122
+ shN: [],
123
+ };
124
+ const single = {
125
+ x: 0, y: 0, z: 0,
126
+ sx: 0, sy: 0, sz: 0,
127
+ qx: 0, qy: 0, qz: 0, qw: 0,
128
+ r: 0, g: 0, b: 0, a: 0,
129
+ shN: [],
130
+ };
131
+ for (let i = 0; i < mergeChucks.length; i++) {
132
+ const chunk = mergeChucks[i];
133
+ for (let j = 0; j < chunk.length; j++) {
134
+ splat.get(chunk[j], single);
135
+ result.x += single.x;
136
+ result.y += single.y;
137
+ result.z += single.z;
138
+ result.r += single.r;
139
+ result.g += single.g;
140
+ result.b += single.b;
141
+ result.a += single.a;
142
+ }
143
+ result.x /= chunk.length;
144
+ result.y /= chunk.length;
145
+ result.z /= chunk.length;
146
+ result.r /= chunk.length;
147
+ result.g /= chunk.length;
148
+ result.b /= chunk.length;
149
+ result.a /= chunk.length;
150
+ raw.set(i, result);
151
+ result.x = result.y = result.z = result.r = result.g = result.b = result.a = 0;
152
+ }
153
+ resources.set(output, raw);
154
+ logger.info(`stored -> key="${output}"`);
155
+ }
156
+ }
@@ -0,0 +1,30 @@
1
+ import { Context, BaseTask } from './BaseTask.js';
2
+ export interface VoxelTaskConfig {
3
+ input: string;
4
+ output: string;
5
+ voxelResolution?: number;
6
+ opacityCutoff?: number;
7
+ backend?: 'cpu' | 'gpu';
8
+ collisionMesh?: boolean | 'smooth' | 'faces';
9
+ navExteriorRadius?: number;
10
+ floorFill?: boolean;
11
+ floorFillDilation?: number;
12
+ cpuWorkerCount?: number;
13
+ box?: {
14
+ minCorner: [number, number, number];
15
+ maxCorner: [number, number, number];
16
+ };
17
+ navCapsule?: {
18
+ height: number;
19
+ radius: number;
20
+ };
21
+ navSeed?: {
22
+ x: number;
23
+ y: number;
24
+ z: number;
25
+ };
26
+ }
27
+ export declare class VoxelTask extends BaseTask<VoxelTaskConfig> {
28
+ exec(config: VoxelTaskConfig, { logger, resources }: Context): Promise<void>;
29
+ requiresGPU(_config: VoxelTaskConfig): boolean;
30
+ }
@@ -0,0 +1,37 @@
1
+ import { SplatData } from '../SplatData.js';
2
+ import { writeVoxelFiles } from '../file/voxel.js';
3
+ import { BaseTask } from './BaseTask.js';
4
+ export class VoxelTask extends BaseTask {
5
+ async exec(config, { logger, resources }) {
6
+ const { input, output, voxelResolution = 0.05, opacityCutoff = 0.1, backend = 'gpu', collisionMesh = false, navExteriorRadius, floorFill = false, floorFillDilation = 0, cpuWorkerCount = -1, box = { minCorner: [-100, -100, -100], maxCorner: [100, 100, 100] }, navCapsule, navSeed } = config;
7
+ const source = resources.get(input);
8
+ if (!(source instanceof SplatData)) {
9
+ throw new Error(`VoxelTask: resource "${input}" must be SplatData`);
10
+ }
11
+ const options = {
12
+ voxelResolution,
13
+ opacityCutoff,
14
+ backend,
15
+ collisionMesh,
16
+ floorFill,
17
+ floorFillDilation,
18
+ cpuWorkerCount,
19
+ box
20
+ };
21
+ if (navExteriorRadius !== undefined) {
22
+ options.navExteriorRadius = navExteriorRadius;
23
+ }
24
+ if (navCapsule !== undefined) {
25
+ options.navCapsule = navCapsule;
26
+ }
27
+ if (navSeed !== undefined) {
28
+ options.navSeed = navSeed;
29
+ }
30
+ logger.info(`writing voxel -> dir="${output}" count=${source.counts} SH=${source.shDegree}`);
31
+ await writeVoxelFiles(output, source, options);
32
+ logger.info('voxelizing done');
33
+ }
34
+ requiresGPU(_config) {
35
+ return (_config.backend ?? 'gpu') === 'gpu';
36
+ }
37
+ }