@needle-tools/gltf-build-pipeline 2.15.9 → 2.15.10-next.0a00d78

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/cli/index.js CHANGED
@@ -31,13 +31,14 @@ Caches are limited to ${cacheSizeLimit} MB disc space by default.
31
31
  `)
32
32
  .argument('<input>', "Path to a glTF, GLB or VRM file OR a directory")
33
33
  .argument('<output>', "Output path for the stats", { default: null, validator: program.STRING })
34
+ .option("--verbose", "Enable verbose output", { validator: program.BOOLEAN, default: false })
34
35
  .action(async ({ logger, args, options }) => {
35
- logger.level = "info";
36
+ logger.level = options.verbose ? "debug" : "info";
36
37
  logger.info("Calculating stats for \"" + args.input + "\"");
37
38
  const input = args.input.toString();
38
39
  const filestats = new Array();
39
40
  await foreachGLTF(input, async (file) => {
40
- const stats = await calculateStats(file);
41
+ const stats = await calculateStats(file, { verbose: !!options.verbose });
41
42
  filestats.push(stats);
42
43
  });
43
44
  let output = args.output?.toString();
@@ -79,12 +79,12 @@ export function getConfig(configPath, fallbackSearchDirectory, opts) {
79
79
  const result = searchConfig(fallbackSearchDirectory, logger);
80
80
  if (result) {
81
81
  _cachedConfig = { ...defaultConfig, ...result };
82
- logger.info("Using config with usecase: " + _cachedConfig.usecase);
82
+ logger.info(`Using config with usecase: ${_cachedConfig.usecase}`);
83
83
  }
84
84
  }
85
85
  // If no config was found, use the default config
86
86
  if (!_cachedConfig) {
87
- logger.info("No config found. Using default config.");
87
+ logger.debug("No config found. Using default config.");
88
88
  _cachedConfig = defaultConfig;
89
89
  }
90
90
  return _cachedConfig;
@@ -112,13 +112,14 @@ export function searchConfig(directory, logger) {
112
112
  }
113
113
  return null;
114
114
  }
115
+ const loggedConfigPaths = new Set();
115
116
  function resolveConfig(filename, content, logger) {
116
117
  // Handle config being part of `needle.config.json`
117
118
  if (filename.endsWith("needle.config.json")) {
118
119
  try {
119
120
  const config = JSON.parse(content);
120
121
  if (config.gltf) {
121
- logger.info("Found \"gltf\" config at \"" + filename + "\".");
122
+ logger.info(`Found \"gltf\" config at \"${filename.replaceAll("\\", "/")}\".`);
122
123
  // If the optimization config is a string, it's the usecase
123
124
  if (typeof config.gltf === "string") {
124
125
  return createConfig(config.gltf);
@@ -126,12 +127,15 @@ function resolveConfig(filename, content, logger) {
126
127
  return config.gltf;
127
128
  }
128
129
  else {
129
- logger.info("No \"gltf\" config found in needle.config.json. Using default config.");
130
+ if (!loggedConfigPaths.has(filename)) {
131
+ logger.debug(`No \"gltf\" config found in needle.config.json at \"${filename.replaceAll("\\", "/")}\". You may add a \"gltf\" field to the config with the optimization config. Example: { \"gltf\": { \"usecase\": \"product\" } }. Using default config for glTF optimization now.`);
132
+ loggedConfigPaths.add(filename);
133
+ }
130
134
  return null;
131
135
  }
132
136
  }
133
137
  catch (err) {
134
- logger.error(`Failed parsing config \"${err.message}\" at ${filename}`);
138
+ logger.error(`Failed parsing config \"${err.message}\" at ${filename.replaceAll("\\", "/")}`);
135
139
  }
136
140
  }
137
141
  // Handle config being the content of the file
@@ -141,12 +145,12 @@ function resolveConfig(filename, content, logger) {
141
145
  }
142
146
  catch (err) {
143
147
  if (err instanceof Error)
144
- logger.error(`Failed parsing config \"${err.message}\" at ${filename}`);
148
+ logger.error(`Failed parsing config \"${err.message}\" at ${filename.replaceAll("\\", "/")}`);
145
149
  else
146
- logger.error(`Failed parsing config \"${err}\" at ${filename}`);
150
+ logger.error(`Failed parsing config \"${err}\" at ${filename.replaceAll("\\", "/")}`);
147
151
  }
