@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.
- package/CHANGELOG.md +102 -0
- package/README.md +33 -0
- package/bin/cli.js +118 -0
- package/dist/SplatData.d.ts +67 -0
- package/dist/SplatData.js +156 -0
- package/dist/constant.d.ts +3 -0
- package/dist/constant.js +13 -0
- package/dist/file/IFile.d.ts +5 -0
- package/dist/file/IFile.js +1 -0
- package/dist/file/index.d.ts +7 -0
- package/dist/file/index.js +6 -0
- package/dist/file/ksplat.d.ts +12 -0
- package/dist/file/ksplat.js +232 -0
- package/dist/file/lcc.d.ts +11 -0
- package/dist/file/lcc.js +157 -0
- package/dist/file/ply.d.ts +13 -0
- package/dist/file/ply.js +388 -0
- package/dist/file/sog.d.ts +80 -0
- package/dist/file/sog.js +504 -0
- package/dist/file/splat.d.ts +6 -0
- package/dist/file/splat.js +99 -0
- package/dist/file/spz.d.ts +8 -0
- package/dist/file/spz.js +400 -0
- package/dist/file/voxel.d.ts +37 -0
- package/dist/file/voxel.js +280 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +54 -0
- package/dist/native/cpp/bin/linux/binding.node +0 -0
- package/dist/native/cpp/bin/windows/binding.node +0 -0
- package/dist/native/index.d.ts +54 -0
- package/dist/native/index.js +128 -0
- package/dist/tasks/AutoChunkLodTask.d.ts +13 -0
- package/dist/tasks/AutoChunkLodTask.js +117 -0
- package/dist/tasks/AutoLodTask.d.ts +10 -0
- package/dist/tasks/AutoLodTask.js +20 -0
- package/dist/tasks/BaseTask.d.ts +15 -0
- package/dist/tasks/BaseTask.js +5 -0
- package/dist/tasks/FlexLodTask.d.ts +12 -0
- package/dist/tasks/FlexLodTask.js +44 -0
- package/dist/tasks/ModifyTask.d.ts +9 -0
- package/dist/tasks/ModifyTask.js +156 -0
- package/dist/tasks/ReadTask.d.ts +8 -0
- package/dist/tasks/ReadTask.js +29 -0
- package/dist/tasks/SkeletonLodTask.d.ts +10 -0
- package/dist/tasks/SkeletonLodTask.js +156 -0
- package/dist/tasks/VoxelTask.d.ts +30 -0
- package/dist/tasks/VoxelTask.js +37 -0
- package/dist/tasks/WriteTask.d.ts +11 -0
- package/dist/tasks/WriteTask.js +70 -0
- package/dist/utils/BufferReader.d.ts +12 -0
- package/dist/utils/BufferReader.js +47 -0
- package/dist/utils/Logger.d.ts +11 -0
- package/dist/utils/Logger.js +38 -0
- package/dist/utils/StreamChunkDecoder.d.ts +16 -0
- package/dist/utils/StreamChunkDecoder.js +36 -0
- package/dist/utils/index.d.ts +27 -0
- package/dist/utils/index.js +101 -0
- package/dist/utils/k-means.d.ts +4 -0
- package/dist/utils/k-means.js +350 -0
- package/dist/utils/math.d.ts +46 -0
- package/dist/utils/math.js +351 -0
- package/dist/utils/quantize-1d.d.ts +4 -0
- package/dist/utils/quantize-1d.js +164 -0
- package/dist/utils/sh-rotate.d.ts +2 -0
- package/dist/utils/sh-rotate.js +175 -0
- package/dist/utils/splat.d.ts +20 -0
- package/dist/utils/splat.js +378 -0
- package/dist/utils/voxel/common.d.ts +162 -0
- package/dist/utils/voxel/common.js +1700 -0
- package/dist/utils/voxel/coplanar-merge.d.ts +63 -0
- package/dist/utils/voxel/coplanar-merge.js +819 -0
- package/dist/utils/voxel/gpu-dilation.d.ts +2 -0
- package/dist/utils/voxel/gpu-dilation.js +665 -0
- package/dist/utils/voxel/marching-cubes.d.ts +42 -0
- package/dist/utils/voxel/marching-cubes.js +1657 -0
- package/dist/utils/voxel/mesh.d.ts +3 -0
- package/dist/utils/voxel/mesh.js +130 -0
- package/dist/utils/voxel/nav.d.ts +29 -0
- package/dist/utils/voxel/nav.js +1043 -0
- package/dist/utils/voxel/postprocess.d.ts +23 -0
- package/dist/utils/voxel/postprocess.js +375 -0
- package/dist/utils/voxel/voxel-faces.d.ts +18 -0
- package/dist/utils/voxel/voxel-faces.js +663 -0
- package/dist/utils/voxel/voxelize.d.ts +33 -0
- package/dist/utils/voxel/voxelize.js +1193 -0
- package/dist/utils/webgpu.d.ts +8 -0
- package/dist/utils/webgpu.js +122 -0
- package/package.json +32 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Buffer } from 'node:buffer';
|
|
4
|
+
import { logger, cpuVoxelize, gpuVoxelize } from '../utils/index.js';
|
|
5
|
+
import { fillExterior, fillFloor, carve } from '../utils/voxel/nav.js';
|
|
6
|
+
import { buildCollisionMesh } from '../utils/voxel/mesh.js';
|
|
7
|
+
import { cropToNavigable, cropToOccupied, filterAndFillBlocks } from '../utils/voxel/postprocess.js';
|
|
8
|
+
import { alignGridBounds, ALPHA_THRESHOLD, BlockMaskBuffer, buildSparseOctree, decodeMorton3, encodeMorton3, extentsFromQuatScale, getChildOffset, SparseVoxelGrid } from '../utils/voxel/common.js';
|
|
9
|
+
/**
|
|
10
|
+
* Build a sparse voxel octree from gaussian splat data.
|
|
11
|
+
*
|
|
12
|
+
* Pipeline (based on https://github.com/playcanvas/splat-transform/blob/8f3b843efdc378f97d4f6a66a3a90a2de6d479a4/src/lib/writers/write-voxel.ts):
|
|
13
|
+
* 1) Compute gaussian extents and scene bounds (`extentsFromQuatScale`).
|
|
14
|
+
* 2) Align grid bounds.
|
|
15
|
+
* 3) Voxelize to `BlockMaskBuffer` via `gpuVoxelize`.
|
|
16
|
+
* 4) Post-process occupancy (`filterAndFillBlocks`, optional `fillExterior`, `fillFloor`, `carve`).
|
|
17
|
+
* 5) Crop bounds (`cropToNavigable` or `cropToOccupied`), then build octree (`buildSparseOctree`).
|
|
18
|
+
* 6) Optionally generate collision mesh (`buildCollisionMesh` → `collision.glb` next to `voxel.bin`).
|
|
19
|
+
*
|
|
20
|
+
* @param data - Gaussian splat source data.
|
|
21
|
+
* @param voxelResolution - Voxel size in world units.
|
|
22
|
+
* @param opacityCutoff - Opacity threshold used during voxelization.
|
|
23
|
+
* @param collisionMesh - Whether to generate collision GLB.
|
|
24
|
+
* @param navExteriorRadius - Exterior fill radius; requires `navSeed`.
|
|
25
|
+
* @param floorFill - Whether to run floor fill before carve.
|
|
26
|
+
* @param floorFillDilation - Horizontal dilation radius for floor fill.
|
|
27
|
+
* @param box - Axis-aligned world-space clamp box for voxelization.
|
|
28
|
+
* @param navCapsule - Capsule config used by `carve`.
|
|
29
|
+
* @param navSeed - Seed position used by exterior/carve flood fills.
|
|
30
|
+
*/
|
|
31
|
+
const writeVoxels = async (data, 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) => {
|
|
32
|
+
const hasNav = !!(navCapsule && navSeed && navCapsule.height > 0);
|
|
33
|
+
const hasFillExterior = !!(navExteriorRadius && navSeed);
|
|
34
|
+
const hasFloorFill = floorFill;
|
|
35
|
+
logger.info(`voxel params: resolution=${voxelResolution}, opacityCutoff=${opacityCutoff}, backend=${backend}, ` +
|
|
36
|
+
`collisionMesh=${collisionMesh}, navExteriorRadius=${navExteriorRadius ?? 'none'}, ` +
|
|
37
|
+
`floorFill=${floorFill}, floorFillDilation=${floorFillDilation}, cpuWorkerCount=${cpuWorkerCount === -1 ? 'auto' : cpuWorkerCount}, ` +
|
|
38
|
+
`box=${JSON.stringify(box)}, navCapsule=${navCapsule ? JSON.stringify(navCapsule) : 'none'}, ` +
|
|
39
|
+
`navSeed=${navSeed ? JSON.stringify(navSeed) : 'none'}, hasFillExterior=${hasFillExterior}, hasFloorFill=${hasFloorFill}, hasNav=${hasNav}`);
|
|
40
|
+
const xCol = data.table[0 /* ColIdx.x */];
|
|
41
|
+
const yCol = data.table[1 /* ColIdx.y */];
|
|
42
|
+
const zCol = data.table[2 /* ColIdx.z */];
|
|
43
|
+
const sxCol = data.table[3 /* ColIdx.sx */];
|
|
44
|
+
const syCol = data.table[4 /* ColIdx.sy */];
|
|
45
|
+
const szCol = data.table[5 /* ColIdx.sz */];
|
|
46
|
+
const qxCol = data.table[6 /* ColIdx.qx */];
|
|
47
|
+
const qyCol = data.table[7 /* ColIdx.qy */];
|
|
48
|
+
const qzCol = data.table[8 /* ColIdx.qz */];
|
|
49
|
+
const qwCol = data.table[9 /* ColIdx.qw */];
|
|
50
|
+
const aCol = data.table[13 /* ColIdx.a */];
|
|
51
|
+
const sceneBounds = {
|
|
52
|
+
min: { x: Infinity, y: Infinity, z: Infinity },
|
|
53
|
+
max: { x: -Infinity, y: -Infinity, z: -Infinity }
|
|
54
|
+
};
|
|
55
|
+
// Compute per-gaussian AABB extents from quaternion+scale and accumulate scene bounds.
|
|
56
|
+
logger.time('Voxel bounding/extents');
|
|
57
|
+
const extents = new Float32Array(data.counts * 3);
|
|
58
|
+
const extentOpacityThreshold = ALPHA_THRESHOLD;
|
|
59
|
+
let invalidExtentCount = 0;
|
|
60
|
+
for (let i = 0; i < data.counts; i++) {
|
|
61
|
+
const e = extentsFromQuatScale(sxCol[i], syCol[i], szCol[i], qxCol[i], qyCol[i], qzCol[i], qwCol[i], aCol[i], extentOpacityThreshold);
|
|
62
|
+
if (!Number.isFinite(e.ex) || !Number.isFinite(e.ey) || !Number.isFinite(e.ez)) {
|
|
63
|
+
extents[i * 3 + 0] = 0;
|
|
64
|
+
extents[i * 3 + 1] = 0;
|
|
65
|
+
extents[i * 3 + 2] = 0;
|
|
66
|
+
invalidExtentCount++;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
extents[i * 3 + 0] = e.ex;
|
|
70
|
+
extents[i * 3 + 1] = e.ey;
|
|
71
|
+
extents[i * 3 + 2] = e.ez;
|
|
72
|
+
sceneBounds.min.x = Math.min(sceneBounds.min.x, xCol[i] - e.ex);
|
|
73
|
+
sceneBounds.min.y = Math.min(sceneBounds.min.y, yCol[i] - e.ey);
|
|
74
|
+
sceneBounds.min.z = Math.min(sceneBounds.min.z, zCol[i] - e.ez);
|
|
75
|
+
sceneBounds.max.x = Math.max(sceneBounds.max.x, xCol[i] + e.ex);
|
|
76
|
+
sceneBounds.max.y = Math.max(sceneBounds.max.y, yCol[i] + e.ey);
|
|
77
|
+
sceneBounds.max.z = Math.max(sceneBounds.max.z, zCol[i] + e.ez);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (invalidExtentCount > 0) {
|
|
81
|
+
logger.info(`voxel: skipped ${invalidExtentCount} gaussians with invalid extent values`);
|
|
82
|
+
}
|
|
83
|
+
logger.info(`scene extents: (${sceneBounds.min.x.toFixed(2)},${sceneBounds.min.y.toFixed(2)},${sceneBounds.min.z.toFixed(2)}) - (${sceneBounds.max.x.toFixed(2)},${sceneBounds.max.y.toFixed(2)},${sceneBounds.max.z.toFixed(2)})`);
|
|
84
|
+
logger.timeEnd('Voxel bounding/extents');
|
|
85
|
+
const exteriorPad = hasFillExterior
|
|
86
|
+
? (Math.ceil(navExteriorRadius / voxelResolution) + 1) * voxelResolution
|
|
87
|
+
: 0;
|
|
88
|
+
const floorPad = hasFloorFill
|
|
89
|
+
? (Math.ceil(floorFillDilation / voxelResolution) + 1) * voxelResolution
|
|
90
|
+
: 0;
|
|
91
|
+
const padXZ = Math.max(exteriorPad, floorPad);
|
|
92
|
+
const padY = exteriorPad;
|
|
93
|
+
const rawVoxelBounds = {
|
|
94
|
+
min: {
|
|
95
|
+
x: sceneBounds.min.x - padXZ,
|
|
96
|
+
y: sceneBounds.min.y - padY,
|
|
97
|
+
z: sceneBounds.min.z - padXZ
|
|
98
|
+
},
|
|
99
|
+
max: {
|
|
100
|
+
x: sceneBounds.max.x + padXZ,
|
|
101
|
+
y: sceneBounds.max.y + padY,
|
|
102
|
+
z: sceneBounds.max.z + padXZ
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const voxelBounds = {
|
|
106
|
+
min: {
|
|
107
|
+
x: Math.max(rawVoxelBounds.min.x, box.minCorner[0]),
|
|
108
|
+
y: Math.max(rawVoxelBounds.min.y, box.minCorner[1]),
|
|
109
|
+
z: Math.max(rawVoxelBounds.min.z, box.minCorner[2])
|
|
110
|
+
},
|
|
111
|
+
max: {
|
|
112
|
+
x: Math.min(rawVoxelBounds.max.x, box.maxCorner[0]),
|
|
113
|
+
y: Math.min(rawVoxelBounds.max.y, box.maxCorner[1]),
|
|
114
|
+
z: Math.min(rawVoxelBounds.max.z, box.maxCorner[2])
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const boxCropApplied = voxelBounds.min.x > rawVoxelBounds.min.x ||
|
|
118
|
+
voxelBounds.min.y > rawVoxelBounds.min.y ||
|
|
119
|
+
voxelBounds.min.z > rawVoxelBounds.min.z ||
|
|
120
|
+
voxelBounds.max.x < rawVoxelBounds.max.x ||
|
|
121
|
+
voxelBounds.max.y < rawVoxelBounds.max.y ||
|
|
122
|
+
voxelBounds.max.z < rawVoxelBounds.max.z;
|
|
123
|
+
if (boxCropApplied) {
|
|
124
|
+
logger.info(`voxel box crop applied: ` +
|
|
125
|
+
`raw=(${rawVoxelBounds.min.x.toFixed(2)},${rawVoxelBounds.min.y.toFixed(2)},${rawVoxelBounds.min.z.toFixed(2)})-` +
|
|
126
|
+
`(${rawVoxelBounds.max.x.toFixed(2)},${rawVoxelBounds.max.y.toFixed(2)},${rawVoxelBounds.max.z.toFixed(2)}), ` +
|
|
127
|
+
`cropped=(${voxelBounds.min.x.toFixed(2)},${voxelBounds.min.y.toFixed(2)},${voxelBounds.min.z.toFixed(2)})-` +
|
|
128
|
+
`(${voxelBounds.max.x.toFixed(2)},${voxelBounds.max.y.toFixed(2)},${voxelBounds.max.z.toFixed(2)})`);
|
|
129
|
+
}
|
|
130
|
+
if (voxelBounds.min.x >= voxelBounds.max.x || voxelBounds.min.y >= voxelBounds.max.y || voxelBounds.min.z >= voxelBounds.max.z) {
|
|
131
|
+
throw new Error(`voxel box does not overlap scene bounds: box=${JSON.stringify(box)}`);
|
|
132
|
+
}
|
|
133
|
+
// Align to 4x4x4 block grid.
|
|
134
|
+
const gridBounds = alignGridBounds(voxelBounds, voxelResolution);
|
|
135
|
+
let blocks = new BlockMaskBuffer();
|
|
136
|
+
if (backend === 'gpu') {
|
|
137
|
+
const gpuStart = Date.now();
|
|
138
|
+
try {
|
|
139
|
+
blocks = await gpuVoxelize(xCol, yCol, zCol, sxCol, syCol, szCol, qxCol, qyCol, qzCol, qwCol, aCol, extents, gridBounds, voxelResolution, opacityCutoff);
|
|
140
|
+
const gpuElapsed = Date.now() - gpuStart;
|
|
141
|
+
logger.info(`Voxelizing (GPU) done: ${(gpuElapsed / 1000).toFixed(3)}s`);
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
const gpuElapsed = Date.now() - gpuStart;
|
|
145
|
+
logger.error('Voxel GPU backend failed, fallback to CPU.');
|
|
146
|
+
logger.error(`Voxelizing (GPU) failed after ${(gpuElapsed / 1000).toFixed(3)}s`);
|
|
147
|
+
if (e instanceof Error) {
|
|
148
|
+
logger.error(`GPU error message: ${e.message}`);
|
|
149
|
+
if (e.stack) {
|
|
150
|
+
logger.error(`GPU error stack: ${e.stack}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
logger.error(`GPU error: ${String(e)}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (backend === 'cpu' || blocks.count === 0) {
|
|
159
|
+
logger.time('Voxelizing (CPU)');
|
|
160
|
+
blocks = await cpuVoxelize(xCol, yCol, zCol, sxCol, syCol, szCol, qxCol, qyCol, qzCol, qwCol, aCol, extents, gridBounds, voxelResolution, opacityCutoff, { workerCount: cpuWorkerCount });
|
|
161
|
+
logger.timeEnd('Voxelizing (CPU)');
|
|
162
|
+
}
|
|
163
|
+
// Remove isolated noise voxels and fill tiny holes at block-mask level.
|
|
164
|
+
logger.time('Filter blocks');
|
|
165
|
+
const blockSize = 4 * voxelResolution;
|
|
166
|
+
const numBlocksX = Math.round((gridBounds.max.x - gridBounds.min.x) / blockSize);
|
|
167
|
+
const numBlocksY = Math.round((gridBounds.max.y - gridBounds.min.y) / blockSize);
|
|
168
|
+
const numBlocksZ = Math.round((gridBounds.max.z - gridBounds.min.z) / blockSize);
|
|
169
|
+
blocks = filterAndFillBlocks(blocks, numBlocksX, numBlocksY, numBlocksZ);
|
|
170
|
+
logger.timeEnd('Filter blocks');
|
|
171
|
+
logger.time('Loading grid');
|
|
172
|
+
let grid = SparseVoxelGrid.fromBuffer(blocks, numBlocksX << 2, numBlocksY << 2, numBlocksZ << 2);
|
|
173
|
+
blocks.clear();
|
|
174
|
+
logger.timeEnd('Loading grid');
|
|
175
|
+
let navGridBounds = gridBounds;
|
|
176
|
+
// Optional navigability passes (aligned with reference order):
|
|
177
|
+
// fillExterior -> fillFloor -> carve.
|
|
178
|
+
if (hasFillExterior) {
|
|
179
|
+
logger.time('Fill exterior');
|
|
180
|
+
const fillResult = await fillExterior(grid, navGridBounds, voxelResolution, navExteriorRadius, navSeed, backend);
|
|
181
|
+
grid = fillResult.grid;
|
|
182
|
+
navGridBounds = fillResult.gridBounds;
|
|
183
|
+
logger.timeEnd('Fill exterior');
|
|
184
|
+
}
|
|
185
|
+
if (hasFloorFill) {
|
|
186
|
+
logger.time('Fill floor');
|
|
187
|
+
const floorResult = await fillFloor(grid, navGridBounds, voxelResolution, floorFillDilation, backend);
|
|
188
|
+
grid = floorResult.grid;
|
|
189
|
+
navGridBounds = floorResult.gridBounds;
|
|
190
|
+
logger.timeEnd('Fill floor');
|
|
191
|
+
}
|
|
192
|
+
if (hasNav) {
|
|
193
|
+
logger.time('Carve nav');
|
|
194
|
+
const navResult = await carve(grid, navGridBounds, voxelResolution, navCapsule.height, navCapsule.radius, navSeed, backend);
|
|
195
|
+
grid = navResult.grid;
|
|
196
|
+
navGridBounds = navResult.gridBounds;
|
|
197
|
+
logger.timeEnd('Carve nav');
|
|
198
|
+
}
|
|
199
|
+
// Crop padded bounds before octree build.
|
|
200
|
+
// If navigability passes ran, keep navigable extent; otherwise crop to occupied extent.
|
|
201
|
+
logger.time('Crop voxel bounds');
|
|
202
|
+
const finalCrop = (hasFillExterior || hasFloorFill)
|
|
203
|
+
? cropToNavigable(grid, navGridBounds, voxelResolution)
|
|
204
|
+
: cropToOccupied(grid, navGridBounds, voxelResolution);
|
|
205
|
+
grid = finalCrop.grid;
|
|
206
|
+
navGridBounds = finalCrop.gridBounds;
|
|
207
|
+
logger.timeEnd('Crop voxel bounds');
|
|
208
|
+
const collisionMeshShape = (() => {
|
|
209
|
+
if (collisionMesh === false || collisionMesh === undefined) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
if (collisionMesh === true) {
|
|
213
|
+
return 'smooth';
|
|
214
|
+
}
|
|
215
|
+
if (collisionMesh === 'smooth' || collisionMesh === 'faces') {
|
|
216
|
+
return collisionMesh;
|
|
217
|
+
}
|
|
218
|
+
throw new Error(`Invalid collisionMesh value: ${String(collisionMesh)}. Expected true, false, "smooth", or "faces"`);
|
|
219
|
+
})();
|
|
220
|
+
const collisionGlb = collisionMeshShape
|
|
221
|
+
? buildCollisionMesh(grid, navGridBounds, voxelResolution, collisionMeshShape)
|
|
222
|
+
: undefined;
|
|
223
|
+
// BuildSparseOctree emits Laine-Karras nodes + mixed leaf masks.
|
|
224
|
+
logger.time('Build octree');
|
|
225
|
+
const octree = buildSparseOctree(grid, navGridBounds, sceneBounds, voxelResolution, { consumeGrid: true });
|
|
226
|
+
logger.timeEnd('Build octree');
|
|
227
|
+
logger.info(`octree: depth=${octree.treeDepth}, interior=${octree.numInteriorNodes}, mixed=${octree.numMixedLeaves}`);
|
|
228
|
+
const metadata = {
|
|
229
|
+
version: '1.1',
|
|
230
|
+
gridBounds: {
|
|
231
|
+
min: [octree.gridBounds.min.x, octree.gridBounds.min.y, octree.gridBounds.min.z],
|
|
232
|
+
max: [octree.gridBounds.max.x, octree.gridBounds.max.y, octree.gridBounds.max.z]
|
|
233
|
+
},
|
|
234
|
+
sceneBounds: {
|
|
235
|
+
min: [octree.sceneBounds.min.x, octree.sceneBounds.min.y, octree.sceneBounds.min.z],
|
|
236
|
+
max: [octree.sceneBounds.max.x, octree.sceneBounds.max.y, octree.sceneBounds.max.z]
|
|
237
|
+
},
|
|
238
|
+
voxelResolution: octree.voxelResolution,
|
|
239
|
+
leafSize: octree.leafSize,
|
|
240
|
+
treeDepth: octree.treeDepth,
|
|
241
|
+
numInteriorNodes: octree.numInteriorNodes,
|
|
242
|
+
numMixedLeaves: octree.numMixedLeaves,
|
|
243
|
+
nodeCount: octree.nodes.length,
|
|
244
|
+
leafDataCount: octree.leafData.length,
|
|
245
|
+
files: ['voxel.bin']
|
|
246
|
+
};
|
|
247
|
+
const binarySize = (octree.nodes.length + octree.leafData.length) * 4;
|
|
248
|
+
const binary = new Uint8Array(binarySize);
|
|
249
|
+
const view = new Uint32Array(binary.buffer);
|
|
250
|
+
view.set(octree.nodes, 0);
|
|
251
|
+
view.set(octree.leafData, octree.nodes.length);
|
|
252
|
+
return { metadata, binary, collisionGlb };
|
|
253
|
+
};
|
|
254
|
+
export async function writeVoxelFiles(outputDir, data, options) {
|
|
255
|
+
const { metadata, binary, collisionGlb } = await writeVoxels(data, options?.voxelResolution ?? 0.05, options?.opacityCutoff ?? 0.1, options?.backend ?? 'gpu', options?.collisionMesh ?? false, options?.navExteriorRadius, options?.floorFill ?? false, options?.floorFillDilation ?? 0, options?.cpuWorkerCount ?? -1, options?.box ?? { minCorner: [-100, -100, -100], maxCorner: [100, 100, 100] }, options?.navCapsule, options?.navSeed);
|
|
256
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
257
|
+
const metaPath = path.join(outputDir, 'voxel-meta.json');
|
|
258
|
+
const binPath = path.join(outputDir, 'voxel.bin');
|
|
259
|
+
logger.info(`writing '${metaPath}'...`);
|
|
260
|
+
fs.writeFileSync(metaPath, Buffer.from(JSON.stringify(metadata, null, 2), 'utf-8'));
|
|
261
|
+
logger.info(`writing '${binPath}'...`);
|
|
262
|
+
fs.writeFileSync(binPath, binary);
|
|
263
|
+
if (collisionGlb && collisionGlb.length > 0) {
|
|
264
|
+
const glbPath = path.join(outputDir, 'collision.glb');
|
|
265
|
+
logger.info(`writing '${glbPath}'...`);
|
|
266
|
+
fs.writeFileSync(glbPath, collisionGlb);
|
|
267
|
+
}
|
|
268
|
+
const totalBytes = binary.length;
|
|
269
|
+
if (collisionGlb && collisionGlb.length > 0) {
|
|
270
|
+
logger.info(`total size: octree ${(totalBytes / 1024).toFixed(1)} KB, collision mesh ${(collisionGlb.length / 1024).toFixed(1)} KB`);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
logger.info(`total size: ${(totalBytes / 1024).toFixed(1)} KB`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
export const voxelUtils = {
|
|
277
|
+
getChildOffset,
|
|
278
|
+
encodeMorton3,
|
|
279
|
+
decodeMorton3
|
|
280
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Config as AutoChunkLodConfig } from './tasks/AutoChunkLodTask.js';
|
|
2
|
+
import { Config as AutoLodConfig } from './tasks/AutoLodTask.js';
|
|
3
|
+
import { Config as FlexLodConfig } from './tasks/FlexLodTask.js';
|
|
4
|
+
import { Config as SkeletonLodConfig } from './tasks/SkeletonLodTask.js';
|
|
5
|
+
import { Config as ModifyConfig } from './tasks/ModifyTask.js';
|
|
6
|
+
import { Config as ReadConfig } from './tasks/ReadTask.js';
|
|
7
|
+
import { Config as WriteConfig } from './tasks/WriteTask.js';
|
|
8
|
+
import { VoxelTaskConfig } from './tasks/VoxelTask.js';
|
|
9
|
+
interface TaskConfigMap {
|
|
10
|
+
Read: ReadConfig;
|
|
11
|
+
Write: WriteConfig;
|
|
12
|
+
Voxel: VoxelTaskConfig;
|
|
13
|
+
Modify: ModifyConfig;
|
|
14
|
+
SkeletonLod: SkeletonLodConfig;
|
|
15
|
+
FlexLod: FlexLodConfig;
|
|
16
|
+
AutoLod: AutoLodConfig;
|
|
17
|
+
AutoChunkLod: AutoChunkLodConfig;
|
|
18
|
+
}
|
|
19
|
+
type PipelineTask = {
|
|
20
|
+
[K in keyof TaskConfigMap]: {
|
|
21
|
+
id: string;
|
|
22
|
+
type: K;
|
|
23
|
+
config: TaskConfigMap[K];
|
|
24
|
+
release?: string[];
|
|
25
|
+
};
|
|
26
|
+
}[keyof TaskConfigMap];
|
|
27
|
+
interface PipelineConfig {
|
|
28
|
+
version: number;
|
|
29
|
+
gpu?: number;
|
|
30
|
+
tasks: PipelineTask[];
|
|
31
|
+
}
|
|
32
|
+
export declare function runner(config: PipelineConfig): Promise<void>;
|
|
33
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { AutoChunkLodTask } from './tasks/AutoChunkLodTask.js';
|
|
2
|
+
import { AutoLodTask } from './tasks/AutoLodTask.js';
|
|
3
|
+
import { FlexLodTask } from './tasks/FlexLodTask.js';
|
|
4
|
+
import { SkeletonLodTask } from './tasks/SkeletonLodTask.js';
|
|
5
|
+
import { ModifyTask } from './tasks/ModifyTask.js';
|
|
6
|
+
import { ReadTask } from './tasks/ReadTask.js';
|
|
7
|
+
import { WriteTask } from './tasks/WriteTask.js';
|
|
8
|
+
import { VoxelTask } from './tasks/VoxelTask.js';
|
|
9
|
+
import { enumerateAdapters, logger, releaseSharedDevice, initGPUAdapter } from './utils/index.js';
|
|
10
|
+
const TaskMap = {
|
|
11
|
+
Read: new ReadTask(),
|
|
12
|
+
Write: new WriteTask(),
|
|
13
|
+
Voxel: new VoxelTask(),
|
|
14
|
+
Modify: new ModifyTask(),
|
|
15
|
+
SkeletonLod: new SkeletonLodTask(),
|
|
16
|
+
FlexLod: new FlexLodTask(),
|
|
17
|
+
AutoLod: new AutoLodTask(),
|
|
18
|
+
AutoChunkLod: new AutoChunkLodTask(),
|
|
19
|
+
};
|
|
20
|
+
function anyTaskRequireGPU(tasks) {
|
|
21
|
+
for (const t of tasks) {
|
|
22
|
+
if (TaskMap[t.type].requiresGPU(t.config)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
export async function runner(config) {
|
|
29
|
+
console.time('Total elapsed time');
|
|
30
|
+
const ctx = {
|
|
31
|
+
logger,
|
|
32
|
+
resources: new Map(),
|
|
33
|
+
};
|
|
34
|
+
if (anyTaskRequireGPU(config.tasks)) {
|
|
35
|
+
logger.prefix = `[Task:GPU]`;
|
|
36
|
+
logger.info('Any task requires GPU detected, initialize GPU adapter.');
|
|
37
|
+
const adapter = (await enumerateAdapters())[config.gpu ?? 0];
|
|
38
|
+
initGPUAdapter([`adapter=${adapter.name}`]);
|
|
39
|
+
}
|
|
40
|
+
for (const taskDef of config.tasks) {
|
|
41
|
+
const { id, type, config: taskConfig, release = [] } = taskDef;
|
|
42
|
+
const task = TaskMap[type];
|
|
43
|
+
if (!task) {
|
|
44
|
+
throw new Error(`Task not found: ${type} (id: ${id})`);
|
|
45
|
+
}
|
|
46
|
+
logger.prefix = `[Task:${type}#${id}]`;
|
|
47
|
+
logger.time('elapsed time');
|
|
48
|
+
await task.exec(taskConfig, ctx);
|
|
49
|
+
release.forEach(v => ctx.resources.delete(v));
|
|
50
|
+
logger.timeEnd('elapsed time');
|
|
51
|
+
}
|
|
52
|
+
releaseSharedDevice();
|
|
53
|
+
console.timeEnd('Total elapsed time');
|
|
54
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { SplatData } from '../SplatData.js';
|
|
2
|
+
import { Buffer } from 'node:buffer';
|
|
3
|
+
export interface LevelParameter {
|
|
4
|
+
precision: number;
|
|
5
|
+
scaleBoost: number;
|
|
6
|
+
}
|
|
7
|
+
export interface BlockedSplats {
|
|
8
|
+
box: {
|
|
9
|
+
min: [number, number, number];
|
|
10
|
+
max: [number, number, number];
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* current block referenced splats, level ordered.
|
|
14
|
+
*/
|
|
15
|
+
refs: number[];
|
|
16
|
+
}
|
|
17
|
+
export interface BlockedResult {
|
|
18
|
+
splats: SplatData[];
|
|
19
|
+
blocks: BlockedSplats[];
|
|
20
|
+
}
|
|
21
|
+
export declare function generateLod(splat: SplatData, levelParameters: LevelParameter[], blockPrecision: number, minSize: number, maxStep: number): BlockedResult;
|
|
22
|
+
export declare class WebPLosslessProfile {
|
|
23
|
+
readonly lossless = true;
|
|
24
|
+
}
|
|
25
|
+
export declare class WebPQualityProfile {
|
|
26
|
+
readonly quality: number;
|
|
27
|
+
readonly lossless = false;
|
|
28
|
+
constructor(quality: number);
|
|
29
|
+
}
|
|
30
|
+
export declare function encodeWebP(data: Uint8Array | Buffer, width: number, height: number, profile: WebPLosslessProfile | WebPQualityProfile): Buffer<ArrayBufferLike>;
|
|
31
|
+
export declare function decodeWebP(data: Uint8Array | Buffer): {
|
|
32
|
+
data: Buffer;
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
};
|
|
36
|
+
export declare function encodeAVIF(data: Uint8Array | Buffer, width: number, height: number, quality: number): Buffer<ArrayBufferLike>;
|
|
37
|
+
export interface AVIFEncodeInput {
|
|
38
|
+
data: Uint8Array | Buffer;
|
|
39
|
+
width: number;
|
|
40
|
+
height: number;
|
|
41
|
+
quality: number;
|
|
42
|
+
}
|
|
43
|
+
export declare function encodeAVIFBatched(inputs: AVIFEncodeInput[]): Buffer<ArrayBufferLike>[];
|
|
44
|
+
export declare function decodeAVIF(data: Uint8Array | Buffer): {
|
|
45
|
+
data: Buffer;
|
|
46
|
+
width: number;
|
|
47
|
+
height: number;
|
|
48
|
+
};
|
|
49
|
+
export declare function decodeAVIFBatched(inputs: Array<Uint8Array | Buffer>): {
|
|
50
|
+
data: Buffer;
|
|
51
|
+
width: number;
|
|
52
|
+
height: number;
|
|
53
|
+
}[];
|
|
54
|
+
export declare function clusterAverage(dataTable: Float32Array[], clusters: Uint32Array[], output: Float32Array[]): void;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as child_process from 'node:child_process';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { SplatData } from '../SplatData.js';
|
|
6
|
+
import { Buffer } from 'node:buffer';
|
|
7
|
+
const getModule = (function () {
|
|
8
|
+
let m = undefined;
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const platform = os.platform();
|
|
11
|
+
const binaryPath = `./cpp/bin/${platform === 'win32' ? 'windows' : platform}/binding.node`;
|
|
12
|
+
return function () {
|
|
13
|
+
if (!m) {
|
|
14
|
+
try {
|
|
15
|
+
m = require(binaryPath);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
console.warn(`cannot find a valid binary at: ${binaryPath}, try rebuild`);
|
|
19
|
+
child_process.spawnSync('node', [
|
|
20
|
+
require.resolve('cmake-js/bin/cmake-js'),
|
|
21
|
+
'build',
|
|
22
|
+
'--',
|
|
23
|
+
'--preset',
|
|
24
|
+
'default'
|
|
25
|
+
], {
|
|
26
|
+
cwd: path.join(import.meta.dirname, 'cpp'),
|
|
27
|
+
stdio: 'inherit'
|
|
28
|
+
});
|
|
29
|
+
m = require(binaryPath);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return m;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
export function generateLod(splat, levelParameters, blockPrecision, minSize, maxStep) {
|
|
36
|
+
if (splat.counts === 0) {
|
|
37
|
+
return {
|
|
38
|
+
splats: [splat],
|
|
39
|
+
blocks: [{
|
|
40
|
+
box: { min: [0, 0, 0], max: [0, 0, 0] },
|
|
41
|
+
refs: new Array(levelParameters.length).fill(0),
|
|
42
|
+
}],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const levels = levelParameters.length;
|
|
46
|
+
const inputBuffers = splat.table.map(b => Buffer.from(b.buffer, b.byteOffset, b.byteLength));
|
|
47
|
+
const buffer = Buffer.alloc(levels * 8);
|
|
48
|
+
{
|
|
49
|
+
const parameters = new Float32Array(buffer.buffer, buffer.byteOffset, levels * 2);
|
|
50
|
+
for (let i = 0; i < levelParameters.length; i++) {
|
|
51
|
+
const { precision, scaleBoost } = levelParameters[i];
|
|
52
|
+
parameters[i * 2] = precision;
|
|
53
|
+
parameters[i * 2 + 1] = scaleBoost;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const { blockBoxes, blockRefs, gaussianCount, data, } = getModule().generate_lod(inputBuffers, splat.shCounts, buffer, blockPrecision, minSize, maxStep);
|
|
57
|
+
const blockView = new Float32Array(blockBoxes.buffer, blockBoxes.byteOffset, blockBoxes.byteLength / 4);
|
|
58
|
+
const blockRefsView = new Uint32Array(blockRefs.buffer, blockRefs.byteOffset, blockRefs.byteLength / 4);
|
|
59
|
+
const blockCount = blockView.length / 6;
|
|
60
|
+
const gaussianCountView = new Uint32Array(gaussianCount.buffer, gaussianCount.byteOffset, gaussianCount.byteLength / 4);
|
|
61
|
+
const blocks = [];
|
|
62
|
+
const splats = [];
|
|
63
|
+
// read splats
|
|
64
|
+
{
|
|
65
|
+
let gaussianOffset = 0;
|
|
66
|
+
for (const count of gaussianCountView) {
|
|
67
|
+
const splatData = new SplatData(1, splat.shDegree);
|
|
68
|
+
splatData.shDegree = splat.shDegree;
|
|
69
|
+
splatData.shCounts = splat.shCounts;
|
|
70
|
+
splatData.counts = count;
|
|
71
|
+
splatData.table = data.map(buffer => new Float32Array(buffer.buffer, buffer.byteOffset + gaussianOffset * 4, count));
|
|
72
|
+
splats.push(splatData);
|
|
73
|
+
gaussianOffset += count;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (let i = 0; i < blockCount; i++) {
|
|
77
|
+
const block = {
|
|
78
|
+
box: {
|
|
79
|
+
min: [blockView[i * 6], blockView[i * 6 + 1], blockView[i * 6 + 2]],
|
|
80
|
+
max: [blockView[i * 6 + 3], blockView[i * 6 + 4], blockView[i * 6 + 5]]
|
|
81
|
+
},
|
|
82
|
+
refs: Array.from(blockRefsView.subarray(i * levels, i * levels + levels)),
|
|
83
|
+
};
|
|
84
|
+
blocks.push(block);
|
|
85
|
+
}
|
|
86
|
+
return { splats, blocks };
|
|
87
|
+
}
|
|
88
|
+
export class WebPLosslessProfile {
|
|
89
|
+
lossless = true;
|
|
90
|
+
}
|
|
91
|
+
export class WebPQualityProfile {
|
|
92
|
+
quality;
|
|
93
|
+
lossless = false;
|
|
94
|
+
constructor(quality) {
|
|
95
|
+
this.quality = quality;
|
|
96
|
+
}
|
|
97
|
+
;
|
|
98
|
+
}
|
|
99
|
+
export function encodeWebP(data, width, height, profile) {
|
|
100
|
+
const buffer = data instanceof Buffer ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
101
|
+
if (profile.lossless) {
|
|
102
|
+
return getModule().webp_encode_rgba_lossless(buffer, width, height);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
return getModule().webp_encode_rgba(buffer, width, height, profile.quality);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function decodeWebP(data) {
|
|
109
|
+
const buffer = data instanceof Buffer ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
110
|
+
return getModule().webp_decode_rgba(buffer);
|
|
111
|
+
}
|
|
112
|
+
export function encodeAVIF(data, width, height, quality) {
|
|
113
|
+
const buffer = data instanceof Buffer ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
114
|
+
return getModule().avif_encode_rgba(buffer, width, height, quality);
|
|
115
|
+
}
|
|
116
|
+
export function encodeAVIFBatched(inputs) {
|
|
117
|
+
return getModule().avif_encode_rgba_batched(inputs.map(i => ({ ...i, data: i.data instanceof Buffer ? i.data : Buffer.from(i.data.buffer, i.data.byteOffset, i.data.byteLength) })));
|
|
118
|
+
}
|
|
119
|
+
export function decodeAVIF(data) {
|
|
120
|
+
const buffer = data instanceof Buffer ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
121
|
+
return getModule().avif_decode_rgba(buffer);
|
|
122
|
+
}
|
|
123
|
+
export function decodeAVIFBatched(inputs) {
|
|
124
|
+
return getModule().avif_decode_rgba_batched(inputs.map(i => i instanceof Buffer ? i : Buffer.from(i.buffer, i.byteOffset, i.byteLength)));
|
|
125
|
+
}
|
|
126
|
+
export function clusterAverage(dataTable, clusters, output) {
|
|
127
|
+
return getModule().cluster_average(dataTable.map(t => Buffer.from(t.buffer, t.byteOffset, t.byteLength)), clusters.map(t => Buffer.from(t.buffer, t.byteOffset, t.byteLength)), output.map(t => Buffer.from(t.buffer, t.byteOffset, t.byteLength)));
|
|
128
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Context, BaseTask } from './BaseTask.js';
|
|
2
|
+
import { LevelParameter } from '../native/index.js';
|
|
3
|
+
export interface Config {
|
|
4
|
+
input: string;
|
|
5
|
+
output: string;
|
|
6
|
+
type: string;
|
|
7
|
+
maxChunkCounts?: number;
|
|
8
|
+
levels?: LevelParameter[];
|
|
9
|
+
}
|
|
10
|
+
export declare class AutoChunkLodTask extends BaseTask<Config> {
|
|
11
|
+
exec(config: Config, { logger, resources }: Context): Promise<void>;
|
|
12
|
+
requiresGPU(config: Config): boolean;
|
|
13
|
+
}
|