@playcanvas/splat-transform 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -6
- package/dist/index.mjs +140 -50
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ splat-transform input_a.ply input_b.ply output/meta.json
|
|
|
22
22
|
The input and output files can optionally be transformed. For example:
|
|
23
23
|
```
|
|
24
24
|
# load input.ply and translate it by (1, 0, 0) and write the result to output.ply
|
|
25
|
-
splat-transform input.ply
|
|
25
|
+
splat-transform input.ply -t 1,0,0 output.ply
|
|
26
26
|
|
|
27
27
|
# remove entries containing NaN and Inf and bands larger than 2
|
|
28
28
|
splat-transform input.ply output.ply --filterNaN --filterBands 2
|
|
@@ -37,8 +37,3 @@ The full list of possible actions are as follows:
|
|
|
37
37
|
-filterByValue -c name,comparator,value Filter gaussians by a value. Specify the value name, comparator (lt, lte, gt, gte, eq, neq) and value
|
|
38
38
|
-filterBands -h 1 Filter spherical harmonic band data. Value must be 0, 1, 2 or 3.
|
|
39
39
|
```
|
|
40
|
-
|
|
41
|
-
## Note
|
|
42
|
-
This very first version supports the following:
|
|
43
|
-
- reading gaussian splat ply files containing only float32 data
|
|
44
|
-
- writing PlayCanvas' compressed.ply format
|
package/dist/index.mjs
CHANGED
|
@@ -1881,7 +1881,7 @@ class Quat {
|
|
|
1881
1881
|
}
|
|
1882
1882
|
}
|
|
1883
1883
|
|
|
1884
|
-
var version = "0.
|
|
1884
|
+
var version = "0.3.0";
|
|
1885
1885
|
|
|
1886
1886
|
class Column {
|
|
1887
1887
|
name;
|
|
@@ -2770,6 +2770,23 @@ const writeCompressedPly = async (fileHandle, dataTable) => {
|
|
|
2770
2770
|
await fileHandle.write(shData);
|
|
2771
2771
|
};
|
|
2772
2772
|
|
|
2773
|
+
const writeCsv = async (fileHandle, dataTable) => {
|
|
2774
|
+
const len = dataTable.numRows;
|
|
2775
|
+
// write header
|
|
2776
|
+
await fileHandle.write(`${dataTable.columnNames.join(',')}\n`);
|
|
2777
|
+
const columns = dataTable.columns.map(c => c.data);
|
|
2778
|
+
// write rows
|
|
2779
|
+
for (let i = 0; i < len; ++i) {
|
|
2780
|
+
let row = '';
|
|
2781
|
+
for (let c = 0; c < dataTable.columns.length; ++c) {
|
|
2782
|
+
if (c)
|
|
2783
|
+
row += ',';
|
|
2784
|
+
row += columns[c][i];
|
|
2785
|
+
}
|
|
2786
|
+
await fileHandle.write(`${row}\n`);
|
|
2787
|
+
}
|
|
2788
|
+
};
|
|
2789
|
+
|
|
2773
2790
|
const columnTypeToPlyType = (type) => {
|
|
2774
2791
|
switch (type) {
|
|
2775
2792
|
case 'float32': return 'float';
|
|
@@ -2846,18 +2863,11 @@ const calcMinMax = (dataTable, columnNames) => {
|
|
|
2846
2863
|
const logTransform = (value) => {
|
|
2847
2864
|
return Math.sign(value) * Math.log(Math.abs(value) + 1);
|
|
2848
2865
|
};
|
|
2849
|
-
//
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
const chunkWidth = width / 16;
|
|
2853
|
-
const chunkIndex = Math.floor(index / 256);
|
|
2854
|
-
const chunkX = chunkIndex % chunkWidth;
|
|
2855
|
-
const chunkY = Math.floor(chunkIndex / chunkWidth);
|
|
2856
|
-
const x = chunkX * 16 + (index % 16);
|
|
2857
|
-
const y = chunkY * 16 + Math.floor((index % 256) / 16);
|
|
2858
|
-
return x + y * width;
|
|
2866
|
+
// no packing
|
|
2867
|
+
const identity = (index, width) => {
|
|
2868
|
+
return index;
|
|
2859
2869
|
};
|
|
2860
|
-
const writeSogs = async (
|
|
2870
|
+
const writeSogs = async (fileHandle, dataTable, outputFilename) => {
|
|
2861
2871
|
// generate an optimal ordering
|
|
2862
2872
|
const sortIndices = generateOrdering(dataTable);
|
|
2863
2873
|
const numRows = dataTable.numRows;
|
|
@@ -2871,6 +2881,8 @@ const writeSogs = async (outputFilename, dataTable) => {
|
|
|
2871
2881
|
.webp({ lossless: true })
|
|
2872
2882
|
.toFile(pathname);
|
|
2873
2883
|
};
|
|
2884
|
+
// the layout function determines how the data is packed into the output texture.
|
|
2885
|
+
const layout = identity; // rectChunks;
|
|
2874
2886
|
const row = {};
|
|
2875
2887
|
// convert position/means
|
|
2876
2888
|
const meansL = new Uint8Array(width * height * channels);
|
|
@@ -2883,7 +2895,7 @@ const writeSogs = async (outputFilename, dataTable) => {
|
|
|
2883
2895
|
const x = 65535 * (logTransform(row.x) - meansMinMax[0][0]) / (meansMinMax[0][1] - meansMinMax[0][0]);
|
|
2884
2896
|
const y = 65535 * (logTransform(row.y) - meansMinMax[1][0]) / (meansMinMax[1][1] - meansMinMax[1][0]);
|
|
2885
2897
|
const z = 65535 * (logTransform(row.z) - meansMinMax[2][0]) / (meansMinMax[2][1] - meansMinMax[2][0]);
|
|
2886
|
-
const ti =
|
|
2898
|
+
const ti = layout(i);
|
|
2887
2899
|
meansL[ti * 4] = x & 0xff;
|
|
2888
2900
|
meansL[ti * 4 + 1] = y & 0xff;
|
|
2889
2901
|
meansL[ti * 4 + 2] = z & 0xff;
|
|
@@ -2930,7 +2942,7 @@ const writeSogs = async (outputFilename, dataTable) => {
|
|
|
2930
2942
|
[0, 1, 3],
|
|
2931
2943
|
[0, 1, 2]
|
|
2932
2944
|
][maxComp];
|
|
2933
|
-
const ti =
|
|
2945
|
+
const ti = layout(i);
|
|
2934
2946
|
quats[ti * 4] = 255 * (q[idx[0]] * 0.5 + 0.5);
|
|
2935
2947
|
quats[ti * 4 + 1] = 255 * (q[idx[1]] * 0.5 + 0.5);
|
|
2936
2948
|
quats[ti * 4 + 2] = 255 * (q[idx[2]] * 0.5 + 0.5);
|
|
@@ -2944,7 +2956,7 @@ const writeSogs = async (outputFilename, dataTable) => {
|
|
|
2944
2956
|
const scaleMinMax = calcMinMax(dataTable, scaleNames);
|
|
2945
2957
|
for (let i = 0; i < dataTable.numRows; ++i) {
|
|
2946
2958
|
dataTable.getRow(sortIndices[i], row, scaleColumns);
|
|
2947
|
-
const ti =
|
|
2959
|
+
const ti = layout(i);
|
|
2948
2960
|
scales[ti * 4] = 255 * (row.scale_0 - scaleMinMax[0][0]) / (scaleMinMax[0][1] - scaleMinMax[0][0]);
|
|
2949
2961
|
scales[ti * 4 + 1] = 255 * (row.scale_1 - scaleMinMax[1][0]) / (scaleMinMax[1][1] - scaleMinMax[1][0]);
|
|
2950
2962
|
scales[ti * 4 + 2] = 255 * (row.scale_2 - scaleMinMax[2][0]) / (scaleMinMax[2][1] - scaleMinMax[2][0]);
|
|
@@ -2958,7 +2970,7 @@ const writeSogs = async (outputFilename, dataTable) => {
|
|
|
2958
2970
|
const sh0MinMax = calcMinMax(dataTable, sh0Names);
|
|
2959
2971
|
for (let i = 0; i < dataTable.numRows; ++i) {
|
|
2960
2972
|
dataTable.getRow(sortIndices[i], row, sh0Columns);
|
|
2961
|
-
const ti =
|
|
2973
|
+
const ti = layout(i);
|
|
2962
2974
|
sh0[ti * 4] = 255 * (row.f_dc_0 - sh0MinMax[0][0]) / (sh0MinMax[0][1] - sh0MinMax[0][0]);
|
|
2963
2975
|
sh0[ti * 4 + 1] = 255 * (row.f_dc_1 - sh0MinMax[1][0]) / (sh0MinMax[1][1] - sh0MinMax[1][0]);
|
|
2964
2976
|
sh0[ti * 4 + 2] = 255 * (row.f_dc_2 - sh0MinMax[2][0]) / (sh0MinMax[2][1] - sh0MinMax[2][0]);
|
|
@@ -2998,9 +3010,7 @@ const writeSogs = async (outputFilename, dataTable) => {
|
|
|
2998
3010
|
files: ['sh0.webp']
|
|
2999
3011
|
}
|
|
3000
3012
|
};
|
|
3001
|
-
|
|
3002
|
-
await outputFile.write((new TextEncoder()).encode(JSON.stringify(meta, null, 4)));
|
|
3003
|
-
await outputFile.close();
|
|
3013
|
+
await fileHandle.write((new TextEncoder()).encode(JSON.stringify(meta, null, 4)));
|
|
3004
3014
|
};
|
|
3005
3015
|
|
|
3006
3016
|
const readFile = async (filename) => {
|
|
@@ -3010,28 +3020,63 @@ const readFile = async (filename) => {
|
|
|
3010
3020
|
await inputFile.close();
|
|
3011
3021
|
return plyData;
|
|
3012
3022
|
};
|
|
3013
|
-
const
|
|
3014
|
-
|
|
3015
|
-
|
|
3023
|
+
const getOutputFormat = (filename) => {
|
|
3024
|
+
const lowerFilename = filename.toLowerCase();
|
|
3025
|
+
if (lowerFilename.endsWith('.csv')) {
|
|
3026
|
+
return 'csv';
|
|
3027
|
+
}
|
|
3028
|
+
else if (lowerFilename.endsWith('.json')) {
|
|
3029
|
+
return 'json';
|
|
3016
3030
|
}
|
|
3017
|
-
else if (
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3031
|
+
else if (lowerFilename.endsWith('.compressed.ply')) {
|
|
3032
|
+
return 'compressed-ply';
|
|
3033
|
+
}
|
|
3034
|
+
else if (lowerFilename.endsWith('.ply')) {
|
|
3035
|
+
return 'ply';
|
|
3022
3036
|
}
|
|
3023
3037
|
else {
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3038
|
+
throw new Error(`Unsupported output file type: ${filename}`);
|
|
3039
|
+
}
|
|
3040
|
+
};
|
|
3041
|
+
const writeFile = async (filename, dataTable, options) => {
|
|
3042
|
+
const outputFormat = getOutputFormat(filename);
|
|
3043
|
+
// open the output file
|
|
3044
|
+
let outputFile;
|
|
3045
|
+
try {
|
|
3046
|
+
outputFile = await open(filename, options.overwrite ? 'w' : 'wx');
|
|
3047
|
+
}
|
|
3048
|
+
catch (err) {
|
|
3049
|
+
if (err.code === 'EEXIST') {
|
|
3050
|
+
console.error(`File '${filename}' already exists. Use -w option to overwrite.`);
|
|
3051
|
+
exit(1);
|
|
3052
|
+
}
|
|
3053
|
+
else {
|
|
3054
|
+
throw err;
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
console.log(`writing '${filename}'...`);
|
|
3058
|
+
// write the data
|
|
3059
|
+
switch (outputFormat) {
|
|
3060
|
+
case 'csv':
|
|
3061
|
+
await writeCsv(outputFile, dataTable);
|
|
3062
|
+
break;
|
|
3063
|
+
case 'json':
|
|
3064
|
+
await writeSogs(outputFile, dataTable, filename);
|
|
3065
|
+
break;
|
|
3066
|
+
case 'compressed-ply':
|
|
3067
|
+
await writeCompressedPly(outputFile, dataTable);
|
|
3068
|
+
break;
|
|
3069
|
+
case 'ply':
|
|
3070
|
+
await writePly(outputFile, {
|
|
3071
|
+
comments: [],
|
|
3072
|
+
elements: [{
|
|
3073
|
+
name: 'vertex',
|
|
3074
|
+
dataTable: dataTable
|
|
3075
|
+
}]
|
|
3076
|
+
});
|
|
3077
|
+
break;
|
|
3034
3078
|
}
|
|
3079
|
+
await outputFile.close();
|
|
3035
3080
|
};
|
|
3036
3081
|
// combine multiple tables into one
|
|
3037
3082
|
// columns with matching name and type are combined
|
|
@@ -3098,12 +3143,17 @@ const parseArguments = () => {
|
|
|
3098
3143
|
strict: true,
|
|
3099
3144
|
allowPositionals: true,
|
|
3100
3145
|
options: {
|
|
3146
|
+
// global options
|
|
3147
|
+
overwrite: { type: 'boolean', short: 'w' },
|
|
3148
|
+
help: { type: 'boolean', short: 'h' },
|
|
3149
|
+
version: { type: 'boolean', short: 'v' },
|
|
3150
|
+
// file options
|
|
3101
3151
|
translate: { type: 'string', short: 't', multiple: true },
|
|
3102
3152
|
rotate: { type: 'string', short: 'r', multiple: true },
|
|
3103
3153
|
scale: { type: 'string', short: 's', multiple: true },
|
|
3104
3154
|
filterNaN: { type: 'boolean', short: 'n', multiple: true },
|
|
3105
3155
|
filterByValue: { type: 'string', short: 'c', multiple: true },
|
|
3106
|
-
filterBands: { type: 'string', short: '
|
|
3156
|
+
filterBands: { type: 'string', short: 'b', multiple: true },
|
|
3107
3157
|
}
|
|
3108
3158
|
});
|
|
3109
3159
|
const parseNumber = (value) => {
|
|
@@ -3133,6 +3183,11 @@ const parseArguments = () => {
|
|
|
3133
3183
|
}
|
|
3134
3184
|
};
|
|
3135
3185
|
const files = [];
|
|
3186
|
+
const options = {
|
|
3187
|
+
overwrite: v.overwrite || false,
|
|
3188
|
+
help: v.help || false,
|
|
3189
|
+
version: v.version || false
|
|
3190
|
+
};
|
|
3136
3191
|
for (const t of tokens) {
|
|
3137
3192
|
if (t.kind === 'positional') {
|
|
3138
3193
|
files.push({
|
|
@@ -3193,22 +3248,57 @@ const parseArguments = () => {
|
|
|
3193
3248
|
}
|
|
3194
3249
|
}
|
|
3195
3250
|
}
|
|
3196
|
-
return files;
|
|
3251
|
+
return { files, options };
|
|
3197
3252
|
};
|
|
3198
|
-
const usage = `
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3253
|
+
const usage = `
|
|
3254
|
+
Apply geometric transforms & filters to Gaussian-splat point clouds
|
|
3255
|
+
===================================================================
|
|
3256
|
+
|
|
3257
|
+
USAGE
|
|
3258
|
+
splat-transform [GLOBAL] <input.ply> [ACTIONS] ... <output.{ply|compressed.ply|meta.json|csv}> [ACTIONS]
|
|
3259
|
+
|
|
3260
|
+
• Every time an *.ply* appears, it becomes the current working set; the following
|
|
3261
|
+
ACTIONS are applied in the order listed.
|
|
3262
|
+
• The last file on the command line is treated as the output; anything after it is
|
|
3263
|
+
interpreted as actions that modify the final result.
|
|
3264
|
+
|
|
3265
|
+
SUPPORTED INPUTS
|
|
3266
|
+
.ply
|
|
3267
|
+
|
|
3268
|
+
SUPPORTED OUTPUTS
|
|
3269
|
+
.ply .compressed.ply meta.json (SOGS) .csv
|
|
3270
|
+
|
|
3271
|
+
ACTIONS (can be repeated, in any order)
|
|
3272
|
+
-t, --translate x,y,z Translate splats by (x, y, z)
|
|
3273
|
+
-r, --rotate x,y,z Rotate splats by Euler angles (deg)
|
|
3274
|
+
-s, --scale x Uniformly scale splats by factor x
|
|
3275
|
+
-n, --filterNaN Remove any Gaussian containing NaN/Inf
|
|
3276
|
+
-c, --filterByValue name,cmp,value Keep splats where <name> <cmp> <value>
|
|
3277
|
+
cmp ∈ {lt,lte,gt,gte,eq,neq}
|
|
3278
|
+
-h, --filterBands {0|1|2|3} Strip spherical-harmonic bands > N
|
|
3279
|
+
|
|
3280
|
+
GLOBAL OPTIONS
|
|
3281
|
+
-w, --overwrite Overwrite output file if it already exists
|
|
3282
|
+
-h, --help Show this help and exit
|
|
3283
|
+
-v, --version Show version and exit
|
|
3284
|
+
|
|
3285
|
+
EXAMPLES
|
|
3286
|
+
# Simple scale-then-translate
|
|
3287
|
+
splat-transform bunny.ply -s 0.5 -t 0,0,10 bunny_scaled.ply
|
|
3288
|
+
|
|
3289
|
+
# Chain two inputs and write compressed output, overwriting if necessary
|
|
3290
|
+
splat-transform -w cloudA.ply -r 0,90,0 cloudB.ply -s 2 merged.compressed.ply
|
|
3206
3291
|
`;
|
|
3207
3292
|
const main = async () => {
|
|
3208
3293
|
console.log(`splat-transform v${version}`);
|
|
3209
3294
|
// read args
|
|
3210
|
-
const files = parseArguments();
|
|
3211
|
-
|
|
3295
|
+
const { files, options } = parseArguments();
|
|
3296
|
+
// show version and exit
|
|
3297
|
+
if (options.version) {
|
|
3298
|
+
exit(0);
|
|
3299
|
+
}
|
|
3300
|
+
// invalid args or show help
|
|
3301
|
+
if (files.length < 2 || options.help) {
|
|
3212
3302
|
console.error(usage);
|
|
3213
3303
|
exit(1);
|
|
3214
3304
|
}
|
|
@@ -3236,7 +3326,7 @@ const main = async () => {
|
|
|
3236
3326
|
// combine inputs into a single output dataTable
|
|
3237
3327
|
const dataTable = process(combine(inputFiles.map(file => file.elements[0].dataTable)), outputArg.processActions);
|
|
3238
3328
|
// write file
|
|
3239
|
-
await writeFile(resolve(outputArg.filename), dataTable);
|
|
3329
|
+
await writeFile(resolve(outputArg.filename), dataTable, options);
|
|
3240
3330
|
}
|
|
3241
3331
|
catch (err) {
|
|
3242
3332
|
// handle errors
|