@needle-tools/gltf-build-pipeline 1.6.0-alpha.1 → 1.6.0-alpha.2

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 CHANGED
@@ -4,7 +4,7 @@ All notable changes to this package will be documented in this file.
4
4
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5
5
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [1.6.0-alpha] - 2023-05-27
7
+ ## [1.6.0-alpha.2] - 2023-05-27
8
8
  - add: `gltf-build-pipeline` commandline support with `transform` and `compress` (pack-gltf) options.
9
9
 
10
10
  ## [1.5.0-alpha.12] - 2023-05-20
@@ -5,6 +5,7 @@ import { Logger } from "@donmccurdy/caporal";
5
5
  * This will remove all files in the cache directory.
6
6
  */
7
7
  export declare function clearCache(logger?: Logger): void;
8
+ export declare const cacheSizeLimit = 1024;
8
9
  /**
9
10
  * Ensure that the cache size does not exceed the given maximum size in MB.
10
11
  * @param maxSizeMB The maximum size in MB.
@@ -31,11 +31,12 @@ export function clearCache(logger) {
31
31
  else
32
32
  logger.info(`Cleared cache at ${dir}`);
33
33
  }
34
+ export const cacheSizeLimit = 1024;
34
35
  /**
35
36
  * Ensure that the cache size does not exceed the given maximum size in MB.
36
37
  * @param maxSizeMB The maximum size in MB.
37
38
  */
38
- export function limitCacheSize(maxSizeMB = 1024) {
39
+ export function limitCacheSize(maxSizeMB = cacheSizeLimit) {
39
40
  const dir = getCacheDirectory(true);
40
41
  console.log(`Limit cache size to ${maxSizeMB.toFixed(2)} MB at ${dir}...`);
41
42
  let totalSize = 0;
package/dist/cli/index.js CHANGED
@@ -5,7 +5,8 @@ import { printStats } from '../utils/stats.js';
5
5
  import { getVersion } from '../utils/version.js';
6
6
  import { UsecaseOptions, createConfig, getConfig } from '../config/index.js';
7
7
  import { make_progressive } from '../transforms/needle_progressive.js';
8
- import { clearCache } from '../cache/cache.js';
8
+ import { cacheSizeLimit, clearCache } from '../cache/cache.js';
9
+ import { foreachGLTF } from '../utils/fileutils.js';
9
10
  // For testing / dev you can run `npm link` in the package directory
10
11
  // For removing the link run `npm rm --global @needle-tools/gltf-build-pipeline`
11
12
  program
@@ -14,8 +15,8 @@ program
14
15
  logger.info(getVersion());
15
16
  })
16
17
  .command("clear-caches", "Clear all caches")
17
- .help(`
18
- Clear all caches.
18
+ .help(`Clears all cached files
19
+ Caches are limited to ${cacheSizeLimit} MB disc space by default.
19
20
  `)
20
21
  .action(async ({ logger }) => {
21
22
  logger.info("Clearing caches");
@@ -25,8 +26,8 @@ Clear all caches.
25
26
  .help(`
26
27
  Compress a single glTF file. The input file is compressed and written to the output file.
27
28
  `)
28
- .argument('<input>', "The input glTF, GLB or VRM file that should be compressed")
29
- .option("--usecase <usecase>", `The usecase for the compression. This will set the compression settings. Options are ${UsecaseOptions.join(", ")}`, { validator: program.STRING })
29
+ .argument('<input>', "The input glTF, GLB or VRM file that should be compressed OR a directory that contains files to be compressed")
30
+ .option("--usecase <usecase>", `The usecase for the compression. This will set the compression settings. Possible options: [${UsecaseOptions.join(", ")}] e.g. \`--usecase ${UsecaseOptions[0]}\"`, { validator: program.STRING })
30
31
  .option("--config <config>", "The path to a JSON file containing the compression settings. This can also be a needle.config.json file with a \"compression\" object or usecase string.", { validator: program.STRING })
31
32
  .option("--output <output>", "The output file. If not provided, the input file will be overwritten.", { validator: program.STRING })