148
152
  }
149
153
  else {
150
- logger.error("Invalid config file format. Expected JSON object at " + filename);
154
+ logger.error("Invalid config file format. Expected JSON object at " + filename.replaceAll("\\", "/"));
151
155
  }
152
156
  }
@@ -1,4 +1,4 @@
1
- import { Extension, ExtensionProperty, PropertyType } from '@gltf-transform/core';
1
+ import { Extension, ExtensionProperty, PropertyType, TextureChannel } from '@gltf-transform/core';
2
2
  import { existsSync, readFileSync } from 'fs';
3
3
  import { join, dirname } from 'path';
4
4
  import { isLightmap } from '../../utils/texture.js';
@@ -21,8 +21,8 @@ class LightmapProperty extends ExtensionProperty {
21
21
  return Object.assign(super.getDefaults(), {});
22
22
  }
23
23
  /** Public wrapper around protected setRef to register references */
24
- setReference(name, value) {
25
- this.setRef(name, value);
24
+ setReference(name, value, attributes) {
25
+ this.setRef(name, value, attributes);
26
26
  }
27
27
  }
28
28
  /**
@@ -110,7 +110,7 @@ export class NEEDLE_lightmaps_ext extends Extension {
110
110
  let texture = null;
111
111
  let inputTextureDef;
112
112
  if (entry.pointer.startsWith('/textures/') || entry.pointer.startsWith('textures/')) {
113
- // Embedded pointer — resolve to Texture via the JSON texture definition
113
+ // Embedded pointer — resolve Found lightmapto Texture via the JSON texture definition
114
114
  const prefix = entry.pointer.startsWith('/') ? '/textures/' : 'textures/';
115
115
  const texIndex = parseInt(entry.pointer.substring(prefix.length));
116
116
  const texDef = textureDefs[texIndex];
@@ -142,8 +142,9 @@ export class NEEDLE_lightmaps_ext extends Extension {
142
142
  if (texture) {
143
143
  this.resolved.push({ entry, texture, inputTextureDef });
144
144
  // Set a reference so gltf-transform won't prune/dedup this texture
145
- const refName = `lightmap_${entry.pointer}`;
146
- this.lightmapProp.setReference(refName, texture);
145
+ const refName = `lightmapTexture_${entry.pointer}`;
146
+ const channels = TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A;
147
+ this.lightmapProp.setReference(refName, texture, { channels });
147
148
  // Also keep a reference to the buffer
148
149
  const buffer = this.document.getRoot().listBuffers()[0];
149
150
  if (buffer) {
@@ -151,7 +152,7 @@ export class NEEDLE_lightmaps_ext extends Extension {
151
152
  }
152
153
  }
153
154
  else {
154
- logger.error(`NEEDLE_lightmaps: Could not resolve texture for pointer "${entry.pointer}"`);
155
+ this.document.getLogger().error(`NEEDLE_lightmaps: Could not resolve texture for pointer "${entry.pointer}"`);
155
156
  testAssert(false, `NEEDLE_lightmaps: Could not resolve texture for pointer "${entry.pointer}"`);
156
157
  }
157
158
  }
@@ -160,8 +161,9 @@ export class NEEDLE_lightmaps_ext extends Extension {
160
161
  for (const tex of textures) {
161
162
  if (isLightmap(tex) && tex.getMimeType() !== 'image/exr') {
162
163
  const refName = `lightmapTexture_${tex.getName()}`;
163
- this.lightmapProp.setReference(refName, tex);
164
- logger.info(`Found lightmap texture "${tex.getName()}" (${tex.getMimeType()}) in ${EXTENSION_NAME}`);
164
+ const channels = TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A;
165
+ this.lightmapProp.setReference(refName, tex, { channels });
166
+ this.document.getLogger().debug(`[NEEDLE_lightmaps] Found lightmap texture "${tex.getName()}" (${tex.getMimeType()})`);
165
167
  }
166
168
  }
167
169
  return this;
@@ -200,7 +200,7 @@ export function createOpaqueExtension(name, types, opts) {
200
200
  prop.setReference("lightmapTexture", tex, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
201
201
  const texDef = textureDefinitions[i]; // @TODO: textureDefinitions and textures might not be in the same order - we need to find the texture by source index instead of assuming the order is the same
202
202
  const name = tex.getName() || texDef?.name;
203
- logger.info(`Found lightmap texture \"${name}\" (${tex.getMimeType()}) in ${EXTENSION_NAME}`);
203
+ logger.debug(`Found lightmap texture \"${name}\" (${tex.getMimeType()}) in ${EXTENSION_NAME}`);
204
204
  }
205
205
  }
206
206
  }
@@ -1,2 +1,5 @@
1
1
  import { FileStats } from "../utils/stats.js";
2
- export declare function calculateStats(inputfile: string): Promise<FileStats>;
2
+ export interface StatsOptions {
3
+ verbose?: boolean;
4
+ }
5
+ export declare function calculateStats(inputfile: string, options?: StatsOptions): Promise<FileStats>;
@@ -1,10 +1,11 @@
1
- import { NodeIO } from "@gltf-transform/core";
1
+ import { Logger, NodeIO, Verbosity } from "@gltf-transform/core";
2
2
  import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
3
3
  import draco3d from 'draco3dgltf';
4
4
  import { MeshoptDecoder, MeshoptEncoder } from "meshoptimizer";
5
5
  import { createNewFilestats } from "../utils/stats.js";
6
- export async function calculateStats(inputfile) {
6
+ export async function calculateStats(inputfile, options) {
7
7
  const io = new NodeIO()
8
+ .setLogger(new Logger(options?.verbose ? Verbosity.DEBUG : Verbosity.ERROR))
8
9
  .registerExtensions(ALL_EXTENSIONS)
9
10
  .registerDependencies({
10
11
  'draco3d.decoder': await draco3d.createDecoderModule(),
@@ -376,6 +376,11 @@ async function createTextureLod(args, opts) {
376
376
  setLightmapName(newTexture);
377
377
  break;
378
378
  default:
379
+ if (slot.startsWith("lightmapTexture_") || slot.startsWith("lightmap_")) {
380
+ newMaterial.setBaseColorTexture(newTexture);
381
+ setLightmapName(newTexture);
382
+ break;
383
+ }
379
384
  if (slot.startsWith("/textures/")) {
380
385
  // Ensure the texture is using the alpha channel since we don't know what slots the original texture was used for
381
386
  // see https://linear.app/needle/issue/NE-4985
@@ -408,11 +413,11 @@ async function createTextureLod(args, opts) {
408
413
  // Pass compression settings to the new texture
409
414
  const compressionSettingsExt = texture.getExtension(NEEDLE_compression_texture.EXTENSION_NAME);
410
415
  if (compressionSettingsExt !== undefined && compressionSettingsExt !== null && compressionSettingsExt.getExtensionDefinition) {
411
- logger.info("> Pass compression extension to new texture");
416
+ logger.debug("> Pass compression extension to new texture");
412
417
  const newExt = newDoc.createExtension(NEEDLE_compression_texture);
413
418
  const compressionSettings = compressionSettingsExt.getExtensionDefinition();
414
- if (compressionSettings?.mode === "none") {
415
- // if the mode is set to None then we keep that setting for the new texture
419
+ if (compressionSettings?.mode === "none" || compressionSettings?.mode === "webp" || compressionSettings?.mode === "UASTC" || compressionSettings?.mode === "ETC1S") {
420
+ // explicit compression mode set by the user — keep it for LOD textures
416
421
  }
417
422
  else {
418
423
  switch (opts.config.usecase) {
@@ -53,6 +53,11 @@ export class NeedleToktxContext {
53
53
  if (existsSync(defaultPath))
54
54
  this.env.PATH = `${this.env.PATH};${defaultPath}`;
55
55
  }
56
+ else {
57
+ const defaultPath = "/usr/local/bin";
58
+ if (existsSync(defaultPath))
59
+ this.env.PATH = `${this.env.PATH}:${defaultPath}`;
60
+ }
56
61
  // append default system paths
57
62
  if (isUsingWindows && process.env.Path?.length)
58
63
  this.env.PATH = `${this.env.PATH};${process.env.Path}`;
@@ -60,11 +65,11 @@ export class NeedleToktxContext {
60
65
  if (options.debug) {
61
66
  console.log("INFO: Found these paths including ktx:\n\t" + ktxPaths.join("\n\t") + "");
62
67
  }
63
- else if (ktxPaths?.length <= 0) {
64
- if (didLogEnvironmentInfo === false)
65
- console.log("INFO: Environment variable 'NEEDLE_TOKTX' not set. Use 'NEEDLE_TOKTX' to specify the directory of the toktx installation (path/to/bin)");
66
- didLogEnvironmentInfo = true;
67
- }
68
+ // else if (ktxPaths?.length <= 0) {
69
+ // if (didLogEnvironmentInfo === false)
70
+ // console.log("INFO: You may set the 'NEEDLE_TOKTX' environment variable to specify the directory of the toktx installation (path/to/bin)")
71
+ // didLogEnvironmentInfo = true;
72
+ // }
68
73
  }
69
74
  async prepare(document) {
70
75
  try {
@@ -72,7 +77,7 @@ export class NeedleToktxContext {
72
77
  const logger = document.getLogger();
73
78
  this.version = await checkKTXSoftware(logger, this.env);
74
79
  if (this.version === null) {
75
- logger.warn(`WARN: toktx not found, skipping texture compression. Please install KTX-Software >= ${KTX_SOFTWARE_VERSION_MIN}`);
80
+ logger.warn(`WARN: toktx not found, skipping KTX2 texture compression. Please install KTX-Software >= ${KTX_SOFTWARE_VERSION_MIN}. You may set the 'NEEDLE_TOKTX' environment variable to specify the directory of the toktx installation (path/to/bin)`);
76
81
  this._prepareFailed = true;
77
82
  return false;
78
83
  }
@@ -84,8 +89,7 @@ export class NeedleToktxContext {
84
89
  this.numTextures = document.getRoot().listTextures().length;
85
90
  }
86
91
  catch (e) {
87
- console.error(e);
88
- console.log("Error preparing toktx transform");
92
+ console.error("ERR: preparing toktx transform failed: " + e);
89
93
  this._prepareFailed = true;
90
94
  return false;
91
95
  }
@@ -1 +1 @@
1
- export declare const version = "2.15.9";
1
+ export declare const version = "2.15.10-next.0a00d78";
@@ -1 +1 @@
1
- export const version = "2.15.9";
1
+ export const version = "2.15.10-next.0a00d78";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/gltf-build-pipeline",
3
- "version": "2.15.9",
3
+ "version": "2.15.10-next.0a00d78",
4
4
  "description": "Pipeline and tools for optimizing gltf files using gltf-transform and compression settings within glTF extensions",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -55,10 +55,10 @@
55
55
  "vitest": "^3.1.1"
56
56
  },
57
57
  "optionalDependencies": {
58
- "@img/sharp-darwin-arm64": "^0.33.4",
59
- "@img/sharp-darwin-x64": "^0.33.4",
60
- "@img/sharp-linux-arm": "^0.33.4",
61
- "@img/sharp-win32-x64": "^0.33.4"
58
+ "@img/sharp-darwin-arm64": "^0.34.5",
59
+ "@img/sharp-darwin-x64": "^0.34.5",
60
+ "@img/sharp-linux-arm": "^0.34.5",
61
+ "@img/sharp-win32-x64": "^0.34.5"
62
62
  },
63
63
  "dependencies": {
64
64
  "@donmccurdy/caporal": "~0.0.10",
@@ -73,10 +73,10 @@
73
73
  "micromatch": "^4.0.5",
74
74
  "ndarray": "^1.0.19",
75
75
  "ndarray-lanczos": "^0.3.0",
76
- "ndarray-pixels": "^2.0.1",
76
+ "ndarray-pixels": "^5.0.1",
77
77
  "p-limit": "3.1.0",
78
78
  "semver": "^7.3.7",
79
- "sharp": "^0.33.4",
79
+ "sharp": "^0.34.5",
80
80
  "tmp": "^0.2.1",
81
81
  "uuid": "^9.0.1",
82
82
  "xxhash-wasm": "^1.1.0"