@playcanvas/splat-transform 0.2.1 → 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/dist/index.mjs CHANGED
@@ -1881,7 +1881,7 @@ class Quat {
1881
1881
  }
1882
1882
  }
1883
1883
 
1884
- var version = "0.2.1";
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
- // calculate the output index of the gaussian given its index. chunks of
2850
- // 256 gaussians are packed into 16x16 tiles on the output texture.
2851
- const target = (index, width) => {
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 (outputFilename, dataTable) => {
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 = target(i, width);
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 = target(i, width);
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 = target(i, width);
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 = target(i, width);
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
- const outputFile = await open(outputFilename, 'w');
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 writeFile = async (filename, dataTable) => {
3014
- if (filename.endsWith('.json')) {
3015
- await writeSogs(filename, dataTable);
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 (filename.endsWith('.compressed.ply')) {
3018
- console.log(`writing '${filename}'...`);
3019
- const outputFile = await open(filename, 'w');
3020
- await writeCompressedPly(outputFile, dataTable);
3021
- await outputFile.close();
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
- console.log(`writing '${filename}'...`);
3025
- const outputFile = await open(filename, 'w');
3026
- await writePly(outputFile, {
3027
- comments: [],
3028
- elements: [{
3029
- name: 'vertex',
3030
- dataTable: dataTable
3031
- }]
3032
- });
3033
- await outputFile.close();
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: 'h', multiple: true }
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 = `Usage: splat-transform input.ply [actions] input.ply [actions] ... output.ply [actions]
3199
- actions:
3200
- -translate -t x,y,z Translate splats by (x, y, z)
3201
- -rotate -r x,y,z Rotate splats by euler angles (x, y, z) (in degrees)
3202
- -scale -s x Scale splats by x (uniform scaling)
3203
- -filterNaN -n Remove gaussians containing any NaN or Inf value
3204
- -filterByValue -c name,comparator,value Filter gaussians by a value. Specify the value name, comparator (lt, lte, gt, gte, eq, neq) and value
3205
- -filterBands -h 1 Filter spherical harmonic band data. Value must be 0, 1, 2 or 3.
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
- if (files.length < 2) {
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