32
33
  .action(async ({ logger, args, options }) => {
@@ -41,28 +42,36 @@ Compress a single glTF file. The input file is compressed and written to the out
41
42
  let config = getConfig(options.config?.toString());
42
43
  if (options.usecase) {
43
44
  logger.info("Using usecase: " + options.usecase);
44
- config = { ...config, ...createConfig(options.usecase.toString()) };
45
+ config = { ...createConfig(options.usecase.toString()), ...config, };
46
+ if (config.usecase != "unknown" && config.usecase != options.usecase.toString()) {
47
+ logger.warn(`Conflicting usecase. Your specific config at \"${options.config}\" has a different usecase than the one provided by the CLI: \"${options.usecase}\". Using the usecase from the config file.`);
48
+ }
49
+ else {
50
+ config.usecase = options.usecase.toString();
51
+ }
45
52
  }
46
53
  const file = args.input.toString();
47
54
  const set = new Set();
48
- await packGLTF(file, options.output?.toString() || file, {
49
- config,
50
- packedFiles: set,
51
- stats,
52
- debug: false,
53
- useCache: true,
55
+ await foreachGLTF(file, async (file) => {
56
+ await packGLTF(file, options.output?.toString() || file, {
57
+ config,
58
+ packedFiles: set,
59
+ stats,
60
+ debug: false,
61
+ useCache: true,
62
+ logger: logger,
63
+ });
54
64
  });
55
65
  printStats(stats);
56
66
  })
57
- .command("transform", "Transform a glTF file")
67
+ .command("transform", "Transform and optimize glTF, GLB or VRM files")
58
68
  .help(`
59
69
  This will produce multiple versions of the input file, each with a different level of optimization.
60
70
  Each version will be compressed and written to the output directory.
61
71
  `)
62
- .argument('<input>', "The input glTF, GLB or VRM file that should be compressed")
63
- .option("--usecase <usecase>", `The usecase for the compression. This will set the compression settings. Options are ${UsecaseOptions.join(", ")}`, { validator: program.STRING })
72
+ .argument('<input>', "The input glTF, GLB or VRM file that should be compressed OR a directory that contains files to be compressed")
73
+ .option("--usecase <usecase>", `The usecase for the compression. This will set the compression settings. Possible options: [${UsecaseOptions.join(", ")}]`, { validator: program.STRING })
64
74
  .option("--config <config>", "The path to a JSON file containing the compression settings. This can also be a needle.config.json file with a \"compression\" object or usecase string.", { validator: program.STRING })
65
- // .option("--output <output>", "The output file. If not provided, the input file will be overwritten.", { validator: program.STRING })
66
75
  .action(async ({ logger, args, options }) => {
67
76
  logger.info("Transforming glTF file: " + args.input);
68
77
  const stats = {
@@ -75,28 +84,41 @@ Each version will be compressed and written to the output directory.
75
84
  let config = getConfig(options.config?.toString());
76
85
  if (options.usecase) {
77
86
  logger.info("Using usecase: " + options.usecase);
78
- config = { ...config, ...createConfig(options.usecase.toString()) };
87
+ config = { ...createConfig(options.usecase.toString()), ...config, };
88
+ if (config.usecase != "unknown" && config.usecase != options.usecase.toString()) {
89
+ logger.warn(`Conflicting usecase. Your specific config at \"${options.config}\" has a different usecase than the one provided by the CLI: \"${options.usecase}\". Using the usecase from the config file.`);
90
+ }
91
+ else {
92
+ config.usecase = options.usecase.toString();
93
+ }
79
94
  }
80
95
  const file = args.input.toString();
81
- const progressive_results = new Array();
82
- await make_progressive({
83
- path: file,
84
- results: progressive_results,
85
- config,
86
- debug: false,
87
- useCache: true,
96
+ const set = new Set();
97
+ await foreachGLTF(file, async (file) => {
98
+ logger.info("> Transform " + file);
99
+ const progressive_results = new Array();
100
+ await make_progressive({
101
+ path: file,
102
+ results: progressive_results,
103
+ config,
104
+ debug: false,
105
+ useCache: true,
106
+ });
107
+ const opts = {
108
+ config,
109
+ stats,
110
+ packedFiles: set,
111
+ debug: false,
112
+ useCache: true,
113
+ logger: logger,
114
+ };
115
+ for (const res of progressive_results) {
116
+ logger.info("> Compress " + res);
117
+ await packGLTF(res, res, opts);
118
+ }
119
+ logger.info("> Compress " + file);
120
+ await packGLTF(file, file, opts);
88
121
  });
89
- console.log("Progressive results: ", progressive_results);
90
- const opts = {
91
- config,
92
- stats,
93
- debug: false,
94
- useCache: true,
95
- };
96
- for (const prog of progressive_results) {
97
- await packGLTF(prog, prog, opts);
98
- }
99
- await packGLTF(file, file, opts);
100
122
  printStats(stats);
101
123
  });
102
124
  program.run();
@@ -1,4 +1,4 @@
1
- declare type Usecase = "unknown" | "product" | "game" | "metaverse";
1
+ declare type Usecase = "unknown" | "product" | "world";
2
2
  export declare const UsecaseOptions: string[];
3
3
  /**
4
4
  * The config object contains information about how a file should be processed.
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from "fs";
2
- export const UsecaseOptions = ["product", "game", "metaverse"];
2
+ export const UsecaseOptions = ["product"];
3
3
  const defaultConfig = {
4
4
  usecase: "unknown",
5
5
  };
@@ -1,3 +1,4 @@
1
+ import { ILogger } from '@gltf-transform/core';
1
2
  import { Stats } from "../utils/index.js";
2
3
  import { Config } from '../config/index.js';
3
4
  export type PackGLTFOptions = {
@@ -6,6 +7,7 @@ export type PackGLTFOptions = {
6
7
  packedFiles?: Set<string>;
7
8
  stats?: Stats;
8
9
  config?: Config;
10
+ logger?: ILogger;
9
11
  };
10
12
  /** Compress a glTF file
11
13
  */
@@ -1,5 +1,5 @@
1
1
  import path from 'path';
2
- import { NodeIO } from '@gltf-transform/core';
2
+ import { Logger, NodeIO, Verbosity } from '@gltf-transform/core';
3
3
  import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
4
4
  import draco3d from 'draco3dgltf';
5
5
  import { existsSync, readFileSync, statSync, writeFileSync } from 'fs';
@@ -13,11 +13,14 @@ import { getConfig } from '../config/index.js';
13
13
  /** Compress a glTF file
14
14
  */
15
15
  export async function packGLTF(inputFile, outputFile, options) {
16
+ if (!options.logger) {
17
+ options.logger = new Logger(Verbosity.INFO);
18
+ }
19
+ const { debug, stats, packedFiles, useCache, logger } = options;
16
20
  if (!existsSync(inputFile)) {
17
- console.error("WARN: can not pack, file does not exist: " + inputFile);
21
+ logger.error("WARN: can not pack, file does not exist: " + inputFile);
18
22
  return;
19
23
  }
20
- const { debug, stats, packedFiles, useCache } = options;
21
24
  if (debug && inputFile.includes(".debug.")) {
22
25
  return;
23
26
  }
@@ -31,7 +34,7 @@ export async function packGLTF(inputFile, outputFile, options) {
31
34
  cacheKey = "packed-" + hash;
32
35
  const cached = tryGetFromCache(cacheKey);
33
36
  if (cached) {
34
- console.log(`→ Loaded compressed file from cache: \"${path.basename(inputFile)}\"`);
37
+ logger.info(`→ Loaded compressed file from cache: \"${path.basename(inputFile)}\"`);
35
38
  writeFileSync(inputFile, cached);
36
39
  return;
37
40
  }
@@ -44,7 +47,7 @@ export async function packGLTF(inputFile, outputFile, options) {
44
47
  try {
45
48
  const config = options.config || getConfig();
46
49
  const prevSizeInMB = statSync(inputFile).size / 1024 / 1024;
47
- console.log(`→ Compressing ${prevSizeInMB.toFixed(1)} MB: ${inputFile}`);
50
+ logger.info(`→ Compressing ${prevSizeInMB.toFixed(1)} MB: ${inputFile}`);
48
51
  // const isProgressiveAsset = isLOD(file);
49
52
  const startTime = Date.now();
50
53
  const io = new NodeIO()
@@ -62,7 +65,7 @@ export async function packGLTF(inputFile, outputFile, options) {
62
65
  });
63
66
  const document = await io.read(inputFile);
64
67
  /** @ts-ignore */
65
- document.getLogger().verbosity = 0;
68
+ document.getLogger().verbosity = Verbosity.ERROR;
66
69
  let totalGPUTextureMemmory = 0;
67
70
  /** @type {Array<import('@gltf-transform/core').Transform>} */
68
71
  const transforms = [
@@ -100,7 +103,7 @@ export async function packGLTF(inputFile, outputFile, options) {
100
103
  const sizeInMB = statSync(outputFile).size / 1024 / 1024;
101
104
  const filename = path.basename(outputFile);
102
105
  const gpuMemoryInMB = totalGPUTextureMemmory / 1024 / 1024;
103
- console.log(`← Compressing done in ${(duration / 1000).toFixed(1)} sec, ${prevSizeInMB.toFixed(1)} MB → ${sizeInMB.toFixed(1)} MB (GPU: ${gpuMemoryInMB.toFixed(1)} MB textures): ${filename} at ${outputFile}`);
106
+ logger.info(`← Compressing done in ${(duration / 1000).toFixed(1)} sec, ${prevSizeInMB.toFixed(1)} MB → ${sizeInMB.toFixed(1)} MB (GPU: ${gpuMemoryInMB.toFixed(1)} MB textures): ${filename} at ${outputFile}`);
104
107
  if (stats) {
105
108
  stats.totalFilesProcessed++;
106
109
  stats.totalFileSizeInMB += sizeInMB;
@@ -110,7 +113,7 @@ export async function packGLTF(inputFile, outputFile, options) {
110
113
  return true;
111
114
  }
112
115
  catch (err) {
113
- console.error("ERR: packing " + inputFile + " failed: " + err.message);
116
+ logger.error("ERR: packing " + inputFile + " failed: " + err.message);
114
117
  if (debug)
115
118
  throw err;
116
119
  return false;
@@ -9,7 +9,7 @@ declare interface ProgressiveOptions {
9
9
  }
10
10
  export declare function make_progressive(opts: ProgressiveOptions): Promise<void>;
11
11
  export declare function createTransform(name: string, fn: Transform): Transform;
12
- export declare function make_progressive_textures(filePath: string, _options?: TextureResizeOptions, opts?: ProgressiveOptions): Transform;
12
+ export declare function make_progressive_textures(filePath: string, _options: TextureResizeOptions, opts: ProgressiveOptions): Transform;
13
13
  /** Options for the {@link textureResize} function. */
14
14
  export interface TextureResizeOptions {
15
15
  /**
@@ -69,11 +69,15 @@ async function onProcess(file, opts) {
69
69
  return;
70
70
  }
71
71
  }
72
+ let smallesTextureSize = 128;
73
+ if (opts.config.usecase === "product") {
74
+ smallesTextureSize = 512;
75
+ }
72
76
  //@ts-ignore
73
77
  document.getLogger().verbosity = 0;
74
78
  const transforms = [
75
79
  needle_asset(),
76
- make_progressive_textures(file, { size: [256, 256] }, opts),
80
+ make_progressive_textures(file, { size: [smallesTextureSize, smallesTextureSize] }, opts),
77
81
  sparse(),
78
82
  ];
79
83
  // TODO: progressive meshes aren't supported yet for VRMs
@@ -143,14 +147,14 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
143
147
  let settings = texture.getExtension(NEEDLE_progressive_texture_settings.EXTENSION_NAME);
144
148
  if (!settings) {
145
149
  const size = texture.getSize();
146
- if (size?.length >= 2 && size[0] <= 128 && size[1] <= 128) {
147
- logger.warn(`[${NAME}] Skipping texture[${textureIndex}], no settings found and the texture is already < 128x128`);
150
+ if (size?.length >= 2 && size[0] <= options.size[0] && size[1] <= options.size[1]) {
151
+ logger.warn(`[${NAME}] Skipping texture[${textureIndex}], no settings found and the texture is already <= ${size[0]}x${size[1]}`);
148
152
  continue;
149
153
  }
150
154
  else {
151
155
  settings = {
152
156
  guid: generateGuid(filename + "_texture_" + index.toString()),
153
- maxSize: 128,
157
+ maxSize: _options.size[0],
154
158
  };
155
159
  logger.debug(`[${NAME}] No settings found for texture[${index}] - will use default settings (maxSize: ${settings.maxSize}, guid: ${settings.guid})`);
156
160
  }
@@ -360,7 +364,10 @@ export function make_progressive_meshes(filePath, _options, opts) {
360
364
  console.log(`[${NAME}]: Begin generating progressive meshes for \"${filename}\"`);
361
365
  let index = 0;
362
366
  // the lowest LOD (last entry) will be saved in the original file
363
- const MAX_LODS = 6;
367
+ let MAX_LODS = 6;
368
+ if (opts.config.usecase === "product") {
369
+ MAX_LODS = 3;
370
+ }
364
371
  const LODS = [
365
372
  {
366
373
  ratio: 1,
@@ -369,10 +376,13 @@ export function make_progressive_meshes(filePath, _options, opts) {
369
376
  },
370
377
  ];
371
378
  for (let l = 0; l < MAX_LODS; l++) {
372
- const t = l / MAX_LODS;
379
+ // const t = l / MAX_LODS;
373
380
  const latest = LODS[LODS.length - 1];
374
381
  const ratio = latest.ratio * .5;
375
- const error = Math.min(.013 * (l + 1), 0.05);
382
+ let error = Math.min(.013 * (l + 1), 0.05);
383
+ if (opts.config.usecase === "product") {
384
+ error *= .5;
385
+ }
376
386
  LODS.push({
377
387
  ratio: ratio,
378
388
  error,
@@ -626,6 +636,7 @@ async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLeve
626
636
  mesh["needle:mesh-hash"] = hash;
627
637
  }
628
638
  hash += getHash(lodLevel);
639
+ hash += getHash(opts.config);
629
640
  if (opts.useCache !== false) {
630
641
  cacheKey = `mesh_lod_${level}_${hash}`;
631
642
  const cachedValue = tryGetFromCache(cacheKey);
@@ -1,3 +1,4 @@
1
+ import { Logger } from "@donmccurdy/caporal";
1
2
  export type Stats = {
2
3
  startTime: number;
3
4
  totalGPUMemoryInMB: number;
@@ -5,4 +6,4 @@ export type Stats = {
5
6
  totalFileSizeInMB: number;
6
7
  totalFileSizeInMBBefore: number;
7
8
  };
8
- export declare function printStats(stats: any): void;
9
+ export declare function printStats(stats: Stats, logger?: Logger): void;
@@ -1,6 +1,10 @@
1
1
  import { getVersion } from "./version.js";
2
- export function printStats(stats) {
2
+ export function printStats(stats, logger) {
3
3
  const duration = Date.now() - stats.startTime;
4
4
  const durationInMinutes = duration / 1000 / 60;
5
- console.log(`[Needle Build Pipeline] v${getVersion()} — Finished packing ${stats.totalFilesProcessed} files in ${durationInMinutes.toFixed(1)} minutes; total size ${stats.totalFileSizeInMBBefore.toFixed(1)} MB → ${stats.totalFileSizeInMB.toFixed(1)} MB, total GPU memory: ${stats.totalGPUMemoryInMB.toFixed(1)} MB`);
5
+ const msg = `[Needle Build Pipeline] v${getVersion()} — Finished packing ${stats.totalFilesProcessed} files in ${durationInMinutes.toFixed(1)} minutes; total size ${stats.totalFileSizeInMBBefore.toFixed(1)} MB → ${stats.totalFileSizeInMB.toFixed(1)} MB, total GPU memory: ${stats.totalGPUMemoryInMB.toFixed(1)} MB`;
6
+ if (logger)
7
+ logger.info(msg);
8
+ else
9
+ console.log(msg);
6
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/gltf-build-pipeline",
3
- "version": "1.6.0-alpha.1",
3
+ "version": "1.6.0-alpha.2",
4
4
  "description": "Pipeline and tools for optimizing gltf files using gltf-transform and compression settings within glTF extensions",
5
5
  "main": "index.mjs",
6
6
  "type": "module",